From c946b98ea16bee7099c820879be7188692808d84 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Thu, 31 Jul 2025 15:21:58 -0500 Subject: [PATCH 001/693] onboarding: Expand power of theme selector (#35421) Closes #ISSUE The behavior of the theme selector is documented above the function, copied here for reference: ```rust /// separates theme "mode" ("dark" | "light" | "system") into two separate states /// - appearance = "dark" | "light" /// - "system" true/false /// when system selected: /// - toggling between light and dark does not change theme.mode, just which variant will be changed /// when system not selected: /// - toggling between light and dark does change theme.mode /// selecting a theme preview will always change theme.["light" | "dark"] to the selected theme, /// /// this allows for selecting a dark and light theme option regardless of whether the mode is set to system or not /// it does not support setting theme to a static value ``` Release Notes: - N/A *or* Added/Fixed/Improved ... --- Cargo.lock | 2 + crates/onboarding/Cargo.toml | 2 + crates/onboarding/src/basics_page.rs | 298 +++++++++++++----- crates/onboarding/src/onboarding.rs | 37 +-- .../src}/theme_preview.rs | 84 ++--- crates/theme/src/settings.rs | 2 +- crates/ui/src/components.rs | 2 - 7 files changed, 254 insertions(+), 173 deletions(-) rename crates/{ui/src/components => onboarding/src}/theme_preview.rs (73%) diff --git a/Cargo.lock b/Cargo.lock index 1c61972093..d2ded690c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10925,7 +10925,9 @@ dependencies = [ "anyhow", "client", "command_palette_hooks", + "component", "db", + "documented", "editor", "feature_flags", "fs", diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index c6536afecd..7727597e94 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -18,6 +18,8 @@ default = [] anyhow.workspace = true client.workspace = true command_palette_hooks.workspace = true +component.workspace = true +documented.workspace = true db.workspace = true editor.workspace = true feature_flags.workspace = true diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index a57e49977a..bfbe0374d3 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -1,36 +1,228 @@ -use std::sync::Arc; - use client::TelemetrySettings; use fs::Fs; -use gpui::{App, IntoElement}; +use gpui::{App, Entity, IntoElement, Window}; use settings::{BaseKeymap, Settings, update_settings_file}; -use theme::{Appearance, SystemAppearance, ThemeMode, ThemeSettings}; +use theme::{Appearance, ThemeMode, ThemeName, ThemeRegistry, ThemeSelection, ThemeSettings}; use ui::{ - SwitchField, ThemePreviewTile, ToggleButtonGroup, ToggleButtonSimple, ToggleButtonWithIcon, - prelude::*, + ParentElement as _, StatefulInteractiveElement, SwitchField, ToggleButtonGroup, + ToggleButtonSimple, ToggleButtonWithIcon, prelude::*, rems_from_px, }; use vim_mode_setting::VimModeSetting; -use crate::Onboarding; +use crate::theme_preview::ThemePreviewTile; -fn read_theme_selection(cx: &App) -> (ThemeMode, SharedString) { - let settings = ThemeSettings::get_global(cx); - ( - settings - .theme_selection +/// separates theme "mode" ("dark" | "light" | "system") into two separate states +/// - appearance = "dark" | "light" +/// - "system" true/false +/// when system selected: +/// - toggling between light and dark does not change theme.mode, just which variant will be changed +/// when system not selected: +/// - toggling between light and dark does change theme.mode +/// selecting a theme preview will always change theme.["light" | "dark"] to the selected theme, +/// +/// this allows for selecting a dark and light theme option regardless of whether the mode is set to system or not +/// it does not support setting theme to a static value +fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement { + let theme_selection = ThemeSettings::get_global(cx).theme_selection.clone(); + let system_appearance = theme::SystemAppearance::global(cx); + let appearance_state = window.use_state(cx, |_, _cx| { + theme_selection .as_ref() .and_then(|selection| selection.mode()) - .unwrap_or_default(), - settings.active_theme.name.clone(), - ) -} - -fn write_theme_selection(theme_mode: ThemeMode, cx: &App) { - let fs = ::global(cx); - - update_settings_file::(fs, cx, move |settings, _| { - settings.set_mode(theme_mode); + .and_then(|mode| match mode { + ThemeMode::System => None, + ThemeMode::Light => Some(Appearance::Light), + ThemeMode::Dark => Some(Appearance::Dark), + }) + .unwrap_or(*system_appearance) }); + let appearance = *appearance_state.read(cx); + let theme_selection = theme_selection.unwrap_or_else(|| ThemeSelection::Dynamic { + mode: match *system_appearance { + Appearance::Light => ThemeMode::Light, + Appearance::Dark => ThemeMode::Dark, + }, + light: ThemeName("One Light".into()), + dark: ThemeName("One Dark".into()), + }); + let theme_registry = ThemeRegistry::global(cx); + + let current_theme_name = theme_selection.theme(appearance); + let theme_mode = theme_selection.mode(); + + let selected_index = match appearance { + Appearance::Light => 0, + Appearance::Dark => 1, + }; + + let theme_seed = 0xBEEF as f32; + + const LIGHT_THEMES: [&'static str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"]; + const DARK_THEMES: [&'static str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"]; + + let theme_names = match appearance { + Appearance::Light => LIGHT_THEMES, + Appearance::Dark => DARK_THEMES, + }; + let themes = theme_names + .map(|theme_name| theme_registry.get(theme_name)) + .map(Result::unwrap); + + let theme_previews = themes.map(|theme| { + let is_selected = theme.name == current_theme_name; + let name = theme.name.clone(); + let colors = cx.theme().colors(); + v_flex() + .id(name.clone()) + .on_click({ + let theme_name = theme.name.clone(); + move |_, _, cx| { + let fs = ::global(cx); + let theme_name = theme_name.clone(); + update_settings_file::(fs, cx, move |settings, _| { + settings.set_theme(theme_name, appearance); + }); + } + }) + .flex_1() + .child( + div() + .border_2() + .border_color(colors.border_transparent) + .rounded(ThemePreviewTile::CORNER_RADIUS) + .hover(|mut style| { + if !is_selected { + style.border_color = Some(colors.element_hover); + } + style + }) + .when(is_selected, |this| { + this.border_color(colors.border_selected) + }) + .cursor_pointer() + .child(ThemePreviewTile::new(theme, theme_seed)), + ) + .child( + h_flex() + .justify_center() + .items_baseline() + .child(Label::new(name).color(Color::Muted)), + ) + }); + + return v_flex() + .child( + h_flex().justify_between().child(Label::new("Theme")).child( + h_flex() + .gap_2() + .child( + ToggleButtonGroup::single_row( + "theme-selector-onboarding-dark-light", + [ + ToggleButtonSimple::new("Light", { + let appearance_state = appearance_state.clone(); + move |_, _, cx| { + write_appearance_change( + &appearance_state, + Appearance::Light, + cx, + ); + } + }), + ToggleButtonSimple::new("Dark", { + let appearance_state = appearance_state.clone(); + move |_, _, cx| { + write_appearance_change( + &appearance_state, + Appearance::Dark, + cx, + ); + } + }), + ], + ) + .selected_index(selected_index) + .style(ui::ToggleButtonGroupStyle::Outlined) + .button_width(rems_from_px(64.)), + ) + .child( + ToggleButtonGroup::single_row( + "theme-selector-onboarding-system", + [ToggleButtonSimple::new("System", { + let theme = theme_selection.clone(); + move |_, _, cx| { + toggle_system_theme_mode(theme.clone(), appearance, cx); + } + })], + ) + .selected_index((theme_mode != Some(ThemeMode::System)) as usize) + .style(ui::ToggleButtonGroupStyle::Outlined) + .button_width(rems_from_px(64.)), + ), + ), + ) + .child(h_flex().justify_between().children(theme_previews)); + + fn write_appearance_change( + appearance_state: &Entity, + new_appearance: Appearance, + cx: &mut App, + ) { + appearance_state.update(cx, |appearance, _| { + *appearance = new_appearance; + }); + let fs = ::global(cx); + + update_settings_file::(fs, cx, move |settings, _| { + if settings.theme.as_ref().and_then(ThemeSelection::mode) == Some(ThemeMode::System) { + return; + } + let new_mode = match new_appearance { + Appearance::Light => ThemeMode::Light, + Appearance::Dark => ThemeMode::Dark, + }; + settings.set_mode(new_mode); + }); + } + + fn toggle_system_theme_mode( + theme_selection: ThemeSelection, + appearance: Appearance, + cx: &mut App, + ) { + let fs = ::global(cx); + + update_settings_file::(fs, cx, move |settings, _| { + settings.theme = Some(match theme_selection { + ThemeSelection::Static(theme_name) => ThemeSelection::Dynamic { + mode: ThemeMode::System, + light: theme_name.clone(), + dark: theme_name.clone(), + }, + ThemeSelection::Dynamic { + mode: ThemeMode::System, + light, + dark, + } => { + let mode = match appearance { + Appearance::Light => ThemeMode::Light, + Appearance::Dark => ThemeMode::Dark, + }; + ThemeSelection::Dynamic { mode, light, dark } + } + + ThemeSelection::Dynamic { + mode: _, + light, + dark, + } => ThemeSelection::Dynamic { + mode: ThemeMode::System, + light, + dark, + }, + }); + }); + } } fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) { @@ -41,35 +233,10 @@ fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) { }); } -fn render_theme_section(theme_mode: ThemeMode) -> impl IntoElement { - h_flex().justify_between().child(Label::new("Theme")).child( - ToggleButtonGroup::single_row( - "theme-selector-onboarding", - [ - ToggleButtonSimple::new("Light", |_, _, cx| { - write_theme_selection(ThemeMode::Light, cx) - }), - ToggleButtonSimple::new("Dark", |_, _, cx| { - write_theme_selection(ThemeMode::Dark, cx) - }), - ToggleButtonSimple::new("System", |_, _, cx| { - write_theme_selection(ThemeMode::System, cx) - }), - ], - ) - .selected_index(match theme_mode { - ThemeMode::Light => 0, - ThemeMode::Dark => 1, - ThemeMode::System => 2, - }) - .style(ui::ToggleButtonGroupStyle::Outlined) - .button_width(rems_from_px(64.)), - ) -} +fn render_telemetry_section(cx: &App) -> impl IntoElement { + let fs = ::global(cx); -fn render_telemetry_section(fs: Arc, cx: &App) -> impl IntoElement { v_flex() - .gap_4() .child(Label::new("Telemetry").size(LabelSize::Large)) .child(SwitchField::new( @@ -125,17 +292,7 @@ fn render_telemetry_section(fs: Arc, cx: &App) -> impl IntoElement { )) } -pub(crate) fn render_basics_page(onboarding: &Onboarding, cx: &mut App) -> impl IntoElement { - let (theme_mode, active_theme_name) = read_theme_selection(cx); - let themes = match theme_mode { - ThemeMode::Dark => &onboarding.dark_themes, - ThemeMode::Light => &onboarding.light_themes, - ThemeMode::System => match SystemAppearance::global(cx).0 { - Appearance::Light => &onboarding.light_themes, - Appearance::Dark => &onboarding.dark_themes, - }, - }; - +pub(crate) fn render_basics_page(window: &mut Window, cx: &mut App) -> impl IntoElement { let base_keymap = match BaseKeymap::get_global(cx) { BaseKeymap::VSCode => Some(0), BaseKeymap::JetBrains => Some(1), @@ -148,22 +305,7 @@ pub(crate) fn render_basics_page(onboarding: &Onboarding, cx: &mut App) -> impl v_flex() .gap_6() - .child(render_theme_section(theme_mode)) - .child(h_flex().children( - themes.iter().map(|theme| { - ThemePreviewTile::new(theme.clone(), active_theme_name == theme.name, 0.48) - .on_click({ - let theme_name = theme.name.clone(); - let fs = onboarding.fs.clone(); - move |_, _, cx| { - let theme_name = theme_name.clone(); - update_settings_file::(fs.clone(), cx, move |settings, cx| { - settings.set_theme(theme_name.to_string(), SystemAppearance::global(cx).0); - }); - } - }) - }) - )) + .child(render_theme_section(window, cx)) .child( v_flex().gap_2().child(Label::new("Base Keymap")).child( ToggleButtonGroup::two_rows( @@ -206,7 +348,7 @@ pub(crate) fn render_basics_page(onboarding: &Onboarding, cx: &mut App) -> impl ui::ToggleState::Unselected }, { - let fs = onboarding.fs.clone(); + let fs = ::global(cx); move |selection, _, cx| { let enabled = match selection { ToggleState::Selected => true, @@ -222,5 +364,5 @@ pub(crate) fn render_basics_page(onboarding: &Onboarding, cx: &mut App) -> impl } }, ))) - .child(render_telemetry_section(onboarding.fs.clone(), cx)) + .child(render_telemetry_section(cx)) } diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 9b18119b83..6496c09e79 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -13,8 +13,10 @@ use schemars::JsonSchema; use serde::Deserialize; use settings::{SettingsStore, VsCodeSettingsSource}; use std::sync::Arc; -use theme::{Theme, ThemeRegistry}; -use ui::{Avatar, FluentBuilder, KeyBinding, Vector, VectorName, prelude::*, rems_from_px}; +use ui::{ + Avatar, FluentBuilder, Headline, KeyBinding, ParentElement as _, StatefulInteractiveElement, + Vector, VectorName, prelude::*, rems_from_px, +}; use workspace::{ AppState, Workspace, WorkspaceId, dock::DockPosition, @@ -25,6 +27,7 @@ use workspace::{ mod basics_page; mod editing_page; +mod theme_preview; mod welcome; pub struct OnBoardingFeatureFlag {} @@ -219,11 +222,8 @@ enum SelectedPage { struct Onboarding { workspace: WeakEntity, - light_themes: [Arc; 3], - dark_themes: [Arc; 3], focus_handle: FocusHandle, selected_page: SelectedPage, - fs: Arc, user_store: Entity, _settings_subscription: Subscription, } @@ -234,36 +234,11 @@ impl Onboarding { user_store: Entity, cx: &mut App, ) -> Entity { - let theme_registry = ThemeRegistry::global(cx); - - let one_dark = theme_registry - .get("One Dark") - .expect("Default themes are always present"); - let ayu_dark = theme_registry - .get("Ayu Dark") - .expect("Default themes are always present"); - let gruvbox_dark = theme_registry - .get("Gruvbox Dark") - .expect("Default themes are always present"); - - let one_light = theme_registry - .get("One Light") - .expect("Default themes are always present"); - let ayu_light = theme_registry - .get("Ayu Light") - .expect("Default themes are always present"); - let gruvbox_light = theme_registry - .get("Gruvbox Light") - .expect("Default themes are always present"); - cx.new(|cx| Self { workspace, user_store, focus_handle: cx.focus_handle(), - light_themes: [one_light, ayu_light, gruvbox_light], - dark_themes: [one_dark, ayu_dark, gruvbox_dark], selected_page: SelectedPage::Basics, - fs: ::global(cx), _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), }) } @@ -411,7 +386,7 @@ impl Onboarding { fn render_page(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { match self.selected_page { SelectedPage::Basics => { - crate::basics_page::render_basics_page(&self, cx).into_any_element() + crate::basics_page::render_basics_page(window, cx).into_any_element() } SelectedPage::Editing => { crate::editing_page::render_editing_page(window, cx).into_any_element() diff --git a/crates/ui/src/components/theme_preview.rs b/crates/onboarding/src/theme_preview.rs similarity index 73% rename from crates/ui/src/components/theme_preview.rs rename to crates/onboarding/src/theme_preview.rs index d2ff778279..73b540bd40 100644 --- a/crates/ui/src/components/theme_preview.rs +++ b/crates/onboarding/src/theme_preview.rs @@ -1,47 +1,32 @@ -use crate::{component_prelude::Documented, prelude::*, utils::inner_corner_radius}; -use gpui::{App, ClickEvent, Hsla, IntoElement, Length, RenderOnce, Window}; -use std::{rc::Rc, sync::Arc}; +#![allow(unused, dead_code)] +use gpui::{Hsla, Length}; +use std::sync::Arc; use theme::{Theme, ThemeRegistry}; +use ui::{ + IntoElement, RenderOnce, component_prelude::Documented, prelude::*, utils::inner_corner_radius, +}; /// Shows a preview of a theme as an abstract illustration /// of a thumbnail-sized editor. #[derive(IntoElement, RegisterComponent, Documented)] pub struct ThemePreviewTile { theme: Arc, - selected: bool, - on_click: Option>, seed: f32, } impl ThemePreviewTile { - pub fn new(theme: Arc, selected: bool, seed: f32) -> Self { - Self { - theme, - seed, - selected, - on_click: None, - } - } + pub const CORNER_RADIUS: Pixels = px(8.0); - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - - pub fn on_click( - mut self, - listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_click = Some(Rc::new(listener)); - self + pub fn new(theme: Arc, seed: f32) -> Self { + Self { theme, seed } } } impl RenderOnce for ThemePreviewTile { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + fn render(self, _window: &mut ui::Window, _cx: &mut ui::App) -> impl IntoElement { let color = self.theme.colors(); - let root_radius = px(8.0); + let root_radius = Self::CORNER_RADIUS; let root_border = px(2.0); let root_padding = px(2.0); let child_border = px(1.0); @@ -188,21 +173,9 @@ impl RenderOnce for ThemePreviewTile { let content = div().size_full().flex().child(sidebar).child(pane); div() - // Note: If two theme preview tiles are rendering the same theme they'll share an ID - // this will mean on hover and on click events will be shared between them - .id(SharedString::from(self.theme.id.clone())) - .when_some(self.on_click.clone(), |this, on_click| { - this.on_click(move |event, window, cx| on_click(event, window, cx)) - .hover(|style| style.cursor_pointer().border_color(color.element_hover)) - }) .size_full() .rounded(root_radius) .p(root_padding) - .border(root_border) - .border_color(color.border_transparent) - .when(self.selected, |this| { - this.border_color(color.border_selected) - }) .child( div() .size_full() @@ -244,24 +217,14 @@ impl Component for ThemePreviewTile { .p_4() .children({ if let Some(one_dark) = one_dark.ok() { - vec![example_group(vec![ - single_example( - "Default", - div() - .w(px(240.)) - .h(px(180.)) - .child(ThemePreviewTile::new(one_dark.clone(), false, 0.42)) - .into_any_element(), - ), - single_example( - "Selected", - div() - .w(px(240.)) - .h(px(180.)) - .child(ThemePreviewTile::new(one_dark, true, 0.42)) - .into_any_element(), - ), - ])] + vec![example_group(vec![single_example( + "Default", + div() + .w(px(240.)) + .h(px(180.)) + .child(ThemePreviewTile::new(one_dark.clone(), 0.42)) + .into_any_element(), + )])] } else { vec![] } @@ -276,11 +239,10 @@ impl Component for ThemePreviewTile { .iter() .enumerate() .map(|(_, theme)| { - div().w(px(200.)).h(px(140.)).child(ThemePreviewTile::new( - theme.clone(), - false, - 0.42, - )) + div() + .w(px(200.)) + .h(px(140.)) + .child(ThemePreviewTile::new(theme.clone(), 0.42)) }) .collect::>(), ) diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 47783283d5..20c837f287 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -438,7 +438,7 @@ fn default_font_fallbacks() -> Option { impl ThemeSettingsContent { /// Sets the theme for the given appearance to the theme with the specified name. - pub fn set_theme(&mut self, theme_name: String, appearance: Appearance) { + pub fn set_theme(&mut self, theme_name: impl Into>, appearance: Appearance) { if let Some(selection) = self.theme.as_mut() { let theme_to_update = match selection { ThemeSelection::Static(theme) => theme, diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 07ea331ef5..9c2961c55f 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -34,7 +34,6 @@ mod stack; mod sticky_items; mod tab; mod tab_bar; -mod theme_preview; mod toggle; mod tooltip; @@ -77,7 +76,6 @@ pub use stack::*; pub use sticky_items::*; pub use tab::*; pub use tab_bar::*; -pub use theme_preview::*; pub use toggle::*; pub use tooltip::*; From aea1d481844f98691d794f6d434e01c5ffeae47e Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 31 Jul 2025 17:01:21 -0400 Subject: [PATCH 002/693] cloud_api_client: Add `create_llm_token` method (#35428) This PR adds a `create_llm_token` method to the `CloudApiClient`. Release Notes: - N/A --- .../cloud_api_client/src/cloud_api_client.rs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/crates/cloud_api_client/src/cloud_api_client.rs b/crates/cloud_api_client/src/cloud_api_client.rs index 2d017cf2ee..5a768810c0 100644 --- a/crates/cloud_api_client/src/cloud_api_client.rs +++ b/crates/cloud_api_client/src/cloud_api_client.rs @@ -80,4 +80,42 @@ impl CloudApiClient { Ok(serde_json::from_str(&body)?) } + + pub async fn create_llm_token( + &self, + system_id: Option, + ) -> Result { + let mut request_builder = Request::builder() + .method(Method::POST) + .uri( + self.http_client + .build_zed_cloud_url("/client/llm_tokens", &[])? + .as_ref(), + ) + .header("Content-Type", "application/json") + .header("Authorization", self.authorization_header()?); + + if let Some(system_id) = system_id { + request_builder = request_builder.header(ZED_SYSTEM_ID_HEADER_NAME, system_id); + } + + let request = request_builder.body(AsyncBody::default())?; + + let mut response = self.http_client.send(request).await?; + + if !response.status().is_success() { + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + + anyhow::bail!( + "Failed to create LLM token.\nStatus: {:?}\nBody: {body}", + response.status() + ) + } + + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + + Ok(serde_json::from_str(&body)?) + } } From 8e7f1899e1bfc0613845237faa18b5bf85aaa93f Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 31 Jul 2025 14:31:29 -0700 Subject: [PATCH 003/693] Revert "Increase the number of parallel request handlers per connection" (#35435) Reverts zed-industries/zed#35046 This made the problem worse ;-; Release Notes: - N/A --- crates/collab/src/rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 5c35394e1d..56d44c0ae4 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -838,7 +838,7 @@ impl Server { // This arrangement ensures we will attempt to process earlier messages first, but fall // back to processing messages arrived later in the spirit of making progress. let mut foreground_message_handlers = FuturesUnordered::new(); - let concurrent_handlers = Arc::new(Semaphore::new(512)); + let concurrent_handlers = Arc::new(Semaphore::new(256)); loop { let next_message = async { let permit = concurrent_handlers.clone().acquire_owned().await.unwrap(); From 410348deb077c078b129ce209c6e32127b4471c9 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 31 Jul 2025 18:12:04 -0400 Subject: [PATCH 004/693] Acquire LLM token from Cloud instead of Collab for Edit Predictions (#35431) This PR updates the Zed Edit Prediction provider to acquire the LLM token from Cloud instead of Collab to allow using Edit Predictions even when disconnected from or unable to connect to the Collab server. Release Notes: - N/A --------- Co-authored-by: Richard Feldman --- Cargo.lock | 2 +- crates/client/src/cloud/user_store.rs | 44 +++- crates/client/src/user.rs | 21 -- .../language_model/src/model/cloud_model.rs | 11 +- crates/zed/src/main.rs | 2 +- .../zed/src/zed/inline_completion_registry.rs | 31 ++- crates/zeta/Cargo.toml | 6 +- crates/zeta/src/zeta.rs | 219 +++++++++++------- 8 files changed, 211 insertions(+), 125 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d2ded690c6..61875e878f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20570,6 +20570,7 @@ dependencies = [ "call", "client", "clock", + "cloud_api_types", "cloud_llm_client", "collections", "command_palette_hooks", @@ -20590,7 +20591,6 @@ dependencies = [ "menu", "postage", "project", - "proto", "regex", "release_channel", "reqwest_client", diff --git a/crates/client/src/cloud/user_store.rs b/crates/client/src/cloud/user_store.rs index a9b13ca23c..ea432f71ed 100644 --- a/crates/client/src/cloud/user_store.rs +++ b/crates/client/src/cloud/user_store.rs @@ -8,13 +8,14 @@ use cloud_llm_client::Plan; use gpui::{Context, Entity, Subscription, Task}; use util::{ResultExt as _, maybe}; -use crate::UserStore; use crate::user::Event as RpcUserStoreEvent; +use crate::{EditPredictionUsage, RequestUsage, UserStore}; pub struct CloudUserStore { cloud_client: Arc, authenticated_user: Option>, plan_info: Option>, + edit_prediction_usage: Option, _maintain_authenticated_user_task: Task<()>, _rpc_plan_updated_subscription: Subscription, } @@ -32,6 +33,7 @@ impl CloudUserStore { cloud_client: cloud_client.clone(), authenticated_user: None, plan_info: None, + edit_prediction_usage: None, _maintain_authenticated_user_task: cx.spawn(async move |this, cx| { maybe!(async move { loop { @@ -102,8 +104,48 @@ impl CloudUserStore { }) } + pub fn has_accepted_tos(&self) -> bool { + self.authenticated_user + .as_ref() + .map(|user| user.accepted_tos_at.is_some()) + .unwrap_or_default() + } + + /// Returns whether the user's account is too new to use the service. + pub fn account_too_young(&self) -> bool { + self.plan_info + .as_ref() + .map(|plan| plan.is_account_too_young) + .unwrap_or_default() + } + + /// Returns whether the current user has overdue invoices and usage should be blocked. + pub fn has_overdue_invoices(&self) -> bool { + self.plan_info + .as_ref() + .map(|plan| plan.has_overdue_invoices) + .unwrap_or_default() + } + + pub fn edit_prediction_usage(&self) -> Option { + self.edit_prediction_usage + } + + pub fn update_edit_prediction_usage( + &mut self, + usage: EditPredictionUsage, + cx: &mut Context, + ) { + self.edit_prediction_usage = Some(usage); + cx.notify(); + } + fn update_authenticated_user(&mut self, response: GetAuthenticatedUserResponse) { self.authenticated_user = Some(Arc::new(response.user)); + self.edit_prediction_usage = Some(EditPredictionUsage(RequestUsage { + limit: response.plan.usage.edit_predictions.limit, + amount: response.plan.usage.edit_predictions.used as i32, + })); self.plan_info = Some(Arc::new(response.plan)); } diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 97fb959171..0ba7d1472b 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -114,7 +114,6 @@ pub struct UserStore { subscription_period: Option<(DateTime, DateTime)>, trial_started_at: Option>, model_request_usage: Option, - edit_prediction_usage: Option, is_usage_based_billing_enabled: Option, account_too_young: Option, has_overdue_invoices: Option, @@ -193,7 +192,6 @@ impl UserStore { subscription_period: None, trial_started_at: None, model_request_usage: None, - edit_prediction_usage: None, is_usage_based_billing_enabled: None, account_too_young: None, has_overdue_invoices: None, @@ -381,12 +379,6 @@ impl UserStore { RequestUsage::from_proto(usage.model_requests_usage_amount, limit) }) .map(ModelRequestUsage); - this.edit_prediction_usage = usage - .edit_predictions_usage_limit - .and_then(|limit| { - RequestUsage::from_proto(usage.model_requests_usage_amount, limit) - }) - .map(EditPredictionUsage); } cx.emit(Event::PlanUpdated); @@ -400,15 +392,6 @@ impl UserStore { cx.notify(); } - pub fn update_edit_prediction_usage( - &mut self, - usage: EditPredictionUsage, - cx: &mut Context, - ) { - self.edit_prediction_usage = Some(usage); - cx.notify(); - } - fn update_contacts(&mut self, message: UpdateContacts, cx: &Context) -> Task> { match message { UpdateContacts::Wait(barrier) => { @@ -797,10 +780,6 @@ impl UserStore { self.model_request_usage } - pub fn edit_prediction_usage(&self) -> Option { - self.edit_prediction_usage - } - pub fn watch_current_user(&self) -> watch::Receiver>> { self.current_user.clone() } diff --git a/crates/language_model/src/model/cloud_model.rs b/crates/language_model/src/model/cloud_model.rs index 72b7132c60..a5d2ac34f5 100644 --- a/crates/language_model/src/model/cloud_model.rs +++ b/crates/language_model/src/model/cloud_model.rs @@ -64,9 +64,14 @@ impl LlmApiToken { mut lock: RwLockWriteGuard<'_, Option>, client: &Arc, ) -> Result { - let response = client.request(proto::GetLlmToken {}).await?; - *lock = Some(response.token.clone()); - Ok(response.token.clone()) + let system_id = client + .telemetry() + .system_id() + .map(|system_id| system_id.to_string()); + + let response = client.cloud_client().create_llm_token(system_id).await?; + *lock = Some(response.token.0.clone()); + Ok(response.token.0.clone()) } } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 338840607b..a18c112c7e 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -564,7 +564,7 @@ pub fn main() { snippet_provider::init(cx); inline_completion_registry::init( app_state.client.clone(), - app_state.user_store.clone(), + app_state.cloud_user_store.clone(), cx, ); let prompt_builder = PromptBuilder::load(app_state.fs.clone(), stdout_is_a_pty(), cx); diff --git a/crates/zed/src/zed/inline_completion_registry.rs b/crates/zed/src/zed/inline_completion_registry.rs index 52b7166a11..ba19457d39 100644 --- a/crates/zed/src/zed/inline_completion_registry.rs +++ b/crates/zed/src/zed/inline_completion_registry.rs @@ -1,4 +1,4 @@ -use client::{Client, DisableAiSettings, UserStore}; +use client::{Client, CloudUserStore, DisableAiSettings}; use collections::HashMap; use copilot::{Copilot, CopilotCompletionProvider}; use editor::Editor; @@ -13,12 +13,12 @@ use util::ResultExt; use workspace::Workspace; use zeta::{ProviderDataCollection, ZetaInlineCompletionProvider}; -pub fn init(client: Arc, user_store: Entity, cx: &mut App) { +pub fn init(client: Arc, cloud_user_store: Entity, cx: &mut App) { let editors: Rc, AnyWindowHandle>>> = Rc::default(); cx.observe_new({ let editors = editors.clone(); let client = client.clone(); - let user_store = user_store.clone(); + let cloud_user_store = cloud_user_store.clone(); move |editor: &mut Editor, window, cx: &mut Context| { if !editor.mode().is_full() { return; @@ -48,7 +48,7 @@ pub fn init(client: Arc, user_store: Entity, cx: &mut App) { editor, provider, &client, - user_store.clone(), + cloud_user_store.clone(), window, cx, ); @@ -60,7 +60,7 @@ pub fn init(client: Arc, user_store: Entity, cx: &mut App) { let mut provider = all_language_settings(None, cx).edit_predictions.provider; cx.spawn({ - let user_store = user_store.clone(); + let cloud_user_store = cloud_user_store.clone(); let editors = editors.clone(); let client = client.clone(); @@ -72,7 +72,7 @@ pub fn init(client: Arc, user_store: Entity, cx: &mut App) { &editors, provider, &client, - user_store.clone(), + cloud_user_store.clone(), cx, ); }) @@ -85,15 +85,12 @@ pub fn init(client: Arc, user_store: Entity, cx: &mut App) { cx.observe_global::({ let editors = editors.clone(); let client = client.clone(); - let user_store = user_store.clone(); + let cloud_user_store = cloud_user_store.clone(); move |cx| { let new_provider = all_language_settings(None, cx).edit_predictions.provider; if new_provider != provider { - let tos_accepted = user_store - .read(cx) - .current_user_has_accepted_terms() - .unwrap_or(false); + let tos_accepted = cloud_user_store.read(cx).has_accepted_tos(); telemetry::event!( "Edit Prediction Provider Changed", @@ -107,7 +104,7 @@ pub fn init(client: Arc, user_store: Entity, cx: &mut App) { &editors, provider, &client, - user_store.clone(), + cloud_user_store.clone(), cx, ); @@ -148,7 +145,7 @@ fn assign_edit_prediction_providers( editors: &Rc, AnyWindowHandle>>>, provider: EditPredictionProvider, client: &Arc, - user_store: Entity, + cloud_user_store: Entity, cx: &mut App, ) { for (editor, window) in editors.borrow().iter() { @@ -158,7 +155,7 @@ fn assign_edit_prediction_providers( editor, provider, &client, - user_store.clone(), + cloud_user_store.clone(), window, cx, ); @@ -213,7 +210,7 @@ fn assign_edit_prediction_provider( editor: &mut Editor, provider: EditPredictionProvider, client: &Arc, - user_store: Entity, + cloud_user_store: Entity, window: &mut Window, cx: &mut Context, ) { @@ -244,7 +241,7 @@ fn assign_edit_prediction_provider( } } EditPredictionProvider::Zed => { - if client.status().borrow().is_connected() { + if cloud_user_store.read(cx).is_authenticated() { let mut worktree = None; if let Some(buffer) = &singleton_buffer { @@ -266,7 +263,7 @@ fn assign_edit_prediction_provider( .map(|workspace| workspace.downgrade()); let zeta = - zeta::Zeta::register(workspace, worktree, client.clone(), user_store, cx); + zeta::Zeta::register(workspace, worktree, client.clone(), cloud_user_store, cx); if let Some(buffer) = &singleton_buffer { if buffer.read(cx).file().is_some() { diff --git a/crates/zeta/Cargo.toml b/crates/zeta/Cargo.toml index 294d95aefd..26eeda3f22 100644 --- a/crates/zeta/Cargo.toml +++ b/crates/zeta/Cargo.toml @@ -40,7 +40,6 @@ log.workspace = true menu.workspace = true postage.workspace = true project.workspace = true -proto.workspace = true regex.workspace = true release_channel.workspace = true serde.workspace = true @@ -59,9 +58,11 @@ worktree.workspace = true zed_actions.workspace = true [dev-dependencies] -collections = { workspace = true, features = ["test-support"] } +call = { workspace = true, features = ["test-support"] } client = { workspace = true, features = ["test-support"] } clock = { workspace = true, features = ["test-support"] } +cloud_api_types.workspace = true +collections = { workspace = true, features = ["test-support"] } ctor.workspace = true editor = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } @@ -77,5 +78,4 @@ tree-sitter-rust.workspace = true unindent.workspace = true workspace = { workspace = true, features = ["test-support"] } worktree = { workspace = true, features = ["test-support"] } -call = { workspace = true, features = ["test-support"] } zlog.workspace = true diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index d5c6be278b..d295b7d17c 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -16,7 +16,7 @@ pub use rate_completion_modal::*; use anyhow::{Context as _, Result, anyhow}; use arrayvec::ArrayVec; -use client::{Client, EditPredictionUsage, UserStore}; +use client::{Client, CloudUserStore, EditPredictionUsage, UserStore}; use cloud_llm_client::{ AcceptEditPredictionBody, EXPIRED_LLM_TOKEN_HEADER_NAME, MINIMUM_REQUIRED_VERSION_HEADER_NAME, PredictEditsBody, PredictEditsResponse, ZED_VERSION_HEADER_NAME, @@ -226,12 +226,9 @@ pub struct Zeta { data_collection_choice: Entity, llm_token: LlmApiToken, _llm_token_subscription: Subscription, - /// Whether the terms of service have been accepted. - tos_accepted: bool, /// Whether an update to a newer version of Zed is required to continue using Zeta. update_required: bool, - user_store: Entity, - _user_store_subscription: Subscription, + cloud_user_store: Entity, license_detection_watchers: HashMap>, } @@ -244,11 +241,11 @@ impl Zeta { workspace: Option>, worktree: Option>, client: Arc, - user_store: Entity, + cloud_user_store: Entity, cx: &mut App, ) -> Entity { let this = Self::global(cx).unwrap_or_else(|| { - let entity = cx.new(|cx| Self::new(workspace, client, user_store, cx)); + let entity = cx.new(|cx| Self::new(workspace, client, cloud_user_store, cx)); cx.set_global(ZetaGlobal(entity.clone())); entity }); @@ -271,13 +268,13 @@ impl Zeta { } pub fn usage(&self, cx: &App) -> Option { - self.user_store.read(cx).edit_prediction_usage() + self.cloud_user_store.read(cx).edit_prediction_usage() } fn new( workspace: Option>, client: Arc, - user_store: Entity, + cloud_user_store: Entity, cx: &mut Context, ) -> Self { let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx); @@ -306,24 +303,9 @@ impl Zeta { .detach_and_log_err(cx); }, ), - tos_accepted: user_store - .read(cx) - .current_user_has_accepted_terms() - .unwrap_or(false), update_required: false, - _user_store_subscription: cx.subscribe(&user_store, |this, user_store, event, cx| { - match event { - client::user::Event::PrivateUserInfoUpdated => { - this.tos_accepted = user_store - .read(cx) - .current_user_has_accepted_terms() - .unwrap_or(false); - } - _ => {} - } - }), license_detection_watchers: HashMap::default(), - user_store, + cloud_user_store, } } @@ -552,8 +534,8 @@ impl Zeta { if let Some(usage) = usage { this.update(cx, |this, cx| { - this.user_store.update(cx, |user_store, cx| { - user_store.update_edit_prediction_usage(usage, cx); + this.cloud_user_store.update(cx, |cloud_user_store, cx| { + cloud_user_store.update_edit_prediction_usage(usage, cx); }); }) .ok(); @@ -894,8 +876,8 @@ and then another if response.status().is_success() { if let Some(usage) = EditPredictionUsage::from_headers(response.headers()).ok() { this.update(cx, |this, cx| { - this.user_store.update(cx, |user_store, cx| { - user_store.update_edit_prediction_usage(usage, cx); + this.cloud_user_store.update(cx, |cloud_user_store, cx| { + cloud_user_store.update_edit_prediction_usage(usage, cx); }); })?; } @@ -1573,7 +1555,12 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider } fn needs_terms_acceptance(&self, cx: &App) -> bool { - !self.zeta.read(cx).tos_accepted + !self + .zeta + .read(cx) + .cloud_user_store + .read(cx) + .has_accepted_tos() } fn is_refreshing(&self) -> bool { @@ -1588,7 +1575,7 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider _debounce: bool, cx: &mut Context, ) { - if !self.zeta.read(cx).tos_accepted { + if self.needs_terms_acceptance(cx) { return; } @@ -1599,9 +1586,9 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider if self .zeta .read(cx) - .user_store - .read_with(cx, |user_store, _| { - user_store.account_too_young() || user_store.has_overdue_invoices() + .cloud_user_store + .read_with(cx, |cloud_user_store, _cx| { + cloud_user_store.account_too_young() || cloud_user_store.has_overdue_invoices() }) { return; @@ -1819,15 +1806,51 @@ fn tokens_for_bytes(bytes: usize) -> usize { mod tests { use client::test::FakeServer; use clock::FakeSystemClock; + use cloud_api_types::{ + AuthenticatedUser, CreateLlmTokenResponse, GetAuthenticatedUserResponse, LlmToken, PlanInfo, + }; + use cloud_llm_client::{CurrentUsage, Plan, UsageData, UsageLimit}; use gpui::TestAppContext; use http_client::FakeHttpClient; use indoc::indoc; use language::Point; - use rpc::proto; use settings::SettingsStore; use super::*; + fn make_get_authenticated_user_response() -> GetAuthenticatedUserResponse { + GetAuthenticatedUserResponse { + user: AuthenticatedUser { + id: 1, + metrics_id: "metrics-id-1".to_string(), + avatar_url: "".to_string(), + github_login: "".to_string(), + name: None, + is_staff: false, + accepted_tos_at: None, + }, + feature_flags: vec![], + plan: PlanInfo { + plan: Plan::ZedPro, + subscription_period: None, + usage: CurrentUsage { + model_requests: UsageData { + used: 0, + limit: UsageLimit::Limited(500), + }, + edit_predictions: UsageData { + used: 250, + limit: UsageLimit::Unlimited, + }, + }, + trial_started_at: None, + is_usage_based_billing_enabled: false, + is_account_too_young: false, + has_overdue_invoices: false, + }, + } + } + #[gpui::test] async fn test_inline_completion_basic_interpolation(cx: &mut TestAppContext) { let buffer = cx.new(|cx| Buffer::local("Lorem ipsum dolor", cx)); @@ -2027,28 +2050,55 @@ mod tests { <|editable_region_end|> ```"}; - let http_client = FakeHttpClient::create(move |_| async move { - Ok(http_client::Response::builder() - .status(200) - .body( - serde_json::to_string(&PredictEditsResponse { - request_id: Uuid::parse_str("7e86480f-3536-4d2c-9334-8213e3445d45") - .unwrap(), - output_excerpt: completion_response.to_string(), - }) - .unwrap() - .into(), - ) - .unwrap()) + let http_client = FakeHttpClient::create(move |req| async move { + match (req.method(), req.uri().path()) { + (&Method::GET, "/client/users/me") => Ok(http_client::Response::builder() + .status(200) + .body( + serde_json::to_string(&make_get_authenticated_user_response()) + .unwrap() + .into(), + ) + .unwrap()), + (&Method::POST, "/client/llm_tokens") => Ok(http_client::Response::builder() + .status(200) + .body( + serde_json::to_string(&CreateLlmTokenResponse { + token: LlmToken("the-llm-token".to_string()), + }) + .unwrap() + .into(), + ) + .unwrap()), + (&Method::POST, "/predict_edits/v2") => Ok(http_client::Response::builder() + .status(200) + .body( + serde_json::to_string(&PredictEditsResponse { + request_id: Uuid::parse_str("7e86480f-3536-4d2c-9334-8213e3445d45") + .unwrap(), + output_excerpt: completion_response.to_string(), + }) + .unwrap() + .into(), + ) + .unwrap()), + _ => Ok(http_client::Response::builder() + .status(404) + .body("Not Found".into()) + .unwrap()), + } }); let client = cx.update(|cx| Client::new(Arc::new(FakeSystemClock::new()), http_client, cx)); cx.update(|cx| { RefreshLlmTokenListener::register(client.clone(), cx); }); - let server = FakeServer::for_client(42, &client, cx).await; + // Construct the fake server to authenticate. + let _server = FakeServer::for_client(42, &client, cx).await; let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - let zeta = cx.new(|cx| Zeta::new(None, client, user_store, cx)); + let cloud_user_store = + cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store.clone(), cx)); + let zeta = cx.new(|cx| Zeta::new(None, client, cloud_user_store, cx)); let buffer = cx.new(|cx| Buffer::local(buffer_content, cx)); let cursor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(1, 0))); @@ -2056,13 +2106,6 @@ mod tests { zeta.request_completion(None, &buffer, cursor, false, cx) }); - server.receive::().await.unwrap(); - let token_request = server.receive::().await.unwrap(); - server.respond( - token_request.receipt(), - proto::GetLlmTokenResponse { token: "".into() }, - ); - let completion = completion_task.await.unwrap().unwrap(); buffer.update(cx, |buffer, cx| { buffer.edit(completion.edits.iter().cloned(), None, cx) @@ -2079,20 +2122,44 @@ mod tests { cx: &mut TestAppContext, ) -> Vec<(Range, String)> { let completion_response = completion_response.to_string(); - let http_client = FakeHttpClient::create(move |_| { + let http_client = FakeHttpClient::create(move |req| { let completion = completion_response.clone(); async move { - Ok(http_client::Response::builder() - .status(200) - .body( - serde_json::to_string(&PredictEditsResponse { - request_id: Uuid::new_v4(), - output_excerpt: completion, - }) - .unwrap() - .into(), - ) - .unwrap()) + match (req.method(), req.uri().path()) { + (&Method::GET, "/client/users/me") => Ok(http_client::Response::builder() + .status(200) + .body( + serde_json::to_string(&make_get_authenticated_user_response()) + .unwrap() + .into(), + ) + .unwrap()), + (&Method::POST, "/client/llm_tokens") => Ok(http_client::Response::builder() + .status(200) + .body( + serde_json::to_string(&CreateLlmTokenResponse { + token: LlmToken("the-llm-token".to_string()), + }) + .unwrap() + .into(), + ) + .unwrap()), + (&Method::POST, "/predict_edits/v2") => Ok(http_client::Response::builder() + .status(200) + .body( + serde_json::to_string(&PredictEditsResponse { + request_id: Uuid::new_v4(), + output_excerpt: completion, + }) + .unwrap() + .into(), + ) + .unwrap()), + _ => Ok(http_client::Response::builder() + .status(404) + .body("Not Found".into()) + .unwrap()), + } } }); @@ -2100,9 +2167,12 @@ mod tests { cx.update(|cx| { RefreshLlmTokenListener::register(client.clone(), cx); }); - let server = FakeServer::for_client(42, &client, cx).await; + // Construct the fake server to authenticate. + let _server = FakeServer::for_client(42, &client, cx).await; let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - let zeta = cx.new(|cx| Zeta::new(None, client, user_store, cx)); + let cloud_user_store = + cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store.clone(), cx)); + let zeta = cx.new(|cx| Zeta::new(None, client, cloud_user_store, cx)); let buffer = cx.new(|cx| Buffer::local(buffer_content, cx)); let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); @@ -2111,13 +2181,6 @@ mod tests { zeta.request_completion(None, &buffer, cursor, false, cx) }); - server.receive::().await.unwrap(); - let token_request = server.receive::().await.unwrap(); - server.respond( - token_request.receipt(), - proto::GetLlmTokenResponse { token: "".into() }, - ); - let completion = completion_task.await.unwrap().unwrap(); completion .edits From 5feb759c20f4a66c06496ac7001b20c937ea69f3 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 31 Jul 2025 18:20:35 -0400 Subject: [PATCH 005/693] Additions for settings profile selector (#35439) - Added profile selector to `zed > settings` submenu. - Added examples to the `default.json` docs. - Reduced length of the setting description that shows on autocomplete, since it was cutoff in the autocomplete popover. Release Notes: - N/A --- assets/settings/default.json | 21 +++++++++++++++++++-- crates/settings/src/settings_store.rs | 2 +- crates/zed/src/zed/app_menus.rs | 4 ++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 13f56fae49..4734b5d118 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1878,7 +1878,24 @@ "dock": "bottom", "button": true }, - // Configures any number of settings profiles that are temporarily applied - // when selected from `settings profile selector: toggle`. + // Configures any number of settings profiles that are temporarily applied on + // top of your existing user settings when selected from + // `settings profile selector: toggle`. + // Examples: + // "profiles": { + // "Presenting": { + // "agent_font_size": 20.0, + // "buffer_font_size": 20.0, + // "theme": "One Light", + // "ui_font_size": 20.0 + // }, + // "Python (ty)": { + // "languages": { + // "Python": { + // "language_servers": ["ty"] + // } + // } + // } + // } "profiles": [] } diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 278dcc4c03..7f6437dac8 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -1078,7 +1078,7 @@ impl SettingsStore { "preview": zed_settings_override_ref, "profiles": { "type": "object", - "description": "Configures any number of settings profiles that are temporarily applied when selected from `settings profile selector: toggle`.", + "description": "Configures any number of settings profiles.", "additionalProperties": zed_settings_override_ref } } diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index 78532b10b4..15d5659f03 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -24,6 +24,10 @@ pub fn app_menus() -> Vec { zed_actions::OpenDefaultKeymap, ), MenuItem::action("Open Project Settings", super::OpenProjectSettings), + MenuItem::action( + "Select Settings Profile...", + zed_actions::settings_profile_selector::Toggle, + ), MenuItem::action( "Select Theme...", zed_actions::theme_selector::Toggle::default(), From 4a82b6c5ee9232e7e2dcc752123bb6429ab66eb2 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Thu, 31 Jul 2025 18:29:51 -0400 Subject: [PATCH 006/693] jetbrains: Unmap cmd-k in Jetbrains keymap (#35443) This only works after a delay in most situations because of the all chorded `cmd-k` mappings in the so disable them for now. Reported by @jer-k: https://x.com/J_Kreutzbender/status/1951033355434336606 Release Notes: - Undo mapping of `cmd-k` for Git Panel in default Jetbrains keymap (thanks [@jer-k](https://github.com/jer-k)) --- assets/keymaps/linux/jetbrains.json | 2 +- assets/keymaps/macos/jetbrains.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/keymaps/linux/jetbrains.json b/assets/keymaps/linux/jetbrains.json index f81f363ae0..9bc1f24bfb 100644 --- a/assets/keymaps/linux/jetbrains.json +++ b/assets/keymaps/linux/jetbrains.json @@ -95,7 +95,7 @@ "ctrl-shift-r": ["pane::DeploySearch", { "replace_enabled": true }], "alt-shift-f10": "task::Spawn", "ctrl-e": "file_finder::Toggle", - "ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor + // "ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor "ctrl-shift-n": "file_finder::Toggle", "ctrl-shift-a": "command_palette::Toggle", "shift shift": "command_palette::Toggle", diff --git a/assets/keymaps/macos/jetbrains.json b/assets/keymaps/macos/jetbrains.json index 5795d2ac7e..b1cd51a338 100644 --- a/assets/keymaps/macos/jetbrains.json +++ b/assets/keymaps/macos/jetbrains.json @@ -97,7 +97,7 @@ "cmd-shift-r": ["pane::DeploySearch", { "replace_enabled": true }], "ctrl-alt-r": "task::Spawn", "cmd-e": "file_finder::Toggle", - "cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor + // "cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor "cmd-shift-o": "file_finder::Toggle", "cmd-shift-a": "command_palette::Toggle", "shift shift": "command_palette::Toggle", From 2b36d4ec94e1013bff014adfae1110e1d7824651 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 31 Jul 2025 15:40:19 -0700 Subject: [PATCH 007/693] Add a field to MultiLSPQuery span showing the current request (#35372) Release Notes: - N/A --- crates/collab/src/rpc.rs | 14 ++++++++++++-- crates/proto/src/proto.rs | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 56d44c0ae4..e648617fe1 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -42,7 +42,7 @@ use collections::{HashMap, HashSet}; pub use connection_pool::{ConnectionPool, ZedVersion}; use core::fmt::{self, Debug, Formatter}; use reqwest_client::ReqwestClient; -use rpc::proto::split_repository_update; +use rpc::proto::{MultiLspQuery, split_repository_update}; use supermaven_api::{CreateExternalUserRequest, SupermavenAdminApi}; use futures::{ @@ -374,7 +374,7 @@ impl Server { .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) - .add_request_handler(forward_mutating_project_request::) + .add_request_handler(multi_lsp_query) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) @@ -865,6 +865,7 @@ impl Server { user_id=field::Empty, login=field::Empty, impersonator=field::Empty, + multi_lsp_query_request=field::Empty, ); principal.update_span(&span); let span_enter = span.enter(); @@ -2329,6 +2330,15 @@ where Ok(()) } +async fn multi_lsp_query( + request: MultiLspQuery, + response: Response, + session: Session, +) -> Result<()> { + tracing::Span::current().record("multi_lsp_query_request", request.request_str()); + forward_mutating_project_request(request, response, session).await +} + /// Notify other participants that a new buffer has been created async fn create_buffer_for_peer( request: proto::CreateBufferForPeer, diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 9f586a7839..83e5a77c86 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -784,6 +784,25 @@ pub fn split_repository_update( }]) } +impl MultiLspQuery { + pub fn request_str(&self) -> &str { + match self.request { + Some(multi_lsp_query::Request::GetHover(_)) => "GetHover", + Some(multi_lsp_query::Request::GetCodeActions(_)) => "GetCodeActions", + Some(multi_lsp_query::Request::GetSignatureHelp(_)) => "GetSignatureHelp", + Some(multi_lsp_query::Request::GetCodeLens(_)) => "GetCodeLens", + Some(multi_lsp_query::Request::GetDocumentDiagnostics(_)) => "GetDocumentDiagnostics", + Some(multi_lsp_query::Request::GetDocumentColor(_)) => "GetDocumentColor", + Some(multi_lsp_query::Request::GetDefinition(_)) => "GetDefinition", + Some(multi_lsp_query::Request::GetDeclaration(_)) => "GetDeclaration", + Some(multi_lsp_query::Request::GetTypeDefinition(_)) => "GetTypeDefinition", + Some(multi_lsp_query::Request::GetImplementation(_)) => "GetImplementation", + Some(multi_lsp_query::Request::GetReferences(_)) => "GetReferences", + None => "", + } + } +} + #[cfg(test)] mod tests { use super::*; From 7c169fc9b50a53d0b78f4ad1d14c1bb747ae5ca4 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 31 Jul 2025 19:45:02 -0400 Subject: [PATCH 008/693] debugger: Send initialized event from fake server at a more realistic time (#35446) The spec says: > :arrow_left: Initialized Event > This event indicates that the debug adapter is ready to accept configuration requests (e.g. setBreakpoints, setExceptionBreakpoints). > > A debug adapter is expected to send this event when it is ready to accept configuration requests (but not before the initialize request has finished). Previously in tests, `intercept_debug_sessions` was just spawning off a background task to send the event after setting up the client, so the event wasn't actually synchronized with the flow of messages in the way the spec says it should be. This PR makes it so that the `FakeTransport` injects the event right after a successful response to the initialize request, and doesn't send it otherwise. Release Notes: - N/A --- crates/dap/src/client.rs | 2 +- crates/dap/src/transport.rs | 20 ++++++++++++++++++++ crates/project/src/debugger/test.rs | 10 +--------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 86a15b2d8a..7b791450ec 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -295,7 +295,7 @@ mod tests { request: dap_types::StartDebuggingRequestArgumentsRequest::Launch, }, }, - Box::new(|_| panic!("Did not expect to hit this code path")), + Box::new(|_| {}), &mut cx.to_async(), ) .await diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 6dadf1cf35..f9fbbfc842 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -883,6 +883,7 @@ impl FakeTransport { break Err(anyhow!("exit in response to request")); } }; + let success = response.success; let message = serde_json::to_string(&Message::Response(response)).unwrap(); @@ -893,6 +894,25 @@ impl FakeTransport { ) .await .unwrap(); + + if request.command == dap_types::requests::Initialize::COMMAND + && success + { + let message = serde_json::to_string(&Message::Event(Box::new( + dap_types::messages::Events::Initialized(Some( + Default::default(), + )), + ))) + .unwrap(); + writer + .write_all( + TransportDelegate::build_rpc_message(message) + .as_bytes(), + ) + .await + .unwrap(); + } + writer.flush().await.unwrap(); } } diff --git a/crates/project/src/debugger/test.rs b/crates/project/src/debugger/test.rs index 3b9425e369..53b88323e6 100644 --- a/crates/project/src/debugger/test.rs +++ b/crates/project/src/debugger/test.rs @@ -1,7 +1,7 @@ use std::{path::Path, sync::Arc}; use dap::client::DebugAdapterClient; -use gpui::{App, AppContext, Subscription}; +use gpui::{App, Subscription}; use super::session::{Session, SessionStateEvent}; @@ -19,14 +19,6 @@ pub fn intercept_debug_sessions) + 'static>( let client = session.adapter_client().unwrap(); register_default_handlers(session, &client, cx); configure(&client); - cx.background_spawn(async move { - client - .fake_event(dap::messages::Events::Initialized( - Some(Default::default()), - )) - .await - }) - .detach(); } }) .detach(); From 09b93caa9bfb2f48ff2e491d0df737c53c97196d Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 31 Jul 2025 20:55:17 -0400 Subject: [PATCH 009/693] Rework authentication for local Cloud/Collab development (#35450) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR reworks authentication for developing Zed against a local version of Cloud and/or Collab. You will still connect the same way—using the `zed-local` script—but will need to be running an instance of Cloud locally. Release Notes: - N/A --- crates/client/src/client.rs | 116 ++++++++---------------------------- crates/collab/src/api.rs | 46 -------------- script/zed-local | 2 +- 3 files changed, 25 insertions(+), 139 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 998ce04636..230e1ce634 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -7,14 +7,13 @@ pub mod telemetry; pub mod user; pub mod zed_urls; -use anyhow::{Context as _, Result, anyhow, bail}; +use anyhow::{Context as _, Result, anyhow}; use async_recursion::async_recursion; use async_tungstenite::tungstenite::{ client::IntoClientRequest, error::Error as WebsocketError, http::{HeaderValue, Request, StatusCode}, }; -use chrono::{DateTime, Utc}; use clock::SystemClock; use cloud_api_client::CloudApiClient; use credentials_provider::CredentialsProvider; @@ -23,7 +22,7 @@ use futures::{ channel::oneshot, future::BoxFuture, }; use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions}; -use http_client::{AsyncBody, HttpClient, HttpClientWithUrl, http}; +use http_client::{HttpClient, HttpClientWithUrl, http}; use parking_lot::RwLock; use postage::watch; use proxy::connect_proxy_stream; @@ -1379,96 +1378,31 @@ impl Client { self: &Arc, http: Arc, login: String, - mut api_token: String, + api_token: String, ) -> Result { - #[derive(Deserialize)] - struct AuthenticatedUserResponse { - user: User, + #[derive(Serialize)] + struct ImpersonateUserBody { + github_login: String, } #[derive(Deserialize)] - struct User { - id: u64, + struct ImpersonateUserResponse { + user_id: u64, + access_token: String, } - let github_user = { - #[derive(Deserialize)] - struct GithubUser { - id: i32, - login: String, - created_at: DateTime, - } - - let request = { - let mut request_builder = - Request::get(&format!("https://api.github.com/users/{login}")); - if let Ok(github_token) = std::env::var("GITHUB_TOKEN") { - request_builder = - request_builder.header("Authorization", format!("Bearer {}", github_token)); - } - - request_builder.body(AsyncBody::empty())? - }; - - let mut response = http - .send(request) - .await - .context("error fetching GitHub user")?; - - let mut body = Vec::new(); - response - .body_mut() - .read_to_end(&mut body) - .await - .context("error reading GitHub user")?; - - if !response.status().is_success() { - let text = String::from_utf8_lossy(body.as_slice()); - bail!( - "status error {}, response: {text:?}", - response.status().as_u16() - ); - } - - serde_json::from_slice::(body.as_slice()).map_err(|err| { - log::error!("Error deserializing: {:?}", err); - log::error!( - "GitHub API response text: {:?}", - String::from_utf8_lossy(body.as_slice()) - ); - anyhow!("error deserializing GitHub user") - })? - }; - - let query_params = [ - ("github_login", &github_user.login), - ("github_user_id", &github_user.id.to_string()), - ( - "github_user_created_at", - &github_user.created_at.to_rfc3339(), - ), - ]; - - // Use the collab server's admin API to retrieve the ID - // of the impersonated user. - let mut url = self.rpc_url(http.clone(), None).await?; - url.set_path("/user"); - url.set_query(Some( - &query_params - .iter() - .map(|(key, value)| { - format!( - "{}={}", - key, - url::form_urlencoded::byte_serialize(value.as_bytes()).collect::() - ) - }) - .collect::>() - .join("&"), - )); - let request: http_client::Request = Request::get(url.as_str()) - .header("Authorization", format!("token {api_token}")) - .body("".into())?; + let url = self + .http + .build_zed_cloud_url("/internal/users/impersonate", &[])?; + let request = Request::post(url.as_str()) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {api_token}")) + .body( + serde_json::to_string(&ImpersonateUserBody { + github_login: login, + })? + .into(), + )?; let mut response = http.send(request).await?; let mut body = String::new(); @@ -1479,13 +1413,11 @@ impl Client { response.status().as_u16(), body, ); - let response: AuthenticatedUserResponse = serde_json::from_str(&body)?; + let response: ImpersonateUserResponse = serde_json::from_str(&body)?; - // Use the admin API token to authenticate as the impersonated user. - api_token.insert_str(0, "ADMIN_TOKEN:"); Ok(Credentials { - user_id: response.user.id, - access_token: api_token, + user_id: response.user_id, + access_token: response.access_token, }) } diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 609fdd128c..6cf3f68f54 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -100,7 +100,6 @@ impl std::fmt::Display for SystemIdHeader { pub fn routes(rpc_server: Arc) -> Router<(), Body> { Router::new() - .route("/user", get(legacy_update_or_create_authenticated_user)) .route("/users/look_up", get(look_up_user)) .route("/users/:id/access_tokens", post(create_access_token)) .route("/users/:id/refresh_llm_tokens", post(refresh_llm_tokens)) @@ -145,51 +144,6 @@ pub async fn validate_api_token(req: Request, next: Next) -> impl IntoR Ok::<_, Error>(next.run(req).await) } -#[derive(Debug, Deserialize)] -struct AuthenticatedUserParams { - github_user_id: i32, - github_login: String, - github_email: Option, - github_name: Option, - github_user_created_at: chrono::DateTime, -} - -#[derive(Debug, Serialize)] -struct AuthenticatedUserResponse { - user: User, - metrics_id: String, - feature_flags: Vec, -} - -/// This is a legacy endpoint that is no longer used in production. -/// -/// It currently only exists to be used when developing Collab locally. -async fn legacy_update_or_create_authenticated_user( - Query(params): Query, - Extension(app): Extension>, -) -> Result> { - let initial_channel_id = app.config.auto_join_channel_id; - - let user = app - .db - .update_or_create_user_by_github_account( - ¶ms.github_login, - params.github_user_id, - params.github_email.as_deref(), - params.github_name.as_deref(), - params.github_user_created_at, - initial_channel_id, - ) - .await?; - let metrics_id = app.db.get_user_metrics_id(user.id).await?; - let feature_flags = app.db.get_user_flags(user.id).await?; - Ok(Json(AuthenticatedUserResponse { - user, - metrics_id, - feature_flags, - })) -} - #[derive(Debug, Deserialize)] struct LookUpUserParams { identifier: String, diff --git a/script/zed-local b/script/zed-local index 2568931246..99d9308232 100755 --- a/script/zed-local +++ b/script/zed-local @@ -213,7 +213,7 @@ setTimeout(() => { platform === "win32" ? "http://127.0.0.1:8080/rpc" : "http://localhost:8080/rpc", - ZED_ADMIN_API_TOKEN: "secret", + ZED_ADMIN_API_TOKEN: "internal-api-key-secret", ZED_WINDOW_SIZE: size, ZED_CLIENT_CHECKSUM_SEED: "development-checksum-seed", RUST_LOG: process.env.RUST_LOG || "info", From 72d354de6c034b45a27712dc414e00f8ce229910 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 31 Jul 2025 21:44:43 -0400 Subject: [PATCH 010/693] Update Agent panel to work with `CloudUserStore` (#35436) This PR updates the Agent panel to work with the `CloudUserStore` instead of the `UserStore`, reducing its reliance on being connected to Collab to function. Release Notes: - N/A --------- Co-authored-by: Richard Feldman --- crates/agent/src/thread.rs | 32 ++++--- crates/agent/src/thread_store.rs | 18 +++- crates/agent_ui/src/active_thread.rs | 7 ++ crates/agent_ui/src/agent_diff.rs | 13 +++ crates/agent_ui/src/agent_panel.rs | 16 ++-- crates/agent_ui/src/message_editor.rs | 33 +++---- .../assistant_tools/src/edit_agent/evals.rs | 6 +- crates/client/src/cloud/user_store.rs | 31 ++++++- crates/client/src/user.rs | 21 ----- crates/eval/src/eval.rs | 13 ++- crates/eval/src/instance.rs | 1 + crates/language_models/src/language_models.rs | 25 +++++- crates/language_models/src/provider/cloud.rs | 87 +++++++++++-------- crates/zed/src/main.rs | 7 +- crates/zed/src/zed.rs | 7 +- .../preview_support/active_thread.rs | 3 +- 16 files changed, 212 insertions(+), 108 deletions(-) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 0e5da2d43b..ee16f83dc4 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -12,7 +12,7 @@ use agent_settings::{AgentProfileId, AgentSettings, CompletionMode}; use anyhow::{Result, anyhow}; use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet}; use chrono::{DateTime, Utc}; -use client::{ModelRequestUsage, RequestUsage}; +use client::{CloudUserStore, ModelRequestUsage, RequestUsage}; use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit}; use collections::HashMap; use feature_flags::{self, FeatureFlagAppExt}; @@ -374,6 +374,7 @@ pub struct Thread { completion_count: usize, pending_completions: Vec, project: Entity, + cloud_user_store: Entity, prompt_builder: Arc, tools: Entity, tool_use: ToolUseState, @@ -444,6 +445,7 @@ pub struct ExceededWindowError { impl Thread { pub fn new( project: Entity, + cloud_user_store: Entity, tools: Entity, prompt_builder: Arc, system_prompt: SharedProjectContext, @@ -470,6 +472,7 @@ impl Thread { completion_count: 0, pending_completions: Vec::new(), project: project.clone(), + cloud_user_store, prompt_builder, tools: tools.clone(), last_restore_checkpoint: None, @@ -503,6 +506,7 @@ impl Thread { id: ThreadId, serialized: SerializedThread, project: Entity, + cloud_user_store: Entity, tools: Entity, prompt_builder: Arc, project_context: SharedProjectContext, @@ -603,6 +607,7 @@ impl Thread { last_restore_checkpoint: None, pending_checkpoint: None, project: project.clone(), + cloud_user_store, prompt_builder, tools: tools.clone(), tool_use, @@ -3255,16 +3260,14 @@ impl Thread { } fn update_model_request_usage(&self, amount: u32, limit: UsageLimit, cx: &mut Context) { - self.project.update(cx, |project, cx| { - project.user_store().update(cx, |user_store, cx| { - user_store.update_model_request_usage( - ModelRequestUsage(RequestUsage { - amount: amount as i32, - limit, - }), - cx, - ) - }) + self.cloud_user_store.update(cx, |cloud_user_store, cx| { + cloud_user_store.update_model_request_usage( + ModelRequestUsage(RequestUsage { + amount: amount as i32, + limit, + }), + cx, + ) }); } @@ -3883,6 +3886,7 @@ fn main() {{ thread.id.clone(), serialized, thread.project.clone(), + thread.cloud_user_store.clone(), thread.tools.clone(), thread.prompt_builder.clone(), thread.project_context.clone(), @@ -5479,10 +5483,16 @@ fn main() {{ let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (client, user_store) = + project.read_with(cx, |project, _cx| (project.client(), project.user_store())); + let cloud_user_store = + cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store, cx)); + let thread_store = cx .update(|_, cx| { ThreadStore::load( project.clone(), + cloud_user_store, cx.new(|_| ToolWorkingSet::default()), None, Arc::new(PromptBuilder::new(None).unwrap()), diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index cc7cb50c91..6efa56f233 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -8,6 +8,7 @@ use agent_settings::{AgentProfileId, CompletionMode}; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::{Tool, ToolId, ToolWorkingSet}; use chrono::{DateTime, Utc}; +use client::CloudUserStore; use collections::HashMap; use context_server::ContextServerId; use futures::{ @@ -104,6 +105,7 @@ pub type TextThreadStore = assistant_context::ContextStore; pub struct ThreadStore { project: Entity, + cloud_user_store: Entity, tools: Entity, prompt_builder: Arc, prompt_store: Option>, @@ -124,6 +126,7 @@ impl EventEmitter for ThreadStore {} impl ThreadStore { pub fn load( project: Entity, + cloud_user_store: Entity, tools: Entity, prompt_store: Option>, prompt_builder: Arc, @@ -133,8 +136,14 @@ impl ThreadStore { let (thread_store, ready_rx) = cx.update(|cx| { let mut option_ready_rx = None; let thread_store = cx.new(|cx| { - let (thread_store, ready_rx) = - Self::new(project, tools, prompt_builder, prompt_store, cx); + let (thread_store, ready_rx) = Self::new( + project, + cloud_user_store, + tools, + prompt_builder, + prompt_store, + cx, + ); option_ready_rx = Some(ready_rx); thread_store }); @@ -147,6 +156,7 @@ impl ThreadStore { fn new( project: Entity, + cloud_user_store: Entity, tools: Entity, prompt_builder: Arc, prompt_store: Option>, @@ -190,6 +200,7 @@ impl ThreadStore { let this = Self { project, + cloud_user_store, tools, prompt_builder, prompt_store, @@ -407,6 +418,7 @@ impl ThreadStore { cx.new(|cx| { Thread::new( self.project.clone(), + self.cloud_user_store.clone(), self.tools.clone(), self.prompt_builder.clone(), self.project_context.clone(), @@ -425,6 +437,7 @@ impl ThreadStore { ThreadId::new(), serialized, self.project.clone(), + self.cloud_user_store.clone(), self.tools.clone(), self.prompt_builder.clone(), self.project_context.clone(), @@ -456,6 +469,7 @@ impl ThreadStore { id.clone(), thread, this.project.clone(), + this.cloud_user_store.clone(), this.tools.clone(), this.prompt_builder.clone(), this.project_context.clone(), diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 04a093c7d0..1669c24a1b 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -3820,6 +3820,7 @@ mod tests { use super::*; use agent::{MessageSegment, context::ContextLoadResult, thread_store}; use assistant_tool::{ToolRegistry, ToolWorkingSet}; + use client::CloudUserStore; use editor::EditorSettings; use fs::FakeFs; use gpui::{AppContext, TestAppContext, VisualTestContext}; @@ -4116,10 +4117,16 @@ mod tests { let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (client, user_store) = + project.read_with(cx, |project, _cx| (project.client(), project.user_store())); + let cloud_user_store = + cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store, cx)); + let thread_store = cx .update(|_, cx| { ThreadStore::load( project.clone(), + cloud_user_store, cx.new(|_| ToolWorkingSet::default()), None, Arc::new(PromptBuilder::new(None).unwrap()), diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index ec0a11f86b..5c8011cb18 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1893,6 +1893,7 @@ mod tests { use agent::thread_store::{self, ThreadStore}; use agent_settings::AgentSettings; use assistant_tool::ToolWorkingSet; + use client::CloudUserStore; use editor::EditorSettings; use gpui::{TestAppContext, UpdateGlobal, VisualTestContext}; use project::{FakeFs, Project}; @@ -1932,11 +1933,17 @@ mod tests { }) .unwrap(); + let (client, user_store) = + project.read_with(cx, |project, _cx| (project.client(), project.user_store())); + let cloud_user_store = + cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store, cx)); + let prompt_store = None; let thread_store = cx .update(|cx| { ThreadStore::load( project.clone(), + cloud_user_store, cx.new(|_| ToolWorkingSet::default()), prompt_store, Arc::new(PromptBuilder::new(None).unwrap()), @@ -2098,11 +2105,17 @@ mod tests { }) .unwrap(); + let (client, user_store) = + project.read_with(cx, |project, _cx| (project.client(), project.user_store())); + let cloud_user_store = + cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store, cx)); + let prompt_store = None; let thread_store = cx .update(|cx| { ThreadStore::load( project.clone(), + cloud_user_store, cx.new(|_| ToolWorkingSet::default()), prompt_store, Arc::new(PromptBuilder::new(None).unwrap()), diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index e7b1943561..a39e022df4 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -43,7 +43,7 @@ use anyhow::{Result, anyhow}; use assistant_context::{AssistantContext, ContextEvent, ContextSummary}; use assistant_slash_command::SlashCommandWorkingSet; use assistant_tool::ToolWorkingSet; -use client::{DisableAiSettings, UserStore, zed_urls}; +use client::{CloudUserStore, DisableAiSettings, UserStore, zed_urls}; use cloud_llm_client::{CompletionIntent, UsageLimit}; use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer}; use feature_flags::{self, FeatureFlagAppExt}; @@ -427,6 +427,7 @@ impl ActiveView { pub struct AgentPanel { workspace: WeakEntity, user_store: Entity, + cloud_user_store: Entity, project: Entity, fs: Arc, language_registry: Arc, @@ -486,6 +487,7 @@ impl AgentPanel { let project = workspace.project().clone(); ThreadStore::load( project, + workspace.app_state().cloud_user_store.clone(), tools.clone(), prompt_store.clone(), prompt_builder.clone(), @@ -553,6 +555,7 @@ impl AgentPanel { let thread = thread_store.update(cx, |this, cx| this.create_thread(cx)); let fs = workspace.app_state().fs.clone(); let user_store = workspace.app_state().user_store.clone(); + let cloud_user_store = workspace.app_state().cloud_user_store.clone(); let project = workspace.project(); let language_registry = project.read(cx).languages().clone(); let client = workspace.client().clone(); @@ -579,7 +582,7 @@ impl AgentPanel { MessageEditor::new( fs.clone(), workspace.clone(), - user_store.clone(), + cloud_user_store.clone(), message_editor_context_store.clone(), prompt_store.clone(), thread_store.downgrade(), @@ -706,6 +709,7 @@ impl AgentPanel { active_view, workspace, user_store, + cloud_user_store, project: project.clone(), fs: fs.clone(), language_registry, @@ -848,7 +852,7 @@ impl AgentPanel { MessageEditor::new( self.fs.clone(), self.workspace.clone(), - self.user_store.clone(), + self.cloud_user_store.clone(), context_store.clone(), self.prompt_store.clone(), self.thread_store.downgrade(), @@ -1122,7 +1126,7 @@ impl AgentPanel { MessageEditor::new( self.fs.clone(), self.workspace.clone(), - self.user_store.clone(), + self.cloud_user_store.clone(), context_store, self.prompt_store.clone(), self.thread_store.downgrade(), @@ -1821,8 +1825,8 @@ impl AgentPanel { } fn render_toolbar(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let user_store = self.user_store.read(cx); - let usage = user_store.model_request_usage(); + let cloud_user_store = self.cloud_user_store.read(cx); + let usage = cloud_user_store.model_request_usage(); let account_url = zed_urls::account_url(cx); diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 082d1dfb51..e00a0087eb 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -17,7 +17,7 @@ use agent::{ use agent_settings::{AgentSettings, CompletionMode}; use ai_onboarding::ApiKeysWithProviders; use buffer_diff::BufferDiff; -use client::UserStore; +use client::CloudUserStore; use cloud_llm_client::CompletionIntent; use collections::{HashMap, HashSet}; use editor::actions::{MoveUp, Paste}; @@ -43,7 +43,6 @@ use language_model::{ use multi_buffer; use project::Project; use prompt_store::PromptStore; -use proto::Plan; use settings::Settings; use std::time::Duration; use theme::ThemeSettings; @@ -79,7 +78,7 @@ pub struct MessageEditor { editor: Entity, workspace: WeakEntity, project: Entity, - user_store: Entity, + cloud_user_store: Entity, context_store: Entity, prompt_store: Option>, history_store: Option>, @@ -159,7 +158,7 @@ impl MessageEditor { pub fn new( fs: Arc, workspace: WeakEntity, - user_store: Entity, + cloud_user_store: Entity, context_store: Entity, prompt_store: Option>, thread_store: WeakEntity, @@ -231,7 +230,7 @@ impl MessageEditor { Self { editor: editor.clone(), project: thread.read(cx).project().clone(), - user_store, + cloud_user_store, thread, incompatible_tools_state: incompatible_tools.clone(), workspace, @@ -1287,26 +1286,16 @@ impl MessageEditor { return None; } - let user_store = self.user_store.read(cx); - - let ubb_enable = user_store - .usage_based_billing_enabled() - .map_or(false, |enabled| enabled); - - if ubb_enable { + let cloud_user_store = self.cloud_user_store.read(cx); + if cloud_user_store.is_usage_based_billing_enabled() { return None; } - let plan = user_store - .current_plan() - .map(|plan| match plan { - Plan::Free => cloud_llm_client::Plan::ZedFree, - Plan::ZedPro => cloud_llm_client::Plan::ZedPro, - Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial, - }) + let plan = cloud_user_store + .plan() .unwrap_or(cloud_llm_client::Plan::ZedFree); - let usage = user_store.model_request_usage()?; + let usage = cloud_user_store.model_request_usage()?; Some( div() @@ -1769,7 +1758,7 @@ impl AgentPreview for MessageEditor { ) -> Option { if let Some(workspace) = workspace.upgrade() { let fs = workspace.read(cx).app_state().fs.clone(); - let user_store = workspace.read(cx).app_state().user_store.clone(); + let cloud_user_store = workspace.read(cx).app_state().cloud_user_store.clone(); let project = workspace.read(cx).project().clone(); let weak_project = project.downgrade(); let context_store = cx.new(|_cx| ContextStore::new(weak_project, None)); @@ -1782,7 +1771,7 @@ impl AgentPreview for MessageEditor { MessageEditor::new( fs, workspace.downgrade(), - user_store, + cloud_user_store, context_store, None, thread_store.downgrade(), diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/assistant_tools/src/edit_agent/evals.rs index eda7eee0e3..13619da25c 100644 --- a/crates/assistant_tools/src/edit_agent/evals.rs +++ b/crates/assistant_tools/src/edit_agent/evals.rs @@ -7,7 +7,7 @@ use crate::{ }; use Role::*; use assistant_tool::ToolRegistry; -use client::{Client, UserStore}; +use client::{Client, CloudUserStore, UserStore}; use collections::HashMap; use fs::FakeFs; use futures::{FutureExt, future::LocalBoxFuture}; @@ -1470,12 +1470,14 @@ impl EditAgentTest { client::init_settings(cx); let client = Client::production(cx); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); + let cloud_user_store = + cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store.clone(), cx)); settings::init(cx); Project::init_settings(cx); language::init(cx); language_model::init(client.clone(), cx); - language_models::init(user_store.clone(), client.clone(), cx); + language_models::init(user_store.clone(), cloud_user_store, client.clone(), cx); crate::init(client.http_client(), cx); }); diff --git a/crates/client/src/cloud/user_store.rs b/crates/client/src/cloud/user_store.rs index ea432f71ed..78444b3f95 100644 --- a/crates/client/src/cloud/user_store.rs +++ b/crates/client/src/cloud/user_store.rs @@ -9,12 +9,13 @@ use gpui::{Context, Entity, Subscription, Task}; use util::{ResultExt as _, maybe}; use crate::user::Event as RpcUserStoreEvent; -use crate::{EditPredictionUsage, RequestUsage, UserStore}; +use crate::{EditPredictionUsage, ModelRequestUsage, RequestUsage, UserStore}; pub struct CloudUserStore { cloud_client: Arc, authenticated_user: Option>, plan_info: Option>, + model_request_usage: Option, edit_prediction_usage: Option, _maintain_authenticated_user_task: Task<()>, _rpc_plan_updated_subscription: Subscription, @@ -33,6 +34,7 @@ impl CloudUserStore { cloud_client: cloud_client.clone(), authenticated_user: None, plan_info: None, + model_request_usage: None, edit_prediction_usage: None, _maintain_authenticated_user_task: cx.spawn(async move |this, cx| { maybe!(async move { @@ -104,6 +106,13 @@ impl CloudUserStore { }) } + pub fn trial_started_at(&self) -> Option> { + self.plan_info + .as_ref() + .and_then(|plan| plan.trial_started_at) + .map(|trial_started_at| trial_started_at.0) + } + pub fn has_accepted_tos(&self) -> bool { self.authenticated_user .as_ref() @@ -127,6 +136,22 @@ impl CloudUserStore { .unwrap_or_default() } + pub fn is_usage_based_billing_enabled(&self) -> bool { + self.plan_info + .as_ref() + .map(|plan| plan.is_usage_based_billing_enabled) + .unwrap_or_default() + } + + pub fn model_request_usage(&self) -> Option { + self.model_request_usage + } + + pub fn update_model_request_usage(&mut self, usage: ModelRequestUsage, cx: &mut Context) { + self.model_request_usage = Some(usage); + cx.notify(); + } + pub fn edit_prediction_usage(&self) -> Option { self.edit_prediction_usage } @@ -142,6 +167,10 @@ impl CloudUserStore { fn update_authenticated_user(&mut self, response: GetAuthenticatedUserResponse) { self.authenticated_user = Some(Arc::new(response.user)); + self.model_request_usage = Some(ModelRequestUsage(RequestUsage { + limit: response.plan.usage.model_requests.limit, + amount: response.plan.usage.model_requests.used as i32, + })); self.edit_prediction_usage = Some(EditPredictionUsage(RequestUsage { limit: response.plan.usage.edit_predictions.limit, amount: response.plan.usage.edit_predictions.used as i32, diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 0ba7d1472b..dc762efa5d 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -113,7 +113,6 @@ pub struct UserStore { current_plan: Option, subscription_period: Option<(DateTime, DateTime)>, trial_started_at: Option>, - model_request_usage: Option, is_usage_based_billing_enabled: Option, account_too_young: Option, has_overdue_invoices: Option, @@ -191,7 +190,6 @@ impl UserStore { current_plan: None, subscription_period: None, trial_started_at: None, - model_request_usage: None, is_usage_based_billing_enabled: None, account_too_young: None, has_overdue_invoices: None, @@ -371,27 +369,12 @@ impl UserStore { this.account_too_young = message.payload.account_too_young; this.has_overdue_invoices = message.payload.has_overdue_invoices; - if let Some(usage) = message.payload.usage { - // limits are always present even though they are wrapped in Option - this.model_request_usage = usage - .model_requests_usage_limit - .and_then(|limit| { - RequestUsage::from_proto(usage.model_requests_usage_amount, limit) - }) - .map(ModelRequestUsage); - } - cx.emit(Event::PlanUpdated); cx.notify(); })?; Ok(()) } - pub fn update_model_request_usage(&mut self, usage: ModelRequestUsage, cx: &mut Context) { - self.model_request_usage = Some(usage); - cx.notify(); - } - fn update_contacts(&mut self, message: UpdateContacts, cx: &Context) -> Task> { match message { UpdateContacts::Wait(barrier) => { @@ -776,10 +759,6 @@ impl UserStore { self.is_usage_based_billing_enabled } - pub fn model_request_usage(&self) -> Option { - self.model_request_usage - } - pub fn watch_current_user(&self) -> watch::Receiver>> { self.current_user.clone() } diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index a02b4a7f0b..8d257a37a7 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -13,7 +13,7 @@ pub(crate) use tool_metrics::*; use ::fs::RealFs; use clap::Parser; -use client::{Client, ProxySettings, UserStore}; +use client::{Client, CloudUserStore, ProxySettings, UserStore}; use collections::{HashMap, HashSet}; use extension::ExtensionHostProxy; use futures::future; @@ -329,6 +329,7 @@ pub struct AgentAppState { pub languages: Arc, pub client: Arc, pub user_store: Entity, + pub cloud_user_store: Entity, pub fs: Arc, pub node_runtime: NodeRuntime, @@ -383,6 +384,8 @@ pub fn init(cx: &mut App) -> Arc { let languages = Arc::new(languages); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); + let cloud_user_store = + cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store.clone(), cx)); extension::init(cx); @@ -422,7 +425,12 @@ pub fn init(cx: &mut App) -> Arc { languages.clone(), ); language_model::init(client.clone(), cx); - language_models::init(user_store.clone(), client.clone(), cx); + language_models::init( + user_store.clone(), + cloud_user_store.clone(), + client.clone(), + cx, + ); languages::init(languages.clone(), node_runtime.clone(), cx); prompt_store::init(cx); terminal_view::init(cx); @@ -447,6 +455,7 @@ pub fn init(cx: &mut App) -> Arc { languages, client, user_store, + cloud_user_store, fs, node_runtime, prompt_builder, diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs index 0f2b4c18ea..54d864ea21 100644 --- a/crates/eval/src/instance.rs +++ b/crates/eval/src/instance.rs @@ -221,6 +221,7 @@ impl ExampleInstance { let prompt_store = None; let thread_store = ThreadStore::load( project.clone(), + app_state.cloud_user_store.clone(), tools, prompt_store, app_state.prompt_builder.clone(), diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index 18e6f47ed0..a88f12283a 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use ::settings::{Settings, SettingsStore}; -use client::{Client, UserStore}; +use client::{Client, CloudUserStore, UserStore}; use collections::HashSet; use gpui::{App, Context, Entity}; use language_model::{LanguageModelProviderId, LanguageModelRegistry}; @@ -26,11 +26,22 @@ use crate::provider::vercel::VercelLanguageModelProvider; use crate::provider::x_ai::XAiLanguageModelProvider; pub use crate::settings::*; -pub fn init(user_store: Entity, client: Arc, cx: &mut App) { +pub fn init( + user_store: Entity, + cloud_user_store: Entity, + client: Arc, + cx: &mut App, +) { crate::settings::init_settings(cx); let registry = LanguageModelRegistry::global(cx); registry.update(cx, |registry, cx| { - register_language_model_providers(registry, user_store, client.clone(), cx); + register_language_model_providers( + registry, + user_store, + cloud_user_store, + client.clone(), + cx, + ); }); let mut openai_compatible_providers = AllLanguageModelSettings::get_global(cx) @@ -100,11 +111,17 @@ fn register_openai_compatible_providers( fn register_language_model_providers( registry: &mut LanguageModelRegistry, user_store: Entity, + cloud_user_store: Entity, client: Arc, cx: &mut Context, ) { registry.register_provider( - CloudLanguageModelProvider::new(user_store.clone(), client.clone(), cx), + CloudLanguageModelProvider::new( + user_store.clone(), + cloud_user_store.clone(), + client.clone(), + cx, + ), cx, ); diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 3de135c5a2..a5de7f3442 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -2,11 +2,11 @@ use ai_onboarding::YoungAccountBanner; use anthropic::AnthropicModelMode; use anyhow::{Context as _, Result, anyhow}; use chrono::{DateTime, Utc}; -use client::{Client, ModelRequestUsage, UserStore, zed_urls}; +use client::{Client, CloudUserStore, ModelRequestUsage, UserStore, zed_urls}; use cloud_llm_client::{ CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, CURRENT_PLAN_HEADER_NAME, CompletionBody, CompletionEvent, CompletionRequestStatus, CountTokensBody, CountTokensResponse, - EXPIRED_LLM_TOKEN_HEADER_NAME, ListModelsResponse, MODEL_REQUESTS_RESOURCE_HEADER_VALUE, + EXPIRED_LLM_TOKEN_HEADER_NAME, ListModelsResponse, MODEL_REQUESTS_RESOURCE_HEADER_VALUE, Plan, SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME, TOOL_USE_LIMIT_REACHED_HEADER_NAME, ZED_VERSION_HEADER_NAME, }; @@ -27,7 +27,6 @@ use language_model::{ LanguageModelToolChoice, LanguageModelToolSchemaFormat, LlmApiToken, ModelRequestLimitReachedError, PaymentRequiredError, RateLimiter, RefreshLlmTokenListener, }; -use proto::Plan; use release_channel::AppVersion; use schemars::JsonSchema; use serde::{Deserialize, Serialize, de::DeserializeOwned}; @@ -118,6 +117,7 @@ pub struct State { client: Arc, llm_api_token: LlmApiToken, user_store: Entity, + cloud_user_store: Entity, status: client::Status, accept_terms_of_service_task: Option>>, models: Vec>, @@ -133,6 +133,7 @@ impl State { fn new( client: Arc, user_store: Entity, + cloud_user_store: Entity, status: client::Status, cx: &mut Context, ) -> Self { @@ -142,6 +143,7 @@ impl State { client: client.clone(), llm_api_token: LlmApiToken::default(), user_store, + cloud_user_store, status, accept_terms_of_service_task: None, models: Vec::new(), @@ -150,12 +152,19 @@ impl State { recommended_models: Vec::new(), _fetch_models_task: cx.spawn(async move |this, cx| { maybe!(async move { - let (client, llm_api_token) = this - .read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?; + let (client, cloud_user_store, llm_api_token) = + this.read_with(cx, |this, _cx| { + ( + client.clone(), + this.cloud_user_store.clone(), + this.llm_api_token.clone(), + ) + })?; loop { - let status = this.read_with(cx, |this, _cx| this.status)?; - if matches!(status, client::Status::Connected { .. }) { + let is_authenticated = + cloud_user_store.read_with(cx, |this, _cx| this.is_authenticated())?; + if is_authenticated { break; } @@ -194,8 +203,8 @@ impl State { } } - fn is_signed_out(&self) -> bool { - self.status.is_signed_out() + fn is_signed_out(&self, cx: &App) -> bool { + !self.cloud_user_store.read(cx).is_authenticated() } fn authenticate(&self, cx: &mut Context) -> Task> { @@ -210,10 +219,7 @@ impl State { } fn has_accepted_terms_of_service(&self, cx: &App) -> bool { - self.user_store - .read(cx) - .current_user_has_accepted_terms() - .unwrap_or(false) + self.cloud_user_store.read(cx).has_accepted_tos() } fn accept_terms_of_service(&mut self, cx: &mut Context) { @@ -297,11 +303,24 @@ impl State { } impl CloudLanguageModelProvider { - pub fn new(user_store: Entity, client: Arc, cx: &mut App) -> Self { + pub fn new( + user_store: Entity, + cloud_user_store: Entity, + client: Arc, + cx: &mut App, + ) -> Self { let mut status_rx = client.status(); let status = *status_rx.borrow(); - let state = cx.new(|cx| State::new(client.clone(), user_store.clone(), status, cx)); + let state = cx.new(|cx| { + State::new( + client.clone(), + user_store.clone(), + cloud_user_store.clone(), + status, + cx, + ) + }); let state_ref = state.downgrade(); let maintain_client_status = cx.spawn(async move |cx| { @@ -398,7 +417,7 @@ impl LanguageModelProvider for CloudLanguageModelProvider { fn is_authenticated(&self, cx: &App) -> bool { let state = self.state.read(cx); - !state.is_signed_out() && state.has_accepted_terms_of_service(cx) + !state.is_signed_out(cx) && state.has_accepted_terms_of_service(cx) } fn authenticate(&self, _cx: &mut App) -> Task> { @@ -614,9 +633,9 @@ impl CloudLanguageModel { .and_then(|plan| cloud_llm_client::Plan::from_str(plan).ok()) { let plan = match plan { - cloud_llm_client::Plan::ZedFree => Plan::Free, - cloud_llm_client::Plan::ZedPro => Plan::ZedPro, - cloud_llm_client::Plan::ZedProTrial => Plan::ZedProTrial, + cloud_llm_client::Plan::ZedFree => proto::Plan::Free, + cloud_llm_client::Plan::ZedPro => proto::Plan::ZedPro, + cloud_llm_client::Plan::ZedProTrial => proto::Plan::ZedProTrial, }; return Err(anyhow!(ModelRequestLimitReachedError { plan })); } @@ -1118,7 +1137,7 @@ fn response_lines( #[derive(IntoElement, RegisterComponent)] struct ZedAiConfiguration { is_connected: bool, - plan: Option, + plan: Option, subscription_period: Option<(DateTime, DateTime)>, eligible_for_trial: bool, has_accepted_terms_of_service: bool, @@ -1132,15 +1151,15 @@ impl RenderOnce for ZedAiConfiguration { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { let young_account_banner = YoungAccountBanner; - let is_pro = self.plan == Some(proto::Plan::ZedPro); + let is_pro = self.plan == Some(Plan::ZedPro); let subscription_text = match (self.plan, self.subscription_period) { - (Some(proto::Plan::ZedPro), Some(_)) => { + (Some(Plan::ZedPro), Some(_)) => { "You have access to Zed's hosted models through your Pro subscription." } - (Some(proto::Plan::ZedProTrial), Some(_)) => { + (Some(Plan::ZedProTrial), Some(_)) => { "You have access to Zed's hosted models through your Pro trial." } - (Some(proto::Plan::Free), Some(_)) => { + (Some(Plan::ZedFree), Some(_)) => { "You have basic access to Zed's hosted models through the Free plan." } _ => { @@ -1262,15 +1281,15 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let state = self.state.read(cx); - let user_store = state.user_store.read(cx); + let cloud_user_store = state.cloud_user_store.read(cx); ZedAiConfiguration { - is_connected: !state.is_signed_out(), - plan: user_store.current_plan(), - subscription_period: user_store.subscription_period(), - eligible_for_trial: user_store.trial_started_at().is_none(), + is_connected: !state.is_signed_out(cx), + plan: cloud_user_store.plan(), + subscription_period: cloud_user_store.subscription_period(), + eligible_for_trial: cloud_user_store.trial_started_at().is_none(), has_accepted_terms_of_service: state.has_accepted_terms_of_service(cx), - account_too_young: user_store.account_too_young(), + account_too_young: cloud_user_store.account_too_young(), accept_terms_of_service_in_progress: state.accept_terms_of_service_task.is_some(), accept_terms_of_service_callback: self.accept_terms_of_service_callback.clone(), sign_in_callback: self.sign_in_callback.clone(), @@ -1286,7 +1305,7 @@ impl Component for ZedAiConfiguration { fn preview(_window: &mut Window, _cx: &mut App) -> Option { fn configuration( is_connected: bool, - plan: Option, + plan: Option, eligible_for_trial: bool, account_too_young: bool, has_accepted_terms_of_service: bool, @@ -1330,15 +1349,15 @@ impl Component for ZedAiConfiguration { ), single_example( "Free Plan", - configuration(true, Some(proto::Plan::Free), true, false, true), + configuration(true, Some(Plan::ZedFree), true, false, true), ), single_example( "Zed Pro Trial Plan", - configuration(true, Some(proto::Plan::ZedProTrial), true, false, true), + configuration(true, Some(Plan::ZedProTrial), true, false, true), ), single_example( "Zed Pro Plan", - configuration(true, Some(proto::Plan::ZedPro), true, false, true), + configuration(true, Some(Plan::ZedPro), true, false, true), ), ]) .into_any_element(), diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index a18c112c7e..9859702bf8 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -556,7 +556,12 @@ pub fn main() { ); supermaven::init(app_state.client.clone(), cx); language_model::init(app_state.client.clone(), cx); - language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx); + language_models::init( + app_state.user_store.clone(), + app_state.cloud_user_store.clone(), + app_state.client.clone(), + cx, + ); agent_settings::init(cx); agent_servers::init(cx); web_search::init(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 8c6da335ab..0a43ec0bbe 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4488,7 +4488,12 @@ mod tests { ); image_viewer::init(cx); language_model::init(app_state.client.clone(), cx); - language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx); + language_models::init( + app_state.user_store.clone(), + app_state.cloud_user_store.clone(), + app_state.client.clone(), + cx, + ); web_search::init(cx); web_search_providers::init(app_state.client.clone(), cx); let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx); diff --git a/crates/zed/src/zed/component_preview/preview_support/active_thread.rs b/crates/zed/src/zed/component_preview/preview_support/active_thread.rs index 825744572d..1076ee49ea 100644 --- a/crates/zed/src/zed/component_preview/preview_support/active_thread.rs +++ b/crates/zed/src/zed/component_preview/preview_support/active_thread.rs @@ -17,9 +17,10 @@ pub fn load_preview_thread_store( cx: &mut AsyncApp, ) -> Task>> { workspace - .update(cx, |_, cx| { + .update(cx, |workspace, cx| { ThreadStore::load( project.clone(), + workspace.app_state().cloud_user_store.clone(), cx.new(|_| ToolWorkingSet::default()), None, Arc::new(PromptBuilder::new(None).unwrap()), From f8673dacf515048169474d69d0e3c9d06ef2be37 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 31 Jul 2025 22:08:21 -0400 Subject: [PATCH 011/693] ai_onboarding: Read the plan from the `CloudUserStore` (#35451) This PR updates the AI onboarding to read the plan from the `CloudUserStore` so that we don't need to connect to Collab. Release Notes: - N/A --- Cargo.lock | 1 + crates/agent_ui/src/agent_panel.rs | 1 + crates/ai_onboarding/Cargo.toml | 1 + .../src/agent_panel_onboarding_content.rs | 17 +++++++---------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 61875e878f..f76d4d520d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -355,6 +355,7 @@ name = "ai_onboarding" version = "0.1.0" dependencies = [ "client", + "cloud_llm_client", "component", "gpui", "language_model", diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index a39e022df4..7e0d766f91 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -697,6 +697,7 @@ impl AgentPanel { let onboarding = cx.new(|cx| { AgentPanelOnboarding::new( user_store.clone(), + cloud_user_store.clone(), client, |_window, cx| { OnboardingUpsell::set_dismissed(true, cx); diff --git a/crates/ai_onboarding/Cargo.toml b/crates/ai_onboarding/Cargo.toml index 9031e14e29..20fd54339e 100644 --- a/crates/ai_onboarding/Cargo.toml +++ b/crates/ai_onboarding/Cargo.toml @@ -16,6 +16,7 @@ default = [] [dependencies] client.workspace = true +cloud_llm_client.workspace = true component.workspace = true gpui.workspace = true language_model.workspace = true diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs index e8a62f7ff2..237b0ae046 100644 --- a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs +++ b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs @@ -1,6 +1,7 @@ use std::sync::Arc; -use client::{Client, UserStore}; +use client::{Client, CloudUserStore, UserStore}; +use cloud_llm_client::Plan; use gpui::{Entity, IntoElement, ParentElement}; use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID}; use ui::prelude::*; @@ -9,6 +10,7 @@ use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding}; pub struct AgentPanelOnboarding { user_store: Entity, + cloud_user_store: Entity, client: Arc, configured_providers: Vec<(IconName, SharedString)>, continue_with_zed_ai: Arc, @@ -17,6 +19,7 @@ pub struct AgentPanelOnboarding { impl AgentPanelOnboarding { pub fn new( user_store: Entity, + cloud_user_store: Entity, client: Arc, continue_with_zed_ai: impl Fn(&mut Window, &mut App) + 'static, cx: &mut Context, @@ -36,6 +39,7 @@ impl AgentPanelOnboarding { Self { user_store, + cloud_user_store, client, configured_providers: Self::compute_available_providers(cx), continue_with_zed_ai: Arc::new(continue_with_zed_ai), @@ -56,15 +60,8 @@ impl AgentPanelOnboarding { impl Render for AgentPanelOnboarding { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let enrolled_in_trial = matches!( - self.user_store.read(cx).current_plan(), - Some(proto::Plan::ZedProTrial) - ); - - let is_pro_user = matches!( - self.user_store.read(cx).current_plan(), - Some(proto::Plan::ZedPro) - ); + let enrolled_in_trial = self.cloud_user_store.read(cx).plan() == Some(Plan::ZedProTrial); + let is_pro_user = self.cloud_user_store.read(cx).plan() == Some(Plan::ZedPro); AgentPanelOnboardingCard::new() .child( From 2315962e18d31a35950e6d06e91d4e2a6ac9bfc9 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 31 Jul 2025 22:50:38 -0400 Subject: [PATCH 012/693] cloud_api_client: Add `accept_terms_of_service` method (#35452) This PR adds an `accept_terms_of_service` method to the `CloudApiClient`. Release Notes: - N/A --- .../cloud_api_client/src/cloud_api_client.rs | 68 ++++++++++++++----- crates/cloud_api_types/src/cloud_api_types.rs | 5 ++ 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/crates/cloud_api_client/src/cloud_api_client.rs b/crates/cloud_api_client/src/cloud_api_client.rs index 5a768810c0..6689475dae 100644 --- a/crates/cloud_api_client/src/cloud_api_client.rs +++ b/crates/cloud_api_client/src/cloud_api_client.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use anyhow::{Result, anyhow}; pub use cloud_api_types::*; use futures::AsyncReadExt as _; +use http_client::http::request; use http_client::{AsyncBody, HttpClientWithUrl, Method, Request}; use parking_lot::RwLock; @@ -51,17 +52,26 @@ impl CloudApiClient { )) } + fn build_request( + &self, + req: request::Builder, + body: impl Into, + ) -> Result> { + Ok(req + .header("Content-Type", "application/json") + .header("Authorization", self.authorization_header()?) + .body(body.into())?) + } + pub async fn get_authenticated_user(&self) -> Result { - let request = Request::builder() - .method(Method::GET) - .uri( + let request = self.build_request( + Request::builder().method(Method::GET).uri( self.http_client .build_zed_cloud_url("/client/users/me", &[])? .as_ref(), - ) - .header("Content-Type", "application/json") - .header("Authorization", self.authorization_header()?) - .body(AsyncBody::default())?; + ), + AsyncBody::default(), + )?; let mut response = self.http_client.send(request).await?; @@ -81,25 +91,49 @@ impl CloudApiClient { Ok(serde_json::from_str(&body)?) } + pub async fn accept_terms_of_service(&self) -> Result { + let request = self.build_request( + Request::builder().method(Method::POST).uri( + self.http_client + .build_zed_cloud_url("/client/terms_of_service/accept", &[])? + .as_ref(), + ), + AsyncBody::default(), + )?; + + let mut response = self.http_client.send(request).await?; + + if !response.status().is_success() { + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + + anyhow::bail!( + "Failed to accept terms of service.\nStatus: {:?}\nBody: {body}", + response.status() + ) + } + + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + + Ok(serde_json::from_str(&body)?) + } + pub async fn create_llm_token( &self, system_id: Option, ) -> Result { - let mut request_builder = Request::builder() - .method(Method::POST) - .uri( - self.http_client - .build_zed_cloud_url("/client/llm_tokens", &[])? - .as_ref(), - ) - .header("Content-Type", "application/json") - .header("Authorization", self.authorization_header()?); + let mut request_builder = Request::builder().method(Method::POST).uri( + self.http_client + .build_zed_cloud_url("/client/llm_tokens", &[])? + .as_ref(), + ); if let Some(system_id) = system_id { request_builder = request_builder.header(ZED_SYSTEM_ID_HEADER_NAME, system_id); } - let request = request_builder.body(AsyncBody::default())?; + let request = self.build_request(request_builder, AsyncBody::default())?; let mut response = self.http_client.send(request).await?; diff --git a/crates/cloud_api_types/src/cloud_api_types.rs b/crates/cloud_api_types/src/cloud_api_types.rs index e4d4a27af5..b38b38cde1 100644 --- a/crates/cloud_api_types/src/cloud_api_types.rs +++ b/crates/cloud_api_types/src/cloud_api_types.rs @@ -41,6 +41,11 @@ pub struct SubscriptionPeriod { pub ended_at: Timestamp, } +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct AcceptTermsOfServiceResponse { + pub user: AuthenticatedUser, +} + #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] pub struct LlmToken(pub String); From 76a8293cc6d3acfef5b2cc73b1b0f7f45b60afe0 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Thu, 31 Jul 2025 23:05:03 -0400 Subject: [PATCH 013/693] editor_tests: Fix for potential race loading editor languages (#35453) Fix for potential race when loading HTML and JS languages (JS is slower). Wait for both to load before continue tests. Observed failure on linux: [job](https://github.com/zed-industries/zed/actions/runs/16662438526/job/47162345259) as part of https://github.com/zed-industries/zed/pull/35436 ``` thread 'editor_tests::test_autoclose_with_embedded_language' panicked at crates/editor/src/editor_tests.rs:8724:8: assertion failed: `(left == right)`: unexpected buffer text Diff < left / right > : <> <> ``` Inserted `<` incorrect gets paired bracket inserted `>`. I believe because the JS language injection hasn't fully loaded. Release Notes: - N/A --- crates/editor/src/editor_tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 503fe2abc3..1a4f444275 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -8612,6 +8612,7 @@ async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) { cx.language_registry().add(html_language.clone()); cx.language_registry().add(javascript_language.clone()); + cx.executor().run_until_parked(); cx.update_buffer(|buffer, cx| { buffer.set_language(Some(html_language), cx); From 8be3f48f3732e1e9027598ac283f8f3082e8c5c0 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 31 Jul 2025 23:10:16 -0400 Subject: [PATCH 014/693] client: Remove unused `subscription_period` from `UserStore` (#35454) This PR removes the `subscription_period` field from the `UserStore`, as its usage has been replaced by the `CloudUserStore`. Release Notes: - N/A --- crates/client/src/user.rs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index dc762efa5d..82fc7f4713 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -20,7 +20,7 @@ use std::{ sync::{Arc, Weak}, }; use text::ReplicaId; -use util::{TryFutureExt as _, maybe}; +use util::TryFutureExt as _; pub type UserId = u64; @@ -111,7 +111,6 @@ pub struct UserStore { participant_indices: HashMap, update_contacts_tx: mpsc::UnboundedSender, current_plan: Option, - subscription_period: Option<(DateTime, DateTime)>, trial_started_at: Option>, is_usage_based_billing_enabled: Option, account_too_young: Option, @@ -188,7 +187,6 @@ impl UserStore { by_github_login: Default::default(), current_user: current_user_rx, current_plan: None, - subscription_period: None, trial_started_at: None, is_usage_based_billing_enabled: None, account_too_young: None, @@ -354,13 +352,6 @@ impl UserStore { ) -> Result<()> { this.update(&mut cx, |this, cx| { this.current_plan = Some(message.payload.plan()); - this.subscription_period = maybe!({ - let period = message.payload.subscription_period?; - let started_at = DateTime::from_timestamp(period.started_at as i64, 0)?; - let ended_at = DateTime::from_timestamp(period.ended_at as i64, 0)?; - - Some((started_at, ended_at)) - }); this.trial_started_at = message .payload .trial_started_at @@ -747,10 +738,6 @@ impl UserStore { self.current_plan } - pub fn subscription_period(&self) -> Option<(DateTime, DateTime)> { - self.subscription_period - } - pub fn trial_started_at(&self) -> Option> { self.trial_started_at } From f7f90593ac4ef0e4269e44a3efd29c2b7676eee4 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 31 Jul 2025 23:25:23 -0400 Subject: [PATCH 015/693] inline_completion_button: Replace `UserStore` with `CloudUserStore` (#35456) This PR replaces usages of the `UserStore` in the inline completion button with the `CloudUserStore`. Release Notes: - N/A --- crates/client/src/user.rs | 8 ----- .../src/inline_completion_button.rs | 29 ++++++++++--------- crates/zed/src/zed.rs | 2 +- crates/zeta/src/zeta.rs | 12 ++++---- 4 files changed, 24 insertions(+), 27 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 82fc7f4713..df5ce67be3 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -114,7 +114,6 @@ pub struct UserStore { trial_started_at: Option>, is_usage_based_billing_enabled: Option, account_too_young: Option, - has_overdue_invoices: Option, current_user: watch::Receiver>>, accepted_tos_at: Option>>, contacts: Vec>, @@ -190,7 +189,6 @@ impl UserStore { trial_started_at: None, is_usage_based_billing_enabled: None, account_too_young: None, - has_overdue_invoices: None, accepted_tos_at: None, contacts: Default::default(), incoming_contact_requests: Default::default(), @@ -358,7 +356,6 @@ impl UserStore { .and_then(|trial_started_at| DateTime::from_timestamp(trial_started_at as i64, 0)); this.is_usage_based_billing_enabled = message.payload.is_usage_based_billing_enabled; this.account_too_young = message.payload.account_too_young; - this.has_overdue_invoices = message.payload.has_overdue_invoices; cx.emit(Event::PlanUpdated); cx.notify(); @@ -755,11 +752,6 @@ impl UserStore { self.account_too_young.unwrap_or(false) } - /// Returns whether the current user has overdue invoices and usage should be blocked. - pub fn has_overdue_invoices(&self) -> bool { - self.has_overdue_invoices.unwrap_or(false) - } - pub fn current_user_has_accepted_terms(&self) -> Option { self.accepted_tos_at .map(|accepted_tos_at| accepted_tos_at.is_some()) diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index 81d9181cfc..d402b87382 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use client::{DisableAiSettings, UserStore, zed_urls}; +use client::{CloudUserStore, DisableAiSettings, zed_urls}; use cloud_llm_client::UsageLimit; use copilot::{Copilot, Status}; use editor::{ @@ -59,7 +59,7 @@ pub struct InlineCompletionButton { file: Option>, edit_prediction_provider: Option>, fs: Arc, - user_store: Entity, + cloud_user_store: Entity, popover_menu_handle: PopoverMenuHandle, } @@ -245,13 +245,16 @@ impl Render for InlineCompletionButton { IconName::ZedPredictDisabled }; - if zeta::should_show_upsell_modal(&self.user_store, cx) { - let tooltip_meta = - match self.user_store.read(cx).current_user_has_accepted_terms() { - Some(true) => "Choose a Plan", - Some(false) => "Accept the Terms of Service", - None => "Sign In", - }; + if zeta::should_show_upsell_modal(&self.cloud_user_store, cx) { + let tooltip_meta = if self.cloud_user_store.read(cx).is_authenticated() { + if self.cloud_user_store.read(cx).has_accepted_tos() { + "Choose a Plan" + } else { + "Accept the Terms of Service" + } + } else { + "Sign In" + }; return div().child( IconButton::new("zed-predict-pending-button", zeta_icon) @@ -368,7 +371,7 @@ impl Render for InlineCompletionButton { impl InlineCompletionButton { pub fn new( fs: Arc, - user_store: Entity, + cloud_user_store: Entity, popover_menu_handle: PopoverMenuHandle, cx: &mut Context, ) -> Self { @@ -389,7 +392,7 @@ impl InlineCompletionButton { edit_prediction_provider: None, popover_menu_handle, fs, - user_store, + cloud_user_store, } } @@ -760,7 +763,7 @@ impl InlineCompletionButton { }) }) .separator(); - } else if self.user_store.read(cx).account_too_young() { + } else if self.cloud_user_store.read(cx).account_too_young() { menu = menu .custom_entry( |_window, _cx| { @@ -775,7 +778,7 @@ impl InlineCompletionButton { cx.open_url(&zed_urls::account_url(cx)) }) .separator(); - } else if self.user_store.read(cx).has_overdue_invoices() { + } else if self.cloud_user_store.read(cx).has_overdue_invoices() { menu = menu .custom_entry( |_window, _cx| { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 0a43ec0bbe..060efdf26a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -336,7 +336,7 @@ pub fn initialize_workspace( let edit_prediction_button = cx.new(|cx| { inline_completion_button::InlineCompletionButton::new( app_state.fs.clone(), - app_state.user_store.clone(), + app_state.cloud_user_store.clone(), inline_completion_menu_handle.clone(), cx, ) diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index d295b7d17c..0ef6bef59d 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -16,7 +16,7 @@ pub use rate_completion_modal::*; use anyhow::{Context as _, Result, anyhow}; use arrayvec::ArrayVec; -use client::{Client, CloudUserStore, EditPredictionUsage, UserStore}; +use client::{Client, CloudUserStore, EditPredictionUsage}; use cloud_llm_client::{ AcceptEditPredictionBody, EXPIRED_LLM_TOKEN_HEADER_NAME, MINIMUM_REQUIRED_VERSION_HEADER_NAME, PredictEditsBody, PredictEditsResponse, ZED_VERSION_HEADER_NAME, @@ -120,10 +120,11 @@ impl Dismissable for ZedPredictUpsell { } } -pub fn should_show_upsell_modal(user_store: &Entity, cx: &App) -> bool { - match user_store.read(cx).current_user_has_accepted_terms() { - Some(true) => !ZedPredictUpsell::dismissed(), - Some(false) | None => true, +pub fn should_show_upsell_modal(cloud_user_store: &Entity, cx: &App) -> bool { + if cloud_user_store.read(cx).has_accepted_tos() { + !ZedPredictUpsell::dismissed() + } else { + true } } @@ -1804,6 +1805,7 @@ fn tokens_for_bytes(bytes: usize) -> usize { #[cfg(test)] mod tests { + use client::UserStore; use client::test::FakeServer; use clock::FakeSystemClock; use cloud_api_types::{ From 106aa0d9cc8628df16a37e3703c18f67eb8f1876 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 1 Aug 2025 01:53:40 -0400 Subject: [PATCH 016/693] Add default binding to open settings profile selector (#35459) Release Notes: - N/A --- assets/keymaps/default-linux.json | 1 + assets/keymaps/default-macos.json | 1 + 2 files changed, 2 insertions(+) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 9d5c6b2043..8a8dbd8a90 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -598,6 +598,7 @@ "ctrl-shift-t": "pane::ReopenClosedItem", "ctrl-k ctrl-s": "zed::OpenKeymapEditor", "ctrl-k ctrl-t": "theme_selector::Toggle", + "ctrl-alt-super-p": "settings_profile_selector::Toggle", "ctrl-t": "project_symbols::Toggle", "ctrl-p": "file_finder::Toggle", "ctrl-tab": "tab_switcher::Toggle", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 4c44906d55..62ba187851 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -665,6 +665,7 @@ "cmd-shift-t": "pane::ReopenClosedItem", "cmd-k cmd-s": "zed::OpenKeymapEditor", "cmd-k cmd-t": "theme_selector::Toggle", + "ctrl-alt-cmd-p": "settings_profile_selector::Toggle", "cmd-t": "project_symbols::Toggle", "cmd-p": "file_finder::Toggle", "ctrl-tab": "tab_switcher::Toggle", From e5c6a596a9ff35ec7925b0e3522edff6209e686f Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 1 Aug 2025 16:29:02 +0200 Subject: [PATCH 017/693] agent_ui: More agent notifications (#35441) Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 32 +- crates/agent_ui/src/acp/thread_view.rs | 523 ++++++++++++++++++++++++- crates/agent_ui/src/agent_diff.rs | 3 + 3 files changed, 547 insertions(+), 11 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 7203580410..7a10f3bd72 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -580,6 +580,9 @@ pub struct AcpThread { pub enum AcpThreadEvent { NewEntry, EntryUpdated(usize), + ToolAuthorizationRequired, + Stopped, + Error, } impl EventEmitter for AcpThread {} @@ -676,6 +679,18 @@ impl AcpThread { false } + pub fn used_tools_since_last_user_message(&self) -> bool { + for entry in self.entries.iter().rev() { + match entry { + AgentThreadEntry::UserMessage(..) => return false, + AgentThreadEntry::AssistantMessage(..) => continue, + AgentThreadEntry::ToolCall(..) => return true, + } + } + + false + } + pub fn handle_session_update( &mut self, update: acp::SessionUpdate, @@ -879,6 +894,7 @@ impl AcpThread { }; self.upsert_tool_call_inner(tool_call, status, cx); + cx.emit(AcpThreadEvent::ToolAuthorizationRequired); rx } @@ -1018,12 +1034,18 @@ impl AcpThread { .log_err(); })); - async move { - match rx.await { - Ok(Err(e)) => Err(e)?, - _ => Ok(()), + cx.spawn(async move |this, cx| match rx.await { + Ok(Err(e)) => { + this.update(cx, |_, cx| cx.emit(AcpThreadEvent::Error)) + .log_err(); + Err(e)? } - } + _ => { + this.update(cx, |_, cx| cx.emit(AcpThreadEvent::Stopped)) + .log_err(); + Ok(()) + } + }) .boxed() } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index e46e1ae3ab..8820e4a73d 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1,5 +1,7 @@ use acp_thread::{AgentConnection, Plan}; use agent_servers::AgentServer; +use agent_settings::{AgentSettings, NotifyWhenAgentWaiting}; +use audio::{Audio, Sound}; use std::cell::RefCell; use std::collections::BTreeMap; use std::path::Path; @@ -18,10 +20,10 @@ use editor::{ use file_icons::FileIcons; use gpui::{ Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId, - FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement, - Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, - Window, div, linear_color_stop, linear_gradient, list, percentage, point, prelude::*, - pulsating_between, + FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, PlatformDisplay, SharedString, + StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation, + UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop, linear_gradient, + list, percentage, point, prelude::*, pulsating_between, }; use language::language_settings::SoftWrap; use language::{Buffer, Language}; @@ -45,7 +47,10 @@ use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSe use crate::acp::message_history::MessageHistory; use crate::agent_diff::AgentDiff; use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES}; -use crate::{AgentDiffPane, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll}; +use crate::ui::{AgentNotification, AgentNotificationEvent}; +use crate::{ + AgentDiffPane, AgentPanel, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll, +}; const RESPONSE_PADDING_X: Pixels = px(19.); @@ -59,6 +64,8 @@ pub struct AcpThreadView { message_set_from_history: bool, _message_editor_subscription: Subscription, mention_set: Arc>, + notifications: Vec>, + notification_subscriptions: HashMap, Vec>, last_error: Option>, list_state: ListState, auth_task: Option>, @@ -174,6 +181,8 @@ impl AcpThreadView { message_set_from_history: false, _message_editor_subscription: message_editor_subscription, mention_set, + notifications: Vec::new(), + notification_subscriptions: HashMap::default(), diff_editors: Default::default(), list_state: list_state, last_error: None, @@ -381,7 +390,9 @@ impl AcpThreadView { return; } - let Some(thread) = self.thread() else { return }; + let Some(thread) = self.thread() else { + return; + }; let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx)); cx.spawn(async move |this, cx| { @@ -564,6 +575,30 @@ impl AcpThreadView { self.sync_thread_entry_view(index, window, cx); self.list_state.splice(index..index + 1, 1); } + AcpThreadEvent::ToolAuthorizationRequired => { + self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx); + } + AcpThreadEvent::Stopped => { + let used_tools = thread.read(cx).used_tools_since_last_user_message(); + self.notify_with_sound( + if used_tools { + "Finished running tools" + } else { + "New message" + }, + IconName::ZedAssistant, + window, + cx, + ); + } + AcpThreadEvent::Error => { + self.notify_with_sound( + "Agent stopped due to an error", + IconName::Warning, + window, + cx, + ); + } } cx.notify(); } @@ -2160,6 +2195,154 @@ impl AcpThreadView { self.list_state.scroll_to(ListOffset::default()); cx.notify(); } + + fn notify_with_sound( + &mut self, + caption: impl Into, + icon: IconName, + window: &mut Window, + cx: &mut Context, + ) { + self.play_notification_sound(window, cx); + self.show_notification(caption, icon, window, cx); + } + + fn play_notification_sound(&self, window: &Window, cx: &mut App) { + let settings = AgentSettings::get_global(cx); + if settings.play_sound_when_agent_done && !window.is_window_active() { + Audio::play_sound(Sound::AgentDone, cx); + } + } + + fn show_notification( + &mut self, + caption: impl Into, + icon: IconName, + window: &mut Window, + cx: &mut Context, + ) { + if window.is_window_active() || !self.notifications.is_empty() { + return; + } + + let title = self.title(cx); + + match AgentSettings::get_global(cx).notify_when_agent_waiting { + NotifyWhenAgentWaiting::PrimaryScreen => { + if let Some(primary) = cx.primary_display() { + self.pop_up(icon, caption.into(), title, window, primary, cx); + } + } + NotifyWhenAgentWaiting::AllScreens => { + let caption = caption.into(); + for screen in cx.displays() { + self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx); + } + } + NotifyWhenAgentWaiting::Never => { + // Don't show anything + } + } + } + + fn pop_up( + &mut self, + icon: IconName, + caption: SharedString, + title: SharedString, + window: &mut Window, + screen: Rc, + cx: &mut Context, + ) { + let options = AgentNotification::window_options(screen, cx); + + let project_name = self.workspace.upgrade().and_then(|workspace| { + workspace + .read(cx) + .project() + .read(cx) + .visible_worktrees(cx) + .next() + .map(|worktree| worktree.read(cx).root_name().to_string()) + }); + + if let Some(screen_window) = cx + .open_window(options, |_, cx| { + cx.new(|_| { + AgentNotification::new(title.clone(), caption.clone(), icon, project_name) + }) + }) + .log_err() + { + if let Some(pop_up) = screen_window.entity(cx).log_err() { + self.notification_subscriptions + .entry(screen_window) + .or_insert_with(Vec::new) + .push(cx.subscribe_in(&pop_up, window, { + |this, _, event, window, cx| match event { + AgentNotificationEvent::Accepted => { + let handle = window.window_handle(); + cx.activate(true); + + let workspace_handle = this.workspace.clone(); + + // If there are multiple Zed windows, activate the correct one. + cx.defer(move |cx| { + handle + .update(cx, |_view, window, _cx| { + window.activate_window(); + + if let Some(workspace) = workspace_handle.upgrade() { + workspace.update(_cx, |workspace, cx| { + workspace.focus_panel::(window, cx); + }); + } + }) + .log_err(); + }); + + this.dismiss_notifications(cx); + } + AgentNotificationEvent::Dismissed => { + this.dismiss_notifications(cx); + } + } + })); + + self.notifications.push(screen_window); + + // If the user manually refocuses the original window, dismiss the popup. + self.notification_subscriptions + .entry(screen_window) + .or_insert_with(Vec::new) + .push({ + let pop_up_weak = pop_up.downgrade(); + + cx.observe_window_activation(window, move |_, window, cx| { + if window.is_window_active() { + if let Some(pop_up) = pop_up_weak.upgrade() { + pop_up.update(cx, |_, cx| { + cx.emit(AgentNotificationEvent::Dismissed); + }); + } + } + }) + }); + } + } + } + + fn dismiss_notifications(&mut self, cx: &mut Context) { + for window in self.notifications.drain(..) { + window + .update(cx, |_, window, _| { + window.remove_window(); + }) + .ok(); + + self.notification_subscriptions.remove(&window); + } + } } impl Focusable for AcpThreadView { @@ -2441,3 +2624,331 @@ fn plan_label_markdown_style( ..default_md_style } } + +#[cfg(test)] +mod tests { + use agent_client_protocol::SessionId; + use editor::EditorSettings; + use fs::FakeFs; + use futures::future::try_join_all; + use gpui::{SemanticVersion, TestAppContext, VisualTestContext}; + use rand::Rng; + use settings::SettingsStore; + + use super::*; + + #[gpui::test] + async fn test_notification_for_stop_event(cx: &mut TestAppContext) { + init_test(cx); + + let (thread_view, cx) = setup_thread_view(StubAgentServer::default(), cx).await; + + let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Hello", window, cx); + }); + + cx.deactivate_window(); + + thread_view.update_in(cx, |thread_view, window, cx| { + thread_view.chat(&Chat, window, cx); + }); + + cx.run_until_parked(); + + assert!( + cx.windows() + .iter() + .any(|window| window.downcast::().is_some()) + ); + } + + #[gpui::test] + async fn test_notification_for_error(cx: &mut TestAppContext) { + init_test(cx); + + let (thread_view, cx) = + setup_thread_view(StubAgentServer::new(SaboteurAgentConnection), cx).await; + + let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Hello", window, cx); + }); + + cx.deactivate_window(); + + thread_view.update_in(cx, |thread_view, window, cx| { + thread_view.chat(&Chat, window, cx); + }); + + cx.run_until_parked(); + + assert!( + cx.windows() + .iter() + .any(|window| window.downcast::().is_some()) + ); + } + + #[gpui::test] + async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) { + init_test(cx); + + let tool_call_id = acp::ToolCallId("1".into()); + let tool_call = acp::ToolCall { + id: tool_call_id.clone(), + label: "Label".into(), + kind: acp::ToolKind::Edit, + status: acp::ToolCallStatus::Pending, + content: vec!["hi".into()], + locations: vec![], + raw_input: None, + }; + let connection = StubAgentConnection::new(vec![acp::SessionUpdate::ToolCall(tool_call)]) + .with_permission_requests(HashMap::from_iter([( + tool_call_id, + vec![acp::PermissionOption { + id: acp::PermissionOptionId("1".into()), + label: "Allow".into(), + kind: acp::PermissionOptionKind::AllowOnce, + }], + )])); + let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; + + let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Hello", window, cx); + }); + + cx.deactivate_window(); + + thread_view.update_in(cx, |thread_view, window, cx| { + thread_view.chat(&Chat, window, cx); + }); + + cx.run_until_parked(); + + assert!( + cx.windows() + .iter() + .any(|window| window.downcast::().is_some()) + ); + } + + async fn setup_thread_view( + agent: impl AgentServer + 'static, + cx: &mut TestAppContext, + ) -> (Entity, &mut VisualTestContext) { + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [], cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + let thread_view = cx.update(|window, cx| { + cx.new(|cx| { + AcpThreadView::new( + Rc::new(agent), + workspace.downgrade(), + project, + Rc::new(RefCell::new(MessageHistory::default())), + 1, + None, + window, + cx, + ) + }) + }); + cx.run_until_parked(); + (thread_view, cx) + } + + struct StubAgentServer { + connection: C, + } + + impl StubAgentServer { + fn new(connection: C) -> Self { + Self { connection } + } + } + + impl StubAgentServer { + fn default() -> Self { + Self::new(StubAgentConnection::default()) + } + } + + impl AgentServer for StubAgentServer + where + C: 'static + AgentConnection + Send + Clone, + { + fn logo(&self) -> ui::IconName { + unimplemented!() + } + + fn name(&self) -> &'static str { + unimplemented!() + } + + fn empty_state_headline(&self) -> &'static str { + unimplemented!() + } + + fn empty_state_message(&self) -> &'static str { + unimplemented!() + } + + fn connect( + &self, + _root_dir: &Path, + _project: &Entity, + _cx: &mut App, + ) -> Task>> { + Task::ready(Ok(Rc::new(self.connection.clone()))) + } + } + + #[derive(Clone, Default)] + struct StubAgentConnection { + sessions: Arc>>>, + permission_requests: HashMap>, + updates: Vec, + } + + impl StubAgentConnection { + fn new(updates: Vec) -> Self { + Self { + updates, + permission_requests: HashMap::default(), + sessions: Arc::default(), + } + } + + fn with_permission_requests( + mut self, + permission_requests: HashMap>, + ) -> Self { + self.permission_requests = permission_requests; + self + } + } + + impl AgentConnection for StubAgentConnection { + fn name(&self) -> &'static str { + "StubAgentConnection" + } + + fn new_thread( + self: Rc, + project: Entity, + _cwd: &Path, + cx: &mut gpui::AsyncApp, + ) -> Task>> { + let session_id = SessionId( + rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(7) + .map(char::from) + .collect::() + .into(), + ); + let thread = cx + .new(|cx| AcpThread::new(self.clone(), project, session_id.clone(), cx)) + .unwrap(); + self.sessions.lock().insert(session_id, thread.downgrade()); + Task::ready(Ok(thread)) + } + + fn authenticate(&self, _cx: &mut App) -> Task> { + unimplemented!() + } + + fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task> { + let sessions = self.sessions.lock(); + let thread = sessions.get(¶ms.session_id).unwrap(); + let mut tasks = vec![]; + for update in &self.updates { + let thread = thread.clone(); + let update = update.clone(); + let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) = &update + && let Some(options) = self.permission_requests.get(&tool_call.id) + { + Some((tool_call.clone(), options.clone())) + } else { + None + }; + let task = cx.spawn(async move |cx| { + if let Some((tool_call, options)) = permission_request { + let permission = thread.update(cx, |thread, cx| { + thread.request_tool_call_permission( + tool_call.clone(), + options.clone(), + cx, + ) + })?; + permission.await?; + } + thread.update(cx, |thread, cx| { + thread.handle_session_update(update.clone(), cx).unwrap(); + })?; + anyhow::Ok(()) + }); + tasks.push(task); + } + cx.spawn(async move |_| { + try_join_all(tasks).await?; + Ok(()) + }) + } + + fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) { + unimplemented!() + } + } + + #[derive(Clone)] + struct SaboteurAgentConnection; + + impl AgentConnection for SaboteurAgentConnection { + fn name(&self) -> &'static str { + "SaboteurAgentConnection" + } + + fn new_thread( + self: Rc, + project: Entity, + _cwd: &Path, + cx: &mut gpui::AsyncApp, + ) -> Task>> { + Task::ready(Ok(cx + .new(|cx| AcpThread::new(self, project, SessionId("test".into()), cx)) + .unwrap())) + } + + fn authenticate(&self, _cx: &mut App) -> Task> { + unimplemented!() + } + + fn prompt(&self, _params: acp::PromptArguments, _cx: &mut App) -> Task> { + Task::ready(Err(anyhow::anyhow!("Error prompting"))) + } + + fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) { + unimplemented!() + } + } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + Project::init_settings(cx); + AgentSettings::register(cx); + workspace::init_settings(cx); + ThemeSettings::register(cx); + release_channel::init(SemanticVersion::default(), cx); + EditorSettings::register(cx); + }); + } +} diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 5c8011cb18..135f07a934 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1521,6 +1521,9 @@ impl AgentDiff { self.update_reviewing_editors(workspace, window, cx); } } + AcpThreadEvent::Stopped + | AcpThreadEvent::ToolAuthorizationRequired + | AcpThreadEvent::Error => {} } } From b01d1872cc2bdde7ca6d52f2c065a5ef550afaa7 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Fri, 1 Aug 2025 16:43:59 +0200 Subject: [PATCH 018/693] onboarding: Add the AI page (#35351) This PR starts the work on the AI onboarding page as well as the configuration modal Release Notes: - N/A --------- Co-authored-by: Danilo Leal Co-authored-by: Anthony --- Cargo.lock | 4 + crates/agent_ui/src/agent_configuration.rs | 14 +- crates/ai_onboarding/src/ai_upsell_card.rs | 27 +- crates/onboarding/Cargo.toml | 4 + crates/onboarding/src/ai_setup_page.rs | 362 +++++++++++++++++++++ crates/onboarding/src/basics_page.rs | 10 +- crates/onboarding/src/editing_page.rs | 4 +- crates/onboarding/src/onboarding.rs | 44 +-- crates/ui/src/components.rs | 2 + crates/ui/src/components/badge.rs | 71 ++++ crates/ui/src/components/modal.rs | 24 +- crates/ui/src/components/toggle.rs | 47 ++- 12 files changed, 550 insertions(+), 63 deletions(-) create mode 100644 crates/onboarding/src/ai_setup_page.rs create mode 100644 crates/ui/src/components/badge.rs diff --git a/Cargo.lock b/Cargo.lock index f76d4d520d..63a66d7150 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10923,6 +10923,7 @@ dependencies = [ name = "onboarding" version = "0.1.0" dependencies = [ + "ai_onboarding", "anyhow", "client", "command_palette_hooks", @@ -10933,7 +10934,10 @@ dependencies = [ "feature_flags", "fs", "gpui", + "itertools 0.14.0", "language", + "language_model", + "menu", "project", "schemars", "serde", diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index fae04188eb..b88b85d85b 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -406,7 +406,9 @@ impl AgentConfiguration { SwitchField::new( "always-allow-tool-actions-switch", "Allow running commands without asking for confirmation", - "The agent can perform potentially destructive actions without asking for your confirmation.", + Some( + "The agent can perform potentially destructive actions without asking for your confirmation.".into(), + ), always_allow_tool_actions, move |state, _window, cx| { let allow = state == &ToggleState::Selected; @@ -424,7 +426,7 @@ impl AgentConfiguration { SwitchField::new( "single-file-review", "Enable single-file agent reviews", - "Agent edits are also displayed in single-file editors for review.", + Some("Agent edits are also displayed in single-file editors for review.".into()), single_file_review, move |state, _window, cx| { let allow = state == &ToggleState::Selected; @@ -442,7 +444,9 @@ impl AgentConfiguration { SwitchField::new( "sound-notification", "Play sound when finished generating", - "Hear a notification sound when the agent is done generating changes or needs your input.", + Some( + "Hear a notification sound when the agent is done generating changes or needs your input.".into(), + ), play_sound_when_agent_done, move |state, _window, cx| { let allow = state == &ToggleState::Selected; @@ -460,7 +464,9 @@ impl AgentConfiguration { SwitchField::new( "modifier-send", "Use modifier to submit a message", - "Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.", + Some( + "Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.".into(), + ), use_modifier_to_send, move |state, _window, cx| { let allow = state == &ToggleState::Selected; diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index 041e0d87ec..56eaca2392 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use client::{Client, zed_urls}; +use cloud_llm_client::Plan; use gpui::{AnyElement, App, IntoElement, RenderOnce, Window}; use ui::{Divider, List, Vector, VectorName, prelude::*}; @@ -10,13 +11,15 @@ use crate::{BulletItem, SignInStatus}; pub struct AiUpsellCard { pub sign_in_status: SignInStatus, pub sign_in: Arc, + pub user_plan: Option, } impl AiUpsellCard { - pub fn new(client: Arc) -> Self { + pub fn new(client: Arc, user_plan: Option) -> Self { let status = *client.status().borrow(); Self { + user_plan, sign_in_status: status.into(), sign_in: Arc::new(move |_window, cx| { cx.spawn({ @@ -34,6 +37,7 @@ impl AiUpsellCard { impl RenderOnce for AiUpsellCard { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let pro_section = v_flex() + .flex_grow() .w_full() .gap_1() .child( @@ -56,6 +60,7 @@ impl RenderOnce for AiUpsellCard { ); let free_section = v_flex() + .flex_grow() .w_full() .gap_1() .child( @@ -71,7 +76,7 @@ impl RenderOnce for AiUpsellCard { ) .child( List::new() - .child(BulletItem::new("50 prompts with the Claude models")) + .child(BulletItem::new("50 prompts with Claude models")) .child(BulletItem::new("2,000 accepted edit predictions")), ); @@ -132,22 +137,28 @@ impl RenderOnce for AiUpsellCard { v_flex() .relative() - .p_6() - .pt_4() + .p_4() + .pt_3() .border_1() .border_color(cx.theme().colors().border) .rounded_lg() .overflow_hidden() .child(grid_bg) .child(gradient_bg) - .child(Headline::new("Try Zed AI")) - .child(Label::new(DESCRIPTION).color(Color::Muted).mb_2()) + .child(Label::new("Try Zed AI").size(LabelSize::Large)) + .child( + div() + .max_w_3_4() + .mb_2() + .child(Label::new(DESCRIPTION).color(Color::Muted)), + ) .child( h_flex() + .w_full() .mt_1p5() .mb_2p5() .items_start() - .gap_12() + .gap_6() .child(free_section) .child(pro_section), ) @@ -183,6 +194,7 @@ impl Component for AiUpsellCard { AiUpsellCard { sign_in_status: SignInStatus::SignedOut, sign_in: Arc::new(|_, _| {}), + user_plan: None, } .into_any_element(), ), @@ -191,6 +203,7 @@ impl Component for AiUpsellCard { AiUpsellCard { sign_in_status: SignInStatus::SignedIn, sign_in: Arc::new(|_, _| {}), + user_plan: None, } .into_any_element(), ), diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index 7727597e94..8f684dd1b8 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -16,6 +16,7 @@ default = [] [dependencies] anyhow.workspace = true +ai_onboarding.workspace = true client.workspace = true command_palette_hooks.workspace = true component.workspace = true @@ -25,7 +26,10 @@ editor.workspace = true feature_flags.workspace = true fs.workspace = true gpui.workspace = true +itertools.workspace = true language.workspace = true +language_model.workspace = true +menu.workspace = true project.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs new file mode 100644 index 0000000000..a5b4b1d7be --- /dev/null +++ b/crates/onboarding/src/ai_setup_page.rs @@ -0,0 +1,362 @@ +use std::sync::Arc; + +use ai_onboarding::{AiUpsellCard, SignInStatus}; +use client::DisableAiSettings; +use fs::Fs; +use gpui::{ + Action, AnyView, App, DismissEvent, EventEmitter, FocusHandle, Focusable, Window, prelude::*, +}; +use itertools; + +use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry}; +use settings::{Settings, update_settings_file}; +use ui::{ + Badge, ButtonLike, Divider, Modal, ModalFooter, ModalHeader, Section, SwitchField, ToggleState, + prelude::*, +}; +use workspace::ModalView; + +use util::ResultExt; +use zed_actions::agent::OpenSettings; + +use crate::Onboarding; + +const FEATURED_PROVIDERS: [&'static str; 4] = ["anthropic", "google", "openai", "ollama"]; + +fn render_llm_provider_section( + onboarding: &Onboarding, + disabled: bool, + window: &mut Window, + cx: &mut App, +) -> impl IntoElement { + v_flex() + .gap_4() + .child( + v_flex() + .child(Label::new("Or use other LLM providers").size(LabelSize::Large)) + .child( + Label::new("Bring your API keys to use the available providers with Zed's UI for free.") + .color(Color::Muted), + ), + ) + .child(render_llm_provider_card(onboarding, disabled, window, cx)) +} + +fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement { + v_flex() + .relative() + .pt_2() + .pb_2p5() + .pl_3() + .pr_2() + .border_1() + .border_dashed() + .border_color(cx.theme().colors().border.opacity(0.5)) + .bg(cx.theme().colors().surface_background.opacity(0.3)) + .rounded_lg() + .overflow_hidden() + .map(|this| { + if disabled { + this.child( + h_flex() + .gap_2() + .justify_between() + .child( + h_flex() + .gap_1() + .child(Label::new("AI is disabled across Zed")) + .child( + Icon::new(IconName::Check) + .color(Color::Success) + .size(IconSize::XSmall), + ), + ) + .child(Badge::new("PRIVACY").icon(IconName::FileLock)), + ) + .child( + Label::new("Re-enable it any time in Settings.") + .size(LabelSize::Small) + .color(Color::Muted), + ) + } else { + this.child( + h_flex() + .gap_2() + .justify_between() + .child(Label::new("We don't train models using your data")) + .child( + h_flex() + .gap_1() + .child(Badge::new("Privacy").icon(IconName::FileLock)) + .child( + Button::new("learn_more", "Learn More") + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .on_click(|_, _, cx| { + cx.open_url( + "https://zed.dev/docs/ai/privacy-and-security", + ); + }), + ), + ), + ) + .child( + Label::new( + "Feel confident in the security and privacy of your projects using Zed.", + ) + .size(LabelSize::Small) + .color(Color::Muted), + ) + } + }) +} + +fn render_llm_provider_card( + onboarding: &Onboarding, + disabled: bool, + _: &mut Window, + cx: &mut App, +) -> impl IntoElement { + let registry = LanguageModelRegistry::read_global(cx); + + v_flex() + .border_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().surface_background.opacity(0.5)) + .rounded_lg() + .overflow_hidden() + .children(itertools::intersperse_with( + FEATURED_PROVIDERS + .into_iter() + .flat_map(|provider_name| { + registry.provider(&LanguageModelProviderId::new(provider_name)) + }) + .enumerate() + .map(|(index, provider)| { + let group_name = SharedString::new(format!("onboarding-hover-group-{}", index)); + let is_authenticated = provider.is_authenticated(cx); + + ButtonLike::new(("onboarding-ai-setup-buttons", index)) + .size(ButtonSize::Large) + .child( + h_flex() + .group(&group_name) + .px_0p5() + .w_full() + .gap_2() + .justify_between() + .child( + h_flex() + .gap_1() + .child( + Icon::new(provider.icon()) + .color(Color::Muted) + .size(IconSize::XSmall), + ) + .child(Label::new(provider.name().0)), + ) + .child( + h_flex() + .gap_1() + .when(!is_authenticated, |el| { + el.visible_on_hover(group_name.clone()) + .child( + Icon::new(IconName::Settings) + .color(Color::Muted) + .size(IconSize::XSmall), + ) + .child( + Label::new("Configure") + .color(Color::Muted) + .size(LabelSize::Small), + ) + }) + .when(is_authenticated && !disabled, |el| { + el.child( + Icon::new(IconName::Check) + .color(Color::Success) + .size(IconSize::XSmall), + ) + .child( + Label::new("Configured") + .color(Color::Muted) + .size(LabelSize::Small), + ) + }), + ), + ) + .on_click({ + let workspace = onboarding.workspace.clone(); + move |_, window, cx| { + workspace + .update(cx, |workspace, cx| { + workspace.toggle_modal(window, cx, |window, cx| { + let modal = AiConfigurationModal::new( + provider.clone(), + window, + cx, + ); + window.focus(&modal.focus_handle(cx)); + modal + }); + }) + .log_err(); + } + }) + .into_any_element() + }), + || Divider::horizontal().into_any_element(), + )) + .child(Divider::horizontal()) + .child( + Button::new("agent_settings", "Add Many Others") + .size(ButtonSize::Large) + .icon(IconName::Plus) + .icon_position(IconPosition::Start) + .icon_color(Color::Muted) + .icon_size(IconSize::XSmall) + .on_click(|_event, window, cx| { + window.dispatch_action(OpenSettings.boxed_clone(), cx) + }), + ) +} + +pub(crate) fn render_ai_setup_page( + onboarding: &Onboarding, + window: &mut Window, + cx: &mut App, +) -> impl IntoElement { + let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai; + + let backdrop = div() + .id("backdrop") + .size_full() + .absolute() + .inset_0() + .bg(cx.theme().colors().editor_background) + .opacity(0.8) + .block_mouse_except_scroll(); + + v_flex() + .gap_2() + .child(SwitchField::new( + "enable_ai", + "Enable AI features", + None, + if is_ai_disabled { + ToggleState::Unselected + } else { + ToggleState::Selected + }, + |toggle_state, _, cx| { + let enabled = match toggle_state { + ToggleState::Indeterminate => { + return; + } + ToggleState::Unselected => false, + ToggleState::Selected => true, + }; + + let fs = ::global(cx); + update_settings_file::( + fs, + cx, + move |ai_settings: &mut Option, _| { + *ai_settings = Some(!enabled); + }, + ); + }, + )) + .child(render_privacy_card(is_ai_disabled, cx)) + .child( + v_flex() + .mt_2() + .gap_6() + .child(AiUpsellCard { + sign_in_status: SignInStatus::SignedIn, + sign_in: Arc::new(|_, _| {}), + user_plan: onboarding.cloud_user_store.read(cx).plan(), + }) + .child(render_llm_provider_section( + onboarding, + is_ai_disabled, + window, + cx, + )) + .when(is_ai_disabled, |this| this.child(backdrop)), + ) +} + +struct AiConfigurationModal { + focus_handle: FocusHandle, + selected_provider: Arc, + configuration_view: AnyView, +} + +impl AiConfigurationModal { + fn new( + selected_provider: Arc, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let focus_handle = cx.focus_handle(); + let configuration_view = selected_provider.configuration_view(window, cx); + + Self { + focus_handle, + configuration_view, + selected_provider, + } + } +} + +impl ModalView for AiConfigurationModal {} + +impl EventEmitter for AiConfigurationModal {} + +impl Focusable for AiConfigurationModal { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for AiConfigurationModal { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .w(rems(34.)) + .elevation_3(cx) + .track_focus(&self.focus_handle) + .child( + Modal::new("onboarding-ai-setup-modal", None) + .header( + ModalHeader::new() + .icon( + Icon::new(self.selected_provider.icon()) + .color(Color::Muted) + .size(IconSize::Small), + ) + .headline(self.selected_provider.name().0), + ) + .section(Section::new().child(self.configuration_view.clone())) + .footer( + ModalFooter::new().end_slot( + h_flex() + .gap_1() + .child( + Button::new("onboarding-closing-cancel", "Cancel") + .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), + ) + .child(Button::new("save-btn", "Done").on_click(cx.listener( + |_, _, window, cx| { + window.dispatch_action(menu::Confirm.boxed_clone(), cx); + cx.emit(DismissEvent); + }, + ))), + ), + ), + ) + } +} diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index bfbe0374d3..aac8241251 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -242,7 +242,7 @@ fn render_telemetry_section(cx: &App) -> impl IntoElement { .child(SwitchField::new( "onboarding-telemetry-metrics", "Help Improve Zed", - "Sending anonymous usage data helps us build the right features and create the best experience.", + Some("Sending anonymous usage data helps us build the right features and create the best experience.".into()), if TelemetrySettings::get_global(cx).metrics { ui::ToggleState::Selected } else { @@ -267,7 +267,7 @@ fn render_telemetry_section(cx: &App) -> impl IntoElement { .child(SwitchField::new( "onboarding-telemetry-crash-reports", "Help Fix Zed", - "Send crash reports so we can fix critical issues fast.", + Some("Send crash reports so we can fix critical issues fast.".into()), if TelemetrySettings::get_global(cx).diagnostics { ui::ToggleState::Selected } else { @@ -338,10 +338,10 @@ pub(crate) fn render_basics_page(window: &mut Window, cx: &mut App) -> impl Into .style(ui::ToggleButtonGroupStyle::Outlined) ), ) - .child(v_flex().justify_center().child(div().h_0().child("hack").invisible()).child(SwitchField::new( + .child(SwitchField::new( "onboarding-vim-mode", "Vim Mode", - "Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back.", + Some("Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back.".into()), if VimModeSetting::get_global(cx).0 { ui::ToggleState::Selected } else { @@ -363,6 +363,6 @@ pub(crate) fn render_basics_page(window: &mut Window, cx: &mut App) -> impl Into ); } }, - ))) + )) .child(render_telemetry_section(cx)) } diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 3fb9aaf0cc..759d557805 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -349,7 +349,7 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In .child(SwitchField::new( "onboarding-enable-inlay-hints", "Inlay Hints", - "See parameter names for function and method calls inline.", + Some("See parameter names for function and method calls inline.".into()), if read_inlay_hints(cx) { ui::ToggleState::Selected } else { @@ -362,7 +362,7 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In .child(SwitchField::new( "onboarding-git-blame-switch", "Git Blame", - "See who committed each line on a given file.", + Some("See who committed each line on a given file.".into()), if read_git_blame(cx) { ui::ToggleState::Selected } else { diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 6496c09e79..bf60da4aab 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -1,5 +1,5 @@ use crate::welcome::{ShowWelcome, WelcomePage}; -use client::{Client, UserStore}; +use client::{Client, CloudUserStore, UserStore}; use command_palette_hooks::CommandPaletteFilter; use db::kvp::KEY_VALUE_STORE; use feature_flags::{FeatureFlag, FeatureFlagViewExt as _}; @@ -25,6 +25,7 @@ use workspace::{ open_new, with_active_or_new_workspace, }; +mod ai_setup_page; mod basics_page; mod editing_page; mod theme_preview; @@ -78,11 +79,7 @@ pub fn init(cx: &mut App) { if let Some(existing) = existing { workspace.activate_item(&existing, true, true, window, cx); } else { - let settings_page = Onboarding::new( - workspace.weak_handle(), - workspace.user_store().clone(), - cx, - ); + let settings_page = Onboarding::new(workspace, cx); workspace.add_item_to_active_pane( Box::new(settings_page), None, @@ -198,8 +195,7 @@ pub fn show_onboarding_view(app_state: Arc, cx: &mut App) -> Task, focus_handle: FocusHandle, selected_page: SelectedPage, + cloud_user_store: Entity, user_store: Entity, _settings_subscription: Subscription, } impl Onboarding { - fn new( - workspace: WeakEntity, - user_store: Entity, - cx: &mut App, - ) -> Entity { + fn new(workspace: &Workspace, cx: &mut App) -> Entity { cx.new(|cx| Self { - workspace, - user_store, + workspace: workspace.weak_handle(), focus_handle: cx.focus_handle(), selected_page: SelectedPage::Basics, + cloud_user_store: workspace.app_state().cloud_user_store.clone(), + user_store: workspace.user_store().clone(), _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), }) } @@ -391,13 +385,11 @@ impl Onboarding { SelectedPage::Editing => { crate::editing_page::render_editing_page(window, cx).into_any_element() } - SelectedPage::AiSetup => self.render_ai_setup_page(window, cx).into_any_element(), + SelectedPage::AiSetup => { + crate::ai_setup_page::render_ai_setup_page(&self, window, cx).into_any_element() + } } } - - fn render_ai_setup_page(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - div().child("ai setup page") - } } impl Render for Onboarding { @@ -418,7 +410,9 @@ impl Render for Onboarding { .gap_12() .child(self.render_nav(window, cx)) .child( - div() + v_flex() + .max_w_full() + .min_w_0() .pl_12() .border_l_1() .border_color(cx.theme().colors().border_variant.opacity(0.5)) @@ -458,11 +452,9 @@ impl Item for Onboarding { _: &mut Window, cx: &mut Context, ) -> Option> { - Some(Onboarding::new( - self.workspace.clone(), - self.user_store.clone(), - cx, - )) + self.workspace + .update(cx, |workspace, cx| Onboarding::new(workspace, cx)) + .ok() } fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 9c2961c55f..486673e733 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -1,4 +1,5 @@ mod avatar; +mod badge; mod banner; mod button; mod callout; @@ -41,6 +42,7 @@ mod tooltip; mod stories; pub use avatar::*; +pub use badge::*; pub use banner::*; pub use button::*; pub use callout::*; diff --git a/crates/ui/src/components/badge.rs b/crates/ui/src/components/badge.rs new file mode 100644 index 0000000000..9073c88500 --- /dev/null +++ b/crates/ui/src/components/badge.rs @@ -0,0 +1,71 @@ +use crate::Divider; +use crate::DividerColor; +use crate::component_prelude::*; +use crate::prelude::*; +use gpui::{AnyElement, IntoElement, SharedString, Window}; + +#[derive(IntoElement, RegisterComponent)] +pub struct Badge { + label: SharedString, + icon: IconName, +} + +impl Badge { + pub fn new(label: impl Into) -> Self { + Self { + label: label.into(), + icon: IconName::Check, + } + } + + pub fn icon(mut self, icon: IconName) -> Self { + self.icon = icon; + self + } +} + +impl RenderOnce for Badge { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + h_flex() + .h_full() + .gap_1() + .pl_1() + .pr_2() + .border_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().element_background) + .rounded_sm() + .overflow_hidden() + .child( + Icon::new(self.icon) + .size(IconSize::XSmall) + .color(Color::Muted), + ) + .child(Divider::vertical().color(DividerColor::Border)) + .child( + Label::new(self.label.clone()) + .size(LabelSize::XSmall) + .buffer_font(cx) + .ml_1(), + ) + } +} + +impl Component for Badge { + fn scope() -> ComponentScope { + ComponentScope::DataDisplay + } + + fn description() -> Option<&'static str> { + Some( + "A compact, labeled component with optional icon for displaying status, categories, or metadata.", + ) + } + + fn preview(_window: &mut Window, _cx: &mut App) -> Option { + Some( + single_example("Basic Badge", Badge::new("Default").into_any_element()) + .into_any_element(), + ) + } +} diff --git a/crates/ui/src/components/modal.rs b/crates/ui/src/components/modal.rs index 2145b34ef2..a70f5e1ea5 100644 --- a/crates/ui/src/components/modal.rs +++ b/crates/ui/src/components/modal.rs @@ -1,5 +1,5 @@ use crate::{ - Clickable, Color, DynamicSpacing, Headline, HeadlineSize, IconButton, IconButtonShape, + Clickable, Color, DynamicSpacing, Headline, HeadlineSize, Icon, IconButton, IconButtonShape, IconName, Label, LabelCommon, LabelSize, h_flex, v_flex, }; use gpui::{prelude::FluentBuilder, *}; @@ -92,6 +92,7 @@ impl RenderOnce for Modal { #[derive(IntoElement)] pub struct ModalHeader { + icon: Option, headline: Option, description: Option, children: SmallVec<[AnyElement; 2]>, @@ -108,6 +109,7 @@ impl Default for ModalHeader { impl ModalHeader { pub fn new() -> Self { Self { + icon: None, headline: None, description: None, children: SmallVec::new(), @@ -116,6 +118,11 @@ impl ModalHeader { } } + pub fn icon(mut self, icon: Icon) -> Self { + self.icon = Some(icon); + self + } + /// Set the headline of the modal. /// /// This will insert the headline as the first item @@ -179,12 +186,17 @@ impl RenderOnce for ModalHeader { ) }) .child( - v_flex().flex_1().children(children).when_some( - self.description, - |this, description| { + v_flex() + .flex_1() + .child( + h_flex() + .gap_1() + .when_some(self.icon, |this, icon| this.child(icon)) + .children(children), + ) + .when_some(self.description, |this, description| { this.child(Label::new(description).color(Color::Muted).mb_2()) - }, - ), + }), ) .when(self.show_dismiss_button, |this| { this.child( diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index daa8aa7fbe..0d8f5c4107 100644 --- a/crates/ui/src/components/toggle.rs +++ b/crates/ui/src/components/toggle.rs @@ -566,7 +566,7 @@ impl RenderOnce for Switch { pub struct SwitchField { id: ElementId, label: SharedString, - description: SharedString, + description: Option, toggle_state: ToggleState, on_click: Arc, disabled: bool, @@ -577,14 +577,14 @@ impl SwitchField { pub fn new( id: impl Into, label: impl Into, - description: impl Into, + description: Option, toggle_state: impl Into, on_click: impl Fn(&ToggleState, &mut Window, &mut App) + 'static, ) -> Self { Self { id: id.into(), label: label.into(), - description: description.into(), + description: description, toggle_state: toggle_state.into(), on_click: Arc::new(on_click), disabled: false, @@ -592,6 +592,11 @@ impl SwitchField { } } + pub fn description(mut self, description: impl Into) -> Self { + self.description = Some(description.into()); + self + } + pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self @@ -616,13 +621,15 @@ impl RenderOnce for SwitchField { .gap_4() .justify_between() .flex_wrap() - .child( - v_flex() + .child(match &self.description { + Some(description) => v_flex() .gap_0p5() .max_w_5_6() - .child(Label::new(self.label)) - .child(Label::new(self.description).color(Color::Muted)), - ) + .child(Label::new(self.label.clone())) + .child(Label::new(description.clone()).color(Color::Muted)) + .into_any_element(), + None => Label::new(self.label.clone()).into_any_element(), + }) .child( Switch::new( SharedString::from(format!("{}-switch", self.id)), @@ -671,7 +678,7 @@ impl Component for SwitchField { SwitchField::new( "switch_field_unselected", "Enable notifications", - "Receive notifications when new messages arrive.", + Some("Receive notifications when new messages arrive.".into()), ToggleState::Unselected, |_, _, _| {}, ) @@ -682,7 +689,7 @@ impl Component for SwitchField { SwitchField::new( "switch_field_selected", "Enable notifications", - "Receive notifications when new messages arrive.", + Some("Receive notifications when new messages arrive.".into()), ToggleState::Selected, |_, _, _| {}, ) @@ -698,7 +705,7 @@ impl Component for SwitchField { SwitchField::new( "switch_field_default", "Default color", - "This uses the default switch color.", + Some("This uses the default switch color.".into()), ToggleState::Selected, |_, _, _| {}, ) @@ -709,7 +716,7 @@ impl Component for SwitchField { SwitchField::new( "switch_field_accent", "Accent color", - "This uses the accent color scheme.", + Some("This uses the accent color scheme.".into()), ToggleState::Selected, |_, _, _| {}, ) @@ -725,7 +732,7 @@ impl Component for SwitchField { SwitchField::new( "switch_field_disabled", "Disabled field", - "This field is disabled and cannot be toggled.", + Some("This field is disabled and cannot be toggled.".into()), ToggleState::Selected, |_, _, _| {}, ) @@ -733,6 +740,20 @@ impl Component for SwitchField { .into_any_element(), )], ), + example_group_with_title( + "No Description", + vec![single_example( + "No Description", + SwitchField::new( + "switch_field_disabled", + "Disabled field", + None, + ToggleState::Selected, + |_, _, _| {}, + ) + .into_any_element(), + )], + ), ]) .into_any_element(), ) From f888f3fc0bb6b9e35dc31032f79a42578b4066a5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 1 Aug 2025 19:37:38 +0200 Subject: [PATCH 019/693] Start separating authentication from connection to collab (#35471) This pull request should be idempotent, but lays the groundwork for avoiding to connect to collab in order to interact with AI features provided by Zed. Release Notes: - N/A --------- Co-authored-by: Marshall Bowers Co-authored-by: Richard Feldman --- Cargo.lock | 24 +- crates/agent/Cargo.toml | 1 - crates/agent/src/thread.rs | 38 ++- crates/agent/src/thread_store.rs | 18 +- crates/agent_ui/src/active_thread.rs | 7 - crates/agent_ui/src/agent_configuration.rs | 6 +- crates/agent_ui/src/agent_diff.rs | 13 - crates/agent_ui/src/agent_panel.rs | 23 +- crates/agent_ui/src/message_editor.rs | 16 +- crates/ai_onboarding/Cargo.toml | 1 - .../src/agent_panel_onboarding_content.rs | 9 +- crates/ai_onboarding/src/ai_onboarding.rs | 37 +-- crates/ai_onboarding/src/ai_upsell_card.rs | 6 +- .../assistant_tools/src/edit_agent/evals.rs | 6 +- crates/channel/src/channel_store_tests.rs | 18 +- crates/client/Cargo.toml | 1 - crates/client/src/client.rs | 203 ++++++++++------ crates/client/src/cloud.rs | 3 - crates/client/src/cloud/user_store.rs | 211 ---------------- crates/client/src/test.rs | 94 ++++++- crates/client/src/user.rs | 230 +++++++++++------- crates/collab/src/tests/integration_tests.rs | 10 +- crates/collab/src/tests/notification_tests.rs | 4 + crates/collab/src/tests/test_server.rs | 59 ++++- crates/collab_ui/src/collab_panel.rs | 2 +- crates/collab_ui/src/notification_panel.rs | 11 +- crates/eval/src/eval.rs | 13 +- crates/eval/src/instance.rs | 1 - crates/http_client/Cargo.toml | 1 + crates/http_client/src/http_client.rs | 71 ++++-- .../src/inline_completion_button.rs | 18 +- .../language_model/src/model/cloud_model.rs | 5 +- crates/language_models/Cargo.toml | 1 - crates/language_models/src/language_models.rs | 25 +- crates/language_models/src/provider/cloud.rs | 63 ++--- crates/onboarding/src/ai_setup_page.rs | 2 +- crates/onboarding/src/onboarding.rs | 7 +- crates/project/src/project.rs | 5 +- crates/title_bar/src/title_bar.rs | 26 +- crates/workspace/src/workspace.rs | 14 +- crates/zed/src/main.rs | 48 +--- crates/zed/src/zed.rs | 9 +- crates/zed/src/zed/component_preview.rs | 3 +- .../preview_support/active_thread.rs | 23 +- .../zed/src/zed/inline_completion_registry.rs | 28 +-- crates/zeta/src/zeta.rs | 94 ++----- 46 files changed, 653 insertions(+), 855 deletions(-) delete mode 100644 crates/client/src/cloud.rs delete mode 100644 crates/client/src/cloud/user_store.rs diff --git a/Cargo.lock b/Cargo.lock index 63a66d7150..94ba0cf0ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,7 +114,6 @@ dependencies = [ "pretty_assertions", "project", "prompt_store", - "proto", "rand 0.8.5", "ref-cast", "rope", @@ -359,7 +358,6 @@ dependencies = [ "component", "gpui", "language_model", - "proto", "serde", "smallvec", "telemetry", @@ -1076,17 +1074,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "async-recursion" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "async-recursion" version = "1.1.1" @@ -2972,7 +2959,6 @@ name = "client" version = "0.1.0" dependencies = [ "anyhow", - "async-recursion 0.3.2", "async-tungstenite", "base64 0.22.1", "chrono", @@ -7814,6 +7800,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "log", + "parking_lot", "serde", "serde_json", "url", @@ -9085,7 +9072,6 @@ dependencies = [ "open_router", "partial-json-fixer", "project", - "proto", "release_channel", "schemars", "serde", @@ -9823,7 +9809,7 @@ name = "markdown_preview" version = "0.1.0" dependencies = [ "anyhow", - "async-recursion 1.1.1", + "async-recursion", "collections", "editor", "fs", @@ -16192,7 +16178,7 @@ version = "0.1.0" dependencies = [ "anyhow", "assistant_slash_command", - "async-recursion 1.1.1", + "async-recursion", "breadcrumbs", "client", "collections", @@ -19617,7 +19603,7 @@ version = "0.1.0" dependencies = [ "any_vec", "anyhow", - "async-recursion 1.1.1", + "async-recursion", "bincode", "call", "client", @@ -20142,7 +20128,7 @@ dependencies = [ "async-io", "async-lock", "async-process", - "async-recursion 1.1.1", + "async-recursion", "async-task", "async-trait", "blocking", diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index c89a7f3303..7bc0e82cad 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -47,7 +47,6 @@ paths.workspace = true postage.workspace = true project.workspace = true prompt_store.workspace = true -proto.workspace = true ref-cast.workspace = true rope.workspace = true schemars.workspace = true diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index ee16f83dc4..8558dd528d 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -12,8 +12,8 @@ use agent_settings::{AgentProfileId, AgentSettings, CompletionMode}; use anyhow::{Result, anyhow}; use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet}; use chrono::{DateTime, Utc}; -use client::{CloudUserStore, ModelRequestUsage, RequestUsage}; -use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit}; +use client::{ModelRequestUsage, RequestUsage}; +use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit}; use collections::HashMap; use feature_flags::{self, FeatureFlagAppExt}; use futures::{FutureExt, StreamExt as _, future::Shared}; @@ -37,7 +37,6 @@ use project::{ git_store::{GitStore, GitStoreCheckpoint, RepositoryState}, }; use prompt_store::{ModelContext, PromptBuilder}; -use proto::Plan; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; @@ -374,7 +373,6 @@ pub struct Thread { completion_count: usize, pending_completions: Vec, project: Entity, - cloud_user_store: Entity, prompt_builder: Arc, tools: Entity, tool_use: ToolUseState, @@ -445,7 +443,6 @@ pub struct ExceededWindowError { impl Thread { pub fn new( project: Entity, - cloud_user_store: Entity, tools: Entity, prompt_builder: Arc, system_prompt: SharedProjectContext, @@ -472,7 +469,6 @@ impl Thread { completion_count: 0, pending_completions: Vec::new(), project: project.clone(), - cloud_user_store, prompt_builder, tools: tools.clone(), last_restore_checkpoint: None, @@ -506,7 +502,6 @@ impl Thread { id: ThreadId, serialized: SerializedThread, project: Entity, - cloud_user_store: Entity, tools: Entity, prompt_builder: Arc, project_context: SharedProjectContext, @@ -607,7 +602,6 @@ impl Thread { last_restore_checkpoint: None, pending_checkpoint: None, project: project.clone(), - cloud_user_store, prompt_builder, tools: tools.clone(), tool_use, @@ -3260,15 +3254,18 @@ impl Thread { } fn update_model_request_usage(&self, amount: u32, limit: UsageLimit, cx: &mut Context) { - self.cloud_user_store.update(cx, |cloud_user_store, cx| { - cloud_user_store.update_model_request_usage( - ModelRequestUsage(RequestUsage { - amount: amount as i32, - limit, - }), - cx, - ) - }); + self.project + .read(cx) + .user_store() + .update(cx, |user_store, cx| { + user_store.update_model_request_usage( + ModelRequestUsage(RequestUsage { + amount: amount as i32, + limit, + }), + cx, + ) + }); } pub fn deny_tool_use( @@ -3886,7 +3883,6 @@ fn main() {{ thread.id.clone(), serialized, thread.project.clone(), - thread.cloud_user_store.clone(), thread.tools.clone(), thread.prompt_builder.clone(), thread.project_context.clone(), @@ -5483,16 +5479,10 @@ fn main() {{ let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let (client, user_store) = - project.read_with(cx, |project, _cx| (project.client(), project.user_store())); - let cloud_user_store = - cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store, cx)); - let thread_store = cx .update(|_, cx| { ThreadStore::load( project.clone(), - cloud_user_store, cx.new(|_| ToolWorkingSet::default()), None, Arc::new(PromptBuilder::new(None).unwrap()), diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index 6efa56f233..cc7cb50c91 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -8,7 +8,6 @@ use agent_settings::{AgentProfileId, CompletionMode}; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::{Tool, ToolId, ToolWorkingSet}; use chrono::{DateTime, Utc}; -use client::CloudUserStore; use collections::HashMap; use context_server::ContextServerId; use futures::{ @@ -105,7 +104,6 @@ pub type TextThreadStore = assistant_context::ContextStore; pub struct ThreadStore { project: Entity, - cloud_user_store: Entity, tools: Entity, prompt_builder: Arc, prompt_store: Option>, @@ -126,7 +124,6 @@ impl EventEmitter for ThreadStore {} impl ThreadStore { pub fn load( project: Entity, - cloud_user_store: Entity, tools: Entity, prompt_store: Option>, prompt_builder: Arc, @@ -136,14 +133,8 @@ impl ThreadStore { let (thread_store, ready_rx) = cx.update(|cx| { let mut option_ready_rx = None; let thread_store = cx.new(|cx| { - let (thread_store, ready_rx) = Self::new( - project, - cloud_user_store, - tools, - prompt_builder, - prompt_store, - cx, - ); + let (thread_store, ready_rx) = + Self::new(project, tools, prompt_builder, prompt_store, cx); option_ready_rx = Some(ready_rx); thread_store }); @@ -156,7 +147,6 @@ impl ThreadStore { fn new( project: Entity, - cloud_user_store: Entity, tools: Entity, prompt_builder: Arc, prompt_store: Option>, @@ -200,7 +190,6 @@ impl ThreadStore { let this = Self { project, - cloud_user_store, tools, prompt_builder, prompt_store, @@ -418,7 +407,6 @@ impl ThreadStore { cx.new(|cx| { Thread::new( self.project.clone(), - self.cloud_user_store.clone(), self.tools.clone(), self.prompt_builder.clone(), self.project_context.clone(), @@ -437,7 +425,6 @@ impl ThreadStore { ThreadId::new(), serialized, self.project.clone(), - self.cloud_user_store.clone(), self.tools.clone(), self.prompt_builder.clone(), self.project_context.clone(), @@ -469,7 +456,6 @@ impl ThreadStore { id.clone(), thread, this.project.clone(), - this.cloud_user_store.clone(), this.tools.clone(), this.prompt_builder.clone(), this.project_context.clone(), diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 1669c24a1b..04a093c7d0 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -3820,7 +3820,6 @@ mod tests { use super::*; use agent::{MessageSegment, context::ContextLoadResult, thread_store}; use assistant_tool::{ToolRegistry, ToolWorkingSet}; - use client::CloudUserStore; use editor::EditorSettings; use fs::FakeFs; use gpui::{AppContext, TestAppContext, VisualTestContext}; @@ -4117,16 +4116,10 @@ mod tests { let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let (client, user_store) = - project.read_with(cx, |project, _cx| (project.client(), project.user_store())); - let cloud_user_store = - cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store, cx)); - let thread_store = cx .update(|_, cx| { ThreadStore::load( project.clone(), - cloud_user_store, cx.new(|_| ToolWorkingSet::default()), None, Arc::new(PromptBuilder::new(None).unwrap()), diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index b88b85d85b..dad930be9e 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -7,6 +7,7 @@ use std::{sync::Arc, time::Duration}; use agent_settings::AgentSettings; use assistant_tool::{ToolSource, ToolWorkingSet}; +use cloud_llm_client::Plan; use collections::HashMap; use context_server::ContextServerId; use extension::ExtensionManifest; @@ -25,7 +26,6 @@ use project::{ context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore}, project_settings::{ContextServerSettings, ProjectSettings}, }; -use proto::Plan; use settings::{Settings, update_settings_file}; use ui::{ Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu, @@ -180,7 +180,7 @@ impl AgentConfiguration { let current_plan = if is_zed_provider { self.workspace .upgrade() - .and_then(|workspace| workspace.read(cx).user_store().read(cx).current_plan()) + .and_then(|workspace| workspace.read(cx).user_store().read(cx).plan()) } else { None }; @@ -508,7 +508,7 @@ impl AgentConfiguration { .blend(cx.theme().colors().text_accent.opacity(0.2)); let (plan_name, label_color, bg_color) = match plan { - Plan::Free => ("Free", Color::Default, free_chip_bg), + Plan::ZedFree => ("Free", Color::Default, free_chip_bg), Plan::ZedProTrial => ("Pro Trial", Color::Accent, pro_chip_bg), Plan::ZedPro => ("Pro", Color::Accent, pro_chip_bg), }; diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 135f07a934..c4dc359093 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1896,7 +1896,6 @@ mod tests { use agent::thread_store::{self, ThreadStore}; use agent_settings::AgentSettings; use assistant_tool::ToolWorkingSet; - use client::CloudUserStore; use editor::EditorSettings; use gpui::{TestAppContext, UpdateGlobal, VisualTestContext}; use project::{FakeFs, Project}; @@ -1936,17 +1935,11 @@ mod tests { }) .unwrap(); - let (client, user_store) = - project.read_with(cx, |project, _cx| (project.client(), project.user_store())); - let cloud_user_store = - cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store, cx)); - let prompt_store = None; let thread_store = cx .update(|cx| { ThreadStore::load( project.clone(), - cloud_user_store, cx.new(|_| ToolWorkingSet::default()), prompt_store, Arc::new(PromptBuilder::new(None).unwrap()), @@ -2108,17 +2101,11 @@ mod tests { }) .unwrap(); - let (client, user_store) = - project.read_with(cx, |project, _cx| (project.client(), project.user_store())); - let cloud_user_store = - cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store, cx)); - let prompt_store = None; let thread_store = cx .update(|cx| { ThreadStore::load( project.clone(), - cloud_user_store, cx.new(|_| ToolWorkingSet::default()), prompt_store, Arc::new(PromptBuilder::new(None).unwrap()), diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 7e0d766f91..fcb8dfbac2 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -43,8 +43,8 @@ use anyhow::{Result, anyhow}; use assistant_context::{AssistantContext, ContextEvent, ContextSummary}; use assistant_slash_command::SlashCommandWorkingSet; use assistant_tool::ToolWorkingSet; -use client::{CloudUserStore, DisableAiSettings, UserStore, zed_urls}; -use cloud_llm_client::{CompletionIntent, UsageLimit}; +use client::{DisableAiSettings, UserStore, zed_urls}; +use cloud_llm_client::{CompletionIntent, Plan, UsageLimit}; use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer}; use feature_flags::{self, FeatureFlagAppExt}; use fs::Fs; @@ -60,7 +60,6 @@ use language_model::{ }; use project::{Project, ProjectPath, Worktree}; use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; -use proto::Plan; use rules_library::{RulesLibrary, open_rules_library}; use search::{BufferSearchBar, buffer_search}; use settings::{Settings, update_settings_file}; @@ -427,7 +426,6 @@ impl ActiveView { pub struct AgentPanel { workspace: WeakEntity, user_store: Entity, - cloud_user_store: Entity, project: Entity, fs: Arc, language_registry: Arc, @@ -487,7 +485,6 @@ impl AgentPanel { let project = workspace.project().clone(); ThreadStore::load( project, - workspace.app_state().cloud_user_store.clone(), tools.clone(), prompt_store.clone(), prompt_builder.clone(), @@ -555,7 +552,6 @@ impl AgentPanel { let thread = thread_store.update(cx, |this, cx| this.create_thread(cx)); let fs = workspace.app_state().fs.clone(); let user_store = workspace.app_state().user_store.clone(); - let cloud_user_store = workspace.app_state().cloud_user_store.clone(); let project = workspace.project(); let language_registry = project.read(cx).languages().clone(); let client = workspace.client().clone(); @@ -582,7 +578,6 @@ impl AgentPanel { MessageEditor::new( fs.clone(), workspace.clone(), - cloud_user_store.clone(), message_editor_context_store.clone(), prompt_store.clone(), thread_store.downgrade(), @@ -697,7 +692,6 @@ impl AgentPanel { let onboarding = cx.new(|cx| { AgentPanelOnboarding::new( user_store.clone(), - cloud_user_store.clone(), client, |_window, cx| { OnboardingUpsell::set_dismissed(true, cx); @@ -710,7 +704,6 @@ impl AgentPanel { active_view, workspace, user_store, - cloud_user_store, project: project.clone(), fs: fs.clone(), language_registry, @@ -853,7 +846,6 @@ impl AgentPanel { MessageEditor::new( self.fs.clone(), self.workspace.clone(), - self.cloud_user_store.clone(), context_store.clone(), self.prompt_store.clone(), self.thread_store.downgrade(), @@ -1127,7 +1119,6 @@ impl AgentPanel { MessageEditor::new( self.fs.clone(), self.workspace.clone(), - self.cloud_user_store.clone(), context_store, self.prompt_store.clone(), self.thread_store.downgrade(), @@ -1826,8 +1817,8 @@ impl AgentPanel { } fn render_toolbar(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let cloud_user_store = self.cloud_user_store.read(cx); - let usage = cloud_user_store.model_request_usage(); + let user_store = self.user_store.read(cx); + let usage = user_store.model_request_usage(); let account_url = zed_urls::account_url(cx); @@ -2298,10 +2289,10 @@ impl AgentPanel { | ActiveView::Configuration => return false, } - let plan = self.user_store.read(cx).current_plan(); + let plan = self.user_store.read(cx).plan(); let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some(); - matches!(plan, Some(Plan::Free)) && has_previous_trial + matches!(plan, Some(Plan::ZedFree)) && has_previous_trial } fn should_render_onboarding(&self, cx: &mut Context) -> bool { @@ -2916,7 +2907,7 @@ impl AgentPanel { ) -> AnyElement { let error_message = match plan { Plan::ZedPro => "Upgrade to usage-based billing for more prompts.", - Plan::ZedProTrial | Plan::Free => "Upgrade to Zed Pro for more prompts.", + Plan::ZedProTrial | Plan::ZedFree => "Upgrade to Zed Pro for more prompts.", }; let icon = Icon::new(IconName::XCircle) diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index e00a0087eb..2185885347 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -17,7 +17,6 @@ use agent::{ use agent_settings::{AgentSettings, CompletionMode}; use ai_onboarding::ApiKeysWithProviders; use buffer_diff::BufferDiff; -use client::CloudUserStore; use cloud_llm_client::CompletionIntent; use collections::{HashMap, HashSet}; use editor::actions::{MoveUp, Paste}; @@ -78,7 +77,6 @@ pub struct MessageEditor { editor: Entity, workspace: WeakEntity, project: Entity, - cloud_user_store: Entity, context_store: Entity, prompt_store: Option>, history_store: Option>, @@ -158,7 +156,6 @@ impl MessageEditor { pub fn new( fs: Arc, workspace: WeakEntity, - cloud_user_store: Entity, context_store: Entity, prompt_store: Option>, thread_store: WeakEntity, @@ -230,7 +227,6 @@ impl MessageEditor { Self { editor: editor.clone(), project: thread.read(cx).project().clone(), - cloud_user_store, thread, incompatible_tools_state: incompatible_tools.clone(), workspace, @@ -1286,16 +1282,14 @@ impl MessageEditor { return None; } - let cloud_user_store = self.cloud_user_store.read(cx); - if cloud_user_store.is_usage_based_billing_enabled() { + let user_store = self.project.read(cx).user_store().read(cx); + if user_store.is_usage_based_billing_enabled() { return None; } - let plan = cloud_user_store - .plan() - .unwrap_or(cloud_llm_client::Plan::ZedFree); + let plan = user_store.plan().unwrap_or(cloud_llm_client::Plan::ZedFree); - let usage = cloud_user_store.model_request_usage()?; + let usage = user_store.model_request_usage()?; Some( div() @@ -1758,7 +1752,6 @@ impl AgentPreview for MessageEditor { ) -> Option { if let Some(workspace) = workspace.upgrade() { let fs = workspace.read(cx).app_state().fs.clone(); - let cloud_user_store = workspace.read(cx).app_state().cloud_user_store.clone(); let project = workspace.read(cx).project().clone(); let weak_project = project.downgrade(); let context_store = cx.new(|_cx| ContextStore::new(weak_project, None)); @@ -1771,7 +1764,6 @@ impl AgentPreview for MessageEditor { MessageEditor::new( fs, workspace.downgrade(), - cloud_user_store, context_store, None, thread_store.downgrade(), diff --git a/crates/ai_onboarding/Cargo.toml b/crates/ai_onboarding/Cargo.toml index 20fd54339e..95a45b1a6f 100644 --- a/crates/ai_onboarding/Cargo.toml +++ b/crates/ai_onboarding/Cargo.toml @@ -20,7 +20,6 @@ cloud_llm_client.workspace = true component.workspace = true gpui.workspace = true language_model.workspace = true -proto.workspace = true serde.workspace = true smallvec.workspace = true telemetry.workspace = true diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs index 237b0ae046..f1629eeff8 100644 --- a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs +++ b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use client::{Client, CloudUserStore, UserStore}; +use client::{Client, UserStore}; use cloud_llm_client::Plan; use gpui::{Entity, IntoElement, ParentElement}; use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID}; @@ -10,7 +10,6 @@ use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding}; pub struct AgentPanelOnboarding { user_store: Entity, - cloud_user_store: Entity, client: Arc, configured_providers: Vec<(IconName, SharedString)>, continue_with_zed_ai: Arc, @@ -19,7 +18,6 @@ pub struct AgentPanelOnboarding { impl AgentPanelOnboarding { pub fn new( user_store: Entity, - cloud_user_store: Entity, client: Arc, continue_with_zed_ai: impl Fn(&mut Window, &mut App) + 'static, cx: &mut Context, @@ -39,7 +37,6 @@ impl AgentPanelOnboarding { Self { user_store, - cloud_user_store, client, configured_providers: Self::compute_available_providers(cx), continue_with_zed_ai: Arc::new(continue_with_zed_ai), @@ -60,8 +57,8 @@ impl AgentPanelOnboarding { impl Render for AgentPanelOnboarding { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let enrolled_in_trial = self.cloud_user_store.read(cx).plan() == Some(Plan::ZedProTrial); - let is_pro_user = self.cloud_user_store.read(cx).plan() == Some(Plan::ZedPro); + let enrolled_in_trial = self.user_store.read(cx).plan() == Some(Plan::ZedProTrial); + let is_pro_user = self.user_store.read(cx).plan() == Some(Plan::ZedPro); AgentPanelOnboardingCard::new() .child( diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index 3aec9c62cd..c252b65f20 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -9,6 +9,7 @@ pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProvider pub use agent_panel_onboarding_card::AgentPanelOnboardingCard; pub use agent_panel_onboarding_content::AgentPanelOnboarding; pub use ai_upsell_card::AiUpsellCard; +use cloud_llm_client::Plan; pub use edit_prediction_onboarding_content::EditPredictionOnboarding; pub use young_account_banner::YoungAccountBanner; @@ -79,7 +80,7 @@ impl From for SignInStatus { pub struct ZedAiOnboarding { pub sign_in_status: SignInStatus, pub has_accepted_terms_of_service: bool, - pub plan: Option, + pub plan: Option, pub account_too_young: bool, pub continue_with_zed_ai: Arc, pub sign_in: Arc, @@ -99,8 +100,8 @@ impl ZedAiOnboarding { Self { sign_in_status: status.into(), - has_accepted_terms_of_service: store.current_user_has_accepted_terms().unwrap_or(false), - plan: store.current_plan(), + has_accepted_terms_of_service: store.has_accepted_terms_of_service(), + plan: store.plan(), account_too_young: store.account_too_young(), continue_with_zed_ai, accept_terms_of_service: Arc::new({ @@ -113,11 +114,9 @@ impl ZedAiOnboarding { sign_in: Arc::new(move |_window, cx| { cx.spawn({ let client = client.clone(); - async move |cx| { - client.authenticate_and_connect(true, cx).await; - } + async move |cx| client.sign_in_with_optional_connect(true, cx).await }) - .detach(); + .detach_and_log_err(cx); }), dismiss_onboarding: None, } @@ -411,9 +410,9 @@ impl RenderOnce for ZedAiOnboarding { if matches!(self.sign_in_status, SignInStatus::SignedIn) { if self.has_accepted_terms_of_service { match self.plan { - None | Some(proto::Plan::Free) => self.render_free_plan_state(cx), - Some(proto::Plan::ZedProTrial) => self.render_trial_state(cx), - Some(proto::Plan::ZedPro) => self.render_pro_plan_state(cx), + None | Some(Plan::ZedFree) => self.render_free_plan_state(cx), + Some(Plan::ZedProTrial) => self.render_trial_state(cx), + Some(Plan::ZedPro) => self.render_pro_plan_state(cx), } } else { self.render_accept_terms_of_service() @@ -433,7 +432,7 @@ impl Component for ZedAiOnboarding { fn onboarding( sign_in_status: SignInStatus, has_accepted_terms_of_service: bool, - plan: Option, + plan: Option, account_too_young: bool, ) -> AnyElement { ZedAiOnboarding { @@ -468,25 +467,15 @@ impl Component for ZedAiOnboarding { ), single_example( "Free Plan", - onboarding(SignInStatus::SignedIn, true, Some(proto::Plan::Free), false), + onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedFree), false), ), single_example( "Pro Trial", - onboarding( - SignInStatus::SignedIn, - true, - Some(proto::Plan::ZedProTrial), - false, - ), + onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedProTrial), false), ), single_example( "Pro Plan", - onboarding( - SignInStatus::SignedIn, - true, - Some(proto::Plan::ZedPro), - false, - ), + onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedPro), false), ), ]) .into_any_element(), diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index 56eaca2392..2408b6aa37 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -24,11 +24,9 @@ impl AiUpsellCard { sign_in: Arc::new(move |_window, cx| { cx.spawn({ let client = client.clone(); - async move |cx| { - client.authenticate_and_connect(true, cx).await; - } + async move |cx| client.sign_in_with_optional_connect(true, cx).await }) - .detach(); + .detach_and_log_err(cx); }), } } diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/assistant_tools/src/edit_agent/evals.rs index 13619da25c..eda7eee0e3 100644 --- a/crates/assistant_tools/src/edit_agent/evals.rs +++ b/crates/assistant_tools/src/edit_agent/evals.rs @@ -7,7 +7,7 @@ use crate::{ }; use Role::*; use assistant_tool::ToolRegistry; -use client::{Client, CloudUserStore, UserStore}; +use client::{Client, UserStore}; use collections::HashMap; use fs::FakeFs; use futures::{FutureExt, future::LocalBoxFuture}; @@ -1470,14 +1470,12 @@ impl EditAgentTest { client::init_settings(cx); let client = Client::production(cx); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - let cloud_user_store = - cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store.clone(), cx)); settings::init(cx); Project::init_settings(cx); language::init(cx); language_model::init(client.clone(), cx); - language_models::init(user_store.clone(), cloud_user_store, client.clone(), cx); + language_models::init(user_store.clone(), client.clone(), cx); crate::init(client.http_client(), cx); }); diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index f8f5de3c39..c92226eeeb 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -259,20 +259,6 @@ async fn test_channel_messages(cx: &mut TestAppContext) { assert_channels(&channel_store, &[(0, "the-channel".to_string())], cx); }); - let get_users = server.receive::().await.unwrap(); - assert_eq!(get_users.payload.user_ids, vec![5]); - server.respond( - get_users.receipt(), - proto::UsersResponse { - users: vec![proto::User { - id: 5, - github_login: "nathansobo".into(), - avatar_url: "http://avatar.com/nathansobo".into(), - name: None, - }], - }, - ); - // Join a channel and populate its existing messages. let channel = channel_store.update(cx, |store, cx| { let channel_id = store.ordered_channels().next().unwrap().1.id; @@ -334,7 +320,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) { .map(|message| (message.sender.github_login.clone(), message.body.clone())) .collect::>(), &[ - ("nathansobo".into(), "a".into()), + ("user-5".into(), "a".into()), ("maxbrunsfeld".into(), "b".into()) ] ); @@ -437,7 +423,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) { .map(|message| (message.sender.github_login.clone(), message.body.clone())) .collect::>(), &[ - ("nathansobo".into(), "y".into()), + ("user-5".into(), "y".into()), ("maxbrunsfeld".into(), "z".into()) ] ); diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 3ff03114ea..365625b445 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -17,7 +17,6 @@ test-support = ["clock/test-support", "collections/test-support", "gpui/test-sup [dependencies] anyhow.workspace = true -async-recursion = "0.3" async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manual-roots"] } base64.workspace = true chrono = { workspace = true, features = ["serde"] } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 230e1ce634..b9b20aa4f2 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,14 +1,12 @@ #[cfg(any(test, feature = "test-support"))] pub mod test; -mod cloud; mod proxy; pub mod telemetry; pub mod user; pub mod zed_urls; use anyhow::{Context as _, Result, anyhow}; -use async_recursion::async_recursion; use async_tungstenite::tungstenite::{ client::IntoClientRequest, error::Error as WebsocketError, @@ -52,7 +50,6 @@ use tokio::net::TcpStream; use url::Url; use util::{ConnectionResult, ResultExt}; -pub use cloud::*; pub use rpc::*; pub use telemetry_events::Event; pub use user::*; @@ -164,20 +161,8 @@ pub fn init(client: &Arc, cx: &mut App) { let client = client.clone(); move |_: &SignIn, cx| { if let Some(client) = client.upgrade() { - cx.spawn( - async move |cx| match client.authenticate_and_connect(true, &cx).await { - ConnectionResult::Timeout => { - log::error!("Initial authentication timed out"); - } - ConnectionResult::ConnectionReset => { - log::error!("Initial authentication connection reset"); - } - ConnectionResult::Result(r) => { - r.log_err(); - } - }, - ) - .detach(); + cx.spawn(async move |cx| client.sign_in_with_optional_connect(true, &cx).await) + .detach_and_log_err(cx); } } }); @@ -286,6 +271,8 @@ pub enum Status { SignedOut, UpgradeRequired, Authenticating, + Authenticated, + AuthenticationError, Connecting, ConnectionError, Connected { @@ -712,7 +699,7 @@ impl Client { let mut delay = INITIAL_RECONNECTION_DELAY; loop { - match client.authenticate_and_connect(true, &cx).await { + match client.connect(true, &cx).await { ConnectionResult::Timeout => { log::error!("client connect attempt timed out") } @@ -882,17 +869,122 @@ impl Client { .is_some() } - #[async_recursion(?Send)] - pub async fn authenticate_and_connect( + pub async fn sign_in( + self: &Arc, + try_provider: bool, + cx: &AsyncApp, + ) -> Result { + if self.status().borrow().is_signed_out() { + self.set_status(Status::Authenticating, cx); + } else { + self.set_status(Status::Reauthenticating, cx); + } + + let mut credentials = None; + + let old_credentials = self.state.read().credentials.clone(); + if let Some(old_credentials) = old_credentials { + self.cloud_client.set_credentials( + old_credentials.user_id as u32, + old_credentials.access_token.clone(), + ); + + // Fetch the authenticated user with the old credentials, to ensure they are still valid. + if self.cloud_client.get_authenticated_user().await.is_ok() { + credentials = Some(old_credentials); + } + } + + if credentials.is_none() && try_provider { + if let Some(stored_credentials) = self.credentials_provider.read_credentials(cx).await { + self.cloud_client.set_credentials( + stored_credentials.user_id as u32, + stored_credentials.access_token.clone(), + ); + + // Fetch the authenticated user with the stored credentials, and + // clear them from the credentials provider if that fails. + if self.cloud_client.get_authenticated_user().await.is_ok() { + credentials = Some(stored_credentials); + } else { + self.credentials_provider + .delete_credentials(cx) + .await + .log_err(); + } + } + } + + if credentials.is_none() { + let mut status_rx = self.status(); + let _ = status_rx.next().await; + futures::select_biased! { + authenticate = self.authenticate(cx).fuse() => { + match authenticate { + Ok(creds) => { + if IMPERSONATE_LOGIN.is_none() { + self.credentials_provider + .write_credentials(creds.user_id, creds.access_token.clone(), cx) + .await + .log_err(); + } + + credentials = Some(creds); + }, + Err(err) => { + self.set_status(Status::AuthenticationError, cx); + return Err(err); + } + } + } + _ = status_rx.next().fuse() => { + return Err(anyhow!("authentication canceled")); + } + } + } + + let credentials = credentials.unwrap(); + self.set_id(credentials.user_id); + self.cloud_client + .set_credentials(credentials.user_id as u32, credentials.access_token.clone()); + self.state.write().credentials = Some(credentials.clone()); + self.set_status(Status::Authenticated, cx); + + Ok(credentials) + } + + /// Performs a sign-in and also connects to Collab. + /// + /// This is called in places where we *don't* need to connect in the future. We will replace these calls with calls + /// to `sign_in` when we're ready to remove auto-connection to Collab. + pub async fn sign_in_with_optional_connect( + self: &Arc, + try_provider: bool, + cx: &AsyncApp, + ) -> Result<()> { + let credentials = self.sign_in(try_provider, cx).await?; + + let connect_result = match self.connect_with_credentials(credentials, cx).await { + ConnectionResult::Timeout => Err(anyhow!("connection timed out")), + ConnectionResult::ConnectionReset => Err(anyhow!("connection reset")), + ConnectionResult::Result(result) => result.context("client auth and connect"), + }; + connect_result.log_err(); + + Ok(()) + } + + pub async fn connect( self: &Arc, try_provider: bool, cx: &AsyncApp, ) -> ConnectionResult<()> { let was_disconnected = match *self.status().borrow() { - Status::SignedOut => true, + Status::SignedOut | Status::Authenticated => true, Status::ConnectionError | Status::ConnectionLost | Status::Authenticating { .. } + | Status::AuthenticationError | Status::Reauthenticating { .. } | Status::ReconnectionError { .. } => false, Status::Connected { .. } | Status::Connecting { .. } | Status::Reconnecting { .. } => { @@ -905,41 +997,10 @@ impl Client { ); } }; - if was_disconnected { - self.set_status(Status::Authenticating, cx); - } else { - self.set_status(Status::Reauthenticating, cx) - } - - let mut read_from_provider = false; - let mut credentials = self.state.read().credentials.clone(); - if credentials.is_none() && try_provider { - credentials = self.credentials_provider.read_credentials(cx).await; - read_from_provider = credentials.is_some(); - } - - if credentials.is_none() { - let mut status_rx = self.status(); - let _ = status_rx.next().await; - futures::select_biased! { - authenticate = self.authenticate(cx).fuse() => { - match authenticate { - Ok(creds) => credentials = Some(creds), - Err(err) => { - self.set_status(Status::ConnectionError, cx); - return ConnectionResult::Result(Err(err)); - } - } - } - _ = status_rx.next().fuse() => { - return ConnectionResult::Result(Err(anyhow!("authentication canceled"))); - } - } - } - let credentials = credentials.unwrap(); - self.set_id(credentials.user_id); - self.cloud_client - .set_credentials(credentials.user_id as u32, credentials.access_token.clone()); + let credentials = match self.sign_in(try_provider, cx).await { + Ok(credentials) => credentials, + Err(err) => return ConnectionResult::Result(Err(err)), + }; if was_disconnected { self.set_status(Status::Connecting, cx); @@ -947,17 +1008,20 @@ impl Client { self.set_status(Status::Reconnecting, cx); } + self.connect_with_credentials(credentials, cx).await + } + + async fn connect_with_credentials( + self: &Arc, + credentials: Credentials, + cx: &AsyncApp, + ) -> ConnectionResult<()> { let mut timeout = futures::FutureExt::fuse(cx.background_executor().timer(CONNECTION_TIMEOUT)); futures::select_biased! { connection = self.establish_connection(&credentials, cx).fuse() => { match connection { Ok(conn) => { - self.state.write().credentials = Some(credentials.clone()); - if !read_from_provider && IMPERSONATE_LOGIN.is_none() { - self.credentials_provider.write_credentials(credentials.user_id, credentials.access_token, cx).await.log_err(); - } - futures::select_biased! { result = self.set_connection(conn, cx).fuse() => { match result.context("client auth and connect") { @@ -975,15 +1039,8 @@ impl Client { } } Err(EstablishConnectionError::Unauthorized) => { - self.state.write().credentials.take(); - if read_from_provider { - self.credentials_provider.delete_credentials(cx).await.log_err(); - self.set_status(Status::SignedOut, cx); - self.authenticate_and_connect(false, cx).await - } else { - self.set_status(Status::ConnectionError, cx); - ConnectionResult::Result(Err(EstablishConnectionError::Unauthorized).context("client auth and connect")) - } + self.set_status(Status::ConnectionError, cx); + ConnectionResult::Result(Err(EstablishConnectionError::Unauthorized).context("client auth and connect")) } Err(EstablishConnectionError::UpgradeRequired) => { self.set_status(Status::UpgradeRequired, cx); @@ -1733,7 +1790,7 @@ mod tests { }); let auth_and_connect = cx.spawn({ let client = client.clone(); - |cx| async move { client.authenticate_and_connect(false, &cx).await } + |cx| async move { client.connect(false, &cx).await } }); executor.run_until_parked(); assert!(matches!(status.next().await, Some(Status::Connecting))); @@ -1810,7 +1867,7 @@ mod tests { let _authenticate = cx.spawn({ let client = client.clone(); - move |cx| async move { client.authenticate_and_connect(false, &cx).await } + move |cx| async move { client.connect(false, &cx).await } }); executor.run_until_parked(); assert_eq!(*auth_count.lock(), 1); @@ -1818,7 +1875,7 @@ mod tests { let _authenticate = cx.spawn({ let client = client.clone(); - |cx| async move { client.authenticate_and_connect(false, &cx).await } + |cx| async move { client.connect(false, &cx).await } }); executor.run_until_parked(); assert_eq!(*auth_count.lock(), 2); diff --git a/crates/client/src/cloud.rs b/crates/client/src/cloud.rs deleted file mode 100644 index 39c9d04887..0000000000 --- a/crates/client/src/cloud.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod user_store; - -pub use user_store::*; diff --git a/crates/client/src/cloud/user_store.rs b/crates/client/src/cloud/user_store.rs deleted file mode 100644 index 78444b3f95..0000000000 --- a/crates/client/src/cloud/user_store.rs +++ /dev/null @@ -1,211 +0,0 @@ -use std::sync::Arc; -use std::time::Duration; - -use anyhow::Context as _; -use chrono::{DateTime, Utc}; -use cloud_api_client::{AuthenticatedUser, CloudApiClient, GetAuthenticatedUserResponse, PlanInfo}; -use cloud_llm_client::Plan; -use gpui::{Context, Entity, Subscription, Task}; -use util::{ResultExt as _, maybe}; - -use crate::user::Event as RpcUserStoreEvent; -use crate::{EditPredictionUsage, ModelRequestUsage, RequestUsage, UserStore}; - -pub struct CloudUserStore { - cloud_client: Arc, - authenticated_user: Option>, - plan_info: Option>, - model_request_usage: Option, - edit_prediction_usage: Option, - _maintain_authenticated_user_task: Task<()>, - _rpc_plan_updated_subscription: Subscription, -} - -impl CloudUserStore { - pub fn new( - cloud_client: Arc, - rpc_user_store: Entity, - cx: &mut Context, - ) -> Self { - let rpc_plan_updated_subscription = - cx.subscribe(&rpc_user_store, Self::handle_rpc_user_store_event); - - Self { - cloud_client: cloud_client.clone(), - authenticated_user: None, - plan_info: None, - model_request_usage: None, - edit_prediction_usage: None, - _maintain_authenticated_user_task: cx.spawn(async move |this, cx| { - maybe!(async move { - loop { - let Some(this) = this.upgrade() else { - return anyhow::Ok(()); - }; - - if cloud_client.has_credentials() { - let already_fetched_authenticated_user = this - .read_with(cx, |this, _cx| this.authenticated_user().is_some()) - .unwrap_or(false); - - if already_fetched_authenticated_user { - // We already fetched the authenticated user; nothing to do. - } else { - let authenticated_user_result = cloud_client - .get_authenticated_user() - .await - .context("failed to fetch authenticated user"); - if let Some(response) = authenticated_user_result.log_err() { - this.update(cx, |this, _cx| { - this.update_authenticated_user(response); - }) - .ok(); - } - } - } else { - this.update(cx, |this, _cx| { - this.authenticated_user.take(); - this.plan_info.take(); - }) - .ok(); - } - - cx.background_executor() - .timer(Duration::from_millis(100)) - .await; - } - }) - .await - .log_err(); - }), - _rpc_plan_updated_subscription: rpc_plan_updated_subscription, - } - } - - pub fn is_authenticated(&self) -> bool { - self.authenticated_user.is_some() - } - - pub fn authenticated_user(&self) -> Option> { - self.authenticated_user.clone() - } - - pub fn plan(&self) -> Option { - self.plan_info.as_ref().map(|plan| plan.plan) - } - - pub fn subscription_period(&self) -> Option<(DateTime, DateTime)> { - self.plan_info - .as_ref() - .and_then(|plan| plan.subscription_period) - .map(|subscription_period| { - ( - subscription_period.started_at.0, - subscription_period.ended_at.0, - ) - }) - } - - pub fn trial_started_at(&self) -> Option> { - self.plan_info - .as_ref() - .and_then(|plan| plan.trial_started_at) - .map(|trial_started_at| trial_started_at.0) - } - - pub fn has_accepted_tos(&self) -> bool { - self.authenticated_user - .as_ref() - .map(|user| user.accepted_tos_at.is_some()) - .unwrap_or_default() - } - - /// Returns whether the user's account is too new to use the service. - pub fn account_too_young(&self) -> bool { - self.plan_info - .as_ref() - .map(|plan| plan.is_account_too_young) - .unwrap_or_default() - } - - /// Returns whether the current user has overdue invoices and usage should be blocked. - pub fn has_overdue_invoices(&self) -> bool { - self.plan_info - .as_ref() - .map(|plan| plan.has_overdue_invoices) - .unwrap_or_default() - } - - pub fn is_usage_based_billing_enabled(&self) -> bool { - self.plan_info - .as_ref() - .map(|plan| plan.is_usage_based_billing_enabled) - .unwrap_or_default() - } - - pub fn model_request_usage(&self) -> Option { - self.model_request_usage - } - - pub fn update_model_request_usage(&mut self, usage: ModelRequestUsage, cx: &mut Context) { - self.model_request_usage = Some(usage); - cx.notify(); - } - - pub fn edit_prediction_usage(&self) -> Option { - self.edit_prediction_usage - } - - pub fn update_edit_prediction_usage( - &mut self, - usage: EditPredictionUsage, - cx: &mut Context, - ) { - self.edit_prediction_usage = Some(usage); - cx.notify(); - } - - fn update_authenticated_user(&mut self, response: GetAuthenticatedUserResponse) { - self.authenticated_user = Some(Arc::new(response.user)); - self.model_request_usage = Some(ModelRequestUsage(RequestUsage { - limit: response.plan.usage.model_requests.limit, - amount: response.plan.usage.model_requests.used as i32, - })); - self.edit_prediction_usage = Some(EditPredictionUsage(RequestUsage { - limit: response.plan.usage.edit_predictions.limit, - amount: response.plan.usage.edit_predictions.used as i32, - })); - self.plan_info = Some(Arc::new(response.plan)); - } - - fn handle_rpc_user_store_event( - &mut self, - _: Entity, - event: &RpcUserStoreEvent, - cx: &mut Context, - ) { - match event { - RpcUserStoreEvent::PlanUpdated => { - cx.spawn(async move |this, cx| { - let cloud_client = - cx.update(|cx| this.read_with(cx, |this, _cx| this.cloud_client.clone()))??; - - let response = cloud_client - .get_authenticated_user() - .await - .context("failed to fetch authenticated user")?; - - cx.update(|cx| { - this.update(cx, |this, _cx| { - this.update_authenticated_user(response); - }) - })??; - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - _ => {} - } - } -} diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index 6ce79fa9c5..439fb100d2 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -1,8 +1,11 @@ use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore}; use anyhow::{Context as _, Result, anyhow}; use chrono::Duration; +use cloud_api_client::{AuthenticatedUser, GetAuthenticatedUserResponse, PlanInfo}; +use cloud_llm_client::{CurrentUsage, Plan, UsageData, UsageLimit}; use futures::{StreamExt, stream::BoxStream}; use gpui::{AppContext as _, BackgroundExecutor, Entity, TestAppContext}; +use http_client::{AsyncBody, Method, Request, http}; use parking_lot::Mutex; use rpc::{ ConnectionId, Peer, Receipt, TypedEnvelope, @@ -39,6 +42,44 @@ impl FakeServer { executor: cx.executor(), }; + client.http_client().as_fake().replace_handler({ + let state = server.state.clone(); + move |old_handler, req| { + let state = state.clone(); + let old_handler = old_handler.clone(); + async move { + match (req.method(), req.uri().path()) { + (&Method::GET, "/client/users/me") => { + let credentials = parse_authorization_header(&req); + if credentials + != Some(Credentials { + user_id: client_user_id, + access_token: state.lock().access_token.to_string(), + }) + { + return Ok(http_client::Response::builder() + .status(401) + .body("Unauthorized".into()) + .unwrap()); + } + + Ok(http_client::Response::builder() + .status(200) + .body( + serde_json::to_string(&make_get_authenticated_user_response( + client_user_id as i32, + format!("user-{client_user_id}"), + )) + .unwrap() + .into(), + ) + .unwrap()) + } + _ => old_handler(req).await, + } + } + } + }); client .override_authenticate({ let state = Arc::downgrade(&server.state); @@ -105,7 +146,7 @@ impl FakeServer { }); client - .authenticate_and_connect(false, &cx.to_async()) + .connect(false, &cx.to_async()) .await .into_response() .unwrap(); @@ -223,3 +264,54 @@ impl Drop for FakeServer { self.disconnect(); } } + +pub fn parse_authorization_header(req: &Request) -> Option { + let mut auth_header = req + .headers() + .get(http::header::AUTHORIZATION)? + .to_str() + .ok()? + .split_whitespace(); + let user_id = auth_header.next()?.parse().ok()?; + let access_token = auth_header.next()?; + Some(Credentials { + user_id, + access_token: access_token.to_string(), + }) +} + +pub fn make_get_authenticated_user_response( + user_id: i32, + github_login: String, +) -> GetAuthenticatedUserResponse { + GetAuthenticatedUserResponse { + user: AuthenticatedUser { + id: user_id, + metrics_id: format!("metrics-id-{user_id}"), + avatar_url: "".to_string(), + github_login, + name: None, + is_staff: false, + accepted_tos_at: None, + }, + feature_flags: vec![], + plan: PlanInfo { + plan: Plan::ZedPro, + subscription_period: None, + usage: CurrentUsage { + model_requests: UsageData { + used: 0, + limit: UsageLimit::Limited(500), + }, + edit_predictions: UsageData { + used: 250, + limit: UsageLimit::Unlimited, + }, + }, + trial_started_at: None, + is_usage_based_billing_enabled: false, + is_account_too_young: false, + has_overdue_invoices: false, + }, + } +} diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index df5ce67be3..3c125a0882 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,6 +1,7 @@ use super::{Client, Status, TypedEnvelope, proto}; use anyhow::{Context as _, Result, anyhow}; use chrono::{DateTime, Utc}; +use cloud_api_client::{GetAuthenticatedUserResponse, PlanInfo}; use cloud_llm_client::{ EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME, EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME, MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME, MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME, UsageLimit, @@ -20,7 +21,7 @@ use std::{ sync::{Arc, Weak}, }; use text::ReplicaId; -use util::TryFutureExt as _; +use util::{ResultExt, TryFutureExt as _}; pub type UserId = u64; @@ -110,12 +111,11 @@ pub struct UserStore { by_github_login: HashMap, participant_indices: HashMap, update_contacts_tx: mpsc::UnboundedSender, - current_plan: Option, - trial_started_at: Option>, - is_usage_based_billing_enabled: Option, - account_too_young: Option, + model_request_usage: Option, + edit_prediction_usage: Option, + plan_info: Option, current_user: watch::Receiver>>, - accepted_tos_at: Option>>, + accepted_tos_at: Option>, contacts: Vec>, incoming_contact_requests: Vec>, outgoing_contact_requests: Vec>, @@ -185,10 +185,9 @@ impl UserStore { users: Default::default(), by_github_login: Default::default(), current_user: current_user_rx, - current_plan: None, - trial_started_at: None, - is_usage_based_billing_enabled: None, - account_too_young: None, + plan_info: None, + model_request_usage: None, + edit_prediction_usage: None, accepted_tos_at: None, contacts: Default::default(), incoming_contact_requests: Default::default(), @@ -218,53 +217,30 @@ impl UserStore { return Ok(()); }; match status { - Status::Connected { .. } => { + Status::Authenticated | Status::Connected { .. } => { if let Some(user_id) = client.user_id() { - let fetch_user = if let Ok(fetch_user) = - this.update(cx, |this, cx| this.get_user(user_id, cx).log_err()) - { - fetch_user - } else { - break; - }; - let fetch_private_user_info = - client.request(proto::GetPrivateUserInfo {}).log_err(); - let (user, info) = - futures::join!(fetch_user, fetch_private_user_info); - + let response = client.cloud_client().get_authenticated_user().await; + let mut current_user = None; cx.update(|cx| { - if let Some(info) = info { - let staff = - info.staff && !*feature_flags::ZED_DISABLE_STAFF; - cx.update_flags(staff, info.flags); - client.telemetry.set_authenticated_user_info( - Some(info.metrics_id.clone()), - staff, - ); - + if let Some(response) = response.log_err() { + let user = Arc::new(User { + id: user_id, + github_login: response.user.github_login.clone().into(), + avatar_uri: response.user.avatar_url.clone().into(), + name: response.user.name.clone(), + }); + current_user = Some(user.clone()); this.update(cx, |this, cx| { - let accepted_tos_at = { - #[cfg(debug_assertions)] - if std::env::var("ZED_IGNORE_ACCEPTED_TOS").is_ok() - { - None - } else { - info.accepted_tos_at - } - - #[cfg(not(debug_assertions))] - info.accepted_tos_at - }; - - this.set_current_user_accepted_tos_at(accepted_tos_at); - cx.emit(Event::PrivateUserInfoUpdated); + this.by_github_login + .insert(user.github_login.clone(), user_id); + this.users.insert(user_id, user); + this.update_authenticated_user(response, cx) }) } else { anyhow::Ok(()) } })??; - - current_user_tx.send(user).await.ok(); + current_user_tx.send(current_user).await.ok(); this.update(cx, |_, cx| cx.notify())?; } @@ -345,22 +321,22 @@ impl UserStore { async fn handle_update_plan( this: Entity, - message: TypedEnvelope, + _message: TypedEnvelope, mut cx: AsyncApp, ) -> Result<()> { - this.update(&mut cx, |this, cx| { - this.current_plan = Some(message.payload.plan()); - this.trial_started_at = message - .payload - .trial_started_at - .and_then(|trial_started_at| DateTime::from_timestamp(trial_started_at as i64, 0)); - this.is_usage_based_billing_enabled = message.payload.is_usage_based_billing_enabled; - this.account_too_young = message.payload.account_too_young; + let client = this + .read_with(&cx, |this, _| this.client.upgrade())? + .context("client was dropped")?; - cx.emit(Event::PlanUpdated); - cx.notify(); - })?; - Ok(()) + let response = client + .cloud_client() + .get_authenticated_user() + .await + .context("failed to fetch authenticated user")?; + + this.update(&mut cx, |this, cx| { + this.update_authenticated_user(response, cx); + }) } fn update_contacts(&mut self, message: UpdateContacts, cx: &Context) -> Task> { @@ -719,42 +695,131 @@ impl UserStore { self.current_user.borrow().clone() } - pub fn current_plan(&self) -> Option { + pub fn plan(&self) -> Option { #[cfg(debug_assertions)] if let Ok(plan) = std::env::var("ZED_SIMULATE_PLAN").as_ref() { return match plan.as_str() { - "free" => Some(proto::Plan::Free), - "trial" => Some(proto::Plan::ZedProTrial), - "pro" => Some(proto::Plan::ZedPro), + "free" => Some(cloud_llm_client::Plan::ZedFree), + "trial" => Some(cloud_llm_client::Plan::ZedProTrial), + "pro" => Some(cloud_llm_client::Plan::ZedPro), _ => { panic!("ZED_SIMULATE_PLAN must be one of 'free', 'trial', or 'pro'"); } }; } - self.current_plan + self.plan_info.as_ref().map(|info| info.plan) + } + + pub fn subscription_period(&self) -> Option<(DateTime, DateTime)> { + self.plan_info + .as_ref() + .and_then(|plan| plan.subscription_period) + .map(|subscription_period| { + ( + subscription_period.started_at.0, + subscription_period.ended_at.0, + ) + }) } pub fn trial_started_at(&self) -> Option> { - self.trial_started_at + self.plan_info + .as_ref() + .and_then(|plan| plan.trial_started_at) + .map(|trial_started_at| trial_started_at.0) } - pub fn usage_based_billing_enabled(&self) -> Option { - self.is_usage_based_billing_enabled + /// Returns whether the user's account is too new to use the service. + pub fn account_too_young(&self) -> bool { + self.plan_info + .as_ref() + .map(|plan| plan.is_account_too_young) + .unwrap_or_default() + } + + /// Returns whether the current user has overdue invoices and usage should be blocked. + pub fn has_overdue_invoices(&self) -> bool { + self.plan_info + .as_ref() + .map(|plan| plan.has_overdue_invoices) + .unwrap_or_default() + } + + pub fn is_usage_based_billing_enabled(&self) -> bool { + self.plan_info + .as_ref() + .map(|plan| plan.is_usage_based_billing_enabled) + .unwrap_or_default() + } + + pub fn model_request_usage(&self) -> Option { + self.model_request_usage + } + + pub fn update_model_request_usage(&mut self, usage: ModelRequestUsage, cx: &mut Context) { + self.model_request_usage = Some(usage); + cx.notify(); + } + + pub fn edit_prediction_usage(&self) -> Option { + self.edit_prediction_usage + } + + pub fn update_edit_prediction_usage( + &mut self, + usage: EditPredictionUsage, + cx: &mut Context, + ) { + self.edit_prediction_usage = Some(usage); + cx.notify(); + } + + fn update_authenticated_user( + &mut self, + response: GetAuthenticatedUserResponse, + cx: &mut Context, + ) { + let staff = response.user.is_staff && !*feature_flags::ZED_DISABLE_STAFF; + cx.update_flags(staff, response.feature_flags); + if let Some(client) = self.client.upgrade() { + client + .telemetry + .set_authenticated_user_info(Some(response.user.metrics_id.clone()), staff); + } + + let accepted_tos_at = { + #[cfg(debug_assertions)] + if std::env::var("ZED_IGNORE_ACCEPTED_TOS").is_ok() { + None + } else { + response.user.accepted_tos_at + } + + #[cfg(not(debug_assertions))] + response.user.accepted_tos_at + }; + + self.accepted_tos_at = Some(accepted_tos_at); + self.model_request_usage = Some(ModelRequestUsage(RequestUsage { + limit: response.plan.usage.model_requests.limit, + amount: response.plan.usage.model_requests.used as i32, + })); + self.edit_prediction_usage = Some(EditPredictionUsage(RequestUsage { + limit: response.plan.usage.edit_predictions.limit, + amount: response.plan.usage.edit_predictions.used as i32, + })); + self.plan_info = Some(response.plan); + cx.emit(Event::PrivateUserInfoUpdated); } pub fn watch_current_user(&self) -> watch::Receiver>> { self.current_user.clone() } - /// Returns whether the user's account is too new to use the service. - pub fn account_too_young(&self) -> bool { - self.account_too_young.unwrap_or(false) - } - - pub fn current_user_has_accepted_terms(&self) -> Option { + pub fn has_accepted_terms_of_service(&self) -> bool { self.accepted_tos_at - .map(|accepted_tos_at| accepted_tos_at.is_some()) + .map_or(false, |accepted_tos_at| accepted_tos_at.is_some()) } pub fn accept_terms_of_service(&self, cx: &Context) -> Task> { @@ -766,23 +831,18 @@ impl UserStore { cx.spawn(async move |this, cx| -> anyhow::Result<()> { let client = client.upgrade().context("client not found")?; let response = client - .request(proto::AcceptTermsOfService {}) + .cloud_client() + .accept_terms_of_service() .await .context("error accepting tos")?; this.update(cx, |this, cx| { - this.set_current_user_accepted_tos_at(Some(response.accepted_tos_at)); + this.accepted_tos_at = Some(response.user.accepted_tos_at); cx.emit(Event::PrivateUserInfoUpdated); })?; Ok(()) }) } - fn set_current_user_accepted_tos_at(&mut self, accepted_tos_at: Option) { - self.accepted_tos_at = Some( - accepted_tos_at.and_then(|timestamp| DateTime::from_timestamp(timestamp as i64, 0)), - ); - } - fn load_users( &self, request: impl RequestMessage, diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 7aa41e0e7d..aea359d75b 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -1286,7 +1286,7 @@ async fn test_calls_on_multiple_connections( client_b1.disconnect(&cx_b1.to_async()); executor.advance_clock(RECEIVE_TIMEOUT); client_b1 - .authenticate_and_connect(false, &cx_b1.to_async()) + .connect(false, &cx_b1.to_async()) .await .into_response() .unwrap(); @@ -1667,7 +1667,7 @@ async fn test_project_reconnect( // Client A reconnects. Their project is re-shared, and client B re-joins it. server.allow_connections(); client_a - .authenticate_and_connect(false, &cx_a.to_async()) + .connect(false, &cx_a.to_async()) .await .into_response() .unwrap(); @@ -1796,7 +1796,7 @@ async fn test_project_reconnect( // Client B reconnects. They re-join the room and the remaining shared project. server.allow_connections(); client_b - .authenticate_and_connect(false, &cx_b.to_async()) + .connect(false, &cx_b.to_async()) .await .into_response() .unwrap(); @@ -5738,7 +5738,7 @@ async fn test_contacts( server.allow_connections(); client_c - .authenticate_and_connect(false, &cx_c.to_async()) + .connect(false, &cx_c.to_async()) .await .into_response() .unwrap(); @@ -6269,7 +6269,7 @@ async fn test_contact_requests( client.disconnect(&cx.to_async()); client.clear_contacts(cx).await; client - .authenticate_and_connect(false, &cx.to_async()) + .connect(false, &cx.to_async()) .await .into_response() .unwrap(); diff --git a/crates/collab/src/tests/notification_tests.rs b/crates/collab/src/tests/notification_tests.rs index 4e64b5526b..9bf906694e 100644 --- a/crates/collab/src/tests/notification_tests.rs +++ b/crates/collab/src/tests/notification_tests.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use gpui::{BackgroundExecutor, TestAppContext}; use notifications::NotificationEvent; use parking_lot::Mutex; +use pretty_assertions::assert_eq; use rpc::{Notification, proto}; use crate::tests::TestServer; @@ -17,6 +18,9 @@ async fn test_notifications( let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; + // Wait for authentication/connection to Collab to be established. + executor.run_until_parked(); + let notification_events_a = Arc::new(Mutex::new(Vec::new())); let notification_events_b = Arc::new(Mutex::new(Vec::new())); client_a.notification_store().update(cx_a, |_, cx| { diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 3751d6918e..5fcc622fc1 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -8,7 +8,7 @@ use crate::{ use anyhow::anyhow; use call::ActiveCall; use channel::{ChannelBuffer, ChannelStore}; -use client::CloudUserStore; +use client::test::{make_get_authenticated_user_response, parse_authorization_header}; use client::{ self, ChannelId, Client, Connection, Credentials, EstablishConnectionError, UserStore, proto::PeerId, @@ -21,7 +21,7 @@ use fs::FakeFs; use futures::{StreamExt as _, channel::oneshot}; use git::GitHostingProviderRegistry; use gpui::{AppContext as _, BackgroundExecutor, Entity, Task, TestAppContext, VisualTestContext}; -use http_client::FakeHttpClient; +use http_client::{FakeHttpClient, Method}; use language::LanguageRegistry; use node_runtime::NodeRuntime; use notifications::NotificationStore; @@ -162,6 +162,8 @@ impl TestServer { } pub async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient { + const ACCESS_TOKEN: &str = "the-token"; + let fs = FakeFs::new(cx.executor()); cx.update(|cx| { @@ -176,7 +178,7 @@ impl TestServer { }); let clock = Arc::new(FakeSystemClock::new()); - let http = FakeHttpClient::with_404_response(); + let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await { user.id @@ -198,6 +200,47 @@ impl TestServer { .expect("creating user failed") .user_id }; + + let http = FakeHttpClient::create({ + let name = name.to_string(); + move |req| { + let name = name.clone(); + async move { + match (req.method(), req.uri().path()) { + (&Method::GET, "/client/users/me") => { + let credentials = parse_authorization_header(&req); + if credentials + != Some(Credentials { + user_id: user_id.to_proto(), + access_token: ACCESS_TOKEN.into(), + }) + { + return Ok(http_client::Response::builder() + .status(401) + .body("Unauthorized".into()) + .unwrap()); + } + + Ok(http_client::Response::builder() + .status(200) + .body( + serde_json::to_string(&make_get_authenticated_user_response( + user_id.0, name, + )) + .unwrap() + .into(), + ) + .unwrap()) + } + _ => Ok(http_client::Response::builder() + .status(404) + .body("Not Found".into()) + .unwrap()), + } + } + } + }); + let client_name = name.to_string(); let mut client = cx.update(|cx| Client::new(clock, http.clone(), cx)); let server = self.server.clone(); @@ -209,11 +252,10 @@ impl TestServer { .unwrap() .set_id(user_id.to_proto()) .override_authenticate(move |cx| { - let access_token = "the-token".to_string(); cx.spawn(async move |_| { Ok(Credentials { user_id: user_id.to_proto(), - access_token, + access_token: ACCESS_TOKEN.into(), }) }) }) @@ -222,7 +264,7 @@ impl TestServer { credentials, &Credentials { user_id: user_id.0 as u64, - access_token: "the-token".into() + access_token: ACCESS_TOKEN.into(), } ); @@ -282,15 +324,12 @@ impl TestServer { .register_hosting_provider(Arc::new(git_hosting_providers::Github::public_instance())); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - let cloud_user_store = - cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store.clone(), cx)); let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx)); let language_registry = Arc::new(LanguageRegistry::test(cx.executor())); let session = cx.new(|cx| AppSession::new(Session::test(), cx)); let app_state = Arc::new(workspace::AppState { client: client.clone(), user_store: user_store.clone(), - cloud_user_store, workspace_store, languages: language_registry, fs: fs.clone(), @@ -323,7 +362,7 @@ impl TestServer { }); client - .authenticate_and_connect(false, &cx.to_async()) + .connect(false, &cx.to_async()) .await .into_response() .unwrap(); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index f53b94c209..54077303a1 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2331,7 +2331,7 @@ impl CollabPanel { let client = this.client.clone(); cx.spawn_in(window, async move |_, cx| { client - .authenticate_and_connect(true, &cx) + .connect(true, &cx) .await .into_response() .notify_async_err(cx); diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index fba8f66c2d..c3e834b645 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -634,13 +634,13 @@ impl Render for NotificationPanel { .child(Icon::new(IconName::Envelope)), ) .map(|this| { - if self.client.user_id().is_none() { + if !self.client.status().borrow().is_connected() { this.child( v_flex() .gap_2() .p_4() .child( - Button::new("sign_in_prompt_button", "Sign in") + Button::new("connect_prompt_button", "Connect") .icon_color(Color::Muted) .icon(IconName::Github) .icon_position(IconPosition::Start) @@ -652,10 +652,7 @@ impl Render for NotificationPanel { let client = client.clone(); window .spawn(cx, async move |cx| { - match client - .authenticate_and_connect(true, &cx) - .await - { + match client.connect(true, &cx).await { util::ConnectionResult::Timeout => { log::error!("Connection timeout"); } @@ -673,7 +670,7 @@ impl Render for NotificationPanel { ) .child( div().flex().w_full().items_center().child( - Label::new("Sign in to view notifications.") + Label::new("Connect to view notifications.") .color(Color::Muted) .size(LabelSize::Small), ), diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index 8d257a37a7..a02b4a7f0b 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -13,7 +13,7 @@ pub(crate) use tool_metrics::*; use ::fs::RealFs; use clap::Parser; -use client::{Client, CloudUserStore, ProxySettings, UserStore}; +use client::{Client, ProxySettings, UserStore}; use collections::{HashMap, HashSet}; use extension::ExtensionHostProxy; use futures::future; @@ -329,7 +329,6 @@ pub struct AgentAppState { pub languages: Arc, pub client: Arc, pub user_store: Entity, - pub cloud_user_store: Entity, pub fs: Arc, pub node_runtime: NodeRuntime, @@ -384,8 +383,6 @@ pub fn init(cx: &mut App) -> Arc { let languages = Arc::new(languages); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - let cloud_user_store = - cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store.clone(), cx)); extension::init(cx); @@ -425,12 +422,7 @@ pub fn init(cx: &mut App) -> Arc { languages.clone(), ); language_model::init(client.clone(), cx); - language_models::init( - user_store.clone(), - cloud_user_store.clone(), - client.clone(), - cx, - ); + language_models::init(user_store.clone(), client.clone(), cx); languages::init(languages.clone(), node_runtime.clone(), cx); prompt_store::init(cx); terminal_view::init(cx); @@ -455,7 +447,6 @@ pub fn init(cx: &mut App) -> Arc { languages, client, user_store, - cloud_user_store, fs, node_runtime, prompt_builder, diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs index 54d864ea21..0f2b4c18ea 100644 --- a/crates/eval/src/instance.rs +++ b/crates/eval/src/instance.rs @@ -221,7 +221,6 @@ impl ExampleInstance { let prompt_store = None; let thread_store = ThreadStore::load( project.clone(), - app_state.cloud_user_store.clone(), tools, prompt_store, app_state.prompt_builder.clone(), diff --git a/crates/http_client/Cargo.toml b/crates/http_client/Cargo.toml index 2045708ff2..3f51cc5a23 100644 --- a/crates/http_client/Cargo.toml +++ b/crates/http_client/Cargo.toml @@ -23,6 +23,7 @@ futures.workspace = true http.workspace = true http-body.workspace = true log.workspace = true +parking_lot.workspace = true serde.workspace = true serde_json.workspace = true url.workspace = true diff --git a/crates/http_client/src/http_client.rs b/crates/http_client/src/http_client.rs index 06875718d9..d33bbefc06 100644 --- a/crates/http_client/src/http_client.rs +++ b/crates/http_client/src/http_client.rs @@ -9,12 +9,10 @@ pub use http::{self, Method, Request, Response, StatusCode, Uri}; use futures::future::BoxFuture; use http::request::Builder; +use parking_lot::Mutex; #[cfg(feature = "test-support")] use std::fmt; -use std::{ - any::type_name, - sync::{Arc, Mutex}, -}; +use std::{any::type_name, sync::Arc}; pub use url::Url; #[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] @@ -86,6 +84,11 @@ pub trait HttpClient: 'static + Send + Sync { } fn proxy(&self) -> Option<&Url>; + + #[cfg(feature = "test-support")] + fn as_fake(&self) -> &FakeHttpClient { + panic!("called as_fake on {}", type_name::()) + } } /// An [`HttpClient`] that may have a proxy. @@ -132,6 +135,11 @@ impl HttpClient for HttpClientWithProxy { fn type_name(&self) -> &'static str { self.client.type_name() } + + #[cfg(feature = "test-support")] + fn as_fake(&self) -> &FakeHttpClient { + self.client.as_fake() + } } impl HttpClient for Arc { @@ -153,6 +161,11 @@ impl HttpClient for Arc { fn type_name(&self) -> &'static str { self.client.type_name() } + + #[cfg(feature = "test-support")] + fn as_fake(&self) -> &FakeHttpClient { + self.client.as_fake() + } } /// An [`HttpClient`] that has a base URL. @@ -199,20 +212,13 @@ impl HttpClientWithUrl { /// Returns the base URL. pub fn base_url(&self) -> String { - self.base_url - .lock() - .map_or_else(|_| Default::default(), |url| url.clone()) + self.base_url.lock().clone() } /// Sets the base URL. pub fn set_base_url(&self, base_url: impl Into) { let base_url = base_url.into(); - self.base_url - .lock() - .map(|mut url| { - *url = base_url; - }) - .ok(); + *self.base_url.lock() = base_url; } /// Builds a URL using the given path. @@ -288,6 +294,11 @@ impl HttpClient for Arc { fn type_name(&self) -> &'static str { self.client.type_name() } + + #[cfg(feature = "test-support")] + fn as_fake(&self) -> &FakeHttpClient { + self.client.as_fake() + } } impl HttpClient for HttpClientWithUrl { @@ -309,6 +320,11 @@ impl HttpClient for HttpClientWithUrl { fn type_name(&self) -> &'static str { self.client.type_name() } + + #[cfg(feature = "test-support")] + fn as_fake(&self) -> &FakeHttpClient { + self.client.as_fake() + } } pub fn read_proxy_from_env() -> Option { @@ -360,10 +376,15 @@ impl HttpClient for BlockedHttpClient { fn type_name(&self) -> &'static str { type_name::() } + + #[cfg(feature = "test-support")] + fn as_fake(&self) -> &FakeHttpClient { + panic!("called as_fake on {}", type_name::()) + } } #[cfg(feature = "test-support")] -type FakeHttpHandler = Box< +type FakeHttpHandler = Arc< dyn Fn(Request) -> BoxFuture<'static, anyhow::Result>> + Send + Sync @@ -372,7 +393,7 @@ type FakeHttpHandler = Box< #[cfg(feature = "test-support")] pub struct FakeHttpClient { - handler: FakeHttpHandler, + handler: Mutex>, user_agent: HeaderValue, } @@ -387,7 +408,7 @@ impl FakeHttpClient { base_url: Mutex::new("http://test.example".into()), client: HttpClientWithProxy { client: Arc::new(Self { - handler: Box::new(move |req| Box::pin(handler(req))), + handler: Mutex::new(Some(Arc::new(move |req| Box::pin(handler(req))))), user_agent: HeaderValue::from_static(type_name::()), }), proxy: None, @@ -412,6 +433,18 @@ impl FakeHttpClient { .unwrap()) }) } + + pub fn replace_handler(&self, new_handler: F) + where + Fut: futures::Future>> + Send + 'static, + F: Fn(FakeHttpHandler, Request) -> Fut + Send + Sync + 'static, + { + let mut handler = self.handler.lock(); + let old_handler = handler.take().unwrap(); + *handler = Some(Arc::new(move |req| { + Box::pin(new_handler(old_handler.clone(), req)) + })); + } } #[cfg(feature = "test-support")] @@ -427,7 +460,7 @@ impl HttpClient for FakeHttpClient { &self, req: Request, ) -> BoxFuture<'static, anyhow::Result>> { - let future = (self.handler)(req); + let future = (self.handler.lock().as_ref().unwrap())(req); future } @@ -442,4 +475,8 @@ impl HttpClient for FakeHttpClient { fn type_name(&self) -> &'static str { type_name::() } + + fn as_fake(&self) -> &FakeHttpClient { + self + } } diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index d402b87382..2d7f211942 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use client::{CloudUserStore, DisableAiSettings, zed_urls}; +use client::{DisableAiSettings, UserStore, zed_urls}; use cloud_llm_client::UsageLimit; use copilot::{Copilot, Status}; use editor::{ @@ -59,7 +59,7 @@ pub struct InlineCompletionButton { file: Option>, edit_prediction_provider: Option>, fs: Arc, - cloud_user_store: Entity, + user_store: Entity, popover_menu_handle: PopoverMenuHandle, } @@ -245,9 +245,9 @@ impl Render for InlineCompletionButton { IconName::ZedPredictDisabled }; - if zeta::should_show_upsell_modal(&self.cloud_user_store, cx) { - let tooltip_meta = if self.cloud_user_store.read(cx).is_authenticated() { - if self.cloud_user_store.read(cx).has_accepted_tos() { + if zeta::should_show_upsell_modal(&self.user_store, cx) { + let tooltip_meta = if self.user_store.read(cx).current_user().is_some() { + if self.user_store.read(cx).has_accepted_terms_of_service() { "Choose a Plan" } else { "Accept the Terms of Service" @@ -371,7 +371,7 @@ impl Render for InlineCompletionButton { impl InlineCompletionButton { pub fn new( fs: Arc, - cloud_user_store: Entity, + user_store: Entity, popover_menu_handle: PopoverMenuHandle, cx: &mut Context, ) -> Self { @@ -390,9 +390,9 @@ impl InlineCompletionButton { language: None, file: None, edit_prediction_provider: None, + user_store, popover_menu_handle, fs, - cloud_user_store, } } @@ -763,7 +763,7 @@ impl InlineCompletionButton { }) }) .separator(); - } else if self.cloud_user_store.read(cx).account_too_young() { + } else if self.user_store.read(cx).account_too_young() { menu = menu .custom_entry( |_window, _cx| { @@ -778,7 +778,7 @@ impl InlineCompletionButton { cx.open_url(&zed_urls::account_url(cx)) }) .separator(); - } else if self.cloud_user_store.read(cx).has_overdue_invoices() { + } else if self.user_store.read(cx).has_overdue_invoices() { menu = menu .custom_entry( |_window, _cx| { diff --git a/crates/language_model/src/model/cloud_model.rs b/crates/language_model/src/model/cloud_model.rs index a5d2ac34f5..8ae5893410 100644 --- a/crates/language_model/src/model/cloud_model.rs +++ b/crates/language_model/src/model/cloud_model.rs @@ -3,10 +3,11 @@ use std::sync::Arc; use anyhow::Result; use client::Client; +use cloud_llm_client::Plan; use gpui::{ App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, ReadGlobal as _, }; -use proto::{Plan, TypedEnvelope}; +use proto::TypedEnvelope; use smol::lock::{RwLock, RwLockUpgradableReadGuard, RwLockWriteGuard}; use thiserror::Error; @@ -30,7 +31,7 @@ pub struct ModelRequestLimitReachedError { impl fmt::Display for ModelRequestLimitReachedError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let message = match self.plan { - Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.", + Plan::ZedFree => "Model request limit reached. Upgrade to Zed Pro for more requests.", Plan::ZedPro => { "Model request limit reached. Upgrade to usage-based billing for more requests." } diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index 208b0d99c9..b5bfb870f6 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -44,7 +44,6 @@ ollama = { workspace = true, features = ["schemars"] } open_ai = { workspace = true, features = ["schemars"] } open_router = { workspace = true, features = ["schemars"] } partial-json-fixer.workspace = true -proto.workspace = true release_channel.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index a88f12283a..18e6f47ed0 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use ::settings::{Settings, SettingsStore}; -use client::{Client, CloudUserStore, UserStore}; +use client::{Client, UserStore}; use collections::HashSet; use gpui::{App, Context, Entity}; use language_model::{LanguageModelProviderId, LanguageModelRegistry}; @@ -26,22 +26,11 @@ use crate::provider::vercel::VercelLanguageModelProvider; use crate::provider::x_ai::XAiLanguageModelProvider; pub use crate::settings::*; -pub fn init( - user_store: Entity, - cloud_user_store: Entity, - client: Arc, - cx: &mut App, -) { +pub fn init(user_store: Entity, client: Arc, cx: &mut App) { crate::settings::init_settings(cx); let registry = LanguageModelRegistry::global(cx); registry.update(cx, |registry, cx| { - register_language_model_providers( - registry, - user_store, - cloud_user_store, - client.clone(), - cx, - ); + register_language_model_providers(registry, user_store, client.clone(), cx); }); let mut openai_compatible_providers = AllLanguageModelSettings::get_global(cx) @@ -111,17 +100,11 @@ fn register_openai_compatible_providers( fn register_language_model_providers( registry: &mut LanguageModelRegistry, user_store: Entity, - cloud_user_store: Entity, client: Arc, cx: &mut Context, ) { registry.register_provider( - CloudLanguageModelProvider::new( - user_store.clone(), - cloud_user_store.clone(), - client.clone(), - cx, - ), + CloudLanguageModelProvider::new(user_store.clone(), client.clone(), cx), cx, ); diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index a5de7f3442..2108547c4f 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -2,7 +2,7 @@ use ai_onboarding::YoungAccountBanner; use anthropic::AnthropicModelMode; use anyhow::{Context as _, Result, anyhow}; use chrono::{DateTime, Utc}; -use client::{Client, CloudUserStore, ModelRequestUsage, UserStore, zed_urls}; +use client::{Client, ModelRequestUsage, UserStore, zed_urls}; use cloud_llm_client::{ CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, CURRENT_PLAN_HEADER_NAME, CompletionBody, CompletionEvent, CompletionRequestStatus, CountTokensBody, CountTokensResponse, @@ -117,7 +117,6 @@ pub struct State { client: Arc, llm_api_token: LlmApiToken, user_store: Entity, - cloud_user_store: Entity, status: client::Status, accept_terms_of_service_task: Option>>, models: Vec>, @@ -133,17 +132,14 @@ impl State { fn new( client: Arc, user_store: Entity, - cloud_user_store: Entity, status: client::Status, cx: &mut Context, ) -> Self { let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx); - Self { client: client.clone(), llm_api_token: LlmApiToken::default(), - user_store, - cloud_user_store, + user_store: user_store.clone(), status, accept_terms_of_service_task: None, models: Vec::new(), @@ -152,18 +148,12 @@ impl State { recommended_models: Vec::new(), _fetch_models_task: cx.spawn(async move |this, cx| { maybe!(async move { - let (client, cloud_user_store, llm_api_token) = - this.read_with(cx, |this, _cx| { - ( - client.clone(), - this.cloud_user_store.clone(), - this.llm_api_token.clone(), - ) - })?; + let (client, llm_api_token) = this + .read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?; loop { - let is_authenticated = - cloud_user_store.read_with(cx, |this, _cx| this.is_authenticated())?; + let is_authenticated = user_store + .read_with(cx, |user_store, _cx| user_store.current_user().is_some())?; if is_authenticated { break; } @@ -204,22 +194,19 @@ impl State { } fn is_signed_out(&self, cx: &App) -> bool { - !self.cloud_user_store.read(cx).is_authenticated() + self.user_store.read(cx).current_user().is_none() } fn authenticate(&self, cx: &mut Context) -> Task> { let client = self.client.clone(); cx.spawn(async move |state, cx| { - client - .authenticate_and_connect(true, &cx) - .await - .into_response()?; + client.sign_in_with_optional_connect(true, &cx).await?; state.update(cx, |_, cx| cx.notify()) }) } fn has_accepted_terms_of_service(&self, cx: &App) -> bool { - self.cloud_user_store.read(cx).has_accepted_tos() + self.user_store.read(cx).has_accepted_terms_of_service() } fn accept_terms_of_service(&mut self, cx: &mut Context) { @@ -303,24 +290,11 @@ impl State { } impl CloudLanguageModelProvider { - pub fn new( - user_store: Entity, - cloud_user_store: Entity, - client: Arc, - cx: &mut App, - ) -> Self { + pub fn new(user_store: Entity, client: Arc, cx: &mut App) -> Self { let mut status_rx = client.status(); let status = *status_rx.borrow(); - let state = cx.new(|cx| { - State::new( - client.clone(), - user_store.clone(), - cloud_user_store.clone(), - status, - cx, - ) - }); + let state = cx.new(|cx| State::new(client.clone(), user_store.clone(), status, cx)); let state_ref = state.downgrade(); let maintain_client_status = cx.spawn(async move |cx| { @@ -632,11 +606,6 @@ impl CloudLanguageModel { .and_then(|plan| plan.to_str().ok()) .and_then(|plan| cloud_llm_client::Plan::from_str(plan).ok()) { - let plan = match plan { - cloud_llm_client::Plan::ZedFree => proto::Plan::Free, - cloud_llm_client::Plan::ZedPro => proto::Plan::ZedPro, - cloud_llm_client::Plan::ZedProTrial => proto::Plan::ZedProTrial, - }; return Err(anyhow!(ModelRequestLimitReachedError { plan })); } } @@ -1281,15 +1250,15 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let state = self.state.read(cx); - let cloud_user_store = state.cloud_user_store.read(cx); + let user_store = state.user_store.read(cx); ZedAiConfiguration { is_connected: !state.is_signed_out(cx), - plan: cloud_user_store.plan(), - subscription_period: cloud_user_store.subscription_period(), - eligible_for_trial: cloud_user_store.trial_started_at().is_none(), + plan: user_store.plan(), + subscription_period: user_store.subscription_period(), + eligible_for_trial: user_store.trial_started_at().is_none(), has_accepted_terms_of_service: state.has_accepted_terms_of_service(cx), - account_too_young: cloud_user_store.account_too_young(), + account_too_young: user_store.account_too_young(), accept_terms_of_service_in_progress: state.accept_terms_of_service_task.is_some(), accept_terms_of_service_callback: self.accept_terms_of_service_callback.clone(), sign_in_callback: self.sign_in_callback.clone(), diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index a5b4b1d7be..c33dcb9ad1 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -278,7 +278,7 @@ pub(crate) fn render_ai_setup_page( .child(AiUpsellCard { sign_in_status: SignInStatus::SignedIn, sign_in: Arc::new(|_, _| {}), - user_plan: onboarding.cloud_user_store.read(cx).plan(), + user_plan: onboarding.user_store.read(cx).plan(), }) .child(render_llm_provider_section( onboarding, diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index bf60da4aab..2ae07b7cd5 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -1,5 +1,5 @@ use crate::welcome::{ShowWelcome, WelcomePage}; -use client::{Client, CloudUserStore, UserStore}; +use client::{Client, UserStore}; use command_palette_hooks::CommandPaletteFilter; use db::kvp::KEY_VALUE_STORE; use feature_flags::{FeatureFlag, FeatureFlagViewExt as _}; @@ -220,7 +220,6 @@ struct Onboarding { workspace: WeakEntity, focus_handle: FocusHandle, selected_page: SelectedPage, - cloud_user_store: Entity, user_store: Entity, _settings_subscription: Subscription, } @@ -231,7 +230,6 @@ impl Onboarding { workspace: workspace.weak_handle(), focus_handle: cx.focus_handle(), selected_page: SelectedPage::Basics, - cloud_user_store: workspace.app_state().cloud_user_store.clone(), user_store: workspace.user_store().clone(), _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), }) @@ -365,9 +363,8 @@ impl Onboarding { window .spawn(cx, async move |cx| { client - .authenticate_and_connect(true, &cx) + .sign_in_with_optional_connect(true, &cx) .await - .into_response() .notify_async_err(cx); }) .detach(); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 13587b43e7..623f48d3c9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1362,10 +1362,7 @@ impl Project { fs: Arc, cx: AsyncApp, ) -> Result> { - client - .authenticate_and_connect(true, &cx) - .await - .into_response()?; + client.connect(true, &cx).await.into_response()?; let subscriptions = [ EntitySubscription::Project(client.subscribe_to_entity::(remote_id)?), diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 552ef915cb..426d87ad13 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -20,7 +20,7 @@ use crate::application_menu::{ use auto_update::AutoUpdateStatus; use call::ActiveCall; -use client::{Client, CloudUserStore, UserStore, zed_urls}; +use client::{Client, UserStore, zed_urls}; use cloud_llm_client::Plan; use gpui::{ Action, AnyElement, App, Context, Corner, Element, Entity, Focusable, InteractiveElement, @@ -126,7 +126,6 @@ pub struct TitleBar { platform_titlebar: Entity, project: Entity, user_store: Entity, - cloud_user_store: Entity, client: Arc, workspace: WeakEntity, application_menu: Option>, @@ -180,11 +179,9 @@ impl Render for TitleBar { children.push(self.banner.clone().into_any_element()) } - let is_authenticated = self.cloud_user_store.read(cx).is_authenticated(); let status = self.client.status(); let status = &*status.borrow(); - - let show_sign_in = !is_authenticated || !matches!(status, client::Status::Connected { .. }); + let user = self.user_store.read(cx).current_user(); children.push( h_flex() @@ -194,10 +191,10 @@ impl Render for TitleBar { .children(self.render_call_controls(window, cx)) .children(self.render_connection_status(status, cx)) .when( - show_sign_in && TitleBarSettings::get_global(cx).show_sign_in, + user.is_none() && TitleBarSettings::get_global(cx).show_sign_in, |el| el.child(self.render_sign_in_button(cx)), ) - .when(is_authenticated, |parent| { + .when(user.is_some(), |parent| { parent.child(self.render_user_menu_button(cx)) }) .into_any_element(), @@ -248,7 +245,6 @@ impl TitleBar { ) -> Self { let project = workspace.project().clone(); let user_store = workspace.app_state().user_store.clone(); - let cloud_user_store = workspace.app_state().cloud_user_store.clone(); let client = workspace.app_state().client.clone(); let active_call = ActiveCall::global(cx); @@ -296,7 +292,6 @@ impl TitleBar { workspace: workspace.weak_handle(), project, user_store, - cloud_user_store, client, _subscriptions: subscriptions, banner, @@ -622,9 +617,8 @@ impl TitleBar { window .spawn(cx, async move |cx| { client - .authenticate_and_connect(true, &cx) + .sign_in_with_optional_connect(true, &cx) .await - .into_response() .notify_async_err(cx); }) .detach(); @@ -632,15 +626,15 @@ impl TitleBar { } pub fn render_user_menu_button(&mut self, cx: &mut Context) -> impl Element { - let cloud_user_store = self.cloud_user_store.read(cx); - if let Some(user) = cloud_user_store.authenticated_user() { - let has_subscription_period = cloud_user_store.subscription_period().is_some(); - let plan = cloud_user_store.plan().filter(|_| { + let user_store = self.user_store.read(cx); + if let Some(user) = user_store.current_user() { + let has_subscription_period = user_store.subscription_period().is_some(); + let plan = user_store.plan().filter(|_| { // Since the user might be on the legacy free plan we filter based on whether we have a subscription period. has_subscription_period }); - let user_avatar = user.avatar_url.clone(); + let user_avatar = user.avatar_uri.clone(); let free_chip_bg = cx .theme() .colors() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index aad585e419..6f7db668dd 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -15,7 +15,6 @@ mod toast_layer; mod toolbar; mod workspace_settings; -use client::CloudUserStore; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; use anyhow::{Context as _, Result, anyhow}; @@ -840,7 +839,6 @@ pub struct AppState { pub languages: Arc, pub client: Arc, pub user_store: Entity, - pub cloud_user_store: Entity, pub workspace_store: Entity, pub fs: Arc, pub build_window_options: fn(Option, &mut App) -> WindowOptions, @@ -913,8 +911,6 @@ impl AppState { let client = Client::new(clock, http_client.clone(), cx); let session = cx.new(|cx| AppSession::new(Session::test(), cx)); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - let cloud_user_store = - cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store.clone(), cx)); let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx)); theme::init(theme::LoadThemes::JustBase, cx); @@ -926,7 +922,6 @@ impl AppState { fs, languages, user_store, - cloud_user_store, workspace_store, node_runtime: NodeRuntime::unavailable(), build_window_options: |_, _| Default::default(), @@ -5739,16 +5734,12 @@ impl Workspace { let client = project.read(cx).client(); let user_store = project.read(cx).user_store(); - let cloud_user_store = - cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store.clone(), cx)); - let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx)); let session = cx.new(|cx| AppSession::new(Session::test(), cx)); window.activate_window(); let app_state = Arc::new(AppState { languages: project.read(cx).languages().clone(), workspace_store, - cloud_user_store, client, user_store, fs: project.read(cx).fs().clone(), @@ -6947,10 +6938,13 @@ async fn join_channel_internal( match status { Status::Connecting | Status::Authenticating + | Status::Authenticated | Status::Reconnecting | Status::Reauthenticating => continue, Status::Connected { .. } => break 'outer, - Status::SignedOut => return Err(ErrorCode::SignedOut.into()), + Status::SignedOut | Status::AuthenticationError => { + return Err(ErrorCode::SignedOut.into()); + } Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()), Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => { return Err(ErrorCode::Disconnected.into()); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 9859702bf8..c264135e5c 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -5,7 +5,7 @@ use agent_ui::AgentPanel; use anyhow::{Context as _, Result}; use clap::{Parser, command}; use cli::FORCE_CLI_MODE_ENV_VAR_NAME; -use client::{Client, CloudUserStore, ProxySettings, UserStore, parse_zed_link}; +use client::{Client, ProxySettings, UserStore, parse_zed_link}; use collab_ui::channel_view::ChannelView; use collections::HashMap; use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE}; @@ -42,7 +42,7 @@ use theme::{ ActiveTheme, IconThemeNotFoundError, SystemAppearance, ThemeNotFoundError, ThemeRegistry, ThemeSettings, }; -use util::{ConnectionResult, ResultExt, TryFutureExt, maybe}; +use util::{ResultExt, TryFutureExt, maybe}; use uuid::Uuid; use welcome::{FIRST_OPEN, show_welcome_view}; use workspace::{ @@ -457,8 +457,6 @@ pub fn main() { language::init(cx); languages::init(languages.clone(), node_runtime.clone(), cx); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - let cloud_user_store = - cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store.clone(), cx)); let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx)); language_extension::init( @@ -518,7 +516,6 @@ pub fn main() { languages: languages.clone(), client: client.clone(), user_store: user_store.clone(), - cloud_user_store, fs: fs.clone(), build_window_options, workspace_store, @@ -556,12 +553,7 @@ pub fn main() { ); supermaven::init(app_state.client.clone(), cx); language_model::init(app_state.client.clone(), cx); - language_models::init( - app_state.user_store.clone(), - app_state.cloud_user_store.clone(), - app_state.client.clone(), - cx, - ); + language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx); agent_settings::init(cx); agent_servers::init(cx); web_search::init(cx); @@ -569,7 +561,7 @@ pub fn main() { snippet_provider::init(cx); inline_completion_registry::init( app_state.client.clone(), - app_state.cloud_user_store.clone(), + app_state.user_store.clone(), cx, ); let prompt_builder = PromptBuilder::load(app_state.fs.clone(), stdout_is_a_pty(), cx); @@ -690,17 +682,9 @@ pub fn main() { cx.spawn({ let client = app_state.client.clone(); - async move |cx| match authenticate(client, &cx).await { - ConnectionResult::Timeout => log::error!("Timeout during initial auth"), - ConnectionResult::ConnectionReset => { - log::error!("Connection reset during initial auth") - } - ConnectionResult::Result(r) => { - r.log_err(); - } - } + async move |cx| authenticate(client, &cx).await }) - .detach(); + .detach_and_log_err(cx); let urls: Vec<_> = args .paths_or_urls @@ -850,15 +834,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut let client = app_state.client.clone(); // we continue even if authentication fails as join_channel/ open channel notes will // show a visible error message. - match authenticate(client, &cx).await { - ConnectionResult::Timeout => { - log::error!("Timeout during open request handling") - } - ConnectionResult::ConnectionReset => { - log::error!("Connection reset during open request handling") - } - ConnectionResult::Result(r) => r?, - }; + authenticate(client, &cx).await.log_err(); if let Some(channel_id) = request.join_channel { cx.update(|cx| { @@ -908,18 +884,18 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut } } -async fn authenticate(client: Arc, cx: &AsyncApp) -> ConnectionResult<()> { +async fn authenticate(client: Arc, cx: &AsyncApp) -> Result<()> { if stdout_is_a_pty() { if client::IMPERSONATE_LOGIN.is_some() { - return client.authenticate_and_connect(false, cx).await; + client.sign_in_with_optional_connect(false, cx).await?; } else if client.has_credentials(cx).await { - return client.authenticate_and_connect(true, cx).await; + client.sign_in_with_optional_connect(true, cx).await?; } } else if client.has_credentials(cx).await { - return client.authenticate_and_connect(true, cx).await; + client.sign_in_with_optional_connect(true, cx).await?; } - ConnectionResult::Result(Ok(())) + Ok(()) } async fn system_id() -> Result { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 060efdf26a..8c6da335ab 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -336,7 +336,7 @@ pub fn initialize_workspace( let edit_prediction_button = cx.new(|cx| { inline_completion_button::InlineCompletionButton::new( app_state.fs.clone(), - app_state.cloud_user_store.clone(), + app_state.user_store.clone(), inline_completion_menu_handle.clone(), cx, ) @@ -4488,12 +4488,7 @@ mod tests { ); image_viewer::init(cx); language_model::init(app_state.client.clone(), cx); - language_models::init( - app_state.user_store.clone(), - app_state.cloud_user_store.clone(), - app_state.client.clone(), - cx, - ); + language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx); web_search::init(cx); web_search_providers::init(app_state.client.clone(), cx); let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx); diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index 2e57152c62..480505338b 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/crates/zed/src/zed/component_preview.rs @@ -139,8 +139,7 @@ impl ComponentPreview { let project_clone = project.clone(); cx.spawn_in(window, async move |entity, cx| { - let thread_store_future = - load_preview_thread_store(workspace_clone.clone(), project_clone.clone(), cx); + let thread_store_future = load_preview_thread_store(project_clone.clone(), cx); let text_thread_store_future = load_preview_text_thread_store(workspace_clone.clone(), project_clone.clone(), cx); diff --git a/crates/zed/src/zed/component_preview/preview_support/active_thread.rs b/crates/zed/src/zed/component_preview/preview_support/active_thread.rs index 1076ee49ea..de98106fae 100644 --- a/crates/zed/src/zed/component_preview/preview_support/active_thread.rs +++ b/crates/zed/src/zed/component_preview/preview_support/active_thread.rs @@ -12,22 +12,19 @@ use ui::{App, Window}; use workspace::Workspace; pub fn load_preview_thread_store( - workspace: WeakEntity, project: Entity, cx: &mut AsyncApp, ) -> Task>> { - workspace - .update(cx, |workspace, cx| { - ThreadStore::load( - project.clone(), - workspace.app_state().cloud_user_store.clone(), - cx.new(|_| ToolWorkingSet::default()), - None, - Arc::new(PromptBuilder::new(None).unwrap()), - cx, - ) - }) - .unwrap_or(Task::ready(Err(anyhow!("workspace dropped")))) + cx.update(|cx| { + ThreadStore::load( + project.clone(), + cx.new(|_| ToolWorkingSet::default()), + None, + Arc::new(PromptBuilder::new(None).unwrap()), + cx, + ) + }) + .unwrap_or(Task::ready(Err(anyhow!("workspace dropped")))) } pub fn load_preview_text_thread_store( diff --git a/crates/zed/src/zed/inline_completion_registry.rs b/crates/zed/src/zed/inline_completion_registry.rs index ba19457d39..55dbea4fe1 100644 --- a/crates/zed/src/zed/inline_completion_registry.rs +++ b/crates/zed/src/zed/inline_completion_registry.rs @@ -1,4 +1,4 @@ -use client::{Client, CloudUserStore, DisableAiSettings}; +use client::{Client, DisableAiSettings, UserStore}; use collections::HashMap; use copilot::{Copilot, CopilotCompletionProvider}; use editor::Editor; @@ -13,12 +13,12 @@ use util::ResultExt; use workspace::Workspace; use zeta::{ProviderDataCollection, ZetaInlineCompletionProvider}; -pub fn init(client: Arc, cloud_user_store: Entity, cx: &mut App) { +pub fn init(client: Arc, user_store: Entity, cx: &mut App) { let editors: Rc, AnyWindowHandle>>> = Rc::default(); cx.observe_new({ let editors = editors.clone(); let client = client.clone(); - let cloud_user_store = cloud_user_store.clone(); + let user_store = user_store.clone(); move |editor: &mut Editor, window, cx: &mut Context| { if !editor.mode().is_full() { return; @@ -48,7 +48,7 @@ pub fn init(client: Arc, cloud_user_store: Entity, cx: & editor, provider, &client, - cloud_user_store.clone(), + user_store.clone(), window, cx, ); @@ -60,7 +60,7 @@ pub fn init(client: Arc, cloud_user_store: Entity, cx: & let mut provider = all_language_settings(None, cx).edit_predictions.provider; cx.spawn({ - let cloud_user_store = cloud_user_store.clone(); + let user_store = user_store.clone(); let editors = editors.clone(); let client = client.clone(); @@ -72,7 +72,7 @@ pub fn init(client: Arc, cloud_user_store: Entity, cx: & &editors, provider, &client, - cloud_user_store.clone(), + user_store.clone(), cx, ); }) @@ -85,12 +85,12 @@ pub fn init(client: Arc, cloud_user_store: Entity, cx: & cx.observe_global::({ let editors = editors.clone(); let client = client.clone(); - let cloud_user_store = cloud_user_store.clone(); + let user_store = user_store.clone(); move |cx| { let new_provider = all_language_settings(None, cx).edit_predictions.provider; if new_provider != provider { - let tos_accepted = cloud_user_store.read(cx).has_accepted_tos(); + let tos_accepted = user_store.read(cx).has_accepted_terms_of_service(); telemetry::event!( "Edit Prediction Provider Changed", @@ -104,7 +104,7 @@ pub fn init(client: Arc, cloud_user_store: Entity, cx: & &editors, provider, &client, - cloud_user_store.clone(), + user_store.clone(), cx, ); @@ -145,7 +145,7 @@ fn assign_edit_prediction_providers( editors: &Rc, AnyWindowHandle>>>, provider: EditPredictionProvider, client: &Arc, - cloud_user_store: Entity, + user_store: Entity, cx: &mut App, ) { for (editor, window) in editors.borrow().iter() { @@ -155,7 +155,7 @@ fn assign_edit_prediction_providers( editor, provider, &client, - cloud_user_store.clone(), + user_store.clone(), window, cx, ); @@ -210,7 +210,7 @@ fn assign_edit_prediction_provider( editor: &mut Editor, provider: EditPredictionProvider, client: &Arc, - cloud_user_store: Entity, + user_store: Entity, window: &mut Window, cx: &mut Context, ) { @@ -241,7 +241,7 @@ fn assign_edit_prediction_provider( } } EditPredictionProvider::Zed => { - if cloud_user_store.read(cx).is_authenticated() { + if user_store.read(cx).current_user().is_some() { let mut worktree = None; if let Some(buffer) = &singleton_buffer { @@ -263,7 +263,7 @@ fn assign_edit_prediction_provider( .map(|workspace| workspace.downgrade()); let zeta = - zeta::Zeta::register(workspace, worktree, client.clone(), cloud_user_store, cx); + zeta::Zeta::register(workspace, worktree, client.clone(), user_store, cx); if let Some(buffer) = &singleton_buffer { if buffer.read(cx).file().is_some() { diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 0ef6bef59d..18b9217b95 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -16,7 +16,7 @@ pub use rate_completion_modal::*; use anyhow::{Context as _, Result, anyhow}; use arrayvec::ArrayVec; -use client::{Client, CloudUserStore, EditPredictionUsage}; +use client::{Client, EditPredictionUsage, UserStore}; use cloud_llm_client::{ AcceptEditPredictionBody, EXPIRED_LLM_TOKEN_HEADER_NAME, MINIMUM_REQUIRED_VERSION_HEADER_NAME, PredictEditsBody, PredictEditsResponse, ZED_VERSION_HEADER_NAME, @@ -120,8 +120,8 @@ impl Dismissable for ZedPredictUpsell { } } -pub fn should_show_upsell_modal(cloud_user_store: &Entity, cx: &App) -> bool { - if cloud_user_store.read(cx).has_accepted_tos() { +pub fn should_show_upsell_modal(user_store: &Entity, cx: &App) -> bool { + if user_store.read(cx).has_accepted_terms_of_service() { !ZedPredictUpsell::dismissed() } else { true @@ -229,7 +229,7 @@ pub struct Zeta { _llm_token_subscription: Subscription, /// Whether an update to a newer version of Zed is required to continue using Zeta. update_required: bool, - cloud_user_store: Entity, + user_store: Entity, license_detection_watchers: HashMap>, } @@ -242,11 +242,11 @@ impl Zeta { workspace: Option>, worktree: Option>, client: Arc, - cloud_user_store: Entity, + user_store: Entity, cx: &mut App, ) -> Entity { let this = Self::global(cx).unwrap_or_else(|| { - let entity = cx.new(|cx| Self::new(workspace, client, cloud_user_store, cx)); + let entity = cx.new(|cx| Self::new(workspace, client, user_store, cx)); cx.set_global(ZetaGlobal(entity.clone())); entity }); @@ -269,13 +269,13 @@ impl Zeta { } pub fn usage(&self, cx: &App) -> Option { - self.cloud_user_store.read(cx).edit_prediction_usage() + self.user_store.read(cx).edit_prediction_usage() } fn new( workspace: Option>, client: Arc, - cloud_user_store: Entity, + user_store: Entity, cx: &mut Context, ) -> Self { let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx); @@ -306,7 +306,7 @@ impl Zeta { ), update_required: false, license_detection_watchers: HashMap::default(), - cloud_user_store, + user_store, } } @@ -535,8 +535,8 @@ impl Zeta { if let Some(usage) = usage { this.update(cx, |this, cx| { - this.cloud_user_store.update(cx, |cloud_user_store, cx| { - cloud_user_store.update_edit_prediction_usage(usage, cx); + this.user_store.update(cx, |user_store, cx| { + user_store.update_edit_prediction_usage(usage, cx); }); }) .ok(); @@ -877,8 +877,8 @@ and then another if response.status().is_success() { if let Some(usage) = EditPredictionUsage::from_headers(response.headers()).ok() { this.update(cx, |this, cx| { - this.cloud_user_store.update(cx, |cloud_user_store, cx| { - cloud_user_store.update_edit_prediction_usage(usage, cx); + this.user_store.update(cx, |user_store, cx| { + user_store.update_edit_prediction_usage(usage, cx); }); })?; } @@ -1559,9 +1559,9 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider !self .zeta .read(cx) - .cloud_user_store + .user_store .read(cx) - .has_accepted_tos() + .has_accepted_terms_of_service() } fn is_refreshing(&self) -> bool { @@ -1587,7 +1587,7 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider if self .zeta .read(cx) - .cloud_user_store + .user_store .read_with(cx, |cloud_user_store, _cx| { cloud_user_store.account_too_young() || cloud_user_store.has_overdue_invoices() }) @@ -1808,10 +1808,7 @@ mod tests { use client::UserStore; use client::test::FakeServer; use clock::FakeSystemClock; - use cloud_api_types::{ - AuthenticatedUser, CreateLlmTokenResponse, GetAuthenticatedUserResponse, LlmToken, PlanInfo, - }; - use cloud_llm_client::{CurrentUsage, Plan, UsageData, UsageLimit}; + use cloud_api_types::{CreateLlmTokenResponse, LlmToken}; use gpui::TestAppContext; use http_client::FakeHttpClient; use indoc::indoc; @@ -1820,39 +1817,6 @@ mod tests { use super::*; - fn make_get_authenticated_user_response() -> GetAuthenticatedUserResponse { - GetAuthenticatedUserResponse { - user: AuthenticatedUser { - id: 1, - metrics_id: "metrics-id-1".to_string(), - avatar_url: "".to_string(), - github_login: "".to_string(), - name: None, - is_staff: false, - accepted_tos_at: None, - }, - feature_flags: vec![], - plan: PlanInfo { - plan: Plan::ZedPro, - subscription_period: None, - usage: CurrentUsage { - model_requests: UsageData { - used: 0, - limit: UsageLimit::Limited(500), - }, - edit_predictions: UsageData { - used: 250, - limit: UsageLimit::Unlimited, - }, - }, - trial_started_at: None, - is_usage_based_billing_enabled: false, - is_account_too_young: false, - has_overdue_invoices: false, - }, - } - } - #[gpui::test] async fn test_inline_completion_basic_interpolation(cx: &mut TestAppContext) { let buffer = cx.new(|cx| Buffer::local("Lorem ipsum dolor", cx)); @@ -2054,14 +2018,6 @@ mod tests { let http_client = FakeHttpClient::create(move |req| async move { match (req.method(), req.uri().path()) { - (&Method::GET, "/client/users/me") => Ok(http_client::Response::builder() - .status(200) - .body( - serde_json::to_string(&make_get_authenticated_user_response()) - .unwrap() - .into(), - ) - .unwrap()), (&Method::POST, "/client/llm_tokens") => Ok(http_client::Response::builder() .status(200) .body( @@ -2098,9 +2054,7 @@ mod tests { // Construct the fake server to authenticate. let _server = FakeServer::for_client(42, &client, cx).await; let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - let cloud_user_store = - cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store.clone(), cx)); - let zeta = cx.new(|cx| Zeta::new(None, client, cloud_user_store, cx)); + let zeta = cx.new(|cx| Zeta::new(None, client, user_store.clone(), cx)); let buffer = cx.new(|cx| Buffer::local(buffer_content, cx)); let cursor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(1, 0))); @@ -2128,14 +2082,6 @@ mod tests { let completion = completion_response.clone(); async move { match (req.method(), req.uri().path()) { - (&Method::GET, "/client/users/me") => Ok(http_client::Response::builder() - .status(200) - .body( - serde_json::to_string(&make_get_authenticated_user_response()) - .unwrap() - .into(), - ) - .unwrap()), (&Method::POST, "/client/llm_tokens") => Ok(http_client::Response::builder() .status(200) .body( @@ -2172,9 +2118,7 @@ mod tests { // Construct the fake server to authenticate. let _server = FakeServer::for_client(42, &client, cx).await; let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - let cloud_user_store = - cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store.clone(), cx)); - let zeta = cx.new(|cx| Zeta::new(None, client, cloud_user_store, cx)); + let zeta = cx.new(|cx| Zeta::new(None, client, user_store.clone(), cx)); let buffer = cx.new(|cx| Buffer::local(buffer_content, cx)); let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); From b31f893408e275ce9ab2e1ec611651246644d778 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 1 Aug 2025 10:46:09 -0700 Subject: [PATCH 020/693] Rasterize glyphs without D2D (#35376) This allows debugging Zed with Renderdoc, and also fixes an issue where glyphs' bounds were miscalculated for certain sizes and scale factors. Release Notes: - N/A --------- Co-authored-by: Kate Co-authored-by: Julia Co-authored-by: Junkui Zhang <364772080@qq.com> --- Cargo.toml | 3 - crates/gpui/build.rs | 12 + crates/gpui/examples/text.rs | 2 +- crates/gpui/src/color.rs | 1 + .../platform/windows/color_text_raster.hlsl | 39 + .../gpui/src/platform/windows/direct_write.rs | 870 ++++++++++++------ .../src/platform/windows/directx_atlas.rs | 4 +- .../src/platform/windows/directx_renderer.rs | 32 +- crates/gpui/src/platform/windows/platform.rs | 10 +- crates/gpui/src/platform/windows/shaders.hlsl | 9 +- crates/gpui/src/platform/windows/window.rs | 3 +- 11 files changed, 698 insertions(+), 287 deletions(-) create mode 100644 crates/gpui/src/platform/windows/color_text_raster.hlsl diff --git a/Cargo.toml b/Cargo.toml index 9a05d89e53..93fa9644a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -678,8 +678,6 @@ features = [ "UI_ViewManagement", "Wdk_System_SystemServices", "Win32_Globalization", - "Win32_Graphics_Direct2D", - "Win32_Graphics_Direct2D_Common", "Win32_Graphics_Direct3D", "Win32_Graphics_Direct3D11", "Win32_Graphics_Direct3D_Fxc", @@ -690,7 +688,6 @@ features = [ "Win32_Graphics_Dxgi_Common", "Win32_Graphics_Gdi", "Win32_Graphics_Imaging", - "Win32_Graphics_Imaging_D2D", "Win32_Networking_WinSock", "Win32_Security", "Win32_Security_Credentials", diff --git a/crates/gpui/build.rs b/crates/gpui/build.rs index 2b574ebdd8..93a1c15c41 100644 --- a/crates/gpui/build.rs +++ b/crates/gpui/build.rs @@ -310,6 +310,18 @@ mod windows { &rust_binding_path, ); } + + { + let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("src/platform/windows/color_text_raster.hlsl"); + compile_shader_for_module( + "emoji_rasterization", + &out_dir, + &fxc_path, + shader_path.to_str().unwrap(), + &rust_binding_path, + ); + } } /// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler. diff --git a/crates/gpui/examples/text.rs b/crates/gpui/examples/text.rs index 19214aebde..1166bb2795 100644 --- a/crates/gpui/examples/text.rs +++ b/crates/gpui/examples/text.rs @@ -198,7 +198,7 @@ impl RenderOnce for CharacterGrid { "χ", "ψ", "∂", "а", "в", "Ж", "ж", "З", "з", "К", "к", "л", "м", "Н", "н", "Р", "р", "У", "у", "ф", "ч", "ь", "ы", "Э", "э", "Я", "я", "ij", "öẋ", ".,", "⣝⣑", "~", "*", "_", "^", "`", "'", "(", "{", "«", "#", "&", "@", "$", "¢", "%", "|", "?", "¶", "µ", - "❮", "<=", "!=", "==", "--", "++", "=>", "->", + "❮", "<=", "!=", "==", "--", "++", "=>", "->", "🏀", "🎊", "😍", "❤️", "👍", "👎", ]; let columns = 11; diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index a16c8f46be..639c84c101 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -35,6 +35,7 @@ pub(crate) fn swap_rgba_pa_to_bgra(color: &mut [u8]) { /// An RGBA color #[derive(PartialEq, Clone, Copy, Default)] +#[repr(C)] pub struct Rgba { /// The red component of the color, in the range 0.0 to 1.0 pub r: f32, diff --git a/crates/gpui/src/platform/windows/color_text_raster.hlsl b/crates/gpui/src/platform/windows/color_text_raster.hlsl new file mode 100644 index 0000000000..ccc5fa26f0 --- /dev/null +++ b/crates/gpui/src/platform/windows/color_text_raster.hlsl @@ -0,0 +1,39 @@ +struct RasterVertexOutput { + float4 position : SV_Position; + float2 texcoord : TEXCOORD0; +}; + +RasterVertexOutput emoji_rasterization_vertex(uint vertexID : SV_VERTEXID) +{ + RasterVertexOutput output; + output.texcoord = float2((vertexID << 1) & 2, vertexID & 2); + output.position = float4(output.texcoord * 2.0f - 1.0f, 0.0f, 1.0f); + output.position.y = -output.position.y; + + return output; +} + +struct PixelInput { + float4 position: SV_Position; + float2 texcoord : TEXCOORD0; +}; + +struct Bounds { + int2 origin; + int2 size; +}; + +Texture2D t_layer : register(t0); +SamplerState s_layer : register(s0); + +cbuffer GlyphLayerTextureParams : register(b0) { + Bounds bounds; + float4 run_color; +}; + +float4 emoji_rasterization_fragment(PixelInput input): SV_Target { + float3 sampled = t_layer.Sample(s_layer, input.texcoord.xy).rgb; + float alpha = (sampled.r + sampled.g + sampled.b) / 3; + + return float4(run_color.rgb, alpha); +} diff --git a/crates/gpui/src/platform/windows/direct_write.rs b/crates/gpui/src/platform/windows/direct_write.rs index ada306c15c..587cb7b4a6 100644 --- a/crates/gpui/src/platform/windows/direct_write.rs +++ b/crates/gpui/src/platform/windows/direct_write.rs @@ -10,10 +10,11 @@ use windows::{ Foundation::*, Globalization::GetUserDefaultLocaleName, Graphics::{ - Direct2D::{Common::*, *}, + Direct3D::D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, + Direct3D11::*, DirectWrite::*, Dxgi::Common::*, - Gdi::LOGFONTW, + Gdi::{IsRectEmpty, LOGFONTW}, Imaging::*, }, System::SystemServices::LOCALE_NAME_MAX_LENGTH, @@ -40,16 +41,21 @@ struct DirectWriteComponent { locale: String, factory: IDWriteFactory5, bitmap_factory: AgileReference, - d2d1_factory: ID2D1Factory, in_memory_loader: IDWriteInMemoryFontFileLoader, builder: IDWriteFontSetBuilder1, text_renderer: Arc, - render_context: GlyphRenderContext, + + render_params: IDWriteRenderingParams3, + gpu_state: GPUState, } -struct GlyphRenderContext { - params: IDWriteRenderingParams3, - dc_target: ID2D1DeviceContext4, +struct GPUState { + device: ID3D11Device, + device_context: ID3D11DeviceContext, + sampler: [Option; 1], + blend_state: ID3D11BlendState, + vertex_shader: ID3D11VertexShader, + pixel_shader: ID3D11PixelShader, } struct DirectWriteState { @@ -70,12 +76,11 @@ struct FontIdentifier { } impl DirectWriteComponent { - pub fn new(bitmap_factory: &IWICImagingFactory) -> Result { + pub fn new(bitmap_factory: &IWICImagingFactory, gpu_context: &DirectXDevices) -> Result { + // todo: ideally this would not be a large unsafe block but smaller isolated ones for easier auditing unsafe { let factory: IDWriteFactory5 = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)?; let bitmap_factory = AgileReference::new(bitmap_factory)?; - let d2d1_factory: ID2D1Factory = - D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, None)?; // The `IDWriteInMemoryFontFileLoader` here is supported starting from // Windows 10 Creators Update, which consequently requires the entire // `DirectWriteTextSystem` to run on `win10 1703`+. @@ -86,60 +91,132 @@ impl DirectWriteComponent { GetUserDefaultLocaleName(&mut locale_vec); let locale = String::from_utf16_lossy(&locale_vec); let text_renderer = Arc::new(TextRendererWrapper::new(&locale)); - let render_context = GlyphRenderContext::new(&factory, &d2d1_factory)?; + + let render_params = { + let default_params: IDWriteRenderingParams3 = + factory.CreateRenderingParams()?.cast()?; + let gamma = default_params.GetGamma(); + let enhanced_contrast = default_params.GetEnhancedContrast(); + let gray_contrast = default_params.GetGrayscaleEnhancedContrast(); + let cleartype_level = default_params.GetClearTypeLevel(); + let grid_fit_mode = default_params.GetGridFitMode(); + + factory.CreateCustomRenderingParams( + gamma, + enhanced_contrast, + gray_contrast, + cleartype_level, + DWRITE_PIXEL_GEOMETRY_RGB, + DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC, + grid_fit_mode, + )? + }; + + let gpu_state = GPUState::new(gpu_context)?; Ok(DirectWriteComponent { locale, factory, bitmap_factory, - d2d1_factory, in_memory_loader, builder, text_renderer, - render_context, + render_params, + gpu_state, }) } } } -impl GlyphRenderContext { - pub fn new(factory: &IDWriteFactory5, d2d1_factory: &ID2D1Factory) -> Result { - unsafe { - let default_params: IDWriteRenderingParams3 = - factory.CreateRenderingParams()?.cast()?; - let gamma = default_params.GetGamma(); - let enhanced_contrast = default_params.GetEnhancedContrast(); - let gray_contrast = default_params.GetGrayscaleEnhancedContrast(); - let cleartype_level = default_params.GetClearTypeLevel(); - let grid_fit_mode = default_params.GetGridFitMode(); +impl GPUState { + fn new(gpu_context: &DirectXDevices) -> Result { + let device = gpu_context.device.clone(); + let device_context = gpu_context.device_context.clone(); - let params = factory.CreateCustomRenderingParams( - gamma, - enhanced_contrast, - gray_contrast, - cleartype_level, - DWRITE_PIXEL_GEOMETRY_RGB, - DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC, - grid_fit_mode, - )?; - let dc_target = { - let target = d2d1_factory.CreateDCRenderTarget(&get_render_target_property( - DXGI_FORMAT_B8G8R8A8_UNORM, - D2D1_ALPHA_MODE_PREMULTIPLIED, - ))?; - let target = target.cast::()?; - target.SetTextRenderingParams(¶ms); - target + let blend_state = { + let mut blend_state = None; + let desc = D3D11_BLEND_DESC { + AlphaToCoverageEnable: false.into(), + IndependentBlendEnable: false.into(), + RenderTarget: [ + D3D11_RENDER_TARGET_BLEND_DESC { + BlendEnable: true.into(), + SrcBlend: D3D11_BLEND_SRC_ALPHA, + DestBlend: D3D11_BLEND_INV_SRC_ALPHA, + BlendOp: D3D11_BLEND_OP_ADD, + SrcBlendAlpha: D3D11_BLEND_SRC_ALPHA, + DestBlendAlpha: D3D11_BLEND_INV_SRC_ALPHA, + BlendOpAlpha: D3D11_BLEND_OP_ADD, + RenderTargetWriteMask: D3D11_COLOR_WRITE_ENABLE_ALL.0 as u8, + }, + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + ], }; + unsafe { device.CreateBlendState(&desc, Some(&mut blend_state)) }?; + blend_state.unwrap() + }; - Ok(Self { params, dc_target }) - } + let sampler = { + let mut sampler = None; + let desc = D3D11_SAMPLER_DESC { + Filter: D3D11_FILTER_MIN_MAG_MIP_POINT, + AddressU: D3D11_TEXTURE_ADDRESS_BORDER, + AddressV: D3D11_TEXTURE_ADDRESS_BORDER, + AddressW: D3D11_TEXTURE_ADDRESS_BORDER, + MipLODBias: 0.0, + MaxAnisotropy: 1, + ComparisonFunc: D3D11_COMPARISON_ALWAYS, + BorderColor: [0.0, 0.0, 0.0, 0.0], + MinLOD: 0.0, + MaxLOD: 0.0, + }; + unsafe { device.CreateSamplerState(&desc, Some(&mut sampler)) }?; + [sampler] + }; + + let vertex_shader = { + let source = shader_resources::RawShaderBytes::new( + shader_resources::ShaderModule::EmojiRasterization, + shader_resources::ShaderTarget::Vertex, + )?; + let mut shader = None; + unsafe { device.CreateVertexShader(source.as_bytes(), None, Some(&mut shader)) }?; + shader.unwrap() + }; + + let pixel_shader = { + let source = shader_resources::RawShaderBytes::new( + shader_resources::ShaderModule::EmojiRasterization, + shader_resources::ShaderTarget::Fragment, + )?; + let mut shader = None; + unsafe { device.CreatePixelShader(source.as_bytes(), None, Some(&mut shader)) }?; + shader.unwrap() + }; + + Ok(Self { + device, + device_context, + sampler, + blend_state, + vertex_shader, + pixel_shader, + }) } } impl DirectWriteTextSystem { - pub(crate) fn new(bitmap_factory: &IWICImagingFactory) -> Result { - let components = DirectWriteComponent::new(bitmap_factory)?; + pub(crate) fn new( + gpu_context: &DirectXDevices, + bitmap_factory: &IWICImagingFactory, + ) -> Result { + let components = DirectWriteComponent::new(bitmap_factory, gpu_context)?; let system_font_collection = unsafe { let mut result = std::mem::zeroed(); components @@ -648,15 +725,13 @@ impl DirectWriteState { } } - fn raster_bounds(&self, params: &RenderGlyphParams) -> Result> { - let render_target = &self.components.render_context.dc_target; - unsafe { - render_target.SetUnitMode(D2D1_UNIT_MODE_DIPS); - render_target.SetDpi(96.0 * params.scale_factor, 96.0 * params.scale_factor); - } + fn create_glyph_run_analysis( + &self, + params: &RenderGlyphParams, + ) -> Result { let font = &self.fonts[params.font_id.0]; let glyph_id = [params.glyph_id.0 as u16]; - let advance = [0.0f32]; + let advance = [0.0]; let offset = [DWRITE_GLYPH_OFFSET::default()]; let glyph_run = DWRITE_GLYPH_RUN { fontFace: unsafe { std::mem::transmute_copy(&font.font_face) }, @@ -668,44 +743,87 @@ impl DirectWriteState { isSideways: BOOL(0), bidiLevel: 0, }; - let bounds = unsafe { - render_target.GetGlyphRunWorldBounds( - Vector2 { X: 0.0, Y: 0.0 }, - &glyph_run, - DWRITE_MEASURING_MODE_NATURAL, - )? + let transform = DWRITE_MATRIX { + m11: params.scale_factor, + m12: 0.0, + m21: 0.0, + m22: params.scale_factor, + dx: 0.0, + dy: 0.0, }; - // todo(windows) - // This is a walkaround, deleted when figured out. - let y_offset; - let extra_height; - if params.is_emoji { - y_offset = 0; - extra_height = 0; - } else { - // make some room for scaler. - y_offset = -1; - extra_height = 2; + let subpixel_shift = params + .subpixel_variant + .map(|v| v as f32 / SUBPIXEL_VARIANTS as f32); + let baseline_origin_x = subpixel_shift.x / params.scale_factor; + let baseline_origin_y = subpixel_shift.y / params.scale_factor; + + let mut rendering_mode = DWRITE_RENDERING_MODE1::default(); + let mut grid_fit_mode = DWRITE_GRID_FIT_MODE::default(); + unsafe { + font.font_face.GetRecommendedRenderingMode( + params.font_size.0, + // The dpi here seems that it has the same effect with `Some(&transform)` + 1.0, + 1.0, + Some(&transform), + false, + DWRITE_OUTLINE_THRESHOLD_ANTIALIASED, + DWRITE_MEASURING_MODE_NATURAL, + &self.components.render_params, + &mut rendering_mode, + &mut grid_fit_mode, + )?; } - if bounds.right < bounds.left { + let glyph_analysis = unsafe { + self.components.factory.CreateGlyphRunAnalysis( + &glyph_run, + Some(&transform), + rendering_mode, + DWRITE_MEASURING_MODE_NATURAL, + grid_fit_mode, + // We're using cleartype not grayscale for monochrome is because it provides better quality + DWRITE_TEXT_ANTIALIAS_MODE_CLEARTYPE, + baseline_origin_x, + baseline_origin_y, + ) + }?; + Ok(glyph_analysis) + } + + fn raster_bounds(&self, params: &RenderGlyphParams) -> Result> { + let glyph_analysis = self.create_glyph_run_analysis(params)?; + + let bounds = unsafe { glyph_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_CLEARTYPE_3x1)? }; + // Some glyphs cannot be drawn with ClearType, such as bitmap fonts. In that case + // GetAlphaTextureBounds() supposedly returns an empty RECT, but I haven't tested that yet. + if !unsafe { IsRectEmpty(&bounds) }.as_bool() { Ok(Bounds { - origin: point(0.into(), 0.into()), - size: size(0.into(), 0.into()), + origin: point(bounds.left.into(), bounds.top.into()), + size: size( + (bounds.right - bounds.left).into(), + (bounds.bottom - bounds.top).into(), + ), }) } else { - Ok(Bounds { - origin: point( - ((bounds.left * params.scale_factor).ceil() as i32).into(), - ((bounds.top * params.scale_factor).ceil() as i32 + y_offset).into(), - ), - size: size( - (((bounds.right - bounds.left) * params.scale_factor).ceil() as i32).into(), - (((bounds.bottom - bounds.top) * params.scale_factor).ceil() as i32 - + extra_height) - .into(), - ), - }) + // If it's empty, retry with grayscale AA. + let bounds = + unsafe { glyph_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_ALIASED_1x1)? }; + + if bounds.right < bounds.left { + Ok(Bounds { + origin: point(0.into(), 0.into()), + size: size(0.into(), 0.into()), + }) + } else { + Ok(Bounds { + origin: point(bounds.left.into(), bounds.top.into()), + size: size( + (bounds.right - bounds.left).into(), + (bounds.bottom - bounds.top).into(), + ), + }) + } } } @@ -731,7 +849,95 @@ impl DirectWriteState { anyhow::bail!("glyph bounds are empty"); } - let font_info = &self.fonts[params.font_id.0]; + let bitmap_data = if params.is_emoji { + if let Ok(color) = self.rasterize_color(¶ms, glyph_bounds) { + color + } else { + let monochrome = self.rasterize_monochrome(params, glyph_bounds)?; + monochrome + .into_iter() + .flat_map(|pixel| [0, 0, 0, pixel]) + .collect::>() + } + } else { + self.rasterize_monochrome(params, glyph_bounds)? + }; + + Ok((glyph_bounds.size, bitmap_data)) + } + + fn rasterize_monochrome( + &self, + params: &RenderGlyphParams, + glyph_bounds: Bounds, + ) -> Result> { + let mut bitmap_data = + vec![0u8; glyph_bounds.size.width.0 as usize * glyph_bounds.size.height.0 as usize * 3]; + + let glyph_analysis = self.create_glyph_run_analysis(params)?; + unsafe { + glyph_analysis.CreateAlphaTexture( + // We're using cleartype not grayscale for monochrome is because it provides better quality + DWRITE_TEXTURE_CLEARTYPE_3x1, + &RECT { + left: glyph_bounds.origin.x.0, + top: glyph_bounds.origin.y.0, + right: glyph_bounds.size.width.0 + glyph_bounds.origin.x.0, + bottom: glyph_bounds.size.height.0 + glyph_bounds.origin.y.0, + }, + &mut bitmap_data, + )?; + } + + let bitmap_factory = self.components.bitmap_factory.resolve()?; + let bitmap = unsafe { + bitmap_factory.CreateBitmapFromMemory( + glyph_bounds.size.width.0 as u32, + glyph_bounds.size.height.0 as u32, + &GUID_WICPixelFormat24bppRGB, + glyph_bounds.size.width.0 as u32 * 3, + &bitmap_data, + ) + }?; + + let grayscale_bitmap = + unsafe { WICConvertBitmapSource(&GUID_WICPixelFormat8bppGray, &bitmap) }?; + + let mut bitmap_data = + vec![0u8; glyph_bounds.size.width.0 as usize * glyph_bounds.size.height.0 as usize]; + unsafe { + grayscale_bitmap.CopyPixels( + std::ptr::null() as _, + glyph_bounds.size.width.0 as u32, + &mut bitmap_data, + ) + }?; + + Ok(bitmap_data) + } + + fn rasterize_color( + &self, + params: &RenderGlyphParams, + glyph_bounds: Bounds, + ) -> Result> { + let bitmap_size = glyph_bounds.size; + let subpixel_shift = params + .subpixel_variant + .map(|v| v as f32 / SUBPIXEL_VARIANTS as f32); + let baseline_origin_x = subpixel_shift.x / params.scale_factor; + let baseline_origin_y = subpixel_shift.y / params.scale_factor; + + let transform = DWRITE_MATRIX { + m11: params.scale_factor, + m12: 0.0, + m21: 0.0, + m22: params.scale_factor, + dx: 0.0, + dy: 0.0, + }; + + let font = &self.fonts[params.font_id.0]; let glyph_id = [params.glyph_id.0 as u16]; let advance = [glyph_bounds.size.width.0 as f32]; let offset = [DWRITE_GLYPH_OFFSET { @@ -739,7 +945,7 @@ impl DirectWriteState { ascenderOffset: glyph_bounds.origin.y.0 as f32 / params.scale_factor, }]; let glyph_run = DWRITE_GLYPH_RUN { - fontFace: unsafe { std::mem::transmute_copy(&font_info.font_face) }, + fontFace: unsafe { std::mem::transmute_copy(&font.font_face) }, fontEmSize: params.font_size.0, glyphCount: 1, glyphIndices: glyph_id.as_ptr(), @@ -749,160 +955,254 @@ impl DirectWriteState { bidiLevel: 0, }; - // Add an extra pixel when the subpixel variant isn't zero to make room for anti-aliasing. - let mut bitmap_size = glyph_bounds.size; - if params.subpixel_variant.x > 0 { - bitmap_size.width += DevicePixels(1); - } - if params.subpixel_variant.y > 0 { - bitmap_size.height += DevicePixels(1); - } - let bitmap_size = bitmap_size; + // todo: support formats other than COLR + let color_enumerator = unsafe { + self.components.factory.TranslateColorGlyphRun( + Vector2::new(baseline_origin_x, baseline_origin_y), + &glyph_run, + None, + DWRITE_GLYPH_IMAGE_FORMATS_COLR, + DWRITE_MEASURING_MODE_NATURAL, + Some(&transform), + 0, + ) + }?; - let total_bytes; - let bitmap_format; - let render_target_property; - let bitmap_width; - let bitmap_height; - let bitmap_stride; - let bitmap_dpi; - if params.is_emoji { - total_bytes = bitmap_size.height.0 as usize * bitmap_size.width.0 as usize * 4; - bitmap_format = &GUID_WICPixelFormat32bppPBGRA; - render_target_property = get_render_target_property( - DXGI_FORMAT_B8G8R8A8_UNORM, - D2D1_ALPHA_MODE_PREMULTIPLIED, - ); - bitmap_width = bitmap_size.width.0 as u32; - bitmap_height = bitmap_size.height.0 as u32; - bitmap_stride = bitmap_size.width.0 as u32 * 4; - bitmap_dpi = 96.0; - } else { - total_bytes = bitmap_size.height.0 as usize * bitmap_size.width.0 as usize; - bitmap_format = &GUID_WICPixelFormat8bppAlpha; - render_target_property = - get_render_target_property(DXGI_FORMAT_A8_UNORM, D2D1_ALPHA_MODE_STRAIGHT); - bitmap_width = bitmap_size.width.0 as u32 * 2; - bitmap_height = bitmap_size.height.0 as u32 * 2; - bitmap_stride = bitmap_size.width.0 as u32; - bitmap_dpi = 192.0; + let mut glyph_layers = Vec::new(); + loop { + let color_run = unsafe { color_enumerator.GetCurrentRun() }?; + let color_run = unsafe { &*color_run }; + let image_format = color_run.glyphImageFormat & !DWRITE_GLYPH_IMAGE_FORMATS_TRUETYPE; + if image_format == DWRITE_GLYPH_IMAGE_FORMATS_COLR { + let color_analysis = unsafe { + self.components.factory.CreateGlyphRunAnalysis( + &color_run.Base.glyphRun as *const _, + Some(&transform), + DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC, + DWRITE_MEASURING_MODE_NATURAL, + DWRITE_GRID_FIT_MODE_DEFAULT, + DWRITE_TEXT_ANTIALIAS_MODE_CLEARTYPE, + baseline_origin_x, + baseline_origin_y, + ) + }?; + + let color_bounds = + unsafe { color_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_CLEARTYPE_3x1) }?; + + let color_size = size( + color_bounds.right - color_bounds.left, + color_bounds.bottom - color_bounds.top, + ); + if color_size.width > 0 && color_size.height > 0 { + let mut alpha_data = + vec![0u8; (color_size.width * color_size.height * 3) as usize]; + unsafe { + color_analysis.CreateAlphaTexture( + DWRITE_TEXTURE_CLEARTYPE_3x1, + &color_bounds, + &mut alpha_data, + ) + }?; + + let run_color = { + let run_color = color_run.Base.runColor; + Rgba { + r: run_color.r, + g: run_color.g, + b: run_color.b, + a: run_color.a, + } + }; + let bounds = bounds(point(color_bounds.left, color_bounds.top), color_size); + let alpha_data = alpha_data + .chunks_exact(3) + .flat_map(|chunk| [chunk[0], chunk[1], chunk[2], 255]) + .collect::>(); + glyph_layers.push(GlyphLayerTexture::new( + &self.components.gpu_state, + run_color, + bounds, + &alpha_data, + )?); + } + } + + let has_next = unsafe { color_enumerator.MoveNext() } + .map(|e| e.as_bool()) + .unwrap_or(false); + if !has_next { + break; + } } - let bitmap_factory = self.components.bitmap_factory.resolve()?; - unsafe { - let bitmap = bitmap_factory.CreateBitmap( - bitmap_width, - bitmap_height, - bitmap_format, - WICBitmapCacheOnLoad, - )?; - let render_target = self - .components - .d2d1_factory - .CreateWicBitmapRenderTarget(&bitmap, &render_target_property)?; - let brush = render_target.CreateSolidColorBrush(&BRUSH_COLOR, None)?; - let subpixel_shift = params - .subpixel_variant - .map(|v| v as f32 / SUBPIXEL_VARIANTS as f32); - let baseline_origin = Vector2 { - X: subpixel_shift.x / params.scale_factor, - Y: subpixel_shift.y / params.scale_factor, + let gpu_state = &self.components.gpu_state; + let params_buffer = { + let desc = D3D11_BUFFER_DESC { + ByteWidth: std::mem::size_of::() as u32, + Usage: D3D11_USAGE_DYNAMIC, + BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32, + CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, + MiscFlags: 0, + StructureByteStride: 0, }; - // This `cast()` action here should never fail since we are running on Win10+, and - // ID2D1DeviceContext4 requires Win8+ - let render_target = render_target.cast::().unwrap(); - render_target.SetUnitMode(D2D1_UNIT_MODE_DIPS); - render_target.SetDpi( - bitmap_dpi * params.scale_factor, - bitmap_dpi * params.scale_factor, - ); - render_target.SetTextRenderingParams(&self.components.render_context.params); - render_target.BeginDraw(); + let mut buffer = None; + unsafe { + gpu_state + .device + .CreateBuffer(&desc, None, Some(&mut buffer)) + }?; + [buffer] + }; - if params.is_emoji { - // WARN: only DWRITE_GLYPH_IMAGE_FORMATS_COLR has been tested - let enumerator = self.components.factory.TranslateColorGlyphRun( - baseline_origin, - &glyph_run as _, - None, - DWRITE_GLYPH_IMAGE_FORMATS_COLR - | DWRITE_GLYPH_IMAGE_FORMATS_SVG - | DWRITE_GLYPH_IMAGE_FORMATS_PNG - | DWRITE_GLYPH_IMAGE_FORMATS_JPEG - | DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8, - DWRITE_MEASURING_MODE_NATURAL, - None, + let render_target_texture = { + let mut texture = None; + let desc = D3D11_TEXTURE2D_DESC { + Width: bitmap_size.width.0 as u32, + Height: bitmap_size.height.0 as u32, + MipLevels: 1, + ArraySize: 1, + Format: DXGI_FORMAT_B8G8R8A8_UNORM, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_DEFAULT, + BindFlags: D3D11_BIND_RENDER_TARGET.0 as u32, + CPUAccessFlags: 0, + MiscFlags: 0, + }; + unsafe { + gpu_state + .device + .CreateTexture2D(&desc, None, Some(&mut texture)) + }?; + texture.unwrap() + }; + + let render_target_view = { + let desc = D3D11_RENDER_TARGET_VIEW_DESC { + Format: DXGI_FORMAT_B8G8R8A8_UNORM, + ViewDimension: D3D11_RTV_DIMENSION_TEXTURE2D, + Anonymous: D3D11_RENDER_TARGET_VIEW_DESC_0 { + Texture2D: D3D11_TEX2D_RTV { MipSlice: 0 }, + }, + }; + let mut rtv = None; + unsafe { + gpu_state.device.CreateRenderTargetView( + &render_target_texture, + Some(&desc), + Some(&mut rtv), + ) + }?; + [rtv] + }; + + let staging_texture = { + let mut texture = None; + let desc = D3D11_TEXTURE2D_DESC { + Width: bitmap_size.width.0 as u32, + Height: bitmap_size.height.0 as u32, + MipLevels: 1, + ArraySize: 1, + Format: DXGI_FORMAT_B8G8R8A8_UNORM, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_STAGING, + BindFlags: 0, + CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32, + MiscFlags: 0, + }; + unsafe { + gpu_state + .device + .CreateTexture2D(&desc, None, Some(&mut texture)) + }?; + texture.unwrap() + }; + + let device_context = &gpu_state.device_context; + unsafe { device_context.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP) }; + unsafe { device_context.VSSetShader(&gpu_state.vertex_shader, None) }; + unsafe { device_context.PSSetShader(&gpu_state.pixel_shader, None) }; + unsafe { device_context.VSSetConstantBuffers(0, Some(¶ms_buffer)) }; + unsafe { device_context.PSSetConstantBuffers(0, Some(¶ms_buffer)) }; + unsafe { device_context.OMSetRenderTargets(Some(&render_target_view), None) }; + unsafe { device_context.PSSetSamplers(0, Some(&gpu_state.sampler)) }; + unsafe { device_context.OMSetBlendState(&gpu_state.blend_state, None, 0xffffffff) }; + + for layer in glyph_layers { + let params = GlyphLayerTextureParams { + run_color: layer.run_color, + bounds: layer.bounds, + }; + unsafe { + let mut dest = std::mem::zeroed(); + gpu_state.device_context.Map( + params_buffer[0].as_ref().unwrap(), 0, + D3D11_MAP_WRITE_DISCARD, + 0, + Some(&mut dest), )?; - while enumerator.MoveNext().is_ok() { - let Ok(color_glyph) = enumerator.GetCurrentRun() else { - break; - }; - let color_glyph = &*color_glyph; - let brush_color = translate_color(&color_glyph.Base.runColor); - brush.SetColor(&brush_color); - match color_glyph.glyphImageFormat { - DWRITE_GLYPH_IMAGE_FORMATS_PNG - | DWRITE_GLYPH_IMAGE_FORMATS_JPEG - | DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8 => render_target - .DrawColorBitmapGlyphRun( - color_glyph.glyphImageFormat, - baseline_origin, - &color_glyph.Base.glyphRun, - color_glyph.measuringMode, - D2D1_COLOR_BITMAP_GLYPH_SNAP_OPTION_DEFAULT, - ), - DWRITE_GLYPH_IMAGE_FORMATS_SVG => render_target.DrawSvgGlyphRun( - baseline_origin, - &color_glyph.Base.glyphRun, - &brush, - None, - color_glyph.Base.paletteIndex as u32, - color_glyph.measuringMode, - ), - _ => render_target.DrawGlyphRun( - baseline_origin, - &color_glyph.Base.glyphRun, - Some(color_glyph.Base.glyphRunDescription as *const _), - &brush, - color_glyph.measuringMode, - ), - } - } - } else { - render_target.DrawGlyphRun( - baseline_origin, - &glyph_run, - None, - &brush, - DWRITE_MEASURING_MODE_NATURAL, - ); - } - render_target.EndDraw(None, None)?; + std::ptr::copy_nonoverlapping(¶ms as *const _, dest.pData as *mut _, 1); + gpu_state + .device_context + .Unmap(params_buffer[0].as_ref().unwrap(), 0); + }; - let mut raw_data = vec![0u8; total_bytes]; - if params.is_emoji { - bitmap.CopyPixels(std::ptr::null() as _, bitmap_stride, &mut raw_data)?; - // Convert from BGRA with premultiplied alpha to BGRA with straight alpha. - for pixel in raw_data.chunks_exact_mut(4) { - let a = pixel[3] as f32 / 255.; - pixel[0] = (pixel[0] as f32 / a) as u8; - pixel[1] = (pixel[1] as f32 / a) as u8; - pixel[2] = (pixel[2] as f32 / a) as u8; - } - } else { - let scaler = bitmap_factory.CreateBitmapScaler()?; - scaler.Initialize( - &bitmap, - bitmap_size.width.0 as u32, - bitmap_size.height.0 as u32, - WICBitmapInterpolationModeHighQualityCubic, - )?; - scaler.CopyPixels(std::ptr::null() as _, bitmap_stride, &mut raw_data)?; - } - Ok((bitmap_size, raw_data)) + let texture = [Some(layer.texture_view)]; + unsafe { device_context.PSSetShaderResources(0, Some(&texture)) }; + + let viewport = [D3D11_VIEWPORT { + TopLeftX: layer.bounds.origin.x as f32, + TopLeftY: layer.bounds.origin.y as f32, + Width: layer.bounds.size.width as f32, + Height: layer.bounds.size.height as f32, + MinDepth: 0.0, + MaxDepth: 1.0, + }]; + unsafe { device_context.RSSetViewports(Some(&viewport)) }; + + unsafe { device_context.Draw(4, 0) }; } + + unsafe { device_context.CopyResource(&staging_texture, &render_target_texture) }; + + let mapped_data = { + let mut mapped_data = D3D11_MAPPED_SUBRESOURCE::default(); + unsafe { + device_context.Map( + &staging_texture, + 0, + D3D11_MAP_READ, + 0, + Some(&mut mapped_data), + ) + }?; + mapped_data + }; + let mut rasterized = + vec![0u8; (bitmap_size.width.0 as u32 * bitmap_size.height.0 as u32 * 4) as usize]; + + for y in 0..bitmap_size.height.0 as usize { + let width = bitmap_size.width.0 as usize; + unsafe { + std::ptr::copy_nonoverlapping::( + (mapped_data.pData as *const u8).byte_add(mapped_data.RowPitch as usize * y), + rasterized + .as_mut_ptr() + .byte_add(width * y * std::mem::size_of::()), + width * std::mem::size_of::(), + ) + }; + } + + Ok(rasterized) } fn get_typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { @@ -976,6 +1276,84 @@ impl Drop for DirectWriteState { } } +struct GlyphLayerTexture { + run_color: Rgba, + bounds: Bounds, + texture_view: ID3D11ShaderResourceView, + // holding on to the texture to not RAII drop it + _texture: ID3D11Texture2D, +} + +impl GlyphLayerTexture { + pub fn new( + gpu_state: &GPUState, + run_color: Rgba, + bounds: Bounds, + alpha_data: &[u8], + ) -> Result { + let texture_size = bounds.size; + + let desc = D3D11_TEXTURE2D_DESC { + Width: texture_size.width as u32, + Height: texture_size.height as u32, + MipLevels: 1, + ArraySize: 1, + Format: DXGI_FORMAT_R8G8B8A8_UNORM, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_DEFAULT, + BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32, + CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, + MiscFlags: 0, + }; + + let texture = { + let mut texture: Option = None; + unsafe { + gpu_state + .device + .CreateTexture2D(&desc, None, Some(&mut texture))? + }; + texture.unwrap() + }; + let texture_view = { + let mut view: Option = None; + unsafe { + gpu_state + .device + .CreateShaderResourceView(&texture, None, Some(&mut view))? + }; + view.unwrap() + }; + + unsafe { + gpu_state.device_context.UpdateSubresource( + &texture, + 0, + None, + alpha_data.as_ptr() as _, + (texture_size.width * 4) as u32, + 0, + ) + }; + + Ok(GlyphLayerTexture { + run_color, + bounds, + texture_view, + _texture: texture, + }) + } +} + +#[repr(C)] +struct GlyphLayerTextureParams { + bounds: Bounds, + run_color: Rgba, +} + struct TextRendererWrapper(pub IDWriteTextRenderer); impl TextRendererWrapper { @@ -1470,16 +1848,6 @@ fn get_name(string: IDWriteLocalizedStrings, locale: &str) -> Result { Ok(String::from_utf16_lossy(&name_vec[..name_length])) } -#[inline] -fn translate_color(color: &DWRITE_COLOR_F) -> D2D1_COLOR_F { - D2D1_COLOR_F { - r: color.r, - g: color.g, - b: color.b, - a: color.a, - } -} - fn get_system_ui_font_name() -> SharedString { unsafe { let mut info: LOGFONTW = std::mem::zeroed(); @@ -1504,24 +1872,6 @@ fn get_system_ui_font_name() -> SharedString { } } -#[inline] -fn get_render_target_property( - pixel_format: DXGI_FORMAT, - alpha_mode: D2D1_ALPHA_MODE, -) -> D2D1_RENDER_TARGET_PROPERTIES { - D2D1_RENDER_TARGET_PROPERTIES { - r#type: D2D1_RENDER_TARGET_TYPE_DEFAULT, - pixelFormat: D2D1_PIXEL_FORMAT { - format: pixel_format, - alphaMode: alpha_mode, - }, - dpiX: 96.0, - dpiY: 96.0, - usage: D2D1_RENDER_TARGET_USAGE_NONE, - minLevel: D2D1_FEATURE_LEVEL_DEFAULT, - } -} - // One would think that with newer DirectWrite method: IDWriteFontFace4::GetGlyphImageFormats // but that doesn't seem to work for some glyphs, say ❤ fn is_color_glyph( @@ -1561,12 +1911,6 @@ fn is_color_glyph( } const DEFAULT_LOCALE_NAME: PCWSTR = windows::core::w!("en-US"); -const BRUSH_COLOR: D2D1_COLOR_F = D2D1_COLOR_F { - r: 1.0, - g: 1.0, - b: 1.0, - a: 1.0, -}; #[cfg(test)] mod tests { diff --git a/crates/gpui/src/platform/windows/directx_atlas.rs b/crates/gpui/src/platform/windows/directx_atlas.rs index 988943c766..6bced4c11d 100644 --- a/crates/gpui/src/platform/windows/directx_atlas.rs +++ b/crates/gpui/src/platform/windows/directx_atlas.rs @@ -7,7 +7,7 @@ use windows::Win32::Graphics::{ D3D11_USAGE_DEFAULT, ID3D11Device, ID3D11DeviceContext, ID3D11ShaderResourceView, ID3D11Texture2D, }, - Dxgi::Common::{DXGI_FORMAT_A8_UNORM, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC}, + Dxgi::Common::*, }; use crate::{ @@ -167,7 +167,7 @@ impl DirectXAtlasState { let bytes_per_pixel; match kind { AtlasTextureKind::Monochrome => { - pixel_format = DXGI_FORMAT_A8_UNORM; + pixel_format = DXGI_FORMAT_R8_UNORM; bind_flag = D3D11_BIND_SHADER_RESOURCE; bytes_per_pixel = 1; } diff --git a/crates/gpui/src/platform/windows/directx_renderer.rs b/crates/gpui/src/platform/windows/directx_renderer.rs index fcd52b6956..72cc12a5b4 100644 --- a/crates/gpui/src/platform/windows/directx_renderer.rs +++ b/crates/gpui/src/platform/windows/directx_renderer.rs @@ -42,8 +42,8 @@ pub(crate) struct DirectXRenderer { pub(crate) struct DirectXDevices { adapter: IDXGIAdapter1, dxgi_factory: IDXGIFactory6, - device: ID3D11Device, - device_context: ID3D11DeviceContext, + pub(crate) device: ID3D11Device, + pub(crate) device_context: ID3D11DeviceContext, dxgi_device: Option, } @@ -187,7 +187,7 @@ impl DirectXRenderer { self.resources.viewport[0].Width, self.resources.viewport[0].Height, ], - ..Default::default() + _pad: 0, }], )?; unsafe { @@ -1441,7 +1441,7 @@ fn report_live_objects(device: &ID3D11Device) -> Result<()> { const BUFFER_COUNT: usize = 3; -mod shader_resources { +pub(crate) mod shader_resources { use anyhow::Result; #[cfg(debug_assertions)] @@ -1454,7 +1454,7 @@ mod shader_resources { }; #[derive(Copy, Clone, Debug, Eq, PartialEq)] - pub(super) enum ShaderModule { + pub(crate) enum ShaderModule { Quad, Shadow, Underline, @@ -1462,15 +1462,16 @@ mod shader_resources { PathSprite, MonochromeSprite, PolychromeSprite, + EmojiRasterization, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] - pub(super) enum ShaderTarget { + pub(crate) enum ShaderTarget { Vertex, Fragment, } - pub(super) struct RawShaderBytes<'t> { + pub(crate) struct RawShaderBytes<'t> { inner: &'t [u8], #[cfg(debug_assertions)] @@ -1478,7 +1479,7 @@ mod shader_resources { } impl<'t> RawShaderBytes<'t> { - pub(super) fn new(module: ShaderModule, target: ShaderTarget) -> Result { + pub(crate) fn new(module: ShaderModule, target: ShaderTarget) -> Result { #[cfg(not(debug_assertions))] { Ok(Self::from_bytes(module, target)) @@ -1496,7 +1497,7 @@ mod shader_resources { } } - pub(super) fn as_bytes(&'t self) -> &'t [u8] { + pub(crate) fn as_bytes(&'t self) -> &'t [u8] { self.inner } @@ -1531,6 +1532,10 @@ mod shader_resources { ShaderTarget::Vertex => POLYCHROME_SPRITE_VERTEX_BYTES, ShaderTarget::Fragment => POLYCHROME_SPRITE_FRAGMENT_BYTES, }, + ShaderModule::EmojiRasterization => match target { + ShaderTarget::Vertex => EMOJI_RASTERIZATION_VERTEX_BYTES, + ShaderTarget::Fragment => EMOJI_RASTERIZATION_FRAGMENT_BYTES, + }, }; Self { inner: bytes } } @@ -1539,6 +1544,12 @@ mod shader_resources { #[cfg(debug_assertions)] pub(super) fn build_shader_blob(entry: ShaderModule, target: ShaderTarget) -> Result { unsafe { + let shader_name = if matches!(entry, ShaderModule::EmojiRasterization) { + "color_text_raster.hlsl" + } else { + "shaders.hlsl" + }; + let entry = format!( "{}_{}\0", entry.as_str(), @@ -1555,7 +1566,7 @@ mod shader_resources { let mut compile_blob = None; let mut error_blob = None; let shader_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("src/platform/windows/shaders.hlsl") + .join(&format!("src/platform/windows/{}", shader_name)) .canonicalize()?; let entry_point = PCSTR::from_raw(entry.as_ptr()); @@ -1601,6 +1612,7 @@ mod shader_resources { ShaderModule::PathSprite => "path_sprite", ShaderModule::MonochromeSprite => "monochrome_sprite", ShaderModule::PolychromeSprite => "polychrome_sprite", + ShaderModule::EmojiRasterization => "emoji_rasterization", } } } diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 8433e29c6d..bc09cc199d 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -44,6 +44,7 @@ pub(crate) struct WindowsPlatform { drop_target_helper: IDropTargetHelper, validation_number: usize, main_thread_id_win32: u32, + disable_direct_composition: bool, } pub(crate) struct WindowsPlatformState { @@ -93,14 +94,18 @@ impl WindowsPlatform { main_thread_id_win32, validation_number, )); + let disable_direct_composition = std::env::var(DISABLE_DIRECT_COMPOSITION) + .is_ok_and(|value| value == "true" || value == "1"); let background_executor = BackgroundExecutor::new(dispatcher.clone()); let foreground_executor = ForegroundExecutor::new(dispatcher); + let directx_devices = DirectXDevices::new(disable_direct_composition) + .context("Unable to init directx devices.")?; let bitmap_factory = ManuallyDrop::new(unsafe { CoCreateInstance(&CLSID_WICImagingFactory, None, CLSCTX_INPROC_SERVER) .context("Error creating bitmap factory.")? }); let text_system = Arc::new( - DirectWriteTextSystem::new(&bitmap_factory) + DirectWriteTextSystem::new(&directx_devices, &bitmap_factory) .context("Error creating DirectWriteTextSystem")?, ); let drop_target_helper: IDropTargetHelper = unsafe { @@ -120,6 +125,7 @@ impl WindowsPlatform { background_executor, foreground_executor, text_system, + disable_direct_composition, windows_version, bitmap_factory, drop_target_helper, @@ -184,6 +190,7 @@ impl WindowsPlatform { validation_number: self.validation_number, main_receiver: self.main_receiver.clone(), main_thread_id_win32: self.main_thread_id_win32, + disable_direct_composition: self.disable_direct_composition, } } @@ -715,6 +722,7 @@ pub(crate) struct WindowCreationInfo { pub(crate) validation_number: usize, pub(crate) main_receiver: flume::Receiver, pub(crate) main_thread_id_win32: u32, + pub(crate) disable_direct_composition: bool, } fn open_target(target: &str) { diff --git a/crates/gpui/src/platform/windows/shaders.hlsl b/crates/gpui/src/platform/windows/shaders.hlsl index 954040c4c3..25830e4b6c 100644 --- a/crates/gpui/src/platform/windows/shaders.hlsl +++ b/crates/gpui/src/platform/windows/shaders.hlsl @@ -1,6 +1,6 @@ cbuffer GlobalParams: register(b0) { float2 global_viewport_size; - uint2 _global_pad; + uint2 _pad; }; Texture2D t_sprite: register(t0); @@ -1069,6 +1069,7 @@ struct MonochromeSpriteFragmentInput { float4 position: SV_Position; float2 tile_position: POSITION; nointerpolation float4 color: COLOR; + float4 clip_distance: SV_ClipDistance; }; StructuredBuffer mono_sprites: register(t1); @@ -1091,10 +1092,8 @@ MonochromeSpriteVertexOutput monochrome_sprite_vertex(uint vertex_id: SV_VertexI } float4 monochrome_sprite_fragment(MonochromeSpriteFragmentInput input): SV_Target { - float4 sample = t_sprite.Sample(s_sprite, input.tile_position); - float4 color = input.color; - color.a *= sample.a; - return color; + float sample = t_sprite.Sample(s_sprite, input.tile_position).r; + return float4(input.color.rgb, input.color.a * sample); } /* diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index 1141e93565..68b667569b 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -360,6 +360,7 @@ impl WindowsWindow { validation_number, main_receiver, main_thread_id_win32, + disable_direct_composition, } = creation_info; let classname = register_wnd_class(icon); let hide_title_bar = params @@ -375,8 +376,6 @@ impl WindowsWindow { .map(|title| title.as_ref()) .unwrap_or(""), ); - let disable_direct_composition = std::env::var(DISABLE_DIRECT_COMPOSITION) - .is_ok_and(|value| value == "true" || value == "1"); let (mut dwexstyle, dwstyle) = if params.kind == WindowKind::PopUp { (WS_EX_TOOLWINDOW, WINDOW_STYLE(0x0)) From faa45c53d7754cfdd91d2f7edd3c786abc703ec7 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 1 Aug 2025 15:08:15 -0300 Subject: [PATCH 021/693] onboarding: Add design adjustments (#35480) Release Notes: - N/A --------- Co-authored-by: Anthony --- assets/icons/editor_atom.svg | 3 + assets/icons/editor_cursor.svg | 9 ++ assets/icons/editor_emacs.svg | 10 ++ assets/icons/editor_jet_brains.svg | 3 + assets/icons/editor_sublime.svg | 5 + assets/icons/editor_vs_code.svg | 3 + assets/icons/shield_check.svg | 4 + crates/icons/src/icons.rs | 7 + crates/onboarding/src/ai_setup_page.rs | 31 ++-- crates/onboarding/src/basics_page.rs | 145 ++++++++---------- crates/onboarding/src/editing_page.rs | 4 +- crates/onboarding/src/onboarding.rs | 67 +++++++- crates/onboarding/src/theme_preview.rs | 39 ++--- crates/onboarding/src/welcome.rs | 66 +++++++- crates/ui/src/components/badge.rs | 9 +- .../ui/src/components/button/button_like.rs | 4 +- .../ui/src/components/button/toggle_button.rs | 124 +++++++++------ 17 files changed, 348 insertions(+), 185 deletions(-) create mode 100644 assets/icons/editor_atom.svg create mode 100644 assets/icons/editor_cursor.svg create mode 100644 assets/icons/editor_emacs.svg create mode 100644 assets/icons/editor_jet_brains.svg create mode 100644 assets/icons/editor_sublime.svg create mode 100644 assets/icons/editor_vs_code.svg create mode 100644 assets/icons/shield_check.svg diff --git a/assets/icons/editor_atom.svg b/assets/icons/editor_atom.svg new file mode 100644 index 0000000000..cc5fa83843 --- /dev/null +++ b/assets/icons/editor_atom.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/editor_cursor.svg b/assets/icons/editor_cursor.svg new file mode 100644 index 0000000000..338697be8a --- /dev/null +++ b/assets/icons/editor_cursor.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/editor_emacs.svg b/assets/icons/editor_emacs.svg new file mode 100644 index 0000000000..951d7b2be1 --- /dev/null +++ b/assets/icons/editor_emacs.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/editor_jet_brains.svg b/assets/icons/editor_jet_brains.svg new file mode 100644 index 0000000000..7d9cf0c65c --- /dev/null +++ b/assets/icons/editor_jet_brains.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/editor_sublime.svg b/assets/icons/editor_sublime.svg new file mode 100644 index 0000000000..95a04f6b54 --- /dev/null +++ b/assets/icons/editor_sublime.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/editor_vs_code.svg b/assets/icons/editor_vs_code.svg new file mode 100644 index 0000000000..2a71ad52af --- /dev/null +++ b/assets/icons/editor_vs_code.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/shield_check.svg b/assets/icons/shield_check.svg new file mode 100644 index 0000000000..6e58c31468 --- /dev/null +++ b/assets/icons/shield_check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 7552060be4..fe68cdd2d6 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -107,6 +107,12 @@ pub enum IconName { Disconnected, DocumentText, Download, + EditorAtom, + EditorCursor, + EditorEmacs, + EditorJetBrains, + EditorSublime, + EditorVsCode, Ellipsis, EllipsisVertical, Envelope, @@ -229,6 +235,7 @@ pub enum IconName { Server, Settings, SettingsAlt, + ShieldCheck, Shift, Slash, SlashSquare, diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index c33dcb9ad1..2f031e7bb8 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -43,6 +43,8 @@ fn render_llm_provider_section( } fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement { + let privacy_badge = || Badge::new("Privacy").icon(IconName::ShieldCheck); + v_flex() .relative() .pt_2() @@ -71,7 +73,7 @@ fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement { .size(IconSize::XSmall), ), ) - .child(Badge::new("PRIVACY").icon(IconName::FileLock)), + .child(privacy_badge()), ) .child( Label::new("Re-enable it any time in Settings.") @@ -85,22 +87,17 @@ fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement { .justify_between() .child(Label::new("We don't train models using your data")) .child( - h_flex() - .gap_1() - .child(Badge::new("Privacy").icon(IconName::FileLock)) - .child( - Button::new("learn_more", "Learn More") - .style(ButtonStyle::Outlined) - .label_size(LabelSize::Small) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .on_click(|_, _, cx| { - cx.open_url( - "https://zed.dev/docs/ai/privacy-and-security", - ); - }), - ), + h_flex().gap_1().child(privacy_badge()).child( + Button::new("learn_more", "Learn More") + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .on_click(|_, _, cx| { + cx.open_url("https://zed.dev/docs/ai/privacy-and-security"); + }), + ), ), ) .child( diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index aac8241251..327256968a 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -48,7 +48,11 @@ fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement { let theme_registry = ThemeRegistry::global(cx); let current_theme_name = theme_selection.theme(appearance); - let theme_mode = theme_selection.mode(); + let theme_mode = theme_selection.mode().unwrap_or_default(); + + // let theme_mode = theme_selection.mode(); + // TODO: Clean this up once the "System" button inside the + // toggle button group is done let selected_index = match appearance { Appearance::Light => 0, @@ -72,8 +76,28 @@ fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement { let is_selected = theme.name == current_theme_name; let name = theme.name.clone(); let colors = cx.theme().colors(); + v_flex() .id(name.clone()) + .w_full() + .items_center() + .gap_1() + .child( + div() + .w_full() + .border_2() + .border_color(colors.border_transparent) + .rounded(ThemePreviewTile::CORNER_RADIUS) + .map(|this| { + if is_selected { + this.border_color(colors.border_selected) + } else { + this.opacity(0.8).hover(|s| s.border_color(colors.border)) + } + }) + .child(ThemePreviewTile::new(theme.clone(), theme_seed)), + ) + .child(Label::new(name).color(Color::Muted).size(LabelSize::Small)) .on_click({ let theme_name = theme.name.clone(); move |_, _, cx| { @@ -84,84 +108,45 @@ fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement { }); } }) - .flex_1() - .child( - div() - .border_2() - .border_color(colors.border_transparent) - .rounded(ThemePreviewTile::CORNER_RADIUS) - .hover(|mut style| { - if !is_selected { - style.border_color = Some(colors.element_hover); - } - style - }) - .when(is_selected, |this| { - this.border_color(colors.border_selected) - }) - .cursor_pointer() - .child(ThemePreviewTile::new(theme, theme_seed)), - ) - .child( - h_flex() - .justify_center() - .items_baseline() - .child(Label::new(name).color(Color::Muted)), - ) }); return v_flex() + .gap_2() .child( h_flex().justify_between().child(Label::new("Theme")).child( - h_flex() - .gap_2() - .child( - ToggleButtonGroup::single_row( - "theme-selector-onboarding-dark-light", - [ - ToggleButtonSimple::new("Light", { - let appearance_state = appearance_state.clone(); - move |_, _, cx| { - write_appearance_change( - &appearance_state, - Appearance::Light, - cx, - ); - } - }), - ToggleButtonSimple::new("Dark", { - let appearance_state = appearance_state.clone(); - move |_, _, cx| { - write_appearance_change( - &appearance_state, - Appearance::Dark, - cx, - ); - } - }), - ], - ) - .selected_index(selected_index) - .style(ui::ToggleButtonGroupStyle::Outlined) - .button_width(rems_from_px(64.)), - ) - .child( - ToggleButtonGroup::single_row( - "theme-selector-onboarding-system", - [ToggleButtonSimple::new("System", { - let theme = theme_selection.clone(); - move |_, _, cx| { - toggle_system_theme_mode(theme.clone(), appearance, cx); - } - })], - ) - .selected_index((theme_mode != Some(ThemeMode::System)) as usize) - .style(ui::ToggleButtonGroupStyle::Outlined) - .button_width(rems_from_px(64.)), - ), + ToggleButtonGroup::single_row( + "theme-selector-onboarding-dark-light", + [ + ToggleButtonSimple::new("Light", { + let appearance_state = appearance_state.clone(); + move |_, _, cx| { + write_appearance_change(&appearance_state, Appearance::Light, cx); + } + }), + ToggleButtonSimple::new("Dark", { + let appearance_state = appearance_state.clone(); + move |_, _, cx| { + write_appearance_change(&appearance_state, Appearance::Dark, cx); + } + }), + // TODO: Properly put the System back as a button within this group + // Currently, given "System" is not an option in the Appearance enum, + // this button doesn't get selected + ToggleButtonSimple::new("System", { + let theme = theme_selection.clone(); + move |_, _, cx| { + toggle_system_theme_mode(theme.clone(), appearance, cx); + } + }) + .selected(theme_mode == ThemeMode::System), + ], + ) + .selected_index(selected_index) + .style(ui::ToggleButtonGroupStyle::Outlined) + .button_width(rems_from_px(64.)), ), ) - .child(h_flex().justify_between().children(theme_previews)); + .child(h_flex().gap_4().justify_between().children(theme_previews)); fn write_appearance_change( appearance_state: &Entity, @@ -210,7 +195,6 @@ fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement { }; ThemeSelection::Dynamic { mode, light, dark } } - ThemeSelection::Dynamic { mode: _, light, @@ -311,30 +295,31 @@ pub(crate) fn render_basics_page(window: &mut Window, cx: &mut App) -> impl Into ToggleButtonGroup::two_rows( "multiple_row_test", [ - ToggleButtonWithIcon::new("VS Code", IconName::AiZed, |_, _, cx| { + ToggleButtonWithIcon::new("VS Code", IconName::EditorVsCode, |_, _, cx| { write_keymap_base(BaseKeymap::VSCode, cx); }), - ToggleButtonWithIcon::new("Jetbrains", IconName::AiZed, |_, _, cx| { + ToggleButtonWithIcon::new("Jetbrains", IconName::EditorJetBrains, |_, _, cx| { write_keymap_base(BaseKeymap::JetBrains, cx); }), - ToggleButtonWithIcon::new("Sublime Text", IconName::AiZed, |_, _, cx| { + ToggleButtonWithIcon::new("Sublime Text", IconName::EditorSublime, |_, _, cx| { write_keymap_base(BaseKeymap::SublimeText, cx); }), ], [ - ToggleButtonWithIcon::new("Atom", IconName::AiZed, |_, _, cx| { + ToggleButtonWithIcon::new("Atom", IconName::EditorAtom, |_, _, cx| { write_keymap_base(BaseKeymap::Atom, cx); }), - ToggleButtonWithIcon::new("Emacs", IconName::AiZed, |_, _, cx| { + ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| { write_keymap_base(BaseKeymap::Emacs, cx); }), - ToggleButtonWithIcon::new("Cursor (Beta)", IconName::AiZed, |_, _, cx| { + ToggleButtonWithIcon::new("Cursor (Beta)", IconName::EditorCursor, |_, _, cx| { write_keymap_base(BaseKeymap::Cursor, cx); }), ], ) .when_some(base_keymap, |this, base_keymap| this.selected_index(base_keymap)) - .button_width(rems_from_px(230.)) + .button_width(rems_from_px(216.)) + .size(ui::ToggleButtonGroupSize::Medium) .style(ui::ToggleButtonGroupStyle::Outlined) ), ) diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 759d557805..33d0955d19 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -143,7 +143,7 @@ fn render_import_settings_section() -> impl IntoElement { .gap_1p5() .px_1() .child( - Icon::new(IconName::Sparkle) + Icon::new(IconName::EditorVsCode) .color(Color::Muted) .size(IconSize::XSmall), ) @@ -169,7 +169,7 @@ fn render_import_settings_section() -> impl IntoElement { .gap_1p5() .px_1() .child( - Icon::new(IconName::Sparkle) + Icon::new(IconName::EditorCursor) .color(Color::Muted) .size(IconSize::XSmall), ) diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 2ae07b7cd5..21fbeb5d97 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -14,8 +14,8 @@ use serde::Deserialize; use settings::{SettingsStore, VsCodeSettingsSource}; use std::sync::Arc; use ui::{ - Avatar, FluentBuilder, Headline, KeyBinding, ParentElement as _, StatefulInteractiveElement, - Vector, VectorName, prelude::*, rems_from_px, + Avatar, ButtonLike, FluentBuilder, Headline, KeyBinding, ParentElement as _, + StatefulInteractiveElement, Vector, VectorName, prelude::*, rems_from_px, }; use workspace::{ AppState, Workspace, WorkspaceId, @@ -344,12 +344,73 @@ impl Onboarding { .into_element(), ]), ) - .child(Button::new("skip_all", "Skip All")), + .child( + ButtonLike::new("skip_all") + .child(Label::new("Skip All").ml_1()) + .on_click(|_, _, cx| { + with_active_or_new_workspace( + cx, + |workspace, window, cx| { + let Some((onboarding_id, onboarding_idx)) = + workspace + .active_pane() + .read(cx) + .items() + .enumerate() + .find_map(|(idx, item)| { + let _ = + item.downcast::()?; + Some((item.item_id(), idx)) + }) + else { + return; + }; + + workspace.active_pane().update(cx, |pane, cx| { + // Get the index here to get around the borrow checker + let idx = pane.items().enumerate().find_map( + |(idx, item)| { + let _ = + item.downcast::()?; + Some(idx) + }, + ); + + if let Some(idx) = idx { + pane.activate_item( + idx, true, true, window, cx, + ); + } else { + let item = + Box::new(WelcomePage::new(window, cx)); + pane.add_item( + item, + true, + true, + Some(onboarding_idx), + window, + cx, + ); + } + + pane.remove_item( + onboarding_id, + false, + false, + window, + cx, + ); + }); + }, + ); + }), + ), ), ) .child( if let Some(user) = self.user_store.read(cx).current_user() { h_flex() + .pl_1p5() .gap_2() .child(Avatar::new(user.avatar_uri.clone())) .child(Label::new(user.github_login.clone())) diff --git a/crates/onboarding/src/theme_preview.rs b/crates/onboarding/src/theme_preview.rs index 73b540bd40..d51511b7f4 100644 --- a/crates/onboarding/src/theme_preview.rs +++ b/crates/onboarding/src/theme_preview.rs @@ -35,7 +35,7 @@ impl RenderOnce for ThemePreviewTile { let item_skeleton = |w: Length, h: Pixels, bg: Hsla| div().w(w).h(h).rounded_full().bg(bg); - let skeleton_height = px(4.); + let skeleton_height = px(2.); let sidebar_seeded_width = |seed: f32, index: usize| { let value = (seed * 1000.0 + index as f32 * 10.0).sin() * 0.5 + 0.5; @@ -62,12 +62,10 @@ impl RenderOnce for ThemePreviewTile { .border_color(color.border_transparent) .bg(color.panel_background) .child( - div() + v_flex() .p_2() - .flex() - .flex_col() .size_full() - .gap(px(4.)) + .gap_1() .children(sidebar_skeleton), ); @@ -143,32 +141,19 @@ impl RenderOnce for ThemePreviewTile { v_flex() .size_full() .p_1() - .gap(px(6.)) + .gap_1p5() .children(lines) .into_any_element() }; - let pane = div() - .h_full() - .flex_grow() - .flex() - .flex_col() - // .child( - // div() - // .w_full() - // .border_color(color.border) - // .border_b(px(1.)) - // .h(relative(0.1)) - // .bg(color.tab_bar_background), - // ) - .child( - div() - .size_full() - .overflow_hidden() - .bg(color.editor_background) - .p_2() - .child(pseudo_code_skeleton(self.theme.clone(), self.seed)), - ); + let pane = v_flex().h_full().flex_grow().child( + div() + .size_full() + .overflow_hidden() + .bg(color.editor_background) + .p_2() + .child(pseudo_code_skeleton(self.theme.clone(), self.seed)), + ); let content = div().size_full().flex().child(sidebar).child(pane); diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index 9e524a5e8a..3d2c034367 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -4,11 +4,14 @@ use gpui::{ }; use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*}; use workspace::{ - NewFile, Open, Workspace, WorkspaceId, + NewFile, Open, WorkspaceId, item::{Item, ItemEvent}, + with_active_or_new_workspace, }; use zed_actions::{Extensions, OpenSettings, agent, command_palette}; +use crate::{Onboarding, OpenOnboarding}; + actions!( zed, [ @@ -216,7 +219,64 @@ impl Render for WelcomePage { div().child( Button::new("welcome-exit", "Return to Setup") .full_width() - .label_size(LabelSize::XSmall), + .label_size(LabelSize::XSmall) + .on_click(|_, window, cx| { + window.dispatch_action( + OpenOnboarding.boxed_clone(), + cx, + ); + + with_active_or_new_workspace(cx, |workspace, window, cx| { + let Some((welcome_id, welcome_idx)) = workspace + .active_pane() + .read(cx) + .items() + .enumerate() + .find_map(|(idx, item)| { + let _ = item.downcast::()?; + Some((item.item_id(), idx)) + }) + else { + return; + }; + + workspace.active_pane().update(cx, |pane, cx| { + // Get the index here to get around the borrow checker + let idx = pane.items().enumerate().find_map( + |(idx, item)| { + let _ = + item.downcast::()?; + Some(idx) + }, + ); + + if let Some(idx) = idx { + pane.activate_item( + idx, true, true, window, cx, + ); + } else { + let item = + Box::new(Onboarding::new(workspace, cx)); + pane.add_item( + item, + true, + true, + Some(welcome_idx), + window, + cx, + ); + } + + pane.remove_item( + welcome_id, + false, + false, + window, + cx, + ); + }); + }); + }), ), ), ), @@ -227,7 +287,7 @@ impl Render for WelcomePage { } impl WelcomePage { - pub fn new(window: &mut Window, cx: &mut Context) -> Entity { + pub fn new(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| { let focus_handle = cx.focus_handle(); cx.on_focus(&focus_handle, window, |_, _, cx| cx.notify()) diff --git a/crates/ui/src/components/badge.rs b/crates/ui/src/components/badge.rs index 9073c88500..2eee084bbb 100644 --- a/crates/ui/src/components/badge.rs +++ b/crates/ui/src/components/badge.rs @@ -32,7 +32,7 @@ impl RenderOnce for Badge { .pl_1() .pr_2() .border_1() - .border_color(cx.theme().colors().border) + .border_color(cx.theme().colors().border.opacity(0.6)) .bg(cx.theme().colors().element_background) .rounded_sm() .overflow_hidden() @@ -42,12 +42,7 @@ impl RenderOnce for Badge { .color(Color::Muted), ) .child(Divider::vertical().color(DividerColor::Border)) - .child( - Label::new(self.label.clone()) - .size(LabelSize::XSmall) - .buffer_font(cx) - .ml_1(), - ) + .child(Label::new(self.label.clone()).size(LabelSize::Small).ml_1()) } } diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index 135ecdfe62..03f7964f35 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -358,6 +358,7 @@ impl ButtonStyle { #[derive(Default, PartialEq, Clone, Copy)] pub enum ButtonSize { Large, + Medium, #[default] Default, Compact, @@ -368,6 +369,7 @@ impl ButtonSize { pub fn rems(self) -> Rems { match self { ButtonSize::Large => rems_from_px(32.), + ButtonSize::Medium => rems_from_px(28.), ButtonSize::Default => rems_from_px(22.), ButtonSize::Compact => rems_from_px(18.), ButtonSize::None => rems_from_px(16.), @@ -573,7 +575,7 @@ impl RenderOnce for ButtonLike { }) .gap(DynamicSpacing::Base04.rems(cx)) .map(|this| match self.size { - ButtonSize::Large => this.px(DynamicSpacing::Base06.rems(cx)), + ButtonSize::Large | ButtonSize::Medium => this.px(DynamicSpacing::Base06.rems(cx)), ButtonSize::Default | ButtonSize::Compact => { this.px(DynamicSpacing::Base04.rems(cx)) } diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs index a621585349..a1e4d65a24 100644 --- a/crates/ui/src/components/button/toggle_button.rs +++ b/crates/ui/src/components/button/toggle_button.rs @@ -295,6 +295,7 @@ pub struct ButtonConfiguration { label: SharedString, icon: Option, on_click: Box, + selected: bool, } mod private { @@ -308,6 +309,7 @@ pub trait ButtonBuilder: 'static + private::ToggleButtonStyle { pub struct ToggleButtonSimple { label: SharedString, on_click: Box, + selected: bool, } impl ToggleButtonSimple { @@ -318,8 +320,14 @@ impl ToggleButtonSimple { Self { label: label.into(), on_click: Box::new(on_click), + selected: false, } } + + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } } impl private::ToggleButtonStyle for ToggleButtonSimple {} @@ -330,6 +338,7 @@ impl ButtonBuilder for ToggleButtonSimple { label: self.label, icon: None, on_click: self.on_click, + selected: self.selected, } } } @@ -338,6 +347,7 @@ pub struct ToggleButtonWithIcon { label: SharedString, icon: IconName, on_click: Box, + selected: bool, } impl ToggleButtonWithIcon { @@ -350,8 +360,14 @@ impl ToggleButtonWithIcon { label: label.into(), icon, on_click: Box::new(on_click), + selected: false, } } + + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } } impl private::ToggleButtonStyle for ToggleButtonWithIcon {} @@ -362,6 +378,7 @@ impl ButtonBuilder for ToggleButtonWithIcon { label: self.label, icon: Some(self.icon), on_click: self.on_click, + selected: self.selected, } } } @@ -373,6 +390,12 @@ pub enum ToggleButtonGroupStyle { Outlined, } +#[derive(Clone, Copy, PartialEq)] +pub enum ToggleButtonGroupSize { + Default, + Medium, +} + #[derive(IntoElement)] pub struct ToggleButtonGroup where @@ -381,6 +404,7 @@ where group_name: &'static str, rows: [[T; COLS]; ROWS], style: ToggleButtonGroupStyle, + size: ToggleButtonGroupSize, button_width: Rems, selected_index: usize, } @@ -391,6 +415,7 @@ impl ToggleButtonGroup { group_name, rows: [buttons], style: ToggleButtonGroupStyle::Transparent, + size: ToggleButtonGroupSize::Default, button_width: rems_from_px(100.), selected_index: 0, } @@ -403,6 +428,7 @@ impl ToggleButtonGroup { group_name, rows: [first_row, second_row], style: ToggleButtonGroupStyle::Transparent, + size: ToggleButtonGroupSize::Default, button_width: rems_from_px(100.), selected_index: 0, } @@ -415,6 +441,11 @@ impl ToggleButtonGroup Self { + self.size = size; + self + } + pub fn button_width(mut self, button_width: Rems) -> Self { self.button_width = button_width; self @@ -430,53 +461,56 @@ impl RenderOnce for ToggleButtonGroup { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let entries = self.rows.into_iter().enumerate().map(|(row_index, row)| { - row.into_iter().enumerate().map(move |(col_index, button)| { - let ButtonConfiguration { - label, - icon, - on_click, - } = button.into_configuration(); + let entries = + self.rows.into_iter().enumerate().map(|(row_index, row)| { + row.into_iter().enumerate().map(move |(col_index, button)| { + let ButtonConfiguration { + label, + icon, + on_click, + selected, + } = button.into_configuration(); - let entry_index = row_index * COLS + col_index; + let entry_index = row_index * COLS + col_index; - ButtonLike::new((self.group_name, entry_index)) - .when(entry_index == self.selected_index, |this| { - this.toggle_state(true) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - }) - .rounding(None) - .when(self.style == ToggleButtonGroupStyle::Filled, |button| { - button.style(ButtonStyle::Filled) - }) - .child( - h_flex() - .min_w(self.button_width) - .gap_1p5() - .px_3() - .py_1() - .justify_center() - .when_some(icon, |this, icon| { - this.child(Icon::new(icon).size(IconSize::XSmall).map(|this| { - if entry_index == self.selected_index { - this.color(Color::Accent) - } else { - this.color(Color::Muted) - } - })) - }) - .child( - Label::new(label) - .size(LabelSize::Small) - .when(entry_index == self.selected_index, |this| { - this.color(Color::Accent) - }), - ), - ) - .on_click(on_click) - .into_any_element() - }) - }); + ButtonLike::new((self.group_name, entry_index)) + .when(entry_index == self.selected_index || selected, |this| { + this.toggle_state(true) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + }) + .rounding(None) + .when(self.style == ToggleButtonGroupStyle::Filled, |button| { + button.style(ButtonStyle::Filled) + }) + .when(self.size == ToggleButtonGroupSize::Medium, |button| { + button.size(ButtonSize::Medium) + }) + .child( + h_flex() + .min_w(self.button_width) + .gap_1p5() + .px_3() + .py_1() + .justify_center() + .when_some(icon, |this, icon| { + this.py_2() + .child(Icon::new(icon).size(IconSize::XSmall).map(|this| { + if entry_index == self.selected_index || selected { + this.color(Color::Accent) + } else { + this.color(Color::Muted) + } + })) + }) + .child(Label::new(label).size(LabelSize::Small).when( + entry_index == self.selected_index || selected, + |this| this.color(Color::Accent), + )), + ) + .on_click(on_click) + .into_any_element() + }) + }); let border_color = cx.theme().colors().border.opacity(0.6); let is_outlined_or_filled = self.style == ToggleButtonGroupStyle::Outlined From a3a3f111f86fba244ca9b26382bbab065785e61a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 1 Aug 2025 14:44:17 -0400 Subject: [PATCH 022/693] zeta: Rename binding back to `user_store` (#35486) This PR renames a binding from `cloud_user_store` to `user_store` now that we've consolidated the two into the `UserStore`. Release Notes: - N/A --- crates/zeta/src/zeta.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 18b9217b95..f051dfde0b 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -1588,8 +1588,8 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider .zeta .read(cx) .user_store - .read_with(cx, |cloud_user_store, _cx| { - cloud_user_store.account_too_young() || cloud_user_store.has_overdue_invoices() + .read_with(cx, |user_store, _cx| { + user_store.account_too_young() || user_store.has_overdue_invoices() }) { return; From ac75593198b3db83144820ef146c05285df7d659 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Fri, 1 Aug 2025 14:30:25 -0500 Subject: [PATCH 023/693] onboarding: Actions for page navigation (#35484) Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --- assets/keymaps/default-linux.json | 9 ++ assets/keymaps/default-macos.json | 9 ++ crates/onboarding/src/basics_page.rs | 4 +- crates/onboarding/src/onboarding.rs | 158 +++++++++++++++------------ crates/zed/src/zed.rs | 1 + 5 files changed, 111 insertions(+), 70 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 8a8dbd8a90..ef5354e82d 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1168,5 +1168,14 @@ "up": "menu::SelectPrevious", "down": "menu::SelectNext" } + }, + { + "context": "Onboarding", + "use_key_equivalents": true, + "bindings": { + "ctrl-1": "onboarding::ActivateBasicsPage", + "ctrl-2": "onboarding::ActivateEditingPage", + "ctrl-3": "onboarding::ActivateAISetupPage" + } } ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 62ba187851..3287e50acb 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1270,5 +1270,14 @@ "up": "menu::SelectPrevious", "down": "menu::SelectNext" } + }, + { + "context": "Onboarding", + "use_key_equivalents": true, + "bindings": { + "cmd-1": "onboarding::ActivateBasicsPage", + "cmd-2": "onboarding::ActivateEditingPage", + "cmd-3": "onboarding::ActivateAISetupPage" + } } ] diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 327256968a..82688e6220 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -153,10 +153,8 @@ fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement { new_appearance: Appearance, cx: &mut App, ) { - appearance_state.update(cx, |appearance, _| { - *appearance = new_appearance; - }); let fs = ::global(cx); + appearance_state.write(cx, new_appearance); update_settings_file::(fs, cx, move |settings, _| { if settings.theme.as_ref().and_then(ThemeSelection::mode) == Some(ThemeMode::System) { diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 21fbeb5d97..2e6025285c 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -6,8 +6,8 @@ use feature_flags::{FeatureFlag, FeatureFlagViewExt as _}; use fs::Fs; use gpui::{ Action, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, EventEmitter, - FocusHandle, Focusable, IntoElement, Render, SharedString, Subscription, Task, WeakEntity, - Window, actions, + FocusHandle, Focusable, IntoElement, KeyContext, Render, SharedString, Subscription, Task, + WeakEntity, Window, actions, }; use schemars::JsonSchema; use serde::Deserialize; @@ -65,6 +65,18 @@ actions!( ] ); +actions!( + onboarding, + [ + /// Activates the Basics page. + ActivateBasicsPage, + /// Activates the Editing page. + ActivateEditingPage, + /// Activates the AI Setup page. + ActivateAISetupPage, + ] +); + pub fn init(cx: &mut App) { cx.on_action(|_: &OpenOnboarding, cx| { with_active_or_new_workspace(cx, |workspace, window, cx| { @@ -235,67 +247,69 @@ impl Onboarding { }) } - fn render_nav_button( + fn render_nav_buttons( &mut self, - page: SelectedPage, - _: &mut Window, + window: &mut Window, cx: &mut Context, - ) -> impl IntoElement { - let text = match page { - SelectedPage::Basics => "Basics", - SelectedPage::Editing => "Editing", - SelectedPage::AiSetup => "AI Setup", - }; + ) -> [impl IntoElement; 3] { + let pages = [ + SelectedPage::Basics, + SelectedPage::Editing, + SelectedPage::AiSetup, + ]; - let binding = match page { - SelectedPage::Basics => { - KeyBinding::new(vec![gpui::Keystroke::parse("cmd-1").unwrap()], cx) - .map(|kb| kb.size(rems_from_px(12.))) - } - SelectedPage::Editing => { - KeyBinding::new(vec![gpui::Keystroke::parse("cmd-2").unwrap()], cx) - .map(|kb| kb.size(rems_from_px(12.))) - } - SelectedPage::AiSetup => { - KeyBinding::new(vec![gpui::Keystroke::parse("cmd-3").unwrap()], cx) - .map(|kb| kb.size(rems_from_px(12.))) - } - }; + let text = ["Basics", "Editing", "AI Setup"]; - let selected = self.selected_page == page; + let actions: [&dyn Action; 3] = [ + &ActivateBasicsPage, + &ActivateEditingPage, + &ActivateAISetupPage, + ]; - h_flex() - .id(text) - .relative() - .w_full() - .gap_2() - .px_2() - .py_0p5() - .justify_between() - .rounded_sm() - .when(selected, |this| { - this.child( - div() - .h_4() - .w_px() - .bg(cx.theme().colors().text_accent) - .absolute() - .left_0(), - ) - }) - .hover(|style| style.bg(cx.theme().colors().element_hover)) - .child(Label::new(text).map(|this| { - if selected { - this.color(Color::Default) - } else { - this.color(Color::Muted) - } - })) - .child(binding) - .on_click(cx.listener(move |this, _, _, cx| { - this.selected_page = page; - cx.notify(); - })) + let mut binding = actions.map(|action| { + KeyBinding::for_action_in(action, &self.focus_handle, window, cx) + .map(|kb| kb.size(rems_from_px(12.))) + }); + + pages.map(|page| { + let i = page as usize; + let selected = self.selected_page == page; + h_flex() + .id(text[i]) + .relative() + .w_full() + .gap_2() + .px_2() + .py_0p5() + .justify_between() + .rounded_sm() + .when(selected, |this| { + this.child( + div() + .h_4() + .w_px() + .bg(cx.theme().colors().text_accent) + .absolute() + .left_0(), + ) + }) + .hover(|style| style.bg(cx.theme().colors().element_hover)) + .child(Label::new(text[i]).map(|this| { + if selected { + this.color(Color::Default) + } else { + this.color(Color::Muted) + } + })) + .child(binding[i].take().map_or( + gpui::Empty.into_any_element(), + IntoElement::into_any_element, + )) + .on_click(cx.listener(move |this, _, _, cx| { + this.selected_page = page; + cx.notify(); + })) + }) } fn render_nav(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { @@ -335,14 +349,7 @@ impl Onboarding { .border_y_1() .border_color(cx.theme().colors().border_variant.opacity(0.5)) .gap_1() - .children([ - self.render_nav_button(SelectedPage::Basics, window, cx) - .into_element(), - self.render_nav_button(SelectedPage::Editing, window, cx) - .into_element(), - self.render_nav_button(SelectedPage::AiSetup, window, cx) - .into_element(), - ]), + .children(self.render_nav_buttons(window, cx)), ) .child( ButtonLike::new("skip_all") @@ -454,9 +461,26 @@ impl Render for Onboarding { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { h_flex() .image_cache(gpui::retain_all("onboarding-page")) - .key_context("onboarding-page") + .key_context({ + let mut ctx = KeyContext::new_with_defaults(); + ctx.add("Onboarding"); + ctx + }) + .track_focus(&self.focus_handle) .size_full() .bg(cx.theme().colors().editor_background) + .on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| { + this.selected_page = SelectedPage::Basics; + cx.notify(); + })) + .on_action(cx.listener(|this, _: &ActivateEditingPage, _, cx| { + this.selected_page = SelectedPage::Editing; + cx.notify(); + })) + .on_action(cx.listener(|this, _: &ActivateAISetupPage, _, cx| { + this.selected_page = SelectedPage::AiSetup; + cx.notify(); + })) .child( h_flex() .max_w(rems_from_px(1100.)) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 8c6da335ab..af317edeee 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4354,6 +4354,7 @@ mod tests { "menu", "notebook", "notification_panel", + "onboarding", "outline", "outline_panel", "pane", From 561ccf86aa23c53cafe8a4ddff15ebcb6909d47f Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Fri, 1 Aug 2025 15:45:29 -0500 Subject: [PATCH 024/693] onboarding: Serialize onboarding page (#35490) Closes #ISSUE Serializes the onboarding page to the database to ensure that if Zed is closed during onboarding, re-opening Zed restores the onboarding state and the most recently active page (Basics, Editing, etc) restored. Also has the nice side effect of making dev a bit nicer as it removes the need to re-open onboarding and navigate to the correct page on each build. Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/onboarding/src/onboarding.rs | 143 ++++++++++++++++++++++++++-- 1 file changed, 134 insertions(+), 9 deletions(-) diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 2e6025285c..f7e76f2f34 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -22,7 +22,7 @@ use workspace::{ dock::DockPosition, item::{Item, ItemEvent}, notifications::NotifyResultExt as _, - open_new, with_active_or_new_workspace, + open_new, register_serializable_item, with_active_or_new_workspace, }; mod ai_setup_page; @@ -197,6 +197,7 @@ pub fn init(cx: &mut App) { .detach(); }) .detach(); + register_serializable_item::(cx); } pub fn show_onboarding_view(app_state: Arc, cx: &mut App) -> Task> { @@ -247,6 +248,12 @@ impl Onboarding { }) } + fn set_page(&mut self, page: SelectedPage, cx: &mut Context) { + self.selected_page = page; + cx.notify(); + cx.emit(ItemEvent::UpdateTab); + } + fn render_nav_buttons( &mut self, window: &mut Window, @@ -306,8 +313,7 @@ impl Onboarding { IntoElement::into_any_element, )) .on_click(cx.listener(move |this, _, _, cx| { - this.selected_page = page; - cx.notify(); + this.set_page(page, cx); })) }) } @@ -470,16 +476,13 @@ impl Render for Onboarding { .size_full() .bg(cx.theme().colors().editor_background) .on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| { - this.selected_page = SelectedPage::Basics; - cx.notify(); + this.set_page(SelectedPage::Basics, cx); })) .on_action(cx.listener(|this, _: &ActivateEditingPage, _, cx| { - this.selected_page = SelectedPage::Editing; - cx.notify(); + this.set_page(SelectedPage::Editing, cx); })) .on_action(cx.listener(|this, _: &ActivateAISetupPage, _, cx| { - this.selected_page = SelectedPage::AiSetup; - cx.notify(); + this.set_page(SelectedPage::AiSetup, cx); })) .child( h_flex() @@ -594,3 +597,125 @@ pub async fn handle_import_vscode_settings( }) .ok(); } + +impl workspace::SerializableItem for Onboarding { + fn serialized_item_kind() -> &'static str { + "OnboardingPage" + } + + fn cleanup( + workspace_id: workspace::WorkspaceId, + alive_items: Vec, + _window: &mut Window, + cx: &mut App, + ) -> gpui::Task> { + workspace::delete_unloaded_items( + alive_items, + workspace_id, + "onboarding_pages", + &persistence::ONBOARDING_PAGES, + cx, + ) + } + + fn deserialize( + _project: Entity, + workspace: WeakEntity, + workspace_id: workspace::WorkspaceId, + item_id: workspace::ItemId, + window: &mut Window, + cx: &mut App, + ) -> gpui::Task>> { + window.spawn(cx, async move |cx| { + if let Some(page_number) = + persistence::ONBOARDING_PAGES.get_onboarding_page(item_id, workspace_id)? + { + let page = match page_number { + 0 => Some(SelectedPage::Basics), + 1 => Some(SelectedPage::Editing), + 2 => Some(SelectedPage::AiSetup), + _ => None, + }; + workspace.update(cx, |workspace, cx| { + let onboarding_page = Onboarding::new(workspace, cx); + if let Some(page) = page { + zlog::info!("Onboarding page {page:?} loaded"); + onboarding_page.update(cx, |onboarding_page, cx| { + onboarding_page.set_page(page, cx); + }) + } + onboarding_page + }) + } else { + Err(anyhow::anyhow!("No onboarding page to deserialize")) + } + }) + } + + fn serialize( + &mut self, + workspace: &mut Workspace, + item_id: workspace::ItemId, + _closing: bool, + _window: &mut Window, + cx: &mut ui::Context, + ) -> Option>> { + let workspace_id = workspace.database_id()?; + let page_number = self.selected_page as u16; + Some(cx.background_spawn(async move { + persistence::ONBOARDING_PAGES + .save_onboarding_page(item_id, workspace_id, page_number) + .await + })) + } + + fn should_serialize(&self, event: &Self::Event) -> bool { + event == &ItemEvent::UpdateTab + } +} + +mod persistence { + use db::{define_connection, query, sqlez_macros::sql}; + use workspace::WorkspaceDb; + + define_connection! { + pub static ref ONBOARDING_PAGES: OnboardingPagesDb = + &[ + sql!( + CREATE TABLE onboarding_pages ( + workspace_id INTEGER, + item_id INTEGER UNIQUE, + page_number INTEGER, + + PRIMARY KEY(workspace_id, item_id), + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ) STRICT; + ), + ]; + } + + impl OnboardingPagesDb { + query! { + pub async fn save_onboarding_page( + item_id: workspace::ItemId, + workspace_id: workspace::WorkspaceId, + page_number: u16 + ) -> Result<()> { + INSERT OR REPLACE INTO onboarding_pages(item_id, workspace_id, page_number) + VALUES (?, ?, ?) + } + } + + query! { + pub fn get_onboarding_page( + item_id: workspace::ItemId, + workspace_id: workspace::WorkspaceId + ) -> Result> { + SELECT page_number + FROM onboarding_pages + WHERE item_id = ? AND workspace_id = ? + } + } + } +} From 605211582582730a9e4e5f6583a3bb1c9cd38ad1 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Fri, 1 Aug 2025 15:08:09 -0600 Subject: [PATCH 025/693] zeta: Add CLI tool for querying edit predictions and related context (#35491) Release Notes: - N/A --------- Co-authored-by: Marshall Bowers --- Cargo.lock | 36 +++ Cargo.toml | 1 + crates/eval/src/eval.rs | 7 +- crates/zeta/src/zeta.rs | 213 +++++++++--------- crates/zeta_cli/Cargo.toml | 45 ++++ crates/zeta_cli/LICENSE-GPL | 1 + crates/zeta_cli/build.rs | 14 ++ crates/zeta_cli/src/headless.rs | 128 +++++++++++ crates/zeta_cli/src/main.rs | 376 ++++++++++++++++++++++++++++++++ 9 files changed, 719 insertions(+), 102 deletions(-) create mode 100644 crates/zeta_cli/Cargo.toml create mode 120000 crates/zeta_cli/LICENSE-GPL create mode 100644 crates/zeta_cli/build.rs create mode 100644 crates/zeta_cli/src/headless.rs create mode 100644 crates/zeta_cli/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 94ba0cf0ba..64470b5abe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20606,6 +20606,42 @@ dependencies = [ "zlog", ] +[[package]] +name = "zeta_cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "client", + "debug_adapter_extension", + "extension", + "fs", + "futures 0.3.31", + "gpui", + "gpui_tokio", + "language", + "language_extension", + "language_model", + "language_models", + "languages", + "node_runtime", + "paths", + "project", + "prompt_store", + "release_channel", + "reqwest_client", + "serde", + "serde_json", + "settings", + "shellexpand 2.1.2", + "smol", + "terminal_view", + "util", + "watch", + "workspace-hack", + "zeta", +] + [[package]] name = "zip" version = "0.6.6" diff --git a/Cargo.toml b/Cargo.toml index 93fa9644a1..5b97596d0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -189,6 +189,7 @@ members = [ "crates/zed", "crates/zed_actions", "crates/zeta", + "crates/zeta_cli", "crates/zlog", "crates/zlog_settings", diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index a02b4a7f0b..d638ac171f 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -18,7 +18,7 @@ use collections::{HashMap, HashSet}; use extension::ExtensionHostProxy; use futures::future; use gpui::http_client::read_proxy_from_env; -use gpui::{App, AppContext, Application, AsyncApp, Entity, SemanticVersion, UpdateGlobal}; +use gpui::{App, AppContext, Application, AsyncApp, Entity, UpdateGlobal}; use gpui_tokio::Tokio; use language::LanguageRegistry; use language_model::{ConfiguredModel, LanguageModel, LanguageModelRegistry, SelectedModel}; @@ -337,7 +337,8 @@ pub struct AgentAppState { } pub fn init(cx: &mut App) -> Arc { - release_channel::init(SemanticVersion::default(), cx); + let app_version = AppVersion::global(cx); + release_channel::init(app_version, cx); gpui_tokio::init(cx); let mut settings_store = SettingsStore::new(cx); @@ -350,7 +351,7 @@ pub fn init(cx: &mut App) -> Arc { // Set User-Agent so we can download language servers from GitHub let user_agent = format!( "Zed/{} ({}; {})", - AppVersion::global(cx), + app_version, std::env::consts::OS, std::env::consts::ARCH ); diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index f051dfde0b..f130c3a965 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -146,14 +146,14 @@ pub struct InlineCompletion { input_events: Arc, input_excerpt: Arc, output_excerpt: Arc, - request_sent_at: Instant, + buffer_snapshotted_at: Instant, response_received_at: Instant, } impl InlineCompletion { fn latency(&self) -> Duration { self.response_received_at - .duration_since(self.request_sent_at) + .duration_since(self.buffer_snapshotted_at) } fn interpolate(&self, new_snapshot: &BufferSnapshot) -> Option, String)>> { @@ -391,104 +391,48 @@ impl Zeta { + Send + 'static, { + let buffer = buffer.clone(); + let buffer_snapshotted_at = Instant::now(); let snapshot = self.report_changes_for_buffer(&buffer, cx); - let diagnostic_groups = snapshot.diagnostic_groups(None); - let cursor_point = cursor.to_point(&snapshot); - let cursor_offset = cursor_point.to_offset(&snapshot); - let events = self.events.clone(); - let path: Arc = snapshot - .file() - .map(|f| Arc::from(f.full_path(cx).as_path())) - .unwrap_or_else(|| Arc::from(Path::new("untitled"))); - let zeta = cx.entity(); + let events = self.events.clone(); let client = self.client.clone(); let llm_token = self.llm_token.clone(); let app_version = AppVersion::global(cx); - let buffer = buffer.clone(); - - let local_lsp_store = - project.and_then(|project| project.read(cx).lsp_store().read(cx).as_local()); - let diagnostic_groups = if let Some(local_lsp_store) = local_lsp_store { - Some( - diagnostic_groups - .into_iter() - .filter_map(|(language_server_id, diagnostic_group)| { - let language_server = - local_lsp_store.running_language_server_for_id(language_server_id)?; - - Some(( - language_server.name(), - diagnostic_group.resolve::(&snapshot), - )) - }) - .collect::>(), - ) - } else { - None - }; + let full_path: Arc = snapshot + .file() + .map(|f| Arc::from(f.full_path(cx).as_path())) + .unwrap_or_else(|| Arc::from(Path::new("untitled"))); + let full_path_str = full_path.to_string_lossy().to_string(); + let cursor_point = cursor.to_point(&snapshot); + let cursor_offset = cursor_point.to_offset(&snapshot); + let make_events_prompt = move || prompt_for_events(&events, MAX_EVENT_TOKENS); + let gather_task = gather_context( + project, + full_path_str, + &snapshot, + cursor_point, + make_events_prompt, + can_collect_data, + cx, + ); cx.spawn(async move |this, cx| { - let request_sent_at = Instant::now(); - - struct BackgroundValues { - input_events: String, - input_excerpt: String, - speculated_output: String, - editable_range: Range, - input_outline: String, - } - - let values = cx - .background_spawn({ - let snapshot = snapshot.clone(); - let path = path.clone(); - async move { - let path = path.to_string_lossy(); - let input_excerpt = excerpt_for_cursor_position( - cursor_point, - &path, - &snapshot, - MAX_REWRITE_TOKENS, - MAX_CONTEXT_TOKENS, - ); - let input_events = prompt_for_events(&events, MAX_EVENT_TOKENS); - let input_outline = prompt_for_outline(&snapshot); - - anyhow::Ok(BackgroundValues { - input_events, - input_excerpt: input_excerpt.prompt, - speculated_output: input_excerpt.speculated_output, - editable_range: input_excerpt.editable_range.to_offset(&snapshot), - input_outline, - }) - } - }) - .await?; + let GatherContextOutput { + body, + editable_range, + } = gather_task.await?; log::debug!( "Events:\n{}\nExcerpt:\n{:?}", - values.input_events, - values.input_excerpt + body.input_events, + body.input_excerpt ); - let body = PredictEditsBody { - input_events: values.input_events.clone(), - input_excerpt: values.input_excerpt.clone(), - speculated_output: Some(values.speculated_output), - outline: Some(values.input_outline.clone()), - can_collect_data, - diagnostic_groups: diagnostic_groups.and_then(|diagnostic_groups| { - diagnostic_groups - .into_iter() - .map(|(name, diagnostic_group)| { - Ok((name.to_string(), serde_json::to_value(diagnostic_group)?)) - }) - .collect::>>() - .log_err() - }), - }; + let input_outline = body.outline.clone().unwrap_or_default(); + let input_events = body.input_events.clone(); + let input_excerpt = body.input_excerpt.clone(); let response = perform_predict_edits(PerformPredictEditsParams { client, @@ -546,13 +490,13 @@ impl Zeta { response, buffer, &snapshot, - values.editable_range, + editable_range, cursor_offset, - path, - values.input_outline, - values.input_events, - values.input_excerpt, - request_sent_at, + full_path, + input_outline, + input_events, + input_excerpt, + buffer_snapshotted_at, &cx, ) .await @@ -751,7 +695,7 @@ and then another ) } - fn perform_predict_edits( + pub fn perform_predict_edits( params: PerformPredictEditsParams, ) -> impl Future)>> { async move { @@ -906,7 +850,7 @@ and then another input_outline: String, input_events: String, input_excerpt: String, - request_sent_at: Instant, + buffer_snapshotted_at: Instant, cx: &AsyncApp, ) -> Task>> { let snapshot = snapshot.clone(); @@ -952,7 +896,7 @@ and then another input_events: input_events.into(), input_excerpt: input_excerpt.into(), output_excerpt, - request_sent_at, + buffer_snapshotted_at, response_received_at: Instant::now(), })) }) @@ -1136,7 +1080,7 @@ and then another } } -struct PerformPredictEditsParams { +pub struct PerformPredictEditsParams { pub client: Arc, pub llm_token: LlmApiToken, pub app_version: SemanticVersion, @@ -1211,6 +1155,77 @@ fn common_prefix, T2: Iterator>(a: T1, b: .sum() } +pub struct GatherContextOutput { + pub body: PredictEditsBody, + pub editable_range: Range, +} + +pub fn gather_context( + project: Option<&Entity>, + full_path_str: String, + snapshot: &BufferSnapshot, + cursor_point: language::Point, + make_events_prompt: impl FnOnce() -> String + Send + 'static, + can_collect_data: bool, + cx: &App, +) -> Task> { + let local_lsp_store = + project.and_then(|project| project.read(cx).lsp_store().read(cx).as_local()); + let diagnostic_groups: Vec<(String, serde_json::Value)> = + if let Some(local_lsp_store) = local_lsp_store { + snapshot + .diagnostic_groups(None) + .into_iter() + .filter_map(|(language_server_id, diagnostic_group)| { + let language_server = + local_lsp_store.running_language_server_for_id(language_server_id)?; + let diagnostic_group = diagnostic_group.resolve::(&snapshot); + let language_server_name = language_server.name().to_string(); + let serialized = serde_json::to_value(diagnostic_group).unwrap(); + Some((language_server_name, serialized)) + }) + .collect::>() + } else { + Vec::new() + }; + + cx.background_spawn({ + let snapshot = snapshot.clone(); + async move { + let diagnostic_groups = if diagnostic_groups.is_empty() { + None + } else { + Some(diagnostic_groups) + }; + + let input_excerpt = excerpt_for_cursor_position( + cursor_point, + &full_path_str, + &snapshot, + MAX_REWRITE_TOKENS, + MAX_CONTEXT_TOKENS, + ); + let input_events = make_events_prompt(); + let input_outline = prompt_for_outline(&snapshot); + let editable_range = input_excerpt.editable_range.to_offset(&snapshot); + + let body = PredictEditsBody { + input_events, + input_excerpt: input_excerpt.prompt, + speculated_output: Some(input_excerpt.speculated_output), + outline: Some(input_outline), + can_collect_data, + diagnostic_groups, + }; + + Ok(GatherContextOutput { + body, + editable_range, + }) + } + }) +} + fn prompt_for_outline(snapshot: &BufferSnapshot) -> String { let mut input_outline = String::new(); @@ -1261,7 +1276,7 @@ struct RegisteredBuffer { } #[derive(Clone)] -enum Event { +pub enum Event { BufferChange { old_snapshot: BufferSnapshot, new_snapshot: BufferSnapshot, @@ -1845,7 +1860,7 @@ mod tests { input_events: "".into(), input_excerpt: "".into(), output_excerpt: "".into(), - request_sent_at: Instant::now(), + buffer_snapshotted_at: Instant::now(), response_received_at: Instant::now(), }; diff --git a/crates/zeta_cli/Cargo.toml b/crates/zeta_cli/Cargo.toml new file mode 100644 index 0000000000..e77351c219 --- /dev/null +++ b/crates/zeta_cli/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "zeta_cli" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[[bin]] +name = "zeta" +path = "src/main.rs" + +[dependencies] +anyhow.workspace = true +clap.workspace = true +client.workspace = true +debug_adapter_extension.workspace = true +extension.workspace = true +fs.workspace = true +futures.workspace = true +gpui.workspace = true +gpui_tokio.workspace = true +language.workspace = true +language_extension.workspace = true +language_model.workspace = true +language_models.workspace = true +languages = { workspace = true, features = ["load-grammars"] } +node_runtime.workspace = true +paths.workspace = true +project.workspace = true +prompt_store.workspace = true +release_channel.workspace = true +reqwest_client.workspace = true +serde.workspace = true +serde_json.workspace = true +settings.workspace = true +shellexpand.workspace = true +terminal_view.workspace = true +util.workspace = true +watch.workspace = true +workspace-hack.workspace = true +zeta.workspace = true +smol.workspace = true diff --git a/crates/zeta_cli/LICENSE-GPL b/crates/zeta_cli/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/zeta_cli/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/zeta_cli/build.rs b/crates/zeta_cli/build.rs new file mode 100644 index 0000000000..ccbb54c5b4 --- /dev/null +++ b/crates/zeta_cli/build.rs @@ -0,0 +1,14 @@ +fn main() { + let cargo_toml = + std::fs::read_to_string("../zed/Cargo.toml").expect("Failed to read Cargo.toml"); + let version = cargo_toml + .lines() + .find(|line| line.starts_with("version = ")) + .expect("Version not found in crates/zed/Cargo.toml") + .split('=') + .nth(1) + .expect("Invalid version format") + .trim() + .trim_matches('"'); + println!("cargo:rustc-env=ZED_PKG_VERSION={}", version); +} diff --git a/crates/zeta_cli/src/headless.rs b/crates/zeta_cli/src/headless.rs new file mode 100644 index 0000000000..959bb91a8f --- /dev/null +++ b/crates/zeta_cli/src/headless.rs @@ -0,0 +1,128 @@ +use client::{Client, ProxySettings, UserStore}; +use extension::ExtensionHostProxy; +use fs::RealFs; +use gpui::http_client::read_proxy_from_env; +use gpui::{App, AppContext, Entity}; +use gpui_tokio::Tokio; +use language::LanguageRegistry; +use language_extension::LspAccess; +use node_runtime::{NodeBinaryOptions, NodeRuntime}; +use project::Project; +use project::project_settings::ProjectSettings; +use release_channel::AppVersion; +use reqwest_client::ReqwestClient; +use settings::{Settings, SettingsStore}; +use std::path::PathBuf; +use std::sync::Arc; +use util::ResultExt as _; + +/// Headless subset of `workspace::AppState`. +pub struct ZetaCliAppState { + pub languages: Arc, + pub client: Arc, + pub user_store: Entity, + pub fs: Arc, + pub node_runtime: NodeRuntime, +} + +// TODO: dedupe with crates/eval/src/eval.rs +pub fn init(cx: &mut App) -> ZetaCliAppState { + let app_version = AppVersion::load(env!("ZED_PKG_VERSION")); + release_channel::init(app_version, cx); + gpui_tokio::init(cx); + + let mut settings_store = SettingsStore::new(cx); + settings_store + .set_default_settings(settings::default_settings().as_ref(), cx) + .unwrap(); + cx.set_global(settings_store); + client::init_settings(cx); + + // Set User-Agent so we can download language servers from GitHub + let user_agent = format!( + "Zed/{} ({}; {})", + app_version, + std::env::consts::OS, + std::env::consts::ARCH + ); + let proxy_str = ProxySettings::get_global(cx).proxy.to_owned(); + let proxy_url = proxy_str + .as_ref() + .and_then(|input| input.parse().ok()) + .or_else(read_proxy_from_env); + let http = { + let _guard = Tokio::handle(cx).enter(); + + ReqwestClient::proxy_and_user_agent(proxy_url, &user_agent) + .expect("could not start HTTP client") + }; + cx.set_http_client(Arc::new(http)); + + Project::init_settings(cx); + + let client = Client::production(cx); + cx.set_http_client(client.http_client()); + + let git_binary_path = None; + let fs = Arc::new(RealFs::new( + git_binary_path, + cx.background_executor().clone(), + )); + + let mut languages = LanguageRegistry::new(cx.background_executor().clone()); + languages.set_language_server_download_dir(paths::languages_dir().clone()); + let languages = Arc::new(languages); + + let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); + + extension::init(cx); + + let (mut tx, rx) = watch::channel(None); + cx.observe_global::(move |cx| { + let settings = &ProjectSettings::get_global(cx).node; + let options = NodeBinaryOptions { + allow_path_lookup: !settings.ignore_system_version, + allow_binary_download: true, + use_paths: settings.path.as_ref().map(|node_path| { + let node_path = PathBuf::from(shellexpand::tilde(node_path).as_ref()); + let npm_path = settings + .npm_path + .as_ref() + .map(|path| PathBuf::from(shellexpand::tilde(&path).as_ref())); + ( + node_path.clone(), + npm_path.unwrap_or_else(|| { + let base_path = PathBuf::new(); + node_path.parent().unwrap_or(&base_path).join("npm") + }), + ) + }), + }; + tx.send(Some(options)).log_err(); + }) + .detach(); + let node_runtime = NodeRuntime::new(client.http_client(), None, rx); + + let extension_host_proxy = ExtensionHostProxy::global(cx); + + language::init(cx); + debug_adapter_extension::init(extension_host_proxy.clone(), cx); + language_extension::init( + LspAccess::Noop, + extension_host_proxy.clone(), + languages.clone(), + ); + language_model::init(client.clone(), cx); + language_models::init(user_store.clone(), client.clone(), cx); + languages::init(languages.clone(), node_runtime.clone(), cx); + prompt_store::init(cx); + terminal_view::init(cx); + + ZetaCliAppState { + languages, + client, + user_store, + fs, + node_runtime, + } +} diff --git a/crates/zeta_cli/src/main.rs b/crates/zeta_cli/src/main.rs new file mode 100644 index 0000000000..c5374b56c9 --- /dev/null +++ b/crates/zeta_cli/src/main.rs @@ -0,0 +1,376 @@ +mod headless; + +use anyhow::{Result, anyhow}; +use clap::{Args, Parser, Subcommand}; +use futures::channel::mpsc; +use futures::{FutureExt as _, StreamExt as _}; +use gpui::{AppContext, Application, AsyncApp}; +use gpui::{Entity, Task}; +use language::Bias; +use language::Buffer; +use language::Point; +use language_model::LlmApiToken; +use project::{Project, ProjectPath}; +use release_channel::AppVersion; +use reqwest_client::ReqwestClient; +use std::path::{Path, PathBuf}; +use std::process::exit; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; +use zeta::{GatherContextOutput, PerformPredictEditsParams, Zeta, gather_context}; + +use crate::headless::ZetaCliAppState; + +#[derive(Parser, Debug)] +#[command(name = "zeta")] +struct ZetaCliArgs { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + Context(ContextArgs), + Predict { + #[arg(long)] + predict_edits_body: Option, + #[clap(flatten)] + context_args: Option, + }, +} + +#[derive(Debug, Args)] +#[group(requires = "worktree")] +struct ContextArgs { + #[arg(long)] + worktree: PathBuf, + #[arg(long)] + cursor: CursorPosition, + #[arg(long)] + use_language_server: bool, + #[arg(long)] + events: Option, +} + +#[derive(Debug, Clone)] +enum FileOrStdin { + File(PathBuf), + Stdin, +} + +impl FileOrStdin { + async fn read_to_string(&self) -> Result { + match self { + FileOrStdin::File(path) => smol::fs::read_to_string(path).await, + FileOrStdin::Stdin => smol::unblock(|| std::io::read_to_string(std::io::stdin())).await, + } + } +} + +impl FromStr for FileOrStdin { + type Err = ::Err; + + fn from_str(s: &str) -> Result { + match s { + "-" => Ok(Self::Stdin), + _ => Ok(Self::File(PathBuf::from_str(s)?)), + } + } +} + +#[derive(Debug, Clone)] +struct CursorPosition { + path: PathBuf, + point: Point, +} + +impl FromStr for CursorPosition { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let parts: Vec<&str> = s.split(':').collect(); + if parts.len() != 3 { + return Err(anyhow!( + "Invalid cursor format. Expected 'file.rs:line:column', got '{}'", + s + )); + } + + let path = PathBuf::from(parts[0]); + let line: u32 = parts[1] + .parse() + .map_err(|_| anyhow!("Invalid line number: '{}'", parts[1]))?; + let column: u32 = parts[2] + .parse() + .map_err(|_| anyhow!("Invalid column number: '{}'", parts[2]))?; + + // Convert from 1-based to 0-based indexing + let point = Point::new(line.saturating_sub(1), column.saturating_sub(1)); + + Ok(CursorPosition { path, point }) + } +} + +async fn get_context( + args: ContextArgs, + app_state: &Arc, + cx: &mut AsyncApp, +) -> Result { + let ContextArgs { + worktree: worktree_path, + cursor, + use_language_server, + events, + } = args; + + let worktree_path = worktree_path.canonicalize()?; + if cursor.path.is_absolute() { + return Err(anyhow!("Absolute paths are not supported in --cursor")); + } + + let (project, _lsp_open_handle, buffer) = if use_language_server { + let (project, lsp_open_handle, buffer) = + open_buffer_with_language_server(&worktree_path, &cursor.path, &app_state, cx).await?; + (Some(project), Some(lsp_open_handle), buffer) + } else { + let abs_path = worktree_path.join(&cursor.path); + let content = smol::fs::read_to_string(&abs_path).await?; + let buffer = cx.new(|cx| Buffer::local(content, cx))?; + (None, None, buffer) + }; + + let worktree_name = worktree_path + .file_name() + .ok_or_else(|| anyhow!("--worktree path must end with a folder name"))?; + let full_path_str = PathBuf::from(worktree_name) + .join(&cursor.path) + .to_string_lossy() + .to_string(); + + let snapshot = cx.update(|cx| buffer.read(cx).snapshot())?; + let clipped_cursor = snapshot.clip_point(cursor.point, Bias::Left); + if clipped_cursor != cursor.point { + let max_row = snapshot.max_point().row; + if cursor.point.row < max_row { + return Err(anyhow!( + "Cursor position {:?} is out of bounds (line length is {})", + cursor.point, + snapshot.line_len(cursor.point.row) + )); + } else { + return Err(anyhow!( + "Cursor position {:?} is out of bounds (max row is {})", + cursor.point, + max_row + )); + } + } + + let events = match events { + Some(events) => events.read_to_string().await?, + None => String::new(), + }; + let can_collect_data = false; + cx.update(|cx| { + gather_context( + project.as_ref(), + full_path_str, + &snapshot, + clipped_cursor, + move || events, + can_collect_data, + cx, + ) + })? + .await +} + +pub async fn open_buffer_with_language_server( + worktree_path: &Path, + path: &Path, + app_state: &Arc, + cx: &mut AsyncApp, +) -> Result<(Entity, Entity>, Entity)> { + let project = cx.update(|cx| { + Project::local( + app_state.client.clone(), + app_state.node_runtime.clone(), + app_state.user_store.clone(), + app_state.languages.clone(), + app_state.fs.clone(), + None, + cx, + ) + })?; + + let worktree = project + .update(cx, |project, cx| { + project.create_worktree(worktree_path, true, cx) + })? + .await?; + + let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath { + worktree_id: worktree.id(), + path: path.to_path_buf().into(), + })?; + + let buffer = project + .update(cx, |project, cx| project.open_buffer(project_path, cx))? + .await?; + + let lsp_open_handle = project.update(cx, |project, cx| { + project.register_buffer_with_language_servers(&buffer, cx) + })?; + + let log_prefix = path.to_string_lossy().to_string(); + wait_for_lang_server(&project, &buffer, log_prefix, cx).await?; + + Ok((project, lsp_open_handle, buffer)) +} + +// TODO: Dedupe with similar function in crates/eval/src/instance.rs +pub fn wait_for_lang_server( + project: &Entity, + buffer: &Entity, + log_prefix: String, + cx: &mut AsyncApp, +) -> Task> { + println!("{}⏵ Waiting for language server", log_prefix); + + let (mut tx, mut rx) = mpsc::channel(1); + + let lsp_store = project + .read_with(cx, |project, _| project.lsp_store()) + .unwrap(); + + let has_lang_server = buffer + .update(cx, |buffer, cx| { + lsp_store.update(cx, |lsp_store, cx| { + lsp_store + .language_servers_for_local_buffer(&buffer, cx) + .next() + .is_some() + }) + }) + .unwrap_or(false); + + if has_lang_server { + project + .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) + .unwrap() + .detach(); + } + + let subscriptions = [ + cx.subscribe(&lsp_store, { + let log_prefix = log_prefix.clone(); + move |_, event, _| match event { + project::LspStoreEvent::LanguageServerUpdate { + message: + client::proto::update_language_server::Variant::WorkProgress( + client::proto::LspWorkProgress { + message: Some(message), + .. + }, + ), + .. + } => println!("{}⟲ {message}", log_prefix), + _ => {} + } + }), + cx.subscribe(&project, { + let buffer = buffer.clone(); + move |project, event, cx| match event { + project::Event::LanguageServerAdded(_, _, _) => { + let buffer = buffer.clone(); + project + .update(cx, |project, cx| project.save_buffer(buffer, cx)) + .detach(); + } + project::Event::DiskBasedDiagnosticsFinished { .. } => { + tx.try_send(()).ok(); + } + _ => {} + } + }), + ]; + + cx.spawn(async move |cx| { + let timeout = cx.background_executor().timer(Duration::new(60 * 5, 0)); + let result = futures::select! { + _ = rx.next() => { + println!("{}⚑ Language server idle", log_prefix); + anyhow::Ok(()) + }, + _ = timeout.fuse() => { + anyhow::bail!("LSP wait timed out after 5 minutes"); + } + }; + drop(subscriptions); + result + }) +} + +fn main() { + let args = ZetaCliArgs::parse(); + let http_client = Arc::new(ReqwestClient::new()); + let app = Application::headless().with_http_client(http_client); + + app.run(move |cx| { + let app_state = Arc::new(headless::init(cx)); + cx.spawn(async move |cx| { + let result = match args.command { + Commands::Context(context_args) => get_context(context_args, &app_state, cx) + .await + .map(|output| serde_json::to_string_pretty(&output.body).unwrap()), + Commands::Predict { + predict_edits_body, + context_args, + } => { + cx.spawn(async move |cx| { + let app_version = cx.update(|cx| AppVersion::global(cx))?; + app_state.client.sign_in(true, cx).await?; + let llm_token = LlmApiToken::default(); + llm_token.refresh(&app_state.client).await?; + + let predict_edits_body = + if let Some(predict_edits_body) = predict_edits_body { + serde_json::from_str(&predict_edits_body.read_to_string().await?)? + } else if let Some(context_args) = context_args { + get_context(context_args, &app_state, cx).await?.body + } else { + return Err(anyhow!( + "Expected either --predict-edits-body-file \ + or the required args of the `context` command." + )); + }; + + let (response, _usage) = + Zeta::perform_predict_edits(PerformPredictEditsParams { + client: app_state.client.clone(), + llm_token, + app_version, + body: predict_edits_body, + }) + .await?; + + Ok(response.output_excerpt) + }) + .await + } + }; + match result { + Ok(output) => { + println!("{}", output); + let _ = cx.update(|cx| cx.quit()); + } + Err(e) => { + eprintln!("Failed: {:?}", e); + exit(1); + } + } + }) + .detach(); + }); +} From edac6e4246b06f2bfabc263c46dd2a7bea3e20d3 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 1 Aug 2025 17:50:51 -0400 Subject: [PATCH 026/693] Add font ligatures and format on save buttons to onboarding UI (#35487) Release Notes: - N/A --- crates/onboarding/src/editing_page.rs | 79 ++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 33d0955d19..2972f41348 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -1,7 +1,9 @@ +use std::sync::Arc; + use editor::{EditorSettings, ShowMinimap}; use fs::Fs; -use gpui::{Action, App, IntoElement, Pixels, Window}; -use language::language_settings::AllLanguageSettings; +use gpui::{Action, App, FontFeatures, IntoElement, Pixels, Window}; +use language::language_settings::{AllLanguageSettings, FormatOnSave}; use project::project_settings::ProjectSettings; use settings::{Settings as _, update_settings_file}; use theme::{FontFamilyCache, FontFamilyName, ThemeSettings}; @@ -116,6 +118,53 @@ fn write_buffer_font_family(font_family: SharedString, cx: &mut App) { }); } +fn read_font_ligatures(cx: &App) -> bool { + ThemeSettings::get_global(cx) + .buffer_font + .features + .is_calt_enabled() + .unwrap_or(true) +} + +fn write_font_ligatures(enabled: bool, cx: &mut App) { + let fs = ::global(cx); + let bit = if enabled { 1 } else { 0 }; + + update_settings_file::(fs, cx, move |theme_settings, _| { + let mut features = theme_settings + .buffer_font_features + .as_mut() + .map(|features| features.tag_value_list().to_vec()) + .unwrap_or_default(); + + if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") { + features[calt_index].1 = bit; + } else { + features.push(("calt".into(), bit)); + } + + theme_settings.buffer_font_features = Some(FontFeatures(Arc::new(features))); + }); +} + +fn read_format_on_save(cx: &App) -> bool { + match AllLanguageSettings::get_global(cx).defaults.format_on_save { + FormatOnSave::On | FormatOnSave::List(_) => true, + FormatOnSave::Off => false, + } +} + +fn write_format_on_save(format_on_save: bool, cx: &mut App) { + let fs = ::global(cx); + + update_settings_file::(fs, cx, move |language_settings, _| { + language_settings.defaults.format_on_save = Some(match format_on_save { + true => FormatOnSave::On, + false => FormatOnSave::Off, + }); + }); +} + fn render_import_settings_section() -> impl IntoElement { v_flex() .gap_4() @@ -312,6 +361,32 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In .gap_5() .child(Label::new("Popular Settings").size(LabelSize::Large).mt_8()) .child(render_font_customization_section(window, cx)) + .child(SwitchField::new( + "onboarding-font-ligatures", + "Font Ligatures", + Some("Combine text characters into their associated symbols.".into()), + if read_font_ligatures(cx) { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + |toggle_state, _, cx| { + write_font_ligatures(toggle_state == &ToggleState::Selected, cx); + }, + )) + .child(SwitchField::new( + "onboarding-format-on-save", + "Format on Save", + Some("Format code automatically when saving.".into()), + if read_format_on_save(cx) { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + |toggle_state, _, cx| { + write_format_on_save(toggle_state == &ToggleState::Selected, cx); + }, + )) .child( h_flex() .items_start() From 4d79edc7533d6d0a8e29004c789bc6041a97993d Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Sat, 2 Aug 2025 03:42:11 +0530 Subject: [PATCH 027/693] project: Fix extra } at the end of import on completion accept (#35494) Closes #34094 Bug in https://github.com/zed-industries/zed/pull/11157 **Context:** In https://github.com/zed-industries/zed/pull/31872, we added logic to avoid re-querying language server completions (`textDocument/completion`) when possible. This means the list of `lsp::CompletionItem` objects we have might be stale and not contain accurate data like `text_edit`, which is only valid for the buffer at the initial position when these completions were requested. We don't really care about this because we already extract all the useful data we need (like insert/replace ranges) into `Completion`, which converts `text_edit` to anchors. This means further user edits simply push/move those anchors, and our insert/replace ranges persist for completion accept. ```jsonc // on initial textDocument/completion "textEdit":{"insert":{"start":{"line":2,"character":0},"end":{"line":2,"character":11}},"replace":{"start":{"line":2,"character":0},"end":{"line":2,"character":11}} ``` However, for showing documentation of visible `Completion` items, we need to call resolve (`completionItem/resolve`) with the existing `lsp::CompletionItem`, which returns the same `text_edit` and other existing data along with additional new data that was previously optional, like `documentation` and `detail`. **Problem:** This new data like `documentation` and `detail` doesn't really change on buffer edits for a given completion item, so we can use it. But `text_edit` from this resolved `lsp::CompletionItem` was valid when the the initial (`textDocument/completion`) was queried but now the underlying buffer is different. Hence, creating anchors from this ends up creating them in wrong places. ```jsonc // calling completionItem/resolve on cached lsp::CompletionItem results into same textEdit, despite buffer edits "textEdit":{"insert":{"start":{"line":2,"character":0},"end":{"line":2,"character":11}},"replace":{"start":{"line":2,"character":0},"end":{"line":2,"character":11}} ``` It looks like the only reason to override the new text and these ranges was to handle an edge case with `typescript-language-server`, as mentioned in the code comment. However, according to the LSP specification for [Completion Request](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion): > All other properties (usually sortText, filterText, insertText and textEdit) must be provided in the textDocument/completion response and **must not be changed during resolve.** If any language server responds with different `textEdit`, `insertText`, etc. in `completionItem/resolve` than in `textDocument/completion`, they should fix that. Bug in this case in `typescript-language-server`: https://github.com/typescript-language-server/typescript-language-server/pull/303#discussion_r869102064 We don't really need to override these at all. Keeping the existing Anchors results in correct replacement. Release Notes: - Fixed issue where in some cases there would be an extra `}` at the end of imports when accepting completions. --- crates/editor/src/editor.rs | 7 ------- crates/project/src/lsp_store.rs | 26 ++------------------------ 2 files changed, 2 insertions(+), 31 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3516eff45c..97d2a10f63 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -21129,13 +21129,6 @@ fn process_completion_for_edit( .is_le(), "replace_range should start before or at cursor position" ); - debug_assert!( - insert_range - .end - .cmp(&cursor_position, &buffer_snapshot) - .is_le(), - "insert_range should end before or at cursor position" - ); let should_replace = match intent { CompletionIntent::CompleteWithInsert => false, diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index af3df72c29..696892abb7 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -6044,7 +6044,6 @@ impl LspStore { let resolved = Self::resolve_completion_local( server, - &buffer_snapshot, completions.clone(), completion_index, ) @@ -6077,7 +6076,6 @@ impl LspStore { async fn resolve_completion_local( server: Arc, - snapshot: &BufferSnapshot, completions: Rc>>, completion_index: usize, ) -> Result<()> { @@ -6122,26 +6120,8 @@ impl LspStore { .into_response() .context("resolve completion")?; - if let Some(text_edit) = resolved_completion.text_edit.as_ref() { - // Technically we don't have to parse the whole `text_edit`, since the only - // language server we currently use that does update `text_edit` in `completionItem/resolve` - // is `typescript-language-server` and they only update `text_edit.new_text`. - // But we should not rely on that. - let edit = parse_completion_text_edit(text_edit, snapshot); - - if let Some(mut parsed_edit) = edit { - LineEnding::normalize(&mut parsed_edit.new_text); - - let mut completions = completions.borrow_mut(); - let completion = &mut completions[completion_index]; - - completion.new_text = parsed_edit.new_text; - completion.replace_range = parsed_edit.replace_range; - if let CompletionSource::Lsp { insert_range, .. } = &mut completion.source { - *insert_range = parsed_edit.insert_range; - } - } - } + // We must not use any data such as sortText, filterText, insertText and textEdit to edit `Completion` since they are not suppose change during resolve. + // Refer: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion let mut completions = completions.borrow_mut(); let completion = &mut completions[completion_index]; @@ -6391,12 +6371,10 @@ impl LspStore { }) else { return Task::ready(Ok(None)); }; - let snapshot = buffer_handle.read(&cx).snapshot(); cx.spawn(async move |this, cx| { Self::resolve_completion_local( server.clone(), - &snapshot, completions.clone(), completion_index, ) From a8422d4f77710f5983404c46ff276d228e938895 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Fri, 1 Aug 2025 22:09:05 -0600 Subject: [PATCH 028/693] Fix showing/hiding copilot actions when `disable_ai` setting is changed (#35506) Release Notes: - N/A --- crates/copilot/src/copilot.rs | 84 +++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index e11242cb15..cacf834e0d 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -85,45 +85,13 @@ pub fn init( move |cx| Copilot::start(new_server_id, fs, node_runtime, cx) }); Copilot::set_global(copilot.clone(), cx); - cx.observe(&copilot, |handle, cx| { - let copilot_action_types = [ - TypeId::of::(), - TypeId::of::(), - TypeId::of::(), - TypeId::of::(), - ]; - let copilot_auth_action_types = [TypeId::of::()]; - let copilot_no_auth_action_types = [TypeId::of::()]; - let status = handle.read(cx).status(); - - let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai; - let filter = CommandPaletteFilter::global_mut(cx); - - if is_ai_disabled { - filter.hide_action_types(&copilot_action_types); - filter.hide_action_types(&copilot_auth_action_types); - filter.hide_action_types(&copilot_no_auth_action_types); - } else { - match status { - Status::Disabled => { - filter.hide_action_types(&copilot_action_types); - filter.hide_action_types(&copilot_auth_action_types); - filter.hide_action_types(&copilot_no_auth_action_types); - } - Status::Authorized => { - filter.hide_action_types(&copilot_no_auth_action_types); - filter.show_action_types( - copilot_action_types - .iter() - .chain(&copilot_auth_action_types), - ); - } - _ => { - filter.hide_action_types(&copilot_action_types); - filter.hide_action_types(&copilot_auth_action_types); - filter.show_action_types(copilot_no_auth_action_types.iter()); - } - } + cx.observe(&copilot, |copilot, cx| { + copilot.update(cx, |copilot, cx| copilot.update_action_visibilities(cx)); + }) + .detach(); + cx.observe_global::(|cx| { + if let Some(copilot) = Copilot::global(cx) { + copilot.update(cx, |copilot, cx| copilot.update_action_visibilities(cx)); } }) .detach(); @@ -1131,6 +1099,44 @@ impl Copilot { cx.notify(); } } + + fn update_action_visibilities(&self, cx: &mut App) { + let signed_in_actions = [ + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + ]; + let auth_actions = [TypeId::of::()]; + let no_auth_actions = [TypeId::of::()]; + let status = self.status(); + + let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai; + let filter = CommandPaletteFilter::global_mut(cx); + + if is_ai_disabled { + filter.hide_action_types(&signed_in_actions); + filter.hide_action_types(&auth_actions); + filter.hide_action_types(&no_auth_actions); + } else { + match status { + Status::Disabled => { + filter.hide_action_types(&signed_in_actions); + filter.hide_action_types(&auth_actions); + filter.hide_action_types(&no_auth_actions); + } + Status::Authorized => { + filter.hide_action_types(&no_auth_actions); + filter.show_action_types(signed_in_actions.iter().chain(&auth_actions)); + } + _ => { + filter.hide_action_types(&signed_in_actions); + filter.hide_action_types(&auth_actions); + filter.show_action_types(no_auth_actions.iter()); + } + } + } + } } fn id_for_language(language: Option<&Arc>) -> String { From a50d0f2586873fa92d4bfa3b09a3c332455002fa Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Fri, 1 Aug 2025 22:15:58 -0600 Subject: [PATCH 029/693] Make `editor::AcceptPartialCopilotSuggestion` a deprecated alias (#35507) This is consistent with there being no copilot expecific variant of `editor::AcceptEditPrediction`. It also fixes a case where the `disable_ai: true` has effects at init time that aren't undone when changed, added in #35327. Release Notes: - N/A --- crates/editor/src/actions.rs | 3 +-- crates/zed/src/zed/inline_completion_registry.rs | 16 ++-------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 1212651cb3..3a3a57ca64 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -315,9 +315,8 @@ actions!( [ /// Accepts the full edit prediction. AcceptEditPrediction, - /// Accepts a partial Copilot suggestion. - AcceptPartialCopilotSuggestion, /// Accepts a partial edit prediction. + #[action(deprecated_aliases = ["editor::AcceptPartialCopilotSuggestion"])] AcceptPartialEditPrediction, /// Adds a cursor above the current selection. AddSelectionAbove, diff --git a/crates/zed/src/zed/inline_completion_registry.rs b/crates/zed/src/zed/inline_completion_registry.rs index 55dbea4fe1..bbecd26417 100644 --- a/crates/zed/src/zed/inline_completion_registry.rs +++ b/crates/zed/src/zed/inline_completion_registry.rs @@ -1,10 +1,10 @@ -use client::{Client, DisableAiSettings, UserStore}; +use client::{Client, UserStore}; use collections::HashMap; use copilot::{Copilot, CopilotCompletionProvider}; use editor::Editor; use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, WeakEntity}; use language::language_settings::{EditPredictionProvider, all_language_settings}; -use settings::{Settings as _, SettingsStore}; +use settings::SettingsStore; use smol::stream::StreamExt; use std::{cell::RefCell, rc::Rc, sync::Arc}; use supermaven::{Supermaven, SupermavenCompletionProvider}; @@ -192,18 +192,6 @@ fn register_backward_compatible_actions(editor: &mut Editor, cx: &mut Context| { - editor.accept_partial_inline_completion(&Default::default(), window, cx); - }, - )) - .detach(); - } } fn assign_edit_prediction_provider( From f4391ed6318396f971617797a243485ed4931cf8 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Fri, 1 Aug 2025 23:05:03 -0600 Subject: [PATCH 030/693] Cleanup `editor.rs` imports (#35509) Release Notes: - N/A --- crates/editor/src/editor.rs | 131 ++++++++++------------ crates/outline_panel/src/outline_panel.rs | 6 +- crates/vim/src/vim.rs | 2 +- 3 files changed, 66 insertions(+), 73 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 97d2a10f63..49484ed137 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -51,42 +51,56 @@ mod signature_help; pub mod test; pub(crate) use actions::*; -pub use actions::{AcceptEditPrediction, OpenExcerpts, OpenExcerptsSplit}; +pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder}; +pub use editor_settings::{ + CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode, + ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap, ShowScrollbar, +}; +pub use editor_settings_controls::*; +pub use element::{ + CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, +}; +pub use git::blame::BlameRenderer; +pub use hover_popover::hover_markdown_style; +pub use inline_completion::Direction; +pub use items::MAX_TAB_TITLE_LEN; +pub use lsp::CompletionContext; +pub use lsp_ext::lsp_tasks; +pub use multi_buffer::{ + Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, PathKey, + RowInfo, ToOffset, ToPoint, +}; +pub use proposed_changes_editor::{ + ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar, +}; +pub use text::Bias; + +use ::git::{ + Restore, + blame::{BlameEntry, ParsedCommitMessage}, +}; use aho_corasick::AhoCorasick; use anyhow::{Context as _, Result, anyhow}; use blink_manager::BlinkManager; use buffer_diff::DiffHunkStatus; use client::{Collaborator, DisableAiSettings, ParticipantIndex}; use clock::{AGENT_REPLICA_ID, ReplicaId}; +use code_context_menus::{ + AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu, + CompletionsMenu, ContextMenuOrigin, +}; use collections::{BTreeMap, HashMap, HashSet, VecDeque}; use convert_case::{Case, Casing}; use dap::TelemetrySpawnLocation; use display_map::*; -pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder}; -pub use editor_settings::{ - CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode, - ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap, ShowScrollbar, -}; use editor_settings::{GoToDefinitionFallback, Minimap as MinimapSettings}; -pub use editor_settings_controls::*; use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap, layout_line}; -pub use element::{ - CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, -}; use futures::{ FutureExt, StreamExt as _, future::{self, Shared, join}, stream::FuturesUnordered, }; use fuzzy::{StringMatch, StringMatchCandidate}; -use lsp_colors::LspColorData; - -use ::git::blame::BlameEntry; -use ::git::{Restore, blame::ParsedCommitMessage}; -use code_context_menus::{ - AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu, - CompletionsMenu, ContextMenuOrigin, -}; use git::blame::{GitBlame, GlobalBlameRenderer}; use gpui::{ Action, Animation, AnimationExt, AnyElement, App, AppContext, AsyncWindowContext, @@ -100,32 +114,43 @@ use gpui::{ }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file}; -pub use hover_popover::hover_markdown_style; use hover_popover::{HoverState, hide_hover}; use indent_guides::ActiveIndentGuidesState; use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; -pub use inline_completion::Direction; use inline_completion::{EditPredictionProvider, InlineCompletionProviderHandle}; -pub use items::MAX_TAB_TITLE_LEN; use itertools::Itertools; use language::{ - AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, Capability, CharKind, - CodeLabel, CursorShape, DiagnosticEntry, DiffOptions, EditPredictionsMode, EditPreview, - HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, - SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery, + AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow, + BufferSnapshot, Capability, CharClassifier, CharKind, CodeLabel, CursorShape, DiagnosticEntry, + DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind, IndentSize, + Language, OffsetRangeExt, Point, Runnable, RunnableRange, Selection, SelectionGoal, TextObject, + TransactionId, TreeSitterOptions, WordsQuery, language_settings::{ self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode, all_language_settings, language_settings, }, - point_from_lsp, text_diff_with_options, + point_from_lsp, point_to_lsp, text_diff_with_options, }; -use language::{BufferRow, CharClassifier, Runnable, RunnableRange, point_to_lsp}; use linked_editing_ranges::refresh_linked_ranges; +use lsp::{ + CodeActionKind, CompletionItemKind, CompletionTriggerKind, InsertTextFormat, InsertTextMode, + LanguageServerId, LanguageServerName, +}; +use lsp_colors::LspColorData; use markdown::Markdown; use mouse_context_menu::MouseContextMenu; +use movement::TextLayoutDetails; +use multi_buffer::{ + ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, + MultiOrSingleBufferOffsetRange, ToOffsetUtf16, +}; +use parking_lot::Mutex; use persistence::DB; use project::{ - BreakpointWithPosition, CompletionResponse, ProjectPath, + BreakpointWithPosition, CodeAction, Completion, CompletionIntent, CompletionResponse, + CompletionSource, DocumentHighlight, InlayHint, Location, LocationLink, PrepareRenameResponse, + Project, ProjectItem, ProjectPath, ProjectTransaction, TaskSourceKind, + debugger::breakpoint_store::Breakpoint, debugger::{ breakpoint_store::{ BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore, @@ -134,44 +159,12 @@ use project::{ session::{Session, SessionEvent}, }, git_store::{GitStoreEvent, RepositoryEvent}, - project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter}, -}; - -pub use git::blame::BlameRenderer; -pub use proposed_changes_editor::{ - ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar, -}; -use std::{cell::OnceCell, iter::Peekable, ops::Not}; -use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables}; - -pub use lsp::CompletionContext; -use lsp::{ - CodeActionKind, CompletionItemKind, CompletionTriggerKind, InsertTextFormat, InsertTextMode, - LanguageServerId, LanguageServerName, -}; - -use language::BufferSnapshot; -pub use lsp_ext::lsp_tasks; -use movement::TextLayoutDetails; -pub use multi_buffer::{ - Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, PathKey, - RowInfo, ToOffset, ToPoint, -}; -use multi_buffer::{ - ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, - MultiOrSingleBufferOffsetRange, ToOffsetUtf16, -}; -use parking_lot::Mutex; -use project::{ - CodeAction, Completion, CompletionIntent, CompletionSource, DocumentHighlight, InlayHint, - Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, ProjectTransaction, - TaskSourceKind, - debugger::breakpoint_store::Breakpoint, lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle}, + project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter}, project_settings::{GitGutterSetting, ProjectSettings}, }; -use rand::prelude::*; -use rpc::{ErrorExt, proto::*}; +use rand::{seq::SliceRandom, thread_rng}; +use rpc::{ErrorCode, ErrorExt, proto::PeerId}; use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide}; use selections_collection::{ MutableSelectionsCollection, SelectionsCollection, resolve_selections, @@ -180,21 +173,24 @@ use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsLocation, SettingsStore, update_settings_file}; use smallvec::{SmallVec, smallvec}; use snippet::Snippet; -use std::sync::Arc; use std::{ any::TypeId, borrow::Cow, + cell::OnceCell, cell::RefCell, cmp::{self, Ordering, Reverse}, + iter::Peekable, mem, num::NonZeroU32, + ops::Not, ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive}, path::{Path, PathBuf}, rc::Rc, + sync::Arc, time::{Duration, Instant}, }; -pub use sum_tree::Bias; use sum_tree::TreeMap; +use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables}; use text::{BufferId, FromAnchor, OffsetUtf16, Rope}; use theme::{ ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, Theme, ThemeSettings, @@ -213,14 +209,11 @@ use workspace::{ notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt}, searchable::SearchEvent, }; -use zed_actions; use crate::{ code_context_menus::CompletionsMenuSource, - hover_links::{find_url, find_url_from_range}, -}; -use crate::{ editor_settings::MultiCursorModifier, + hover_links::{find_url, find_url_from_range}, signature_help::{SignatureHelpHiddenBy, SignatureHelpState}, }; diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 50c6c2dcce..ad96670db9 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -1041,7 +1041,7 @@ impl OutlinePanel { fn open_excerpts( &mut self, - action: &editor::OpenExcerpts, + action: &editor::actions::OpenExcerpts, window: &mut Window, cx: &mut Context, ) { @@ -1057,7 +1057,7 @@ impl OutlinePanel { fn open_excerpts_split( &mut self, - action: &editor::OpenExcerptsSplit, + action: &editor::actions::OpenExcerptsSplit, window: &mut Window, cx: &mut Context, ) { @@ -5958,7 +5958,7 @@ mod tests { }); outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.open_excerpts(&editor::OpenExcerpts, window, cx); + outline_panel.open_excerpts(&editor::actions::OpenExcerpts, window, cx); }); cx.executor() .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index c747c30462..2f759ec8af 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -747,7 +747,7 @@ impl Vim { Vim::action( editor, cx, - |vim, action: &editor::AcceptEditPrediction, window, cx| { + |vim, action: &editor::actions::AcceptEditPrediction, window, cx| { vim.update_editor(window, cx, |_, editor, window, cx| { editor.accept_edit_prediction(action, window, cx); }); From 986e3e7cbcf7f92f95ed70435c832013ce7e2d34 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Sat, 2 Aug 2025 23:49:22 +0200 Subject: [PATCH 031/693] agent_ui: Improve message editor history navigation (#35532) - We no longer move through history if a message has been edited by the user - It is possible to navigate back down to an empty message Co-authored-by: Cole Miller Release Notes: - N/A Co-authored-by: Cole Miller --- crates/agent_ui/src/acp/thread_view.rs | 82 ++++++++++++++++++++------ 1 file changed, 63 insertions(+), 19 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 8820e4a73d..e058284abc 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -31,7 +31,7 @@ use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; use parking_lot::Mutex; use project::Project; use settings::Settings as _; -use text::Anchor; +use text::{Anchor, BufferSnapshot}; use theme::ThemeSettings; use ui::{Disclosure, Divider, DividerColor, KeyBinding, Tooltip, prelude::*}; use util::ResultExt; @@ -61,7 +61,7 @@ pub struct AcpThreadView { thread_state: ThreadState, diff_editors: HashMap>, message_editor: Entity, - message_set_from_history: bool, + message_set_from_history: Option, _message_editor_subscription: Subscription, mention_set: Arc>, notifications: Vec>, @@ -144,14 +144,28 @@ impl AcpThreadView { editor }); - let message_editor_subscription = cx.subscribe(&message_editor, |this, _, event, _| { - if let editor::EditorEvent::BufferEdited = &event { - if !this.message_set_from_history { - this.message_history.borrow_mut().reset_position(); + let message_editor_subscription = + cx.subscribe(&message_editor, |this, editor, event, cx| { + if let editor::EditorEvent::BufferEdited = &event { + let buffer = editor + .read(cx) + .buffer() + .read(cx) + .as_singleton() + .unwrap() + .read(cx) + .snapshot(); + if let Some(message) = this.message_set_from_history.clone() + && message.version() != buffer.version() + { + this.message_set_from_history = None; + } + + if this.message_set_from_history.is_none() { + this.message_history.borrow_mut().reset_position(); + } } - this.message_set_from_history = false; - } - }); + }); let mention_set = mention_set.clone(); @@ -178,7 +192,7 @@ impl AcpThreadView { project: project.clone(), thread_state: Self::initial_state(agent, workspace, project, window, cx), message_editor, - message_set_from_history: false, + message_set_from_history: None, _message_editor_subscription: message_editor_subscription, mention_set, notifications: Vec::new(), @@ -424,11 +438,21 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) { + if self.message_set_from_history.is_none() && !self.message_editor.read(cx).is_empty(cx) { + self.message_editor.update(cx, |editor, cx| { + editor.move_up(&Default::default(), window, cx); + }); + return; + } + self.message_set_from_history = Self::set_draft_message( self.message_editor.clone(), self.mention_set.clone(), self.project.clone(), - self.message_history.borrow_mut().prev(), + self.message_history + .borrow_mut() + .prev() + .map(|blocks| blocks.as_slice()), window, cx, ); @@ -440,14 +464,35 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) { - self.message_set_from_history = Self::set_draft_message( + if self.message_set_from_history.is_none() { + self.message_editor.update(cx, |editor, cx| { + editor.move_down(&Default::default(), window, cx); + }); + return; + } + + let mut message_history = self.message_history.borrow_mut(); + let next_history = message_history.next(); + + let set_draft_message = Self::set_draft_message( self.message_editor.clone(), self.mention_set.clone(), self.project.clone(), - self.message_history.borrow_mut().next(), + Some( + next_history + .map(|blocks| blocks.as_slice()) + .unwrap_or_else(|| &[]), + ), window, cx, ); + // If we reset the text to an empty string because we ran out of history, + // we don't want to mark it as coming from the history + self.message_set_from_history = if next_history.is_some() { + set_draft_message + } else { + None + }; } fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context) { @@ -481,15 +526,13 @@ impl AcpThreadView { message_editor: Entity, mention_set: Arc>, project: Entity, - message: Option<&Vec>, + message: Option<&[acp::ContentBlock]>, window: &mut Window, cx: &mut Context, - ) -> bool { + ) -> Option { cx.notify(); - let Some(message) = message else { - return false; - }; + let message = message?; let mut text = String::new(); let mut mentions = Vec::new(); @@ -553,7 +596,8 @@ impl AcpThreadView { } } - true + let snapshot = snapshot.as_singleton().unwrap().2.clone(); + Some(snapshot.text) } fn handle_thread_event( From 4417bfe30b73eb0607f2e9a2337fb8fc5494b9f4 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sun, 3 Aug 2025 01:56:33 -0400 Subject: [PATCH 032/693] Fix pinned tab becoming unpinned when dragged onto unpinned tab (#35539) Case A - Correct: https://github.com/user-attachments/assets/2ab943ea-ca5b-4b6b-a8ca-a0b02072293e Case B - Incorrect: https://github.com/user-attachments/assets/912be46a-73b2-48a8-b490-277a1e89d17d Case B - Fixed: https://github.com/user-attachments/assets/98c2311d-eebc-4091-ad7a-6cf857fda9c3 Release Notes: - Fixed a bug where dragging a pinned tab onto an unpinned tab wouldn't decrease the pinned tab count --- crates/workspace/src/pane.rs | 49 +++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index c7a2562a1b..74f9fc18d9 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -3030,7 +3030,7 @@ impl Pane { || cfg!(not(target_os = "macos")) && window.modifiers().control; let from_pane = dragged_tab.pane.clone(); - let from_ix = dragged_tab.ix; + self.workspace .update(cx, |_, cx| { cx.defer_in(window, move |workspace, window, cx| { @@ -3062,9 +3062,13 @@ impl Pane { } to_pane.update(cx, |this, _| { if to_pane == from_pane { - let moved_right = ix > from_ix; - let ix = if moved_right { ix - 1 } else { ix }; - let is_pinned_in_to_pane = this.is_tab_pinned(ix); + let to_ix = this + .items + .iter() + .position(|item| item.item_id() == item_id) + .unwrap_or(0); + + let is_pinned_in_to_pane = to_ix < this.pinned_tab_count; if !was_pinned_in_from_pane && is_pinned_in_to_pane { this.pinned_tab_count += 1; @@ -4950,6 +4954,43 @@ mod tests { assert_item_labels(&pane_a, ["B!", "A*!"], cx); } + #[gpui::test] + async fn test_dragging_pinned_tab_onto_unpinned_tab_reduces_unpinned_tab_count( + cx: &mut TestAppContext, + ) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + + let project = Project::test(fs, None, cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + // Add A, B to pane A and pin A + let item_a = add_labeled_item(&pane_a, "A", false, cx); + add_labeled_item(&pane_a, "B", false, cx); + pane_a.update_in(cx, |pane, window, cx| { + let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); + pane.pin_tab_at(ix, window, cx); + }); + assert_item_labels(&pane_a, ["A!", "B*"], cx); + + // Drag pinned A on top of B in the same pane, which changes tab order to B, A + pane_a.update_in(cx, |pane, window, cx| { + let dragged_tab = DraggedTab { + pane: pane_a.clone(), + item: item_a.boxed_clone(), + ix: 0, + detail: 0, + is_active: true, + }; + pane.handle_tab_drop(&dragged_tab, 1, window, cx); + }); + + // Neither are pinned + assert_item_labels(&pane_a, ["B", "A*"], cx); + } + #[gpui::test] async fn test_drag_pinned_tab_beyond_unpinned_tab_in_same_pane_becomes_unpinned( cx: &mut TestAppContext, From 1b9302d452a345e8de280f99a2dd45a2afbbe343 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sun, 3 Aug 2025 15:45:26 -0400 Subject: [PATCH 033/693] Reuse `is_tab_pinned` method (#35551) Release Notes: - N/A --- crates/workspace/src/pane.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 74f9fc18d9..ad1c74a040 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2832,7 +2832,7 @@ impl Pane { }) .collect::>(); let tab_count = tab_items.len(); - if self.pinned_tab_count > tab_count { + if self.is_tab_pinned(tab_count) { log::warn!( "Pinned tab count ({}) exceeds actual tab count ({}). \ This should not happen. If possible, add reproduction steps, \ @@ -3062,13 +3062,13 @@ impl Pane { } to_pane.update(cx, |this, _| { if to_pane == from_pane { - let to_ix = this + let actual_ix = this .items .iter() .position(|item| item.item_id() == item_id) .unwrap_or(0); - let is_pinned_in_to_pane = to_ix < this.pinned_tab_count; + let is_pinned_in_to_pane = this.is_tab_pinned(actual_ix); if !was_pinned_in_from_pane && is_pinned_in_to_pane { this.pinned_tab_count += 1; From f14f0c24d6fe6d67e64c43b31ecf58fe6b011ab6 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Sun, 3 Aug 2025 22:50:25 +0200 Subject: [PATCH 034/693] Fix false positive for editing status in agent panel (#35554) Release Notes: - N/A --- Cargo.lock | 2 + crates/acp_thread/Cargo.toml | 2 + crates/acp_thread/src/acp_thread.rs | 148 +++++++++++++++++++++++++++- 3 files changed, 149 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 64470b5abe..45684b8920 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,7 +20,9 @@ dependencies = [ "itertools 0.14.0", "language", "markdown", + "parking_lot", "project", + "rand 0.8.5", "serde", "serde_json", "settings", diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index 011f26f364..cd7a5c3808 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -41,7 +41,9 @@ async-pipe.workspace = true env_logger.workspace = true gpui = { workspace = true, "features" = ["test-support"] } indoc.workspace = true +parking_lot.workspace = true project = { workspace = true, "features" = ["test-support"] } +rand.workspace = true tempfile.workspace = true util.workspace = true settings.workspace = true diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 7a10f3bd72..0996dee723 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -671,7 +671,18 @@ impl AcpThread { for entry in self.entries.iter().rev() { match entry { AgentThreadEntry::UserMessage(_) => return false, - AgentThreadEntry::ToolCall(call) if call.diffs().next().is_some() => return true, + AgentThreadEntry::ToolCall( + call @ ToolCall { + status: + ToolCallStatus::Allowed { + status: + acp::ToolCallStatus::InProgress | acp::ToolCallStatus::Pending, + }, + .. + }, + ) if call.diffs().next().is_some() => { + return true; + } AgentThreadEntry::ToolCall(_) | AgentThreadEntry::AssistantMessage(_) => {} } } @@ -1231,10 +1242,15 @@ mod tests { use agentic_coding_protocol as acp_old; use anyhow::anyhow; use async_pipe::{PipeReader, PipeWriter}; - use futures::{channel::mpsc, future::LocalBoxFuture, select}; - use gpui::{AsyncApp, TestAppContext}; + use futures::{ + channel::mpsc, + future::{LocalBoxFuture, try_join_all}, + select, + }; + use gpui::{AsyncApp, TestAppContext, WeakEntity}; use indoc::indoc; use project::FakeFs; + use rand::Rng as _; use serde_json::json; use settings::SettingsStore; use smol::{future::BoxedLocal, stream::StreamExt as _}; @@ -1562,6 +1578,42 @@ mod tests { }); } + #[gpui::test] + async fn test_no_pending_edits_if_tool_calls_are_completed(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree(path!("/test"), json!({})).await; + let project = Project::test(fs, [path!("/test").as_ref()], cx).await; + + let connection = Rc::new(StubAgentConnection::new(vec![ + acp::SessionUpdate::ToolCall(acp::ToolCall { + id: acp::ToolCallId("test".into()), + label: "Label".into(), + kind: acp::ToolKind::Edit, + status: acp::ToolCallStatus::Completed, + content: vec![acp::ToolCallContent::Diff { + diff: acp::Diff { + path: "/test/test.txt".into(), + old_text: None, + new_text: "foo".into(), + }, + }], + locations: vec![], + raw_input: None, + }), + ])); + + let thread = connection + .new_thread(project, Path::new(path!("/test")), &mut cx.to_async()) + .await + .unwrap(); + cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["Hi".into()], cx))) + .await + .unwrap(); + + assert!(cx.read(|cx| !thread.read(cx).has_pending_edit_tool_calls())); + } + async fn run_until_first_tool_call( thread: &Entity, cx: &mut TestAppContext, @@ -1589,6 +1641,96 @@ mod tests { } } + #[derive(Clone, Default)] + struct StubAgentConnection { + sessions: Arc>>>, + permission_requests: HashMap>, + updates: Vec, + } + + impl StubAgentConnection { + fn new(updates: Vec) -> Self { + Self { + updates, + permission_requests: HashMap::default(), + sessions: Arc::default(), + } + } + } + + impl AgentConnection for StubAgentConnection { + fn name(&self) -> &'static str { + "StubAgentConnection" + } + + fn new_thread( + self: Rc, + project: Entity, + _cwd: &Path, + cx: &mut gpui::AsyncApp, + ) -> Task>> { + let session_id = acp::SessionId( + rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(7) + .map(char::from) + .collect::() + .into(), + ); + let thread = cx + .new(|cx| AcpThread::new(self.clone(), project, session_id.clone(), cx)) + .unwrap(); + self.sessions.lock().insert(session_id, thread.downgrade()); + Task::ready(Ok(thread)) + } + + fn authenticate(&self, _cx: &mut App) -> Task> { + unimplemented!() + } + + fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task> { + let sessions = self.sessions.lock(); + let thread = sessions.get(¶ms.session_id).unwrap(); + let mut tasks = vec![]; + for update in &self.updates { + let thread = thread.clone(); + let update = update.clone(); + let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) = &update + && let Some(options) = self.permission_requests.get(&tool_call.id) + { + Some((tool_call.clone(), options.clone())) + } else { + None + }; + let task = cx.spawn(async move |cx| { + if let Some((tool_call, options)) = permission_request { + let permission = thread.update(cx, |thread, cx| { + thread.request_tool_call_permission( + tool_call.clone(), + options.clone(), + cx, + ) + })?; + permission.await?; + } + thread.update(cx, |thread, cx| { + thread.handle_session_update(update.clone(), cx).unwrap(); + })?; + anyhow::Ok(()) + }); + tasks.push(task); + } + cx.spawn(async move |_| { + try_join_all(tasks).await?; + Ok(()) + }) + } + + fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) { + unimplemented!() + } + } + pub fn fake_acp_thread( project: Entity, cx: &mut TestAppContext, From ea7c3a23fb2eb0f2c5d811eec3337897886482ee Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sun, 3 Aug 2025 19:25:23 -0400 Subject: [PATCH 035/693] Add option to open settings profile selector in user menu (#35556) Release Notes: - N/A --- crates/title_bar/src/title_bar.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 426d87ad13..a8b16d881f 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -682,6 +682,10 @@ impl TitleBar { ) .separator() .action("Settings", zed_actions::OpenSettings.boxed_clone()) + .action( + "Settings Profiles", + zed_actions::settings_profile_selector::Toggle.boxed_clone(), + ) .action("Key Bindings", Box::new(keybindings::OpenKeymapEditor)) .action( "Themes…", @@ -726,6 +730,10 @@ impl TitleBar { .menu(|window, cx| { ContextMenu::build(window, cx, |menu, _, _| { menu.action("Settings", zed_actions::OpenSettings.boxed_clone()) + .action( + "Settings Profiles", + zed_actions::settings_profile_selector::Toggle.boxed_clone(), + ) .action("Key Bindings", Box::new(keybindings::OpenKeymapEditor)) .action( "Themes…", From 2db19e19a540c5a6cfd4e16dbae90493f2a3a8be Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sun, 3 Aug 2025 21:17:56 -0700 Subject: [PATCH 036/693] Improve the yaml outline to show the key value if it's a simple string (#35562) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before: Screenshot 2025-08-03 at 8 58 16 PM After: Screenshot 2025-08-03 at 8 59 30 PM Release Notes: - Improved the yaml outline to include the key's value if it's a simple string. --- crates/languages/src/yaml/outline.scm | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/languages/src/yaml/outline.scm b/crates/languages/src/yaml/outline.scm index 7ab007835f..c5a7f8e5d4 100644 --- a/crates/languages/src/yaml/outline.scm +++ b/crates/languages/src/yaml/outline.scm @@ -1 +1,9 @@ -(block_mapping_pair key: (flow_node (plain_scalar (string_scalar) @name))) @item +(block_mapping_pair + key: + (flow_node + (plain_scalar + (string_scalar) @name)) + value: + (flow_node + (plain_scalar + (string_scalar) @context))?) @item From 1b3d6139b8709c697b9270e3531f0a2bf1862323 Mon Sep 17 00:00:00 2001 From: Ahmed ElSayed Date: Sun, 3 Aug 2025 23:17:42 -0700 Subject: [PATCH 037/693] Add libx11 to openSUSE build dependencies (#35553) building on opensuse fails without `libx11-devel` **Repro:** ```bash $ cd $(mktemp -d) $ git clone https://github.com/zed-industries/zed . $ docker run --rm -it -v $(pwd):/zed -w /zed opensuse/tumbleweed (opensuse) $ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh (opensuse) $ ./script/linux (opensuse) $ cargo build --release ``` **Expected:** to work **Actual:** ``` thread 'main' panicked at /root/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/x11-2.21.0/build.rs:42:14: called `Result::unwrap()` on an `Err` value: pkg-config exited with status code 1 > PKG_CONFIG_ALLOW_SYSTEM_LIBS=1 PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1 pkg-config --libs --cflags x11 'x11 >= 1.4.99.1' The system library `x11` required by crate `x11` was not found. The file `x11.pc` needs to be installed and the PKG_CONFIG_PATH environment variable must contain its parent directory. The PKG_CONFIG_PATH environment variable is not set. HINT: if you have installed the library, try setting PKG_CONFIG_PATH to the directory containing `x11.pc`. note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace warning: build failed, waiting for other jobs to finish... ``` --- script/linux | 1 + 1 file changed, 1 insertion(+) diff --git a/script/linux b/script/linux index 98ae026896..029278bea3 100755 --- a/script/linux +++ b/script/linux @@ -143,6 +143,7 @@ if [[ -n $zyp ]]; then gzip jq libvulkan1 + libx11-devel libxcb-devel libxkbcommon-devel libxkbcommon-x11-devel From 5ca5d90234e89f502e96ec48f13ea1ed964460dc Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 4 Aug 2025 10:12:02 +0300 Subject: [PATCH 038/693] Use a better type for language IDs field (#35566) Part of the preparation for proto capabilities. Release Notes: - N/A --- crates/extension/src/extension_manifest.rs | 2 +- crates/language/src/language.rs | 9 +++--- .../src/extension_lsp_adapter.rs | 4 +-- crates/languages/src/json.rs | 10 +++---- crates/languages/src/tailwind.rs | 28 +++++++++---------- crates/languages/src/typescript.rs | 11 ++++---- crates/languages/src/vtsls.rs | 10 +++---- crates/project/src/lsp_command.rs | 23 ++++++++------- crates/project/src/lsp_store.rs | 5 +--- 9 files changed, 52 insertions(+), 50 deletions(-) diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index e3235cf561..5852b3e3fc 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -163,7 +163,7 @@ pub struct LanguageServerManifestEntry { #[serde(default)] languages: Vec, #[serde(default)] - pub language_ids: HashMap, + pub language_ids: HashMap, #[serde(default)] pub code_action_kinds: Option>, } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 549afc931c..894625b982 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -161,7 +161,7 @@ pub struct CachedLspAdapter { pub name: LanguageServerName, pub disk_based_diagnostic_sources: Vec, pub disk_based_diagnostics_progress_token: Option, - language_ids: HashMap, + language_ids: HashMap, pub adapter: Arc, pub reinstall_attempt_count: AtomicU64, cached_binary: futures::lock::Mutex>, @@ -277,10 +277,11 @@ impl CachedLspAdapter { pub fn language_id(&self, language_name: &LanguageName) -> String { self.language_ids - .get(language_name.as_ref()) + .get(language_name) .cloned() .unwrap_or_else(|| language_name.lsp_id()) } + pub fn manifest_name(&self) -> Option { self.manifest_name .get_or_init(|| self.adapter.manifest_name()) @@ -573,8 +574,8 @@ pub trait LspAdapter: 'static + Send + Sync { None } - fn language_ids(&self) -> HashMap { - Default::default() + fn language_ids(&self) -> HashMap { + HashMap::default() } /// Support custom initialize params. diff --git a/crates/language_extension/src/extension_lsp_adapter.rs b/crates/language_extension/src/extension_lsp_adapter.rs index 58fbe6cda2..98b6fd4b5a 100644 --- a/crates/language_extension/src/extension_lsp_adapter.rs +++ b/crates/language_extension/src/extension_lsp_adapter.rs @@ -242,7 +242,7 @@ impl LspAdapter for ExtensionLspAdapter { ])) } - fn language_ids(&self) -> HashMap { + fn language_ids(&self) -> HashMap { // TODO: The language IDs can be provided via the language server options // in `extension.toml now but we're leaving these existing usages in place temporarily // to avoid any compatibility issues between Zed and the extension versions. @@ -250,7 +250,7 @@ impl LspAdapter for ExtensionLspAdapter { // We can remove once the following extension versions no longer see any use: // - php@0.0.1 if self.extension.manifest().id.as_ref() == "php" { - return HashMap::from_iter([("PHP".into(), "php".into())]); + return HashMap::from_iter([(LanguageName::new("PHP"), "php".into())]); } self.extension diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 15818730b8..601b4620c5 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -8,8 +8,8 @@ use futures::StreamExt; use gpui::{App, AsyncApp, Task}; use http_client::github::{GitHubLspBinaryVersion, latest_github_release}; use language::{ - ContextProvider, LanguageRegistry, LanguageToolchainStore, LocalFile as _, LspAdapter, - LspAdapterDelegate, + ContextProvider, LanguageName, LanguageRegistry, LanguageToolchainStore, LocalFile as _, + LspAdapter, LspAdapterDelegate, }; use lsp::{LanguageServerBinary, LanguageServerName}; use node_runtime::NodeRuntime; @@ -408,10 +408,10 @@ impl LspAdapter for JsonLspAdapter { Ok(config) } - fn language_ids(&self) -> HashMap { + fn language_ids(&self) -> HashMap { [ - ("JSON".into(), "json".into()), - ("JSONC".into(), "jsonc".into()), + (LanguageName::new("JSON"), "json".into()), + (LanguageName::new("JSONC"), "jsonc".into()), ] .into_iter() .collect() diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index cb4e939083..a7edbb148c 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -3,7 +3,7 @@ use async_trait::async_trait; use collections::HashMap; use futures::StreamExt; use gpui::AsyncApp; -use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; +use language::{LanguageName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; use lsp::{LanguageServerBinary, LanguageServerName}; use node_runtime::NodeRuntime; use project::{Fs, lsp_store::language_server_settings}; @@ -168,20 +168,20 @@ impl LspAdapter for TailwindLspAdapter { })) } - fn language_ids(&self) -> HashMap { + fn language_ids(&self) -> HashMap { HashMap::from_iter([ - ("Astro".to_string(), "astro".to_string()), - ("HTML".to_string(), "html".to_string()), - ("CSS".to_string(), "css".to_string()), - ("JavaScript".to_string(), "javascript".to_string()), - ("TSX".to_string(), "typescriptreact".to_string()), - ("Svelte".to_string(), "svelte".to_string()), - ("Elixir".to_string(), "phoenix-heex".to_string()), - ("HEEX".to_string(), "phoenix-heex".to_string()), - ("ERB".to_string(), "erb".to_string()), - ("HTML/ERB".to_string(), "erb".to_string()), - ("PHP".to_string(), "php".to_string()), - ("Vue.js".to_string(), "vue".to_string()), + (LanguageName::new("Astro"), "astro".to_string()), + (LanguageName::new("HTML"), "html".to_string()), + (LanguageName::new("CSS"), "css".to_string()), + (LanguageName::new("JavaScript"), "javascript".to_string()), + (LanguageName::new("TSX"), "typescriptreact".to_string()), + (LanguageName::new("Svelte"), "svelte".to_string()), + (LanguageName::new("Elixir"), "phoenix-heex".to_string()), + (LanguageName::new("HEEX"), "phoenix-heex".to_string()), + (LanguageName::new("ERB"), "erb".to_string()), + (LanguageName::new("HTML/ERB"), "erb".to_string()), + (LanguageName::new("PHP"), "php".to_string()), + (LanguageName::new("Vue.js"), "vue".to_string()), ]) } } diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index fb51544841..9dc3ee303d 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -8,7 +8,8 @@ use futures::future::join_all; use gpui::{App, AppContext, AsyncApp, Task}; use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url}; use language::{ - ContextLocation, ContextProvider, File, LanguageToolchainStore, LspAdapter, LspAdapterDelegate, + ContextLocation, ContextProvider, File, LanguageName, LanguageToolchainStore, LspAdapter, + LspAdapterDelegate, }; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName}; use node_runtime::NodeRuntime; @@ -741,11 +742,11 @@ impl LspAdapter for TypeScriptLspAdapter { })) } - fn language_ids(&self) -> HashMap { + fn language_ids(&self) -> HashMap { HashMap::from_iter([ - ("TypeScript".into(), "typescript".into()), - ("JavaScript".into(), "javascript".into()), - ("TSX".into(), "typescriptreact".into()), + (LanguageName::new("TypeScript"), "typescript".into()), + (LanguageName::new("JavaScript"), "javascript".into()), + (LanguageName::new("TSX"), "typescriptreact".into()), ]) } } diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index ca07673d5f..33751f733e 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -2,7 +2,7 @@ use anyhow::Result; use async_trait::async_trait; use collections::HashMap; use gpui::AsyncApp; -use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; +use language::{LanguageName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName}; use node_runtime::NodeRuntime; use project::{Fs, lsp_store::language_server_settings}; @@ -273,11 +273,11 @@ impl LspAdapter for VtslsLspAdapter { Ok(default_workspace_configuration) } - fn language_ids(&self) -> HashMap { + fn language_ids(&self) -> HashMap { HashMap::from_iter([ - ("TypeScript".into(), "typescript".into()), - ("JavaScript".into(), "javascript".into()), - ("TSX".into(), "typescriptreact".into()), + (LanguageName::new("TypeScript"), "typescript".into()), + (LanguageName::new("JavaScript"), "javascript".into()), + (LanguageName::new("TSX"), "typescriptreact".into()), ]) } } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 958921a0e6..2fd61ea0b2 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -3580,6 +3580,18 @@ impl LspCommand for GetCodeLens { } } +impl LinkedEditingRange { + pub fn check_server_capabilities(capabilities: ServerCapabilities) -> bool { + let Some(linked_editing_options) = capabilities.linked_editing_range_provider else { + return false; + }; + if let LinkedEditingRangeServerCapabilities::Simple(false) = linked_editing_options { + return false; + } + true + } +} + #[async_trait(?Send)] impl LspCommand for LinkedEditingRange { type Response = Vec>; @@ -3591,16 +3603,7 @@ impl LspCommand for LinkedEditingRange { } fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - let Some(linked_editing_options) = &capabilities - .server_capabilities - .linked_editing_range_provider - else { - return false; - }; - if let LinkedEditingRangeServerCapabilities::Simple(false) = linked_editing_options { - return false; - } - true + Self::check_server_capabilities(capabilities.server_capabilities) } fn to_lsp( diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 696892abb7..98cecc2e9b 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -5069,10 +5069,7 @@ impl LspStore { local .language_servers_for_buffer(buffer, cx) .filter(|(_, server)| { - server - .capabilities() - .linked_editing_range_provider - .is_some() + LinkedEditingRange::check_server_capabilities(server.capabilities()) }) .filter(|(adapter, _)| { scope From 7217439c97d0b9c0ff0c4e719b81678311b72c2b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 4 Aug 2025 10:41:23 +0200 Subject: [PATCH 039/693] Don't trigger authentication flow unless credentials expired (#35570) This fixes a regression introduced in https://github.com/zed-industries/zed/pull/35471, where we treated stored credentials as invalid when failing to retrieve the authenticated user for any reason. This had the side effect of triggering the auth flow even when e.g. the client/server had temporary networking issues. This pull request changes the logic to only trigger authentication when getting a 401 from the server. Release Notes: - N/A --- crates/client/src/client.rs | 102 +++++++++++++++--- .../cloud_api_client/src/cloud_api_client.rs | 69 ++++++++---- 2 files changed, 137 insertions(+), 34 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index b9b20aa4f2..e6d8f10d12 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -884,27 +884,28 @@ impl Client { let old_credentials = self.state.read().credentials.clone(); if let Some(old_credentials) = old_credentials { - self.cloud_client.set_credentials( - old_credentials.user_id as u32, - old_credentials.access_token.clone(), - ); - - // Fetch the authenticated user with the old credentials, to ensure they are still valid. - if self.cloud_client.get_authenticated_user().await.is_ok() { + if self + .cloud_client + .validate_credentials( + old_credentials.user_id as u32, + &old_credentials.access_token, + ) + .await? + { credentials = Some(old_credentials); } } if credentials.is_none() && try_provider { if let Some(stored_credentials) = self.credentials_provider.read_credentials(cx).await { - self.cloud_client.set_credentials( - stored_credentials.user_id as u32, - stored_credentials.access_token.clone(), - ); - - // Fetch the authenticated user with the stored credentials, and - // clear them from the credentials provider if that fails. - if self.cloud_client.get_authenticated_user().await.is_ok() { + if self + .cloud_client + .validate_credentials( + stored_credentials.user_id as u32, + &stored_credentials.access_token, + ) + .await? + { credentials = Some(stored_credentials); } else { self.credentials_provider @@ -1709,7 +1710,7 @@ pub fn parse_zed_link<'a>(link: &'a str, cx: &App) -> Option<&'a str> { #[cfg(test)] mod tests { use super::*; - use crate::test::FakeServer; + use crate::test::{FakeServer, parse_authorization_header}; use clock::FakeSystemClock; use gpui::{AppContext as _, BackgroundExecutor, TestAppContext}; @@ -1835,6 +1836,75 @@ mod tests { )); } + #[gpui::test(iterations = 10)] + async fn test_reauthenticate_only_if_unauthorized(cx: &mut TestAppContext) { + init_test(cx); + let auth_count = Arc::new(Mutex::new(0)); + let http_client = FakeHttpClient::create(|_request| async move { + Ok(http_client::Response::builder() + .status(200) + .body("".into()) + .unwrap()) + }); + let client = + cx.update(|cx| Client::new(Arc::new(FakeSystemClock::new()), http_client.clone(), cx)); + client.override_authenticate({ + let auth_count = auth_count.clone(); + move |cx| { + let auth_count = auth_count.clone(); + cx.background_spawn(async move { + *auth_count.lock() += 1; + Ok(Credentials { + user_id: 1, + access_token: auth_count.lock().to_string(), + }) + }) + } + }); + + let credentials = client.sign_in(false, &cx.to_async()).await.unwrap(); + assert_eq!(*auth_count.lock(), 1); + assert_eq!(credentials.access_token, "1"); + + // If credentials are still valid, signing in doesn't trigger authentication. + let credentials = client.sign_in(false, &cx.to_async()).await.unwrap(); + assert_eq!(*auth_count.lock(), 1); + assert_eq!(credentials.access_token, "1"); + + // If the server is unavailable, signing in doesn't trigger authentication. + http_client + .as_fake() + .replace_handler(|_, _request| async move { + Ok(http_client::Response::builder() + .status(503) + .body("".into()) + .unwrap()) + }); + client.sign_in(false, &cx.to_async()).await.unwrap_err(); + assert_eq!(*auth_count.lock(), 1); + + // If credentials became invalid, signing in triggers authentication. + http_client + .as_fake() + .replace_handler(|_, request| async move { + let credentials = parse_authorization_header(&request).unwrap(); + if credentials.access_token == "2" { + Ok(http_client::Response::builder() + .status(200) + .body("".into()) + .unwrap()) + } else { + Ok(http_client::Response::builder() + .status(401) + .body("".into()) + .unwrap()) + } + }); + let credentials = client.sign_in(false, &cx.to_async()).await.unwrap(); + assert_eq!(*auth_count.lock(), 2); + assert_eq!(credentials.access_token, "2"); + } + #[gpui::test(iterations = 10)] async fn test_authenticating_more_than_once( cx: &mut TestAppContext, diff --git a/crates/cloud_api_client/src/cloud_api_client.rs b/crates/cloud_api_client/src/cloud_api_client.rs index 6689475dae..edac051a0e 100644 --- a/crates/cloud_api_client/src/cloud_api_client.rs +++ b/crates/cloud_api_client/src/cloud_api_client.rs @@ -1,10 +1,10 @@ use std::sync::Arc; -use anyhow::{Result, anyhow}; +use anyhow::{Context, Result, anyhow}; pub use cloud_api_types::*; use futures::AsyncReadExt as _; use http_client::http::request; -use http_client::{AsyncBody, HttpClientWithUrl, Method, Request}; +use http_client::{AsyncBody, HttpClientWithUrl, Method, Request, StatusCode}; use parking_lot::RwLock; struct Credentials { @@ -40,27 +40,14 @@ impl CloudApiClient { *self.credentials.write() = None; } - fn authorization_header(&self) -> Result { - let guard = self.credentials.read(); - let credentials = guard - .as_ref() - .ok_or_else(|| anyhow!("No credentials provided"))?; - - Ok(format!( - "{} {}", - credentials.user_id, credentials.access_token - )) - } - fn build_request( &self, req: request::Builder, body: impl Into, ) -> Result> { - Ok(req - .header("Content-Type", "application/json") - .header("Authorization", self.authorization_header()?) - .body(body.into())?) + let credentials = self.credentials.read(); + let credentials = credentials.as_ref().context("no credentials provided")?; + build_request(req, body, credentials) } pub async fn get_authenticated_user(&self) -> Result { @@ -152,4 +139,50 @@ impl CloudApiClient { Ok(serde_json::from_str(&body)?) } + + pub async fn validate_credentials(&self, user_id: u32, access_token: &str) -> Result { + let request = build_request( + Request::builder().method(Method::GET).uri( + self.http_client + .build_zed_cloud_url("/client/users/me", &[])? + .as_ref(), + ), + AsyncBody::default(), + &Credentials { + user_id, + access_token: access_token.into(), + }, + )?; + + let mut response = self.http_client.send(request).await?; + + if response.status().is_success() { + Ok(true) + } else { + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + if response.status() == StatusCode::UNAUTHORIZED { + return Ok(false); + } else { + return Err(anyhow!( + "Failed to get authenticated user.\nStatus: {:?}\nBody: {body}", + response.status() + )); + } + } + } +} + +fn build_request( + req: request::Builder, + body: impl Into, + credentials: &Credentials, +) -> Result> { + Ok(req + .header("Content-Type", "application/json") + .header( + "Authorization", + format!("{} {}", credentials.user_id, credentials.access_token), + ) + .body(body.into())?) } From dea64d3373965ebc4f241bdee1edb703576c6505 Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Mon, 4 Aug 2025 05:49:51 -0700 Subject: [PATCH 040/693] Add icon for KDL files (#35377) 1753920601 Release Notes: - Added icon for KDL (`.kdl`) files --- assets/icons/file_icons/kdl.svg | 1 + crates/theme/src/icon_theme.rs | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 assets/icons/file_icons/kdl.svg diff --git a/assets/icons/file_icons/kdl.svg b/assets/icons/file_icons/kdl.svg new file mode 100644 index 0000000000..92d9f28428 --- /dev/null +++ b/assets/icons/file_icons/kdl.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crates/theme/src/icon_theme.rs b/crates/theme/src/icon_theme.rs index baa928d722..10fd1e002d 100644 --- a/crates/theme/src/icon_theme.rs +++ b/crates/theme/src/icon_theme.rs @@ -152,6 +152,7 @@ const FILE_SUFFIXES_BY_ICON_KEY: &[(&str, &[&str])] = &[ ("javascript", &["cjs", "js", "mjs"]), ("json", &["json"]), ("julia", &["jl"]), + ("kdl", &["kdl"]), ("kotlin", &["kt"]), ("lock", &["lock"]), ("log", &["log"]), @@ -315,6 +316,7 @@ const FILE_ICONS: &[(&str, &str)] = &[ ("javascript", "icons/file_icons/javascript.svg"), ("json", "icons/file_icons/code.svg"), ("julia", "icons/file_icons/julia.svg"), + ("kdl", "icons/file_icons/kdl.svg"), ("kotlin", "icons/file_icons/kotlin.svg"), ("lock", "icons/file_icons/lock.svg"), ("log", "icons/file_icons/info.svg"), From f17943e4a3ce724c13d724c33c87f4b62868857e Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Mon, 4 Aug 2025 15:49:41 +0200 Subject: [PATCH 041/693] Update to new agent schema (#35578) Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga --- Cargo.lock | 23 +- Cargo.toml | 2 +- crates/acp_thread/Cargo.toml | 2 - crates/acp_thread/src/acp_thread.rs | 570 +++++++----------- crates/acp_thread/src/connection.rs | 22 +- crates/agent_servers/Cargo.toml | 3 +- crates/agent_servers/src/acp.rs | 34 ++ .../src/acp/v0.rs} | 98 ++- crates/agent_servers/src/acp/v1.rs | 254 ++++++++ crates/agent_servers/src/agent_servers.rs | 5 +- crates/agent_servers/src/claude.rs | 17 +- crates/agent_servers/src/codex.rs | 319 ---------- crates/agent_servers/src/e2e_tests.rs | 3 - crates/agent_servers/src/gemini.rs | 157 +---- crates/agent_servers/src/mcp_server.rs | 207 ------- crates/agent_servers/src/settings.rs | 11 +- crates/agent_ui/src/acp/thread_view.rs | 87 ++- crates/agent_ui/src/agent_panel.rs | 33 - crates/agent_ui/src/agent_ui.rs | 2 - crates/context_server/src/client.rs | 14 +- crates/context_server/src/context_server.rs | 27 +- crates/context_server/src/listener.rs | 10 +- crates/context_server/src/protocol.rs | 9 +- 23 files changed, 741 insertions(+), 1168 deletions(-) create mode 100644 crates/agent_servers/src/acp.rs rename crates/{acp_thread/src/old_acp_support.rs => agent_servers/src/acp/v0.rs} (84%) create mode 100644 crates/agent_servers/src/acp/v1.rs delete mode 100644 crates/agent_servers/src/codex.rs delete mode 100644 crates/agent_servers/src/mcp_server.rs diff --git a/Cargo.lock b/Cargo.lock index 45684b8920..56210557d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,10 +7,8 @@ name = "acp_thread" version = "0.1.0" dependencies = [ "agent-client-protocol", - "agentic-coding-protocol", "anyhow", "assistant_tool", - "async-pipe", "buffer_diff", "editor", "env_logger 0.11.8", @@ -139,10 +137,14 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.11" +version = "0.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ec54650c1fc2d63498bab47eeeaa9eddc7d239d53f615b797a0e84f7ccc87b" +checksum = "22c5180e40d31a9998ffa5f8eb067667f0870908a4aeed65a6a299e2d1d95443" dependencies = [ + "anyhow", + "futures 0.3.31", + "log", + "parking_lot", "schemars", "serde", "serde_json", @@ -177,6 +179,7 @@ dependencies = [ "smol", "strum 0.27.1", "tempfile", + "thiserror 2.0.12", "ui", "util", "uuid", @@ -9572,9 +9575,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -11288,9 +11291,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -11298,9 +11301,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", diff --git a/Cargo.toml b/Cargo.toml index 5b97596d0c..5d852f8842 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -421,7 +421,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.11" +agent-client-protocol = "0.0.17" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index cd7a5c3808..225597415c 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -17,7 +17,6 @@ test-support = ["gpui/test-support", "project/test-support"] [dependencies] agent-client-protocol.workspace = true -agentic-coding-protocol.workspace = true anyhow.workspace = true assistant_tool.workspace = true buffer_diff.workspace = true @@ -37,7 +36,6 @@ util.workspace = true workspace-hack.workspace = true [dev-dependencies] -async-pipe.workspace = true env_logger.workspace = true gpui = { workspace = true, "features" = ["test-support"] } indoc.workspace = true diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 0996dee723..079a207358 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1,7 +1,5 @@ mod connection; -mod old_acp_support; pub use connection::*; -pub use old_acp_support::*; use agent_client_protocol as acp; use anyhow::{Context as _, Result}; @@ -391,7 +389,7 @@ impl ToolCallContent { cx: &mut App, ) -> Self { match content { - acp::ToolCallContent::ContentBlock(content) => Self::ContentBlock { + acp::ToolCallContent::Content { content } => Self::ContentBlock { content: ContentBlock::new(content, &language_registry, cx), }, acp::ToolCallContent::Diff { diff } => Self::Diff { @@ -619,6 +617,7 @@ impl Error for LoadError {} impl AcpThread { pub fn new( + title: impl Into, connection: Rc, project: Entity, session_id: acp::SessionId, @@ -631,7 +630,7 @@ impl AcpThread { shared_buffers: Default::default(), entries: Default::default(), plan: Default::default(), - title: connection.name().into(), + title: title.into(), project, send_task: None, connection, @@ -708,14 +707,14 @@ impl AcpThread { cx: &mut Context, ) -> Result<()> { match update { - acp::SessionUpdate::UserMessage(content_block) => { - self.push_user_content_block(content_block, cx); + acp::SessionUpdate::UserMessageChunk { content } => { + self.push_user_content_block(content, cx); } - acp::SessionUpdate::AgentMessageChunk(content_block) => { - self.push_assistant_content_block(content_block, false, cx); + acp::SessionUpdate::AgentMessageChunk { content } => { + self.push_assistant_content_block(content, false, cx); } - acp::SessionUpdate::AgentThoughtChunk(content_block) => { - self.push_assistant_content_block(content_block, true, cx); + acp::SessionUpdate::AgentThoughtChunk { content } => { + self.push_assistant_content_block(content, true, cx); } acp::SessionUpdate::ToolCall(tool_call) => { self.upsert_tool_call(tool_call, cx); @@ -984,10 +983,6 @@ impl AcpThread { cx.notify(); } - pub fn authenticate(&self, cx: &mut App) -> impl use<> + Future> { - self.connection.authenticate(cx) - } - #[cfg(any(test, feature = "test-support"))] pub fn send_raw( &mut self, @@ -1029,7 +1024,7 @@ impl AcpThread { let result = this .update(cx, |this, cx| { this.connection.prompt( - acp::PromptArguments { + acp::PromptRequest { prompt: message, session_id: this.session_id.clone(), }, @@ -1239,21 +1234,15 @@ impl AcpThread { #[cfg(test)] mod tests { use super::*; - use agentic_coding_protocol as acp_old; use anyhow::anyhow; - use async_pipe::{PipeReader, PipeWriter}; - use futures::{ - channel::mpsc, - future::{LocalBoxFuture, try_join_all}, - select, - }; + use futures::{channel::mpsc, future::LocalBoxFuture, select}; use gpui::{AsyncApp, TestAppContext, WeakEntity}; use indoc::indoc; use project::FakeFs; use rand::Rng as _; use serde_json::json; use settings::SettingsStore; - use smol::{future::BoxedLocal, stream::StreamExt as _}; + use smol::stream::StreamExt as _; use std::{cell::RefCell, rc::Rc, time::Duration}; use util::path; @@ -1274,7 +1263,15 @@ mod tests { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let (thread, _fake_server) = fake_acp_thread(project, cx); + let connection = Rc::new(FakeAgentConnection::new()); + let thread = cx + .spawn(async move |mut cx| { + connection + .new_thread(project, Path::new(path!("/test")), &mut cx) + .await + }) + .await + .unwrap(); // Test creating a new user message thread.update(cx, |thread, cx| { @@ -1354,34 +1351,40 @@ mod tests { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let (thread, fake_server) = fake_acp_thread(project, cx); + let connection = Rc::new(FakeAgentConnection::new().on_user_message( + |_, thread, mut cx| { + async move { + thread.update(&mut cx, |thread, cx| { + thread + .handle_session_update( + acp::SessionUpdate::AgentThoughtChunk { + content: "Thinking ".into(), + }, + cx, + ) + .unwrap(); + thread + .handle_session_update( + acp::SessionUpdate::AgentThoughtChunk { + content: "hard!".into(), + }, + cx, + ) + .unwrap(); + }) + } + .boxed_local() + }, + )); - fake_server.update(cx, |fake_server, _| { - fake_server.on_user_message(move |_, server, mut cx| async move { - server - .update(&mut cx, |server, _| { - server.send_to_zed(acp_old::StreamAssistantMessageChunkParams { - chunk: acp_old::AssistantMessageChunk::Thought { - thought: "Thinking ".into(), - }, - }) - })? + let thread = cx + .spawn(async move |mut cx| { + connection + .new_thread(project, Path::new(path!("/test")), &mut cx) .await - .unwrap(); - server - .update(&mut cx, |server, _| { - server.send_to_zed(acp_old::StreamAssistantMessageChunkParams { - chunk: acp_old::AssistantMessageChunk::Thought { - thought: "hard!".into(), - }, - }) - })? - .await - .unwrap(); - - Ok(()) }) - }); + .await + .unwrap(); thread .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx)) @@ -1414,7 +1417,38 @@ mod tests { fs.insert_tree(path!("/tmp"), json!({"foo": "one\ntwo\nthree\n"})) .await; let project = Project::test(fs.clone(), [], cx).await; - let (thread, fake_server) = fake_acp_thread(project.clone(), cx); + let (read_file_tx, read_file_rx) = oneshot::channel::<()>(); + let read_file_tx = Rc::new(RefCell::new(Some(read_file_tx))); + let connection = Rc::new(FakeAgentConnection::new().on_user_message( + move |_, thread, mut cx| { + let read_file_tx = read_file_tx.clone(); + async move { + let content = thread + .update(&mut cx, |thread, cx| { + thread.read_text_file(path!("/tmp/foo").into(), None, None, false, cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(content, "one\ntwo\nthree\n"); + read_file_tx.take().unwrap().send(()).unwrap(); + thread + .update(&mut cx, |thread, cx| { + thread.write_text_file( + path!("/tmp/foo").into(), + "one\ntwo\nthree\nfour\nfive\n".to_string(), + cx, + ) + }) + .unwrap() + .await + .unwrap(); + Ok(()) + } + .boxed_local() + }, + )); + let (worktree, pathbuf) = project .update(cx, |project, cx| { project.find_or_create_worktree(path!("/tmp/foo"), true, cx) @@ -1428,38 +1462,10 @@ mod tests { .await .unwrap(); - let (read_file_tx, read_file_rx) = oneshot::channel::<()>(); - let read_file_tx = Rc::new(RefCell::new(Some(read_file_tx))); - - fake_server.update(cx, |fake_server, _| { - fake_server.on_user_message(move |_, server, mut cx| { - let read_file_tx = read_file_tx.clone(); - async move { - let content = server - .update(&mut cx, |server, _| { - server.send_to_zed(acp_old::ReadTextFileParams { - path: path!("/tmp/foo").into(), - line: None, - limit: None, - }) - })? - .await - .unwrap(); - assert_eq!(content.content, "one\ntwo\nthree\n"); - read_file_tx.take().unwrap().send(()).unwrap(); - server - .update(&mut cx, |server, _| { - server.send_to_zed(acp_old::WriteTextFileParams { - path: path!("/tmp/foo").into(), - content: "one\ntwo\nthree\nfour\nfive\n".to_string(), - }) - })? - .await - .unwrap(); - Ok(()) - } - }) - }); + let thread = cx + .spawn(|mut cx| connection.new_thread(project, Path::new(path!("/tmp")), &mut cx)) + .await + .unwrap(); let request = thread.update(cx, |thread, cx| { thread.send_raw("Extend the count in /tmp/foo", cx) @@ -1486,36 +1492,44 @@ mod tests { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let (thread, fake_server) = fake_acp_thread(project, cx); + let id = acp::ToolCallId("test".into()); - let (end_turn_tx, end_turn_rx) = oneshot::channel::<()>(); - - let tool_call_id = Rc::new(RefCell::new(None)); - let end_turn_rx = Rc::new(RefCell::new(Some(end_turn_rx))); - fake_server.update(cx, |fake_server, _| { - let tool_call_id = tool_call_id.clone(); - fake_server.on_user_message(move |_, server, mut cx| { - let end_turn_rx = end_turn_rx.clone(); - let tool_call_id = tool_call_id.clone(); + let connection = Rc::new(FakeAgentConnection::new().on_user_message({ + let id = id.clone(); + move |_, thread, mut cx| { + let id = id.clone(); async move { - let tool_call_result = server - .update(&mut cx, |server, _| { - server.send_to_zed(acp_old::PushToolCallParams { - label: "Fetch".to_string(), - icon: acp_old::Icon::Globe, - content: None, - locations: vec![], - }) - })? - .await + thread + .update(&mut cx, |thread, cx| { + thread.handle_session_update( + acp::SessionUpdate::ToolCall(acp::ToolCall { + id: id.clone(), + label: "Label".into(), + kind: acp::ToolKind::Fetch, + status: acp::ToolCallStatus::InProgress, + content: vec![], + locations: vec![], + raw_input: None, + }), + cx, + ) + }) + .unwrap() .unwrap(); - *tool_call_id.clone().borrow_mut() = Some(tool_call_result.id); - end_turn_rx.take().unwrap().await.ok(); - Ok(()) } + .boxed_local() + } + })); + + let thread = cx + .spawn(async move |mut cx| { + connection + .new_thread(project, Path::new(path!("/test")), &mut cx) + .await }) - }); + .await + .unwrap(); let request = thread.update(cx, |thread, cx| { thread.send_raw("Fetch https://example.com", cx) @@ -1536,8 +1550,6 @@ mod tests { )); }); - cx.run_until_parked(); - thread.update(cx, |thread, cx| thread.cancel(cx)).await; thread.read_with(cx, |thread, _| { @@ -1550,19 +1562,22 @@ mod tests { )); }); - fake_server - .update(cx, |fake_server, _| { - fake_server.send_to_zed(acp_old::UpdateToolCallParams { - tool_call_id: tool_call_id.borrow().unwrap(), - status: acp_old::ToolCallStatus::Finished, - content: None, - }) + thread + .update(cx, |thread, cx| { + thread.handle_session_update( + acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate { + id, + fields: acp::ToolCallUpdateFields { + status: Some(acp::ToolCallStatus::Completed), + ..Default::default() + }, + }), + cx, + ) }) - .await .unwrap(); - drop(end_turn_tx); - assert!(request.await.unwrap_err().to_string().contains("canceled")); + request.await.unwrap(); thread.read_with(cx, |thread, _| { assert!(matches!( @@ -1585,23 +1600,37 @@ mod tests { fs.insert_tree(path!("/test"), json!({})).await; let project = Project::test(fs, [path!("/test").as_ref()], cx).await; - let connection = Rc::new(StubAgentConnection::new(vec![ - acp::SessionUpdate::ToolCall(acp::ToolCall { - id: acp::ToolCallId("test".into()), - label: "Label".into(), - kind: acp::ToolKind::Edit, - status: acp::ToolCallStatus::Completed, - content: vec![acp::ToolCallContent::Diff { - diff: acp::Diff { - path: "/test/test.txt".into(), - old_text: None, - new_text: "foo".into(), - }, - }], - locations: vec![], - raw_input: None, - }), - ])); + let connection = Rc::new(FakeAgentConnection::new().on_user_message({ + move |_, thread, mut cx| { + async move { + thread + .update(&mut cx, |thread, cx| { + thread.handle_session_update( + acp::SessionUpdate::ToolCall(acp::ToolCall { + id: acp::ToolCallId("test".into()), + label: "Label".into(), + kind: acp::ToolKind::Edit, + status: acp::ToolCallStatus::Completed, + content: vec![acp::ToolCallContent::Diff { + diff: acp::Diff { + path: "/test/test.txt".into(), + old_text: None, + new_text: "foo".into(), + }, + }], + locations: vec![], + raw_input: None, + }), + cx, + ) + }) + .unwrap() + .unwrap(); + Ok(()) + } + .boxed_local() + } + })); let thread = connection .new_thread(project, Path::new(path!("/test")), &mut cx.to_async()) @@ -1642,25 +1671,53 @@ mod tests { } #[derive(Clone, Default)] - struct StubAgentConnection { + struct FakeAgentConnection { + auth_methods: Vec, sessions: Arc>>>, - permission_requests: HashMap>, - updates: Vec, + on_user_message: Option< + Rc< + dyn Fn( + acp::PromptRequest, + WeakEntity, + AsyncApp, + ) -> LocalBoxFuture<'static, Result<()>> + + 'static, + >, + >, } - impl StubAgentConnection { - fn new(updates: Vec) -> Self { + impl FakeAgentConnection { + fn new() -> Self { Self { - updates, - permission_requests: HashMap::default(), + auth_methods: Vec::new(), + on_user_message: None, sessions: Arc::default(), } } + + #[expect(unused)] + fn with_auth_methods(mut self, auth_methods: Vec) -> Self { + self.auth_methods = auth_methods; + self + } + + fn on_user_message( + mut self, + handler: impl Fn( + acp::PromptRequest, + WeakEntity, + AsyncApp, + ) -> LocalBoxFuture<'static, Result<()>> + + 'static, + ) -> Self { + self.on_user_message.replace(Rc::new(handler)); + self + } } - impl AgentConnection for StubAgentConnection { - fn name(&self) -> &'static str { - "StubAgentConnection" + impl AgentConnection for FakeAgentConnection { + fn auth_methods(&self) -> &[acp::AuthMethod] { + &self.auth_methods } fn new_thread( @@ -1678,222 +1735,43 @@ mod tests { .into(), ); let thread = cx - .new(|cx| AcpThread::new(self.clone(), project, session_id.clone(), cx)) + .new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx)) .unwrap(); self.sessions.lock().insert(session_id, thread.downgrade()); Task::ready(Ok(thread)) } - fn authenticate(&self, _cx: &mut App) -> Task> { - unimplemented!() + fn authenticate(&self, method: acp::AuthMethodId, _cx: &mut App) -> Task> { + if self.auth_methods().iter().any(|m| m.id == method) { + Task::ready(Ok(())) + } else { + Task::ready(Err(anyhow!("Invalid Auth Method"))) + } } - fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task> { + fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task> { let sessions = self.sessions.lock(); let thread = sessions.get(¶ms.session_id).unwrap(); - let mut tasks = vec![]; - for update in &self.updates { + if let Some(handler) = &self.on_user_message { + let handler = handler.clone(); let thread = thread.clone(); - let update = update.clone(); - let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) = &update - && let Some(options) = self.permission_requests.get(&tool_call.id) - { - Some((tool_call.clone(), options.clone())) - } else { - None - }; - let task = cx.spawn(async move |cx| { - if let Some((tool_call, options)) = permission_request { - let permission = thread.update(cx, |thread, cx| { - thread.request_tool_call_permission( - tool_call.clone(), - options.clone(), - cx, - ) - })?; - permission.await?; - } - thread.update(cx, |thread, cx| { - thread.handle_session_update(update.clone(), cx).unwrap(); - })?; - anyhow::Ok(()) - }); - tasks.push(task); - } - cx.spawn(async move |_| { - try_join_all(tasks).await?; - Ok(()) - }) - } - - fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) { - unimplemented!() - } - } - - pub fn fake_acp_thread( - project: Entity, - cx: &mut TestAppContext, - ) -> (Entity, Entity) { - let (stdin_tx, stdin_rx) = async_pipe::pipe(); - let (stdout_tx, stdout_rx) = async_pipe::pipe(); - - let thread = cx.new(|cx| { - let foreground_executor = cx.foreground_executor().clone(); - let thread_rc = Rc::new(RefCell::new(cx.entity().downgrade())); - - let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent( - OldAcpClientDelegate::new(thread_rc.clone(), cx.to_async()), - stdin_tx, - stdout_rx, - move |fut| { - foreground_executor.spawn(fut).detach(); - }, - ); - - let io_task = cx.background_spawn({ - async move { - io_fut.await.log_err(); - Ok(()) - } - }); - let connection = OldAcpAgentConnection { - name: "test", - connection, - child_status: io_task, - current_thread: thread_rc, - }; - - AcpThread::new( - Rc::new(connection), - project, - acp::SessionId("test".into()), - cx, - ) - }); - let agent = cx.update(|cx| cx.new(|cx| FakeAcpServer::new(stdin_rx, stdout_tx, cx))); - (thread, agent) - } - - pub struct FakeAcpServer { - connection: acp_old::ClientConnection, - - _io_task: Task<()>, - on_user_message: Option< - Rc< - dyn Fn( - acp_old::SendUserMessageParams, - Entity, - AsyncApp, - ) -> LocalBoxFuture<'static, Result<(), acp_old::Error>>, - >, - >, - } - - #[derive(Clone)] - struct FakeAgent { - server: Entity, - cx: AsyncApp, - cancel_tx: Rc>>>, - } - - impl acp_old::Agent for FakeAgent { - async fn initialize( - &self, - params: acp_old::InitializeParams, - ) -> Result { - Ok(acp_old::InitializeResponse { - protocol_version: params.protocol_version, - is_authenticated: true, - }) - } - - async fn authenticate(&self) -> Result<(), acp_old::Error> { - Ok(()) - } - - async fn cancel_send_message(&self) -> Result<(), acp_old::Error> { - if let Some(cancel_tx) = self.cancel_tx.take() { - cancel_tx.send(()).log_err(); - } - Ok(()) - } - - async fn send_user_message( - &self, - request: acp_old::SendUserMessageParams, - ) -> Result<(), acp_old::Error> { - let (cancel_tx, cancel_rx) = oneshot::channel(); - self.cancel_tx.replace(Some(cancel_tx)); - - let mut cx = self.cx.clone(); - let handler = self - .server - .update(&mut cx, |server, _| server.on_user_message.clone()) - .ok() - .flatten(); - if let Some(handler) = handler { - select! { - _ = cancel_rx.fuse() => Err(anyhow::anyhow!("Message sending canceled").into()), - _ = handler(request, self.server.clone(), self.cx.clone()).fuse() => Ok(()), - } + cx.spawn(async move |cx| handler(params, thread, cx.clone()).await) } else { - Err(anyhow::anyhow!("No handler for on_user_message").into()) - } - } - } - - impl FakeAcpServer { - fn new(stdin: PipeReader, stdout: PipeWriter, cx: &Context) -> Self { - let agent = FakeAgent { - server: cx.entity(), - cx: cx.to_async(), - cancel_tx: Default::default(), - }; - let foreground_executor = cx.foreground_executor().clone(); - - let (connection, io_fut) = acp_old::ClientConnection::connect_to_client( - agent.clone(), - stdout, - stdin, - move |fut| { - foreground_executor.spawn(fut).detach(); - }, - ); - FakeAcpServer { - connection: connection, - on_user_message: None, - _io_task: cx.background_spawn(async move { - io_fut.await.log_err(); - }), + Task::ready(Ok(())) } } - fn on_user_message( - &mut self, - handler: impl for<'a> Fn( - acp_old::SendUserMessageParams, - Entity, - AsyncApp, - ) -> F - + 'static, - ) where - F: Future> + 'static, - { - self.on_user_message - .replace(Rc::new(move |request, server, cx| { - handler(request, server, cx).boxed_local() - })); - } + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { + let sessions = self.sessions.lock(); + let thread = sessions.get(&session_id).unwrap().clone(); - fn send_to_zed( - &self, - message: T, - ) -> BoxedLocal> { - self.connection - .request(message) - .map(|f| f.map_err(|err| anyhow!(err))) - .boxed_local() + cx.spawn(async move |cx| { + thread + .update(cx, |thread, cx| thread.cancel(cx)) + .unwrap() + .await + }) + .detach(); } } } diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 5b25b71863..929500a67b 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -1,6 +1,6 @@ -use std::{path::Path, rc::Rc}; +use std::{error::Error, fmt, path::Path, rc::Rc}; -use agent_client_protocol as acp; +use agent_client_protocol::{self as acp}; use anyhow::Result; use gpui::{AsyncApp, Entity, Task}; use project::Project; @@ -9,8 +9,6 @@ use ui::App; use crate::AcpThread; pub trait AgentConnection { - fn name(&self) -> &'static str; - fn new_thread( self: Rc, project: Entity, @@ -18,9 +16,21 @@ pub trait AgentConnection { cx: &mut AsyncApp, ) -> Task>>; - fn authenticate(&self, cx: &mut App) -> Task>; + fn auth_methods(&self) -> &[acp::AuthMethod]; - fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task>; + fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task>; + + fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task>; fn cancel(&self, session_id: &acp::SessionId, cx: &mut App); } + +#[derive(Debug)] +pub struct AuthRequired; + +impl Error for AuthRequired {} +impl fmt::Display for AuthRequired { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "AuthRequired") + } +} diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index dcffb05bc0..81c97c8aa6 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -25,6 +25,7 @@ collections.workspace = true context_server.workspace = true futures.workspace = true gpui.workspace = true +indoc.workspace = true itertools.workspace = true log.workspace = true paths.workspace = true @@ -37,11 +38,11 @@ settings.workspace = true smol.workspace = true strum.workspace = true tempfile.workspace = true +thiserror.workspace = true ui.workspace = true util.workspace = true uuid.workspace = true watch.workspace = true -indoc.workspace = true which.workspace = true workspace-hack.workspace = true diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs new file mode 100644 index 0000000000..00e3e3df50 --- /dev/null +++ b/crates/agent_servers/src/acp.rs @@ -0,0 +1,34 @@ +use std::{path::Path, rc::Rc}; + +use crate::AgentServerCommand; +use acp_thread::AgentConnection; +use anyhow::Result; +use gpui::AsyncApp; +use thiserror::Error; + +mod v0; +mod v1; + +#[derive(Debug, Error)] +#[error("Unsupported version")] +pub struct UnsupportedVersion; + +pub async fn connect( + server_name: &'static str, + command: AgentServerCommand, + root_dir: &Path, + cx: &mut AsyncApp, +) -> Result> { + let conn = v1::AcpConnection::stdio(server_name, command.clone(), &root_dir, cx).await; + + match conn { + Ok(conn) => Ok(Rc::new(conn) as _), + Err(err) if err.is::() => { + // Consider re-using initialize response and subprocess when adding another version here + let conn: Rc = + Rc::new(v0::AcpConnection::stdio(server_name, command, &root_dir, cx).await?); + Ok(conn) + } + Err(err) => Err(err), + } +} diff --git a/crates/acp_thread/src/old_acp_support.rs b/crates/agent_servers/src/acp/v0.rs similarity index 84% rename from crates/acp_thread/src/old_acp_support.rs rename to crates/agent_servers/src/acp/v0.rs index 571023239f..6839ff2462 100644 --- a/crates/acp_thread/src/old_acp_support.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -1,18 +1,19 @@ // Translates old acp agents into the new schema use agent_client_protocol as acp; use agentic_coding_protocol::{self as acp_old, AgentRequest as _}; -use anyhow::{Context as _, Result}; +use anyhow::{Context as _, Result, anyhow}; use futures::channel::oneshot; use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity}; use project::Project; -use std::{cell::RefCell, error::Error, fmt, path::Path, rc::Rc}; +use std::{cell::RefCell, path::Path, rc::Rc}; use ui::App; use util::ResultExt as _; -use crate::{AcpThread, AgentConnection}; +use crate::AgentServerCommand; +use acp_thread::{AcpThread, AgentConnection, AuthRequired}; #[derive(Clone)] -pub struct OldAcpClientDelegate { +struct OldAcpClientDelegate { thread: Rc>>, cx: AsyncApp, next_tool_call_id: Rc>, @@ -20,7 +21,7 @@ pub struct OldAcpClientDelegate { } impl OldAcpClientDelegate { - pub fn new(thread: Rc>>, cx: AsyncApp) -> Self { + fn new(thread: Rc>>, cx: AsyncApp) -> Self { Self { thread, cx, @@ -351,28 +352,71 @@ fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatu } } -#[derive(Debug)] -pub struct Unauthenticated; - -impl Error for Unauthenticated {} -impl fmt::Display for Unauthenticated { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Unauthenticated") - } -} - -pub struct OldAcpAgentConnection { +pub struct AcpConnection { pub name: &'static str, pub connection: acp_old::AgentConnection, - pub child_status: Task>, + pub _child_status: Task>, pub current_thread: Rc>>, } -impl AgentConnection for OldAcpAgentConnection { - fn name(&self) -> &'static str { - self.name - } +impl AcpConnection { + pub fn stdio( + name: &'static str, + command: AgentServerCommand, + root_dir: &Path, + cx: &mut AsyncApp, + ) -> Task> { + let root_dir = root_dir.to_path_buf(); + cx.spawn(async move |cx| { + let mut child = util::command::new_smol_command(&command.path) + .args(command.args.iter()) + .current_dir(root_dir) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .kill_on_drop(true) + .spawn()?; + + let stdin = child.stdin.take().unwrap(); + let stdout = child.stdout.take().unwrap(); + + let foreground_executor = cx.foreground_executor().clone(); + + let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid())); + + let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent( + OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()), + stdin, + stdout, + move |fut| foreground_executor.spawn(fut).detach(), + ); + + let io_task = cx.background_spawn(async move { + io_fut.await.log_err(); + }); + + let child_status = cx.background_spawn(async move { + let result = match child.status().await { + Err(e) => Err(anyhow!(e)), + Ok(result) if result.success() => Ok(()), + Ok(result) => Err(anyhow!(result)), + }; + drop(io_task); + result + }); + + Ok(Self { + name, + connection, + _child_status: child_status, + current_thread: thread_rc, + }) + }) + } +} + +impl AgentConnection for AcpConnection { fn new_thread( self: Rc, project: Entity, @@ -391,13 +435,13 @@ impl AgentConnection for OldAcpAgentConnection { let result = acp_old::InitializeParams::response_from_any(result)?; if !result.is_authenticated { - anyhow::bail!(Unauthenticated) + anyhow::bail!(AuthRequired) } cx.update(|cx| { let thread = cx.new(|cx| { let session_id = acp::SessionId("acp-old-no-id".into()); - AcpThread::new(self.clone(), project, session_id, cx) + AcpThread::new(self.name, self.clone(), project, session_id, cx) }); current_thread.replace(thread.downgrade()); thread @@ -405,7 +449,11 @@ impl AgentConnection for OldAcpAgentConnection { }) } - fn authenticate(&self, cx: &mut App) -> Task> { + fn auth_methods(&self) -> &[acp::AuthMethod] { + &[] + } + + fn authenticate(&self, _method_id: acp::AuthMethodId, cx: &mut App) -> Task> { let task = self .connection .request_any(acp_old::AuthenticateParams.into_any()); @@ -415,7 +463,7 @@ impl AgentConnection for OldAcpAgentConnection { }) } - fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task> { + fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task> { let chunks = params .prompt .into_iter() diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs new file mode 100644 index 0000000000..9e2193ce18 --- /dev/null +++ b/crates/agent_servers/src/acp/v1.rs @@ -0,0 +1,254 @@ +use agent_client_protocol::{self as acp, Agent as _}; +use collections::HashMap; +use futures::channel::oneshot; +use project::Project; +use std::cell::RefCell; +use std::path::Path; +use std::rc::Rc; + +use anyhow::{Context as _, Result}; +use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity}; + +use crate::{AgentServerCommand, acp::UnsupportedVersion}; +use acp_thread::{AcpThread, AgentConnection, AuthRequired}; + +pub struct AcpConnection { + server_name: &'static str, + connection: Rc, + sessions: Rc>>, + auth_methods: Vec, + _io_task: Task>, + _child: smol::process::Child, +} + +pub struct AcpSession { + thread: WeakEntity, +} + +const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1; + +impl AcpConnection { + pub async fn stdio( + server_name: &'static str, + command: AgentServerCommand, + root_dir: &Path, + cx: &mut AsyncApp, + ) -> Result { + let mut child = util::command::new_smol_command(&command.path) + .args(command.args.iter().map(|arg| arg.as_str())) + .envs(command.env.iter().flatten()) + .current_dir(root_dir) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .kill_on_drop(true) + .spawn()?; + + let stdout = child.stdout.take().expect("Failed to take stdout"); + let stdin = child.stdin.take().expect("Failed to take stdin"); + + let sessions = Rc::new(RefCell::new(HashMap::default())); + + let client = ClientDelegate { + sessions: sessions.clone(), + cx: cx.clone(), + }; + let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, { + let foreground_executor = cx.foreground_executor().clone(); + move |fut| { + foreground_executor.spawn(fut).detach(); + } + }); + + let io_task = cx.background_spawn(io_task); + + let response = connection + .initialize(acp::InitializeRequest { + protocol_version: acp::VERSION, + client_capabilities: acp::ClientCapabilities { + fs: acp::FileSystemCapability { + read_text_file: true, + write_text_file: true, + }, + }, + }) + .await?; + + if response.protocol_version < MINIMUM_SUPPORTED_VERSION { + return Err(UnsupportedVersion.into()); + } + + Ok(Self { + auth_methods: response.auth_methods, + connection: connection.into(), + server_name, + sessions, + _child: child, + _io_task: io_task, + }) + } +} + +impl AgentConnection for AcpConnection { + fn new_thread( + self: Rc, + project: Entity, + cwd: &Path, + cx: &mut AsyncApp, + ) -> Task>> { + let conn = self.connection.clone(); + let sessions = self.sessions.clone(); + let cwd = cwd.to_path_buf(); + cx.spawn(async move |cx| { + let response = conn + .new_session(acp::NewSessionRequest { + mcp_servers: vec![], + cwd, + }) + .await?; + + let Some(session_id) = response.session_id else { + anyhow::bail!(AuthRequired); + }; + + let thread = cx.new(|cx| { + AcpThread::new( + self.server_name, + self.clone(), + project, + session_id.clone(), + cx, + ) + })?; + + let session = AcpSession { + thread: thread.downgrade(), + }; + sessions.borrow_mut().insert(session_id, session); + + Ok(thread) + }) + } + + fn auth_methods(&self) -> &[acp::AuthMethod] { + &self.auth_methods + } + + fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task> { + let conn = self.connection.clone(); + cx.foreground_executor().spawn(async move { + let result = conn + .authenticate(acp::AuthenticateRequest { + method_id: method_id.clone(), + }) + .await?; + + Ok(result) + }) + } + + fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task> { + let conn = self.connection.clone(); + cx.foreground_executor() + .spawn(async move { Ok(conn.prompt(params).await?) }) + } + + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { + let conn = self.connection.clone(); + let params = acp::CancelledNotification { + session_id: session_id.clone(), + }; + cx.foreground_executor() + .spawn(async move { conn.cancelled(params).await }) + .detach(); + } +} + +struct ClientDelegate { + sessions: Rc>>, + cx: AsyncApp, +} + +impl acp::Client for ClientDelegate { + async fn request_permission( + &self, + arguments: acp::RequestPermissionRequest, + ) -> Result { + let cx = &mut self.cx.clone(); + let rx = self + .sessions + .borrow() + .get(&arguments.session_id) + .context("Failed to get session")? + .thread + .update(cx, |thread, cx| { + thread.request_tool_call_permission(arguments.tool_call, arguments.options, cx) + })?; + + let result = rx.await; + + let outcome = match result { + Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option }, + Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled, + }; + + Ok(acp::RequestPermissionResponse { outcome }) + } + + async fn write_text_file( + &self, + arguments: acp::WriteTextFileRequest, + ) -> Result<(), acp::Error> { + let cx = &mut self.cx.clone(); + let task = self + .sessions + .borrow() + .get(&arguments.session_id) + .context("Failed to get session")? + .thread + .update(cx, |thread, cx| { + thread.write_text_file(arguments.path, arguments.content, cx) + })?; + + task.await?; + + Ok(()) + } + + async fn read_text_file( + &self, + arguments: acp::ReadTextFileRequest, + ) -> Result { + let cx = &mut self.cx.clone(); + let task = self + .sessions + .borrow() + .get(&arguments.session_id) + .context("Failed to get session")? + .thread + .update(cx, |thread, cx| { + thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx) + })?; + + let content = task.await?; + + Ok(acp::ReadTextFileResponse { content }) + } + + async fn session_notification( + &self, + notification: acp::SessionNotification, + ) -> Result<(), acp::Error> { + let cx = &mut self.cx.clone(); + let sessions = self.sessions.borrow(); + let session = sessions + .get(¬ification.session_id) + .context("Failed to get session")?; + + session.thread.update(cx, |thread, cx| { + thread.handle_session_update(notification.update, cx) + })??; + + Ok(()) + } +} diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 212bb74d8a..ec69290206 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -1,14 +1,12 @@ +mod acp; mod claude; -mod codex; mod gemini; -mod mcp_server; mod settings; #[cfg(test)] mod e2e_tests; pub use claude::*; -pub use codex::*; pub use gemini::*; pub use settings::*; @@ -38,7 +36,6 @@ pub trait AgentServer: Send { fn connect( &self, - // these will go away when old_acp is fully removed root_dir: &Path, project: &Entity, cx: &mut App, diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 6565786204..9040b83085 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -70,10 +70,6 @@ struct ClaudeAgentConnection { } impl AgentConnection for ClaudeAgentConnection { - fn name(&self) -> &'static str { - ClaudeCode.name() - } - fn new_thread( self: Rc, project: Entity, @@ -168,8 +164,9 @@ impl AgentConnection for ClaudeAgentConnection { } }); - let thread = - cx.new(|cx| AcpThread::new(self.clone(), project, session_id.clone(), cx))?; + let thread = cx.new(|cx| { + AcpThread::new("Claude Code", self.clone(), project, session_id.clone(), cx) + })?; thread_tx.send(thread.downgrade())?; @@ -186,11 +183,15 @@ impl AgentConnection for ClaudeAgentConnection { }) } - fn authenticate(&self, _cx: &mut App) -> Task> { + fn auth_methods(&self) -> &[acp::AuthMethod] { + &[] + } + + fn authenticate(&self, _: acp::AuthMethodId, _cx: &mut App) -> Task> { Task::ready(Err(anyhow!("Authentication not supported"))) } - fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task> { + fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task> { let sessions = self.sessions.borrow(); let Some(session) = sessions.get(¶ms.session_id) else { return Task::ready(Err(anyhow!( diff --git a/crates/agent_servers/src/codex.rs b/crates/agent_servers/src/codex.rs deleted file mode 100644 index 712c333221..0000000000 --- a/crates/agent_servers/src/codex.rs +++ /dev/null @@ -1,319 +0,0 @@ -use agent_client_protocol as acp; -use anyhow::anyhow; -use collections::HashMap; -use context_server::listener::McpServerTool; -use context_server::types::requests; -use context_server::{ContextServer, ContextServerCommand, ContextServerId}; -use futures::channel::{mpsc, oneshot}; -use project::Project; -use settings::SettingsStore; -use smol::stream::StreamExt as _; -use std::cell::RefCell; -use std::rc::Rc; -use std::{path::Path, sync::Arc}; -use util::ResultExt; - -use anyhow::{Context, Result}; -use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity}; - -use crate::mcp_server::ZedMcpServer; -use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings, mcp_server}; -use acp_thread::{AcpThread, AgentConnection}; - -#[derive(Clone)] -pub struct Codex; - -impl AgentServer for Codex { - fn name(&self) -> &'static str { - "Codex" - } - - fn empty_state_headline(&self) -> &'static str { - "Welcome to Codex" - } - - fn empty_state_message(&self) -> &'static str { - "What can I help with?" - } - - fn logo(&self) -> ui::IconName { - ui::IconName::AiOpenAi - } - - fn connect( - &self, - _root_dir: &Path, - project: &Entity, - cx: &mut App, - ) -> Task>> { - let project = project.clone(); - let working_directory = project.read(cx).active_project_directory(cx); - cx.spawn(async move |cx| { - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings.get::(None).codex.clone() - })?; - - let Some(command) = - AgentServerCommand::resolve("codex", &["mcp"], settings, &project, cx).await - else { - anyhow::bail!("Failed to find codex binary"); - }; - - let client: Arc = ContextServer::stdio( - ContextServerId("codex-mcp-server".into()), - ContextServerCommand { - path: command.path, - args: command.args, - env: command.env, - }, - working_directory, - ) - .into(); - ContextServer::start(client.clone(), cx).await?; - - let (notification_tx, mut notification_rx) = mpsc::unbounded(); - client - .client() - .context("Failed to subscribe")? - .on_notification(acp::SESSION_UPDATE_METHOD_NAME, { - move |notification, _cx| { - let notification_tx = notification_tx.clone(); - log::trace!( - "ACP Notification: {}", - serde_json::to_string_pretty(¬ification).unwrap() - ); - - if let Some(notification) = - serde_json::from_value::(notification) - .log_err() - { - notification_tx.unbounded_send(notification).ok(); - } - } - }); - - let sessions = Rc::new(RefCell::new(HashMap::default())); - - let notification_handler_task = cx.spawn({ - let sessions = sessions.clone(); - async move |cx| { - while let Some(notification) = notification_rx.next().await { - CodexConnection::handle_session_notification( - notification, - sessions.clone(), - cx, - ) - } - } - }); - - let connection = CodexConnection { - client, - sessions, - _notification_handler_task: notification_handler_task, - }; - Ok(Rc::new(connection) as _) - }) - } -} - -struct CodexConnection { - client: Arc, - sessions: Rc>>, - _notification_handler_task: Task<()>, -} - -struct CodexSession { - thread: WeakEntity, - cancel_tx: Option>, - _mcp_server: ZedMcpServer, -} - -impl AgentConnection for CodexConnection { - fn name(&self) -> &'static str { - "Codex" - } - - fn new_thread( - self: Rc, - project: Entity, - cwd: &Path, - cx: &mut AsyncApp, - ) -> Task>> { - let client = self.client.client(); - let sessions = self.sessions.clone(); - let cwd = cwd.to_path_buf(); - cx.spawn(async move |cx| { - let client = client.context("MCP server is not initialized yet")?; - let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid()); - - let mcp_server = ZedMcpServer::new(thread_rx, cx).await?; - - let response = client - .request::(context_server::types::CallToolParams { - name: acp::NEW_SESSION_TOOL_NAME.into(), - arguments: Some(serde_json::to_value(acp::NewSessionArguments { - mcp_servers: [( - mcp_server::SERVER_NAME.to_string(), - mcp_server.server_config()?, - )] - .into(), - client_tools: acp::ClientTools { - request_permission: Some(acp::McpToolId { - mcp_server: mcp_server::SERVER_NAME.into(), - tool_name: mcp_server::RequestPermissionTool::NAME.into(), - }), - read_text_file: Some(acp::McpToolId { - mcp_server: mcp_server::SERVER_NAME.into(), - tool_name: mcp_server::ReadTextFileTool::NAME.into(), - }), - write_text_file: Some(acp::McpToolId { - mcp_server: mcp_server::SERVER_NAME.into(), - tool_name: mcp_server::WriteTextFileTool::NAME.into(), - }), - }, - cwd, - })?), - meta: None, - }) - .await?; - - if response.is_error.unwrap_or_default() { - return Err(anyhow!(response.text_contents())); - } - - let result = serde_json::from_value::( - response.structured_content.context("Empty response")?, - )?; - - let thread = - cx.new(|cx| AcpThread::new(self.clone(), project, result.session_id.clone(), cx))?; - - thread_tx.send(thread.downgrade())?; - - let session = CodexSession { - thread: thread.downgrade(), - cancel_tx: None, - _mcp_server: mcp_server, - }; - sessions.borrow_mut().insert(result.session_id, session); - - Ok(thread) - }) - } - - fn authenticate(&self, _cx: &mut App) -> Task> { - Task::ready(Err(anyhow!("Authentication not supported"))) - } - - fn prompt( - &self, - params: agent_client_protocol::PromptArguments, - cx: &mut App, - ) -> Task> { - let client = self.client.client(); - let sessions = self.sessions.clone(); - - cx.foreground_executor().spawn(async move { - let client = client.context("MCP server is not initialized yet")?; - - let (new_cancel_tx, cancel_rx) = oneshot::channel(); - { - let mut sessions = sessions.borrow_mut(); - let session = sessions - .get_mut(¶ms.session_id) - .context("Session not found")?; - session.cancel_tx.replace(new_cancel_tx); - } - - let result = client - .request_with::( - context_server::types::CallToolParams { - name: acp::PROMPT_TOOL_NAME.into(), - arguments: Some(serde_json::to_value(params)?), - meta: None, - }, - Some(cancel_rx), - None, - ) - .await; - - if let Err(err) = &result - && err.is::() - { - return Ok(()); - } - - let response = result?; - - if response.is_error.unwrap_or_default() { - return Err(anyhow!(response.text_contents())); - } - - Ok(()) - }) - } - - fn cancel(&self, session_id: &agent_client_protocol::SessionId, _cx: &mut App) { - let mut sessions = self.sessions.borrow_mut(); - - if let Some(cancel_tx) = sessions - .get_mut(session_id) - .and_then(|session| session.cancel_tx.take()) - { - cancel_tx.send(()).ok(); - } - } -} - -impl CodexConnection { - pub fn handle_session_notification( - notification: acp::SessionNotification, - threads: Rc>>, - cx: &mut AsyncApp, - ) { - let threads = threads.borrow(); - let Some(thread) = threads - .get(¬ification.session_id) - .and_then(|session| session.thread.upgrade()) - else { - log::error!( - "Thread not found for session ID: {}", - notification.session_id - ); - return; - }; - - thread - .update(cx, |thread, cx| { - thread.handle_session_update(notification.update, cx) - }) - .log_err(); - } -} - -impl Drop for CodexConnection { - fn drop(&mut self) { - self.client.stop().log_err(); - } -} - -#[cfg(test)] -pub(crate) mod tests { - use super::*; - use crate::AgentServerCommand; - use std::path::Path; - - crate::common_e2e_tests!(Codex, allow_option_id = "approve"); - - pub fn local_command() -> AgentServerCommand { - let cli_path = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../../codex/codex-rs/target/debug/codex"); - - AgentServerCommand { - path: cli_path, - args: vec![], - env: None, - } - } -} diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index e9c72eabc9..16bf1e6b47 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -375,9 +375,6 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc { gemini: Some(AgentServerSettings { command: crate::gemini::tests::local_command(), }), - codex: Some(AgentServerSettings { - command: crate::codex::tests::local_command(), - }), }, cx, ); diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index a97ff3f462..1119a8b4ee 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -1,14 +1,10 @@ -use anyhow::anyhow; -use std::cell::RefCell; use std::path::Path; use std::rc::Rc; -use util::ResultExt as _; -use crate::{AgentServer, AgentServerCommand, AgentServerVersion}; -use acp_thread::{AgentConnection, LoadError, OldAcpAgentConnection, OldAcpClientDelegate}; -use agentic_coding_protocol as acp_old; -use anyhow::{Context as _, Result}; -use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity}; +use crate::{AgentServer, AgentServerCommand}; +use acp_thread::AgentConnection; +use anyhow::Result; +use gpui::{Entity, Task}; use project::Project; use settings::SettingsStore; use ui::App; @@ -43,146 +39,25 @@ impl AgentServer for Gemini { project: &Entity, cx: &mut App, ) -> Task>> { - let root_dir = root_dir.to_path_buf(); let project = project.clone(); - let this = self.clone(); - let name = self.name(); - + let root_dir = root_dir.to_path_buf(); + let server_name = self.name(); cx.spawn(async move |cx| { - let command = this.command(&project, cx).await?; + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings.get::(None).gemini.clone() + })?; - let mut child = util::command::new_smol_command(&command.path) - .args(command.args.iter()) - .current_dir(root_dir) - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::inherit()) - .kill_on_drop(true) - .spawn()?; + let Some(command) = + AgentServerCommand::resolve("gemini", &[ACP_ARG], settings, &project, cx).await + else { + anyhow::bail!("Failed to find gemini binary"); + }; - let stdin = child.stdin.take().unwrap(); - let stdout = child.stdout.take().unwrap(); - - let foreground_executor = cx.foreground_executor().clone(); - - let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid())); - - let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent( - OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()), - stdin, - stdout, - move |fut| foreground_executor.spawn(fut).detach(), - ); - - let io_task = cx.background_spawn(async move { - io_fut.await.log_err(); - }); - - let child_status = cx.background_spawn(async move { - let result = match child.status().await { - Err(e) => Err(anyhow!(e)), - Ok(result) if result.success() => Ok(()), - Ok(result) => { - if let Some(AgentServerVersion::Unsupported { - error_message, - upgrade_message, - upgrade_command, - }) = this.version(&command).await.log_err() - { - Err(anyhow!(LoadError::Unsupported { - error_message, - upgrade_message, - upgrade_command - })) - } else { - Err(anyhow!(LoadError::Exited(result.code().unwrap_or(-127)))) - } - } - }; - drop(io_task); - result - }); - - let connection: Rc = Rc::new(OldAcpAgentConnection { - name, - connection, - child_status, - current_thread: thread_rc, - }); - - Ok(connection) + crate::acp::connect(server_name, command, &root_dir, cx).await }) } } -impl Gemini { - async fn command( - &self, - project: &Entity, - cx: &mut AsyncApp, - ) -> Result { - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings.get::(None).gemini.clone() - })?; - - if let Some(command) = - AgentServerCommand::resolve("gemini", &[ACP_ARG], settings, &project, cx).await - { - return Ok(command); - }; - - let (fs, node_runtime) = project.update(cx, |project, _| { - (project.fs().clone(), project.node_runtime().cloned()) - })?; - let node_runtime = node_runtime.context("gemini not found on path")?; - - let directory = ::paths::agent_servers_dir().join("gemini"); - fs.create_dir(&directory).await?; - node_runtime - .npm_install_packages(&directory, &[("@google/gemini-cli", "latest")]) - .await?; - let path = directory.join("node_modules/.bin/gemini"); - - Ok(AgentServerCommand { - path, - args: vec![ACP_ARG.into()], - env: None, - }) - } - - async fn version(&self, command: &AgentServerCommand) -> Result { - let version_fut = util::command::new_smol_command(&command.path) - .args(command.args.iter()) - .arg("--version") - .kill_on_drop(true) - .output(); - - let help_fut = util::command::new_smol_command(&command.path) - .args(command.args.iter()) - .arg("--help") - .kill_on_drop(true) - .output(); - - let (version_output, help_output) = futures::future::join(version_fut, help_fut).await; - - let current_version = String::from_utf8(version_output?.stdout)?; - let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG); - - if supported { - Ok(AgentServerVersion::Supported) - } else { - Ok(AgentServerVersion::Unsupported { - error_message: format!( - "Your installed version of Gemini {} doesn't support the Agentic Coding Protocol (ACP).", - current_version - ).into(), - upgrade_message: "Upgrade Gemini to Latest".into(), - upgrade_command: "npm install -g @google/gemini-cli@latest".into(), - }) - } - } -} - #[cfg(test)] pub(crate) mod tests { use super::*; @@ -199,7 +74,7 @@ pub(crate) mod tests { AgentServerCommand { path: "node".into(), - args: vec![cli_path, ACP_ARG.into()], + args: vec![cli_path], env: None, } } diff --git a/crates/agent_servers/src/mcp_server.rs b/crates/agent_servers/src/mcp_server.rs deleted file mode 100644 index 055b89dfe2..0000000000 --- a/crates/agent_servers/src/mcp_server.rs +++ /dev/null @@ -1,207 +0,0 @@ -use acp_thread::AcpThread; -use agent_client_protocol as acp; -use anyhow::Result; -use context_server::listener::{McpServerTool, ToolResponse}; -use context_server::types::{ - Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities, - ToolsCapabilities, requests, -}; -use futures::channel::oneshot; -use gpui::{App, AsyncApp, Task, WeakEntity}; -use indoc::indoc; - -pub struct ZedMcpServer { - server: context_server::listener::McpServer, -} - -pub const SERVER_NAME: &str = "zed"; - -impl ZedMcpServer { - pub async fn new( - thread_rx: watch::Receiver>, - cx: &AsyncApp, - ) -> Result { - let mut mcp_server = context_server::listener::McpServer::new(cx).await?; - mcp_server.handle_request::(Self::handle_initialize); - - mcp_server.add_tool(RequestPermissionTool { - thread_rx: thread_rx.clone(), - }); - mcp_server.add_tool(ReadTextFileTool { - thread_rx: thread_rx.clone(), - }); - mcp_server.add_tool(WriteTextFileTool { - thread_rx: thread_rx.clone(), - }); - - Ok(Self { server: mcp_server }) - } - - pub fn server_config(&self) -> Result { - #[cfg(not(test))] - let zed_path = anyhow::Context::context( - std::env::current_exe(), - "finding current executable path for use in mcp_server", - )?; - - #[cfg(test)] - let zed_path = crate::e2e_tests::get_zed_path(); - - Ok(acp::McpServerConfig { - command: zed_path, - args: vec![ - "--nc".into(), - self.server.socket_path().display().to_string(), - ], - env: None, - }) - } - - fn handle_initialize(_: InitializeParams, cx: &App) -> Task> { - cx.foreground_executor().spawn(async move { - Ok(InitializeResponse { - protocol_version: ProtocolVersion("2025-06-18".into()), - capabilities: ServerCapabilities { - experimental: None, - logging: None, - completions: None, - prompts: None, - resources: None, - tools: Some(ToolsCapabilities { - list_changed: Some(false), - }), - }, - server_info: Implementation { - name: SERVER_NAME.into(), - version: "0.1.0".into(), - }, - meta: None, - }) - }) - } -} - -// Tools - -#[derive(Clone)] -pub struct RequestPermissionTool { - thread_rx: watch::Receiver>, -} - -impl McpServerTool for RequestPermissionTool { - type Input = acp::RequestPermissionArguments; - type Output = acp::RequestPermissionOutput; - - const NAME: &'static str = "Confirmation"; - - fn description(&self) -> &'static str { - indoc! {" - Request permission for tool calls. - - This tool is meant to be called programmatically by the agent loop, not the LLM. - "} - } - - async fn run( - &self, - input: Self::Input, - cx: &mut AsyncApp, - ) -> Result> { - let mut thread_rx = self.thread_rx.clone(); - let Some(thread) = thread_rx.recv().await?.upgrade() else { - anyhow::bail!("Thread closed"); - }; - - let result = thread - .update(cx, |thread, cx| { - thread.request_tool_call_permission(input.tool_call, input.options, cx) - })? - .await; - - let outcome = match result { - Ok(option_id) => acp::RequestPermissionOutcome::Selected { option_id }, - Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Canceled, - }; - - Ok(ToolResponse { - content: vec![], - structured_content: acp::RequestPermissionOutput { outcome }, - }) - } -} - -#[derive(Clone)] -pub struct ReadTextFileTool { - thread_rx: watch::Receiver>, -} - -impl McpServerTool for ReadTextFileTool { - type Input = acp::ReadTextFileArguments; - type Output = acp::ReadTextFileOutput; - - const NAME: &'static str = "Read"; - - fn description(&self) -> &'static str { - "Reads the content of the given file in the project including unsaved changes." - } - - async fn run( - &self, - input: Self::Input, - cx: &mut AsyncApp, - ) -> Result> { - let mut thread_rx = self.thread_rx.clone(); - let Some(thread) = thread_rx.recv().await?.upgrade() else { - anyhow::bail!("Thread closed"); - }; - - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(input.path, input.line, input.limit, false, cx) - })? - .await?; - - Ok(ToolResponse { - content: vec![], - structured_content: acp::ReadTextFileOutput { content }, - }) - } -} - -#[derive(Clone)] -pub struct WriteTextFileTool { - thread_rx: watch::Receiver>, -} - -impl McpServerTool for WriteTextFileTool { - type Input = acp::WriteTextFileArguments; - type Output = (); - - const NAME: &'static str = "Write"; - - fn description(&self) -> &'static str { - "Write to a file replacing its contents" - } - - async fn run( - &self, - input: Self::Input, - cx: &mut AsyncApp, - ) -> Result> { - let mut thread_rx = self.thread_rx.clone(); - let Some(thread) = thread_rx.recv().await?.upgrade() else { - anyhow::bail!("Thread closed"); - }; - - thread - .update(cx, |thread, cx| { - thread.write_text_file(input.path, input.content, cx) - })? - .await?; - - Ok(ToolResponse { - content: vec![], - structured_content: (), - }) - } -} diff --git a/crates/agent_servers/src/settings.rs b/crates/agent_servers/src/settings.rs index aeb34a5e61..645674b5f1 100644 --- a/crates/agent_servers/src/settings.rs +++ b/crates/agent_servers/src/settings.rs @@ -13,7 +13,6 @@ pub fn init(cx: &mut App) { pub struct AllAgentServersSettings { pub gemini: Option, pub claude: Option, - pub codex: Option, } #[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)] @@ -30,21 +29,13 @@ impl settings::Settings for AllAgentServersSettings { fn load(sources: SettingsSources, _: &mut App) -> Result { let mut settings = AllAgentServersSettings::default(); - for AllAgentServersSettings { - gemini, - claude, - codex, - } in sources.defaults_and_customizations() - { + for AllAgentServersSettings { gemini, claude } in sources.defaults_and_customizations() { if gemini.is_some() { settings.gemini = gemini.clone(); } if claude.is_some() { settings.claude = claude.clone(); } - if codex.is_some() { - settings.codex = codex.clone(); - } } Ok(settings) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index e058284abc..57d3257f4d 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -246,7 +246,7 @@ impl AcpThreadView { { Err(e) => { let mut cx = cx.clone(); - if e.downcast_ref::().is_some() { + if e.is::() { this.update(&mut cx, |this, cx| { this.thread_state = ThreadState::Unauthenticated { connection }; cx.notify(); @@ -719,13 +719,18 @@ impl AcpThreadView { Some(entry.diffs().map(|diff| diff.multibuffer.clone())) } - fn authenticate(&mut self, window: &mut Window, cx: &mut Context) { + fn authenticate( + &mut self, + method: acp::AuthMethodId, + window: &mut Window, + cx: &mut Context, + ) { let ThreadState::Unauthenticated { ref connection } = self.thread_state else { return; }; self.last_error.take(); - let authenticate = connection.authenticate(cx); + let authenticate = connection.authenticate(method, cx); self.auth_task = Some(cx.spawn_in(window, { let project = self.project.clone(); let agent = self.agent.clone(); @@ -2424,22 +2429,26 @@ impl Render for AcpThreadView { .on_action(cx.listener(Self::next_history_message)) .on_action(cx.listener(Self::open_agent_diff)) .child(match &self.thread_state { - ThreadState::Unauthenticated { .. } => { - v_flex() - .p_2() - .flex_1() - .items_center() - .justify_center() - .child(self.render_pending_auth_state()) - .child( - h_flex().mt_1p5().justify_center().child( - Button::new("sign-in", format!("Sign in to {}", self.agent.name())) - .on_click(cx.listener(|this, _, window, cx| { - this.authenticate(window, cx) - })), - ), - ) - } + ThreadState::Unauthenticated { connection } => v_flex() + .p_2() + .flex_1() + .items_center() + .justify_center() + .child(self.render_pending_auth_state()) + .child(h_flex().mt_1p5().justify_center().children( + connection.auth_methods().into_iter().map(|method| { + Button::new( + SharedString::from(method.id.0.clone()), + method.label.clone(), + ) + .on_click({ + let method_id = method.id.clone(); + cx.listener(move |this, _, window, cx| { + this.authenticate(method_id.clone(), window, cx) + }) + }) + }), + )), ThreadState::Loading { .. } => v_flex().flex_1().child(self.render_empty_state(cx)), ThreadState::LoadError(e) => v_flex() .p_2() @@ -2878,8 +2887,8 @@ mod tests { } impl AgentConnection for StubAgentConnection { - fn name(&self) -> &'static str { - "StubAgentConnection" + fn auth_methods(&self) -> &[acp::AuthMethod] { + &[] } fn new_thread( @@ -2897,17 +2906,21 @@ mod tests { .into(), ); let thread = cx - .new(|cx| AcpThread::new(self.clone(), project, session_id.clone(), cx)) + .new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx)) .unwrap(); self.sessions.lock().insert(session_id, thread.downgrade()); Task::ready(Ok(thread)) } - fn authenticate(&self, _cx: &mut App) -> Task> { + fn authenticate( + &self, + _method_id: acp::AuthMethodId, + _cx: &mut App, + ) -> Task> { unimplemented!() } - fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task> { + fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task> { let sessions = self.sessions.lock(); let thread = sessions.get(¶ms.session_id).unwrap(); let mut tasks = vec![]; @@ -2954,10 +2967,6 @@ mod tests { struct SaboteurAgentConnection; impl AgentConnection for SaboteurAgentConnection { - fn name(&self) -> &'static str { - "SaboteurAgentConnection" - } - fn new_thread( self: Rc, project: Entity, @@ -2965,15 +2974,31 @@ mod tests { cx: &mut gpui::AsyncApp, ) -> Task>> { Task::ready(Ok(cx - .new(|cx| AcpThread::new(self, project, SessionId("test".into()), cx)) + .new(|cx| { + AcpThread::new( + "SaboteurAgentConnection", + self, + project, + SessionId("test".into()), + cx, + ) + }) .unwrap())) } - fn authenticate(&self, _cx: &mut App) -> Task> { + fn auth_methods(&self) -> &[acp::AuthMethod] { + &[] + } + + fn authenticate( + &self, + _method_id: acp::AuthMethodId, + _cx: &mut App, + ) -> Task> { unimplemented!() } - fn prompt(&self, _params: acp::PromptArguments, _cx: &mut App) -> Task> { + fn prompt(&self, _params: acp::PromptRequest, _cx: &mut App) -> Task> { Task::ready(Err(anyhow::anyhow!("Error prompting"))) } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index fcb8dfbac2..a09c669769 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1987,20 +1987,6 @@ impl AgentPanel { ); }), ) - .item( - ContextMenuEntry::new("New Codex Thread") - .icon(IconName::AiOpenAi) - .icon_color(Color::Muted) - .handler(move |window, cx| { - window.dispatch_action( - NewExternalAgentThread { - agent: Some(crate::ExternalAgent::Codex), - } - .boxed_clone(), - cx, - ); - }), - ) }); menu })) @@ -2662,25 +2648,6 @@ impl AgentPanel { ) }, ), - ) - .child( - NewThreadButton::new( - "new-codex-thread-btn", - "New Codex Thread", - IconName::AiOpenAi, - ) - .on_click( - |window, cx| { - window.dispatch_action( - Box::new(NewExternalAgentThread { - agent: Some( - crate::ExternalAgent::Codex, - ), - }), - cx, - ) - }, - ), ), ) }), diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 0800031abe..c5574c2371 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -150,7 +150,6 @@ enum ExternalAgent { #[default] Gemini, ClaudeCode, - Codex, } impl ExternalAgent { @@ -158,7 +157,6 @@ impl ExternalAgent { match self { ExternalAgent::Gemini => Rc::new(agent_servers::Gemini), ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode), - ExternalAgent::Codex => Rc::new(agent_servers::Codex), } } } diff --git a/crates/context_server/src/client.rs b/crates/context_server/src/client.rs index 1eb29bbbf9..65283afa87 100644 --- a/crates/context_server/src/client.rs +++ b/crates/context_server/src/client.rs @@ -441,14 +441,12 @@ impl Client { Ok(()) } - #[allow(unused)] - pub fn on_notification(&self, method: &'static str, f: F) - where - F: 'static + Send + FnMut(Value, AsyncApp), - { - self.notification_handlers - .lock() - .insert(method, Box::new(f)); + pub fn on_notification( + &self, + method: &'static str, + f: Box, + ) { + self.notification_handlers.lock().insert(method, f); } } diff --git a/crates/context_server/src/context_server.rs b/crates/context_server/src/context_server.rs index e76e7972f7..34fa29678d 100644 --- a/crates/context_server/src/context_server.rs +++ b/crates/context_server/src/context_server.rs @@ -95,8 +95,28 @@ impl ContextServer { self.client.read().clone() } - pub async fn start(self: Arc, cx: &AsyncApp) -> Result<()> { - let client = match &self.configuration { + pub async fn start(&self, cx: &AsyncApp) -> Result<()> { + self.initialize(self.new_client(cx)?).await + } + + /// Starts the context server, making sure handlers are registered before initialization happens + pub async fn start_with_handlers( + &self, + notification_handlers: Vec<( + &'static str, + Box, + )>, + cx: &AsyncApp, + ) -> Result<()> { + let client = self.new_client(cx)?; + for (method, handler) in notification_handlers { + client.on_notification(method, handler); + } + self.initialize(client).await + } + + fn new_client(&self, cx: &AsyncApp) -> Result { + Ok(match &self.configuration { ContextServerTransport::Stdio(command, working_directory) => Client::stdio( client::ContextServerId(self.id.0.clone()), client::ModelContextServerBinary { @@ -113,8 +133,7 @@ impl ContextServer { transport.clone(), cx.clone(), )?, - }; - self.initialize(client).await + }) } async fn initialize(&self, client: Client) -> Result<()> { diff --git a/crates/context_server/src/listener.rs b/crates/context_server/src/listener.rs index 34e3a9a78c..0e85fb2129 100644 --- a/crates/context_server/src/listener.rs +++ b/crates/context_server/src/listener.rs @@ -83,14 +83,18 @@ impl McpServer { } pub fn add_tool(&mut self, tool: T) { - let output_schema = schemars::schema_for!(T::Output); - let unit_schema = schemars::schema_for!(()); + let mut settings = schemars::generate::SchemaSettings::draft07(); + settings.inline_subschemas = true; + let mut generator = settings.into_generator(); + + let output_schema = generator.root_schema_for::(); + let unit_schema = generator.root_schema_for::(); let registered_tool = RegisteredTool { tool: Tool { name: T::NAME.into(), description: Some(tool.description().into()), - input_schema: schemars::schema_for!(T::Input).into(), + input_schema: generator.root_schema_for::().into(), output_schema: if output_schema == unit_schema { None } else { diff --git a/crates/context_server/src/protocol.rs b/crates/context_server/src/protocol.rs index 9ccbc8a553..5355f20f62 100644 --- a/crates/context_server/src/protocol.rs +++ b/crates/context_server/src/protocol.rs @@ -115,10 +115,11 @@ impl InitializedContextServerProtocol { self.inner.notify(T::METHOD, params) } - pub fn on_notification(&self, method: &'static str, f: F) - where - F: 'static + Send + FnMut(Value, AsyncApp), - { + pub fn on_notification( + &self, + method: &'static str, + f: Box, + ) { self.inner.on_notification(method, f); } } From 8b573d43953832b52acf057da021c4981aa01023 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Mon, 4 Aug 2025 16:56:56 +0300 Subject: [PATCH 042/693] evals: Retry on Anthropic's internal and transient I/O errors (#35395) Release Notes: - N/A --- .../assistant_tools/src/edit_agent/evals.rs | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/assistant_tools/src/edit_agent/evals.rs index eda7eee0e3..9a8e762455 100644 --- a/crates/assistant_tools/src/edit_agent/evals.rs +++ b/crates/assistant_tools/src/edit_agent/evals.rs @@ -1658,23 +1658,24 @@ impl EditAgentTest { } async fn retry_on_rate_limit(mut request: impl AsyncFnMut() -> Result) -> Result { + const MAX_RETRIES: usize = 20; let mut attempt = 0; + loop { attempt += 1; - match request().await { - Ok(result) => return Ok(result), - Err(err) => match err.downcast::() { - Ok(err) => match &err { + let response = request().await; + + if attempt >= MAX_RETRIES { + return response; + } + + let retry_delay = match &response { + Ok(_) => None, + Err(err) => match err.downcast_ref::() { + Some(err) => match &err { LanguageModelCompletionError::RateLimitExceeded { retry_after, .. } | LanguageModelCompletionError::ServerOverloaded { retry_after, .. } => { - let retry_after = retry_after.unwrap_or(Duration::from_secs(5)); - // Wait for the duration supplied, with some jitter to avoid all requests being made at the same time. - let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0)); - eprintln!( - "Attempt #{attempt}: {err}. Retry after {retry_after:?} + jitter of {jitter:?}" - ); - Timer::after(retry_after + jitter).await; - continue; + Some(retry_after.unwrap_or(Duration::from_secs(5))) } LanguageModelCompletionError::UpstreamProviderError { status, @@ -1687,23 +1688,31 @@ async fn retry_on_rate_limit(mut request: impl AsyncFnMut() -> Result) -> StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE ) || status.as_u16() == 529; - if !should_retry { - return Err(err.into()); + if should_retry { + // Use server-provided retry_after if available, otherwise use default + Some(retry_after.unwrap_or(Duration::from_secs(5))) + } else { + None } - - // Use server-provided retry_after if available, otherwise use default - let retry_after = retry_after.unwrap_or(Duration::from_secs(5)); - let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0)); - eprintln!( - "Attempt #{attempt}: {err}. Retry after {retry_after:?} + jitter of {jitter:?}" - ); - Timer::after(retry_after + jitter).await; - continue; } - _ => return Err(err.into()), + LanguageModelCompletionError::ApiReadResponseError { .. } + | LanguageModelCompletionError::ApiInternalServerError { .. } + | LanguageModelCompletionError::HttpSend { .. } => { + // Exponential backoff for transient I/O and internal server errors + Some(Duration::from_secs(2_u64.pow((attempt - 1) as u32).min(30))) + } + _ => None, }, - Err(err) => return Err(err), + _ => None, }, + }; + + if let Some(retry_after) = retry_delay { + let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0)); + eprintln!("Attempt #{attempt}: Retry after {retry_after:?} + jitter of {jitter:?}"); + Timer::after(retry_after + jitter).await; + } else { + return response; } } } From 0609c8b953243b93c41e4f8453dbac6646cf333c Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:58:31 -0300 Subject: [PATCH 043/693] Revise and clean up some icons (#35582) This is really just a small beginning, as there are many other icons to be revised and cleaned up. Our current set is a bit of a mess in terms of dimension, spacing, stroke width, and terminology. I'm sure there are more non-used icons I'm not covering here, too. We'll hopefully tackle it all soon leading up to 1.0. Closes https://github.com/zed-industries/zed/issues/35576 Release Notes: - N/A --- assets/icons/ai_bedrock.svg | 10 ++-- assets/icons/ai_deep_seek.svg | 4 +- assets/icons/ai_lm_studio.svg | 46 ++++++------------ assets/icons/ai_mistral.svg | 9 +++- assets/icons/ai_ollama.svg | 17 ++----- assets/icons/ai_open_ai.svg | 2 +- assets/icons/ai_open_router.svg | 14 +++--- assets/icons/ai_x_ai.svg | 2 +- assets/icons/ai_zed.svg | 9 +--- assets/icons/at_sign.svg | 1 - assets/icons/bolt.svg | 3 -- assets/icons/bolt_filled.svg | 4 +- assets/icons/bolt_filled_alt.svg | 3 -- assets/icons/bolt_outlined.svg | 3 ++ assets/icons/book_plus.svg | 1 - assets/icons/brain.svg | 1 - assets/icons/chat.svg | 4 ++ assets/icons/file_text.svg | 7 ++- assets/icons/git_onboarding_bg.svg | 40 ---------------- assets/icons/message_bubbles.svg | 6 --- assets/icons/microscope.svg | 1 - assets/icons/new_from_summary.svg | 7 --- assets/icons/play.svg | 3 -- assets/icons/play_bug.svg | 8 ---- assets/icons/play_filled.svg | 4 +- .../icons/{play_alt.svg => play_outlined.svg} | 2 +- assets/icons/reveal.svg | 1 - assets/icons/spinner.svg | 13 ----- assets/icons/strikethrough.svg | 3 -- .../{new_text_thread.svg => text_thread.svg} | 0 assets/icons/{new_thread.svg => thread.svg} | 0 assets/icons/thread_from_summary.svg | 6 +++ assets/icons/trash.svg | 6 ++- assets/icons/trash_alt.svg | 1 - assets/icons/zed_predict_bg.svg | 19 -------- crates/agent/src/context.rs | 4 +- .../manage_profiles_modal.rs | 2 +- crates/agent_ui/src/agent_panel.rs | 48 +++++++++---------- crates/agent_ui/src/context_picker.rs | 2 +- .../src/context_picker/completion_provider.rs | 4 +- .../context_picker/thread_context_picker.rs | 2 +- crates/agent_ui/src/inline_prompt_editor.rs | 2 +- crates/agent_ui/src/thread_history.rs | 2 +- crates/collab_ui/src/chat_panel.rs | 2 +- crates/collab_ui/src/collab_panel.rs | 6 +-- crates/editor/src/editor.rs | 44 +++++++++-------- crates/icons/src/icons.rs | 23 +++------ .../language_models/src/provider/lmstudio.rs | 2 +- crates/language_models/src/provider/ollama.rs | 2 +- crates/language_tools/src/lsp_tool.rs | 2 +- crates/recent_projects/src/remote_servers.rs | 2 +- crates/repl/src/notebook/cell.rs | 2 +- crates/repl/src/notebook/notebook_ui.rs | 2 +- crates/rules_library/src/rules_library.rs | 4 +- crates/tasks_ui/src/modal.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 2 +- .../ui/src/components/stories/icon_button.rs | 2 +- crates/zed/src/zed/quick_action_bar.rs | 2 +- 58 files changed, 154 insertions(+), 271 deletions(-) delete mode 100644 assets/icons/at_sign.svg delete mode 100644 assets/icons/bolt.svg delete mode 100644 assets/icons/bolt_filled_alt.svg create mode 100644 assets/icons/bolt_outlined.svg delete mode 100644 assets/icons/book_plus.svg delete mode 100644 assets/icons/brain.svg create mode 100644 assets/icons/chat.svg delete mode 100644 assets/icons/git_onboarding_bg.svg delete mode 100644 assets/icons/message_bubbles.svg delete mode 100644 assets/icons/microscope.svg delete mode 100644 assets/icons/new_from_summary.svg delete mode 100644 assets/icons/play.svg delete mode 100644 assets/icons/play_bug.svg rename assets/icons/{play_alt.svg => play_outlined.svg} (70%) delete mode 100644 assets/icons/reveal.svg delete mode 100644 assets/icons/spinner.svg delete mode 100644 assets/icons/strikethrough.svg rename assets/icons/{new_text_thread.svg => text_thread.svg} (100%) rename assets/icons/{new_thread.svg => thread.svg} (100%) create mode 100644 assets/icons/thread_from_summary.svg delete mode 100644 assets/icons/trash_alt.svg delete mode 100644 assets/icons/zed_predict_bg.svg diff --git a/assets/icons/ai_bedrock.svg b/assets/icons/ai_bedrock.svg index 2b672c364e..c9bbcc82e1 100644 --- a/assets/icons/ai_bedrock.svg +++ b/assets/icons/ai_bedrock.svg @@ -1,4 +1,8 @@ - - - + + + + + + + diff --git a/assets/icons/ai_deep_seek.svg b/assets/icons/ai_deep_seek.svg index cf480c834c..c8e5483fb3 100644 --- a/assets/icons/ai_deep_seek.svg +++ b/assets/icons/ai_deep_seek.svg @@ -1 +1,3 @@ -DeepSeek + + + diff --git a/assets/icons/ai_lm_studio.svg b/assets/icons/ai_lm_studio.svg index 0b455f48a7..5cfdeb5578 100644 --- a/assets/icons/ai_lm_studio.svg +++ b/assets/icons/ai_lm_studio.svg @@ -1,33 +1,15 @@ - - - Artboard - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/assets/icons/ai_mistral.svg b/assets/icons/ai_mistral.svg index 23b8f2ef6c..f11c177e2f 100644 --- a/assets/icons/ai_mistral.svg +++ b/assets/icons/ai_mistral.svg @@ -1 +1,8 @@ -Mistral \ No newline at end of file + + + + + + + + diff --git a/assets/icons/ai_ollama.svg b/assets/icons/ai_ollama.svg index d433df3981..36a88c1ad6 100644 --- a/assets/icons/ai_ollama.svg +++ b/assets/icons/ai_ollama.svg @@ -1,14 +1,7 @@ - - - - - - - - - - - - + + + + + diff --git a/assets/icons/ai_open_ai.svg b/assets/icons/ai_open_ai.svg index e659a472d8..e45ac315a0 100644 --- a/assets/icons/ai_open_ai.svg +++ b/assets/icons/ai_open_ai.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/ai_open_router.svg b/assets/icons/ai_open_router.svg index 94f2849146..b6f5164e0b 100644 --- a/assets/icons/ai_open_router.svg +++ b/assets/icons/ai_open_router.svg @@ -1,8 +1,8 @@ - - - - - - - + + + + + + + diff --git a/assets/icons/ai_x_ai.svg b/assets/icons/ai_x_ai.svg index 289525c8ef..d3400fbe9c 100644 --- a/assets/icons/ai_x_ai.svg +++ b/assets/icons/ai_x_ai.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/ai_zed.svg b/assets/icons/ai_zed.svg index 1c6bb8ad63..6d78efacd5 100644 --- a/assets/icons/ai_zed.svg +++ b/assets/icons/ai_zed.svg @@ -1,10 +1,3 @@ - - - - - - - - + diff --git a/assets/icons/at_sign.svg b/assets/icons/at_sign.svg deleted file mode 100644 index 4cf8cd468f..0000000000 --- a/assets/icons/at_sign.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/bolt.svg b/assets/icons/bolt.svg deleted file mode 100644 index 2688ede2a5..0000000000 --- a/assets/icons/bolt.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/bolt_filled.svg b/assets/icons/bolt_filled.svg index 543e72adf8..14d8f53e02 100644 --- a/assets/icons/bolt_filled.svg +++ b/assets/icons/bolt_filled.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/bolt_filled_alt.svg b/assets/icons/bolt_filled_alt.svg deleted file mode 100644 index 141e1c5f57..0000000000 --- a/assets/icons/bolt_filled_alt.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/bolt_outlined.svg b/assets/icons/bolt_outlined.svg new file mode 100644 index 0000000000..58fccf7788 --- /dev/null +++ b/assets/icons/bolt_outlined.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/book_plus.svg b/assets/icons/book_plus.svg deleted file mode 100644 index 2868f07cd0..0000000000 --- a/assets/icons/book_plus.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/brain.svg b/assets/icons/brain.svg deleted file mode 100644 index 80c93814f7..0000000000 --- a/assets/icons/brain.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/chat.svg b/assets/icons/chat.svg new file mode 100644 index 0000000000..a0548c3d3e --- /dev/null +++ b/assets/icons/chat.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/file_text.svg b/assets/icons/file_text.svg index 7c602f2ac7..a9b8f971e0 100644 --- a/assets/icons/file_text.svg +++ b/assets/icons/file_text.svg @@ -1 +1,6 @@ - + + + + + + diff --git a/assets/icons/git_onboarding_bg.svg b/assets/icons/git_onboarding_bg.svg deleted file mode 100644 index 18da0230a2..0000000000 --- a/assets/icons/git_onboarding_bg.svg +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/message_bubbles.svg b/assets/icons/message_bubbles.svg deleted file mode 100644 index 03a6c7760c..0000000000 --- a/assets/icons/message_bubbles.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/microscope.svg b/assets/icons/microscope.svg deleted file mode 100644 index 2b3009a28b..0000000000 --- a/assets/icons/microscope.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/new_from_summary.svg b/assets/icons/new_from_summary.svg deleted file mode 100644 index 3b61ca51a0..0000000000 --- a/assets/icons/new_from_summary.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/play.svg b/assets/icons/play.svg deleted file mode 100644 index 2481bda7d6..0000000000 --- a/assets/icons/play.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/play_bug.svg b/assets/icons/play_bug.svg deleted file mode 100644 index 7d265dd42a..0000000000 --- a/assets/icons/play_bug.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/assets/icons/play_filled.svg b/assets/icons/play_filled.svg index 387304ef04..c632434305 100644 --- a/assets/icons/play_filled.svg +++ b/assets/icons/play_filled.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/play_alt.svg b/assets/icons/play_outlined.svg similarity index 70% rename from assets/icons/play_alt.svg rename to assets/icons/play_outlined.svg index b327ab07b5..7e1cacd5af 100644 --- a/assets/icons/play_alt.svg +++ b/assets/icons/play_outlined.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/reveal.svg b/assets/icons/reveal.svg deleted file mode 100644 index ff5444d8f8..0000000000 --- a/assets/icons/reveal.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/spinner.svg b/assets/icons/spinner.svg deleted file mode 100644 index 4f4034ae89..0000000000 --- a/assets/icons/spinner.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/assets/icons/strikethrough.svg b/assets/icons/strikethrough.svg deleted file mode 100644 index d7d0905912..0000000000 --- a/assets/icons/strikethrough.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/new_text_thread.svg b/assets/icons/text_thread.svg similarity index 100% rename from assets/icons/new_text_thread.svg rename to assets/icons/text_thread.svg diff --git a/assets/icons/new_thread.svg b/assets/icons/thread.svg similarity index 100% rename from assets/icons/new_thread.svg rename to assets/icons/thread.svg diff --git a/assets/icons/thread_from_summary.svg b/assets/icons/thread_from_summary.svg new file mode 100644 index 0000000000..7519935aff --- /dev/null +++ b/assets/icons/thread_from_summary.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/trash.svg b/assets/icons/trash.svg index b71035b99c..1322e90f9f 100644 --- a/assets/icons/trash.svg +++ b/assets/icons/trash.svg @@ -1 +1,5 @@ - + + + + + diff --git a/assets/icons/trash_alt.svg b/assets/icons/trash_alt.svg deleted file mode 100644 index 6867b42147..0000000000 --- a/assets/icons/trash_alt.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/zed_predict_bg.svg b/assets/icons/zed_predict_bg.svg deleted file mode 100644 index 1dccbb51af..0000000000 --- a/assets/icons/zed_predict_bg.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/crates/agent/src/context.rs b/crates/agent/src/context.rs index ddd13de491..cd366b8308 100644 --- a/crates/agent/src/context.rs +++ b/crates/agent/src/context.rs @@ -42,8 +42,8 @@ impl ContextKind { ContextKind::Symbol => IconName::Code, ContextKind::Selection => IconName::Context, ContextKind::FetchedUrl => IconName::Globe, - ContextKind::Thread => IconName::MessageBubbles, - ContextKind::TextThread => IconName::MessageBubbles, + ContextKind::Thread => IconName::Thread, + ContextKind::TextThread => IconName::TextThread, ContextKind::Rules => RULES_ICON, ContextKind::Image => IconName::Image, } diff --git a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs index 45536ff13b..5d44bb2d92 100644 --- a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs +++ b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs @@ -483,7 +483,7 @@ impl ManageProfilesModal { let icon = match mode.profile_id.as_str() { "write" => IconName::Pencil, - "ask" => IconName::MessageBubbles, + "ask" => IconName::Chat, _ => IconName::UserRoundPen, }; diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index a09c669769..b552a701f0 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1911,27 +1911,6 @@ impl AgentPanel { .when(cx.has_flag::(), |this| { this.header("Zed Agent") }) - .item( - ContextMenuEntry::new("New Thread") - .icon(IconName::NewThread) - .icon_color(Color::Muted) - .action(NewThread::default().boxed_clone()) - .handler(move |window, cx| { - window.dispatch_action( - NewThread::default().boxed_clone(), - cx, - ); - }), - ) - .item( - ContextMenuEntry::new("New Text Thread") - .icon(IconName::NewTextThread) - .icon_color(Color::Muted) - .action(NewTextThread.boxed_clone()) - .handler(move |window, cx| { - window.dispatch_action(NewTextThread.boxed_clone(), cx); - }), - ) .when_some(active_thread, |this, active_thread| { let thread = active_thread.read(cx); @@ -1939,7 +1918,7 @@ impl AgentPanel { let thread_id = thread.id().clone(); this.item( ContextMenuEntry::new("New From Summary") - .icon(IconName::NewFromSummary) + .icon(IconName::ThreadFromSummary) .icon_color(Color::Muted) .handler(move |window, cx| { window.dispatch_action( @@ -1954,6 +1933,27 @@ impl AgentPanel { this } }) + .item( + ContextMenuEntry::new("New Thread") + .icon(IconName::Thread) + .icon_color(Color::Muted) + .action(NewThread::default().boxed_clone()) + .handler(move |window, cx| { + window.dispatch_action( + NewThread::default().boxed_clone(), + cx, + ); + }), + ) + .item( + ContextMenuEntry::new("New Text Thread") + .icon(IconName::TextThread) + .icon_color(Color::Muted) + .action(NewTextThread.boxed_clone()) + .handler(move |window, cx| { + window.dispatch_action(NewTextThread.boxed_clone(), cx); + }), + ) .when(cx.has_flag::(), |this| { this.separator() .header("External Agents") @@ -2558,7 +2558,7 @@ impl AgentPanel { NewThreadButton::new( "new-thread-btn", "New Thread", - IconName::NewThread, + IconName::Thread, ) .keybinding(KeyBinding::for_action_in( &NewThread::default(), @@ -2579,7 +2579,7 @@ impl AgentPanel { NewThreadButton::new( "new-text-thread-btn", "New Text Thread", - IconName::NewTextThread, + IconName::TextThread, ) .keybinding(KeyBinding::for_action_in( &NewTextThread, diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index 5cc56b014e..32f9a096d9 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -148,7 +148,7 @@ impl ContextPickerMode { Self::File => IconName::File, Self::Symbol => IconName::Code, Self::Fetch => IconName::Globe, - Self::Thread => IconName::MessageBubbles, + Self::Thread => IconName::Thread, Self::Rules => RULES_ICON, } } diff --git a/crates/agent_ui/src/context_picker/completion_provider.rs b/crates/agent_ui/src/context_picker/completion_provider.rs index b377e40b19..5ca0913be7 100644 --- a/crates/agent_ui/src/context_picker/completion_provider.rs +++ b/crates/agent_ui/src/context_picker/completion_provider.rs @@ -423,7 +423,7 @@ impl ContextPickerCompletionProvider { let icon_for_completion = if recent { IconName::HistoryRerun } else { - IconName::MessageBubbles + IconName::Thread }; let new_text = format!("{} ", MentionLink::for_thread(&thread_entry)); let new_text_len = new_text.len(); @@ -436,7 +436,7 @@ impl ContextPickerCompletionProvider { source: project::CompletionSource::Custom, icon_path: Some(icon_for_completion.path().into()), confirm: Some(confirm_completion_callback( - IconName::MessageBubbles.path().into(), + IconName::Thread.path().into(), thread_entry.title().clone(), excerpt_id, source_range.start, diff --git a/crates/agent_ui/src/context_picker/thread_context_picker.rs b/crates/agent_ui/src/context_picker/thread_context_picker.rs index cb2e97a493..15cc731f8f 100644 --- a/crates/agent_ui/src/context_picker/thread_context_picker.rs +++ b/crates/agent_ui/src/context_picker/thread_context_picker.rs @@ -253,7 +253,7 @@ pub fn render_thread_context_entry( .gap_1p5() .max_w_72() .child( - Icon::new(IconName::MessageBubbles) + Icon::new(IconName::Thread) .size(IconSize::XSmall) .color(Color::Muted), ) diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index ade7a5e13d..a5f90edb57 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -541,7 +541,7 @@ impl PromptEditor { match &self.mode { PromptEditorMode::Terminal { .. } => vec![ accept, - IconButton::new("confirm", IconName::Play) + IconButton::new("confirm", IconName::PlayOutlined) .icon_color(Color::Info) .shape(IconButtonShape::Square) .tooltip(|window, cx| { diff --git a/crates/agent_ui/src/thread_history.rs b/crates/agent_ui/src/thread_history.rs index a2ee816f73..b8d1db88d6 100644 --- a/crates/agent_ui/src/thread_history.rs +++ b/crates/agent_ui/src/thread_history.rs @@ -701,7 +701,7 @@ impl RenderOnce for HistoryEntryElement { .on_hover(self.on_hover) .end_slot::(if self.hovered || self.selected { Some( - IconButton::new("delete", IconName::TrashAlt) + IconButton::new("delete", IconName::Trash) .shape(IconButtonShape::Square) .icon_size(IconSize::XSmall) .icon_color(Color::Muted) diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 3e2d813f1b..3a9b568264 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -1162,7 +1162,7 @@ impl Panel for ChatPanel { } fn icon(&self, _window: &Window, cx: &App) -> Option { - self.enabled(cx).then(|| ui::IconName::MessageBubbles) + self.enabled(cx).then(|| ui::IconName::Chat) } fn icon_tooltip(&self, _: &Window, _: &App) -> Option<&'static str> { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 54077303a1..689591df12 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1124,7 +1124,7 @@ impl CollabPanel { .relative() .gap_1() .child(render_tree_branch(false, false, window, cx)) - .child(IconButton::new(0, IconName::MessageBubbles)) + .child(IconButton::new(0, IconName::Chat)) .children(has_messages_notification.then(|| { div() .w_1p5() @@ -2923,7 +2923,7 @@ impl CollabPanel { .gap_1() .px_1() .child( - IconButton::new("channel_chat", IconName::MessageBubbles) + IconButton::new("channel_chat", IconName::Chat) .style(ButtonStyle::Filled) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::Small) @@ -2939,7 +2939,7 @@ impl CollabPanel { .visible_on_hover(""), ) .child( - IconButton::new("channel_notes", IconName::File) + IconButton::new("channel_notes", IconName::FileText) .style(ButtonStyle::Filled) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::Small) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 49484ed137..e4628b43aa 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6403,7 +6403,6 @@ impl Editor { IconButton::new("inline_code_actions", ui::IconName::BoltFilled) .icon_size(icon_size) .shape(ui::IconButtonShape::Square) - .style(ButtonStyle::Transparent) .icon_color(ui::Color::Hidden) .toggle_state(is_active) .when(show_tooltip, |this| { @@ -8338,26 +8337,29 @@ impl Editor { let color = Color::Muted; let position = breakpoint.as_ref().map(|(anchor, _, _)| *anchor); - IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(color) - .toggle_state(is_active) - .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| { - let quick_launch = e.down.button == MouseButton::Left; - window.focus(&editor.focus_handle(cx)); - editor.toggle_code_actions( - &ToggleCodeActions { - deployed_from: Some(CodeActionSource::RunMenu(row)), - quick_launch, - }, - window, - cx, - ); - })) - .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { - editor.set_breakpoint_context_menu(row, position, event.down.position, window, cx); - })) + IconButton::new( + ("run_indicator", row.0 as usize), + ui::IconName::PlayOutlined, + ) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(color) + .toggle_state(is_active) + .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| { + let quick_launch = e.down.button == MouseButton::Left; + window.focus(&editor.focus_handle(cx)); + editor.toggle_code_actions( + &ToggleCodeActions { + deployed_from: Some(CodeActionSource::RunMenu(row)), + quick_launch, + }, + window, + cx, + ); + })) + .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { + editor.set_breakpoint_context_menu(row, position, event.down.position, window, cx); + })) } pub fn context_menu_visible(&self) -> bool { diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index fe68cdd2d6..a94d89bdc8 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -38,7 +38,6 @@ pub enum IconName { ArrowUpFromLine, ArrowUpRight, ArrowUpRightAlt, - AtSign, AudioOff, AudioOn, Backspace, @@ -48,15 +47,13 @@ pub enum IconName { BellRing, Binary, Blocks, - Bolt, + BoltOutlined, BoltFilled, - BoltFilledAlt, Book, BookCopy, - BookPlus, - Brain, BugOff, CaseSensitive, + Chat, Check, CheckDouble, ChevronDown, @@ -184,14 +181,9 @@ pub enum IconName { Maximize, Menu, MenuAlt, - MessageBubbles, Mic, MicMute, - Microscope, Minimize, - NewFromSummary, - NewTextThread, - NewThread, Option, PageDown, PageUp, @@ -202,9 +194,7 @@ pub enum IconName { PersonCircle, PhoneIncoming, Pin, - Play, - PlayAlt, - PlayBug, + PlayOutlined, PlayFilled, Plus, PocketKnife, @@ -221,7 +211,6 @@ pub enum IconName { ReplyArrowRight, Rerun, Return, - Reveal, RotateCcw, RotateCw, Route, @@ -246,7 +235,6 @@ pub enum IconName { Sparkle, SparkleAlt, SparkleFilled, - Spinner, Split, SplitAlt, SquareDot, @@ -256,7 +244,6 @@ pub enum IconName { StarFilled, Stop, StopFilled, - Strikethrough, Supermaven, SupermavenDisabled, SupermavenError, @@ -266,6 +253,9 @@ pub enum IconName { Terminal, TerminalAlt, TextSnippet, + TextThread, + Thread, + ThreadFromSummary, ThumbsDown, ThumbsUp, TodoComplete, @@ -285,7 +275,6 @@ pub enum IconName { ToolTerminal, ToolWeb, Trash, - TrashAlt, Triangle, TriangleRight, Undo, diff --git a/crates/language_models/src/provider/lmstudio.rs b/crates/language_models/src/provider/lmstudio.rs index 01600f3646..9792b4f27b 100644 --- a/crates/language_models/src/provider/lmstudio.rs +++ b/crates/language_models/src/provider/lmstudio.rs @@ -744,7 +744,7 @@ impl Render for ConfigurationView { Button::new("retry_lmstudio_models", "Connect") .icon_position(IconPosition::Start) .icon_size(IconSize::XSmall) - .icon(IconName::Play) + .icon(IconName::PlayOutlined) .on_click(cx.listener(move |this, _, _window, cx| { this.retry_connection(cx) })), diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index c20ea0ee1e..d4739bcab8 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -658,7 +658,7 @@ impl Render for ConfigurationView { Button::new("retry_ollama_models", "Connect") .icon_position(IconPosition::Start) .icon_size(IconSize::XSmall) - .icon(IconName::Play) + .icon(IconName::PlayOutlined) .on_click(cx.listener(move |this, _, _, cx| { this.retry_connection(cx) })), diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_tool.rs index a339f3b941..50547253a9 100644 --- a/crates/language_tools/src/lsp_tool.rs +++ b/crates/language_tools/src/lsp_tool.rs @@ -1015,7 +1015,7 @@ impl Render for LspTool { .anchor(Corner::BottomLeft) .with_handle(self.popover_menu_handle.clone()) .trigger_with_tooltip( - IconButton::new("zed-lsp-tool-button", IconName::Bolt) + IconButton::new("zed-lsp-tool-button", IconName::BoltOutlined) .when_some(indicator, IconButton::indicator) .icon_size(IconSize::Small) .indicator_border_color(Some(cx.theme().colors().status_bar_background)), diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index aa5103e62b..655e24860a 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -963,7 +963,7 @@ impl RemoteServerProjects { .child({ let project = project.clone(); // Right-margin to offset it from the Scrollbar - IconButton::new("remove-remote-project", IconName::TrashAlt) + IconButton::new("remove-remote-project", IconName::Trash) .icon_size(IconSize::Small) .shape(IconButtonShape::Square) .size(ButtonSize::Large) diff --git a/crates/repl/src/notebook/cell.rs b/crates/repl/src/notebook/cell.rs index 2ed68c17d1..18851417c0 100644 --- a/crates/repl/src/notebook/cell.rs +++ b/crates/repl/src/notebook/cell.rs @@ -38,7 +38,7 @@ pub enum CellControlType { impl CellControlType { fn icon_name(&self) -> IconName { match self { - CellControlType::RunCell => IconName::Play, + CellControlType::RunCell => IconName::PlayOutlined, CellControlType::RerunCell => IconName::ArrowCircle, CellControlType::ClearCell => IconName::ListX, CellControlType::CellOptions => IconName::Ellipsis, diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index d14f458fa9..3e96cc4d11 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -343,7 +343,7 @@ impl NotebookEditor { .child( Self::render_notebook_control( "run-all-cells", - IconName::Play, + IconName::PlayOutlined, window, cx, ) diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index be6a69c23b..2f77b4f3cc 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -319,7 +319,7 @@ impl PickerDelegate for RulePickerDelegate { }) .into_any() } else { - IconButton::new("delete-rule", IconName::TrashAlt) + IconButton::new("delete-rule", IconName::Trash) .icon_color(Color::Muted) .icon_size(IconSize::Small) .shape(IconButtonShape::Square) @@ -1163,7 +1163,7 @@ impl RulesLibrary { }) .into_any() } else { - IconButton::new("delete-rule", IconName::TrashAlt) + IconButton::new("delete-rule", IconName::Trash) .icon_size(IconSize::Small) .tooltip(move |window, cx| { Tooltip::for_action( diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 1510f613e3..c4b0931c35 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -500,7 +500,7 @@ impl PickerDelegate for TasksModalDelegate { .map(|icon| icon.color(Color::Muted).size(IconSize::Small)); let indicator = if matches!(source_kind, TaskSourceKind::Lsp { .. }) { Some(Indicator::icon( - Icon::new(IconName::Bolt).size(IconSize::Small), + Icon::new(IconName::BoltOutlined).size(IconSize::Small), )) } else { None diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index bf65a736e8..2e6be5aaf4 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1591,7 +1591,7 @@ impl Item for TerminalView { let (icon, icon_color, rerun_button) = match terminal.task() { Some(terminal_task) => match &terminal_task.status { TaskStatus::Running => ( - IconName::Play, + IconName::PlayOutlined, Color::Disabled, TerminalView::rerun_button(&terminal_task), ), diff --git a/crates/ui/src/components/stories/icon_button.rs b/crates/ui/src/components/stories/icon_button.rs index e787e81b55..ad6886252d 100644 --- a/crates/ui/src/components/stories/icon_button.rs +++ b/crates/ui/src/components/stories/icon_button.rs @@ -77,7 +77,7 @@ impl Render for IconButtonStory { let with_tooltip_button = StoryItem::new( "With `tooltip`", - IconButton::new("with_tooltip_button", IconName::MessageBubbles) + IconButton::new("with_tooltip_button", IconName::Chat) .tooltip(Tooltip::text("Open messages")), ) .description("Displays an icon button that has a tooltip when hovered.") diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index aff124a0bc..1164704ce6 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -192,7 +192,7 @@ impl Render for QuickActionBar { }; v_flex() .child( - IconButton::new("toggle_code_actions_icon", IconName::Bolt) + IconButton::new("toggle_code_actions_icon", IconName::BoltOutlined) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) .disabled(!has_available_code_actions) From 5f77c6a68fdc7f982a00d02d539cacbf017fbd1c Mon Sep 17 00:00:00 2001 From: Joshua Byrd Date: Tue, 5 Aug 2025 00:58:41 +1000 Subject: [PATCH 044/693] docs: Rewrite the OpenAI compatible API section (#35558) This PR updates the OpenAI compatible API section clarifying that API keys aren't stored in the `settings.json`. It also updates the JSON as some fields are not available anymore. Release Notes: - docs: Updated the OpenAI compatible API section to clarify API keys aren't stored in your `settings.json`. --------- Co-authored-by: Danilo Leal --- docs/src/ai/llm-providers.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/docs/src/ai/llm-providers.md b/docs/src/ai/llm-providers.md index bd208e94ac..a6e6f7c774 100644 --- a/docs/src/ai/llm-providers.md +++ b/docs/src/ai/llm-providers.md @@ -441,30 +441,26 @@ Custom models will be listed in the model dropdown in the Agent Panel. ### OpenAI API Compatible {#openai-api-compatible} -Zed supports using [OpenAI compatible APIs](https://platform.openai.com/docs/api-reference/chat) by specifying a custom `api_url` and `available_models` for the OpenAI provider. This is useful for connecting to other hosted services (like Together AI, Anyscale, etc.) or local models. +Zed supports using [OpenAI compatible APIs](https://platform.openai.com/docs/api-reference/chat) by specifying a custom `api_url` and `available_models` for the OpenAI provider. +This is useful for connecting to other hosted services (like Together AI, Anyscale, etc.) or local models. -To configure a compatible API, you can add a custom API URL for OpenAI either via the UI (currently available only in Preview) or by editing your `settings.json`. +You can add a custom, OpenAI-compatible model via either via the UI or by editing your `settings.json`. -For example, to connect to [Together AI](https://www.together.ai/) via the UI: +To do it via the UI, go to the Agent Panel settings (`agent: open settings`) and look for the "Add Provider" button to the right of the "LLM Providers" section title. +Then, fill up the input fields available in the modal. -1. Get an API key from your [Together AI account](https://api.together.ai/settings/api-keys). -2. Go to the Agent Panel's settings view, click on the "Add Provider" button, and then on the "OpenAI" menu item -3. Add the requested fields, such as `api_url`, `api_key`, available models, and others - -Alternatively, you can also add it via the `settings.json`: +To do it via your `settings.json`, add the following snippet under `language_models`: ```json { "language_models": { "openai": { - "api_url": "https://api.together.xyz/v1", - "api_key": "YOUR_TOGETHER_AI_API_KEY", + "api_url": "https://api.together.xyz/v1", // Using Together AI as an example "available_models": [ { "name": "mistralai/Mixtral-8x7B-Instruct-v0.1", "display_name": "Together Mixtral 8x7B", - "max_tokens": 32768, - "supports_tools": true + "max_tokens": 32768 } ] } @@ -472,6 +468,9 @@ Alternatively, you can also add it via the `settings.json`: } ``` +Note that LLM API keys aren't stored in your settings file. +So, ensure you have it set in your environment variables (`OPENAI_API_KEY=`) so your settings can pick it up. + ### OpenRouter {#openrouter} > ✅ Supports tool use From a6a34dad0fd82dd945fbe799209195483afdb770 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Mon, 4 Aug 2025 12:02:22 -0300 Subject: [PATCH 045/693] Fix gemini e2e tests (#35583) Release Notes: - N/A --- crates/agent_servers/src/e2e_tests.rs | 6 +++--- crates/agent_servers/src/gemini.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 16bf1e6b47..a60aefb7b9 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -150,7 +150,7 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp drop(tempdir); } -pub async fn test_tool_call_with_confirmation( +pub async fn test_tool_call_with_permission( server: impl AgentServer + 'static, allow_option_id: acp::PermissionOptionId, cx: &mut TestAppContext, @@ -337,8 +337,8 @@ macro_rules! common_e2e_tests { #[::gpui::test] #[cfg_attr(not(feature = "e2e"), ignore)] - async fn tool_call_with_confirmation(cx: &mut ::gpui::TestAppContext) { - $crate::e2e_tests::test_tool_call_with_confirmation( + async fn tool_call_with_permission(cx: &mut ::gpui::TestAppContext) { + $crate::e2e_tests::test_tool_call_with_permission( $server, ::agent_client_protocol::PermissionOptionId($allow_option_id.into()), cx, diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 1119a8b4ee..2366783d22 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -64,7 +64,7 @@ pub(crate) mod tests { use crate::AgentServerCommand; use std::path::Path; - crate::common_e2e_tests!(Gemini, allow_option_id = "0"); + crate::common_e2e_tests!(Gemini, allow_option_id = "proceed_once"); pub fn local_command() -> AgentServerCommand { let cli_path = Path::new(env!("CARGO_MANIFEST_DIR")) From bb5af6f76d1042feb75644b986b560909a38ae5e Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Mon, 4 Aug 2025 11:37:34 -0400 Subject: [PATCH 046/693] Fix escape in terminal with JetBrains keymap (#35585) Closes https://github.com/zed-industries/zed/issues/35429 Closes https://github.com/zed-industries/zed/issues/35091 Follow-up to: https://github.com/zed-industries/zed/pull/35230 Release Notes: - Fix `escape` in Terminal broken in JetBrains compatability keymaps --- assets/keymaps/linux/jetbrains.json | 2 +- assets/keymaps/macos/jetbrains.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/keymaps/linux/jetbrains.json b/assets/keymaps/linux/jetbrains.json index 9bc1f24bfb..3df1243fed 100644 --- a/assets/keymaps/linux/jetbrains.json +++ b/assets/keymaps/linux/jetbrains.json @@ -166,7 +166,7 @@ { "context": "Diagnostics > Editor", "bindings": { "alt-6": "pane::CloseActiveItem" } }, { "context": "OutlinePanel", "bindings": { "alt-7": "workspace::CloseActiveDock" } }, { - "context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)", + "context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)", "bindings": { "escape": "editor::ToggleFocus", "shift-escape": "workspace::CloseActiveDock" diff --git a/assets/keymaps/macos/jetbrains.json b/assets/keymaps/macos/jetbrains.json index b1cd51a338..66962811f4 100644 --- a/assets/keymaps/macos/jetbrains.json +++ b/assets/keymaps/macos/jetbrains.json @@ -167,7 +167,7 @@ { "context": "Diagnostics > Editor", "bindings": { "cmd-6": "pane::CloseActiveItem" } }, { "context": "OutlinePanel", "bindings": { "cmd-7": "workspace::CloseActiveDock" } }, { - "context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)", + "context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)", "bindings": { "escape": "editor::ToggleFocus", "shift-escape": "workspace::CloseActiveDock" From d577ef52cb2a62f40063fba3f91a9b2e5d4b66a8 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:44:29 -0300 Subject: [PATCH 047/693] thread view: Scroll to the bottom when sending new messages + adjust controls display (#35586) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 120 +++++++++++++++---------- 1 file changed, 71 insertions(+), 49 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 57d3257f4d..24d8b73396 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -424,11 +424,14 @@ impl AcpThreadView { let mention_set = self.mention_set.clone(); self.set_editor_is_expanded(false, cx); + self.message_editor.update(cx, |editor, cx| { editor.clear(window, cx); editor.remove_creases(mention_set.lock().drain(), cx) }); + self.scroll_to_bottom(cx); + self.message_history.borrow_mut().push(chunks); } @@ -2022,15 +2025,15 @@ impl AcpThreadView { .icon_color(Color::Accent) .style(ButtonStyle::Filled) .disabled(self.thread().is_none() || is_editor_empty) - .on_click(cx.listener(|this, _, window, cx| { - this.chat(&Chat, window, cx); - })) .when(!is_editor_empty, |button| { button.tooltip(move |window, cx| Tooltip::for_action("Send", &Chat, window, cx)) }) .when(is_editor_empty, |button| { button.tooltip(Tooltip::text("Type a message to submit")) }) + .on_click(cx.listener(|this, _, window, cx| { + this.chat(&Chat, window, cx); + })) .into_any_element() } else { IconButton::new("stop-generation", IconName::StopFilled) @@ -2245,6 +2248,14 @@ impl AcpThreadView { cx.notify(); } + pub fn scroll_to_bottom(&mut self, cx: &mut Context) { + if let Some(thread) = self.thread() { + let entry_count = thread.read(cx).entries().len(); + self.list_state.reset(entry_count); + cx.notify(); + } + } + fn notify_with_sound( &mut self, caption: impl Into, @@ -2392,17 +2403,9 @@ impl AcpThreadView { self.notification_subscriptions.remove(&window); } } -} -impl Focusable for AcpThreadView { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.message_editor.focus_handle(cx) - } -} - -impl Render for AcpThreadView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let open_as_markdown = IconButton::new("open-as-markdown", IconName::DocumentText) + fn render_thread_controls(&mut self, cx: &mut Context) -> impl IntoElement { + let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileText) .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) .tooltip(Tooltip::text("Open Thread as Markdown")) @@ -2421,6 +2424,28 @@ impl Render for AcpThreadView { this.scroll_to_top(cx); })); + h_flex() + .mt_1() + .mr_1() + .py_2() + .px(RESPONSE_PADDING_X) + .opacity(0.4) + .hover(|style| style.opacity(1.)) + .flex_wrap() + .justify_end() + .child(open_as_markdown) + .child(scroll_to_top) + } +} + +impl Focusable for AcpThreadView { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.message_editor.focus_handle(cx) + } +} + +impl Render for AcpThreadView { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .key_context("AcpThread") @@ -2456,42 +2481,39 @@ impl Render for AcpThreadView { .items_center() .justify_center() .child(self.render_error_state(e, cx)), - ThreadState::Ready { thread, .. } => v_flex().flex_1().map(|this| { - if self.list_state.item_count() > 0 { - this.child( - list(self.list_state.clone()) - .with_sizing_behavior(gpui::ListSizingBehavior::Auto) - .flex_grow() - .into_any(), - ) - .child( - h_flex() - .group("controls") - .mt_1() - .mr_1() - .py_2() - .px(RESPONSE_PADDING_X) - .opacity(0.4) - .hover(|style| style.opacity(1.)) - .flex_wrap() - .justify_end() - .child(open_as_markdown) - .child(scroll_to_top) - .into_any_element(), - ) - .children(match thread.read(cx).status() { - ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => None, - ThreadStatus::Generating => div() - .px_5() - .py_2() - .child(LoadingLabel::new("").size(LabelSize::Small)) - .into(), - }) - .children(self.render_activity_bar(&thread, window, cx)) - } else { - this.child(self.render_empty_state(cx)) - } - }), + ThreadState::Ready { thread, .. } => { + let thread_clone = thread.clone(); + + v_flex().flex_1().map(|this| { + if self.list_state.item_count() > 0 { + let is_generating = + matches!(thread_clone.read(cx).status(), ThreadStatus::Generating); + + this.child( + list(self.list_state.clone()) + .with_sizing_behavior(gpui::ListSizingBehavior::Auto) + .flex_grow() + .into_any(), + ) + .when(!is_generating, |this| { + this.child(self.render_thread_controls(cx)) + }) + .children(match thread_clone.read(cx).status() { + ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => { + None + } + ThreadStatus::Generating => div() + .px_5() + .py_2() + .child(LoadingLabel::new("").size(LabelSize::Small)) + .into(), + }) + .children(self.render_activity_bar(&thread_clone, window, cx)) + } else { + this.child(self.render_empty_state(cx)) + } + }) + } }) .when_some(self.last_error.clone(), |el, error| { el.child( From 899bc8a8fd9c82565cc1b37659b226587b699929 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 4 Aug 2025 09:45:11 -0600 Subject: [PATCH 048/693] Fix edit prediction disablement with `"disable_ai": true` setting (#35513) Even after #35327 edit predictions were still being queried and shown after setting `"disable_ai": true` Also moves `DisableAiSettings` to the `project` crate so that it gets included in tests via existing use of `Project::init_settings(cx)`. Release Notes: - Fixed `"disable_ai": true` setting disabling edit predictions. --- Cargo.lock | 1 - crates/agent_ui/src/agent_panel.rs | 4 +-- crates/agent_ui/src/agent_ui.rs | 3 +- crates/agent_ui/src/inline_assistant.rs | 4 +-- crates/client/src/client.rs | 28 ----------------- crates/copilot/src/copilot.rs | 2 +- crates/editor/src/editor.rs | 15 ++++++++-- crates/git_ui/Cargo.toml | 1 - crates/git_ui/src/commit_modal.rs | 2 +- crates/git_ui/src/git_panel.rs | 7 ++--- crates/inline_completion_button/Cargo.toml | 1 + .../src/inline_completion_button.rs | 3 +- crates/onboarding/src/ai_setup_page.rs | 6 ++-- crates/project/src/project.rs | 30 ++++++++++++++++++- crates/welcome/src/welcome.rs | 3 +- crates/zed/src/zed/quick_action_bar.rs | 2 +- crates/zeta/src/init.rs | 2 +- 17 files changed, 60 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 56210557d2..7021506502 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6315,7 +6315,6 @@ dependencies = [ "buffer_diff", "call", "chrono", - "client", "cloud_llm_client", "collections", "command_palette_hooks", diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index b552a701f0..4751eff15e 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -43,7 +43,7 @@ use anyhow::{Result, anyhow}; use assistant_context::{AssistantContext, ContextEvent, ContextSummary}; use assistant_slash_command::SlashCommandWorkingSet; use assistant_tool::ToolWorkingSet; -use client::{DisableAiSettings, UserStore, zed_urls}; +use client::{UserStore, zed_urls}; use cloud_llm_client::{CompletionIntent, Plan, UsageLimit}; use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer}; use feature_flags::{self, FeatureFlagAppExt}; @@ -58,7 +58,7 @@ use language::LanguageRegistry; use language_model::{ ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry, }; -use project::{Project, ProjectPath, Worktree}; +use project::{DisableAiSettings, Project, ProjectPath, Worktree}; use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; use rules_library::{RulesLibrary, open_rules_library}; use search::{BufferSearchBar, buffer_search}; diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index c5574c2371..30faf5ef2e 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -31,7 +31,7 @@ use std::sync::Arc; use agent::{Thread, ThreadId}; use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection}; use assistant_slash_command::SlashCommandRegistry; -use client::{Client, DisableAiSettings}; +use client::Client; use command_palette_hooks::CommandPaletteFilter; use feature_flags::FeatureFlagAppExt as _; use fs::Fs; @@ -40,6 +40,7 @@ use language::LanguageRegistry; use language_model::{ ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, }; +use project::DisableAiSettings; use prompt_store::PromptBuilder; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index ffa654d12b..159ccd0635 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -16,7 +16,7 @@ use agent::{ }; use agent_settings::AgentSettings; use anyhow::{Context as _, Result}; -use client::{DisableAiSettings, telemetry::Telemetry}; +use client::telemetry::Telemetry; use collections::{HashMap, HashSet, VecDeque, hash_map}; use editor::SelectionEffects; use editor::{ @@ -39,7 +39,7 @@ use language_model::{ }; use multi_buffer::MultiBufferRow; use parking_lot::Mutex; -use project::{CodeAction, LspAction, Project, ProjectTransaction}; +use project::{CodeAction, DisableAiSettings, LspAction, Project, ProjectTransaction}; use prompt_store::{PromptBuilder, PromptStore}; use settings::{Settings, SettingsStore}; use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase}; diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index e6d8f10d12..309e4d892f 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -150,7 +150,6 @@ impl Settings for ProxySettings { pub fn init_settings(cx: &mut App) { TelemetrySettings::register(cx); - DisableAiSettings::register(cx); ClientSettings::register(cx); ProxySettings::register(cx); } @@ -539,33 +538,6 @@ impl settings::Settings for TelemetrySettings { } } -/// Whether to disable all AI features in Zed. -/// -/// Default: false -#[derive(Copy, Clone, Debug)] -pub struct DisableAiSettings { - pub disable_ai: bool, -} - -impl settings::Settings for DisableAiSettings { - const KEY: Option<&'static str> = Some("disable_ai"); - - type FileContent = Option; - - fn load(sources: SettingsSources, _: &mut App) -> Result { - Ok(Self { - disable_ai: sources - .user - .or(sources.server) - .copied() - .flatten() - .unwrap_or(sources.default.ok_or_else(Self::missing_default)?), - }) - } - - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} -} - impl Client { pub fn new( clock: Arc, diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index cacf834e0d..efe6fb743a 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -6,7 +6,6 @@ mod sign_in; use crate::sign_in::initiate_sign_in_within_workspace; use ::fs::Fs; use anyhow::{Context as _, Result, anyhow}; -use client::DisableAiSettings; use collections::{HashMap, HashSet}; use command_palette_hooks::CommandPaletteFilter; use futures::{Future, FutureExt, TryFutureExt, channel::oneshot, future::Shared}; @@ -24,6 +23,7 @@ use language::{ use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName}; use node_runtime::NodeRuntime; use parking_lot::Mutex; +use project::DisableAiSettings; use request::StatusNotification; use serde_json::json; use settings::Settings; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e4628b43aa..8e1efc0701 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -83,7 +83,7 @@ use aho_corasick::AhoCorasick; use anyhow::{Context as _, Result, anyhow}; use blink_manager::BlinkManager; use buffer_diff::DiffHunkStatus; -use client::{Collaborator, DisableAiSettings, ParticipantIndex}; +use client::{Collaborator, ParticipantIndex}; use clock::{AGENT_REPLICA_ID, ReplicaId}; use code_context_menus::{ AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu, @@ -148,8 +148,8 @@ use parking_lot::Mutex; use persistence::DB; use project::{ BreakpointWithPosition, CodeAction, Completion, CompletionIntent, CompletionResponse, - CompletionSource, DocumentHighlight, InlayHint, Location, LocationLink, PrepareRenameResponse, - Project, ProjectItem, ProjectPath, ProjectTransaction, TaskSourceKind, + CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint, Location, LocationLink, + PrepareRenameResponse, Project, ProjectItem, ProjectPath, ProjectTransaction, TaskSourceKind, debugger::breakpoint_store::Breakpoint, debugger::{ breakpoint_store::{ @@ -6995,6 +6995,10 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Option<()> { + if DisableAiSettings::get_global(cx).disable_ai { + return None; + } + let provider = self.edit_prediction_provider()?; let cursor = self.selections.newest_anchor().head(); let (buffer, cursor_buffer_position) = @@ -7052,6 +7056,7 @@ impl Editor { pub fn update_edit_prediction_settings(&mut self, cx: &mut Context) { if self.edit_prediction_provider.is_none() || DisableAiSettings::get_global(cx).disable_ai { self.edit_prediction_settings = EditPredictionSettings::Disabled; + self.discard_inline_completion(false, cx); } else { let selection = self.selections.newest_anchor(); let cursor = selection.head(); @@ -7669,6 +7674,10 @@ impl Editor { _window: &mut Window, cx: &mut Context, ) -> Option<()> { + if DisableAiSettings::get_global(cx).disable_ai { + return None; + } + let selection = self.selections.newest_anchor(); let cursor = selection.head(); let multibuffer = self.buffer.read(cx).snapshot(cx); diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 4c919249ee..e6547e7ae9 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -23,7 +23,6 @@ askpass.workspace = true buffer_diff.workspace = true call.workspace = true chrono.workspace = true -client.workspace = true cloud_llm_client.workspace = true collections.workspace = true command_palette_hooks.workspace = true diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index 88ec2dc84e..5dfa800ae5 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -1,9 +1,9 @@ use crate::branch_picker::{self, BranchList}; use crate::git_panel::{GitPanel, commit_message_editor}; -use client::DisableAiSettings; use git::repository::CommitOptions; use git::{Amend, Commit, GenerateCommitMessage, Signoff}; use panel::{panel_button, panel_editor_style}; +use project::DisableAiSettings; use settings::Settings; use ui::{ ContextMenu, KeybindingHint, PopoverMenu, PopoverMenuHandle, SplitButton, Tooltip, prelude::*, diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index ee74ac4d54..344fa86142 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -12,7 +12,6 @@ use crate::{ use agent_settings::AgentSettings; use anyhow::Context as _; use askpass::AskPassDelegate; -use client::DisableAiSettings; use db::kvp::KEY_VALUE_STORE; use editor::{ Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar, @@ -51,10 +50,9 @@ use panel::{ PanelHeader, panel_button, panel_editor_container, panel_editor_style, panel_filled_button, panel_icon_button, }; -use project::git_store::{RepositoryEvent, RepositoryId}; use project::{ - Fs, Project, ProjectPath, - git_store::{GitStoreEvent, Repository}, + DisableAiSettings, Fs, Project, ProjectPath, + git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId}, }; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; @@ -5115,7 +5113,6 @@ mod tests { language::init(cx); editor::init(cx); Project::init_settings(cx); - client::DisableAiSettings::register(cx); crate::init(cx); }); } diff --git a/crates/inline_completion_button/Cargo.toml b/crates/inline_completion_button/Cargo.toml index b34e59336b..7b6ae43465 100644 --- a/crates/inline_completion_button/Cargo.toml +++ b/crates/inline_completion_button/Cargo.toml @@ -25,6 +25,7 @@ indoc.workspace = true inline_completion.workspace = true language.workspace = true paths.workspace = true +project.workspace = true regex.workspace = true settings.workspace = true supermaven.workspace = true diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index 2d7f211942..79ebc573df 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use client::{DisableAiSettings, UserStore, zed_urls}; +use client::{UserStore, zed_urls}; use cloud_llm_client::UsageLimit; use copilot::{Copilot, Status}; use editor::{ @@ -19,6 +19,7 @@ use language::{ EditPredictionsMode, File, Language, language_settings::{self, AllLanguageSettings, EditPredictionProvider, all_language_settings}, }; +use project::DisableAiSettings; use regex::Regex; use settings::{Settings, SettingsStore, update_settings_file}; use std::{ diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index 2f031e7bb8..b4b043196b 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -1,22 +1,20 @@ use std::sync::Arc; use ai_onboarding::{AiUpsellCard, SignInStatus}; -use client::DisableAiSettings; use fs::Fs; use gpui::{ Action, AnyView, App, DismissEvent, EventEmitter, FocusHandle, Focusable, Window, prelude::*, }; use itertools; - use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry}; +use project::DisableAiSettings; use settings::{Settings, update_settings_file}; use ui::{ Badge, ButtonLike, Divider, Modal, ModalFooter, ModalHeader, Section, SwitchField, ToggleState, prelude::*, }; -use workspace::ModalView; - use util::ResultExt; +use workspace::ModalView; use zed_actions::agent::OpenSettings; use crate::Onboarding; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 623f48d3c9..5000ba93be 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -97,7 +97,7 @@ use rpc::{ }; use search::{SearchInputKind, SearchQuery, SearchResult}; use search_history::SearchHistory; -use settings::{InvalidSettingsError, Settings, SettingsLocation, SettingsStore}; +use settings::{InvalidSettingsError, Settings, SettingsLocation, SettingsSources, SettingsStore}; use smol::channel::Receiver; use snippet::Snippet; use snippet_provider::SnippetProvider; @@ -942,10 +942,38 @@ pub enum PulledDiagnostics { }, } +/// Whether to disable all AI features in Zed. +/// +/// Default: false +#[derive(Copy, Clone, Debug)] +pub struct DisableAiSettings { + pub disable_ai: bool, +} + +impl settings::Settings for DisableAiSettings { + const KEY: Option<&'static str> = Some("disable_ai"); + + type FileContent = Option; + + fn load(sources: SettingsSources, _: &mut App) -> Result { + Ok(Self { + disable_ai: sources + .user + .or(sources.server) + .copied() + .flatten() + .unwrap_or(sources.default.ok_or_else(Self::missing_default)?), + }) + } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} +} + impl Project { pub fn init_settings(cx: &mut App) { WorktreeSettings::register(cx); ProjectSettings::register(cx); + DisableAiSettings::register(cx); } pub fn init(client: &Arc, cx: &mut App) { diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 352118eee8..b0a1c316f4 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -1,10 +1,11 @@ -use client::{DisableAiSettings, TelemetrySettings, telemetry::Telemetry}; +use client::{TelemetrySettings, telemetry::Telemetry}; use db::kvp::KEY_VALUE_STORE; use gpui::{ Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, actions, svg, }; use language::language_settings::{EditPredictionProvider, all_language_settings}; +use project::DisableAiSettings; use settings::{Settings, SettingsStore}; use std::sync::Arc; use ui::{CheckboxWithLabel, ElevationIndex, Tooltip, prelude::*}; diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 1164704ce6..7ab7293573 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -2,7 +2,6 @@ mod preview; mod repl_menu; use agent_settings::AgentSettings; -use client::DisableAiSettings; use editor::actions::{ AddSelectionAbove, AddSelectionBelow, CodeActionSource, DuplicateLineDown, GoToDiagnostic, GoToHunk, GoToPreviousDiagnostic, GoToPreviousHunk, MoveLineDown, MoveLineUp, SelectAll, @@ -16,6 +15,7 @@ use gpui::{ FocusHandle, Focusable, InteractiveElement, ParentElement, Render, Styled, Subscription, WeakEntity, Window, anchored, deferred, point, }; +use project::DisableAiSettings; use project::project_settings::DiagnosticSeverity; use search::{BufferSearchBar, buffer_search}; use settings::{Settings, SettingsStore}; diff --git a/crates/zeta/src/init.rs b/crates/zeta/src/init.rs index 4a65771223..a01e3a89a2 100644 --- a/crates/zeta/src/init.rs +++ b/crates/zeta/src/init.rs @@ -1,10 +1,10 @@ use std::any::{Any, TypeId}; -use client::DisableAiSettings; use command_palette_hooks::CommandPaletteFilter; use feature_flags::{FeatureFlagAppExt as _, PredictEditsRateCompletionsFeatureFlag}; use gpui::actions; use language::language_settings::{AllLanguageSettings, EditPredictionProvider}; +use project::DisableAiSettings; use settings::{Settings, SettingsStore, update_settings_file}; use ui::App; use workspace::Workspace; From 85885723a958b2b8af1ee401f26590fb3bf196a5 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:55:19 -0300 Subject: [PATCH 049/693] agent: Fix scrolling in the "Add LLM Provider" modal (#35584) Closes https://github.com/zed-industries/zed/issues/35402 Release Notes: - agent: Fix scrolling in the "Add LLM Provider" modal --- .../add_llm_provider_modal.rs | 72 +++++++++---------- 1 file changed, 34 insertions(+), 38 deletions(-) diff --git a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs index 94b32d156b..401a633488 100644 --- a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs +++ b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs @@ -272,42 +272,34 @@ impl AddLlmProviderModal { cx.emit(DismissEvent); } - fn render_section(&self) -> Section { - Section::new() - .child(self.input.provider_name.clone()) - .child(self.input.api_url.clone()) - .child(self.input.api_key.clone()) - } - - fn render_model_section(&self, cx: &mut Context) -> Section { - Section::new().child( - v_flex() - .gap_2() - .child( - h_flex() - .justify_between() - .child(Label::new("Models").size(LabelSize::Small)) - .child( - Button::new("add-model", "Add Model") - .icon(IconName::Plus) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - this.input.add_model(window, cx); - cx.notify(); - })), - ), - ) - .children( - self.input - .models - .iter() - .enumerate() - .map(|(ix, _)| self.render_model(ix, cx)), - ), - ) + fn render_model_section(&self, cx: &mut Context) -> impl IntoElement { + v_flex() + .mt_1() + .gap_2() + .child( + h_flex() + .justify_between() + .child(Label::new("Models").size(LabelSize::Small)) + .child( + Button::new("add-model", "Add Model") + .icon(IconName::Plus) + .icon_position(IconPosition::Start) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .label_size(LabelSize::Small) + .on_click(cx.listener(|this, _, window, cx| { + this.input.add_model(window, cx); + cx.notify(); + })), + ), + ) + .children( + self.input + .models + .iter() + .enumerate() + .map(|(ix, _)| self.render_model(ix, cx)), + ) } fn render_model(&self, ix: usize, cx: &mut Context) -> impl IntoElement + use<> { @@ -393,10 +385,14 @@ impl Render for AddLlmProviderModal { .child( v_flex() .id("modal_content") + .size_full() .max_h_128() .overflow_y_scroll() - .gap_2() - .child(self.render_section()) + .px(DynamicSpacing::Base12.rems(cx)) + .gap(DynamicSpacing::Base04.rems(cx)) + .child(self.input.provider_name.clone()) + .child(self.input.api_url.clone()) + .child(self.input.api_key.clone()) .child(self.render_model_section(cx)), ) .footer( From 65018c28c0efb14b4c166141193b3c57dd960259 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 4 Aug 2025 10:22:18 -0600 Subject: [PATCH 050/693] Rename remaining mentions of "inline completion" to "edit prediction" (#35512) Release Notes: - N/A --- Cargo.lock | 96 ++--- Cargo.toml | 10 +- crates/agent_ui/src/inline_assistant.rs | 6 +- crates/agent_ui/src/text_thread_editor.rs | 4 +- crates/collab/src/api/events.rs | 4 +- crates/copilot/Cargo.toml | 2 +- .../src/copilot_completion_provider.rs | 90 ++--- crates/diagnostics/src/diagnostics_tests.rs | 2 +- .../Cargo.toml | 4 +- .../LICENSE-GPL | 0 .../src/edit_prediction.rs} | 14 +- .../Cargo.toml | 6 +- .../LICENSE-GPL | 0 .../src/edit_prediction_button.rs} | 22 +- crates/editor/Cargo.toml | 2 +- crates/editor/src/display_map.rs | 8 +- crates/editor/src/display_map/inlay_map.rs | 26 +- ...tion_tests.rs => edit_prediction_tests.rs} | 67 ++-- crates/editor/src/editor.rs | 343 +++++++++--------- crates/editor/src/editor_tests.rs | 16 +- crates/editor/src/element.rs | 54 +-- crates/editor/src/movement.rs | 4 +- crates/language_tools/src/lsp_log.rs | 4 +- .../src/migrations/m_2025_01_29/keymap.rs | 12 +- crates/rules_library/src/rules_library.rs | 2 +- crates/supermaven/Cargo.toml | 2 +- .../src/supermaven_completion_provider.rs | 8 +- .../telemetry_events/src/telemetry_events.rs | 12 +- crates/vim/src/motion.rs | 4 +- crates/vim/src/normal/change.rs | 4 +- crates/vim/src/normal/delete.rs | 4 +- crates/vim/src/vim.rs | 4 +- crates/zed/Cargo.toml | 2 +- crates/zed/src/main.rs | 12 +- crates/zed/src/zed.rs | 12 +- ...egistry.rs => edit_prediction_registry.rs} | 8 +- crates/zed/src/zed/quick_action_bar.rs | 6 +- crates/zeta/Cargo.toml | 2 +- crates/zeta/src/completion_diff_element.rs | 4 +- crates/zeta/src/rate_completion_modal.rs | 10 +- crates/zeta/src/zeta.rs | 82 ++--- docs/src/ai/llm-providers.md | 2 +- docs/src/key-bindings.md | 2 +- 43 files changed, 480 insertions(+), 498 deletions(-) rename crates/{inline_completion => edit_prediction}/Cargo.toml (82%) rename crates/{inline_completion => edit_prediction}/LICENSE-GPL (100%) rename crates/{inline_completion/src/inline_completion.rs => edit_prediction/src/edit_prediction.rs} (95%) rename crates/{inline_completion_button => edit_prediction_button}/Cargo.toml (90%) rename crates/{inline_completion_button => edit_prediction_button}/LICENSE-GPL (100%) rename crates/{inline_completion_button/src/inline_completion_button.rs => edit_prediction_button/src/edit_prediction_button.rs} (98%) rename crates/editor/src/{inline_completion_tests.rs => edit_prediction_tests.rs} (81%) rename crates/zed/src/zed/{inline_completion_registry.rs => edit_prediction_registry.rs} (96%) diff --git a/Cargo.lock b/Cargo.lock index 7021506502..c21aec93ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3542,13 +3542,13 @@ dependencies = [ "command_palette_hooks", "ctor", "dirs 4.0.0", + "edit_prediction", "editor", "fs", "futures 0.3.31", "gpui", "http_client", "indoc", - "inline_completion", "itertools 0.14.0", "language", "log", @@ -4855,6 +4855,49 @@ dependencies = [ "signature 1.6.4", ] +[[package]] +name = "edit_prediction" +version = "0.1.0" +dependencies = [ + "client", + "gpui", + "language", + "project", + "workspace-hack", +] + +[[package]] +name = "edit_prediction_button" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "cloud_llm_client", + "copilot", + "edit_prediction", + "editor", + "feature_flags", + "fs", + "futures 0.3.31", + "gpui", + "indoc", + "language", + "lsp", + "paths", + "project", + "regex", + "serde_json", + "settings", + "supermaven", + "telemetry", + "theme", + "ui", + "workspace", + "workspace-hack", + "zed_actions", + "zeta", +] + [[package]] name = "editor" version = "0.1.0" @@ -4870,6 +4913,7 @@ dependencies = [ "ctor", "dap", "db", + "edit_prediction", "emojis", "file_icons", "fs", @@ -4879,7 +4923,6 @@ dependencies = [ "gpui", "http_client", "indoc", - "inline_completion", "itertools 0.14.0", "language", "languages", @@ -8287,49 +8330,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "inline_completion" -version = "0.1.0" -dependencies = [ - "client", - "gpui", - "language", - "project", - "workspace-hack", -] - -[[package]] -name = "inline_completion_button" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "cloud_llm_client", - "copilot", - "editor", - "feature_flags", - "fs", - "futures 0.3.31", - "gpui", - "indoc", - "inline_completion", - "language", - "lsp", - "paths", - "project", - "regex", - "serde_json", - "settings", - "supermaven", - "telemetry", - "theme", - "ui", - "workspace", - "workspace-hack", - "zed_actions", - "zeta", -] - [[package]] name = "inotify" version = "0.9.6" @@ -15584,12 +15584,12 @@ dependencies = [ "anyhow", "client", "collections", + "edit_prediction", "editor", "env_logger 0.11.8", "futures 0.3.31", "gpui", "http_client", - "inline_completion", "language", "log", "postage", @@ -20221,6 +20221,7 @@ dependencies = [ "debugger_tools", "debugger_ui", "diagnostics", + "edit_prediction_button", "editor", "env_logger 0.11.8", "extension", @@ -20240,7 +20241,6 @@ dependencies = [ "http_client", "image_viewer", "indoc", - "inline_completion_button", "inspector_ui", "install_cli", "itertools 0.14.0", @@ -20572,6 +20572,7 @@ dependencies = [ "copilot", "ctor", "db", + "edit_prediction", "editor", "feature_flags", "fs", @@ -20579,7 +20580,6 @@ dependencies = [ "gpui", "http_client", "indoc", - "inline_completion", "language", "language_model", "log", diff --git a/Cargo.toml b/Cargo.toml index 5d852f8842..80796018eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,8 +79,8 @@ members = [ "crates/icons", "crates/image_viewer", "crates/indexed_docs", - "crates/inline_completion", - "crates/inline_completion_button", + "crates/edit_prediction", + "crates/edit_prediction_button", "crates/inspector_ui", "crates/install_cli", "crates/jj", @@ -302,8 +302,8 @@ http_client_tls = { path = "crates/http_client_tls" } icons = { path = "crates/icons" } image_viewer = { path = "crates/image_viewer" } indexed_docs = { path = "crates/indexed_docs" } -inline_completion = { path = "crates/inline_completion" } -inline_completion_button = { path = "crates/inline_completion_button" } +edit_prediction = { path = "crates/edit_prediction" } +edit_prediction_button = { path = "crates/edit_prediction_button" } inspector_ui = { path = "crates/inspector_ui" } install_cli = { path = "crates/install_cli" } jj = { path = "crates/jj" } @@ -756,7 +756,7 @@ feature_flags = { codegen-units = 1 } file_icons = { codegen-units = 1 } fsevent = { codegen-units = 1 } image_viewer = { codegen-units = 1 } -inline_completion_button = { codegen-units = 1 } +edit_prediction_button = { codegen-units = 1 } install_cli = { codegen-units = 1 } journal = { codegen-units = 1 } lmstudio = { codegen-units = 1 } diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 159ccd0635..4a4a747899 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -162,7 +162,7 @@ impl InlineAssistant { let window = windows[0]; let _ = window.update(cx, |_, window, cx| { editor.update(cx, |editor, cx| { - if editor.has_active_inline_completion() { + if editor.has_active_edit_prediction() { editor.cancel(&Default::default(), window, cx); } }); @@ -231,8 +231,8 @@ impl InlineAssistant { ); if DisableAiSettings::get_global(cx).disable_ai { - // Cancel any active completions - if editor.has_active_inline_completion() { + // Cancel any active edit predictions + if editor.has_active_edit_prediction() { editor.cancel(&Default::default(), window, cx); } } diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 3df0a48aa4..4836a95c8e 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -12,7 +12,7 @@ use assistant_slash_commands::{ use client::{proto, zed_urls}; use collections::{BTreeSet, HashMap, HashSet, hash_map}; use editor::{ - Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, MultiBuffer, MultiBufferSnapshot, + Anchor, Editor, EditorEvent, MenuEditPredictionsPolicy, MultiBuffer, MultiBufferSnapshot, RowExt, ToOffset as _, ToPoint, actions::{MoveToEndOfLine, Newline, ShowCompletions}, display_map::{ @@ -254,7 +254,7 @@ impl TextThreadEditor { editor.set_show_wrap_guides(false, cx); editor.set_show_indent_guides(false, cx); editor.set_completion_provider(Some(Rc::new(completion_provider))); - editor.set_menu_inline_completions_policy(MenuInlineCompletionsPolicy::Never); + editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::Never); editor.set_collaboration_hub(Box::new(project.clone())); let show_edit_predictions = all_language_settings(None, cx) diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index bc7dd152b0..2f34a843a8 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -580,7 +580,7 @@ fn for_snowflake( }, serde_json::to_value(e).unwrap(), ), - Event::InlineCompletion(e) => ( + Event::EditPrediction(e) => ( format!( "Edit Prediction {}", if e.suggestion_accepted { @@ -591,7 +591,7 @@ fn for_snowflake( ), serde_json::to_value(e).unwrap(), ), - Event::InlineCompletionRating(e) => ( + Event::EditPredictionRating(e) => ( "Edit Prediction Rated".to_string(), serde_json::to_value(e).unwrap(), ), diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 234875d420..8908143324 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -34,7 +34,7 @@ fs.workspace = true futures.workspace = true gpui.workspace = true http_client.workspace = true -inline_completion.workspace = true +edit_prediction.workspace = true language.workspace = true log.workspace = true lsp.workspace = true diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 8dc04622f9..2a7225c4e3 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -1,7 +1,7 @@ use crate::{Completion, Copilot}; use anyhow::Result; +use edit_prediction::{Direction, EditPrediction, EditPredictionProvider}; use gpui::{App, Context, Entity, EntityId, Task}; -use inline_completion::{Direction, EditPredictionProvider, InlineCompletion}; use language::{Buffer, OffsetRangeExt, ToOffset, language_settings::AllLanguageSettings}; use project::Project; use settings::Settings; @@ -210,7 +210,7 @@ impl EditPredictionProvider for CopilotCompletionProvider { buffer: &Entity, cursor_position: language::Anchor, cx: &mut Context, - ) -> Option { + ) -> Option { let buffer_id = buffer.entity_id(); let buffer = buffer.read(cx); let completion = self.active_completion()?; @@ -241,7 +241,7 @@ impl EditPredictionProvider for CopilotCompletionProvider { None } else { let position = cursor_position.bias_right(buffer); - Some(InlineCompletion { + Some(EditPrediction { id: None, edits: vec![(position..position, completion_text.into())], edit_preview: None, @@ -343,7 +343,7 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { assert!(editor.context_menu_visible()); - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); // Since we have both, the copilot suggestion is not shown inline assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n"); @@ -355,7 +355,7 @@ mod tests { .unwrap() .detach(); assert!(!editor.context_menu_visible()); - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n"); assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n"); }); @@ -389,7 +389,7 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, _, cx| { assert!(!editor.context_menu_visible()); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); // Since only the copilot is available, it's shown inline assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); @@ -400,7 +400,7 @@ mod tests { executor.run_until_parked(); cx.update_editor(|editor, _, cx| { assert!(!editor.context_menu_visible()); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); }); @@ -418,25 +418,25 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { assert!(!editor.context_menu_visible()); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); // Canceling should remove the active Copilot suggestion. editor.cancel(&Default::default(), window, cx); - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); // After canceling, tabbing shouldn't insert the previously shown suggestion. editor.tab(&Default::default(), window, cx); - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n"); // When undoing the previously active suggestion is shown again. editor.undo(&Default::default(), window, cx); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); }); @@ -444,25 +444,25 @@ mod tests { // If an edit occurs outside of this editor, the suggestion is still correctly interpolated. cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx)); cx.update_editor(|editor, window, cx| { - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); // AcceptEditPrediction when there is an active suggestion inserts it. editor.accept_edit_prediction(&Default::default(), window, cx); - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n"); // When undoing the previously active suggestion is shown again. editor.undo(&Default::default(), window, cx); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); // Hide suggestion. editor.cancel(&Default::default(), window, cx); - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); }); @@ -471,7 +471,7 @@ mod tests { // we won't make it visible. cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx)); cx.update_editor(|editor, _, cx| { - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n"); }); @@ -498,19 +498,19 @@ mod tests { }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); assert_eq!(editor.text(cx), "fn foo() {\n \n}"); // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion. editor.tab(&Default::default(), window, cx); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.text(cx), "fn foo() {\n \n}"); assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); // Using AcceptEditPrediction again accepts the suggestion. editor.accept_edit_prediction(&Default::default(), window, cx); - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}"); assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); }); @@ -575,17 +575,17 @@ mod tests { ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); // Accepting the first word of the suggestion should only accept the first word and still show the rest. - editor.accept_partial_inline_completion(&Default::default(), window, cx); - assert!(editor.has_active_inline_completion()); + editor.accept_partial_edit_prediction(&Default::default(), window, cx); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.text(cx), "one.copilot\ntwo\nthree\n"); assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); // Accepting next word should accept the non-word and copilot suggestion should be gone - editor.accept_partial_inline_completion(&Default::default(), window, cx); - assert!(!editor.has_active_inline_completion()); + editor.accept_partial_edit_prediction(&Default::default(), window, cx); + assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.text(cx), "one.copilot1\ntwo\nthree\n"); assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); }); @@ -617,11 +617,11 @@ mod tests { ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); // Accepting the first word (non-word) of the suggestion should only accept the first word and still show the rest. - editor.accept_partial_inline_completion(&Default::default(), window, cx); - assert!(editor.has_active_inline_completion()); + editor.accept_partial_edit_prediction(&Default::default(), window, cx); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.text(cx), "one.123. \ntwo\nthree\n"); assert_eq!( editor.display_text(cx), @@ -629,8 +629,8 @@ mod tests { ); // Accepting next word should accept the next word and copilot suggestion should still exist - editor.accept_partial_inline_completion(&Default::default(), window, cx); - assert!(editor.has_active_inline_completion()); + editor.accept_partial_edit_prediction(&Default::default(), window, cx); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.text(cx), "one.123. copilot\ntwo\nthree\n"); assert_eq!( editor.display_text(cx), @@ -638,8 +638,8 @@ mod tests { ); // Accepting the whitespace should accept the non-word/whitespaces with newline and copilot suggestion should be gone - editor.accept_partial_inline_completion(&Default::default(), window, cx); - assert!(!editor.has_active_inline_completion()); + editor.accept_partial_edit_prediction(&Default::default(), window, cx); + assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.text(cx), "one.123. copilot\n 456\ntwo\nthree\n"); assert_eq!( editor.display_text(cx), @@ -692,29 +692,29 @@ mod tests { }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\ntw\nthree\n"); editor.backspace(&Default::default(), window, cx); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\nt\nthree\n"); editor.backspace(&Default::default(), window, cx); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\n\nthree\n"); // Deleting across the original suggestion range invalidates it. editor.backspace(&Default::default(), window, cx); - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one\nthree\n"); assert_eq!(editor.text(cx), "one\nthree\n"); // Undoing the deletion restores the suggestion. editor.undo(&Default::default(), window, cx); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\n\nthree\n"); }); @@ -775,7 +775,7 @@ mod tests { }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); _ = editor.update(cx, |editor, _, cx| { - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!( editor.display_text(cx), "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n" @@ -797,7 +797,7 @@ mod tests { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(4, 5)..Point::new(4, 5)]) }); - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); assert_eq!( editor.display_text(cx), "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n" @@ -806,7 +806,7 @@ mod tests { // Type a character, ensuring we don't even try to interpolate the previous suggestion. editor.handle_input(" ", window, cx); - assert!(!editor.has_active_inline_completion()); + assert!(!editor.has_active_edit_prediction()); assert_eq!( editor.display_text(cx), "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n" @@ -817,7 +817,7 @@ mod tests { // Ensure the new suggestion is displayed when the debounce timeout expires. executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); _ = editor.update(cx, |editor, _, cx| { - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!( editor.display_text(cx), "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n" @@ -880,7 +880,7 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, _, cx| { assert!(!editor.context_menu_visible()); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\ntw\nthree\n"); }); @@ -907,7 +907,7 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, _, cx| { assert!(!editor.context_menu_visible()); - assert!(editor.has_active_inline_completion()); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\ntwo\nthree\n"); }); @@ -934,7 +934,7 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, _, cx| { assert!(editor.context_menu_visible()); - assert!(!editor.has_active_inline_completion(),); + assert!(!editor.has_active_edit_prediction(),); assert_eq!(editor.text(cx), "one\ntwo.\nthree\n"); }); } @@ -1023,7 +1023,7 @@ mod tests { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)]) }); - editor.refresh_inline_completion(true, false, window, cx); + editor.refresh_edit_prediction(true, false, window, cx); }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); @@ -1033,7 +1033,7 @@ mod tests { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(5, 0)..Point::new(5, 0)]) }); - editor.refresh_inline_completion(true, false, window, cx); + editor.refresh_edit_prediction(true, false, window, cx); }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 1364aaf853..1bb84488e8 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -873,7 +873,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S editor.splice_inlays( &[], - vec![Inlay::inline_completion( + vec![Inlay::edit_prediction( post_inc(&mut next_inlay_id), snapshot.buffer_snapshot.anchor_before(position), format!("Test inlay {next_inlay_id}"), diff --git a/crates/inline_completion/Cargo.toml b/crates/edit_prediction/Cargo.toml similarity index 82% rename from crates/inline_completion/Cargo.toml rename to crates/edit_prediction/Cargo.toml index 3a90875def..81c1e5dec2 100644 --- a/crates/inline_completion/Cargo.toml +++ b/crates/edit_prediction/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "inline_completion" +name = "edit_prediction" version = "0.1.0" edition.workspace = true publish.workspace = true @@ -9,7 +9,7 @@ license = "GPL-3.0-or-later" workspace = true [lib] -path = "src/inline_completion.rs" +path = "src/edit_prediction.rs" [dependencies] client.workspace = true diff --git a/crates/inline_completion/LICENSE-GPL b/crates/edit_prediction/LICENSE-GPL similarity index 100% rename from crates/inline_completion/LICENSE-GPL rename to crates/edit_prediction/LICENSE-GPL diff --git a/crates/inline_completion/src/inline_completion.rs b/crates/edit_prediction/src/edit_prediction.rs similarity index 95% rename from crates/inline_completion/src/inline_completion.rs rename to crates/edit_prediction/src/edit_prediction.rs index c8f35bf16a..fd4e9bb21d 100644 --- a/crates/inline_completion/src/inline_completion.rs +++ b/crates/edit_prediction/src/edit_prediction.rs @@ -7,7 +7,7 @@ use project::Project; // TODO: Find a better home for `Direction`. // -// This should live in an ancestor crate of `editor` and `inline_completion`, +// This should live in an ancestor crate of `editor` and `edit_prediction`, // but at time of writing there isn't an obvious spot. #[derive(Copy, Clone, PartialEq, Eq)] pub enum Direction { @@ -16,7 +16,7 @@ pub enum Direction { } #[derive(Clone)] -pub struct InlineCompletion { +pub struct EditPrediction { /// The ID of the completion, if it has one. pub id: Option, pub edits: Vec<(Range, String)>, @@ -102,10 +102,10 @@ pub trait EditPredictionProvider: 'static + Sized { buffer: &Entity, cursor_position: language::Anchor, cx: &mut Context, - ) -> Option; + ) -> Option; } -pub trait InlineCompletionProviderHandle { +pub trait EditPredictionProviderHandle { fn name(&self) -> &'static str; fn display_name(&self) -> &'static str; fn is_enabled( @@ -143,10 +143,10 @@ pub trait InlineCompletionProviderHandle { buffer: &Entity, cursor_position: language::Anchor, cx: &mut App, - ) -> Option; + ) -> Option; } -impl InlineCompletionProviderHandle for Entity +impl EditPredictionProviderHandle for Entity where T: EditPredictionProvider, { @@ -233,7 +233,7 @@ where buffer: &Entity, cursor_position: language::Anchor, cx: &mut App, - ) -> Option { + ) -> Option { self.update(cx, |this, cx| this.suggest(buffer, cursor_position, cx)) } } diff --git a/crates/inline_completion_button/Cargo.toml b/crates/edit_prediction_button/Cargo.toml similarity index 90% rename from crates/inline_completion_button/Cargo.toml rename to crates/edit_prediction_button/Cargo.toml index 7b6ae43465..07447280fa 100644 --- a/crates/inline_completion_button/Cargo.toml +++ b/crates/edit_prediction_button/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "inline_completion_button" +name = "edit_prediction_button" version = "0.1.0" edition.workspace = true publish.workspace = true @@ -9,7 +9,7 @@ license = "GPL-3.0-or-later" workspace = true [lib] -path = "src/inline_completion_button.rs" +path = "src/edit_prediction_button.rs" doctest = false [dependencies] @@ -22,7 +22,7 @@ feature_flags.workspace = true fs.workspace = true gpui.workspace = true indoc.workspace = true -inline_completion.workspace = true +edit_prediction.workspace = true language.workspace = true paths.workspace = true project.workspace = true diff --git a/crates/inline_completion_button/LICENSE-GPL b/crates/edit_prediction_button/LICENSE-GPL similarity index 100% rename from crates/inline_completion_button/LICENSE-GPL rename to crates/edit_prediction_button/LICENSE-GPL diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs similarity index 98% rename from crates/inline_completion_button/src/inline_completion_button.rs rename to crates/edit_prediction_button/src/edit_prediction_button.rs index 79ebc573df..33165bccf8 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -41,7 +41,7 @@ use zeta::RateCompletions; actions!( edit_prediction, [ - /// Toggles the inline completion menu. + /// Toggles the edit prediction menu. ToggleMenu ] ); @@ -51,14 +51,14 @@ const PRIVACY_DOCS: &str = "https://zed.dev/docs/ai/privacy-and-security"; struct CopilotErrorToast; -pub struct InlineCompletionButton { +pub struct EditPredictionButton { editor_subscription: Option<(Subscription, usize)>, editor_enabled: Option, editor_show_predictions: bool, editor_focus_handle: Option, language: Option>, file: Option>, - edit_prediction_provider: Option>, + edit_prediction_provider: Option>, fs: Arc, user_store: Entity, popover_menu_handle: PopoverMenuHandle, @@ -71,7 +71,7 @@ enum SupermavenButtonStatus { Initializing, } -impl Render for InlineCompletionButton { +impl Render for EditPredictionButton { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { // Return empty div if AI is disabled if DisableAiSettings::get_global(cx).disable_ai { @@ -369,7 +369,7 @@ impl Render for InlineCompletionButton { } } -impl InlineCompletionButton { +impl EditPredictionButton { pub fn new( fs: Arc, user_store: Entity, @@ -470,7 +470,7 @@ impl InlineCompletionButton { IconPosition::Start, None, move |_, cx| { - toggle_show_inline_completions_for_language(language.clone(), fs.clone(), cx) + toggle_show_edit_predictions_for_language(language.clone(), fs.clone(), cx) }, ); } @@ -480,7 +480,7 @@ impl InlineCompletionButton { let globally_enabled = settings.show_edit_predictions(None, cx); menu = menu.toggleable_entry("All Files", globally_enabled, IconPosition::Start, None, { let fs = fs.clone(); - move |_, cx| toggle_inline_completions_globally(fs.clone(), cx) + move |_, cx| toggle_edit_predictions_globally(fs.clone(), cx) }); let provider = settings.edit_predictions.provider; @@ -835,7 +835,7 @@ impl InlineCompletionButton { } } -impl StatusItemView for InlineCompletionButton { +impl StatusItemView for EditPredictionButton { fn set_active_pane_item( &mut self, item: Option<&dyn ItemHandle>, @@ -905,7 +905,7 @@ async fn open_disabled_globs_setting_in_editor( let settings = cx.global::(); - // Ensure that we always have "inline_completions { "disabled_globs": [] }" + // Ensure that we always have "edit_predictions { "disabled_globs": [] }" let edits = settings.edits_for_update::(&text, |file| { file.edit_predictions .get_or_insert_with(Default::default) @@ -943,7 +943,7 @@ async fn open_disabled_globs_setting_in_editor( anyhow::Ok(()) } -fn toggle_inline_completions_globally(fs: Arc, cx: &mut App) { +fn toggle_edit_predictions_globally(fs: Arc, cx: &mut App) { let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx); update_settings_file::(fs, cx, move |file, _| { file.defaults.show_edit_predictions = Some(!show_edit_predictions) @@ -958,7 +958,7 @@ fn set_completion_provider(fs: Arc, cx: &mut App, provider: EditPredicti }); } -fn toggle_show_inline_completions_for_language( +fn toggle_show_edit_predictions_for_language( language: Arc, fs: Arc, cx: &mut App, diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index ab2d1c8ecb..339f98ae8b 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -48,7 +48,7 @@ fs.workspace = true git.workspace = true gpui.workspace = true indoc.workspace = true -inline_completion.workspace = true +edit_prediction.workspace = true itertools.workspace = true language.workspace = true linkify.workspace = true diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 5425d5a8b9..a16e516a70 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -635,7 +635,7 @@ pub(crate) struct Highlights<'a> { } #[derive(Clone, Copy, Debug)] -pub struct InlineCompletionStyles { +pub struct EditPredictionStyles { pub insertion: HighlightStyle, pub whitespace: HighlightStyle, } @@ -643,7 +643,7 @@ pub struct InlineCompletionStyles { #[derive(Default, Debug, Clone, Copy)] pub struct HighlightStyles { pub inlay_hint: Option, - pub inline_completion: Option, + pub edit_prediction: Option, } #[derive(Clone)] @@ -958,7 +958,7 @@ impl DisplaySnapshot { language_aware, HighlightStyles { inlay_hint: Some(editor_style.inlay_hints_style), - inline_completion: Some(editor_style.inline_completion_styles), + edit_prediction: Some(editor_style.edit_prediction_styles), }, ) .flat_map(|chunk| { @@ -2036,7 +2036,7 @@ pub mod tests { map.update(cx, |map, cx| { map.splice_inlays( &[], - vec![Inlay::inline_completion( + vec![Inlay::edit_prediction( 0, buffer_snapshot.anchor_after(0), "\n", diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index a36d18ff6d..0b1c7a4bed 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -81,9 +81,9 @@ impl Inlay { } } - pub fn inline_completion>(id: usize, position: Anchor, text: T) -> Self { + pub fn edit_prediction>(id: usize, position: Anchor, text: T) -> Self { Self { - id: InlayId::InlineCompletion(id), + id: InlayId::EditPrediction(id), position, text: text.into(), color: None, @@ -340,15 +340,13 @@ impl<'a> Iterator for InlayChunks<'a> { let mut renderer = None; let mut highlight_style = match inlay.id { - InlayId::InlineCompletion(_) => { - self.highlight_styles.inline_completion.map(|s| { - if inlay.text.chars().all(|c| c.is_whitespace()) { - s.whitespace - } else { - s.insertion - } - }) - } + InlayId::EditPrediction(_) => self.highlight_styles.edit_prediction.map(|s| { + if inlay.text.chars().all(|c| c.is_whitespace()) { + s.whitespace + } else { + s.insertion + } + }), InlayId::Hint(_) => self.highlight_styles.inlay_hint, InlayId::DebuggerValue(_) => self.highlight_styles.inlay_hint, InlayId::Color(_) => { @@ -740,7 +738,7 @@ impl InlayMap { text.clone(), ) } else { - Inlay::inline_completion( + Inlay::edit_prediction( post_inc(next_inlay_id), snapshot.buffer.anchor_at(position, bias), text.clone(), @@ -1389,7 +1387,7 @@ mod tests { buffer.read(cx).snapshot(cx).anchor_before(3), "|123|", ), - Inlay::inline_completion( + Inlay::edit_prediction( post_inc(&mut next_inlay_id), buffer.read(cx).snapshot(cx).anchor_after(3), "|456|", @@ -1609,7 +1607,7 @@ mod tests { buffer.read(cx).snapshot(cx).anchor_before(4), "|456|", ), - Inlay::inline_completion( + Inlay::edit_prediction( post_inc(&mut next_inlay_id), buffer.read(cx).snapshot(cx).anchor_before(7), "\n|567|\n", diff --git a/crates/editor/src/inline_completion_tests.rs b/crates/editor/src/edit_prediction_tests.rs similarity index 81% rename from crates/editor/src/inline_completion_tests.rs rename to crates/editor/src/edit_prediction_tests.rs index 5ac34c94f5..527dfb8832 100644 --- a/crates/editor/src/inline_completion_tests.rs +++ b/crates/editor/src/edit_prediction_tests.rs @@ -1,26 +1,26 @@ +use edit_prediction::EditPredictionProvider; use gpui::{Entity, prelude::*}; use indoc::indoc; -use inline_completion::EditPredictionProvider; use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint}; use project::Project; use std::ops::Range; use text::{Point, ToOffset}; use crate::{ - InlineCompletion, editor_tests::init_test, test::editor_test_context::EditorTestContext, + EditPrediction, editor_tests::init_test, test::editor_test_context::EditorTestContext, }; #[gpui::test] -async fn test_inline_completion_insert(cx: &mut gpui::TestAppContext) { +async fn test_edit_prediction_insert(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeInlineCompletionProvider::default()); + let provider = cx.new(|_| FakeEditPredictionProvider::default()); assign_editor_completion_provider(provider.clone(), &mut cx); cx.set_state("let absolute_zero_celsius = ˇ;"); propose_edits(&provider, vec![(28..28, "-273.15")], &mut cx); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_edit_completion(&mut cx, |_, edits| { assert_eq!(edits.len(), 1); @@ -33,16 +33,16 @@ async fn test_inline_completion_insert(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_inline_completion_modification(cx: &mut gpui::TestAppContext) { +async fn test_edit_prediction_modification(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeInlineCompletionProvider::default()); + let provider = cx.new(|_| FakeEditPredictionProvider::default()); assign_editor_completion_provider(provider.clone(), &mut cx); cx.set_state("let pi = ˇ\"foo\";"); propose_edits(&provider, vec![(9..14, "3.14159")], &mut cx); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_edit_completion(&mut cx, |_, edits| { assert_eq!(edits.len(), 1); @@ -55,11 +55,11 @@ async fn test_inline_completion_modification(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) { +async fn test_edit_prediction_jump_button(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeInlineCompletionProvider::default()); + let provider = cx.new(|_| FakeEditPredictionProvider::default()); assign_editor_completion_provider(provider.clone(), &mut cx); // Cursor is 2+ lines above the proposed edit @@ -77,7 +77,7 @@ async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) { &mut cx, ); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), Point::new(4, 3)); }); @@ -107,7 +107,7 @@ async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) { &mut cx, ); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), Point::new(1, 3)); }); @@ -124,11 +124,11 @@ async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext) { +async fn test_edit_prediction_invalidation_range(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeInlineCompletionProvider::default()); + let provider = cx.new(|_| FakeEditPredictionProvider::default()); assign_editor_completion_provider(provider.clone(), &mut cx); // Cursor is 3+ lines above the proposed edit @@ -148,7 +148,7 @@ async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext &mut cx, ); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), edit_location); }); @@ -176,7 +176,7 @@ async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext line "}); cx.editor(|editor, _, _| { - assert!(editor.active_inline_completion.is_none()); + assert!(editor.active_edit_prediction.is_none()); }); // Cursor is 3+ lines below the proposed edit @@ -196,7 +196,7 @@ async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext &mut cx, ); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), edit_location); }); @@ -224,7 +224,7 @@ async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext line ˇ5 "}); cx.editor(|editor, _, _| { - assert!(editor.active_inline_completion.is_none()); + assert!(editor.active_edit_prediction.is_none()); }); } @@ -234,11 +234,11 @@ fn assert_editor_active_edit_completion( ) { cx.editor(|editor, _, cx| { let completion_state = editor - .active_inline_completion + .active_edit_prediction .as_ref() .expect("editor has no active completion"); - if let InlineCompletion::Edit { edits, .. } = &completion_state.completion { + if let EditPrediction::Edit { edits, .. } = &completion_state.completion { assert(editor.buffer().read(cx).snapshot(cx), edits); } else { panic!("expected edit completion"); @@ -252,11 +252,11 @@ fn assert_editor_active_move_completion( ) { cx.editor(|editor, _, cx| { let completion_state = editor - .active_inline_completion + .active_edit_prediction .as_ref() .expect("editor has no active completion"); - if let InlineCompletion::Move { target, .. } = &completion_state.completion { + if let EditPrediction::Move { target, .. } = &completion_state.completion { assert(editor.buffer().read(cx).snapshot(cx), *target); } else { panic!("expected move completion"); @@ -271,7 +271,7 @@ fn accept_completion(cx: &mut EditorTestContext) { } fn propose_edits( - provider: &Entity, + provider: &Entity, edits: Vec<(Range, &str)>, cx: &mut EditorTestContext, ) { @@ -283,7 +283,7 @@ fn propose_edits( cx.update(|_, cx| { provider.update(cx, |provider, _| { - provider.set_inline_completion(Some(inline_completion::InlineCompletion { + provider.set_edit_prediction(Some(edit_prediction::EditPrediction { id: None, edits: edits.collect(), edit_preview: None, @@ -293,7 +293,7 @@ fn propose_edits( } fn assign_editor_completion_provider( - provider: Entity, + provider: Entity, cx: &mut EditorTestContext, ) { cx.update_editor(|editor, window, cx| { @@ -302,20 +302,17 @@ fn assign_editor_completion_provider( } #[derive(Default, Clone)] -pub struct FakeInlineCompletionProvider { - pub completion: Option, +pub struct FakeEditPredictionProvider { + pub completion: Option, } -impl FakeInlineCompletionProvider { - pub fn set_inline_completion( - &mut self, - completion: Option, - ) { +impl FakeEditPredictionProvider { + pub fn set_edit_prediction(&mut self, completion: Option) { self.completion = completion; } } -impl EditPredictionProvider for FakeInlineCompletionProvider { +impl EditPredictionProvider for FakeEditPredictionProvider { fn name() -> &'static str { "fake-completion-provider" } @@ -355,7 +352,7 @@ impl EditPredictionProvider for FakeInlineCompletionProvider { &mut self, _buffer: gpui::Entity, _cursor_position: language::Anchor, - _direction: inline_completion::Direction, + _direction: edit_prediction::Direction, _cx: &mut gpui::Context, ) { } @@ -369,7 +366,7 @@ impl EditPredictionProvider for FakeInlineCompletionProvider { _buffer: &gpui::Entity, _cursor_position: language::Anchor, _cx: &mut gpui::Context, - ) -> Option { + ) -> Option { self.completion.clone() } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8e1efc0701..2912708b56 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -43,15 +43,16 @@ pub mod tasks; #[cfg(test)] mod code_completion_tests; #[cfg(test)] -mod editor_tests; +mod edit_prediction_tests; #[cfg(test)] -mod inline_completion_tests; +mod editor_tests; mod signature_help; #[cfg(any(test, feature = "test-support"))] pub mod test; pub(crate) use actions::*; pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder}; +pub use edit_prediction::Direction; pub use editor_settings::{ CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode, ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap, ShowScrollbar, @@ -62,7 +63,6 @@ pub use element::{ }; pub use git::blame::BlameRenderer; pub use hover_popover::hover_markdown_style; -pub use inline_completion::Direction; pub use items::MAX_TAB_TITLE_LEN; pub use lsp::CompletionContext; pub use lsp_ext::lsp_tasks; @@ -93,6 +93,7 @@ use collections::{BTreeMap, HashMap, HashSet, VecDeque}; use convert_case::{Case, Casing}; use dap::TelemetrySpawnLocation; use display_map::*; +use edit_prediction::{EditPredictionProvider, EditPredictionProviderHandle}; use editor_settings::{GoToDefinitionFallback, Minimap as MinimapSettings}; use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap, layout_line}; use futures::{ @@ -117,7 +118,6 @@ use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file}; use hover_popover::{HoverState, hide_hover}; use indent_guides::ActiveIndentGuidesState; use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; -use inline_completion::{EditPredictionProvider, InlineCompletionProviderHandle}; use itertools::Itertools; use language::{ AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow, @@ -268,7 +268,7 @@ impl InlineValueCache { #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum InlayId { - InlineCompletion(usize), + EditPrediction(usize), DebuggerValue(usize), // LSP Hint(usize), @@ -278,7 +278,7 @@ pub enum InlayId { impl InlayId { fn id(&self) -> usize { match self { - Self::InlineCompletion(id) => *id, + Self::EditPrediction(id) => *id, Self::DebuggerValue(id) => *id, Self::Hint(id) => *id, Self::Color(id) => *id, @@ -547,7 +547,7 @@ pub struct EditorStyle { pub syntax: Arc, pub status: StatusColors, pub inlay_hints_style: HighlightStyle, - pub inline_completion_styles: InlineCompletionStyles, + pub edit_prediction_styles: EditPredictionStyles, pub unnecessary_code_fade: f32, pub show_underlines: bool, } @@ -566,7 +566,7 @@ impl Default for EditorStyle { // style and retrieve them directly from the theme. status: StatusColors::dark(), inlay_hints_style: HighlightStyle::default(), - inline_completion_styles: InlineCompletionStyles { + edit_prediction_styles: EditPredictionStyles { insertion: HighlightStyle::default(), whitespace: HighlightStyle::default(), }, @@ -588,8 +588,8 @@ pub fn make_inlay_hints_style(cx: &mut App) -> HighlightStyle { } } -pub fn make_suggestion_styles(cx: &mut App) -> InlineCompletionStyles { - InlineCompletionStyles { +pub fn make_suggestion_styles(cx: &mut App) -> EditPredictionStyles { + EditPredictionStyles { insertion: HighlightStyle { color: Some(cx.theme().status().predictive), ..HighlightStyle::default() @@ -609,7 +609,7 @@ pub(crate) enum EditDisplayMode { Inline, } -enum InlineCompletion { +enum EditPrediction { Edit { edits: Vec<(Range, String)>, edit_preview: Option, @@ -622,9 +622,9 @@ enum InlineCompletion { }, } -struct InlineCompletionState { +struct EditPredictionState { inlay_ids: Vec, - completion: InlineCompletion, + completion: EditPrediction, completion_id: Option, invalidation_range: Range, } @@ -637,7 +637,7 @@ enum EditPredictionSettings { }, } -enum InlineCompletionHighlight {} +enum EditPredictionHighlight {} #[derive(Debug, Clone)] struct InlineDiagnostic { @@ -648,7 +648,7 @@ struct InlineDiagnostic { severity: lsp::DiagnosticSeverity, } -pub enum MenuInlineCompletionsPolicy { +pub enum MenuEditPredictionsPolicy { Never, ByProvider, } @@ -1087,15 +1087,15 @@ pub struct Editor { pending_mouse_down: Option>>>, gutter_hovered: bool, hovered_link_state: Option, - edit_prediction_provider: Option, + edit_prediction_provider: Option, code_action_providers: Vec>, - active_inline_completion: Option, + active_edit_prediction: Option, /// Used to prevent flickering as the user types while the menu is open - stale_inline_completion_in_menu: Option, + stale_edit_prediction_in_menu: Option, edit_prediction_settings: EditPredictionSettings, - inline_completions_hidden_for_vim_mode: bool, - show_inline_completions_override: Option, - menu_inline_completions_policy: MenuInlineCompletionsPolicy, + edit_predictions_hidden_for_vim_mode: bool, + show_edit_predictions_override: Option, + menu_edit_predictions_policy: MenuEditPredictionsPolicy, edit_prediction_preview: EditPredictionPreview, edit_prediction_indent_conflict: bool, edit_prediction_requires_modifier_in_indent_conflict: bool, @@ -1510,8 +1510,8 @@ pub struct RenameState { struct InvalidationStack(Vec); -struct RegisteredInlineCompletionProvider { - provider: Arc, +struct RegisteredEditPredictionProvider { + provider: Arc, _subscription: Subscription, } @@ -2096,8 +2096,8 @@ impl Editor { pending_mouse_down: None, hovered_link_state: None, edit_prediction_provider: None, - active_inline_completion: None, - stale_inline_completion_in_menu: None, + active_edit_prediction: None, + stale_edit_prediction_in_menu: None, edit_prediction_preview: EditPredictionPreview::Inactive { released_too_fast: false, }, @@ -2116,9 +2116,9 @@ impl Editor { hovered_cursors: HashMap::default(), next_editor_action_id: EditorActionId::default(), editor_actions: Rc::default(), - inline_completions_hidden_for_vim_mode: false, - show_inline_completions_override: None, - menu_inline_completions_policy: MenuInlineCompletionsPolicy::ByProvider, + edit_predictions_hidden_for_vim_mode: false, + show_edit_predictions_override: None, + menu_edit_predictions_policy: MenuEditPredictionsPolicy::ByProvider, edit_prediction_settings: EditPredictionSettings::Disabled, edit_prediction_indent_conflict: false, edit_prediction_requires_modifier_in_indent_conflict: true, @@ -2350,7 +2350,7 @@ impl Editor { } pub fn key_context(&self, window: &Window, cx: &App) -> KeyContext { - self.key_context_internal(self.has_active_inline_completion(), window, cx) + self.key_context_internal(self.has_active_edit_prediction(), window, cx) } fn key_context_internal( @@ -2717,17 +2717,16 @@ impl Editor { ) where T: EditPredictionProvider, { - self.edit_prediction_provider = - provider.map(|provider| RegisteredInlineCompletionProvider { - _subscription: cx.observe_in(&provider, window, |this, _, window, cx| { - if this.focus_handle.is_focused(window) { - this.update_visible_inline_completion(window, cx); - } - }), - provider: Arc::new(provider), - }); + self.edit_prediction_provider = provider.map(|provider| RegisteredEditPredictionProvider { + _subscription: cx.observe_in(&provider, window, |this, _, window, cx| { + if this.focus_handle.is_focused(window) { + this.update_visible_edit_prediction(window, cx); + } + }), + provider: Arc::new(provider), + }); self.update_edit_prediction_settings(cx); - self.refresh_inline_completion(false, false, window, cx); + self.refresh_edit_prediction(false, false, window, cx); } pub fn placeholder_text(&self) -> Option<&str> { @@ -2798,24 +2797,24 @@ impl Editor { self.input_enabled = input_enabled; } - pub fn set_inline_completions_hidden_for_vim_mode( + pub fn set_edit_predictions_hidden_for_vim_mode( &mut self, hidden: bool, window: &mut Window, cx: &mut Context, ) { - if hidden != self.inline_completions_hidden_for_vim_mode { - self.inline_completions_hidden_for_vim_mode = hidden; + if hidden != self.edit_predictions_hidden_for_vim_mode { + self.edit_predictions_hidden_for_vim_mode = hidden; if hidden { - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); } else { - self.refresh_inline_completion(true, false, window, cx); + self.refresh_edit_prediction(true, false, window, cx); } } } - pub fn set_menu_inline_completions_policy(&mut self, value: MenuInlineCompletionsPolicy) { - self.menu_inline_completions_policy = value; + pub fn set_menu_edit_predictions_policy(&mut self, value: MenuEditPredictionsPolicy) { + self.menu_edit_predictions_policy = value; } pub fn set_autoindent(&mut self, autoindent: bool) { @@ -2852,7 +2851,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if self.show_inline_completions_override.is_some() { + if self.show_edit_predictions_override.is_some() { self.set_show_edit_predictions(None, window, cx); } else { let show_edit_predictions = !self.edit_predictions_enabled(); @@ -2866,17 +2865,17 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.show_inline_completions_override = show_edit_predictions; + self.show_edit_predictions_override = show_edit_predictions; self.update_edit_prediction_settings(cx); if let Some(false) = show_edit_predictions { - self.discard_inline_completion(false, cx); + self.discard_edit_prediction(false, cx); } else { - self.refresh_inline_completion(false, true, window, cx); + self.refresh_edit_prediction(false, true, window, cx); } } - fn inline_completions_disabled_in_scope( + fn edit_predictions_disabled_in_scope( &self, buffer: &Entity, buffer_position: language::Anchor, @@ -3043,7 +3042,7 @@ impl Editor { self.refresh_document_highlights(cx); self.refresh_selected_text_highlights(false, window, cx); refresh_matching_bracket_highlights(self, window, cx); - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); self.edit_prediction_requires_modifier_in_indent_conflict = true; linked_editing_ranges::refresh_linked_ranges(self, window, cx); self.inline_blame_popover.take(); @@ -3833,7 +3832,7 @@ impl Editor { return true; } - if is_user_requested && self.discard_inline_completion(true, cx) { + if is_user_requested && self.discard_edit_prediction(true, cx) { return true; } @@ -4239,7 +4238,7 @@ impl Editor { ); } - let had_active_inline_completion = this.has_active_inline_completion(); + let had_active_edit_prediction = this.has_active_edit_prediction(); this.change_selections( SelectionEffects::scroll(Autoscroll::fit()).completions(false), window, @@ -4264,7 +4263,7 @@ impl Editor { } let trigger_in_words = - this.show_edit_predictions_in_menu() || !had_active_inline_completion; + this.show_edit_predictions_in_menu() || !had_active_edit_prediction; if this.hard_wrap.is_some() { let latest: Range = this.selections.newest(cx).range(); if latest.is_empty() @@ -4286,7 +4285,7 @@ impl Editor { } this.trigger_completion_on_input(&text, trigger_in_words, window, cx); linked_editing_ranges::refresh_linked_ranges(this, window, cx); - this.refresh_inline_completion(true, false, window, cx); + this.refresh_edit_prediction(true, false, window, cx); jsx_tag_auto_close::handle_from(this, initial_buffer_versions, window, cx); }); } @@ -4621,7 +4620,7 @@ impl Editor { .collect(); this.change_selections(Default::default(), window, cx, |s| s.select(new_selections)); - this.refresh_inline_completion(true, false, window, cx); + this.refresh_edit_prediction(true, false, window, cx); }); } @@ -5669,9 +5668,9 @@ impl Editor { crate::hover_popover::hide_hover(editor, cx); if editor.show_edit_predictions_in_menu() { - editor.update_visible_inline_completion(window, cx); + editor.update_visible_edit_prediction(window, cx); } else { - editor.discard_inline_completion(false, cx); + editor.discard_edit_prediction(false, cx); } cx.notify(); @@ -5682,10 +5681,10 @@ impl Editor { if editor.completion_tasks.len() <= 1 { // If there are no more completion tasks and the last menu was empty, we should hide it. let was_hidden = editor.hide_context_menu(window, cx).is_none(); - // If it was already hidden and we don't show inline completions in the menu, we should - // also show the inline-completion when available. + // If it was already hidden and we don't show edit predictions in the menu, + // we should also show the edit prediction when available. if was_hidden && editor.show_edit_predictions_in_menu() { - editor.update_visible_inline_completion(window, cx); + editor.update_visible_edit_prediction(window, cx); } } }) @@ -5779,7 +5778,7 @@ impl Editor { let entries = completions_menu.entries.borrow(); let mat = entries.get(item_ix.unwrap_or(completions_menu.selected_item))?; if self.show_edit_predictions_in_menu() { - self.discard_inline_completion(true, cx); + self.discard_edit_prediction(true, cx); } mat.candidate_id }; @@ -5923,7 +5922,7 @@ impl Editor { }) } - editor.refresh_inline_completion(true, false, window, cx); + editor.refresh_edit_prediction(true, false, window, cx); }); self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), &snapshot); @@ -5983,7 +5982,7 @@ impl Editor { let deployed_from = action.deployed_from.clone(); let action = action.clone(); self.completion_tasks.clear(); - self.discard_inline_completion(false, cx); + self.discard_edit_prediction(false, cx); let multibuffer_point = match &action.deployed_from { Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => { @@ -6988,7 +6987,7 @@ impl Editor { } } - pub fn refresh_inline_completion( + pub fn refresh_edit_prediction( &mut self, debounce: bool, user_requested: bool, @@ -7005,7 +7004,7 @@ impl Editor { self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; if !self.edit_predictions_enabled_in_buffer(&buffer, cursor_buffer_position, cx) { - self.discard_inline_completion(false, cx); + self.discard_edit_prediction(false, cx); return None; } @@ -7014,11 +7013,11 @@ impl Editor { || !self.is_focused(window) || buffer.read(cx).is_empty()) { - self.discard_inline_completion(false, cx); + self.discard_edit_prediction(false, cx); return None; } - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); provider.refresh( self.project.clone(), buffer, @@ -7056,7 +7055,7 @@ impl Editor { pub fn update_edit_prediction_settings(&mut self, cx: &mut Context) { if self.edit_prediction_provider.is_none() || DisableAiSettings::get_global(cx).disable_ai { self.edit_prediction_settings = EditPredictionSettings::Disabled; - self.discard_inline_completion(false, cx); + self.discard_edit_prediction(false, cx); } else { let selection = self.selections.newest_anchor(); let cursor = selection.head(); @@ -7077,8 +7076,8 @@ impl Editor { cx: &App, ) -> EditPredictionSettings { if !self.mode.is_full() - || !self.show_inline_completions_override.unwrap_or(true) - || self.inline_completions_disabled_in_scope(buffer, buffer_position, cx) + || !self.show_edit_predictions_override.unwrap_or(true) + || self.edit_predictions_disabled_in_scope(buffer, buffer_position, cx) { return EditPredictionSettings::Disabled; } @@ -7092,8 +7091,8 @@ impl Editor { }; let by_provider = matches!( - self.menu_inline_completions_policy, - MenuInlineCompletionsPolicy::ByProvider + self.menu_edit_predictions_policy, + MenuEditPredictionsPolicy::ByProvider ); let show_in_menu = by_provider @@ -7163,7 +7162,7 @@ impl Editor { .unwrap_or(false) } - fn cycle_inline_completion( + fn cycle_edit_prediction( &mut self, direction: Direction, window: &mut Window, @@ -7173,28 +7172,28 @@ impl Editor { let cursor = self.selections.newest_anchor().head(); let (buffer, cursor_buffer_position) = self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; - if self.inline_completions_hidden_for_vim_mode || !self.should_show_edit_predictions() { + if self.edit_predictions_hidden_for_vim_mode || !self.should_show_edit_predictions() { return None; } provider.cycle(buffer, cursor_buffer_position, direction, cx); - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); Some(()) } - pub fn show_inline_completion( + pub fn show_edit_prediction( &mut self, _: &ShowEditPrediction, window: &mut Window, cx: &mut Context, ) { - if !self.has_active_inline_completion() { - self.refresh_inline_completion(false, true, window, cx); + if !self.has_active_edit_prediction() { + self.refresh_edit_prediction(false, true, window, cx); return; } - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); } pub fn display_cursor_names( @@ -7226,11 +7225,11 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if self.has_active_inline_completion() { - self.cycle_inline_completion(Direction::Next, window, cx); + if self.has_active_edit_prediction() { + self.cycle_edit_prediction(Direction::Next, window, cx); } else { let is_copilot_disabled = self - .refresh_inline_completion(false, true, window, cx) + .refresh_edit_prediction(false, true, window, cx) .is_none(); if is_copilot_disabled { cx.propagate(); @@ -7244,11 +7243,11 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if self.has_active_inline_completion() { - self.cycle_inline_completion(Direction::Prev, window, cx); + if self.has_active_edit_prediction() { + self.cycle_edit_prediction(Direction::Prev, window, cx); } else { let is_copilot_disabled = self - .refresh_inline_completion(false, true, window, cx) + .refresh_edit_prediction(false, true, window, cx) .is_none(); if is_copilot_disabled { cx.propagate(); @@ -7266,18 +7265,14 @@ impl Editor { self.hide_context_menu(window, cx); } - let Some(active_inline_completion) = self.active_inline_completion.as_ref() else { + let Some(active_edit_prediction) = self.active_edit_prediction.as_ref() else { return; }; - self.report_inline_completion_event( - active_inline_completion.completion_id.clone(), - true, - cx, - ); + self.report_edit_prediction_event(active_edit_prediction.completion_id.clone(), true, cx); - match &active_inline_completion.completion { - InlineCompletion::Move { target, .. } => { + match &active_edit_prediction.completion { + EditPrediction::Move { target, .. } => { let target = *target; if let Some(position_map) = &self.last_position_map { @@ -7319,7 +7314,7 @@ impl Editor { } } } - InlineCompletion::Edit { edits, .. } => { + EditPrediction::Edit { edits, .. } => { if let Some(provider) = self.edit_prediction_provider() { provider.accept(cx); } @@ -7347,9 +7342,9 @@ impl Editor { } } - self.update_visible_inline_completion(window, cx); - if self.active_inline_completion.is_none() { - self.refresh_inline_completion(true, true, window, cx); + self.update_visible_edit_prediction(window, cx); + if self.active_edit_prediction.is_none() { + self.refresh_edit_prediction(true, true, window, cx); } cx.notify(); @@ -7359,27 +7354,23 @@ impl Editor { self.edit_prediction_requires_modifier_in_indent_conflict = false; } - pub fn accept_partial_inline_completion( + pub fn accept_partial_edit_prediction( &mut self, _: &AcceptPartialEditPrediction, window: &mut Window, cx: &mut Context, ) { - let Some(active_inline_completion) = self.active_inline_completion.as_ref() else { + let Some(active_edit_prediction) = self.active_edit_prediction.as_ref() else { return; }; if self.selections.count() != 1 { return; } - self.report_inline_completion_event( - active_inline_completion.completion_id.clone(), - true, - cx, - ); + self.report_edit_prediction_event(active_edit_prediction.completion_id.clone(), true, cx); - match &active_inline_completion.completion { - InlineCompletion::Move { target, .. } => { + match &active_edit_prediction.completion { + EditPrediction::Move { target, .. } => { let target = *target; self.change_selections( SelectionEffects::scroll(Autoscroll::newest()), @@ -7390,7 +7381,7 @@ impl Editor { }, ); } - InlineCompletion::Edit { edits, .. } => { + EditPrediction::Edit { edits, .. } => { // Find an insertion that starts at the cursor position. let snapshot = self.buffer.read(cx).snapshot(cx); let cursor_offset = self.selections.newest::(cx).head(); @@ -7424,7 +7415,7 @@ impl Editor { self.insert_with_autoindent_mode(&partial_completion, None, window, cx); - self.refresh_inline_completion(true, true, window, cx); + self.refresh_edit_prediction(true, true, window, cx); cx.notify(); } else { self.accept_edit_prediction(&Default::default(), window, cx); @@ -7433,28 +7424,28 @@ impl Editor { } } - fn discard_inline_completion( + fn discard_edit_prediction( &mut self, - should_report_inline_completion_event: bool, + should_report_edit_prediction_event: bool, cx: &mut Context, ) -> bool { - if should_report_inline_completion_event { + if should_report_edit_prediction_event { let completion_id = self - .active_inline_completion + .active_edit_prediction .as_ref() .and_then(|active_completion| active_completion.completion_id.clone()); - self.report_inline_completion_event(completion_id, false, cx); + self.report_edit_prediction_event(completion_id, false, cx); } if let Some(provider) = self.edit_prediction_provider() { provider.discard(cx); } - self.take_active_inline_completion(cx) + self.take_active_edit_prediction(cx) } - fn report_inline_completion_event(&self, id: Option, accepted: bool, cx: &App) { + fn report_edit_prediction_event(&self, id: Option, accepted: bool, cx: &App) { let Some(provider) = self.edit_prediction_provider() else { return; }; @@ -7485,18 +7476,18 @@ impl Editor { ); } - pub fn has_active_inline_completion(&self) -> bool { - self.active_inline_completion.is_some() + pub fn has_active_edit_prediction(&self) -> bool { + self.active_edit_prediction.is_some() } - fn take_active_inline_completion(&mut self, cx: &mut Context) -> bool { - let Some(active_inline_completion) = self.active_inline_completion.take() else { + fn take_active_edit_prediction(&mut self, cx: &mut Context) -> bool { + let Some(active_edit_prediction) = self.active_edit_prediction.take() else { return false; }; - self.splice_inlays(&active_inline_completion.inlay_ids, Default::default(), cx); - self.clear_highlights::(cx); - self.stale_inline_completion_in_menu = Some(active_inline_completion); + self.splice_inlays(&active_edit_prediction.inlay_ids, Default::default(), cx); + self.clear_highlights::(cx); + self.stale_edit_prediction_in_menu = Some(active_edit_prediction); true } @@ -7641,7 +7632,7 @@ impl Editor { since: Instant::now(), }; - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); cx.notify(); } } else if let EditPredictionPreview::Active { @@ -7664,12 +7655,12 @@ impl Editor { released_too_fast: since.elapsed() < Duration::from_millis(200), }; self.clear_row_highlights::(); - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); cx.notify(); } } - fn update_visible_inline_completion( + fn update_visible_edit_prediction( &mut self, _window: &mut Window, cx: &mut Context, @@ -7687,12 +7678,12 @@ impl Editor { let show_in_menu = self.show_edit_predictions_in_menu(); let completions_menu_has_precedence = !show_in_menu && (self.context_menu.borrow().is_some() - || (!self.completion_tasks.is_empty() && !self.has_active_inline_completion())); + || (!self.completion_tasks.is_empty() && !self.has_active_edit_prediction())); if completions_menu_has_precedence || !offset_selection.is_empty() || self - .active_inline_completion + .active_edit_prediction .as_ref() .map_or(false, |completion| { let invalidation_range = completion.invalidation_range.to_offset(&multibuffer); @@ -7700,11 +7691,11 @@ impl Editor { !invalidation_range.contains(&offset_selection.head()) }) { - self.discard_inline_completion(false, cx); + self.discard_edit_prediction(false, cx); return None; } - self.take_active_inline_completion(cx); + self.take_active_edit_prediction(cx); let Some(provider) = self.edit_prediction_provider() else { self.edit_prediction_settings = EditPredictionSettings::Disabled; return None; @@ -7730,8 +7721,8 @@ impl Editor { } } - let inline_completion = provider.suggest(&buffer, cursor_buffer_position, cx)?; - let edits = inline_completion + let edit_prediction = provider.suggest(&buffer, cursor_buffer_position, cx)?; + let edits = edit_prediction .edits .into_iter() .flat_map(|(range, new_text)| { @@ -7766,15 +7757,15 @@ impl Editor { None }; let is_move = - move_invalidation_row_range.is_some() || self.inline_completions_hidden_for_vim_mode; + move_invalidation_row_range.is_some() || self.edit_predictions_hidden_for_vim_mode; let completion = if is_move { invalidation_row_range = move_invalidation_row_range.unwrap_or(edit_start_row..edit_end_row); let target = first_edit_start; - InlineCompletion::Move { target, snapshot } + EditPrediction::Move { target, snapshot } } else { let show_completions_in_buffer = !self.edit_prediction_visible_in_cursor_popover(true) - && !self.inline_completions_hidden_for_vim_mode; + && !self.edit_predictions_hidden_for_vim_mode; if show_completions_in_buffer { if edits @@ -7783,7 +7774,7 @@ impl Editor { { let mut inlays = Vec::new(); for (range, new_text) in &edits { - let inlay = Inlay::inline_completion( + let inlay = Inlay::edit_prediction( post_inc(&mut self.next_inlay_id), range.start, new_text.as_str(), @@ -7795,7 +7786,7 @@ impl Editor { self.splice_inlays(&[], inlays, cx); } else { let background_color = cx.theme().status().deleted_background; - self.highlight_text::( + self.highlight_text::( edits.iter().map(|(range, _)| range.clone()).collect(), HighlightStyle { background_color: Some(background_color), @@ -7818,9 +7809,9 @@ impl Editor { EditDisplayMode::DiffPopover }; - InlineCompletion::Edit { + EditPrediction::Edit { edits, - edit_preview: inline_completion.edit_preview, + edit_preview: edit_prediction.edit_preview, display_mode, snapshot, } @@ -7833,11 +7824,11 @@ impl Editor { multibuffer.line_len(MultiBufferRow(invalidation_row_range.end)), )); - self.stale_inline_completion_in_menu = None; - self.active_inline_completion = Some(InlineCompletionState { + self.stale_edit_prediction_in_menu = None; + self.active_edit_prediction = Some(EditPredictionState { inlay_ids, completion, - completion_id: inline_completion.id, + completion_id: edit_prediction.id, invalidation_range, }); @@ -7846,7 +7837,7 @@ impl Editor { Some(()) } - pub fn edit_prediction_provider(&self) -> Option> { + pub fn edit_prediction_provider(&self) -> Option> { Some(self.edit_prediction_provider.as_ref()?.provider.clone()) } @@ -8415,14 +8406,14 @@ impl Editor { if self.mode().is_minimap() { return None; } - let active_inline_completion = self.active_inline_completion.as_ref()?; + let active_edit_prediction = self.active_edit_prediction.as_ref()?; if self.edit_prediction_visible_in_cursor_popover(true) { return None; } - match &active_inline_completion.completion { - InlineCompletion::Move { target, .. } => { + match &active_edit_prediction.completion { + EditPrediction::Move { target, .. } => { let target_display_point = target.to_display_point(editor_snapshot); if self.edit_prediction_requires_modifier() { @@ -8459,11 +8450,11 @@ impl Editor { ) } } - InlineCompletion::Edit { + EditPrediction::Edit { display_mode: EditDisplayMode::Inline, .. } => None, - InlineCompletion::Edit { + EditPrediction::Edit { display_mode: EditDisplayMode::TabAccept, edits, .. @@ -8484,7 +8475,7 @@ impl Editor { cx, ) } - InlineCompletion::Edit { + EditPrediction::Edit { edits, edit_preview, display_mode: EditDisplayMode::DiffPopover, @@ -8801,7 +8792,7 @@ impl Editor { } let highlighted_edits = - crate::inline_completion_edit_text(&snapshot, edits, edit_preview.as_ref()?, false, cx); + crate::edit_prediction_edit_text(&snapshot, edits, edit_preview.as_ref()?, false, cx); let styled_text = highlighted_edits.to_styled_text(&style.text); let line_count = highlighted_edits.text.lines().count(); @@ -9131,7 +9122,7 @@ impl Editor { .child(Icon::new(IconName::ZedPredict)) } - let completion = match &self.active_inline_completion { + let completion = match &self.active_edit_prediction { Some(prediction) => { if !self.has_visible_completions_menu() { const RADIUS: Pixels = px(6.); @@ -9149,7 +9140,7 @@ impl Editor { .rounded_tl(px(0.)) .overflow_hidden() .child(div().px_1p5().child(match &prediction.completion { - InlineCompletion::Move { target, snapshot } => { + EditPrediction::Move { target, snapshot } => { use text::ToPoint as _; if target.text_anchor.to_point(&snapshot).row > cursor_point.row { @@ -9158,7 +9149,7 @@ impl Editor { Icon::new(IconName::ZedPredictUp) } } - InlineCompletion::Edit { .. } => Icon::new(IconName::ZedPredict), + EditPrediction::Edit { .. } => Icon::new(IconName::ZedPredict), })) .child( h_flex() @@ -9217,7 +9208,7 @@ impl Editor { )? } - None if is_refreshing => match &self.stale_inline_completion_in_menu { + None if is_refreshing => match &self.stale_edit_prediction_in_menu { Some(stale_completion) => self.render_edit_prediction_cursor_popover_preview( stale_completion, cursor_point, @@ -9247,7 +9238,7 @@ impl Editor { completion.into_any_element() }; - let has_completion = self.active_inline_completion.is_some(); + let has_completion = self.active_edit_prediction.is_some(); let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; Some( @@ -9306,7 +9297,7 @@ impl Editor { fn render_edit_prediction_cursor_popover_preview( &self, - completion: &InlineCompletionState, + completion: &EditPredictionState, cursor_point: Point, style: &EditorStyle, cx: &mut Context, @@ -9334,7 +9325,7 @@ impl Editor { } match &completion.completion { - InlineCompletion::Move { + EditPrediction::Move { target, snapshot, .. } => Some( h_flex() @@ -9351,7 +9342,7 @@ impl Editor { .child(Label::new("Jump to Edit")), ), - InlineCompletion::Edit { + EditPrediction::Edit { edits, edit_preview, snapshot, @@ -9359,7 +9350,7 @@ impl Editor { } => { let first_edit_row = edits.first()?.0.start.text_anchor.to_point(&snapshot).row; - let (highlighted_edits, has_more_lines) = crate::inline_completion_edit_text( + let (highlighted_edits, has_more_lines) = crate::edit_prediction_edit_text( &snapshot, &edits, edit_preview.as_ref()?, @@ -9437,8 +9428,8 @@ impl Editor { cx.notify(); self.completion_tasks.clear(); let context_menu = self.context_menu.borrow_mut().take(); - self.stale_inline_completion_in_menu.take(); - self.update_visible_inline_completion(window, cx); + self.stale_edit_prediction_in_menu.take(); + self.update_visible_edit_prediction(window, cx); if let Some(CodeContextMenu::Completions(_)) = &context_menu { if let Some(completion_provider) = &self.completion_provider { completion_provider.selection_changed(None, window, cx); @@ -9796,7 +9787,7 @@ impl Editor { this.edit(edits, None, cx); }) } - this.refresh_inline_completion(true, false, window, cx); + this.refresh_edit_prediction(true, false, window, cx); linked_editing_ranges::refresh_linked_ranges(this, window, cx); }); } @@ -9815,7 +9806,7 @@ impl Editor { }) }); this.insert("", window, cx); - this.refresh_inline_completion(true, false, window, cx); + this.refresh_edit_prediction(true, false, window, cx); }); } @@ -9948,7 +9939,7 @@ impl Editor { self.transact(window, cx, |this, window, cx| { this.buffer.update(cx, |b, cx| b.edit(edits, None, cx)); this.change_selections(Default::default(), window, cx, |s| s.select(selections)); - this.refresh_inline_completion(true, false, window, cx); + this.refresh_edit_prediction(true, false, window, cx); }); } @@ -12277,7 +12268,7 @@ impl Editor { } self.request_autoscroll(Autoscroll::fit(), cx); self.unmark_text(window, cx); - self.refresh_inline_completion(true, false, window, cx); + self.refresh_edit_prediction(true, false, window, cx); cx.emit(EditorEvent::Edited { transaction_id }); cx.emit(EditorEvent::TransactionUndone { transaction_id }); } @@ -12307,7 +12298,7 @@ impl Editor { } self.request_autoscroll(Autoscroll::fit(), cx); self.unmark_text(window, cx); - self.refresh_inline_completion(true, false, window, cx); + self.refresh_edit_prediction(true, false, window, cx); cx.emit(EditorEvent::Edited { transaction_id }); } } @@ -15294,7 +15285,7 @@ impl Editor { ]) }); self.activate_diagnostics(buffer_id, next_diagnostic, window, cx); - self.refresh_inline_completion(false, true, window, cx); + self.refresh_edit_prediction(false, true, window, cx); } pub fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context) { @@ -16258,7 +16249,7 @@ impl Editor { font_weight: Some(FontWeight::BOLD), ..make_inlay_hints_style(cx.app) }, - inline_completion_styles: make_suggestion_styles( + edit_prediction_styles: make_suggestion_styles( cx.app, ), ..EditorStyle::default() @@ -19032,7 +19023,7 @@ impl Editor { (selection.range(), uuid.to_string()) }); this.edit(edits, cx); - this.refresh_inline_completion(true, false, window, cx); + this.refresh_edit_prediction(true, false, window, cx); }); } @@ -19885,8 +19876,8 @@ impl Editor { self.refresh_selected_text_highlights(true, window, cx); self.refresh_single_line_folds(window, cx); refresh_matching_bracket_highlights(self, window, cx); - if self.has_active_inline_completion() { - self.update_visible_inline_completion(window, cx); + if self.has_active_edit_prediction() { + self.update_visible_edit_prediction(window, cx); } if let Some(project) = self.project.as_ref() { if let Some(edited_buffer) = edited_buffer { @@ -20088,7 +20079,7 @@ impl Editor { } self.tasks_update_task = Some(self.refresh_runnables(window, cx)); self.update_edit_prediction_settings(cx); - self.refresh_inline_completion(true, false, window, cx); + self.refresh_edit_prediction(true, false, window, cx); self.refresh_inline_values(cx); self.refresh_inlay_hints( InlayHintRefreshReason::SettingsChange(inlay_hint_settings( @@ -20720,7 +20711,7 @@ impl Editor { { self.hide_context_menu(window, cx); } - self.discard_inline_completion(false, cx); + self.discard_edit_prediction(false, cx); cx.emit(EditorEvent::Blurred); cx.notify(); } @@ -22782,7 +22773,7 @@ impl Render for Editor { syntax: cx.theme().syntax().clone(), status: cx.theme().status().clone(), inlay_hints_style: make_inlay_hints_style(cx), - inline_completion_styles: make_suggestion_styles(cx), + edit_prediction_styles: make_suggestion_styles(cx), unnecessary_code_fade: ThemeSettings::get_global(cx).unnecessary_code_fade, show_underlines: self.diagnostics_enabled(), }, @@ -23177,7 +23168,7 @@ impl InvalidationRegion for SnippetState { } } -fn inline_completion_edit_text( +fn edit_prediction_edit_text( current_snapshot: &BufferSnapshot, edits: &[(Range, String)], edit_preview: &EditPreview, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 1a4f444275..1cb3565733 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -2,7 +2,7 @@ use super::*; use crate::{ JoinLines, code_context_menus::CodeContextMenu, - inline_completion_tests::FakeInlineCompletionProvider, + edit_prediction_tests::FakeEditPredictionProvider, linked_editing_ranges::LinkedEditingRanges, scroll::scroll_amount::ScrollAmount, test::{ @@ -7251,12 +7251,12 @@ async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) { +async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeInlineCompletionProvider::default()); + let provider = cx.new(|_| FakeEditPredictionProvider::default()); cx.update_editor(|editor, window, cx| { editor.set_edit_prediction_provider(Some(provider.clone()), window, cx); }); @@ -7279,7 +7279,7 @@ async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext cx.update(|_, cx| { provider.update(cx, |provider, _| { - provider.set_inline_completion(Some(inline_completion::InlineCompletion { + provider.set_edit_prediction(Some(edit_prediction::EditPrediction { id: None, edits: vec![(edit_position..edit_position, "X".into())], edit_preview: None, @@ -7287,7 +7287,7 @@ async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext }) }); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); cx.update_editor(|editor, window, cx| { editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx) }); @@ -20552,7 +20552,7 @@ async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContex } #[gpui::test] -async fn test_inline_completion_text(cx: &mut TestAppContext) { +async fn test_edit_prediction_text(cx: &mut TestAppContext) { init_test(cx, |_| {}); // Simple insertion @@ -20651,7 +20651,7 @@ async fn test_inline_completion_text(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) { +async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) { init_test(cx, |_| {}); // Deletion @@ -20741,7 +20741,7 @@ async fn assert_highlighted_edits( .await; cx.update(|_window, cx| { - let highlighted_edits = inline_completion_edit_text( + let highlighted_edits = edit_prediction_edit_text( &snapshot.as_singleton().unwrap().2, &edits, &edit_preview, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 7e77f113ac..268855ab61 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -3,11 +3,11 @@ use crate::{ CodeActionSource, ColumnarMode, ConflictsOurs, ConflictsOursMarker, ConflictsOuter, ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, - EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, - FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, - HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, - LineUp, MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, - PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase, + EditDisplayMode, EditPrediction, Editor, EditorMode, EditorSettings, EditorSnapshot, + EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, + HandleInput, HoveredCursor, InlayHintRefreshReason, JumpData, LineDown, LineHighlight, LineUp, + MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown, + PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SelectionDragState, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, ToggleFoldAll, code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP}, @@ -554,7 +554,7 @@ impl EditorElement { register_action(editor, window, Editor::signature_help_next); register_action(editor, window, Editor::next_edit_prediction); register_action(editor, window, Editor::previous_edit_prediction); - register_action(editor, window, Editor::show_inline_completion); + register_action(editor, window, Editor::show_edit_prediction); register_action(editor, window, Editor::context_menu_first); register_action(editor, window, Editor::context_menu_prev); register_action(editor, window, Editor::context_menu_next); @@ -562,7 +562,7 @@ impl EditorElement { register_action(editor, window, Editor::display_cursor_names); register_action(editor, window, Editor::unique_lines_case_insensitive); register_action(editor, window, Editor::unique_lines_case_sensitive); - register_action(editor, window, Editor::accept_partial_inline_completion); + register_action(editor, window, Editor::accept_partial_edit_prediction); register_action(editor, window, Editor::accept_edit_prediction); register_action(editor, window, Editor::restore_file); register_action(editor, window, Editor::git_restore); @@ -2093,7 +2093,7 @@ impl EditorElement { row_block_types: &HashMap, content_origin: gpui::Point, scroll_pixel_position: gpui::Point, - inline_completion_popover_origin: Option>, + edit_prediction_popover_origin: Option>, start_row: DisplayRow, end_row: DisplayRow, line_height: Pixels, @@ -2210,12 +2210,13 @@ impl EditorElement { cmp::max(padded_line, min_start) }; - let behind_inline_completion_popover = inline_completion_popover_origin - .as_ref() - .map_or(false, |inline_completion_popover_origin| { - (pos_y..pos_y + line_height).contains(&inline_completion_popover_origin.y) - }); - let opacity = if behind_inline_completion_popover { + let behind_edit_prediction_popover = edit_prediction_popover_origin.as_ref().map_or( + false, + |edit_prediction_popover_origin| { + (pos_y..pos_y + line_height).contains(&edit_prediction_popover_origin.y) + }, + ); + let opacity = if behind_edit_prediction_popover { 0.5 } else { 1.0 @@ -2427,9 +2428,9 @@ impl EditorElement { let mut padding = INLINE_BLAME_PADDING_EM_WIDTHS; - if let Some(inline_completion) = editor.active_inline_completion.as_ref() { - match &inline_completion.completion { - InlineCompletion::Edit { + if let Some(edit_prediction) = editor.active_edit_prediction.as_ref() { + match &edit_prediction.completion { + EditPrediction::Edit { display_mode: EditDisplayMode::TabAccept, .. } => padding += INLINE_ACCEPT_SUGGESTION_EM_WIDTHS, @@ -4086,8 +4087,7 @@ impl EditorElement { { let editor = self.editor.read(cx); - if editor - .edit_prediction_visible_in_cursor_popover(editor.has_active_inline_completion()) + if editor.edit_prediction_visible_in_cursor_popover(editor.has_active_edit_prediction()) { height_above_menu += editor.edit_prediction_cursor_popover_height() + POPOVER_Y_PADDING; @@ -6676,14 +6676,14 @@ impl EditorElement { } } - fn paint_inline_completion_popover( + fn paint_edit_prediction_popover( &mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App, ) { - if let Some(inline_completion_popover) = layout.inline_completion_popover.as_mut() { - inline_completion_popover.paint(window, cx); + if let Some(edit_prediction_popover) = layout.edit_prediction_popover.as_mut() { + edit_prediction_popover.paint(window, cx); } } @@ -8501,7 +8501,7 @@ impl Element for EditorElement { ) }); - let (inline_completion_popover, inline_completion_popover_origin) = self + let (edit_prediction_popover, edit_prediction_popover_origin) = self .editor .update(cx, |editor, cx| { editor.render_edit_prediction_popover( @@ -8530,7 +8530,7 @@ impl Element for EditorElement { &row_block_types, content_origin, scroll_pixel_position, - inline_completion_popover_origin, + edit_prediction_popover_origin, start_row, end_row, line_height, @@ -8919,7 +8919,7 @@ impl Element for EditorElement { cursors, visible_cursors, selections, - inline_completion_popover, + edit_prediction_popover, diff_hunk_controls, mouse_context_menu, test_indicators, @@ -9001,7 +9001,7 @@ impl Element for EditorElement { self.paint_minimap(layout, window, cx); self.paint_scrollbars(layout, window, cx); - self.paint_inline_completion_popover(layout, window, cx); + self.paint_edit_prediction_popover(layout, window, cx); self.paint_mouse_context_menu(layout, window, cx); }); }) @@ -9102,7 +9102,7 @@ pub struct EditorLayout { expand_toggles: Vec)>>, diff_hunk_controls: Vec, crease_trailers: Vec>, - inline_completion_popover: Option, + edit_prediction_popover: Option, mouse_context_menu: Option, tab_invisible: ShapedLine, space_invisible: ShapedLine, diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index b9b7cb2e58..a8850984a1 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -907,12 +907,12 @@ mod tests { let inlays = (0..buffer_snapshot.len()) .flat_map(|offset| { [ - Inlay::inline_completion( + Inlay::edit_prediction( post_inc(&mut id), buffer_snapshot.anchor_at(offset, Bias::Left), "test", ), - Inlay::inline_completion( + Inlay::edit_prediction( post_inc(&mut id), buffer_snapshot.anchor_at(offset, Bias::Right), "test", diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 2b0e13f4be..606f3a3f0e 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -253,8 +253,8 @@ impl LogStore { let copilot_subscription = Copilot::global(cx).map(|copilot| { let copilot = &copilot; - cx.subscribe(copilot, |this, copilot, inline_completion_event, cx| { - if let copilot::Event::CopilotLanguageServerStarted = inline_completion_event { + cx.subscribe(copilot, |this, copilot, edit_prediction_event, cx| { + if let copilot::Event::CopilotLanguageServerStarted = edit_prediction_event { if let Some(server) = copilot.read(cx).language_server() { let server_id = server.server_id(); let weak_this = cx.weak_entity(); diff --git a/crates/migrator/src/migrations/m_2025_01_29/keymap.rs b/crates/migrator/src/migrations/m_2025_01_29/keymap.rs index c32da88229..646af8f63d 100644 --- a/crates/migrator/src/migrations/m_2025_01_29/keymap.rs +++ b/crates/migrator/src/migrations/m_2025_01_29/keymap.rs @@ -242,22 +242,22 @@ static STRING_REPLACE: LazyLock> = LazyLock::new(|| { "inline_completion::ToggleMenu", "edit_prediction::ToggleMenu", ), - ("editor::NextInlineCompletion", "editor::NextEditPrediction"), + ("editor::NextEditPrediction", "editor::NextEditPrediction"), ( - "editor::PreviousInlineCompletion", + "editor::PreviousEditPrediction", "editor::PreviousEditPrediction", ), ( - "editor::AcceptPartialInlineCompletion", + "editor::AcceptPartialEditPrediction", "editor::AcceptPartialEditPrediction", ), - ("editor::ShowInlineCompletion", "editor::ShowEditPrediction"), + ("editor::ShowEditPrediction", "editor::ShowEditPrediction"), ( - "editor::AcceptInlineCompletion", + "editor::AcceptEditPrediction", "editor::AcceptEditPrediction", ), ( - "editor::ToggleInlineCompletions", + "editor::ToggleEditPredictions", "editor::ToggleEditPrediction", ), ]) diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index 2f77b4f3cc..ebec96dd7b 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -1101,7 +1101,7 @@ impl RulesLibrary { inlay_hints_style: editor::make_inlay_hints_style( cx, ), - inline_completion_styles: + edit_prediction_styles: editor::make_suggestion_styles(cx), ..EditorStyle::default() }, diff --git a/crates/supermaven/Cargo.toml b/crates/supermaven/Cargo.toml index d0451f34f2..4fc6a618ff 100644 --- a/crates/supermaven/Cargo.toml +++ b/crates/supermaven/Cargo.toml @@ -16,9 +16,9 @@ doctest = false anyhow.workspace = true client.workspace = true collections.workspace = true +edit_prediction.workspace = true futures.workspace = true gpui.workspace = true -inline_completion.workspace = true language.workspace = true log.workspace = true postage.workspace = true diff --git a/crates/supermaven/src/supermaven_completion_provider.rs b/crates/supermaven/src/supermaven_completion_provider.rs index c49272e66e..2660a03e6f 100644 --- a/crates/supermaven/src/supermaven_completion_provider.rs +++ b/crates/supermaven/src/supermaven_completion_provider.rs @@ -1,8 +1,8 @@ use crate::{Supermaven, SupermavenCompletionStateId}; use anyhow::Result; +use edit_prediction::{Direction, EditPrediction, EditPredictionProvider}; use futures::StreamExt as _; use gpui::{App, Context, Entity, EntityId, Task}; -use inline_completion::{Direction, EditPredictionProvider, InlineCompletion}; use language::{Anchor, Buffer, BufferSnapshot}; use project::Project; use std::{ @@ -44,7 +44,7 @@ fn completion_from_diff( completion_text: &str, position: Anchor, delete_range: Range, -) -> InlineCompletion { +) -> EditPrediction { let buffer_text = snapshot .text_for_range(delete_range.clone()) .collect::(); @@ -91,7 +91,7 @@ fn completion_from_diff( edits.push((edit_range, edit_text)); } - InlineCompletion { + EditPrediction { id: None, edits, edit_preview: None, @@ -182,7 +182,7 @@ impl EditPredictionProvider for SupermavenCompletionProvider { buffer: &Entity, cursor_position: Anchor, cx: &mut Context, - ) -> Option { + ) -> Option { let completion_text = self .supermaven .read(cx) diff --git a/crates/telemetry_events/src/telemetry_events.rs b/crates/telemetry_events/src/telemetry_events.rs index dfe167fcd4..735a1310ae 100644 --- a/crates/telemetry_events/src/telemetry_events.rs +++ b/crates/telemetry_events/src/telemetry_events.rs @@ -94,8 +94,8 @@ impl Display for AssistantPhase { pub enum Event { Flexible(FlexibleEvent), Editor(EditorEvent), - InlineCompletion(InlineCompletionEvent), - InlineCompletionRating(InlineCompletionRatingEvent), + EditPrediction(EditPredictionEvent), + EditPredictionRating(EditPredictionRatingEvent), Call(CallEvent), Assistant(AssistantEventData), Cpu(CpuEvent), @@ -132,7 +132,7 @@ pub struct EditorEvent { } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct InlineCompletionEvent { +pub struct EditPredictionEvent { /// Provider of the completion suggestion (e.g. copilot, supermaven) pub provider: String, pub suggestion_accepted: bool, @@ -140,14 +140,14 @@ pub struct InlineCompletionEvent { } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub enum InlineCompletionRating { +pub enum EditPredictionRating { Positive, Negative, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct InlineCompletionRatingEvent { - pub rating: InlineCompletionRating, +pub struct EditPredictionRatingEvent { + pub rating: EditPredictionRating, pub input_events: Arc, pub input_excerpt: Arc, pub output_excerpt: Arc, diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index c22cf0ef00..0e487f4410 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -3808,7 +3808,7 @@ mod test { cx.update_editor(|editor, _window, cx| { let range = editor.selections.newest_anchor().range(); let inlay_text = " field: int,\n field2: string\n field3: float"; - let inlay = Inlay::inline_completion(1, range.start, inlay_text); + let inlay = Inlay::edit_prediction(1, range.start, inlay_text); editor.splice_inlays(&[], vec![inlay], cx); }); @@ -3840,7 +3840,7 @@ mod test { let end_of_line = snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0)))); let inlay_text = " hint"; - let inlay = Inlay::inline_completion(1, end_of_line, inlay_text); + let inlay = Inlay::edit_prediction(1, end_of_line, inlay_text); editor.splice_inlays(&[], vec![inlay], cx); }); cx.simulate_keystrokes("$"); diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 135cdd687f..c1bc7a70ae 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -90,7 +90,7 @@ impl Vim { if let Some(kind) = motion_kind { vim.copy_selections_content(editor, kind, window, cx); editor.insert("", window, cx); - editor.refresh_inline_completion(true, false, window, cx); + editor.refresh_edit_prediction(true, false, window, cx); } }); }); @@ -123,7 +123,7 @@ impl Vim { if objects_found { vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx); editor.insert("", window, cx); - editor.refresh_inline_completion(true, false, window, cx); + editor.refresh_edit_prediction(true, false, window, cx); } }); }); diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index ccbb3dd0fd..2cf40292cf 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -82,7 +82,7 @@ impl Vim { selection.collapse_to(cursor, selection.goal) }); }); - editor.refresh_inline_completion(true, false, window, cx); + editor.refresh_edit_prediction(true, false, window, cx); }); }); } @@ -169,7 +169,7 @@ impl Vim { selection.collapse_to(cursor, selection.goal) }); }); - editor.refresh_inline_completion(true, false, window, cx); + editor.refresh_edit_prediction(true, false, window, cx); }); }); } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 2f759ec8af..72edbe77ed 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1741,11 +1741,11 @@ impl Vim { editor.set_autoindent(vim.should_autoindent()); editor.selections.line_mode = matches!(vim.mode, Mode::VisualLine); - let hide_inline_completions = match vim.mode { + let hide_edit_predictions = match vim.mode { Mode::Insert | Mode::Replace => false, _ => true, }; - editor.set_inline_completions_hidden_for_vim_mode(hide_inline_completions, window, cx); + editor.set_edit_predictions_hidden_for_vim_mode(hide_edit_predictions, window, cx); }); cx.notify() } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 536af7b7b9..bdd8db9027 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -76,7 +76,7 @@ gpui_tokio.workspace = true http_client.workspace = true image_viewer.workspace = true indoc.workspace = true -inline_completion_button.workspace = true +edit_prediction_button.workspace = true inspector_ui.workspace = true install_cli.workspace = true jj_ui.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index c264135e5c..825aea615f 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -51,9 +51,9 @@ use workspace::{ }; use zed::{ OpenListener, OpenRequest, RawOpenRequest, app_menus, build_window_options, - derive_paths_with_position, handle_cli_connection, handle_keymap_file_changes, - handle_settings_changed, handle_settings_file_changes, initialize_workspace, - inline_completion_registry, open_paths_with_positions, + derive_paths_with_position, edit_prediction_registry, handle_cli_connection, + handle_keymap_file_changes, handle_settings_changed, handle_settings_file_changes, + initialize_workspace, open_paths_with_positions, }; use crate::zed::OpenRequestKind; @@ -559,11 +559,7 @@ pub fn main() { web_search::init(cx); web_search_providers::init(app_state.client.clone(), cx); snippet_provider::init(cx); - inline_completion_registry::init( - app_state.client.clone(), - app_state.user_store.clone(), - cx, - ); + edit_prediction_registry::init(app_state.client.clone(), app_state.user_store.clone(), cx); let prompt_builder = PromptBuilder::load(app_state.fs.clone(), stdout_is_a_pty(), cx); agent_ui::init( app_state.fs.clone(), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index af317edeee..ec62ed33fd 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,6 +1,6 @@ mod app_menus; pub mod component_preview; -pub mod inline_completion_registry; +pub mod edit_prediction_registry; #[cfg(target_os = "macos")] pub(crate) mod mac_only_instance; mod migrate; @@ -332,18 +332,18 @@ pub fn initialize_workspace( show_software_emulation_warning_if_needed(specs, window, cx); } - let inline_completion_menu_handle = PopoverMenuHandle::default(); + let edit_prediction_menu_handle = PopoverMenuHandle::default(); let edit_prediction_button = cx.new(|cx| { - inline_completion_button::InlineCompletionButton::new( + edit_prediction_button::EditPredictionButton::new( app_state.fs.clone(), app_state.user_store.clone(), - inline_completion_menu_handle.clone(), + edit_prediction_menu_handle.clone(), cx, ) }); workspace.register_action({ - move |_, _: &inline_completion_button::ToggleMenu, window, cx| { - inline_completion_menu_handle.toggle(window, cx); + move |_, _: &edit_prediction_button::ToggleMenu, window, cx| { + edit_prediction_menu_handle.toggle(window, cx); } }); diff --git a/crates/zed/src/zed/inline_completion_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs similarity index 96% rename from crates/zed/src/zed/inline_completion_registry.rs rename to crates/zed/src/zed/edit_prediction_registry.rs index bbecd26417..b9f561c0e7 100644 --- a/crates/zed/src/zed/inline_completion_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -11,7 +11,7 @@ use supermaven::{Supermaven, SupermavenCompletionProvider}; use ui::Window; use util::ResultExt; use workspace::Workspace; -use zeta::{ProviderDataCollection, ZetaInlineCompletionProvider}; +use zeta::{ProviderDataCollection, ZetaEditPredictionProvider}; pub fn init(client: Arc, user_store: Entity, cx: &mut App) { let editors: Rc, AnyWindowHandle>>> = Rc::default(); @@ -171,7 +171,7 @@ fn register_backward_compatible_actions(editor: &mut Editor, cx: &mut Context| { - editor.show_inline_completion(&Default::default(), window, cx); + editor.show_edit_prediction(&Default::default(), window, cx); }, )) .detach(); @@ -207,7 +207,7 @@ fn assign_edit_prediction_provider( match provider { EditPredictionProvider::None => { - editor.set_edit_prediction_provider::(None, window, cx); + editor.set_edit_prediction_provider::(None, window, cx); } EditPredictionProvider::Copilot => { if let Some(copilot) = Copilot::global(cx) { @@ -265,7 +265,7 @@ fn assign_edit_prediction_provider( ProviderDataCollection::new(zeta.clone(), singleton_buffer, cx); let provider = - cx.new(|_| zeta::ZetaInlineCompletionProvider::new(zeta, data_collection)); + cx.new(|_| zeta::ZetaEditPredictionProvider::new(zeta, data_collection)); editor.set_edit_prediction_provider(Some(provider), window, cx); } diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 7ab7293573..e76bef59a3 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -381,7 +381,7 @@ impl Render for QuickActionBar { } if has_edit_prediction_provider { - let mut inline_completion_entry = ContextMenuEntry::new("Edit Predictions") + let mut edit_prediction_entry = ContextMenuEntry::new("Edit Predictions") .toggleable(IconPosition::Start, edit_predictions_enabled_at_cursor && show_edit_predictions) .disabled(!edit_predictions_enabled_at_cursor) .action( @@ -401,12 +401,12 @@ impl Render for QuickActionBar { } }); if !edit_predictions_enabled_at_cursor { - inline_completion_entry = inline_completion_entry.documentation_aside(DocumentationSide::Left, |_| { + edit_prediction_entry = edit_prediction_entry.documentation_aside(DocumentationSide::Left, |_| { Label::new("You can't toggle edit predictions for this file as it is within the excluded files list.").into_any_element() }); } - menu = menu.item(inline_completion_entry); + menu = menu.item(edit_prediction_entry); } menu = menu.separator(); diff --git a/crates/zeta/Cargo.toml b/crates/zeta/Cargo.toml index 26eeda3f22..9f1d02b790 100644 --- a/crates/zeta/Cargo.toml +++ b/crates/zeta/Cargo.toml @@ -33,7 +33,7 @@ futures.workspace = true gpui.workspace = true http_client.workspace = true indoc.workspace = true -inline_completion.workspace = true +edit_prediction.workspace = true language.workspace = true language_model.workspace = true log.workspace = true diff --git a/crates/zeta/src/completion_diff_element.rs b/crates/zeta/src/completion_diff_element.rs index 3b7355d797..73c3cb20cd 100644 --- a/crates/zeta/src/completion_diff_element.rs +++ b/crates/zeta/src/completion_diff_element.rs @@ -1,6 +1,6 @@ use std::cmp; -use crate::InlineCompletion; +use crate::EditPrediction; use gpui::{ AnyElement, App, BorderStyle, Bounds, Corners, Edges, HighlightStyle, Hsla, StyledText, TextLayout, TextStyle, point, prelude::*, quad, size, @@ -17,7 +17,7 @@ pub struct CompletionDiffElement { } impl CompletionDiffElement { - pub fn new(completion: &InlineCompletion, cx: &App) -> Self { + pub fn new(completion: &EditPrediction, cx: &App) -> Self { let mut diff = completion .snapshot .text_for_range(completion.excerpt_range.clone()) diff --git a/crates/zeta/src/rate_completion_modal.rs b/crates/zeta/src/rate_completion_modal.rs index 5a873fb8de..ac7fcade91 100644 --- a/crates/zeta/src/rate_completion_modal.rs +++ b/crates/zeta/src/rate_completion_modal.rs @@ -1,4 +1,4 @@ -use crate::{CompletionDiffElement, InlineCompletion, InlineCompletionRating, Zeta}; +use crate::{CompletionDiffElement, EditPrediction, EditPredictionRating, Zeta}; use editor::Editor; use gpui::{App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, actions, prelude::*}; use language::language_settings; @@ -34,7 +34,7 @@ pub struct RateCompletionModal { } struct ActiveCompletion { - completion: InlineCompletion, + completion: EditPrediction, feedback_editor: Entity, } @@ -157,7 +157,7 @@ impl RateCompletionModal { if let Some(active) = &self.active_completion { zeta.rate_completion( &active.completion, - InlineCompletionRating::Positive, + EditPredictionRating::Positive, active.feedback_editor.read(cx).text(cx), cx, ); @@ -189,7 +189,7 @@ impl RateCompletionModal { self.zeta.update(cx, |zeta, cx| { zeta.rate_completion( &active.completion, - InlineCompletionRating::Negative, + EditPredictionRating::Negative, active.feedback_editor.read(cx).text(cx), cx, ); @@ -250,7 +250,7 @@ impl RateCompletionModal { pub fn select_completion( &mut self, - completion: Option, + completion: Option, focus: bool, window: &mut Window, cx: &mut Context, diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index f130c3a965..1cd8e8d17f 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -8,8 +8,8 @@ mod rate_completion_modal; pub(crate) use completion_diff_element::*; use db::kvp::{Dismissable, KEY_VALUE_STORE}; +use edit_prediction::DataCollectionState; pub use init::*; -use inline_completion::DataCollectionState; use license_detection::LICENSE_FILES_TO_CHECK; pub use license_detection::is_license_eligible_for_data_collection; pub use rate_completion_modal::*; @@ -50,7 +50,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use telemetry_events::InlineCompletionRating; +use telemetry_events::EditPredictionRating; use thiserror::Error; use util::ResultExt; use uuid::Uuid; @@ -81,15 +81,15 @@ actions!( ); #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)] -pub struct InlineCompletionId(Uuid); +pub struct EditPredictionId(Uuid); -impl From for gpui::ElementId { - fn from(value: InlineCompletionId) -> Self { +impl From for gpui::ElementId { + fn from(value: EditPredictionId) -> Self { gpui::ElementId::Uuid(value.0) } } -impl std::fmt::Display for InlineCompletionId { +impl std::fmt::Display for EditPredictionId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } @@ -134,8 +134,8 @@ struct ZetaGlobal(Entity); impl Global for ZetaGlobal {} #[derive(Clone)] -pub struct InlineCompletion { - id: InlineCompletionId, +pub struct EditPrediction { + id: EditPredictionId, path: Arc, excerpt_range: Range, cursor_offset: usize, @@ -150,7 +150,7 @@ pub struct InlineCompletion { response_received_at: Instant, } -impl InlineCompletion { +impl EditPrediction { fn latency(&self) -> Duration { self.response_received_at .duration_since(self.buffer_snapshotted_at) @@ -207,9 +207,9 @@ fn interpolate( if edits.is_empty() { None } else { Some(edits) } } -impl std::fmt::Debug for InlineCompletion { +impl std::fmt::Debug for EditPrediction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("InlineCompletion") + f.debug_struct("EditPrediction") .field("id", &self.id) .field("path", &self.path) .field("edits", &self.edits) @@ -222,8 +222,8 @@ pub struct Zeta { client: Arc, events: VecDeque, registered_buffers: HashMap, - shown_completions: VecDeque, - rated_completions: HashSet, + shown_completions: VecDeque, + rated_completions: HashSet, data_collection_choice: Entity, llm_token: LlmApiToken, _llm_token_subscription: Subscription, @@ -384,7 +384,7 @@ impl Zeta { can_collect_data: bool, cx: &mut Context, perform_predict_edits: F, - ) -> Task>> + ) -> Task>> where F: FnOnce(PerformPredictEditsParams) -> R + 'static, R: Future)>> @@ -664,7 +664,7 @@ and then another position: language::Anchor, response: PredictEditsResponse, cx: &mut Context, - ) -> Task>> { + ) -> Task>> { use std::future::ready; self.request_completion_impl(None, project, buffer, position, false, cx, |_params| { @@ -679,7 +679,7 @@ and then another position: language::Anchor, can_collect_data: bool, cx: &mut Context, - ) -> Task>> { + ) -> Task>> { let workspace = self .workspace .as_ref() @@ -773,7 +773,7 @@ and then another fn accept_edit_prediction( &mut self, - request_id: InlineCompletionId, + request_id: EditPredictionId, cx: &mut Context, ) -> Task> { let client = self.client.clone(); @@ -852,7 +852,7 @@ and then another input_excerpt: String, buffer_snapshotted_at: Instant, cx: &AsyncApp, - ) -> Task>> { + ) -> Task>> { let snapshot = snapshot.clone(); let request_id = prediction_response.request_id; let output_excerpt = prediction_response.output_excerpt; @@ -884,8 +884,8 @@ and then another let edit_preview = edit_preview.await; - Ok(Some(InlineCompletion { - id: InlineCompletionId(request_id), + Ok(Some(EditPrediction { + id: EditPredictionId(request_id), path, excerpt_range: editable_range, cursor_offset, @@ -995,11 +995,11 @@ and then another .collect() } - pub fn is_completion_rated(&self, completion_id: InlineCompletionId) -> bool { + pub fn is_completion_rated(&self, completion_id: EditPredictionId) -> bool { self.rated_completions.contains(&completion_id) } - pub fn completion_shown(&mut self, completion: &InlineCompletion, cx: &mut Context) { + pub fn completion_shown(&mut self, completion: &EditPrediction, cx: &mut Context) { self.shown_completions.push_front(completion.clone()); if self.shown_completions.len() > 50 { let completion = self.shown_completions.pop_back().unwrap(); @@ -1010,8 +1010,8 @@ and then another pub fn rate_completion( &mut self, - completion: &InlineCompletion, - rating: InlineCompletionRating, + completion: &EditPrediction, + rating: EditPredictionRating, feedback: String, cx: &mut Context, ) { @@ -1029,7 +1029,7 @@ and then another cx.notify(); } - pub fn shown_completions(&self) -> impl DoubleEndedIterator { + pub fn shown_completions(&self) -> impl DoubleEndedIterator { self.shown_completions.iter() } @@ -1323,12 +1323,12 @@ impl Event { } #[derive(Debug, Clone)] -struct CurrentInlineCompletion { +struct CurrentEditPrediction { buffer_id: EntityId, - completion: InlineCompletion, + completion: EditPrediction, } -impl CurrentInlineCompletion { +impl CurrentEditPrediction { fn should_replace_completion(&self, old_completion: &Self, snapshot: &BufferSnapshot) -> bool { if self.buffer_id != old_completion.buffer_id { return true; @@ -1497,17 +1497,17 @@ async fn llm_token_retry( } } -pub struct ZetaInlineCompletionProvider { +pub struct ZetaEditPredictionProvider { zeta: Entity, pending_completions: ArrayVec, next_pending_completion_id: usize, - current_completion: Option, + current_completion: Option, /// None if this is entirely disabled for this provider provider_data_collection: ProviderDataCollection, last_request_timestamp: Instant, } -impl ZetaInlineCompletionProvider { +impl ZetaEditPredictionProvider { pub const THROTTLE_TIMEOUT: Duration = Duration::from_millis(300); pub fn new(zeta: Entity, provider_data_collection: ProviderDataCollection) -> Self { @@ -1522,7 +1522,7 @@ impl ZetaInlineCompletionProvider { } } -impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider { +impl edit_prediction::EditPredictionProvider for ZetaEditPredictionProvider { fn name() -> &'static str { "zed-predict" } @@ -1650,7 +1650,7 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider Ok(completion_request) => { let completion_request = completion_request.await; completion_request.map(|c| { - c.map(|completion| CurrentInlineCompletion { + c.map(|completion| CurrentEditPrediction { buffer_id: buffer.entity_id(), completion, }) @@ -1723,7 +1723,7 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider &mut self, _buffer: Entity, _cursor_position: language::Anchor, - _direction: inline_completion::Direction, + _direction: edit_prediction::Direction, _cx: &mut Context, ) { // Right now we don't support cycling. @@ -1754,8 +1754,8 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider buffer: &Entity, cursor_position: language::Anchor, cx: &mut Context, - ) -> Option { - let CurrentInlineCompletion { + ) -> Option { + let CurrentEditPrediction { buffer_id, completion, .. @@ -1803,7 +1803,7 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider } } - Some(inline_completion::InlineCompletion { + Some(edit_prediction::EditPrediction { id: Some(completion.id.to_string().into()), edits: edits[edit_start_ix..edit_end_ix].to_vec(), edit_preview: Some(completion.edit_preview.clone()), @@ -1833,7 +1833,7 @@ mod tests { use super::*; #[gpui::test] - async fn test_inline_completion_basic_interpolation(cx: &mut TestAppContext) { + async fn test_edit_prediction_basic_interpolation(cx: &mut TestAppContext) { let buffer = cx.new(|cx| Buffer::local("Lorem ipsum dolor", cx)); let edits: Arc<[(Range, String)]> = cx.update(|cx| { to_completion_edits( @@ -1848,12 +1848,12 @@ mod tests { .read(|cx| buffer.read(cx).preview_edits(edits.clone(), cx)) .await; - let completion = InlineCompletion { + let completion = EditPrediction { edits, edit_preview, path: Path::new("").into(), snapshot: cx.read(|cx| buffer.read(cx).snapshot()), - id: InlineCompletionId(Uuid::new_v4()), + id: EditPredictionId(Uuid::new_v4()), excerpt_range: 0..0, cursor_offset: 0, input_outline: "".into(), @@ -2014,7 +2014,7 @@ mod tests { } #[gpui::test] - async fn test_inline_completion_end_of_buffer(cx: &mut TestAppContext) { + async fn test_edit_prediction_end_of_buffer(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); diff --git a/docs/src/ai/llm-providers.md b/docs/src/ai/llm-providers.md index a6e6f7c774..04646213e6 100644 --- a/docs/src/ai/llm-providers.md +++ b/docs/src/ai/llm-providers.md @@ -220,7 +220,7 @@ Alternatively, you can provide an OAuth token via the `GH_COPILOT_TOKEN` environ > **Note**: If you don't see specific models in the dropdown, you may need to enable them in your [GitHub Copilot settings](https://github.com/settings/copilot/features). -To use Copilot Enterprise with Zed (for both agent and inline completions), you must configure your enterprise endpoint as described in [Configuring GitHub Copilot Enterprise](./edit-prediction.md#github-copilot-enterprise). +To use Copilot Enterprise with Zed (for both agent and completions), you must configure your enterprise endpoint as described in [Configuring GitHub Copilot Enterprise](./edit-prediction.md#github-copilot-enterprise). ### Google AI {#google-ai} diff --git a/docs/src/key-bindings.md b/docs/src/key-bindings.md index 9984f234ad..feed912787 100644 --- a/docs/src/key-bindings.md +++ b/docs/src/key-bindings.md @@ -93,7 +93,7 @@ For example: # in an editor, it might look like this: Workspace os=macos keyboard_layout=com.apple.keylayout.QWERTY Pane - Editor mode=full extension=md inline_completion vim_mode=insert + Editor mode=full extension=md vim_mode=insert # in the project panel Workspace os=macos From bf361c316d43469a568d5a621eccf5de9b42b906 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 4 Aug 2025 18:25:42 +0200 Subject: [PATCH 051/693] search: Update results multi-buffer before search is finished (#35470) I'm not sure when we've lost that notify, but it's causing the time to first search result equal to the time to run the whole search, which is not great. Co-authored-by: Remco This discussion has originally started in #35444 Release Notes: - Improved project search speed. Co-authored-by: Remco --- crates/search/src/project_search.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 3b9700c5f1..15c1099aec 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -355,8 +355,9 @@ impl ProjectSearch { while let Some(new_ranges) = new_ranges.next().await { project_search - .update(cx, |project_search, _| { + .update(cx, |project_search, cx| { project_search.match_ranges.extend(new_ranges); + cx.notify(); }) .ok()?; } From 2c8f144e6b26d029e64821dfcabc3a6c060329a1 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Mon, 4 Aug 2025 23:08:22 +0530 Subject: [PATCH 052/693] workspace: Fix not able to close tab when buffer save fails (#35589) Closes #26216, Closes #35517 Now we prompt user if buffer save failed, asking them to close without saving or cancel the action. Release Notes: - Fixed issue where closing read-only or deleted buffer would not close that tab. Co-authored-by: Lukas Wirth --- crates/workspace/src/pane.rs | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index ad1c74a040..2062255f4b 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1664,10 +1664,33 @@ impl Pane { } if should_save { - if !Self::save_item(project.clone(), &pane, &*item_to_close, save_intent, cx) - .await? + match Self::save_item(project.clone(), &pane, &*item_to_close, save_intent, cx) + .await { - break; + Ok(success) => { + if !success { + break; + } + } + Err(err) => { + let answer = pane.update_in(cx, |_, window, cx| { + let detail = Self::file_names_for_prompt( + &mut [&item_to_close].into_iter(), + cx, + ); + window.prompt( + PromptLevel::Warning, + &format!("Unable to save file: {}", &err), + Some(&detail), + &["Close Without Saving", "Cancel"], + cx, + ) + })?; + match answer.await { + Ok(0) => {} + Ok(1..) | Err(_) => break, + } + } } } From fa8dd1c54740dbab38e2b4488d51c89d7ad6bb03 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:01:32 -0300 Subject: [PATCH 053/693] agent: Adjust full screen menu item label and background color (#35592) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 1 + crates/agent_ui/src/agent_panel.rs | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 24d8b73396..9ea9209189 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2453,6 +2453,7 @@ impl Render for AcpThreadView { .on_action(cx.listener(Self::previous_history_message)) .on_action(cx.listener(Self::next_history_message)) .on_action(cx.listener(Self::open_agent_diff)) + .bg(cx.theme().colors().panel_background) .child(match &self.thread_state { ThreadState::Unauthenticated { connection } => v_flex() .p_2() diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 4751eff15e..5f3315f69a 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1880,10 +1880,10 @@ impl AgentPanel { }), ); - let zoom_in_label = if self.is_zoomed(window, cx) { - "Zoom Out" + let full_screen_label = if self.is_zoomed(window, cx) { + "Disable Full Screen" } else { - "Zoom In" + "Enable Full Screen" }; let active_thread = match &self.active_view { @@ -2071,7 +2071,8 @@ impl AgentPanel { menu = menu .action("Rules…", Box::new(OpenRulesLibrary::default())) .action("Settings", Box::new(OpenSettings)) - .action(zoom_in_label, Box::new(ToggleZoom)); + .separator() + .action(full_screen_label, Box::new(ToggleZoom)); menu })) } From 9fa634f02faeec4ac0c428e55ba01755472b98f5 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:08:00 -0400 Subject: [PATCH 054/693] git: Add option to branch from default branch in branch picker (#34663) Closes #33700 The option shows up as an icon that appears on entries that would create a new branch. You can also branch from the default by secondary confirming, which the icon has a tooltip for as well. We based the default branch on the results from this command: `git symbolic-ref refs/remotes/upstream/HEAD` and fallback to `git symbolic-ref refs/remotes/origin/HEAD` Release Notes: - Add option to create a branch from a default branch in git branch picker --------- Co-authored-by: Cole Miller --- crates/fs/src/fake_git_repo.rs | 6 ++- crates/git/src/repository.rs | 33 ++++++++++++++++ crates/git_ui/src/branch_picker.rs | 60 ++++++++++++++++++++++++++++-- crates/project/src/git_store.rs | 19 ++++++++++ crates/proto/proto/git.proto | 9 +++++ crates/proto/proto/zed.proto | 5 ++- crates/proto/src/proto.rs | 10 +++-- 7 files changed, 133 insertions(+), 9 deletions(-) diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 378a8fb7df..04ba656232 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -10,7 +10,7 @@ use git::{ }, status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus}, }; -use gpui::{AsyncApp, BackgroundExecutor}; +use gpui::{AsyncApp, BackgroundExecutor, SharedString}; use ignore::gitignore::GitignoreBuilder; use rope::Rope; use smol::future::FutureExt as _; @@ -491,4 +491,8 @@ impl GitRepository for FakeGitRepository { ) -> BoxFuture<'_, Result> { unimplemented!() } + + fn default_branch(&self) -> BoxFuture<'_, Result>> { + unimplemented!() + } } diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index a63315e69e..b536bed710 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -463,6 +463,8 @@ pub trait GitRepository: Send + Sync { base_checkpoint: GitRepositoryCheckpoint, target_checkpoint: GitRepositoryCheckpoint, ) -> BoxFuture<'_, Result>; + + fn default_branch(&self) -> BoxFuture<'_, Result>>; } pub enum DiffType { @@ -1607,6 +1609,37 @@ impl GitRepository for RealGitRepository { }) .boxed() } + + fn default_branch(&self) -> BoxFuture<'_, Result>> { + let working_directory = self.working_directory(); + let git_binary_path = self.git_binary_path.clone(); + + let executor = self.executor.clone(); + self.executor + .spawn(async move { + let working_directory = working_directory?; + let git = GitBinary::new(git_binary_path, working_directory, executor); + + if let Ok(output) = git + .run(&["symbolic-ref", "refs/remotes/upstream/HEAD"]) + .await + { + let output = output + .strip_prefix("refs/remotes/upstream/") + .map(|s| SharedString::from(s.to_owned())); + return Ok(output); + } + + let output = git + .run(&["symbolic-ref", "refs/remotes/origin/HEAD"]) + .await?; + + Ok(output + .strip_prefix("refs/remotes/origin/") + .map(|s| SharedString::from(s.to_owned()))) + }) + .boxed() + } } fn git_status_args(path_prefixes: &[RepoPath]) -> Vec { diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index 9eac3ce5af..1092ba33d1 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -13,7 +13,7 @@ use project::git_store::Repository; use std::sync::Arc; use time::OffsetDateTime; use time_format::format_local_timestamp; -use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*}; +use ui::{HighlightedLabel, ListItem, ListItemSpacing, Tooltip, prelude::*}; use util::ResultExt; use workspace::notifications::DetachAndPromptErr; use workspace::{ModalView, Workspace}; @@ -90,11 +90,21 @@ impl BranchList { let all_branches_request = repository .clone() .map(|repository| repository.update(cx, |repository, _| repository.branches())); + let default_branch_request = repository + .clone() + .map(|repository| repository.update(cx, |repository, _| repository.default_branch())); cx.spawn_in(window, async move |this, cx| { let mut all_branches = all_branches_request .context("No active repository")? .await??; + let default_branch = default_branch_request + .context("No active repository")? + .await + .map(Result::ok) + .ok() + .flatten() + .flatten(); let all_branches = cx .background_spawn(async move { @@ -124,6 +134,7 @@ impl BranchList { this.update_in(cx, |this, window, cx| { this.picker.update(cx, |picker, cx| { + picker.delegate.default_branch = default_branch; picker.delegate.all_branches = Some(all_branches); picker.refresh(window, cx); }) @@ -192,6 +203,7 @@ struct BranchEntry { pub struct BranchListDelegate { matches: Vec, all_branches: Option>, + default_branch: Option, repo: Option>, style: BranchListStyle, selected_index: usize, @@ -206,6 +218,7 @@ impl BranchListDelegate { repo, style, all_branches: None, + default_branch: None, selected_index: 0, last_query: Default::default(), modifiers: Default::default(), @@ -214,6 +227,7 @@ impl BranchListDelegate { fn create_branch( &self, + from_branch: Option, new_branch_name: SharedString, window: &mut Window, cx: &mut Context>, @@ -223,6 +237,11 @@ impl BranchListDelegate { }; let new_branch_name = new_branch_name.to_string().replace(' ', "-"); cx.spawn(async move |_, cx| { + if let Some(based_branch) = from_branch { + repo.update(cx, |repo, _| repo.change_branch(based_branch.to_string()))? + .await??; + } + repo.update(cx, |repo, _| { repo.create_branch(new_branch_name.to_string()) })? @@ -353,12 +372,22 @@ impl PickerDelegate for BranchListDelegate { }) } - fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { + fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) { let Some(entry) = self.matches.get(self.selected_index()) else { return; }; if entry.is_new { - self.create_branch(entry.branch.name().to_owned().into(), window, cx); + let from_branch = if secondary { + self.default_branch.clone() + } else { + None + }; + self.create_branch( + from_branch, + entry.branch.name().to_owned().into(), + window, + cx, + ); return; } @@ -439,6 +468,28 @@ impl PickerDelegate for BranchListDelegate { }) .unwrap_or_else(|| (None, None)); + let icon = if let Some(default_branch) = self.default_branch.clone() + && entry.is_new + { + Some( + IconButton::new("branch-from-default", IconName::GitBranchSmall) + .on_click(cx.listener(move |this, _, window, cx| { + this.delegate.set_selected_index(ix, window, cx); + this.delegate.confirm(true, window, cx); + })) + .tooltip(move |window, cx| { + Tooltip::for_action( + format!("Create branch based off default: {default_branch}"), + &menu::SecondaryConfirm, + window, + cx, + ) + }), + ) + } else { + None + }; + let branch_name = if entry.is_new { h_flex() .gap_1() @@ -504,7 +555,8 @@ impl PickerDelegate for BranchListDelegate { .color(Color::Muted) })) }), - ), + ) + .end_slot::(icon), ) } diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 28dd0e91e3..c9f0fc7959 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -4025,6 +4025,25 @@ impl Repository { }) } + pub fn default_branch(&mut self) -> oneshot::Receiver>> { + let id = self.id; + self.send_job(None, move |repo, _| async move { + match repo { + RepositoryState::Local { backend, .. } => backend.default_branch().await, + RepositoryState::Remote { project_id, client } => { + let response = client + .request(proto::GetDefaultBranch { + project_id: project_id.0, + repository_id: id.to_proto(), + }) + .await?; + + anyhow::Ok(response.branch.map(SharedString::from)) + } + } + }) + } + pub fn diff(&mut self, diff_type: DiffType, _cx: &App) -> oneshot::Receiver> { let id = self.id; self.send_job(None, move |repo, _cx| async move { diff --git a/crates/proto/proto/git.proto b/crates/proto/proto/git.proto index ea08d36371..c32da9b110 100644 --- a/crates/proto/proto/git.proto +++ b/crates/proto/proto/git.proto @@ -422,3 +422,12 @@ message BlameBufferResponse { reserved 1 to 4; } + +message GetDefaultBranch { + uint64 project_id = 1; + uint64 repository_id = 2; +} + +message GetDefaultBranchResponse { + optional string branch = 1; +} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 29ab2b1e90..d511ea5e8f 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -399,7 +399,10 @@ message Envelope { GetColorPresentationResponse get_color_presentation_response = 356; Stash stash = 357; - StashPop stash_pop = 358; // current max + StashPop stash_pop = 358; + + GetDefaultBranch get_default_branch = 359; + GetDefaultBranchResponse get_default_branch_response = 360; // current max } reserved 87 to 88; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 83e5a77c86..72b3807deb 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -315,7 +315,9 @@ messages!( (LogToDebugConsole, Background), (GetDocumentDiagnostics, Background), (GetDocumentDiagnosticsResponse, Background), - (PullWorkspaceDiagnostics, Background) + (PullWorkspaceDiagnostics, Background), + (GetDefaultBranch, Background), + (GetDefaultBranchResponse, Background), ); request_messages!( @@ -483,7 +485,8 @@ request_messages!( (GetDebugAdapterBinary, DebugAdapterBinary), (RunDebugLocators, DebugRequest), (GetDocumentDiagnostics, GetDocumentDiagnosticsResponse), - (PullWorkspaceDiagnostics, Ack) + (PullWorkspaceDiagnostics, Ack), + (GetDefaultBranch, GetDefaultBranchResponse), ); entity_messages!( @@ -615,7 +618,8 @@ entity_messages!( GetDebugAdapterBinary, LogToDebugConsole, GetDocumentDiagnostics, - PullWorkspaceDiagnostics + PullWorkspaceDiagnostics, + GetDefaultBranch ); entity_messages!( From 0ea4016e6613e9102baab34f7e128756aeb6776e Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:16:52 -0300 Subject: [PATCH 055/693] onboarding: Adjust skip button as flow progresses (#35596) Release Notes: - N/A --- crates/onboarding/src/onboarding.rs | 131 +++++++++++++++------------- 1 file changed, 69 insertions(+), 62 deletions(-) diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index f7e76f2f34..a79d1d5aef 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -254,6 +254,40 @@ impl Onboarding { cx.emit(ItemEvent::UpdateTab); } + fn go_to_welcome_page(&self, cx: &mut App) { + with_active_or_new_workspace(cx, |workspace, window, cx| { + let Some((onboarding_id, onboarding_idx)) = workspace + .active_pane() + .read(cx) + .items() + .enumerate() + .find_map(|(idx, item)| { + let _ = item.downcast::()?; + Some((item.item_id(), idx)) + }) + else { + return; + }; + + workspace.active_pane().update(cx, |pane, cx| { + // Get the index here to get around the borrow checker + let idx = pane.items().enumerate().find_map(|(idx, item)| { + let _ = item.downcast::()?; + Some(idx) + }); + + if let Some(idx) = idx { + pane.activate_item(idx, true, true, window, cx); + } else { + let item = Box::new(WelcomePage::new(window, cx)); + pane.add_item(item, true, true, Some(onboarding_idx), window, cx); + } + + pane.remove_item(onboarding_id, false, false, window, cx); + }); + }); + } + fn render_nav_buttons( &mut self, window: &mut Window, @@ -319,6 +353,8 @@ impl Onboarding { } fn render_nav(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let ai_setup_page = matches!(self.selected_page, SelectedPage::AiSetup); + v_flex() .h_full() .w(rems_from_px(220.)) @@ -357,67 +393,38 @@ impl Onboarding { .gap_1() .children(self.render_nav_buttons(window, cx)), ) - .child( - ButtonLike::new("skip_all") - .child(Label::new("Skip All").ml_1()) - .on_click(|_, _, cx| { - with_active_or_new_workspace( - cx, - |workspace, window, cx| { - let Some((onboarding_id, onboarding_idx)) = - workspace - .active_pane() - .read(cx) - .items() - .enumerate() - .find_map(|(idx, item)| { - let _ = - item.downcast::()?; - Some((item.item_id(), idx)) - }) - else { - return; - }; - - workspace.active_pane().update(cx, |pane, cx| { - // Get the index here to get around the borrow checker - let idx = pane.items().enumerate().find_map( - |(idx, item)| { - let _ = - item.downcast::()?; - Some(idx) - }, - ); - - if let Some(idx) = idx { - pane.activate_item( - idx, true, true, window, cx, - ); - } else { - let item = - Box::new(WelcomePage::new(window, cx)); - pane.add_item( - item, - true, - true, - Some(onboarding_idx), - window, - cx, - ); - } - - pane.remove_item( - onboarding_id, - false, - false, - window, - cx, - ); - }); - }, - ); - }), - ), + .map(|this| { + if ai_setup_page { + this.child( + ButtonLike::new("start_building") + .style(ButtonStyle::Outlined) + .size(ButtonSize::Medium) + .child( + h_flex() + .ml_1() + .w_full() + .justify_between() + .child(Label::new("Start Building")) + .child( + Icon::new(IconName::Check) + .size(IconSize::Small), + ), + ) + .on_click(cx.listener(|this, _, _, cx| { + this.go_to_welcome_page(cx); + })), + ) + } else { + this.child( + ButtonLike::new("skip_all") + .size(ButtonSize::Medium) + .child(Label::new("Skip All").ml_1()) + .on_click(cx.listener(|this, _, _, cx| { + this.go_to_welcome_page(cx); + })), + ) + } + }), ), ) .child( @@ -430,8 +437,8 @@ impl Onboarding { .into_any_element() } else { Button::new("sign_in", "Sign In") - .style(ButtonStyle::Outlined) .full_width() + .style(ButtonStyle::Outlined) .on_click(|_, window, cx| { let client = Client::global(cx); window From f3f2dba606229a9c9dd3af728b9ed430fbcf9548 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 4 Aug 2025 11:22:49 -0700 Subject: [PATCH 056/693] Minor stylistic cleanup in Windows platform (#35503) This PR doesn't change any logic, it just cleans up some naming and style in the windows platform layer. * Rename `WindowsWindowStatePtr` to `WindowsWindowInner`, since it isn't a pointer type. * Move window event handler methods into an impl on this type, so that all of the `state_ptr: &Rc` parameters can just be replaced with `&self`. * In window creation, use a `match` instead of a conditional followed by an unwrap There's a lot of whitespace in the diff, so view it with `w=1`. Release Notes: - N/A --- crates/gpui/src/platform/windows/events.rs | 2409 +++++++++--------- crates/gpui/src/platform/windows/platform.rs | 6 +- crates/gpui/src/platform/windows/window.rs | 127 +- 3 files changed, 1233 insertions(+), 1309 deletions(-) diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 61f410a8c6..00b22fa807 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -28,997 +28,863 @@ pub(crate) const WM_GPUI_FORCE_UPDATE_WINDOW: u32 = WM_USER + 5; const SIZE_MOVE_LOOP_TIMER_ID: usize = 1; const AUTO_HIDE_TASKBAR_THICKNESS_PX: i32 = 1; -pub(crate) fn handle_msg( - handle: HWND, - msg: u32, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> LRESULT { - let handled = match msg { - WM_ACTIVATE => handle_activate_msg(wparam, state_ptr), - WM_CREATE => handle_create_msg(handle, state_ptr), - WM_DEVICECHANGE => handle_device_change_msg(handle, wparam, state_ptr), - WM_MOVE => handle_move_msg(handle, lparam, state_ptr), - WM_SIZE => handle_size_msg(wparam, lparam, state_ptr), - WM_GETMINMAXINFO => handle_get_min_max_info_msg(lparam, state_ptr), - WM_ENTERSIZEMOVE | WM_ENTERMENULOOP => handle_size_move_loop(handle), - WM_EXITSIZEMOVE | WM_EXITMENULOOP => handle_size_move_loop_exit(handle), - WM_TIMER => handle_timer_msg(handle, wparam, state_ptr), - WM_NCCALCSIZE => handle_calc_client_size(handle, wparam, lparam, state_ptr), - WM_DPICHANGED => handle_dpi_changed_msg(handle, wparam, lparam, state_ptr), - WM_DISPLAYCHANGE => handle_display_change_msg(handle, state_ptr), - WM_NCHITTEST => handle_hit_test_msg(handle, msg, wparam, lparam, state_ptr), - WM_PAINT => handle_paint_msg(handle, state_ptr), - WM_CLOSE => handle_close_msg(state_ptr), - WM_DESTROY => handle_destroy_msg(handle, state_ptr), - WM_MOUSEMOVE => handle_mouse_move_msg(handle, lparam, wparam, state_ptr), - WM_MOUSELEAVE | WM_NCMOUSELEAVE => handle_mouse_leave_msg(state_ptr), - WM_NCMOUSEMOVE => handle_nc_mouse_move_msg(handle, lparam, state_ptr), - WM_NCLBUTTONDOWN => { - handle_nc_mouse_down_msg(handle, MouseButton::Left, wparam, lparam, state_ptr) +impl WindowsWindowInner { + pub(crate) fn handle_msg( + self: &Rc, + handle: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> LRESULT { + let handled = match msg { + WM_ACTIVATE => self.handle_activate_msg(wparam), + WM_CREATE => self.handle_create_msg(handle), + WM_DEVICECHANGE => self.handle_device_change_msg(handle, wparam), + WM_MOVE => self.handle_move_msg(handle, lparam), + WM_SIZE => self.handle_size_msg(wparam, lparam), + WM_GETMINMAXINFO => self.handle_get_min_max_info_msg(lparam), + WM_ENTERSIZEMOVE | WM_ENTERMENULOOP => self.handle_size_move_loop(handle), + WM_EXITSIZEMOVE | WM_EXITMENULOOP => self.handle_size_move_loop_exit(handle), + WM_TIMER => self.handle_timer_msg(handle, wparam), + WM_NCCALCSIZE => self.handle_calc_client_size(handle, wparam, lparam), + WM_DPICHANGED => self.handle_dpi_changed_msg(handle, wparam, lparam), + WM_DISPLAYCHANGE => self.handle_display_change_msg(handle), + WM_NCHITTEST => self.handle_hit_test_msg(handle, msg, wparam, lparam), + WM_PAINT => self.handle_paint_msg(handle), + WM_CLOSE => self.handle_close_msg(), + WM_DESTROY => self.handle_destroy_msg(handle), + WM_MOUSEMOVE => self.handle_mouse_move_msg(handle, lparam, wparam), + WM_MOUSELEAVE | WM_NCMOUSELEAVE => self.handle_mouse_leave_msg(), + WM_NCMOUSEMOVE => self.handle_nc_mouse_move_msg(handle, lparam), + WM_NCLBUTTONDOWN => { + self.handle_nc_mouse_down_msg(handle, MouseButton::Left, wparam, lparam) + } + WM_NCRBUTTONDOWN => { + self.handle_nc_mouse_down_msg(handle, MouseButton::Right, wparam, lparam) + } + WM_NCMBUTTONDOWN => { + self.handle_nc_mouse_down_msg(handle, MouseButton::Middle, wparam, lparam) + } + WM_NCLBUTTONUP => { + self.handle_nc_mouse_up_msg(handle, MouseButton::Left, wparam, lparam) + } + WM_NCRBUTTONUP => { + self.handle_nc_mouse_up_msg(handle, MouseButton::Right, wparam, lparam) + } + WM_NCMBUTTONUP => { + self.handle_nc_mouse_up_msg(handle, MouseButton::Middle, wparam, lparam) + } + WM_LBUTTONDOWN => self.handle_mouse_down_msg(handle, MouseButton::Left, lparam), + WM_RBUTTONDOWN => self.handle_mouse_down_msg(handle, MouseButton::Right, lparam), + WM_MBUTTONDOWN => self.handle_mouse_down_msg(handle, MouseButton::Middle, lparam), + WM_XBUTTONDOWN => { + self.handle_xbutton_msg(handle, wparam, lparam, Self::handle_mouse_down_msg) + } + WM_LBUTTONUP => self.handle_mouse_up_msg(handle, MouseButton::Left, lparam), + WM_RBUTTONUP => self.handle_mouse_up_msg(handle, MouseButton::Right, lparam), + WM_MBUTTONUP => self.handle_mouse_up_msg(handle, MouseButton::Middle, lparam), + WM_XBUTTONUP => { + self.handle_xbutton_msg(handle, wparam, lparam, Self::handle_mouse_up_msg) + } + WM_MOUSEWHEEL => self.handle_mouse_wheel_msg(handle, wparam, lparam), + WM_MOUSEHWHEEL => self.handle_mouse_horizontal_wheel_msg(handle, wparam, lparam), + WM_SYSKEYDOWN => self.handle_syskeydown_msg(handle, wparam, lparam), + WM_SYSKEYUP => self.handle_syskeyup_msg(handle, wparam, lparam), + WM_SYSCOMMAND => self.handle_system_command(wparam), + WM_KEYDOWN => self.handle_keydown_msg(handle, wparam, lparam), + WM_KEYUP => self.handle_keyup_msg(handle, wparam, lparam), + WM_CHAR => self.handle_char_msg(wparam), + WM_DEADCHAR => self.handle_dead_char_msg(wparam), + WM_IME_STARTCOMPOSITION => self.handle_ime_position(handle), + WM_IME_COMPOSITION => self.handle_ime_composition(handle, lparam), + WM_SETCURSOR => self.handle_set_cursor(handle, lparam), + WM_SETTINGCHANGE => self.handle_system_settings_changed(handle, wparam, lparam), + WM_INPUTLANGCHANGE => self.handle_input_language_changed(lparam), + WM_GPUI_CURSOR_STYLE_CHANGED => self.handle_cursor_changed(lparam), + WM_GPUI_FORCE_UPDATE_WINDOW => self.draw_window(handle, true), + _ => None, + }; + if let Some(n) = handled { + LRESULT(n) + } else { + unsafe { DefWindowProcW(handle, msg, wparam, lparam) } } - WM_NCRBUTTONDOWN => { - handle_nc_mouse_down_msg(handle, MouseButton::Right, wparam, lparam, state_ptr) - } - WM_NCMBUTTONDOWN => { - handle_nc_mouse_down_msg(handle, MouseButton::Middle, wparam, lparam, state_ptr) - } - WM_NCLBUTTONUP => { - handle_nc_mouse_up_msg(handle, MouseButton::Left, wparam, lparam, state_ptr) - } - WM_NCRBUTTONUP => { - handle_nc_mouse_up_msg(handle, MouseButton::Right, wparam, lparam, state_ptr) - } - WM_NCMBUTTONUP => { - handle_nc_mouse_up_msg(handle, MouseButton::Middle, wparam, lparam, state_ptr) - } - WM_LBUTTONDOWN => handle_mouse_down_msg(handle, MouseButton::Left, lparam, state_ptr), - WM_RBUTTONDOWN => handle_mouse_down_msg(handle, MouseButton::Right, lparam, state_ptr), - WM_MBUTTONDOWN => handle_mouse_down_msg(handle, MouseButton::Middle, lparam, state_ptr), - WM_XBUTTONDOWN => { - handle_xbutton_msg(handle, wparam, lparam, handle_mouse_down_msg, state_ptr) - } - WM_LBUTTONUP => handle_mouse_up_msg(handle, MouseButton::Left, lparam, state_ptr), - WM_RBUTTONUP => handle_mouse_up_msg(handle, MouseButton::Right, lparam, state_ptr), - WM_MBUTTONUP => handle_mouse_up_msg(handle, MouseButton::Middle, lparam, state_ptr), - WM_XBUTTONUP => handle_xbutton_msg(handle, wparam, lparam, handle_mouse_up_msg, state_ptr), - WM_MOUSEWHEEL => handle_mouse_wheel_msg(handle, wparam, lparam, state_ptr), - WM_MOUSEHWHEEL => handle_mouse_horizontal_wheel_msg(handle, wparam, lparam, state_ptr), - WM_SYSKEYDOWN => handle_syskeydown_msg(handle, wparam, lparam, state_ptr), - WM_SYSKEYUP => handle_syskeyup_msg(handle, wparam, lparam, state_ptr), - WM_SYSCOMMAND => handle_system_command(wparam, state_ptr), - WM_KEYDOWN => handle_keydown_msg(handle, wparam, lparam, state_ptr), - WM_KEYUP => handle_keyup_msg(handle, wparam, lparam, state_ptr), - WM_CHAR => handle_char_msg(wparam, state_ptr), - WM_DEADCHAR => handle_dead_char_msg(wparam, state_ptr), - WM_IME_STARTCOMPOSITION => handle_ime_position(handle, state_ptr), - WM_IME_COMPOSITION => handle_ime_composition(handle, lparam, state_ptr), - WM_SETCURSOR => handle_set_cursor(handle, lparam, state_ptr), - WM_SETTINGCHANGE => handle_system_settings_changed(handle, wparam, lparam, state_ptr), - WM_INPUTLANGCHANGE => handle_input_language_changed(lparam, state_ptr), - WM_GPUI_CURSOR_STYLE_CHANGED => handle_cursor_changed(lparam, state_ptr), - WM_GPUI_FORCE_UPDATE_WINDOW => draw_window(handle, true, state_ptr), - _ => None, - }; - if let Some(n) = handled { - LRESULT(n) - } else { - unsafe { DefWindowProcW(handle, msg, wparam, lparam) } - } -} - -fn handle_move_msg( - handle: HWND, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - let origin = logical_point( - lparam.signed_loword() as f32, - lparam.signed_hiword() as f32, - lock.scale_factor, - ); - lock.origin = origin; - let size = lock.logical_size; - let center_x = origin.x.0 + size.width.0 / 2.; - let center_y = origin.y.0 + size.height.0 / 2.; - let monitor_bounds = lock.display.bounds(); - if center_x < monitor_bounds.left().0 - || center_x > monitor_bounds.right().0 - || center_y < monitor_bounds.top().0 - || center_y > monitor_bounds.bottom().0 - { - // center of the window may have moved to another monitor - let monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) }; - // minimize the window can trigger this event too, in this case, - // monitor is invalid, we do nothing. - if !monitor.is_invalid() && lock.display.handle != monitor { - // we will get the same monitor if we only have one - lock.display = WindowsDisplay::new_with_handle(monitor); - } - } - if let Some(mut callback) = lock.callbacks.moved.take() { - drop(lock); - callback(); - state_ptr.state.borrow_mut().callbacks.moved = Some(callback); - } - Some(0) -} - -fn handle_get_min_max_info_msg( - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let lock = state_ptr.state.borrow(); - let min_size = lock.min_size?; - let scale_factor = lock.scale_factor; - let boarder_offset = lock.border_offset; - drop(lock); - unsafe { - let minmax_info = &mut *(lparam.0 as *mut MINMAXINFO); - minmax_info.ptMinTrackSize.x = - min_size.width.scale(scale_factor).0 as i32 + boarder_offset.width_offset; - minmax_info.ptMinTrackSize.y = - min_size.height.scale(scale_factor).0 as i32 + boarder_offset.height_offset; - } - Some(0) -} - -fn handle_size_msg( - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - - // Don't resize the renderer when the window is minimized, but record that it was minimized so - // that on restore the swap chain can be recreated via `update_drawable_size_even_if_unchanged`. - if wparam.0 == SIZE_MINIMIZED as usize { - lock.restore_from_minimized = lock.callbacks.request_frame.take(); - return Some(0); } - let width = lparam.loword().max(1) as i32; - let height = lparam.hiword().max(1) as i32; - let new_size = size(DevicePixels(width), DevicePixels(height)); - let scale_factor = lock.scale_factor; - if lock.restore_from_minimized.is_some() { - lock.callbacks.request_frame = lock.restore_from_minimized.take(); - } else { - lock.renderer.resize(new_size).log_err(); - } - let new_size = new_size.to_pixels(scale_factor); - lock.logical_size = new_size; - if let Some(mut callback) = lock.callbacks.resize.take() { - drop(lock); - callback(new_size, scale_factor); - state_ptr.state.borrow_mut().callbacks.resize = Some(callback); - } - Some(0) -} - -fn handle_size_move_loop(handle: HWND) -> Option { - unsafe { - let ret = SetTimer( - Some(handle), - SIZE_MOVE_LOOP_TIMER_ID, - USER_TIMER_MINIMUM, - None, + fn handle_move_msg(&self, handle: HWND, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + let origin = logical_point( + lparam.signed_loword() as f32, + lparam.signed_hiword() as f32, + lock.scale_factor, ); - if ret == 0 { - log::error!( - "unable to create timer: {}", - std::io::Error::last_os_error() - ); - } - } - None -} - -fn handle_size_move_loop_exit(handle: HWND) -> Option { - unsafe { - KillTimer(Some(handle), SIZE_MOVE_LOOP_TIMER_ID).log_err(); - } - None -} - -fn handle_timer_msg( - handle: HWND, - wparam: WPARAM, - state_ptr: Rc, -) -> Option { - if wparam.0 == SIZE_MOVE_LOOP_TIMER_ID { - for runnable in state_ptr.main_receiver.drain() { - runnable.run(); - } - handle_paint_msg(handle, state_ptr) - } else { - None - } -} - -fn handle_paint_msg(handle: HWND, state_ptr: Rc) -> Option { - draw_window(handle, false, state_ptr) -} - -fn handle_close_msg(state_ptr: Rc) -> Option { - let mut callback = state_ptr.state.borrow_mut().callbacks.should_close.take()?; - let should_close = callback(); - state_ptr.state.borrow_mut().callbacks.should_close = Some(callback); - if should_close { None } else { Some(0) } -} - -fn handle_destroy_msg(handle: HWND, state_ptr: Rc) -> Option { - let callback = { - let mut lock = state_ptr.state.borrow_mut(); - lock.callbacks.close.take() - }; - if let Some(callback) = callback { - callback(); - } - unsafe { - PostThreadMessageW( - state_ptr.main_thread_id_win32, - WM_GPUI_CLOSE_ONE_WINDOW, - WPARAM(state_ptr.validation_number), - LPARAM(handle.0 as isize), - ) - .log_err(); - } - Some(0) -} - -fn handle_mouse_move_msg( - handle: HWND, - lparam: LPARAM, - wparam: WPARAM, - state_ptr: Rc, -) -> Option { - start_tracking_mouse(handle, &state_ptr, TME_LEAVE); - - let mut lock = state_ptr.state.borrow_mut(); - let Some(mut func) = lock.callbacks.input.take() else { - return Some(1); - }; - let scale_factor = lock.scale_factor; - drop(lock); - - let pressed_button = match MODIFIERKEYS_FLAGS(wparam.loword() as u32) { - flags if flags.contains(MK_LBUTTON) => Some(MouseButton::Left), - flags if flags.contains(MK_RBUTTON) => Some(MouseButton::Right), - flags if flags.contains(MK_MBUTTON) => Some(MouseButton::Middle), - flags if flags.contains(MK_XBUTTON1) => { - Some(MouseButton::Navigate(NavigationDirection::Back)) - } - flags if flags.contains(MK_XBUTTON2) => { - Some(MouseButton::Navigate(NavigationDirection::Forward)) - } - _ => None, - }; - let x = lparam.signed_loword() as f32; - let y = lparam.signed_hiword() as f32; - let input = PlatformInput::MouseMove(MouseMoveEvent { - position: logical_point(x, y, scale_factor), - pressed_button, - modifiers: current_modifiers(), - }); - let handled = !func(input).propagate; - state_ptr.state.borrow_mut().callbacks.input = Some(func); - - if handled { Some(0) } else { Some(1) } -} - -fn handle_mouse_leave_msg(state_ptr: Rc) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - lock.hovered = false; - if let Some(mut callback) = lock.callbacks.hovered_status_change.take() { - drop(lock); - callback(false); - state_ptr.state.borrow_mut().callbacks.hovered_status_change = Some(callback); - } - - Some(0) -} - -fn handle_syskeydown_msg( - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { - PlatformInput::KeyDown(KeyDownEvent { - keystroke, - is_held: lparam.0 & (0x1 << 30) > 0, - }) - })?; - let mut func = lock.callbacks.input.take()?; - drop(lock); - - let handled = !func(input).propagate; - - let mut lock = state_ptr.state.borrow_mut(); - lock.callbacks.input = Some(func); - - if handled { - lock.system_key_handled = true; - Some(0) - } else { - // we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}` - // shortcuts. - None - } -} - -fn handle_syskeyup_msg( - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { - PlatformInput::KeyUp(KeyUpEvent { keystroke }) - })?; - let mut func = lock.callbacks.input.take()?; - drop(lock); - func(input); - state_ptr.state.borrow_mut().callbacks.input = Some(func); - - // Always return 0 to indicate that the message was handled, so we could properly handle `ModifiersChanged` event. - Some(0) -} - -// It's a known bug that you can't trigger `ctrl-shift-0`. See: -// https://superuser.com/questions/1455762/ctrl-shift-number-key-combination-has-stopped-working-for-a-few-numbers -fn handle_keydown_msg( - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { - PlatformInput::KeyDown(KeyDownEvent { - keystroke, - is_held: lparam.0 & (0x1 << 30) > 0, - }) - }) else { - return Some(1); - }; - drop(lock); - - let is_composing = with_input_handler(&state_ptr, |input_handler| { - input_handler.marked_text_range() - }) - .flatten() - .is_some(); - if is_composing { - translate_message(handle, wparam, lparam); - return Some(0); - } - - let Some(mut func) = state_ptr.state.borrow_mut().callbacks.input.take() else { - return Some(1); - }; - - let handled = !func(input).propagate; - - state_ptr.state.borrow_mut().callbacks.input = Some(func); - - if handled { - Some(0) - } else { - translate_message(handle, wparam, lparam); - Some(1) - } -} - -fn handle_keyup_msg( - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { - PlatformInput::KeyUp(KeyUpEvent { keystroke }) - }) else { - return Some(1); - }; - - let Some(mut func) = lock.callbacks.input.take() else { - return Some(1); - }; - drop(lock); - - let handled = !func(input).propagate; - state_ptr.state.borrow_mut().callbacks.input = Some(func); - - if handled { Some(0) } else { Some(1) } -} - -fn handle_char_msg(wparam: WPARAM, state_ptr: Rc) -> Option { - let input = parse_char_message(wparam, &state_ptr)?; - with_input_handler(&state_ptr, |input_handler| { - input_handler.replace_text_in_range(None, &input); - }); - - Some(0) -} - -fn handle_dead_char_msg(wparam: WPARAM, state_ptr: Rc) -> Option { - let ch = char::from_u32(wparam.0 as u32)?.to_string(); - with_input_handler(&state_ptr, |input_handler| { - input_handler.replace_and_mark_text_in_range(None, &ch, None); - }); - None -} - -fn handle_mouse_down_msg( - handle: HWND, - button: MouseButton, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - unsafe { SetCapture(handle) }; - let mut lock = state_ptr.state.borrow_mut(); - let Some(mut func) = lock.callbacks.input.take() else { - return Some(1); - }; - let x = lparam.signed_loword(); - let y = lparam.signed_hiword(); - let physical_point = point(DevicePixels(x as i32), DevicePixels(y as i32)); - let click_count = lock.click_state.update(button, physical_point); - let scale_factor = lock.scale_factor; - drop(lock); - - let input = PlatformInput::MouseDown(MouseDownEvent { - button, - position: logical_point(x as f32, y as f32, scale_factor), - modifiers: current_modifiers(), - click_count, - first_mouse: false, - }); - let handled = !func(input).propagate; - state_ptr.state.borrow_mut().callbacks.input = Some(func); - - if handled { Some(0) } else { Some(1) } -} - -fn handle_mouse_up_msg( - _handle: HWND, - button: MouseButton, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - unsafe { ReleaseCapture().log_err() }; - let mut lock = state_ptr.state.borrow_mut(); - let Some(mut func) = lock.callbacks.input.take() else { - return Some(1); - }; - let x = lparam.signed_loword() as f32; - let y = lparam.signed_hiword() as f32; - let click_count = lock.click_state.current_count; - let scale_factor = lock.scale_factor; - drop(lock); - - let input = PlatformInput::MouseUp(MouseUpEvent { - button, - position: logical_point(x, y, scale_factor), - modifiers: current_modifiers(), - click_count, - }); - let handled = !func(input).propagate; - state_ptr.state.borrow_mut().callbacks.input = Some(func); - - if handled { Some(0) } else { Some(1) } -} - -fn handle_xbutton_msg( - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - handler: impl Fn(HWND, MouseButton, LPARAM, Rc) -> Option, - state_ptr: Rc, -) -> Option { - let nav_dir = match wparam.hiword() { - XBUTTON1 => NavigationDirection::Back, - XBUTTON2 => NavigationDirection::Forward, - _ => return Some(1), - }; - handler(handle, MouseButton::Navigate(nav_dir), lparam, state_ptr) -} - -fn handle_mouse_wheel_msg( - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let modifiers = current_modifiers(); - let mut lock = state_ptr.state.borrow_mut(); - let Some(mut func) = lock.callbacks.input.take() else { - return Some(1); - }; - let scale_factor = lock.scale_factor; - let wheel_scroll_amount = match modifiers.shift { - true => lock.system_settings.mouse_wheel_settings.wheel_scroll_chars, - false => lock.system_settings.mouse_wheel_settings.wheel_scroll_lines, - }; - drop(lock); - - let wheel_distance = - (wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_amount as f32; - let mut cursor_point = POINT { - x: lparam.signed_loword().into(), - y: lparam.signed_hiword().into(), - }; - unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - let input = PlatformInput::ScrollWheel(ScrollWheelEvent { - position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), - delta: ScrollDelta::Lines(match modifiers.shift { - true => Point { - x: wheel_distance, - y: 0.0, - }, - false => Point { - y: wheel_distance, - x: 0.0, - }, - }), - modifiers, - touch_phase: TouchPhase::Moved, - }); - let handled = !func(input).propagate; - state_ptr.state.borrow_mut().callbacks.input = Some(func); - - if handled { Some(0) } else { Some(1) } -} - -fn handle_mouse_horizontal_wheel_msg( - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - let Some(mut func) = lock.callbacks.input.take() else { - return Some(1); - }; - let scale_factor = lock.scale_factor; - let wheel_scroll_chars = lock.system_settings.mouse_wheel_settings.wheel_scroll_chars; - drop(lock); - - let wheel_distance = - (-wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_chars as f32; - let mut cursor_point = POINT { - x: lparam.signed_loword().into(), - y: lparam.signed_hiword().into(), - }; - unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - let event = PlatformInput::ScrollWheel(ScrollWheelEvent { - position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), - delta: ScrollDelta::Lines(Point { - x: wheel_distance, - y: 0.0, - }), - modifiers: current_modifiers(), - touch_phase: TouchPhase::Moved, - }); - let handled = !func(event).propagate; - state_ptr.state.borrow_mut().callbacks.input = Some(func); - - if handled { Some(0) } else { Some(1) } -} - -fn retrieve_caret_position(state_ptr: &Rc) -> Option { - with_input_handler_and_scale_factor(state_ptr, |input_handler, scale_factor| { - let caret_range = input_handler.selected_text_range(false)?; - let caret_position = input_handler.bounds_for_range(caret_range.range)?; - Some(POINT { - // logical to physical - x: (caret_position.origin.x.0 * scale_factor) as i32, - y: (caret_position.origin.y.0 * scale_factor) as i32 - + ((caret_position.size.height.0 * scale_factor) as i32 / 2), - }) - }) -} - -fn handle_ime_position(handle: HWND, state_ptr: Rc) -> Option { - unsafe { - let ctx = ImmGetContext(handle); - - let Some(caret_position) = retrieve_caret_position(&state_ptr) else { - return Some(0); - }; + lock.origin = origin; + let size = lock.logical_size; + let center_x = origin.x.0 + size.width.0 / 2.; + let center_y = origin.y.0 + size.height.0 / 2.; + let monitor_bounds = lock.display.bounds(); + if center_x < monitor_bounds.left().0 + || center_x > monitor_bounds.right().0 + || center_y < monitor_bounds.top().0 + || center_y > monitor_bounds.bottom().0 { - let config = COMPOSITIONFORM { - dwStyle: CFS_POINT, - ptCurrentPos: caret_position, - ..Default::default() - }; - ImmSetCompositionWindow(ctx, &config as _).ok().log_err(); - } - { - let config = CANDIDATEFORM { - dwStyle: CFS_CANDIDATEPOS, - ptCurrentPos: caret_position, - ..Default::default() - }; - ImmSetCandidateWindow(ctx, &config as _).ok().log_err(); - } - ImmReleaseContext(handle, ctx).ok().log_err(); - Some(0) - } -} - -fn handle_ime_composition( - handle: HWND, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let ctx = unsafe { ImmGetContext(handle) }; - let result = handle_ime_composition_inner(ctx, lparam, state_ptr); - unsafe { ImmReleaseContext(handle, ctx).ok().log_err() }; - result -} - -fn handle_ime_composition_inner( - ctx: HIMC, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let lparam = lparam.0 as u32; - if lparam == 0 { - // Japanese IME may send this message with lparam = 0, which indicates that - // there is no composition string. - with_input_handler(&state_ptr, |input_handler| { - input_handler.replace_text_in_range(None, ""); - })?; - Some(0) - } else { - if lparam & GCS_COMPSTR.0 > 0 { - let comp_string = parse_ime_composition_string(ctx, GCS_COMPSTR)?; - let caret_pos = (!comp_string.is_empty() && lparam & GCS_CURSORPOS.0 > 0).then(|| { - let pos = retrieve_composition_cursor_position(ctx); - pos..pos - }); - with_input_handler(&state_ptr, |input_handler| { - input_handler.replace_and_mark_text_in_range(None, &comp_string, caret_pos); - })?; - } - if lparam & GCS_RESULTSTR.0 > 0 { - let comp_result = parse_ime_composition_string(ctx, GCS_RESULTSTR)?; - with_input_handler(&state_ptr, |input_handler| { - input_handler.replace_text_in_range(None, &comp_result); - })?; - return Some(0); - } - - // currently, we don't care other stuff - None - } -} - -/// SEE: https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize -fn handle_calc_client_size( - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - if !state_ptr.hide_title_bar || state_ptr.state.borrow().is_fullscreen() || wparam.0 == 0 { - return None; - } - - let is_maximized = state_ptr.state.borrow().is_maximized(); - let insets = get_client_area_insets(handle, is_maximized, state_ptr.windows_version); - // wparam is TRUE so lparam points to an NCCALCSIZE_PARAMS structure - let mut params = lparam.0 as *mut NCCALCSIZE_PARAMS; - let mut requested_client_rect = unsafe { &mut ((*params).rgrc) }; - - requested_client_rect[0].left += insets.left; - requested_client_rect[0].top += insets.top; - requested_client_rect[0].right -= insets.right; - requested_client_rect[0].bottom -= insets.bottom; - - // Fix auto hide taskbar not showing. This solution is based on the approach - // used by Chrome. However, it may result in one row of pixels being obscured - // in our client area. But as Chrome says, "there seems to be no better solution." - if is_maximized { - if let Some(ref taskbar_position) = state_ptr - .state - .borrow() - .system_settings - .auto_hide_taskbar_position - { - // Fot the auto-hide taskbar, adjust in by 1 pixel on taskbar edge, - // so the window isn't treated as a "fullscreen app", which would cause - // the taskbar to disappear. - match taskbar_position { - AutoHideTaskbarPosition::Left => { - requested_client_rect[0].left += AUTO_HIDE_TASKBAR_THICKNESS_PX - } - AutoHideTaskbarPosition::Top => { - requested_client_rect[0].top += AUTO_HIDE_TASKBAR_THICKNESS_PX - } - AutoHideTaskbarPosition::Right => { - requested_client_rect[0].right -= AUTO_HIDE_TASKBAR_THICKNESS_PX - } - AutoHideTaskbarPosition::Bottom => { - requested_client_rect[0].bottom -= AUTO_HIDE_TASKBAR_THICKNESS_PX - } + // center of the window may have moved to another monitor + let monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) }; + // minimize the window can trigger this event too, in this case, + // monitor is invalid, we do nothing. + if !monitor.is_invalid() && lock.display.handle != monitor { + // we will get the same monitor if we only have one + lock.display = WindowsDisplay::new_with_handle(monitor); } } - } - - Some(0) -} - -fn handle_activate_msg(wparam: WPARAM, state_ptr: Rc) -> Option { - let activated = wparam.loword() > 0; - let this = state_ptr.clone(); - state_ptr - .executor - .spawn(async move { - let mut lock = this.state.borrow_mut(); - if let Some(mut func) = lock.callbacks.active_status_change.take() { - drop(lock); - func(activated); - this.state.borrow_mut().callbacks.active_status_change = Some(func); - } - }) - .detach(); - - None -} - -fn handle_create_msg(handle: HWND, state_ptr: Rc) -> Option { - if state_ptr.hide_title_bar { - notify_frame_changed(handle); - Some(0) - } else { - None - } -} - -fn handle_dpi_changed_msg( - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let new_dpi = wparam.loword() as f32; - let mut lock = state_ptr.state.borrow_mut(); - lock.scale_factor = new_dpi / USER_DEFAULT_SCREEN_DPI as f32; - lock.border_offset.update(handle).log_err(); - drop(lock); - - let rect = unsafe { &*(lparam.0 as *const RECT) }; - let width = rect.right - rect.left; - let height = rect.bottom - rect.top; - // this will emit `WM_SIZE` and `WM_MOVE` right here - // even before this function returns - // the new size is handled in `WM_SIZE` - unsafe { - SetWindowPos( - handle, - None, - rect.left, - rect.top, - width, - height, - SWP_NOZORDER | SWP_NOACTIVATE, - ) - .context("unable to set window position after dpi has changed") - .log_err(); - } - - Some(0) -} - -/// The following conditions will trigger this event: -/// 1. The monitor on which the window is located goes offline or changes resolution. -/// 2. Another monitor goes offline, is plugged in, or changes resolution. -/// -/// In either case, the window will only receive information from the monitor on which -/// it is located. -/// -/// For example, in the case of condition 2, where the monitor on which the window is -/// located has actually changed nothing, it will still receive this event. -fn handle_display_change_msg(handle: HWND, state_ptr: Rc) -> Option { - // NOTE: - // Even the `lParam` holds the resolution of the screen, we just ignore it. - // Because WM_DPICHANGED, WM_MOVE, WM_SIZE will come first, window reposition and resize - // are handled there. - // So we only care about if monitor is disconnected. - let previous_monitor = state_ptr.state.borrow().display; - if WindowsDisplay::is_connected(previous_monitor.handle) { - // we are fine, other display changed - return None; - } - // display disconnected - // in this case, the OS will move our window to another monitor, and minimize it. - // we deminimize the window and query the monitor after moving - unsafe { - let _ = ShowWindow(handle, SW_SHOWNORMAL); - }; - let new_monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) }; - // all monitors disconnected - if new_monitor.is_invalid() { - log::error!("No monitor detected!"); - return None; - } - let new_display = WindowsDisplay::new_with_handle(new_monitor); - state_ptr.state.borrow_mut().display = new_display; - Some(0) -} - -fn handle_hit_test_msg( - handle: HWND, - msg: u32, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - if !state_ptr.is_movable || state_ptr.state.borrow().is_fullscreen() { - return None; - } - - let mut lock = state_ptr.state.borrow_mut(); - if let Some(mut callback) = lock.callbacks.hit_test_window_control.take() { - drop(lock); - let area = callback(); - state_ptr - .state - .borrow_mut() - .callbacks - .hit_test_window_control = Some(callback); - if let Some(area) = area { - return match area { - WindowControlArea::Drag => Some(HTCAPTION as _), - WindowControlArea::Close => Some(HTCLOSE as _), - WindowControlArea::Max => Some(HTMAXBUTTON as _), - WindowControlArea::Min => Some(HTMINBUTTON as _), - }; + if let Some(mut callback) = lock.callbacks.moved.take() { + drop(lock); + callback(); + self.state.borrow_mut().callbacks.moved = Some(callback); } - } else { - drop(lock); + Some(0) } - if !state_ptr.hide_title_bar { - // If the OS draws the title bar, we don't need to handle hit test messages. - return None; - } - - // default handler for resize areas - let hit = unsafe { DefWindowProcW(handle, msg, wparam, lparam) }; - if matches!( - hit.0 as u32, - HTNOWHERE - | HTRIGHT - | HTLEFT - | HTTOPLEFT - | HTTOP - | HTTOPRIGHT - | HTBOTTOMRIGHT - | HTBOTTOM - | HTBOTTOMLEFT - ) { - return Some(hit.0); - } - - if state_ptr.state.borrow().is_fullscreen() { - return Some(HTCLIENT as _); - } - - let dpi = unsafe { GetDpiForWindow(handle) }; - let frame_y = unsafe { GetSystemMetricsForDpi(SM_CYFRAME, dpi) }; - - let mut cursor_point = POINT { - x: lparam.signed_loword().into(), - y: lparam.signed_hiword().into(), - }; - unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - if !state_ptr.state.borrow().is_maximized() && cursor_point.y >= 0 && cursor_point.y <= frame_y - { - return Some(HTTOP as _); - } - - Some(HTCLIENT as _) -} - -fn handle_nc_mouse_move_msg( - handle: HWND, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - start_tracking_mouse(handle, &state_ptr, TME_LEAVE | TME_NONCLIENT); - - let mut lock = state_ptr.state.borrow_mut(); - let mut func = lock.callbacks.input.take()?; - let scale_factor = lock.scale_factor; - drop(lock); - - let mut cursor_point = POINT { - x: lparam.signed_loword().into(), - y: lparam.signed_hiword().into(), - }; - unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - let input = PlatformInput::MouseMove(MouseMoveEvent { - position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), - pressed_button: None, - modifiers: current_modifiers(), - }); - let handled = !func(input).propagate; - state_ptr.state.borrow_mut().callbacks.input = Some(func); - - if handled { Some(0) } else { None } -} - -fn handle_nc_mouse_down_msg( - handle: HWND, - button: MouseButton, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - if let Some(mut func) = lock.callbacks.input.take() { + fn handle_get_min_max_info_msg(&self, lparam: LPARAM) -> Option { + let lock = self.state.borrow(); + let min_size = lock.min_size?; let scale_factor = lock.scale_factor; - let mut cursor_point = POINT { - x: lparam.signed_loword().into(), - y: lparam.signed_hiword().into(), + let boarder_offset = lock.border_offset; + drop(lock); + unsafe { + let minmax_info = &mut *(lparam.0 as *mut MINMAXINFO); + minmax_info.ptMinTrackSize.x = + min_size.width.scale(scale_factor).0 as i32 + boarder_offset.width_offset; + minmax_info.ptMinTrackSize.y = + min_size.height.scale(scale_factor).0 as i32 + boarder_offset.height_offset; + } + Some(0) + } + + fn handle_size_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + + // Don't resize the renderer when the window is minimized, but record that it was minimized so + // that on restore the swap chain can be recreated via `update_drawable_size_even_if_unchanged`. + if wparam.0 == SIZE_MINIMIZED as usize { + lock.restore_from_minimized = lock.callbacks.request_frame.take(); + return Some(0); + } + + let width = lparam.loword().max(1) as i32; + let height = lparam.hiword().max(1) as i32; + let new_size = size(DevicePixels(width), DevicePixels(height)); + let scale_factor = lock.scale_factor; + if lock.restore_from_minimized.is_some() { + lock.callbacks.request_frame = lock.restore_from_minimized.take(); + } else { + lock.renderer.resize(new_size).log_err(); + } + let new_size = new_size.to_pixels(scale_factor); + lock.logical_size = new_size; + if let Some(mut callback) = lock.callbacks.resize.take() { + drop(lock); + callback(new_size, scale_factor); + self.state.borrow_mut().callbacks.resize = Some(callback); + } + Some(0) + } + + fn handle_size_move_loop(&self, handle: HWND) -> Option { + unsafe { + let ret = SetTimer( + Some(handle), + SIZE_MOVE_LOOP_TIMER_ID, + USER_TIMER_MINIMUM, + None, + ); + if ret == 0 { + log::error!( + "unable to create timer: {}", + std::io::Error::last_os_error() + ); + } + } + None + } + + fn handle_size_move_loop_exit(&self, handle: HWND) -> Option { + unsafe { + KillTimer(Some(handle), SIZE_MOVE_LOOP_TIMER_ID).log_err(); + } + None + } + + fn handle_timer_msg(&self, handle: HWND, wparam: WPARAM) -> Option { + if wparam.0 == SIZE_MOVE_LOOP_TIMER_ID { + for runnable in self.main_receiver.drain() { + runnable.run(); + } + self.handle_paint_msg(handle) + } else { + None + } + } + + fn handle_paint_msg(&self, handle: HWND) -> Option { + self.draw_window(handle, false) + } + + fn handle_close_msg(&self) -> Option { + let mut callback = self.state.borrow_mut().callbacks.should_close.take()?; + let should_close = callback(); + self.state.borrow_mut().callbacks.should_close = Some(callback); + if should_close { None } else { Some(0) } + } + + fn handle_destroy_msg(&self, handle: HWND) -> Option { + let callback = { + let mut lock = self.state.borrow_mut(); + lock.callbacks.close.take() }; - unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - let physical_point = point(DevicePixels(cursor_point.x), DevicePixels(cursor_point.y)); + if let Some(callback) = callback { + callback(); + } + unsafe { + PostThreadMessageW( + self.main_thread_id_win32, + WM_GPUI_CLOSE_ONE_WINDOW, + WPARAM(self.validation_number), + LPARAM(handle.0 as isize), + ) + .log_err(); + } + Some(0) + } + + fn handle_mouse_move_msg(&self, handle: HWND, lparam: LPARAM, wparam: WPARAM) -> Option { + self.start_tracking_mouse(handle, TME_LEAVE); + + let mut lock = self.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + let scale_factor = lock.scale_factor; + drop(lock); + + let pressed_button = match MODIFIERKEYS_FLAGS(wparam.loword() as u32) { + flags if flags.contains(MK_LBUTTON) => Some(MouseButton::Left), + flags if flags.contains(MK_RBUTTON) => Some(MouseButton::Right), + flags if flags.contains(MK_MBUTTON) => Some(MouseButton::Middle), + flags if flags.contains(MK_XBUTTON1) => { + Some(MouseButton::Navigate(NavigationDirection::Back)) + } + flags if flags.contains(MK_XBUTTON2) => { + Some(MouseButton::Navigate(NavigationDirection::Forward)) + } + _ => None, + }; + let x = lparam.signed_loword() as f32; + let y = lparam.signed_hiword() as f32; + let input = PlatformInput::MouseMove(MouseMoveEvent { + position: logical_point(x, y, scale_factor), + pressed_button, + modifiers: current_modifiers(), + }); + let handled = !func(input).propagate; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) } else { Some(1) } + } + + fn handle_mouse_leave_msg(&self) -> Option { + let mut lock = self.state.borrow_mut(); + lock.hovered = false; + if let Some(mut callback) = lock.callbacks.hovered_status_change.take() { + drop(lock); + callback(false); + self.state.borrow_mut().callbacks.hovered_status_change = Some(callback); + } + + Some(0) + } + + fn handle_syskeydown_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { + PlatformInput::KeyDown(KeyDownEvent { + keystroke, + is_held: lparam.0 & (0x1 << 30) > 0, + }) + })?; + let mut func = lock.callbacks.input.take()?; + drop(lock); + + let handled = !func(input).propagate; + + let mut lock = self.state.borrow_mut(); + lock.callbacks.input = Some(func); + + if handled { + lock.system_key_handled = true; + Some(0) + } else { + // we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}` + // shortcuts. + None + } + } + + fn handle_syskeyup_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { + PlatformInput::KeyUp(KeyUpEvent { keystroke }) + })?; + let mut func = lock.callbacks.input.take()?; + drop(lock); + func(input); + self.state.borrow_mut().callbacks.input = Some(func); + + // Always return 0 to indicate that the message was handled, so we could properly handle `ModifiersChanged` event. + Some(0) + } + + // It's a known bug that you can't trigger `ctrl-shift-0`. See: + // https://superuser.com/questions/1455762/ctrl-shift-number-key-combination-has-stopped-working-for-a-few-numbers + fn handle_keydown_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { + PlatformInput::KeyDown(KeyDownEvent { + keystroke, + is_held: lparam.0 & (0x1 << 30) > 0, + }) + }) else { + return Some(1); + }; + drop(lock); + + let is_composing = self + .with_input_handler(|input_handler| input_handler.marked_text_range()) + .flatten() + .is_some(); + if is_composing { + translate_message(handle, wparam, lparam); + return Some(0); + } + + let Some(mut func) = self.state.borrow_mut().callbacks.input.take() else { + return Some(1); + }; + + let handled = !func(input).propagate; + + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { + Some(0) + } else { + translate_message(handle, wparam, lparam); + Some(1) + } + } + + fn handle_keyup_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option { + let mut lock = self.state.borrow_mut(); + let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { + PlatformInput::KeyUp(KeyUpEvent { keystroke }) + }) else { + return Some(1); + }; + + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + drop(lock); + + let handled = !func(input).propagate; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) } else { Some(1) } + } + + fn handle_char_msg(&self, wparam: WPARAM) -> Option { + let input = self.parse_char_message(wparam)?; + self.with_input_handler(|input_handler| { + input_handler.replace_text_in_range(None, &input); + }); + + Some(0) + } + + fn handle_dead_char_msg(&self, wparam: WPARAM) -> Option { + let ch = char::from_u32(wparam.0 as u32)?.to_string(); + self.with_input_handler(|input_handler| { + input_handler.replace_and_mark_text_in_range(None, &ch, None); + }); + None + } + + fn handle_mouse_down_msg( + &self, + handle: HWND, + button: MouseButton, + lparam: LPARAM, + ) -> Option { + unsafe { SetCapture(handle) }; + let mut lock = self.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + let x = lparam.signed_loword(); + let y = lparam.signed_hiword(); + let physical_point = point(DevicePixels(x as i32), DevicePixels(y as i32)); let click_count = lock.click_state.update(button, physical_point); + let scale_factor = lock.scale_factor; drop(lock); let input = PlatformInput::MouseDown(MouseDownEvent { button, - position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + position: logical_point(x as f32, y as f32, scale_factor), modifiers: current_modifiers(), click_count, first_mouse: false, }); - let result = func(input.clone()); - let handled = !result.propagate || result.default_prevented; - state_ptr.state.borrow_mut().callbacks.input = Some(func); + let handled = !func(input).propagate; + self.state.borrow_mut().callbacks.input = Some(func); - if handled { - return Some(0); - } - } else { - drop(lock); - }; + if handled { Some(0) } else { Some(1) } + } - // Since these are handled in handle_nc_mouse_up_msg we must prevent the default window proc - if button == MouseButton::Left { - match wparam.0 as u32 { - HTMINBUTTON => state_ptr.state.borrow_mut().nc_button_pressed = Some(HTMINBUTTON), - HTMAXBUTTON => state_ptr.state.borrow_mut().nc_button_pressed = Some(HTMAXBUTTON), - HTCLOSE => state_ptr.state.borrow_mut().nc_button_pressed = Some(HTCLOSE), - _ => return None, + fn handle_mouse_up_msg( + &self, + _handle: HWND, + button: MouseButton, + lparam: LPARAM, + ) -> Option { + unsafe { ReleaseCapture().log_err() }; + let mut lock = self.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); }; + let x = lparam.signed_loword() as f32; + let y = lparam.signed_hiword() as f32; + let click_count = lock.click_state.current_count; + let scale_factor = lock.scale_factor; + drop(lock); + + let input = PlatformInput::MouseUp(MouseUpEvent { + button, + position: logical_point(x, y, scale_factor), + modifiers: current_modifiers(), + click_count, + }); + let handled = !func(input).propagate; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) } else { Some(1) } + } + + fn handle_xbutton_msg( + &self, + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + handler: impl Fn(&Self, HWND, MouseButton, LPARAM) -> Option, + ) -> Option { + let nav_dir = match wparam.hiword() { + XBUTTON1 => NavigationDirection::Back, + XBUTTON2 => NavigationDirection::Forward, + _ => return Some(1), + }; + handler(self, handle, MouseButton::Navigate(nav_dir), lparam) + } + + fn handle_mouse_wheel_msg( + &self, + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + let modifiers = current_modifiers(); + let mut lock = self.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + let scale_factor = lock.scale_factor; + let wheel_scroll_amount = match modifiers.shift { + true => lock.system_settings.mouse_wheel_settings.wheel_scroll_chars, + false => lock.system_settings.mouse_wheel_settings.wheel_scroll_lines, + }; + drop(lock); + + let wheel_distance = + (wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_amount as f32; + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; + let input = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + delta: ScrollDelta::Lines(match modifiers.shift { + true => Point { + x: wheel_distance, + y: 0.0, + }, + false => Point { + y: wheel_distance, + x: 0.0, + }, + }), + modifiers, + touch_phase: TouchPhase::Moved, + }); + let handled = !func(input).propagate; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) } else { Some(1) } + } + + fn handle_mouse_horizontal_wheel_msg( + &self, + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + let mut lock = self.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + let scale_factor = lock.scale_factor; + let wheel_scroll_chars = lock.system_settings.mouse_wheel_settings.wheel_scroll_chars; + drop(lock); + + let wheel_distance = + (-wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_chars as f32; + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; + let event = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + delta: ScrollDelta::Lines(Point { + x: wheel_distance, + y: 0.0, + }), + modifiers: current_modifiers(), + touch_phase: TouchPhase::Moved, + }); + let handled = !func(event).propagate; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) } else { Some(1) } + } + + fn retrieve_caret_position(&self) -> Option { + self.with_input_handler_and_scale_factor(|input_handler, scale_factor| { + let caret_range = input_handler.selected_text_range(false)?; + let caret_position = input_handler.bounds_for_range(caret_range.range)?; + Some(POINT { + // logical to physical + x: (caret_position.origin.x.0 * scale_factor) as i32, + y: (caret_position.origin.y.0 * scale_factor) as i32 + + ((caret_position.size.height.0 * scale_factor) as i32 / 2), + }) + }) + } + + fn handle_ime_position(&self, handle: HWND) -> Option { + unsafe { + let ctx = ImmGetContext(handle); + + let Some(caret_position) = self.retrieve_caret_position() else { + return Some(0); + }; + { + let config = COMPOSITIONFORM { + dwStyle: CFS_POINT, + ptCurrentPos: caret_position, + ..Default::default() + }; + ImmSetCompositionWindow(ctx, &config as _).ok().log_err(); + } + { + let config = CANDIDATEFORM { + dwStyle: CFS_CANDIDATEPOS, + ptCurrentPos: caret_position, + ..Default::default() + }; + ImmSetCandidateWindow(ctx, &config as _).ok().log_err(); + } + ImmReleaseContext(handle, ctx).ok().log_err(); + Some(0) + } + } + + fn handle_ime_composition(&self, handle: HWND, lparam: LPARAM) -> Option { + let ctx = unsafe { ImmGetContext(handle) }; + let result = self.handle_ime_composition_inner(ctx, lparam); + unsafe { ImmReleaseContext(handle, ctx).ok().log_err() }; + result + } + + fn handle_ime_composition_inner(&self, ctx: HIMC, lparam: LPARAM) -> Option { + let lparam = lparam.0 as u32; + if lparam == 0 { + // Japanese IME may send this message with lparam = 0, which indicates that + // there is no composition string. + self.with_input_handler(|input_handler| { + input_handler.replace_text_in_range(None, ""); + })?; + Some(0) + } else { + if lparam & GCS_COMPSTR.0 > 0 { + let comp_string = parse_ime_composition_string(ctx, GCS_COMPSTR)?; + let caret_pos = + (!comp_string.is_empty() && lparam & GCS_CURSORPOS.0 > 0).then(|| { + let pos = retrieve_composition_cursor_position(ctx); + pos..pos + }); + self.with_input_handler(|input_handler| { + input_handler.replace_and_mark_text_in_range(None, &comp_string, caret_pos); + })?; + } + if lparam & GCS_RESULTSTR.0 > 0 { + let comp_result = parse_ime_composition_string(ctx, GCS_RESULTSTR)?; + self.with_input_handler(|input_handler| { + input_handler.replace_text_in_range(None, &comp_result); + })?; + return Some(0); + } + + // currently, we don't care other stuff + None + } + } + + /// SEE: https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize + fn handle_calc_client_size( + &self, + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + if !self.hide_title_bar || self.state.borrow().is_fullscreen() || wparam.0 == 0 { + return None; + } + + let is_maximized = self.state.borrow().is_maximized(); + let insets = get_client_area_insets(handle, is_maximized, self.windows_version); + // wparam is TRUE so lparam points to an NCCALCSIZE_PARAMS structure + let mut params = lparam.0 as *mut NCCALCSIZE_PARAMS; + let mut requested_client_rect = unsafe { &mut ((*params).rgrc) }; + + requested_client_rect[0].left += insets.left; + requested_client_rect[0].top += insets.top; + requested_client_rect[0].right -= insets.right; + requested_client_rect[0].bottom -= insets.bottom; + + // Fix auto hide taskbar not showing. This solution is based on the approach + // used by Chrome. However, it may result in one row of pixels being obscured + // in our client area. But as Chrome says, "there seems to be no better solution." + if is_maximized { + if let Some(ref taskbar_position) = self + .state + .borrow() + .system_settings + .auto_hide_taskbar_position + { + // Fot the auto-hide taskbar, adjust in by 1 pixel on taskbar edge, + // so the window isn't treated as a "fullscreen app", which would cause + // the taskbar to disappear. + match taskbar_position { + AutoHideTaskbarPosition::Left => { + requested_client_rect[0].left += AUTO_HIDE_TASKBAR_THICKNESS_PX + } + AutoHideTaskbarPosition::Top => { + requested_client_rect[0].top += AUTO_HIDE_TASKBAR_THICKNESS_PX + } + AutoHideTaskbarPosition::Right => { + requested_client_rect[0].right -= AUTO_HIDE_TASKBAR_THICKNESS_PX + } + AutoHideTaskbarPosition::Bottom => { + requested_client_rect[0].bottom -= AUTO_HIDE_TASKBAR_THICKNESS_PX + } + } + } + } + Some(0) - } else { + } + + fn handle_activate_msg(self: &Rc, wparam: WPARAM) -> Option { + let activated = wparam.loword() > 0; + let this = self.clone(); + self.executor + .spawn(async move { + let mut lock = this.state.borrow_mut(); + if let Some(mut func) = lock.callbacks.active_status_change.take() { + drop(lock); + func(activated); + this.state.borrow_mut().callbacks.active_status_change = Some(func); + } + }) + .detach(); + None } -} -fn handle_nc_mouse_up_msg( - handle: HWND, - button: MouseButton, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - if let Some(mut func) = lock.callbacks.input.take() { + fn handle_create_msg(&self, handle: HWND) -> Option { + if self.hide_title_bar { + notify_frame_changed(handle); + Some(0) + } else { + None + } + } + + fn handle_dpi_changed_msg( + &self, + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + let new_dpi = wparam.loword() as f32; + let mut lock = self.state.borrow_mut(); + lock.scale_factor = new_dpi / USER_DEFAULT_SCREEN_DPI as f32; + lock.border_offset.update(handle).log_err(); + drop(lock); + + let rect = unsafe { &*(lparam.0 as *const RECT) }; + let width = rect.right - rect.left; + let height = rect.bottom - rect.top; + // this will emit `WM_SIZE` and `WM_MOVE` right here + // even before this function returns + // the new size is handled in `WM_SIZE` + unsafe { + SetWindowPos( + handle, + None, + rect.left, + rect.top, + width, + height, + SWP_NOZORDER | SWP_NOACTIVATE, + ) + .context("unable to set window position after dpi has changed") + .log_err(); + } + + Some(0) + } + + /// The following conditions will trigger this event: + /// 1. The monitor on which the window is located goes offline or changes resolution. + /// 2. Another monitor goes offline, is plugged in, or changes resolution. + /// + /// In either case, the window will only receive information from the monitor on which + /// it is located. + /// + /// For example, in the case of condition 2, where the monitor on which the window is + /// located has actually changed nothing, it will still receive this event. + fn handle_display_change_msg(&self, handle: HWND) -> Option { + // NOTE: + // Even the `lParam` holds the resolution of the screen, we just ignore it. + // Because WM_DPICHANGED, WM_MOVE, WM_SIZE will come first, window reposition and resize + // are handled there. + // So we only care about if monitor is disconnected. + let previous_monitor = self.state.borrow().display; + if WindowsDisplay::is_connected(previous_monitor.handle) { + // we are fine, other display changed + return None; + } + // display disconnected + // in this case, the OS will move our window to another monitor, and minimize it. + // we deminimize the window and query the monitor after moving + unsafe { + let _ = ShowWindow(handle, SW_SHOWNORMAL); + }; + let new_monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) }; + // all monitors disconnected + if new_monitor.is_invalid() { + log::error!("No monitor detected!"); + return None; + } + let new_display = WindowsDisplay::new_with_handle(new_monitor); + self.state.borrow_mut().display = new_display; + Some(0) + } + + fn handle_hit_test_msg( + &self, + handle: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + if !self.is_movable || self.state.borrow().is_fullscreen() { + return None; + } + + let mut lock = self.state.borrow_mut(); + if let Some(mut callback) = lock.callbacks.hit_test_window_control.take() { + drop(lock); + let area = callback(); + self.state.borrow_mut().callbacks.hit_test_window_control = Some(callback); + if let Some(area) = area { + return match area { + WindowControlArea::Drag => Some(HTCAPTION as _), + WindowControlArea::Close => Some(HTCLOSE as _), + WindowControlArea::Max => Some(HTMAXBUTTON as _), + WindowControlArea::Min => Some(HTMINBUTTON as _), + }; + } + } else { + drop(lock); + } + + if !self.hide_title_bar { + // If the OS draws the title bar, we don't need to handle hit test messages. + return None; + } + + // default handler for resize areas + let hit = unsafe { DefWindowProcW(handle, msg, wparam, lparam) }; + if matches!( + hit.0 as u32, + HTNOWHERE + | HTRIGHT + | HTLEFT + | HTTOPLEFT + | HTTOP + | HTTOPRIGHT + | HTBOTTOMRIGHT + | HTBOTTOM + | HTBOTTOMLEFT + ) { + return Some(hit.0); + } + + if self.state.borrow().is_fullscreen() { + return Some(HTCLIENT as _); + } + + let dpi = unsafe { GetDpiForWindow(handle) }; + let frame_y = unsafe { GetSystemMetricsForDpi(SM_CYFRAME, dpi) }; + + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; + if !self.state.borrow().is_maximized() && cursor_point.y >= 0 && cursor_point.y <= frame_y { + return Some(HTTOP as _); + } + + Some(HTCLIENT as _) + } + + fn handle_nc_mouse_move_msg(&self, handle: HWND, lparam: LPARAM) -> Option { + self.start_tracking_mouse(handle, TME_LEAVE | TME_NONCLIENT); + + let mut lock = self.state.borrow_mut(); + let mut func = lock.callbacks.input.take()?; let scale_factor = lock.scale_factor; drop(lock); @@ -1027,253 +893,355 @@ fn handle_nc_mouse_up_msg( y: lparam.signed_hiword().into(), }; unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - let input = PlatformInput::MouseUp(MouseUpEvent { - button, + let input = PlatformInput::MouseMove(MouseMoveEvent { position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + pressed_button: None, modifiers: current_modifiers(), - click_count: 1, }); let handled = !func(input).propagate; - state_ptr.state.borrow_mut().callbacks.input = Some(func); + self.state.borrow_mut().callbacks.input = Some(func); - if handled { - return Some(0); - } - } else { - drop(lock); + if handled { Some(0) } else { None } } - let last_pressed = state_ptr.state.borrow_mut().nc_button_pressed.take(); - if button == MouseButton::Left - && let Some(last_pressed) = last_pressed - { - let handled = match (wparam.0 as u32, last_pressed) { - (HTMINBUTTON, HTMINBUTTON) => { - unsafe { ShowWindowAsync(handle, SW_MINIMIZE).ok().log_err() }; - true + fn handle_nc_mouse_down_msg( + &self, + handle: HWND, + button: MouseButton, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + let mut lock = self.state.borrow_mut(); + if let Some(mut func) = lock.callbacks.input.take() { + let scale_factor = lock.scale_factor; + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; + let physical_point = point(DevicePixels(cursor_point.x), DevicePixels(cursor_point.y)); + let click_count = lock.click_state.update(button, physical_point); + drop(lock); + + let input = PlatformInput::MouseDown(MouseDownEvent { + button, + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + modifiers: current_modifiers(), + click_count, + first_mouse: false, + }); + let result = func(input.clone()); + let handled = !result.propagate || result.default_prevented; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { + return Some(0); } - (HTMAXBUTTON, HTMAXBUTTON) => { - if state_ptr.state.borrow().is_maximized() { - unsafe { ShowWindowAsync(handle, SW_NORMAL).ok().log_err() }; - } else { - unsafe { ShowWindowAsync(handle, SW_MAXIMIZE).ok().log_err() }; - } - true - } - (HTCLOSE, HTCLOSE) => { - unsafe { - PostMessageW(Some(handle), WM_CLOSE, WPARAM::default(), LPARAM::default()) - .log_err() - }; - true - } - _ => false, + } else { + drop(lock); }; - if handled { - return Some(0); - } - } - None -} - -fn handle_cursor_changed(lparam: LPARAM, state_ptr: Rc) -> Option { - let mut state = state_ptr.state.borrow_mut(); - let had_cursor = state.current_cursor.is_some(); - - state.current_cursor = if lparam.0 == 0 { - None - } else { - Some(HCURSOR(lparam.0 as _)) - }; - - if had_cursor != state.current_cursor.is_some() { - unsafe { SetCursor(state.current_cursor) }; - } - - Some(0) -} - -fn handle_set_cursor( - handle: HWND, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - if unsafe { !IsWindowEnabled(handle).as_bool() } - || matches!( - lparam.loword() as u32, - HTLEFT - | HTRIGHT - | HTTOP - | HTTOPLEFT - | HTTOPRIGHT - | HTBOTTOM - | HTBOTTOMLEFT - | HTBOTTOMRIGHT - ) - { - return None; - } - unsafe { - SetCursor(state_ptr.state.borrow().current_cursor); - }; - Some(1) -} - -fn handle_system_settings_changed( - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - if wparam.0 != 0 { - let mut lock = state_ptr.state.borrow_mut(); - let display = lock.display; - lock.system_settings.update(display, wparam.0); - lock.click_state.system_update(wparam.0); - lock.border_offset.update(handle).log_err(); - } else { - handle_system_theme_changed(handle, lparam, state_ptr)?; - }; - // Force to trigger WM_NCCALCSIZE event to ensure that we handle auto hide - // taskbar correctly. - notify_frame_changed(handle); - - Some(0) -} - -fn handle_system_command(wparam: WPARAM, state_ptr: Rc) -> Option { - if wparam.0 == SC_KEYMENU as usize { - let mut lock = state_ptr.state.borrow_mut(); - if lock.system_key_handled { - lock.system_key_handled = false; - return Some(0); - } - } - None -} - -fn handle_system_theme_changed( - handle: HWND, - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - // lParam is a pointer to a string that indicates the area containing the system parameter - // that was changed. - let parameter = PCWSTR::from_raw(lparam.0 as _); - if unsafe { !parameter.is_null() && !parameter.is_empty() } { - if let Some(parameter_string) = unsafe { parameter.to_string() }.log_err() { - log::info!("System settings changed: {}", parameter_string); - match parameter_string.as_str() { - "ImmersiveColorSet" => { - let new_appearance = system_appearance() - .context("unable to get system appearance when handling ImmersiveColorSet") - .log_err()?; - let mut lock = state_ptr.state.borrow_mut(); - if new_appearance != lock.appearance { - lock.appearance = new_appearance; - let mut callback = lock.callbacks.appearance_changed.take()?; - drop(lock); - callback(); - state_ptr.state.borrow_mut().callbacks.appearance_changed = Some(callback); - configure_dwm_dark_mode(handle, new_appearance); - } - } - _ => {} - } - } - } - Some(0) -} - -fn handle_input_language_changed( - lparam: LPARAM, - state_ptr: Rc, -) -> Option { - let thread = state_ptr.main_thread_id_win32; - let validation = state_ptr.validation_number; - unsafe { - PostThreadMessageW(thread, WM_INPUTLANGCHANGE, WPARAM(validation), lparam).log_err(); - } - Some(0) -} - -fn handle_device_change_msg( - handle: HWND, - wparam: WPARAM, - state_ptr: Rc, -) -> Option { - if wparam.0 == DBT_DEVNODES_CHANGED as usize { - // The reason for sending this message is to actually trigger a redraw of the window. - unsafe { - PostMessageW( - Some(handle), - WM_GPUI_FORCE_UPDATE_WINDOW, - WPARAM(0), - LPARAM(0), - ) - .log_err(); - } - // If the GPU device is lost, this redraw will take care of recreating the device context. - // The WM_GPUI_FORCE_UPDATE_WINDOW message will take care of redrawing the window, after - // the device context has been recreated. - draw_window(handle, true, state_ptr) - } else { - // Other device change messages are not handled. - None - } -} - -#[inline] -fn draw_window( - handle: HWND, - force_render: bool, - state_ptr: Rc, -) -> Option { - let mut request_frame = state_ptr - .state - .borrow_mut() - .callbacks - .request_frame - .take()?; - request_frame(RequestFrameOptions { - require_presentation: false, - force_render, - }); - state_ptr.state.borrow_mut().callbacks.request_frame = Some(request_frame); - unsafe { ValidateRect(Some(handle), None).ok().log_err() }; - Some(0) -} - -#[inline] -fn parse_char_message(wparam: WPARAM, state_ptr: &Rc) -> Option { - let code_point = wparam.loword(); - let mut lock = state_ptr.state.borrow_mut(); - // https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-3/#G2630 - match code_point { - 0xD800..=0xDBFF => { - // High surrogate, wait for low surrogate - lock.pending_surrogate = Some(code_point); + // Since these are handled in handle_nc_mouse_up_msg we must prevent the default window proc + if button == MouseButton::Left { + match wparam.0 as u32 { + HTMINBUTTON => self.state.borrow_mut().nc_button_pressed = Some(HTMINBUTTON), + HTMAXBUTTON => self.state.borrow_mut().nc_button_pressed = Some(HTMAXBUTTON), + HTCLOSE => self.state.borrow_mut().nc_button_pressed = Some(HTCLOSE), + _ => return None, + }; + Some(0) + } else { None } - 0xDC00..=0xDFFF => { - if let Some(high_surrogate) = lock.pending_surrogate.take() { - // Low surrogate, combine with pending high surrogate - String::from_utf16(&[high_surrogate, code_point]).ok() - } else { - // Invalid low surrogate without a preceding high surrogate - log::warn!( - "Received low surrogate without a preceding high surrogate: {code_point:x}" - ); - None + } + + fn handle_nc_mouse_up_msg( + &self, + handle: HWND, + button: MouseButton, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + let mut lock = self.state.borrow_mut(); + if let Some(mut func) = lock.callbacks.input.take() { + let scale_factor = lock.scale_factor; + drop(lock); + + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; + let input = PlatformInput::MouseUp(MouseUpEvent { + button, + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + modifiers: current_modifiers(), + click_count: 1, + }); + let handled = !func(input).propagate; + self.state.borrow_mut().callbacks.input = Some(func); + + if handled { + return Some(0); + } + } else { + drop(lock); + } + + let last_pressed = self.state.borrow_mut().nc_button_pressed.take(); + if button == MouseButton::Left + && let Some(last_pressed) = last_pressed + { + let handled = match (wparam.0 as u32, last_pressed) { + (HTMINBUTTON, HTMINBUTTON) => { + unsafe { ShowWindowAsync(handle, SW_MINIMIZE).ok().log_err() }; + true + } + (HTMAXBUTTON, HTMAXBUTTON) => { + if self.state.borrow().is_maximized() { + unsafe { ShowWindowAsync(handle, SW_NORMAL).ok().log_err() }; + } else { + unsafe { ShowWindowAsync(handle, SW_MAXIMIZE).ok().log_err() }; + } + true + } + (HTCLOSE, HTCLOSE) => { + unsafe { + PostMessageW(Some(handle), WM_CLOSE, WPARAM::default(), LPARAM::default()) + .log_err() + }; + true + } + _ => false, + }; + if handled { + return Some(0); } } - _ => { - lock.pending_surrogate = None; - char::from_u32(code_point as u32) - .filter(|c| !c.is_control()) - .map(|c| c.to_string()) + + None + } + + fn handle_cursor_changed(&self, lparam: LPARAM) -> Option { + let mut state = self.state.borrow_mut(); + let had_cursor = state.current_cursor.is_some(); + + state.current_cursor = if lparam.0 == 0 { + None + } else { + Some(HCURSOR(lparam.0 as _)) + }; + + if had_cursor != state.current_cursor.is_some() { + unsafe { SetCursor(state.current_cursor) }; } + + Some(0) + } + + fn handle_set_cursor(&self, handle: HWND, lparam: LPARAM) -> Option { + if unsafe { !IsWindowEnabled(handle).as_bool() } + || matches!( + lparam.loword() as u32, + HTLEFT + | HTRIGHT + | HTTOP + | HTTOPLEFT + | HTTOPRIGHT + | HTBOTTOM + | HTBOTTOMLEFT + | HTBOTTOMRIGHT + ) + { + return None; + } + unsafe { + SetCursor(self.state.borrow().current_cursor); + }; + Some(1) + } + + fn handle_system_settings_changed( + &self, + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + if wparam.0 != 0 { + let mut lock = self.state.borrow_mut(); + let display = lock.display; + lock.system_settings.update(display, wparam.0); + lock.click_state.system_update(wparam.0); + lock.border_offset.update(handle).log_err(); + } else { + self.handle_system_theme_changed(handle, lparam)?; + }; + // Force to trigger WM_NCCALCSIZE event to ensure that we handle auto hide + // taskbar correctly. + notify_frame_changed(handle); + + Some(0) + } + + fn handle_system_command(&self, wparam: WPARAM) -> Option { + if wparam.0 == SC_KEYMENU as usize { + let mut lock = self.state.borrow_mut(); + if lock.system_key_handled { + lock.system_key_handled = false; + return Some(0); + } + } + None + } + + fn handle_system_theme_changed(&self, handle: HWND, lparam: LPARAM) -> Option { + // lParam is a pointer to a string that indicates the area containing the system parameter + // that was changed. + let parameter = PCWSTR::from_raw(lparam.0 as _); + if unsafe { !parameter.is_null() && !parameter.is_empty() } { + if let Some(parameter_string) = unsafe { parameter.to_string() }.log_err() { + log::info!("System settings changed: {}", parameter_string); + match parameter_string.as_str() { + "ImmersiveColorSet" => { + let new_appearance = system_appearance() + .context( + "unable to get system appearance when handling ImmersiveColorSet", + ) + .log_err()?; + let mut lock = self.state.borrow_mut(); + if new_appearance != lock.appearance { + lock.appearance = new_appearance; + let mut callback = lock.callbacks.appearance_changed.take()?; + drop(lock); + callback(); + self.state.borrow_mut().callbacks.appearance_changed = Some(callback); + configure_dwm_dark_mode(handle, new_appearance); + } + } + _ => {} + } + } + } + Some(0) + } + + fn handle_input_language_changed(&self, lparam: LPARAM) -> Option { + let thread = self.main_thread_id_win32; + let validation = self.validation_number; + unsafe { + PostThreadMessageW(thread, WM_INPUTLANGCHANGE, WPARAM(validation), lparam).log_err(); + } + Some(0) + } + + fn handle_device_change_msg(&self, handle: HWND, wparam: WPARAM) -> Option { + if wparam.0 == DBT_DEVNODES_CHANGED as usize { + // The reason for sending this message is to actually trigger a redraw of the window. + unsafe { + PostMessageW( + Some(handle), + WM_GPUI_FORCE_UPDATE_WINDOW, + WPARAM(0), + LPARAM(0), + ) + .log_err(); + } + // If the GPU device is lost, this redraw will take care of recreating the device context. + // The WM_GPUI_FORCE_UPDATE_WINDOW message will take care of redrawing the window, after + // the device context has been recreated. + self.draw_window(handle, true) + } else { + // Other device change messages are not handled. + None + } + } + + #[inline] + fn draw_window(&self, handle: HWND, force_render: bool) -> Option { + let mut request_frame = self.state.borrow_mut().callbacks.request_frame.take()?; + request_frame(RequestFrameOptions { + require_presentation: false, + force_render, + }); + self.state.borrow_mut().callbacks.request_frame = Some(request_frame); + unsafe { ValidateRect(Some(handle), None).ok().log_err() }; + Some(0) + } + + #[inline] + fn parse_char_message(&self, wparam: WPARAM) -> Option { + let code_point = wparam.loword(); + let mut lock = self.state.borrow_mut(); + // https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-3/#G2630 + match code_point { + 0xD800..=0xDBFF => { + // High surrogate, wait for low surrogate + lock.pending_surrogate = Some(code_point); + None + } + 0xDC00..=0xDFFF => { + if let Some(high_surrogate) = lock.pending_surrogate.take() { + // Low surrogate, combine with pending high surrogate + String::from_utf16(&[high_surrogate, code_point]).ok() + } else { + // Invalid low surrogate without a preceding high surrogate + log::warn!( + "Received low surrogate without a preceding high surrogate: {code_point:x}" + ); + None + } + } + _ => { + lock.pending_surrogate = None; + char::from_u32(code_point as u32) + .filter(|c| !c.is_control()) + .map(|c| c.to_string()) + } + } + } + + fn start_tracking_mouse(&self, handle: HWND, flags: TRACKMOUSEEVENT_FLAGS) { + let mut lock = self.state.borrow_mut(); + if !lock.hovered { + lock.hovered = true; + unsafe { + TrackMouseEvent(&mut TRACKMOUSEEVENT { + cbSize: std::mem::size_of::() as u32, + dwFlags: flags, + hwndTrack: handle, + dwHoverTime: HOVER_DEFAULT, + }) + .log_err() + }; + if let Some(mut callback) = lock.callbacks.hovered_status_change.take() { + drop(lock); + callback(true); + self.state.borrow_mut().callbacks.hovered_status_change = Some(callback); + } + } + } + + fn with_input_handler(&self, f: F) -> Option + where + F: FnOnce(&mut PlatformInputHandler) -> R, + { + let mut input_handler = self.state.borrow_mut().input_handler.take()?; + let result = f(&mut input_handler); + self.state.borrow_mut().input_handler = Some(input_handler); + Some(result) + } + + fn with_input_handler_and_scale_factor(&self, f: F) -> Option + where + F: FnOnce(&mut PlatformInputHandler, f32) -> Option, + { + let mut lock = self.state.borrow_mut(); + let mut input_handler = lock.input_handler.take()?; + let scale_factor = lock.scale_factor; + drop(lock); + let result = f(&mut input_handler, scale_factor); + self.state.borrow_mut().input_handler = Some(input_handler); + result } } @@ -1543,54 +1511,3 @@ fn notify_frame_changed(handle: HWND) { .log_err(); } } - -fn start_tracking_mouse( - handle: HWND, - state_ptr: &Rc, - flags: TRACKMOUSEEVENT_FLAGS, -) { - let mut lock = state_ptr.state.borrow_mut(); - if !lock.hovered { - lock.hovered = true; - unsafe { - TrackMouseEvent(&mut TRACKMOUSEEVENT { - cbSize: std::mem::size_of::() as u32, - dwFlags: flags, - hwndTrack: handle, - dwHoverTime: HOVER_DEFAULT, - }) - .log_err() - }; - if let Some(mut callback) = lock.callbacks.hovered_status_change.take() { - drop(lock); - callback(true); - state_ptr.state.borrow_mut().callbacks.hovered_status_change = Some(callback); - } - } -} - -fn with_input_handler(state_ptr: &Rc, f: F) -> Option -where - F: FnOnce(&mut PlatformInputHandler) -> R, -{ - let mut input_handler = state_ptr.state.borrow_mut().input_handler.take()?; - let result = f(&mut input_handler); - state_ptr.state.borrow_mut().input_handler = Some(input_handler); - Some(result) -} - -fn with_input_handler_and_scale_factor( - state_ptr: &Rc, - f: F, -) -> Option -where - F: FnOnce(&mut PlatformInputHandler, f32) -> Option, -{ - let mut lock = state_ptr.state.borrow_mut(); - let mut input_handler = lock.input_handler.take()?; - let scale_factor = lock.scale_factor; - drop(lock); - let result = f(&mut input_handler, scale_factor); - state_ptr.state.borrow_mut().input_handler = Some(input_handler); - result -} diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index bc09cc199d..01b043a755 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -144,12 +144,12 @@ impl WindowsPlatform { } } - pub fn try_get_windows_inner_from_hwnd(&self, hwnd: HWND) -> Option> { + pub fn window_from_hwnd(&self, hwnd: HWND) -> Option> { self.raw_window_handles .read() .iter() .find(|entry| *entry == &hwnd) - .and_then(|hwnd| try_get_window_inner(*hwnd)) + .and_then(|hwnd| window_from_hwnd(*hwnd)) } #[inline] @@ -434,7 +434,7 @@ impl Platform for WindowsPlatform { fn active_window(&self) -> Option { let active_window_hwnd = unsafe { GetActiveWindow() }; - self.try_get_windows_inner_from_hwnd(active_window_hwnd) + self.window_from_hwnd(active_window_hwnd) .map(|inner| inner.handle) } diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index 68b667569b..4043001a35 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -28,7 +28,7 @@ use windows::{ use crate::*; -pub(crate) struct WindowsWindow(pub Rc); +pub(crate) struct WindowsWindow(pub Rc); pub struct WindowsWindowState { pub origin: Point, @@ -61,9 +61,9 @@ pub struct WindowsWindowState { hwnd: HWND, } -pub(crate) struct WindowsWindowStatePtr { +pub(crate) struct WindowsWindowInner { hwnd: HWND, - this: Weak, + pub(super) this: Weak, drop_target_helper: IDropTargetHelper, pub(crate) state: RefCell, pub(crate) handle: AnyWindowHandle, @@ -79,7 +79,7 @@ pub(crate) struct WindowsWindowStatePtr { impl WindowsWindowState { fn new( hwnd: HWND, - cs: &CREATESTRUCTW, + window_params: &CREATESTRUCTW, current_cursor: Option, display: WindowsDisplay, min_size: Option>, @@ -90,9 +90,12 @@ impl WindowsWindowState { let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32; monitor_dpi / USER_DEFAULT_SCREEN_DPI as f32 }; - let origin = logical_point(cs.x as f32, cs.y as f32, scale_factor); + let origin = logical_point(window_params.x as f32, window_params.y as f32, scale_factor); let logical_size = { - let physical_size = size(DevicePixels(cs.cx), DevicePixels(cs.cy)); + let physical_size = size( + DevicePixels(window_params.cx), + DevicePixels(window_params.cy), + ); physical_size.to_pixels(scale_factor) }; let fullscreen_restore_bounds = Bounds { @@ -201,7 +204,7 @@ impl WindowsWindowState { } } -impl WindowsWindowStatePtr { +impl WindowsWindowInner { fn new(context: &WindowCreateContext, hwnd: HWND, cs: &CREATESTRUCTW) -> Result> { let state = RefCell::new(WindowsWindowState::new( hwnd, @@ -230,13 +233,13 @@ impl WindowsWindowStatePtr { } fn toggle_fullscreen(&self) { - let Some(state_ptr) = self.this.upgrade() else { + let Some(this) = self.this.upgrade() else { log::error!("Unable to toggle fullscreen: window has been dropped"); return; }; self.executor .spawn(async move { - let mut lock = state_ptr.state.borrow_mut(); + let mut lock = this.state.borrow_mut(); let StyleAndBounds { style, x, @@ -248,10 +251,9 @@ impl WindowsWindowStatePtr { } else { let (window_bounds, _) = lock.calculate_window_bounds(); lock.fullscreen_restore_bounds = window_bounds; - let style = - WINDOW_STYLE(unsafe { get_window_long(state_ptr.hwnd, GWL_STYLE) } as _); + let style = WINDOW_STYLE(unsafe { get_window_long(this.hwnd, GWL_STYLE) } as _); let mut rc = RECT::default(); - unsafe { GetWindowRect(state_ptr.hwnd, &mut rc) }.log_err(); + unsafe { GetWindowRect(this.hwnd, &mut rc) }.log_err(); let _ = lock.fullscreen.insert(StyleAndBounds { style, x: rc.left, @@ -275,10 +277,10 @@ impl WindowsWindowStatePtr { } }; drop(lock); - unsafe { set_window_long(state_ptr.hwnd, GWL_STYLE, style.0 as isize) }; + unsafe { set_window_long(this.hwnd, GWL_STYLE, style.0 as isize) }; unsafe { SetWindowPos( - state_ptr.hwnd, + this.hwnd, None, x, y, @@ -328,7 +330,7 @@ pub(crate) struct Callbacks { } struct WindowCreateContext { - inner: Option>>, + inner: Option>>, handle: AnyWindowHandle, hide_title_bar: bool, display: WindowsDisplay, @@ -362,13 +364,13 @@ impl WindowsWindow { main_thread_id_win32, disable_direct_composition, } = creation_info; - let classname = register_wnd_class(icon); + register_window_class(icon); let hide_title_bar = params .titlebar .as_ref() .map(|titlebar| titlebar.appears_transparent) .unwrap_or(true); - let windowname = HSTRING::from( + let window_name = HSTRING::from( params .titlebar .as_ref() @@ -414,12 +416,11 @@ impl WindowsWindow { appearance, disable_direct_composition, }; - let lpparam = Some(&context as *const _ as *const _); let creation_result = unsafe { CreateWindowExW( dwexstyle, - classname, - &windowname, + WINDOW_CLASS_NAME, + &window_name, dwstyle, CW_USEDEFAULT, CW_USEDEFAULT, @@ -428,33 +429,35 @@ impl WindowsWindow { None, None, Some(hinstance.into()), - lpparam, + Some(&context as *const _ as *const _), ) }; - // We should call `?` on state_ptr first, then call `?` on hwnd. - // Or, we will lose the error info reported by `WindowsWindowState::new` - let state_ptr = context.inner.take().unwrap()?; + + // Failure to create a `WindowsWindowState` can cause window creation to fail, + // so check the inner result first. + let this = context.inner.take().unwrap()?; let hwnd = creation_result?; - register_drag_drop(state_ptr.clone())?; + + register_drag_drop(&this)?; configure_dwm_dark_mode(hwnd, appearance); - state_ptr.state.borrow_mut().border_offset.update(hwnd)?; + this.state.borrow_mut().border_offset.update(hwnd)?; let placement = retrieve_window_placement( hwnd, display, params.bounds, - state_ptr.state.borrow().scale_factor, - state_ptr.state.borrow().border_offset, + this.state.borrow().scale_factor, + this.state.borrow().border_offset, )?; if params.show { unsafe { SetWindowPlacement(hwnd, &placement)? }; } else { - state_ptr.state.borrow_mut().initial_placement = Some(WindowOpenStatus { + this.state.borrow_mut().initial_placement = Some(WindowOpenStatus { placement, state: WindowOpenState::Windowed, }); } - Ok(Self(state_ptr)) + Ok(Self(this)) } } @@ -803,7 +806,7 @@ impl PlatformWindow for WindowsWindow { } #[implement(IDropTarget)] -struct WindowsDragDropHandler(pub Rc); +struct WindowsDragDropHandler(pub Rc); impl WindowsDragDropHandler { fn handle_drag_drop(&self, input: PlatformInput) { @@ -1084,15 +1087,15 @@ enum WindowOpenState { Windowed, } -fn register_wnd_class(icon_handle: HICON) -> PCWSTR { - const CLASS_NAME: PCWSTR = w!("Zed::Window"); +const WINDOW_CLASS_NAME: PCWSTR = w!("Zed::Window"); +fn register_window_class(icon_handle: HICON) { static ONCE: Once = Once::new(); ONCE.call_once(|| { let wc = WNDCLASSW { - lpfnWndProc: Some(wnd_proc), + lpfnWndProc: Some(window_procedure), hIcon: icon_handle, - lpszClassName: PCWSTR(CLASS_NAME.as_ptr()), + lpszClassName: PCWSTR(WINDOW_CLASS_NAME.as_ptr()), style: CS_HREDRAW | CS_VREDRAW, hInstance: get_module_handle().into(), hbrBackground: unsafe { CreateSolidBrush(COLORREF(0x00000000)) }, @@ -1100,54 +1103,58 @@ fn register_wnd_class(icon_handle: HICON) -> PCWSTR { }; unsafe { RegisterClassW(&wc) }; }); - - CLASS_NAME } -unsafe extern "system" fn wnd_proc( +unsafe extern "system" fn window_procedure( hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM, ) -> LRESULT { if msg == WM_NCCREATE { - let cs = lparam.0 as *const CREATESTRUCTW; - let cs = unsafe { &*cs }; - let ctx = cs.lpCreateParams as *mut WindowCreateContext; - let ctx = unsafe { &mut *ctx }; - let creation_result = WindowsWindowStatePtr::new(ctx, hwnd, cs); - if creation_result.is_err() { - ctx.inner = Some(creation_result); - return LRESULT(0); - } - let weak = Box::new(Rc::downgrade(creation_result.as_ref().unwrap())); - unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) }; - ctx.inner = Some(creation_result); - return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }; + let window_params = lparam.0 as *const CREATESTRUCTW; + let window_params = unsafe { &*window_params }; + let window_creation_context = window_params.lpCreateParams as *mut WindowCreateContext; + let window_creation_context = unsafe { &mut *window_creation_context }; + return match WindowsWindowInner::new(window_creation_context, hwnd, window_params) { + Ok(window_state) => { + let weak = Box::new(Rc::downgrade(&window_state)); + unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) }; + window_creation_context.inner = Some(Ok(window_state)); + unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } + } + Err(error) => { + window_creation_context.inner = Some(Err(error)); + LRESULT(0) + } + }; } - let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; + + let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; if ptr.is_null() { return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }; } let inner = unsafe { &*ptr }; - let r = if let Some(state) = inner.upgrade() { - handle_msg(hwnd, msg, wparam, lparam, state) + let result = if let Some(inner) = inner.upgrade() { + inner.handle_msg(hwnd, msg, wparam, lparam) } else { unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } }; + if msg == WM_NCDESTROY { unsafe { set_window_long(hwnd, GWLP_USERDATA, 0) }; unsafe { drop(Box::from_raw(ptr)) }; } - r + + result } -pub(crate) fn try_get_window_inner(hwnd: HWND) -> Option> { +pub(crate) fn window_from_hwnd(hwnd: HWND) -> Option> { if hwnd.is_invalid() { return None; } - let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; + let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; if !ptr.is_null() { let inner = unsafe { &*ptr }; inner.upgrade() @@ -1170,9 +1177,9 @@ fn get_module_handle() -> HMODULE { } } -fn register_drag_drop(state_ptr: Rc) -> Result<()> { - let window_handle = state_ptr.hwnd; - let handler = WindowsDragDropHandler(state_ptr); +fn register_drag_drop(window: &Rc) -> Result<()> { + let window_handle = window.hwnd; + let handler = WindowsDragDropHandler(window.clone()); // The lifetime of `IDropTarget` is handled by Windows, it won't release until // we call `RevokeDragDrop`. // So, it's safe to drop it here. From 1325bf1420763004238877217df76b13aca760f0 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Mon, 4 Aug 2025 15:45:17 -0300 Subject: [PATCH 057/693] Update to acp 0.0.18 (#35595) Release Notes: - N/A --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- crates/acp_thread/src/acp_thread.rs | 12 ++++++------ crates/agent_servers/src/acp/v0.rs | 4 ++-- crates/agent_servers/src/acp/v1.rs | 18 ++++++++++++------ crates/agent_servers/src/claude/mcp_server.rs | 4 ++-- crates/agent_servers/src/claude/tools.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 8 ++++---- 8 files changed, 30 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c21aec93ed..cf35a467a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,9 +137,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.17" +version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22c5180e40d31a9998ffa5f8eb067667f0870908a4aeed65a6a299e2d1d95443" +checksum = "f8e4c1dccb35e69d32566f0d11948d902f9942fc3f038821816c1150cf5925f4" dependencies = [ "anyhow", "futures 0.3.31", diff --git a/Cargo.toml b/Cargo.toml index 80796018eb..d5982116f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -421,7 +421,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.17" +agent-client-protocol = "0.0.18" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 079a207358..44190a4860 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -178,7 +178,7 @@ impl ToolCall { id: tool_call.id, label: cx.new(|cx| { Markdown::new( - tool_call.label.into(), + tool_call.title.into(), Some(language_registry.clone()), None, cx, @@ -205,7 +205,7 @@ impl ToolCall { let acp::ToolCallUpdateFields { kind, status, - label, + title, content, locations, raw_input, @@ -219,8 +219,8 @@ impl ToolCall { self.status = ToolCallStatus::Allowed { status }; } - if let Some(label) = label { - self.label = cx.new(|cx| Markdown::new_text(label.into(), cx)); + if let Some(title) = title { + self.label = cx.new(|cx| Markdown::new_text(title.into(), cx)); } if let Some(content) = content { @@ -1504,7 +1504,7 @@ mod tests { thread.handle_session_update( acp::SessionUpdate::ToolCall(acp::ToolCall { id: id.clone(), - label: "Label".into(), + title: "Label".into(), kind: acp::ToolKind::Fetch, status: acp::ToolCallStatus::InProgress, content: vec![], @@ -1608,7 +1608,7 @@ mod tests { thread.handle_session_update( acp::SessionUpdate::ToolCall(acp::ToolCall { id: acp::ToolCallId("test".into()), - label: "Label".into(), + title: "Label".into(), kind: acp::ToolKind::Edit, status: acp::ToolCallStatus::Completed, content: vec![acp::ToolCallContent::Diff { diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index 6839ff2462..3dcda4ce8d 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -127,7 +127,7 @@ impl acp_old::Client for OldAcpClientDelegate { outcomes.push(outcome); acp_options.push(acp::PermissionOption { id: acp::PermissionOptionId(index.to_string().into()), - label, + name: label, kind, }) } @@ -266,7 +266,7 @@ impl acp_old::Client for OldAcpClientDelegate { fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall { acp::ToolCall { id: id, - label: request.label, + title: request.label, kind: acp_kind_from_old_icon(request.icon), status: acp::ToolCallStatus::InProgress, content: request diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index 9e2193ce18..a4f0e996b5 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -1,4 +1,5 @@ use agent_client_protocol::{self as acp, Agent as _}; +use anyhow::anyhow; use collections::HashMap; use futures::channel::oneshot; use project::Project; @@ -105,11 +106,16 @@ impl AgentConnection for AcpConnection { mcp_servers: vec![], cwd, }) - .await?; + .await + .map_err(|err| { + if err.code == acp::ErrorCode::AUTH_REQUIRED.code { + anyhow!(AuthRequired) + } else { + anyhow!(err) + } + })?; - let Some(session_id) = response.session_id else { - anyhow::bail!(AuthRequired); - }; + let session_id = response.session_id; let thread = cx.new(|cx| { AcpThread::new( @@ -155,11 +161,11 @@ impl AgentConnection for AcpConnection { fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { let conn = self.connection.clone(); - let params = acp::CancelledNotification { + let params = acp::CancelNotification { session_id: session_id.clone(), }; cx.foreground_executor() - .spawn(async move { conn.cancelled(params).await }) + .spawn(async move { conn.cancel(params).await }) .detach(); } } diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/claude/mcp_server.rs index cc303016f1..c6f8bb5b69 100644 --- a/crates/agent_servers/src/claude/mcp_server.rs +++ b/crates/agent_servers/src/claude/mcp_server.rs @@ -158,12 +158,12 @@ impl McpServerTool for PermissionTool { vec![ acp::PermissionOption { id: allow_option_id.clone(), - label: "Allow".into(), + name: "Allow".into(), kind: acp::PermissionOptionKind::AllowOnce, }, acp::PermissionOption { id: reject_option_id.clone(), - label: "Reject".into(), + name: "Reject".into(), kind: acp::PermissionOptionKind::RejectOnce, }, ], diff --git a/crates/agent_servers/src/claude/tools.rs b/crates/agent_servers/src/claude/tools.rs index 6acb6355aa..e7d33e5298 100644 --- a/crates/agent_servers/src/claude/tools.rs +++ b/crates/agent_servers/src/claude/tools.rs @@ -308,7 +308,7 @@ impl ClaudeTool { id, kind: self.kind(), status: acp::ToolCallStatus::InProgress, - label: self.label(), + title: self.label(), content: self.content(), locations: self.locations(), raw_input: None, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 9ea9209189..a8e2d59b62 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1233,7 +1233,7 @@ impl AcpThreadView { }) .children(options.iter().map(|option| { let option_id = SharedString::from(option.id.0.clone()); - Button::new((option_id, entry_ix), option.label.clone()) + Button::new((option_id, entry_ix), option.name.clone()) .map(|this| match option.kind { acp::PermissionOptionKind::AllowOnce => { this.icon(IconName::Check).icon_color(Color::Success) @@ -2465,7 +2465,7 @@ impl Render for AcpThreadView { connection.auth_methods().into_iter().map(|method| { Button::new( SharedString::from(method.id.0.clone()), - method.label.clone(), + method.name.clone(), ) .on_click({ let method_id = method.id.clone(); @@ -2773,7 +2773,7 @@ mod tests { let tool_call_id = acp::ToolCallId("1".into()); let tool_call = acp::ToolCall { id: tool_call_id.clone(), - label: "Label".into(), + title: "Label".into(), kind: acp::ToolKind::Edit, status: acp::ToolCallStatus::Pending, content: vec!["hi".into()], @@ -2785,7 +2785,7 @@ mod tests { tool_call_id, vec![acp::PermissionOption { id: acp::PermissionOptionId("1".into()), - label: "Allow".into(), + name: "Allow".into(), kind: acp::PermissionOptionKind::AllowOnce, }], )])); From 6e77c6a5ef1e0d6e4b137b4ec07dff1c332c8b2a Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:02:29 -0300 Subject: [PATCH 058/693] onboarding: Adjust the welcome page a bit (#35600) Release Notes: - N/A --- crates/onboarding/src/welcome.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index 3d2c034367..213032f1b3 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -87,18 +87,18 @@ impl Section { ) -> impl IntoElement { v_flex() .min_w_full() - .gap_2() .child( h_flex() .px_1() - .gap_4() + .mb_2() + .gap_2() .child( Label::new(self.title.to_ascii_uppercase()) .buffer_font(cx) .color(Color::Muted) .size(LabelSize::XSmall), ) - .child(Divider::horizontal().color(DividerColor::Border)), + .child(Divider::horizontal().color(DividerColor::BorderVariant)), ) .children( self.entries @@ -125,10 +125,10 @@ impl SectionEntry { ) -> impl IntoElement { ButtonLike::new(("onboarding-button-id", button_index)) .full_width() + .size(ButtonSize::Medium) .child( h_flex() .w_full() - .gap_1() .justify_between() .child( h_flex() @@ -140,7 +140,10 @@ impl SectionEntry { ) .child(Label::new(self.title)), ) - .children(KeyBinding::for_action_in(self.action, focus, window, cx)), + .children( + KeyBinding::for_action_in(self.action, focus, window, cx) + .map(|s| s.size(rems_from_px(12.))), + ), ) .on_click(|_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx)) } @@ -191,8 +194,8 @@ impl Render for WelcomePage { ) .child( v_flex() - .mt_12() - .gap_8() + .mt_10() + .gap_6() .child(first_section.render( Default::default(), &self.focus_handle, @@ -213,10 +216,9 @@ impl Render for WelcomePage { // We call this a hack .rounded_b_xs() .border_t_1() - .border_color(DividerColor::Border.hsla(cx)) + .border_color(cx.theme().colors().border.opacity(0.6)) .border_dashed() .child( - div().child( Button::new("welcome-exit", "Return to Setup") .full_width() .label_size(LabelSize::XSmall) @@ -278,7 +280,6 @@ impl Render for WelcomePage { }); }), ), - ), ), ), ), From 3df5394a8c4a4f4aebdbe0fac90b862327b7c41a Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Mon, 4 Aug 2025 15:35:19 -0400 Subject: [PATCH 059/693] linux: Make desktop file executable (#35597) Closes https://github.com/zed-industries/zed/issues/35545 Release Notes: - linux: Improved support for `zed://` urls on Linux --- crates/zed/resources/flatpak/manifest-template.json | 2 +- docs/src/development/linux.md | 2 +- nix/build.nix | 1 + script/bundle-freebsd | 1 + script/bundle-linux | 1 + 5 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/zed/resources/flatpak/manifest-template.json b/crates/zed/resources/flatpak/manifest-template.json index 1560027e9f..0a14a1c2b0 100644 --- a/crates/zed/resources/flatpak/manifest-template.json +++ b/crates/zed/resources/flatpak/manifest-template.json @@ -38,7 +38,7 @@ }, "build-commands": [ "install -Dm644 $ICON_FILE.png /app/share/icons/hicolor/512x512/apps/$APP_ID.png", - "envsubst < zed.desktop.in > zed.desktop && install -Dm644 zed.desktop /app/share/applications/$APP_ID.desktop", + "envsubst < zed.desktop.in > zed.desktop && install -Dm755 zed.desktop /app/share/applications/$APP_ID.desktop", "envsubst < flatpak/zed.metainfo.xml.in > zed.metainfo.xml && install -Dm644 zed.metainfo.xml /app/share/metainfo/$APP_ID.metainfo.xml", "sed -i -e '/@release_info@/{r flatpak/release-info/$CHANNEL' -e 'd}' /app/share/metainfo/$APP_ID.metainfo.xml", "install -Dm755 bin/zed /app/bin/zed", diff --git a/docs/src/development/linux.md b/docs/src/development/linux.md index 6fff25f6c1..d7b586be34 100644 --- a/docs/src/development/linux.md +++ b/docs/src/development/linux.md @@ -91,7 +91,7 @@ Zed has two main binaries: - You will need to build `crates/cli` and make its binary available in `$PATH` with the name `zed`. - You will need to build `crates/zed` and put it at `$PATH/to/cli/../../libexec/zed-editor`. For example, if you are going to put the cli at `~/.local/bin/zed` put zed at `~/.local/libexec/zed-editor`. As some linux distributions (notably Arch) discourage the use of `libexec`, you can also put this binary at `$PATH/to/cli/../../lib/zed/zed-editor` (e.g. `~/.local/lib/zed/zed-editor`) instead. -- If you are going to provide a `.desktop` file you can find a template in `crates/zed/resources/zed.desktop.in`, and use `envsubst` to populate it with the values required. This file should also be renamed to `$APP_ID.desktop` so that the file [follows the FreeDesktop standards](https://github.com/zed-industries/zed/issues/12707#issuecomment-2168742761). +- If you are going to provide a `.desktop` file you can find a template in `crates/zed/resources/zed.desktop.in`, and use `envsubst` to populate it with the values required. This file should also be renamed to `$APP_ID.desktop` so that the file [follows the FreeDesktop standards](https://github.com/zed-industries/zed/issues/12707#issuecomment-2168742761). You should also make this desktop file executable (`chmod 755`). - You will need to ensure that the necessary libraries are installed. You can get the current list by [inspecting the built binary](https://github.com/zed-industries/zed/blob/935cf542aebf55122ce6ed1c91d0fe8711970c82/script/bundle-linux#L65-L67) on your system. - For an example of a complete build script, see [script/bundle-linux](https://github.com/zed-industries/zed/blob/935cf542aebf55122ce6ed1c91d0fe8711970c82/script/bundle-linux). - You can disable Zed's auto updates and provide instructions for users who try to update Zed manually by building (or running) Zed with the environment variable `ZED_UPDATE_EXPLANATION`. For example: `ZED_UPDATE_EXPLANATION="Please use flatpak to update zed."`. diff --git a/nix/build.nix b/nix/build.nix index 873431a427..70b4f76932 100644 --- a/nix/build.nix +++ b/nix/build.nix @@ -298,6 +298,7 @@ craneLib.buildPackage ( export APP_ARGS="%U" mkdir -p "$out/share/applications" ${lib.getExe envsubst} < "crates/zed/resources/zed.desktop.in" > "$out/share/applications/dev.zed.Zed-Nightly.desktop" + chmod +x "$out/share/applications/dev.zed.Zed-Nightly.desktop" ) runHook postInstall diff --git a/script/bundle-freebsd b/script/bundle-freebsd index 7222a06256..87c9459ffb 100755 --- a/script/bundle-freebsd +++ b/script/bundle-freebsd @@ -138,6 +138,7 @@ fi # mkdir -p "${zed_dir}/share/applications" # envsubst <"crates/zed/resources/zed.desktop.in" >"${zed_dir}/share/applications/zed$suffix.desktop" +# chmod +x "${zed_dir}/share/applications/zed$suffix.desktop" # Copy generated licenses so they'll end up in archive too # cp "assets/licenses.md" "${zed_dir}/licenses.md" diff --git a/script/bundle-linux b/script/bundle-linux index 64de62ce9b..ad67b7a0f7 100755 --- a/script/bundle-linux +++ b/script/bundle-linux @@ -179,6 +179,7 @@ fi mkdir -p "${zed_dir}/share/applications" envsubst < "crates/zed/resources/zed.desktop.in" > "${zed_dir}/share/applications/zed$suffix.desktop" +chmod +x "${zed_dir}/share/applications/zed$suffix.desktop" # Copy generated licenses so they'll end up in archive too cp "assets/licenses.md" "${zed_dir}/licenses.md" From 68c24655e98f31991513336a0ead975885d7c294 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 4 Aug 2025 14:18:06 -0600 Subject: [PATCH 060/693] zeta: Collect git sha / remote urls when data collection from OSS is enabled (#35514) Release Notes: - Edit Prediction: Added Git info to edit predictions requests (only sent for opensource projects when data collection is enabled). The sent Git info is the SHA of the current commit and the URLs for the `origin` and `upstream` remotes. --- .../cloud_llm_client/src/cloud_llm_client.rs | 16 +++++++ crates/project/src/git_store.rs | 10 +++++ crates/zeta/src/zeta.rs | 44 ++++++++++++++++++- crates/zeta_cli/src/main.rs | 2 + 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/crates/cloud_llm_client/src/cloud_llm_client.rs b/crates/cloud_llm_client/src/cloud_llm_client.rs index 171c923154..e78957ec49 100644 --- a/crates/cloud_llm_client/src/cloud_llm_client.rs +++ b/crates/cloud_llm_client/src/cloud_llm_client.rs @@ -149,6 +149,22 @@ pub struct PredictEditsBody { pub can_collect_data: bool, #[serde(skip_serializing_if = "Option::is_none", default)] pub diagnostic_groups: Option>, + /// Info about the git repository state, only present when can_collect_data is true. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub git_info: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PredictEditsGitInfo { + /// SHA of git HEAD commit at time of prediction. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub head_sha: Option, + /// URL of the remote called `origin`. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub remote_origin_url: Option, + /// URL of the remote called `upstream`. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub remote_upstream_url: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index c9f0fc7959..01fc987816 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -246,6 +246,8 @@ pub struct RepositorySnapshot { pub head_commit: Option, pub scan_id: u64, pub merge: MergeDetails, + pub remote_origin_url: Option, + pub remote_upstream_url: Option, } type JobId = u64; @@ -2673,6 +2675,8 @@ impl RepositorySnapshot { head_commit: None, scan_id: 0, merge: Default::default(), + remote_origin_url: None, + remote_upstream_url: None, } } @@ -4818,6 +4822,10 @@ async fn compute_snapshot( None => None, }; + // Used by edit prediction data collection + let remote_origin_url = backend.remote_url("origin"); + let remote_upstream_url = backend.remote_url("upstream"); + let snapshot = RepositorySnapshot { id, statuses_by_path, @@ -4826,6 +4834,8 @@ async fn compute_snapshot( branch, head_commit, merge: merge_details, + remote_origin_url, + remote_upstream_url, }; Ok((snapshot, events)) diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 1cd8e8d17f..b1bd737dbf 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -19,7 +19,7 @@ use arrayvec::ArrayVec; use client::{Client, EditPredictionUsage, UserStore}; use cloud_llm_client::{ AcceptEditPredictionBody, EXPIRED_LLM_TOKEN_HEADER_NAME, MINIMUM_REQUIRED_VERSION_HEADER_NAME, - PredictEditsBody, PredictEditsResponse, ZED_VERSION_HEADER_NAME, + PredictEditsBody, PredictEditsGitInfo, PredictEditsResponse, ZED_VERSION_HEADER_NAME, }; use collections::{HashMap, HashSet, VecDeque}; use futures::AsyncReadExt; @@ -34,7 +34,7 @@ use language::{ }; use language_model::{LlmApiToken, RefreshLlmTokenListener}; use postage::watch; -use project::Project; +use project::{Project, ProjectPath}; use release_channel::AppVersion; use settings::WorktreeId; use std::str::FromStr; @@ -400,6 +400,14 @@ impl Zeta { let llm_token = self.llm_token.clone(); let app_version = AppVersion::global(cx); + let git_info = if let (true, Some(project), Some(file)) = + (can_collect_data, project, snapshot.file()) + { + git_info_for_file(project, &ProjectPath::from_file(file.as_ref(), cx), cx) + } else { + None + }; + let full_path: Arc = snapshot .file() .map(|f| Arc::from(f.full_path(cx).as_path())) @@ -415,6 +423,7 @@ impl Zeta { cursor_point, make_events_prompt, can_collect_data, + git_info, cx, ); @@ -1155,6 +1164,35 @@ fn common_prefix, T2: Iterator>(a: T1, b: .sum() } +fn git_info_for_file( + project: &Entity, + project_path: &ProjectPath, + cx: &App, +) -> Option { + let git_store = project.read(cx).git_store().read(cx); + if let Some((repository, _repo_path)) = + git_store.repository_and_path_for_project_path(project_path, cx) + { + let repository = repository.read(cx); + let head_sha = repository + .head_commit + .as_ref() + .map(|head_commit| head_commit.sha.to_string()); + let remote_origin_url = repository.remote_origin_url.clone(); + let remote_upstream_url = repository.remote_upstream_url.clone(); + if head_sha.is_none() && remote_origin_url.is_none() && remote_upstream_url.is_none() { + return None; + } + Some(PredictEditsGitInfo { + head_sha, + remote_origin_url, + remote_upstream_url, + }) + } else { + None + } +} + pub struct GatherContextOutput { pub body: PredictEditsBody, pub editable_range: Range, @@ -1167,6 +1205,7 @@ pub fn gather_context( cursor_point: language::Point, make_events_prompt: impl FnOnce() -> String + Send + 'static, can_collect_data: bool, + git_info: Option, cx: &App, ) -> Task> { let local_lsp_store = @@ -1216,6 +1255,7 @@ pub fn gather_context( outline: Some(input_outline), can_collect_data, diagnostic_groups, + git_info, }; Ok(GatherContextOutput { diff --git a/crates/zeta_cli/src/main.rs b/crates/zeta_cli/src/main.rs index c5374b56c9..adf7683152 100644 --- a/crates/zeta_cli/src/main.rs +++ b/crates/zeta_cli/src/main.rs @@ -172,6 +172,7 @@ async fn get_context( None => String::new(), }; let can_collect_data = false; + let git_info = None; cx.update(|cx| { gather_context( project.as_ref(), @@ -180,6 +181,7 @@ async fn get_context( clipped_cursor, move || events, can_collect_data, + git_info, cx, ) })? From 24e7f868ad24439e8723005f6fb62d55c01d0e45 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 4 Aug 2025 16:56:56 -0500 Subject: [PATCH 061/693] onboarding: Go back to not having system be separate (#35499) Going back to having system be mutually exclusive with light/dark to simplify the system. We instead just show both light and dark when system is selected Release Notes: - N/A --------- Co-authored-by: Danilo Leal Co-authored-by: Anthony Eid --- crates/gpui/src/geometry.rs | 2 +- crates/onboarding/src/basics_page.rs | 302 +++++++++--------- crates/onboarding/src/theme_preview.rs | 421 ++++++++++++++++--------- 3 files changed, 415 insertions(+), 310 deletions(-) diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index 74be6344f9..3d2d9cd9db 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -3522,7 +3522,7 @@ impl Serialize for Length { /// # Returns /// /// A `DefiniteLength` representing the relative length as a fraction of the parent's size. -pub fn relative(fraction: f32) -> DefiniteLength { +pub const fn relative(fraction: f32) -> DefiniteLength { DefiniteLength::Fraction(fraction) } diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 82688e6220..21ea74f01c 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -1,42 +1,24 @@ +use std::sync::Arc; + use client::TelemetrySettings; use fs::Fs; -use gpui::{App, Entity, IntoElement, Window}; +use gpui::{App, IntoElement, Window}; use settings::{BaseKeymap, Settings, update_settings_file}; -use theme::{Appearance, ThemeMode, ThemeName, ThemeRegistry, ThemeSelection, ThemeSettings}; +use theme::{ + Appearance, SystemAppearance, ThemeMode, ThemeName, ThemeRegistry, ThemeSelection, + ThemeSettings, +}; use ui::{ ParentElement as _, StatefulInteractiveElement, SwitchField, ToggleButtonGroup, ToggleButtonSimple, ToggleButtonWithIcon, prelude::*, rems_from_px, }; use vim_mode_setting::VimModeSetting; -use crate::theme_preview::ThemePreviewTile; +use crate::theme_preview::{ThemePreviewStyle, ThemePreviewTile}; -/// separates theme "mode" ("dark" | "light" | "system") into two separate states -/// - appearance = "dark" | "light" -/// - "system" true/false -/// when system selected: -/// - toggling between light and dark does not change theme.mode, just which variant will be changed -/// when system not selected: -/// - toggling between light and dark does change theme.mode -/// selecting a theme preview will always change theme.["light" | "dark"] to the selected theme, -/// -/// this allows for selecting a dark and light theme option regardless of whether the mode is set to system or not -/// it does not support setting theme to a static value -fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement { +fn render_theme_section(_window: &mut Window, cx: &mut App) -> impl IntoElement { let theme_selection = ThemeSettings::get_global(cx).theme_selection.clone(); let system_appearance = theme::SystemAppearance::global(cx); - let appearance_state = window.use_state(cx, |_, _cx| { - theme_selection - .as_ref() - .and_then(|selection| selection.mode()) - .and_then(|mode| match mode { - ThemeMode::System => None, - ThemeMode::Light => Some(Appearance::Light), - ThemeMode::Dark => Some(Appearance::Dark), - }) - .unwrap_or(*system_appearance) - }); - let appearance = *appearance_state.read(cx); let theme_selection = theme_selection.unwrap_or_else(|| ThemeSelection::Dynamic { mode: match *system_appearance { Appearance::Light => ThemeMode::Light, @@ -45,70 +27,13 @@ fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement { light: ThemeName("One Light".into()), dark: ThemeName("One Dark".into()), }); - let theme_registry = ThemeRegistry::global(cx); - let current_theme_name = theme_selection.theme(appearance); - let theme_mode = theme_selection.mode().unwrap_or_default(); - - // let theme_mode = theme_selection.mode(); - // TODO: Clean this up once the "System" button inside the - // toggle button group is done - - let selected_index = match appearance { - Appearance::Light => 0, - Appearance::Dark => 1, - }; - - let theme_seed = 0xBEEF as f32; - - const LIGHT_THEMES: [&'static str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"]; - const DARK_THEMES: [&'static str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"]; - - let theme_names = match appearance { - Appearance::Light => LIGHT_THEMES, - Appearance::Dark => DARK_THEMES, - }; - let themes = theme_names - .map(|theme_name| theme_registry.get(theme_name)) - .map(Result::unwrap); - - let theme_previews = themes.map(|theme| { - let is_selected = theme.name == current_theme_name; - let name = theme.name.clone(); - let colors = cx.theme().colors(); - - v_flex() - .id(name.clone()) - .w_full() - .items_center() - .gap_1() - .child( - div() - .w_full() - .border_2() - .border_color(colors.border_transparent) - .rounded(ThemePreviewTile::CORNER_RADIUS) - .map(|this| { - if is_selected { - this.border_color(colors.border_selected) - } else { - this.opacity(0.8).hover(|s| s.border_color(colors.border)) - } - }) - .child(ThemePreviewTile::new(theme.clone(), theme_seed)), - ) - .child(Label::new(name).color(Color::Muted).size(LabelSize::Small)) - .on_click({ - let theme_name = theme.name.clone(); - move |_, _, cx| { - let fs = ::global(cx); - let theme_name = theme_name.clone(); - update_settings_file::(fs, cx, move |settings, _| { - settings.set_theme(theme_name, appearance); - }); - } - }) - }); + let theme_mode = theme_selection + .mode() + .unwrap_or_else(|| match *system_appearance { + Appearance::Light => ThemeMode::Light, + Appearance::Dark => ThemeMode::Dark, + }); return v_flex() .gap_2() @@ -116,93 +41,148 @@ fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement { h_flex().justify_between().child(Label::new("Theme")).child( ToggleButtonGroup::single_row( "theme-selector-onboarding-dark-light", - [ - ToggleButtonSimple::new("Light", { - let appearance_state = appearance_state.clone(); + [ThemeMode::Light, ThemeMode::Dark, ThemeMode::System].map(|mode| { + const MODE_NAMES: [SharedString; 3] = [ + SharedString::new_static("Light"), + SharedString::new_static("Dark"), + SharedString::new_static("System"), + ]; + ToggleButtonSimple::new( + MODE_NAMES[mode as usize].clone(), move |_, _, cx| { - write_appearance_change(&appearance_state, Appearance::Light, cx); - } - }), - ToggleButtonSimple::new("Dark", { - let appearance_state = appearance_state.clone(); - move |_, _, cx| { - write_appearance_change(&appearance_state, Appearance::Dark, cx); - } - }), - // TODO: Properly put the System back as a button within this group - // Currently, given "System" is not an option in the Appearance enum, - // this button doesn't get selected - ToggleButtonSimple::new("System", { - let theme = theme_selection.clone(); - move |_, _, cx| { - toggle_system_theme_mode(theme.clone(), appearance, cx); - } - }) - .selected(theme_mode == ThemeMode::System), - ], + write_mode_change(mode, cx); + }, + ) + }), ) - .selected_index(selected_index) + .selected_index(theme_mode as usize) .style(ui::ToggleButtonGroupStyle::Outlined) .button_width(rems_from_px(64.)), ), ) - .child(h_flex().gap_4().justify_between().children(theme_previews)); + .child( + h_flex() + .gap_4() + .justify_between() + .children(render_theme_previews(&theme_selection, cx)), + ); - fn write_appearance_change( - appearance_state: &Entity, - new_appearance: Appearance, + fn render_theme_previews( + theme_selection: &ThemeSelection, cx: &mut App, - ) { - let fs = ::global(cx); - appearance_state.write(cx, new_appearance); + ) -> [impl IntoElement; 3] { + let system_appearance = SystemAppearance::global(cx); + let theme_registry = ThemeRegistry::global(cx); - update_settings_file::(fs, cx, move |settings, _| { - if settings.theme.as_ref().and_then(ThemeSelection::mode) == Some(ThemeMode::System) { - return; - } - let new_mode = match new_appearance { + let theme_seed = 0xBEEF as f32; + let theme_mode = theme_selection + .mode() + .unwrap_or_else(|| match *system_appearance { Appearance::Light => ThemeMode::Light, Appearance::Dark => ThemeMode::Dark, - }; - settings.set_mode(new_mode); + }); + let appearance = match theme_mode { + ThemeMode::Light => Appearance::Light, + ThemeMode::Dark => Appearance::Dark, + ThemeMode::System => *system_appearance, + }; + let current_theme_name = theme_selection.theme(appearance); + + const LIGHT_THEMES: [&'static str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"]; + const DARK_THEMES: [&'static str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"]; + const FAMILY_NAMES: [SharedString; 3] = [ + SharedString::new_static("One"), + SharedString::new_static("Ayu"), + SharedString::new_static("Gruvbox"), + ]; + + let theme_names = match appearance { + Appearance::Light => LIGHT_THEMES, + Appearance::Dark => DARK_THEMES, + }; + + let themes = theme_names.map(|theme| theme_registry.get(theme).unwrap()); + + let theme_previews = [0, 1, 2].map(|index| { + let theme = &themes[index]; + let is_selected = theme.name == current_theme_name; + let name = theme.name.clone(); + let colors = cx.theme().colors(); + + v_flex() + .id(name.clone()) + .w_full() + .items_center() + .gap_1() + .child( + h_flex() + .relative() + .w_full() + .border_2() + .border_color(colors.border_transparent) + .rounded(ThemePreviewTile::ROOT_RADIUS) + .map(|this| { + if is_selected { + this.border_color(colors.border_selected) + } else { + this.opacity(0.8).hover(|s| s.border_color(colors.border)) + } + }) + .map(|this| { + if theme_mode == ThemeMode::System { + let (light, dark) = ( + theme_registry.get(LIGHT_THEMES[index]).unwrap(), + theme_registry.get(DARK_THEMES[index]).unwrap(), + ); + this.child( + ThemePreviewTile::new(light, theme_seed) + .style(ThemePreviewStyle::SideBySide(dark)), + ) + } else { + this.child( + ThemePreviewTile::new(theme.clone(), theme_seed) + .style(ThemePreviewStyle::Bordered), + ) + } + }), + ) + .child( + Label::new(FAMILY_NAMES[index].clone()) + .color(Color::Muted) + .size(LabelSize::Small), + ) + .on_click({ + let theme_name = theme.name.clone(); + move |_, _, cx| { + write_theme_change(theme_name.clone(), theme_mode, cx); + } + }) + }); + + theme_previews + } + + fn write_mode_change(mode: ThemeMode, cx: &mut App) { + let fs = ::global(cx); + update_settings_file::(fs, cx, move |settings, _cx| { + settings.set_mode(mode); }); } - fn toggle_system_theme_mode( - theme_selection: ThemeSelection, - appearance: Appearance, - cx: &mut App, - ) { + fn write_theme_change(theme: impl Into>, theme_mode: ThemeMode, cx: &mut App) { let fs = ::global(cx); - - update_settings_file::(fs, cx, move |settings, _| { - settings.theme = Some(match theme_selection { - ThemeSelection::Static(theme_name) => ThemeSelection::Dynamic { + let theme = theme.into(); + update_settings_file::(fs, cx, move |settings, cx| { + if theme_mode == ThemeMode::System { + settings.theme = Some(ThemeSelection::Dynamic { mode: ThemeMode::System, - light: theme_name.clone(), - dark: theme_name.clone(), - }, - ThemeSelection::Dynamic { - mode: ThemeMode::System, - light, - dark, - } => { - let mode = match appearance { - Appearance::Light => ThemeMode::Light, - Appearance::Dark => ThemeMode::Dark, - }; - ThemeSelection::Dynamic { mode, light, dark } - } - ThemeSelection::Dynamic { - mode: _, - light, - dark, - } => ThemeSelection::Dynamic { - mode: ThemeMode::System, - light, - dark, - }, - }); + light: ThemeName(theme.clone()), + dark: ThemeName(theme.clone()), + }); + } else { + let appearance = *SystemAppearance::global(cx); + settings.set_theme(theme.clone(), appearance); + } }); } } diff --git a/crates/onboarding/src/theme_preview.rs b/crates/onboarding/src/theme_preview.rs index d51511b7f4..53631be1c9 100644 --- a/crates/onboarding/src/theme_preview.rs +++ b/crates/onboarding/src/theme_preview.rs @@ -1,175 +1,300 @@ #![allow(unused, dead_code)] use gpui::{Hsla, Length}; use std::sync::Arc; -use theme::{Theme, ThemeRegistry}; +use theme::{Theme, ThemeColors, ThemeRegistry}; use ui::{ IntoElement, RenderOnce, component_prelude::Documented, prelude::*, utils::inner_corner_radius, }; +#[derive(Clone, PartialEq)] +pub enum ThemePreviewStyle { + Bordered, + Borderless, + SideBySide(Arc), +} + /// Shows a preview of a theme as an abstract illustration /// of a thumbnail-sized editor. #[derive(IntoElement, RegisterComponent, Documented)] pub struct ThemePreviewTile { theme: Arc, seed: f32, + style: ThemePreviewStyle, } impl ThemePreviewTile { - pub const CORNER_RADIUS: Pixels = px(8.0); + pub const SKELETON_HEIGHT_DEFAULT: Pixels = px(2.); + pub const SIDEBAR_SKELETON_ITEM_COUNT: usize = 8; + pub const SIDEBAR_WIDTH_DEFAULT: DefiniteLength = relative(0.25); + pub const ROOT_RADIUS: Pixels = px(8.0); + pub const ROOT_BORDER: Pixels = px(2.0); + pub const ROOT_PADDING: Pixels = px(2.0); + pub const CHILD_BORDER: Pixels = px(1.0); + pub const CHILD_RADIUS: std::cell::LazyCell = std::cell::LazyCell::new(|| { + inner_corner_radius( + Self::ROOT_RADIUS, + Self::ROOT_BORDER, + Self::ROOT_PADDING, + Self::CHILD_BORDER, + ) + }); pub fn new(theme: Arc, seed: f32) -> Self { - Self { theme, seed } + Self { + theme, + seed, + style: ThemePreviewStyle::Bordered, + } + } + + pub fn style(mut self, style: ThemePreviewStyle) -> Self { + self.style = style; + self + } + + pub fn item_skeleton(w: Length, h: Length, bg: Hsla) -> impl IntoElement { + div().w(w).h(h).rounded_full().bg(bg) + } + + pub fn render_sidebar_skeleton_items( + seed: f32, + colors: &ThemeColors, + skeleton_height: impl Into + Clone, + ) -> [impl IntoElement; Self::SIDEBAR_SKELETON_ITEM_COUNT] { + let skeleton_height = skeleton_height.into(); + std::array::from_fn(|index| { + let width = { + let value = (seed * 1000.0 + index as f32 * 10.0).sin() * 0.5 + 0.5; + 0.5 + value * 0.45 + }; + Self::item_skeleton( + relative(width).into(), + skeleton_height, + colors.text.alpha(0.45), + ) + }) + } + + pub fn render_pseudo_code_skeleton( + seed: f32, + theme: Arc, + skeleton_height: impl Into, + ) -> impl IntoElement { + let colors = theme.colors(); + let syntax = theme.syntax(); + + let keyword_color = syntax.get("keyword").color; + let function_color = syntax.get("function").color; + let string_color = syntax.get("string").color; + let comment_color = syntax.get("comment").color; + let variable_color = syntax.get("variable").color; + let type_color = syntax.get("type").color; + let punctuation_color = syntax.get("punctuation").color; + + let syntax_colors = [ + keyword_color, + function_color, + string_color, + variable_color, + type_color, + punctuation_color, + comment_color, + ]; + + let skeleton_height = skeleton_height.into(); + + let line_width = |line_idx: usize, block_idx: usize| -> f32 { + let val = + (seed * 100.0 + line_idx as f32 * 20.0 + block_idx as f32 * 5.0).sin() * 0.5 + 0.5; + 0.05 + val * 0.2 + }; + + let indentation = |line_idx: usize| -> f32 { + let step = line_idx % 6; + if step < 3 { + step as f32 * 0.1 + } else { + (5 - step) as f32 * 0.1 + } + }; + + let pick_color = |line_idx: usize, block_idx: usize| -> Hsla { + let idx = ((seed * 10.0 + line_idx as f32 * 7.0 + block_idx as f32 * 3.0).sin() * 3.5) + .abs() as usize + % syntax_colors.len(); + syntax_colors[idx].unwrap_or(colors.text) + }; + + let line_count = 13; + + let lines = (0..line_count) + .map(|line_idx| { + let block_count = (((seed * 30.0 + line_idx as f32 * 12.0).sin() * 0.5 + 0.5) * 3.0) + .round() as usize + + 2; + + let indent = indentation(line_idx); + + let blocks = (0..block_count) + .map(|block_idx| { + let width = line_width(line_idx, block_idx); + let color = pick_color(line_idx, block_idx); + Self::item_skeleton(relative(width).into(), skeleton_height, color) + }) + .collect::>(); + + h_flex().gap(px(2.)).ml(relative(indent)).children(blocks) + }) + .collect::>(); + + v_flex().size_full().p_1().gap_1p5().children(lines) + } + + pub fn render_sidebar( + seed: f32, + colors: &ThemeColors, + width: impl Into + Clone, + skeleton_height: impl Into, + ) -> impl IntoElement { + div() + .h_full() + .w(width) + .border_r(px(1.)) + .border_color(colors.border_transparent) + .bg(colors.panel_background) + .child(v_flex().p_2().size_full().gap_1().children( + Self::render_sidebar_skeleton_items(seed, colors, skeleton_height.into()), + )) + } + + pub fn render_pane( + seed: f32, + theme: Arc, + skeleton_height: impl Into, + ) -> impl IntoElement { + v_flex().h_full().flex_grow().child( + div() + .size_full() + .overflow_hidden() + .bg(theme.colors().editor_background) + .p_2() + .child(Self::render_pseudo_code_skeleton( + seed, + theme, + skeleton_height.into(), + )), + ) + } + + pub fn render_editor( + seed: f32, + theme: Arc, + sidebar_width: impl Into + Clone, + skeleton_height: impl Into + Clone, + ) -> impl IntoElement { + div() + .size_full() + .flex() + .bg(theme.colors().background.alpha(1.00)) + .child(Self::render_sidebar( + seed, + theme.colors(), + sidebar_width, + skeleton_height.clone(), + )) + .child(Self::render_pane(seed, theme, skeleton_height.clone())) + } + + fn render_borderless(seed: f32, theme: Arc) -> impl IntoElement { + return Self::render_editor( + seed, + theme, + Self::SIDEBAR_WIDTH_DEFAULT, + Self::SKELETON_HEIGHT_DEFAULT, + ); + } + + fn render_border(seed: f32, theme: Arc) -> impl IntoElement { + div() + .size_full() + .p(Self::ROOT_PADDING) + .rounded(Self::ROOT_RADIUS) + .child( + div() + .size_full() + .rounded(*Self::CHILD_RADIUS) + .border(Self::CHILD_BORDER) + .border_color(theme.colors().border) + .child(Self::render_editor( + seed, + theme.clone(), + Self::SIDEBAR_WIDTH_DEFAULT, + Self::SKELETON_HEIGHT_DEFAULT, + )), + ) + } + + fn render_side_by_side( + seed: f32, + theme: Arc, + other_theme: Arc, + border_color: Hsla, + ) -> impl IntoElement { + let sidebar_width = relative(0.20); + + return div() + .size_full() + .p(Self::ROOT_PADDING) + .rounded(Self::ROOT_RADIUS) + .child( + h_flex() + .size_full() + .relative() + .rounded(*Self::CHILD_RADIUS) + .border(Self::CHILD_BORDER) + .border_color(border_color) + .overflow_hidden() + .child(div().size_full().child(Self::render_editor( + seed, + theme.clone(), + sidebar_width, + Self::SKELETON_HEIGHT_DEFAULT, + ))) + .child( + div() + .size_full() + .absolute() + .left_1_2() + .bg(other_theme.colors().editor_background) + .child(Self::render_editor( + seed, + other_theme, + sidebar_width, + Self::SKELETON_HEIGHT_DEFAULT, + )), + ), + ) + .into_any_element(); } } impl RenderOnce for ThemePreviewTile { fn render(self, _window: &mut ui::Window, _cx: &mut ui::App) -> impl IntoElement { - let color = self.theme.colors(); - - let root_radius = Self::CORNER_RADIUS; - let root_border = px(2.0); - let root_padding = px(2.0); - let child_border = px(1.0); - let inner_radius = - inner_corner_radius(root_radius, root_border, root_padding, child_border); - - let item_skeleton = |w: Length, h: Pixels, bg: Hsla| div().w(w).h(h).rounded_full().bg(bg); - - let skeleton_height = px(2.); - - let sidebar_seeded_width = |seed: f32, index: usize| { - let value = (seed * 1000.0 + index as f32 * 10.0).sin() * 0.5 + 0.5; - 0.5 + value * 0.45 - }; - - let sidebar_skeleton_items = 8; - - let sidebar_skeleton = (0..sidebar_skeleton_items) - .map(|i| { - let width = sidebar_seeded_width(self.seed, i); - item_skeleton( - relative(width).into(), - skeleton_height, - color.text.alpha(0.45), - ) - }) - .collect::>(); - - let sidebar = div() - .h_full() - .w(relative(0.25)) - .border_r(px(1.)) - .border_color(color.border_transparent) - .bg(color.panel_background) - .child( - v_flex() - .p_2() - .size_full() - .gap_1() - .children(sidebar_skeleton), - ); - - let pseudo_code_skeleton = |theme: Arc, seed: f32| -> AnyElement { - let colors = theme.colors(); - let syntax = theme.syntax(); - - let keyword_color = syntax.get("keyword").color; - let function_color = syntax.get("function").color; - let string_color = syntax.get("string").color; - let comment_color = syntax.get("comment").color; - let variable_color = syntax.get("variable").color; - let type_color = syntax.get("type").color; - let punctuation_color = syntax.get("punctuation").color; - - let syntax_colors = [ - keyword_color, - function_color, - string_color, - variable_color, - type_color, - punctuation_color, - comment_color, - ]; - - let line_width = |line_idx: usize, block_idx: usize| -> f32 { - let val = (seed * 100.0 + line_idx as f32 * 20.0 + block_idx as f32 * 5.0).sin() - * 0.5 - + 0.5; - 0.05 + val * 0.2 - }; - - let indentation = |line_idx: usize| -> f32 { - let step = line_idx % 6; - if step < 3 { - step as f32 * 0.1 - } else { - (5 - step) as f32 * 0.1 - } - }; - - let pick_color = |line_idx: usize, block_idx: usize| -> Hsla { - let idx = ((seed * 10.0 + line_idx as f32 * 7.0 + block_idx as f32 * 3.0).sin() - * 3.5) - .abs() as usize - % syntax_colors.len(); - syntax_colors[idx].unwrap_or(colors.text) - }; - - let line_count = 13; - - let lines = (0..line_count) - .map(|line_idx| { - let block_count = (((seed * 30.0 + line_idx as f32 * 12.0).sin() * 0.5 + 0.5) - * 3.0) - .round() as usize - + 2; - - let indent = indentation(line_idx); - - let blocks = (0..block_count) - .map(|block_idx| { - let width = line_width(line_idx, block_idx); - let color = pick_color(line_idx, block_idx); - item_skeleton(relative(width).into(), skeleton_height, color) - }) - .collect::>(); - - h_flex().gap(px(2.)).ml(relative(indent)).children(blocks) - }) - .collect::>(); - - v_flex() - .size_full() - .p_1() - .gap_1p5() - .children(lines) - .into_any_element() - }; - - let pane = v_flex().h_full().flex_grow().child( - div() - .size_full() - .overflow_hidden() - .bg(color.editor_background) - .p_2() - .child(pseudo_code_skeleton(self.theme.clone(), self.seed)), - ); - - let content = div().size_full().flex().child(sidebar).child(pane); - - div() - .size_full() - .rounded(root_radius) - .p(root_padding) - .child( - div() - .size_full() - .rounded(inner_radius) - .border(child_border) - .border_color(color.border) - .bg(color.background) - .child(content), + match self.style { + ThemePreviewStyle::Bordered => { + Self::render_border(self.seed, self.theme).into_any_element() + } + ThemePreviewStyle::Borderless => { + Self::render_borderless(self.seed, self.theme).into_any_element() + } + ThemePreviewStyle::SideBySide(other_theme) => Self::render_side_by_side( + self.seed, + self.theme, + other_theme, + _cx.theme().colors().border, ) + .into_any_element(), + } } } From 182edbf526302000ca0e6cf5a14085a6a6b1034c Mon Sep 17 00:00:00 2001 From: Guillaume Launay Date: Tue, 5 Aug 2025 00:20:20 +0200 Subject: [PATCH 062/693] git_panel: Improve toast messages for push/pull/fetch (#35092) On GitLab, when pushing a branch and a MR already existing the remote log contains "View merge request" and the link to the MR. Fixed `Already up to date` stdout check on pull (was `Everything up to date` on stderr) Fixed `Everything up-to-date` check on push (was `Everything up to date`) Improved messaging for up-to-date for fetch/push/pull Fixed tests introduced in https://github.com/zed-industries/zed/pull/33833. Screenshot 2025-07-31 at 18 37 05 Release Notes: - Git UI: Add "View Pull Request" when pushing to Gitlab remotes - git: Improved toast messages on fetch/push/pull --------- Co-authored-by: Peter Tripp --- Cargo.lock | 1 + crates/git_ui/Cargo.toml | 1 + crates/git_ui/src/git_panel.rs | 8 +- crates/git_ui/src/remote_output.rs | 150 +++++++++++++++++------------ 4 files changed, 95 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf35a467a2..69386b3020 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6369,6 +6369,7 @@ dependencies = [ "fuzzy", "git", "gpui", + "indoc", "itertools 0.14.0", "language", "language_model", diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index e6547e7ae9..35f7a60354 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -70,6 +70,7 @@ windows.workspace = true ctor.workspace = true editor = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } +indoc.workspace = true pretty_assertions.workspace = true project = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 344fa86142..44222b8299 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -2899,7 +2899,9 @@ impl GitPanel { let status_toast = StatusToast::new(message, cx, move |this, _cx| { use remote_output::SuccessStyle::*; match style { - Toast { .. } => this, + Toast { .. } => { + this.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted)) + } ToastWithLog { output } => this .icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted)) .action("View Log", move |window, cx| { @@ -2912,9 +2914,9 @@ impl GitPanel { }) .ok(); }), - PushPrLink { link } => this + PushPrLink { text, link } => this .icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted)) - .action("Open Pull Request", move |_, cx| cx.open_url(&link)), + .action(text, move |_, cx| cx.open_url(&link)), } }); workspace.toggle_status_toast(status_toast, cx) diff --git a/crates/git_ui/src/remote_output.rs b/crates/git_ui/src/remote_output.rs index 03fbf4f917..8437bf0d0d 100644 --- a/crates/git_ui/src/remote_output.rs +++ b/crates/git_ui/src/remote_output.rs @@ -24,7 +24,7 @@ impl RemoteAction { pub enum SuccessStyle { Toast, ToastWithLog { output: RemoteCommandOutput }, - PushPrLink { link: String }, + PushPrLink { text: String, link: String }, } pub struct SuccessMessage { @@ -37,7 +37,7 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ RemoteAction::Fetch(remote) => { if output.stderr.is_empty() { SuccessMessage { - message: "Already up to date".into(), + message: "Fetch: Already up to date".into(), style: SuccessStyle::Toast, } } else { @@ -68,10 +68,9 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ Ok(files_changed) }; - - if output.stderr.starts_with("Everything up to date") { + if output.stdout.ends_with("Already up to date.\n") { SuccessMessage { - message: output.stderr.trim().to_owned(), + message: "Pull: Already up to date".into(), style: SuccessStyle::Toast, } } else if output.stdout.starts_with("Updating") { @@ -119,48 +118,42 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ } } RemoteAction::Push(branch_name, remote_ref) => { - if output.stderr.contains("* [new branch]") { - let pr_hints = [ - // GitHub - "Create a pull request", - // Bitbucket - "Create pull request", - // GitLab - "create a merge request", - ]; - let style = if pr_hints - .iter() - .any(|indicator| output.stderr.contains(indicator)) - { - let finder = LinkFinder::new(); - let first_link = finder - .links(&output.stderr) - .filter(|link| *link.kind() == LinkKind::Url) - .map(|link| link.start()..link.end()) - .next(); - if let Some(link) = first_link { - let link = output.stderr[link].to_string(); - SuccessStyle::PushPrLink { link } - } else { - SuccessStyle::ToastWithLog { output } - } - } else { - SuccessStyle::ToastWithLog { output } - }; - SuccessMessage { - message: format!("Published {} to {}", branch_name, remote_ref.name), - style, - } - } else if output.stderr.starts_with("Everything up to date") { - SuccessMessage { - message: output.stderr.trim().to_owned(), - style: SuccessStyle::Toast, - } + let message = if output.stderr.ends_with("Everything up-to-date\n") { + "Push: Everything is up-to-date".to_string() } else { - SuccessMessage { - message: format!("Pushed {} to {}", branch_name, remote_ref.name), - style: SuccessStyle::ToastWithLog { output }, - } + format!("Pushed {} to {}", branch_name, remote_ref.name) + }; + + let style = if output.stderr.ends_with("Everything up-to-date\n") { + Some(SuccessStyle::Toast) + } else if output.stderr.contains("\nremote: ") { + let pr_hints = [ + ("Create a pull request", "Create Pull Request"), // GitHub + ("Create pull request", "Create Pull Request"), // Bitbucket + ("create a merge request", "Create Merge Request"), // GitLab + ("View merge request", "View Merge Request"), // GitLab + ]; + pr_hints + .iter() + .find(|(indicator, _)| output.stderr.contains(indicator)) + .and_then(|(_, mapped)| { + let finder = LinkFinder::new(); + finder + .links(&output.stderr) + .filter(|link| *link.kind() == LinkKind::Url) + .map(|link| link.start()..link.end()) + .next() + .map(|link| SuccessStyle::PushPrLink { + text: mapped.to_string(), + link: output.stderr[link].to_string(), + }) + }) + } else { + None + }; + SuccessMessage { + message, + style: style.unwrap_or(SuccessStyle::ToastWithLog { output }), } } } @@ -169,6 +162,7 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ #[cfg(test)] mod tests { use super::*; + use indoc::indoc; #[test] fn test_push_new_branch_pull_request() { @@ -181,8 +175,7 @@ mod tests { let output = RemoteCommandOutput { stdout: String::new(), - stderr: String::from( - " + stderr: indoc! { " Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) remote: remote: Create a pull request for 'test' on GitHub by visiting: @@ -190,13 +183,14 @@ mod tests { remote: To example.com:test/test.git * [new branch] test -> test - ", - ), + "} + .to_string(), }; let msg = format_output(&action, output); - if let SuccessStyle::PushPrLink { link } = &msg.style { + if let SuccessStyle::PushPrLink { text: hint, link } = &msg.style { + assert_eq!(hint, "Create Pull Request"); assert_eq!(link, "https://example.com/test/test/pull/new/test"); } else { panic!("Expected PushPrLink variant"); @@ -214,7 +208,7 @@ mod tests { let output = RemoteCommandOutput { stdout: String::new(), - stderr: String::from(" + stderr: indoc! {" Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) remote: remote: To create a merge request for test, visit: @@ -222,12 +216,14 @@ mod tests { remote: To example.com:test/test.git * [new branch] test -> test - "), - }; + "} + .to_string() + }; let msg = format_output(&action, output); - if let SuccessStyle::PushPrLink { link } = &msg.style { + if let SuccessStyle::PushPrLink { text, link } = &msg.style { + assert_eq!(text, "Create Merge Request"); assert_eq!( link, "https://example.com/test/test/-/merge_requests/new?merge_request%5Bsource_branch%5D=test" @@ -237,6 +233,39 @@ mod tests { } } + #[test] + fn test_push_branch_existing_merge_request() { + let action = RemoteAction::Push( + SharedString::new("test_branch"), + Remote { + name: SharedString::new("test_remote"), + }, + ); + + let output = RemoteCommandOutput { + stdout: String::new(), + stderr: indoc! {" + Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) + remote: + remote: View merge request for test: + remote: https://example.com/test/test/-/merge_requests/99999 + remote: + To example.com:test/test.git + + 80bd3c83be...e03d499d2e test -> test + "} + .to_string(), + }; + + let msg = format_output(&action, output); + + if let SuccessStyle::PushPrLink { text, link } = &msg.style { + assert_eq!(text, "View Merge Request"); + assert_eq!(link, "https://example.com/test/test/-/merge_requests/99999"); + } else { + panic!("Expected PushPrLink variant"); + } + } + #[test] fn test_push_new_branch_no_link() { let action = RemoteAction::Push( @@ -248,12 +277,12 @@ mod tests { let output = RemoteCommandOutput { stdout: String::new(), - stderr: String::from( - " + stderr: indoc! { " To http://example.com/test/test.git * [new branch] test -> test ", - ), + } + .to_string(), }; let msg = format_output(&action, output); @@ -261,10 +290,7 @@ mod tests { if let SuccessStyle::ToastWithLog { output } = &msg.style { assert_eq!( output.stderr, - " - To http://example.com/test/test.git - * [new branch] test -> test - " + "To http://example.com/test/test.git\n * [new branch] test -> test\n" ); } else { panic!("Expected ToastWithLog variant"); From 91bbdb7002b6e8298c9876ed4e4e22b0e021b98c Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Aug 2025 00:37:06 +0200 Subject: [PATCH 063/693] debugger: Install debugpy into user's venv if there's one selected (#35617) Closes #35388 Release Notes: - debugger: Fixed Python debug sessions failing to launch due to a missing debugpy installation. Debugpy is now installed into user's venv if there's one available. --- crates/dap_adapters/src/python.rs | 61 ++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index aa64fea6ed..455440d6d3 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -126,38 +126,42 @@ impl PythonDebugAdapter { } None } - + const BINARY_DIR: &str = if cfg!(target_os = "windows") { + "Scripts" + } else { + "bin" + }; async fn base_venv(&self, delegate: &dyn DapDelegate) -> Result, String> { - const BINARY_DIR: &str = if cfg!(target_os = "windows") { - "Scripts" - } else { - "bin" - }; self.python_venv_base .get_or_init(move || async move { let venv_base = Self::ensure_venv(delegate) .await .map_err(|e| format!("{e}"))?; - let pip_path = venv_base.join(BINARY_DIR).join("pip3"); - let installation_succeeded = util::command::new_smol_command(pip_path.as_path()) - .arg("install") - .arg("debugpy") - .arg("-U") - .output() - .await - .map_err(|e| format!("{e}"))? - .status - .success(); - if !installation_succeeded { - return Err("debugpy installation failed".into()); - } - + Self::install_debugpy_into_venv(&venv_base).await?; Ok(venv_base) }) .await .clone() } + async fn install_debugpy_into_venv(venv_path: &Path) -> Result<(), String> { + let pip_path = venv_path.join(Self::BINARY_DIR).join("pip3"); + let installation_succeeded = util::command::new_smol_command(pip_path.as_path()) + .arg("install") + .arg("debugpy") + .arg("-U") + .output() + .await + .map_err(|e| format!("{e}"))? + .status + .success(); + if !installation_succeeded { + return Err("debugpy installation failed".into()); + } + + Ok(()) + } + async fn get_installed_binary( &self, delegate: &Arc, @@ -629,11 +633,22 @@ impl DebugAdapter for PythonDebugAdapter { .await; } + let base_path = config + .config + .get("cwd") + .and_then(|cwd| { + cwd.as_str() + .map(Path::new)? + .strip_prefix(delegate.worktree_root_path()) + .ok() + }) + .unwrap_or_else(|| "".as_ref()) + .into(); let toolchain = delegate .toolchain_store() .active_toolchain( delegate.worktree_id(), - Arc::from("".as_ref()), + base_path, language::LanguageName::new(Self::LANGUAGE_NAME), cx, ) @@ -641,6 +656,10 @@ impl DebugAdapter for PythonDebugAdapter { if let Some(toolchain) = &toolchain { if let Some(path) = Path::new(&toolchain.path.to_string()).parent() { + if let Some(parent) = path.parent() { + Self::install_debugpy_into_venv(parent).await.ok(); + } + let debugpy_path = path.join("debugpy"); if delegate.fs().is_file(&debugpy_path).await { log::debug!( From afc4f50300024e66f8b9745a90e778c952efc6c9 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Aug 2025 00:51:40 +0200 Subject: [PATCH 064/693] debugger: Ensure that Python's adapter work dir exists (#35618) Closes #ISSUE cc @Sansui233 who triaged this in https://github.com/zed-industries/zed/issues/35388#issuecomment-3146977431 Release Notes: - debugger: Fixed an issue where a Python debug adapter could not be installed when debugging Python projects for the first time. --- crates/dap_adapters/src/python.rs | 3 +++ crates/zed/src/main.rs | 1 + 2 files changed, 4 insertions(+) diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 455440d6d3..f499244966 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -101,6 +101,9 @@ impl PythonDebugAdapter { .await .context("Could not find Python installation for DebugPy")?; let work_dir = debug_adapters_dir().join(Self::ADAPTER_NAME); + if !work_dir.exists() { + std::fs::create_dir_all(&work_dir)?; + } let mut path = work_dir.clone(); path.push("debugpy-venv"); if !path.exists() { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 825aea615f..71b29909a1 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -1125,6 +1125,7 @@ fn init_paths() -> HashMap> { paths::config_dir(), paths::extensions_dir(), paths::languages_dir(), + paths::debug_adapters_dir(), paths::database_dir(), paths::logs_dir(), paths::temp_dir(), From e1d0e3fc34ad6a192816719b4bb0a5af7d938283 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 4 Aug 2025 20:52:22 -0300 Subject: [PATCH 065/693] onboarding: Add explainer tooltips for the editing and AI section (#35619) Includes the ability to add a tooltip for both the badge and switch field components. Release Notes: - N/A --- crates/onboarding/src/ai_setup_page.rs | 42 ++++++++++++- crates/onboarding/src/editing_page.rs | 85 ++++++++++++++------------ crates/ui/src/components/badge.rs | 30 ++++++++- crates/ui/src/components/toggle.rs | 82 +++++++++++++++++++++++-- 4 files changed, 191 insertions(+), 48 deletions(-) diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index b4b043196b..b5dda7601f 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -11,7 +11,7 @@ use project::DisableAiSettings; use settings::{Settings, update_settings_file}; use ui::{ Badge, ButtonLike, Divider, Modal, ModalFooter, ModalHeader, Section, SwitchField, ToggleState, - prelude::*, + prelude::*, tooltip_container, }; use util::ResultExt; use workspace::ModalView; @@ -41,7 +41,11 @@ fn render_llm_provider_section( } fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement { - let privacy_badge = || Badge::new("Privacy").icon(IconName::ShieldCheck); + let privacy_badge = || { + Badge::new("Privacy") + .icon(IconName::ShieldCheck) + .tooltip(move |_, cx| cx.new(|_| AiPrivacyTooltip::new()).into()) + }; v_flex() .relative() @@ -355,3 +359,37 @@ impl Render for AiConfigurationModal { ) } } + +pub struct AiPrivacyTooltip {} + +impl AiPrivacyTooltip { + pub fn new() -> Self { + Self {} + } +} + +impl Render for AiPrivacyTooltip { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + const DESCRIPTION: &'static str = "One of Zed's most important principles is transparency. This is why we are and value open-source so much. And it wouldn't be any different with AI."; + + tooltip_container(window, cx, move |this, _, _| { + this.child( + h_flex() + .gap_1() + .child( + Icon::new(IconName::ShieldCheck) + .size(IconSize::Small) + .color(Color::Muted), + ) + .child(Label::new("Privacy Principle")), + ) + .child( + div().max_w_64().child( + Label::new(DESCRIPTION) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + }) + } +} diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 2972f41348..20ef17c7aa 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -9,7 +9,7 @@ use settings::{Settings as _, update_settings_file}; use theme::{FontFamilyCache, FontFamilyName, ThemeSettings}; use ui::{ ButtonLike, ContextMenu, DropdownMenu, NumericStepper, SwitchField, ToggleButtonGroup, - ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, prelude::*, + ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, prelude::*, }; use crate::{ImportCursorSettings, ImportVsCodeSettings}; @@ -357,23 +357,28 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl } fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl IntoElement { + const LIGATURE_TOOLTIP: &'static str = "Ligatures are when a font creates a special character out of combining two characters into one. For example, with ligatures turned on, =/= would become ≠."; + v_flex() .gap_5() .child(Label::new("Popular Settings").size(LabelSize::Large).mt_8()) .child(render_font_customization_section(window, cx)) - .child(SwitchField::new( - "onboarding-font-ligatures", - "Font Ligatures", - Some("Combine text characters into their associated symbols.".into()), - if read_font_ligatures(cx) { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - |toggle_state, _, cx| { - write_font_ligatures(toggle_state == &ToggleState::Selected, cx); - }, - )) + .child( + SwitchField::new( + "onboarding-font-ligatures", + "Font Ligatures", + Some("Combine text characters into their associated symbols.".into()), + if read_font_ligatures(cx) { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + |toggle_state, _, cx| { + write_font_ligatures(toggle_state == &ToggleState::Selected, cx); + }, + ) + .tooltip(Tooltip::text(LIGATURE_TOOLTIP)), + ) .child(SwitchField::new( "onboarding-format-on-save", "Format on Save", @@ -387,6 +392,32 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In write_format_on_save(toggle_state == &ToggleState::Selected, cx); }, )) + .child(SwitchField::new( + "onboarding-enable-inlay-hints", + "Inlay Hints", + Some("See parameter names for function and method calls inline.".into()), + if read_inlay_hints(cx) { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + |toggle_state, _, cx| { + write_inlay_hints(toggle_state == &ToggleState::Selected, cx); + }, + )) + .child(SwitchField::new( + "onboarding-git-blame-switch", + "Git Blame", + Some("See who committed each line on a given file.".into()), + if read_git_blame(cx) { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + |toggle_state, _, cx| { + set_git_blame(toggle_state == &ToggleState::Selected, cx); + }, + )) .child( h_flex() .items_start() @@ -421,32 +452,6 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In .button_width(ui::rems_from_px(64.)), ), ) - .child(SwitchField::new( - "onboarding-enable-inlay-hints", - "Inlay Hints", - Some("See parameter names for function and method calls inline.".into()), - if read_inlay_hints(cx) { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - |toggle_state, _, cx| { - write_inlay_hints(toggle_state == &ToggleState::Selected, cx); - }, - )) - .child(SwitchField::new( - "onboarding-git-blame-switch", - "Git Blame", - Some("See who committed each line on a given file.".into()), - if read_git_blame(cx) { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - |toggle_state, _, cx| { - set_git_blame(toggle_state == &ToggleState::Selected, cx); - }, - )) } pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement { diff --git a/crates/ui/src/components/badge.rs b/crates/ui/src/components/badge.rs index 2eee084bbb..f36e03291c 100644 --- a/crates/ui/src/components/badge.rs +++ b/crates/ui/src/components/badge.rs @@ -1,13 +1,18 @@ +use std::rc::Rc; + use crate::Divider; use crate::DividerColor; +use crate::Tooltip; use crate::component_prelude::*; use crate::prelude::*; +use gpui::AnyView; use gpui::{AnyElement, IntoElement, SharedString, Window}; #[derive(IntoElement, RegisterComponent)] pub struct Badge { label: SharedString, icon: IconName, + tooltip: Option AnyView>>, } impl Badge { @@ -15,6 +20,7 @@ impl Badge { Self { label: label.into(), icon: IconName::Check, + tooltip: None, } } @@ -22,11 +28,19 @@ impl Badge { self.icon = icon; self } + + pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { + self.tooltip = Some(Rc::new(tooltip)); + self + } } impl RenderOnce for Badge { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let tooltip = self.tooltip; + h_flex() + .id(self.label.clone()) .h_full() .gap_1() .pl_1() @@ -43,6 +57,9 @@ impl RenderOnce for Badge { ) .child(Divider::vertical().color(DividerColor::Border)) .child(Label::new(self.label.clone()).size(LabelSize::Small).ml_1()) + .when_some(tooltip, |this, tooltip| { + this.tooltip(move |window, cx| tooltip(window, cx)) + }) } } @@ -59,7 +76,18 @@ impl Component for Badge { fn preview(_window: &mut Window, _cx: &mut App) -> Option { Some( - single_example("Basic Badge", Badge::new("Default").into_any_element()) + v_flex() + .gap_6() + .child(single_example( + "Basic Badge", + Badge::new("Default").into_any_element(), + )) + .child(single_example( + "With Tooltip", + Badge::new("Tooltip") + .tooltip(Tooltip::text("This is a tooltip.")) + .into_any_element(), + )) .into_any_element(), ) } diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index 0d8f5c4107..a3a3f23889 100644 --- a/crates/ui/src/components/toggle.rs +++ b/crates/ui/src/components/toggle.rs @@ -2,10 +2,10 @@ use gpui::{ AnyElement, AnyView, ClickEvent, ElementId, Hsla, IntoElement, Styled, Window, div, hsla, prelude::*, }; -use std::sync::Arc; +use std::{rc::Rc, sync::Arc}; use crate::utils::is_light; -use crate::{Color, Icon, IconName, ToggleState}; +use crate::{Color, Icon, IconName, ToggleState, Tooltip}; use crate::{ElevationIndex, KeyBinding, prelude::*}; // TODO: Checkbox, CheckboxWithLabel, and Switch could all be @@ -571,6 +571,7 @@ pub struct SwitchField { on_click: Arc, disabled: bool, color: SwitchColor, + tooltip: Option AnyView>>, } impl SwitchField { @@ -589,6 +590,7 @@ impl SwitchField { on_click: Arc::new(on_click), disabled: false, color: SwitchColor::Accent, + tooltip: None, } } @@ -608,10 +610,17 @@ impl SwitchField { self.color = color; self } + + pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { + self.tooltip = Some(Rc::new(tooltip)); + self + } } impl RenderOnce for SwitchField { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + let tooltip = self.tooltip; + h_flex() .id(SharedString::from(format!("{}-container", self.id))) .when(!self.disabled, |this| { @@ -621,14 +630,48 @@ impl RenderOnce for SwitchField { .gap_4() .justify_between() .flex_wrap() - .child(match &self.description { - Some(description) => v_flex() + .child(match (&self.description, &tooltip) { + (Some(description), Some(tooltip)) => v_flex() + .gap_0p5() + .max_w_5_6() + .child( + h_flex() + .gap_0p5() + .child(Label::new(self.label.clone())) + .child( + IconButton::new("tooltip_button", IconName::Info) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .shape(crate::IconButtonShape::Square) + .tooltip({ + let tooltip = tooltip.clone(); + move |window, cx| tooltip(window, cx) + }), + ), + ) + .child(Label::new(description.clone()).color(Color::Muted)) + .into_any_element(), + (Some(description), None) => v_flex() .gap_0p5() .max_w_5_6() .child(Label::new(self.label.clone())) .child(Label::new(description.clone()).color(Color::Muted)) .into_any_element(), - None => Label::new(self.label.clone()).into_any_element(), + (None, Some(tooltip)) => h_flex() + .gap_0p5() + .child(Label::new(self.label.clone())) + .child( + IconButton::new("tooltip_button", IconName::Info) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .shape(crate::IconButtonShape::Square) + .tooltip({ + let tooltip = tooltip.clone(); + move |window, cx| tooltip(window, cx) + }), + ) + .into_any_element(), + (None, None) => Label::new(self.label.clone()).into_any_element(), }) .child( Switch::new( @@ -754,6 +797,35 @@ impl Component for SwitchField { .into_any_element(), )], ), + example_group_with_title( + "With Tooltip", + vec![ + single_example( + "Tooltip with Description", + SwitchField::new( + "switch_field_tooltip_with_desc", + "Nice Feature", + Some("Enable advanced configuration options.".into()), + ToggleState::Unselected, + |_, _, _| {}, + ) + .tooltip(Tooltip::text("This is content for this tooltip!")) + .into_any_element(), + ), + single_example( + "Tooltip without Description", + SwitchField::new( + "switch_field_tooltip_no_desc", + "Nice Feature", + None, + ToggleState::Selected, + |_, _, _| {}, + ) + .tooltip(Tooltip::text("This is content for this tooltip!")) + .into_any_element(), + ), + ], + ), ]) .into_any_element(), ) From 06226e1cbd00eac371cc5d9c291a551881317744 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 4 Aug 2025 19:01:53 -0500 Subject: [PATCH 066/693] onboarding: Show indication that settings have already been imported (#35615) Co-Authored-By: Danilo Co-Authored-By: Anthony Release Notes: - N/A --- Cargo.lock | 1 + crates/onboarding/Cargo.toml | 1 + crates/onboarding/src/editing_page.rs | 128 ++++++++++++++------------ crates/onboarding/src/onboarding.rs | 79 ++++++++++++++-- crates/settings/src/settings_store.rs | 73 ++++++++++----- 5 files changed, 192 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 69386b3020..2ef41eafc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10929,6 +10929,7 @@ dependencies = [ "language", "language_model", "menu", + "notifications", "project", "schemars", "serde", diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index 8f684dd1b8..b3056ff39e 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -30,6 +30,7 @@ itertools.workspace = true language.workspace = true language_model.workspace = true menu.workspace = true +notifications.workspace = true project.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 20ef17c7aa..a5e3a6bf05 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -12,7 +12,7 @@ use ui::{ ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, prelude::*, }; -use crate::{ImportCursorSettings, ImportVsCodeSettings}; +use crate::{ImportCursorSettings, ImportVsCodeSettings, SettingsImportState}; fn read_show_mini_map(cx: &App) -> ShowMinimap { editor::EditorSettings::get_global(cx).minimap.show @@ -165,7 +165,71 @@ fn write_format_on_save(format_on_save: bool, cx: &mut App) { }); } -fn render_import_settings_section() -> impl IntoElement { +fn render_setting_import_button( + label: SharedString, + icon_name: IconName, + action: &dyn Action, + imported: bool, +) -> impl IntoElement { + let action = action.boxed_clone(); + h_flex().w_full().child( + ButtonLike::new(label.clone()) + .full_width() + .style(ButtonStyle::Outlined) + .size(ButtonSize::Large) + .child( + h_flex() + .w_full() + .justify_between() + .child( + h_flex() + .gap_1p5() + .px_1() + .child( + Icon::new(icon_name) + .color(Color::Muted) + .size(IconSize::XSmall), + ) + .child(Label::new(label)), + ) + .when(imported, |this| { + this.child( + h_flex() + .gap_1p5() + .child( + Icon::new(IconName::Check) + .color(Color::Success) + .size(IconSize::XSmall), + ) + .child(Label::new("Imported").size(LabelSize::Small)), + ) + }), + ) + .on_click(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)), + ) +} + +fn render_import_settings_section(cx: &App) -> impl IntoElement { + let import_state = SettingsImportState::global(cx); + let imports: [(SharedString, IconName, &dyn Action, bool); 2] = [ + ( + "VS Code".into(), + IconName::EditorVsCode, + &ImportVsCodeSettings { skip_prompt: false }, + import_state.vscode, + ), + ( + "Cursor".into(), + IconName::EditorCursor, + &ImportCursorSettings { skip_prompt: false }, + import_state.cursor, + ), + ]; + + let [vscode, cursor] = imports.map(|(label, icon_name, action, imported)| { + render_setting_import_button(label, icon_name, action, imported) + }); + v_flex() .gap_4() .child( @@ -176,63 +240,7 @@ fn render_import_settings_section() -> impl IntoElement { .color(Color::Muted), ), ) - .child( - h_flex() - .w_full() - .gap_4() - .child( - h_flex().w_full().child( - ButtonLike::new("import_vs_code") - .full_width() - .style(ButtonStyle::Outlined) - .size(ButtonSize::Large) - .child( - h_flex() - .w_full() - .gap_1p5() - .px_1() - .child( - Icon::new(IconName::EditorVsCode) - .color(Color::Muted) - .size(IconSize::XSmall), - ) - .child(Label::new("VS Code")), - ) - .on_click(|_, window, cx| { - window.dispatch_action( - ImportVsCodeSettings::default().boxed_clone(), - cx, - ) - }), - ), - ) - .child( - h_flex().w_full().child( - ButtonLike::new("import_cursor") - .full_width() - .style(ButtonStyle::Outlined) - .size(ButtonSize::Large) - .child( - h_flex() - .w_full() - .gap_1p5() - .px_1() - .child( - Icon::new(IconName::EditorCursor) - .color(Color::Muted) - .size(IconSize::XSmall), - ) - .child(Label::new("Cursor")), - ) - .on_click(|_, window, cx| { - window.dispatch_action( - ImportCursorSettings::default().boxed_clone(), - cx, - ) - }), - ), - ), - ) + .child(h_flex().w_full().gap_4().child(vscode).child(cursor)) } fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl IntoElement { @@ -457,6 +465,6 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement { v_flex() .gap_4() - .child(render_import_settings_section()) + .child(render_import_settings_section(cx)) .child(render_popular_settings_section(window, cx)) } diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index a79d1d5aef..42e75ac2f8 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -6,9 +6,10 @@ use feature_flags::{FeatureFlag, FeatureFlagViewExt as _}; use fs::Fs; use gpui::{ Action, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, EventEmitter, - FocusHandle, Focusable, IntoElement, KeyContext, Render, SharedString, Subscription, Task, - WeakEntity, Window, actions, + FocusHandle, Focusable, Global, IntoElement, KeyContext, Render, SharedString, Subscription, + Task, WeakEntity, Window, actions, }; +use notifications::status_toast::{StatusToast, ToastIcon}; use schemars::JsonSchema; use serde::Deserialize; use settings::{SettingsStore, VsCodeSettingsSource}; @@ -137,9 +138,12 @@ pub fn init(cx: &mut App) { let fs = ::global(cx); let action = *action; + let workspace = cx.weak_entity(); + window .spawn(cx, async move |cx: &mut AsyncWindowContext| { handle_import_vscode_settings( + workspace, VsCodeSettingsSource::VsCode, action.skip_prompt, fs, @@ -154,9 +158,12 @@ pub fn init(cx: &mut App) { let fs = ::global(cx); let action = *action; + let workspace = cx.weak_entity(); + window .spawn(cx, async move |cx: &mut AsyncWindowContext| { handle_import_vscode_settings( + workspace, VsCodeSettingsSource::Cursor, action.skip_prompt, fs, @@ -555,6 +562,7 @@ impl Item for Onboarding { } pub async fn handle_import_vscode_settings( + workspace: WeakEntity, source: VsCodeSettingsSource, skip_prompt: bool, fs: Arc, @@ -595,14 +603,73 @@ pub async fn handle_import_vscode_settings( } }; - cx.update(|_, cx| { + let Ok(result_channel) = cx.update(|_, cx| { let source = vscode_settings.source; let path = vscode_settings.path.clone(); - cx.global::() + let result_channel = cx + .global::() .import_vscode_settings(fs, vscode_settings); zlog::info!("Imported {source} settings from {}", path.display()); - }) - .ok(); + result_channel + }) else { + return; + }; + + let result = result_channel.await; + workspace + .update_in(cx, |workspace, _, cx| match result { + Ok(_) => { + let confirmation_toast = StatusToast::new( + format!("Your {} settings were successfully imported.", source), + cx, + |this, _| { + this.icon(ToastIcon::new(IconName::Check).color(Color::Success)) + .dismiss_button(true) + }, + ); + SettingsImportState::update(cx, |state, _| match source { + VsCodeSettingsSource::VsCode => { + state.vscode = true; + } + VsCodeSettingsSource::Cursor => { + state.cursor = true; + } + }); + workspace.toggle_status_toast(confirmation_toast, cx); + } + Err(_) => { + let error_toast = StatusToast::new( + "Failed to import settings. See log for details", + cx, + |this, _| { + this.icon(ToastIcon::new(IconName::X).color(Color::Error)) + .action("Open Log", |window, cx| { + window.dispatch_action(workspace::OpenLog.boxed_clone(), cx) + }) + .dismiss_button(true) + }, + ); + workspace.toggle_status_toast(error_toast, cx); + } + }) + .ok(); +} + +#[derive(Default, Copy, Clone)] +pub struct SettingsImportState { + pub cursor: bool, + pub vscode: bool, +} + +impl Global for SettingsImportState {} + +impl SettingsImportState { + pub fn global(cx: &App) -> Self { + cx.try_global().cloned().unwrap_or_default() + } + pub fn update(cx: &mut App, f: impl FnOnce(&mut Self, &mut App) -> R) -> R { + cx.update_default_global(f) + } } impl workspace::SerializableItem for Onboarding { diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 7f6437dac8..bc42d2c886 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -2,7 +2,11 @@ use anyhow::{Context as _, Result}; use collections::{BTreeMap, HashMap, btree_map, hash_map}; use ec4rs::{ConfigParser, PropertiesSource, Section}; use fs::Fs; -use futures::{FutureExt, StreamExt, channel::mpsc, future::LocalBoxFuture}; +use futures::{ + FutureExt, StreamExt, + channel::{mpsc, oneshot}, + future::LocalBoxFuture, +}; use gpui::{App, AsyncApp, BorrowAppContext, Global, Task, UpdateGlobal}; use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name}; @@ -531,39 +535,60 @@ impl SettingsStore { .ok(); } - pub fn import_vscode_settings(&self, fs: Arc, vscode_settings: VsCodeSettings) { + pub fn import_vscode_settings( + &self, + fs: Arc, + vscode_settings: VsCodeSettings, + ) -> oneshot::Receiver> { + let (tx, rx) = oneshot::channel::>(); self.setting_file_updates_tx .unbounded_send(Box::new(move |cx: AsyncApp| { async move { - let old_text = Self::load_settings(&fs).await?; - let new_text = cx.read_global(|store: &SettingsStore, _cx| { - store.get_vscode_edits(old_text, &vscode_settings) - })?; - let settings_path = paths::settings_file().as_path(); - if fs.is_file(settings_path).await { - let resolved_path = - fs.canonicalize(settings_path).await.with_context(|| { - format!("Failed to canonicalize settings path {:?}", settings_path) - })?; + let res = async move { + let old_text = Self::load_settings(&fs).await?; + let new_text = cx.read_global(|store: &SettingsStore, _cx| { + store.get_vscode_edits(old_text, &vscode_settings) + })?; + let settings_path = paths::settings_file().as_path(); + if fs.is_file(settings_path).await { + let resolved_path = + fs.canonicalize(settings_path).await.with_context(|| { + format!( + "Failed to canonicalize settings path {:?}", + settings_path + ) + })?; - fs.atomic_write(resolved_path.clone(), new_text) - .await - .with_context(|| { - format!("Failed to write settings to file {:?}", resolved_path) - })?; - } else { - fs.atomic_write(settings_path.to_path_buf(), new_text) - .await - .with_context(|| { - format!("Failed to write settings to file {:?}", settings_path) - })?; + fs.atomic_write(resolved_path.clone(), new_text) + .await + .with_context(|| { + format!("Failed to write settings to file {:?}", resolved_path) + })?; + } else { + fs.atomic_write(settings_path.to_path_buf(), new_text) + .await + .with_context(|| { + format!("Failed to write settings to file {:?}", settings_path) + })?; + } + + anyhow::Ok(()) } + .await; - anyhow::Ok(()) + let new_res = match &res { + Ok(_) => anyhow::Ok(()), + Err(e) => Err(anyhow::anyhow!("Failed to write settings to file {:?}", e)), + }; + + _ = tx.send(new_res); + res } .boxed_local() })) .ok(); + + rx } } From a9c44ac5511dc55239c7d125252a613e872844d0 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 5 Aug 2025 00:32:42 +0000 Subject: [PATCH 067/693] assistant_tool: Fix rejecting edits deletes newly created and accepted files (#35622) Closes #34108 Closes #33234 This PR fixes a bug where a file remained in a Created state after accept, causing following reject actions to incorrectly delete the file instead of reverting back to previous state. Now it changes it to Modified state upon "Accept All" and "Accept Hunk" (when all edits are accepted). - [x] Tests Release Notes: - Fixed issue where rejecting AI edits on newly created files would delete the file instead of reverting to previous accepted state. --- crates/assistant_tool/src/action_log.rs | 136 ++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/crates/assistant_tool/src/action_log.rs b/crates/assistant_tool/src/action_log.rs index 672c048872..025aba060d 100644 --- a/crates/assistant_tool/src/action_log.rs +++ b/crates/assistant_tool/src/action_log.rs @@ -630,6 +630,11 @@ impl ActionLog { false } }); + if tracked_buffer.unreviewed_edits.is_empty() { + if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status { + tracked_buffer.status = TrackedBufferStatus::Modified; + } + } tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); } } @@ -775,6 +780,9 @@ impl ActionLog { .retain(|_buffer, tracked_buffer| match tracked_buffer.status { TrackedBufferStatus::Deleted => false, _ => { + if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status { + tracked_buffer.status = TrackedBufferStatus::Modified; + } tracked_buffer.unreviewed_edits.clear(); tracked_buffer.diff_base = tracked_buffer.snapshot.as_rope().clone(); tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); @@ -2075,6 +2083,134 @@ mod tests { assert_eq!(content, "ai content\nuser added this line"); } + #[gpui::test] + async fn test_reject_after_accepting_hunk_on_created_file(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + + let file_path = project + .read_with(cx, |project, cx| { + project.find_project_path("dir/new_file", cx) + }) + .unwrap(); + let buffer = project + .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx)) + .await + .unwrap(); + + // AI creates file with initial content + cx.update(|cx| { + action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); + buffer.update(cx, |buffer, cx| buffer.set_text("ai content v1", cx)); + action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); + }); + project + .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) + .await + .unwrap(); + cx.run_until_parked(); + assert_ne!(unreviewed_hunks(&action_log, cx), vec![]); + + // User accepts the single hunk + action_log.update(cx, |log, cx| { + log.keep_edits_in_range(buffer.clone(), Anchor::MIN..Anchor::MAX, cx) + }); + cx.run_until_parked(); + assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); + assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); + + // AI modifies the file + cx.update(|cx| { + buffer.update(cx, |buffer, cx| buffer.set_text("ai content v2", cx)); + action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); + }); + project + .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) + .await + .unwrap(); + cx.run_until_parked(); + assert_ne!(unreviewed_hunks(&action_log, cx), vec![]); + + // User rejects the hunk + action_log + .update(cx, |log, cx| { + log.reject_edits_in_ranges(buffer.clone(), vec![Anchor::MIN..Anchor::MAX], cx) + }) + .await + .unwrap(); + cx.run_until_parked(); + assert!(fs.is_file(path!("/dir/new_file").as_ref()).await,); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.text()), + "ai content v1" + ); + assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); + } + + #[gpui::test] + async fn test_reject_edits_on_previously_accepted_created_file(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + + let file_path = project + .read_with(cx, |project, cx| { + project.find_project_path("dir/new_file", cx) + }) + .unwrap(); + let buffer = project + .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx)) + .await + .unwrap(); + + // AI creates file with initial content + cx.update(|cx| { + action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); + buffer.update(cx, |buffer, cx| buffer.set_text("ai content v1", cx)); + action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); + }); + project + .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) + .await + .unwrap(); + cx.run_until_parked(); + + // User clicks "Accept All" + action_log.update(cx, |log, cx| log.keep_all_edits(cx)); + cx.run_until_parked(); + assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); + assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); // Hunks are cleared + + // AI modifies file again + cx.update(|cx| { + buffer.update(cx, |buffer, cx| buffer.set_text("ai content v2", cx)); + action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); + }); + project + .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) + .await + .unwrap(); + cx.run_until_parked(); + assert_ne!(unreviewed_hunks(&action_log, cx), vec![]); + + // User clicks "Reject All" + action_log + .update(cx, |log, cx| log.reject_all_edits(cx)) + .await; + cx.run_until_parked(); + assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.text()), + "ai content v1" + ); + assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); + } + #[gpui::test(iterations = 100)] async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) { init_test(cx); From be2f54b2339b502cd6aee6a3b03b549768f9a9b0 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 4 Aug 2025 21:36:43 -0300 Subject: [PATCH 068/693] agent: Update pieces of copy in the settings view (#35621) Some tiny updates to make the agent panel's copywriting sharper. Release Notes: - N/A --- crates/agent_ui/src/agent_configuration.rs | 2 +- crates/language_models/src/provider/anthropic.rs | 2 +- crates/language_models/src/provider/bedrock.rs | 2 +- crates/language_models/src/provider/copilot_chat.rs | 3 ++- crates/language_models/src/provider/google.rs | 2 +- crates/language_models/src/provider/mistral.rs | 2 +- crates/language_models/src/provider/ollama.rs | 2 +- crates/language_models/src/provider/open_ai.rs | 2 +- crates/language_models/src/provider/open_ai_compatible.rs | 2 +- crates/language_models/src/provider/open_router.rs | 2 +- 10 files changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index dad930be9e..02c15b7e41 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -539,7 +539,7 @@ impl AgentConfiguration { v_flex() .gap_0p5() .child(Headline::new("Model Context Protocol (MCP) Servers")) - .child(Label::new("Connect to context servers via the Model Context Protocol either via Zed extensions or directly.").color(Color::Muted)), + .child(Label::new("Connect to context servers through the Model Context Protocol, either using Zed extensions or directly.").color(Color::Muted)), ) .children( context_server_ids.into_iter().map(|context_server_id| { diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index 959cbccf39..ef21e85f71 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -1012,7 +1012,7 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new("To use Zed's assistant with Anthropic, you need to add an API key. Follow these steps:")) + .child(Label::new("To use Zed's agent with Anthropic, you need to add an API key. Follow these steps:")) .child( List::new() .child( diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index a86b3e78f5..6df96c5c56 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -1251,7 +1251,7 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(ConfigurationView::save_credentials)) - .child(Label::new("To use Zed's assistant with Bedrock, you can set a custom authentication strategy through the settings.json, or use static credentials.")) + .child(Label::new("To use Zed's agent with Bedrock, you can set a custom authentication strategy through the settings.json, or use static credentials.")) .child(Label::new("But, to access models on AWS, you need to:").mt_1()) .child( List::new() diff --git a/crates/language_models/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs index 3cdc2e5401..73f73a9a31 100644 --- a/crates/language_models/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -706,7 +706,8 @@ impl Render for ConfigurationView { .child(svg().size_8().path(IconName::CopilotError.path())) } _ => { - const LABEL: &str = "To use Zed's assistant with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription."; + const LABEL: &str = "To use Zed's agent with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription."; + v_flex().gap_2().child(Label::new(LABEL)).child( Button::new("sign_in", "Sign in to use GitHub Copilot") .icon_color(Color::Muted) diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index bd8a09970a..b287e8181a 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -880,7 +880,7 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new("To use Zed's assistant with Google AI, you need to add an API key. Follow these steps:")) + .child(Label::new("To use Zed's agent with Google AI, you need to add an API key. Follow these steps:")) .child( List::new() .child(InstructionListItem::new( diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index fb385308fa..02e53cb99a 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/crates/language_models/src/provider/mistral.rs @@ -807,7 +807,7 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new("To use Zed's assistant with Mistral, you need to add an API key. Follow these steps:")) + .child(Label::new("To use Zed's agent with Mistral, you need to add an API key. Follow these steps:")) .child( List::new() .child(InstructionListItem::new( diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index d4739bcab8..c845c97b09 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -631,7 +631,7 @@ impl Render for ConfigurationView { } }) .child( - Button::new("view-models", "All Models") + Button::new("view-models", "View All Models") .style(ButtonStyle::Subtle) .icon(IconName::ArrowUpRight) .icon_size(IconSize::XSmall) diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 6c4d4c9b3e..79ef4a0ee0 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -780,7 +780,7 @@ impl Render for ConfigurationView { let api_key_section = if self.should_render_editor(cx) { v_flex() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new("To use Zed's assistant with OpenAI, you need to add an API key. Follow these steps:")) + .child(Label::new("To use Zed's agent with OpenAI, you need to add an API key. Follow these steps:")) .child( List::new() .child(InstructionListItem::new( diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs index 64add5483d..38bd7cee06 100644 --- a/crates/language_models/src/provider/open_ai_compatible.rs +++ b/crates/language_models/src/provider/open_ai_compatible.rs @@ -466,7 +466,7 @@ impl Render for ConfigurationView { let api_key_section = if self.should_render_editor(cx) { v_flex() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new("To use Zed's assistant with an OpenAI compatible provider, you need to add an API key.")) + .child(Label::new("To use Zed's agent with an OpenAI-compatible provider, you need to add an API key.")) .child( div() .pt(DynamicSpacing::Base04.rems(cx)) diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index 5a6acc4329..3a492086f1 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -855,7 +855,7 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new("To use Zed's assistant with OpenRouter, you need to add an API key. Follow these steps:")) + .child(Label::new("To use Zed's agent with OpenRouter, you need to add an API key. Follow these steps:")) .child( List::new() .child(InstructionListItem::new( From 07e3d53d58b5fad7d267d71556a96ffd41b4108e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Aug 2025 02:37:22 +0200 Subject: [PATCH 069/693] sum_tree: Do not implement Dimension on tuples, use new Dimensions wrapper instead (#35482) This is a bit of a readability improvement IMHO; I often find myself confused when dealing when dimension pairs, as there's no easy way to jump to the implementation of a dimension for tuples to remind myself for the n-th time how exactly that impl works. Now it should be possible to jump directly to that impl. Another bonus is that Dimension supports 3-ary tuples as well - by using a () as a default value of a 3rd dimension. Release Notes: - N/A --- Cargo.lock | 1 + crates/channel/src/channel_chat.rs | 10 ++-- crates/copilot/Cargo.toml | 1 + crates/copilot/src/copilot.rs | 3 +- crates/editor/src/display_map/block_map.rs | 42 ++++++++-------- crates/editor/src/display_map/fold_map.rs | 43 ++++++++++------ crates/editor/src/display_map/inlay_map.rs | 50 +++++++++++-------- crates/editor/src/display_map/wrap_map.rs | 38 ++++++++++---- crates/gpui/src/elements/list.rs | 6 +-- crates/language/src/syntax_map.rs | 11 ++-- crates/multi_buffer/src/multi_buffer.rs | 47 +++++++++-------- .../notifications/src/notification_store.rs | 6 ++- crates/project/src/lsp_store.rs | 5 +- crates/rope/src/rope.rs | 25 +++++----- crates/sum_tree/src/sum_tree.rs | 27 ++++------ crates/text/src/anchor.rs | 6 ++- crates/text/src/text.rs | 24 ++++++--- crates/worktree/src/worktree.rs | 11 ++-- 18 files changed, 215 insertions(+), 141 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ef41eafc5..6239c83fdc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3562,6 +3562,7 @@ dependencies = [ "serde", "serde_json", "settings", + "sum_tree", "task", "theme", "ui", diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index 866e3ccd90..4ac37ffd14 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -13,7 +13,7 @@ use std::{ ops::{ControlFlow, Range}, sync::Arc, }; -use sum_tree::{Bias, SumTree}; +use sum_tree::{Bias, Dimensions, SumTree}; use time::OffsetDateTime; use util::{ResultExt as _, TryFutureExt, post_inc}; @@ -331,7 +331,9 @@ impl ChannelChat { .update(&mut cx, |chat, cx| { if let Some(first_id) = chat.first_loaded_message_id() { if first_id <= message_id { - let mut cursor = chat.messages.cursor::<(ChannelMessageId, Count)>(&()); + let mut cursor = chat + .messages + .cursor::>(&()); let message_id = ChannelMessageId::Saved(message_id); cursor.seek(&message_id, Bias::Left); return ControlFlow::Break( @@ -587,7 +589,9 @@ impl ChannelChat { .map(|m| m.nonce) .collect::>(); - let mut old_cursor = self.messages.cursor::<(ChannelMessageId, Count)>(&()); + let mut old_cursor = self + .messages + .cursor::>(&()); let mut new_messages = old_cursor.slice(&first_message.id, Bias::Left); let start_ix = old_cursor.start().1.0; let removed_messages = old_cursor.slice(&last_message.id, Bias::Right); diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 8908143324..0fc119f311 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -46,6 +46,7 @@ project.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true +sum_tree.workspace = true task.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index efe6fb743a..49ae2b9d9c 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -39,6 +39,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; +use sum_tree::Dimensions; use util::{ResultExt, fs::remove_matching}; use workspace::Workspace; @@ -239,7 +240,7 @@ impl RegisteredBuffer { let new_snapshot = new_snapshot.clone(); async move { new_snapshot - .edits_since::<(PointUtf16, usize)>(&old_version) + .edits_since::>(&old_version) .map(|edit| { let edit_start = edit.new.start.0; let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 85495a2611..e25c02432d 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -22,7 +22,7 @@ use std::{ atomic::{AtomicUsize, Ordering::SeqCst}, }, }; -use sum_tree::{Bias, SumTree, Summary, TreeMap}; +use sum_tree::{Bias, Dimensions, SumTree, Summary, TreeMap}; use text::{BufferId, Edit}; use ui::ElementId; @@ -416,7 +416,7 @@ struct TransformSummary { } pub struct BlockChunks<'a> { - transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>, + transforms: sum_tree::Cursor<'a, Transform, Dimensions>, input_chunks: wrap_map::WrapChunks<'a>, input_chunk: Chunk<'a>, output_row: u32, @@ -426,7 +426,7 @@ pub struct BlockChunks<'a> { #[derive(Clone)] pub struct BlockRows<'a> { - transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>, + transforms: sum_tree::Cursor<'a, Transform, Dimensions>, input_rows: wrap_map::WrapRows<'a>, output_row: BlockRow, started: bool, @@ -970,7 +970,7 @@ impl BlockMapReader<'_> { .unwrap_or(self.wrap_snapshot.max_point().row() + 1), ); - let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&start_wrap_row, Bias::Left); while let Some(transform) = cursor.item() { if cursor.start().0 > end_wrap_row { @@ -1292,7 +1292,7 @@ impl BlockSnapshot { ) -> BlockChunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&BlockRow(rows.start), Bias::Right); let transform_output_start = cursor.start().0.0; let transform_input_start = cursor.start().1.0; @@ -1324,9 +1324,9 @@ impl BlockSnapshot { } pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows<'_> { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&start_row, Bias::Right); - let (output_start, input_start) = cursor.start(); + let Dimensions(output_start, input_start, _) = cursor.start(); let overshoot = if cursor .item() .map_or(false, |transform| transform.block.is_none()) @@ -1441,14 +1441,14 @@ impl BlockSnapshot { } pub fn longest_row_in_range(&self, range: Range) -> BlockRow { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&range.start, Bias::Right); let mut longest_row = range.start; let mut longest_row_chars = 0; if let Some(transform) = cursor.item() { if transform.block.is_none() { - let (output_start, input_start) = cursor.start(); + let Dimensions(output_start, input_start, _) = cursor.start(); let overshoot = range.start.0 - output_start.0; let wrap_start_row = input_start.0 + overshoot; let wrap_end_row = cmp::min( @@ -1474,7 +1474,7 @@ impl BlockSnapshot { if let Some(transform) = cursor.item() { if transform.block.is_none() { - let (output_start, input_start) = cursor.start(); + let Dimensions(output_start, input_start, _) = cursor.start(); let overshoot = range.end.0 - output_start.0; let wrap_start_row = input_start.0; let wrap_end_row = input_start.0 + overshoot; @@ -1492,10 +1492,10 @@ impl BlockSnapshot { } pub(super) fn line_len(&self, row: BlockRow) -> u32 { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&BlockRow(row.0), Bias::Right); if let Some(transform) = cursor.item() { - let (output_start, input_start) = cursor.start(); + let Dimensions(output_start, input_start, _) = cursor.start(); let overshoot = row.0 - output_start.0; if transform.block.is_some() { 0 @@ -1510,13 +1510,13 @@ impl BlockSnapshot { } pub(super) fn is_block_line(&self, row: BlockRow) -> bool { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&row, Bias::Right); cursor.item().map_or(false, |t| t.block.is_some()) } pub(super) fn is_folded_buffer_header(&self, row: BlockRow) -> bool { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&row, Bias::Right); let Some(transform) = cursor.item() else { return false; @@ -1528,7 +1528,7 @@ impl BlockSnapshot { let wrap_point = self .wrap_snapshot .make_wrap_point(Point::new(row.0, 0), Bias::Left); - let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&WrapRow(wrap_point.row()), Bias::Right); cursor.item().map_or(false, |transform| { transform @@ -1539,7 +1539,7 @@ impl BlockSnapshot { } pub fn clip_point(&self, point: BlockPoint, bias: Bias) -> BlockPoint { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&BlockRow(point.row), Bias::Right); let max_input_row = WrapRow(self.transforms.summary().input_rows); @@ -1549,8 +1549,8 @@ impl BlockSnapshot { loop { if let Some(transform) = cursor.item() { - let (output_start_row, input_start_row) = cursor.start(); - let (output_end_row, input_end_row) = cursor.end(); + let Dimensions(output_start_row, input_start_row, _) = cursor.start(); + let Dimensions(output_end_row, input_end_row, _) = cursor.end(); let output_start = Point::new(output_start_row.0, 0); let input_start = Point::new(input_start_row.0, 0); let input_end = Point::new(input_end_row.0, 0); @@ -1599,13 +1599,13 @@ impl BlockSnapshot { } pub fn to_block_point(&self, wrap_point: WrapPoint) -> BlockPoint { - let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&WrapRow(wrap_point.row()), Bias::Right); if let Some(transform) = cursor.item() { if transform.block.is_some() { BlockPoint::new(cursor.start().1.0, 0) } else { - let (input_start_row, output_start_row) = cursor.start(); + let Dimensions(input_start_row, output_start_row, _) = cursor.start(); let input_start = Point::new(input_start_row.0, 0); let output_start = Point::new(output_start_row.0, 0); let input_overshoot = wrap_point.0 - input_start; @@ -1617,7 +1617,7 @@ impl BlockSnapshot { } pub fn to_wrap_point(&self, block_point: BlockPoint, bias: Bias) -> WrapPoint { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&BlockRow(block_point.row), Bias::Right); if let Some(transform) = cursor.item() { match transform.block.as_ref() { diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 829d34ff58..c4e53a0f43 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -17,7 +17,7 @@ use std::{ sync::Arc, usize, }; -use sum_tree::{Bias, Cursor, FilterCursor, SumTree, Summary, TreeMap}; +use sum_tree::{Bias, Cursor, Dimensions, FilterCursor, SumTree, Summary, TreeMap}; use ui::IntoElement as _; use util::post_inc; @@ -98,7 +98,9 @@ impl FoldPoint { } pub fn to_inlay_point(self, snapshot: &FoldSnapshot) -> InlayPoint { - let mut cursor = snapshot.transforms.cursor::<(FoldPoint, InlayPoint)>(&()); + let mut cursor = snapshot + .transforms + .cursor::>(&()); cursor.seek(&self, Bias::Right); let overshoot = self.0 - cursor.start().0.0; InlayPoint(cursor.start().1.0 + overshoot) @@ -107,7 +109,7 @@ impl FoldPoint { pub fn to_offset(self, snapshot: &FoldSnapshot) -> FoldOffset { let mut cursor = snapshot .transforms - .cursor::<(FoldPoint, TransformSummary)>(&()); + .cursor::>(&()); cursor.seek(&self, Bias::Right); let overshoot = self.0 - cursor.start().1.output.lines; let mut offset = cursor.start().1.output.len; @@ -567,8 +569,9 @@ impl FoldMap { let mut old_transforms = self .snapshot .transforms - .cursor::<(InlayOffset, FoldOffset)>(&()); - let mut new_transforms = new_transforms.cursor::<(InlayOffset, FoldOffset)>(&()); + .cursor::>(&()); + let mut new_transforms = + new_transforms.cursor::>(&()); for mut edit in inlay_edits { old_transforms.seek(&edit.old.start, Bias::Left); @@ -651,7 +654,9 @@ impl FoldSnapshot { pub fn text_summary_for_range(&self, range: Range) -> TextSummary { let mut summary = TextSummary::default(); - let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&range.start, Bias::Right); if let Some(transform) = cursor.item() { let start_in_transform = range.start.0 - cursor.start().0.0; @@ -700,7 +705,9 @@ impl FoldSnapshot { } pub fn to_fold_point(&self, point: InlayPoint, bias: Bias) -> FoldPoint { - let mut cursor = self.transforms.cursor::<(InlayPoint, FoldPoint)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&point, Bias::Right); if cursor.item().map_or(false, |t| t.is_fold()) { if bias == Bias::Left || point == cursor.start().0 { @@ -734,7 +741,9 @@ impl FoldSnapshot { } let fold_point = FoldPoint::new(start_row, 0); - let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&fold_point, Bias::Left); let overshoot = fold_point.0 - cursor.start().0.0; @@ -816,7 +825,9 @@ impl FoldSnapshot { language_aware: bool, highlights: Highlights<'a>, ) -> FoldChunks<'a> { - let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>(&()); + let mut transform_cursor = self + .transforms + .cursor::>(&()); transform_cursor.seek(&range.start, Bias::Right); let inlay_start = { @@ -871,7 +882,9 @@ impl FoldSnapshot { } pub fn clip_point(&self, point: FoldPoint, bias: Bias) -> FoldPoint { - let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&point, Bias::Right); if let Some(transform) = cursor.item() { let transform_start = cursor.start().0.0; @@ -1196,7 +1209,7 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize { #[derive(Clone)] pub struct FoldRows<'a> { - cursor: Cursor<'a, Transform, (FoldPoint, InlayPoint)>, + cursor: Cursor<'a, Transform, Dimensions>, input_rows: InlayBufferRows<'a>, fold_point: FoldPoint, } @@ -1313,7 +1326,7 @@ impl DerefMut for ChunkRendererContext<'_, '_> { } pub struct FoldChunks<'a> { - transform_cursor: Cursor<'a, Transform, (FoldOffset, InlayOffset)>, + transform_cursor: Cursor<'a, Transform, Dimensions>, inlay_chunks: InlayChunks<'a>, inlay_chunk: Option<(InlayOffset, InlayChunk<'a>)>, inlay_offset: InlayOffset, @@ -1448,7 +1461,7 @@ impl FoldOffset { pub fn to_point(self, snapshot: &FoldSnapshot) -> FoldPoint { let mut cursor = snapshot .transforms - .cursor::<(FoldOffset, TransformSummary)>(&()); + .cursor::>(&()); cursor.seek(&self, Bias::Right); let overshoot = if cursor.item().map_or(true, |t| t.is_fold()) { Point::new(0, (self.0 - cursor.start().0.0) as u32) @@ -1462,7 +1475,9 @@ impl FoldOffset { #[cfg(test)] pub fn to_inlay_offset(self, snapshot: &FoldSnapshot) -> InlayOffset { - let mut cursor = snapshot.transforms.cursor::<(FoldOffset, InlayOffset)>(&()); + let mut cursor = snapshot + .transforms + .cursor::>(&()); cursor.seek(&self, Bias::Right); let overshoot = self.0 - cursor.start().0.0; InlayOffset(cursor.start().1.0 + overshoot) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 0b1c7a4bed..fd49c262c6 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -10,7 +10,7 @@ use std::{ ops::{Add, AddAssign, Range, Sub, SubAssign}, sync::Arc, }; -use sum_tree::{Bias, Cursor, SumTree}; +use sum_tree::{Bias, Cursor, Dimensions, SumTree}; use text::{Patch, Rope}; use ui::{ActiveTheme, IntoElement as _, ParentElement as _, Styled as _, div}; @@ -235,14 +235,14 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for Point { #[derive(Clone)] pub struct InlayBufferRows<'a> { - transforms: Cursor<'a, Transform, (InlayPoint, Point)>, + transforms: Cursor<'a, Transform, Dimensions>, buffer_rows: MultiBufferRows<'a>, inlay_row: u32, max_buffer_row: MultiBufferRow, } pub struct InlayChunks<'a> { - transforms: Cursor<'a, Transform, (InlayOffset, usize)>, + transforms: Cursor<'a, Transform, Dimensions>, buffer_chunks: CustomHighlightsChunks<'a>, buffer_chunk: Option>, inlay_chunks: Option>, @@ -551,7 +551,9 @@ impl InlayMap { } else { let mut inlay_edits = Patch::default(); let mut new_transforms = SumTree::default(); - let mut cursor = snapshot.transforms.cursor::<(usize, InlayOffset)>(&()); + let mut cursor = snapshot + .transforms + .cursor::>(&()); let mut buffer_edits_iter = buffer_edits.iter().peekable(); while let Some(buffer_edit) = buffer_edits_iter.next() { new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left), &()); @@ -770,20 +772,20 @@ impl InlaySnapshot { pub fn to_point(&self, offset: InlayOffset) -> InlayPoint { let mut cursor = self .transforms - .cursor::<(InlayOffset, (InlayPoint, usize))>(&()); + .cursor::>(&()); cursor.seek(&offset, Bias::Right); let overshoot = offset.0 - cursor.start().0.0; match cursor.item() { Some(Transform::Isomorphic(_)) => { - let buffer_offset_start = cursor.start().1.1; + let buffer_offset_start = cursor.start().2; let buffer_offset_end = buffer_offset_start + overshoot; let buffer_start = self.buffer.offset_to_point(buffer_offset_start); let buffer_end = self.buffer.offset_to_point(buffer_offset_end); - InlayPoint(cursor.start().1.0.0 + (buffer_end - buffer_start)) + InlayPoint(cursor.start().1.0 + (buffer_end - buffer_start)) } Some(Transform::Inlay(inlay)) => { let overshoot = inlay.text.offset_to_point(overshoot); - InlayPoint(cursor.start().1.0.0 + overshoot) + InlayPoint(cursor.start().1.0 + overshoot) } None => self.max_point(), } @@ -800,26 +802,26 @@ impl InlaySnapshot { pub fn to_offset(&self, point: InlayPoint) -> InlayOffset { let mut cursor = self .transforms - .cursor::<(InlayPoint, (InlayOffset, Point))>(&()); + .cursor::>(&()); cursor.seek(&point, Bias::Right); let overshoot = point.0 - cursor.start().0.0; match cursor.item() { Some(Transform::Isomorphic(_)) => { - let buffer_point_start = cursor.start().1.1; + let buffer_point_start = cursor.start().2; let buffer_point_end = buffer_point_start + overshoot; let buffer_offset_start = self.buffer.point_to_offset(buffer_point_start); let buffer_offset_end = self.buffer.point_to_offset(buffer_point_end); - InlayOffset(cursor.start().1.0.0 + (buffer_offset_end - buffer_offset_start)) + InlayOffset(cursor.start().1.0 + (buffer_offset_end - buffer_offset_start)) } Some(Transform::Inlay(inlay)) => { let overshoot = inlay.text.point_to_offset(overshoot); - InlayOffset(cursor.start().1.0.0 + overshoot) + InlayOffset(cursor.start().1.0 + overshoot) } None => self.len(), } } pub fn to_buffer_point(&self, point: InlayPoint) -> Point { - let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&point, Bias::Right); match cursor.item() { Some(Transform::Isomorphic(_)) => { @@ -831,7 +833,9 @@ impl InlaySnapshot { } } pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize { - let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&offset, Bias::Right); match cursor.item() { Some(Transform::Isomorphic(_)) => { @@ -844,7 +848,9 @@ impl InlaySnapshot { } pub fn to_inlay_offset(&self, offset: usize) -> InlayOffset { - let mut cursor = self.transforms.cursor::<(usize, InlayOffset)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&offset, Bias::Left); loop { match cursor.item() { @@ -877,7 +883,7 @@ impl InlaySnapshot { } } pub fn to_inlay_point(&self, point: Point) -> InlayPoint { - let mut cursor = self.transforms.cursor::<(Point, InlayPoint)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&point, Bias::Left); loop { match cursor.item() { @@ -911,7 +917,7 @@ impl InlaySnapshot { } pub fn clip_point(&self, mut point: InlayPoint, mut bias: Bias) -> InlayPoint { - let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&()); + let mut cursor = self.transforms.cursor::>(&()); cursor.seek(&point, Bias::Left); loop { match cursor.item() { @@ -1008,7 +1014,9 @@ impl InlaySnapshot { pub fn text_summary_for_range(&self, range: Range) -> TextSummary { let mut summary = TextSummary::default(); - let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&range.start, Bias::Right); let overshoot = range.start.0 - cursor.start().0.0; @@ -1056,7 +1064,7 @@ impl InlaySnapshot { } pub fn row_infos(&self, row: u32) -> InlayBufferRows<'_> { - let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&()); + let mut cursor = self.transforms.cursor::>(&()); let inlay_point = InlayPoint::new(row, 0); cursor.seek(&inlay_point, Bias::Left); @@ -1098,7 +1106,9 @@ impl InlaySnapshot { language_aware: bool, highlights: Highlights<'a>, ) -> InlayChunks<'a> { - let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&range.start, Bias::Right); let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end); diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index d55577826e..269f8f0c40 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -9,7 +9,7 @@ use multi_buffer::{MultiBufferSnapshot, RowInfo}; use smol::future::yield_now; use std::sync::LazyLock; use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration}; -use sum_tree::{Bias, Cursor, SumTree}; +use sum_tree::{Bias, Cursor, Dimensions, SumTree}; use text::Patch; pub use super::tab_map::TextSummary; @@ -55,7 +55,7 @@ pub struct WrapChunks<'a> { input_chunk: Chunk<'a>, output_position: WrapPoint, max_output_row: u32, - transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>, + transforms: Cursor<'a, Transform, Dimensions>, snapshot: &'a WrapSnapshot, } @@ -66,7 +66,7 @@ pub struct WrapRows<'a> { output_row: u32, soft_wrapped: bool, max_output_row: u32, - transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>, + transforms: Cursor<'a, Transform, Dimensions>, } impl WrapRows<'_> { @@ -598,7 +598,9 @@ impl WrapSnapshot { ) -> WrapChunks<'a> { let output_start = WrapPoint::new(rows.start, 0); let output_end = WrapPoint::new(rows.end, 0); - let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); + let mut transforms = self + .transforms + .cursor::>(&()); transforms.seek(&output_start, Bias::Right); let mut input_start = TabPoint(transforms.start().1.0); if transforms.item().map_or(false, |t| t.is_isomorphic()) { @@ -626,7 +628,9 @@ impl WrapSnapshot { } pub fn line_len(&self, row: u32) -> u32 { - let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left); if cursor .item() @@ -651,7 +655,9 @@ impl WrapSnapshot { let start = WrapPoint::new(rows.start, 0); let end = WrapPoint::new(rows.end, 0); - let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&start, Bias::Right); if let Some(transform) = cursor.item() { let start_in_transform = start.0 - cursor.start().0.0; @@ -721,7 +727,9 @@ impl WrapSnapshot { } pub fn row_infos(&self, start_row: u32) -> WrapRows<'_> { - let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); + let mut transforms = self + .transforms + .cursor::>(&()); transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left); let mut input_row = transforms.start().1.row(); if transforms.item().map_or(false, |t| t.is_isomorphic()) { @@ -741,7 +749,9 @@ impl WrapSnapshot { } pub fn to_tab_point(&self, point: WrapPoint) -> TabPoint { - let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&point, Bias::Right); let mut tab_point = cursor.start().1.0; if cursor.item().map_or(false, |t| t.is_isomorphic()) { @@ -759,7 +769,9 @@ impl WrapSnapshot { } pub fn tab_point_to_wrap_point(&self, point: TabPoint) -> WrapPoint { - let mut cursor = self.transforms.cursor::<(TabPoint, WrapPoint)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&point, Bias::Right); WrapPoint(cursor.start().1.0 + (point.0 - cursor.start().0.0)) } @@ -784,7 +796,9 @@ impl WrapSnapshot { *point.column_mut() = 0; - let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&point, Bias::Right); if cursor.item().is_none() { cursor.prev(); @@ -804,7 +818,9 @@ impl WrapSnapshot { pub fn next_row_boundary(&self, mut point: WrapPoint) -> Option { point.0 += Point::new(1, 0); - let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); + let mut cursor = self + .transforms + .cursor::>(&()); cursor.seek(&point, Bias::Right); while let Some(transform) = cursor.item() { if transform.is_isomorphic() && cursor.start().1.column() == 0 { diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 328a6a4cc1..709323ef58 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -16,7 +16,7 @@ use crate::{ use collections::VecDeque; use refineable::Refineable as _; use std::{cell::RefCell, ops::Range, rc::Rc}; -use sum_tree::{Bias, SumTree}; +use sum_tree::{Bias, Dimensions, SumTree}; /// Construct a new list element pub fn list(state: ListState) -> List { @@ -371,14 +371,14 @@ impl ListState { return None; } - let mut cursor = state.items.cursor::<(Count, Height)>(&()); + let mut cursor = state.items.cursor::>(&()); cursor.seek(&Count(scroll_top.item_ix), Bias::Right); let scroll_top = cursor.start().1.0 + scroll_top.offset_in_item; cursor.seek_forward(&Count(ix), Bias::Right); if let Some(&ListItem::Measured { size, .. }) = cursor.item() { - let &(Count(count), Height(top)) = cursor.start(); + let &Dimensions(Count(count), Height(top), _) = cursor.start(); if count == ix { let top = bounds.top() + top - scroll_top; return Some(Bounds::from_corners( diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index f441114a90..c56ffed066 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -17,7 +17,7 @@ use std::{ sync::Arc, }; use streaming_iterator::StreamingIterator; -use sum_tree::{Bias, SeekTarget, SumTree}; +use sum_tree::{Bias, Dimensions, SeekTarget, SumTree}; use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, Rope, ToOffset, ToPoint}; use tree_sitter::{Node, Query, QueryCapture, QueryCaptures, QueryCursor, QueryMatches, Tree}; @@ -285,7 +285,7 @@ impl SyntaxSnapshot { pub fn interpolate(&mut self, text: &BufferSnapshot) { let edits = text - .anchored_edits_since::<(usize, Point)>(&self.interpolated_version) + .anchored_edits_since::>(&self.interpolated_version) .collect::>(); self.interpolated_version = text.version().clone(); @@ -333,7 +333,8 @@ impl SyntaxSnapshot { }; let Some(layer) = cursor.item() else { break }; - let (start_byte, start_point) = layer.range.start.summary::<(usize, Point)>(text); + let Dimensions(start_byte, start_point, _) = + layer.range.start.summary::>(text); // Ignore edits that end before the start of this layer, and don't consider them // for any subsequent layers at this same depth. @@ -562,8 +563,8 @@ impl SyntaxSnapshot { } let Some(step) = step else { break }; - let (step_start_byte, step_start_point) = - step.range.start.summary::<(usize, Point)>(text); + let Dimensions(step_start_byte, step_start_point, _) = + step.range.start.summary::>(text); let step_end_byte = step.range.end.to_offset(text); let mut old_layer = cursor.item(); diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index f0913e30fb..eb12e6929c 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -43,7 +43,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use sum_tree::{Bias, Cursor, Dimension, SumTree, Summary, TreeMap}; +use sum_tree::{Bias, Cursor, Dimension, Dimensions, SumTree, Summary, TreeMap}; use text::{ BufferId, Edit, LineIndent, TextSummary, locator::Locator, @@ -474,7 +474,7 @@ pub struct MultiBufferRows<'a> { pub struct MultiBufferChunks<'a> { excerpts: Cursor<'a, Excerpt, ExcerptOffset>, - diff_transforms: Cursor<'a, DiffTransform, (usize, ExcerptOffset)>, + diff_transforms: Cursor<'a, DiffTransform, Dimensions>, diffs: &'a TreeMap, diff_base_chunks: Option<(BufferId, BufferChunks<'a>)>, buffer_chunk: Option>, @@ -2120,10 +2120,10 @@ impl MultiBuffer { let buffers = self.buffers.borrow(); let mut excerpts = snapshot .excerpts - .cursor::<(Option<&Locator>, ExcerptDimension)>(&()); + .cursor::, ExcerptDimension>>(&()); let mut diff_transforms = snapshot .diff_transforms - .cursor::<(ExcerptDimension, OutputDimension)>(&()); + .cursor::, OutputDimension>>(&()); diff_transforms.next(); let locators = buffers .get(&buffer_id) @@ -2281,7 +2281,7 @@ impl MultiBuffer { let mut new_excerpts = SumTree::default(); let mut cursor = snapshot .excerpts - .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); + .cursor::, ExcerptOffset>>(&()); let mut edits = Vec::new(); let mut excerpt_ids = ids.iter().copied().peekable(); let mut removed_buffer_ids = Vec::new(); @@ -2492,7 +2492,7 @@ impl MultiBuffer { for locator in &buffer_state.excerpts { let mut cursor = snapshot .excerpts - .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); + .cursor::, ExcerptOffset>>(&()); cursor.seek_forward(&Some(locator), Bias::Left); if let Some(excerpt) = cursor.item() { if excerpt.locator == *locator { @@ -2845,7 +2845,7 @@ impl MultiBuffer { let mut new_excerpts = SumTree::default(); let mut cursor = snapshot .excerpts - .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); + .cursor::, ExcerptOffset>>(&()); let mut edits = Vec::>::new(); let prefix = cursor.slice(&Some(locator), Bias::Left); @@ -2921,7 +2921,7 @@ impl MultiBuffer { let mut new_excerpts = SumTree::default(); let mut cursor = snapshot .excerpts - .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); + .cursor::, ExcerptOffset>>(&()); let mut edits = Vec::>::new(); for locator in &locators { @@ -3067,7 +3067,7 @@ impl MultiBuffer { let mut new_excerpts = SumTree::default(); let mut cursor = snapshot .excerpts - .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); + .cursor::, ExcerptOffset>>(&()); for (locator, buffer, buffer_edited) in excerpts_to_edit { new_excerpts.append(cursor.slice(&Some(locator), Bias::Left), &()); @@ -3135,7 +3135,7 @@ impl MultiBuffer { let mut excerpts = snapshot.excerpts.cursor::(&()); let mut old_diff_transforms = snapshot .diff_transforms - .cursor::<(ExcerptOffset, usize)>(&()); + .cursor::>(&()); let mut new_diff_transforms = SumTree::default(); let mut old_expanded_hunks = HashSet::default(); let mut output_edits = Vec::new(); @@ -3260,7 +3260,7 @@ impl MultiBuffer { &self, edit: &Edit>, excerpts: &mut Cursor>, - old_diff_transforms: &mut Cursor, usize)>, + old_diff_transforms: &mut Cursor, usize>>, new_diff_transforms: &mut SumTree, end_of_current_insert: &mut Option<(TypedOffset, DiffTransformHunkInfo)>, old_expanded_hunks: &mut HashSet, @@ -4713,7 +4713,9 @@ impl MultiBufferSnapshot { O: ToOffset, { let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut cursor = self.diff_transforms.cursor::<(usize, ExcerptOffset)>(&()); + let mut cursor = self + .diff_transforms + .cursor::>(&()); cursor.seek(&range.start, Bias::Right); let Some(first_transform) = cursor.item() else { @@ -4867,7 +4869,10 @@ impl MultiBufferSnapshot { &self, anchor: &Anchor, excerpt_position: D, - diff_transforms: &mut Cursor, OutputDimension)>, + diff_transforms: &mut Cursor< + DiffTransform, + Dimensions, OutputDimension>, + >, ) -> D where D: TextDimension + Ord + Sub, @@ -4927,7 +4932,7 @@ impl MultiBufferSnapshot { fn excerpt_offset_for_anchor(&self, anchor: &Anchor) -> ExcerptOffset { let mut cursor = self .excerpts - .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); + .cursor::, ExcerptOffset>>(&()); let locator = self.excerpt_locator_for_id(anchor.excerpt_id); cursor.seek(&Some(locator), Bias::Left); @@ -4971,7 +4976,7 @@ impl MultiBufferSnapshot { let mut cursor = self.excerpts.cursor::(&()); let mut diff_transforms_cursor = self .diff_transforms - .cursor::<(ExcerptDimension, OutputDimension)>(&()); + .cursor::, OutputDimension>>(&()); diff_transforms_cursor.next(); let mut summaries = Vec::new(); @@ -5201,7 +5206,9 @@ impl MultiBufferSnapshot { // Find the given position in the diff transforms. Determine the corresponding // offset in the excerpts, and whether the position is within a deleted hunk. - let mut diff_transforms = self.diff_transforms.cursor::<(usize, ExcerptOffset)>(&()); + let mut diff_transforms = self + .diff_transforms + .cursor::>(&()); diff_transforms.seek(&offset, Bias::Right); if offset == diff_transforms.start().0 && bias == Bias::Left { @@ -5250,7 +5257,7 @@ impl MultiBufferSnapshot { let mut excerpts = self .excerpts - .cursor::<(ExcerptOffset, Option)>(&()); + .cursor::>>(&()); excerpts.seek(&excerpt_offset, Bias::Right); if excerpts.item().is_none() && excerpt_offset == excerpts.start().0 && bias == Bias::Left { excerpts.prev(); @@ -5341,7 +5348,7 @@ impl MultiBufferSnapshot { let start_locator = self.excerpt_locator_for_id(id); let mut excerpts = self .excerpts - .cursor::<(Option<&Locator>, ExcerptDimension)>(&()); + .cursor::, ExcerptDimension>>(&()); excerpts.seek(&Some(start_locator), Bias::Left); excerpts.prev(); @@ -6242,14 +6249,14 @@ impl MultiBufferSnapshot { pub fn range_for_excerpt(&self, excerpt_id: ExcerptId) -> Option> { let mut cursor = self .excerpts - .cursor::<(Option<&Locator>, ExcerptDimension)>(&()); + .cursor::, ExcerptDimension>>(&()); let locator = self.excerpt_locator_for_id(excerpt_id); if cursor.seek(&Some(locator), Bias::Left) { let start = cursor.start().1.clone(); let end = cursor.end().1; let mut diff_transforms = self .diff_transforms - .cursor::<(ExcerptDimension, OutputDimension)>(&()); + .cursor::, OutputDimension>>(&()); diff_transforms.seek(&start, Bias::Left); let overshoot = start.0 - diff_transforms.start().0.0; let start = diff_transforms.start().1.0 + overshoot; diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index 0329a53cc7..29653748e4 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -6,7 +6,7 @@ use db::smol::stream::StreamExt; use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, Task}; use rpc::{Notification, TypedEnvelope, proto}; use std::{ops::Range, sync::Arc}; -use sum_tree::{Bias, SumTree}; +use sum_tree::{Bias, Dimensions, SumTree}; use time::OffsetDateTime; use util::ResultExt; @@ -360,7 +360,9 @@ impl NotificationStore { is_new: bool, cx: &mut Context, ) { - let mut cursor = self.notifications.cursor::<(NotificationId, Count)>(&()); + let mut cursor = self + .notifications + .cursor::>(&()); let mut new_notifications = SumTree::default(); let mut old_range = 0..0; diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 98cecc2e9b..6122204991 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -96,6 +96,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; +use sum_tree::Dimensions; use text::{Anchor, BufferId, LineEnding, OffsetRangeExt}; use url::Url; use util::{ @@ -7253,7 +7254,9 @@ impl LspStore { let build_incremental_change = || { buffer - .edits_since::<(PointUtf16, usize)>(previous_snapshot.snapshot.version()) + .edits_since::>( + previous_snapshot.snapshot.version(), + ) .map(|edit| { let edit_start = edit.new.start.0; let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0); diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 515cd71331..aa3ed5db57 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -12,7 +12,7 @@ use std::{ ops::{self, AddAssign, Range}, str, }; -use sum_tree::{Bias, Dimension, SumTree}; +use sum_tree::{Bias, Dimension, Dimensions, SumTree}; pub use chunk::ChunkSlice; pub use offset_utf16::OffsetUtf16; @@ -282,7 +282,7 @@ impl Rope { if offset >= self.summary().len { return self.summary().len_utf16; } - let mut cursor = self.chunks.cursor::<(usize, OffsetUtf16)>(&()); + let mut cursor = self.chunks.cursor::>(&()); cursor.seek(&offset, Bias::Left); let overshoot = offset - cursor.start().0; cursor.start().1 @@ -295,7 +295,7 @@ impl Rope { if offset >= self.summary().len_utf16 { return self.summary().len; } - let mut cursor = self.chunks.cursor::<(OffsetUtf16, usize)>(&()); + let mut cursor = self.chunks.cursor::>(&()); cursor.seek(&offset, Bias::Left); let overshoot = offset - cursor.start().0; cursor.start().1 @@ -308,7 +308,7 @@ impl Rope { if offset >= self.summary().len { return self.summary().lines; } - let mut cursor = self.chunks.cursor::<(usize, Point)>(&()); + let mut cursor = self.chunks.cursor::>(&()); cursor.seek(&offset, Bias::Left); let overshoot = offset - cursor.start().0; cursor.start().1 @@ -321,7 +321,7 @@ impl Rope { if offset >= self.summary().len { return self.summary().lines_utf16(); } - let mut cursor = self.chunks.cursor::<(usize, PointUtf16)>(&()); + let mut cursor = self.chunks.cursor::>(&()); cursor.seek(&offset, Bias::Left); let overshoot = offset - cursor.start().0; cursor.start().1 @@ -334,7 +334,7 @@ impl Rope { if point >= self.summary().lines { return self.summary().lines_utf16(); } - let mut cursor = self.chunks.cursor::<(Point, PointUtf16)>(&()); + let mut cursor = self.chunks.cursor::>(&()); cursor.seek(&point, Bias::Left); let overshoot = point - cursor.start().0; cursor.start().1 @@ -347,7 +347,7 @@ impl Rope { if point >= self.summary().lines { return self.summary().len; } - let mut cursor = self.chunks.cursor::<(Point, usize)>(&()); + let mut cursor = self.chunks.cursor::>(&()); cursor.seek(&point, Bias::Left); let overshoot = point - cursor.start().0; cursor.start().1 @@ -368,7 +368,7 @@ impl Rope { if point >= self.summary().lines_utf16() { return self.summary().len; } - let mut cursor = self.chunks.cursor::<(PointUtf16, usize)>(&()); + let mut cursor = self.chunks.cursor::>(&()); cursor.seek(&point, Bias::Left); let overshoot = point - cursor.start().0; cursor.start().1 @@ -381,7 +381,7 @@ impl Rope { if point.0 >= self.summary().lines_utf16() { return self.summary().lines; } - let mut cursor = self.chunks.cursor::<(PointUtf16, Point)>(&()); + let mut cursor = self.chunks.cursor::>(&()); cursor.seek(&point.0, Bias::Left); let overshoot = Unclipped(point.0 - cursor.start().0); cursor.start().1 @@ -1168,16 +1168,17 @@ pub trait TextDimension: fn add_assign(&mut self, other: &Self); } -impl TextDimension for (D1, D2) { +impl TextDimension for Dimensions { fn from_text_summary(summary: &TextSummary) -> Self { - ( + Dimensions( D1::from_text_summary(summary), D2::from_text_summary(summary), + (), ) } fn from_chunk(chunk: ChunkSlice) -> Self { - (D1::from_chunk(chunk), D2::from_chunk(chunk)) + Dimensions(D1::from_chunk(chunk), D2::from_chunk(chunk), ()) } fn add_assign(&mut self, other: &Self) { diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index 4c5ce39590..3a12e3a681 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -101,37 +101,32 @@ impl<'a, T: Summary> Dimension<'a, T> for () { fn add_summary(&mut self, _: &'a T, _: &T::Context) {} } -impl<'a, T: Summary, D1: Dimension<'a, T>, D2: Dimension<'a, T>> Dimension<'a, T> for (D1, D2) { +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub struct Dimensions(pub D1, pub D2, pub D3); + +impl<'a, T: Summary, D1: Dimension<'a, T>, D2: Dimension<'a, T>, D3: Dimension<'a, T>> + Dimension<'a, T> for Dimensions +{ fn zero(cx: &T::Context) -> Self { - (D1::zero(cx), D2::zero(cx)) + Dimensions(D1::zero(cx), D2::zero(cx), D3::zero(cx)) } fn add_summary(&mut self, summary: &'a T, cx: &T::Context) { self.0.add_summary(summary, cx); self.1.add_summary(summary, cx); + self.2.add_summary(summary, cx); } } -impl<'a, S, D1, D2> SeekTarget<'a, S, (D1, D2)> for D1 -where - S: Summary, - D1: SeekTarget<'a, S, D1> + Dimension<'a, S>, - D2: Dimension<'a, S>, -{ - fn cmp(&self, cursor_location: &(D1, D2), cx: &S::Context) -> Ordering { - self.cmp(&cursor_location.0, cx) - } -} - -impl<'a, S, D1, D2, D3> SeekTarget<'a, S, ((D1, D2), D3)> for D1 +impl<'a, S, D1, D2, D3> SeekTarget<'a, S, Dimensions> for D1 where S: Summary, D1: SeekTarget<'a, S, D1> + Dimension<'a, S>, D2: Dimension<'a, S>, D3: Dimension<'a, S>, { - fn cmp(&self, cursor_location: &((D1, D2), D3), cx: &S::Context) -> Ordering { - self.cmp(&cursor_location.0.0, cx) + fn cmp(&self, cursor_location: &Dimensions, cx: &S::Context) -> Ordering { + self.cmp(&cursor_location.0, cx) } } diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index bf17336f9d..c4778216e0 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -3,7 +3,7 @@ use crate::{ locator::Locator, }; use std::{cmp::Ordering, fmt::Debug, ops::Range}; -use sum_tree::Bias; +use sum_tree::{Bias, Dimensions}; /// A timestamped position in a buffer #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, Default)] @@ -102,7 +102,9 @@ impl Anchor { let Some(fragment_id) = buffer.try_fragment_id_for_anchor(self) else { return false; }; - let mut fragment_cursor = buffer.fragments.cursor::<(Option<&Locator>, usize)>(&None); + let mut fragment_cursor = buffer + .fragments + .cursor::, usize>>(&None); fragment_cursor.seek(&Some(fragment_id), Bias::Left); fragment_cursor .item() diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index aded03d46a..68c7b2a2cd 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -37,7 +37,7 @@ use std::{ }; pub use subscription::*; pub use sum_tree::Bias; -use sum_tree::{FilterCursor, SumTree, TreeMap, TreeSet}; +use sum_tree::{Dimensions, FilterCursor, SumTree, TreeMap, TreeSet}; use undo_map::UndoMap; #[cfg(any(test, feature = "test-support"))] @@ -1071,7 +1071,9 @@ impl Buffer { let mut insertion_offset = 0; let mut new_ropes = RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); - let mut old_fragments = self.fragments.cursor::<(VersionedFullOffset, usize)>(&cx); + let mut old_fragments = self + .fragments + .cursor::>(&cx); let mut new_fragments = old_fragments.slice(&VersionedFullOffset::Offset(ranges[0].start), Bias::Left); new_ropes.append(new_fragments.summary().text); @@ -1298,7 +1300,9 @@ impl Buffer { self.snapshot.undo_map.insert(undo); let mut edits = Patch::default(); - let mut old_fragments = self.fragments.cursor::<(Option<&Locator>, usize)>(&None); + let mut old_fragments = self + .fragments + .cursor::, usize>>(&None); let mut new_fragments = SumTree::new(&None); let mut new_ropes = RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); @@ -1561,7 +1565,9 @@ impl Buffer { D: TextDimension, { // get fragment ranges - let mut cursor = self.fragments.cursor::<(Option<&Locator>, usize)>(&None); + let mut cursor = self + .fragments + .cursor::, usize>>(&None); let offset_ranges = self .fragment_ids_for_edits(edit_ids.into_iter()) .into_iter() @@ -2232,7 +2238,9 @@ impl BufferSnapshot { { let anchors = anchors.into_iter(); let mut insertion_cursor = self.insertions.cursor::(&()); - let mut fragment_cursor = self.fragments.cursor::<(Option<&Locator>, usize)>(&None); + let mut fragment_cursor = self + .fragments + .cursor::, usize>>(&None); let mut text_cursor = self.visible_text.cursor(0); let mut position = D::zero(&()); @@ -2318,7 +2326,9 @@ impl BufferSnapshot { ); }; - let mut fragment_cursor = self.fragments.cursor::<(Option<&Locator>, usize)>(&None); + let mut fragment_cursor = self + .fragments + .cursor::, usize>>(&None); fragment_cursor.seek(&Some(&insertion.fragment_id), Bias::Left); let fragment = fragment_cursor.item().unwrap(); let mut fragment_offset = fragment_cursor.start().1; @@ -2476,7 +2486,7 @@ impl BufferSnapshot { }; let mut cursor = self .fragments - .cursor::<(Option<&Locator>, FragmentTextSummary)>(&None); + .cursor::, FragmentTextSummary>>(&None); let start_fragment_id = self.fragment_id_for_anchor(&range.start); cursor.seek(&Some(start_fragment_id), Bias::Left); diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index e6949f62df..b5a0f71e81 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -62,7 +62,7 @@ use std::{ }, time::{Duration, Instant}, }; -use sum_tree::{Bias, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet}; +use sum_tree::{Bias, Dimensions, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet}; use text::{LineEnding, Rope}; use util::{ ResultExt, @@ -3566,10 +3566,15 @@ impl<'a> sum_tree::Dimension<'a, PathSummary> for GitSummary { } } -impl<'a> sum_tree::SeekTarget<'a, PathSummary, (TraversalProgress<'a>, GitSummary)> +impl<'a> + sum_tree::SeekTarget<'a, PathSummary, Dimensions, GitSummary>> for PathTarget<'_> { - fn cmp(&self, cursor_location: &(TraversalProgress<'a>, GitSummary), _: &()) -> Ordering { + fn cmp( + &self, + cursor_location: &Dimensions, GitSummary>, + _: &(), + ) -> Ordering { self.cmp_path(&cursor_location.0.max_path) } } From 669c57b45ff7a214b6a3efce8835bb1e4d137489 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Mon, 4 Aug 2025 20:19:42 -0500 Subject: [PATCH 070/693] Add minidump crash reporting (#35263) - [x] Handle uploading minidumps from the remote_server - [x] Associate minidumps with panics with some sort of ID (we don't use session_id on the remote) - [x] Update the protobufs and client/server code to request panics - [x] Upload minidumps with no corresponding panic - [x] Fill in panic info when there _is_ a corresponding panic - [x] Use an env var for the sentry endpoint instead of hardcoding it Release Notes: - Zed now generates minidumps for crash reporting --------- Co-authored-by: Max Brunsfeld --- .github/workflows/ci.yml | 1 + .github/workflows/nix.yml | 1 + .github/workflows/release_nightly.yml | 1 + Cargo.lock | 189 +++++++++++++++++++- Cargo.toml | 5 + crates/client/src/telemetry.rs | 6 + crates/crashes/Cargo.toml | 20 +++ crates/crashes/LICENSE-GPL | 1 + crates/crashes/src/crashes.rs | 172 ++++++++++++++++++ crates/http_client/Cargo.toml | 1 + crates/http_client/src/async_body.rs | 11 ++ crates/http_client/src/http_client.rs | 77 +++----- crates/proto/proto/app.proto | 11 +- crates/proto/proto/zed.proto | 9 +- crates/proto/src/proto.rs | 6 +- crates/remote_server/Cargo.toml | 3 + crates/remote_server/src/main.rs | 9 + crates/remote_server/src/unix.rs | 108 +++++++---- crates/reqwest_client/src/reqwest_client.rs | 23 ++- crates/zed/Cargo.toml | 2 + crates/zed/src/main.rs | 14 ++ crates/zed/src/reliability.rs | 139 +++++++++++--- docs/src/development/debugging-crashes.md | 1 + docs/src/telemetry.md | 6 +- tooling/workspace-hack/Cargo.toml | 28 +-- 25 files changed, 709 insertions(+), 135 deletions(-) create mode 100644 crates/crashes/Cargo.toml create mode 120000 crates/crashes/LICENSE-GPL create mode 100644 crates/crashes/src/crashes.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7dfc33e0d2..c08f4ac211 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,7 @@ env: DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} + ZED_SENTRY_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} jobs: job_spec: diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index beacd27774..c019f805fe 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -29,6 +29,7 @@ jobs: runs-on: ${{ matrix.system.runner }} env: ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} + ZED_SENTRY_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }} GIT_LFS_SKIP_SMUDGE: 1 # breaks the livekit rust sdk examples which we don't actually depend on steps: diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 4f7506967b..69e5f86cb6 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -13,6 +13,7 @@ env: CARGO_INCREMENTAL: 0 RUST_BACKTRACE: 1 ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} + ZED_SENTRY_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} diff --git a/Cargo.lock b/Cargo.lock index 6239c83fdc..eae04776d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1172,7 +1172,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_qs 0.10.1", - "smart-default", + "smart-default 0.6.0", "smol_str 0.1.24", "thiserror 1.0.69", "tokio", @@ -3927,6 +3927,42 @@ dependencies = [ "target-lexicon 0.13.2", ] +[[package]] +name = "crash-context" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031ed29858d90cfdf27fe49fae28028a1f20466db97962fa2f4ea34809aeebf3" +dependencies = [ + "cfg-if", + "libc", + "mach2", +] + +[[package]] +name = "crash-handler" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2066907075af649bcb8bcb1b9b986329b243677e6918b2d920aa64b0aac5ace3" +dependencies = [ + "cfg-if", + "crash-context", + "libc", + "mach2", + "parking_lot", +] + +[[package]] +name = "crashes" +version = "0.1.0" +dependencies = [ + "crash-handler", + "log", + "minidumper", + "paths", + "smol", + "workspace-hack", +] + [[package]] name = "crc" version = "3.2.1" @@ -4453,6 +4489,15 @@ dependencies = [ "zlog", ] +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + [[package]] name = "deepseek" version = "0.1.0" @@ -7235,6 +7280,17 @@ dependencies = [ "workspace-hack", ] +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + [[package]] name = "google_ai" version = "0.1.0" @@ -7850,6 +7906,7 @@ dependencies = [ "http-body 1.0.1", "log", "parking_lot", + "reqwest 0.12.15 (git+https://github.com/zed-industries/reqwest.git?rev=951c770a32f1998d6e999cef3e59e0013e6c4415)", "serde", "serde_json", "url", @@ -10080,6 +10137,63 @@ dependencies = [ "unicase", ] +[[package]] +name = "minidump-common" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c4d14bcca0fd3ed165a03000480aaa364c6860c34e900cb2dafdf3b95340e77" +dependencies = [ + "bitflags 2.9.0", + "debugid", + "num-derive", + "num-traits", + "range-map", + "scroll", + "smart-default 0.7.1", +] + +[[package]] +name = "minidump-writer" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abcd9c8a1e6e1e9d56ce3627851f39a17ea83e17c96bc510f29d7e43d78a7d" +dependencies = [ + "bitflags 2.9.0", + "byteorder", + "cfg-if", + "crash-context", + "goblin", + "libc", + "log", + "mach2", + "memmap2", + "memoffset", + "minidump-common", + "nix 0.28.0", + "procfs-core", + "scroll", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "minidumper" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4ebc9d1f8847ec1d078f78b35ed598e0ebefa1f242d5f83cd8d7f03960a7d1" +dependencies = [ + "cfg-if", + "crash-context", + "libc", + "log", + "minidump-writer", + "parking_lot", + "polling", + "scroll", + "thiserror 1.0.69", + "uds", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -12069,6 +12183,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plist" version = "1.7.1" @@ -12329,6 +12449,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "procfs-core" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" +dependencies = [ + "bitflags 2.9.0", + "hex", +] + [[package]] name = "prodash" version = "29.0.2" @@ -12979,6 +13109,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "range-map" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12a5a2d6c7039059af621472a4389be1215a816df61aa4d531cfe85264aee95f" +dependencies = [ + "num-traits", +] + [[package]] name = "rangemap" version = "1.5.1" @@ -13321,6 +13460,8 @@ dependencies = [ "clap", "client", "clock", + "crash-handler", + "crashes", "dap", "dap_adapters", "debug_adapter_extension", @@ -13344,6 +13485,7 @@ dependencies = [ "libc", "log", "lsp", + "minidumper", "node_runtime", "paths", "project", @@ -13532,6 +13674,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "once_cell", "percent-encoding", "pin-project-lite", @@ -14260,6 +14403,26 @@ dependencies = [ "once_cell", ] +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "scrypt" version = "0.11.0" @@ -15005,6 +15168,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "smart-default" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "smol" version = "2.0.2" @@ -17288,6 +17462,15 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "uds" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "885c31f06fce836457fe3ef09a59f83fe8db95d270b11cd78f40a4666c4d1661" +dependencies = [ + "libc", +] + [[package]] name = "uds_windows" version = "1.1.0" @@ -19743,9 +19926,11 @@ dependencies = [ "lyon_path", "md-5", "memchr", + "mime_guess", "miniz_oxide", "mio 1.0.3", "naga", + "nix 0.28.0", "nix 0.29.0", "nom", "num-bigint", @@ -20217,6 +20402,7 @@ dependencies = [ "command_palette", "component", "copilot", + "crashes", "dap", "dap_adapters", "db", @@ -20284,6 +20470,7 @@ dependencies = [ "release_channel", "remote", "repl", + "reqwest 0.12.15 (git+https://github.com/zed-industries/reqwest.git?rev=951c770a32f1998d6e999cef3e59e0013e6c4415)", "reqwest_client", "rope", "search", diff --git a/Cargo.toml b/Cargo.toml index d5982116f3..733db92ce9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ members = [ "crates/component", "crates/context_server", "crates/copilot", + "crates/crashes", "crates/credentials_provider", "crates/dap", "crates/dap_adapters", @@ -266,6 +267,7 @@ command_palette_hooks = { path = "crates/command_palette_hooks" } component = { path = "crates/component" } context_server = { path = "crates/context_server" } copilot = { path = "crates/copilot" } +crashes = { path = "crates/crashes" } credentials_provider = { path = "crates/credentials_provider" } dap = { path = "crates/dap" } dap_adapters = { path = "crates/dap_adapters" } @@ -466,6 +468,7 @@ core-foundation = "0.10.0" core-foundation-sys = "0.8.6" core-video = { version = "0.4.3", features = ["metal"] } cpal = "0.16" +crash-handler = "0.6" criterion = { version = "0.5", features = ["html_reports"] } ctor = "0.4.0" dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "1b461b310481d01e02b2603c16d7144b926339f8" } @@ -513,6 +516,7 @@ log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] } lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "39f629bdd03d59abd786ed9fc27e8bca02c0c0ec" } markup5ever_rcdom = "0.3.0" metal = "0.29" +minidumper = "0.8" moka = { version = "0.12.10", features = ["sync"] } naga = { version = "25.0", features = ["wgsl-in"] } nanoid = "0.4" @@ -552,6 +556,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77 "charset", "http2", "macos-system-configuration", + "multipart", "rustls-tls-native-roots", "socks", "stream", diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 7d39464e4a..4a8e745fcb 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -74,6 +74,12 @@ static ZED_CLIENT_CHECKSUM_SEED: LazyLock>> = LazyLock::new(|| { }) }); +pub static SENTRY_MINIDUMP_ENDPOINT: LazyLock> = LazyLock::new(|| { + option_env!("SENTRY_MINIDUMP_ENDPOINT") + .map(|s| s.to_owned()) + .or_else(|| env::var("SENTRY_MINIDUMP_ENDPOINT").ok()) +}); + static DOTNET_PROJECT_FILES_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"^(global\.json|Directory\.Build\.props|.*\.(csproj|fsproj|vbproj|sln))$").unwrap() }); diff --git a/crates/crashes/Cargo.toml b/crates/crashes/Cargo.toml new file mode 100644 index 0000000000..641a97765a --- /dev/null +++ b/crates/crashes/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "crashes" +version = "0.1.0" +publish.workspace = true +edition.workspace = true +license = "GPL-3.0-or-later" + +[dependencies] +crash-handler.workspace = true +log.workspace = true +minidumper.workspace = true +paths.workspace = true +smol.workspace = true +workspace-hack.workspace = true + +[lints] +workspace = true + +[lib] +path = "src/crashes.rs" diff --git a/crates/crashes/LICENSE-GPL b/crates/crashes/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/crashes/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/crashes/src/crashes.rs b/crates/crashes/src/crashes.rs new file mode 100644 index 0000000000..cfb4b57d5d --- /dev/null +++ b/crates/crashes/src/crashes.rs @@ -0,0 +1,172 @@ +use crash_handler::CrashHandler; +use log::info; +use minidumper::{Client, LoopAction, MinidumpBinary}; + +use std::{ + env, + fs::File, + io, + path::{Path, PathBuf}, + process::{self, Command}, + sync::{ + OnceLock, + atomic::{AtomicBool, Ordering}, + }, + thread, + time::Duration, +}; + +// set once the crash handler has initialized and the client has connected to it +pub static CRASH_HANDLER: AtomicBool = AtomicBool::new(false); +// set when the first minidump request is made to avoid generating duplicate crash reports +pub static REQUESTED_MINIDUMP: AtomicBool = AtomicBool::new(false); +const CRASH_HANDLER_TIMEOUT: Duration = Duration::from_secs(60); + +pub async fn init(id: String) { + let exe = env::current_exe().expect("unable to find ourselves"); + let zed_pid = process::id(); + // TODO: we should be able to get away with using 1 crash-handler process per machine, + // but for now we append the PID of the current process which makes it unique per remote + // server or interactive zed instance. This solves an issue where occasionally the socket + // used by the crash handler isn't destroyed correctly which causes it to stay on the file + // system and block further attempts to initialize crash handlers with that socket path. + let socket_name = paths::temp_dir().join(format!("zed-crash-handler-{zed_pid}")); + #[allow(unused)] + let server_pid = Command::new(exe) + .arg("--crash-handler") + .arg(&socket_name) + .spawn() + .expect("unable to spawn server process") + .id(); + info!("spawning crash handler process"); + + let mut elapsed = Duration::ZERO; + let retry_frequency = Duration::from_millis(100); + let mut maybe_client = None; + while maybe_client.is_none() { + if let Ok(client) = Client::with_name(socket_name.as_path()) { + maybe_client = Some(client); + info!("connected to crash handler process after {elapsed:?}"); + break; + } + elapsed += retry_frequency; + smol::Timer::after(retry_frequency).await; + } + let client = maybe_client.unwrap(); + client.send_message(1, id).unwrap(); // set session id on the server + + let client = std::sync::Arc::new(client); + let handler = crash_handler::CrashHandler::attach(unsafe { + let client = client.clone(); + crash_handler::make_crash_event(move |crash_context: &crash_handler::CrashContext| { + // only request a minidump once + let res = if REQUESTED_MINIDUMP + .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) + .is_ok() + { + client.send_message(2, "mistakes were made").unwrap(); + client.ping().unwrap(); + client.request_dump(crash_context).is_ok() + } else { + true + }; + crash_handler::CrashEventResult::Handled(res) + }) + }) + .expect("failed to attach signal handler"); + + #[cfg(target_os = "linux")] + { + handler.set_ptracer(Some(server_pid)); + } + CRASH_HANDLER.store(true, Ordering::Release); + std::mem::forget(handler); + info!("crash handler registered"); + + loop { + client.ping().ok(); + smol::Timer::after(Duration::from_secs(10)).await; + } +} + +pub struct CrashServer { + session_id: OnceLock, +} + +impl minidumper::ServerHandler for CrashServer { + fn create_minidump_file(&self) -> Result<(File, PathBuf), io::Error> { + let err_message = "Need to send a message with the ID upon starting the crash handler"; + let dump_path = paths::logs_dir() + .join(self.session_id.get().expect(err_message)) + .with_extension("dmp"); + let file = File::create(&dump_path)?; + Ok((file, dump_path)) + } + + fn on_minidump_created(&self, result: Result) -> LoopAction { + match result { + Ok(mut md_bin) => { + use io::Write; + let _ = md_bin.file.flush(); + info!("wrote minidump to disk {:?}", md_bin.path); + } + Err(e) => { + info!("failed to write minidump: {:#}", e); + } + } + LoopAction::Exit + } + + fn on_message(&self, kind: u32, buffer: Vec) { + let message = String::from_utf8(buffer).expect("invalid utf-8"); + info!("kind: {kind}, message: {message}",); + if kind == 1 { + self.session_id + .set(message) + .expect("session id already initialized"); + } + } + + fn on_client_disconnected(&self, clients: usize) -> LoopAction { + info!("client disconnected, {clients} remaining"); + if clients == 0 { + LoopAction::Exit + } else { + LoopAction::Continue + } + } +} + +pub fn handle_panic() { + // wait 500ms for the crash handler process to start up + // if it's still not there just write panic info and no minidump + let retry_frequency = Duration::from_millis(100); + for _ in 0..5 { + if CRASH_HANDLER.load(Ordering::Acquire) { + log::error!("triggering a crash to generate a minidump..."); + #[cfg(target_os = "linux")] + CrashHandler.simulate_signal(crash_handler::Signal::Trap as u32); + #[cfg(not(target_os = "linux"))] + CrashHandler.simulate_exception(None); + break; + } + thread::sleep(retry_frequency); + } +} + +pub fn crash_server(socket: &Path) { + let Ok(mut server) = minidumper::Server::with_name(socket) else { + log::info!("Couldn't create socket, there may already be a running crash server"); + return; + }; + let ab = AtomicBool::new(false); + server + .run( + Box::new(CrashServer { + session_id: OnceLock::new(), + }), + &ab, + Some(CRASH_HANDLER_TIMEOUT), + ) + .expect("failed to run server"); +} diff --git a/crates/http_client/Cargo.toml b/crates/http_client/Cargo.toml index 3f51cc5a23..f63bff295e 100644 --- a/crates/http_client/Cargo.toml +++ b/crates/http_client/Cargo.toml @@ -24,6 +24,7 @@ http.workspace = true http-body.workspace = true log.workspace = true parking_lot.workspace = true +reqwest.workspace = true serde.workspace = true serde_json.workspace = true url.workspace = true diff --git a/crates/http_client/src/async_body.rs b/crates/http_client/src/async_body.rs index 88972d279c..473849f3cd 100644 --- a/crates/http_client/src/async_body.rs +++ b/crates/http_client/src/async_body.rs @@ -88,6 +88,17 @@ impl From<&'static str> for AsyncBody { } } +impl TryFrom for AsyncBody { + type Error = anyhow::Error; + + fn try_from(value: reqwest::Body) -> Result { + value + .as_bytes() + .ok_or_else(|| anyhow::anyhow!("Underlying data is a stream")) + .map(|bytes| Self::from_bytes(Bytes::copy_from_slice(bytes))) + } +} + impl> From> for AsyncBody { fn from(body: Option) -> Self { match body { diff --git a/crates/http_client/src/http_client.rs b/crates/http_client/src/http_client.rs index d33bbefc06..a7f75b0962 100644 --- a/crates/http_client/src/http_client.rs +++ b/crates/http_client/src/http_client.rs @@ -7,7 +7,10 @@ use derive_more::Deref; use http::HeaderValue; pub use http::{self, Method, Request, Response, StatusCode, Uri}; -use futures::future::BoxFuture; +use futures::{ + FutureExt as _, + future::{self, BoxFuture}, +}; use http::request::Builder; use parking_lot::Mutex; #[cfg(feature = "test-support")] @@ -89,6 +92,14 @@ pub trait HttpClient: 'static + Send + Sync { fn as_fake(&self) -> &FakeHttpClient { panic!("called as_fake on {}", type_name::()) } + + fn send_multipart_form<'a>( + &'a self, + _url: &str, + _request: reqwest::multipart::Form, + ) -> BoxFuture<'a, anyhow::Result>> { + future::ready(Err(anyhow!("not implemented"))).boxed() + } } /// An [`HttpClient`] that may have a proxy. @@ -140,31 +151,13 @@ impl HttpClient for HttpClientWithProxy { fn as_fake(&self) -> &FakeHttpClient { self.client.as_fake() } -} -impl HttpClient for Arc { - fn send( - &self, - req: Request, - ) -> BoxFuture<'static, anyhow::Result>> { - self.client.send(req) - } - - fn user_agent(&self) -> Option<&HeaderValue> { - self.client.user_agent() - } - - fn proxy(&self) -> Option<&Url> { - self.proxy.as_ref() - } - - fn type_name(&self) -> &'static str { - self.client.type_name() - } - - #[cfg(feature = "test-support")] - fn as_fake(&self) -> &FakeHttpClient { - self.client.as_fake() + fn send_multipart_form<'a>( + &'a self, + url: &str, + form: reqwest::multipart::Form, + ) -> BoxFuture<'a, anyhow::Result>> { + self.client.send_multipart_form(url, form) } } @@ -275,32 +268,6 @@ impl HttpClientWithUrl { } } -impl HttpClient for Arc { - fn send( - &self, - req: Request, - ) -> BoxFuture<'static, anyhow::Result>> { - self.client.send(req) - } - - fn user_agent(&self) -> Option<&HeaderValue> { - self.client.user_agent() - } - - fn proxy(&self) -> Option<&Url> { - self.client.proxy.as_ref() - } - - fn type_name(&self) -> &'static str { - self.client.type_name() - } - - #[cfg(feature = "test-support")] - fn as_fake(&self) -> &FakeHttpClient { - self.client.as_fake() - } -} - impl HttpClient for HttpClientWithUrl { fn send( &self, @@ -325,6 +292,14 @@ impl HttpClient for HttpClientWithUrl { fn as_fake(&self) -> &FakeHttpClient { self.client.as_fake() } + + fn send_multipart_form<'a>( + &'a self, + url: &str, + request: reqwest::multipart::Form, + ) -> BoxFuture<'a, anyhow::Result>> { + self.client.send_multipart_form(url, request) + } } pub fn read_proxy_from_env() -> Option { diff --git a/crates/proto/proto/app.proto b/crates/proto/proto/app.proto index 5330ee506a..353f19adb2 100644 --- a/crates/proto/proto/app.proto +++ b/crates/proto/proto/app.proto @@ -79,11 +79,16 @@ message OpenServerSettings { uint64 project_id = 1; } -message GetPanicFiles { +message GetCrashFiles { } -message GetPanicFilesResponse { - repeated string file_contents = 2; +message GetCrashFilesResponse { + repeated CrashReport crashes = 1; +} + +message CrashReport { + optional string panic_contents = 1; + optional bytes minidump_contents = 2; } message Extension { diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index d511ea5e8f..9de5c2c0c7 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -294,9 +294,6 @@ message Envelope { GetPathMetadata get_path_metadata = 278; GetPathMetadataResponse get_path_metadata_response = 279; - GetPanicFiles get_panic_files = 280; - GetPanicFilesResponse get_panic_files_response = 281; - CancelLanguageServerWork cancel_language_server_work = 282; LspExtOpenDocs lsp_ext_open_docs = 283; @@ -402,7 +399,10 @@ message Envelope { StashPop stash_pop = 358; GetDefaultBranch get_default_branch = 359; - GetDefaultBranchResponse get_default_branch_response = 360; // current max + GetDefaultBranchResponse get_default_branch_response = 360; + + GetCrashFiles get_crash_files = 361; + GetCrashFilesResponse get_crash_files_response = 362; // current max } reserved 87 to 88; @@ -423,6 +423,7 @@ message Envelope { reserved 270; reserved 247 to 254; reserved 255 to 256; + reserved 280 to 281; } message Hello { diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 72b3807deb..4c447e2eca 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -99,8 +99,8 @@ messages!( (GetHoverResponse, Background), (GetNotifications, Foreground), (GetNotificationsResponse, Foreground), - (GetPanicFiles, Background), - (GetPanicFilesResponse, Background), + (GetCrashFiles, Background), + (GetCrashFilesResponse, Background), (GetPathMetadata, Background), (GetPathMetadataResponse, Background), (GetPermalinkToLine, Foreground), @@ -462,7 +462,7 @@ request_messages!( (ActivateToolchain, Ack), (ActiveToolchain, ActiveToolchainResponse), (GetPathMetadata, GetPathMetadataResponse), - (GetPanicFiles, GetPanicFilesResponse), + (GetCrashFiles, GetCrashFilesResponse), (CancelLanguageServerWork, Ack), (SyncExtensions, SyncExtensionsResponse), (InstallExtension, Ack), diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 443c47919f..c6a546f345 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -67,8 +67,11 @@ watch.workspace = true worktree.workspace = true [target.'cfg(not(windows))'.dependencies] +crashes.workspace = true +crash-handler.workspace = true fork.workspace = true libc.workspace = true +minidumper.workspace = true [dev-dependencies] assistant_tool.workspace = true diff --git a/crates/remote_server/src/main.rs b/crates/remote_server/src/main.rs index 98f635d856..03b0c3eda3 100644 --- a/crates/remote_server/src/main.rs +++ b/crates/remote_server/src/main.rs @@ -12,6 +12,10 @@ struct Cli { /// by having Zed act like netcat communicating over a Unix socket. #[arg(long, hide = true)] askpass: Option, + /// Used for recording minidumps on crashes by having the server run a separate + /// process communicating over a socket. + #[arg(long, hide = true)] + crash_handler: Option, /// Used for loading the environment from the project. #[arg(long, hide = true)] printenv: bool, @@ -58,6 +62,11 @@ fn main() { return; } + if let Some(socket) = &cli.crash_handler { + crashes::crash_server(socket.as_path()); + return; + } + if cli.printenv { util::shell_env::print_env(); return; diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 84ce08ff25..9bb5645dc7 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -17,6 +17,7 @@ use node_runtime::{NodeBinaryOptions, NodeRuntime}; use paths::logs_dir; use project::project_settings::ProjectSettings; +use proto::CrashReport; use release_channel::{AppVersion, RELEASE_CHANNEL, ReleaseChannel}; use remote::proxy::ProxyLaunchError; use remote::ssh_session::ChannelClient; @@ -33,6 +34,7 @@ use smol::io::AsyncReadExt; use smol::Async; use smol::{net::unix::UnixListener, stream::StreamExt as _}; +use std::collections::HashMap; use std::ffi::OsStr; use std::ops::ControlFlow; use std::str::FromStr; @@ -109,8 +111,9 @@ fn init_logging_server(log_file_path: PathBuf) -> Result>> { Ok(rx) } -fn init_panic_hook() { - std::panic::set_hook(Box::new(|info| { +fn init_panic_hook(session_id: String) { + std::panic::set_hook(Box::new(move |info| { + crashes::handle_panic(); let payload = info .payload() .downcast_ref::<&str>() @@ -171,9 +174,11 @@ fn init_panic_hook() { architecture: env::consts::ARCH.into(), panicked_on: Utc::now().timestamp_millis(), backtrace, - system_id: None, // Set on SSH client - installation_id: None, // Set on SSH client - session_id: "".to_string(), // Set on SSH client + system_id: None, // Set on SSH client + installation_id: None, // Set on SSH client + + // used on this end to associate panics with minidumps, but will be replaced on the SSH client + session_id: session_id.clone(), }; if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() { @@ -194,44 +199,69 @@ fn init_panic_hook() { })); } -fn handle_panic_requests(project: &Entity, client: &Arc) { +fn handle_crash_files_requests(project: &Entity, client: &Arc) { let client: AnyProtoClient = client.clone().into(); client.add_request_handler( project.downgrade(), - |_, _: TypedEnvelope, _cx| async move { + |_, _: TypedEnvelope, _cx| async move { + let mut crashes = Vec::new(); + let mut minidumps_by_session_id = HashMap::new(); let mut children = smol::fs::read_dir(paths::logs_dir()).await?; - let mut panic_files = Vec::new(); while let Some(child) = children.next().await { let child = child?; let child_path = child.path(); - if child_path.extension() != Some(OsStr::new("panic")) { - continue; + let extension = child_path.extension(); + if extension == Some(OsStr::new("panic")) { + let filename = if let Some(filename) = child_path.file_name() { + filename.to_string_lossy() + } else { + continue; + }; + + if !filename.starts_with("zed") { + continue; + } + + let file_contents = smol::fs::read_to_string(&child_path) + .await + .context("error reading panic file")?; + + crashes.push(proto::CrashReport { + panic_contents: Some(file_contents), + minidump_contents: None, + }); + } else if extension == Some(OsStr::new("dmp")) { + let session_id = child_path.file_stem().unwrap().to_string_lossy(); + minidumps_by_session_id + .insert(session_id.to_string(), smol::fs::read(&child_path).await?); } - let filename = if let Some(filename) = child_path.file_name() { - filename.to_string_lossy() - } else { - continue; - }; - - if !filename.starts_with("zed") { - continue; - } - - let file_contents = smol::fs::read_to_string(&child_path) - .await - .context("error reading panic file")?; - - panic_files.push(file_contents); // We've done what we can, delete the file - std::fs::remove_file(child_path) + smol::fs::remove_file(&child_path) + .await .context("error removing panic") .log_err(); } - anyhow::Ok(proto::GetPanicFilesResponse { - file_contents: panic_files, - }) + + for crash in &mut crashes { + let panic: telemetry_events::Panic = + serde_json::from_str(crash.panic_contents.as_ref().unwrap())?; + if let dump @ Some(_) = minidumps_by_session_id.remove(&panic.session_id) { + crash.minidump_contents = dump; + } + } + + crashes.extend( + minidumps_by_session_id + .into_values() + .map(|dmp| CrashReport { + panic_contents: None, + minidump_contents: Some(dmp), + }), + ); + + anyhow::Ok(proto::GetCrashFilesResponse { crashes }) }, ); } @@ -409,7 +439,12 @@ pub fn execute_run( ControlFlow::Continue(_) => {} } - init_panic_hook(); + let app = gpui::Application::headless(); + let id = std::process::id().to_string(); + app.background_executor() + .spawn(crashes::init(id.clone())) + .detach(); + init_panic_hook(id); let log_rx = init_logging_server(log_file)?; log::info!( "starting up. pid_file: {:?}, stdin_socket: {:?}, stdout_socket: {:?}, stderr_socket: {:?}", @@ -425,7 +460,7 @@ pub fn execute_run( let listeners = ServerListeners::new(stdin_socket, stdout_socket, stderr_socket)?; let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new()); - gpui::Application::headless().run(move |cx| { + app.run(move |cx| { settings::init(cx); let app_version = AppVersion::load(env!("ZED_PKG_VERSION")); release_channel::init(app_version, cx); @@ -486,7 +521,7 @@ pub fn execute_run( ) }); - handle_panic_requests(&project, &session); + handle_crash_files_requests(&project, &session); cx.background_spawn(async move { cleanup_old_binaries() }) .detach(); @@ -530,12 +565,15 @@ impl ServerPaths { pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> { init_logging_proxy(); - init_panic_hook(); - - log::info!("starting proxy process. PID: {}", std::process::id()); let server_paths = ServerPaths::new(&identifier)?; + let id = std::process::id().to_string(); + smol::spawn(crashes::init(id.clone())).detach(); + init_panic_hook(id); + + log::info!("starting proxy process. PID: {}", std::process::id()); + let server_pid = check_pid_file(&server_paths.pid_file)?; let server_running = server_pid.is_some(); if is_reconnecting { diff --git a/crates/reqwest_client/src/reqwest_client.rs b/crates/reqwest_client/src/reqwest_client.rs index e02768876d..6461a0ae17 100644 --- a/crates/reqwest_client/src/reqwest_client.rs +++ b/crates/reqwest_client/src/reqwest_client.rs @@ -4,14 +4,13 @@ use std::{any::type_name, borrow::Cow, mem, pin::Pin, task::Poll, time::Duration use anyhow::anyhow; use bytes::{BufMut, Bytes, BytesMut}; -use futures::{AsyncRead, TryStreamExt as _}; +use futures::{AsyncRead, FutureExt as _, TryStreamExt as _}; use http_client::{RedirectPolicy, Url, http}; use regex::Regex; use reqwest::{ header::{HeaderMap, HeaderValue}, redirect, }; -use smol::future::FutureExt; const DEFAULT_CAPACITY: usize = 4096; static RUNTIME: OnceLock = OnceLock::new(); @@ -274,6 +273,26 @@ impl http_client::HttpClient for ReqwestClient { } .boxed() } + + fn send_multipart_form<'a>( + &'a self, + url: &str, + form: reqwest::multipart::Form, + ) -> futures::future::BoxFuture<'a, anyhow::Result>> + { + let response = self.client.post(url).multipart(form).send(); + self.handle + .spawn(async move { + let response = response.await?; + let mut builder = http::response::Builder::new().status(response.status()); + for (k, v) in response.headers() { + builder = builder.header(k, v) + } + Ok(builder.body(response.bytes().await?.into())?) + }) + .map(|e| e?) + .boxed() + } } #[cfg(test)] diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index bdd8db9027..5bd6d981fa 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -45,6 +45,7 @@ collections.workspace = true command_palette.workspace = true component.workspace = true copilot.workspace = true +crashes.workspace = true dap_adapters.workspace = true db.workspace = true debug_adapter_extension.workspace = true @@ -117,6 +118,7 @@ recent_projects.workspace = true release_channel.workspace = true remote.workspace = true repl.workspace = true +reqwest.workspace = true reqwest_client.workspace = true rope.workspace = true search.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 71b29909a1..e4a14b5d32 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -172,6 +172,12 @@ pub fn main() { let args = Args::parse(); + // `zed --crash-handler` Makes zed operate in minidump crash handler mode + if let Some(socket) = &args.crash_handler { + crashes::crash_server(socket.as_path()); + return; + } + // `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass if let Some(socket) = &args.askpass { askpass::main(socket); @@ -264,6 +270,9 @@ pub fn main() { let session_id = Uuid::new_v4().to_string(); let session = app.background_executor().block(Session::new()); + app.background_executor() + .spawn(crashes::init(session_id.clone())) + .detach(); reliability::init_panic_hook( app_version, app_commit_sha.clone(), @@ -1185,6 +1194,11 @@ struct Args { #[arg(long, hide = true)] nc: Option, + /// Used for recording minidumps on crashes by having Zed run a separate + /// process communicating over a socket. + #[arg(long, hide = true)] + crash_handler: Option, + /// Run zed in the foreground, only used on Windows, to match the behavior on macOS. #[arg(long)] #[cfg(target_os = "windows")] diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index d7f1473288..9157f66216 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -2,21 +2,32 @@ use crate::stdout_is_a_pty; use anyhow::{Context as _, Result}; use backtrace::{self, Backtrace}; use chrono::Utc; -use client::{TelemetrySettings, telemetry}; +use client::{ + TelemetrySettings, + telemetry::{self, SENTRY_MINIDUMP_ENDPOINT}, +}; use db::kvp::KEY_VALUE_STORE; +use futures::AsyncReadExt; use gpui::{App, AppContext as _, SemanticVersion}; use http_client::{self, HttpClient, HttpClientWithUrl, HttpRequestExt, Method}; use paths::{crashes_dir, crashes_retired_dir}; use project::Project; use release_channel::{AppCommitSha, RELEASE_CHANNEL, ReleaseChannel}; +use reqwest::multipart::{Form, Part}; use settings::Settings; use smol::stream::StreamExt; use std::{ env, ffi::{OsStr, c_void}, - sync::{Arc, atomic::Ordering}, + fs, + io::Write, + panic, + sync::{ + Arc, + atomic::{AtomicU32, Ordering}, + }, + thread, }; -use std::{io::Write, panic, sync::atomic::AtomicU32, thread}; use telemetry_events::{LocationData, Panic, PanicRequest}; use url::Url; use util::ResultExt; @@ -37,9 +48,10 @@ pub fn init_panic_hook( if prior_panic_count > 0 { // Give the panic-ing thread time to write the panic file loop { - std::thread::yield_now(); + thread::yield_now(); } } + crashes::handle_panic(); let thread = thread::current(); let thread_name = thread.name().unwrap_or(""); @@ -136,9 +148,8 @@ pub fn init_panic_hook( if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() { let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string(); let panic_file_path = paths::logs_dir().join(format!("zed-{timestamp}.panic")); - let panic_file = std::fs::OpenOptions::new() - .append(true) - .create(true) + let panic_file = fs::OpenOptions::new() + .create_new(true) .open(&panic_file_path) .log_err(); if let Some(mut panic_file) = panic_file { @@ -205,27 +216,31 @@ pub fn init( if let Some(ssh_client) = project.ssh_client() { ssh_client.update(cx, |client, cx| { if TelemetrySettings::get_global(cx).diagnostics { - let request = client.proto_client().request(proto::GetPanicFiles {}); + let request = client.proto_client().request(proto::GetCrashFiles {}); cx.background_spawn(async move { - let panic_files = request.await?; - for file in panic_files.file_contents { - let panic: Option = serde_json::from_str(&file) - .log_err() - .or_else(|| { - file.lines() - .next() - .and_then(|line| serde_json::from_str(line).ok()) - }) - .unwrap_or_else(|| { - log::error!("failed to deserialize panic file {:?}", file); - None - }); + let crash_files = request.await?; + for crash in crash_files.crashes { + let mut panic: Option = crash + .panic_contents + .and_then(|s| serde_json::from_str(&s).log_err()); - if let Some(mut panic) = panic { + if let Some(panic) = panic.as_mut() { panic.session_id = session_id.clone(); panic.system_id = system_id.clone(); panic.installation_id = installation_id.clone(); + } + if let Some(minidump) = crash.minidump_contents { + upload_minidump( + http_client.clone(), + minidump.clone(), + panic.as_ref(), + ) + .await + .log_err(); + } + + if let Some(panic) = panic { upload_panic(&http_client, &panic_report_url, panic, &mut None) .await?; } @@ -510,6 +525,22 @@ async fn upload_previous_panics( }); if let Some(panic) = panic { + let minidump_path = paths::logs_dir() + .join(&panic.session_id) + .with_extension("dmp"); + if minidump_path.exists() { + let minidump = smol::fs::read(&minidump_path) + .await + .context("Failed to read minidump")?; + if upload_minidump(http.clone(), minidump, Some(&panic)) + .await + .log_err() + .is_some() + { + fs::remove_file(minidump_path).ok(); + } + } + if !upload_panic(&http, &panic_report_url, panic, &mut most_recent_panic).await? { continue; } @@ -517,13 +548,75 @@ async fn upload_previous_panics( } // We've done what we can, delete the file - std::fs::remove_file(child_path) + fs::remove_file(child_path) .context("error removing panic") .log_err(); } + + // loop back over the directory again to upload any minidumps that are missing panics + let mut children = smol::fs::read_dir(paths::logs_dir()).await?; + while let Some(child) = children.next().await { + let child = child?; + let child_path = child.path(); + if child_path.extension() != Some(OsStr::new("dmp")) { + continue; + } + if upload_minidump( + http.clone(), + smol::fs::read(&child_path) + .await + .context("Failed to read minidump")?, + None, + ) + .await + .log_err() + .is_some() + { + fs::remove_file(child_path).ok(); + } + } + Ok(most_recent_panic) } +async fn upload_minidump( + http: Arc, + minidump: Vec, + panic: Option<&Panic>, +) -> Result<()> { + let sentry_upload_url = SENTRY_MINIDUMP_ENDPOINT + .to_owned() + .ok_or_else(|| anyhow::anyhow!("Minidump endpoint not set"))?; + + let mut form = Form::new() + .part( + "upload_file_minidump", + Part::bytes(minidump) + .file_name("minidump.dmp") + .mime_str("application/octet-stream")?, + ) + .text("platform", "rust"); + if let Some(panic) = panic { + form = form.text( + "release", + format!("{}-{}", panic.release_channel, panic.app_version), + ); + // TODO: tack on more fields + } + + let mut response_text = String::new(); + let mut response = http.send_multipart_form(&sentry_upload_url, form).await?; + response + .body_mut() + .read_to_string(&mut response_text) + .await?; + if !response.status().is_success() { + anyhow::bail!("failed to upload minidump: {response_text}"); + } + log::info!("Uploaded minidump. event id: {response_text}"); + Ok(()) +} + async fn upload_panic( http: &Arc, panic_report_url: &Url, diff --git a/docs/src/development/debugging-crashes.md b/docs/src/development/debugging-crashes.md index d08ab961cc..ed0a5807a3 100644 --- a/docs/src/development/debugging-crashes.md +++ b/docs/src/development/debugging-crashes.md @@ -6,6 +6,7 @@ When an app crashes, - macOS creates a `.ips` file in `~/Library/Logs/DiagnosticReports`. You can view these using the built in Console app (`cmd-space Console`) under "Crash Reports". - Linux creates a core dump. See the [man pages](https://man7.org/linux/man-pages/man5/core.5.html) for pointers to how your system might be configured to manage core dumps. +- Windows doesn't create crash reports by default, but can be configured to create "minidump" memory dumps upon applications crashing. If you have enabled Zed's telemetry these will be uploaded to us when you restart the app. They end up in a [Slack channel (internal only)](https://zed-industries.slack.com/archives/C04S6T1T7TQ). diff --git a/docs/src/telemetry.md b/docs/src/telemetry.md index 7f5994be0c..107aef5a96 100644 --- a/docs/src/telemetry.md +++ b/docs/src/telemetry.md @@ -21,7 +21,7 @@ The telemetry settings can also be configured via the welcome screen, which can Telemetry is sent from the application to our servers. Data is proxied through our servers to enable us to easily switch analytics services. We currently use: -- [Axiom](https://axiom.co): Cloud-monitoring service - stores diagnostic events +- [Sentry](https://sentry.io): Crash-monitoring service - stores diagnostic events - [Snowflake](https://snowflake.com): Data warehouse - stores both diagnostic and metric events - [Hex](https://www.hex.tech): Dashboards and data exploration - accesses data stored in Snowflake - [Amplitude](https://www.amplitude.com): Dashboards and data exploration - accesses data stored in Snowflake @@ -30,9 +30,9 @@ Telemetry is sent from the application to our servers. Data is proxied through o ### Diagnostics -Diagnostic events include debug information (stack traces) from crash reports. Reports are sent on the first application launch after the crash occurred. We've built dashboards that allow us to visualize the frequency and severity of issues experienced by users. Having these reports sent automatically allows us to begin implementing fixes without the user needing to file a report in our issue tracker. The plots in the dashboards also give us an informal measurement of the stability of Zed. +Crash reports consist of a [minidump](https://learn.microsoft.com/en-us/windows/win32/debug/minidump-files) and some extra debug information. Reports are sent on the first application launch after the crash occurred. We've built dashboards that allow us to visualize the frequency and severity of issues experienced by users. Having these reports sent automatically allows us to begin implementing fixes without the user needing to file a report in our issue tracker. The plots in the dashboards also give us an informal measurement of the stability of Zed. -You can see what data is sent when a panic occurs by inspecting the `Panic` struct in [crates/telemetry_events/src/telemetry_events.rs](https://github.com/zed-industries/zed/blob/main/crates/telemetry_events/src/telemetry_events.rs) in the Zed repo. You can find additional information in the [Debugging Crashes](./development/debugging-crashes.md) documentation. +You can see what extra data is sent alongside the minidump in the `Panic` struct in [crates/telemetry_events/src/telemetry_events.rs](https://github.com/zed-industries/zed/blob/main/crates/telemetry_events/src/telemetry_events.rs) in the Zed repo. You can find additional information in the [Debugging Crashes](./development/debugging-crashes.md) documentation. ### Client-Side Usage Data {#client-metrics} diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index 4196696f47..5678e46236 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -82,6 +82,7 @@ lyon = { version = "1", default-features = false, features = ["extra"] } lyon_path = { version = "1" } md-5 = { version = "0.10" } memchr = { version = "2" } +mime_guess = { version = "2" } miniz_oxide = { version = "0.8", features = ["simd"] } nom = { version = "7" } num-bigint = { version = "0.4" } @@ -212,6 +213,7 @@ lyon = { version = "1", default-features = false, features = ["extra"] } lyon_path = { version = "1" } md-5 = { version = "0.10" } memchr = { version = "2" } +mime_guess = { version = "2" } miniz_oxide = { version = "0.8", features = ["simd"] } nom = { version = "7" } num-bigint = { version = "0.4" } @@ -290,7 +292,7 @@ gimli = { version = "0.31", default-features = false, features = ["read", "std", hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } naga = { version = "25", features = ["msl-out", "wgsl-in"] } -nix = { version = "0.29", features = ["fs", "pthread", "signal", "user"] } +nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } objc2 = { version = "0.6" } objc2-core-foundation = { version = "0.3", default-features = false, features = ["CFArray", "CFCGTypes", "CFData", "CFDate", "CFDictionary", "CFRunLoop", "CFString", "CFURL", "objc2", "std"] } objc2-foundation = { version = "0.3", default-features = false, features = ["NSArray", "NSAttributedString", "NSBundle", "NSCoder", "NSData", "NSDate", "NSDictionary", "NSEnumerator", "NSError", "NSGeometry", "NSNotification", "NSNull", "NSObjCRuntime", "NSObject", "NSProcessInfo", "NSRange", "NSRunLoop", "NSString", "NSURL", "NSUndoManager", "NSValue", "objc2-core-foundation", "std"] } @@ -318,7 +320,7 @@ gimli = { version = "0.31", default-features = false, features = ["read", "std", hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } naga = { version = "25", features = ["msl-out", "wgsl-in"] } -nix = { version = "0.29", features = ["fs", "pthread", "signal", "user"] } +nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } objc2 = { version = "0.6" } objc2-core-foundation = { version = "0.3", default-features = false, features = ["CFArray", "CFCGTypes", "CFData", "CFDate", "CFDictionary", "CFRunLoop", "CFString", "CFURL", "objc2", "std"] } objc2-foundation = { version = "0.3", default-features = false, features = ["NSArray", "NSAttributedString", "NSBundle", "NSCoder", "NSData", "NSDate", "NSDictionary", "NSEnumerator", "NSError", "NSGeometry", "NSNotification", "NSNull", "NSObjCRuntime", "NSObject", "NSProcessInfo", "NSRange", "NSRunLoop", "NSString", "NSURL", "NSUndoManager", "NSValue", "objc2-core-foundation", "std"] } @@ -347,7 +349,7 @@ gimli = { version = "0.31", default-features = false, features = ["read", "std", hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } naga = { version = "25", features = ["msl-out", "wgsl-in"] } -nix = { version = "0.29", features = ["fs", "pthread", "signal", "user"] } +nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } objc2 = { version = "0.6" } objc2-core-foundation = { version = "0.3", default-features = false, features = ["CFArray", "CFCGTypes", "CFData", "CFDate", "CFDictionary", "CFRunLoop", "CFString", "CFURL", "objc2", "std"] } objc2-foundation = { version = "0.3", default-features = false, features = ["NSArray", "NSAttributedString", "NSBundle", "NSCoder", "NSData", "NSDate", "NSDictionary", "NSEnumerator", "NSError", "NSGeometry", "NSNotification", "NSNull", "NSObjCRuntime", "NSObject", "NSProcessInfo", "NSRange", "NSRunLoop", "NSString", "NSURL", "NSUndoManager", "NSValue", "objc2-core-foundation", "std"] } @@ -375,7 +377,7 @@ gimli = { version = "0.31", default-features = false, features = ["read", "std", hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } naga = { version = "25", features = ["msl-out", "wgsl-in"] } -nix = { version = "0.29", features = ["fs", "pthread", "signal", "user"] } +nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } objc2 = { version = "0.6" } objc2-core-foundation = { version = "0.3", default-features = false, features = ["CFArray", "CFCGTypes", "CFData", "CFDate", "CFDictionary", "CFRunLoop", "CFString", "CFURL", "objc2", "std"] } objc2-foundation = { version = "0.3", default-features = false, features = ["NSArray", "NSAttributedString", "NSBundle", "NSCoder", "NSData", "NSDate", "NSDictionary", "NSEnumerator", "NSError", "NSGeometry", "NSNotification", "NSNull", "NSObjCRuntime", "NSObject", "NSProcessInfo", "NSRange", "NSRunLoop", "NSString", "NSURL", "NSUndoManager", "NSValue", "objc2-core-foundation", "std"] } @@ -414,7 +416,8 @@ linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", d linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } mio = { version = "1", features = ["net", "os-ext"] } naga = { version = "25", features = ["spv-out", "wgsl-in"] } -nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } +nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } +nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] } object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } proc-macro2 = { version = "1", features = ["span-locations"] } @@ -454,7 +457,8 @@ linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", d linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } mio = { version = "1", features = ["net", "os-ext"] } naga = { version = "25", features = ["spv-out", "wgsl-in"] } -nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } +nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } +nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] } object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] } @@ -492,7 +496,8 @@ linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", d linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } mio = { version = "1", features = ["net", "os-ext"] } naga = { version = "25", features = ["spv-out", "wgsl-in"] } -nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } +nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } +nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] } object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } proc-macro2 = { version = "1", features = ["span-locations"] } @@ -532,7 +537,8 @@ linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", d linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } mio = { version = "1", features = ["net", "os-ext"] } naga = { version = "25", features = ["spv-out", "wgsl-in"] } -nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } +nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } +nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] } object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] } @@ -617,7 +623,8 @@ linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", d linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } mio = { version = "1", features = ["net", "os-ext"] } naga = { version = "25", features = ["spv-out", "wgsl-in"] } -nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } +nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } +nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] } object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } proc-macro2 = { version = "1", features = ["span-locations"] } @@ -657,7 +664,8 @@ linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", d linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } mio = { version = "1", features = ["net", "os-ext"] } naga = { version = "25", features = ["spv-out", "wgsl-in"] } -nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } +nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } +nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] } num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] } object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] } From 6c83a3bcdea1212bc74fc6d46cc6fca869137808 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 4 Aug 2025 18:37:10 -0700 Subject: [PATCH 071/693] Add more information to our logs (#35557) Add more logging to collab in order to help diagnose throughput issues. IMPORTANT: Do not deploy this PR without pinging me. Release Notes: - N/A --------- Co-authored-by: Max Brunsfeld --- crates/collab/src/rpc.rs | 82 +++++++++++++++++++++------------------- crates/rpc/src/peer.rs | 18 +++++++++ 2 files changed, 61 insertions(+), 39 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index e648617fe1..8540a671be 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -315,7 +315,7 @@ impl Server { .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) - .add_request_handler(forward_find_search_candidates_request) + .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) @@ -666,7 +666,6 @@ impl Server { let total_duration_ms = received_at.elapsed().as_micros() as f64 / 1000.0; let processing_duration_ms = start_time.elapsed().as_micros() as f64 / 1000.0; let queue_duration_ms = total_duration_ms - processing_duration_ms; - let payload_type = M::NAME; match result { Err(error) => { @@ -675,7 +674,6 @@ impl Server { total_duration_ms, processing_duration_ms, queue_duration_ms, - payload_type, "error handling message" ) } @@ -780,12 +778,11 @@ impl Server { async move { if *teardown.borrow() { tracing::error!("server is tearing down"); - return + return; } - let (connection_id, handle_io, mut incoming_rx) = this - .peer - .add_connection(connection, { + let (connection_id, handle_io, mut incoming_rx) = + this.peer.add_connection(connection, { let executor = executor.clone(); move |duration| executor.sleep(duration) }); @@ -802,10 +799,14 @@ impl Server { } }; - let supermaven_client = this.app_state.config.supermaven_admin_api_key.clone().map(|supermaven_admin_api_key| Arc::new(SupermavenAdminApi::new( - supermaven_admin_api_key.to_string(), - http_client.clone(), - ))); + let supermaven_client = this.app_state.config.supermaven_admin_api_key.clone().map( + |supermaven_admin_api_key| { + Arc::new(SupermavenAdminApi::new( + supermaven_admin_api_key.to_string(), + http_client.clone(), + )) + }, + ); let session = Session { principal: principal.clone(), @@ -820,7 +821,15 @@ impl Server { supermaven_client, }; - if let Err(error) = this.send_initial_client_update(connection_id, zed_version, send_connection_id, &session).await { + if let Err(error) = this + .send_initial_client_update( + connection_id, + zed_version, + send_connection_id, + &session, + ) + .await + { tracing::error!(?error, "failed to send initial client update"); return; } @@ -837,14 +846,22 @@ impl Server { // // This arrangement ensures we will attempt to process earlier messages first, but fall // back to processing messages arrived later in the spirit of making progress. + const MAX_CONCURRENT_HANDLERS: usize = 256; let mut foreground_message_handlers = FuturesUnordered::new(); - let concurrent_handlers = Arc::new(Semaphore::new(256)); + let concurrent_handlers = Arc::new(Semaphore::new(MAX_CONCURRENT_HANDLERS)); + let get_concurrent_handlers = { + let concurrent_handlers = concurrent_handlers.clone(); + move || MAX_CONCURRENT_HANDLERS - concurrent_handlers.available_permits() + }; loop { let next_message = async { let permit = concurrent_handlers.clone().acquire_owned().await.unwrap(); let message = incoming_rx.next().await; - (permit, message) - }.fuse(); + // Cache the concurrent_handlers here, so that we know what the + // queue looks like as each handler starts + (permit, message, get_concurrent_handlers()) + } + .fuse(); futures::pin_mut!(next_message); futures::select_biased! { _ = teardown.changed().fuse() => return, @@ -856,12 +873,16 @@ impl Server { } _ = foreground_message_handlers.next() => {} next_message = next_message => { - let (permit, message) = next_message; + let (permit, message, concurrent_handlers) = next_message; if let Some(message) = message { let type_name = message.payload_type_name(); // note: we copy all the fields from the parent span so we can query them in the logs. // (https://github.com/tokio-rs/tracing/issues/2670). - let span = tracing::info_span!("receive message", %connection_id, %address, type_name, + let span = tracing::info_span!("receive message", + %connection_id, + %address, + type_name, + concurrent_handlers, user_id=field::Empty, login=field::Empty, impersonator=field::Empty, @@ -895,12 +916,13 @@ impl Server { } drop(foreground_message_handlers); - tracing::info!("signing out"); + let concurrent_handlers = get_concurrent_handlers(); + tracing::info!(concurrent_handlers, "signing out"); if let Err(error) = connection_lost(session, teardown, executor).await { tracing::error!(?error, "error signing out"); } - - }.instrument(span) + } + .instrument(span) } async fn send_initial_client_update( @@ -2286,25 +2308,6 @@ where Ok(()) } -async fn forward_find_search_candidates_request( - request: proto::FindSearchCandidates, - response: Response, - session: Session, -) -> Result<()> { - let project_id = ProjectId::from_proto(request.remote_entity_id()); - let host_connection_id = session - .db() - .await - .host_for_read_only_project_request(project_id, session.connection_id) - .await?; - let payload = session - .peer - .forward_request(session.connection_id, host_connection_id, request) - .await?; - response.send(payload)?; - Ok(()) -} - /// forward a project request to the host. These requests are disallowed /// for guests. async fn forward_mutating_project_request( @@ -2336,6 +2339,7 @@ async fn multi_lsp_query( session: Session, ) -> Result<()> { tracing::Span::current().record("multi_lsp_query_request", request.request_str()); + tracing::info!("multi_lsp_query message received"); forward_mutating_project_request(request, response, session).await } diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 80a104641f..c1fd1df5ff 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -422,8 +422,26 @@ impl Peer { receiver_id: ConnectionId, request: T, ) -> impl Future> { + let request_start_time = Instant::now(); + let payload_type = T::NAME; + let elapsed_time = move || request_start_time.elapsed().as_millis(); + tracing::info!(payload_type, "start forwarding request"); self.request_internal(Some(sender_id), receiver_id, request) .map_ok(|envelope| envelope.payload) + .inspect_err(move |_| { + tracing::error!( + waiting_for_host_ms = elapsed_time(), + payload_type, + "error forwarding request" + ) + }) + .inspect_ok(move |_| { + tracing::info!( + waiting_for_host_ms = elapsed_time(), + payload_type, + "finished forwarding request" + ) + }) } fn request_internal( From efba4364fd28757122489f2564fa6d6b71e717ba Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Aug 2025 11:33:33 +0200 Subject: [PATCH 072/693] Ensure client reconnects if an error occurs during authentication (#35629) In #35471, we added a new `AuthenticationError` variant to the client enum `Status`, but the reconnection logic was ignoring it when determining whether to reconnect. This pull request fixes that regression and introduces test coverage for this case. Release Notes: - N/A --- crates/client/src/client.rs | 81 +++++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 17 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 309e4d892f..b4894cddcf 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -687,7 +687,10 @@ impl Client { } } - if matches!(*client.status().borrow(), Status::ConnectionError) { + if matches!( + *client.status().borrow(), + Status::AuthenticationError | Status::ConnectionError + ) { client.set_status( Status::ReconnectionError { next_reconnection: Instant::now() + delay, @@ -856,28 +859,14 @@ impl Client { let old_credentials = self.state.read().credentials.clone(); if let Some(old_credentials) = old_credentials { - if self - .cloud_client - .validate_credentials( - old_credentials.user_id as u32, - &old_credentials.access_token, - ) - .await? - { + if self.validate_credentials(&old_credentials, cx).await? { credentials = Some(old_credentials); } } if credentials.is_none() && try_provider { if let Some(stored_credentials) = self.credentials_provider.read_credentials(cx).await { - if self - .cloud_client - .validate_credentials( - stored_credentials.user_id as u32, - &stored_credentials.access_token, - ) - .await? - { + if self.validate_credentials(&stored_credentials, cx).await? { credentials = Some(stored_credentials); } else { self.credentials_provider @@ -926,6 +915,24 @@ impl Client { Ok(credentials) } + async fn validate_credentials( + self: &Arc, + credentials: &Credentials, + cx: &AsyncApp, + ) -> Result { + match self + .cloud_client + .validate_credentials(credentials.user_id as u32, &credentials.access_token) + .await + { + Ok(valid) => Ok(valid), + Err(err) => { + self.set_status(Status::AuthenticationError, cx); + Err(anyhow!("failed to validate credentials: {}", err)) + } + } + } + /// Performs a sign-in and also connects to Collab. /// /// This is called in places where we *don't* need to connect in the future. We will replace these calls with calls @@ -1733,6 +1740,46 @@ mod tests { assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token } + #[gpui::test(iterations = 10)] + async fn test_auth_failure_during_reconnection(cx: &mut TestAppContext) { + init_test(cx); + let http_client = FakeHttpClient::with_200_response(); + let client = + cx.update(|cx| Client::new(Arc::new(FakeSystemClock::new()), http_client.clone(), cx)); + let server = FakeServer::for_client(42, &client, cx).await; + let mut status = client.status(); + assert!(matches!( + status.next().await, + Some(Status::Connected { .. }) + )); + assert_eq!(server.auth_count(), 1); + + // Simulate an auth failure during reconnection. + http_client + .as_fake() + .replace_handler(|_, _request| async move { + Ok(http_client::Response::builder() + .status(503) + .body("".into()) + .unwrap()) + }); + server.disconnect(); + while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {} + + // Restore the ability to authenticate. + http_client + .as_fake() + .replace_handler(|_, _request| async move { + Ok(http_client::Response::builder() + .status(200) + .body("".into()) + .unwrap()) + }); + cx.executor().advance_clock(Duration::from_secs(10)); + while !matches!(status.next().await, Some(Status::Connected { .. })) {} + assert_eq!(server.auth_count(), 1); // Client reused the cached credentials when reconnecting + } + #[gpui::test(iterations = 10)] async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) { init_test(cx); From 919b888387108a8adbd0e94a24d2a5e47989d34b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Aug 2025 14:56:49 +0200 Subject: [PATCH 073/693] ruff: Bump to 0.1.1 (#35635) We want Ruff to be built with newer Rust version (as it was built pre-1.84 where we've fixed a bug in std). Closes #35627 Release Notes: - N/A --- Cargo.lock | 2 +- extensions/ruff/Cargo.toml | 2 +- extensions/ruff/extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eae04776d1..a8e3e81434 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20585,7 +20585,7 @@ dependencies = [ [[package]] name = "zed_ruff" -version = "0.1.0" +version = "0.1.1" dependencies = [ "zed_extension_api 0.1.0", ] diff --git a/extensions/ruff/Cargo.toml b/extensions/ruff/Cargo.toml index 830897279a..24616f963b 100644 --- a/extensions/ruff/Cargo.toml +++ b/extensions/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_ruff" -version = "0.1.0" +version = "0.1.1" edition.workspace = true publish.workspace = true license = "Apache-2.0" diff --git a/extensions/ruff/extension.toml b/extensions/ruff/extension.toml index 63929fc191..1f5a7314f4 100644 --- a/extensions/ruff/extension.toml +++ b/extensions/ruff/extension.toml @@ -1,7 +1,7 @@ id = "ruff" name = "Ruff" description = "Support for Ruff, the Python linter and formatter" -version = "0.1.0" +version = "0.1.1" schema_version = 1 authors = [] repository = "https://github.com/zed-industries/zed" From 497252480ca40508d2b4778ec25001ad810ed68b Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:05:05 -0300 Subject: [PATCH 074/693] agent: Update link to OpenAI compatible docs (#35620) Release Notes: - N/A --- crates/language_models/src/provider/open_ai.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 79ef4a0ee0..5185e979b7 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -868,7 +868,7 @@ impl Render for ConfigurationView { .icon_size(IconSize::XSmall) .icon_color(Color::Muted) .on_click(move |_, _window, cx| { - cx.open_url("https://zed.dev/docs/ai/configuration#openai-api-compatible") + cx.open_url("https://zed.dev/docs/ai/llm-providers#openai-api-compatible") }), ); From 5b40b3618fbca8dc06bcfa101c7d42225c169151 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 5 Aug 2025 09:35:52 -0400 Subject: [PATCH 075/693] Add `workspace::ToggleEditPrediction` for toggling inline completions globally (#35418) Closes: https://github.com/zed-industries/zed/issues/23704 Existing action is `editor::ToggleEditPrediction` ("This Buffer"). This action is `workspace::ToggleEditPredction` ("All Files"). You can add a custom keybind wi shortcut with: ```json { "context": "Workspace", "bindings": { "ctrl-alt-cmd-e": "workspace::ToggleEditPrediction" } }, ``` Screenshot 2025-07-31 at 12 52 19 Release Notes: - Added `workspace::ToggleEditPrediction` action for toggling `show_edit_predictions` in settings (Edit Predictions menu -> All Files). --- .../src/edit_prediction_button.rs | 32 ++++++++----------- crates/workspace/src/workspace.rs | 23 +++++++++++-- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/crates/edit_prediction_button/src/edit_prediction_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs index 33165bccf8..9ab94a4095 100644 --- a/crates/edit_prediction_button/src/edit_prediction_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -2,11 +2,7 @@ use anyhow::Result; use client::{UserStore, zed_urls}; use cloud_llm_client::UsageLimit; use copilot::{Copilot, Status}; -use editor::{ - Editor, SelectionEffects, - actions::{ShowEditPrediction, ToggleEditPrediction}, - scroll::Autoscroll, -}; +use editor::{Editor, SelectionEffects, actions::ShowEditPrediction, scroll::Autoscroll}; use feature_flags::{FeatureFlagAppExt, PredictEditsRateCompletionsFeatureFlag}; use fs::Fs; use gpui::{ @@ -441,9 +437,13 @@ impl EditPredictionButton { if let Some(editor_focus_handle) = self.editor_focus_handle.clone() { let entry = ContextMenuEntry::new("This Buffer") .toggleable(IconPosition::Start, self.editor_show_predictions) - .action(Box::new(ToggleEditPrediction)) + .action(Box::new(editor::actions::ToggleEditPrediction)) .handler(move |window, cx| { - editor_focus_handle.dispatch_action(&ToggleEditPrediction, window, cx); + editor_focus_handle.dispatch_action( + &editor::actions::ToggleEditPrediction, + window, + cx, + ); }); match language_state.clone() { @@ -478,10 +478,13 @@ impl EditPredictionButton { let settings = AllLanguageSettings::get_global(cx); let globally_enabled = settings.show_edit_predictions(None, cx); - menu = menu.toggleable_entry("All Files", globally_enabled, IconPosition::Start, None, { - let fs = fs.clone(); - move |_, cx| toggle_edit_predictions_globally(fs.clone(), cx) - }); + let entry = ContextMenuEntry::new("All Files") + .toggleable(IconPosition::Start, globally_enabled) + .action(workspace::ToggleEditPrediction.boxed_clone()) + .handler(|window, cx| { + window.dispatch_action(workspace::ToggleEditPrediction.boxed_clone(), cx) + }); + menu = menu.item(entry); let provider = settings.edit_predictions.provider; let current_mode = settings.edit_predictions_mode(); @@ -943,13 +946,6 @@ async fn open_disabled_globs_setting_in_editor( anyhow::Ok(()) } -fn toggle_edit_predictions_globally(fs: Arc, cx: &mut App) { - let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx); - update_settings_file::(fs, cx, move |file, _| { - file.defaults.show_edit_predictions = Some(!show_edit_predictions) - }); -} - fn set_completion_provider(fs: Arc, cx: &mut App, provider: EditPredictionProvider) { update_settings_file::(fs, cx, move |file, _| { file.features diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6f7db668dd..63953ff802 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -48,7 +48,10 @@ pub use item::{ ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle, }; use itertools::Itertools; -use language::{Buffer, LanguageRegistry, Rope}; +use language::{ + Buffer, LanguageRegistry, Rope, + language_settings::{AllLanguageSettings, all_language_settings}, +}; pub use modal_layer::*; use node_runtime::NodeRuntime; use notifications::{ @@ -74,7 +77,7 @@ use remote::{SshClientDelegate, SshConnectionOptions, ssh_session::ConnectionIde use schemars::JsonSchema; use serde::Deserialize; use session::AppSession; -use settings::Settings; +use settings::{Settings, update_settings_file}; use shared_screen::SharedScreen; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, @@ -233,6 +236,8 @@ actions!( ToggleBottomDock, /// Toggles centered layout mode. ToggleCenteredLayout, + /// Toggles edit prediction feature globally for all files. + ToggleEditPrediction, /// Toggles the left dock. ToggleLeftDock, /// Toggles the right dock. @@ -5546,6 +5551,7 @@ impl Workspace { .on_action(cx.listener(Self::activate_pane_at_index)) .on_action(cx.listener(Self::move_item_to_pane_at_index)) .on_action(cx.listener(Self::move_focused_panel_to_next_position)) + .on_action(cx.listener(Self::toggle_edit_predictions_all_files)) .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| { let pane = workspace.active_pane().clone(); workspace.unfollow_in_pane(&pane, window, cx); @@ -5977,6 +5983,19 @@ impl Workspace { } }); } + + fn toggle_edit_predictions_all_files( + &mut self, + _: &ToggleEditPrediction, + _window: &mut Window, + cx: &mut Context, + ) { + let fs = self.project().read(cx).fs().clone(); + let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx); + update_settings_file::(fs, cx, move |file, _| { + file.defaults.show_edit_predictions = Some(!show_edit_predictions) + }); + } } fn leader_border_for_pane( From 22473fc6119d41a63714cde9d0139c7a943c7dd8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 5 Aug 2025 16:36:05 +0300 Subject: [PATCH 076/693] Stop sending redundant LSP proto requests (#35581) Before, each time any LSP feature was used on client remote, it always produced a `proto::` request that always had been sent to the host, from where returned as an empty response. Instead, propagate more language server-related data to the client, `lsp::ServerCapability`, so Zed client can omit certain requests if those are not supported. On top of that, rework the approach Zed uses to query for the data refreshes: before, editors tried to fetch the data when the server start was reported (locally and remotely). Now, a later event is selected: on each `textDocument/didOpen` for the buffer contained in this editor, we will query for new LSP data, reusing the cache if needed. Before, servers could reject unregistered files' LSP queries, or process them slowly when starting up. Now, such refreshes are happening later and should be cached. This requires a collab DB change, to restore server data on rejoin. Release Notes: - Fixed excessive LSP requests sent during remote sessions --- .../20221109000000_test_schema.sql | 1 + ...804080620_language_server_capabilities.sql | 5 + crates/collab/src/db.rs | 19 +- crates/collab/src/db/queries/buffers.rs | 26 + crates/collab/src/db/queries/projects.rs | 12 +- crates/collab/src/db/queries/rooms.rs | 11 +- .../collab/src/db/tables/language_server.rs | 1 + crates/collab/src/rpc.rs | 27 +- crates/collab/src/tests/editor_tests.rs | 192 ++++--- crates/collab/src/tests/integration_tests.rs | 160 +++++- crates/editor/src/editor.rs | 10 +- crates/editor/src/linked_editing_ranges.rs | 2 +- crates/language/src/language_registry.rs | 24 - crates/project/src/lsp_command.rs | 45 +- crates/project/src/lsp_store.rs | 507 ++++++++++++------ crates/project/src/project.rs | 80 +-- crates/project/src/project_tests.rs | 13 +- crates/proto/proto/call.proto | 2 + crates/proto/proto/lsp.proto | 7 + 19 files changed, 793 insertions(+), 351 deletions(-) create mode 100644 crates/collab/migrations/20250804080620_language_server_capabilities.sql diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index ca840493ad..73d473ab76 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -173,6 +173,7 @@ CREATE TABLE "language_servers" ( "id" INTEGER NOT NULL, "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, "name" VARCHAR NOT NULL, + "capabilities" TEXT NOT NULL, PRIMARY KEY (project_id, id) ); diff --git a/crates/collab/migrations/20250804080620_language_server_capabilities.sql b/crates/collab/migrations/20250804080620_language_server_capabilities.sql new file mode 100644 index 0000000000..f74f094ed2 --- /dev/null +++ b/crates/collab/migrations/20250804080620_language_server_capabilities.sql @@ -0,0 +1,5 @@ +ALTER TABLE language_servers + ADD COLUMN capabilities TEXT NOT NULL DEFAULT '{}'; + +ALTER TABLE language_servers + ALTER COLUMN capabilities DROP DEFAULT; diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 8cd1e3ea83..2c22ca2069 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -529,11 +529,17 @@ pub struct RejoinedProject { pub worktrees: Vec, pub updated_repositories: Vec, pub removed_repositories: Vec, - pub language_servers: Vec, + pub language_servers: Vec, } impl RejoinedProject { pub fn to_proto(&self) -> proto::RejoinedProject { + let (language_servers, language_server_capabilities) = self + .language_servers + .clone() + .into_iter() + .map(|server| (server.server, server.capabilities)) + .unzip(); proto::RejoinedProject { id: self.id.to_proto(), worktrees: self @@ -551,7 +557,8 @@ impl RejoinedProject { .iter() .map(|collaborator| collaborator.to_proto()) .collect(), - language_servers: self.language_servers.clone(), + language_servers, + language_server_capabilities, } } } @@ -598,7 +605,7 @@ pub struct Project { pub collaborators: Vec, pub worktrees: BTreeMap, pub repositories: Vec, - pub language_servers: Vec, + pub language_servers: Vec, } pub struct ProjectCollaborator { @@ -623,6 +630,12 @@ impl ProjectCollaborator { } } +#[derive(Debug, Clone)] +pub struct LanguageServer { + pub server: proto::LanguageServer, + pub capabilities: String, +} + #[derive(Debug)] pub struct LeftProject { pub id: ProjectId, diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index a288a4e7eb..2e6b4719d1 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -786,6 +786,32 @@ impl Database { }) .collect()) } + + /// Update language server capabilities for a given id. + pub async fn update_server_capabilities( + &self, + project_id: ProjectId, + server_id: u64, + new_capabilities: String, + ) -> Result<()> { + self.transaction(|tx| { + let new_capabilities = new_capabilities.clone(); + async move { + Ok( + language_server::Entity::update(language_server::ActiveModel { + project_id: ActiveValue::unchanged(project_id), + id: ActiveValue::unchanged(server_id as i64), + capabilities: ActiveValue::set(new_capabilities), + ..Default::default() + }) + .exec(&*tx) + .await?, + ) + } + }) + .await?; + Ok(()) + } } fn operation_to_storage( diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index ba22a7b4e3..31635575a8 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -692,6 +692,7 @@ impl Database { project_id: ActiveValue::set(project_id), id: ActiveValue::set(server.id as i64), name: ActiveValue::set(server.name.clone()), + capabilities: ActiveValue::set(update.capabilities.clone()), }) .on_conflict( OnConflict::columns([ @@ -1054,10 +1055,13 @@ impl Database { repositories, language_servers: language_servers .into_iter() - .map(|language_server| proto::LanguageServer { - id: language_server.id as u64, - name: language_server.name, - worktree_id: None, + .map(|language_server| LanguageServer { + server: proto::LanguageServer { + id: language_server.id as u64, + name: language_server.name, + worktree_id: None, + }, + capabilities: language_server.capabilities, }) .collect(), }; diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index cb805786dd..c63d7133be 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -804,10 +804,13 @@ impl Database { .all(tx) .await? .into_iter() - .map(|language_server| proto::LanguageServer { - id: language_server.id as u64, - name: language_server.name, - worktree_id: None, + .map(|language_server| LanguageServer { + server: proto::LanguageServer { + id: language_server.id as u64, + name: language_server.name, + worktree_id: None, + }, + capabilities: language_server.capabilities, }) .collect::>(); diff --git a/crates/collab/src/db/tables/language_server.rs b/crates/collab/src/db/tables/language_server.rs index 9ff8c75fc6..34c7514d91 100644 --- a/crates/collab/src/db/tables/language_server.rs +++ b/crates/collab/src/db/tables/language_server.rs @@ -9,6 +9,7 @@ pub struct Model { #[sea_orm(primary_key)] pub id: i64, pub name: String, + pub capabilities: String, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 8540a671be..22b21f2c7a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1990,12 +1990,19 @@ async fn join_project( } // First, we send the metadata associated with each worktree. + let (language_servers, language_server_capabilities) = project + .language_servers + .clone() + .into_iter() + .map(|server| (server.server, server.capabilities)) + .unzip(); response.send(proto::JoinProjectResponse { project_id: project.id.0 as u64, worktrees: worktrees.clone(), replica_id: replica_id.0 as u32, collaborators: collaborators.clone(), - language_servers: project.language_servers.clone(), + language_servers, + language_server_capabilities, role: project.role.into(), })?; @@ -2054,8 +2061,8 @@ async fn join_project( session.connection_id, proto::UpdateLanguageServer { project_id: project_id.to_proto(), - server_name: Some(language_server.name.clone()), - language_server_id: language_server.id, + server_name: Some(language_server.server.name.clone()), + language_server_id: language_server.server.id, variant: Some( proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( proto::LspDiskBasedDiagnosticsUpdated {}, @@ -2267,9 +2274,17 @@ async fn update_language_server( session: Session, ) -> Result<()> { let project_id = ProjectId::from_proto(request.project_id); - let project_connection_ids = session - .db() - .await + let db = session.db().await; + + if let Some(proto::update_language_server::Variant::MetadataUpdated(update)) = &request.variant + { + if let Some(capabilities) = update.capabilities.clone() { + db.update_server_capabilities(project_id, request.language_server_id, capabilities) + .await?; + } + } + + let project_connection_ids = db .project_connection_ids(project_id, session.connection_id, true) .await?; broadcast( diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 73ab2b8167..1d28c7f6ef 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -296,19 +296,28 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu .await; let active_call_a = cx_a.read(ActiveCall::global); + let capabilities = lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + resolve_provider: Some(true), + ..lsp::CompletionOptions::default() + }), + ..lsp::ServerCapabilities::default() + }; client_a.language_registry().add(rust_lang()); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string()]), - resolve_provider: Some(true), - ..Default::default() - }), - ..Default::default() - }, - ..Default::default() + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, + ..FakeLspAdapter::default() }, ); @@ -566,11 +575,14 @@ async fn test_collaborating_with_code_actions( cx_b.update(editor::init); - // Set up a fake language server. client_a.language_registry().add(rust_lang()); let mut fake_language_servers = client_a .language_registry() .register_fake_lsp("Rust", FakeLspAdapter::default()); + client_b.language_registry().add(rust_lang()); + client_b + .language_registry() + .register_fake_lsp("Rust", FakeLspAdapter::default()); client_a .fs() @@ -775,19 +787,27 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T cx_b.update(editor::init); - // Set up a fake language server. + let capabilities = lsp::ServerCapabilities { + rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { + prepare_provider: Some(true), + work_done_progress_options: Default::default(), + })), + ..lsp::ServerCapabilities::default() + }; client_a.language_registry().add(rust_lang()); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { - prepare_provider: Some(true), - work_done_progress_options: Default::default(), - })), - ..Default::default() - }, - ..Default::default() + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, + ..FakeLspAdapter::default() }, ); @@ -818,6 +838,8 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T .downcast::() .unwrap(); let fake_language_server = fake_language_servers.next().await.unwrap(); + cx_a.run_until_parked(); + cx_b.run_until_parked(); // Move cursor to a location that can be renamed. let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| { @@ -1055,7 +1077,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes project_a.read_with(cx_a, |project, cx| { let status = project.language_server_statuses(cx).next().unwrap().1; - assert_eq!(status.name, "the-language-server"); + assert_eq!(status.name.0, "the-language-server"); assert_eq!(status.pending_work.len(), 1); assert_eq!( status.pending_work["the-token"].message.as_ref().unwrap(), @@ -1072,7 +1094,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes project_b.read_with(cx_b, |project, cx| { let status = project.language_server_statuses(cx).next().unwrap().1; - assert_eq!(status.name, "the-language-server"); + assert_eq!(status.name.0, "the-language-server"); }); executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT); @@ -1089,7 +1111,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes project_a.read_with(cx_a, |project, cx| { let status = project.language_server_statuses(cx).next().unwrap().1; - assert_eq!(status.name, "the-language-server"); + assert_eq!(status.name.0, "the-language-server"); assert_eq!(status.pending_work.len(), 1); assert_eq!( status.pending_work["the-token"].message.as_ref().unwrap(), @@ -1099,7 +1121,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes project_b.read_with(cx_b, |project, cx| { let status = project.language_server_statuses(cx).next().unwrap().1; - assert_eq!(status.name, "the-language-server"); + assert_eq!(status.name.0, "the-language-server"); assert_eq!(status.pending_work.len(), 1); assert_eq!( status.pending_work["the-token"].message.as_ref().unwrap(), @@ -1422,18 +1444,27 @@ async fn test_on_input_format_from_guest_to_host( .await; let active_call_a = cx_a.read(ActiveCall::global); + let capabilities = lsp::ServerCapabilities { + document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { + first_trigger_character: ":".to_string(), + more_trigger_character: Some(vec![">".to_string()]), + }), + ..lsp::ServerCapabilities::default() + }; client_a.language_registry().add(rust_lang()); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { - first_trigger_character: ":".to_string(), - more_trigger_character: Some(vec![">".to_string()]), - }), - ..Default::default() - }, - ..Default::default() + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, + ..FakeLspAdapter::default() }, ); @@ -1588,16 +1619,24 @@ async fn test_mutual_editor_inlay_hint_cache_update( }); }); + let capabilities = lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }; client_a.language_registry().add(rust_lang()); - client_b.language_registry().add(rust_lang()); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, + ..FakeLspAdapter::default() }, ); @@ -1830,16 +1869,24 @@ async fn test_inlay_hint_refresh_is_forwarded( }); }); + let capabilities = lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }; client_a.language_registry().add(rust_lang()); - client_b.language_registry().add(rust_lang()); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, + ..FakeLspAdapter::default() }, ); @@ -2004,15 +2051,23 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo }); }); + let capabilities = lsp::ServerCapabilities { + color_provider: Some(lsp::ColorProviderCapability::Simple(true)), + ..lsp::ServerCapabilities::default() + }; client_a.language_registry().add(rust_lang()); - client_b.language_registry().add(rust_lang()); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - color_provider: Some(lsp::ColorProviderCapability::Simple(true)), - ..lsp::ServerCapabilities::default() - }, + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, ..FakeLspAdapter::default() }, ); @@ -2063,6 +2118,8 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo .unwrap(); let fake_language_server = fake_language_servers.next().await.unwrap(); + cx_a.run_until_parked(); + cx_b.run_until_parked(); let requests_made = Arc::new(AtomicUsize::new(0)); let closure_requests_made = Arc::clone(&requests_made); @@ -2264,24 +2321,32 @@ async fn test_lsp_pull_diagnostics( cx_a.update(editor::init); cx_b.update(editor::init); + let capabilities = lsp::ServerCapabilities { + diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options( + lsp::DiagnosticOptions { + identifier: Some("test-pulls".to_string()), + inter_file_dependencies: true, + workspace_diagnostics: true, + work_done_progress_options: lsp::WorkDoneProgressOptions { + work_done_progress: None, + }, + }, + )), + ..lsp::ServerCapabilities::default() + }; client_a.language_registry().add(rust_lang()); - client_b.language_registry().add(rust_lang()); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options( - lsp::DiagnosticOptions { - identifier: Some("test-pulls".to_string()), - inter_file_dependencies: true, - workspace_diagnostics: true, - work_done_progress_options: lsp::WorkDoneProgressOptions { - work_done_progress: None, - }, - }, - )), - ..lsp::ServerCapabilities::default() - }, + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, ..FakeLspAdapter::default() }, ); @@ -2334,6 +2399,8 @@ async fn test_lsp_pull_diagnostics( .unwrap(); let fake_language_server = fake_language_servers.next().await.unwrap(); + cx_a.run_until_parked(); + cx_b.run_until_parked(); let expected_push_diagnostic_main_message = "pushed main diagnostic"; let expected_push_diagnostic_lib_message = "pushed lib diagnostic"; let expected_pull_diagnostic_main_message = "pulled main diagnostic"; @@ -2689,6 +2756,7 @@ async fn test_lsp_pull_diagnostics( .unwrap() .downcast::() .unwrap(); + cx_b.run_until_parked(); pull_diagnostics_handle.next().await.unwrap(); assert_eq!( diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index aea359d75b..5a2c40b890 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -4778,10 +4778,27 @@ async fn test_definition( .await; let active_call_a = cx_a.read(ActiveCall::global); - let mut fake_language_servers = client_a - .language_registry() - .register_fake_lsp("Rust", Default::default()); + let capabilities = lsp::ServerCapabilities { + definition_provider: Some(OneOf::Left(true)), + type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)), + ..lsp::ServerCapabilities::default() + }; client_a.language_registry().add(rust_lang()); + let mut fake_language_servers = client_a.language_registry().register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, + ..FakeLspAdapter::default() + }, + ); client_a .fs() @@ -4827,13 +4844,19 @@ async fn test_definition( ))) }, ); + cx_a.run_until_parked(); + cx_b.run_until_parked(); let definitions_1 = project_b .update(cx_b, |p, cx| p.definitions(&buffer_b, 23, cx)) .await .unwrap(); cx_b.read(|cx| { - assert_eq!(definitions_1.len(), 1); + assert_eq!( + definitions_1.len(), + 1, + "Unexpected definitions: {definitions_1:?}" + ); assert_eq!(project_b.read(cx).worktrees(cx).count(), 2); let target_buffer = definitions_1[0].target.buffer.read(cx); assert_eq!( @@ -4901,7 +4924,11 @@ async fn test_definition( .await .unwrap(); cx_b.read(|cx| { - assert_eq!(type_definitions.len(), 1); + assert_eq!( + type_definitions.len(), + 1, + "Unexpected type definitions: {type_definitions:?}" + ); let target_buffer = type_definitions[0].target.buffer.read(cx); assert_eq!(target_buffer.text(), "type T2 = usize;"); assert_eq!( @@ -4925,16 +4952,26 @@ async fn test_references( .await; let active_call_a = cx_a.read(ActiveCall::global); + let capabilities = lsp::ServerCapabilities { + references_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }; client_a.language_registry().add(rust_lang()); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { name: "my-fake-lsp-adapter", - capabilities: lsp::ServerCapabilities { - references_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + name: "my-fake-lsp-adapter", + capabilities: capabilities, + ..FakeLspAdapter::default() }, ); @@ -4989,6 +5026,8 @@ async fn test_references( } } }); + cx_a.run_until_parked(); + cx_b.run_until_parked(); let references = project_b.update(cx_b, |p, cx| p.references(&buffer_b, 7, cx)); @@ -4996,7 +5035,7 @@ async fn test_references( executor.run_until_parked(); project_b.read_with(cx_b, |project, cx| { let status = project.language_server_statuses(cx).next().unwrap().1; - assert_eq!(status.name, "my-fake-lsp-adapter"); + assert_eq!(status.name.0, "my-fake-lsp-adapter"); assert_eq!( status.pending_work.values().next().unwrap().message, Some("Finding references...".into()) @@ -5054,7 +5093,7 @@ async fn test_references( executor.run_until_parked(); project_b.read_with(cx_b, |project, cx| { let status = project.language_server_statuses(cx).next().unwrap().1; - assert_eq!(status.name, "my-fake-lsp-adapter"); + assert_eq!(status.name.0, "my-fake-lsp-adapter"); assert_eq!( status.pending_work.values().next().unwrap().message, Some("Finding references...".into()) @@ -5204,10 +5243,26 @@ async fn test_document_highlights( ) .await; - let mut fake_language_servers = client_a - .language_registry() - .register_fake_lsp("Rust", Default::default()); client_a.language_registry().add(rust_lang()); + let capabilities = lsp::ServerCapabilities { + document_highlight_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }; + let mut fake_language_servers = client_a.language_registry().register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, + ..FakeLspAdapter::default() + }, + ); let (project_a, worktree_id) = client_a.build_local_project(path!("/root-1"), cx_a).await; let project_id = active_call_a @@ -5256,6 +5311,8 @@ async fn test_document_highlights( ])) }, ); + cx_a.run_until_parked(); + cx_b.run_until_parked(); let highlights = project_b .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx)) @@ -5306,30 +5363,49 @@ async fn test_lsp_hover( client_a.language_registry().add(rust_lang()); let language_server_names = ["rust-analyzer", "CrabLang-ls"]; + let capabilities_1 = lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..lsp::ServerCapabilities::default() + }; + let capabilities_2 = lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..lsp::ServerCapabilities::default() + }; let mut language_servers = [ client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { - name: "rust-analyzer", - capabilities: lsp::ServerCapabilities { - hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), - ..lsp::ServerCapabilities::default() - }, + name: language_server_names[0], + capabilities: capabilities_1.clone(), ..FakeLspAdapter::default() }, ), client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { - name: "CrabLang-ls", - capabilities: lsp::ServerCapabilities { - hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), - ..lsp::ServerCapabilities::default() - }, + name: language_server_names[1], + capabilities: capabilities_2.clone(), ..FakeLspAdapter::default() }, ), ]; + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + name: language_server_names[0], + capabilities: capabilities_1, + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + name: language_server_names[1], + capabilities: capabilities_2, + ..FakeLspAdapter::default() + }, + ); let (project_a, worktree_id) = client_a.build_local_project(path!("/root-1"), cx_a).await; let project_id = active_call_a @@ -5423,6 +5499,8 @@ async fn test_lsp_hover( unexpected => panic!("Unexpected server name: {unexpected}"), } } + cx_a.run_until_parked(); + cx_b.run_until_parked(); // Request hover information as the guest. let mut hovers = project_b @@ -5605,10 +5683,26 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( .await; let active_call_a = cx_a.read(ActiveCall::global); + let capabilities = lsp::ServerCapabilities { + definition_provider: Some(OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }; client_a.language_registry().add(rust_lang()); - let mut fake_language_servers = client_a - .language_registry() - .register_fake_lsp("Rust", Default::default()); + let mut fake_language_servers = client_a.language_registry().register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, + ..FakeLspAdapter::default() + }, + ); client_a .fs() @@ -5649,6 +5743,8 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( let definitions; let buffer_b2; if rng.r#gen() { + cx_a.run_until_parked(); + cx_b.run_until_parked(); definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx)); (buffer_b2, _) = project_b .update(cx_b, |p, cx| { @@ -5663,11 +5759,17 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( }) .await .unwrap(); + cx_a.run_until_parked(); + cx_b.run_until_parked(); definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx)); } let definitions = definitions.await.unwrap(); - assert_eq!(definitions.len(), 1); + assert_eq!( + definitions.len(), + 1, + "Unexpected definitions: {definitions:?}" + ); assert_eq!(definitions[0].target.buffer, buffer_b2); } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2912708b56..ff9b703d66 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -134,7 +134,7 @@ use language::{ use linked_editing_ranges::refresh_linked_ranges; use lsp::{ CodeActionKind, CompletionItemKind, CompletionTriggerKind, InsertTextFormat, InsertTextMode, - LanguageServerId, LanguageServerName, + LanguageServerId, }; use lsp_colors::LspColorData; use markdown::Markdown; @@ -1864,7 +1864,6 @@ impl Editor { editor.tasks_update_task = Some(editor.refresh_runnables(window, cx)); } - editor.update_lsp_data(true, None, window, cx); } project::Event::SnippetEdit(id, snippet_edits) => { if let Some(buffer) = editor.buffer.read(cx).buffer(*id) { @@ -1886,6 +1885,11 @@ impl Editor { } } } + project::Event::LanguageServerBufferRegistered { buffer_id, .. } => { + if editor.buffer().read(cx).buffer(*buffer_id).is_some() { + editor.update_lsp_data(false, Some(*buffer_id), window, cx); + } + } _ => {} }, )); @@ -15846,7 +15850,7 @@ impl Editor { let language_server_name = project .language_server_statuses(cx) .find(|(id, _)| server_id == *id) - .map(|(_, status)| LanguageServerName::from(status.name.as_str())); + .map(|(_, status)| status.name.clone()); language_server_name.map(|language_server_name| { project.open_local_buffer_via_lsp( lsp_location.uri.clone(), diff --git a/crates/editor/src/linked_editing_ranges.rs b/crates/editor/src/linked_editing_ranges.rs index 7c2672fc0d..a185de33ca 100644 --- a/crates/editor/src/linked_editing_ranges.rs +++ b/crates/editor/src/linked_editing_ranges.rs @@ -95,7 +95,7 @@ pub(super) fn refresh_linked_ranges( let snapshot = buffer.read(cx).snapshot(); let buffer_id = buffer.read(cx).remote_id(); - let linked_edits_task = project.linked_edit(buffer, *start, cx); + let linked_edits_task = project.linked_edits(buffer, *start, cx); let highlights = move || async move { let edits = linked_edits_task.await.log_err()?; // Find the range containing our current selection. diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index ab3c0f9b37..85123d2373 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -411,30 +411,6 @@ impl LanguageRegistry { cached } - pub fn get_or_register_lsp_adapter( - &self, - language_name: LanguageName, - server_name: LanguageServerName, - build_adapter: impl FnOnce() -> Arc + 'static, - ) -> Arc { - let registered = self - .state - .write() - .lsp_adapters - .entry(language_name.clone()) - .or_default() - .iter() - .find(|cached_adapter| cached_adapter.name == server_name) - .cloned(); - - if let Some(found) = registered { - found - } else { - let adapter = build_adapter(); - self.register_lsp_adapter(language_name, adapter) - } - } - /// Register a fake language server and adapter /// The returned channel receives a new instance of the language server every time it is started #[cfg(any(feature = "test-support", test))] diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 2fd61ea0b2..f8e69e2185 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2154,6 +2154,16 @@ impl LspCommand for GetHover { } } +impl GetCompletions { + pub fn can_resolve_completions(capabilities: &lsp::ServerCapabilities) -> bool { + capabilities + .completion_provider + .as_ref() + .and_then(|options| options.resolve_provider) + .unwrap_or(false) + } +} + #[async_trait(?Send)] impl LspCommand for GetCompletions { type Response = CoreCompletionResponse; @@ -2762,6 +2772,23 @@ impl GetCodeActions { } } +impl OnTypeFormatting { + pub fn supports_on_type_formatting(trigger: &str, capabilities: &ServerCapabilities) -> bool { + let Some(on_type_formatting_options) = &capabilities.document_on_type_formatting_provider + else { + return false; + }; + on_type_formatting_options + .first_trigger_character + .contains(trigger) + || on_type_formatting_options + .more_trigger_character + .iter() + .flatten() + .any(|chars| chars.contains(trigger)) + } +} + #[async_trait(?Send)] impl LspCommand for OnTypeFormatting { type Response = Option; @@ -2773,20 +2800,7 @@ impl LspCommand for OnTypeFormatting { } fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - let Some(on_type_formatting_options) = &capabilities - .server_capabilities - .document_on_type_formatting_provider - else { - return false; - }; - on_type_formatting_options - .first_trigger_character - .contains(&self.trigger) - || on_type_formatting_options - .more_trigger_character - .iter() - .flatten() - .any(|chars| chars.contains(&self.trigger)) + Self::supports_on_type_formatting(&self.trigger, &capabilities.server_capabilities) } fn to_lsp( @@ -4221,8 +4235,9 @@ impl LspCommand for GetDocumentColor { server_capabilities .server_capabilities .color_provider + .as_ref() .is_some_and(|capability| match capability { - lsp::ColorProviderCapability::Simple(supported) => supported, + lsp::ColorProviderCapability::Simple(supported) => *supported, lsp::ColorProviderCapability::ColorProvider(..) => true, lsp::ColorProviderCapability::Options(..) => true, }) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 6122204991..6d448a6fea 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -58,12 +58,12 @@ use language::{ range_from_lsp, range_to_lsp, }; use lsp::{ - CodeActionKind, CompletionContext, DiagnosticSeverity, DiagnosticTag, - DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter, FileOperationPatternKind, - FileOperationRegistrationOptions, FileRename, FileSystemWatcher, LanguageServer, - LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, LanguageServerName, - LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, OneOf, - RenameFilesParams, SymbolKind, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams, + AdapterServerCapabilities, CodeActionKind, CompletionContext, DiagnosticSeverity, + DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter, + FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher, + LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, + LanguageServerName, LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, + OneOf, RenameFilesParams, SymbolKind, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder, notification::DidRenameFiles, }; use node_runtime::read_package_installed_version; @@ -622,7 +622,7 @@ impl LocalLspStore { .on_request::({ let this = this.clone(); move |params, cx| { - let this = this.clone(); + let lsp_store = this.clone(); let mut cx = cx.clone(); async move { for reg in params.registrations { @@ -630,7 +630,7 @@ impl LocalLspStore { "workspace/didChangeWatchedFiles" => { if let Some(options) = reg.register_options { let options = serde_json::from_value(options)?; - this.update(&mut cx, |this, cx| { + lsp_store.update(&mut cx, |this, cx| { this.as_local_mut()?.on_lsp_did_change_watched_files( server_id, ®.id, options, cx, ); @@ -639,8 +639,9 @@ impl LocalLspStore { } } "textDocument/rangeFormatting" => { - this.read_with(&mut cx, |this, _| { - if let Some(server) = this.language_server_for_id(server_id) + lsp_store.update(&mut cx, |lsp_store, cx| { + if let Some(server) = + lsp_store.language_server_for_id(server_id) { let options = reg .register_options @@ -659,14 +660,16 @@ impl LocalLspStore { server.update_capabilities(|capabilities| { capabilities.document_range_formatting_provider = Some(provider); - }) + }); + notify_server_capabilities_updated(&server, cx); } anyhow::Ok(()) })??; } "textDocument/onTypeFormatting" => { - this.read_with(&mut cx, |this, _| { - if let Some(server) = this.language_server_for_id(server_id) + lsp_store.update(&mut cx, |lsp_store, cx| { + if let Some(server) = + lsp_store.language_server_for_id(server_id) { let options = reg .register_options @@ -683,15 +686,17 @@ impl LocalLspStore { capabilities .document_on_type_formatting_provider = Some(options); - }) + }); + notify_server_capabilities_updated(&server, cx); } } anyhow::Ok(()) })??; } "textDocument/formatting" => { - this.read_with(&mut cx, |this, _| { - if let Some(server) = this.language_server_for_id(server_id) + lsp_store.update(&mut cx, |lsp_store, cx| { + if let Some(server) = + lsp_store.language_server_for_id(server_id) { let options = reg .register_options @@ -710,7 +715,8 @@ impl LocalLspStore { server.update_capabilities(|capabilities| { capabilities.document_formatting_provider = Some(provider); - }) + }); + notify_server_capabilities_updated(&server, cx); } anyhow::Ok(()) })??; @@ -719,8 +725,9 @@ impl LocalLspStore { // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. } "textDocument/rename" => { - this.read_with(&mut cx, |this, _| { - if let Some(server) = this.language_server_for_id(server_id) + lsp_store.update(&mut cx, |lsp_store, cx| { + if let Some(server) = + lsp_store.language_server_for_id(server_id) { let options = reg .register_options @@ -737,7 +744,8 @@ impl LocalLspStore { server.update_capabilities(|capabilities| { capabilities.rename_provider = Some(options); - }) + }); + notify_server_capabilities_updated(&server, cx); } anyhow::Ok(()) })??; @@ -755,14 +763,15 @@ impl LocalLspStore { .on_request::({ let this = this.clone(); move |params, cx| { - let this = this.clone(); + let lsp_store = this.clone(); let mut cx = cx.clone(); async move { for unreg in params.unregisterations.iter() { match unreg.method.as_str() { "workspace/didChangeWatchedFiles" => { - this.update(&mut cx, |this, cx| { - this.as_local_mut()? + lsp_store.update(&mut cx, |lsp_store, cx| { + lsp_store + .as_local_mut()? .on_lsp_unregister_did_change_watched_files( server_id, &unreg.id, cx, ); @@ -773,44 +782,52 @@ impl LocalLspStore { // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. } "textDocument/rename" => { - this.read_with(&mut cx, |this, _| { - if let Some(server) = this.language_server_for_id(server_id) + lsp_store.update(&mut cx, |lsp_store, cx| { + if let Some(server) = + lsp_store.language_server_for_id(server_id) { server.update_capabilities(|capabilities| { capabilities.rename_provider = None - }) + }); + notify_server_capabilities_updated(&server, cx); } })?; } "textDocument/rangeFormatting" => { - this.read_with(&mut cx, |this, _| { - if let Some(server) = this.language_server_for_id(server_id) + lsp_store.update(&mut cx, |lsp_store, cx| { + if let Some(server) = + lsp_store.language_server_for_id(server_id) { server.update_capabilities(|capabilities| { capabilities.document_range_formatting_provider = None - }) + }); + notify_server_capabilities_updated(&server, cx); } })?; } "textDocument/onTypeFormatting" => { - this.read_with(&mut cx, |this, _| { - if let Some(server) = this.language_server_for_id(server_id) + lsp_store.update(&mut cx, |lsp_store, cx| { + if let Some(server) = + lsp_store.language_server_for_id(server_id) { server.update_capabilities(|capabilities| { capabilities.document_on_type_formatting_provider = None; - }) + }); + notify_server_capabilities_updated(&server, cx); } })?; } "textDocument/formatting" => { - this.read_with(&mut cx, |this, _| { - if let Some(server) = this.language_server_for_id(server_id) + lsp_store.update(&mut cx, |lsp_store, cx| { + if let Some(server) = + lsp_store.language_server_for_id(server_id) { server.update_capabilities(|capabilities| { capabilities.document_formatting_provider = None; - }) + }); + notify_server_capabilities_updated(&server, cx); } })?; } @@ -2426,7 +2443,6 @@ impl LocalLspStore { let server_id = server_node.server_id_or_init( |LaunchDisposition { server_name, - path, settings, }| { @@ -2468,18 +2484,6 @@ impl LocalLspStore { } }; - let lsp_store = self.weak.clone(); - let server_name = server_node.name(); - let buffer_abs_path = abs_path.to_string_lossy().to_string(); - cx.defer(move |cx| { - lsp_store.update(cx, |_, cx| cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id: server_id, - name: server_name, - message: proto::update_language_server::Variant::RegisteredForBuffer(proto::RegisteredForBuffer { - buffer_abs_path, - }) - })).ok(); - }); server_id }, )?; @@ -2515,11 +2519,13 @@ impl LocalLspStore { snapshot: initial_snapshot.clone(), }; + let mut registered = false; self.buffer_snapshots .entry(buffer_id) .or_default() .entry(server.server_id()) .or_insert_with(|| { + registered = true; server.register_buffer( uri.clone(), adapter.language_id(&language.name()), @@ -2534,15 +2540,18 @@ impl LocalLspStore { .entry(buffer_id) .or_default() .insert(server.server_id()); - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id: server.server_id(), - name: None, - message: proto::update_language_server::Variant::RegisteredForBuffer( - proto::RegisteredForBuffer { - buffer_abs_path: abs_path.to_string_lossy().to_string(), - }, - ), - }); + if registered { + cx.emit(LspStoreEvent::LanguageServerUpdate { + language_server_id: server.server_id(), + name: None, + message: proto::update_language_server::Variant::RegisteredForBuffer( + proto::RegisteredForBuffer { + buffer_abs_path: abs_path.to_string_lossy().to_string(), + buffer_id: buffer_id.to_proto(), + }, + ), + }); + } } } @@ -3494,6 +3503,20 @@ impl LocalLspStore { } } +fn notify_server_capabilities_updated(server: &LanguageServer, cx: &mut Context) { + if let Some(capabilities) = serde_json::to_string(&server.capabilities()).ok() { + cx.emit(LspStoreEvent::LanguageServerUpdate { + language_server_id: server.server_id(), + name: Some(server.name()), + message: proto::update_language_server::Variant::MetadataUpdated( + proto::ServerMetadataUpdated { + capabilities: Some(capabilities), + }, + ), + }); + } +} + #[derive(Debug)] pub struct FormattableBuffer { handle: Entity, @@ -3533,6 +3556,7 @@ pub struct LspStore { _maintain_buffer_languages: Task<()>, diagnostic_summaries: HashMap, HashMap>>, + pub(super) lsp_server_capabilities: HashMap, lsp_document_colors: HashMap, lsp_code_lens: HashMap, } @@ -3604,7 +3628,7 @@ pub enum LspStoreEvent { #[derive(Clone, Debug, Serialize)] pub struct LanguageServerStatus { - pub name: String, + pub name: LanguageServerName, pub pending_work: BTreeMap, pub has_pending_diagnostic_updates: bool, progress_tokens: HashSet, @@ -3795,6 +3819,7 @@ impl LspStore { language_server_statuses: Default::default(), nonce: StdRng::from_entropy().r#gen(), diagnostic_summaries: HashMap::default(), + lsp_server_capabilities: HashMap::default(), lsp_document_colors: HashMap::default(), lsp_code_lens: HashMap::default(), active_entry: None, @@ -3811,6 +3836,9 @@ impl LspStore { request: R, cx: &mut Context, ) -> Task::Response>> { + if !self.is_capable_for_proto_request(&buffer, &request, cx) { + return Task::ready(Ok(R::Response::default())); + } let message = request.to_proto(upstream_project_id, buffer.read(cx)); cx.spawn(async move |this, cx| { let response = client.request(message).await?; @@ -3853,6 +3881,7 @@ impl LspStore { language_server_statuses: Default::default(), nonce: StdRng::from_entropy().r#gen(), diagnostic_summaries: HashMap::default(), + lsp_server_capabilities: HashMap::default(), lsp_document_colors: HashMap::default(), lsp_code_lens: HashMap::default(), active_entry: None, @@ -4428,20 +4457,73 @@ impl LspStore { } } - pub fn request_lsp( + // TODO: remove MultiLspQuery: instead, the proto handler should pick appropriate server(s) + // Then, use `send_lsp_proto_request` or analogue for most of the LSP proto requests and inline this check inside + fn is_capable_for_proto_request( + &self, + buffer: &Entity, + request: &R, + cx: &Context, + ) -> bool + where + R: LspCommand, + { + self.check_if_capable_for_proto_request( + buffer, + |capabilities| { + request.check_capabilities(AdapterServerCapabilities { + server_capabilities: capabilities.clone(), + code_action_kinds: None, + }) + }, + cx, + ) + } + + fn check_if_capable_for_proto_request( + &self, + buffer: &Entity, + check: F, + cx: &Context, + ) -> bool + where + F: Fn(&lsp::ServerCapabilities) -> bool, + { + let Some(language) = buffer.read(cx).language().cloned() else { + return false; + }; + let relevant_language_servers = self + .languages + .lsp_adapters(&language.name()) + .into_iter() + .map(|lsp_adapter| lsp_adapter.name()) + .collect::>(); + self.language_server_statuses + .iter() + .filter_map(|(server_id, server_status)| { + relevant_language_servers + .contains(&server_status.name) + .then_some(server_id) + }) + .filter_map(|server_id| self.lsp_server_capabilities.get(&server_id)) + .any(check) + } + + pub fn request_lsp( &mut self, - buffer_handle: Entity, + buffer: Entity, server: LanguageServerToQuery, request: R, cx: &mut Context, ) -> Task> where + R: LspCommand, ::Result: Send, ::Params: Send, { if let Some((upstream_client, upstream_project_id)) = self.upstream_client() { return self.send_lsp_proto_request( - buffer_handle, + buffer, upstream_client, upstream_project_id, request, @@ -4449,7 +4531,7 @@ impl LspStore { ); } - let Some(language_server) = buffer_handle.update(cx, |buffer, cx| match server { + let Some(language_server) = buffer.update(cx, |buffer, cx| match server { LanguageServerToQuery::FirstCapable => self.as_local().and_then(|local| { local .language_servers_for_buffer(buffer, cx) @@ -4469,8 +4551,7 @@ impl LspStore { return Task::ready(Ok(Default::default())); }; - let buffer = buffer_handle.read(cx); - let file = File::from_dyn(buffer.file()).and_then(File::as_local); + let file = File::from_dyn(buffer.read(cx).file()).and_then(File::as_local); let Some(file) = file else { return Task::ready(Ok(Default::default())); @@ -4478,7 +4559,7 @@ impl LspStore { let lsp_params = match request.to_lsp_params_or_response( &file.abs_path(cx), - buffer, + buffer.read(cx), &language_server, cx, ) { @@ -4554,7 +4635,7 @@ impl LspStore { .response_from_lsp( response, this.upgrade().context("no app context")?, - buffer_handle, + buffer, language_server.server_id(), cx.clone(), ) @@ -4624,7 +4705,8 @@ impl LspStore { ) }) { let buffer = buffer_handle.read(cx); - if !local.registered_buffers.contains_key(&buffer.remote_id()) { + let buffer_id = buffer.remote_id(); + if !local.registered_buffers.contains_key(&buffer_id) { continue; } if let Some((file, language)) = File::from_dyn(buffer.file()) @@ -4725,6 +4807,7 @@ impl LspStore { proto::update_language_server::Variant::RegisteredForBuffer( proto::RegisteredForBuffer { buffer_abs_path: abs_path.to_string_lossy().to_string(), + buffer_id: buffer_id.to_proto(), }, ), }); @@ -4900,15 +4983,20 @@ impl LspStore { pub fn resolve_inlay_hint( &self, - hint: InlayHint, - buffer_handle: Entity, + mut hint: InlayHint, + buffer: Entity, server_id: LanguageServerId, cx: &mut Context, ) -> Task> { if let Some((upstream_client, project_id)) = self.upstream_client() { + if !self.check_if_capable_for_proto_request(&buffer, InlayHints::can_resolve_inlays, cx) + { + hint.resolve_state = ResolveState::Resolved; + return Task::ready(Ok(hint)); + } let request = proto::ResolveInlayHint { project_id, - buffer_id: buffer_handle.read(cx).remote_id().into(), + buffer_id: buffer.read(cx).remote_id().into(), language_server_id: server_id.0 as u64, hint: Some(InlayHints::project_to_proto_hint(hint.clone())), }; @@ -4924,7 +5012,7 @@ impl LspStore { } }) } else { - let Some(lang_server) = buffer_handle.update(cx, |buffer, cx| { + let Some(lang_server) = buffer.update(cx, |buffer, cx| { self.language_server_for_local_buffer(buffer, server_id, cx) .map(|(_, server)| server.clone()) }) else { @@ -4933,7 +5021,7 @@ impl LspStore { if !InlayHints::can_resolve_inlays(&lang_server.capabilities()) { return Task::ready(Ok(hint)); } - let buffer_snapshot = buffer_handle.read(cx).snapshot(); + let buffer_snapshot = buffer.read(cx).snapshot(); cx.spawn(async move |_, cx| { let resolve_task = lang_server.request::( InlayHints::project_to_lsp_hint(hint, &buffer_snapshot), @@ -4944,7 +5032,7 @@ impl LspStore { .context("inlay hint resolve LSP request")?; let resolved_hint = InlayHints::lsp_to_project_hint( resolved_hint, - &buffer_handle, + &buffer, server_id, ResolveState::Resolved, false, @@ -5055,7 +5143,7 @@ impl LspStore { } } - pub(crate) fn linked_edit( + pub(crate) fn linked_edits( &mut self, buffer: &Entity, position: Anchor, @@ -5097,7 +5185,7 @@ impl LspStore { }) == Some(true) }) else { - return Task::ready(Ok(vec![])); + return Task::ready(Ok(Vec::new())); }; self.request_lsp( @@ -5116,6 +5204,15 @@ impl LspStore { cx: &mut Context, ) -> Task>> { if let Some((client, project_id)) = self.upstream_client() { + if !self.check_if_capable_for_proto_request( + &buffer, + |capabilities| { + OnTypeFormatting::supports_on_type_formatting(&trigger, capabilities) + }, + cx, + ) { + return Task::ready(Ok(None)); + } let request = proto::OnTypeFormatting { project_id, buffer_id: buffer.read(cx).remote_id().into(), @@ -5227,6 +5324,10 @@ impl LspStore { cx: &mut Context, ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { + let request = GetDefinitions { position }; + if !self.is_capable_for_proto_request(buffer_handle, &request, cx) { + return Task::ready(Ok(Vec::new())); + } let request_task = upstream_client.request(proto::MultiLspQuery { buffer_id: buffer_handle.read(cx).remote_id().into(), version: serialize_version(&buffer_handle.read(cx).version()), @@ -5235,7 +5336,7 @@ impl LspStore { proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetDefinition( - GetDefinitions { position }.to_proto(project_id, buffer_handle.read(cx)), + request.to_proto(project_id, buffer_handle.read(cx)), )), }); let buffer = buffer_handle.clone(); @@ -5300,6 +5401,10 @@ impl LspStore { cx: &mut Context, ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { + let request = GetDeclarations { position }; + if !self.is_capable_for_proto_request(buffer_handle, &request, cx) { + return Task::ready(Ok(Vec::new())); + } let request_task = upstream_client.request(proto::MultiLspQuery { buffer_id: buffer_handle.read(cx).remote_id().into(), version: serialize_version(&buffer_handle.read(cx).version()), @@ -5308,7 +5413,7 @@ impl LspStore { proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetDeclaration( - GetDeclarations { position }.to_proto(project_id, buffer_handle.read(cx)), + request.to_proto(project_id, buffer_handle.read(cx)), )), }); let buffer = buffer_handle.clone(); @@ -5368,23 +5473,27 @@ impl LspStore { pub fn type_definitions( &mut self, - buffer_handle: &Entity, + buffer: &Entity, position: PointUtf16, cx: &mut Context, ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { + let request = GetTypeDefinitions { position }; + if !self.is_capable_for_proto_request(&buffer, &request, cx) { + return Task::ready(Ok(Vec::new())); + } let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer_handle.read(cx).remote_id().into(), - version: serialize_version(&buffer_handle.read(cx).version()), + buffer_id: buffer.read(cx).remote_id().into(), + version: serialize_version(&buffer.read(cx).version()), project_id, strategy: Some(proto::multi_lsp_query::Strategy::All( proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetTypeDefinition( - GetTypeDefinitions { position }.to_proto(project_id, buffer_handle.read(cx)), + request.to_proto(project_id, buffer.read(cx)), )), }); - let buffer = buffer_handle.clone(); + let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { return Ok(Vec::new()); @@ -5423,7 +5532,7 @@ impl LspStore { }) } else { let type_definitions_task = self.request_multiple_lsp_locally( - buffer_handle, + buffer, Some(position), GetTypeDefinitions { position }, cx, @@ -5441,23 +5550,27 @@ impl LspStore { pub fn implementations( &mut self, - buffer_handle: &Entity, + buffer: &Entity, position: PointUtf16, cx: &mut Context, ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { + let request = GetImplementations { position }; + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Ok(Vec::new())); + } let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer_handle.read(cx).remote_id().into(), - version: serialize_version(&buffer_handle.read(cx).version()), + buffer_id: buffer.read(cx).remote_id().into(), + version: serialize_version(&buffer.read(cx).version()), project_id, strategy: Some(proto::multi_lsp_query::Strategy::All( proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetImplementation( - GetImplementations { position }.to_proto(project_id, buffer_handle.read(cx)), + request.to_proto(project_id, buffer.read(cx)), )), }); - let buffer = buffer_handle.clone(); + let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { return Ok(Vec::new()); @@ -5496,7 +5609,7 @@ impl LspStore { }) } else { let implementations_task = self.request_multiple_lsp_locally( - buffer_handle, + buffer, Some(position), GetImplementations { position }, cx, @@ -5514,23 +5627,27 @@ impl LspStore { pub fn references( &mut self, - buffer_handle: &Entity, + buffer: &Entity, position: PointUtf16, cx: &mut Context, ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { + let request = GetReferences { position }; + if !self.is_capable_for_proto_request(&buffer, &request, cx) { + return Task::ready(Ok(Vec::new())); + } let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer_handle.read(cx).remote_id().into(), - version: serialize_version(&buffer_handle.read(cx).version()), + buffer_id: buffer.read(cx).remote_id().into(), + version: serialize_version(&buffer.read(cx).version()), project_id, strategy: Some(proto::multi_lsp_query::Strategy::All( proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetReferences( - GetReferences { position }.to_proto(project_id, buffer_handle.read(cx)), + request.to_proto(project_id, buffer.read(cx)), )), }); - let buffer = buffer_handle.clone(); + let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { return Ok(Vec::new()); @@ -5569,7 +5686,7 @@ impl LspStore { }) } else { let references_task = self.request_multiple_lsp_locally( - buffer_handle, + buffer, Some(position), GetReferences { position }, cx, @@ -5587,28 +5704,31 @@ impl LspStore { pub fn code_actions( &mut self, - buffer_handle: &Entity, + buffer: &Entity, range: Range, kinds: Option>, cx: &mut Context, ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { + let request = GetCodeActions { + range: range.clone(), + kinds: kinds.clone(), + }; + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Ok(Vec::new())); + } let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer_handle.read(cx).remote_id().into(), - version: serialize_version(&buffer_handle.read(cx).version()), + buffer_id: buffer.read(cx).remote_id().into(), + version: serialize_version(&buffer.read(cx).version()), project_id, strategy: Some(proto::multi_lsp_query::Strategy::All( proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetCodeActions( - GetCodeActions { - range: range.clone(), - kinds: kinds.clone(), - } - .to_proto(project_id, buffer_handle.read(cx)), + request.to_proto(project_id, buffer.read(cx)), )), }); - let buffer = buffer_handle.clone(); + let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { return Ok(Vec::new()); @@ -5650,7 +5770,7 @@ impl LspStore { }) } else { let all_actions_task = self.request_multiple_lsp_locally( - buffer_handle, + buffer, Some(range.start), GetCodeActions { range: range.clone(), @@ -5752,6 +5872,10 @@ impl LspStore { cx: &mut Context, ) -> Task>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { + let request = GetCodeLens; + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Ok(HashMap::default())); + } let request_task = upstream_client.request(proto::MultiLspQuery { buffer_id: buffer.read(cx).remote_id().into(), version: serialize_version(&buffer.read(cx).version()), @@ -5760,7 +5884,7 @@ impl LspStore { proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetCodeLens( - GetCodeLens.to_proto(project_id, buffer.read(cx)), + request.to_proto(project_id, buffer.read(cx)), )), }); let buffer = buffer.clone(); @@ -5844,11 +5968,15 @@ impl LspStore { let language_registry = self.languages.clone(); if let Some((upstream_client, project_id)) = self.upstream_client() { + let request = GetCompletions { position, context }; + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Ok(Vec::new())); + } let task = self.send_lsp_proto_request( buffer.clone(), upstream_client, project_id, - GetCompletions { position, context }, + request, cx, ); let language = buffer.read(cx).language().cloned(); @@ -5986,11 +6114,17 @@ impl LspStore { cx: &mut Context, ) -> Task> { let client = self.upstream_client(); - let buffer_id = buffer.read(cx).remote_id(); let buffer_snapshot = buffer.read(cx).snapshot(); - cx.spawn(async move |this, cx| { + if !self.check_if_capable_for_proto_request( + &buffer, + GetCompletions::can_resolve_completions, + cx, + ) { + return Task::ready(Ok(false)); + } + cx.spawn(async move |lsp_store, cx| { let mut did_resolve = false; if let Some((client, project_id)) = client { for completion_index in completion_indices { @@ -6027,7 +6161,7 @@ impl LspStore { completion.source.server_id() }; if let Some(server_id) = server_id { - let server_and_adapter = this + let server_and_adapter = lsp_store .read_with(cx, |lsp_store, _| { let server = lsp_store.language_server_for_id(server_id)?; let adapter = @@ -6078,13 +6212,7 @@ impl LspStore { completion_index: usize, ) -> Result<()> { let server_id = server.server_id(); - let can_resolve = server - .capabilities() - .completion_provider - .as_ref() - .and_then(|options| options.resolve_provider) - .unwrap_or(false); - if !can_resolve { + if !GetCompletions::can_resolve_completions(&server.capabilities()) { return Ok(()); } @@ -6435,16 +6563,24 @@ impl LspStore { pub fn pull_diagnostics( &mut self, - buffer_handle: Entity, + buffer: Entity, cx: &mut Context, ) -> Task>> { - let buffer = buffer_handle.read(cx); - let buffer_id = buffer.remote_id(); + let buffer_id = buffer.read(cx).remote_id(); if let Some((client, upstream_project_id)) = self.upstream_client() { + if !self.is_capable_for_proto_request( + &buffer, + &GetDocumentDiagnostics { + previous_result_id: None, + }, + cx, + ) { + return Task::ready(Ok(Vec::new())); + } let request_task = client.request(proto::MultiLspQuery { buffer_id: buffer_id.to_proto(), - version: serialize_version(&buffer_handle.read(cx).version()), + version: serialize_version(&buffer.read(cx).version()), project_id: upstream_project_id, strategy: Some(proto::multi_lsp_query::Strategy::All( proto::AllLanguageServers {}, @@ -6453,7 +6589,7 @@ impl LspStore { proto::GetDocumentDiagnostics { project_id: upstream_project_id, buffer_id: buffer_id.to_proto(), - version: serialize_version(&buffer_handle.read(cx).version()), + version: serialize_version(&buffer.read(cx).version()), }, )), }); @@ -6475,7 +6611,7 @@ impl LspStore { .collect()) }) } else { - let server_ids = buffer_handle.update(cx, |buffer, cx| { + let server_ids = buffer.update(cx, |buffer, cx| { self.language_servers_for_local_buffer(buffer, cx) .map(|(_, server)| server.server_id()) .collect::>() @@ -6485,7 +6621,7 @@ impl LspStore { .map(|server_id| { let result_id = self.result_id(server_id, buffer_id, cx); self.request_lsp( - buffer_handle.clone(), + buffer.clone(), LanguageServerToQuery::Other(server_id), GetDocumentDiagnostics { previous_result_id: result_id, @@ -6507,34 +6643,36 @@ impl LspStore { pub fn inlay_hints( &mut self, - buffer_handle: Entity, + buffer: Entity, range: Range, cx: &mut Context, ) -> Task>> { - let buffer = buffer_handle.read(cx); let range_start = range.start; let range_end = range.end; - let buffer_id = buffer.remote_id().into(); - let lsp_request = InlayHints { range }; + let buffer_id = buffer.read(cx).remote_id().into(); + let request = InlayHints { range }; if let Some((client, project_id)) = self.upstream_client() { - let request = proto::InlayHints { + if !self.is_capable_for_proto_request(&buffer, &request, cx) { + return Task::ready(Ok(Vec::new())); + } + let proto_request = proto::InlayHints { project_id, buffer_id, start: Some(serialize_anchor(&range_start)), end: Some(serialize_anchor(&range_end)), - version: serialize_version(&buffer_handle.read(cx).version()), + version: serialize_version(&buffer.read(cx).version()), }; cx.spawn(async move |project, cx| { let response = client - .request(request) + .request(proto_request) .await .context("inlay hints proto request")?; LspCommand::response_from_proto( - lsp_request, + request, response, project.upgrade().context("No project")?, - buffer_handle.clone(), + buffer.clone(), cx.clone(), ) .await @@ -6542,13 +6680,13 @@ impl LspStore { }) } else { let lsp_request_task = self.request_lsp( - buffer_handle.clone(), + buffer.clone(), LanguageServerToQuery::FirstCapable, - lsp_request, + request, cx, ); cx.spawn(async move |_, cx| { - buffer_handle + buffer .update(cx, |buffer, _| { buffer.wait_for_edits(vec![range_start.timestamp, range_end.timestamp]) })? @@ -6772,6 +6910,11 @@ impl LspStore { cx: &mut Context, ) -> Task>>> { if let Some((client, project_id)) = self.upstream_client() { + let request = GetDocumentColor {}; + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Ok(HashMap::default())); + } + let request_task = client.request(proto::MultiLspQuery { project_id, buffer_id: buffer.read(cx).remote_id().to_proto(), @@ -6780,7 +6923,7 @@ impl LspStore { proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetDocumentColor( - GetDocumentColor {}.to_proto(project_id, buffer.read(cx)), + request.to_proto(project_id, buffer.read(cx)), )), }); let buffer = buffer.clone(); @@ -6808,7 +6951,7 @@ impl LspStore { } }) .map(|(server_id, color_response)| { - let response = GetDocumentColor {}.response_from_proto( + let response = request.response_from_proto( color_response, project.clone(), buffer.clone(), @@ -6855,6 +6998,10 @@ impl LspStore { let position = position.to_point_utf16(buffer.read(cx)); if let Some((client, upstream_project_id)) = self.upstream_client() { + let request = GetSignatureHelp { position }; + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Vec::new()); + } let request_task = client.request(proto::MultiLspQuery { buffer_id: buffer.read(cx).remote_id().into(), version: serialize_version(&buffer.read(cx).version()), @@ -6863,7 +7010,7 @@ impl LspStore { proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetSignatureHelp( - GetSignatureHelp { position }.to_proto(upstream_project_id, buffer.read(cx)), + request.to_proto(upstream_project_id, buffer.read(cx)), )), }); let buffer = buffer.clone(); @@ -6926,6 +7073,10 @@ impl LspStore { cx: &mut Context, ) -> Task> { if let Some((client, upstream_project_id)) = self.upstream_client() { + let request = GetHover { position }; + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Vec::new()); + } let request_task = client.request(proto::MultiLspQuery { buffer_id: buffer.read(cx).remote_id().into(), version: serialize_version(&buffer.read(cx).version()), @@ -6934,7 +7085,7 @@ impl LspStore { proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetHover( - GetHover { position }.to_proto(upstream_project_id, buffer.read(cx)), + request.to_proto(upstream_project_id, buffer.read(cx)), )), }); let buffer = buffer.clone(); @@ -7539,16 +7690,20 @@ impl LspStore { self.downstream_client = Some((downstream_client.clone(), project_id)); for (server_id, status) in &self.language_server_statuses { - downstream_client - .send(proto::StartLanguageServer { - project_id, - server: Some(proto::LanguageServer { - id: server_id.0 as u64, - name: status.name.clone(), - worktree_id: None, - }), - }) - .log_err(); + if let Some(server) = self.language_server_for_id(*server_id) { + downstream_client + .send(proto::StartLanguageServer { + project_id, + server: Some(proto::LanguageServer { + id: server_id.to_proto(), + name: status.name.to_string(), + worktree_id: None, + }), + capabilities: serde_json::to_string(&server.capabilities()) + .expect("serializing server LSP capabilities"), + }) + .log_err(); + } } } @@ -7575,7 +7730,7 @@ impl LspStore { ( LanguageServerId(server.id as usize), LanguageServerStatus { - name: server.name, + name: LanguageServerName::from_proto(server.name), pending_work: Default::default(), has_pending_diagnostic_updates: false, progress_tokens: Default::default(), @@ -8724,18 +8879,29 @@ impl LspStore { } async fn handle_start_language_server( - this: Entity, + lsp_store: Entity, envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result<()> { let server = envelope.payload.server.context("invalid server")?; - - this.update(&mut cx, |this, cx| { + let server_capabilities = + serde_json::from_str::(&envelope.payload.capabilities) + .with_context(|| { + format!( + "incorrect server capabilities {}", + envelope.payload.capabilities + ) + })?; + lsp_store.update(&mut cx, |lsp_store, cx| { let server_id = LanguageServerId(server.id as usize); - this.language_server_statuses.insert( + let server_name = LanguageServerName::from_proto(server.name.clone()); + lsp_store + .lsp_server_capabilities + .insert(server_id, server_capabilities); + lsp_store.language_server_statuses.insert( server_id, LanguageServerStatus { - name: server.name.clone(), + name: server_name.clone(), pending_work: Default::default(), has_pending_diagnostic_updates: false, progress_tokens: Default::default(), @@ -8743,7 +8909,7 @@ impl LspStore { ); cx.emit(LspStoreEvent::LanguageServerAdded( server_id, - LanguageServerName(server.name.into()), + server_name, server.worktree_id.map(WorktreeId::from_proto), )); cx.notify(); @@ -8804,7 +8970,8 @@ impl LspStore { } non_lsp @ proto::update_language_server::Variant::StatusUpdate(_) - | non_lsp @ proto::update_language_server::Variant::RegisteredForBuffer(_) => { + | non_lsp @ proto::update_language_server::Variant::RegisteredForBuffer(_) + | non_lsp @ proto::update_language_server::Variant::MetadataUpdated(_) => { cx.emit(LspStoreEvent::LanguageServerUpdate { language_server_id, name: envelope @@ -10251,7 +10418,7 @@ impl LspStore { let name = self .language_server_statuses .remove(&server_id) - .map(|status| LanguageServerName::from(status.name.as_str())) + .map(|status| status.name.clone()) .or_else(|| { if let Some(LanguageServerState::Running { adapter, .. }) = server_state.as_ref() { Some(adapter.name()) @@ -10744,7 +10911,7 @@ impl LspStore { self.language_server_statuses.insert( server_id, LanguageServerStatus { - name: language_server.name().to_string(), + name: language_server.name(), pending_work: Default::default(), has_pending_diagnostic_updates: false, progress_tokens: Default::default(), @@ -10758,18 +10925,23 @@ impl LspStore { )); cx.emit(LspStoreEvent::RefreshInlayHints); + let server_capabilities = language_server.capabilities(); if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() { downstream_client .send(proto::StartLanguageServer { project_id: *project_id, server: Some(proto::LanguageServer { - id: server_id.0 as u64, + id: server_id.to_proto(), name: language_server.name().to_string(), worktree_id: Some(key.0.to_proto()), }), + capabilities: serde_json::to_string(&server_capabilities) + .expect("serializing server LSP capabilities"), }) .log_err(); } + self.lsp_server_capabilities + .insert(server_id, server_capabilities); // Tell the language server about every open buffer in the worktree that matches the language. // Also check for buffers in worktrees that reused this server @@ -10817,10 +10989,11 @@ impl LspStore { let local = self.as_local_mut().unwrap(); - if local.registered_buffers.contains_key(&buffer.remote_id()) { + let buffer_id = buffer.remote_id(); + if local.registered_buffers.contains_key(&buffer_id) { let versions = local .buffer_snapshots - .entry(buffer.remote_id()) + .entry(buffer_id) .or_default() .entry(server_id) .and_modify(|_| { @@ -10846,10 +11019,10 @@ impl LspStore { version, initial_snapshot.text(), ); - buffer_paths_registered.push(file.abs_path(cx)); + buffer_paths_registered.push((buffer_id, file.abs_path(cx))); local .buffers_opened_in_servers - .entry(buffer.remote_id()) + .entry(buffer_id) .or_default() .insert(server_id); } @@ -10873,13 +11046,14 @@ impl LspStore { } }); - for abs_path in buffer_paths_registered { + for (buffer_id, abs_path) in buffer_paths_registered { cx.emit(LspStoreEvent::LanguageServerUpdate { language_server_id: server_id, name: Some(adapter.name()), message: proto::update_language_server::Variant::RegisteredForBuffer( proto::RegisteredForBuffer { buffer_abs_path: abs_path.to_string_lossy().to_string(), + buffer_id: buffer_id.to_proto(), }, ), }); @@ -11337,6 +11511,7 @@ impl LspStore { } fn cleanup_lsp_data(&mut self, for_server: LanguageServerId) { + self.lsp_server_capabilities.remove(&for_server); for buffer_colors in self.lsp_document_colors.values_mut() { buffer_colors.colors.remove(&for_server); buffer_colors.cache_version += 1; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5000ba93be..398e8bde87 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -277,6 +277,13 @@ pub enum Event { LanguageServerAdded(LanguageServerId, LanguageServerName, Option), LanguageServerRemoved(LanguageServerId), LanguageServerLog(LanguageServerId, LanguageServerLogType, String), + // [`lsp::notification::DidOpenTextDocument`] was sent to this server using the buffer data. + // Zed's buffer-related data is updated accordingly. + LanguageServerBufferRegistered { + server_id: LanguageServerId, + buffer_id: BufferId, + buffer_abs_path: PathBuf, + }, Toast { notification_id: SharedString, message: String, @@ -2931,8 +2938,8 @@ impl Project { } LspStoreEvent::LanguageServerUpdate { language_server_id, - message, name, + message, } => { if self.is_local() { self.enqueue_buffer_ordered_message( @@ -2944,6 +2951,32 @@ impl Project { ) .ok(); } + + match message { + proto::update_language_server::Variant::MetadataUpdated(update) => { + if let Some(capabilities) = update + .capabilities + .as_ref() + .and_then(|capabilities| serde_json::from_str(capabilities).ok()) + { + self.lsp_store.update(cx, |lsp_store, _| { + lsp_store + .lsp_server_capabilities + .insert(*language_server_id, capabilities); + }); + } + } + proto::update_language_server::Variant::RegisteredForBuffer(update) => { + if let Some(buffer_id) = BufferId::new(update.buffer_id).ok() { + cx.emit(Event::LanguageServerBufferRegistered { + buffer_id, + server_id: *language_server_id, + buffer_abs_path: PathBuf::from(&update.buffer_abs_path), + }); + } + } + _ => (), + } } LspStoreEvent::Notification(message) => cx.emit(Event::Toast { notification_id: "lsp".into(), @@ -3476,20 +3509,6 @@ impl Project { }) } - fn document_highlights_impl( - &mut self, - buffer: &Entity, - position: PointUtf16, - cx: &mut Context, - ) -> Task>> { - self.request_lsp( - buffer.clone(), - LanguageServerToQuery::FirstCapable, - GetDocumentHighlights { position }, - cx, - ) - } - pub fn document_highlights( &mut self, buffer: &Entity, @@ -3497,7 +3516,12 @@ impl Project { cx: &mut Context, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.document_highlights_impl(buffer, position, cx) + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::FirstCapable, + GetDocumentHighlights { position }, + cx, + ) } pub fn document_symbols( @@ -3598,14 +3622,14 @@ impl Project { .update(cx, |lsp_store, cx| lsp_store.hover(buffer, position, cx)) } - pub fn linked_edit( + pub fn linked_edits( &self, buffer: &Entity, position: Anchor, cx: &mut Context, ) -> Task>>> { self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.linked_edit(buffer, position, cx) + lsp_store.linked_edits(buffer, position, cx) }) } @@ -3697,19 +3721,6 @@ impl Project { }) } - fn prepare_rename_impl( - &mut self, - buffer: Entity, - position: PointUtf16, - cx: &mut Context, - ) -> Task> { - self.request_lsp( - buffer, - LanguageServerToQuery::FirstCapable, - PrepareRename { position }, - cx, - ) - } pub fn prepare_rename( &mut self, buffer: Entity, @@ -3717,7 +3728,12 @@ impl Project { cx: &mut Context, ) -> Task> { let position = position.to_point_utf16(buffer.read(cx)); - self.prepare_rename_impl(buffer, position, cx) + self.request_lsp( + buffer, + LanguageServerToQuery::FirstCapable, + PrepareRename { position }, + cx, + ) } pub fn perform_rename( diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 779cf95add..75ebc8339a 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1100,7 +1100,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon let fake_server = fake_servers.next().await.unwrap(); let (server_id, server_name) = lsp_store.read_with(cx, |lsp_store, _| { let (id, status) = lsp_store.language_server_statuses().next().unwrap(); - (id, LanguageServerName::from(status.name.as_str())) + (id, status.name.clone()) }); // Simulate jumping to a definition in a dependency outside of the worktree. @@ -1698,7 +1698,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC name: "the-language-server", disk_based_diagnostics_sources: vec!["disk".into()], disk_based_diagnostics_progress_token: Some(progress_token.into()), - ..Default::default() + ..FakeLspAdapter::default() }, ); @@ -1710,6 +1710,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC }) .await .unwrap(); + let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id()); // Simulate diagnostics starting to update. let fake_server = fake_servers.next().await.unwrap(); fake_server.start_progress(progress_token).await; @@ -1736,6 +1737,14 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC ); assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints); fake_server.start_progress(progress_token).await; + assert_eq!( + events.next().await.unwrap(), + Event::LanguageServerBufferRegistered { + server_id: LanguageServerId(1), + buffer_id, + buffer_abs_path: PathBuf::from(path!("/dir/a.rs")), + } + ); assert_eq!( events.next().await.unwrap(), Event::DiskBasedDiagnosticsStarted { diff --git a/crates/proto/proto/call.proto b/crates/proto/proto/call.proto index 5212f3b43f..b5c882db56 100644 --- a/crates/proto/proto/call.proto +++ b/crates/proto/proto/call.proto @@ -71,6 +71,7 @@ message RejoinedProject { repeated WorktreeMetadata worktrees = 2; repeated Collaborator collaborators = 3; repeated LanguageServer language_servers = 4; + repeated string language_server_capabilities = 5; } message LeaveRoom {} @@ -199,6 +200,7 @@ message JoinProjectResponse { repeated WorktreeMetadata worktrees = 2; repeated Collaborator collaborators = 3; repeated LanguageServer language_servers = 4; + repeated string language_server_capabilities = 8; ChannelRole role = 6; reserved 7; } diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index e3c2f69c0b..1e693dfdf3 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -518,6 +518,7 @@ message LanguageServer { message StartLanguageServer { uint64 project_id = 1; LanguageServer server = 2; + string capabilities = 3; } message UpdateDiagnosticSummary { @@ -545,6 +546,7 @@ message UpdateLanguageServer { LspDiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 7; StatusUpdate status_update = 9; RegisteredForBuffer registered_for_buffer = 10; + ServerMetadataUpdated metadata_updated = 11; } } @@ -597,6 +599,11 @@ enum ServerBinaryStatus { message RegisteredForBuffer { string buffer_abs_path = 1; + uint64 buffer_id = 2; +} + +message ServerMetadataUpdated { + optional string capabilities = 1; } message LanguageServerLog { From 064c5daa994989f720afec7a305ffd4539e7c88c Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 5 Aug 2025 10:35:54 -0400 Subject: [PATCH 077/693] docs: Fix incorrect reference to JSX language (#35639) Closes: https://github.com/zed-industries/zed/issues/35633 Release Notes: - N/A --- docs/src/extensions/languages.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/src/extensions/languages.md b/docs/src/extensions/languages.md index 44c673e3e1..6756cb8a23 100644 --- a/docs/src/extensions/languages.md +++ b/docs/src/extensions/languages.md @@ -402,11 +402,10 @@ If your language server supports additional languages, you can use `language_ids [language-servers.my-language-server] name = "Whatever LSP" -languages = ["JavaScript", "JSX", "HTML", "CSS"] +languages = ["JavaScript", "HTML", "CSS"] [language-servers.my-language-server.language_ids] "JavaScript" = "javascript" -"JSX" = "javascriptreact" "TSX" = "typescriptreact" "HTML" = "html" "CSS" = "css" From 351e8c4cd9bb6abba544d90c87c3fed2b6264ca1 Mon Sep 17 00:00:00 2001 From: localcc Date: Tue, 5 Aug 2025 16:36:08 +0200 Subject: [PATCH 078/693] Fix LiveKit audio for devices with different sample formats (#35604) Release Notes: - N/A --- .../src/livekit_client/playback.rs | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/crates/livekit_client/src/livekit_client/playback.rs b/crates/livekit_client/src/livekit_client/playback.rs index c62b8853b4..f14e156125 100644 --- a/crates/livekit_client/src/livekit_client/playback.rs +++ b/crates/livekit_client/src/livekit_client/playback.rs @@ -1,6 +1,7 @@ use anyhow::{Context as _, Result}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait as _}; +use cpal::{Data, FromSample, I24, SampleFormat, SizedSample}; use futures::channel::mpsc::UnboundedSender; use futures::{Stream, StreamExt as _}; use gpui::{ @@ -258,9 +259,15 @@ impl AudioStack { let stream = device .build_input_stream_raw( &config.config(), - cpal::SampleFormat::I16, + config.sample_format(), move |data, _: &_| { - let mut data = data.as_slice::().unwrap(); + let data = + Self::get_sample_data(config.sample_format(), data).log_err(); + let Some(data) = data else { + return; + }; + let mut data = data.as_slice(); + while data.len() > 0 { let remainder = (buf.capacity() - buf.len()).min(data.len()); buf.extend_from_slice(&data[..remainder]); @@ -313,6 +320,33 @@ impl AudioStack { drop(end_on_drop_tx) } } + + fn get_sample_data(sample_format: SampleFormat, data: &Data) -> Result> { + match sample_format { + SampleFormat::I8 => Ok(Self::convert_sample_data::(data)), + SampleFormat::I16 => Ok(data.as_slice::().unwrap().to_vec()), + SampleFormat::I24 => Ok(Self::convert_sample_data::(data)), + SampleFormat::I32 => Ok(Self::convert_sample_data::(data)), + SampleFormat::I64 => Ok(Self::convert_sample_data::(data)), + SampleFormat::U8 => Ok(Self::convert_sample_data::(data)), + SampleFormat::U16 => Ok(Self::convert_sample_data::(data)), + SampleFormat::U32 => Ok(Self::convert_sample_data::(data)), + SampleFormat::U64 => Ok(Self::convert_sample_data::(data)), + SampleFormat::F32 => Ok(Self::convert_sample_data::(data)), + SampleFormat::F64 => Ok(Self::convert_sample_data::(data)), + _ => anyhow::bail!("Unsupported sample format"), + } + } + + fn convert_sample_data>( + data: &Data, + ) -> Vec { + data.as_slice::() + .unwrap() + .iter() + .map(|e| e.to_sample::()) + .collect() + } } use super::LocalVideoTrack; From 5940ed979fc593190e4b8fbb9b48660d61527874 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 5 Aug 2025 11:38:08 -0300 Subject: [PATCH 079/693] onboarding: Use a picker for the font dropdowns (#35638) Release Notes: - N/A --- Cargo.lock | 2 + crates/onboarding/Cargo.toml | 2 + crates/onboarding/src/editing_page.rs | 312 ++++++++++++++++---- crates/picker/src/popover_menu.rs | 1 + crates/ui/src/components/dropdown_menu.rs | 24 +- crates/ui/src/components/numeric_stepper.rs | 7 +- 6 files changed, 272 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a8e3e81434..4cf5a68f1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11039,12 +11039,14 @@ dependencies = [ "editor", "feature_flags", "fs", + "fuzzy", "gpui", "itertools 0.14.0", "language", "language_model", "menu", "notifications", + "picker", "project", "schemars", "serde", diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index b3056ff39e..7e76b150a9 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -25,12 +25,14 @@ db.workspace = true editor.workspace = true feature_flags.workspace = true fs.workspace = true +fuzzy.workspace = true gpui.workspace = true itertools.workspace = true language.workspace = true language_model.workspace = true menu.workspace = true notifications.workspace = true +picker.workspace = true project.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index a5e3a6bf05..6dd272745a 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -2,14 +2,19 @@ use std::sync::Arc; use editor::{EditorSettings, ShowMinimap}; use fs::Fs; -use gpui::{Action, App, FontFeatures, IntoElement, Pixels, Window}; +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{ + Action, AnyElement, App, Context, FontFeatures, IntoElement, Pixels, SharedString, Task, Window, +}; use language::language_settings::{AllLanguageSettings, FormatOnSave}; +use picker::{Picker, PickerDelegate}; use project::project_settings::ProjectSettings; use settings::{Settings as _, update_settings_file}; use theme::{FontFamilyCache, FontFamilyName, ThemeSettings}; use ui::{ - ButtonLike, ContextMenu, DropdownMenu, NumericStepper, SwitchField, ToggleButtonGroup, - ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, prelude::*, + ButtonLike, ListItem, ListItemSpacing, NumericStepper, PopoverMenu, SwitchField, + ToggleButtonGroup, ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, + prelude::*, }; use crate::{ImportCursorSettings, ImportVsCodeSettings, SettingsImportState}; @@ -246,9 +251,25 @@ fn render_import_settings_section(cx: &App) -> impl IntoElement { fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl IntoElement { let theme_settings = ThemeSettings::get_global(cx); let ui_font_size = theme_settings.ui_font_size(cx); - let font_family = theme_settings.buffer_font.family.clone(); + let ui_font_family = theme_settings.ui_font.family.clone(); + let buffer_font_family = theme_settings.buffer_font.family.clone(); let buffer_font_size = theme_settings.buffer_font_size(cx); + let ui_font_picker = + cx.new(|cx| font_picker(ui_font_family.clone(), write_ui_font_family, window, cx)); + + let buffer_font_picker = cx.new(|cx| { + font_picker( + buffer_font_family.clone(), + write_buffer_font_family, + window, + cx, + ) + }); + + let ui_font_handle = ui::PopoverMenuHandle::default(); + let buffer_font_handle = ui::PopoverMenuHandle::default(); + h_flex() .w_full() .gap_4() @@ -263,34 +284,35 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl .justify_between() .gap_2() .child( - DropdownMenu::new( - "ui-font-family", - theme_settings.ui_font.family.clone(), - ContextMenu::build(window, cx, |mut menu, _, cx| { - let font_family_cache = FontFamilyCache::global(cx); - - for font_name in font_family_cache.list_font_families(cx) { - menu = menu.custom_entry( - { - let font_name = font_name.clone(); - move |_window, _cx| { - Label::new(font_name.clone()).into_any_element() - } - }, - { - let font_name = font_name.clone(); - move |_window, cx| { - write_ui_font_family(font_name.clone(), cx); - } - }, - ) - } - - menu - }), - ) - .style(ui::DropdownStyle::Outlined) - .full_width(true), + PopoverMenu::new("ui-font-picker") + .menu({ + let ui_font_picker = ui_font_picker.clone(); + move |_window, _cx| Some(ui_font_picker.clone()) + }) + .trigger( + ButtonLike::new("ui-font-family-button") + .style(ButtonStyle::Outlined) + .size(ButtonSize::Medium) + .full_width() + .child( + h_flex() + .w_full() + .justify_between() + .child(Label::new(ui_font_family)) + .child( + Icon::new(IconName::ChevronUpDown) + .color(Color::Muted) + .size(IconSize::XSmall), + ), + ), + ) + .full_width(true) + .anchor(gpui::Corner::TopLeft) + .offset(gpui::Point { + x: px(0.0), + y: px(4.0), + }) + .with_handle(ui_font_handle), ) .child( NumericStepper::new( @@ -318,34 +340,35 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl .justify_between() .gap_2() .child( - DropdownMenu::new( - "buffer-font-family", - font_family, - ContextMenu::build(window, cx, |mut menu, _, cx| { - let font_family_cache = FontFamilyCache::global(cx); - - for font_name in font_family_cache.list_font_families(cx) { - menu = menu.custom_entry( - { - let font_name = font_name.clone(); - move |_window, _cx| { - Label::new(font_name.clone()).into_any_element() - } - }, - { - let font_name = font_name.clone(); - move |_window, cx| { - write_buffer_font_family(font_name.clone(), cx); - } - }, - ) - } - - menu - }), - ) - .style(ui::DropdownStyle::Outlined) - .full_width(true), + PopoverMenu::new("buffer-font-picker") + .menu({ + let buffer_font_picker = buffer_font_picker.clone(); + move |_window, _cx| Some(buffer_font_picker.clone()) + }) + .trigger( + ButtonLike::new("buffer-font-family-button") + .style(ButtonStyle::Outlined) + .size(ButtonSize::Medium) + .full_width() + .child( + h_flex() + .w_full() + .justify_between() + .child(Label::new(buffer_font_family)) + .child( + Icon::new(IconName::ChevronUpDown) + .color(Color::Muted) + .size(IconSize::XSmall), + ), + ), + ) + .full_width(true) + .anchor(gpui::Corner::TopLeft) + .offset(gpui::Point { + x: px(0.0), + y: px(4.0), + }) + .with_handle(buffer_font_handle), ) .child( NumericStepper::new( @@ -364,6 +387,175 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl ) } +type FontPicker = Picker; + +pub struct FontPickerDelegate { + fonts: Vec, + filtered_fonts: Vec, + selected_index: usize, + current_font: SharedString, + on_font_changed: Arc, +} + +impl FontPickerDelegate { + fn new( + current_font: SharedString, + on_font_changed: impl Fn(SharedString, &mut App) + 'static, + cx: &mut Context, + ) -> Self { + let font_family_cache = FontFamilyCache::global(cx); + + let fonts: Vec = font_family_cache + .list_font_families(cx) + .into_iter() + .collect(); + + let selected_index = fonts + .iter() + .position(|font| *font == current_font) + .unwrap_or(0); + + Self { + fonts: fonts.clone(), + filtered_fonts: fonts + .iter() + .enumerate() + .map(|(index, font)| StringMatch { + candidate_id: index, + string: font.to_string(), + positions: Vec::new(), + score: 0.0, + }) + .collect(), + selected_index, + current_font, + on_font_changed: Arc::new(on_font_changed), + } + } +} + +impl PickerDelegate for FontPickerDelegate { + type ListItem = AnyElement; + + fn match_count(&self) -> usize { + self.filtered_fonts.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context) { + self.selected_index = ix.min(self.filtered_fonts.len().saturating_sub(1)); + cx.notify(); + } + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { + "Search fonts…".into() + } + + fn update_matches( + &mut self, + query: String, + _window: &mut Window, + cx: &mut Context, + ) -> Task<()> { + let fonts = self.fonts.clone(); + let current_font = self.current_font.clone(); + + let matches: Vec = if query.is_empty() { + fonts + .iter() + .enumerate() + .map(|(index, font)| StringMatch { + candidate_id: index, + string: font.to_string(), + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + let _candidates: Vec = fonts + .iter() + .enumerate() + .map(|(id, font)| StringMatchCandidate::new(id, font.as_ref())) + .collect(); + + fonts + .iter() + .enumerate() + .filter(|(_, font)| font.to_lowercase().contains(&query.to_lowercase())) + .map(|(index, font)| StringMatch { + candidate_id: index, + string: font.to_string(), + positions: Vec::new(), + score: 0.0, + }) + .collect() + }; + + let selected_index = if query.is_empty() { + fonts + .iter() + .position(|font| *font == current_font) + .unwrap_or(0) + } else { + matches + .iter() + .position(|m| fonts[m.candidate_id] == current_font) + .unwrap_or(0) + }; + + self.filtered_fonts = matches; + self.selected_index = selected_index; + cx.notify(); + + Task::ready(()) + } + + fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context) { + if let Some(font_match) = self.filtered_fonts.get(self.selected_index) { + let font = font_match.string.clone(); + (self.on_font_changed)(font.into(), cx); + } + } + + fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context) {} + + fn render_match( + &self, + ix: usize, + selected: bool, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + let font_match = self.filtered_fonts.get(ix)?; + + Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .toggle_state(selected) + .child(Label::new(font_match.string.clone())) + .into_any_element(), + ) + } +} + +fn font_picker( + current_font: SharedString, + on_font_changed: impl Fn(SharedString, &mut App) + 'static, + window: &mut Window, + cx: &mut Context, +) -> FontPicker { + let delegate = FontPickerDelegate::new(current_font, on_font_changed, cx); + + Picker::list(delegate, window, cx) + .show_scrollbar(true) + .width(rems_from_px(210.)) + .max_height(Some(rems(20.).into())) +} + fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl IntoElement { const LIGATURE_TOOLTIP: &'static str = "Ligatures are when a font creates a special character out of combining two characters into one. For example, with ligatures turned on, =/= would become ≠."; diff --git a/crates/picker/src/popover_menu.rs b/crates/picker/src/popover_menu.rs index dd1d9c2865..d05308ee71 100644 --- a/crates/picker/src/popover_menu.rs +++ b/crates/picker/src/popover_menu.rs @@ -80,6 +80,7 @@ where { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { let picker = self.picker.clone(); + PopoverMenu::new("popover-menu") .menu(move |_window, _cx| Some(picker.clone())) .trigger_with_tooltip(self.trigger, self.tooltip) diff --git a/crates/ui/src/components/dropdown_menu.rs b/crates/ui/src/components/dropdown_menu.rs index cdb98086ca..7ad9400f0d 100644 --- a/crates/ui/src/components/dropdown_menu.rs +++ b/crates/ui/src/components/dropdown_menu.rs @@ -276,25 +276,25 @@ impl RenderOnce for DropdownMenuTrigger { .gap_2() .justify_between() .rounded_sm() - .bg(style.bg) - .hover(|s| s.bg(cx.theme().colors().element_hover)) + .map(|this| { + if self.full_width { + this.w_full() + } else { + this.flex_none().w_auto() + } + }) .when(is_outlined, |this| { this.border_1() .border_color(cx.theme().colors().border) .overflow_hidden() }) - .map(|el| { - if self.full_width { - el.w_full() - } else { - el.flex_none().w_auto() - } - }) - .map(|el| { + .map(|this| { if disabled { - el.cursor_not_allowed() + this.cursor_not_allowed() + .bg(cx.theme().colors().element_disabled) } else { - el.cursor_pointer() + this.bg(style.bg) + .hover(|s| s.bg(cx.theme().colors().element_hover)) } }) .child(match self.label { diff --git a/crates/ui/src/components/numeric_stepper.rs b/crates/ui/src/components/numeric_stepper.rs index 5a84633d1b..0ec7111a02 100644 --- a/crates/ui/src/components/numeric_stepper.rs +++ b/crates/ui/src/components/numeric_stepper.rs @@ -96,7 +96,7 @@ impl RenderOnce for NumericStepper { this.overflow_hidden() .bg(cx.theme().colors().surface_background) .border_1() - .border_color(cx.theme().colors().border) + .border_color(cx.theme().colors().border_variant) } else { this.px_1().bg(cx.theme().colors().editor_background) } @@ -111,7 +111,7 @@ impl RenderOnce for NumericStepper { .justify_center() .hover(|s| s.bg(cx.theme().colors().element_hover)) .border_r_1() - .border_color(cx.theme().colors().border) + .border_color(cx.theme().colors().border_variant) .child(Icon::new(IconName::Dash).size(IconSize::Small)) .on_click(self.on_decrement), ) @@ -124,7 +124,6 @@ impl RenderOnce for NumericStepper { ) } }) - .when(is_outlined, |this| this) .child(Label::new(self.value).mx_3()) .map(|increment| { if is_outlined { @@ -136,7 +135,7 @@ impl RenderOnce for NumericStepper { .justify_center() .hover(|s| s.bg(cx.theme().colors().element_hover)) .border_l_1() - .border_color(cx.theme().colors().border) + .border_color(cx.theme().colors().border_variant) .child(Icon::new(IconName::Plus).size(IconSize::Small)) .on_click(self.on_increment), ) From 19c1504c8ffa460693ee9a0d53292f860255b8d8 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Tue, 5 Aug 2025 19:05:18 +0200 Subject: [PATCH 080/693] ui: Wire up tab indices within buttons (#35368) This change adds the current tab index functionality to buttons and implements a proof of concept for the new welcome page. Primarily blocked on https://github.com/zed-industries/zed/pull/34804, secondarily on https://github.com/zed-industries/zed/pull/35075 so we can ensure navigation always works as intended. Another thing to consider here is whether we want to assign the tab order more implicitly / "automatically" based on the current layout ordering. This would generally enable us to add a default order to focusable elements if we want this. See [the specification](https://html.spec.whatwg.org/multipage/interaction.html#flattened-tabindex-ordered-focus-navigation-scope) on some more context on how the web usually handles this for focusable elements. Release Notes: - N/A --- crates/onboarding/Cargo.toml | 4 ++-- crates/onboarding/src/welcome.rs | 22 +++++++++++++++++-- crates/ui/src/components/button/button.rs | 5 +++++ .../ui/src/components/button/button_like.rs | 18 +++++++++++++-- .../ui/src/components/button/icon_button.rs | 5 +++++ .../ui/src/components/button/toggle_button.rs | 5 +++++ 6 files changed, 53 insertions(+), 6 deletions(-) diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index 7e76b150a9..436c714cf3 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -15,13 +15,13 @@ path = "src/onboarding.rs" default = [] [dependencies] -anyhow.workspace = true ai_onboarding.workspace = true +anyhow.workspace = true client.workspace = true command_palette_hooks.workspace = true component.workspace = true -documented.workspace = true db.workspace = true +documented.workspace = true editor.workspace = true feature_flags.workspace = true fs.workspace = true diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index 213032f1b3..d4d6c3f701 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -2,6 +2,7 @@ use gpui::{ Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, NoAction, ParentElement, Render, Styled, Window, actions, }; +use menu::{SelectNext, SelectPrevious}; use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*}; use workspace::{ NewFile, Open, WorkspaceId, @@ -124,6 +125,7 @@ impl SectionEntry { cx: &App, ) -> impl IntoElement { ButtonLike::new(("onboarding-button-id", button_index)) + .tab_index(button_index as isize) .full_width() .size(ButtonSize::Medium) .child( @@ -153,10 +155,23 @@ pub struct WelcomePage { focus_handle: FocusHandle, } +impl WelcomePage { + fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context) { + window.focus_next(); + cx.notify(); + } + + fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context) { + window.focus_prev(); + cx.notify(); + } +} + impl Render for WelcomePage { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let (first_section, second_entries) = CONTENT; + let (first_section, second_section) = CONTENT; let first_section_entries = first_section.entries.len(); + let last_index = first_section_entries + second_section.entries.len(); h_flex() .size_full() @@ -165,6 +180,8 @@ impl Render for WelcomePage { .bg(cx.theme().colors().editor_background) .key_context("Welcome") .track_focus(&self.focus_handle(cx)) + .on_action(cx.listener(Self::select_previous)) + .on_action(cx.listener(Self::select_next)) .child( h_flex() .px_12() @@ -202,7 +219,7 @@ impl Render for WelcomePage { window, cx, )) - .child(second_entries.render( + .child(second_section.render( first_section_entries, &self.focus_handle, window, @@ -220,6 +237,7 @@ impl Render for WelcomePage { .border_dashed() .child( Button::new("welcome-exit", "Return to Setup") + .tab_index(last_index as isize) .full_width() .label_size(LabelSize::XSmall) .on_click(|_, window, cx| { diff --git a/crates/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs index cae5d0e2ca..19f782fb98 100644 --- a/crates/ui/src/components/button/button.rs +++ b/crates/ui/src/components/button/button.rs @@ -393,6 +393,11 @@ impl ButtonCommon for Button { self } + fn tab_index(mut self, tab_index: impl Into) -> Self { + self.base = self.base.tab_index(tab_index); + self + } + fn layer(mut self, elevation: ElevationIndex) -> Self { self.base = self.base.layer(elevation); self diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index 03f7964f35..15ab00e7e5 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -1,7 +1,7 @@ use documented::Documented; use gpui::{ AnyElement, AnyView, ClickEvent, CursorStyle, DefiniteLength, Hsla, MouseButton, - MouseDownEvent, MouseUpEvent, Rems, relative, transparent_black, + MouseDownEvent, MouseUpEvent, Rems, StyleRefinement, relative, transparent_black, }; use smallvec::SmallVec; @@ -37,6 +37,8 @@ pub trait ButtonCommon: Clickable + Disableable { /// exceptions might a scroll bar, or a slider. fn tooltip(self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self; + fn tab_index(self, tab_index: impl Into) -> Self; + fn layer(self, elevation: ElevationIndex) -> Self; } @@ -393,6 +395,7 @@ pub struct ButtonLike { pub(super) width: Option, pub(super) height: Option, pub(super) layer: Option, + tab_index: Option, size: ButtonSize, rounding: Option, tooltip: Option AnyView>>, @@ -421,6 +424,7 @@ impl ButtonLike { on_click: None, on_right_click: None, layer: None, + tab_index: None, } } @@ -525,6 +529,11 @@ impl ButtonCommon for ButtonLike { self } + fn tab_index(mut self, tab_index: impl Into) -> Self { + self.tab_index = Some(tab_index.into()); + self + } + fn layer(mut self, elevation: ElevationIndex) -> Self { self.layer = Some(elevation); self @@ -554,6 +563,7 @@ impl RenderOnce for ButtonLike { self.base .h_flex() .id(self.id.clone()) + .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)) .font_ui(cx) .group("") .flex_none() @@ -591,8 +601,12 @@ impl RenderOnce for ButtonLike { } }) .when(!self.disabled, |this| { + let hovered_style = style.hovered(self.layer, cx); + let focus_color = + |refinement: StyleRefinement| refinement.bg(hovered_style.background); this.cursor(self.cursor_style) - .hover(|hover| hover.bg(style.hovered(self.layer, cx).background)) + .hover(focus_color) + .focus(focus_color) .active(|active| active.bg(style.active(cx).background)) }) .when_some( diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index e5d13e09cd..8d8718a634 100644 --- a/crates/ui/src/components/button/icon_button.rs +++ b/crates/ui/src/components/button/icon_button.rs @@ -164,6 +164,11 @@ impl ButtonCommon for IconButton { self } + fn tab_index(mut self, tab_index: impl Into) -> Self { + self.base = self.base.tab_index(tab_index); + self + } + fn layer(mut self, elevation: ElevationIndex) -> Self { self.base = self.base.layer(elevation); self diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs index a1e4d65a24..d4d47da9b6 100644 --- a/crates/ui/src/components/button/toggle_button.rs +++ b/crates/ui/src/components/button/toggle_button.rs @@ -121,6 +121,11 @@ impl ButtonCommon for ToggleButton { self } + fn tab_index(mut self, tab_index: impl Into) -> Self { + self.base = self.base.tab_index(tab_index); + self + } + fn layer(mut self, elevation: ElevationIndex) -> Self { self.base = self.base.layer(elevation); self From f017ffdffcdfe7b2947f5045092d84ffe5469f9f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Aug 2025 10:07:30 -0700 Subject: [PATCH 081/693] Fix minidump endpoint configuration (#35646) Release Notes: - N/A --- .github/workflows/ci.yml | 2 +- .github/workflows/nix.yml | 2 +- .github/workflows/release_nightly.yml | 2 +- crates/client/src/telemetry.rs | 6 +++--- crates/zed/src/reliability.rs | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c08f4ac211..f83a3715a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ env: DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_SENTRY_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} + ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} jobs: job_spec: diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index c019f805fe..6c3a97c163 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -29,7 +29,7 @@ jobs: runs-on: ${{ matrix.system.runner }} env: ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_SENTRY_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} + ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }} GIT_LFS_SKIP_SMUDGE: 1 # breaks the livekit rust sdk examples which we don't actually depend on steps: diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 69e5f86cb6..d62aa78293 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -13,7 +13,7 @@ env: CARGO_INCREMENTAL: 0 RUST_BACKTRACE: 1 ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_SENTRY_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} + ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 4a8e745fcb..43a1a0b7a4 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -74,10 +74,10 @@ static ZED_CLIENT_CHECKSUM_SEED: LazyLock>> = LazyLock::new(|| { }) }); -pub static SENTRY_MINIDUMP_ENDPOINT: LazyLock> = LazyLock::new(|| { - option_env!("SENTRY_MINIDUMP_ENDPOINT") +pub static MINIDUMP_ENDPOINT: LazyLock> = LazyLock::new(|| { + option_env!("ZED_MINIDUMP_ENDPOINT") .map(|s| s.to_owned()) - .or_else(|| env::var("SENTRY_MINIDUMP_ENDPOINT").ok()) + .or_else(|| env::var("ZED_MINIDUMP_ENDPOINT").ok()) }); static DOTNET_PROJECT_FILES_REGEX: LazyLock = LazyLock::new(|| { diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index 9157f66216..ed149a470a 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -4,7 +4,7 @@ use backtrace::{self, Backtrace}; use chrono::Utc; use client::{ TelemetrySettings, - telemetry::{self, SENTRY_MINIDUMP_ENDPOINT}, + telemetry::{self, MINIDUMP_ENDPOINT}, }; use db::kvp::KEY_VALUE_STORE; use futures::AsyncReadExt; @@ -584,7 +584,7 @@ async fn upload_minidump( minidump: Vec, panic: Option<&Panic>, ) -> Result<()> { - let sentry_upload_url = SENTRY_MINIDUMP_ENDPOINT + let minidump_endpoint = MINIDUMP_ENDPOINT .to_owned() .ok_or_else(|| anyhow::anyhow!("Minidump endpoint not set"))?; @@ -605,7 +605,7 @@ async fn upload_minidump( } let mut response_text = String::new(); - let mut response = http.send_multipart_form(&sentry_upload_url, form).await?; + let mut response = http.send_multipart_form(&minidump_endpoint, form).await?; response .body_mut() .read_to_string(&mut response_text) From 844ea3d1ab4ce74577d5a767bbc2ef76974391f9 Mon Sep 17 00:00:00 2001 From: localcc Date: Tue, 5 Aug 2025 19:09:04 +0200 Subject: [PATCH 082/693] Fix open with zed not focusing window (#35645) --- crates/gpui/src/platform/windows/window.rs | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index 4043001a35..32a6da2391 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -677,6 +677,36 @@ impl PlatformWindow for WindowsWindow { this.set_window_placement().log_err(); unsafe { SetActiveWindow(hwnd).log_err() }; unsafe { SetFocus(Some(hwnd)).log_err() }; + + // premium ragebait by windows, this is needed because the window + // must have received an input event to be able to set itself to foreground + // so let's just simulate user input as that seems to be the most reliable way + // some more info: https://gist.github.com/Aetopia/1581b40f00cc0cadc93a0e8ccb65dc8c + // bonus: this bug also doesn't manifest if you have vs attached to the process + let inputs = [ + INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: INPUT_0 { + ki: KEYBDINPUT { + wVk: VK_MENU, + dwFlags: KEYBD_EVENT_FLAGS(0), + ..Default::default() + }, + }, + }, + INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: INPUT_0 { + ki: KEYBDINPUT { + wVk: VK_MENU, + dwFlags: KEYEVENTF_KEYUP, + ..Default::default() + }, + }, + }, + ]; + unsafe { SendInput(&inputs, std::mem::size_of::() as i32) }; + // todo(windows) // crate `windows 0.56` reports true as Err unsafe { SetForegroundWindow(hwnd).as_bool() }; From fc2ba82eb61593b1a2b4d047149b9e591b9f1a4e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:09:42 +0200 Subject: [PATCH 083/693] debugpy: Fetch a wheel into Zed's work dir and use that with users venv (#35640) Another stab at #35388 cc @Sansui233 Closes #35388 Release Notes: - debugger: Fixed Python debug sessions failing to launch due to a missing debugpy installation. --- crates/dap_adapters/src/python.rs | 331 ++++++++++++++++-------------- 1 file changed, 175 insertions(+), 156 deletions(-) diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index f499244966..461ce6fbb3 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -1,38 +1,36 @@ use crate::*; use anyhow::Context as _; use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; +use fs::RemoveOptions; +use futures::{StreamExt, TryStreamExt}; +use gpui::http_client::AsyncBody; use gpui::{AsyncApp, SharedString}; use json_dotpath::DotPaths; use language::LanguageName; use paths::debug_adapters_dir; use serde_json::Value; +use smol::fs::File; +use smol::io::AsyncReadExt; use smol::lock::OnceCell; +use std::ffi::OsString; use std::net::Ipv4Addr; +use std::str::FromStr; use std::{ collections::HashMap, ffi::OsStr, path::{Path, PathBuf}, }; +use util::{ResultExt, maybe}; #[derive(Default)] pub(crate) struct PythonDebugAdapter { - python_venv_base: OnceCell, String>>, + debugpy_whl_base_path: OnceCell, String>>, } impl PythonDebugAdapter { const ADAPTER_NAME: &'static str = "Debugpy"; const DEBUG_ADAPTER_NAME: DebugAdapterName = DebugAdapterName(SharedString::new_static(Self::ADAPTER_NAME)); - const PYTHON_ADAPTER_IN_VENV: &'static str = if cfg!(target_os = "windows") { - "Scripts/python3" - } else { - "bin/python3" - }; - const ADAPTER_PATH: &'static str = if cfg!(target_os = "windows") { - "debugpy-venv/Scripts/debugpy-adapter" - } else { - "debugpy-venv/bin/debugpy-adapter" - }; const LANGUAGE_NAME: &'static str = "Python"; @@ -41,7 +39,6 @@ impl PythonDebugAdapter { port: u16, user_installed_path: Option<&Path>, user_args: Option>, - installed_in_venv: bool, ) -> Result> { let mut args = if let Some(user_installed_path) = user_installed_path { log::debug!( @@ -49,13 +46,11 @@ impl PythonDebugAdapter { user_installed_path.display() ); vec![user_installed_path.to_string_lossy().to_string()] - } else if installed_in_venv { - log::debug!("Using venv-installed debugpy"); - vec!["-m".to_string(), "debugpy.adapter".to_string()] } else { let adapter_path = paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref()); let path = adapter_path - .join(Self::ADAPTER_PATH) + .join("debugpy") + .join("adapter") .to_string_lossy() .into_owned(); log::debug!("Using pip debugpy adapter from: {path}"); @@ -96,63 +91,23 @@ impl PythonDebugAdapter { }) } - async fn ensure_venv(delegate: &dyn DapDelegate) -> Result> { - let python_path = Self::find_base_python(delegate) + async fn fetch_wheel(delegate: &Arc) -> Result, String> { + let system_python = Self::system_python_name(delegate) .await - .context("Could not find Python installation for DebugPy")?; - let work_dir = debug_adapters_dir().join(Self::ADAPTER_NAME); - if !work_dir.exists() { - std::fs::create_dir_all(&work_dir)?; - } - let mut path = work_dir.clone(); - path.push("debugpy-venv"); - if !path.exists() { - util::command::new_smol_command(python_path) - .arg("-m") - .arg("venv") - .arg("debugpy-venv") - .current_dir(work_dir) - .spawn()? - .output() - .await?; - } - - Ok(path.into()) - } - - // Find "baseline", user python version from which we'll create our own venv. - async fn find_base_python(delegate: &dyn DapDelegate) -> Option { - for path in ["python3", "python"] { - if let Some(path) = delegate.which(path.as_ref()).await { - return Some(path); - } - } - None - } - const BINARY_DIR: &str = if cfg!(target_os = "windows") { - "Scripts" - } else { - "bin" - }; - async fn base_venv(&self, delegate: &dyn DapDelegate) -> Result, String> { - self.python_venv_base - .get_or_init(move || async move { - let venv_base = Self::ensure_venv(delegate) - .await - .map_err(|e| format!("{e}"))?; - Self::install_debugpy_into_venv(&venv_base).await?; - Ok(venv_base) - }) - .await - .clone() - } - - async fn install_debugpy_into_venv(venv_path: &Path) -> Result<(), String> { - let pip_path = venv_path.join(Self::BINARY_DIR).join("pip3"); - let installation_succeeded = util::command::new_smol_command(pip_path.as_path()) - .arg("install") - .arg("debugpy") - .arg("-U") + .ok_or_else(|| String::from("Could not find a Python installation"))?; + let command: &OsStr = system_python.as_ref(); + let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME).join("wheels"); + std::fs::create_dir_all(&download_dir).map_err(|e| e.to_string())?; + let installation_succeeded = util::command::new_smol_command(command) + .args([ + "-m", + "pip", + "download", + "debugpy", + "--only-binary=:all:", + "-d", + download_dir.to_string_lossy().as_ref(), + ]) .output() .await .map_err(|e| format!("{e}"))? @@ -162,7 +117,117 @@ impl PythonDebugAdapter { return Err("debugpy installation failed".into()); } - Ok(()) + let wheel_path = std::fs::read_dir(&download_dir) + .map_err(|e| e.to_string())? + .find_map(|entry| { + entry.ok().filter(|e| { + e.file_type().is_ok_and(|typ| typ.is_file()) + && Path::new(&e.file_name()).extension() == Some("whl".as_ref()) + }) + }) + .ok_or_else(|| String::from("Did not find a .whl in {download_dir}"))?; + + util::archive::extract_zip( + &debug_adapters_dir().join(Self::ADAPTER_NAME), + File::open(&wheel_path.path()) + .await + .map_err(|e| e.to_string())?, + ) + .await + .map_err(|e| e.to_string())?; + + Ok(Arc::from(wheel_path.path())) + } + + async fn maybe_fetch_new_wheel(delegate: &Arc) { + let latest_release = delegate + .http_client() + .get( + "https://pypi.org/pypi/debugpy/json", + AsyncBody::empty(), + false, + ) + .await + .log_err(); + maybe!(async move { + let response = latest_release.filter(|response| response.status().is_success())?; + + let mut output = String::new(); + response + .into_body() + .read_to_string(&mut output) + .await + .ok()?; + let as_json = serde_json::Value::from_str(&output).ok()?; + let latest_version = as_json.get("info").and_then(|info| { + info.get("version") + .and_then(|version| version.as_str()) + .map(ToOwned::to_owned) + })?; + let dist_info_dirname: OsString = format!("debugpy-{latest_version}.dist-info").into(); + let is_up_to_date = delegate + .fs() + .read_dir(&debug_adapters_dir().join(Self::ADAPTER_NAME)) + .await + .ok()? + .into_stream() + .any(async |entry| { + entry.is_ok_and(|e| e.file_name().is_some_and(|name| name == dist_info_dirname)) + }) + .await; + + if !is_up_to_date { + delegate + .fs() + .remove_dir( + &debug_adapters_dir().join(Self::ADAPTER_NAME), + RemoveOptions { + recursive: true, + ignore_if_not_exists: true, + }, + ) + .await + .ok()?; + Self::fetch_wheel(delegate).await.ok()?; + } + Some(()) + }) + .await; + } + + async fn fetch_debugpy_whl( + &self, + delegate: &Arc, + ) -> Result, String> { + self.debugpy_whl_base_path + .get_or_init(|| async move { + Self::maybe_fetch_new_wheel(delegate).await; + Ok(Arc::from( + debug_adapters_dir() + .join(Self::ADAPTER_NAME) + .join("debugpy") + .join("adapter") + .as_ref(), + )) + }) + .await + .clone() + } + + async fn system_python_name(delegate: &Arc) -> Option { + const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"]; + let mut name = None; + + for cmd in BINARY_NAMES { + name = delegate + .which(OsStr::new(cmd)) + .await + .map(|path| path.to_string_lossy().to_string()); + if name.is_some() { + break; + } + } + name } async fn get_installed_binary( @@ -172,27 +237,14 @@ impl PythonDebugAdapter { user_installed_path: Option, user_args: Option>, python_from_toolchain: Option, - installed_in_venv: bool, ) -> Result { - const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"]; let tcp_connection = config.tcp_connection.clone().unwrap_or_default(); let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?; let python_path = if let Some(toolchain) = python_from_toolchain { Some(toolchain) } else { - let mut name = None; - - for cmd in BINARY_NAMES { - name = delegate - .which(OsStr::new(cmd)) - .await - .map(|path| path.to_string_lossy().to_string()); - if name.is_some() { - break; - } - } - name + Self::system_python_name(delegate).await }; let python_command = python_path.context("failed to find binary path for Python")?; @@ -203,7 +255,6 @@ impl PythonDebugAdapter { port, user_installed_path.as_deref(), user_args, - installed_in_venv, ) .await?; @@ -625,14 +676,7 @@ impl DebugAdapter for PythonDebugAdapter { local_path.display() ); return self - .get_installed_binary( - delegate, - &config, - Some(local_path.clone()), - user_args, - None, - false, - ) + .get_installed_binary(delegate, &config, Some(local_path.clone()), user_args, None) .await; } @@ -657,46 +701,28 @@ impl DebugAdapter for PythonDebugAdapter { ) .await; - if let Some(toolchain) = &toolchain { - if let Some(path) = Path::new(&toolchain.path.to_string()).parent() { - if let Some(parent) = path.parent() { - Self::install_debugpy_into_venv(parent).await.ok(); - } - - let debugpy_path = path.join("debugpy"); - if delegate.fs().is_file(&debugpy_path).await { - log::debug!( - "Found debugpy in toolchain environment: {}", - debugpy_path.display() - ); - return self - .get_installed_binary( - delegate, - &config, - None, - user_args, - Some(toolchain.path.to_string()), - true, - ) - .await; - } - } - } - let toolchain = self - .base_venv(&**delegate) + let debugpy_path = self + .fetch_debugpy_whl(delegate) .await - .map_err(|e| anyhow::anyhow!(e))? - .join(Self::PYTHON_ADAPTER_IN_VENV); + .map_err(|e| anyhow::anyhow!("{e}"))?; + if let Some(toolchain) = &toolchain { + log::debug!( + "Found debugpy in toolchain environment: {}", + debugpy_path.display() + ); + return self + .get_installed_binary( + delegate, + &config, + None, + user_args, + Some(toolchain.path.to_string()), + ) + .await; + } - self.get_installed_binary( - delegate, - &config, - None, - user_args, - Some(toolchain.to_string_lossy().into_owned()), - false, - ) - .await + self.get_installed_binary(delegate, &config, None, user_args, None) + .await } fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option { @@ -711,6 +737,8 @@ impl DebugAdapter for PythonDebugAdapter { #[cfg(test)] mod tests { + use util::path; + use super::*; use std::{net::Ipv4Addr, path::PathBuf}; @@ -721,30 +749,24 @@ mod tests { // Case 1: User-defined debugpy path (highest precedence) let user_path = PathBuf::from("/custom/path/to/debugpy/src/debugpy/adapter"); - let user_args = PythonDebugAdapter::generate_debugpy_arguments( - &host, - port, - Some(&user_path), - None, - false, - ) - .await - .unwrap(); - - // Case 2: Venv-installed debugpy (uses -m debugpy.adapter) - let venv_args = - PythonDebugAdapter::generate_debugpy_arguments(&host, port, None, None, true) + let user_args = + PythonDebugAdapter::generate_debugpy_arguments(&host, port, Some(&user_path), None) .await .unwrap(); + // Case 2: Venv-installed debugpy (uses -m debugpy.adapter) + let venv_args = PythonDebugAdapter::generate_debugpy_arguments(&host, port, None, None) + .await + .unwrap(); + assert_eq!(user_args[0], "/custom/path/to/debugpy/src/debugpy/adapter"); assert_eq!(user_args[1], "--host=127.0.0.1"); assert_eq!(user_args[2], "--port=5678"); - assert_eq!(venv_args[0], "-m"); - assert_eq!(venv_args[1], "debugpy.adapter"); - assert_eq!(venv_args[2], "--host=127.0.0.1"); - assert_eq!(venv_args[3], "--port=5678"); + let expected_suffix = path!("debug_adapters/Debugpy/debugpy/adapter"); + assert!(venv_args[0].ends_with(expected_suffix)); + assert_eq!(venv_args[1], "--host=127.0.0.1"); + assert_eq!(venv_args[2], "--port=5678"); // The same cases, with arguments overridden by the user let user_args = PythonDebugAdapter::generate_debugpy_arguments( @@ -752,7 +774,6 @@ mod tests { port, Some(&user_path), Some(vec!["foo".into()]), - false, ) .await .unwrap(); @@ -761,7 +782,6 @@ mod tests { port, None, Some(vec!["foo".into()]), - true, ) .await .unwrap(); @@ -769,9 +789,8 @@ mod tests { assert!(user_args[0].ends_with("src/debugpy/adapter")); assert_eq!(user_args[1], "foo"); - assert_eq!(venv_args[0], "-m"); - assert_eq!(venv_args[1], "debugpy.adapter"); - assert_eq!(venv_args[2], "foo"); + assert!(venv_args[0].ends_with(expected_suffix)); + assert_eq!(venv_args[1], "foo"); // Note: Case 3 (GitHub-downloaded debugpy) is not tested since this requires mocking the Github API. } From 307d709adb4801347c6d3e4347a1b1ffc3743540 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 5 Aug 2025 14:09:21 -0400 Subject: [PATCH 084/693] ci: Double Buildjet ARM runner size (24GB to 48GB ram) (#35654) Release Notes: - N/A --- .github/actionlint.yml | 1 - .github/workflows/ci.yml | 2 +- .github/workflows/release_nightly.yml | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/actionlint.yml b/.github/actionlint.yml index d93ec5b15e..6bfbc27705 100644 --- a/.github/actionlint.yml +++ b/.github/actionlint.yml @@ -24,7 +24,6 @@ self-hosted-runner: - buildjet-8vcpu-ubuntu-2204-arm - buildjet-16vcpu-ubuntu-2204-arm - buildjet-32vcpu-ubuntu-2204-arm - - buildjet-64vcpu-ubuntu-2204-arm # Self Hosted Runners - self-mini-macos - self-32vcpu-windows-2022 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f83a3715a8..43d305faae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -650,7 +650,7 @@ jobs: timeout-minutes: 60 name: Linux arm64 release bundle runs-on: - - buildjet-16vcpu-ubuntu-2204-arm + - buildjet-32vcpu-ubuntu-2204-arm if: | startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index d62aa78293..c847149984 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -168,7 +168,7 @@ jobs: name: Create a Linux *.tar.gz bundle for ARM if: github.repository_owner == 'zed-industries' runs-on: - - buildjet-16vcpu-ubuntu-2204-arm + - buildjet-32vcpu-ubuntu-2204-arm needs: tests steps: - name: Checkout repo From 0b5592d788f6e8dcc3b13016a27295f0a9a85b1a Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Tue, 5 Aug 2025 14:16:47 -0400 Subject: [PATCH 085/693] Add Claude Opus 4.1 (#35653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Screenshot 2025-08-05 at 1 55 35 PM Release Notes: - Added support for Claude Opus 4.1 Co-authored-by: Marshall Bowers --- crates/anthropic/src/anthropic.rs | 30 +++++++++++++++++++++ crates/bedrock/src/models.rs | 44 ++++++++++++++++++++++++++----- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/crates/anthropic/src/anthropic.rs b/crates/anthropic/src/anthropic.rs index c73f606045..3ff1666755 100644 --- a/crates/anthropic/src/anthropic.rs +++ b/crates/anthropic/src/anthropic.rs @@ -36,11 +36,18 @@ pub enum AnthropicModelMode { pub enum Model { #[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")] ClaudeOpus4, + #[serde(rename = "claude-opus-4-1", alias = "claude-opus-4-1-latest")] + ClaudeOpus4_1, #[serde( rename = "claude-opus-4-thinking", alias = "claude-opus-4-thinking-latest" )] ClaudeOpus4Thinking, + #[serde( + rename = "claude-opus-4-1-thinking", + alias = "claude-opus-4-1-thinking-latest" + )] + ClaudeOpus4_1Thinking, #[default] #[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")] ClaudeSonnet4, @@ -91,10 +98,18 @@ impl Model { } pub fn from_id(id: &str) -> Result { + if id.starts_with("claude-opus-4-1-thinking") { + return Ok(Self::ClaudeOpus4_1Thinking); + } + if id.starts_with("claude-opus-4-thinking") { return Ok(Self::ClaudeOpus4Thinking); } + if id.starts_with("claude-opus-4-1") { + return Ok(Self::ClaudeOpus4_1); + } + if id.starts_with("claude-opus-4") { return Ok(Self::ClaudeOpus4); } @@ -141,7 +156,9 @@ impl Model { pub fn id(&self) -> &str { match self { Self::ClaudeOpus4 => "claude-opus-4-latest", + Self::ClaudeOpus4_1 => "claude-opus-4-1-latest", Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest", + Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking-latest", Self::ClaudeSonnet4 => "claude-sonnet-4-latest", Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest", Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest", @@ -159,6 +176,7 @@ impl Model { pub fn request_id(&self) -> &str { match self { Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514", + Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-20250805", Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514", Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest", Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest", @@ -173,7 +191,9 @@ impl Model { pub fn display_name(&self) -> &str { match self { Self::ClaudeOpus4 => "Claude Opus 4", + Self::ClaudeOpus4_1 => "Claude Opus 4.1", Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking", + Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking", Self::ClaudeSonnet4 => "Claude Sonnet 4", Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking", Self::Claude3_7Sonnet => "Claude 3.7 Sonnet", @@ -192,7 +212,9 @@ impl Model { pub fn cache_configuration(&self) -> Option { match self { Self::ClaudeOpus4 + | Self::ClaudeOpus4_1 | Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1Thinking | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking | Self::Claude3_5Sonnet @@ -215,7 +237,9 @@ impl Model { pub fn max_token_count(&self) -> u64 { match self { Self::ClaudeOpus4 + | Self::ClaudeOpus4_1 | Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1Thinking | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking | Self::Claude3_5Sonnet @@ -232,7 +256,9 @@ impl Model { pub fn max_output_tokens(&self) -> u64 { match self { Self::ClaudeOpus4 + | Self::ClaudeOpus4_1 | Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1Thinking | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking | Self::Claude3_5Sonnet @@ -249,7 +275,9 @@ impl Model { pub fn default_temperature(&self) -> f32 { match self { Self::ClaudeOpus4 + | Self::ClaudeOpus4_1 | Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1Thinking | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking | Self::Claude3_5Sonnet @@ -269,6 +297,7 @@ impl Model { pub fn mode(&self) -> AnthropicModelMode { match self { Self::ClaudeOpus4 + | Self::ClaudeOpus4_1 | Self::ClaudeSonnet4 | Self::Claude3_5Sonnet | Self::Claude3_7Sonnet @@ -277,6 +306,7 @@ impl Model { | Self::Claude3Sonnet | Self::Claude3Haiku => AnthropicModelMode::Default, Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1Thinking | Self::ClaudeSonnet4Thinking | Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking { budget_tokens: Some(4_096), diff --git a/crates/bedrock/src/models.rs b/crates/bedrock/src/models.rs index b6eeafa2d6..69d2ffb845 100644 --- a/crates/bedrock/src/models.rs +++ b/crates/bedrock/src/models.rs @@ -32,11 +32,18 @@ pub enum Model { ClaudeSonnet4Thinking, #[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")] ClaudeOpus4, + #[serde(rename = "claude-opus-4-1", alias = "claude-opus-4-1-latest")] + ClaudeOpus4_1, #[serde( rename = "claude-opus-4-thinking", alias = "claude-opus-4-thinking-latest" )] ClaudeOpus4Thinking, + #[serde( + rename = "claude-opus-4-1-thinking", + alias = "claude-opus-4-1-thinking-latest" + )] + ClaudeOpus4_1Thinking, #[serde(rename = "claude-3-5-sonnet-v2", alias = "claude-3-5-sonnet-latest")] Claude3_5SonnetV2, #[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")] @@ -147,7 +154,9 @@ impl Model { Model::ClaudeSonnet4 => "claude-4-sonnet", Model::ClaudeSonnet4Thinking => "claude-4-sonnet-thinking", Model::ClaudeOpus4 => "claude-4-opus", + Model::ClaudeOpus4_1 => "claude-4-opus-1", Model::ClaudeOpus4Thinking => "claude-4-opus-thinking", + Model::ClaudeOpus4_1Thinking => "claude-4-opus-1-thinking", Model::Claude3_5SonnetV2 => "claude-3-5-sonnet-v2", Model::Claude3_5Sonnet => "claude-3-5-sonnet", Model::Claude3Opus => "claude-3-opus", @@ -208,6 +217,9 @@ impl Model { Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => { "anthropic.claude-opus-4-20250514-v1:0" } + Model::ClaudeOpus4_1 | Model::ClaudeOpus4_1Thinking => { + "anthropic.claude-opus-4-1-20250805-v1:0" + } Model::Claude3_5SonnetV2 => "anthropic.claude-3-5-sonnet-20241022-v2:0", Model::Claude3_5Sonnet => "anthropic.claude-3-5-sonnet-20240620-v1:0", Model::Claude3Opus => "anthropic.claude-3-opus-20240229-v1:0", @@ -266,7 +278,9 @@ impl Model { Self::ClaudeSonnet4 => "Claude Sonnet 4", Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking", Self::ClaudeOpus4 => "Claude Opus 4", + Self::ClaudeOpus4_1 => "Claude Opus 4.1", Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking", + Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking", Self::Claude3_5SonnetV2 => "Claude 3.5 Sonnet v2", Self::Claude3_5Sonnet => "Claude 3.5 Sonnet", Self::Claude3Opus => "Claude 3 Opus", @@ -330,8 +344,10 @@ impl Model { | Self::Claude3_7Sonnet | Self::ClaudeSonnet4 | Self::ClaudeOpus4 + | Self::ClaudeOpus4_1 | Self::ClaudeSonnet4Thinking - | Self::ClaudeOpus4Thinking => 200_000, + | Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1Thinking => 200_000, Self::AmazonNovaPremier => 1_000_000, Self::PalmyraWriterX5 => 1_000_000, Self::PalmyraWriterX4 => 128_000, @@ -348,7 +364,9 @@ impl Model { | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking | Self::ClaudeOpus4 - | Model::ClaudeOpus4Thinking => 128_000, + | Model::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1 + | Model::ClaudeOpus4_1Thinking => 128_000, Self::Claude3_5SonnetV2 | Self::PalmyraWriterX4 | Self::PalmyraWriterX5 => 8_192, Self::Custom { max_output_tokens, .. @@ -366,6 +384,8 @@ impl Model { | Self::Claude3_7Sonnet | Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1 + | Self::ClaudeOpus4_1Thinking | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => 1.0, Self::Custom { @@ -387,6 +407,8 @@ impl Model { | Self::Claude3_7SonnetThinking | Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1 + | Self::ClaudeOpus4_1Thinking | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking | Self::Claude3_5Haiku => true, @@ -420,7 +442,9 @@ impl Model { | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking | Self::ClaudeOpus4 - | Self::ClaudeOpus4Thinking => true, + | Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1 + | Self::ClaudeOpus4_1Thinking => true, // Custom models - check if they have cache configuration Self::Custom { @@ -440,7 +464,9 @@ impl Model { | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking | Self::ClaudeOpus4 - | Self::ClaudeOpus4Thinking => Some(BedrockModelCacheConfiguration { + | Self::ClaudeOpus4Thinking + | Self::ClaudeOpus4_1 + | Self::ClaudeOpus4_1Thinking => Some(BedrockModelCacheConfiguration { max_cache_anchors: 4, min_total_token: 1024, }), @@ -467,9 +493,11 @@ impl Model { Model::ClaudeSonnet4Thinking => BedrockModelMode::Thinking { budget_tokens: Some(4096), }, - Model::ClaudeOpus4Thinking => BedrockModelMode::Thinking { - budget_tokens: Some(4096), - }, + Model::ClaudeOpus4Thinking | Model::ClaudeOpus4_1Thinking => { + BedrockModelMode::Thinking { + budget_tokens: Some(4096), + } + } _ => BedrockModelMode::Default, } } @@ -518,6 +546,8 @@ impl Model { | Model::ClaudeSonnet4Thinking | Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking + | Model::ClaudeOpus4_1 + | Model::ClaudeOpus4_1Thinking | Model::Claude3Haiku | Model::Claude3Opus | Model::Claude3Sonnet From 6b77654f66c2a5425a8cea2fb6079e6b2ff4d32d Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 5 Aug 2025 14:48:15 -0500 Subject: [PATCH 086/693] onboarding: Wire up tab index (#35659) Closes #ISSUE Allows tabbing through everything in all three pages. Until #35075 is merged it is not possible to actually "click" tab focused buttons with the keyboard. Additionally adds an action `onboarding::Finish` and displays the keybind. The action corresponds to both the "Skip all" and "Start Building" buttons, with the keybind displayed similar to how it is for the page nav buttons Release Notes: - N/A *or* Added/Fixed/Improved ... --------- Co-authored-by: MrSubidubi --- assets/keymaps/default-linux.json | 3 +- assets/keymaps/default-macos.json | 3 +- crates/ai_onboarding/src/ai_upsell_card.rs | 8 +- crates/gpui/src/window.rs | 9 + crates/onboarding/src/ai_setup_page.rs | 127 +++++++----- crates/onboarding/src/basics_page.rs | 196 ++++++++++-------- crates/onboarding/src/editing_page.rs | 147 ++++++++----- crates/onboarding/src/onboarding.rs | 141 ++++++++----- .../ui/src/components/button/toggle_button.rs | 15 ++ crates/ui/src/components/numeric_stepper.rs | 32 +++ crates/ui/src/components/toggle.rs | 104 ++++++---- 11 files changed, 505 insertions(+), 280 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index ef5354e82d..81f5c695a2 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1175,7 +1175,8 @@ "bindings": { "ctrl-1": "onboarding::ActivateBasicsPage", "ctrl-2": "onboarding::ActivateEditingPage", - "ctrl-3": "onboarding::ActivateAISetupPage" + "ctrl-3": "onboarding::ActivateAISetupPage", + "ctrl-escape": "onboarding::Finish" } } ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 3287e50acb..69958fd1f8 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1277,7 +1277,8 @@ "bindings": { "cmd-1": "onboarding::ActivateBasicsPage", "cmd-2": "onboarding::ActivateEditingPage", - "cmd-3": "onboarding::ActivateAISetupPage" + "cmd-3": "onboarding::ActivateAISetupPage", + "cmd-escape": "onboarding::Finish" } } ] diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index 2408b6aa37..89a782a7c2 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -12,6 +12,7 @@ pub struct AiUpsellCard { pub sign_in_status: SignInStatus, pub sign_in: Arc, pub user_plan: Option, + pub tab_index: Option, } impl AiUpsellCard { @@ -28,6 +29,7 @@ impl AiUpsellCard { }) .detach_and_log_err(cx); }), + tab_index: None, } } } @@ -112,7 +114,8 @@ impl RenderOnce for AiUpsellCard { .on_click(move |_, _window, cx| { telemetry::event!("Start Trial Clicked", state = "post-sign-in"); cx.open_url(&zed_urls::start_trial_url(cx)) - }), + }) + .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)), ) .child( Label::new("No credit card required") @@ -123,6 +126,7 @@ impl RenderOnce for AiUpsellCard { _ => Button::new("sign_in", "Sign In") .full_width() .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)) .on_click({ let callback = self.sign_in.clone(); move |_, window, cx| { @@ -193,6 +197,7 @@ impl Component for AiUpsellCard { sign_in_status: SignInStatus::SignedOut, sign_in: Arc::new(|_, _| {}), user_plan: None, + tab_index: Some(0), } .into_any_element(), ), @@ -202,6 +207,7 @@ impl Component for AiUpsellCard { sign_in_status: SignInStatus::SignedIn, sign_in: Arc::new(|_, _| {}), user_plan: None, + tab_index: Some(1), } .into_any_element(), ), diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 6ebb1cac40..9e4c1c26c5 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4699,6 +4699,8 @@ pub enum ElementId { Path(Arc), /// A code location. CodeLocation(core::panic::Location<'static>), + /// A labeled child of an element. + NamedChild(Box, SharedString), } impl ElementId { @@ -4719,6 +4721,7 @@ impl Display for ElementId { ElementId::Uuid(uuid) => write!(f, "{}", uuid)?, ElementId::Path(path) => write!(f, "{}", path.display())?, ElementId::CodeLocation(location) => write!(f, "{}", location)?, + ElementId::NamedChild(id, name) => write!(f, "{}-{}", id, name)?, } Ok(()) @@ -4809,6 +4812,12 @@ impl From<(&'static str, u32)> for ElementId { } } +impl> From<(ElementId, T)> for ElementId { + fn from((id, name): (ElementId, T)) -> Self { + ElementId::NamedChild(Box::new(id), name.into()) + } +} + /// A rectangle to be rendered in the window at the given position and size. /// Passed as an argument [`Window::paint_quad`]. #[derive(Clone)] diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index b5dda7601f..098907870b 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -1,9 +1,11 @@ use std::sync::Arc; use ai_onboarding::{AiUpsellCard, SignInStatus}; +use client::UserStore; use fs::Fs; use gpui::{ - Action, AnyView, App, DismissEvent, EventEmitter, FocusHandle, Focusable, Window, prelude::*, + Action, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, + Window, prelude::*, }; use itertools; use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry}; @@ -14,15 +16,14 @@ use ui::{ prelude::*, tooltip_container, }; use util::ResultExt; -use workspace::ModalView; +use workspace::{ModalView, Workspace}; use zed_actions::agent::OpenSettings; -use crate::Onboarding; - const FEATURED_PROVIDERS: [&'static str; 4] = ["anthropic", "google", "openai", "ollama"]; fn render_llm_provider_section( - onboarding: &Onboarding, + tab_index: &mut isize, + workspace: WeakEntity, disabled: bool, window: &mut Window, cx: &mut App, @@ -37,10 +38,10 @@ fn render_llm_provider_section( .color(Color::Muted), ), ) - .child(render_llm_provider_card(onboarding, disabled, window, cx)) + .child(render_llm_provider_card(tab_index, workspace, disabled, window, cx)) } -fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement { +fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> impl IntoElement { let privacy_badge = || { Badge::new("Privacy") .icon(IconName::ShieldCheck) @@ -98,6 +99,10 @@ fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement { .icon_color(Color::Muted) .on_click(|_, _, cx| { cx.open_url("https://zed.dev/docs/ai/privacy-and-security"); + }) + .tab_index({ + *tab_index += 1; + *tab_index - 1 }), ), ), @@ -114,7 +119,8 @@ fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement { } fn render_llm_provider_card( - onboarding: &Onboarding, + tab_index: &mut isize, + workspace: WeakEntity, disabled: bool, _: &mut Window, cx: &mut App, @@ -140,6 +146,10 @@ fn render_llm_provider_card( ButtonLike::new(("onboarding-ai-setup-buttons", index)) .size(ButtonSize::Large) + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }) .child( h_flex() .group(&group_name) @@ -188,7 +198,7 @@ fn render_llm_provider_card( ), ) .on_click({ - let workspace = onboarding.workspace.clone(); + let workspace = workspace.clone(); move |_, window, cx| { workspace .update(cx, |workspace, cx| { @@ -219,57 +229,56 @@ fn render_llm_provider_card( .icon_size(IconSize::XSmall) .on_click(|_event, window, cx| { window.dispatch_action(OpenSettings.boxed_clone(), cx) + }) + .tab_index({ + *tab_index += 1; + *tab_index - 1 }), ) } pub(crate) fn render_ai_setup_page( - onboarding: &Onboarding, + workspace: WeakEntity, + user_store: Entity, window: &mut Window, cx: &mut App, ) -> impl IntoElement { + let mut tab_index = 0; let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai; - let backdrop = div() - .id("backdrop") - .size_full() - .absolute() - .inset_0() - .bg(cx.theme().colors().editor_background) - .opacity(0.8) - .block_mouse_except_scroll(); - v_flex() .gap_2() - .child(SwitchField::new( - "enable_ai", - "Enable AI features", - None, - if is_ai_disabled { - ToggleState::Unselected - } else { - ToggleState::Selected - }, - |toggle_state, _, cx| { - let enabled = match toggle_state { - ToggleState::Indeterminate => { - return; - } - ToggleState::Unselected => false, - ToggleState::Selected => true, - }; - - let fs = ::global(cx); - update_settings_file::( - fs, - cx, - move |ai_settings: &mut Option, _| { - *ai_settings = Some(!enabled); - }, - ); - }, - )) - .child(render_privacy_card(is_ai_disabled, cx)) + .child( + SwitchField::new( + "enable_ai", + "Enable AI features", + None, + if is_ai_disabled { + ToggleState::Unselected + } else { + ToggleState::Selected + }, + |&toggle_state, _, cx| { + let fs = ::global(cx); + update_settings_file::( + fs, + cx, + move |ai_settings: &mut Option, _| { + *ai_settings = match toggle_state { + ToggleState::Indeterminate => None, + ToggleState::Unselected => Some(true), + ToggleState::Selected => Some(false), + }; + }, + ); + }, + ) + .tab_index({ + tab_index += 1; + tab_index - 1 + }), + ) + .child(render_privacy_card(&mut tab_index, is_ai_disabled, cx)) .child( v_flex() .mt_2() @@ -277,15 +286,31 @@ pub(crate) fn render_ai_setup_page( .child(AiUpsellCard { sign_in_status: SignInStatus::SignedIn, sign_in: Arc::new(|_, _| {}), - user_plan: onboarding.user_store.read(cx).plan(), + user_plan: user_store.read(cx).plan(), + tab_index: Some({ + tab_index += 1; + tab_index - 1 + }), }) .child(render_llm_provider_section( - onboarding, + &mut tab_index, + workspace, is_ai_disabled, window, cx, )) - .when(is_ai_disabled, |this| this.child(backdrop)), + .when(is_ai_disabled, |this| { + this.child( + div() + .id("backdrop") + .size_full() + .absolute() + .inset_0() + .bg(cx.theme().colors().editor_background) + .opacity(0.8) + .block_mouse_except_scroll(), + ) + }), ) } diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 21ea74f01c..a4e4028051 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use client::TelemetrySettings; use fs::Fs; -use gpui::{App, IntoElement, Window}; +use gpui::{App, IntoElement}; use settings::{BaseKeymap, Settings, update_settings_file}; use theme::{ Appearance, SystemAppearance, ThemeMode, ThemeName, ThemeRegistry, ThemeSelection, @@ -16,7 +16,7 @@ use vim_mode_setting::VimModeSetting; use crate::theme_preview::{ThemePreviewStyle, ThemePreviewTile}; -fn render_theme_section(_window: &mut Window, cx: &mut App) -> impl IntoElement { +fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement { let theme_selection = ThemeSettings::get_global(cx).theme_selection.clone(); let system_appearance = theme::SystemAppearance::global(cx); let theme_selection = theme_selection.unwrap_or_else(|| ThemeSelection::Dynamic { @@ -55,6 +55,7 @@ fn render_theme_section(_window: &mut Window, cx: &mut App) -> impl IntoElement ) }), ) + .tab_index(tab_index) .selected_index(theme_mode as usize) .style(ui::ToggleButtonGroupStyle::Outlined) .button_width(rems_from_px(64.)), @@ -64,10 +65,11 @@ fn render_theme_section(_window: &mut Window, cx: &mut App) -> impl IntoElement h_flex() .gap_4() .justify_between() - .children(render_theme_previews(&theme_selection, cx)), + .children(render_theme_previews(tab_index, &theme_selection, cx)), ); fn render_theme_previews( + tab_index: &mut isize, theme_selection: &ThemeSelection, cx: &mut App, ) -> [impl IntoElement; 3] { @@ -110,12 +112,12 @@ fn render_theme_section(_window: &mut Window, cx: &mut App) -> impl IntoElement let colors = cx.theme().colors(); v_flex() - .id(name.clone()) .w_full() .items_center() .gap_1() .child( h_flex() + .id(name.clone()) .relative() .w_full() .border_2() @@ -128,6 +130,20 @@ fn render_theme_section(_window: &mut Window, cx: &mut App) -> impl IntoElement this.opacity(0.8).hover(|s| s.border_color(colors.border)) } }) + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }) + .focus(|mut style| { + style.border_color = Some(colors.border_focused); + style + }) + .on_click({ + let theme_name = theme.name.clone(); + move |_, _, cx| { + write_theme_change(theme_name.clone(), theme_mode, cx); + } + }) .map(|this| { if theme_mode == ThemeMode::System { let (light, dark) = ( @@ -151,12 +167,6 @@ fn render_theme_section(_window: &mut Window, cx: &mut App) -> impl IntoElement .color(Color::Muted) .size(LabelSize::Small), ) - .on_click({ - let theme_name = theme.name.clone(); - move |_, _, cx| { - write_theme_change(theme_name.clone(), theme_mode, cx); - } - }) }); theme_previews @@ -187,15 +197,7 @@ fn render_theme_section(_window: &mut Window, cx: &mut App) -> impl IntoElement } } -fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) { - let fs = ::global(cx); - - update_settings_file::(fs, cx, move |setting, _| { - *setting = Some(keymap_base); - }); -} - -fn render_telemetry_section(cx: &App) -> impl IntoElement { +fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement { let fs = ::global(cx); v_flex() @@ -225,7 +227,10 @@ fn render_telemetry_section(cx: &App) -> impl IntoElement { move |setting, _| setting.metrics = Some(enabled), ); }}, - )) + ).tab_index({ + *tab_index += 1; + *tab_index + })) .child(SwitchField::new( "onboarding-telemetry-crash-reports", "Help Fix Zed", @@ -251,10 +256,13 @@ fn render_telemetry_section(cx: &App) -> impl IntoElement { ); } } - )) + ).tab_index({ + *tab_index += 1; + *tab_index + })) } -pub(crate) fn render_basics_page(window: &mut Window, cx: &mut App) -> impl IntoElement { +fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement { let base_keymap = match BaseKeymap::get_global(cx) { BaseKeymap::VSCode => Some(0), BaseKeymap::JetBrains => Some(1), @@ -265,67 +273,89 @@ pub(crate) fn render_basics_page(window: &mut Window, cx: &mut App) -> impl Into BaseKeymap::TextMate | BaseKeymap::None => None, }; + return v_flex().gap_2().child(Label::new("Base Keymap")).child( + ToggleButtonGroup::two_rows( + "base_keymap_selection", + [ + ToggleButtonWithIcon::new("VS Code", IconName::EditorVsCode, |_, _, cx| { + write_keymap_base(BaseKeymap::VSCode, cx); + }), + ToggleButtonWithIcon::new("Jetbrains", IconName::EditorJetBrains, |_, _, cx| { + write_keymap_base(BaseKeymap::JetBrains, cx); + }), + ToggleButtonWithIcon::new("Sublime Text", IconName::EditorSublime, |_, _, cx| { + write_keymap_base(BaseKeymap::SublimeText, cx); + }), + ], + [ + ToggleButtonWithIcon::new("Atom", IconName::EditorAtom, |_, _, cx| { + write_keymap_base(BaseKeymap::Atom, cx); + }), + ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| { + write_keymap_base(BaseKeymap::Emacs, cx); + }), + ToggleButtonWithIcon::new("Cursor (Beta)", IconName::EditorCursor, |_, _, cx| { + write_keymap_base(BaseKeymap::Cursor, cx); + }), + ], + ) + .when_some(base_keymap, |this, base_keymap| { + this.selected_index(base_keymap) + }) + .tab_index(tab_index) + .button_width(rems_from_px(216.)) + .size(ui::ToggleButtonGroupSize::Medium) + .style(ui::ToggleButtonGroupStyle::Outlined), + ); + + fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) { + let fs = ::global(cx); + + update_settings_file::(fs, cx, move |setting, _| { + *setting = Some(keymap_base); + }); + } +} + +fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoElement { + let toggle_state = if VimModeSetting::get_global(cx).0 { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }; + SwitchField::new( + "onboarding-vim-mode", + "Vim Mode", + Some( + "Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back." + .into(), + ), + toggle_state, + { + let fs = ::global(cx); + move |&selection, _, cx| { + update_settings_file::(fs.clone(), cx, move |setting, _| { + *setting = match selection { + ToggleState::Selected => Some(true), + ToggleState::Unselected => Some(false), + ToggleState::Indeterminate => None, + } + }); + } + }, + ) + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }) +} + +pub(crate) fn render_basics_page(cx: &mut App) -> impl IntoElement { + let mut tab_index = 0; v_flex() .gap_6() - .child(render_theme_section(window, cx)) - .child( - v_flex().gap_2().child(Label::new("Base Keymap")).child( - ToggleButtonGroup::two_rows( - "multiple_row_test", - [ - ToggleButtonWithIcon::new("VS Code", IconName::EditorVsCode, |_, _, cx| { - write_keymap_base(BaseKeymap::VSCode, cx); - }), - ToggleButtonWithIcon::new("Jetbrains", IconName::EditorJetBrains, |_, _, cx| { - write_keymap_base(BaseKeymap::JetBrains, cx); - }), - ToggleButtonWithIcon::new("Sublime Text", IconName::EditorSublime, |_, _, cx| { - write_keymap_base(BaseKeymap::SublimeText, cx); - }), - ], - [ - ToggleButtonWithIcon::new("Atom", IconName::EditorAtom, |_, _, cx| { - write_keymap_base(BaseKeymap::Atom, cx); - }), - ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| { - write_keymap_base(BaseKeymap::Emacs, cx); - }), - ToggleButtonWithIcon::new("Cursor (Beta)", IconName::EditorCursor, |_, _, cx| { - write_keymap_base(BaseKeymap::Cursor, cx); - }), - ], - ) - .when_some(base_keymap, |this, base_keymap| this.selected_index(base_keymap)) - .button_width(rems_from_px(216.)) - .size(ui::ToggleButtonGroupSize::Medium) - .style(ui::ToggleButtonGroupStyle::Outlined) - ), - ) - .child(SwitchField::new( - "onboarding-vim-mode", - "Vim Mode", - Some("Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back.".into()), - if VimModeSetting::get_global(cx).0 { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - { - let fs = ::global(cx); - move |selection, _, cx| { - let enabled = match selection { - ToggleState::Selected => true, - ToggleState::Unselected => false, - ToggleState::Indeterminate => { return; }, - }; - - update_settings_file::( - fs.clone(), - cx, - move |setting, _| *setting = Some(enabled), - ); - } - }, - )) - .child(render_telemetry_section(cx)) + .child(render_theme_section(&mut tab_index, cx)) + .child(render_base_keymap_section(&mut tab_index, cx)) + .child(render_vim_mode_switch(&mut tab_index, cx)) + .child(render_telemetry_section(&mut tab_index, cx)) } diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 6dd272745a..a8f0265b6b 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -171,6 +171,7 @@ fn write_format_on_save(format_on_save: bool, cx: &mut App) { } fn render_setting_import_button( + tab_index: isize, label: SharedString, icon_name: IconName, action: &dyn Action, @@ -182,6 +183,7 @@ fn render_setting_import_button( .full_width() .style(ButtonStyle::Outlined) .size(ButtonSize::Large) + .tab_index(tab_index) .child( h_flex() .w_full() @@ -214,7 +216,7 @@ fn render_setting_import_button( ) } -fn render_import_settings_section(cx: &App) -> impl IntoElement { +fn render_import_settings_section(tab_index: &mut isize, cx: &App) -> impl IntoElement { let import_state = SettingsImportState::global(cx); let imports: [(SharedString, IconName, &dyn Action, bool); 2] = [ ( @@ -232,7 +234,8 @@ fn render_import_settings_section(cx: &App) -> impl IntoElement { ]; let [vscode, cursor] = imports.map(|(label, icon_name, action, imported)| { - render_setting_import_button(label, icon_name, action, imported) + *tab_index += 1; + render_setting_import_button(*tab_index - 1, label, icon_name, action, imported) }); v_flex() @@ -248,7 +251,11 @@ fn render_import_settings_section(cx: &App) -> impl IntoElement { .child(h_flex().w_full().gap_4().child(vscode).child(cursor)) } -fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl IntoElement { +fn render_font_customization_section( + tab_index: &mut isize, + window: &mut Window, + cx: &mut App, +) -> impl IntoElement { let theme_settings = ThemeSettings::get_global(cx); let ui_font_size = theme_settings.ui_font_size(cx); let ui_font_family = theme_settings.ui_font.family.clone(); @@ -294,6 +301,10 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl .style(ButtonStyle::Outlined) .size(ButtonSize::Medium) .full_width() + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }) .child( h_flex() .w_full() @@ -325,7 +336,11 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl write_ui_font_size(ui_font_size + px(1.), cx); }, ) - .style(ui::NumericStepperStyle::Outlined), + .style(ui::NumericStepperStyle::Outlined) + .tab_index({ + *tab_index += 2; + *tab_index - 2 + }), ), ), ) @@ -350,6 +365,10 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl .style(ButtonStyle::Outlined) .size(ButtonSize::Medium) .full_width() + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }) .child( h_flex() .w_full() @@ -381,7 +400,11 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl write_buffer_font_size(buffer_font_size + px(1.), cx); }, ) - .style(ui::NumericStepperStyle::Outlined), + .style(ui::NumericStepperStyle::Outlined) + .tab_index({ + *tab_index += 2; + *tab_index - 2 + }), ), ), ) @@ -556,13 +579,17 @@ fn font_picker( .max_height(Some(rems(20.).into())) } -fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl IntoElement { +fn render_popular_settings_section( + tab_index: &mut isize, + window: &mut Window, + cx: &mut App, +) -> impl IntoElement { const LIGATURE_TOOLTIP: &'static str = "Ligatures are when a font creates a special character out of combining two characters into one. For example, with ligatures turned on, =/= would become ≠."; v_flex() .gap_5() .child(Label::new("Popular Settings").size(LabelSize::Large).mt_8()) - .child(render_font_customization_section(window, cx)) + .child(render_font_customization_section(tab_index, window, cx)) .child( SwitchField::new( "onboarding-font-ligatures", @@ -577,47 +604,69 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In write_font_ligatures(toggle_state == &ToggleState::Selected, cx); }, ) + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }) .tooltip(Tooltip::text(LIGATURE_TOOLTIP)), ) - .child(SwitchField::new( - "onboarding-format-on-save", - "Format on Save", - Some("Format code automatically when saving.".into()), - if read_format_on_save(cx) { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - |toggle_state, _, cx| { - write_format_on_save(toggle_state == &ToggleState::Selected, cx); - }, - )) - .child(SwitchField::new( - "onboarding-enable-inlay-hints", - "Inlay Hints", - Some("See parameter names for function and method calls inline.".into()), - if read_inlay_hints(cx) { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - |toggle_state, _, cx| { - write_inlay_hints(toggle_state == &ToggleState::Selected, cx); - }, - )) - .child(SwitchField::new( - "onboarding-git-blame-switch", - "Git Blame", - Some("See who committed each line on a given file.".into()), - if read_git_blame(cx) { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - |toggle_state, _, cx| { - set_git_blame(toggle_state == &ToggleState::Selected, cx); - }, - )) + .child( + SwitchField::new( + "onboarding-format-on-save", + "Format on Save", + Some("Format code automatically when saving.".into()), + if read_format_on_save(cx) { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + |toggle_state, _, cx| { + write_format_on_save(toggle_state == &ToggleState::Selected, cx); + }, + ) + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }), + ) + .child( + SwitchField::new( + "onboarding-enable-inlay-hints", + "Inlay Hints", + Some("See parameter names for function and method calls inline.".into()), + if read_inlay_hints(cx) { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + |toggle_state, _, cx| { + write_inlay_hints(toggle_state == &ToggleState::Selected, cx); + }, + ) + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }), + ) + .child( + SwitchField::new( + "onboarding-git-blame-switch", + "Git Blame", + Some("See who committed each line on a given file.".into()), + if read_git_blame(cx) { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + |toggle_state, _, cx| { + set_git_blame(toggle_state == &ToggleState::Selected, cx); + }, + ) + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }), + ) .child( h_flex() .items_start() @@ -648,6 +697,7 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In ShowMinimap::Always => 1, ShowMinimap::Never => 2, }) + .tab_index(tab_index) .style(ToggleButtonGroupStyle::Outlined) .button_width(ui::rems_from_px(64.)), ), @@ -655,8 +705,9 @@ fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl In } pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement { + let mut tab_index = 0; v_flex() .gap_4() - .child(render_import_settings_section(cx)) - .child(render_popular_settings_section(window, cx)) + .child(render_import_settings_section(&mut tab_index, cx)) + .child(render_popular_settings_section(&mut tab_index, window, cx)) } diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 42e75ac2f8..c4d2b6847c 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -75,6 +75,8 @@ actions!( ActivateEditingPage, /// Activates the AI Setup page. ActivateAISetupPage, + /// Finish the onboarding process. + Finish, ] ); @@ -261,40 +263,6 @@ impl Onboarding { cx.emit(ItemEvent::UpdateTab); } - fn go_to_welcome_page(&self, cx: &mut App) { - with_active_or_new_workspace(cx, |workspace, window, cx| { - let Some((onboarding_id, onboarding_idx)) = workspace - .active_pane() - .read(cx) - .items() - .enumerate() - .find_map(|(idx, item)| { - let _ = item.downcast::()?; - Some((item.item_id(), idx)) - }) - else { - return; - }; - - workspace.active_pane().update(cx, |pane, cx| { - // Get the index here to get around the borrow checker - let idx = pane.items().enumerate().find_map(|(idx, item)| { - let _ = item.downcast::()?; - Some(idx) - }); - - if let Some(idx) = idx { - pane.activate_item(idx, true, true, window, cx); - } else { - let item = Box::new(WelcomePage::new(window, cx)); - pane.add_item(item, true, true, Some(onboarding_idx), window, cx); - } - - pane.remove_item(onboarding_id, false, false, window, cx); - }); - }); - } - fn render_nav_buttons( &mut self, window: &mut Window, @@ -401,6 +369,13 @@ impl Onboarding { .children(self.render_nav_buttons(window, cx)), ) .map(|this| { + let keybinding = KeyBinding::for_action_in( + &Finish, + &self.focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(12.))); if ai_setup_page { this.child( ButtonLike::new("start_building") @@ -412,23 +387,37 @@ impl Onboarding { .w_full() .justify_between() .child(Label::new("Start Building")) - .child( - Icon::new(IconName::Check) - .size(IconSize::Small), - ), + .child(keybinding.map_or_else( + || { + Icon::new(IconName::Check) + .size(IconSize::Small) + .into_any_element() + }, + IntoElement::into_any_element, + )), ) - .on_click(cx.listener(|this, _, _, cx| { - this.go_to_welcome_page(cx); - })), + .on_click(|_, window, cx| { + window.dispatch_action(Finish.boxed_clone(), cx); + }), ) } else { this.child( ButtonLike::new("skip_all") .size(ButtonSize::Medium) - .child(Label::new("Skip All").ml_1()) - .on_click(cx.listener(|this, _, _, cx| { - this.go_to_welcome_page(cx); - })), + .child( + h_flex() + .ml_1() + .w_full() + .justify_between() + .child(Label::new("Skip All")) + .child(keybinding.map_or_else( + || gpui::Empty.into_any_element(), + IntoElement::into_any_element, + )), + ) + .on_click(|_, window, cx| { + window.dispatch_action(Finish.boxed_clone(), cx); + }), ) } }), @@ -464,17 +453,23 @@ impl Onboarding { fn render_page(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { match self.selected_page { - SelectedPage::Basics => { - crate::basics_page::render_basics_page(window, cx).into_any_element() - } + SelectedPage::Basics => crate::basics_page::render_basics_page(cx).into_any_element(), SelectedPage::Editing => { crate::editing_page::render_editing_page(window, cx).into_any_element() } - SelectedPage::AiSetup => { - crate::ai_setup_page::render_ai_setup_page(&self, window, cx).into_any_element() - } + SelectedPage::AiSetup => crate::ai_setup_page::render_ai_setup_page( + self.workspace.clone(), + self.user_store.clone(), + window, + cx, + ) + .into_any_element(), } } + + fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) { + go_to_welcome_page(cx); + } } impl Render for Onboarding { @@ -484,11 +479,13 @@ impl Render for Onboarding { .key_context({ let mut ctx = KeyContext::new_with_defaults(); ctx.add("Onboarding"); + ctx.add("menu"); ctx }) .track_focus(&self.focus_handle) .size_full() .bg(cx.theme().colors().editor_background) + .on_action(Self::on_finish) .on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| { this.set_page(SelectedPage::Basics, cx); })) @@ -498,6 +495,14 @@ impl Render for Onboarding { .on_action(cx.listener(|this, _: &ActivateAISetupPage, _, cx| { this.set_page(SelectedPage::AiSetup, cx); })) + .on_action(cx.listener(|_, _: &menu::SelectNext, window, cx| { + window.focus_next(); + cx.notify(); + })) + .on_action(cx.listener(|_, _: &menu::SelectPrevious, window, cx| { + window.focus_prev(); + cx.notify(); + })) .child( h_flex() .max_w(rems_from_px(1100.)) @@ -561,6 +566,40 @@ impl Item for Onboarding { } } +fn go_to_welcome_page(cx: &mut App) { + with_active_or_new_workspace(cx, |workspace, window, cx| { + let Some((onboarding_id, onboarding_idx)) = workspace + .active_pane() + .read(cx) + .items() + .enumerate() + .find_map(|(idx, item)| { + let _ = item.downcast::()?; + Some((item.item_id(), idx)) + }) + else { + return; + }; + + workspace.active_pane().update(cx, |pane, cx| { + // Get the index here to get around the borrow checker + let idx = pane.items().enumerate().find_map(|(idx, item)| { + let _ = item.downcast::()?; + Some(idx) + }); + + if let Some(idx) = idx { + pane.activate_item(idx, true, true, window, cx); + } else { + let item = Box::new(WelcomePage::new(window, cx)); + pane.add_item(item, true, true, Some(onboarding_idx), window, cx); + } + + pane.remove_item(onboarding_id, false, false, window, cx); + }); + }); +} + pub async fn handle_import_vscode_settings( workspace: WeakEntity, source: VsCodeSettingsSource, diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs index d4d47da9b6..6fbf834667 100644 --- a/crates/ui/src/components/button/toggle_button.rs +++ b/crates/ui/src/components/button/toggle_button.rs @@ -412,6 +412,7 @@ where size: ToggleButtonGroupSize, button_width: Rems, selected_index: usize, + tab_index: Option, } impl ToggleButtonGroup { @@ -423,6 +424,7 @@ impl ToggleButtonGroup { size: ToggleButtonGroupSize::Default, button_width: rems_from_px(100.), selected_index: 0, + tab_index: None, } } } @@ -436,6 +438,7 @@ impl ToggleButtonGroup { size: ToggleButtonGroupSize::Default, button_width: rems_from_px(100.), selected_index: 0, + tab_index: None, } } } @@ -460,6 +463,15 @@ impl ToggleButtonGroup Self { + self.tab_index = Some(*tab_index); + *tab_index += (COLS * ROWS) as isize; + self + } } impl RenderOnce @@ -479,6 +491,9 @@ impl RenderOnce let entry_index = row_index * COLS + col_index; ButtonLike::new((self.group_name, entry_index)) + .when_some(self.tab_index, |this, tab_index| { + this.tab_index(tab_index + entry_index as isize) + }) .when(entry_index == self.selected_index || selected, |this| { this.toggle_state(true) .selected_style(ButtonStyle::Tinted(TintColor::Accent)) diff --git a/crates/ui/src/components/numeric_stepper.rs b/crates/ui/src/components/numeric_stepper.rs index 0ec7111a02..2ddb86d9a0 100644 --- a/crates/ui/src/components/numeric_stepper.rs +++ b/crates/ui/src/components/numeric_stepper.rs @@ -19,6 +19,7 @@ pub struct NumericStepper { /// Whether to reserve space for the reset button. reserve_space_for_reset: bool, on_reset: Option>, + tab_index: Option, } impl NumericStepper { @@ -36,6 +37,7 @@ impl NumericStepper { on_increment: Box::new(on_increment), reserve_space_for_reset: false, on_reset: None, + tab_index: None, } } @@ -56,6 +58,11 @@ impl NumericStepper { self.on_reset = Some(Box::new(on_reset)); self } + + pub fn tab_index(mut self, tab_index: isize) -> Self { + self.tab_index = Some(tab_index); + self + } } impl RenderOnce for NumericStepper { @@ -64,6 +71,7 @@ impl RenderOnce for NumericStepper { let icon_size = IconSize::Small; let is_outlined = matches!(self.style, NumericStepperStyle::Outlined); + let mut tab_index = self.tab_index; h_flex() .id(self.id) @@ -74,6 +82,10 @@ impl RenderOnce for NumericStepper { IconButton::new("reset", IconName::RotateCcw) .shape(shape) .icon_size(icon_size) + .when_some(tab_index.as_mut(), |this, tab_index| { + *tab_index += 1; + this.tab_index(*tab_index - 1) + }) .on_click(on_reset), ) } else if self.reserve_space_for_reset { @@ -113,6 +125,12 @@ impl RenderOnce for NumericStepper { .border_r_1() .border_color(cx.theme().colors().border_variant) .child(Icon::new(IconName::Dash).size(IconSize::Small)) + .when_some(tab_index.as_mut(), |this, tab_index| { + *tab_index += 1; + this.tab_index(*tab_index - 1).focus(|style| { + style.bg(cx.theme().colors().element_hover) + }) + }) .on_click(self.on_decrement), ) } else { @@ -120,6 +138,10 @@ impl RenderOnce for NumericStepper { IconButton::new("decrement", IconName::Dash) .shape(shape) .icon_size(icon_size) + .when_some(tab_index.as_mut(), |this, tab_index| { + *tab_index += 1; + this.tab_index(*tab_index - 1) + }) .on_click(self.on_decrement), ) } @@ -137,6 +159,12 @@ impl RenderOnce for NumericStepper { .border_l_1() .border_color(cx.theme().colors().border_variant) .child(Icon::new(IconName::Plus).size(IconSize::Small)) + .when_some(tab_index.as_mut(), |this, tab_index| { + *tab_index += 1; + this.tab_index(*tab_index - 1).focus(|style| { + style.bg(cx.theme().colors().element_hover) + }) + }) .on_click(self.on_increment), ) } else { @@ -144,6 +172,10 @@ impl RenderOnce for NumericStepper { IconButton::new("increment", IconName::Dash) .shape(shape) .icon_size(icon_size) + .when_some(tab_index.as_mut(), |this, tab_index| { + *tab_index += 1; + this.tab_index(*tab_index - 1) + }) .on_click(self.on_increment), ) } diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index a3a3f23889..53df4767b0 100644 --- a/crates/ui/src/components/toggle.rs +++ b/crates/ui/src/components/toggle.rs @@ -424,6 +424,7 @@ pub struct Switch { label: Option, key_binding: Option, color: SwitchColor, + tab_index: Option, } impl Switch { @@ -437,6 +438,7 @@ impl Switch { label: None, key_binding: None, color: SwitchColor::default(), + tab_index: None, } } @@ -472,6 +474,11 @@ impl Switch { self.key_binding = key_binding.into(); self } + + pub fn tab_index(mut self, tab_index: impl Into) -> Self { + self.tab_index = Some(tab_index.into()); + self + } } impl RenderOnce for Switch { @@ -501,6 +508,20 @@ impl RenderOnce for Switch { .w(DynamicSpacing::Base32.rems(cx)) .h(DynamicSpacing::Base20.rems(cx)) .group(group_id.clone()) + .border_1() + .p(px(1.0)) + .border_color(cx.theme().colors().border_transparent) + .rounded_full() + .id((self.id.clone(), "switch")) + .when_some( + self.tab_index.filter(|_| !self.disabled), + |this, tab_index| { + this.tab_index(tab_index).focus(|mut style| { + style.border_color = Some(cx.theme().colors().border_focused); + style + }) + }, + ) .child( h_flex() .when(is_on, |on| on.justify_end()) @@ -572,6 +593,7 @@ pub struct SwitchField { disabled: bool, color: SwitchColor, tooltip: Option AnyView>>, + tab_index: Option, } impl SwitchField { @@ -591,6 +613,7 @@ impl SwitchField { disabled: false, color: SwitchColor::Accent, tooltip: None, + tab_index: None, } } @@ -615,14 +638,33 @@ impl SwitchField { self.tooltip = Some(Rc::new(tooltip)); self } + + pub fn tab_index(mut self, tab_index: isize) -> Self { + self.tab_index = Some(tab_index); + self + } } impl RenderOnce for SwitchField { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let tooltip = self.tooltip; + let tooltip = self.tooltip.map(|tooltip_fn| { + h_flex() + .gap_0p5() + .child(Label::new(self.label.clone())) + .child( + IconButton::new("tooltip_button", IconName::Info) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .shape(crate::IconButtonShape::Square) + .tooltip({ + let tooltip = tooltip_fn.clone(); + move |window, cx| tooltip(window, cx) + }), + ) + }); h_flex() - .id(SharedString::from(format!("{}-container", self.id))) + .id((self.id.clone(), "container")) .when(!self.disabled, |this| { this.hover(|this| this.cursor_pointer()) }) @@ -630,25 +672,11 @@ impl RenderOnce for SwitchField { .gap_4() .justify_between() .flex_wrap() - .child(match (&self.description, &tooltip) { + .child(match (&self.description, tooltip) { (Some(description), Some(tooltip)) => v_flex() .gap_0p5() .max_w_5_6() - .child( - h_flex() - .gap_0p5() - .child(Label::new(self.label.clone())) - .child( - IconButton::new("tooltip_button", IconName::Info) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .shape(crate::IconButtonShape::Square) - .tooltip({ - let tooltip = tooltip.clone(); - move |window, cx| tooltip(window, cx) - }), - ), - ) + .child(tooltip) .child(Label::new(description.clone()).color(Color::Muted)) .into_any_element(), (Some(description), None) => v_flex() @@ -657,35 +685,23 @@ impl RenderOnce for SwitchField { .child(Label::new(self.label.clone())) .child(Label::new(description.clone()).color(Color::Muted)) .into_any_element(), - (None, Some(tooltip)) => h_flex() - .gap_0p5() - .child(Label::new(self.label.clone())) - .child( - IconButton::new("tooltip_button", IconName::Info) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .shape(crate::IconButtonShape::Square) - .tooltip({ - let tooltip = tooltip.clone(); - move |window, cx| tooltip(window, cx) - }), - ) - .into_any_element(), + (None, Some(tooltip)) => tooltip.into_any_element(), (None, None) => Label::new(self.label.clone()).into_any_element(), }) .child( - Switch::new( - SharedString::from(format!("{}-switch", self.id)), - self.toggle_state, - ) - .color(self.color) - .disabled(self.disabled) - .on_click({ - let on_click = self.on_click.clone(); - move |state, window, cx| { - (on_click)(state, window, cx); - } - }), + Switch::new((self.id.clone(), "switch"), self.toggle_state) + .color(self.color) + .disabled(self.disabled) + .when_some( + self.tab_index.filter(|_| !self.disabled), + |this, tab_index| this.tab_index(tab_index), + ) + .on_click({ + let on_click = self.on_click.clone(); + move |state, window, cx| { + (on_click)(state, window, cx); + } + }), ) .when(!self.disabled, |this| { this.on_click({ From cf23f93917d2612912d19f78389340259b82f3cf Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 6 Aug 2025 01:58:23 +0530 Subject: [PATCH 087/693] language: Fix no diagnostics are shown for CSS (#35663) Closes #30499 `vscode-css-language-server` throws a null reference error if no workspace configuration is provided from the client. Release Notes: - Fixed issue where no diagnostics were shown for CSS, LESS, and SCSS. --- crates/languages/src/css.rs | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/crates/languages/src/css.rs b/crates/languages/src/css.rs index f2a94809a0..7725e079be 100644 --- a/crates/languages/src/css.rs +++ b/crates/languages/src/css.rs @@ -5,7 +5,7 @@ use gpui::AsyncApp; use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; use lsp::{LanguageServerBinary, LanguageServerName}; use node_runtime::NodeRuntime; -use project::Fs; +use project::{Fs, lsp_store::language_server_settings}; use serde_json::json; use smol::fs; use std::{ @@ -14,7 +14,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use util::{ResultExt, maybe}; +use util::{ResultExt, maybe, merge_json_value_into}; const SERVER_PATH: &str = "node_modules/vscode-langservers-extracted/bin/vscode-css-language-server"; @@ -134,6 +134,37 @@ impl LspAdapter for CssLspAdapter { "provideFormatter": true }))) } + + async fn workspace_configuration( + self: Arc, + _: &dyn Fs, + delegate: &Arc, + _: Arc, + cx: &mut AsyncApp, + ) -> Result { + let mut default_config = json!({ + "css": { + "lint": {} + }, + "less": { + "lint": {} + }, + "scss": { + "lint": {} + } + }); + + let project_options = cx.update(|cx| { + language_server_settings(delegate.as_ref(), &self.name(), cx) + .and_then(|s| s.settings.clone()) + })?; + + if let Some(override_options) = project_options { + merge_json_value_into(override_options, &mut default_config); + } + + Ok(default_config) + } } async fn get_cached_server_binary( From a508a9536fb4a8b46e36ef74ef23a642f17daab6 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Tue, 5 Aug 2025 16:29:19 -0400 Subject: [PATCH 088/693] Handle startup failure for gemini-cli (#35624) This PR adds handling for the case where the user's gemini-cli binary fails to start up because it's too old to support the `--experimental-acp` flag. We previously had such handling, but it got lost as part of #35578. This doesn't yet handle the case where the server binary exits unexpectedly after the connection is established; that'll be dealt with in a follow-up PR since it needs different handling and isn't specific to gemini-cli. Release Notes: - N/A Co-authored-by: Agus --- crates/agent_servers/src/gemini.rs | 34 ++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 2366783d22..4450f4e216 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -2,7 +2,7 @@ use std::path::Path; use std::rc::Rc; use crate::{AgentServer, AgentServerCommand}; -use acp_thread::AgentConnection; +use acp_thread::{AgentConnection, LoadError}; use anyhow::Result; use gpui::{Entity, Task}; use project::Project; @@ -53,7 +53,37 @@ impl AgentServer for Gemini { anyhow::bail!("Failed to find gemini binary"); }; - crate::acp::connect(server_name, command, &root_dir, cx).await + let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await; + if result.is_err() { + let version_fut = util::command::new_smol_command(&command.path) + .args(command.args.iter()) + .arg("--version") + .kill_on_drop(true) + .output(); + + let help_fut = util::command::new_smol_command(&command.path) + .args(command.args.iter()) + .arg("--help") + .kill_on_drop(true) + .output(); + + let (version_output, help_output) = futures::future::join(version_fut, help_fut).await; + + let current_version = String::from_utf8(version_output?.stdout)?; + let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG); + + if !supported { + return Err(LoadError::Unsupported { + error_message: format!( + "Your installed version of Gemini {} doesn't support the Agentic Coding Protocol (ACP).", + current_version + ).into(), + upgrade_message: "Upgrade Gemini to Latest".into(), + upgrade_command: "npm install -g @google/gemini-cli@latest".into(), + }.into()) + } + } + result }) } } From c595ed19d6908fd6a9310013d2cfd75613a8968f Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Tue, 5 Aug 2025 23:46:57 +0300 Subject: [PATCH 089/693] languages: Remove a eager conversion from `LanguageName` to `String` (#35667) This PR changes the signature of `language_names` from ```rust pub fn language_names(&self) -> Vec // Into pub fn language_names(&self) -> Vec ``` The function previously eagerly converted `LanguageName`'s to `String`'s, which requires the reallocation of all of the elements. The functions get called in many places in the code base, but only one of which actually requires the conversion to a `String`. In one case it would do a `SharedString` -> `String` -> `SharedString` conversion, which is now totally bypassed. Release Notes: - N/A --- crates/debugger_ui/src/new_process_modal.rs | 6 ++---- .../src/extension_store_test.rs | 19 +++++++++++++++---- crates/language/src/language.rs | 18 +++++++++--------- crates/language/src/language_registry.rs | 8 ++++---- .../src/language_selector.rs | 4 ++-- crates/languages/src/json.rs | 10 +++++++++- crates/snippets_ui/src/snippets_ui.rs | 5 ++--- crates/zed/src/zed.rs | 2 +- 8 files changed, 44 insertions(+), 28 deletions(-) diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index 42f77ab056..2695941bc0 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -1015,15 +1015,13 @@ impl DebugDelegate { let language_names = languages.language_names(); let language = dap_registry .adapter_language(&scenario.adapter) - .map(|language| TaskSourceKind::Language { - name: language.into(), - }); + .map(|language| TaskSourceKind::Language { name: language.0 }); let language = language.or_else(|| { scenario.label.split_whitespace().find_map(|word| { language_names .iter() - .find(|name| name.eq_ignore_ascii_case(word)) + .find(|name| name.as_ref().eq_ignore_ascii_case(word)) .map(|name| TaskSourceKind::Language { name: name.to_owned().into(), }) diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index 891ab91852..c31774c20d 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -10,7 +10,7 @@ use fs::{FakeFs, Fs, RealFs}; use futures::{AsyncReadExt, StreamExt, io::BufReader}; use gpui::{AppContext as _, SemanticVersion, TestAppContext}; use http_client::{FakeHttpClient, Response}; -use language::{BinaryStatus, LanguageMatcher, LanguageRegistry}; +use language::{BinaryStatus, LanguageMatcher, LanguageName, LanguageRegistry}; use language_extension::LspAccess; use lsp::LanguageServerName; use node_runtime::NodeRuntime; @@ -306,7 +306,11 @@ async fn test_extension_store(cx: &mut TestAppContext) { assert_eq!( language_registry.language_names(), - ["ERB", "Plain Text", "Ruby"] + [ + LanguageName::new("ERB"), + LanguageName::new("Plain Text"), + LanguageName::new("Ruby"), + ] ); assert_eq!( theme_registry.list_names(), @@ -458,7 +462,11 @@ async fn test_extension_store(cx: &mut TestAppContext) { assert_eq!( language_registry.language_names(), - ["ERB", "Plain Text", "Ruby"] + [ + LanguageName::new("ERB"), + LanguageName::new("Plain Text"), + LanguageName::new("Ruby"), + ] ); assert_eq!( language_registry.grammar_names(), @@ -513,7 +521,10 @@ async fn test_extension_store(cx: &mut TestAppContext) { assert_eq!(actual_language.hidden, expected_language.hidden); } - assert_eq!(language_registry.language_names(), ["Plain Text"]); + assert_eq!( + language_registry.language_names(), + [LanguageName::new("Plain Text")] + ); assert_eq!(language_registry.grammar_names(), []); }); } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 894625b982..b9933dfcec 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -2353,9 +2353,9 @@ mod tests { assert_eq!( languages.language_names(), &[ - "JSON".to_string(), - "Plain Text".to_string(), - "Rust".to_string(), + LanguageName::new("JSON"), + LanguageName::new("Plain Text"), + LanguageName::new("Rust"), ] ); @@ -2366,9 +2366,9 @@ mod tests { assert_eq!( languages.language_names(), &[ - "JSON".to_string(), - "Plain Text".to_string(), - "Rust".to_string(), + LanguageName::new("JSON"), + LanguageName::new("Plain Text"), + LanguageName::new("Rust"), ] ); @@ -2379,9 +2379,9 @@ mod tests { assert_eq!( languages.language_names(), &[ - "JSON".to_string(), - "Plain Text".to_string(), - "Rust".to_string(), + LanguageName::new("JSON"), + LanguageName::new("Plain Text"), + LanguageName::new("Rust"), ] ); diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 85123d2373..ea988e8098 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -547,15 +547,15 @@ impl LanguageRegistry { self.state.read().language_settings.clone() } - pub fn language_names(&self) -> Vec { + pub fn language_names(&self) -> Vec { let state = self.state.read(); let mut result = state .available_languages .iter() - .filter_map(|l| l.loaded.not().then_some(l.name.to_string())) - .chain(state.languages.iter().map(|l| l.config.name.to_string())) + .filter_map(|l| l.loaded.not().then_some(l.name.clone())) + .chain(state.languages.iter().map(|l| l.config.name.clone())) .collect::>(); - result.sort_unstable_by_key(|language_name| language_name.to_lowercase()); + result.sort_unstable_by_key(|language_name| language_name.as_ref().to_lowercase()); result } diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs index 4c03430553..7ef57085bb 100644 --- a/crates/language_selector/src/language_selector.rs +++ b/crates/language_selector/src/language_selector.rs @@ -121,13 +121,13 @@ impl LanguageSelectorDelegate { .into_iter() .filter_map(|name| { language_registry - .available_language_for_name(&name)? + .available_language_for_name(name.as_ref())? .hidden() .not() .then_some(name) }) .enumerate() - .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, &name)) + .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name.as_ref())) .collect::>(); Self { diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 601b4620c5..028bf9fb68 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -269,7 +269,15 @@ impl JsonLspAdapter { .await; let config = cx.update(|cx| { - Self::get_workspace_config(self.languages.language_names().clone(), adapter_schemas, cx) + Self::get_workspace_config( + self.languages + .language_names() + .into_iter() + .map(|name| name.to_string()) + .collect(), + adapter_schemas, + cx, + ) })?; writer.replace(config.clone()); return Ok(config); diff --git a/crates/snippets_ui/src/snippets_ui.rs b/crates/snippets_ui/src/snippets_ui.rs index 1cc16c5576..a8710d1672 100644 --- a/crates/snippets_ui/src/snippets_ui.rs +++ b/crates/snippets_ui/src/snippets_ui.rs @@ -149,13 +149,12 @@ impl ScopeSelectorDelegate { scope_selector: WeakEntity, language_registry: Arc, ) -> Self { - let candidates = Vec::from([GLOBAL_SCOPE_NAME.to_string()]).into_iter(); let languages = language_registry.language_names().into_iter(); - let candidates = candidates + let candidates = std::iter::once(LanguageName::new(GLOBAL_SCOPE_NAME)) .chain(languages) .enumerate() - .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, &name)) + .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name.as_ref())) .collect::>(); let mut existing_scopes = HashSet::new(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index ec62ed33fd..8c89a7d85a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4439,7 +4439,7 @@ mod tests { }); for name in languages.language_names() { languages - .language_for_name(&name) + .language_for_name(name.as_ref()) .await .with_context(|| format!("language name {name}")) .unwrap(); From c957f5ba879ae6fc635b5ff75ca531811240ccfb Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Tue, 5 Aug 2025 16:47:17 -0400 Subject: [PATCH 090/693] Unpin agent thread controls (#35661) This PR moves the new agent thread controls so they're attached to the last message and scroll with the thread history, instead of always being shown above the message editor. Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index a8e2d59b62..cecf989a23 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -785,7 +785,7 @@ impl AcpThreadView { window: &mut Window, cx: &Context, ) -> AnyElement { - match &entry { + let primary = match &entry { AgentThreadEntry::UserMessage(message) => div() .py_4() .px_2() @@ -850,6 +850,19 @@ impl AcpThreadView { .px_5() .child(self.render_tool_call(index, tool_call, window, cx)) .into_any(), + }; + + let Some(thread) = self.thread() else { + return primary; + }; + let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); + if index == total_entries - 1 && !is_generating { + v_flex() + .child(primary) + .child(self.render_thread_controls(cx)) + .into_any_element() + } else { + primary } } @@ -2404,7 +2417,7 @@ impl AcpThreadView { } } - fn render_thread_controls(&mut self, cx: &mut Context) -> impl IntoElement { + fn render_thread_controls(&self, cx: &Context) -> impl IntoElement { let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileText) .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) @@ -2425,9 +2438,8 @@ impl AcpThreadView { })); h_flex() - .mt_1() .mr_1() - .py_2() + .pb_2() .px(RESPONSE_PADDING_X) .opacity(0.4) .hover(|style| style.opacity(1.)) @@ -2487,18 +2499,12 @@ impl Render for AcpThreadView { v_flex().flex_1().map(|this| { if self.list_state.item_count() > 0 { - let is_generating = - matches!(thread_clone.read(cx).status(), ThreadStatus::Generating); - this.child( list(self.list_state.clone()) .with_sizing_behavior(gpui::ListSizingBehavior::Auto) .flex_grow() .into_any(), ) - .when(!is_generating, |this| { - this.child(self.render_thread_controls(cx)) - }) .children(match thread_clone.read(cx).status() { ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => { None From dd7fce3f5e3cae7c2e4681671ea00344369ceece Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Tue, 5 Aug 2025 23:49:41 +0300 Subject: [PATCH 091/693] workspace: Remove excess clones (#35664) Removes a few excess clones I found. Minor formatting change by utilizing `map_or` Release Notes: - N/A --- crates/workspace/src/workspace.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 63953ff802..6572574ce1 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1813,10 +1813,7 @@ impl Workspace { .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id)) }); - match latest_project_path_opened { - Some(latest_project_path_opened) => latest_project_path_opened == history_path, - None => true, - } + latest_project_path_opened.map_or(true, |path| path == history_path) }) } @@ -4796,7 +4793,7 @@ impl Workspace { .remote_id(&self.app_state.client, window, cx) .map(|id| id.to_proto()); - if let Some(id) = id.clone() { + if let Some(id) = id { if let Some(variant) = item.to_state_proto(window, cx) { let view = Some(proto::View { id: id.clone(), @@ -4809,7 +4806,7 @@ impl Workspace { update = proto::UpdateActiveView { view, // TODO: Remove after version 0.145.x stabilizes. - id: id.clone(), + id, leader_id: leader_peer_id, }; } From 42699411f6b3d81089d42233621e1f81dbec4eab Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Tue, 5 Aug 2025 15:39:34 -0600 Subject: [PATCH 092/693] Fix update of prompt usage count when using text threads (#35671) Release Notes: - Fixed update of prompt usage count when using agent text threads. Co-authored-by: Oleksiy --- .../src/assistant_context.rs | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/crates/assistant_context/src/assistant_context.rs b/crates/assistant_context/src/assistant_context.rs index 4518bbff79..6a28ec2b94 100644 --- a/crates/assistant_context/src/assistant_context.rs +++ b/crates/assistant_context/src/assistant_context.rs @@ -9,9 +9,9 @@ use assistant_slash_command::{ SlashCommandResult, SlashCommandWorkingSet, }; use assistant_slash_commands::FileCommandMetadata; -use client::{self, Client, proto, telemetry::Telemetry}; +use client::{self, Client, ModelRequestUsage, RequestUsage, proto, telemetry::Telemetry}; use clock::ReplicaId; -use cloud_llm_client::CompletionIntent; +use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit}; use collections::{HashMap, HashSet}; use fs::{Fs, RenameOptions}; use futures::{FutureExt, StreamExt, future::Shared}; @@ -2080,7 +2080,18 @@ impl AssistantContext { }); match event { - LanguageModelCompletionEvent::StatusUpdate { .. } => {} + LanguageModelCompletionEvent::StatusUpdate(status_update) => { + match status_update { + CompletionRequestStatus::UsageUpdated { amount, limit } => { + this.update_model_request_usage( + amount as u32, + limit, + cx, + ); + } + _ => {} + } + } LanguageModelCompletionEvent::StartMessage { .. } => {} LanguageModelCompletionEvent::Stop(reason) => { stop_reason = reason; @@ -2956,6 +2967,21 @@ impl AssistantContext { summary.text = custom_summary; cx.emit(ContextEvent::SummaryChanged); } + + fn update_model_request_usage(&self, amount: u32, limit: UsageLimit, cx: &mut App) { + let Some(project) = &self.project else { + return; + }; + project.read(cx).user_store().update(cx, |user_store, cx| { + user_store.update_model_request_usage( + ModelRequestUsage(RequestUsage { + amount: amount as i32, + limit, + }), + cx, + ) + }); + } } #[derive(Debug, Default)] From 86957a5614335d9bc555ab5ce5f49f4a47fa5b2d Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Tue, 5 Aug 2025 15:47:17 -0600 Subject: [PATCH 093/693] Use the same prompt as agent thread summary for text threads (#35669) This was causing text thread summarization to be counted as a usage of 1 prompt Release Notes: - Fixed bug with agent text threads (not chat threads) counting summarization as a usage of 1 prompt. Co-authored-by: Oleksiy --- crates/agent/src/thread.rs | 6 ++---- crates/agent_settings/src/agent_settings.rs | 3 +++ crates/assistant_context/src/assistant_context.rs | 7 ++----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 8558dd528d..2bb7f358eb 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -8,7 +8,7 @@ use crate::{ }, tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState}, }; -use agent_settings::{AgentProfileId, AgentSettings, CompletionMode}; +use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_PROMPT}; use anyhow::{Result, anyhow}; use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet}; use chrono::{DateTime, Utc}; @@ -2112,12 +2112,10 @@ impl Thread { return; } - let added_user_message = include_str!("./prompts/summarize_thread_prompt.txt"); - let request = self.to_summarize_request( &model.model, CompletionIntent::ThreadSummarization, - added_user_message.into(), + SUMMARIZE_THREAD_PROMPT.into(), cx, ); diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 4e872c78d7..e6a79963d6 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -13,6 +13,9 @@ use std::borrow::Cow; pub use crate::agent_profile::*; +pub const SUMMARIZE_THREAD_PROMPT: &str = + include_str!("../../agent/src/prompts/summarize_thread_prompt.txt"); + pub fn init(cx: &mut App) { AgentSettings::register(cx); } diff --git a/crates/assistant_context/src/assistant_context.rs b/crates/assistant_context/src/assistant_context.rs index 6a28ec2b94..557f9592e4 100644 --- a/crates/assistant_context/src/assistant_context.rs +++ b/crates/assistant_context/src/assistant_context.rs @@ -2,7 +2,7 @@ mod assistant_context_tests; mod context_store; -use agent_settings::AgentSettings; +use agent_settings::{AgentSettings, SUMMARIZE_THREAD_PROMPT}; use anyhow::{Context as _, Result, bail}; use assistant_slash_command::{ SlashCommandContent, SlashCommandEvent, SlashCommandLine, SlashCommandOutputSection, @@ -2688,10 +2688,7 @@ impl AssistantContext { let mut request = self.to_completion_request(Some(&model.model), cx); request.messages.push(LanguageModelRequestMessage { role: Role::User, - content: vec![ - "Generate a concise 3-7 word title for this conversation, omitting punctuation. Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`" - .into(), - ], + content: vec![SUMMARIZE_THREAD_PROMPT.into()], cache: false, }); From f27dc7dec76190862d9c5e2a78371f25c471db54 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 5 Aug 2025 18:07:18 -0400 Subject: [PATCH 094/693] collab: Remove usage meters sync (#35674) This PR removes the usage meters sync from Collab, as it has been moved to Cloud. Release Notes: - N/A --- crates/collab/src/api/billing.rs | 209 +------------------------------ crates/collab/src/main.rs | 26 +--- 2 files changed, 6 insertions(+), 229 deletions(-) diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs index 0e15308ffe..808713ff38 100644 --- a/crates/collab/src/api/billing.rs +++ b/crates/collab/src/api/billing.rs @@ -1,31 +1,23 @@ use anyhow::{Context as _, bail}; use chrono::{DateTime, Utc}; -use cloud_llm_client::LanguageModelProvider; -use collections::{HashMap, HashSet}; use sea_orm::ActiveValue; use std::{sync::Arc, time::Duration}; use stripe::{CancellationDetailsReason, EventObject, EventType, ListEvents, SubscriptionStatus}; -use util::{ResultExt, maybe}; +use util::ResultExt; use crate::AppState; use crate::db::billing_subscription::{ StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind, }; -use crate::llm::db::subscription_usage_meter::{self, CompletionMode}; +use crate::db::{ + CreateBillingCustomerParams, CreateBillingSubscriptionParams, CreateProcessedStripeEventParams, + UpdateBillingCustomerParams, UpdateBillingSubscriptionParams, billing_customer, +}; use crate::rpc::{ResultExt as _, Server}; use crate::stripe_client::{ StripeCancellationDetailsReason, StripeClient, StripeCustomerId, StripeSubscription, StripeSubscriptionId, }; -use crate::{db::UserId, llm::db::LlmDatabase}; -use crate::{ - db::{ - CreateBillingCustomerParams, CreateBillingSubscriptionParams, - CreateProcessedStripeEventParams, UpdateBillingCustomerParams, - UpdateBillingSubscriptionParams, billing_customer, - }, - stripe_billing::StripeBilling, -}; /// The amount of time we wait in between each poll of Stripe events. /// @@ -542,194 +534,3 @@ pub async fn find_or_create_billing_customer( Ok(Some(billing_customer)) } - -const SYNC_LLM_REQUEST_USAGE_WITH_STRIPE_INTERVAL: Duration = Duration::from_secs(60); - -pub fn sync_llm_request_usage_with_stripe_periodically(app: Arc) { - let Some(stripe_billing) = app.stripe_billing.clone() else { - log::warn!("failed to retrieve Stripe billing object"); - return; - }; - let Some(llm_db) = app.llm_db.clone() else { - log::warn!("failed to retrieve LLM database"); - return; - }; - - let executor = app.executor.clone(); - executor.spawn_detached({ - let executor = executor.clone(); - async move { - loop { - sync_model_request_usage_with_stripe(&app, &llm_db, &stripe_billing) - .await - .context("failed to sync LLM request usage to Stripe") - .trace_err(); - executor - .sleep(SYNC_LLM_REQUEST_USAGE_WITH_STRIPE_INTERVAL) - .await; - } - } - }); -} - -async fn sync_model_request_usage_with_stripe( - app: &Arc, - llm_db: &Arc, - stripe_billing: &Arc, -) -> anyhow::Result<()> { - let feature_flags = app.db.list_feature_flags().await?; - let sync_model_request_usage_using_cloud = feature_flags - .iter() - .any(|flag| flag.flag == "cloud-stripe-usage-meters-sync" && flag.enabled_for_all); - if sync_model_request_usage_using_cloud { - return Ok(()); - } - - log::info!("Stripe usage sync: Starting"); - let started_at = Utc::now(); - - let staff_users = app.db.get_staff_users().await?; - let staff_user_ids = staff_users - .iter() - .map(|user| user.id) - .collect::>(); - - let usage_meters = llm_db - .get_current_subscription_usage_meters(Utc::now()) - .await?; - let mut usage_meters_by_user_id = - HashMap::>::default(); - for (usage_meter, usage) in usage_meters { - let meters = usage_meters_by_user_id.entry(usage.user_id).or_default(); - meters.push(usage_meter); - } - - log::info!("Stripe usage sync: Retrieving Zed Pro subscriptions"); - let get_zed_pro_subscriptions_started_at = Utc::now(); - let billing_subscriptions = app.db.get_active_zed_pro_billing_subscriptions().await?; - log::info!( - "Stripe usage sync: Retrieved {} Zed Pro subscriptions in {}", - billing_subscriptions.len(), - Utc::now() - get_zed_pro_subscriptions_started_at - ); - - let claude_sonnet_4 = stripe_billing - .find_price_by_lookup_key("claude-sonnet-4-requests") - .await?; - let claude_sonnet_4_max = stripe_billing - .find_price_by_lookup_key("claude-sonnet-4-requests-max") - .await?; - let claude_opus_4 = stripe_billing - .find_price_by_lookup_key("claude-opus-4-requests") - .await?; - let claude_opus_4_max = stripe_billing - .find_price_by_lookup_key("claude-opus-4-requests-max") - .await?; - let claude_3_5_sonnet = stripe_billing - .find_price_by_lookup_key("claude-3-5-sonnet-requests") - .await?; - let claude_3_7_sonnet = stripe_billing - .find_price_by_lookup_key("claude-3-7-sonnet-requests") - .await?; - let claude_3_7_sonnet_max = stripe_billing - .find_price_by_lookup_key("claude-3-7-sonnet-requests-max") - .await?; - - let model_mode_combinations = [ - ("claude-opus-4", CompletionMode::Max), - ("claude-opus-4", CompletionMode::Normal), - ("claude-sonnet-4", CompletionMode::Max), - ("claude-sonnet-4", CompletionMode::Normal), - ("claude-3-7-sonnet", CompletionMode::Max), - ("claude-3-7-sonnet", CompletionMode::Normal), - ("claude-3-5-sonnet", CompletionMode::Normal), - ]; - - let billing_subscription_count = billing_subscriptions.len(); - - log::info!("Stripe usage sync: Syncing {billing_subscription_count} Zed Pro subscriptions"); - - for (user_id, (billing_customer, billing_subscription)) in billing_subscriptions { - maybe!(async { - if staff_user_ids.contains(&user_id) { - return anyhow::Ok(()); - } - - let stripe_customer_id = - StripeCustomerId(billing_customer.stripe_customer_id.clone().into()); - let stripe_subscription_id = - StripeSubscriptionId(billing_subscription.stripe_subscription_id.clone().into()); - - let usage_meters = usage_meters_by_user_id.get(&user_id); - - for (model, mode) in &model_mode_combinations { - let Ok(model) = - llm_db.model(LanguageModelProvider::Anthropic, model) - else { - log::warn!("Failed to load model for user {user_id}: {model}"); - continue; - }; - - let (price, meter_event_name) = match model.name.as_str() { - "claude-opus-4" => match mode { - CompletionMode::Normal => (&claude_opus_4, "claude_opus_4/requests"), - CompletionMode::Max => (&claude_opus_4_max, "claude_opus_4/requests/max"), - }, - "claude-sonnet-4" => match mode { - CompletionMode::Normal => (&claude_sonnet_4, "claude_sonnet_4/requests"), - CompletionMode::Max => { - (&claude_sonnet_4_max, "claude_sonnet_4/requests/max") - } - }, - "claude-3-5-sonnet" => (&claude_3_5_sonnet, "claude_3_5_sonnet/requests"), - "claude-3-7-sonnet" => match mode { - CompletionMode::Normal => { - (&claude_3_7_sonnet, "claude_3_7_sonnet/requests") - } - CompletionMode::Max => { - (&claude_3_7_sonnet_max, "claude_3_7_sonnet/requests/max") - } - }, - model_name => { - bail!("Attempted to sync usage meter for unsupported model: {model_name:?}") - } - }; - - let model_requests = usage_meters - .and_then(|usage_meters| { - usage_meters - .iter() - .find(|meter| meter.model_id == model.id && meter.mode == *mode) - }) - .map(|usage_meter| usage_meter.requests) - .unwrap_or(0); - - if model_requests > 0 { - stripe_billing - .subscribe_to_price(&stripe_subscription_id, price) - .await?; - } - - stripe_billing - .bill_model_request_usage(&stripe_customer_id, meter_event_name, model_requests) - .await - .with_context(|| { - format!( - "Failed to bill model request usage of {model_requests} for {stripe_customer_id}: {meter_event_name}", - ) - })?; - } - - Ok(()) - }) - .await - .log_err(); - } - - log::info!( - "Stripe usage sync: Synced {billing_subscription_count} Zed Pro subscriptions in {}", - Utc::now() - started_at - ); - - Ok(()) -} diff --git a/crates/collab/src/main.rs b/crates/collab/src/main.rs index 6a78049b3f..0442a69042 100644 --- a/crates/collab/src/main.rs +++ b/crates/collab/src/main.rs @@ -8,7 +8,6 @@ use axum::{ }; use collab::api::CloudflareIpCountryHeader; -use collab::api::billing::sync_llm_request_usage_with_stripe_periodically; use collab::llm::db::LlmDatabase; use collab::migrations::run_database_migrations; use collab::user_backfiller::spawn_user_backfiller; @@ -31,7 +30,7 @@ use tower_http::trace::TraceLayer; use tracing_subscriber::{ Layer, filter::EnvFilter, fmt::format::JsonFields, util::SubscriberInitExt, }; -use util::{ResultExt as _, maybe}; +use util::ResultExt as _; const VERSION: &str = env!("CARGO_PKG_VERSION"); const REVISION: Option<&'static str> = option_env!("GITHUB_SHA"); @@ -133,29 +132,6 @@ async fn main() -> Result<()> { fetch_extensions_from_blob_store_periodically(state.clone()); spawn_user_backfiller(state.clone()); - let llm_db = maybe!(async { - let database_url = state - .config - .llm_database_url - .as_ref() - .context("missing LLM_DATABASE_URL")?; - let max_connections = state - .config - .llm_database_max_connections - .context("missing LLM_DATABASE_MAX_CONNECTIONS")?; - - let mut db_options = db::ConnectOptions::new(database_url); - db_options.max_connections(max_connections); - LlmDatabase::new(db_options, state.executor.clone()).await - }) - .await - .trace_err(); - - if let Some(mut llm_db) = llm_db { - llm_db.initialize().await?; - sync_llm_request_usage_with_stripe_periodically(state.clone()); - } - app = app .merge(collab::api::events::router()) .merge(collab::api::extensions::router()) From b7469f5bc346ca9723a3595f6f00c5fe6a3f7a6c Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Tue, 5 Aug 2025 19:10:51 -0300 Subject: [PATCH 095/693] Fix ACP connection and thread leak (#35670) When you switched away from an ACP thread, the `AcpThreadView` entity (and thus thread, and subprocess) was leaked. This happened because we were using `cx.processor` for the `list` state callback, which uses a strong reference. This PR changes the callback so that it holds a weak reference, and adds some tests and assertions at various levels to make sure we don't reintroduce the leak in the future. Release Notes: - N/A --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- crates/agent_servers/src/acp/v0.rs | 1 + crates/agent_servers/src/acp/v1.rs | 15 ++++++++----- crates/agent_servers/src/claude.rs | 5 ++--- crates/agent_servers/src/e2e_tests.rs | 27 ++++++++++++++++++++++++ crates/agent_ui/src/acp/thread_view.rs | 29 ++++++++++++++++++-------- crates/agent_ui/src/agent_panel.rs | 17 ++++++++------- 8 files changed, 73 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4cf5a68f1d..4803c0de8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,9 +137,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.18" +version = "0.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8e4c1dccb35e69d32566f0d11948d902f9942fc3f038821816c1150cf5925f4" +checksum = "12dbfec3d27680337ed9d3064eecafe97acf0b0f190148bb4e29d96707c9e403" dependencies = [ "anyhow", "futures 0.3.31", diff --git a/Cargo.toml b/Cargo.toml index 733db92ce9..05ceb3bd14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -423,7 +423,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.18" +agent-client-protocol = "0.0.20" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index 3dcda4ce8d..fda28fa176 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -380,6 +380,7 @@ impl AcpConnection { let stdin = child.stdin.take().unwrap(); let stdout = child.stdout.take().unwrap(); + log::trace!("Spawned (pid: {})", child.id()); let foreground_executor = cx.foreground_executor().clone(); diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index a4f0e996b5..0b6fa1c48b 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -19,7 +19,6 @@ pub struct AcpConnection { sessions: Rc>>, auth_methods: Vec, _io_task: Task>, - _child: smol::process::Child, } pub struct AcpSession { @@ -47,6 +46,7 @@ impl AcpConnection { let stdout = child.stdout.take().expect("Failed to take stdout"); let stdin = child.stdin.take().expect("Failed to take stdin"); + log::trace!("Spawned (pid: {})", child.id()); let sessions = Rc::new(RefCell::new(HashMap::default())); @@ -61,7 +61,11 @@ impl AcpConnection { } }); - let io_task = cx.background_spawn(io_task); + let io_task = cx.background_spawn(async move { + io_task.await?; + drop(child); + Ok(()) + }); let response = connection .initialize(acp::InitializeRequest { @@ -84,7 +88,6 @@ impl AcpConnection { connection: connection.into(), server_name, sessions, - _child: child, _io_task: io_task, }) } @@ -155,8 +158,10 @@ impl AgentConnection for AcpConnection { fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task> { let conn = self.connection.clone(); - cx.foreground_executor() - .spawn(async move { Ok(conn.prompt(params).await?) }) + cx.foreground_executor().spawn(async move { + conn.prompt(params).await?; + Ok(()) + }) } fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 9040b83085..b097c0345c 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -125,8 +125,7 @@ impl AgentConnection for ClaudeAgentConnection { session_id.clone(), &mcp_config_path, &cwd, - ) - .await?; + )?; let pid = child.id(); log::trace!("Spawned (pid: {})", pid); @@ -262,7 +261,7 @@ enum ClaudeSessionMode { Resume, } -async fn spawn_claude( +fn spawn_claude( command: &AgentServerCommand, mode: ClaudeSessionMode, session_id: acp::SessionId, diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index a60aefb7b9..05f874bd30 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -311,6 +311,27 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon }); } +pub async fn test_thread_drop(server: impl AgentServer + 'static, cx: &mut TestAppContext) { + let fs = init_test(cx).await; + let project = Project::test(fs, [], cx).await; + let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; + + thread + .update(cx, |thread, cx| thread.send_raw("Hello from test!", cx)) + .await + .unwrap(); + + thread.read_with(cx, |thread, _| { + assert!(thread.entries().len() >= 2, "Expected at least 2 entries"); + }); + + let weak_thread = thread.downgrade(); + drop(thread); + + cx.executor().run_until_parked(); + assert!(!weak_thread.is_upgradable()); +} + #[macro_export] macro_rules! common_e2e_tests { ($server:expr, allow_option_id = $allow_option_id:expr) => { @@ -351,6 +372,12 @@ macro_rules! common_e2e_tests { async fn cancel(cx: &mut ::gpui::TestAppContext) { $crate::e2e_tests::test_cancel($server, cx).await; } + + #[::gpui::test] + #[cfg_attr(not(feature = "e2e"), ignore)] + async fn thread_drop(cx: &mut ::gpui::TestAppContext) { + $crate::e2e_tests::test_thread_drop($server, cx).await; + } } }; } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index cecf989a23..7c6f315fb6 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -169,12 +169,13 @@ impl AcpThreadView { let mention_set = mention_set.clone(); - let list_state = ListState::new( - 0, - gpui::ListAlignment::Bottom, - px(2048.0), - cx.processor({ - move |this: &mut Self, index: usize, window, cx| { + let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0), { + let this = cx.entity().downgrade(); + move |index: usize, window, cx| { + let Some(this) = this.upgrade() else { + return Empty.into_any(); + }; + this.update(cx, |this, cx| { let Some((entry, len)) = this.thread().and_then(|thread| { let entries = &thread.read(cx).entries(); Some((entries.get(index)?, entries.len())) @@ -182,9 +183,9 @@ impl AcpThreadView { return Empty.into_any(); }; this.render_entry(index, len, entry, window, cx) - } - }), - ); + }) + } + }); Self { agent: agent.clone(), @@ -2719,6 +2720,16 @@ mod tests { use super::*; + #[gpui::test] + async fn test_drop(cx: &mut TestAppContext) { + init_test(cx); + + let (thread_view, _cx) = setup_thread_view(StubAgentServer::default(), cx).await; + let weak_view = thread_view.downgrade(); + drop(thread_view); + assert!(!weak_view.is_upgradable()); + } + #[gpui::test] async fn test_notification_for_stop_event(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 5f3315f69a..717778bb98 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -970,13 +970,7 @@ impl AgentPanel { ) }); - this.set_active_view( - ActiveView::ExternalAgentThread { - thread_view: thread_view.clone(), - }, - window, - cx, - ); + this.set_active_view(ActiveView::ExternalAgentThread { thread_view }, window, cx); }) }) .detach_and_log_err(cx); @@ -1477,6 +1471,7 @@ impl AgentPanel { let current_is_special = current_is_history || current_is_config; let new_is_special = new_is_history || new_is_config; + let mut old_acp_thread = None; match &self.active_view { ActiveView::Thread { thread, .. } => { @@ -1488,6 +1483,9 @@ impl AgentPanel { }); } } + ActiveView::ExternalAgentThread { thread_view } => { + old_acp_thread.replace(thread_view.downgrade()); + } _ => {} } @@ -1518,6 +1516,11 @@ impl AgentPanel { self.active_view = new_view; } + debug_assert!( + old_acp_thread.map_or(true, |thread| !thread.is_upgradable()), + "AcpThreadView leaked" + ); + self.acp_message_history.borrow_mut().reset_position(); self.focus_handle(cx).focus(window); From 0025019db431ab7671079baaabaf0de872730160 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Wed, 6 Aug 2025 06:15:30 +0800 Subject: [PATCH 096/693] gpui: Press `enter`, `space` to trigger click to focused element (#35075) Release Notes: - N/A > Any user interaction that is equivalent to a click, such as pressing the Space key or Enter key while the element is focused. Note that this only applies to elements with a default key event handler, and therefore, excludes other elements that have been made focusable by setting the [tabindex](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/tabindex) attribute. https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event --------- Co-authored-by: Anthony Co-authored-by: Mikayla Maki Co-authored-by: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> --- crates/agent_ui/src/context_strip.rs | 2 +- crates/collab_ui/src/collab_panel.rs | 2 +- .../src/session/running/variable_list.rs | 2 +- crates/editor/src/editor.rs | 10 +- crates/editor/src/element.rs | 24 ++-- crates/gpui/examples/tab_stop.rs | 20 ++- crates/gpui/examples/window_shadow.rs | 4 +- crates/gpui/src/elements/div.rs | 60 +++++++-- crates/gpui/src/interactive.rs | 123 ++++++++++++++++-- crates/gpui/src/window.rs | 2 + .../src/markdown_preview_view.rs | 2 +- crates/outline_panel/src/outline_panel.rs | 4 +- crates/project_panel/src/project_panel.rs | 16 ++- crates/recent_projects/src/remote_servers.rs | 2 +- crates/settings_ui/src/keybindings.rs | 2 +- crates/settings_ui/src/ui_components/table.rs | 4 +- crates/title_bar/src/platform_title_bar.rs | 4 +- .../ui/src/components/button/button_like.rs | 7 +- crates/workspace/src/pane.rs | 4 +- 19 files changed, 231 insertions(+), 63 deletions(-) diff --git a/crates/agent_ui/src/context_strip.rs b/crates/agent_ui/src/context_strip.rs index 080ffd2ea0..369964f165 100644 --- a/crates/agent_ui/src/context_strip.rs +++ b/crates/agent_ui/src/context_strip.rs @@ -504,7 +504,7 @@ impl Render for ContextStrip { ) .on_click({ Rc::new(cx.listener(move |this, event: &ClickEvent, window, cx| { - if event.down.click_count > 1 { + if event.click_count() > 1 { this.open_context(&context, window, cx); } else { this.focused_index = Some(i); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 689591df12..bae5dcfdb5 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2605,7 +2605,7 @@ impl CollabPanel { let contact = contact.clone(); move |this, event: &ClickEvent, window, cx| { this.deploy_contact_context_menu( - event.down.position, + event.position(), contact.clone(), window, cx, diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index 906e482687..efbc72e8cf 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -1107,7 +1107,7 @@ impl VariableList { let variable_value = value.clone(); this.on_click(cx.listener( move |this, click: &ClickEvent, window, cx| { - if click.down.click_count < 2 { + if click.click_count() < 2 { return; } let editor = Self::create_variable_editor( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ff9b703d66..30feca7402 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8183,7 +8183,7 @@ impl Editor { editor.set_breakpoint_context_menu( row, Some(position), - event.down.position, + event.position(), window, cx, ); @@ -8350,7 +8350,11 @@ impl Editor { .icon_color(color) .toggle_state(is_active) .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| { - let quick_launch = e.down.button == MouseButton::Left; + let quick_launch = match e { + ClickEvent::Keyboard(_) => true, + ClickEvent::Mouse(e) => e.down.button == MouseButton::Left, + }; + window.focus(&editor.focus_handle(cx)); editor.toggle_code_actions( &ToggleCodeActions { @@ -8362,7 +8366,7 @@ impl Editor { ); })) .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { - editor.set_breakpoint_context_menu(row, position, event.down.position, window, cx); + editor.set_breakpoint_context_menu(row, position, event.position(), window, cx); })) } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 268855ab61..e1647215bc 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -43,11 +43,11 @@ use gpui::{ Bounds, ClickEvent, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, Keystroke, Length, - ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, - ParentElement, Pixels, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, - Size, StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, WeakEntity, - Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, - quad, relative, size, solid_background, transparent_black, + ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, + MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle, ScrollWheelEvent, + ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, TextRun, + TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop, + linear_gradient, outline, point, px, quad, relative, size, solid_background, transparent_black, }; use itertools::Itertools; use language::language_settings::{ @@ -949,8 +949,12 @@ impl EditorElement { let hovered_link_modifier = Editor::multi_cursor_modifier(false, &event.modifiers(), cx); - if !pending_nonempty_selections && hovered_link_modifier && text_hitbox.is_hovered(window) { - let point = position_map.point_for_position(event.up.position); + if let Some(mouse_position) = event.mouse_position() + && !pending_nonempty_selections + && hovered_link_modifier + && text_hitbox.is_hovered(window) + { + let point = position_map.point_for_position(mouse_position); editor.handle_click_hovered_link(point, event.modifiers(), window, cx); editor.selection_drag_state = SelectionDragState::None; @@ -3735,7 +3739,7 @@ impl EditorElement { move |editor, e: &ClickEvent, window, cx| { editor.open_excerpts_common( Some(jump_data.clone()), - e.down.modifiers.secondary(), + e.modifiers().secondary(), window, cx, ); @@ -6882,10 +6886,10 @@ impl EditorElement { // Fire click handlers during the bubble phase. DispatchPhase::Bubble => editor.update(cx, |editor, cx| { if let Some(mouse_down) = captured_mouse_down.take() { - let event = ClickEvent { + let event = ClickEvent::Mouse(MouseClickEvent { down: mouse_down, up: event.clone(), - }; + }); Self::click(editor, &event, &position_map, window, cx); } }), diff --git a/crates/gpui/examples/tab_stop.rs b/crates/gpui/examples/tab_stop.rs index 1f6500f3e6..8dbcbeccb7 100644 --- a/crates/gpui/examples/tab_stop.rs +++ b/crates/gpui/examples/tab_stop.rs @@ -111,8 +111,24 @@ impl Render for Example { .flex_row() .gap_3() .items_center() - .child(button("el1").tab_index(4).child("Button 1")) - .child(button("el2").tab_index(5).child("Button 2")), + .child( + button("el1") + .tab_index(4) + .child("Button 1") + .on_click(cx.listener(|this, _, _, cx| { + this.message = "You have clicked Button 1.".into(); + cx.notify(); + })), + ) + .child( + button("el2") + .tab_index(5) + .child("Button 2") + .on_click(cx.listener(|this, _, _, cx| { + this.message = "You have clicked Button 2.".into(); + cx.notify(); + })), + ), ) } } diff --git a/crates/gpui/examples/window_shadow.rs b/crates/gpui/examples/window_shadow.rs index 06dde91133..469017da79 100644 --- a/crates/gpui/examples/window_shadow.rs +++ b/crates/gpui/examples/window_shadow.rs @@ -165,8 +165,8 @@ impl Render for WindowShadow { }, ) .on_click(|e, window, _| { - if e.down.button == MouseButton::Right { - window.show_window_menu(e.up.position); + if e.is_right_click() { + window.show_window_menu(e.position()); } }) .text_color(black()) diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index fa47758581..09afbff929 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -19,10 +19,10 @@ use crate::{ Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent, DispatchPhase, Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, Hitbox, HitboxBehavior, HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, - LayoutId, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, - Overflow, ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style, - StyleRefinement, Styled, Task, TooltipId, Visibility, Window, WindowControlArea, point, px, - size, + KeyboardButton, KeyboardClickEvent, LayoutId, ModifiersChangedEvent, MouseButton, + MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Overflow, ParentElement, Pixels, + Point, Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task, + TooltipId, Visibility, Window, WindowControlArea, point, px, size, }; use collections::HashMap; use refineable::Refineable; @@ -484,10 +484,9 @@ impl Interactivity { where Self: Sized, { - self.click_listeners - .push(Box::new(move |event, window, cx| { - listener(event, window, cx) - })); + self.click_listeners.push(Rc::new(move |event, window, cx| { + listener(event, window, cx) + })); } /// On drag initiation, this callback will be used to create a new view to render the dragged value for a @@ -1156,7 +1155,7 @@ pub(crate) type MouseMoveListener = pub(crate) type ScrollWheelListener = Box; -pub(crate) type ClickListener = Box; +pub(crate) type ClickListener = Rc; pub(crate) type DragListener = Box, &mut Window, &mut App) -> AnyView + 'static>; @@ -1950,6 +1949,12 @@ impl Interactivity { window: &mut Window, cx: &mut App, ) { + let is_focused = self + .tracked_focus_handle + .as_ref() + .map(|handle| handle.is_focused(window)) + .unwrap_or(false); + // If this element can be focused, register a mouse down listener // that will automatically transfer focus when hitting the element. // This behavior can be suppressed by using `cx.prevent_default()`. @@ -2113,6 +2118,39 @@ impl Interactivity { } }); + if is_focused { + // Press enter, space to trigger click, when the element is focused. + window.on_key_event({ + let click_listeners = click_listeners.clone(); + let hitbox = hitbox.clone(); + move |event: &KeyUpEvent, phase, window, cx| { + if phase.bubble() && !window.default_prevented() { + let stroke = &event.keystroke; + let keyboard_button = if stroke.key.eq("enter") { + Some(KeyboardButton::Enter) + } else if stroke.key.eq("space") { + Some(KeyboardButton::Space) + } else { + None + }; + + if let Some(button) = keyboard_button + && !stroke.modifiers.modified() + { + let click_event = ClickEvent::Keyboard(KeyboardClickEvent { + button, + bounds: hitbox.bounds, + }); + + for listener in &click_listeners { + listener(&click_event, window, cx); + } + } + } + } + }); + } + window.on_mouse_event({ let mut captured_mouse_down = None; let hitbox = hitbox.clone(); @@ -2138,10 +2176,10 @@ impl Interactivity { // Fire click handlers during the bubble phase. DispatchPhase::Bubble => { if let Some(mouse_down) = captured_mouse_down.take() { - let mouse_click = ClickEvent { + let mouse_click = ClickEvent::Mouse(MouseClickEvent { down: mouse_down, up: event.clone(), - }; + }); for listener in &click_listeners { listener(&mouse_click, window, cx); } diff --git a/crates/gpui/src/interactive.rs b/crates/gpui/src/interactive.rs index edd807da11..46af946e69 100644 --- a/crates/gpui/src/interactive.rs +++ b/crates/gpui/src/interactive.rs @@ -1,6 +1,6 @@ use crate::{ - Capslock, Context, Empty, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, Window, - point, seal::Sealed, + Bounds, Capslock, Context, Empty, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, + Window, point, seal::Sealed, }; use smallvec::SmallVec; use std::{any::Any, fmt::Debug, ops::Deref, path::PathBuf}; @@ -141,7 +141,7 @@ impl MouseEvent for MouseUpEvent {} /// A click event, generated when a mouse button is pressed and released. #[derive(Clone, Debug, Default)] -pub struct ClickEvent { +pub struct MouseClickEvent { /// The mouse event when the button was pressed. pub down: MouseDownEvent, @@ -149,18 +149,119 @@ pub struct ClickEvent { pub up: MouseUpEvent, } +/// A click event that was generated by a keyboard button being pressed and released. +#[derive(Clone, Debug)] +pub struct KeyboardClickEvent { + /// The keyboard button that was pressed to trigger the click. + pub button: KeyboardButton, + + /// The bounds of the element that was clicked. + pub bounds: Bounds, +} + +/// A click event, generated when a mouse button or keyboard button is pressed and released. +#[derive(Clone, Debug)] +pub enum ClickEvent { + /// A click event trigger by a mouse button being pressed and released. + Mouse(MouseClickEvent), + /// A click event trigger by a keyboard button being pressed and released. + Keyboard(KeyboardClickEvent), +} + impl ClickEvent { - /// Returns the modifiers that were held down during both the - /// mouse down and mouse up events + /// Returns the modifiers that were held during the click event + /// + /// `Keyboard`: The keyboard click events never have modifiers. + /// `Mouse`: Modifiers that were held during the mouse key up event. pub fn modifiers(&self) -> Modifiers { - Modifiers { - control: self.up.modifiers.control && self.down.modifiers.control, - alt: self.up.modifiers.alt && self.down.modifiers.alt, - shift: self.up.modifiers.shift && self.down.modifiers.shift, - platform: self.up.modifiers.platform && self.down.modifiers.platform, - function: self.up.modifiers.function && self.down.modifiers.function, + match self { + // Click events are only generated from keyboard events _without any modifiers_, so we know the modifiers are always Default + ClickEvent::Keyboard(_) => Modifiers::default(), + // Click events on the web only reflect the modifiers for the keyup event, + // tested via observing the behavior of the `ClickEvent.shiftKey` field in Chrome 138 + // under various combinations of modifiers and keyUp / keyDown events. + ClickEvent::Mouse(event) => event.up.modifiers, } } + + /// Returns the position of the click event + /// + /// `Keyboard`: The bottom left corner of the clicked hitbox + /// `Mouse`: The position of the mouse when the button was released. + pub fn position(&self) -> Point { + match self { + ClickEvent::Keyboard(event) => event.bounds.bottom_left(), + ClickEvent::Mouse(event) => event.up.position, + } + } + + /// Returns the mouse position of the click event + /// + /// `Keyboard`: None + /// `Mouse`: The position of the mouse when the button was released. + pub fn mouse_position(&self) -> Option> { + match self { + ClickEvent::Keyboard(_) => None, + ClickEvent::Mouse(event) => Some(event.up.position), + } + } + + /// Returns if this was a right click + /// + /// `Keyboard`: false + /// `Mouse`: Whether the right button was pressed and released + pub fn is_right_click(&self) -> bool { + match self { + ClickEvent::Keyboard(_) => false, + ClickEvent::Mouse(event) => { + event.down.button == MouseButton::Right && event.up.button == MouseButton::Right + } + } + } + + /// Returns whether the click was a standard click + /// + /// `Keyboard`: Always true + /// `Mouse`: Left button pressed and released + pub fn standard_click(&self) -> bool { + match self { + ClickEvent::Keyboard(_) => true, + ClickEvent::Mouse(event) => { + event.down.button == MouseButton::Left && event.up.button == MouseButton::Left + } + } + } + + /// Returns whether the click focused the element + /// + /// `Keyboard`: false, keyboard clicks only work if an element is already focused + /// `Mouse`: Whether this was the first focusing click + pub fn first_focus(&self) -> bool { + match self { + ClickEvent::Keyboard(_) => false, + ClickEvent::Mouse(event) => event.down.first_mouse, + } + } + + /// Returns the click count of the click event + /// + /// `Keyboard`: Always 1 + /// `Mouse`: Count of clicks from MouseUpEvent + pub fn click_count(&self) -> usize { + match self { + ClickEvent::Keyboard(_) => 1, + ClickEvent::Mouse(event) => event.up.click_count, + } + } +} + +/// An enum representing the keyboard button that was pressed for a click event. +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)] +pub enum KeyboardButton { + /// Enter key was clicked + Enter, + /// Space key was clicked + Space, } /// An enum representing the mouse button that was pressed. diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 9e4c1c26c5..8610993994 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -79,11 +79,13 @@ pub enum DispatchPhase { impl DispatchPhase { /// Returns true if this represents the "bubble" phase. + #[inline] pub fn bubble(self) -> bool { self == DispatchPhase::Bubble } /// Returns true if this represents the "capture" phase. + #[inline] pub fn capture(self) -> bool { self == DispatchPhase::Capture } diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index 03cfd7ee82..96e92de19c 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -261,7 +261,7 @@ impl MarkdownPreviewView { .group("markdown-block") .on_click(cx.listener( move |this, event: &ClickEvent, window, cx| { - if event.down.click_count == 2 { + if event.click_count() == 2 { if let Some(source_range) = this .contents .as_ref() diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index ad96670db9..1cda3897ec 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -2570,11 +2570,11 @@ impl OutlinePanel { .on_click({ let clicked_entry = rendered_entry.clone(); cx.listener(move |outline_panel, event: &gpui::ClickEvent, window, cx| { - if event.down.button == MouseButton::Right || event.down.first_mouse { + if event.is_right_click() || event.first_focus() { return; } - let change_focus = event.down.click_count > 1; + let change_focus = event.click_count() > 1; outline_panel.toggle_expanded(&clicked_entry, window, cx); outline_panel.scroll_editor_to_entry( diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 05e6bfe4df..048b9e73d0 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -4157,13 +4157,12 @@ impl ProjectPanel { ) .on_click( cx.listener(move |this, event: &gpui::ClickEvent, window, cx| { - if event.down.button == MouseButton::Right - || event.down.first_mouse + if event.is_right_click() || event.first_focus() || show_editor { return; } - if event.down.button == MouseButton::Left { + if event.standard_click() { this.mouse_down = false; } cx.stop_propagation(); @@ -4203,7 +4202,7 @@ impl ProjectPanel { this.marked_entries.insert(clicked_entry); } } else if event.modifiers().secondary() { - if event.down.click_count > 1 { + if event.click_count() > 1 { this.split_entry(entry_id, cx); } else { this.selection = Some(selection); @@ -4237,7 +4236,7 @@ impl ProjectPanel { } } else { let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled; - let click_count = event.up.click_count; + let click_count = event.click_count(); let focus_opened_item = !preview_tabs_enabled || click_count > 1; let allow_preview = preview_tabs_enabled && click_count == 1; this.open_entry(entry_id, focus_opened_item, allow_preview, cx); @@ -5138,7 +5137,10 @@ impl Render for ProjectPanel { this.hide_scrollbar(window, cx); } })) - .on_click(cx.listener(|this, _event, _, cx| { + .on_click(cx.listener(|this, event, _, cx| { + if matches!(event, gpui::ClickEvent::Keyboard(_)) { + return; + } cx.stop_propagation(); this.selection = None; this.marked_entries.clear(); @@ -5179,7 +5181,7 @@ impl Render for ProjectPanel { .on_action(cx.listener(Self::paste)) .on_action(cx.listener(Self::duplicate)) .on_click(cx.listener(|this, event: &gpui::ClickEvent, window, cx| { - if event.up.click_count > 1 { + if event.click_count() > 1 { if let Some(entry_id) = this.last_worktree_root_id { let project = this.project.read(cx); diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 655e24860a..354434a7fc 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -953,7 +953,7 @@ impl RemoteServerProjects { ) .child(Label::new(project.paths.join(", "))) .on_click(cx.listener(move |this, e: &ClickEvent, window, cx| { - let secondary_confirm = e.down.modifiers.platform; + let secondary_confirm = e.modifiers().platform; callback(this, secondary_confirm, window, cx) })) .when(is_from_zed, |server_list_item| { diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 70afe1729c..81c461fed6 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -1855,7 +1855,7 @@ impl Render for KeymapEditor { .on_click(cx.listener( move |this, event: &ClickEvent, window, cx| { this.select_index(row_index, None, window, cx); - if event.up.click_count == 2 { + if event.click_count() == 2 { this.open_edit_keybinding_modal( false, window, cx, ); diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index 3c9992bd68..2b3e815f36 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/src/ui_components/table.rs @@ -248,7 +248,7 @@ impl TableInteractionState { .cursor_col_resize() .when_some(columns.clone(), |this, columns| { this.on_click(move |event, window, cx| { - if event.down.click_count >= 2 { + if event.click_count() >= 2 { columns.update(cx, |columns, _| { columns.on_double_click( column_ix, @@ -997,7 +997,7 @@ pub fn render_header( |this, (column_widths, resizables, initial_sizes)| { if resizables[header_idx].is_resizable() { this.on_click(move |event, window, cx| { - if event.down.click_count > 1 { + if event.click_count() > 1 { column_widths .update(cx, |column, _| { column.on_double_click( diff --git a/crates/title_bar/src/platform_title_bar.rs b/crates/title_bar/src/platform_title_bar.rs index 30b1b4c3f8..ef6ef93eed 100644 --- a/crates/title_bar/src/platform_title_bar.rs +++ b/crates/title_bar/src/platform_title_bar.rs @@ -106,14 +106,14 @@ impl Render for PlatformTitleBar { // Note: On Windows the title bar behavior is handled by the platform implementation. .when(self.platform_style == PlatformStyle::Mac, |this| { this.on_click(|event, window, _| { - if event.up.click_count == 2 { + if event.click_count() == 2 { window.titlebar_double_click(); } }) }) .when(self.platform_style == PlatformStyle::Linux, |this| { this.on_click(|event, window, _| { - if event.up.click_count == 2 { + if event.click_count() == 2 { window.zoom_window(); } }) diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index 15ab00e7e5..35c78fbb5d 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -1,7 +1,8 @@ use documented::Documented; use gpui::{ AnyElement, AnyView, ClickEvent, CursorStyle, DefiniteLength, Hsla, MouseButton, - MouseDownEvent, MouseUpEvent, Rems, StyleRefinement, relative, transparent_black, + MouseClickEvent, MouseDownEvent, MouseUpEvent, Rems, StyleRefinement, relative, + transparent_black, }; use smallvec::SmallVec; @@ -620,7 +621,7 @@ impl RenderOnce for ButtonLike { MouseButton::Right, move |event, window, cx| { cx.stop_propagation(); - let click_event = ClickEvent { + let click_event = ClickEvent::Mouse(MouseClickEvent { down: MouseDownEvent { button: MouseButton::Right, position: event.position, @@ -634,7 +635,7 @@ impl RenderOnce for ButtonLike { modifiers: event.modifiers, click_count: 1, }, - }; + }); (on_right_click)(&click_event, window, cx) }, ) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 2062255f4b..fff15d2b52 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2945,7 +2945,7 @@ impl Pane { this.handle_external_paths_drop(paths, window, cx) })) .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| { - if event.up.click_count == 2 { + if event.click_count() == 2 { window.dispatch_action( this.double_click_dispatch_action.boxed_clone(), cx, @@ -3640,7 +3640,7 @@ impl Render for Pane { .justify_center() .on_click(cx.listener( move |this, event: &ClickEvent, window, cx| { - if event.up.click_count == 2 { + if event.click_count() == 2 { window.dispatch_action( this.double_click_dispatch_action.boxed_clone(), cx, From 30414d154e550b19299748d3ed15eb1a07eb49ec Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:22:48 -0300 Subject: [PATCH 097/693] onboarding: Adjust the AI upsell card depending on user's state (#35658) Use includes centralizing what each plan delivers in one single file (`plan_definitions.rs`). Release Notes: - N/A --- assets/images/certified_user_stamp.svg | 1 + assets/images/pro_trial_stamp.svg | 1 + crates/agent_ui/src/ui/end_trial_upsell.rs | 20 +- .../src/agent_api_keys_onboarding.rs | 6 +- crates/ai_onboarding/src/ai_onboarding.rs | 326 ++++++++---------- crates/ai_onboarding/src/ai_upsell_card.rs | 229 ++++++++---- crates/ai_onboarding/src/plan_definitions.rs | 39 +++ .../ai_onboarding/src/young_account_banner.rs | 1 + crates/ui/src/components/image.rs | 17 +- crates/ui/src/components/list.rs | 2 + .../src/components/list/list_bullet_item.rs | 40 +++ 11 files changed, 398 insertions(+), 284 deletions(-) create mode 100644 assets/images/certified_user_stamp.svg create mode 100644 assets/images/pro_trial_stamp.svg create mode 100644 crates/ai_onboarding/src/plan_definitions.rs create mode 100644 crates/ui/src/components/list/list_bullet_item.rs diff --git a/assets/images/certified_user_stamp.svg b/assets/images/certified_user_stamp.svg new file mode 100644 index 0000000000..7e65c4fc9d --- /dev/null +++ b/assets/images/certified_user_stamp.svg @@ -0,0 +1 @@ + diff --git a/assets/images/pro_trial_stamp.svg b/assets/images/pro_trial_stamp.svg new file mode 100644 index 0000000000..501de88a48 --- /dev/null +++ b/assets/images/pro_trial_stamp.svg @@ -0,0 +1 @@ + diff --git a/crates/agent_ui/src/ui/end_trial_upsell.rs b/crates/agent_ui/src/ui/end_trial_upsell.rs index 36770c2197..0d9751afec 100644 --- a/crates/agent_ui/src/ui/end_trial_upsell.rs +++ b/crates/agent_ui/src/ui/end_trial_upsell.rs @@ -1,9 +1,9 @@ use std::sync::Arc; -use ai_onboarding::{AgentPanelOnboardingCard, BulletItem}; +use ai_onboarding::{AgentPanelOnboardingCard, PlanDefinitions}; use client::zed_urls; use gpui::{AnyElement, App, IntoElement, RenderOnce, Window}; -use ui::{Divider, List, Tooltip, prelude::*}; +use ui::{Divider, Tooltip, prelude::*}; #[derive(IntoElement, RegisterComponent)] pub struct EndTrialUpsell { @@ -18,6 +18,8 @@ impl EndTrialUpsell { impl RenderOnce for EndTrialUpsell { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let plan_definitions = PlanDefinitions; + let pro_section = v_flex() .gap_1() .child( @@ -31,13 +33,7 @@ impl RenderOnce for EndTrialUpsell { ) .child(Divider::horizontal()), ) - .child( - List::new() - .child(BulletItem::new("500 prompts with Claude models")) - .child(BulletItem::new( - "Unlimited edit predictions with Zeta, our open-source model", - )), - ) + .child(plan_definitions.pro_plan(false)) .child( Button::new("cta-button", "Upgrade to Zed Pro") .full_width() @@ -68,11 +64,7 @@ impl RenderOnce for EndTrialUpsell { ) .child(Divider::horizontal()), ) - .child( - List::new() - .child(BulletItem::new("50 prompts with the Claude models")) - .child(BulletItem::new("2,000 accepted edit predictions")), - ); + .child(plan_definitions.free_plan()); AgentPanelOnboardingCard::new() .child(Headline::new("Your Zed Pro Trial has expired")) diff --git a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs index e86568fe7a..b55ad4c895 100644 --- a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs +++ b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs @@ -1,8 +1,6 @@ use gpui::{Action, IntoElement, ParentElement, RenderOnce, point}; use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID}; -use ui::{Divider, List, prelude::*}; - -use crate::BulletItem; +use ui::{Divider, List, ListBulletItem, prelude::*}; pub struct ApiKeysWithProviders { configured_providers: Vec<(IconName, SharedString)>, @@ -128,7 +126,7 @@ impl RenderOnce for ApiKeysWithoutProviders { ) .child(Divider::horizontal()), ) - .child(List::new().child(BulletItem::new( + .child(List::new().child(ListBulletItem::new( "Add your own keys to use AI without signing in.", ))) .child( diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index c252b65f20..9372a33fed 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -3,6 +3,7 @@ mod agent_panel_onboarding_card; mod agent_panel_onboarding_content; mod ai_upsell_card; mod edit_prediction_onboarding_content; +mod plan_definitions; mod young_account_banner; pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProviders}; @@ -11,51 +12,14 @@ pub use agent_panel_onboarding_content::AgentPanelOnboarding; pub use ai_upsell_card::AiUpsellCard; use cloud_llm_client::Plan; pub use edit_prediction_onboarding_content::EditPredictionOnboarding; +pub use plan_definitions::PlanDefinitions; pub use young_account_banner::YoungAccountBanner; use std::sync::Arc; use client::{Client, UserStore, zed_urls}; -use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString}; -use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*}; - -#[derive(IntoElement)] -pub struct BulletItem { - label: SharedString, -} - -impl BulletItem { - pub fn new(label: impl Into) -> Self { - Self { - label: label.into(), - } - } -} - -impl RenderOnce for BulletItem { - fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { - let line_height = 0.85 * window.line_height(); - - ListItem::new("list-item") - .selectable(false) - .child( - h_flex() - .w_full() - .min_w_0() - .gap_1() - .items_start() - .child( - h_flex().h(line_height).justify_center().child( - Icon::new(IconName::Dash) - .size(IconSize::XSmall) - .color(Color::Hidden), - ), - ) - .child(div().w_full().min_w_0().child(Label::new(self.label))), - ) - .into_any_element() - } -} +use gpui::{AnyElement, Entity, IntoElement, ParentElement}; +use ui::{Divider, RegisterComponent, TintColor, Tooltip, prelude::*}; #[derive(PartialEq)] pub enum SignInStatus { @@ -130,107 +94,6 @@ impl ZedAiOnboarding { self } - fn free_plan_definition(&self, cx: &mut App) -> impl IntoElement { - v_flex() - .mt_2() - .gap_1() - .child( - h_flex() - .gap_2() - .child( - Label::new("Free") - .size(LabelSize::Small) - .color(Color::Muted) - .buffer_font(cx), - ) - .child( - Label::new("(Current Plan)") - .size(LabelSize::Small) - .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.6))) - .buffer_font(cx), - ) - .child(Divider::horizontal()), - ) - .child( - List::new() - .child(BulletItem::new("50 prompts per month with Claude models")) - .child(BulletItem::new( - "2,000 accepted edit predictions with Zeta, our open-source model", - )), - ) - } - - fn pro_trial_definition(&self) -> impl IntoElement { - List::new() - .child(BulletItem::new("150 prompts with Claude models")) - .child(BulletItem::new( - "Unlimited accepted edit predictions with Zeta, our open-source model", - )) - } - - fn pro_plan_definition(&self, cx: &mut App) -> impl IntoElement { - v_flex().mt_2().gap_1().map(|this| { - if self.account_too_young { - this.child( - h_flex() - .gap_2() - .child( - Label::new("Pro") - .size(LabelSize::Small) - .color(Color::Accent) - .buffer_font(cx), - ) - .child(Divider::horizontal()), - ) - .child( - List::new() - .child(BulletItem::new("500 prompts per month with Claude models")) - .child(BulletItem::new( - "Unlimited accepted edit predictions with Zeta, our open-source model", - )) - .child(BulletItem::new("$20 USD per month")), - ) - .child( - Button::new("pro", "Get Started") - .full_width() - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .on_click(move |_, _window, cx| { - telemetry::event!("Upgrade To Pro Clicked", state = "young-account"); - cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)) - }), - ) - } else { - this.child( - h_flex() - .gap_2() - .child( - Label::new("Pro Trial") - .size(LabelSize::Small) - .color(Color::Accent) - .buffer_font(cx), - ) - .child(Divider::horizontal()), - ) - .child( - List::new() - .child(self.pro_trial_definition()) - .child(BulletItem::new( - "Try it out for 14 days for free, no credit card required", - )), - ) - .child( - Button::new("pro", "Start Free Trial") - .full_width() - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .on_click(move |_, _window, cx| { - telemetry::event!("Start Trial Clicked", state = "post-sign-in"); - cx.open_url(&zed_urls::start_trial_url(cx)) - }), - ) - } - }) - } - fn render_accept_terms_of_service(&self) -> AnyElement { v_flex() .gap_1() @@ -269,6 +132,7 @@ impl ZedAiOnboarding { fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement { let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn); + let plan_definitions = PlanDefinitions; v_flex() .gap_1() @@ -278,7 +142,7 @@ impl ZedAiOnboarding { .color(Color::Muted) .mb_2(), ) - .child(self.pro_trial_definition()) + .child(plan_definitions.pro_plan(false)) .child( Button::new("sign_in", "Try Zed Pro for Free") .disabled(signing_in) @@ -297,43 +161,132 @@ impl ZedAiOnboarding { fn render_free_plan_state(&self, cx: &mut App) -> AnyElement { let young_account_banner = YoungAccountBanner; + let plan_definitions = PlanDefinitions; - v_flex() - .relative() - .gap_1() - .child(Headline::new("Welcome to Zed AI")) - .map(|this| { - if self.account_too_young { - this.child(young_account_banner) - } else { - this.child(self.free_plan_definition(cx)).when_some( - self.dismiss_onboarding.as_ref(), - |this, dismiss_callback| { - let callback = dismiss_callback.clone(); + if self.account_too_young { + v_flex() + .relative() + .max_w_full() + .gap_1() + .child(Headline::new("Welcome to Zed AI")) + .child(young_account_banner) + .child( + v_flex() + .mt_2() + .gap_1() + .child( + h_flex() + .gap_2() + .child( + Label::new("Pro") + .size(LabelSize::Small) + .color(Color::Accent) + .buffer_font(cx), + ) + .child(Divider::horizontal()), + ) + .child(plan_definitions.pro_plan(true)) + .child( + Button::new("pro", "Get Started") + .full_width() + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .on_click(move |_, _window, cx| { + telemetry::event!( + "Upgrade To Pro Clicked", + state = "young-account" + ); + cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)) + }), + ), + ) + .into_any_element() + } else { + v_flex() + .relative() + .gap_1() + .child(Headline::new("Welcome to Zed AI")) + .child( + v_flex() + .mt_2() + .gap_1() + .child( + h_flex() + .gap_2() + .child( + Label::new("Free") + .size(LabelSize::Small) + .color(Color::Muted) + .buffer_font(cx), + ) + .child( + Label::new("(Current Plan)") + .size(LabelSize::Small) + .color(Color::Custom( + cx.theme().colors().text_muted.opacity(0.6), + )) + .buffer_font(cx), + ) + .child(Divider::horizontal()), + ) + .child(plan_definitions.free_plan()), + ) + .when_some( + self.dismiss_onboarding.as_ref(), + |this, dismiss_callback| { + let callback = dismiss_callback.clone(); - this.child( - h_flex().absolute().top_0().right_0().child( - IconButton::new("dismiss_onboarding", IconName::Close) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Dismiss")) - .on_click(move |_, window, cx| { - telemetry::event!( - "Banner Dismissed", - source = "AI Onboarding", - ); - callback(window, cx) - }), - ), - ) - }, - ) - } - }) - .child(self.pro_plan_definition(cx)) - .into_any_element() + this.child( + h_flex().absolute().top_0().right_0().child( + IconButton::new("dismiss_onboarding", IconName::Close) + .icon_size(IconSize::Small) + .tooltip(Tooltip::text("Dismiss")) + .on_click(move |_, window, cx| { + telemetry::event!( + "Banner Dismissed", + source = "AI Onboarding", + ); + callback(window, cx) + }), + ), + ) + }, + ) + .child( + v_flex() + .mt_2() + .gap_1() + .child( + h_flex() + .gap_2() + .child( + Label::new("Pro Trial") + .size(LabelSize::Small) + .color(Color::Accent) + .buffer_font(cx), + ) + .child(Divider::horizontal()), + ) + .child(plan_definitions.pro_trial(true)) + .child( + Button::new("pro", "Start Free Trial") + .full_width() + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .on_click(move |_, _window, cx| { + telemetry::event!( + "Start Trial Clicked", + state = "post-sign-in" + ); + cx.open_url(&zed_urls::start_trial_url(cx)) + }), + ), + ) + .into_any_element() + } } fn render_trial_state(&self, _cx: &mut App) -> AnyElement { + let plan_definitions = PlanDefinitions; + v_flex() .relative() .gap_1() @@ -343,13 +296,7 @@ impl ZedAiOnboarding { .color(Color::Muted) .mb_2(), ) - .child( - List::new() - .child(BulletItem::new("150 prompts with Claude models")) - .child(BulletItem::new( - "Unlimited edit predictions with Zeta, our open-source model", - )), - ) + .child(plan_definitions.pro_trial(false)) .when_some( self.dismiss_onboarding.as_ref(), |this, dismiss_callback| { @@ -374,6 +321,8 @@ impl ZedAiOnboarding { } fn render_pro_plan_state(&self, _cx: &mut App) -> AnyElement { + let plan_definitions = PlanDefinitions; + v_flex() .gap_1() .child(Headline::new("Welcome to Zed Pro")) @@ -382,13 +331,7 @@ impl ZedAiOnboarding { .color(Color::Muted) .mb_2(), ) - .child( - List::new() - .child(BulletItem::new("500 prompts with Claude models")) - .child(BulletItem::new( - "Unlimited edit predictions with Zeta, our open-source model", - )), - ) + .child(plan_definitions.pro_plan(false)) .child( Button::new("pro", "Continue with Zed Pro") .full_width() @@ -450,8 +393,9 @@ impl Component for ZedAiOnboarding { Some( v_flex() - .p_4() .gap_4() + .items_center() + .max_w_4_5() .children(vec![ single_example( "Not Signed-in", @@ -462,8 +406,8 @@ impl Component for ZedAiOnboarding { onboarding(SignInStatus::SignedIn, false, None, false), ), single_example( - "Account too young", - onboarding(SignInStatus::SignedIn, false, None, true), + "Young Account", + onboarding(SignInStatus::SignedIn, true, None, true), ), single_example( "Free Plan", diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index 89a782a7c2..a3fea5dce3 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -1,11 +1,14 @@ -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use client::{Client, zed_urls}; use cloud_llm_client::Plan; -use gpui::{AnyElement, App, IntoElement, RenderOnce, Window}; -use ui::{Divider, List, Vector, VectorName, prelude::*}; +use gpui::{ + Animation, AnimationExt, AnyElement, App, IntoElement, RenderOnce, Transformation, Window, + percentage, +}; +use ui::{Divider, Vector, VectorName, prelude::*}; -use crate::{BulletItem, SignInStatus}; +use crate::{SignInStatus, plan_definitions::PlanDefinitions}; #[derive(IntoElement, RegisterComponent)] pub struct AiUpsellCard { @@ -36,6 +39,8 @@ impl AiUpsellCard { impl RenderOnce for AiUpsellCard { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let plan_definitions = PlanDefinitions; + let pro_section = v_flex() .flex_grow() .w_full() @@ -51,13 +56,7 @@ impl RenderOnce for AiUpsellCard { ) .child(Divider::horizontal()), ) - .child( - List::new() - .child(BulletItem::new("500 prompts with Claude models")) - .child(BulletItem::new( - "Unlimited edit predictions with Zeta, our open-source model", - )), - ); + .child(plan_definitions.pro_plan(false)); let free_section = v_flex() .flex_grow() @@ -74,11 +73,7 @@ impl RenderOnce for AiUpsellCard { ) .child(Divider::horizontal()), ) - .child( - List::new() - .child(BulletItem::new("50 prompts with Claude models")) - .child(BulletItem::new("2,000 accepted edit predictions")), - ); + .child(plan_definitions.free_plan()); let grid_bg = h_flex().absolute().inset_0().w_full().h(px(240.)).child( Vector::new(VectorName::Grid, rems_from_px(500.), rems_from_px(240.)) @@ -101,44 +96,11 @@ impl RenderOnce for AiUpsellCard { ), )); - const DESCRIPTION: &str = "Zed offers a complete agentic experience, with robust editing and reviewing features to collaborate with AI."; + let description = PlanDefinitions::AI_DESCRIPTION; - let footer_buttons = match self.sign_in_status { - SignInStatus::SignedIn => v_flex() - .items_center() - .gap_1() - .child( - Button::new("sign_in", "Start 14-day Free Pro Trial") - .full_width() - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .on_click(move |_, _window, cx| { - telemetry::event!("Start Trial Clicked", state = "post-sign-in"); - cx.open_url(&zed_urls::start_trial_url(cx)) - }) - .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)), - ) - .child( - Label::new("No credit card required") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .into_any_element(), - _ => Button::new("sign_in", "Sign In") - .full_width() - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)) - .on_click({ - let callback = self.sign_in.clone(); - move |_, window, cx| { - telemetry::event!("Start Trial Clicked", state = "pre-sign-in"); - callback(window, cx) - } - }) - .into_any_element(), - }; - - v_flex() + let card = v_flex() .relative() + .flex_grow() .p_4() .pt_3() .border_1() @@ -146,25 +108,129 @@ impl RenderOnce for AiUpsellCard { .rounded_lg() .overflow_hidden() .child(grid_bg) - .child(gradient_bg) - .child(Label::new("Try Zed AI").size(LabelSize::Large)) + .child(gradient_bg); + + let plans_section = h_flex() + .w_full() + .mt_1p5() + .mb_2p5() + .items_start() + .gap_6() + .child(free_section) + .child(pro_section); + + let footer_container = v_flex().items_center().gap_1(); + + let certified_user_stamp = div() + .absolute() + .top_2() + .right_2() + .size(rems_from_px(72.)) .child( - div() - .max_w_3_4() - .mb_2() - .child(Label::new(DESCRIPTION).color(Color::Muted)), - ) + Vector::new( + VectorName::CertifiedUserStamp, + rems_from_px(72.), + rems_from_px(72.), + ) + .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.3))) + .with_animation( + "loading_stamp", + Animation::new(Duration::from_secs(10)).repeat(), + |this, delta| this.transform(Transformation::rotate(percentage(delta))), + ), + ); + + let pro_trial_stamp = div() + .absolute() + .top_2() + .right_2() + .size(rems_from_px(72.)) .child( - h_flex() - .w_full() - .mt_1p5() - .mb_2p5() - .items_start() - .gap_6() - .child(free_section) - .child(pro_section), - ) - .child(footer_buttons) + Vector::new( + VectorName::ProTrialStamp, + rems_from_px(72.), + rems_from_px(72.), + ) + .color(Color::Custom(cx.theme().colors().text.alpha(0.2))), + ); + + match self.sign_in_status { + SignInStatus::SignedIn => match self.user_plan { + None | Some(Plan::ZedFree) => card + .child(Label::new("Try Zed AI").size(LabelSize::Large)) + .child( + div() + .max_w_3_4() + .mb_2() + .child(Label::new(description).color(Color::Muted)), + ) + .child(plans_section) + .child( + footer_container + .child( + Button::new("start_trial", "Start 14-day Free Pro Trial") + .full_width() + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .when_some(self.tab_index, |this, tab_index| { + this.tab_index(tab_index) + }) + .on_click(move |_, _window, cx| { + telemetry::event!( + "Start Trial Clicked", + state = "post-sign-in" + ); + cx.open_url(&zed_urls::start_trial_url(cx)) + }), + ) + .child( + Label::new("No credit card required") + .size(LabelSize::Small) + .color(Color::Muted), + ), + ), + Some(Plan::ZedProTrial) => card + .child(pro_trial_stamp) + .child(Label::new("You're in the Zed Pro Trial").size(LabelSize::Large)) + .child( + Label::new("Here's what you get for the next 14 days:") + .color(Color::Muted) + .mb_2(), + ) + .child(plan_definitions.pro_trial(false)), + Some(Plan::ZedPro) => card + .child(certified_user_stamp) + .child(Label::new("You're in the Zed Pro plan").size(LabelSize::Large)) + .child( + Label::new("Here's what you get:") + .color(Color::Muted) + .mb_2(), + ) + .child(plan_definitions.pro_plan(false)), + }, + // Signed Out State + _ => card + .child(Label::new("Try Zed AI").size(LabelSize::Large)) + .child( + div() + .max_w_3_4() + .mb_2() + .child(Label::new(description).color(Color::Muted)), + ) + .child(plans_section) + .child( + Button::new("sign_in", "Sign In") + .full_width() + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)) + .on_click({ + let callback = self.sign_in.clone(); + move |_, window, cx| { + telemetry::event!("Start Trial Clicked", state = "pre-sign-in"); + callback(window, cx) + } + }), + ), + } } } @@ -188,7 +254,6 @@ impl Component for AiUpsellCard { fn preview(_window: &mut Window, _cx: &mut App) -> Option { Some( v_flex() - .p_4() .gap_4() .children(vec![example_group(vec![ single_example( @@ -202,11 +267,31 @@ impl Component for AiUpsellCard { .into_any_element(), ), single_example( - "Signed In State", + "Free Plan", AiUpsellCard { sign_in_status: SignInStatus::SignedIn, sign_in: Arc::new(|_, _| {}), - user_plan: None, + user_plan: Some(Plan::ZedFree), + tab_index: Some(1), + } + .into_any_element(), + ), + single_example( + "Pro Trial", + AiUpsellCard { + sign_in_status: SignInStatus::SignedIn, + sign_in: Arc::new(|_, _| {}), + user_plan: Some(Plan::ZedProTrial), + tab_index: Some(1), + } + .into_any_element(), + ), + single_example( + "Pro Plan", + AiUpsellCard { + sign_in_status: SignInStatus::SignedIn, + sign_in: Arc::new(|_, _| {}), + user_plan: Some(Plan::ZedPro), tab_index: Some(1), } .into_any_element(), diff --git a/crates/ai_onboarding/src/plan_definitions.rs b/crates/ai_onboarding/src/plan_definitions.rs new file mode 100644 index 0000000000..8d66f6c356 --- /dev/null +++ b/crates/ai_onboarding/src/plan_definitions.rs @@ -0,0 +1,39 @@ +use gpui::{IntoElement, ParentElement}; +use ui::{List, ListBulletItem, prelude::*}; + +/// Centralized definitions for Zed AI plans +pub struct PlanDefinitions; + +impl PlanDefinitions { + pub const AI_DESCRIPTION: &'static str = "Zed offers a complete agentic experience, with robust editing and reviewing features to collaborate with AI."; + + pub fn free_plan(&self) -> impl IntoElement { + List::new() + .child(ListBulletItem::new("50 prompts with Claude models")) + .child(ListBulletItem::new("2,000 accepted edit predictions")) + } + + pub fn pro_trial(&self, period: bool) -> impl IntoElement { + List::new() + .child(ListBulletItem::new("150 prompts with Claude models")) + .child(ListBulletItem::new( + "Unlimited edit predictions with Zeta, our open-source model", + )) + .when(period, |this| { + this.child(ListBulletItem::new( + "Try it out for 14 days for free, no credit card required", + )) + }) + } + + pub fn pro_plan(&self, price: bool) -> impl IntoElement { + List::new() + .child(ListBulletItem::new("500 prompts with Claude models")) + .child(ListBulletItem::new( + "Unlimited edit predictions with Zeta, our open-source model", + )) + .when(price, |this| { + this.child(ListBulletItem::new("$20 USD per month")) + }) + } +} diff --git a/crates/ai_onboarding/src/young_account_banner.rs b/crates/ai_onboarding/src/young_account_banner.rs index a43625a60e..54f563e4aa 100644 --- a/crates/ai_onboarding/src/young_account_banner.rs +++ b/crates/ai_onboarding/src/young_account_banner.rs @@ -15,6 +15,7 @@ impl RenderOnce for YoungAccountBanner { .child(YOUNG_ACCOUNT_DISCLAIMER); div() + .max_w_full() .my_1() .child(Banner::new().severity(ui::Severity::Warning).child(label)) } diff --git a/crates/ui/src/components/image.rs b/crates/ui/src/components/image.rs index 2deba68d88..18f804abe9 100644 --- a/crates/ui/src/components/image.rs +++ b/crates/ui/src/components/image.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use gpui::Transformation; use gpui::{App, IntoElement, Rems, RenderOnce, Size, Styled, Window, svg}; use serde::{Deserialize, Serialize}; use strum::{EnumIter, EnumString, IntoStaticStr}; @@ -12,11 +13,13 @@ use crate::prelude::*; )] #[strum(serialize_all = "snake_case")] pub enum VectorName { + AiGrid, + CertifiedUserStamp, + DebuggerGrid, + Grid, + ProTrialStamp, ZedLogo, ZedXCopilot, - Grid, - AiGrid, - DebuggerGrid, } impl VectorName { @@ -37,6 +40,7 @@ pub struct Vector { path: Arc, color: Color, size: Size, + transformation: Transformation, } impl Vector { @@ -46,6 +50,7 @@ impl Vector { path: vector.path(), color: Color::default(), size: Size { width, height }, + transformation: Transformation::default(), } } @@ -66,6 +71,11 @@ impl Vector { self.size = size; self } + + pub fn transform(mut self, transformation: Transformation) -> Self { + self.transformation = transformation; + self + } } impl RenderOnce for Vector { @@ -81,6 +91,7 @@ impl RenderOnce for Vector { .h(height) .path(self.path) .text_color(self.color.color(cx)) + .with_transformation(self.transformation) } } diff --git a/crates/ui/src/components/list.rs b/crates/ui/src/components/list.rs index 88650b6ae8..6876f290ce 100644 --- a/crates/ui/src/components/list.rs +++ b/crates/ui/src/components/list.rs @@ -1,10 +1,12 @@ mod list; +mod list_bullet_item; mod list_header; mod list_item; mod list_separator; mod list_sub_header; pub use list::*; +pub use list_bullet_item::*; pub use list_header::*; pub use list_item::*; pub use list_separator::*; diff --git a/crates/ui/src/components/list/list_bullet_item.rs b/crates/ui/src/components/list/list_bullet_item.rs new file mode 100644 index 0000000000..6e079d9f11 --- /dev/null +++ b/crates/ui/src/components/list/list_bullet_item.rs @@ -0,0 +1,40 @@ +use crate::{ListItem, prelude::*}; +use gpui::{IntoElement, ParentElement, SharedString}; + +#[derive(IntoElement)] +pub struct ListBulletItem { + label: SharedString, +} + +impl ListBulletItem { + pub fn new(label: impl Into) -> Self { + Self { + label: label.into(), + } + } +} + +impl RenderOnce for ListBulletItem { + fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { + let line_height = 0.85 * window.line_height(); + + ListItem::new("list-item") + .selectable(false) + .child( + h_flex() + .w_full() + .min_w_0() + .gap_1() + .items_start() + .child( + h_flex().h(line_height).justify_center().child( + Icon::new(IconName::Dash) + .size(IconSize::XSmall) + .color(Color::Hidden), + ), + ) + .child(div().w_full().min_w_0().child(Label::new(self.label))), + ) + .into_any_element() + } +} From f10ffc2a720831cabb61faad3db00af5d9d125c5 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:37:25 -0300 Subject: [PATCH 098/693] ui: Fix switch component style when focused (#35678) Just making sure the switch's dimensions aren't affected by the need to having an outer border to represent focus. Release Notes: - N/A --- crates/ui/src/components/toggle.rs | 45 ++++++++++++++++-------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index 53df4767b0..4b985fd2c2 100644 --- a/crates/ui/src/components/toggle.rs +++ b/crates/ui/src/components/toggle.rs @@ -504,15 +504,12 @@ impl RenderOnce for Switch { let group_id = format!("switch_group_{:?}", self.id); - let switch = h_flex() - .w(DynamicSpacing::Base32.rems(cx)) - .h(DynamicSpacing::Base20.rems(cx)) - .group(group_id.clone()) - .border_1() + let switch = div() + .id((self.id.clone(), "switch")) .p(px(1.0)) + .border_2() .border_color(cx.theme().colors().border_transparent) .rounded_full() - .id((self.id.clone(), "switch")) .when_some( self.tab_index.filter(|_| !self.disabled), |this, tab_index| { @@ -524,23 +521,29 @@ impl RenderOnce for Switch { ) .child( h_flex() - .when(is_on, |on| on.justify_end()) - .when(!is_on, |off| off.justify_start()) - .size_full() - .rounded_full() - .px(DynamicSpacing::Base02.px(cx)) - .bg(bg_color) - .when(!self.disabled, |this| { - this.group_hover(group_id.clone(), |el| el.bg(bg_hover_color)) - }) - .border_1() - .border_color(border_color) + .w(DynamicSpacing::Base32.rems(cx)) + .h(DynamicSpacing::Base20.rems(cx)) + .group(group_id.clone()) .child( - div() - .size(DynamicSpacing::Base12.rems(cx)) + h_flex() + .when(is_on, |on| on.justify_end()) + .when(!is_on, |off| off.justify_start()) + .size_full() .rounded_full() - .bg(thumb_color) - .opacity(thumb_opacity), + .px(DynamicSpacing::Base02.px(cx)) + .bg(bg_color) + .when(!self.disabled, |this| { + this.group_hover(group_id.clone(), |el| el.bg(bg_hover_color)) + }) + .border_1() + .border_color(border_color) + .child( + div() + .size(DynamicSpacing::Base12.rems(cx)) + .rounded_full() + .bg(thumb_color) + .opacity(thumb_opacity), + ), ), ); From 142efbac0dcb59e783dd66e5de65bf6ed8a78171 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 5 Aug 2025 18:42:45 -0400 Subject: [PATCH 099/693] collab: Remove unused billing queries (#35679) This PR removes some billing-related queries that are no longer used. Release Notes: - N/A --- .../src/db/queries/billing_subscriptions.rs | 126 ------------------ crates/collab/src/db/tests.rs | 1 - .../db/tests/billing_subscription_tests.rs | 96 ------------- crates/collab/src/llm/db/queries.rs | 1 - .../db/queries/subscription_usage_meters.rs | 72 ---------- .../src/llm/db/queries/subscription_usages.rs | 21 --- 6 files changed, 317 deletions(-) delete mode 100644 crates/collab/src/db/tests/billing_subscription_tests.rs delete mode 100644 crates/collab/src/llm/db/queries/subscription_usage_meters.rs diff --git a/crates/collab/src/db/queries/billing_subscriptions.rs b/crates/collab/src/db/queries/billing_subscriptions.rs index 9f82e3dbc4..8361d6b4d0 100644 --- a/crates/collab/src/db/queries/billing_subscriptions.rs +++ b/crates/collab/src/db/queries/billing_subscriptions.rs @@ -85,19 +85,6 @@ impl Database { .await } - /// Returns the billing subscription with the specified ID. - pub async fn get_billing_subscription_by_id( - &self, - id: BillingSubscriptionId, - ) -> Result> { - self.transaction(|tx| async move { - Ok(billing_subscription::Entity::find_by_id(id) - .one(&*tx) - .await?) - }) - .await - } - /// Returns the billing subscription with the specified Stripe subscription ID. pub async fn get_billing_subscription_by_stripe_subscription_id( &self, @@ -143,119 +130,6 @@ impl Database { .await } - /// Returns all of the billing subscriptions for the user with the specified ID. - /// - /// Note that this returns the subscriptions regardless of their status. - /// If you're wanting to check if a use has an active billing subscription, - /// use `get_active_billing_subscriptions` instead. - pub async fn get_billing_subscriptions( - &self, - user_id: UserId, - ) -> Result> { - self.transaction(|tx| async move { - let subscriptions = billing_subscription::Entity::find() - .inner_join(billing_customer::Entity) - .filter(billing_customer::Column::UserId.eq(user_id)) - .order_by_asc(billing_subscription::Column::Id) - .all(&*tx) - .await?; - - Ok(subscriptions) - }) - .await - } - - pub async fn get_active_billing_subscriptions( - &self, - user_ids: HashSet, - ) -> Result> { - self.transaction(|tx| { - let user_ids = user_ids.clone(); - async move { - let mut rows = billing_subscription::Entity::find() - .inner_join(billing_customer::Entity) - .select_also(billing_customer::Entity) - .filter(billing_customer::Column::UserId.is_in(user_ids)) - .filter( - billing_subscription::Column::StripeSubscriptionStatus - .eq(StripeSubscriptionStatus::Active), - ) - .filter(billing_subscription::Column::Kind.is_null()) - .order_by_asc(billing_subscription::Column::Id) - .stream(&*tx) - .await?; - - let mut subscriptions = HashMap::default(); - while let Some(row) = rows.next().await { - if let (subscription, Some(customer)) = row? { - subscriptions.insert(customer.user_id, (customer, subscription)); - } - } - Ok(subscriptions) - } - }) - .await - } - - pub async fn get_active_zed_pro_billing_subscriptions( - &self, - ) -> Result> { - self.transaction(|tx| async move { - let mut rows = billing_subscription::Entity::find() - .inner_join(billing_customer::Entity) - .select_also(billing_customer::Entity) - .filter( - billing_subscription::Column::StripeSubscriptionStatus - .eq(StripeSubscriptionStatus::Active), - ) - .filter(billing_subscription::Column::Kind.eq(SubscriptionKind::ZedPro)) - .order_by_asc(billing_subscription::Column::Id) - .stream(&*tx) - .await?; - - let mut subscriptions = HashMap::default(); - while let Some(row) = rows.next().await { - if let (subscription, Some(customer)) = row? { - subscriptions.insert(customer.user_id, (customer, subscription)); - } - } - Ok(subscriptions) - }) - .await - } - - pub async fn get_active_zed_pro_billing_subscriptions_for_users( - &self, - user_ids: HashSet, - ) -> Result> { - self.transaction(|tx| { - let user_ids = user_ids.clone(); - async move { - let mut rows = billing_subscription::Entity::find() - .inner_join(billing_customer::Entity) - .select_also(billing_customer::Entity) - .filter(billing_customer::Column::UserId.is_in(user_ids)) - .filter( - billing_subscription::Column::StripeSubscriptionStatus - .eq(StripeSubscriptionStatus::Active), - ) - .filter(billing_subscription::Column::Kind.eq(SubscriptionKind::ZedPro)) - .order_by_asc(billing_subscription::Column::Id) - .stream(&*tx) - .await?; - - let mut subscriptions = HashMap::default(); - while let Some(row) = rows.next().await { - if let (subscription, Some(customer)) = row? { - subscriptions.insert(customer.user_id, (customer, subscription)); - } - } - Ok(subscriptions) - } - }) - .await - } - /// Returns whether the user has an active billing subscription. pub async fn has_active_billing_subscription(&self, user_id: UserId) -> Result { Ok(self.count_active_billing_subscriptions(user_id).await? > 0) diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 9404e2670c..6c2f9dc82a 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -1,4 +1,3 @@ -mod billing_subscription_tests; mod buffer_tests; mod channel_tests; mod contributor_tests; diff --git a/crates/collab/src/db/tests/billing_subscription_tests.rs b/crates/collab/src/db/tests/billing_subscription_tests.rs deleted file mode 100644 index fb5f8552a3..0000000000 --- a/crates/collab/src/db/tests/billing_subscription_tests.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::sync::Arc; - -use crate::db::billing_subscription::StripeSubscriptionStatus; -use crate::db::tests::new_test_user; -use crate::db::{CreateBillingCustomerParams, CreateBillingSubscriptionParams}; -use crate::test_both_dbs; - -use super::Database; - -test_both_dbs!( - test_get_active_billing_subscriptions, - test_get_active_billing_subscriptions_postgres, - test_get_active_billing_subscriptions_sqlite -); - -async fn test_get_active_billing_subscriptions(db: &Arc) { - // A user with no subscription has no active billing subscriptions. - { - let user_id = new_test_user(db, "no-subscription-user@example.com").await; - let subscription_count = db - .count_active_billing_subscriptions(user_id) - .await - .unwrap(); - - assert_eq!(subscription_count, 0); - } - - // A user with an active subscription has one active billing subscription. - { - let user_id = new_test_user(db, "active-user@example.com").await; - let customer = db - .create_billing_customer(&CreateBillingCustomerParams { - user_id, - stripe_customer_id: "cus_active_user".into(), - }) - .await - .unwrap(); - assert_eq!(customer.stripe_customer_id, "cus_active_user".to_string()); - - db.create_billing_subscription(&CreateBillingSubscriptionParams { - billing_customer_id: customer.id, - kind: None, - stripe_subscription_id: "sub_active_user".into(), - stripe_subscription_status: StripeSubscriptionStatus::Active, - stripe_cancellation_reason: None, - stripe_current_period_start: None, - stripe_current_period_end: None, - }) - .await - .unwrap(); - - let subscriptions = db.get_billing_subscriptions(user_id).await.unwrap(); - assert_eq!(subscriptions.len(), 1); - - let subscription = &subscriptions[0]; - assert_eq!( - subscription.stripe_subscription_id, - "sub_active_user".to_string() - ); - assert_eq!( - subscription.stripe_subscription_status, - StripeSubscriptionStatus::Active - ); - } - - // A user with a past-due subscription has no active billing subscriptions. - { - let user_id = new_test_user(db, "past-due-user@example.com").await; - let customer = db - .create_billing_customer(&CreateBillingCustomerParams { - user_id, - stripe_customer_id: "cus_past_due_user".into(), - }) - .await - .unwrap(); - assert_eq!(customer.stripe_customer_id, "cus_past_due_user".to_string()); - - db.create_billing_subscription(&CreateBillingSubscriptionParams { - billing_customer_id: customer.id, - kind: None, - stripe_subscription_id: "sub_past_due_user".into(), - stripe_subscription_status: StripeSubscriptionStatus::PastDue, - stripe_cancellation_reason: None, - stripe_current_period_start: None, - stripe_current_period_end: None, - }) - .await - .unwrap(); - - let subscription_count = db - .count_active_billing_subscriptions(user_id) - .await - .unwrap(); - assert_eq!(subscription_count, 0); - } -} diff --git a/crates/collab/src/llm/db/queries.rs b/crates/collab/src/llm/db/queries.rs index 3565366fdd..0087218b3f 100644 --- a/crates/collab/src/llm/db/queries.rs +++ b/crates/collab/src/llm/db/queries.rs @@ -1,6 +1,5 @@ use super::*; pub mod providers; -pub mod subscription_usage_meters; pub mod subscription_usages; pub mod usages; diff --git a/crates/collab/src/llm/db/queries/subscription_usage_meters.rs b/crates/collab/src/llm/db/queries/subscription_usage_meters.rs deleted file mode 100644 index c0ce5d679b..0000000000 --- a/crates/collab/src/llm/db/queries/subscription_usage_meters.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::db::UserId; -use crate::llm::db::queries::subscription_usages::convert_chrono_to_time; - -use super::*; - -impl LlmDatabase { - /// Returns all current subscription usage meters as of the given timestamp. - pub async fn get_current_subscription_usage_meters( - &self, - now: DateTimeUtc, - ) -> Result> { - let now = convert_chrono_to_time(now)?; - - self.transaction(|tx| async move { - let result = subscription_usage_meter::Entity::find() - .inner_join(subscription_usage::Entity) - .filter( - subscription_usage::Column::PeriodStartAt - .lte(now) - .and(subscription_usage::Column::PeriodEndAt.gte(now)), - ) - .select_also(subscription_usage::Entity) - .all(&*tx) - .await?; - - let result = result - .into_iter() - .filter_map(|(meter, usage)| { - let usage = usage?; - Some((meter, usage)) - }) - .collect(); - - Ok(result) - }) - .await - } - - /// Returns all current subscription usage meters for the given user as of the given timestamp. - pub async fn get_current_subscription_usage_meters_for_user( - &self, - user_id: UserId, - now: DateTimeUtc, - ) -> Result> { - let now = convert_chrono_to_time(now)?; - - self.transaction(|tx| async move { - let result = subscription_usage_meter::Entity::find() - .inner_join(subscription_usage::Entity) - .filter(subscription_usage::Column::UserId.eq(user_id)) - .filter( - subscription_usage::Column::PeriodStartAt - .lte(now) - .and(subscription_usage::Column::PeriodEndAt.gte(now)), - ) - .select_also(subscription_usage::Entity) - .all(&*tx) - .await?; - - let result = result - .into_iter() - .filter_map(|(meter, usage)| { - let usage = usage?; - Some((meter, usage)) - }) - .collect(); - - Ok(result) - }) - .await - } -} diff --git a/crates/collab/src/llm/db/queries/subscription_usages.rs b/crates/collab/src/llm/db/queries/subscription_usages.rs index ee1ebf59b8..8a51979075 100644 --- a/crates/collab/src/llm/db/queries/subscription_usages.rs +++ b/crates/collab/src/llm/db/queries/subscription_usages.rs @@ -1,28 +1,7 @@ -use time::PrimitiveDateTime; - use crate::db::UserId; use super::*; -pub fn convert_chrono_to_time(datetime: DateTimeUtc) -> anyhow::Result { - use chrono::{Datelike as _, Timelike as _}; - - let date = time::Date::from_calendar_date( - datetime.year(), - time::Month::try_from(datetime.month() as u8).unwrap(), - datetime.day() as u8, - )?; - - let time = time::Time::from_hms_nano( - datetime.hour() as u8, - datetime.minute() as u8, - datetime.second() as u8, - datetime.nanosecond(), - )?; - - Ok(PrimitiveDateTime::new(date, time)) -} - impl LlmDatabase { pub async fn get_subscription_usage_for_period( &self, From bc2108cbba4704f7f31e48a8cb80befbe2f626f9 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Tue, 5 Aug 2025 18:52:08 -0400 Subject: [PATCH 100/693] Render error state when agent binary exits unexpectedly (#35651) This PR adds handling for the case where an agent binary exits unexpectedly after successfully establishing a connection. Release Notes: - N/A --------- Co-authored-by: Agus --- crates/acp_thread/src/acp_thread.rs | 6 +++ crates/agent_servers/src/acp/v1.rs | 23 +++++++--- crates/agent_servers/src/claude.rs | 60 +++++++++++++++----------- crates/agent_ui/src/acp/thread_view.rs | 59 +++++++++++++++++++++++-- crates/agent_ui/src/agent_diff.rs | 3 +- 5 files changed, 116 insertions(+), 35 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 44190a4860..892fd16655 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -18,6 +18,7 @@ use project::{AgentLocation, Project}; use std::collections::HashMap; use std::error::Error; use std::fmt::Formatter; +use std::process::ExitStatus; use std::rc::Rc; use std::{ fmt::Display, @@ -581,6 +582,7 @@ pub enum AcpThreadEvent { ToolAuthorizationRequired, Stopped, Error, + ServerExited(ExitStatus), } impl EventEmitter for AcpThread {} @@ -1229,6 +1231,10 @@ impl AcpThread { pub fn to_markdown(&self, cx: &App) -> String { self.entries.iter().map(|e| e.to_markdown(cx)).collect() } + + pub fn emit_server_exited(&mut self, status: ExitStatus, cx: &mut Context) { + cx.emit(AcpThreadEvent::ServerExited(status)); + } } #[cfg(test)] diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index 0b6fa1c48b..cea7d7c1da 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -61,11 +61,24 @@ impl AcpConnection { } }); - let io_task = cx.background_spawn(async move { - io_task.await?; - drop(child); - Ok(()) - }); + let io_task = cx.background_spawn(io_task); + + cx.spawn({ + let sessions = sessions.clone(); + async move |cx| { + let status = child.status().await?; + + for session in sessions.borrow().values() { + session + .thread + .update(cx, |thread, cx| thread.emit_server_exited(status, cx)) + .ok(); + } + + anyhow::Ok(()) + } + }) + .detach(); let response = connection .initialize(acp::InitializeRequest { diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index b097c0345c..216624a932 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -114,42 +114,42 @@ impl AgentConnection for ClaudeAgentConnection { log::trace!("Starting session with id: {}", session_id); - cx.background_spawn({ - let session_id = session_id.clone(); - async move { - let mut outgoing_rx = Some(outgoing_rx); + let mut child = spawn_claude( + &command, + ClaudeSessionMode::Start, + session_id.clone(), + &mcp_config_path, + &cwd, + )?; - let mut child = spawn_claude( - &command, - ClaudeSessionMode::Start, - session_id.clone(), - &mcp_config_path, - &cwd, - )?; + let stdin = child.stdin.take().unwrap(); + let stdout = child.stdout.take().unwrap(); - let pid = child.id(); - log::trace!("Spawned (pid: {})", pid); + let pid = child.id(); + log::trace!("Spawned (pid: {})", pid); - ClaudeAgentSession::handle_io( - outgoing_rx.take().unwrap(), - incoming_message_tx.clone(), - child.stdin.take().unwrap(), - child.stdout.take().unwrap(), - ) - .await?; + cx.background_spawn(async move { + let mut outgoing_rx = Some(outgoing_rx); - log::trace!("Stopped (pid: {})", pid); + ClaudeAgentSession::handle_io( + outgoing_rx.take().unwrap(), + incoming_message_tx.clone(), + stdin, + stdout, + ) + .await?; - drop(mcp_config_path); - anyhow::Ok(()) - } + log::trace!("Stopped (pid: {})", pid); + + drop(mcp_config_path); + anyhow::Ok(()) }) .detach(); let end_turn_tx = Rc::new(RefCell::new(None)); let handler_task = cx.spawn({ let end_turn_tx = end_turn_tx.clone(); - let thread_rx = thread_rx.clone(); + let mut thread_rx = thread_rx.clone(); async move |cx| { while let Some(message) = incoming_message_rx.next().await { ClaudeAgentSession::handle_message( @@ -160,6 +160,16 @@ impl AgentConnection for ClaudeAgentConnection { ) .await } + + if let Some(status) = child.status().await.log_err() { + if let Some(thread) = thread_rx.recv().await.ok() { + thread + .update(cx, |thread, cx| { + thread.emit_server_exited(status, cx); + }) + .ok(); + } + } } }); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 7c6f315fb6..6449643cac 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -5,6 +5,7 @@ use audio::{Audio, Sound}; use std::cell::RefCell; use std::collections::BTreeMap; use std::path::Path; +use std::process::ExitStatus; use std::rc::Rc; use std::sync::Arc; use std::time::Duration; @@ -90,6 +91,9 @@ enum ThreadState { Unauthenticated { connection: Rc, }, + ServerExited { + status: ExitStatus, + }, } impl AcpThreadView { @@ -229,7 +233,7 @@ impl AcpThreadView { let connect_task = agent.connect(&root_dir, &project, cx); let load_task = cx.spawn_in(window, async move |this, cx| { let connection = match connect_task.await { - Ok(thread) => thread, + Ok(connection) => connection, Err(err) => { this.update(cx, |this, cx| { this.handle_load_error(err, cx); @@ -240,6 +244,20 @@ impl AcpThreadView { } }; + // this.update_in(cx, |_this, _window, cx| { + // let status = connection.exit_status(cx); + // cx.spawn(async move |this, cx| { + // let status = status.await.ok(); + // this.update(cx, |this, cx| { + // this.thread_state = ThreadState::ServerExited { status }; + // cx.notify(); + // }) + // .ok(); + // }) + // .detach(); + // }) + // .ok(); + let result = match connection .clone() .new_thread(project.clone(), &root_dir, cx) @@ -308,7 +326,8 @@ impl AcpThreadView { ThreadState::Ready { thread, .. } => Some(thread), ThreadState::Unauthenticated { .. } | ThreadState::Loading { .. } - | ThreadState::LoadError(..) => None, + | ThreadState::LoadError(..) + | ThreadState::ServerExited { .. } => None, } } @@ -318,6 +337,7 @@ impl AcpThreadView { ThreadState::Loading { .. } => "Loading…".into(), ThreadState::LoadError(_) => "Failed to load".into(), ThreadState::Unauthenticated { .. } => "Not authenticated".into(), + ThreadState::ServerExited { .. } => "Server exited unexpectedly".into(), } } @@ -647,6 +667,9 @@ impl AcpThreadView { cx, ); } + AcpThreadEvent::ServerExited(status) => { + self.thread_state = ThreadState::ServerExited { status: *status }; + } } cx.notify(); } @@ -1383,7 +1406,29 @@ impl AcpThreadView { .into_any() } - fn render_error_state(&self, e: &LoadError, cx: &Context) -> AnyElement { + fn render_server_exited(&self, status: ExitStatus, _cx: &Context) -> AnyElement { + v_flex() + .items_center() + .justify_center() + .child(self.render_error_agent_logo()) + .child( + v_flex() + .mt_4() + .mb_2() + .gap_0p5() + .text_center() + .items_center() + .child(Headline::new("Server exited unexpectedly").size(HeadlineSize::Medium)) + .child( + Label::new(format!("Exit status: {}", status.code().unwrap_or(-127))) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + .into_any_element() + } + + fn render_load_error(&self, e: &LoadError, cx: &Context) -> AnyElement { let mut container = v_flex() .items_center() .justify_center() @@ -2494,7 +2539,13 @@ impl Render for AcpThreadView { .flex_1() .items_center() .justify_center() - .child(self.render_error_state(e, cx)), + .child(self.render_load_error(e, cx)), + ThreadState::ServerExited { status } => v_flex() + .p_2() + .flex_1() + .items_center() + .justify_center() + .child(self.render_server_exited(*status, cx)), ThreadState::Ready { thread, .. } => { let thread_clone = thread.clone(); diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index c4dc359093..e1ceaf761d 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1523,7 +1523,8 @@ impl AgentDiff { } AcpThreadEvent::Stopped | AcpThreadEvent::ToolAuthorizationRequired - | AcpThreadEvent::Error => {} + | AcpThreadEvent::Error + | AcpThreadEvent::ServerExited(_) => {} } } From cc9317525631e6bf88672e716d6188814f55968a Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 5 Aug 2025 20:11:43 -0300 Subject: [PATCH 101/693] Recategorize a few items in the component preview (#35681) Release Notes: - N/A --- crates/agent_ui/src/ui/end_trial_upsell.rs | 10 ++++++---- crates/ai_onboarding/src/ai_onboarding.rs | 10 +++++++++- crates/ai_onboarding/src/ai_upsell_card.rs | 2 +- crates/component/src/component.rs | 2 ++ crates/language_models/src/provider/cloud.rs | 10 +++++++++- crates/onboarding/src/theme_preview.rs | 12 ++++++++++++ crates/ui/src/components/banner.rs | 2 +- crates/ui/src/components/callout.rs | 2 +- crates/ui/src/components/tab.rs | 2 +- crates/ui/src/styles/animation.rs | 2 +- crates/ui/src/styles/color.rs | 2 +- 11 files changed, 44 insertions(+), 12 deletions(-) diff --git a/crates/agent_ui/src/ui/end_trial_upsell.rs b/crates/agent_ui/src/ui/end_trial_upsell.rs index 0d9751afec..3a8a119800 100644 --- a/crates/agent_ui/src/ui/end_trial_upsell.rs +++ b/crates/agent_ui/src/ui/end_trial_upsell.rs @@ -94,18 +94,20 @@ impl RenderOnce for EndTrialUpsell { impl Component for EndTrialUpsell { fn scope() -> ComponentScope { - ComponentScope::Agent + ComponentScope::Onboarding + } + + fn name() -> &'static str { + "End of Trial Upsell Banner" } fn sort_name() -> &'static str { - "AgentEndTrialUpsell" + "End of Trial Upsell Banner" } fn preview(_window: &mut Window, _cx: &mut App) -> Option { Some( v_flex() - .p_4() - .gap_4() .child(EndTrialUpsell { dismiss_upsell: Arc::new(|_, _| {}), }) diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index 9372a33fed..b9a1e49a4a 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -368,7 +368,15 @@ impl RenderOnce for ZedAiOnboarding { impl Component for ZedAiOnboarding { fn scope() -> ComponentScope { - ComponentScope::Agent + ComponentScope::Onboarding + } + + fn name() -> &'static str { + "Agent Panel Banners" + } + + fn sort_name() -> &'static str { + "Agent Panel Banners" } fn preview(_window: &mut Window, _cx: &mut App) -> Option { diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index a3fea5dce3..4e4833f770 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -236,7 +236,7 @@ impl RenderOnce for AiUpsellCard { impl Component for AiUpsellCard { fn scope() -> ComponentScope { - ComponentScope::Agent + ComponentScope::Onboarding } fn name() -> &'static str { diff --git a/crates/component/src/component.rs b/crates/component/src/component.rs index 02840cc3cb..0c05ba4a97 100644 --- a/crates/component/src/component.rs +++ b/crates/component/src/component.rs @@ -318,8 +318,10 @@ pub enum ComponentScope { Notification, #[strum(serialize = "Overlays & Layering")] Overlays, + Onboarding, Status, Typography, + Utilities, #[strum(serialize = "Version Control")] VersionControl, } diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 2108547c4f..134b2bef6c 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -1267,8 +1267,16 @@ impl Render for ConfigurationView { } impl Component for ZedAiConfiguration { + fn name() -> &'static str { + "AI Configuration Content" + } + + fn sort_name() -> &'static str { + "AI Configuration Content" + } + fn scope() -> ComponentScope { - ComponentScope::Agent + ComponentScope::Onboarding } fn preview(_window: &mut Window, _cx: &mut App) -> Option { diff --git a/crates/onboarding/src/theme_preview.rs b/crates/onboarding/src/theme_preview.rs index 53631be1c9..81eb14ec4b 100644 --- a/crates/onboarding/src/theme_preview.rs +++ b/crates/onboarding/src/theme_preview.rs @@ -299,6 +299,18 @@ impl RenderOnce for ThemePreviewTile { } impl Component for ThemePreviewTile { + fn scope() -> ComponentScope { + ComponentScope::Onboarding + } + + fn name() -> &'static str { + "Theme Preview Tile" + } + + fn sort_name() -> &'static str { + "Theme Preview Tile" + } + fn description() -> Option<&'static str> { Some(Self::DOCS) } diff --git a/crates/ui/src/components/banner.rs b/crates/ui/src/components/banner.rs index b16ca795b4..d88905d466 100644 --- a/crates/ui/src/components/banner.rs +++ b/crates/ui/src/components/banner.rs @@ -131,7 +131,7 @@ impl RenderOnce for Banner { impl Component for Banner { fn scope() -> ComponentScope { - ComponentScope::Notification + ComponentScope::DataDisplay } fn preview(_window: &mut Window, _cx: &mut App) -> Option { diff --git a/crates/ui/src/components/callout.rs b/crates/ui/src/components/callout.rs index d15fa122ed..9c1c9fb1a9 100644 --- a/crates/ui/src/components/callout.rs +++ b/crates/ui/src/components/callout.rs @@ -158,7 +158,7 @@ impl RenderOnce for Callout { impl Component for Callout { fn scope() -> ComponentScope { - ComponentScope::Notification + ComponentScope::DataDisplay } fn description() -> Option<&'static str> { diff --git a/crates/ui/src/components/tab.rs b/crates/ui/src/components/tab.rs index a205c33358..d704846a68 100644 --- a/crates/ui/src/components/tab.rs +++ b/crates/ui/src/components/tab.rs @@ -179,7 +179,7 @@ impl RenderOnce for Tab { impl Component for Tab { fn scope() -> ComponentScope { - ComponentScope::None + ComponentScope::Navigation } fn description() -> Option<&'static str> { diff --git a/crates/ui/src/styles/animation.rs b/crates/ui/src/styles/animation.rs index 0649bee1f8..ee5352d454 100644 --- a/crates/ui/src/styles/animation.rs +++ b/crates/ui/src/styles/animation.rs @@ -99,7 +99,7 @@ struct Animation {} impl Component for Animation { fn scope() -> ComponentScope { - ComponentScope::None + ComponentScope::Utilities } fn description() -> Option<&'static str> { diff --git a/crates/ui/src/styles/color.rs b/crates/ui/src/styles/color.rs index c7b995d39a..586b2ccc57 100644 --- a/crates/ui/src/styles/color.rs +++ b/crates/ui/src/styles/color.rs @@ -126,7 +126,7 @@ impl From for Color { impl Component for Color { fn scope() -> ComponentScope { - ComponentScope::None + ComponentScope::Utilities } fn description() -> Option<&'static str> { From 9caa9d042a22477c107a24ab77e4325a35efe95b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 6 Aug 2025 02:24:40 +0300 Subject: [PATCH 102/693] Use new language server info on remote servers (#35682) * Straightens out the `*_ext.rs` workflow for clangd and rust-analyzer: no need to asynchronously query for the language server, as we sync that information already. * Fixes inlay hints editor menu toggle not being shown in the remote sessions Release Notes: - Fixed inlay hints editor menu toggle not being shown in the remote sessions --- crates/collab/src/rpc.rs | 3 - crates/collab/src/tests/editor_tests.rs | 16 +- crates/editor/src/clangd_ext.rs | 12 +- crates/editor/src/editor.rs | 1 - crates/editor/src/lsp_ext.rs | 168 +++++++----------- crates/editor/src/rust_analyzer_ext.rs | 64 ++++--- crates/project/src/lsp_command.rs | 22 +-- crates/project/src/lsp_store.rs | 29 --- crates/project/src/lsp_store/clangd_ext.rs | 10 +- .../src/lsp_store/rust_analyzer_ext.rs | 34 ++-- crates/project/src/project.rs | 90 +++++----- crates/proto/proto/lsp.proto | 10 -- crates/proto/proto/zed.proto | 4 +- crates/proto/src/proto.rs | 4 - 14 files changed, 176 insertions(+), 291 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 22b21f2c7a..87172beca4 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -340,9 +340,6 @@ impl Server { .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) - .add_request_handler( - forward_read_only_project_request::, - ) .add_request_handler(forward_read_only_project_request::) .add_request_handler( forward_mutating_project_request::, diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 1d28c7f6ef..8754b53f6e 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -24,10 +24,7 @@ use language::{ }; use project::{ ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT, - lsp_store::{ - lsp_ext_command::{ExpandedMacro, LspExtExpandMacro}, - rust_analyzer_ext::RUST_ANALYZER_NAME, - }, + lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro}, project_settings::{InlineBlameSettings, ProjectSettings}, }; use recent_projects::disconnected_overlay::DisconnectedOverlay; @@ -3786,11 +3783,18 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes cx_b.update(editor::init); client_a.language_registry().add(rust_lang()); - client_b.language_registry().add(rust_lang()); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { - name: RUST_ANALYZER_NAME, + name: "rust-analyzer", + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + name: "rust-analyzer", ..FakeLspAdapter::default() }, ); diff --git a/crates/editor/src/clangd_ext.rs b/crates/editor/src/clangd_ext.rs index b745bf8c37..3239fdc653 100644 --- a/crates/editor/src/clangd_ext.rs +++ b/crates/editor/src/clangd_ext.rs @@ -29,16 +29,14 @@ pub fn switch_source_header( return; }; - let server_lookup = - find_specific_language_server_in_selection(editor, cx, is_c_language, CLANGD_SERVER_NAME); + let Some((_, _, server_to_query, buffer)) = + find_specific_language_server_in_selection(editor, cx, is_c_language, CLANGD_SERVER_NAME) + else { + return; + }; let project = project.clone(); let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); cx.spawn_in(window, async move |_editor, cx| { - let Some((_, _, server_to_query, buffer)) = - server_lookup.await - else { - return Ok(()); - }; let source_file = buffer.read_with(cx, |buffer, _| { buffer.file().map(|file| file.path()).map(|path| path.to_string_lossy().to_string()).unwrap_or_else(|| "Unknown".to_string()) })?; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 30feca7402..156fda1b37 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -22192,7 +22192,6 @@ impl SemanticsProvider for Entity { } fn supports_inlay_hints(&self, buffer: &Entity, cx: &mut App) -> bool { - // TODO: make this work for remote projects self.update(cx, |project, cx| { if project .active_debug_session(cx) diff --git a/crates/editor/src/lsp_ext.rs b/crates/editor/src/lsp_ext.rs index 8d078f304c..6161afbbc0 100644 --- a/crates/editor/src/lsp_ext.rs +++ b/crates/editor/src/lsp_ext.rs @@ -3,9 +3,8 @@ use std::time::Duration; use crate::Editor; use collections::HashMap; -use futures::stream::FuturesUnordered; use gpui::AsyncApp; -use gpui::{App, AppContext as _, Entity, Task}; +use gpui::{App, Entity, Task}; use itertools::Itertools; use language::Buffer; use language::Language; @@ -18,7 +17,6 @@ use project::Project; use project::TaskSourceKind; use project::lsp_store::lsp_ext_command::GetLspRunnables; use smol::future::FutureExt as _; -use smol::stream::StreamExt; use task::ResolvedTask; use task::TaskContext; use text::BufferId; @@ -29,52 +27,32 @@ pub(crate) fn find_specific_language_server_in_selection( editor: &Editor, cx: &mut App, filter_language: F, - language_server_name: &str, -) -> Task, LanguageServerId, Entity)>> + language_server_name: LanguageServerName, +) -> Option<(Anchor, Arc, LanguageServerId, Entity)> where F: Fn(&Language) -> bool, { - let Some(project) = &editor.project else { - return Task::ready(None); - }; - - let applicable_buffers = editor + let project = editor.project.clone()?; + editor .selections .disjoint_anchors() .iter() .filter_map(|selection| Some((selection.head(), selection.head().buffer_id?))) .unique_by(|(_, buffer_id)| *buffer_id) - .filter_map(|(trigger_anchor, buffer_id)| { + .find_map(|(trigger_anchor, buffer_id)| { let buffer = editor.buffer().read(cx).buffer(buffer_id)?; let language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?; if filter_language(&language) { - Some((trigger_anchor, buffer, language)) + let server_id = buffer.update(cx, |buffer, cx| { + project + .read(cx) + .language_server_id_for_name(buffer, &language_server_name, cx) + })?; + Some((trigger_anchor, language, server_id, buffer)) } else { None } }) - .collect::>(); - - let applicable_buffer_tasks = applicable_buffers - .into_iter() - .map(|(trigger_anchor, buffer, language)| { - let task = buffer.update(cx, |buffer, cx| { - project.update(cx, |project, cx| { - project.language_server_id_for_name(buffer, language_server_name, cx) - }) - }); - (trigger_anchor, buffer, language, task) - }) - .collect::>(); - cx.background_spawn(async move { - for (trigger_anchor, buffer, language, task) in applicable_buffer_tasks { - if let Some(server_id) = task.await { - return Some((trigger_anchor, language, server_id, buffer)); - } - } - - None - }) } async fn lsp_task_context( @@ -116,9 +94,9 @@ pub fn lsp_tasks( for_position: Option, cx: &mut App, ) -> Task, ResolvedTask)>)>> { - let mut lsp_task_sources = task_sources + let lsp_task_sources = task_sources .iter() - .map(|(name, buffer_ids)| { + .filter_map(|(name, buffer_ids)| { let buffers = buffer_ids .iter() .filter(|&&buffer_id| match for_position { @@ -127,61 +105,63 @@ pub fn lsp_tasks( }) .filter_map(|&buffer_id| project.read(cx).buffer_for_id(buffer_id, cx)) .collect::>(); - language_server_for_buffers(project.clone(), name.clone(), buffers, cx) + + let server_id = buffers.iter().find_map(|buffer| { + project.read_with(cx, |project, cx| { + project.language_server_id_for_name(buffer.read(cx), name, cx) + }) + }); + server_id.zip(Some(buffers)) }) - .collect::>(); + .collect::>(); cx.spawn(async move |cx| { cx.spawn(async move |cx| { let mut lsp_tasks = HashMap::default(); - while let Some(server_to_query) = lsp_task_sources.next().await { - if let Some((server_id, buffers)) = server_to_query { - let mut new_lsp_tasks = Vec::new(); - for buffer in buffers { - let source_kind = match buffer.update(cx, |buffer, _| { - buffer.language().map(|language| language.name()) - }) { - Ok(Some(language_name)) => TaskSourceKind::Lsp { - server: server_id, - language_name: SharedString::from(language_name), - }, - Ok(None) => continue, - Err(_) => return Vec::new(), - }; - let id_base = source_kind.to_id_base(); - let lsp_buffer_context = lsp_task_context(&project, &buffer, cx) - .await - .unwrap_or_default(); + for (server_id, buffers) in lsp_task_sources { + let mut new_lsp_tasks = Vec::new(); + for buffer in buffers { + let source_kind = match buffer.update(cx, |buffer, _| { + buffer.language().map(|language| language.name()) + }) { + Ok(Some(language_name)) => TaskSourceKind::Lsp { + server: server_id, + language_name: SharedString::from(language_name), + }, + Ok(None) => continue, + Err(_) => return Vec::new(), + }; + let id_base = source_kind.to_id_base(); + let lsp_buffer_context = lsp_task_context(&project, &buffer, cx) + .await + .unwrap_or_default(); - if let Ok(runnables_task) = project.update(cx, |project, cx| { - let buffer_id = buffer.read(cx).remote_id(); - project.request_lsp( - buffer, - LanguageServerToQuery::Other(server_id), - GetLspRunnables { - buffer_id, - position: for_position, + if let Ok(runnables_task) = project.update(cx, |project, cx| { + let buffer_id = buffer.read(cx).remote_id(); + project.request_lsp( + buffer, + LanguageServerToQuery::Other(server_id), + GetLspRunnables { + buffer_id, + position: for_position, + }, + cx, + ) + }) { + if let Some(new_runnables) = runnables_task.await.log_err() { + new_lsp_tasks.extend(new_runnables.runnables.into_iter().filter_map( + |(location, runnable)| { + let resolved_task = + runnable.resolve_task(&id_base, &lsp_buffer_context)?; + Some((location, resolved_task)) }, - cx, - ) - }) { - if let Some(new_runnables) = runnables_task.await.log_err() { - new_lsp_tasks.extend( - new_runnables.runnables.into_iter().filter_map( - |(location, runnable)| { - let resolved_task = runnable - .resolve_task(&id_base, &lsp_buffer_context)?; - Some((location, resolved_task)) - }, - ), - ); - } + )); } - lsp_tasks - .entry(source_kind) - .or_insert_with(Vec::new) - .append(&mut new_lsp_tasks); } + lsp_tasks + .entry(source_kind) + .or_insert_with(Vec::new) + .append(&mut new_lsp_tasks); } } lsp_tasks.into_iter().collect() @@ -198,27 +178,3 @@ pub fn lsp_tasks( .await }) } - -fn language_server_for_buffers( - project: Entity, - name: LanguageServerName, - candidates: Vec>, - cx: &mut App, -) -> Task>)>> { - cx.spawn(async move |cx| { - for buffer in &candidates { - let server_id = buffer - .update(cx, |buffer, cx| { - project.update(cx, |project, cx| { - project.language_server_id_for_name(buffer, &name.0, cx) - }) - }) - .ok()? - .await; - if let Some(server_id) = server_id { - return Some((server_id, candidates)); - } - } - None - }) -} diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index da0f11036f..2b8150de67 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -57,21 +57,21 @@ pub fn go_to_parent_module( return; }; - let server_lookup = find_specific_language_server_in_selection( - editor, - cx, - is_rust_language, - RUST_ANALYZER_NAME, - ); + let Some((trigger_anchor, _, server_to_query, buffer)) = + find_specific_language_server_in_selection( + editor, + cx, + is_rust_language, + RUST_ANALYZER_NAME, + ) + else { + return; + }; let project = project.clone(); let lsp_store = project.read(cx).lsp_store(); let upstream_client = lsp_store.read(cx).upstream_client(); cx.spawn_in(window, async move |editor, cx| { - let Some((trigger_anchor, _, server_to_query, buffer)) = server_lookup.await else { - return anyhow::Ok(()); - }; - let location_links = if let Some((client, project_id)) = upstream_client { let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id())?; @@ -121,7 +121,7 @@ pub fn go_to_parent_module( ) })? .await?; - Ok(()) + anyhow::Ok(()) }) .detach_and_log_err(cx); } @@ -139,21 +139,19 @@ pub fn expand_macro_recursively( return; }; - let server_lookup = find_specific_language_server_in_selection( - editor, - cx, - is_rust_language, - RUST_ANALYZER_NAME, - ); - + let Some((trigger_anchor, rust_language, server_to_query, buffer)) = + find_specific_language_server_in_selection( + editor, + cx, + is_rust_language, + RUST_ANALYZER_NAME, + ) + else { + return; + }; let project = project.clone(); let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); cx.spawn_in(window, async move |_editor, cx| { - let Some((trigger_anchor, rust_language, server_to_query, buffer)) = server_lookup.await - else { - return Ok(()); - }; - let macro_expansion = if let Some((client, project_id)) = upstream_client { let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id())?; let request = proto::LspExtExpandMacro { @@ -231,20 +229,20 @@ pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mu return; }; - let server_lookup = find_specific_language_server_in_selection( - editor, - cx, - is_rust_language, - RUST_ANALYZER_NAME, - ); + let Some((trigger_anchor, _, server_to_query, buffer)) = + find_specific_language_server_in_selection( + editor, + cx, + is_rust_language, + RUST_ANALYZER_NAME, + ) + else { + return; + }; let project = project.clone(); let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); cx.spawn_in(window, async move |_editor, cx| { - let Some((trigger_anchor, _, server_to_query, buffer)) = server_lookup.await else { - return Ok(()); - }; - let docs_urls = if let Some((client, project_id)) = upstream_client { let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id())?; let request = proto::LspExtOpenDocs { diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index f8e69e2185..c458b6b300 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -3284,6 +3284,16 @@ impl InlayHints { }) .unwrap_or(false) } + + pub fn check_capabilities(capabilities: &ServerCapabilities) -> bool { + capabilities + .inlay_hint_provider + .as_ref() + .is_some_and(|inlay_hint_provider| match inlay_hint_provider { + lsp::OneOf::Left(enabled) => *enabled, + lsp::OneOf::Right(_) => true, + }) + } } #[async_trait(?Send)] @@ -3297,17 +3307,7 @@ impl LspCommand for InlayHints { } fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - let Some(inlay_hint_provider) = &capabilities.server_capabilities.inlay_hint_provider - else { - return false; - }; - match inlay_hint_provider { - lsp::OneOf::Left(enabled) => *enabled, - lsp::OneOf::Right(inlay_hint_capabilities) => match inlay_hint_capabilities { - lsp::InlayHintServerCapabilities::Options(_) => true, - lsp::InlayHintServerCapabilities::RegistrationOptions(_) => false, - }, - } + Self::check_capabilities(&capabilities.server_capabilities) } fn to_lsp( diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 6d448a6fea..4489f9f043 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3671,7 +3671,6 @@ impl LspStore { client.add_entity_request_handler(Self::handle_apply_additional_edits_for_completion); client.add_entity_request_handler(Self::handle_register_buffer_with_language_servers); client.add_entity_request_handler(Self::handle_rename_project_entry); - client.add_entity_request_handler(Self::handle_language_server_id_for_name); client.add_entity_request_handler(Self::handle_pull_workspace_diagnostics); client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_lsp_command::); @@ -8745,34 +8744,6 @@ impl LspStore { Ok(proto::Ack {}) } - async fn handle_language_server_id_for_name( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let name = &envelope.payload.name; - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - lsp_store - .update(&mut cx, |lsp_store, cx| { - let buffer = lsp_store.buffer_store.read(cx).get_existing(buffer_id)?; - let server_id = buffer.update(cx, |buffer, cx| { - lsp_store - .language_servers_for_local_buffer(buffer, cx) - .find_map(|(adapter, server)| { - if adapter.name.0.as_ref() == name { - Some(server.server_id()) - } else { - None - } - }) - }); - Ok(server_id) - })? - .map(|server_id| proto::LanguageServerIdForNameResponse { - server_id: server_id.map(|id| id.to_proto()), - }) - } - async fn handle_rename_project_entry( this: Entity, envelope: TypedEnvelope, diff --git a/crates/project/src/lsp_store/clangd_ext.rs b/crates/project/src/lsp_store/clangd_ext.rs index 6a09bb99b4..cf3507352e 100644 --- a/crates/project/src/lsp_store/clangd_ext.rs +++ b/crates/project/src/lsp_store/clangd_ext.rs @@ -3,12 +3,12 @@ use std::sync::Arc; use ::serde::{Deserialize, Serialize}; use gpui::WeakEntity; use language::{CachedLspAdapter, Diagnostic, DiagnosticSourceKind}; -use lsp::LanguageServer; +use lsp::{LanguageServer, LanguageServerName}; use util::ResultExt as _; use crate::LspStore; -pub const CLANGD_SERVER_NAME: &str = "clangd"; +pub const CLANGD_SERVER_NAME: LanguageServerName = LanguageServerName::new_static("clangd"); const INACTIVE_REGION_MESSAGE: &str = "inactive region"; const INACTIVE_DIAGNOSTIC_SEVERITY: lsp::DiagnosticSeverity = lsp::DiagnosticSeverity::INFORMATION; @@ -34,7 +34,7 @@ pub fn is_inactive_region(diag: &Diagnostic) -> bool { && diag .source .as_ref() - .is_some_and(|v| v == CLANGD_SERVER_NAME) + .is_some_and(|v| v == &CLANGD_SERVER_NAME.0) } pub fn is_lsp_inactive_region(diag: &lsp::Diagnostic) -> bool { @@ -43,7 +43,7 @@ pub fn is_lsp_inactive_region(diag: &lsp::Diagnostic) -> bool { && diag .source .as_ref() - .is_some_and(|v| v == CLANGD_SERVER_NAME) + .is_some_and(|v| v == &CLANGD_SERVER_NAME.0) } pub fn register_notifications( @@ -51,7 +51,7 @@ pub fn register_notifications( language_server: &LanguageServer, adapter: Arc, ) { - if language_server.name().0 != CLANGD_SERVER_NAME { + if language_server.name() != CLANGD_SERVER_NAME { return; } let server_id = language_server.server_id(); diff --git a/crates/project/src/lsp_store/rust_analyzer_ext.rs b/crates/project/src/lsp_store/rust_analyzer_ext.rs index d78715d385..6c425717a8 100644 --- a/crates/project/src/lsp_store/rust_analyzer_ext.rs +++ b/crates/project/src/lsp_store/rust_analyzer_ext.rs @@ -2,12 +2,12 @@ use ::serde::{Deserialize, Serialize}; use anyhow::Context as _; use gpui::{App, Entity, Task, WeakEntity}; use language::ServerHealth; -use lsp::LanguageServer; +use lsp::{LanguageServer, LanguageServerName}; use rpc::proto; use crate::{LspStore, LspStoreEvent, Project, ProjectPath, lsp_store}; -pub const RUST_ANALYZER_NAME: &str = "rust-analyzer"; +pub const RUST_ANALYZER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer"); pub const CARGO_DIAGNOSTICS_SOURCE_NAME: &str = "rustc"; /// Experimental: Informs the end user about the state of the server @@ -97,13 +97,9 @@ pub fn cancel_flycheck( cx.spawn(async move |cx| { let buffer = buffer.await?; - let Some(rust_analyzer_server) = project - .update(cx, |project, cx| { - buffer.update(cx, |buffer, cx| { - project.language_server_id_for_name(buffer, RUST_ANALYZER_NAME, cx) - }) - })? - .await + let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| { + project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) + })? else { return Ok(()); }; @@ -148,13 +144,9 @@ pub fn run_flycheck( cx.spawn(async move |cx| { let buffer = buffer.await?; - let Some(rust_analyzer_server) = project - .update(cx, |project, cx| { - buffer.update(cx, |buffer, cx| { - project.language_server_id_for_name(buffer, RUST_ANALYZER_NAME, cx) - }) - })? - .await + let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| { + project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) + })? else { return Ok(()); }; @@ -204,13 +196,9 @@ pub fn clear_flycheck( cx.spawn(async move |cx| { let buffer = buffer.await?; - let Some(rust_analyzer_server) = project - .update(cx, |project, cx| { - buffer.update(cx, |buffer, cx| { - project.language_server_id_for_name(buffer, RUST_ANALYZER_NAME, cx) - }) - })? - .await + let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| { + project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) + })? else { return Ok(()); }; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 398e8bde87..cca026ec87 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5002,63 +5002,53 @@ impl Project { } pub fn any_language_server_supports_inlay_hints(&self, buffer: &Buffer, cx: &mut App) -> bool { - self.lsp_store.update(cx, |this, cx| { - this.language_servers_for_local_buffer(buffer, cx) - .any( - |(_, server)| match server.capabilities().inlay_hint_provider { - Some(lsp::OneOf::Left(enabled)) => enabled, - Some(lsp::OneOf::Right(_)) => true, - None => false, - }, - ) + let Some(language) = buffer.language().cloned() else { + return false; + }; + self.lsp_store.update(cx, |lsp_store, _| { + let relevant_language_servers = lsp_store + .languages + .lsp_adapters(&language.name()) + .into_iter() + .map(|lsp_adapter| lsp_adapter.name()) + .collect::>(); + lsp_store + .language_server_statuses() + .filter_map(|(server_id, server_status)| { + relevant_language_servers + .contains(&server_status.name) + .then_some(server_id) + }) + .filter_map(|server_id| lsp_store.lsp_server_capabilities.get(&server_id)) + .any(InlayHints::check_capabilities) }) } pub fn language_server_id_for_name( &self, buffer: &Buffer, - name: &str, - cx: &mut App, - ) -> Task> { - if self.is_local() { - Task::ready(self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store - .language_servers_for_local_buffer(buffer, cx) - .find_map(|(adapter, server)| { - if adapter.name.0 == name { - Some(server.server_id()) - } else { - None - } - }) - })) - } else if let Some(project_id) = self.remote_id() { - let request = self.client.request(proto::LanguageServerIdForName { - project_id, - buffer_id: buffer.remote_id().to_proto(), - name: name.to_string(), - }); - cx.background_spawn(async move { - let response = request.await.log_err()?; - response.server_id.map(LanguageServerId::from_proto) - }) - } else if let Some(ssh_client) = self.ssh_client.as_ref() { - let request = - ssh_client - .read(cx) - .proto_client() - .request(proto::LanguageServerIdForName { - project_id: SSH_PROJECT_ID, - buffer_id: buffer.remote_id().to_proto(), - name: name.to_string(), - }); - cx.background_spawn(async move { - let response = request.await.log_err()?; - response.server_id.map(LanguageServerId::from_proto) - }) - } else { - Task::ready(None) + name: &LanguageServerName, + cx: &App, + ) -> Option { + let language = buffer.language()?; + let relevant_language_servers = self + .languages + .lsp_adapters(&language.name()) + .into_iter() + .map(|lsp_adapter| lsp_adapter.name()) + .collect::>(); + if !relevant_language_servers.contains(name) { + return None; } + self.language_server_statuses(cx) + .filter(|(_, server_status)| relevant_language_servers.contains(&server_status.name)) + .find_map(|(server_id, server_status)| { + if &server_status.name == name { + Some(server_id) + } else { + None + } + }) } pub fn has_language_servers_for(&self, buffer: &Buffer, cx: &mut App) -> bool { diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index 1e693dfdf3..164a7d3a50 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -818,16 +818,6 @@ message LspResponse { uint64 server_id = 7; } -message LanguageServerIdForName { - uint64 project_id = 1; - uint64 buffer_id = 2; - string name = 3; -} - -message LanguageServerIdForNameResponse { - optional uint64 server_id = 1; -} - message LspExtRunnables { uint64 project_id = 1; uint64 buffer_id = 2; diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 9de5c2c0c7..bb97bd500a 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -362,9 +362,6 @@ message Envelope { GetDocumentSymbols get_document_symbols = 330; GetDocumentSymbolsResponse get_document_symbols_response = 331; - LanguageServerIdForName language_server_id_for_name = 332; - LanguageServerIdForNameResponse language_server_id_for_name_response = 333; - LoadCommitDiff load_commit_diff = 334; LoadCommitDiffResponse load_commit_diff_response = 335; @@ -424,6 +421,7 @@ message Envelope { reserved 247 to 254; reserved 255 to 256; reserved 280 to 281; + reserved 332 to 333; } message Hello { diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 4c447e2eca..9edb041b4b 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -121,8 +121,6 @@ messages!( (GetImplementationResponse, Background), (GetLlmToken, Background), (GetLlmTokenResponse, Background), - (LanguageServerIdForName, Background), - (LanguageServerIdForNameResponse, Background), (OpenUnstagedDiff, Foreground), (OpenUnstagedDiffResponse, Foreground), (OpenUncommittedDiff, Foreground), @@ -431,7 +429,6 @@ request_messages!( (UpdateWorktree, Ack), (UpdateRepository, Ack), (RemoveRepository, Ack), - (LanguageServerIdForName, LanguageServerIdForNameResponse), (LspExtExpandMacro, LspExtExpandMacroResponse), (LspExtOpenDocs, LspExtOpenDocsResponse), (LspExtRunnables, LspExtRunnablesResponse), @@ -588,7 +585,6 @@ entity_messages!( OpenServerSettings, GetPermalinkToLine, LanguageServerPromptRequest, - LanguageServerIdForName, GitGetBranches, UpdateGitBranch, ListToolchains, From d0de81b0b47ad42a360df448001dd8314affe153 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Aug 2025 16:42:17 -0700 Subject: [PATCH 103/693] windows: Handle scale factor change while window is maximized (#35686) Fixes https://github.com/zed-industries/zed/issues/33257 Previously, the scale-factor-change-handling logic relied on `SetWindowPos` enqueuing a `WM_SIZE` window event. But that does not happen when the window is maximized. So when the scale factor changed, maximized windows neglected to call their `resize` callback, and would misinterpret the positions of mouse events. This PR adds special logic for maximized windows, to ensure that the size is updated appropriately. Release Notes: - N/A --- crates/gpui/src/platform/windows/events.rs | 38 ++++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 00b22fa807..4ab257d27a 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -174,20 +174,37 @@ impl WindowsWindowInner { let width = lparam.loword().max(1) as i32; let height = lparam.hiword().max(1) as i32; let new_size = size(DevicePixels(width), DevicePixels(height)); + let scale_factor = lock.scale_factor; + let mut should_resize_renderer = false; if lock.restore_from_minimized.is_some() { lock.callbacks.request_frame = lock.restore_from_minimized.take(); } else { - lock.renderer.resize(new_size).log_err(); + should_resize_renderer = true; + } + drop(lock); + + self.handle_size_change(new_size, scale_factor, should_resize_renderer); + Some(0) + } + + fn handle_size_change( + &self, + device_size: Size, + scale_factor: f32, + should_resize_renderer: bool, + ) { + let new_logical_size = device_size.to_pixels(scale_factor); + let mut lock = self.state.borrow_mut(); + lock.logical_size = new_logical_size; + if should_resize_renderer { + lock.renderer.resize(device_size).log_err(); } - let new_size = new_size.to_pixels(scale_factor); - lock.logical_size = new_size; if let Some(mut callback) = lock.callbacks.resize.take() { drop(lock); - callback(new_size, scale_factor); + callback(new_logical_size, scale_factor); self.state.borrow_mut().callbacks.resize = Some(callback); } - Some(0) } fn handle_size_move_loop(&self, handle: HWND) -> Option { @@ -747,7 +764,9 @@ impl WindowsWindowInner { ) -> Option { let new_dpi = wparam.loword() as f32; let mut lock = self.state.borrow_mut(); - lock.scale_factor = new_dpi / USER_DEFAULT_SCREEN_DPI as f32; + let is_maximized = lock.is_maximized(); + let new_scale_factor = new_dpi / USER_DEFAULT_SCREEN_DPI as f32; + lock.scale_factor = new_scale_factor; lock.border_offset.update(handle).log_err(); drop(lock); @@ -771,6 +790,13 @@ impl WindowsWindowInner { .log_err(); } + // When maximized, SetWindowPos doesn't send WM_SIZE, so we need to manually + // update the size and call the resize callback + if is_maximized { + let device_size = size(DevicePixels(width), DevicePixels(height)); + self.handle_size_change(device_size, new_scale_factor, true); + } + Some(0) } From 53175263a1b4bb6ce99f4b0cadc06476c4e9624f Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 5 Aug 2025 17:02:26 -0700 Subject: [PATCH 104/693] Simplify `ListState` API (#35685) Follow up to: https://github.com/zed-industries/zed/pull/35670, simplifies the List state APIs so you no longer have to worry about strong vs. weak pointers when rendering list items. Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga --- crates/agent_ui/src/acp/thread_view.rs | 37 ++- crates/agent_ui/src/active_thread.rs | 17 +- crates/agent_ui/src/agent_panel.rs | 9 - crates/collab_ui/src/chat_panel.rs | 41 ++-- crates/collab_ui/src/collab_panel.rs | 24 +- crates/collab_ui/src/notification_panel.rs | 22 +- .../src/session/running/loaded_source_list.rs | 24 +- .../src/session/running/stack_frame_list.rs | 21 +- crates/gpui/src/elements/list.rs | 84 ++++--- .../src/markdown_preview_view.rs | 217 +++++++++--------- .../markdown_preview/src/markdown_renderer.rs | 28 ++- crates/picker/src/picker.rs | 39 ++-- crates/repl/src/notebook/notebook_ui.rs | 38 ++- .../src/project_index_debug_view.rs | 22 +- crates/zed/src/zed/component_preview.rs | 102 +++----- 15 files changed, 322 insertions(+), 403 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 6449643cac..f7c359fe99 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -173,23 +173,7 @@ impl AcpThreadView { let mention_set = mention_set.clone(); - let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0), { - let this = cx.entity().downgrade(); - move |index: usize, window, cx| { - let Some(this) = this.upgrade() else { - return Empty.into_any(); - }; - this.update(cx, |this, cx| { - let Some((entry, len)) = this.thread().and_then(|thread| { - let entries = &thread.read(cx).entries(); - Some((entries.get(index)?, entries.len())) - }) else { - return Empty.into_any(); - }; - this.render_entry(index, len, entry, window, cx) - }) - } - }); + let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0)); Self { agent: agent.clone(), @@ -2552,10 +2536,21 @@ impl Render for AcpThreadView { v_flex().flex_1().map(|this| { if self.list_state.item_count() > 0 { this.child( - list(self.list_state.clone()) - .with_sizing_behavior(gpui::ListSizingBehavior::Auto) - .flex_grow() - .into_any(), + list( + self.list_state.clone(), + cx.processor(|this, index: usize, window, cx| { + let Some((entry, len)) = this.thread().and_then(|thread| { + let entries = &thread.read(cx).entries(); + Some((entries.get(index)?, entries.len())) + }) else { + return Empty.into_any(); + }; + this.render_entry(index, len, entry, window, cx) + }), + ) + .with_sizing_behavior(gpui::ListSizingBehavior::Auto) + .flex_grow() + .into_any(), ) .children(match thread_clone.read(cx).status() { ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => { diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 04a093c7d0..ed227f22e4 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -780,13 +780,7 @@ impl ActiveThread { cx.observe_global::(|_, cx| cx.notify()), ]; - let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.), { - let this = cx.entity().downgrade(); - move |ix, window: &mut Window, cx: &mut App| { - this.update(cx, |this, cx| this.render_message(ix, window, cx)) - .unwrap() - } - }); + let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.)); let workspace_subscription = if let Some(workspace) = workspace.upgrade() { Some(cx.observe_release(&workspace, |this, _, cx| { @@ -1846,7 +1840,12 @@ impl ActiveThread { ))) } - fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context) -> AnyElement { + fn render_message( + &mut self, + ix: usize, + window: &mut Window, + cx: &mut Context, + ) -> AnyElement { let message_id = self.messages[ix]; let workspace = self.workspace.clone(); let thread = self.thread.read(cx); @@ -3613,7 +3612,7 @@ impl Render for ActiveThread { this.hide_scrollbar_later(cx); }), ) - .child(list(self.list_state.clone()).flex_grow()) + .child(list(self.list_state.clone(), cx.processor(Self::render_message)).flex_grow()) .when_some(self.render_vertical_scrollbar(cx), |this, scrollbar| { this.child(scrollbar) }) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 717778bb98..0f2d431bc1 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1471,7 +1471,6 @@ impl AgentPanel { let current_is_special = current_is_history || current_is_config; let new_is_special = new_is_history || new_is_config; - let mut old_acp_thread = None; match &self.active_view { ActiveView::Thread { thread, .. } => { @@ -1483,9 +1482,6 @@ impl AgentPanel { }); } } - ActiveView::ExternalAgentThread { thread_view } => { - old_acp_thread.replace(thread_view.downgrade()); - } _ => {} } @@ -1516,11 +1512,6 @@ impl AgentPanel { self.active_view = new_view; } - debug_assert!( - old_acp_thread.map_or(true, |thread| !thread.is_upgradable()), - "AcpThreadView leaked" - ); - self.acp_message_history.borrow_mut().reset_position(); self.focus_handle(cx).focus(window); diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 3a9b568264..51d9f003f8 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -103,28 +103,16 @@ impl ChatPanel { }); cx.new(|cx| { - let entity = cx.entity().downgrade(); - let message_list = ListState::new( - 0, - gpui::ListAlignment::Bottom, - px(1000.), - move |ix, window, cx| { - if let Some(entity) = entity.upgrade() { - entity.update(cx, |this: &mut Self, cx| { - this.render_message(ix, window, cx).into_any_element() - }) - } else { - div().into_any() - } - }, - ); + let message_list = ListState::new(0, gpui::ListAlignment::Bottom, px(1000.)); - message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, _, cx| { - if event.visible_range.start < MESSAGE_LOADING_THRESHOLD { - this.load_more_messages(cx); - } - this.is_scrolled_to_bottom = !event.is_scrolled; - })); + message_list.set_scroll_handler(cx.listener( + |this: &mut Self, event: &ListScrollEvent, _, cx| { + if event.visible_range.start < MESSAGE_LOADING_THRESHOLD { + this.load_more_messages(cx); + } + this.is_scrolled_to_bottom = !event.is_scrolled; + }, + )); let local_offset = chrono::Local::now().offset().local_minus_utc(); let mut this = Self { @@ -399,7 +387,7 @@ impl ChatPanel { ix: usize, window: &mut Window, cx: &mut Context, - ) -> impl IntoElement { + ) -> AnyElement { let active_chat = &self.active_chat.as_ref().unwrap().0; let (message, is_continuation_from_previous, is_admin) = active_chat.update(cx, |active_chat, cx| { @@ -582,6 +570,7 @@ impl ChatPanel { self.render_popover_buttons(message_id, can_delete_message, can_edit_message, cx) .mt_neg_2p5(), ) + .into_any_element() } fn has_open_menu(&self, message_id: Option) -> bool { @@ -979,7 +968,13 @@ impl Render for ChatPanel { ) .child(div().flex_grow().px_2().map(|this| { if self.active_chat.is_some() { - this.child(list(self.message_list.clone()).size_full()) + this.child( + list( + self.message_list.clone(), + cx.processor(Self::render_message), + ) + .size_full(), + ) } else { this.child( div() diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index bae5dcfdb5..bb7c2ba1cd 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -324,20 +324,6 @@ impl CollabPanel { ) .detach(); - let entity = cx.entity().downgrade(); - let list_state = ListState::new( - 0, - gpui::ListAlignment::Top, - px(1000.), - move |ix, window, cx| { - if let Some(entity) = entity.upgrade() { - entity.update(cx, |this, cx| this.render_list_entry(ix, window, cx)) - } else { - div().into_any() - } - }, - ); - let mut this = Self { width: None, focus_handle: cx.focus_handle(), @@ -345,7 +331,7 @@ impl CollabPanel { fs: workspace.app_state().fs.clone(), pending_serialization: Task::ready(None), context_menu: None, - list_state, + list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)), channel_name_editor, filter_editor, entries: Vec::default(), @@ -2431,7 +2417,13 @@ impl CollabPanel { }); v_flex() .size_full() - .child(list(self.list_state.clone()).size_full()) + .child( + list( + self.list_state.clone(), + cx.processor(Self::render_list_entry), + ) + .size_full(), + ) .child( v_flex() .child(div().mx_2().border_primary(cx).border_t_1()) diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index c3e834b645..3a280ff667 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -118,16 +118,7 @@ impl NotificationPanel { }) .detach(); - let entity = cx.entity().downgrade(); - let notification_list = - ListState::new(0, ListAlignment::Top, px(1000.), move |ix, window, cx| { - entity - .upgrade() - .and_then(|entity| { - entity.update(cx, |this, cx| this.render_notification(ix, window, cx)) - }) - .unwrap_or_else(|| div().into_any()) - }); + let notification_list = ListState::new(0, ListAlignment::Top, px(1000.)); notification_list.set_scroll_handler(cx.listener( |this, event: &ListScrollEvent, _, cx| { if event.count.saturating_sub(event.visible_range.end) < LOADING_THRESHOLD { @@ -687,7 +678,16 @@ impl Render for NotificationPanel { ), ) } else { - this.child(list(self.notification_list.clone()).size_full()) + this.child( + list( + self.notification_list.clone(), + cx.processor(|this, ix, window, cx| { + this.render_notification(ix, window, cx) + .unwrap_or_else(|| div().into_any()) + }), + ) + .size_full(), + ) } }) } diff --git a/crates/debugger_ui/src/session/running/loaded_source_list.rs b/crates/debugger_ui/src/session/running/loaded_source_list.rs index dd5487e042..6b376bb892 100644 --- a/crates/debugger_ui/src/session/running/loaded_source_list.rs +++ b/crates/debugger_ui/src/session/running/loaded_source_list.rs @@ -13,22 +13,8 @@ pub(crate) struct LoadedSourceList { impl LoadedSourceList { pub fn new(session: Entity, cx: &mut Context) -> Self { - let weak_entity = cx.weak_entity(); let focus_handle = cx.focus_handle(); - - let list = ListState::new( - 0, - gpui::ListAlignment::Top, - px(1000.), - move |ix, _window, cx| { - weak_entity - .upgrade() - .map(|loaded_sources| { - loaded_sources.update(cx, |this, cx| this.render_entry(ix, cx)) - }) - .unwrap_or(div().into_any()) - }, - ); + let list = ListState::new(0, gpui::ListAlignment::Top, px(1000.)); let _subscription = cx.subscribe(&session, |this, _, event, cx| match event { SessionEvent::Stopped(_) | SessionEvent::LoadedSources => { @@ -98,6 +84,12 @@ impl Render for LoadedSourceList { .track_focus(&self.focus_handle) .size_full() .p_1() - .child(list(self.list.clone()).size_full()) + .child( + list( + self.list.clone(), + cx.processor(|this, ix, _window, cx| this.render_entry(ix, cx)), + ) + .size_full(), + ) } } diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index da3674c8e2..2149502f4a 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -70,13 +70,7 @@ impl StackFrameList { _ => {} }); - let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.), { - let this = cx.weak_entity(); - move |ix, _window, cx| { - this.update(cx, |this, cx| this.render_entry(ix, cx)) - .unwrap_or(div().into_any()) - } - }); + let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.)); let scrollbar_state = ScrollbarState::new(list_state.clone()); let mut this = Self { @@ -708,11 +702,14 @@ impl StackFrameList { self.activate_selected_entry(window, cx); } - fn render_list(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .p_1() - .size_full() - .child(list(self.list_state.clone()).size_full()) + fn render_list(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div().p_1().size_full().child( + list( + self.list_state.clone(), + cx.processor(|this, ix, _window, cx| this.render_entry(ix, cx)), + ) + .size_full(), + ) } } diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 709323ef58..39f38bdc69 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -18,10 +18,16 @@ use refineable::Refineable as _; use std::{cell::RefCell, ops::Range, rc::Rc}; use sum_tree::{Bias, Dimensions, SumTree}; +type RenderItemFn = dyn FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static; + /// Construct a new list element -pub fn list(state: ListState) -> List { +pub fn list( + state: ListState, + render_item: impl FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static, +) -> List { List { state, + render_item: Box::new(render_item), style: StyleRefinement::default(), sizing_behavior: ListSizingBehavior::default(), } @@ -30,6 +36,7 @@ pub fn list(state: ListState) -> List { /// A list element pub struct List { state: ListState, + render_item: Box, style: StyleRefinement, sizing_behavior: ListSizingBehavior, } @@ -55,7 +62,6 @@ impl std::fmt::Debug for ListState { struct StateInner { last_layout_bounds: Option>, last_padding: Option>, - render_item: Box AnyElement>, items: SumTree, logical_scroll_top: Option, alignment: ListAlignment, @@ -186,19 +192,10 @@ impl ListState { /// above and below the visible area. Elements within this area will /// be measured even though they are not visible. This can help ensure /// that the list doesn't flicker or pop in when scrolling. - pub fn new( - item_count: usize, - alignment: ListAlignment, - overdraw: Pixels, - render_item: R, - ) -> Self - where - R: 'static + FnMut(usize, &mut Window, &mut App) -> AnyElement, - { + pub fn new(item_count: usize, alignment: ListAlignment, overdraw: Pixels) -> Self { let this = Self(Rc::new(RefCell::new(StateInner { last_layout_bounds: None, last_padding: None, - render_item: Box::new(render_item), items: SumTree::default(), logical_scroll_top: None, alignment, @@ -532,6 +529,7 @@ impl StateInner { available_width: Option, available_height: Pixels, padding: &Edges, + render_item: &mut RenderItemFn, window: &mut Window, cx: &mut App, ) -> LayoutItemsResponse { @@ -566,7 +564,7 @@ impl StateInner { // If we're within the visible area or the height wasn't cached, render and measure the item's element if visible_height < available_height || size.is_none() { let item_index = scroll_top.item_ix + ix; - let mut element = (self.render_item)(item_index, window, cx); + let mut element = render_item(item_index, window, cx); let element_size = element.layout_as_root(available_item_space, window, cx); size = Some(element_size); if visible_height < available_height { @@ -601,7 +599,7 @@ impl StateInner { cursor.prev(); if let Some(item) = cursor.item() { let item_index = cursor.start().0; - let mut element = (self.render_item)(item_index, window, cx); + let mut element = render_item(item_index, window, cx); let element_size = element.layout_as_root(available_item_space, window, cx); let focus_handle = item.focus_handle(); rendered_height += element_size.height; @@ -650,7 +648,7 @@ impl StateInner { let size = if let ListItem::Measured { size, .. } = item { *size } else { - let mut element = (self.render_item)(cursor.start().0, window, cx); + let mut element = render_item(cursor.start().0, window, cx); element.layout_as_root(available_item_space, window, cx) }; @@ -683,7 +681,7 @@ impl StateInner { while let Some(item) = cursor.item() { if item.contains_focused(window, cx) { let item_index = cursor.start().0; - let mut element = (self.render_item)(cursor.start().0, window, cx); + let mut element = render_item(cursor.start().0, window, cx); let size = element.layout_as_root(available_item_space, window, cx); item_layouts.push_back(ItemLayout { index: item_index, @@ -708,6 +706,7 @@ impl StateInner { bounds: Bounds, padding: Edges, autoscroll: bool, + render_item: &mut RenderItemFn, window: &mut Window, cx: &mut App, ) -> Result { @@ -716,6 +715,7 @@ impl StateInner { Some(bounds.size.width), bounds.size.height, &padding, + render_item, window, cx, ); @@ -753,8 +753,7 @@ impl StateInner { let Some(item) = cursor.item() else { break }; let size = item.size().unwrap_or_else(|| { - let mut item = - (self.render_item)(cursor.start().0, window, cx); + let mut item = render_item(cursor.start().0, window, cx); let item_available_size = size( bounds.size.width.into(), AvailableSpace::MinContent, @@ -876,8 +875,14 @@ impl Element for List { window.rem_size(), ); - let layout_response = - state.layout_items(None, available_height, &padding, window, cx); + let layout_response = state.layout_items( + None, + available_height, + &padding, + &mut self.render_item, + window, + cx, + ); let max_element_width = layout_response.max_item_width; let summary = state.items.summary(); @@ -951,15 +956,16 @@ impl Element for List { let padding = style .padding .to_pixels(bounds.size.into(), window.rem_size()); - let layout = match state.prepaint_items(bounds, padding, true, window, cx) { - Ok(layout) => layout, - Err(autoscroll_request) => { - state.logical_scroll_top = Some(autoscroll_request); - state - .prepaint_items(bounds, padding, false, window, cx) - .unwrap() - } - }; + let layout = + match state.prepaint_items(bounds, padding, true, &mut self.render_item, window, cx) { + Ok(layout) => layout, + Err(autoscroll_request) => { + state.logical_scroll_top = Some(autoscroll_request); + state + .prepaint_items(bounds, padding, false, &mut self.render_item, window, cx) + .unwrap() + } + }; state.last_layout_bounds = Some(bounds); state.last_padding = Some(padding); @@ -1108,9 +1114,7 @@ mod test { let cx = cx.add_empty_window(); - let state = ListState::new(5, crate::ListAlignment::Top, px(10.), |_, _, _| { - div().h(px(10.)).w_full().into_any() - }); + let state = ListState::new(5, crate::ListAlignment::Top, px(10.)); // Ensure that the list is scrolled to the top state.scroll_to(gpui::ListOffset { @@ -1121,7 +1125,11 @@ mod test { struct TestView(ListState); impl Render for TestView { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - list(self.0.clone()).w_full().h_full() + list(self.0.clone(), |_, _, _| { + div().h(px(10.)).w_full().into_any() + }) + .w_full() + .h_full() } } @@ -1154,14 +1162,16 @@ mod test { let cx = cx.add_empty_window(); - let state = ListState::new(5, crate::ListAlignment::Top, px(10.), |_, _, _| { - div().h(px(20.)).w_full().into_any() - }); + let state = ListState::new(5, crate::ListAlignment::Top, px(10.)); struct TestView(ListState); impl Render for TestView { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - list(self.0.clone()).w_full().h_full() + list(self.0.clone(), |_, _, _| { + div().h(px(20.)).w_full().into_any() + }) + .w_full() + .h_full() } } diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index 96e92de19c..a0c8819991 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -18,6 +18,7 @@ use workspace::item::{Item, ItemHandle}; use workspace::{Pane, Workspace}; use crate::markdown_elements::ParsedMarkdownElement; +use crate::markdown_renderer::CheckboxClickedEvent; use crate::{ MovePageDown, MovePageUp, OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide, markdown_elements::ParsedMarkdown, @@ -203,114 +204,7 @@ impl MarkdownPreviewView { cx: &mut Context, ) -> Entity { cx.new(|cx| { - let view = cx.entity().downgrade(); - - let list_state = ListState::new( - 0, - gpui::ListAlignment::Top, - px(1000.), - move |ix, window, cx| { - if let Some(view) = view.upgrade() { - view.update(cx, |this: &mut Self, cx| { - let Some(contents) = &this.contents else { - return div().into_any(); - }; - - let mut render_cx = - RenderContext::new(Some(this.workspace.clone()), window, cx) - .with_checkbox_clicked_callback({ - let view = view.clone(); - move |checked, source_range, window, cx| { - view.update(cx, |view, cx| { - if let Some(editor) = view - .active_editor - .as_ref() - .map(|s| s.editor.clone()) - { - editor.update(cx, |editor, cx| { - let task_marker = - if checked { "[x]" } else { "[ ]" }; - - editor.edit( - vec![(source_range, task_marker)], - cx, - ); - }); - view.parse_markdown_from_active_editor( - false, window, cx, - ); - cx.notify(); - } - }) - } - }); - - let block = contents.children.get(ix).unwrap(); - let rendered_block = render_markdown_block(block, &mut render_cx); - - let should_apply_padding = Self::should_apply_padding_between( - block, - contents.children.get(ix + 1), - ); - - div() - .id(ix) - .when(should_apply_padding, |this| { - this.pb(render_cx.scaled_rems(0.75)) - }) - .group("markdown-block") - .on_click(cx.listener( - move |this, event: &ClickEvent, window, cx| { - if event.click_count() == 2 { - if let Some(source_range) = this - .contents - .as_ref() - .and_then(|c| c.children.get(ix)) - .and_then(|block| block.source_range()) - { - this.move_cursor_to_block( - window, - cx, - source_range.start..source_range.start, - ); - } - } - }, - )) - .map(move |container| { - let indicator = div() - .h_full() - .w(px(4.0)) - .when(ix == this.selected_block, |this| { - this.bg(cx.theme().colors().border) - }) - .group_hover("markdown-block", |s| { - if ix == this.selected_block { - s - } else { - s.bg(cx.theme().colors().border_variant) - } - }) - .rounded_xs(); - - container.child( - div() - .relative() - .child( - div() - .pl(render_cx.scaled_rems(1.0)) - .child(rendered_block), - ) - .child(indicator.absolute().left_0().top_0()), - ) - }) - .into_any() - }) - } else { - div().into_any() - } - }, - ); + let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.)); let mut this = Self { selected_block: 0, @@ -607,10 +501,107 @@ impl Render for MarkdownPreviewView { .p_4() .text_size(buffer_size) .line_height(relative(buffer_line_height.value())) - .child( - div() - .flex_grow() - .map(|this| this.child(list(self.list_state.clone()).size_full())), - ) + .child(div().flex_grow().map(|this| { + this.child( + list( + self.list_state.clone(), + cx.processor(|this, ix, window, cx| { + let Some(contents) = &this.contents else { + return div().into_any(); + }; + + let mut render_cx = + RenderContext::new(Some(this.workspace.clone()), window, cx) + .with_checkbox_clicked_callback(cx.listener( + move |this, e: &CheckboxClickedEvent, window, cx| { + if let Some(editor) = this + .active_editor + .as_ref() + .map(|s| s.editor.clone()) + { + editor.update(cx, |editor, cx| { + let task_marker = + if e.checked() { "[x]" } else { "[ ]" }; + + editor.edit( + vec![(e.source_range(), task_marker)], + cx, + ); + }); + this.parse_markdown_from_active_editor( + false, window, cx, + ); + cx.notify(); + } + }, + )); + + let block = contents.children.get(ix).unwrap(); + let rendered_block = render_markdown_block(block, &mut render_cx); + + let should_apply_padding = Self::should_apply_padding_between( + block, + contents.children.get(ix + 1), + ); + + div() + .id(ix) + .when(should_apply_padding, |this| { + this.pb(render_cx.scaled_rems(0.75)) + }) + .group("markdown-block") + .on_click(cx.listener( + move |this, event: &ClickEvent, window, cx| { + if event.click_count() == 2 { + if let Some(source_range) = this + .contents + .as_ref() + .and_then(|c| c.children.get(ix)) + .and_then(|block: &ParsedMarkdownElement| { + block.source_range() + }) + { + this.move_cursor_to_block( + window, + cx, + source_range.start..source_range.start, + ); + } + } + }, + )) + .map(move |container| { + let indicator = div() + .h_full() + .w(px(4.0)) + .when(ix == this.selected_block, |this| { + this.bg(cx.theme().colors().border) + }) + .group_hover("markdown-block", |s| { + if ix == this.selected_block { + s + } else { + s.bg(cx.theme().colors().border_variant) + } + }) + .rounded_xs(); + + container.child( + div() + .relative() + .child( + div() + .pl(render_cx.scaled_rems(1.0)) + .child(rendered_block), + ) + .child(indicator.absolute().left_0().top_0()), + ) + }) + .into_any() + }), + ) + .size_full(), + ) + })) } } diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 80bed8a6e8..37d2ca2110 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -26,7 +26,22 @@ use ui::{ }; use workspace::{OpenOptions, OpenVisible, Workspace}; -type CheckboxClickedCallback = Arc, &mut Window, &mut App)>>; +pub struct CheckboxClickedEvent { + pub checked: bool, + pub source_range: Range, +} + +impl CheckboxClickedEvent { + pub fn source_range(&self) -> Range { + self.source_range.clone() + } + + pub fn checked(&self) -> bool { + self.checked + } +} + +type CheckboxClickedCallback = Arc>; #[derive(Clone)] pub struct RenderContext { @@ -80,7 +95,7 @@ impl RenderContext { pub fn with_checkbox_clicked_callback( mut self, - callback: impl Fn(bool, Range, &mut Window, &mut App) + 'static, + callback: impl Fn(&CheckboxClickedEvent, &mut Window, &mut App) + 'static, ) -> Self { self.checkbox_clicked_callback = Some(Arc::new(Box::new(callback))); self @@ -229,7 +244,14 @@ fn render_markdown_list_item( }; if window.modifiers().secondary() { - callback(checked, range.clone(), window, cx); + callback( + &CheckboxClickedEvent { + checked, + source_range: range.clone(), + }, + window, + cx, + ); } } }) diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 692bdd5bd7..34af5fed02 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -292,7 +292,7 @@ impl Picker { window: &mut Window, cx: &mut Context, ) -> Self { - let element_container = Self::create_element_container(container, cx); + let element_container = Self::create_element_container(container); let scrollbar_state = match &element_container { ElementContainer::UniformList(scroll_handle) => { ScrollbarState::new(scroll_handle.clone()) @@ -323,31 +323,13 @@ impl Picker { this } - fn create_element_container( - container: ContainerKind, - cx: &mut Context, - ) -> ElementContainer { + fn create_element_container(container: ContainerKind) -> ElementContainer { match container { ContainerKind::UniformList => { ElementContainer::UniformList(UniformListScrollHandle::new()) } ContainerKind::List => { - let entity = cx.entity().downgrade(); - ElementContainer::List(ListState::new( - 0, - gpui::ListAlignment::Top, - px(1000.), - move |ix, window, cx| { - entity - .upgrade() - .map(|entity| { - entity.update(cx, |this, cx| { - this.render_element(window, cx, ix).into_any_element() - }) - }) - .unwrap_or_else(|| div().into_any_element()) - }, - )) + ElementContainer::List(ListState::new(0, gpui::ListAlignment::Top, px(1000.))) } } } @@ -786,11 +768,16 @@ impl Picker { .py_1() .track_scroll(scroll_handle.clone()) .into_any_element(), - ElementContainer::List(state) => list(state.clone()) - .with_sizing_behavior(sizing_behavior) - .flex_grow() - .py_2() - .into_any_element(), + ElementContainer::List(state) => list( + state.clone(), + cx.processor(|this, ix, window, cx| { + this.render_element(window, cx, ix).into_any_element() + }), + ) + .with_sizing_behavior(sizing_behavior) + .flex_grow() + .py_2() + .into_any_element(), } } diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index 3e96cc4d11..2efa51e0cc 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -126,29 +126,7 @@ impl NotebookEditor { let cell_count = cell_order.len(); let this = cx.entity(); - let cell_list = ListState::new( - cell_count, - gpui::ListAlignment::Top, - px(1000.), - move |ix, window, cx| { - notebook_handle - .upgrade() - .and_then(|notebook_handle| { - notebook_handle.update(cx, |notebook, cx| { - notebook - .cell_order - .get(ix) - .and_then(|cell_id| notebook.cell_map.get(cell_id)) - .map(|cell| { - notebook - .render_cell(ix, cell, window, cx) - .into_any_element() - }) - }) - }) - .unwrap_or_else(|| div().into_any()) - }, - ); + let cell_list = ListState::new(cell_count, gpui::ListAlignment::Top, px(1000.)); Self { project, @@ -544,7 +522,19 @@ impl Render for NotebookEditor { .flex_1() .size_full() .overflow_y_scroll() - .child(list(self.cell_list.clone()).size_full()), + .child(list( + self.cell_list.clone(), + cx.processor(|this, ix, window, cx| { + this.cell_order + .get(ix) + .and_then(|cell_id| this.cell_map.get(cell_id)) + .map(|cell| { + this.render_cell(ix, cell, window, cx).into_any_element() + }) + .unwrap_or_else(|| div().into_any()) + }), + )) + .size_full(), ) .child(self.render_notebook_controls(window, cx)) } diff --git a/crates/semantic_index/src/project_index_debug_view.rs b/crates/semantic_index/src/project_index_debug_view.rs index 1b0d87fca0..8d6a49c45c 100644 --- a/crates/semantic_index/src/project_index_debug_view.rs +++ b/crates/semantic_index/src/project_index_debug_view.rs @@ -115,21 +115,9 @@ impl ProjectIndexDebugView { .collect::>(); this.update(cx, |this, cx| { - let view = cx.entity().downgrade(); this.selected_path = Some(PathState { path: file_path, - list_state: ListState::new( - chunks.len(), - gpui::ListAlignment::Top, - px(100.), - move |ix, _, cx| { - if let Some(view) = view.upgrade() { - view.update(cx, |view, cx| view.render_chunk(ix, cx)) - } else { - div().into_any() - } - }, - ), + list_state: ListState::new(chunks.len(), gpui::ListAlignment::Top, px(100.)), chunks, }); cx.notify(); @@ -219,7 +207,13 @@ impl Render for ProjectIndexDebugView { cx.notify(); })), ) - .child(list(selected_path.list_state.clone()).size_full()) + .child( + list( + selected_path.list_state.clone(), + cx.processor(|this, ix, _, cx| this.render_chunk(ix, cx)), + ) + .size_full(), + ) .size_full() .into_any_element() } else { diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index 480505338b..db75b544f6 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/crates/zed/src/zed/component_preview.rs @@ -107,6 +107,7 @@ struct ComponentPreview { active_thread: Option>, reset_key: usize, component_list: ListState, + entries: Vec, component_map: HashMap, components: Vec, cursor_index: usize, @@ -172,17 +173,6 @@ impl ComponentPreview { sorted_components.len(), gpui::ListAlignment::Top, px(1500.0), - { - let this = cx.entity().downgrade(); - move |ix, window: &mut Window, cx: &mut App| { - this.update(cx, |this, cx| { - let component = this.get_component(ix); - this.render_preview(&component, window, cx) - .into_any_element() - }) - .unwrap() - } - }, ); let mut component_preview = Self { @@ -190,6 +180,7 @@ impl ComponentPreview { active_thread: None, reset_key: 0, component_list, + entries: Vec::new(), component_map: component_registry.component_map(), components: sorted_components, cursor_index: selected_index, @@ -276,10 +267,6 @@ impl ComponentPreview { cx.notify(); } - fn get_component(&self, ix: usize) -> ComponentMetadata { - self.components[ix].clone() - } - fn filtered_components(&self) -> Vec { if self.filter_text.is_empty() { return self.components.clone(); @@ -420,7 +407,6 @@ impl ComponentPreview { fn update_component_list(&mut self, cx: &mut Context) { let entries = self.scope_ordered_entries(); let new_len = entries.len(); - let weak_entity = cx.entity().downgrade(); if new_len > 0 { self.nav_scroll_handle @@ -446,56 +432,9 @@ impl ComponentPreview { } } - self.component_list = ListState::new( - filtered_components.len(), - gpui::ListAlignment::Top, - px(1500.0), - { - let components = filtered_components.clone(); - let this = cx.entity().downgrade(); - move |ix, window: &mut Window, cx: &mut App| { - if ix >= components.len() { - return div().w_full().h_0().into_any_element(); - } + self.component_list = ListState::new(new_len, gpui::ListAlignment::Top, px(1500.0)); + self.entries = entries; - this.update(cx, |this, cx| { - let component = &components[ix]; - this.render_preview(component, window, cx) - .into_any_element() - }) - .unwrap() - } - }, - ); - - let new_list = ListState::new( - new_len, - gpui::ListAlignment::Top, - px(1500.0), - move |ix, window, cx| { - if ix >= entries.len() { - return div().w_full().h_0().into_any_element(); - } - - let entry = &entries[ix]; - - weak_entity - .update(cx, |this, cx| match entry { - PreviewEntry::Component(component, _) => this - .render_preview(component, window, cx) - .into_any_element(), - PreviewEntry::SectionHeader(shared_string) => this - .render_scope_header(ix, shared_string.clone(), window, cx) - .into_any_element(), - PreviewEntry::AllComponents => div().w_full().h_0().into_any_element(), - PreviewEntry::ActiveThread => div().w_full().h_0().into_any_element(), - PreviewEntry::Separator => div().w_full().h_0().into_any_element(), - }) - .unwrap() - }, - ); - - self.component_list = new_list; cx.emit(ItemEvent::UpdateTab); } @@ -672,10 +611,35 @@ impl ComponentPreview { .child(format!("No components matching '{}'.", self.filter_text)) .into_any_element() } else { - list(self.component_list.clone()) - .flex_grow() - .with_sizing_behavior(gpui::ListSizingBehavior::Auto) - .into_any_element() + list( + self.component_list.clone(), + cx.processor(|this, ix, window, cx| { + if ix >= this.entries.len() { + return div().w_full().h_0().into_any_element(); + } + + let entry = &this.entries[ix]; + + match entry { + PreviewEntry::Component(component, _) => this + .render_preview(component, window, cx) + .into_any_element(), + PreviewEntry::SectionHeader(shared_string) => this + .render_scope_header(ix, shared_string.clone(), window, cx) + .into_any_element(), + PreviewEntry::AllComponents => { + div().w_full().h_0().into_any_element() + } + PreviewEntry::ActiveThread => { + div().w_full().h_0().into_any_element() + } + PreviewEntry::Separator => div().w_full().h_0().into_any_element(), + } + }), + ) + .flex_grow() + .with_sizing_behavior(gpui::ListSizingBehavior::Auto) + .into_any_element() }, ) } From 74e17c2f64167f33b3caf7181c5a6c9efb7e4706 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Tue, 5 Aug 2025 20:11:16 -0500 Subject: [PATCH 105/693] Fix panic-json writing (#35691) We broke it in #35263 when we changed the open options to use `create_new` Release Notes: - N/A --- crates/zed/src/reliability.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index ed149a470a..df50ecc743 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -149,6 +149,7 @@ pub fn init_panic_hook( let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string(); let panic_file_path = paths::logs_dir().join(format!("zed-{timestamp}.panic")); let panic_file = fs::OpenOptions::new() + .write(true) .create_new(true) .open(&panic_file_path) .log_err(); From e8052d4a4edd948819005bbdc48382a8a63c74d4 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 5 Aug 2025 18:18:21 -0700 Subject: [PATCH 106/693] Remove payload_type (#35690) Release Notes: - N/A --- crates/rpc/src/peer.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index c1fd1df5ff..8ddebfb269 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -423,22 +423,19 @@ impl Peer { request: T, ) -> impl Future> { let request_start_time = Instant::now(); - let payload_type = T::NAME; let elapsed_time = move || request_start_time.elapsed().as_millis(); - tracing::info!(payload_type, "start forwarding request"); + tracing::info!("start forwarding request"); self.request_internal(Some(sender_id), receiver_id, request) .map_ok(|envelope| envelope.payload) .inspect_err(move |_| { tracing::error!( waiting_for_host_ms = elapsed_time(), - payload_type, "error forwarding request" ) }) .inspect_ok(move |_| { tracing::info!( waiting_for_host_ms = elapsed_time(), - payload_type, "finished forwarding request" ) }) From a884e861e98a7f9d05e344e1866d7cb144ce77dd Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Tue, 5 Aug 2025 20:20:42 -0500 Subject: [PATCH 107/693] Tag crash reports with panic message and release (#35692) This _should_ allow sentry to associate related panic events with the same issue, but it doesn't change the issue title. I'm still working on figuring out how to set those fields, but in the meantime this should at least associate zed versions with crashes Release Notes: - N/A --- crates/zed/src/reliability.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index df50ecc743..53539699cc 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -554,6 +554,10 @@ async fn upload_previous_panics( .log_err(); } + if MINIDUMP_ENDPOINT.is_none() { + return Ok(most_recent_panic); + } + // loop back over the directory again to upload any minidumps that are missing panics let mut children = smol::fs::read_dir(paths::logs_dir()).await?; while let Some(child) = children.next().await { @@ -598,11 +602,12 @@ async fn upload_minidump( ) .text("platform", "rust"); if let Some(panic) = panic { - form = form.text( - "release", - format!("{}-{}", panic.release_channel, panic.app_version), - ); - // TODO: tack on more fields + form = form + .text( + "sentry[release]", + format!("{}-{}", panic.release_channel, panic.app_version), + ) + .text("sentry[logentry][formatted]", panic.payload.clone()); } let mut response_text = String::new(); From 40129147c64fbf6f7f021ddbeed7f10a3aa5f91b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Aug 2025 20:40:33 -0700 Subject: [PATCH 108/693] Respect paths' content masks when copying them from MSAA texture to drawable (#35688) Fixes a regression introduced in https://github.com/zed-industries/zed/pull/34992 ### Background Paths are rendered first to an intermediate MSAA texture, and then copied to the final drawable. Because paths can have transparency, it's important that pixels are not copied repeatedly if paths have overlapping bounding boxes. When N paths have the same draw order, we infer that they must have disjoint bounding boxes, so that we can copy them each individually (as opposed to copying a single rect that contains them all). Previously, the bounding box that we were using to copy paths was not accounting for the path's content mask (but it is accounted for in the bounds tree that determines their draw order). This cause bugs like this, where certain path pixels spuriously had their opacity doubled: https://github.com/user-attachments/assets/d792e60c-790b-49ad-b435-6695daba430f This PR fixes that bug. * [x] mac * [x] linux * [x] windows Release Notes: - Fixed a bug where a selection's opacity was computed incorrectly when it overlapped with another editor's selections in a certain way. --- .../gpui/src/platform/blade/blade_renderer.rs | 8 ++++---- crates/gpui/src/platform/mac/metal_renderer.rs | 6 +++--- .../src/platform/windows/directx_renderer.rs | 8 ++++---- crates/gpui/src/scene.rs | 17 ++++++++++++++++- 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/crates/gpui/src/platform/blade/blade_renderer.rs b/crates/gpui/src/platform/blade/blade_renderer.rs index 2e18d2be22..46d3c16c72 100644 --- a/crates/gpui/src/platform/blade/blade_renderer.rs +++ b/crates/gpui/src/platform/blade/blade_renderer.rs @@ -606,7 +606,7 @@ impl BladeRenderer { xy_position: v.xy_position, st_position: v.st_position, color: path.color, - bounds: path.bounds.intersect(&path.content_mask.bounds), + bounds: path.clipped_bounds(), })); } let vertex_buf = unsafe { self.instance_belt.alloc_typed(&vertices, &self.gpu) }; @@ -735,13 +735,13 @@ impl BladeRenderer { paths .iter() .map(|path| PathSprite { - bounds: path.bounds, + bounds: path.clipped_bounds(), }) .collect() } else { - let mut bounds = first_path.bounds; + let mut bounds = first_path.clipped_bounds(); for path in paths.iter().skip(1) { - bounds = bounds.union(&path.bounds); + bounds = bounds.union(&path.clipped_bounds()); } vec![PathSprite { bounds }] }; diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index fb5cb852d6..629654014d 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -791,13 +791,13 @@ impl MetalRenderer { sprites = paths .iter() .map(|path| PathSprite { - bounds: path.bounds, + bounds: path.clipped_bounds(), }) .collect(); } else { - let mut bounds = first_path.bounds; + let mut bounds = first_path.clipped_bounds(); for path in paths.iter().skip(1) { - bounds = bounds.union(&path.bounds); + bounds = bounds.union(&path.clipped_bounds()); } sprites = vec![PathSprite { bounds }]; } diff --git a/crates/gpui/src/platform/windows/directx_renderer.rs b/crates/gpui/src/platform/windows/directx_renderer.rs index 72cc12a5b4..ac285b79ac 100644 --- a/crates/gpui/src/platform/windows/directx_renderer.rs +++ b/crates/gpui/src/platform/windows/directx_renderer.rs @@ -435,7 +435,7 @@ impl DirectXRenderer { xy_position: v.xy_position, st_position: v.st_position, color: path.color, - bounds: path.bounds.intersect(&path.content_mask.bounds), + bounds: path.clipped_bounds(), })); } @@ -487,13 +487,13 @@ impl DirectXRenderer { paths .iter() .map(|path| PathSprite { - bounds: path.bounds, + bounds: path.clipped_bounds(), }) .collect::>() } else { - let mut bounds = first_path.bounds; + let mut bounds = first_path.clipped_bounds(); for path in paths.iter().skip(1) { - bounds = bounds.union(&path.bounds); + bounds = bounds.union(&path.clipped_bounds()); } vec![PathSprite { bounds }] }; diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index ec8d720cdf..c527dfe750 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -8,7 +8,12 @@ use crate::{ AtlasTextureId, AtlasTile, Background, Bounds, ContentMask, Corners, Edges, Hsla, Pixels, Point, Radians, ScaledPixels, Size, bounds_tree::BoundsTree, point, }; -use std::{fmt::Debug, iter::Peekable, ops::Range, slice}; +use std::{ + fmt::Debug, + iter::Peekable, + ops::{Add, Range, Sub}, + slice, +}; #[allow(non_camel_case_types, unused)] pub(crate) type PathVertex_ScaledPixels = PathVertex; @@ -793,6 +798,16 @@ impl Path { } } +impl Path +where + T: Clone + Debug + Default + PartialEq + PartialOrd + Add + Sub, +{ + #[allow(unused)] + pub(crate) fn clipped_bounds(&self) -> Bounds { + self.bounds.intersect(&self.content_mask.bounds) + } +} + impl From> for Primitive { fn from(path: Path) -> Self { Primitive::Path(path) From c59c436a11d80a8c7ae571e0be075e3b5933419d Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 6 Aug 2025 10:32:25 +0200 Subject: [PATCH 109/693] Verify downloaded rust-analyzer and clang binaries by checking the artifact digest (#35642) Release Notes: - Added GitHub artifact digest verification for rust-analyzer and clangd binary downloads, skipping downloads if cached binary digest is up to date - Added verification that cached rust-analyzer and clangd binaries are executable, if not they are redownloaded --------- Co-authored-by: Kirill Bulatov --- Cargo.lock | 3 + crates/http_client/src/github.rs | 2 + crates/languages/Cargo.toml | 3 + crates/languages/src/c.rs | 88 +++++++---- crates/languages/src/github_download.rs | 190 ++++++++++++++++++++++++ crates/languages/src/json.rs | 1 + crates/languages/src/lib.rs | 1 + crates/languages/src/rust.rs | 118 ++++++++------- crates/languages/src/typescript.rs | 53 ++----- crates/util/src/archive.rs | 12 +- crates/util/src/fs.rs | 6 +- 11 files changed, 354 insertions(+), 123 deletions(-) create mode 100644 crates/languages/src/github_download.rs diff --git a/Cargo.lock b/Cargo.lock index 4803c0de8e..f4516c5d60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9208,6 +9208,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-compression", + "async-fs", "async-tar", "async-trait", "chrono", @@ -9239,9 +9240,11 @@ dependencies = [ "serde_json", "serde_json_lenient", "settings", + "sha2", "smol", "snippet_provider", "task", + "tempfile", "text", "theme", "toml 0.8.20", diff --git a/crates/http_client/src/github.rs b/crates/http_client/src/github.rs index a038915e2f..a19c13b0ff 100644 --- a/crates/http_client/src/github.rs +++ b/crates/http_client/src/github.rs @@ -8,6 +8,7 @@ use url::Url; pub struct GitHubLspBinaryVersion { pub name: String, pub url: String, + pub digest: Option, } #[derive(Deserialize, Debug)] @@ -24,6 +25,7 @@ pub struct GithubRelease { pub struct GithubReleaseAsset { pub name: String, pub browser_download_url: String, + pub digest: Option, } pub async fn latest_github_release( diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index 260126da63..8e25818070 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -36,6 +36,7 @@ load-grammars = [ [dependencies] anyhow.workspace = true async-compression.workspace = true +async-fs.workspace = true async-tar.workspace = true async-trait.workspace = true chrono.workspace = true @@ -62,6 +63,7 @@ regex.workspace = true rope.workspace = true rust-embed.workspace = true schemars.workspace = true +sha2.workspace = true serde.workspace = true serde_json.workspace = true serde_json_lenient.workspace = true @@ -69,6 +71,7 @@ settings.workspace = true smol.workspace = true snippet_provider.workspace = true task.workspace = true +tempfile.workspace = true toml.workspace = true tree-sitter = { workspace = true, optional = true } tree-sitter-bash = { workspace = true, optional = true } diff --git a/crates/languages/src/c.rs b/crates/languages/src/c.rs index c06c35ee69..a55d8ff998 100644 --- a/crates/languages/src/c.rs +++ b/crates/languages/src/c.rs @@ -2,14 +2,16 @@ use anyhow::{Context as _, Result, bail}; use async_trait::async_trait; use futures::StreamExt; use gpui::{App, AsyncApp}; -use http_client::github::{GitHubLspBinaryVersion, latest_github_release}; +use http_client::github::{AssetKind, GitHubLspBinaryVersion, latest_github_release}; pub use language::*; use lsp::{InitializeParams, LanguageServerBinary, LanguageServerName}; use project::lsp_store::clangd_ext; use serde_json::json; use smol::fs; use std::{any::Any, env::consts, path::PathBuf, sync::Arc}; -use util::{ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into}; +use util::{ResultExt, fs::remove_matching, maybe, merge_json_value_into}; + +use crate::github_download::{GithubBinaryMetadata, download_server_binary}; pub struct CLspAdapter; @@ -58,6 +60,7 @@ impl super::LspAdapter for CLspAdapter { let version = GitHubLspBinaryVersion { name: release.tag_name, url: asset.browser_download_url.clone(), + digest: asset.digest.clone(), }; Ok(Box::new(version) as Box<_>) } @@ -68,32 +71,67 @@ impl super::LspAdapter for CLspAdapter { container_dir: PathBuf, delegate: &dyn LspAdapterDelegate, ) -> Result { - let version = version.downcast::().unwrap(); - let version_dir = container_dir.join(format!("clangd_{}", version.name)); + let GitHubLspBinaryVersion { name, url, digest } = + &*version.downcast::().unwrap(); + let version_dir = container_dir.join(format!("clangd_{name}")); let binary_path = version_dir.join("bin/clangd"); - if fs::metadata(&binary_path).await.is_err() { - let mut response = delegate - .http_client() - .get(&version.url, Default::default(), true) - .await - .context("error downloading release")?; - anyhow::ensure!( - response.status().is_success(), - "download failed with status {}", - response.status().to_string() - ); - extract_zip(&container_dir, response.body_mut()) - .await - .with_context(|| format!("unzipping clangd archive to {container_dir:?}"))?; - remove_matching(&container_dir, |entry| entry != version_dir).await; - } - - Ok(LanguageServerBinary { - path: binary_path, + let binary = LanguageServerBinary { + path: binary_path.clone(), env: None, - arguments: Vec::new(), - }) + arguments: Default::default(), + }; + + let metadata_path = version_dir.join("metadata"); + let metadata = GithubBinaryMetadata::read_from_file(&metadata_path) + .await + .ok(); + if let Some(metadata) = metadata { + let validity_check = async || { + delegate + .try_exec(LanguageServerBinary { + path: binary_path.clone(), + arguments: vec!["--version".into()], + env: None, + }) + .await + .inspect_err(|err| { + log::warn!("Unable to run {binary_path:?} asset, redownloading: {err}",) + }) + }; + if let (Some(actual_digest), Some(expected_digest)) = (&metadata.digest, digest) { + if actual_digest == expected_digest { + if validity_check().await.is_ok() { + return Ok(binary); + } + } else { + log::info!( + "SHA-256 mismatch for {binary_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}" + ); + } + } else if validity_check().await.is_ok() { + return Ok(binary); + } + } + download_server_binary( + delegate, + url, + digest.as_deref(), + &container_dir, + AssetKind::Zip, + ) + .await?; + remove_matching(&container_dir, |entry| entry != version_dir).await; + GithubBinaryMetadata::write_to_file( + &GithubBinaryMetadata { + metadata_version: 1, + digest: digest.clone(), + }, + &metadata_path, + ) + .await?; + + Ok(binary) } async fn cached_server_binary( diff --git a/crates/languages/src/github_download.rs b/crates/languages/src/github_download.rs new file mode 100644 index 0000000000..a3cd0a964b --- /dev/null +++ b/crates/languages/src/github_download.rs @@ -0,0 +1,190 @@ +use std::{path::Path, pin::Pin, task::Poll}; + +use anyhow::{Context, Result}; +use async_compression::futures::bufread::GzipDecoder; +use futures::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, io::BufReader}; +use http_client::github::AssetKind; +use language::LspAdapterDelegate; +use sha2::{Digest, Sha256}; + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub(crate) struct GithubBinaryMetadata { + pub(crate) metadata_version: u64, + pub(crate) digest: Option, +} + +impl GithubBinaryMetadata { + pub(crate) async fn read_from_file(metadata_path: &Path) -> Result { + let metadata_content = async_fs::read_to_string(metadata_path) + .await + .with_context(|| format!("reading metadata file at {metadata_path:?}"))?; + let metadata: GithubBinaryMetadata = serde_json::from_str(&metadata_content) + .with_context(|| format!("parsing metadata file at {metadata_path:?}"))?; + Ok(metadata) + } + + pub(crate) async fn write_to_file(&self, metadata_path: &Path) -> Result<()> { + let metadata_content = serde_json::to_string(self) + .with_context(|| format!("serializing metadata for {metadata_path:?}"))?; + async_fs::write(metadata_path, metadata_content.as_bytes()) + .await + .with_context(|| format!("writing metadata file at {metadata_path:?}"))?; + Ok(()) + } +} + +pub(crate) async fn download_server_binary( + delegate: &dyn LspAdapterDelegate, + url: &str, + digest: Option<&str>, + destination_path: &Path, + asset_kind: AssetKind, +) -> Result<(), anyhow::Error> { + log::info!("downloading github artifact from {url}"); + let mut response = delegate + .http_client() + .get(url, Default::default(), true) + .await + .with_context(|| format!("downloading release from {url}"))?; + let body = response.body_mut(); + match digest { + Some(expected_sha_256) => { + let temp_asset_file = tempfile::NamedTempFile::new() + .with_context(|| format!("creating a temporary file for {url}"))?; + let (temp_asset_file, _temp_guard) = temp_asset_file.into_parts(); + let mut writer = HashingWriter { + writer: async_fs::File::from(temp_asset_file), + hasher: Sha256::new(), + }; + futures::io::copy(&mut BufReader::new(body), &mut writer) + .await + .with_context(|| { + format!("saving archive contents into the temporary file for {url}",) + })?; + let asset_sha_256 = format!("{:x}", writer.hasher.finalize()); + anyhow::ensure!( + asset_sha_256 == expected_sha_256, + "{url} asset got SHA-256 mismatch. Expected: {expected_sha_256}, Got: {asset_sha_256}", + ); + writer + .writer + .seek(std::io::SeekFrom::Start(0)) + .await + .with_context(|| format!("seeking temporary file {destination_path:?}",))?; + stream_file_archive(&mut writer.writer, url, destination_path, asset_kind) + .await + .with_context(|| { + format!("extracting downloaded asset for {url} into {destination_path:?}",) + })?; + } + None => stream_response_archive(body, url, destination_path, asset_kind) + .await + .with_context(|| { + format!("extracting response for asset {url} into {destination_path:?}",) + })?, + } + Ok(()) +} + +async fn stream_response_archive( + response: impl AsyncRead + Unpin, + url: &str, + destination_path: &Path, + asset_kind: AssetKind, +) -> Result<()> { + match asset_kind { + AssetKind::TarGz => extract_tar_gz(destination_path, url, response).await?, + AssetKind::Gz => extract_gz(destination_path, url, response).await?, + AssetKind::Zip => { + util::archive::extract_zip(&destination_path, response).await?; + } + }; + Ok(()) +} + +async fn stream_file_archive( + file_archive: impl AsyncRead + AsyncSeek + Unpin, + url: &str, + destination_path: &Path, + asset_kind: AssetKind, +) -> Result<()> { + match asset_kind { + AssetKind::TarGz => extract_tar_gz(destination_path, url, file_archive).await?, + AssetKind::Gz => extract_gz(destination_path, url, file_archive).await?, + #[cfg(not(windows))] + AssetKind::Zip => { + util::archive::extract_seekable_zip(&destination_path, file_archive).await?; + } + #[cfg(windows)] + AssetKind::Zip => { + util::archive::extract_zip(&destination_path, file_archive).await?; + } + }; + Ok(()) +} + +async fn extract_tar_gz( + destination_path: &Path, + url: &str, + from: impl AsyncRead + Unpin, +) -> Result<(), anyhow::Error> { + let decompressed_bytes = GzipDecoder::new(BufReader::new(from)); + let archive = async_tar::Archive::new(decompressed_bytes); + archive + .unpack(&destination_path) + .await + .with_context(|| format!("extracting {url} to {destination_path:?}"))?; + Ok(()) +} + +async fn extract_gz( + destination_path: &Path, + url: &str, + from: impl AsyncRead + Unpin, +) -> Result<(), anyhow::Error> { + let mut decompressed_bytes = GzipDecoder::new(BufReader::new(from)); + let mut file = smol::fs::File::create(&destination_path) + .await + .with_context(|| { + format!("creating a file {destination_path:?} for a download from {url}") + })?; + futures::io::copy(&mut decompressed_bytes, &mut file) + .await + .with_context(|| format!("extracting {url} to {destination_path:?}"))?; + Ok(()) +} + +struct HashingWriter { + writer: W, + hasher: Sha256, +} + +impl AsyncWrite for HashingWriter { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> Poll> { + match Pin::new(&mut self.writer).poll_write(cx, buf) { + Poll::Ready(Ok(n)) => { + self.hasher.update(&buf[..n]); + Poll::Ready(Ok(n)) + } + other => other, + } + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + Pin::new(&mut self.writer).poll_flush(cx) + } + + fn poll_close( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + Pin::new(&mut self.writer).poll_close(cx) + } +} diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 028bf9fb68..ca82bb2431 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -517,6 +517,7 @@ impl LspAdapter for NodeVersionAdapter { Ok(Box::new(GitHubLspBinaryVersion { name: release.tag_name, url: asset.browser_download_url.clone(), + digest: asset.digest.clone(), })) } diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 001fd15200..195ba79e1d 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -17,6 +17,7 @@ use crate::{json::JsonTaskProvider, python::BasedPyrightLspAdapter}; mod bash; mod c; mod css; +mod github_download; mod go; mod json; mod package_json; diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 3f83c9c000..084331b2cf 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -1,8 +1,7 @@ use anyhow::{Context as _, Result}; -use async_compression::futures::bufread::GzipDecoder; use async_trait::async_trait; use collections::HashMap; -use futures::{StreamExt, io::BufReader}; +use futures::StreamExt; use gpui::{App, AppContext, AsyncApp, SharedString, Task}; use http_client::github::AssetKind; use http_client::github::{GitHubLspBinaryVersion, latest_github_release}; @@ -23,14 +22,11 @@ use std::{ sync::{Arc, LazyLock}, }; use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName}; -use util::archive::extract_zip; +use util::fs::make_file_executable; use util::merge_json_value_into; -use util::{ - ResultExt, - fs::{make_file_executable, remove_matching}, - maybe, -}; +use util::{ResultExt, maybe}; +use crate::github_download::{GithubBinaryMetadata, download_server_binary}; use crate::language_settings::language_settings; pub struct RustLspAdapter; @@ -163,7 +159,6 @@ impl LspAdapter for RustLspAdapter { ) .await?; let asset_name = Self::build_asset_name(); - let asset = release .assets .iter() @@ -172,6 +167,7 @@ impl LspAdapter for RustLspAdapter { Ok(Box::new(GitHubLspBinaryVersion { name: release.tag_name, url: asset.browser_download_url.clone(), + digest: asset.digest.clone(), })) } @@ -181,58 +177,76 @@ impl LspAdapter for RustLspAdapter { container_dir: PathBuf, delegate: &dyn LspAdapterDelegate, ) -> Result { - let version = version.downcast::().unwrap(); - let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name)); + let GitHubLspBinaryVersion { name, url, digest } = + &*version.downcast::().unwrap(); + let expected_digest = digest + .as_ref() + .and_then(|digest| digest.strip_prefix("sha256:")); + let destination_path = container_dir.join(format!("rust-analyzer-{name}")); let server_path = match Self::GITHUB_ASSET_KIND { AssetKind::TarGz | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place. AssetKind::Zip => destination_path.clone().join("rust-analyzer.exe"), // zip contains a .exe }; - if fs::metadata(&server_path).await.is_err() { - remove_matching(&container_dir, |entry| entry != destination_path).await; + let binary = LanguageServerBinary { + path: server_path.clone(), + env: None, + arguments: Default::default(), + }; - let mut response = delegate - .http_client() - .get(&version.url, Default::default(), true) - .await - .with_context(|| format!("downloading release from {}", version.url))?; - match Self::GITHUB_ASSET_KIND { - AssetKind::TarGz => { - let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); - let archive = async_tar::Archive::new(decompressed_bytes); - archive.unpack(&destination_path).await.with_context(|| { - format!("extracting {} to {:?}", version.url, destination_path) - })?; - } - AssetKind::Gz => { - let mut decompressed_bytes = - GzipDecoder::new(BufReader::new(response.body_mut())); - let mut file = - fs::File::create(&destination_path).await.with_context(|| { - format!( - "creating a file {:?} for a download from {}", - destination_path, version.url, - ) - })?; - futures::io::copy(&mut decompressed_bytes, &mut file) - .await - .with_context(|| { - format!("extracting {} to {:?}", version.url, destination_path) - })?; - } - AssetKind::Zip => { - extract_zip(&destination_path, response.body_mut()) - .await - .with_context(|| { - format!("unzipping {} to {:?}", version.url, destination_path) - })?; - } + let metadata_path = destination_path.with_extension("metadata"); + let metadata = GithubBinaryMetadata::read_from_file(&metadata_path) + .await + .ok(); + if let Some(metadata) = metadata { + let validity_check = async || { + delegate + .try_exec(LanguageServerBinary { + path: server_path.clone(), + arguments: vec!["--version".into()], + env: None, + }) + .await + .inspect_err(|err| { + log::warn!("Unable to run {server_path:?} asset, redownloading: {err}",) + }) }; - - // todo("windows") - make_file_executable(&server_path).await?; + if let (Some(actual_digest), Some(expected_digest)) = + (&metadata.digest, expected_digest) + { + if actual_digest == expected_digest { + if validity_check().await.is_ok() { + return Ok(binary); + } + } else { + log::info!( + "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}" + ); + } + } else if validity_check().await.is_ok() { + return Ok(binary); + } } + _ = fs::remove_dir_all(&destination_path).await; + download_server_binary( + delegate, + url, + expected_digest, + &destination_path, + Self::GITHUB_ASSET_KIND, + ) + .await?; + make_file_executable(&server_path).await?; + GithubBinaryMetadata::write_to_file( + &GithubBinaryMetadata { + metadata_version: 1, + digest: expected_digest.map(ToString::to_string), + }, + &metadata_path, + ) + .await?; + Ok(LanguageServerBinary { path: server_path, env: None, diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index 9dc3ee303d..f976b62614 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -1,6 +1,4 @@ use anyhow::{Context as _, Result}; -use async_compression::futures::bufread::GzipDecoder; -use async_tar::Archive; use async_trait::async_trait; use chrono::{DateTime, Local}; use collections::HashMap; @@ -15,7 +13,7 @@ use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName}; use node_runtime::NodeRuntime; use project::{Fs, lsp_store::language_server_settings}; use serde_json::{Value, json}; -use smol::{fs, io::BufReader, lock::RwLock, stream::StreamExt}; +use smol::{fs, lock::RwLock, stream::StreamExt}; use std::{ any::Any, borrow::Cow, @@ -24,11 +22,10 @@ use std::{ sync::Arc, }; use task::{TaskTemplate, TaskTemplates, VariableName}; -use util::archive::extract_zip; use util::merge_json_value_into; use util::{ResultExt, fs::remove_matching, maybe}; -use crate::{PackageJson, PackageJsonData}; +use crate::{PackageJson, PackageJsonData, github_download::download_server_binary}; #[derive(Debug)] pub(crate) struct TypeScriptContextProvider { @@ -897,6 +894,7 @@ impl LspAdapter for EsLintLspAdapter { Ok(Box::new(GitHubLspBinaryVersion { name: Self::CURRENT_VERSION.into(), + digest: None, url, })) } @@ -914,43 +912,14 @@ impl LspAdapter for EsLintLspAdapter { if fs::metadata(&server_path).await.is_err() { remove_matching(&container_dir, |entry| entry != destination_path).await; - let mut response = delegate - .http_client() - .get(&version.url, Default::default(), true) - .await - .context("downloading release")?; - match Self::GITHUB_ASSET_KIND { - AssetKind::TarGz => { - let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); - let archive = Archive::new(decompressed_bytes); - archive.unpack(&destination_path).await.with_context(|| { - format!("extracting {} to {:?}", version.url, destination_path) - })?; - } - AssetKind::Gz => { - let mut decompressed_bytes = - GzipDecoder::new(BufReader::new(response.body_mut())); - let mut file = - fs::File::create(&destination_path).await.with_context(|| { - format!( - "creating a file {:?} for a download from {}", - destination_path, version.url, - ) - })?; - futures::io::copy(&mut decompressed_bytes, &mut file) - .await - .with_context(|| { - format!("extracting {} to {:?}", version.url, destination_path) - })?; - } - AssetKind::Zip => { - extract_zip(&destination_path, response.body_mut()) - .await - .with_context(|| { - format!("unzipping {} to {:?}", version.url, destination_path) - })?; - } - } + download_server_binary( + delegate, + &version.url, + None, + &destination_path, + Self::GITHUB_ASSET_KIND, + ) + .await?; let mut dir = fs::read_dir(&destination_path).await?; let first = dir.next().await.context("missing first file")??; diff --git a/crates/util/src/archive.rs b/crates/util/src/archive.rs index d10b996716..3e4d281c29 100644 --- a/crates/util/src/archive.rs +++ b/crates/util/src/archive.rs @@ -2,6 +2,8 @@ use std::path::Path; use anyhow::{Context as _, Result}; use async_zip::base::read; +#[cfg(not(windows))] +use futures::AsyncSeek; use futures::{AsyncRead, io::BufReader}; #[cfg(windows)] @@ -62,7 +64,15 @@ pub async fn extract_zip(destination: &Path, reader: R) -> futures::io::copy(&mut BufReader::new(reader), &mut file) .await .context("saving archive contents into the temporary file")?; - let mut reader = read::seek::ZipFileReader::new(BufReader::new(file)) + extract_seekable_zip(destination, file).await +} + +#[cfg(not(windows))] +pub async fn extract_seekable_zip( + destination: &Path, + reader: R, +) -> Result<()> { + let mut reader = read::seek::ZipFileReader::new(BufReader::new(reader)) .await .context("reading the zip archive")?; let destination = &destination diff --git a/crates/util/src/fs.rs b/crates/util/src/fs.rs index 2738b6e213..3e96594f85 100644 --- a/crates/util/src/fs.rs +++ b/crates/util/src/fs.rs @@ -95,9 +95,9 @@ pub async fn move_folder_files_to_folder>( #[cfg(unix)] /// Set the permissions for the given path so that the file becomes executable. /// This is a noop for non-unix platforms. -pub async fn make_file_executable(path: &PathBuf) -> std::io::Result<()> { +pub async fn make_file_executable(path: &Path) -> std::io::Result<()> { fs::set_permissions( - &path, + path, ::from_mode(0o755), ) .await @@ -107,6 +107,6 @@ pub async fn make_file_executable(path: &PathBuf) -> std::io::Result<()> { #[allow(clippy::unused_async)] /// Set the permissions for the given path so that the file becomes executable. /// This is a noop for non-unix platforms. -pub async fn make_file_executable(_path: &PathBuf) -> std::io::Result<()> { +pub async fn make_file_executable(_path: &Path) -> std::io::Result<()> { Ok(()) } From 69794db3316921e778f8d90cd17d403e1e5f000f Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 6 Aug 2025 10:53:20 +0200 Subject: [PATCH 110/693] Prevent out of bounds access in `recursive_score_match` (#35630) Closes https://github.com/zed-industries/zed/issues/33668 The recursive case increments both indices by 1, but only one of the two had a base case check in the function prologue so the other could spill over into a different matrix row or out of bounds entirely. Lacking a test as I haven't figured out a test case yet. Release Notes: - Fixed out of bounds panic in fuzzy matching --- crates/fuzzy/src/matcher.rs | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/crates/fuzzy/src/matcher.rs b/crates/fuzzy/src/matcher.rs index aff6390534..e649d47dd6 100644 --- a/crates/fuzzy/src/matcher.rs +++ b/crates/fuzzy/src/matcher.rs @@ -208,8 +208,15 @@ impl<'a> Matcher<'a> { return 1.0; } - let path_len = prefix.len() + path.len(); + let limit = self.last_positions[query_idx]; + let max_valid_index = (prefix.len() + path_lowercased.len()).saturating_sub(1); + let safe_limit = limit.min(max_valid_index); + if path_idx > safe_limit { + return 0.0; + } + + let path_len = prefix.len() + path.len(); if let Some(memoized) = self.score_matrix[query_idx * path_len + path_idx] { return memoized; } @@ -218,16 +225,13 @@ impl<'a> Matcher<'a> { let mut best_position = 0; let query_char = self.lowercase_query[query_idx]; - let limit = self.last_positions[query_idx]; - - let max_valid_index = (prefix.len() + path_lowercased.len()).saturating_sub(1); - let safe_limit = limit.min(max_valid_index); let mut last_slash = 0; + for j in path_idx..=safe_limit { let extra_lowercase_chars_count = extra_lowercase_chars .iter() - .take_while(|(i, _)| i < &&j) + .take_while(|&(&i, _)| i < j) .map(|(_, increment)| increment) .sum::(); let j_regular = j - extra_lowercase_chars_count; @@ -236,10 +240,9 @@ impl<'a> Matcher<'a> { lowercase_prefix[j] } else { let path_index = j - prefix.len(); - if path_index < path_lowercased.len() { - path_lowercased[path_index] - } else { - continue; + match path_lowercased.get(path_index) { + Some(&char) => char, + None => continue, } }; let is_path_sep = path_char == MAIN_SEPARATOR; @@ -255,18 +258,16 @@ impl<'a> Matcher<'a> { #[cfg(target_os = "windows")] let need_to_score = query_char == path_char || (is_path_sep && query_char == '_'); if need_to_score { - let curr = if j_regular < prefix.len() { - prefix[j_regular] - } else { - path[j_regular - prefix.len()] + let curr = match prefix.get(j_regular) { + Some(&curr) => curr, + None => path[j_regular - prefix.len()], }; let mut char_score = 1.0; if j > path_idx { - let last = if j_regular - 1 < prefix.len() { - prefix[j_regular - 1] - } else { - path[j_regular - 1 - prefix.len()] + let last = match prefix.get(j_regular - 1) { + Some(&last) => last, + None => path[j_regular - 1 - prefix.len()], }; if last == MAIN_SEPARATOR { From eb4b73b88e9e25a37f7dd4b5cf9e773e5ef63eeb Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Wed, 6 Aug 2025 11:01:06 +0200 Subject: [PATCH 111/693] ACP champagne (#35609) cherry pick changes from #35510 onto latest main Release Notes: - N/A --------- Co-authored-by: Nathan Sobo Co-authored-by: Antonio Scandurra Co-authored-by: Lukas Wirth --- Cargo.lock | 39 + Cargo.toml | 2 + crates/acp_thread/Cargo.toml | 1 + crates/acp_thread/src/acp_thread.rs | 33 +- crates/acp_thread/src/connection.rs | 61 +- crates/agent/src/thread.rs | 12 +- crates/agent2/Cargo.toml | 53 ++ crates/agent2/LICENSE-GPL | 1 + crates/agent2/src/agent.rs | 341 ++++++++ crates/agent2/src/agent2.rs | 13 + crates/agent2/src/native_agent_server.rs | 58 ++ crates/agent2/src/prompts.rs | 35 + crates/agent2/src/templates.rs | 57 ++ crates/agent2/src/templates/base.hbs | 56 ++ crates/agent2/src/templates/glob.hbs | 8 + crates/agent2/src/tests/mod.rs | 513 ++++++++++++ crates/agent2/src/tests/test_tools.rs | 106 +++ crates/agent2/src/thread.rs | 754 ++++++++++++++++++ crates/agent2/src/tools.rs | 1 + crates/agent2/src/tools/glob.rs | 76 ++ crates/agent_servers/src/acp/v0.rs | 10 +- crates/agent_servers/src/acp/v1.rs | 10 +- crates/agent_servers/src/claude.rs | 36 +- crates/agent_ui/Cargo.toml | 1 + crates/agent_ui/src/acp/thread_view.rs | 16 +- crates/agent_ui/src/agent_panel.rs | 41 + crates/agent_ui/src/agent_ui.rs | 2 + .../src/assistant_context_tests.rs | 8 +- crates/assistant_tools/src/edit_agent.rs | 32 +- crates/assistant_tools/src/edit_file_tool.rs | 12 +- crates/language_model/src/fake_provider.rs | 40 +- 31 files changed, 2361 insertions(+), 67 deletions(-) create mode 100644 crates/agent2/Cargo.toml create mode 120000 crates/agent2/LICENSE-GPL create mode 100644 crates/agent2/src/agent.rs create mode 100644 crates/agent2/src/agent2.rs create mode 100644 crates/agent2/src/native_agent_server.rs create mode 100644 crates/agent2/src/prompts.rs create mode 100644 crates/agent2/src/templates.rs create mode 100644 crates/agent2/src/templates/base.hbs create mode 100644 crates/agent2/src/templates/glob.hbs create mode 100644 crates/agent2/src/tests/mod.rs create mode 100644 crates/agent2/src/tests/test_tools.rs create mode 100644 crates/agent2/src/thread.rs create mode 100644 crates/agent2/src/tools.rs create mode 100644 crates/agent2/src/tools/glob.rs diff --git a/Cargo.lock b/Cargo.lock index f4516c5d60..76f45bd28e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,7 @@ dependencies = [ "indoc", "itertools 0.14.0", "language", + "language_model", "markdown", "parking_lot", "project", @@ -150,6 +151,43 @@ dependencies = [ "serde_json", ] +[[package]] +name = "agent2" +version = "0.1.0" +dependencies = [ + "acp_thread", + "agent-client-protocol", + "agent_servers", + "anyhow", + "client", + "cloud_llm_client", + "collections", + "ctor", + "env_logger 0.11.8", + "fs", + "futures 0.3.31", + "gpui", + "gpui_tokio", + "handlebars 4.5.0", + "indoc", + "language_model", + "language_models", + "log", + "project", + "reqwest_client", + "rust-embed", + "schemars", + "serde", + "serde_json", + "settings", + "smol", + "ui", + "util", + "uuid", + "workspace-hack", + "worktree", +] + [[package]] name = "agent_servers" version = "0.1.0" @@ -214,6 +252,7 @@ dependencies = [ "acp_thread", "agent", "agent-client-protocol", + "agent2", "agent_servers", "agent_settings", "ai_onboarding", diff --git a/Cargo.toml b/Cargo.toml index 05ceb3bd14..7b82fd1910 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/acp_thread", "crates/activity_indicator", "crates/agent", + "crates/agent2", "crates/agent_servers", "crates/agent_settings", "crates/agent_ui", @@ -229,6 +230,7 @@ edition = "2024" acp_thread = { path = "crates/acp_thread" } agent = { path = "crates/agent" } +agent2 = { path = "crates/agent2" } activity_indicator = { path = "crates/activity_indicator" } agent_ui = { path = "crates/agent_ui" } agent_settings = { path = "crates/agent_settings" } diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index 225597415c..1831c7e473 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -25,6 +25,7 @@ futures.workspace = true gpui.workspace = true itertools.workspace = true language.workspace = true +language_model.workspace = true markdown.workspace = true project.workspace = true serde.workspace = true diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 892fd16655..70d9abd731 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -656,6 +656,10 @@ impl AcpThread { &self.entries } + pub fn session_id(&self) -> &acp::SessionId { + &self.session_id + } + pub fn status(&self) -> ThreadStatus { if self.send_task.is_some() { if self.waiting_for_tool_confirmation() { @@ -1377,6 +1381,9 @@ mod tests { cx, ) .unwrap(); + })?; + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, }) } .boxed_local() @@ -1449,7 +1456,9 @@ mod tests { .unwrap() .await .unwrap(); - Ok(()) + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) } .boxed_local() }, @@ -1522,7 +1531,9 @@ mod tests { }) .unwrap() .unwrap(); - Ok(()) + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) } .boxed_local() } @@ -1632,7 +1643,9 @@ mod tests { }) .unwrap() .unwrap(); - Ok(()) + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) } .boxed_local() } @@ -1686,7 +1699,7 @@ mod tests { acp::PromptRequest, WeakEntity, AsyncApp, - ) -> LocalBoxFuture<'static, Result<()>> + ) -> LocalBoxFuture<'static, Result> + 'static, >, >, @@ -1713,7 +1726,7 @@ mod tests { acp::PromptRequest, WeakEntity, AsyncApp, - ) -> LocalBoxFuture<'static, Result<()>> + ) -> LocalBoxFuture<'static, Result> + 'static, ) -> Self { self.on_user_message.replace(Rc::new(handler)); @@ -1755,7 +1768,11 @@ mod tests { } } - fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task> { + fn prompt( + &self, + params: acp::PromptRequest, + cx: &mut App, + ) -> Task> { let sessions = self.sessions.lock(); let thread = sessions.get(¶ms.session_id).unwrap(); if let Some(handler) = &self.on_user_message { @@ -1763,7 +1780,9 @@ mod tests { let thread = thread.clone(); cx.spawn(async move |cx| handler(params, thread, cx.clone()).await) } else { - Task::ready(Ok(())) + Task::ready(Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + })) } } diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 929500a67b..cf06563bee 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -1,13 +1,61 @@ -use std::{error::Error, fmt, path::Path, rc::Rc}; +use std::{error::Error, fmt, path::Path, rc::Rc, sync::Arc}; use agent_client_protocol::{self as acp}; use anyhow::Result; use gpui::{AsyncApp, Entity, Task}; +use language_model::LanguageModel; use project::Project; use ui::App; use crate::AcpThread; +/// Trait for agents that support listing, selecting, and querying language models. +/// +/// This is an optional capability; agents indicate support via [AgentConnection::model_selector]. +pub trait ModelSelector: 'static { + /// Lists all available language models for this agent. + /// + /// # Parameters + /// - `cx`: The GPUI app context for async operations and global access. + /// + /// # Returns + /// A task resolving to the list of models or an error (e.g., if no models are configured). + fn list_models(&self, cx: &mut AsyncApp) -> Task>>>; + + /// Selects a model for a specific session (thread). + /// + /// This sets the default model for future interactions in the session. + /// If the session doesn't exist or the model is invalid, it returns an error. + /// + /// # Parameters + /// - `session_id`: The ID of the session (thread) to apply the model to. + /// - `model`: The model to select (should be one from [list_models]). + /// - `cx`: The GPUI app context. + /// + /// # Returns + /// A task resolving to `Ok(())` on success or an error. + fn select_model( + &self, + session_id: acp::SessionId, + model: Arc, + cx: &mut AsyncApp, + ) -> Task>; + + /// Retrieves the currently selected model for a specific session (thread). + /// + /// # Parameters + /// - `session_id`: The ID of the session (thread) to query. + /// - `cx`: The GPUI app context. + /// + /// # Returns + /// A task resolving to the selected model (always set) or an error (e.g., session not found). + fn selected_model( + &self, + session_id: &acp::SessionId, + cx: &mut AsyncApp, + ) -> Task>>; +} + pub trait AgentConnection { fn new_thread( self: Rc, @@ -20,9 +68,18 @@ pub trait AgentConnection { fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task>; - fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task>; + fn prompt(&self, params: acp::PromptRequest, cx: &mut App) + -> Task>; fn cancel(&self, session_id: &acp::SessionId, cx: &mut App); + + /// Returns this agent as an [Rc] if the model selection capability is supported. + /// + /// If the agent does not support model selection, returns [None]. + /// This allows sharing the selector in UI components. + fn model_selector(&self) -> Option> { + None // Default impl for agents that don't support it + } } #[derive(Debug)] diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 2bb7f358eb..048aa4245d 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -4045,8 +4045,8 @@ fn main() {{ }); cx.run_until_parked(); - fake_model.stream_last_completion_response("Brief"); - fake_model.stream_last_completion_response(" Introduction"); + fake_model.send_last_completion_stream_text_chunk("Brief"); + fake_model.send_last_completion_stream_text_chunk(" Introduction"); fake_model.end_last_completion_stream(); cx.run_until_parked(); @@ -4139,7 +4139,7 @@ fn main() {{ }); cx.run_until_parked(); - fake_model.stream_last_completion_response("A successful summary"); + fake_model.send_last_completion_stream_text_chunk("A successful summary"); fake_model.end_last_completion_stream(); cx.run_until_parked(); @@ -4772,7 +4772,7 @@ fn main() {{ !pending.is_empty(), "Should have a pending completion after retry" ); - fake_model.stream_completion_response(&pending[0], "Success!"); + fake_model.send_completion_stream_text_chunk(&pending[0], "Success!"); fake_model.end_completion_stream(&pending[0]); cx.run_until_parked(); @@ -4940,7 +4940,7 @@ fn main() {{ // Check for pending completions and complete them if let Some(pending) = inner_fake.pending_completions().first() { - inner_fake.stream_completion_response(pending, "Success!"); + inner_fake.send_completion_stream_text_chunk(pending, "Success!"); inner_fake.end_completion_stream(pending); } cx.run_until_parked(); @@ -5425,7 +5425,7 @@ fn main() {{ fn simulate_successful_response(fake_model: &FakeLanguageModel, cx: &mut TestAppContext) { cx.run_until_parked(); - fake_model.stream_last_completion_response("Assistant response"); + fake_model.send_last_completion_stream_text_chunk("Assistant response"); fake_model.end_last_completion_stream(); cx.run_until_parked(); } diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml new file mode 100644 index 0000000000..70779bba74 --- /dev/null +++ b/crates/agent2/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "agent2" +version = "0.1.0" +edition = "2021" +license = "GPL-3.0-or-later" +publish = false + +[lib] +path = "src/agent2.rs" + +[lints] +workspace = true + +[dependencies] +acp_thread.workspace = true +agent-client-protocol.workspace = true +agent_servers.workspace = true +anyhow.workspace = true +cloud_llm_client.workspace = true +collections.workspace = true +fs.workspace = true +futures.workspace = true +gpui.workspace = true +handlebars = { workspace = true, features = ["rust-embed"] } +indoc.workspace = true +language_model.workspace = true +language_models.workspace = true +log.workspace = true +project.workspace = true +rust-embed.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +settings.workspace = true +smol.workspace = true +ui.workspace = true +util.workspace = true +uuid.workspace = true +worktree.workspace = true +workspace-hack.workspace = true + +[dev-dependencies] +ctor.workspace = true +client = { workspace = true, "features" = ["test-support"] } +env_logger.workspace = true +fs = { workspace = true, "features" = ["test-support"] } +gpui = { workspace = true, "features" = ["test-support"] } +gpui_tokio.workspace = true +language_model = { workspace = true, "features" = ["test-support"] } +project = { workspace = true, "features" = ["test-support"] } +reqwest_client.workspace = true +settings = { workspace = true, "features" = ["test-support"] } +worktree = { workspace = true, "features" = ["test-support"] } diff --git a/crates/agent2/LICENSE-GPL b/crates/agent2/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/agent2/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs new file mode 100644 index 0000000000..bd4f82200b --- /dev/null +++ b/crates/agent2/src/agent.rs @@ -0,0 +1,341 @@ +use acp_thread::ModelSelector; +use agent_client_protocol as acp; +use anyhow::{anyhow, Result}; +use futures::StreamExt; +use gpui::{App, AppContext, AsyncApp, Entity, Task}; +use language_model::{LanguageModel, LanguageModelRegistry}; +use project::Project; +use std::collections::HashMap; +use std::path::Path; +use std::rc::Rc; +use std::sync::Arc; + +use crate::{templates::Templates, AgentResponseEvent, Thread}; + +/// Holds both the internal Thread and the AcpThread for a session +struct Session { + /// The internal thread that processes messages + thread: Entity, + /// The ACP thread that handles protocol communication + acp_thread: Entity, +} + +pub struct NativeAgent { + /// Session ID -> Session mapping + sessions: HashMap, + /// Shared templates for all threads + templates: Arc, +} + +impl NativeAgent { + pub fn new(templates: Arc) -> Self { + log::info!("Creating new NativeAgent"); + Self { + sessions: HashMap::new(), + templates, + } + } +} + +/// Wrapper struct that implements the AgentConnection trait +#[derive(Clone)] +pub struct NativeAgentConnection(pub Entity); + +impl ModelSelector for NativeAgentConnection { + fn list_models(&self, cx: &mut AsyncApp) -> Task>>> { + log::debug!("NativeAgentConnection::list_models called"); + cx.spawn(async move |cx| { + cx.update(|cx| { + let registry = LanguageModelRegistry::read_global(cx); + let models = registry.available_models(cx).collect::>(); + log::info!("Found {} available models", models.len()); + if models.is_empty() { + Err(anyhow::anyhow!("No models available")) + } else { + Ok(models) + } + })? + }) + } + + fn select_model( + &self, + session_id: acp::SessionId, + model: Arc, + cx: &mut AsyncApp, + ) -> Task> { + log::info!( + "Setting model for session {}: {:?}", + session_id, + model.name() + ); + let agent = self.0.clone(); + + cx.spawn(async move |cx| { + agent.update(cx, |agent, cx| { + if let Some(session) = agent.sessions.get(&session_id) { + session.thread.update(cx, |thread, _cx| { + thread.selected_model = model; + }); + Ok(()) + } else { + Err(anyhow!("Session not found")) + } + })? + }) + } + + fn selected_model( + &self, + session_id: &acp::SessionId, + cx: &mut AsyncApp, + ) -> Task>> { + let agent = self.0.clone(); + let session_id = session_id.clone(); + cx.spawn(async move |cx| { + let thread = agent + .read_with(cx, |agent, _| { + agent + .sessions + .get(&session_id) + .map(|session| session.thread.clone()) + })? + .ok_or_else(|| anyhow::anyhow!("Session not found"))?; + let selected = thread.read_with(cx, |thread, _| thread.selected_model.clone())?; + Ok(selected) + }) + } +} + +impl acp_thread::AgentConnection for NativeAgentConnection { + fn new_thread( + self: Rc, + project: Entity, + cwd: &Path, + cx: &mut AsyncApp, + ) -> Task>> { + let agent = self.0.clone(); + log::info!("Creating new thread for project at: {:?}", cwd); + + cx.spawn(async move |cx| { + log::debug!("Starting thread creation in async context"); + // Create Thread + let (session_id, thread) = agent.update( + cx, + |agent, cx: &mut gpui::Context| -> Result<_> { + // Fetch default model from registry settings + let registry = LanguageModelRegistry::read_global(cx); + + // Log available models for debugging + let available_count = registry.available_models(cx).count(); + log::debug!("Total available models: {}", available_count); + + let default_model = registry + .default_model() + .map(|configured| { + log::info!( + "Using configured default model: {:?} from provider: {:?}", + configured.model.name(), + configured.provider.name() + ); + configured.model + }) + .ok_or_else(|| { + log::warn!("No default model configured in settings"); + anyhow!("No default model configured. Please configure a default model in settings.") + })?; + + let thread = cx.new(|_| Thread::new(project.clone(), agent.templates.clone(), default_model)); + + // Generate session ID + let session_id = acp::SessionId(uuid::Uuid::new_v4().to_string().into()); + log::info!("Created session with ID: {}", session_id); + Ok((session_id, thread)) + }, + )??; + + // Create AcpThread + let acp_thread = cx.update(|cx| { + cx.new(|cx| { + acp_thread::AcpThread::new("agent2", self.clone(), project, session_id.clone(), cx) + }) + })?; + + // Store the session + agent.update(cx, |agent, _cx| { + agent.sessions.insert( + session_id, + Session { + thread, + acp_thread: acp_thread.clone(), + }, + ); + })?; + + Ok(acp_thread) + }) + } + + fn auth_methods(&self) -> &[acp::AuthMethod] { + &[] // No auth for in-process + } + + fn authenticate(&self, _method: acp::AuthMethodId, _cx: &mut App) -> Task> { + Task::ready(Ok(())) + } + + fn model_selector(&self) -> Option> { + Some(Rc::new(self.clone()) as Rc) + } + + fn prompt( + &self, + params: acp::PromptRequest, + cx: &mut App, + ) -> Task> { + let session_id = params.session_id.clone(); + let agent = self.0.clone(); + log::info!("Received prompt request for session: {}", session_id); + log::debug!("Prompt blocks count: {}", params.prompt.len()); + + cx.spawn(async move |cx| { + // Get session + let (thread, acp_thread) = agent + .update(cx, |agent, _| { + agent + .sessions + .get_mut(&session_id) + .map(|s| (s.thread.clone(), s.acp_thread.clone())) + })? + .ok_or_else(|| { + log::error!("Session not found: {}", session_id); + anyhow::anyhow!("Session not found") + })?; + log::debug!("Found session for: {}", session_id); + + // Convert prompt to message + let message = convert_prompt_to_message(params.prompt); + log::info!("Converted prompt to message: {} chars", message.len()); + log::debug!("Message content: {}", message); + + // Get model using the ModelSelector capability (always available for agent2) + // Get the selected model from the thread directly + let model = thread.read_with(cx, |thread, _| thread.selected_model.clone())?; + + // Send to thread + log::info!("Sending message to thread with model: {:?}", model.name()); + let mut response_stream = + thread.update(cx, |thread, cx| thread.send(model, message, cx))?; + + // Handle response stream and forward to session.acp_thread + while let Some(result) = response_stream.next().await { + match result { + Ok(event) => { + log::trace!("Received completion event: {:?}", event); + + match event { + AgentResponseEvent::Text(text) => { + acp_thread.update(cx, |thread, cx| { + thread.handle_session_update( + acp::SessionUpdate::AgentMessageChunk { + content: acp::ContentBlock::Text(acp::TextContent { + text, + annotations: None, + }), + }, + cx, + ) + })??; + } + AgentResponseEvent::Thinking(text) => { + acp_thread.update(cx, |thread, cx| { + thread.handle_session_update( + acp::SessionUpdate::AgentThoughtChunk { + content: acp::ContentBlock::Text(acp::TextContent { + text, + annotations: None, + }), + }, + cx, + ) + })??; + } + AgentResponseEvent::ToolCall(tool_call) => { + acp_thread.update(cx, |thread, cx| { + thread.handle_session_update( + acp::SessionUpdate::ToolCall(tool_call), + cx, + ) + })??; + } + AgentResponseEvent::ToolCallUpdate(tool_call_update) => { + acp_thread.update(cx, |thread, cx| { + thread.handle_session_update( + acp::SessionUpdate::ToolCallUpdate(tool_call_update), + cx, + ) + })??; + } + AgentResponseEvent::Stop(stop_reason) => { + log::debug!("Assistant message complete: {:?}", stop_reason); + return Ok(acp::PromptResponse { stop_reason }); + } + } + } + Err(e) => { + log::error!("Error in model response stream: {:?}", e); + // TODO: Consider sending an error message to the UI + break; + } + } + } + + log::info!("Response stream completed"); + anyhow::Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) + }) + } + + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { + log::info!("Cancelling on session: {}", session_id); + self.0.update(cx, |agent, cx| { + if let Some(agent) = agent.sessions.get(session_id) { + agent.thread.update(cx, |thread, _cx| thread.cancel()); + } + }); + } +} + +/// Convert ACP content blocks to a message string +fn convert_prompt_to_message(blocks: Vec) -> String { + log::debug!("Converting {} content blocks to message", blocks.len()); + let mut message = String::new(); + + for block in blocks { + match block { + acp::ContentBlock::Text(text) => { + log::trace!("Processing text block: {} chars", text.text.len()); + message.push_str(&text.text); + } + acp::ContentBlock::ResourceLink(link) => { + log::trace!("Processing resource link: {}", link.uri); + message.push_str(&format!(" @{} ", link.uri)); + } + acp::ContentBlock::Image(_) => { + log::trace!("Processing image block"); + message.push_str(" [image] "); + } + acp::ContentBlock::Audio(_) => { + log::trace!("Processing audio block"); + message.push_str(" [audio] "); + } + acp::ContentBlock::Resource(resource) => { + log::trace!("Processing resource block: {:?}", resource.resource); + message.push_str(&format!(" [resource: {:?}] ", resource.resource)); + } + } + } + + message +} diff --git a/crates/agent2/src/agent2.rs b/crates/agent2/src/agent2.rs new file mode 100644 index 0000000000..aa665fe313 --- /dev/null +++ b/crates/agent2/src/agent2.rs @@ -0,0 +1,13 @@ +mod agent; +mod native_agent_server; +mod prompts; +mod templates; +mod thread; +mod tools; + +#[cfg(test)] +mod tests; + +pub use agent::*; +pub use native_agent_server::NativeAgentServer; +pub use thread::*; diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs new file mode 100644 index 0000000000..aafe70a8a2 --- /dev/null +++ b/crates/agent2/src/native_agent_server.rs @@ -0,0 +1,58 @@ +use std::path::Path; +use std::rc::Rc; + +use agent_servers::AgentServer; +use anyhow::Result; +use gpui::{App, AppContext, Entity, Task}; +use project::Project; + +use crate::{templates::Templates, NativeAgent, NativeAgentConnection}; + +#[derive(Clone)] +pub struct NativeAgentServer; + +impl AgentServer for NativeAgentServer { + fn name(&self) -> &'static str { + "Native Agent" + } + + fn empty_state_headline(&self) -> &'static str { + "Native Agent" + } + + fn empty_state_message(&self) -> &'static str { + "How can I help you today?" + } + + fn logo(&self) -> ui::IconName { + // Using the ZedAssistant icon as it's the native built-in agent + ui::IconName::ZedAssistant + } + + fn connect( + &self, + _root_dir: &Path, + _project: &Entity, + cx: &mut App, + ) -> Task>> { + log::info!( + "NativeAgentServer::connect called for path: {:?}", + _root_dir + ); + cx.spawn(async move |cx| { + log::debug!("Creating templates for native agent"); + // Create templates (you might want to load these from files or resources) + let templates = Templates::new(); + + // Create the native agent + log::debug!("Creating native agent entity"); + let agent = cx.update(|cx| cx.new(|_| NativeAgent::new(templates)))?; + + // Create the connection wrapper + let connection = NativeAgentConnection(agent); + log::info!("NativeAgentServer connection established successfully"); + + Ok(Rc::new(connection) as Rc) + }) + } +} diff --git a/crates/agent2/src/prompts.rs b/crates/agent2/src/prompts.rs new file mode 100644 index 0000000000..28507f4968 --- /dev/null +++ b/crates/agent2/src/prompts.rs @@ -0,0 +1,35 @@ +use crate::{ + templates::{BaseTemplate, Template, Templates, WorktreeData}, + thread::Prompt, +}; +use anyhow::Result; +use gpui::{App, Entity}; +use project::Project; + +pub struct BasePrompt { + project: Entity, +} + +impl BasePrompt { + pub fn new(project: Entity) -> Self { + Self { project } + } +} + +impl Prompt for BasePrompt { + fn render(&self, templates: &Templates, cx: &App) -> Result { + BaseTemplate { + os: std::env::consts::OS.to_string(), + shell: util::get_system_shell(), + worktrees: self + .project + .read(cx) + .worktrees(cx) + .map(|worktree| WorktreeData { + root_name: worktree.read(cx).root_name().to_string(), + }) + .collect(), + } + .render(templates) + } +} diff --git a/crates/agent2/src/templates.rs b/crates/agent2/src/templates.rs new file mode 100644 index 0000000000..04569369be --- /dev/null +++ b/crates/agent2/src/templates.rs @@ -0,0 +1,57 @@ +use std::sync::Arc; + +use anyhow::Result; +use handlebars::Handlebars; +use rust_embed::RustEmbed; +use serde::Serialize; + +#[derive(RustEmbed)] +#[folder = "src/templates"] +#[include = "*.hbs"] +struct Assets; + +pub struct Templates(Handlebars<'static>); + +impl Templates { + pub fn new() -> Arc { + let mut handlebars = Handlebars::new(); + handlebars.register_embed_templates::().unwrap(); + Arc::new(Self(handlebars)) + } +} + +pub trait Template: Sized { + const TEMPLATE_NAME: &'static str; + + fn render(&self, templates: &Templates) -> Result + where + Self: Serialize + Sized, + { + Ok(templates.0.render(Self::TEMPLATE_NAME, self)?) + } +} + +#[derive(Serialize)] +pub struct BaseTemplate { + pub os: String, + pub shell: String, + pub worktrees: Vec, +} + +impl Template for BaseTemplate { + const TEMPLATE_NAME: &'static str = "base.hbs"; +} + +#[derive(Serialize)] +pub struct WorktreeData { + pub root_name: String, +} + +#[derive(Serialize)] +pub struct GlobTemplate { + pub project_roots: String, +} + +impl Template for GlobTemplate { + const TEMPLATE_NAME: &'static str = "glob.hbs"; +} diff --git a/crates/agent2/src/templates/base.hbs b/crates/agent2/src/templates/base.hbs new file mode 100644 index 0000000000..7eef231e32 --- /dev/null +++ b/crates/agent2/src/templates/base.hbs @@ -0,0 +1,56 @@ +You are a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. + +## Communication + +1. Be conversational but professional. +2. Refer to the USER in the second person and yourself in the first person. +3. Format your responses in markdown. Use backticks to format file, directory, function, and class names. +4. NEVER lie or make things up. +5. Refrain from apologizing all the time when results are unexpected. Instead, just try your best to proceed or explain the circumstances to the user without apologizing. + +## Tool Use + +1. Make sure to adhere to the tools schema. +2. Provide every required argument. +3. DO NOT use tools to access items that are already available in the context section. +4. Use only the tools that are currently available. +5. DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off. + +## Searching and Reading + +If you are unsure how to fulfill the user's request, gather more information with tool calls and/or clarifying questions. + +If appropriate, use tool calls to explore the current project, which contains the following root directories: + +{{#each worktrees}} +- `{{root_name}}` +{{/each}} + +- When providing paths to tools, the path should always begin with a path that starts with a project root directory listed above. +- When looking for symbols in the project, prefer the `grep` tool. +- As you learn about the structure of the project, use that information to scope `grep` searches to targeted subtrees of the project. +- Bias towards not asking the user for help if you can find the answer yourself. + +## Fixing Diagnostics + +1. Make 1-2 attempts at fixing diagnostics, then defer to the user. +2. Never simplify code you've written just to solve diagnostics. Complete, mostly correct code is more valuable than perfect code that doesn't solve the problem. + +## Debugging + +When debugging, only make code changes if you are certain that you can solve the problem. +Otherwise, follow debugging best practices: +1. Address the root cause instead of the symptoms. +2. Add descriptive logging statements and error messages to track variable and code state. +3. Add test functions and statements to isolate the problem. + +## Calling External APIs + +1. Unless explicitly requested by the user, use the best suited external APIs and packages to solve the task. There is no need to ask the user for permission. +2. When selecting which version of an API or package to use, choose one that is compatible with the user's dependency management file. If no such file exists or if the package is not present, use the latest version that is in your training data. +3. If an external API requires an API Key, be sure to point this out to the user. Adhere to best security practices (e.g. DO NOT hardcode an API key in a place where it can be exposed) + +## System Information + +Operating System: {{os}} +Default Shell: {{shell}} diff --git a/crates/agent2/src/templates/glob.hbs b/crates/agent2/src/templates/glob.hbs new file mode 100644 index 0000000000..3bf992b093 --- /dev/null +++ b/crates/agent2/src/templates/glob.hbs @@ -0,0 +1,8 @@ +Find paths on disk with glob patterns. + +Assume that all glob patterns are matched in a project directory with the following entries. + +{{project_roots}} + +When searching with patterns that begin with literal path components, e.g. `foo/bar/**/*.rs`, be +sure to anchor them with one of the directories listed above. diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs new file mode 100644 index 0000000000..2e2b25a119 --- /dev/null +++ b/crates/agent2/src/tests/mod.rs @@ -0,0 +1,513 @@ +use super::*; +use crate::templates::Templates; +use acp_thread::AgentConnection as _; +use agent_client_protocol as acp; +use client::{Client, UserStore}; +use fs::FakeFs; +use gpui::{AppContext, Entity, Task, TestAppContext}; +use indoc::indoc; +use language_model::{ + fake_provider::FakeLanguageModel, LanguageModel, LanguageModelCompletionError, + LanguageModelCompletionEvent, LanguageModelId, LanguageModelRegistry, MessageContent, + StopReason, +}; +use project::Project; +use reqwest_client::ReqwestClient; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use smol::stream::StreamExt; +use std::{path::Path, rc::Rc, sync::Arc, time::Duration}; +use util::path; + +mod test_tools; +use test_tools::*; + +#[gpui::test] +#[ignore = "temporarily disabled until it can be run on CI"] +async fn test_echo(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await; + + let events = thread + .update(cx, |thread, cx| { + thread.send(model.clone(), "Testing: Reply with 'Hello'", cx) + }) + .collect() + .await; + thread.update(cx, |thread, _cx| { + assert_eq!( + thread.messages().last().unwrap().content, + vec![MessageContent::Text("Hello".to_string())] + ); + }); + assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); +} + +#[gpui::test] +#[ignore = "temporarily disabled until it can be run on CI"] +async fn test_thinking(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4Thinking).await; + + let events = thread + .update(cx, |thread, cx| { + thread.send( + model.clone(), + indoc! {" + Testing: + + Generate a thinking step where you just think the word 'Think', + and have your final answer be 'Hello' + "}, + cx, + ) + }) + .collect() + .await; + thread.update(cx, |thread, _cx| { + assert_eq!( + thread.messages().last().unwrap().to_markdown(), + indoc! {" + ## assistant + Think + Hello + "} + ) + }); + assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); +} + +#[gpui::test] +#[ignore = "temporarily disabled until it can be run on CI"] +async fn test_basic_tool_calls(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await; + + // Test a tool call that's likely to complete *before* streaming stops. + let events = thread + .update(cx, |thread, cx| { + thread.add_tool(EchoTool); + thread.send( + model.clone(), + "Now test the echo tool with 'Hello'. Does it work? Say 'Yes' or 'No'.", + cx, + ) + }) + .collect() + .await; + assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); + + // Test a tool calls that's likely to complete *after* streaming stops. + let events = thread + .update(cx, |thread, cx| { + thread.remove_tool(&AgentTool::name(&EchoTool)); + thread.add_tool(DelayTool); + thread.send( + model.clone(), + "Now call the delay tool with 200ms. When the timer goes off, then you echo the output of the tool.", + cx, + ) + }) + .collect() + .await; + assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); + thread.update(cx, |thread, _cx| { + assert!(thread + .messages() + .last() + .unwrap() + .content + .iter() + .any(|content| { + if let MessageContent::Text(text) = content { + text.contains("Ding") + } else { + false + } + })); + }); +} + +#[gpui::test] +#[ignore = "temporarily disabled until it can be run on CI"] +async fn test_streaming_tool_calls(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await; + + // Test a tool call that's likely to complete *before* streaming stops. + let mut events = thread.update(cx, |thread, cx| { + thread.add_tool(WordListTool); + thread.send(model.clone(), "Test the word_list tool.", cx) + }); + + let mut saw_partial_tool_use = false; + while let Some(event) = events.next().await { + if let Ok(AgentResponseEvent::ToolCall(tool_call)) = event { + thread.update(cx, |thread, _cx| { + // Look for a tool use in the thread's last message + let last_content = thread.messages().last().unwrap().content.last().unwrap(); + if let MessageContent::ToolUse(last_tool_use) = last_content { + assert_eq!(last_tool_use.name.as_ref(), "word_list"); + if tool_call.status == acp::ToolCallStatus::Pending { + if !last_tool_use.is_input_complete + && last_tool_use.input.get("g").is_none() + { + saw_partial_tool_use = true; + } + } else { + last_tool_use + .input + .get("a") + .expect("'a' has streamed because input is now complete"); + last_tool_use + .input + .get("g") + .expect("'g' has streamed because input is now complete"); + } + } else { + panic!("last content should be a tool use"); + } + }); + } + } + + assert!( + saw_partial_tool_use, + "should see at least one partially streamed tool use in the history" + ); +} + +#[gpui::test] +#[ignore = "temporarily disabled until it can be run on CI"] +async fn test_concurrent_tool_calls(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await; + + // Test concurrent tool calls with different delay times + let events = thread + .update(cx, |thread, cx| { + thread.add_tool(DelayTool); + thread.send( + model.clone(), + "Call the delay tool twice in the same message. Once with 100ms. Once with 300ms. When both timers are complete, describe the outputs.", + cx, + ) + }) + .collect() + .await; + + let stop_reasons = stop_events(events); + assert_eq!(stop_reasons, vec![acp::StopReason::EndTurn]); + + thread.update(cx, |thread, _cx| { + let last_message = thread.messages().last().unwrap(); + let text = last_message + .content + .iter() + .filter_map(|content| { + if let MessageContent::Text(text) = content { + Some(text.as_str()) + } else { + None + } + }) + .collect::(); + + assert!(text.contains("Ding")); + }); +} + +#[gpui::test] +#[ignore = "temporarily disabled until it can be run on CI"] +async fn test_cancellation(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await; + + let mut events = thread.update(cx, |thread, cx| { + thread.add_tool(InfiniteTool); + thread.add_tool(EchoTool); + thread.send( + model.clone(), + "Call the echo tool and then call the infinite tool, then explain their output", + cx, + ) + }); + + // Wait until both tools are called. + let mut expected_tool_calls = vec!["echo", "infinite"]; + let mut echo_id = None; + let mut echo_completed = false; + while let Some(event) = events.next().await { + match event.unwrap() { + AgentResponseEvent::ToolCall(tool_call) => { + assert_eq!(tool_call.title, expected_tool_calls.remove(0)); + if tool_call.title == "echo" { + echo_id = Some(tool_call.id); + } + } + AgentResponseEvent::ToolCallUpdate(acp::ToolCallUpdate { + id, + fields: + acp::ToolCallUpdateFields { + status: Some(acp::ToolCallStatus::Completed), + .. + }, + }) if Some(&id) == echo_id.as_ref() => { + echo_completed = true; + } + _ => {} + } + + if expected_tool_calls.is_empty() && echo_completed { + break; + } + } + + // Cancel the current send and ensure that the event stream is closed, even + // if one of the tools is still running. + thread.update(cx, |thread, _cx| thread.cancel()); + events.collect::>().await; + + // Ensure we can still send a new message after cancellation. + let events = thread + .update(cx, |thread, cx| { + thread.send(model.clone(), "Testing: reply with 'Hello' then stop.", cx) + }) + .collect::>() + .await; + thread.update(cx, |thread, _cx| { + assert_eq!( + thread.messages().last().unwrap().content, + vec![MessageContent::Text("Hello".to_string())] + ); + }); + assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); +} + +#[gpui::test] +async fn test_refusal(cx: &mut TestAppContext) { + let fake_model = Arc::new(FakeLanguageModel::default()); + let ThreadTest { thread, .. } = setup(cx, TestModel::Fake(fake_model.clone())).await; + + let events = thread.update(cx, |thread, cx| { + thread.send(fake_model.clone(), "Hello", cx) + }); + cx.run_until_parked(); + thread.read_with(cx, |thread, _| { + assert_eq!( + thread.to_markdown(), + indoc! {" + ## user + Hello + "} + ); + }); + + fake_model.send_last_completion_stream_text_chunk("Hey!"); + cx.run_until_parked(); + thread.read_with(cx, |thread, _| { + assert_eq!( + thread.to_markdown(), + indoc! {" + ## user + Hello + ## assistant + Hey! + "} + ); + }); + + // If the model refuses to continue, the thread should remove all the messages after the last user message. + fake_model + .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::Refusal)); + let events = events.collect::>().await; + assert_eq!(stop_events(events), vec![acp::StopReason::Refusal]); + thread.read_with(cx, |thread, _| { + assert_eq!(thread.to_markdown(), ""); + }); +} + +#[ignore = "temporarily disabled until it can be run on CI"] +#[gpui::test] +async fn test_agent_connection(cx: &mut TestAppContext) { + cx.executor().allow_parking(); + cx.update(settings::init); + let templates = Templates::new(); + + // Initialize language model system with test provider + cx.update(|cx| { + gpui_tokio::init(cx); + let http_client = ReqwestClient::user_agent("agent tests").unwrap(); + cx.set_http_client(Arc::new(http_client)); + + client::init_settings(cx); + let client = Client::production(cx); + let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); + language_model::init(client.clone(), cx); + language_models::init(user_store.clone(), client.clone(), cx); + + // Initialize project settings + Project::init_settings(cx); + + // Use test registry with fake provider + LanguageModelRegistry::test(cx); + }); + + // Create agent and connection + let agent = cx.new(|_| NativeAgent::new(templates.clone())); + let connection = NativeAgentConnection(agent.clone()); + + // Test model_selector returns Some + let selector_opt = connection.model_selector(); + assert!( + selector_opt.is_some(), + "agent2 should always support ModelSelector" + ); + let selector = selector_opt.unwrap(); + + // Test list_models + let listed_models = cx + .update(|cx| { + let mut async_cx = cx.to_async(); + selector.list_models(&mut async_cx) + }) + .await + .expect("list_models should succeed"); + assert!(!listed_models.is_empty(), "should have at least one model"); + assert_eq!(listed_models[0].id().0, "fake"); + + // Create a project for new_thread + let fake_fs = cx.update(|cx| fs::FakeFs::new(cx.background_executor().clone())); + let project = Project::test(fake_fs, [Path::new("/test")], cx).await; + + // Create a thread using new_thread + let cwd = Path::new("/test"); + let connection_rc = Rc::new(connection.clone()); + let acp_thread = cx + .update(|cx| { + let mut async_cx = cx.to_async(); + connection_rc.new_thread(project, cwd, &mut async_cx) + }) + .await + .expect("new_thread should succeed"); + + // Get the session_id from the AcpThread + let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone()); + + // Test selected_model returns the default + let selected = cx + .update(|cx| { + let mut async_cx = cx.to_async(); + selector.selected_model(&session_id, &mut async_cx) + }) + .await + .expect("selected_model should succeed"); + assert_eq!(selected.id().0, "fake", "should return default model"); + + // The thread was created via prompt with the default model + // We can verify it through selected_model + + // Test prompt uses the selected model + let prompt_request = acp::PromptRequest { + session_id: session_id.clone(), + prompt: vec![acp::ContentBlock::Text(acp::TextContent { + text: "Test prompt".into(), + annotations: None, + })], + }; + + let request = cx.update(|cx| connection.prompt(prompt_request, cx)); + let request = cx.background_spawn(request); + smol::Timer::after(Duration::from_millis(100)).await; + + // Test cancel + cx.update(|cx| connection.cancel(&session_id, cx)); + request.await.expect("prompt should fail gracefully"); +} + +/// Filters out the stop events for asserting against in tests +fn stop_events( + result_events: Vec>, +) -> Vec { + result_events + .into_iter() + .filter_map(|event| match event.unwrap() { + AgentResponseEvent::Stop(stop_reason) => Some(stop_reason), + _ => None, + }) + .collect() +} + +struct ThreadTest { + model: Arc, + thread: Entity, +} + +enum TestModel { + Sonnet4, + Sonnet4Thinking, + Fake(Arc), +} + +impl TestModel { + fn id(&self) -> LanguageModelId { + match self { + TestModel::Sonnet4 => LanguageModelId("claude-sonnet-4-latest".into()), + TestModel::Sonnet4Thinking => LanguageModelId("claude-sonnet-4-thinking-latest".into()), + TestModel::Fake(fake_model) => fake_model.id(), + } + } +} + +async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { + cx.executor().allow_parking(); + cx.update(|cx| { + settings::init(cx); + Project::init_settings(cx); + }); + let templates = Templates::new(); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree(path!("/test"), json!({})).await; + let project = Project::test(fs, [path!("/test").as_ref()], cx).await; + + let model = cx + .update(|cx| { + gpui_tokio::init(cx); + let http_client = ReqwestClient::user_agent("agent tests").unwrap(); + cx.set_http_client(Arc::new(http_client)); + + client::init_settings(cx); + let client = Client::production(cx); + let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); + language_model::init(client.clone(), cx); + language_models::init(user_store.clone(), client.clone(), cx); + + if let TestModel::Fake(model) = model { + Task::ready(model as Arc<_>) + } else { + let model_id = model.id(); + let models = LanguageModelRegistry::read_global(cx); + let model = models + .available_models(cx) + .find(|model| model.id() == model_id) + .unwrap(); + + let provider = models.provider(&model.provider_id()).unwrap(); + let authenticated = provider.authenticate(cx); + + cx.spawn(async move |_cx| { + authenticated.await.unwrap(); + model + }) + } + }) + .await; + + let thread = cx.new(|_| Thread::new(project, templates, model.clone())); + + ThreadTest { model, thread } +} + +#[cfg(test)] +#[ctor::ctor] +fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } +} diff --git a/crates/agent2/src/tests/test_tools.rs b/crates/agent2/src/tests/test_tools.rs new file mode 100644 index 0000000000..1847a14fee --- /dev/null +++ b/crates/agent2/src/tests/test_tools.rs @@ -0,0 +1,106 @@ +use super::*; +use anyhow::Result; +use gpui::{App, SharedString, Task}; +use std::future; + +/// A tool that echoes its input +#[derive(JsonSchema, Serialize, Deserialize)] +pub struct EchoToolInput { + /// The text to echo. + text: String, +} + +pub struct EchoTool; + +impl AgentTool for EchoTool { + type Input = EchoToolInput; + + fn name(&self) -> SharedString { + "echo".into() + } + + fn run(self: Arc, input: Self::Input, _cx: &mut App) -> Task> { + Task::ready(Ok(input.text)) + } +} + +/// A tool that waits for a specified delay +#[derive(JsonSchema, Serialize, Deserialize)] +pub struct DelayToolInput { + /// The delay in milliseconds. + ms: u64, +} + +pub struct DelayTool; + +impl AgentTool for DelayTool { + type Input = DelayToolInput; + + fn name(&self) -> SharedString { + "delay".into() + } + + fn run(self: Arc, input: Self::Input, cx: &mut App) -> Task> + where + Self: Sized, + { + cx.foreground_executor().spawn(async move { + smol::Timer::after(Duration::from_millis(input.ms)).await; + Ok("Ding".to_string()) + }) + } +} + +#[derive(JsonSchema, Serialize, Deserialize)] +pub struct InfiniteToolInput {} + +pub struct InfiniteTool; + +impl AgentTool for InfiniteTool { + type Input = InfiniteToolInput; + + fn name(&self) -> SharedString { + "infinite".into() + } + + fn run(self: Arc, _input: Self::Input, cx: &mut App) -> Task> { + cx.foreground_executor().spawn(async move { + future::pending::<()>().await; + unreachable!() + }) + } +} + +/// A tool that takes an object with map from letters to random words starting with that letter. +/// All fiealds are required! Pass a word for every letter! +#[derive(JsonSchema, Serialize, Deserialize)] +pub struct WordListInput { + /// Provide a random word that starts with A. + a: Option, + /// Provide a random word that starts with B. + b: Option, + /// Provide a random word that starts with C. + c: Option, + /// Provide a random word that starts with D. + d: Option, + /// Provide a random word that starts with E. + e: Option, + /// Provide a random word that starts with F. + f: Option, + /// Provide a random word that starts with G. + g: Option, +} + +pub struct WordListTool; + +impl AgentTool for WordListTool { + type Input = WordListInput; + + fn name(&self) -> SharedString { + "word_list".into() + } + + fn run(self: Arc, _input: Self::Input, _cx: &mut App) -> Task> { + Task::ready(Ok("ok".to_string())) + } +} diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs new file mode 100644 index 0000000000..af3aa17ea8 --- /dev/null +++ b/crates/agent2/src/thread.rs @@ -0,0 +1,754 @@ +use crate::{prompts::BasePrompt, templates::Templates}; +use agent_client_protocol as acp; +use anyhow::{anyhow, Result}; +use cloud_llm_client::{CompletionIntent, CompletionMode}; +use collections::HashMap; +use futures::{channel::mpsc, stream::FuturesUnordered}; +use gpui::{App, Context, Entity, ImageFormat, SharedString, Task}; +use language_model::{ + LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage, + LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, + LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, + LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role, StopReason, +}; +use log; +use project::Project; +use schemars::{JsonSchema, Schema}; +use serde::Deserialize; +use smol::stream::StreamExt; +use std::{collections::BTreeMap, fmt::Write, sync::Arc}; +use util::{markdown::MarkdownCodeBlock, ResultExt}; + +#[derive(Debug, Clone)] +pub struct AgentMessage { + pub role: Role, + pub content: Vec, +} + +impl AgentMessage { + pub fn to_markdown(&self) -> String { + let mut markdown = format!("## {}\n", self.role); + + for content in &self.content { + match content { + MessageContent::Text(text) => { + markdown.push_str(text); + markdown.push('\n'); + } + MessageContent::Thinking { text, .. } => { + markdown.push_str(""); + markdown.push_str(text); + markdown.push_str("\n"); + } + MessageContent::RedactedThinking(_) => markdown.push_str("\n"), + MessageContent::Image(_) => { + markdown.push_str("\n"); + } + MessageContent::ToolUse(tool_use) => { + markdown.push_str(&format!( + "**Tool Use**: {} (ID: {})\n", + tool_use.name, tool_use.id + )); + markdown.push_str(&format!( + "{}\n", + MarkdownCodeBlock { + tag: "json", + text: &format!("{:#}", tool_use.input) + } + )); + } + MessageContent::ToolResult(tool_result) => { + markdown.push_str(&format!( + "**Tool Result**: {} (ID: {})\n\n", + tool_result.tool_name, tool_result.tool_use_id + )); + if tool_result.is_error { + markdown.push_str("**ERROR:**\n"); + } + + match &tool_result.content { + LanguageModelToolResultContent::Text(text) => { + writeln!(markdown, "{text}\n").ok(); + } + LanguageModelToolResultContent::Image(_) => { + writeln!(markdown, "\n").ok(); + } + } + + if let Some(output) = tool_result.output.as_ref() { + writeln!( + markdown, + "**Debug Output**:\n\n```json\n{}\n```\n", + serde_json::to_string_pretty(output).unwrap() + ) + .unwrap(); + } + } + } + } + + markdown + } +} + +#[derive(Debug)] +pub enum AgentResponseEvent { + Text(String), + Thinking(String), + ToolCall(acp::ToolCall), + ToolCallUpdate(acp::ToolCallUpdate), + Stop(acp::StopReason), +} + +pub trait Prompt { + fn render(&self, prompts: &Templates, cx: &App) -> Result; +} + +pub struct Thread { + messages: Vec, + completion_mode: CompletionMode, + /// Holds the task that handles agent interaction until the end of the turn. + /// Survives across multiple requests as the model performs tool calls and + /// we run tools, report their results. + running_turn: Option>, + pending_tool_uses: HashMap, + system_prompts: Vec>, + tools: BTreeMap>, + templates: Arc, + pub selected_model: Arc, + // action_log: Entity, +} + +impl Thread { + pub fn new( + project: Entity, + templates: Arc, + default_model: Arc, + ) -> Self { + Self { + messages: Vec::new(), + completion_mode: CompletionMode::Normal, + system_prompts: vec![Arc::new(BasePrompt::new(project))], + running_turn: None, + pending_tool_uses: HashMap::default(), + tools: BTreeMap::default(), + templates, + selected_model: default_model, + } + } + + pub fn set_mode(&mut self, mode: CompletionMode) { + self.completion_mode = mode; + } + + pub fn messages(&self) -> &[AgentMessage] { + &self.messages + } + + pub fn add_tool(&mut self, tool: impl AgentTool) { + self.tools.insert(tool.name(), tool.erase()); + } + + pub fn remove_tool(&mut self, name: &str) -> bool { + self.tools.remove(name).is_some() + } + + pub fn cancel(&mut self) { + self.running_turn.take(); + + let tool_results = self + .pending_tool_uses + .drain() + .map(|(tool_use_id, tool_use)| { + MessageContent::ToolResult(LanguageModelToolResult { + tool_use_id, + tool_name: tool_use.name.clone(), + is_error: true, + content: LanguageModelToolResultContent::Text("Tool canceled by user".into()), + output: None, + }) + }) + .collect::>(); + self.last_user_message().content.extend(tool_results); + } + + /// Sending a message results in the model streaming a response, which could include tool calls. + /// After calling tools, the model will stops and waits for any outstanding tool calls to be completed and their results sent. + /// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn. + pub fn send( + &mut self, + model: Arc, + content: impl Into, + cx: &mut Context, + ) -> mpsc::UnboundedReceiver> { + let content = content.into(); + log::info!("Thread::send called with model: {:?}", model.name()); + log::debug!("Thread::send content: {:?}", content); + + cx.notify(); + let (events_tx, events_rx) = + mpsc::unbounded::>(); + + let user_message_ix = self.messages.len(); + self.messages.push(AgentMessage { + role: Role::User, + content: vec![content], + }); + log::info!("Total messages in thread: {}", self.messages.len()); + self.running_turn = Some(cx.spawn(async move |thread, cx| { + log::info!("Starting agent turn execution"); + let turn_result = async { + // Perform one request, then keep looping if the model makes tool calls. + let mut completion_intent = CompletionIntent::UserPrompt; + 'outer: loop { + log::debug!( + "Building completion request with intent: {:?}", + completion_intent + ); + let request = thread.update(cx, |thread, cx| { + thread.build_completion_request(completion_intent, cx) + })?; + + // println!( + // "request: {}", + // serde_json::to_string_pretty(&request).unwrap() + // ); + + // Stream events, appending to messages and collecting up tool uses. + log::info!("Calling model.stream_completion"); + let mut events = model.stream_completion(request, cx).await?; + log::debug!("Stream completion started successfully"); + let mut tool_uses = FuturesUnordered::new(); + while let Some(event) = events.next().await { + match event { + Ok(LanguageModelCompletionEvent::Stop(reason)) => { + if let Some(reason) = to_acp_stop_reason(reason) { + events_tx + .unbounded_send(Ok(AgentResponseEvent::Stop(reason))) + .ok(); + } + + if reason == StopReason::Refusal { + thread.update(cx, |thread, _cx| { + thread.messages.truncate(user_message_ix); + })?; + break 'outer; + } + } + Ok(event) => { + log::trace!("Received completion event: {:?}", event); + thread + .update(cx, |thread, cx| { + tool_uses.extend(thread.handle_streamed_completion_event( + event, &events_tx, cx, + )); + }) + .ok(); + } + Err(error) => { + log::error!("Error in completion stream: {:?}", error); + events_tx.unbounded_send(Err(error)).ok(); + break; + } + } + } + + // If there are no tool uses, the turn is done. + if tool_uses.is_empty() { + log::info!("No tool uses found, completing turn"); + break; + } + log::info!("Found {} tool uses to execute", tool_uses.len()); + + // As tool results trickle in, insert them in the last user + // message so that they can be sent on the next tick of the + // agentic loop. + while let Some(tool_result) = tool_uses.next().await { + log::info!("Tool finished {:?}", tool_result); + + events_tx + .unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate( + to_acp_tool_call_update(&tool_result), + ))) + .ok(); + thread + .update(cx, |thread, _cx| { + thread.pending_tool_uses.remove(&tool_result.tool_use_id); + thread + .last_user_message() + .content + .push(MessageContent::ToolResult(tool_result)); + }) + .ok(); + } + + completion_intent = CompletionIntent::ToolResults; + } + + Ok(()) + } + .await; + + if let Err(error) = turn_result { + log::error!("Turn execution failed: {:?}", error); + events_tx.unbounded_send(Err(error)).ok(); + } else { + log::info!("Turn execution completed successfully"); + } + })); + events_rx + } + + pub fn build_system_message(&self, cx: &App) -> Option { + log::debug!("Building system message"); + let mut system_message = AgentMessage { + role: Role::System, + content: Vec::new(), + }; + + for prompt in &self.system_prompts { + if let Some(rendered_prompt) = prompt.render(&self.templates, cx).log_err() { + system_message + .content + .push(MessageContent::Text(rendered_prompt)); + } + } + + let result = (!system_message.content.is_empty()).then_some(system_message); + log::debug!("System message built: {}", result.is_some()); + result + } + + /// A helper method that's called on every streamed completion event. + /// Returns an optional tool result task, which the main agentic loop in + /// send will send back to the model when it resolves. + fn handle_streamed_completion_event( + &mut self, + event: LanguageModelCompletionEvent, + events_tx: &mpsc::UnboundedSender>, + cx: &mut Context, + ) -> Option> { + log::trace!("Handling streamed completion event: {:?}", event); + use LanguageModelCompletionEvent::*; + + match event { + StartMessage { .. } => { + self.messages.push(AgentMessage { + role: Role::Assistant, + content: Vec::new(), + }); + } + Text(new_text) => self.handle_text_event(new_text, events_tx, cx), + Thinking { text, signature } => { + self.handle_thinking_event(text, signature, events_tx, cx) + } + RedactedThinking { data } => self.handle_redacted_thinking_event(data, cx), + ToolUse(tool_use) => { + return self.handle_tool_use_event(tool_use, events_tx, cx); + } + ToolUseJsonParseError { + id, + tool_name, + raw_input, + json_parse_error, + } => { + return Some(Task::ready(self.handle_tool_use_json_parse_error_event( + id, + tool_name, + raw_input, + json_parse_error, + ))); + } + UsageUpdate(_) | StatusUpdate(_) => {} + Stop(_) => unreachable!(), + } + + None + } + + fn handle_text_event( + &mut self, + new_text: String, + events_tx: &mpsc::UnboundedSender>, + cx: &mut Context, + ) { + events_tx + .unbounded_send(Ok(AgentResponseEvent::Text(new_text.clone()))) + .ok(); + + let last_message = self.last_assistant_message(); + if let Some(MessageContent::Text(text)) = last_message.content.last_mut() { + text.push_str(&new_text); + } else { + last_message.content.push(MessageContent::Text(new_text)); + } + + cx.notify(); + } + + fn handle_thinking_event( + &mut self, + new_text: String, + new_signature: Option, + events_tx: &mpsc::UnboundedSender>, + cx: &mut Context, + ) { + events_tx + .unbounded_send(Ok(AgentResponseEvent::Thinking(new_text.clone()))) + .ok(); + + let last_message = self.last_assistant_message(); + if let Some(MessageContent::Thinking { text, signature }) = last_message.content.last_mut() + { + text.push_str(&new_text); + *signature = new_signature.or(signature.take()); + } else { + last_message.content.push(MessageContent::Thinking { + text: new_text, + signature: new_signature, + }); + } + + cx.notify(); + } + + fn handle_redacted_thinking_event(&mut self, data: String, cx: &mut Context) { + let last_message = self.last_assistant_message(); + last_message + .content + .push(MessageContent::RedactedThinking(data)); + cx.notify(); + } + + fn handle_tool_use_event( + &mut self, + tool_use: LanguageModelToolUse, + events_tx: &mpsc::UnboundedSender>, + cx: &mut Context, + ) -> Option> { + cx.notify(); + + self.pending_tool_uses + .insert(tool_use.id.clone(), tool_use.clone()); + let last_message = self.last_assistant_message(); + + // Ensure the last message ends in the current tool use + let push_new_tool_use = last_message.content.last_mut().map_or(true, |content| { + if let MessageContent::ToolUse(last_tool_use) = content { + if last_tool_use.id == tool_use.id { + *last_tool_use = tool_use.clone(); + false + } else { + true + } + } else { + true + } + }); + if push_new_tool_use { + events_tx + .unbounded_send(Ok(AgentResponseEvent::ToolCall(acp::ToolCall { + id: acp::ToolCallId(tool_use.id.to_string().into()), + title: tool_use.name.to_string(), + kind: acp::ToolKind::Other, + status: acp::ToolCallStatus::Pending, + content: vec![], + locations: vec![], + raw_input: Some(tool_use.input.clone()), + }))) + .ok(); + last_message + .content + .push(MessageContent::ToolUse(tool_use.clone())); + } else { + events_tx + .unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate( + acp::ToolCallUpdate { + id: acp::ToolCallId(tool_use.id.to_string().into()), + fields: acp::ToolCallUpdateFields { + raw_input: Some(tool_use.input.clone()), + ..Default::default() + }, + }, + ))) + .ok(); + } + + if !tool_use.is_input_complete { + return None; + } + + if let Some(tool) = self.tools.get(tool_use.name.as_ref()) { + events_tx + .unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate( + acp::ToolCallUpdate { + id: acp::ToolCallId(tool_use.id.to_string().into()), + fields: acp::ToolCallUpdateFields { + status: Some(acp::ToolCallStatus::InProgress), + ..Default::default() + }, + }, + ))) + .ok(); + + let pending_tool_result = tool.clone().run(tool_use.input, cx); + + Some(cx.foreground_executor().spawn(async move { + match pending_tool_result.await { + Ok(tool_output) => LanguageModelToolResult { + tool_use_id: tool_use.id, + tool_name: tool_use.name, + is_error: false, + content: LanguageModelToolResultContent::Text(Arc::from(tool_output)), + output: None, + }, + Err(error) => LanguageModelToolResult { + tool_use_id: tool_use.id, + tool_name: tool_use.name, + is_error: true, + content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())), + output: None, + }, + } + })) + } else { + let content = format!("No tool named {} exists", tool_use.name); + Some(Task::ready(LanguageModelToolResult { + content: LanguageModelToolResultContent::Text(Arc::from(content)), + tool_use_id: tool_use.id, + tool_name: tool_use.name, + is_error: true, + output: None, + })) + } + } + + fn handle_tool_use_json_parse_error_event( + &mut self, + tool_use_id: LanguageModelToolUseId, + tool_name: Arc, + raw_input: Arc, + json_parse_error: String, + ) -> LanguageModelToolResult { + let tool_output = format!("Error parsing input JSON: {json_parse_error}"); + LanguageModelToolResult { + tool_use_id, + tool_name, + is_error: true, + content: LanguageModelToolResultContent::Text(tool_output.into()), + output: Some(serde_json::Value::String(raw_input.to_string())), + } + } + + /// Guarantees the last message is from the assistant and returns a mutable reference. + fn last_assistant_message(&mut self) -> &mut AgentMessage { + if self + .messages + .last() + .map_or(true, |m| m.role != Role::Assistant) + { + self.messages.push(AgentMessage { + role: Role::Assistant, + content: Vec::new(), + }); + } + self.messages.last_mut().unwrap() + } + + /// Guarantees the last message is from the user and returns a mutable reference. + fn last_user_message(&mut self) -> &mut AgentMessage { + if self.messages.last().map_or(true, |m| m.role != Role::User) { + self.messages.push(AgentMessage { + role: Role::User, + content: Vec::new(), + }); + } + self.messages.last_mut().unwrap() + } + + fn build_completion_request( + &self, + completion_intent: CompletionIntent, + cx: &mut App, + ) -> LanguageModelRequest { + log::debug!("Building completion request"); + log::debug!("Completion intent: {:?}", completion_intent); + log::debug!("Completion mode: {:?}", self.completion_mode); + + let messages = self.build_request_messages(cx); + log::info!("Request will include {} messages", messages.len()); + + let tools: Vec = self + .tools + .values() + .filter_map(|tool| { + let tool_name = tool.name().to_string(); + log::trace!("Including tool: {}", tool_name); + Some(LanguageModelRequestTool { + name: tool_name, + description: tool.description(cx).to_string(), + input_schema: tool + .input_schema(LanguageModelToolSchemaFormat::JsonSchema) + .log_err()?, + }) + }) + .collect(); + + log::info!("Request includes {} tools", tools.len()); + + let request = LanguageModelRequest { + thread_id: None, + prompt_id: None, + intent: Some(completion_intent), + mode: Some(self.completion_mode), + messages, + tools, + tool_choice: None, + stop: Vec::new(), + temperature: None, + thinking_allowed: true, + }; + + log::debug!("Completion request built successfully"); + request + } + + fn build_request_messages(&self, cx: &App) -> Vec { + log::trace!( + "Building request messages from {} thread messages", + self.messages.len() + ); + + let messages = self + .build_system_message(cx) + .iter() + .chain(self.messages.iter()) + .map(|message| { + log::trace!( + " - {} message with {} content items", + match message.role { + Role::System => "System", + Role::User => "User", + Role::Assistant => "Assistant", + }, + message.content.len() + ); + LanguageModelRequestMessage { + role: message.role, + content: message.content.clone(), + cache: false, + } + }) + .collect(); + messages + } + + pub fn to_markdown(&self) -> String { + let mut markdown = String::new(); + for message in &self.messages { + markdown.push_str(&message.to_markdown()); + } + markdown + } +} + +pub trait AgentTool +where + Self: 'static + Sized, +{ + type Input: for<'de> Deserialize<'de> + JsonSchema; + + fn name(&self) -> SharedString; + fn description(&self, _cx: &mut App) -> SharedString { + let schema = schemars::schema_for!(Self::Input); + SharedString::new( + schema + .get("description") + .and_then(|description| description.as_str()) + .unwrap_or_default(), + ) + } + + /// Returns the JSON schema that describes the tool's input. + fn input_schema(&self, _format: LanguageModelToolSchemaFormat) -> Schema { + schemars::schema_for!(Self::Input) + } + + /// Runs the tool with the provided input. + fn run(self: Arc, input: Self::Input, cx: &mut App) -> Task>; + + fn erase(self) -> Arc { + Arc::new(Erased(Arc::new(self))) + } +} + +pub struct Erased(T); + +pub trait AnyAgentTool { + fn name(&self) -> SharedString; + fn description(&self, cx: &mut App) -> SharedString; + fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result; + fn run(self: Arc, input: serde_json::Value, cx: &mut App) -> Task>; +} + +impl AnyAgentTool for Erased> +where + T: AgentTool, +{ + fn name(&self) -> SharedString { + self.0.name() + } + + fn description(&self, cx: &mut App) -> SharedString { + self.0.description(cx) + } + + fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { + Ok(serde_json::to_value(self.0.input_schema(format))?) + } + + fn run(self: Arc, input: serde_json::Value, cx: &mut App) -> Task> { + let parsed_input: Result = serde_json::from_value(input).map_err(Into::into); + match parsed_input { + Ok(input) => self.0.clone().run(input, cx), + Err(error) => Task::ready(Err(anyhow!(error))), + } + } +} + +fn to_acp_stop_reason(reason: StopReason) -> Option { + match reason { + StopReason::EndTurn => Some(acp::StopReason::EndTurn), + StopReason::MaxTokens => Some(acp::StopReason::MaxTokens), + StopReason::Refusal => Some(acp::StopReason::Refusal), + StopReason::ToolUse => None, + } +} + +fn to_acp_tool_call_update(tool_result: &LanguageModelToolResult) -> acp::ToolCallUpdate { + let status = if tool_result.is_error { + acp::ToolCallStatus::Failed + } else { + acp::ToolCallStatus::Completed + }; + let content = match &tool_result.content { + LanguageModelToolResultContent::Text(text) => text.to_string().into(), + LanguageModelToolResultContent::Image(LanguageModelImage { source, .. }) => { + acp::ToolCallContent::Content { + content: acp::ContentBlock::Image(acp::ImageContent { + annotations: None, + data: source.to_string(), + mime_type: ImageFormat::Png.mime_type().to_string(), + }), + } + } + }; + acp::ToolCallUpdate { + id: acp::ToolCallId(tool_result.tool_use_id.to_string().into()), + fields: acp::ToolCallUpdateFields { + status: Some(status), + content: Some(vec![content]), + ..Default::default() + }, + } +} diff --git a/crates/agent2/src/tools.rs b/crates/agent2/src/tools.rs new file mode 100644 index 0000000000..cf3162abfa --- /dev/null +++ b/crates/agent2/src/tools.rs @@ -0,0 +1 @@ +mod glob; diff --git a/crates/agent2/src/tools/glob.rs b/crates/agent2/src/tools/glob.rs new file mode 100644 index 0000000000..9434311aaf --- /dev/null +++ b/crates/agent2/src/tools/glob.rs @@ -0,0 +1,76 @@ +use anyhow::{anyhow, Result}; +use gpui::{App, AppContext, Entity, SharedString, Task}; +use project::Project; +use schemars::JsonSchema; +use serde::Deserialize; +use std::{path::PathBuf, sync::Arc}; +use util::paths::PathMatcher; +use worktree::Snapshot as WorktreeSnapshot; + +use crate::{ + templates::{GlobTemplate, Template, Templates}, + thread::AgentTool, +}; + +// Description is dynamic, see `fn description` below +#[derive(Deserialize, JsonSchema)] +struct GlobInput { + /// A POSIX glob pattern + glob: SharedString, +} + +struct GlobTool { + project: Entity, + templates: Arc, +} + +impl AgentTool for GlobTool { + type Input = GlobInput; + + fn name(&self) -> SharedString { + "glob".into() + } + + fn description(&self, cx: &mut App) -> SharedString { + let project_roots = self + .project + .read(cx) + .worktrees(cx) + .map(|worktree| worktree.read(cx).root_name().into()) + .collect::>() + .join("\n"); + + GlobTemplate { project_roots } + .render(&self.templates) + .expect("template failed to render") + .into() + } + + fn run(self: Arc, input: Self::Input, cx: &mut App) -> Task> { + let path_matcher = match PathMatcher::new([&input.glob]) { + Ok(matcher) => matcher, + Err(error) => return Task::ready(Err(anyhow!(error))), + }; + + let snapshots: Vec = self + .project + .read(cx) + .worktrees(cx) + .map(|worktree| worktree.read(cx).snapshot()) + .collect(); + + cx.background_spawn(async move { + let paths = snapshots.iter().flat_map(|snapshot| { + let root_name = PathBuf::from(snapshot.root_name()); + snapshot + .entries(false, 0) + .map(move |entry| root_name.join(&entry.path)) + .filter(|path| path_matcher.is_match(&path)) + }); + let output = paths + .map(|path| format!("{}\n", path.display())) + .collect::(); + Ok(output) + }) + } +} diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index fda28fa176..c0b64fcc41 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -464,7 +464,11 @@ impl AgentConnection for AcpConnection { }) } - fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task> { + fn prompt( + &self, + params: acp::PromptRequest, + cx: &mut App, + ) -> Task> { let chunks = params .prompt .into_iter() @@ -484,7 +488,9 @@ impl AgentConnection for AcpConnection { .request_any(acp_old::SendUserMessageParams { chunks }.into_any()); cx.foreground_executor().spawn(async move { task.await?; - anyhow::Ok(()) + anyhow::Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) }) } diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index cea7d7c1da..178796816a 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -169,11 +169,15 @@ impl AgentConnection for AcpConnection { }) } - fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task> { + fn prompt( + &self, + params: acp::PromptRequest, + cx: &mut App, + ) -> Task> { let conn = self.connection.clone(); cx.foreground_executor().spawn(async move { - conn.prompt(params).await?; - Ok(()) + let response = conn.prompt(params).await?; + Ok(response) }) } diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 216624a932..3c12263f48 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -200,7 +200,11 @@ impl AgentConnection for ClaudeAgentConnection { Task::ready(Err(anyhow!("Authentication not supported"))) } - fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task> { + fn prompt( + &self, + params: acp::PromptRequest, + cx: &mut App, + ) -> Task> { let sessions = self.sessions.borrow(); let Some(session) = sessions.get(¶ms.session_id) else { return Task::ready(Err(anyhow!( @@ -244,10 +248,7 @@ impl AgentConnection for ClaudeAgentConnection { return Task::ready(Err(anyhow!(err))); } - cx.foreground_executor().spawn(async move { - rx.await??; - Ok(()) - }) + cx.foreground_executor().spawn(async move { rx.await? }) } fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) { @@ -261,6 +262,14 @@ impl AgentConnection for ClaudeAgentConnection { .outgoing_tx .unbounded_send(SdkMessage::new_interrupt_message()) .log_err(); + + if let Some(end_turn_tx) = session.end_turn_tx.borrow_mut().take() { + end_turn_tx + .send(Ok(acp::PromptResponse { + stop_reason: acp::StopReason::Cancelled, + })) + .ok(); + } } } @@ -322,7 +331,7 @@ fn spawn_claude( struct ClaudeAgentSession { outgoing_tx: UnboundedSender, - end_turn_tx: Rc>>>>, + end_turn_tx: Rc>>>>, _mcp_server: Option, _handler_task: Task<()>, } @@ -331,7 +340,7 @@ impl ClaudeAgentSession { async fn handle_message( mut thread_rx: watch::Receiver>, message: SdkMessage, - end_turn_tx: Rc>>>>, + end_turn_tx: Rc>>>>, cx: &mut AsyncApp, ) { match message { @@ -436,7 +445,7 @@ impl ClaudeAgentSession { .. } => { if let Some(end_turn_tx) = end_turn_tx.borrow_mut().take() { - if is_error { + if is_error || subtype == ResultErrorType::ErrorDuringExecution { end_turn_tx .send(Err(anyhow!( "Error: {}", @@ -444,7 +453,14 @@ impl ClaudeAgentSession { ))) .ok(); } else { - end_turn_tx.send(Ok(())).ok(); + let stop_reason = match subtype { + ResultErrorType::Success => acp::StopReason::EndTurn, + ResultErrorType::ErrorMaxTurns => acp::StopReason::MaxTurnRequests, + ResultErrorType::ErrorDuringExecution => unreachable!(), + }; + end_turn_tx + .send(Ok(acp::PromptResponse { stop_reason })) + .ok(); } } } @@ -669,7 +685,7 @@ struct ControlResponse { subtype: ResultErrorType, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "snake_case")] enum ResultErrorType { Success, diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 95fd2b1757..c145df0eae 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -19,6 +19,7 @@ test-support = ["gpui/test-support", "language/test-support"] acp_thread.workspace = true agent-client-protocol.workspace = true agent.workspace = true +agent2.workspace = true agent_servers.workspace = true agent_settings.workspace = true ai_onboarding.workspace = true diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index f7c359fe99..43587e7490 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -3006,7 +3006,11 @@ mod tests { unimplemented!() } - fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task> { + fn prompt( + &self, + params: acp::PromptRequest, + cx: &mut App, + ) -> Task> { let sessions = self.sessions.lock(); let thread = sessions.get(¶ms.session_id).unwrap(); let mut tasks = vec![]; @@ -3040,7 +3044,9 @@ mod tests { } cx.spawn(async move |_| { try_join_all(tasks).await?; - Ok(()) + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) }) } @@ -3084,7 +3090,11 @@ mod tests { unimplemented!() } - fn prompt(&self, _params: acp::PromptRequest, _cx: &mut App) -> Task> { + fn prompt( + &self, + _params: acp::PromptRequest, + _cx: &mut App, + ) -> Task> { Task::ready(Err(anyhow::anyhow!("Error prompting"))) } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 0f2d431bc1..8f5fff5da3 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1981,6 +1981,22 @@ impl AgentPanel { ); }), ) + .item( + ContextMenuEntry::new("New Native Agent Thread") + .icon(IconName::ZedAssistant) + .icon_color(Color::Muted) + .handler(move |window, cx| { + window.dispatch_action( + NewExternalAgentThread { + agent: Some( + crate::ExternalAgent::NativeAgent, + ), + } + .boxed_clone(), + cx, + ); + }), + ) }); menu })) @@ -2643,6 +2659,31 @@ impl AgentPanel { ) }, ), + ) + .child( + NewThreadButton::new( + "new-native-agent-thread-btn", + "New Native Agent Thread", + IconName::ZedAssistant, + ) + // .keybinding(KeyBinding::for_action_in( + // &OpenHistory, + // &self.focus_handle(cx), + // window, + // cx, + // )) + .on_click( + |window, cx| { + window.dispatch_action( + Box::new(NewExternalAgentThread { + agent: Some( + crate::ExternalAgent::NativeAgent, + ), + }), + cx, + ) + }, + ), ), ) }), diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 30faf5ef2e..fceb8f4c45 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -151,6 +151,7 @@ enum ExternalAgent { #[default] Gemini, ClaudeCode, + NativeAgent, } impl ExternalAgent { @@ -158,6 +159,7 @@ impl ExternalAgent { match self { ExternalAgent::Gemini => Rc::new(agent_servers::Gemini), ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode), + ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer), } } } diff --git a/crates/assistant_context/src/assistant_context_tests.rs b/crates/assistant_context/src/assistant_context_tests.rs index f139d525d3..efcad8ed96 100644 --- a/crates/assistant_context/src/assistant_context_tests.rs +++ b/crates/assistant_context/src/assistant_context_tests.rs @@ -1210,8 +1210,8 @@ async fn test_summarization(cx: &mut TestAppContext) { }); cx.run_until_parked(); - fake_model.stream_last_completion_response("Brief"); - fake_model.stream_last_completion_response(" Introduction"); + fake_model.send_last_completion_stream_text_chunk("Brief"); + fake_model.send_last_completion_stream_text_chunk(" Introduction"); fake_model.end_last_completion_stream(); cx.run_until_parked(); @@ -1274,7 +1274,7 @@ async fn test_thread_summary_error_retry(cx: &mut TestAppContext) { }); cx.run_until_parked(); - fake_model.stream_last_completion_response("A successful summary"); + fake_model.send_last_completion_stream_text_chunk("A successful summary"); fake_model.end_last_completion_stream(); cx.run_until_parked(); @@ -1356,7 +1356,7 @@ fn setup_context_editor_with_fake_model( fn simulate_successful_response(fake_model: &FakeLanguageModel, cx: &mut TestAppContext) { cx.run_until_parked(); - fake_model.stream_last_completion_response("Assistant response"); + fake_model.send_last_completion_stream_text_chunk("Assistant response"); fake_model.end_last_completion_stream(); cx.run_until_parked(); } diff --git a/crates/assistant_tools/src/edit_agent.rs b/crates/assistant_tools/src/edit_agent.rs index fed79434bb..715d106a26 100644 --- a/crates/assistant_tools/src/edit_agent.rs +++ b/crates/assistant_tools/src/edit_agent.rs @@ -962,7 +962,7 @@ mod tests { ); cx.run_until_parked(); - model.stream_last_completion_response("a"); + model.send_last_completion_stream_text_chunk("a"); cx.run_until_parked(); assert_eq!(drain_events(&mut events), vec![]); assert_eq!( @@ -974,7 +974,7 @@ mod tests { None ); - model.stream_last_completion_response("bc"); + model.send_last_completion_stream_text_chunk("bc"); cx.run_until_parked(); assert_eq!( drain_events(&mut events), @@ -996,7 +996,7 @@ mod tests { }) ); - model.stream_last_completion_response("abX"); + model.send_last_completion_stream_text_chunk("abX"); cx.run_until_parked(); assert_eq!(drain_events(&mut events), [EditAgentOutputEvent::Edited]); assert_eq!( @@ -1011,7 +1011,7 @@ mod tests { }) ); - model.stream_last_completion_response("cY"); + model.send_last_completion_stream_text_chunk("cY"); cx.run_until_parked(); assert_eq!(drain_events(&mut events), [EditAgentOutputEvent::Edited]); assert_eq!( @@ -1026,8 +1026,8 @@ mod tests { }) ); - model.stream_last_completion_response(""); - model.stream_last_completion_response("hall"); + model.send_last_completion_stream_text_chunk(""); + model.send_last_completion_stream_text_chunk("hall"); cx.run_until_parked(); assert_eq!(drain_events(&mut events), vec![]); assert_eq!( @@ -1042,8 +1042,8 @@ mod tests { }) ); - model.stream_last_completion_response("ucinated old"); - model.stream_last_completion_response(""); + model.send_last_completion_stream_text_chunk("ucinated old"); + model.send_last_completion_stream_text_chunk(""); cx.run_until_parked(); assert_eq!( drain_events(&mut events), @@ -1061,8 +1061,8 @@ mod tests { }) ); - model.stream_last_completion_response("hallucinated new"); + model.send_last_completion_stream_text_chunk("hallucinated new"); cx.run_until_parked(); assert_eq!(drain_events(&mut events), vec![]); assert_eq!( @@ -1077,7 +1077,7 @@ mod tests { }) ); - model.stream_last_completion_response("\nghi\nj"); + model.send_last_completion_stream_text_chunk("\nghi\nj"); cx.run_until_parked(); assert_eq!( drain_events(&mut events), @@ -1099,8 +1099,8 @@ mod tests { }) ); - model.stream_last_completion_response("kl"); - model.stream_last_completion_response(""); + model.send_last_completion_stream_text_chunk("kl"); + model.send_last_completion_stream_text_chunk(""); cx.run_until_parked(); assert_eq!( drain_events(&mut events), @@ -1122,7 +1122,7 @@ mod tests { }) ); - model.stream_last_completion_response("GHI"); + model.send_last_completion_stream_text_chunk("GHI"); cx.run_until_parked(); assert_eq!( drain_events(&mut events), @@ -1367,7 +1367,9 @@ mod tests { cx.background_spawn(async move { for chunk in chunks { executor.simulate_random_delay().await; - model.as_fake().stream_last_completion_response(chunk); + model + .as_fake() + .send_last_completion_stream_text_chunk(chunk); } model.as_fake().end_last_completion_stream(); }) diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 1c41b26092..dce9f49abd 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -1577,7 +1577,7 @@ mod tests { // Stream the unformatted content cx.executor().run_until_parked(); - model.stream_last_completion_response(UNFORMATTED_CONTENT.to_string()); + model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); model.end_last_completion_stream(); edit_task.await @@ -1641,7 +1641,7 @@ mod tests { // Stream the unformatted content cx.executor().run_until_parked(); - model.stream_last_completion_response(UNFORMATTED_CONTENT.to_string()); + model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); model.end_last_completion_stream(); edit_task.await @@ -1720,7 +1720,9 @@ mod tests { // Stream the content with trailing whitespace cx.executor().run_until_parked(); - model.stream_last_completion_response(CONTENT_WITH_TRAILING_WHITESPACE.to_string()); + model.send_last_completion_stream_text_chunk( + CONTENT_WITH_TRAILING_WHITESPACE.to_string(), + ); model.end_last_completion_stream(); edit_task.await @@ -1777,7 +1779,9 @@ mod tests { // Stream the content with trailing whitespace cx.executor().run_until_parked(); - model.stream_last_completion_response(CONTENT_WITH_TRAILING_WHITESPACE.to_string()); + model.send_last_completion_stream_text_chunk( + CONTENT_WITH_TRAILING_WHITESPACE.to_string(), + ); model.end_last_completion_stream(); edit_task.await diff --git a/crates/language_model/src/fake_provider.rs b/crates/language_model/src/fake_provider.rs index d54db7554a..a9c7d5c034 100644 --- a/crates/language_model/src/fake_provider.rs +++ b/crates/language_model/src/fake_provider.rs @@ -92,7 +92,12 @@ pub struct ToolUseRequest { pub struct FakeLanguageModel { provider_id: LanguageModelProviderId, provider_name: LanguageModelProviderName, - current_completion_txs: Mutex)>>, + current_completion_txs: Mutex< + Vec<( + LanguageModelRequest, + mpsc::UnboundedSender, + )>, + >, } impl Default for FakeLanguageModel { @@ -118,10 +123,21 @@ impl FakeLanguageModel { self.current_completion_txs.lock().len() } - pub fn stream_completion_response( + pub fn send_completion_stream_text_chunk( &self, request: &LanguageModelRequest, chunk: impl Into, + ) { + self.send_completion_stream_event( + request, + LanguageModelCompletionEvent::Text(chunk.into()), + ); + } + + pub fn send_completion_stream_event( + &self, + request: &LanguageModelRequest, + event: impl Into, ) { let current_completion_txs = self.current_completion_txs.lock(); let tx = current_completion_txs @@ -129,7 +145,7 @@ impl FakeLanguageModel { .find(|(req, _)| req == request) .map(|(_, tx)| tx) .unwrap(); - tx.unbounded_send(chunk.into()).unwrap(); + tx.unbounded_send(event.into()).unwrap(); } pub fn end_completion_stream(&self, request: &LanguageModelRequest) { @@ -138,8 +154,15 @@ impl FakeLanguageModel { .retain(|(req, _)| req != request); } - pub fn stream_last_completion_response(&self, chunk: impl Into) { - self.stream_completion_response(self.pending_completions().last().unwrap(), chunk); + pub fn send_last_completion_stream_text_chunk(&self, chunk: impl Into) { + self.send_completion_stream_text_chunk(self.pending_completions().last().unwrap(), chunk); + } + + pub fn send_last_completion_stream_event( + &self, + event: impl Into, + ) { + self.send_completion_stream_event(self.pending_completions().last().unwrap(), event); } pub fn end_last_completion_stream(&self) { @@ -201,12 +224,7 @@ impl LanguageModel for FakeLanguageModel { > { let (tx, rx) = mpsc::unbounded(); self.current_completion_txs.lock().push((request, tx)); - async move { - Ok(rx - .map(|text| Ok(LanguageModelCompletionEvent::Text(text))) - .boxed()) - } - .boxed() + async move { Ok(rx.map(Ok).boxed()) }.boxed() } fn as_fake(&self) -> &Self { From 0302f6356e14c03c87d812398af6b4ad4594fb74 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 6 Aug 2025 11:08:32 +0200 Subject: [PATCH 112/693] Ignore metadata file in `RustLspAdapter::get_cached_server_binary` (#35708) Follows https://github.com/zed-industries/zed/pull/35642 Release Notes: - Fixed accidentally picking a non executable as a rust-analyzer server when downloading fails --- crates/languages/src/rust.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 084331b2cf..6545bf64a2 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -1039,7 +1039,11 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option Date: Wed, 6 Aug 2025 12:04:07 +0200 Subject: [PATCH 113/693] Fetch models right after signing in (#35711) This uses the `current_user` watch in the `UserStore` instead of looping every 100ms in order to detect if the user had signed in. We are changing this because we noticed it was causing the deterministic executor in tests to never detect a "parking with nothing left to run" situation. This seems better in production as well, especially for users who never sign in. /cc @maxdeviant Release Notes: - N/A Co-authored-by: Ben Brandt --- crates/language_models/src/provider/cloud.rs | 21 +++++++------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 134b2bef6c..40dd120761 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -136,6 +136,7 @@ impl State { cx: &mut Context, ) -> Self { let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx); + let mut current_user = user_store.read(cx).watch_current_user(); Self { client: client.clone(), llm_api_token: LlmApiToken::default(), @@ -151,22 +152,14 @@ impl State { let (client, llm_api_token) = this .read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?; - loop { - let is_authenticated = user_store - .read_with(cx, |user_store, _cx| user_store.current_user().is_some())?; - if is_authenticated { - break; - } - - cx.background_executor() - .timer(Duration::from_millis(100)) - .await; + while current_user.borrow().is_none() { + current_user.next().await; } - let response = Self::fetch_models(client, llm_api_token).await?; - this.update(cx, |this, cx| { - this.update_models(response, cx); - }) + let response = + Self::fetch_models(client.clone(), llm_api_token.clone()).await?; + this.update(cx, |this, cx| this.update_models(response, cx))?; + anyhow::Ok(()) }) .await .context("failed to fetch Zed models") From ecd182c52f11721e04f11e906f6572a68660cf61 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Aug 2025 12:20:40 +0200 Subject: [PATCH 114/693] Drop native agent session when `AcpThread` gets released (#35713) Release Notes: - N/A Co-authored-by: Ben Brandt --- Cargo.lock | 1 + crates/agent2/Cargo.toml | 1 + crates/agent2/src/agent.rs | 12 ++++-- crates/agent2/src/tests/mod.rs | 75 ++++++++++++++++++++++------------ 4 files changed, 58 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 76f45bd28e..e034212748 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,6 +160,7 @@ dependencies = [ "agent_servers", "anyhow", "client", + "clock", "cloud_llm_client", "collections", "ctor", diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 70779bba74..74aa2993dd 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -42,6 +42,7 @@ workspace-hack.workspace = true [dev-dependencies] ctor.workspace = true client = { workspace = true, "features" = ["test-support"] } +clock = { workspace = true, "features" = ["test-support"] } env_logger.workspace = true fs = { workspace = true, "features" = ["test-support"] } gpui = { workspace = true, "features" = ["test-support"] } diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index bd4f82200b..305a31fc98 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -2,7 +2,7 @@ use acp_thread::ModelSelector; use agent_client_protocol as acp; use anyhow::{anyhow, Result}; use futures::StreamExt; -use gpui::{App, AppContext, AsyncApp, Entity, Task}; +use gpui::{App, AppContext, AsyncApp, Entity, Subscription, Task, WeakEntity}; use language_model::{LanguageModel, LanguageModelRegistry}; use project::Project; use std::collections::HashMap; @@ -17,7 +17,8 @@ struct Session { /// The internal thread that processes messages thread: Entity, /// The ACP thread that handles protocol communication - acp_thread: Entity, + acp_thread: WeakEntity, + _subscription: Subscription, } pub struct NativeAgent { @@ -162,12 +163,15 @@ impl acp_thread::AgentConnection for NativeAgentConnection { })?; // Store the session - agent.update(cx, |agent, _cx| { + agent.update(cx, |agent, cx| { agent.sessions.insert( session_id, Session { thread, - acp_thread: acp_thread.clone(), + acp_thread: acp_thread.downgrade(), + _subscription: cx.observe_release(&acp_thread, |this, acp_thread, _cx| { + this.sessions.remove(acp_thread.session_id()); + }) }, ); })?; diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 2e2b25a119..330d04b60c 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1,10 +1,10 @@ use super::*; use crate::templates::Templates; -use acp_thread::AgentConnection as _; +use acp_thread::AgentConnection; use agent_client_protocol as acp; use client::{Client, UserStore}; use fs::FakeFs; -use gpui::{AppContext, Entity, Task, TestAppContext}; +use gpui::{http_client::FakeHttpClient, AppContext, Entity, Task, TestAppContext}; use indoc::indoc; use language_model::{ fake_provider::FakeLanguageModel, LanguageModel, LanguageModelCompletionError, @@ -322,31 +322,26 @@ async fn test_refusal(cx: &mut TestAppContext) { }); } -#[ignore = "temporarily disabled until it can be run on CI"] #[gpui::test] async fn test_agent_connection(cx: &mut TestAppContext) { - cx.executor().allow_parking(); cx.update(settings::init); let templates = Templates::new(); // Initialize language model system with test provider cx.update(|cx| { gpui_tokio::init(cx); - let http_client = ReqwestClient::user_agent("agent tests").unwrap(); - cx.set_http_client(Arc::new(http_client)); - client::init_settings(cx); - let client = Client::production(cx); + + let http_client = FakeHttpClient::with_404_response(); + let clock = Arc::new(clock::FakeSystemClock::new()); + let client = Client::new(clock, http_client, cx); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); language_model::init(client.clone(), cx); language_models::init(user_store.clone(), client.clone(), cx); - - // Initialize project settings Project::init_settings(cx); - - // Use test registry with fake provider LanguageModelRegistry::test(cx); }); + cx.executor().forbid_parking(); // Create agent and connection let agent = cx.new(|_| NativeAgent::new(templates.clone())); @@ -390,34 +385,60 @@ async fn test_agent_connection(cx: &mut TestAppContext) { let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone()); // Test selected_model returns the default - let selected = cx + let model = cx .update(|cx| { let mut async_cx = cx.to_async(); selector.selected_model(&session_id, &mut async_cx) }) .await .expect("selected_model should succeed"); - assert_eq!(selected.id().0, "fake", "should return default model"); + let model = model.as_fake(); + assert_eq!(model.id().0, "fake", "should return default model"); - // The thread was created via prompt with the default model - // We can verify it through selected_model + let request = acp_thread.update(cx, |thread, cx| thread.send(vec!["abc".into()], cx)); + cx.run_until_parked(); + model.send_last_completion_stream_text_chunk("def"); + cx.run_until_parked(); + acp_thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc! {" + ## User - // Test prompt uses the selected model - let prompt_request = acp::PromptRequest { - session_id: session_id.clone(), - prompt: vec![acp::ContentBlock::Text(acp::TextContent { - text: "Test prompt".into(), - annotations: None, - })], - }; + abc - let request = cx.update(|cx| connection.prompt(prompt_request, cx)); - let request = cx.background_spawn(request); - smol::Timer::after(Duration::from_millis(100)).await; + ## Assistant + + def + + "} + ) + }); // Test cancel cx.update(|cx| connection.cancel(&session_id, cx)); request.await.expect("prompt should fail gracefully"); + + // Ensure that dropping the ACP thread causes the native thread to be + // dropped as well. + cx.update(|_| drop(acp_thread)); + let result = cx + .update(|cx| { + connection.prompt( + acp::PromptRequest { + session_id: session_id.clone(), + prompt: vec!["ghi".into()], + }, + cx, + ) + }) + .await; + assert_eq!( + result.as_ref().unwrap_err().to_string(), + "Session not found", + "unexpected result: {:?}", + result + ); } /// Filters out the stop events for asserting against in tests From 3bbd32b70e82bd1a641c160efae53b9e5893c48d Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 6 Aug 2025 09:23:47 -0300 Subject: [PATCH 115/693] Support CC `migrate-installer` path (#35717) If we can't find CC in the PATH, we'll now fall back to a known local install path. Release Notes: - N/A --- crates/agent_servers/src/agent_servers.rs | 20 ++++++++++++++++---- crates/agent_servers/src/claude.rs | 11 +++++++++-- crates/agent_servers/src/gemini.rs | 2 +- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index ec69290206..b3b8a33170 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -89,6 +89,7 @@ impl AgentServerCommand { pub(crate) async fn resolve( path_bin_name: &'static str, extra_args: &[&'static str], + fallback_path: Option<&Path>, settings: Option, project: &Entity, cx: &mut AsyncApp, @@ -105,13 +106,24 @@ impl AgentServerCommand { env: agent_settings.command.env, }); } else { - find_bin_in_path(path_bin_name, project, cx) - .await - .map(|path| Self { + match find_bin_in_path(path_bin_name, project, cx).await { + Some(path) => Some(Self { path, args: extra_args.iter().map(|arg| arg.to_string()).collect(), env: None, - }) + }), + None => fallback_path.and_then(|path| { + if path.exists() { + Some(Self { + path: path.to_path_buf(), + args: extra_args.iter().map(|arg| arg.to_string()).collect(), + env: None, + }) + } else { + None + } + }), + } } } } diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 3c12263f48..dc8c522a5b 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -101,8 +101,15 @@ impl AgentConnection for ClaudeAgentConnection { settings.get::(None).claude.clone() })?; - let Some(command) = - AgentServerCommand::resolve("claude", &[], settings, &project, cx).await + let Some(command) = AgentServerCommand::resolve( + "claude", + &[], + Some(&util::paths::home_dir().join(".claude/local/claude")), + settings, + &project, + cx, + ) + .await else { anyhow::bail!("Failed to find claude binary"); }; diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 4450f4e216..ad883f6da8 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -48,7 +48,7 @@ impl AgentServer for Gemini { })?; let Some(command) = - AgentServerCommand::resolve("gemini", &[ACP_ARG], settings, &project, cx).await + AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await else { anyhow::bail!("Failed to find gemini binary"); }; From 7e790f52c8184431aeda99f6225ee6ff1970b6db Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Aug 2025 09:11:46 -0400 Subject: [PATCH 116/693] Bump Zed to v0.200 (#35719) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🎉 Release Notes: -N/A --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e034212748..1eb5669fa2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20418,7 +20418,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.199.0" +version = "0.200.0" dependencies = [ "activity_indicator", "agent", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 5bd6d981fa..5997e43864 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition.workspace = true name = "zed" -version = "0.199.0" +version = "0.200.0" publish.workspace = true license = "GPL-3.0-or-later" authors = ["Zed Team "] From 22fa41e9c008b4a74d2cd65e186c2cb934cf3950 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 6 Aug 2025 10:20:53 -0300 Subject: [PATCH 117/693] Handle CC thinking (#35722) Release Notes: - N/A --- crates/agent_servers/src/claude.rs | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index dc8c522a5b..913d64aa7b 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -380,6 +380,24 @@ impl ClaudeAgentSession { }) .log_err(); } + ContentChunk::Thinking { thinking } => { + thread + .update(cx, |thread, cx| { + thread.push_assistant_content_block(thinking.into(), true, cx) + }) + .log_err(); + } + ContentChunk::RedactedThinking => { + thread + .update(cx, |thread, cx| { + thread.push_assistant_content_block( + "[REDACTED]".into(), + true, + cx, + ) + }) + .log_err(); + } ContentChunk::ToolUse { id, name, input } => { let claude_tool = ClaudeTool::infer(&name, input); @@ -429,8 +447,6 @@ impl ClaudeAgentSession { } ContentChunk::Image | ContentChunk::Document - | ContentChunk::Thinking - | ContentChunk::RedactedThinking | ContentChunk::WebSearchToolResult => { thread .update(cx, |thread, cx| { @@ -580,11 +596,13 @@ enum ContentChunk { content: Content, tool_use_id: String, }, + Thinking { + thinking: String, + }, + RedactedThinking, // TODO Image, Document, - Thinking, - RedactedThinking, WebSearchToolResult, #[serde(untagged)] UntaggedText(String), @@ -594,12 +612,12 @@ impl Display for ContentChunk { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ContentChunk::Text { text } => write!(f, "{}", text), + ContentChunk::Thinking { thinking } => write!(f, "Thinking: {}", thinking), + ContentChunk::RedactedThinking => write!(f, "Thinking: [REDACTED]"), ContentChunk::UntaggedText(text) => write!(f, "{}", text), ContentChunk::ToolResult { content, .. } => write!(f, "{}", content), ContentChunk::Image | ContentChunk::Document - | ContentChunk::Thinking - | ContentChunk::RedactedThinking | ContentChunk::ToolUse { .. } | ContentChunk::WebSearchToolResult => { write!(f, "\n{:?}\n", &self) From 69dc8708282383903b30370a34c408f40a6f76a0 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 6 Aug 2025 10:27:11 -0300 Subject: [PATCH 118/693] Fix CC todo tool parsing (#35721) It looks like the TODO tool call no longer requires a priority. Release Notes: - N/A --- crates/agent_servers/src/claude.rs | 64 ++++++++++++++++++++++++ crates/agent_servers/src/claude/tools.rs | 33 ++++-------- 2 files changed, 73 insertions(+), 24 deletions(-) diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 913d64aa7b..59e2e87433 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -764,6 +764,8 @@ enum PermissionMode { #[cfg(test)] pub(crate) mod tests { use super::*; + use crate::e2e_tests; + use gpui::TestAppContext; use serde_json::json; crate::common_e2e_tests!(ClaudeCode, allow_option_id = "allow"); @@ -776,6 +778,68 @@ pub(crate) mod tests { } } + #[gpui::test] + #[cfg_attr(not(feature = "e2e"), ignore)] + async fn test_todo_plan(cx: &mut TestAppContext) { + let fs = e2e_tests::init_test(cx).await; + let project = Project::test(fs, [], cx).await; + let thread = + e2e_tests::new_test_thread(ClaudeCode, project.clone(), "/private/tmp", cx).await; + + thread + .update(cx, |thread, cx| { + thread.send_raw( + "Create a todo plan for initializing a new React app. I'll follow it myself, do not execute on it.", + cx, + ) + }) + .await + .unwrap(); + + let mut entries_len = 0; + + thread.read_with(cx, |thread, _| { + entries_len = thread.plan().entries.len(); + assert!(thread.plan().entries.len() > 0, "Empty plan"); + }); + + thread + .update(cx, |thread, cx| { + thread.send_raw( + "Mark the first entry status as in progress without acting on it.", + cx, + ) + }) + .await + .unwrap(); + + thread.read_with(cx, |thread, _| { + assert!(matches!( + thread.plan().entries[0].status, + acp::PlanEntryStatus::InProgress + )); + assert_eq!(thread.plan().entries.len(), entries_len); + }); + + thread + .update(cx, |thread, cx| { + thread.send_raw( + "Now mark the first entry as completed without acting on it.", + cx, + ) + }) + .await + .unwrap(); + + thread.read_with(cx, |thread, _| { + assert!(matches!( + thread.plan().entries[0].status, + acp::PlanEntryStatus::Completed + )); + assert_eq!(thread.plan().entries.len(), entries_len); + }); + } + #[test] fn test_deserialize_content_untagged_text() { let json = json!("Hello, world!"); diff --git a/crates/agent_servers/src/claude/tools.rs b/crates/agent_servers/src/claude/tools.rs index e7d33e5298..85b9a13642 100644 --- a/crates/agent_servers/src/claude/tools.rs +++ b/crates/agent_servers/src/claude/tools.rs @@ -143,25 +143,6 @@ impl ClaudeTool { Self::Grep(Some(params)) => vec![format!("`{params}`").into()], Self::WebFetch(Some(params)) => vec![params.prompt.clone().into()], Self::WebSearch(Some(params)) => vec![params.to_string().into()], - Self::TodoWrite(Some(params)) => vec![ - params - .todos - .iter() - .map(|todo| { - format!( - "- {} {}: {}", - match todo.status { - TodoStatus::Completed => "✅", - TodoStatus::InProgress => "🚧", - TodoStatus::Pending => "⬜", - }, - todo.priority, - todo.content - ) - }) - .join("\n") - .into(), - ], Self::ExitPlanMode(Some(params)) => vec![params.plan.clone().into()], Self::Edit(Some(params)) => vec![acp::ToolCallContent::Diff { diff: acp::Diff { @@ -193,6 +174,10 @@ impl ClaudeTool { }) .unwrap_or_default() } + Self::TodoWrite(Some(_)) => { + // These are mapped to plan updates later + vec![] + } Self::Task(None) | Self::NotebookRead(None) | Self::NotebookEdit(None) @@ -488,10 +473,11 @@ impl std::fmt::Display for GrepToolParams { } } -#[derive(Deserialize, Serialize, JsonSchema, strum::Display, Debug)] +#[derive(Default, Deserialize, Serialize, JsonSchema, strum::Display, Debug)] #[serde(rename_all = "snake_case")] pub enum TodoPriority { High, + #[default] Medium, Low, } @@ -526,14 +512,13 @@ impl Into for TodoStatus { #[derive(Deserialize, Serialize, JsonSchema, Debug)] pub struct Todo { - /// Unique identifier - pub id: String, /// Task description pub content: String, - /// Priority level of the todo - pub priority: TodoPriority, /// Current status of the todo pub status: TodoStatus, + /// Priority level of the todo + #[serde(default)] + pub priority: TodoPriority, } impl Into for Todo { From 334bdd0efcf6f4a3f3cf87bedca4550e8e8e603e Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 6 Aug 2025 10:39:55 -0300 Subject: [PATCH 119/693] Fix acp thread entry width (#35723) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 43587e7490..4566e9af52 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -866,6 +866,7 @@ impl AcpThreadView { let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); if index == total_entries - 1 && !is_generating { v_flex() + .w_full() .child(primary) .child(self.render_thread_controls(cx)) .into_any_element() From 3c602fecbf22759c272896110f221ded3fdb963c Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Wed, 6 Aug 2025 09:59:13 -0400 Subject: [PATCH 120/693] docs: Cleanup tool use documentation (#35725) Remove redundant documentation about tool use. Release Notes: - N/A --- docs/src/ai/llm-providers.md | 55 +++++++++++------------------------- 1 file changed, 16 insertions(+), 39 deletions(-) diff --git a/docs/src/ai/llm-providers.md b/docs/src/ai/llm-providers.md index 04646213e6..8fdb7ea325 100644 --- a/docs/src/ai/llm-providers.md +++ b/docs/src/ai/llm-providers.md @@ -14,25 +14,25 @@ You can add your API key to a given provider either via the Agent Panel's settin Here's all the supported LLM providers for which you can use your own API keys: -| Provider | Tool Use Supported | -| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Amazon Bedrock](#amazon-bedrock) | Depends on the model | -| [Anthropic](#anthropic) | ✅ | -| [DeepSeek](#deepseek) | ✅ | -| [GitHub Copilot Chat](#github-copilot-chat) | For some models ([link](https://github.com/zed-industries/zed/blob/9e0330ba7d848755c9734bf456c716bddf0973f3/crates/language_models/src/provider/copilot_chat.rs#L189-L198)) | -| [Google AI](#google-ai) | ✅ | -| [LM Studio](#lmstudio) | ✅ | -| [Mistral](#mistral) | ✅ | -| [Ollama](#ollama) | ✅ | -| [OpenAI](#openai) | ✅ | -| [OpenAI API Compatible](#openai-api-compatible) | ✅ | -| [OpenRouter](#openrouter) | ✅ | -| [Vercel](#vercel-v0) | ✅ | -| [xAI](#xai) | ✅ | +| Provider | +| ----------------------------------------------- | +| [Amazon Bedrock](#amazon-bedrock) | +| [Anthropic](#anthropic) | +| [DeepSeek](#deepseek) | +| [GitHub Copilot Chat](#github-copilot-chat) | +| [Google AI](#google-ai) | +| [LM Studio](#lmstudio) | +| [Mistral](#mistral) | +| [Ollama](#ollama) | +| [OpenAI](#openai) | +| [OpenAI API Compatible](#openai-api-compatible) | +| [OpenRouter](#openrouter) | +| [Vercel](#vercel-v0) | +| [xAI](#xai) | ### Amazon Bedrock {#amazon-bedrock} -> ✅ Supports tool use with models that support streaming tool use. +> Supports tool use with models that support streaming tool use. > More details can be found in the [Amazon Bedrock's Tool Use documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-supported-models-features.html). To use Amazon Bedrock's models, an AWS authentication is required. @@ -107,8 +107,6 @@ For the most up-to-date supported regions and models, refer to the [Supported Mo ### Anthropic {#anthropic} -> ✅ Supports tool use - You can use Anthropic models by choosing them via the model dropdown in the Agent Panel. 1. Sign up for Anthropic and [create an API key](https://console.anthropic.com/settings/keys) @@ -165,8 +163,6 @@ You can configure a model to use [extended thinking](https://docs.anthropic.com/ ### DeepSeek {#deepseek} -> ✅ Supports tool use - 1. Visit the DeepSeek platform and [create an API key](https://platform.deepseek.com/api_keys) 2. Open the settings view (`agent: open settings`) and go to the DeepSeek section 3. Enter your DeepSeek API key @@ -208,9 +204,6 @@ You can also modify the `api_url` to use a custom endpoint if needed. ### GitHub Copilot Chat {#github-copilot-chat} -> ✅ Supports tool use in some cases. -> Visit [the Copilot Chat code](https://github.com/zed-industries/zed/blob/9e0330ba7d848755c9734bf456c716bddf0973f3/crates/language_models/src/provider/copilot_chat.rs#L189-L198) for the supported subset. - You can use GitHub Copilot Chat with the Zed agent by choosing it via the model dropdown in the Agent Panel. 1. Open the settings view (`agent: open settings`) and go to the GitHub Copilot Chat section @@ -224,8 +217,6 @@ To use Copilot Enterprise with Zed (for both agent and completions), you must co ### Google AI {#google-ai} -> ✅ Supports tool use - You can use Gemini models with the Zed agent by choosing it via the model dropdown in the Agent Panel. 1. Go to the Google AI Studio site and [create an API key](https://aistudio.google.com/app/apikey). @@ -266,8 +257,6 @@ Custom models will be listed in the model dropdown in the Agent Panel. ### LM Studio {#lmstudio} -> ✅ Supports tool use - 1. Download and install [the latest version of LM Studio](https://lmstudio.ai/download) 2. In the app press `cmd/ctrl-shift-m` and download at least one model (e.g., qwen2.5-coder-7b). Alternatively, you can get models via the LM Studio CLI: @@ -285,8 +274,6 @@ Tip: Set [LM Studio as a login item](https://lmstudio.ai/docs/advanced/headless# ### Mistral {#mistral} -> ✅ Supports tool use - 1. Visit the Mistral platform and [create an API key](https://console.mistral.ai/api-keys/) 2. Open the configuration view (`agent: open settings`) and navigate to the Mistral section 3. Enter your Mistral API key @@ -326,8 +313,6 @@ Custom models will be listed in the model dropdown in the Agent Panel. ### Ollama {#ollama} -> ✅ Supports tool use - Download and install Ollama from [ollama.com/download](https://ollama.com/download) (Linux or macOS) and ensure it's running with `ollama --version`. 1. Download one of the [available models](https://ollama.com/models), for example, for `mistral`: @@ -395,8 +380,6 @@ If the model is tagged with `vision` in the Ollama catalog, set this option and ### OpenAI {#openai} -> ✅ Supports tool use - 1. Visit the OpenAI platform and [create an API key](https://platform.openai.com/account/api-keys) 2. Make sure that your OpenAI account has credits 3. Open the settings view (`agent: open settings`) and go to the OpenAI section @@ -473,8 +456,6 @@ So, ensure you have it set in your environment variables (`OPENAI_API_KEY= ✅ Supports tool use - OpenRouter provides access to multiple AI models through a single API. It supports tool use for compatible models. 1. Visit [OpenRouter](https://openrouter.ai) and create an account @@ -531,8 +512,6 @@ Custom models will be listed in the model dropdown in the Agent Panel. ### Vercel v0 {#vercel-v0} -> ✅ Supports tool use - [Vercel v0](https://vercel.com/docs/v0/api) is an expert model for generating full-stack apps, with framework-aware completions optimized for modern stacks like Next.js and Vercel. It supports text and image inputs and provides fast streaming responses. @@ -545,8 +524,6 @@ You should then find it as `v0-1.5-md` in the model dropdown in the Agent Panel. ### xAI {#xai} -> ✅ Supports tool use - Zed has first-class support for [xAI](https://x.ai/) models. You can use your own API key to access Grok models. 1. [Create an API key in the xAI Console](https://console.x.ai/team/default/api-keys) From 33f198fef15ba17144e3971658a12d4b0ffe26e9 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 6 Aug 2025 11:01:34 -0300 Subject: [PATCH 121/693] Thread view scrollbar (#35655) This also adds a convenient `Scrollbar:auto_hide` function so that we don't have to handle that at the callsite. Release Notes: - N/A --------- Co-authored-by: David Kleingeld --- crates/agent_ui/src/acp/thread_view.rs | 50 +++++++- crates/agent_ui/src/active_thread.rs | 109 +++++------------ .../src/session/running/breakpoint_list.rs | 97 +++++---------- .../src/session/running/memory_view.rs | 103 ++++++---------- crates/ui/src/components/scrollbar.rs | 112 +++++++++++++++--- 5 files changed, 238 insertions(+), 233 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 4566e9af52..6475b7eeee 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -21,10 +21,10 @@ use editor::{ use file_icons::FileIcons; use gpui::{ Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId, - FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, PlatformDisplay, SharedString, - StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation, - UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop, linear_gradient, - list, percentage, point, prelude::*, pulsating_between, + FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, PlatformDisplay, + SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, + Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop, + linear_gradient, list, percentage, point, prelude::*, pulsating_between, }; use language::language_settings::SoftWrap; use language::{Buffer, Language}; @@ -34,7 +34,9 @@ use project::Project; use settings::Settings as _; use text::{Anchor, BufferSnapshot}; use theme::ThemeSettings; -use ui::{Disclosure, Divider, DividerColor, KeyBinding, Tooltip, prelude::*}; +use ui::{ + Disclosure, Divider, DividerColor, KeyBinding, Scrollbar, ScrollbarState, Tooltip, prelude::*, +}; use util::ResultExt; use workspace::{CollaboratorId, Workspace}; use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage}; @@ -69,6 +71,7 @@ pub struct AcpThreadView { notification_subscriptions: HashMap, Vec>, last_error: Option>, list_state: ListState, + scrollbar_state: ScrollbarState, auth_task: Option>, expanded_tool_calls: HashSet, expanded_thinking_blocks: HashSet<(usize, usize)>, @@ -187,7 +190,8 @@ impl AcpThreadView { notifications: Vec::new(), notification_subscriptions: HashMap::default(), diff_editors: Default::default(), - list_state: list_state, + list_state: list_state.clone(), + scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), last_error: None, auth_task: None, expanded_tool_calls: HashSet::default(), @@ -2479,6 +2483,39 @@ impl AcpThreadView { .child(open_as_markdown) .child(scroll_to_top) } + + fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ + div() + .id("acp-thread-scrollbar") + .occlude() + .on_mouse_move(cx.listener(|_, _, _, cx| { + cx.notify(); + cx.stop_propagation() + })) + .on_hover(|_, _, cx| { + cx.stop_propagation(); + }) + .on_any_mouse_down(|_, _, cx| { + cx.stop_propagation(); + }) + .on_mouse_up( + MouseButton::Left, + cx.listener(|_, _, _, cx| { + cx.stop_propagation(); + }), + ) + .on_scroll_wheel(cx.listener(|_, _, _, cx| { + cx.notify(); + })) + .h_full() + .absolute() + .right_1() + .top_1() + .bottom_0() + .w(px(12.)) + .cursor_default() + .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx))) + } } impl Focusable for AcpThreadView { @@ -2553,6 +2590,7 @@ impl Render for AcpThreadView { .flex_grow() .into_any(), ) + .child(self.render_vertical_scrollbar(cx)) .children(match thread_clone.read(cx).status() { ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => { None diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index ed227f22e4..c4d4e72252 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -69,8 +69,6 @@ pub struct ActiveThread { messages: Vec, list_state: ListState, scrollbar_state: ScrollbarState, - show_scrollbar: bool, - hide_scrollbar_task: Option>, rendered_messages_by_id: HashMap, rendered_tool_uses: HashMap, editing_message: Option<(MessageId, EditingMessageState)>, @@ -805,9 +803,7 @@ impl ActiveThread { expanded_thinking_segments: HashMap::default(), expanded_code_blocks: HashMap::default(), list_state: list_state.clone(), - scrollbar_state: ScrollbarState::new(list_state), - show_scrollbar: false, - hide_scrollbar_task: None, + scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), editing_message: None, last_error: None, copied_code_block_ids: HashSet::default(), @@ -3502,60 +3498,37 @@ impl ActiveThread { } } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Option> { - if !self.show_scrollbar && !self.scrollbar_state.is_dragging() { - return None; - } - - Some( - div() - .occlude() - .id("active-thread-scrollbar") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { + fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ + div() + .occlude() + .id("active-thread-scrollbar") + .on_mouse_move(cx.listener(|_, _, _, cx| { + cx.notify(); + cx.stop_propagation() + })) + .on_hover(|_, _, cx| { + cx.stop_propagation(); + }) + .on_any_mouse_down(|_, _, cx| { + cx.stop_propagation(); + }) + .on_mouse_up( + MouseButton::Left, + cx.listener(|_, _, _, cx| { cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone())), - ) - } - - fn hide_scrollbar_later(&mut self, cx: &mut Context) { - const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); - self.hide_scrollbar_task = Some(cx.spawn(async move |thread, cx| { - cx.background_executor() - .timer(SCROLLBAR_SHOW_INTERVAL) - .await; - thread - .update(cx, |thread, cx| { - if !thread.scrollbar_state.is_dragging() { - thread.show_scrollbar = false; - cx.notify(); - } - }) - .log_err(); - })) + }), + ) + .on_scroll_wheel(cx.listener(|_, _, _, cx| { + cx.notify(); + })) + .h_full() + .absolute() + .right_1() + .top_1() + .bottom_0() + .w(px(12.)) + .cursor_default() + .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx))) } pub fn is_codeblock_expanded(&self, message_id: MessageId, ix: usize) -> bool { @@ -3596,26 +3569,8 @@ impl Render for ActiveThread { .size_full() .relative() .bg(cx.theme().colors().panel_background) - .on_mouse_move(cx.listener(|this, _, _, cx| { - this.show_scrollbar = true; - this.hide_scrollbar_later(cx); - cx.notify(); - })) - .on_scroll_wheel(cx.listener(|this, _, _, cx| { - this.show_scrollbar = true; - this.hide_scrollbar_later(cx); - cx.notify(); - })) - .on_mouse_up( - MouseButton::Left, - cx.listener(|this, _, _, cx| { - this.hide_scrollbar_later(cx); - }), - ) .child(list(self.list_state.clone(), cx.processor(Self::render_message)).flex_grow()) - .when_some(self.render_vertical_scrollbar(cx), |this, scrollbar| { - this.child(scrollbar) - }) + .child(self.render_vertical_scrollbar(cx)) } } diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index 6ac4b1c878..a6defbbf35 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -29,7 +29,6 @@ use ui::{ Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex, }; -use util::ResultExt; use workspace::Workspace; use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint}; @@ -56,8 +55,6 @@ pub(crate) struct BreakpointList { scrollbar_state: ScrollbarState, breakpoints: Vec, session: Option>, - hide_scrollbar_task: Option>, - show_scrollbar: bool, focus_handle: FocusHandle, scroll_handle: UniformListScrollHandle, selected_ix: Option, @@ -103,8 +100,6 @@ impl BreakpointList { worktree_store, scrollbar_state, breakpoints: Default::default(), - hide_scrollbar_task: None, - show_scrollbar: false, workspace, session, focus_handle, @@ -565,21 +560,6 @@ impl BreakpointList { Ok(()) } - fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context) { - const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); - self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| { - cx.background_executor() - .timer(SCROLLBAR_SHOW_INTERVAL) - .await; - panel - .update(cx, |panel, cx| { - panel.show_scrollbar = false; - cx.notify(); - }) - .log_err(); - })) - } - fn render_list(&mut self, cx: &mut Context) -> impl IntoElement { let selected_ix = self.selected_ix; let focus_handle = self.focus_handle.clone(); @@ -614,43 +594,39 @@ impl BreakpointList { .flex_grow() } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Option> { - if !(self.show_scrollbar || self.scrollbar_state.is_dragging()) { - return None; - } - Some( - div() - .occlude() - .id("breakpoint-list-vertical-scrollbar") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { + fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ + div() + .occlude() + .id("breakpoint-list-vertical-scrollbar") + .on_mouse_move(cx.listener(|_, _, _, cx| { + cx.notify(); + cx.stop_propagation() + })) + .on_hover(|_, _, cx| { + cx.stop_propagation(); + }) + .on_any_mouse_down(|_, _, cx| { + cx.stop_propagation(); + }) + .on_mouse_up( + MouseButton::Left, + cx.listener(|_, _, _, cx| { cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone())), - ) + }), + ) + .on_scroll_wheel(cx.listener(|_, _, _, cx| { + cx.notify(); + })) + .h_full() + .absolute() + .right_1() + .top_1() + .bottom_0() + .w(px(12.)) + .cursor_default() + .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx))) } + pub(crate) fn render_control_strip(&self) -> AnyElement { let selection_kind = self.selection_kind(); let focus_handle = self.focus_handle.clone(); @@ -819,15 +795,6 @@ impl Render for BreakpointList { .id("breakpoint-list") .key_context("BreakpointList") .track_focus(&self.focus_handle) - .on_hover(cx.listener(|this, hovered, window, cx| { - if *hovered { - this.show_scrollbar = true; - this.hide_scrollbar_task.take(); - cx.notify(); - } else if !this.focus_handle.contains_focused(window, cx) { - this.hide_scrollbar(window, cx); - } - })) .on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_previous)) .on_action(cx.listener(Self::select_first)) @@ -844,7 +811,7 @@ impl Render for BreakpointList { v_flex() .size_full() .child(self.render_list(cx)) - .children(self.render_vertical_scrollbar(cx)), + .child(self.render_vertical_scrollbar(cx)), ) .when_some(self.strip_mode, |this, _| { this.child(Divider::horizontal()).child( diff --git a/crates/debugger_ui/src/session/running/memory_view.rs b/crates/debugger_ui/src/session/running/memory_view.rs index 7b62a1d55d..75b8938371 100644 --- a/crates/debugger_ui/src/session/running/memory_view.rs +++ b/crates/debugger_ui/src/session/running/memory_view.rs @@ -23,7 +23,6 @@ use ui::{ ParentElement, Pixels, PopoverMenuHandle, Render, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, TextSize, Tooltip, Window, div, h_flex, px, v_flex, }; -use util::ResultExt; use workspace::Workspace; use crate::{ToggleDataBreakpoint, session::running::stack_frame_list::StackFrameList}; @@ -34,9 +33,7 @@ pub(crate) struct MemoryView { workspace: WeakEntity, scroll_handle: UniformListScrollHandle, scroll_state: ScrollbarState, - show_scrollbar: bool, stack_frame_list: WeakEntity, - hide_scrollbar_task: Option>, focus_handle: FocusHandle, view_state: ViewState, query_editor: Entity, @@ -150,8 +147,6 @@ impl MemoryView { scroll_state, scroll_handle, stack_frame_list, - show_scrollbar: false, - hide_scrollbar_task: None, focus_handle: cx.focus_handle(), view_state, query_editor, @@ -168,61 +163,42 @@ impl MemoryView { .detach(); this } - fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context) { - const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); - self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| { - cx.background_executor() - .timer(SCROLLBAR_SHOW_INTERVAL) - .await; - panel - .update(cx, |panel, cx| { - panel.show_scrollbar = false; - cx.notify(); - }) - .log_err(); - })) - } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Option> { - if !(self.show_scrollbar || self.scroll_state.is_dragging()) { - return None; - } - Some( - div() - .occlude() - .id("memory-view-vertical-scrollbar") - .on_drag_move(cx.listener(|this, evt, _, cx| { - let did_handle = this.handle_scroll_drag(evt); - cx.notify(); - if did_handle { - cx.stop_propagation() - } - })) - .on_drag(ScrollbarDragging, |_, _, _, cx| cx.new(|_| Empty)) - .on_hover(|_, _, cx| { + fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ + div() + .occlude() + .id("memory-view-vertical-scrollbar") + .on_drag_move(cx.listener(|this, evt, _, cx| { + let did_handle = this.handle_scroll_drag(evt); + cx.notify(); + if did_handle { + cx.stop_propagation() + } + })) + .on_drag(ScrollbarDragging, |_, _, _, cx| cx.new(|_| Empty)) + .on_hover(|_, _, cx| { + cx.stop_propagation(); + }) + .on_any_mouse_down(|_, _, cx| { + cx.stop_propagation(); + }) + .on_mouse_up( + MouseButton::Left, + cx.listener(|_, _, _, cx| { cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scroll_state.clone())), - ) + }), + ) + .on_scroll_wheel(cx.listener(|_, _, _, cx| { + cx.notify(); + })) + .h_full() + .absolute() + .right_1() + .top_1() + .bottom_0() + .w(px(12.)) + .cursor_default() + .children(Scrollbar::vertical(self.scroll_state.clone()).map(|s| s.auto_hide(cx))) } fn render_memory(&self, cx: &mut Context) -> UniformList { @@ -920,15 +896,6 @@ impl Render for MemoryView { .on_action(cx.listener(Self::page_up)) .size_full() .track_focus(&self.focus_handle) - .on_hover(cx.listener(|this, hovered, window, cx| { - if *hovered { - this.show_scrollbar = true; - this.hide_scrollbar_task.take(); - cx.notify(); - } else if !this.focus_handle.contains_focused(window, cx) { - this.hide_scrollbar(window, cx); - } - })) .child( h_flex() .w_full() @@ -978,7 +945,7 @@ impl Render for MemoryView { ) .with_priority(1) })) - .children(self.render_vertical_scrollbar(cx)), + .child(self.render_vertical_scrollbar(cx)), ) } } diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs index 7af55b76b7..605028202f 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/crates/ui/src/components/scrollbar.rs @@ -1,11 +1,20 @@ -use std::{any::Any, cell::Cell, fmt::Debug, ops::Range, rc::Rc, sync::Arc}; +use std::{ + any::Any, + cell::{Cell, RefCell}, + fmt::Debug, + ops::Range, + rc::Rc, + sync::Arc, + time::Duration, +}; use crate::{IntoElement, prelude::*, px, relative}; use gpui::{ Along, App, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Corners, CursorStyle, Edges, Element, ElementId, Entity, EntityId, GlobalElementId, Hitbox, HitboxBehavior, Hsla, IsZero, LayoutId, ListState, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, - Point, ScrollHandle, ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, quad, + Point, ScrollHandle, ScrollWheelEvent, Size, Style, Task, UniformListScrollHandle, Window, + quad, }; pub struct Scrollbar { @@ -108,6 +117,25 @@ pub struct ScrollbarState { thumb_state: Rc>, parent_id: Option, scroll_handle: Arc, + auto_hide: Rc>, +} + +#[derive(Debug)] +enum AutoHide { + Disabled, + Hidden { + parent_id: EntityId, + }, + Visible { + parent_id: EntityId, + _task: Task<()>, + }, +} + +impl AutoHide { + fn is_hidden(&self) -> bool { + matches!(self, AutoHide::Hidden { .. }) + } } impl ScrollbarState { @@ -116,6 +144,7 @@ impl ScrollbarState { thumb_state: Default::default(), parent_id: None, scroll_handle: Arc::new(scroll), + auto_hide: Rc::new(RefCell::new(AutoHide::Disabled)), } } @@ -174,6 +203,38 @@ impl ScrollbarState { let thumb_percentage_end = (start_offset + thumb_size) / viewport_size; Some(thumb_percentage_start..thumb_percentage_end) } + + fn show_temporarily(&self, parent_id: EntityId, cx: &mut App) { + const SHOW_INTERVAL: Duration = Duration::from_secs(1); + + let auto_hide = self.auto_hide.clone(); + auto_hide.replace(AutoHide::Visible { + parent_id, + _task: cx.spawn({ + let this = auto_hide.clone(); + async move |cx| { + cx.background_executor().timer(SHOW_INTERVAL).await; + this.replace(AutoHide::Hidden { parent_id }); + cx.update(|cx| { + cx.notify(parent_id); + }) + .ok(); + } + }), + }); + } + + fn unhide(&self, position: &Point, cx: &mut App) { + let parent_id = match &*self.auto_hide.borrow() { + AutoHide::Disabled => return, + AutoHide::Hidden { parent_id } => *parent_id, + AutoHide::Visible { parent_id, _task } => *parent_id, + }; + + if self.scroll_handle().viewport().contains(position) { + self.show_temporarily(parent_id, cx); + } + } } impl Scrollbar { @@ -189,6 +250,14 @@ impl Scrollbar { let thumb = state.thumb_range(kind)?; Some(Self { thumb, state, kind }) } + + /// Automatically hide the scrollbar when idle + pub fn auto_hide(self, cx: &mut Context) -> Self { + if matches!(*self.state.auto_hide.borrow(), AutoHide::Disabled) { + self.state.show_temporarily(cx.entity_id(), cx); + } + self + } } impl Element for Scrollbar { @@ -284,16 +353,18 @@ impl Element for Scrollbar { .apply_along(axis.invert(), |width| width / 1.5), ); - let corners = Corners::all(thumb_bounds.size.along(axis.invert()) / 2.0); + if thumb_state.is_dragging() || !self.state.auto_hide.borrow().is_hidden() { + let corners = Corners::all(thumb_bounds.size.along(axis.invert()) / 2.0); - window.paint_quad(quad( - thumb_bounds, - corners, - thumb_background, - Edges::default(), - Hsla::transparent_black(), - BorderStyle::default(), - )); + window.paint_quad(quad( + thumb_bounds, + corners, + thumb_background, + Edges::default(), + Hsla::transparent_black(), + BorderStyle::default(), + )); + } if thumb_state.is_dragging() { window.set_window_cursor_style(CursorStyle::Arrow); @@ -361,13 +432,18 @@ impl Element for Scrollbar { }); window.on_mouse_event({ + let state = self.state.clone(); let scroll_handle = self.state.scroll_handle().clone(); - move |event: &ScrollWheelEvent, phase, window, _| { - if phase.bubble() && bounds.contains(&event.position) { - let current_offset = scroll_handle.offset(); - scroll_handle.set_offset( - current_offset + event.delta.pixel_delta(window.line_height()), - ); + move |event: &ScrollWheelEvent, phase, window, cx| { + if phase.bubble() { + state.unhide(&event.position, cx); + + if bounds.contains(&event.position) { + let current_offset = scroll_handle.offset(); + scroll_handle.set_offset( + current_offset + event.delta.pixel_delta(window.line_height()), + ); + } } } }); @@ -376,6 +452,8 @@ impl Element for Scrollbar { let state = self.state.clone(); move |event: &MouseMoveEvent, phase, window, cx| { if phase.bubble() { + state.unhide(&event.position, cx); + match state.thumb_state.get() { ThumbState::Dragging(drag_state) if event.dragging() => { let scroll_handle = state.scroll_handle(); From b8e8fbd8e603cdf1d777f7d07d99fd2cc669a3f8 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Wed, 6 Aug 2025 20:14:15 +0530 Subject: [PATCH 122/693] ollama: Add support for gpt-oss (#35648) There is a know bug when calling tool discussion: https://discord.com/channels/1128867683291627614/1402385744038858853 I have raised the issue with ollama team and they are currently fixing it. Release Notes: - ollama: Add support for gpt-oss --- crates/ollama/src/ollama.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ollama/src/ollama.rs b/crates/ollama/src/ollama.rs index 62c32b4161..64cd1cc0cb 100644 --- a/crates/ollama/src/ollama.rs +++ b/crates/ollama/src/ollama.rs @@ -58,7 +58,7 @@ fn get_max_tokens(name: &str) -> u64 { "magistral" => 40000, "llama3.1" | "llama3.2" | "llama3.3" | "phi3" | "phi3.5" | "phi4" | "command-r" | "qwen3" | "gemma3" | "deepseek-coder-v2" | "deepseek-v3" | "deepseek-r1" | "yi-coder" - | "devstral" => 128000, + | "devstral" | "gpt-oss" => 128000, _ => DEFAULT_TOKENS, } .clamp(1, MAXIMUM_TOKENS) From 55b4df4d9f1922785879de268fb4e4fe7d824d4b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 6 Aug 2025 18:47:44 +0300 Subject: [PATCH 123/693] Add a way to distinguish metrics by Zed's release channel (#35729) Release Notes: - N/A --------- Co-authored-by: Oleksiy Syvokon --- crates/collab/src/rpc.rs | 37 ++++++++++++++++++++++++++ crates/collab/src/tests/test_server.rs | 1 + 2 files changed, 38 insertions(+) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 87172beca4..dfa5859f51 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -746,6 +746,7 @@ impl Server { address: String, principal: Principal, zed_version: ZedVersion, + release_channel: Option, user_agent: Option, geoip_country_code: Option, system_id: Option, @@ -766,6 +767,9 @@ impl Server { if let Some(user_agent) = user_agent { span.record("user_agent", user_agent); } + if let Some(release_channel) = release_channel { + span.record("release_channel", release_channel); + } if let Some(country_code) = geoip_country_code.as_ref() { span.record("geoip_country_code", country_code); @@ -1181,6 +1185,35 @@ impl Header for AppVersionHeader { } } +#[derive(Debug)] +pub struct ReleaseChannelHeader(String); + +impl Header for ReleaseChannelHeader { + fn name() -> &'static HeaderName { + static ZED_RELEASE_CHANNEL: OnceLock = OnceLock::new(); + ZED_RELEASE_CHANNEL.get_or_init(|| HeaderName::from_static("x-zed-release-channel")) + } + + fn decode<'i, I>(values: &mut I) -> Result + where + Self: Sized, + I: Iterator, + { + Ok(Self( + values + .next() + .ok_or_else(axum::headers::Error::invalid)? + .to_str() + .map_err(|_| axum::headers::Error::invalid())? + .to_owned(), + )) + } + + fn encode>(&self, values: &mut E) { + values.extend([self.0.parse().unwrap()]); + } +} + pub fn routes(server: Arc) -> Router<(), Body> { Router::new() .route("/rpc", get(handle_websocket_request)) @@ -1196,6 +1229,7 @@ pub fn routes(server: Arc) -> Router<(), Body> { pub async fn handle_websocket_request( TypedHeader(ProtocolVersion(protocol_version)): TypedHeader, app_version_header: Option>, + release_channel_header: Option>, ConnectInfo(socket_address): ConnectInfo, Extension(server): Extension>, Extension(principal): Extension, @@ -1220,6 +1254,8 @@ pub async fn handle_websocket_request( .into_response(); }; + let release_channel = release_channel_header.map(|header| header.0.0); + if !version.can_collaborate() { return ( StatusCode::UPGRADE_REQUIRED, @@ -1255,6 +1291,7 @@ pub async fn handle_websocket_request( socket_address, principal, version, + release_channel, user_agent.map(|header| header.to_string()), country_code_header.map(|header| header.to_string()), system_id_header.map(|header| header.to_string()), diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 5fcc622fc1..f5a0e8ea81 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -297,6 +297,7 @@ impl TestServer { client_name, Principal::User(user), ZedVersion(SemanticVersion::new(1, 0, 0)), + Some("test".to_string()), None, None, None, From ebda6b8a94e8aa8baefc7534f8c940feac500be9 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Wed, 6 Aug 2025 11:16:05 -0500 Subject: [PATCH 124/693] keymap_ui: Show matching bindings (#35732) Closes #ISSUE Adds a bit of text in the keybind editing modal when there are existing keystrokes with the same key, with the ability for the user to click the text and have the keymap editor search be updated to show only bindings with those keystrokes Release Notes: - Keymap Editor: Added a warning to the keybind editing modal when existing bindings have the same keystrokes. Clicking the warning will close the modal and show bindings with the entered keystrokes in the keymap editor. This behavior was previously possible with the `keymap_editor::ShowMatchingKeybinds` action in the Keymap Editor, and is now present in the keybind editing modal as well. --- crates/settings_ui/src/keybindings.rs | 107 ++++++++++++++++++++------ 1 file changed, 85 insertions(+), 22 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 81c461fed6..60e527677a 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -374,6 +374,14 @@ impl Focusable for KeymapEditor { } } } +/// Helper function to check if two keystroke sequences match exactly +fn keystrokes_match_exactly(keystrokes1: &[Keystroke], keystrokes2: &[Keystroke]) -> bool { + keystrokes1.len() == keystrokes2.len() + && keystrokes1 + .iter() + .zip(keystrokes2) + .all(|(k1, k2)| k1.key == k2.key && k1.modifiers == k2.modifiers) +} impl KeymapEditor { fn new(workspace: WeakEntity, window: &mut Window, cx: &mut Context) -> Self { @@ -549,13 +557,7 @@ impl KeymapEditor { .keystrokes() .is_some_and(|keystrokes| { if exact_match { - keystroke_query.len() == keystrokes.len() - && keystroke_query.iter().zip(keystrokes).all( - |(query, keystroke)| { - query.key == keystroke.key - && query.modifiers == keystroke.modifiers - }, - ) + keystrokes_match_exactly(&keystroke_query, keystrokes) } else if keystroke_query.len() > keystrokes.len() { return false; } else { @@ -2340,8 +2342,50 @@ impl KeybindingEditorModal { self.save_or_display_error(cx); } - fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context) { - cx.emit(DismissEvent) + fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { + cx.emit(DismissEvent); + } + + fn get_matching_bindings_count(&self, cx: &Context) -> usize { + let current_keystrokes = self.keybind_editor.read(cx).keystrokes().to_vec(); + + if current_keystrokes.is_empty() { + return 0; + } + + self.keymap_editor + .read(cx) + .keybindings + .iter() + .enumerate() + .filter(|(idx, binding)| { + // Don't count the binding we're currently editing + if !self.creating && *idx == self.editing_keybind_idx { + return false; + } + + binding + .keystrokes() + .map(|keystrokes| keystrokes_match_exactly(keystrokes, ¤t_keystrokes)) + .unwrap_or(false) + }) + .count() + } + + fn show_matching_bindings(&mut self, _window: &mut Window, cx: &mut Context) { + let keystrokes = self.keybind_editor.read(cx).keystrokes().to_vec(); + + // Dismiss the modal + cx.emit(DismissEvent); + + // Update the keymap editor to show matching keystrokes + self.keymap_editor.update(cx, |editor, cx| { + editor.filter_state = FilterState::All; + editor.search_mode = SearchMode::KeyStroke { exact_match: true }; + editor.keystroke_editor.update(cx, |keystroke_editor, cx| { + keystroke_editor.set_keystrokes(keystrokes, cx); + }); + }); } } @@ -2356,6 +2400,7 @@ fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke { impl Render for KeybindingEditorModal { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let theme = cx.theme().colors(); + let matching_bindings_count = self.get_matching_bindings_count(cx); v_flex() .w(rems(34.)) @@ -2427,19 +2472,37 @@ impl Render for KeybindingEditorModal { ), ) .footer( - ModalFooter::new().end_slot( - h_flex() - .gap_1() - .child( - Button::new("cancel", "Cancel") - .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), - ) - .child(Button::new("save-btn", "Save").on_click(cx.listener( - |this, _event, _window, cx| { - this.save_or_display_error(cx); - }, - ))), - ), + ModalFooter::new() + .start_slot( + div().when(matching_bindings_count > 0, |this| { + this.child( + Button::new("show_matching", format!( + "There {} {} {} with the same keystrokes. Click to view", + if matching_bindings_count == 1 { "is" } else { "are" }, + matching_bindings_count, + if matching_bindings_count == 1 { "binding" } else { "bindings" } + )) + .style(ButtonStyle::Transparent) + .color(Color::Accent) + .on_click(cx.listener(|this, _, window, cx| { + this.show_matching_bindings(window, cx); + })) + ) + }) + ) + .end_slot( + h_flex() + .gap_1() + .child( + Button::new("cancel", "Cancel") + .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), + ) + .child(Button::new("save-btn", "Save").on_click(cx.listener( + |this, _event, _window, cx| { + this.save_or_display_error(cx); + }, + ))), + ), ), ) } From 740597492bd88633f21a793dea3fb47c7d04f4f7 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Aug 2025 12:53:43 -0400 Subject: [PATCH 125/693] collab: Remove Stripe events polling (#35736) This PR removes the Stripe event polling from Collab, as it has been moved to Cloud. Release Notes: - N/A --- crates/collab/src/api/billing.rs | 487 +------------------------------ crates/collab/src/main.rs | 4 +- 2 files changed, 6 insertions(+), 485 deletions(-) diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs index 808713ff38..a0325d14c4 100644 --- a/crates/collab/src/api/billing.rs +++ b/crates/collab/src/api/billing.rs @@ -1,477 +1,10 @@ -use anyhow::{Context as _, bail}; -use chrono::{DateTime, Utc}; -use sea_orm::ActiveValue; -use std::{sync::Arc, time::Duration}; -use stripe::{CancellationDetailsReason, EventObject, EventType, ListEvents, SubscriptionStatus}; -use util::ResultExt; +use std::sync::Arc; +use stripe::SubscriptionStatus; use crate::AppState; -use crate::db::billing_subscription::{ - StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind, -}; -use crate::db::{ - CreateBillingCustomerParams, CreateBillingSubscriptionParams, CreateProcessedStripeEventParams, - UpdateBillingCustomerParams, UpdateBillingSubscriptionParams, billing_customer, -}; -use crate::rpc::{ResultExt as _, Server}; -use crate::stripe_client::{ - StripeCancellationDetailsReason, StripeClient, StripeCustomerId, StripeSubscription, - StripeSubscriptionId, -}; - -/// The amount of time we wait in between each poll of Stripe events. -/// -/// This value should strike a balance between: -/// 1. Being short enough that we update quickly when something in Stripe changes -/// 2. Being long enough that we don't eat into our rate limits. -/// -/// As a point of reference, the Sequin folks say they have this at **500ms**: -/// -/// > We poll the Stripe /events endpoint every 500ms per account -/// > -/// > — https://blog.sequinstream.com/events-not-webhooks/ -const POLL_EVENTS_INTERVAL: Duration = Duration::from_secs(5); - -/// The maximum number of events to return per page. -/// -/// We set this to 100 (the max) so we have to make fewer requests to Stripe. -/// -/// > Limit can range between 1 and 100, and the default is 10. -const EVENTS_LIMIT_PER_PAGE: u64 = 100; - -/// The number of pages consisting entirely of already-processed events that we -/// will see before we stop retrieving events. -/// -/// This is used to prevent over-fetching the Stripe events API for events we've -/// already seen and processed. -const NUMBER_OF_ALREADY_PROCESSED_PAGES_BEFORE_WE_STOP: usize = 4; - -/// Polls the Stripe events API periodically to reconcile the records in our -/// database with the data in Stripe. -pub fn poll_stripe_events_periodically(app: Arc, rpc_server: Arc) { - let Some(real_stripe_client) = app.real_stripe_client.clone() else { - log::warn!("failed to retrieve Stripe client"); - return; - }; - let Some(stripe_client) = app.stripe_client.clone() else { - log::warn!("failed to retrieve Stripe client"); - return; - }; - - let executor = app.executor.clone(); - executor.spawn_detached({ - let executor = executor.clone(); - async move { - loop { - poll_stripe_events(&app, &rpc_server, &stripe_client, &real_stripe_client) - .await - .log_err(); - - executor.sleep(POLL_EVENTS_INTERVAL).await; - } - } - }); -} - -async fn poll_stripe_events( - app: &Arc, - rpc_server: &Arc, - stripe_client: &Arc, - real_stripe_client: &stripe::Client, -) -> anyhow::Result<()> { - let feature_flags = app.db.list_feature_flags().await?; - let sync_events_using_cloud = feature_flags - .iter() - .any(|flag| flag.flag == "cloud-stripe-events-polling" && flag.enabled_for_all); - if sync_events_using_cloud { - return Ok(()); - } - - fn event_type_to_string(event_type: EventType) -> String { - // Calling `to_string` on `stripe::EventType` members gives us a quoted string, - // so we need to unquote it. - event_type.to_string().trim_matches('"').to_string() - } - - let event_types = [ - EventType::CustomerCreated, - EventType::CustomerUpdated, - EventType::CustomerSubscriptionCreated, - EventType::CustomerSubscriptionUpdated, - EventType::CustomerSubscriptionPaused, - EventType::CustomerSubscriptionResumed, - EventType::CustomerSubscriptionDeleted, - ] - .into_iter() - .map(event_type_to_string) - .collect::>(); - - let mut pages_of_already_processed_events = 0; - let mut unprocessed_events = Vec::new(); - - log::info!( - "Stripe events: starting retrieval for {}", - event_types.join(", ") - ); - let mut params = ListEvents::new(); - params.types = Some(event_types.clone()); - params.limit = Some(EVENTS_LIMIT_PER_PAGE); - - let mut event_pages = stripe::Event::list(&real_stripe_client, ¶ms) - .await? - .paginate(params); - - loop { - let processed_event_ids = { - let event_ids = event_pages - .page - .data - .iter() - .map(|event| event.id.as_str()) - .collect::>(); - app.db - .get_processed_stripe_events_by_event_ids(&event_ids) - .await? - .into_iter() - .map(|event| event.stripe_event_id) - .collect::>() - }; - - let mut processed_events_in_page = 0; - let events_in_page = event_pages.page.data.len(); - for event in &event_pages.page.data { - if processed_event_ids.contains(&event.id.to_string()) { - processed_events_in_page += 1; - log::debug!("Stripe events: already processed '{}', skipping", event.id); - } else { - unprocessed_events.push(event.clone()); - } - } - - if processed_events_in_page == events_in_page { - pages_of_already_processed_events += 1; - } - - if event_pages.page.has_more { - if pages_of_already_processed_events >= NUMBER_OF_ALREADY_PROCESSED_PAGES_BEFORE_WE_STOP - { - log::info!( - "Stripe events: stopping, saw {pages_of_already_processed_events} pages of already-processed events" - ); - break; - } else { - log::info!("Stripe events: retrieving next page"); - event_pages = event_pages.next(&real_stripe_client).await?; - } - } else { - break; - } - } - - log::info!("Stripe events: unprocessed {}", unprocessed_events.len()); - - // Sort all of the unprocessed events in ascending order, so we can handle them in the order they occurred. - unprocessed_events.sort_by(|a, b| a.created.cmp(&b.created).then_with(|| a.id.cmp(&b.id))); - - for event in unprocessed_events { - let event_id = event.id.clone(); - let processed_event_params = CreateProcessedStripeEventParams { - stripe_event_id: event.id.to_string(), - stripe_event_type: event_type_to_string(event.type_), - stripe_event_created_timestamp: event.created, - }; - - // If the event has happened too far in the past, we don't want to - // process it and risk overwriting other more-recent updates. - // - // 1 day was chosen arbitrarily. This could be made longer or shorter. - let one_day = Duration::from_secs(24 * 60 * 60); - let a_day_ago = Utc::now() - one_day; - if a_day_ago.timestamp() > event.created { - log::info!( - "Stripe events: event '{}' is more than {one_day:?} old, marking as processed", - event_id - ); - app.db - .create_processed_stripe_event(&processed_event_params) - .await?; - - continue; - } - - let process_result = match event.type_ { - EventType::CustomerCreated | EventType::CustomerUpdated => { - handle_customer_event(app, real_stripe_client, event).await - } - EventType::CustomerSubscriptionCreated - | EventType::CustomerSubscriptionUpdated - | EventType::CustomerSubscriptionPaused - | EventType::CustomerSubscriptionResumed - | EventType::CustomerSubscriptionDeleted => { - handle_customer_subscription_event(app, rpc_server, stripe_client, event).await - } - _ => Ok(()), - }; - - if let Some(()) = process_result - .with_context(|| format!("failed to process event {event_id} successfully")) - .log_err() - { - app.db - .create_processed_stripe_event(&processed_event_params) - .await?; - } - } - - Ok(()) -} - -async fn handle_customer_event( - app: &Arc, - _stripe_client: &stripe::Client, - event: stripe::Event, -) -> anyhow::Result<()> { - let EventObject::Customer(customer) = event.data.object else { - bail!("unexpected event payload for {}", event.id); - }; - - log::info!("handling Stripe {} event: {}", event.type_, event.id); - - let Some(email) = customer.email else { - log::info!("Stripe customer has no email: skipping"); - return Ok(()); - }; - - let Some(user) = app.db.get_user_by_email(&email).await? else { - log::info!("no user found for email: skipping"); - return Ok(()); - }; - - if let Some(existing_customer) = app - .db - .get_billing_customer_by_stripe_customer_id(&customer.id) - .await? - { - app.db - .update_billing_customer( - existing_customer.id, - &UpdateBillingCustomerParams { - // For now we just leave the information as-is, as it is not - // likely to change. - ..Default::default() - }, - ) - .await?; - } else { - app.db - .create_billing_customer(&CreateBillingCustomerParams { - user_id: user.id, - stripe_customer_id: customer.id.to_string(), - }) - .await?; - } - - Ok(()) -} - -async fn sync_subscription( - app: &Arc, - stripe_client: &Arc, - subscription: StripeSubscription, -) -> anyhow::Result { - let subscription_kind = if let Some(stripe_billing) = &app.stripe_billing { - stripe_billing - .determine_subscription_kind(&subscription) - .await - } else { - None - }; - - let billing_customer = - find_or_create_billing_customer(app, stripe_client.as_ref(), &subscription.customer) - .await? - .context("billing customer not found")?; - - if let Some(SubscriptionKind::ZedProTrial) = subscription_kind { - if subscription.status == SubscriptionStatus::Trialing { - let current_period_start = - DateTime::from_timestamp(subscription.current_period_start, 0) - .context("No trial subscription period start")?; - - app.db - .update_billing_customer( - billing_customer.id, - &UpdateBillingCustomerParams { - trial_started_at: ActiveValue::set(Some(current_period_start.naive_utc())), - ..Default::default() - }, - ) - .await?; - } - } - - let was_canceled_due_to_payment_failure = subscription.status == SubscriptionStatus::Canceled - && subscription - .cancellation_details - .as_ref() - .and_then(|details| details.reason) - .map_or(false, |reason| { - reason == StripeCancellationDetailsReason::PaymentFailed - }); - - if was_canceled_due_to_payment_failure { - app.db - .update_billing_customer( - billing_customer.id, - &UpdateBillingCustomerParams { - has_overdue_invoices: ActiveValue::set(true), - ..Default::default() - }, - ) - .await?; - } - - if let Some(existing_subscription) = app - .db - .get_billing_subscription_by_stripe_subscription_id(subscription.id.0.as_ref()) - .await? - { - app.db - .update_billing_subscription( - existing_subscription.id, - &UpdateBillingSubscriptionParams { - billing_customer_id: ActiveValue::set(billing_customer.id), - kind: ActiveValue::set(subscription_kind), - stripe_subscription_id: ActiveValue::set(subscription.id.to_string()), - stripe_subscription_status: ActiveValue::set(subscription.status.into()), - stripe_cancel_at: ActiveValue::set( - subscription - .cancel_at - .and_then(|cancel_at| DateTime::from_timestamp(cancel_at, 0)) - .map(|time| time.naive_utc()), - ), - stripe_cancellation_reason: ActiveValue::set( - subscription - .cancellation_details - .and_then(|details| details.reason) - .map(|reason| reason.into()), - ), - stripe_current_period_start: ActiveValue::set(Some( - subscription.current_period_start, - )), - stripe_current_period_end: ActiveValue::set(Some( - subscription.current_period_end, - )), - }, - ) - .await?; - } else { - if let Some(existing_subscription) = app - .db - .get_active_billing_subscription(billing_customer.user_id) - .await? - { - if existing_subscription.kind == Some(SubscriptionKind::ZedFree) - && subscription_kind == Some(SubscriptionKind::ZedProTrial) - { - let stripe_subscription_id = StripeSubscriptionId( - existing_subscription.stripe_subscription_id.clone().into(), - ); - - stripe_client - .cancel_subscription(&stripe_subscription_id) - .await?; - } else { - // If the user already has an active billing subscription, ignore the - // event and return an `Ok` to signal that it was processed - // successfully. - // - // There is the possibility that this could cause us to not create a - // subscription in the following scenario: - // - // 1. User has an active subscription A - // 2. User cancels subscription A - // 3. User creates a new subscription B - // 4. We process the new subscription B before the cancellation of subscription A - // 5. User ends up with no subscriptions - // - // In theory this situation shouldn't arise as we try to process the events in the order they occur. - - log::info!( - "user {user_id} already has an active subscription, skipping creation of subscription {subscription_id}", - user_id = billing_customer.user_id, - subscription_id = subscription.id - ); - return Ok(billing_customer); - } - } - - app.db - .create_billing_subscription(&CreateBillingSubscriptionParams { - billing_customer_id: billing_customer.id, - kind: subscription_kind, - stripe_subscription_id: subscription.id.to_string(), - stripe_subscription_status: subscription.status.into(), - stripe_cancellation_reason: subscription - .cancellation_details - .and_then(|details| details.reason) - .map(|reason| reason.into()), - stripe_current_period_start: Some(subscription.current_period_start), - stripe_current_period_end: Some(subscription.current_period_end), - }) - .await?; - } - - if let Some(stripe_billing) = app.stripe_billing.as_ref() { - if subscription.status == SubscriptionStatus::Canceled - || subscription.status == SubscriptionStatus::Paused - { - let already_has_active_billing_subscription = app - .db - .has_active_billing_subscription(billing_customer.user_id) - .await?; - if !already_has_active_billing_subscription { - let stripe_customer_id = - StripeCustomerId(billing_customer.stripe_customer_id.clone().into()); - - stripe_billing - .subscribe_to_zed_free(stripe_customer_id) - .await?; - } - } - } - - Ok(billing_customer) -} - -async fn handle_customer_subscription_event( - app: &Arc, - rpc_server: &Arc, - stripe_client: &Arc, - event: stripe::Event, -) -> anyhow::Result<()> { - let EventObject::Subscription(subscription) = event.data.object else { - bail!("unexpected event payload for {}", event.id); - }; - - log::info!("handling Stripe {} event: {}", event.type_, event.id); - - let billing_customer = sync_subscription(app, stripe_client, subscription.into()).await?; - - // When the user's subscription changes, push down any changes to their plan. - rpc_server - .update_plan_for_user_legacy(billing_customer.user_id) - .await - .trace_err(); - - // When the user's subscription changes, we want to refresh their LLM tokens - // to either grant/revoke access. - rpc_server - .refresh_llm_tokens_for_user(billing_customer.user_id) - .await; - - Ok(()) -} +use crate::db::billing_subscription::StripeSubscriptionStatus; +use crate::db::{CreateBillingCustomerParams, billing_customer}; +use crate::stripe_client::{StripeClient, StripeCustomerId}; impl From for StripeSubscriptionStatus { fn from(value: SubscriptionStatus) -> Self { @@ -488,16 +21,6 @@ impl From for StripeSubscriptionStatus { } } -impl From for StripeCancellationReason { - fn from(value: CancellationDetailsReason) -> Self { - match value { - CancellationDetailsReason::CancellationRequested => Self::CancellationRequested, - CancellationDetailsReason::PaymentDisputed => Self::PaymentDisputed, - CancellationDetailsReason::PaymentFailed => Self::PaymentFailed, - } - } -} - /// Finds or creates a billing customer using the provided customer. pub async fn find_or_create_billing_customer( app: &Arc, diff --git a/crates/collab/src/main.rs b/crates/collab/src/main.rs index 0442a69042..20641cb232 100644 --- a/crates/collab/src/main.rs +++ b/crates/collab/src/main.rs @@ -7,6 +7,7 @@ use axum::{ routing::get, }; +use collab::ServiceMode; use collab::api::CloudflareIpCountryHeader; use collab::llm::db::LlmDatabase; use collab::migrations::run_database_migrations; @@ -15,7 +16,6 @@ use collab::{ AppState, Config, Result, api::fetch_extensions_from_blob_store_periodically, db, env, executor::Executor, rpc::ResultExt, }; -use collab::{ServiceMode, api::billing::poll_stripe_events_periodically}; use db::Database; use std::{ env::args, @@ -119,8 +119,6 @@ async fn main() -> Result<()> { let rpc_server = collab::rpc::Server::new(epoch, state.clone()); rpc_server.start().await?; - poll_stripe_events_periodically(state.clone(), rpc_server.clone()); - app = app .merge(collab::api::routes(rpc_server.clone())) .merge(collab::rpc::routes(rpc_server.clone())); From b08e26df609780b5d0f889b8a7efa757a4eaa94d Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Aug 2025 13:42:12 -0400 Subject: [PATCH 126/693] collab: Remove unused `StripeBilling` methods (#35740) This PR removes some unused methods from the `StripeBilling` object. Release Notes: - N/A --- crates/collab/src/stripe_billing.rs | 106 +------- .../collab/src/tests/stripe_billing_tests.rs | 239 +----------------- 2 files changed, 4 insertions(+), 341 deletions(-) diff --git a/crates/collab/src/stripe_billing.rs b/crates/collab/src/stripe_billing.rs index 850b716a9f..ef5bef3e7e 100644 --- a/crates/collab/src/stripe_billing.rs +++ b/crates/collab/src/stripe_billing.rs @@ -1,21 +1,15 @@ use std::sync::Arc; use anyhow::anyhow; -use chrono::Utc; use collections::HashMap; use stripe::SubscriptionStatus; use tokio::sync::RwLock; -use uuid::Uuid; use crate::Result; -use crate::db::billing_subscription::SubscriptionKind; use crate::stripe_client::{ - RealStripeClient, StripeAutomaticTax, StripeClient, StripeCreateMeterEventParams, - StripeCreateMeterEventPayload, StripeCreateSubscriptionItems, StripeCreateSubscriptionParams, - StripeCustomerId, StripePrice, StripePriceId, StripeSubscription, StripeSubscriptionId, - StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior, - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateSubscriptionItems, - UpdateSubscriptionParams, + RealStripeClient, StripeAutomaticTax, StripeClient, StripeCreateSubscriptionItems, + StripeCreateSubscriptionParams, StripeCustomerId, StripePrice, StripePriceId, + StripeSubscription, }; pub struct StripeBilling { @@ -94,30 +88,6 @@ impl StripeBilling { .ok_or_else(|| crate::Error::Internal(anyhow!("no price found for {lookup_key:?}"))) } - pub async fn determine_subscription_kind( - &self, - subscription: &StripeSubscription, - ) -> Option { - let zed_pro_price_id = self.zed_pro_price_id().await.ok()?; - let zed_free_price_id = self.zed_free_price_id().await.ok()?; - - subscription.items.iter().find_map(|item| { - let price = item.price.as_ref()?; - - if price.id == zed_pro_price_id { - Some(if subscription.status == SubscriptionStatus::Trialing { - SubscriptionKind::ZedProTrial - } else { - SubscriptionKind::ZedPro - }) - } else if price.id == zed_free_price_id { - Some(SubscriptionKind::ZedFree) - } else { - None - } - }) - } - /// Returns the Stripe customer associated with the provided email address, or creates a new customer, if one does /// not already exist. /// @@ -150,65 +120,6 @@ impl StripeBilling { Ok(customer_id) } - pub async fn subscribe_to_price( - &self, - subscription_id: &StripeSubscriptionId, - price: &StripePrice, - ) -> Result<()> { - let subscription = self.client.get_subscription(subscription_id).await?; - - if subscription_contains_price(&subscription, &price.id) { - return Ok(()); - } - - const BILLING_THRESHOLD_IN_CENTS: i64 = 20 * 100; - - let price_per_unit = price.unit_amount.unwrap_or_default(); - let _units_for_billing_threshold = BILLING_THRESHOLD_IN_CENTS / price_per_unit; - - self.client - .update_subscription( - subscription_id, - UpdateSubscriptionParams { - items: Some(vec![UpdateSubscriptionItems { - price: Some(price.id.clone()), - }]), - trial_settings: Some(StripeSubscriptionTrialSettings { - end_behavior: StripeSubscriptionTrialSettingsEndBehavior { - missing_payment_method: StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel - }, - }), - }, - ) - .await?; - - Ok(()) - } - - pub async fn bill_model_request_usage( - &self, - customer_id: &StripeCustomerId, - event_name: &str, - requests: i32, - ) -> Result<()> { - let timestamp = Utc::now().timestamp(); - let idempotency_key = Uuid::new_v4(); - - self.client - .create_meter_event(StripeCreateMeterEventParams { - identifier: &format!("model_requests/{}", idempotency_key), - event_name, - payload: StripeCreateMeterEventPayload { - value: requests as u64, - stripe_customer_id: customer_id, - }, - timestamp: Some(timestamp), - }) - .await?; - - Ok(()) - } - pub async fn subscribe_to_zed_free( &self, customer_id: StripeCustomerId, @@ -243,14 +154,3 @@ impl StripeBilling { Ok(subscription) } } - -fn subscription_contains_price( - subscription: &StripeSubscription, - price_id: &StripePriceId, -) -> bool { - subscription.items.iter().any(|item| { - item.price - .as_ref() - .map_or(false, |price| price.id == *price_id) - }) -} diff --git a/crates/collab/src/tests/stripe_billing_tests.rs b/crates/collab/src/tests/stripe_billing_tests.rs index 5c5bcd5832..bb84bedfcf 100644 --- a/crates/collab/src/tests/stripe_billing_tests.rs +++ b/crates/collab/src/tests/stripe_billing_tests.rs @@ -1,14 +1,9 @@ use std::sync::Arc; -use chrono::{Duration, Utc}; use pretty_assertions::assert_eq; use crate::stripe_billing::StripeBilling; -use crate::stripe_client::{ - FakeStripeClient, StripeCustomerId, StripeMeter, StripeMeterId, StripePrice, StripePriceId, - StripePriceRecurring, StripeSubscription, StripeSubscriptionId, StripeSubscriptionItem, - StripeSubscriptionItemId, UpdateSubscriptionItems, -}; +use crate::stripe_client::{FakeStripeClient, StripePrice, StripePriceId, StripePriceRecurring}; fn make_stripe_billing() -> (StripeBilling, Arc) { let stripe_client = Arc::new(FakeStripeClient::new()); @@ -21,24 +16,6 @@ fn make_stripe_billing() -> (StripeBilling, Arc) { async fn test_initialize() { let (stripe_billing, stripe_client) = make_stripe_billing(); - // Add test meters - let meter1 = StripeMeter { - id: StripeMeterId("meter_1".into()), - event_name: "event_1".to_string(), - }; - let meter2 = StripeMeter { - id: StripeMeterId("meter_2".into()), - event_name: "event_2".to_string(), - }; - stripe_client - .meters - .lock() - .insert(meter1.id.clone(), meter1); - stripe_client - .meters - .lock() - .insert(meter2.id.clone(), meter2); - // Add test prices let price1 = StripePrice { id: StripePriceId("price_1".into()), @@ -144,217 +121,3 @@ async fn test_find_or_create_customer_by_email() { assert_eq!(customer.email.as_deref(), Some(email)); } } - -#[gpui::test] -async fn test_subscribe_to_price() { - let (stripe_billing, stripe_client) = make_stripe_billing(); - - let price = StripePrice { - id: StripePriceId("price_test".into()), - unit_amount: Some(2000), - lookup_key: Some("test-price".to_string()), - recurring: None, - }; - stripe_client - .prices - .lock() - .insert(price.id.clone(), price.clone()); - - let now = Utc::now(); - let subscription = StripeSubscription { - id: StripeSubscriptionId("sub_test".into()), - customer: StripeCustomerId("cus_test".into()), - status: stripe::SubscriptionStatus::Active, - current_period_start: now.timestamp(), - current_period_end: (now + Duration::days(30)).timestamp(), - items: vec![], - cancel_at: None, - cancellation_details: None, - }; - stripe_client - .subscriptions - .lock() - .insert(subscription.id.clone(), subscription.clone()); - - stripe_billing - .subscribe_to_price(&subscription.id, &price) - .await - .unwrap(); - - let update_subscription_calls = stripe_client - .update_subscription_calls - .lock() - .iter() - .map(|(id, params)| (id.clone(), params.clone())) - .collect::>(); - assert_eq!(update_subscription_calls.len(), 1); - assert_eq!(update_subscription_calls[0].0, subscription.id); - assert_eq!( - update_subscription_calls[0].1.items, - Some(vec![UpdateSubscriptionItems { - price: Some(price.id.clone()) - }]) - ); - - // Subscribing to a price that is already on the subscription is a no-op. - { - let now = Utc::now(); - let subscription = StripeSubscription { - id: StripeSubscriptionId("sub_test".into()), - customer: StripeCustomerId("cus_test".into()), - status: stripe::SubscriptionStatus::Active, - current_period_start: now.timestamp(), - current_period_end: (now + Duration::days(30)).timestamp(), - items: vec![StripeSubscriptionItem { - id: StripeSubscriptionItemId("si_test".into()), - price: Some(price.clone()), - }], - cancel_at: None, - cancellation_details: None, - }; - stripe_client - .subscriptions - .lock() - .insert(subscription.id.clone(), subscription.clone()); - - stripe_billing - .subscribe_to_price(&subscription.id, &price) - .await - .unwrap(); - - assert_eq!(stripe_client.update_subscription_calls.lock().len(), 1); - } -} - -#[gpui::test] -async fn test_subscribe_to_zed_free() { - let (stripe_billing, stripe_client) = make_stripe_billing(); - - let zed_pro_price = StripePrice { - id: StripePriceId("price_1".into()), - unit_amount: Some(0), - lookup_key: Some("zed-pro".to_string()), - recurring: None, - }; - stripe_client - .prices - .lock() - .insert(zed_pro_price.id.clone(), zed_pro_price.clone()); - let zed_free_price = StripePrice { - id: StripePriceId("price_2".into()), - unit_amount: Some(0), - lookup_key: Some("zed-free".to_string()), - recurring: None, - }; - stripe_client - .prices - .lock() - .insert(zed_free_price.id.clone(), zed_free_price.clone()); - - stripe_billing.initialize().await.unwrap(); - - // Customer is subscribed to Zed Free when not already subscribed to a plan. - { - let customer_id = StripeCustomerId("cus_no_plan".into()); - - let subscription = stripe_billing - .subscribe_to_zed_free(customer_id) - .await - .unwrap(); - - assert_eq!(subscription.items[0].price.as_ref(), Some(&zed_free_price)); - } - - // Customer is not subscribed to Zed Free when they already have an active subscription. - { - let customer_id = StripeCustomerId("cus_active_subscription".into()); - - let now = Utc::now(); - let existing_subscription = StripeSubscription { - id: StripeSubscriptionId("sub_existing_active".into()), - customer: customer_id.clone(), - status: stripe::SubscriptionStatus::Active, - current_period_start: now.timestamp(), - current_period_end: (now + Duration::days(30)).timestamp(), - items: vec![StripeSubscriptionItem { - id: StripeSubscriptionItemId("si_test".into()), - price: Some(zed_pro_price.clone()), - }], - cancel_at: None, - cancellation_details: None, - }; - stripe_client.subscriptions.lock().insert( - existing_subscription.id.clone(), - existing_subscription.clone(), - ); - - let subscription = stripe_billing - .subscribe_to_zed_free(customer_id) - .await - .unwrap(); - - assert_eq!(subscription, existing_subscription); - } - - // Customer is not subscribed to Zed Free when they already have a trial subscription. - { - let customer_id = StripeCustomerId("cus_trial_subscription".into()); - - let now = Utc::now(); - let existing_subscription = StripeSubscription { - id: StripeSubscriptionId("sub_existing_trial".into()), - customer: customer_id.clone(), - status: stripe::SubscriptionStatus::Trialing, - current_period_start: now.timestamp(), - current_period_end: (now + Duration::days(14)).timestamp(), - items: vec![StripeSubscriptionItem { - id: StripeSubscriptionItemId("si_test".into()), - price: Some(zed_pro_price.clone()), - }], - cancel_at: None, - cancellation_details: None, - }; - stripe_client.subscriptions.lock().insert( - existing_subscription.id.clone(), - existing_subscription.clone(), - ); - - let subscription = stripe_billing - .subscribe_to_zed_free(customer_id) - .await - .unwrap(); - - assert_eq!(subscription, existing_subscription); - } -} - -#[gpui::test] -async fn test_bill_model_request_usage() { - let (stripe_billing, stripe_client) = make_stripe_billing(); - - let customer_id = StripeCustomerId("cus_test".into()); - - stripe_billing - .bill_model_request_usage(&customer_id, "some_model/requests", 73) - .await - .unwrap(); - - let create_meter_event_calls = stripe_client - .create_meter_event_calls - .lock() - .iter() - .cloned() - .collect::>(); - assert_eq!(create_meter_event_calls.len(), 1); - assert!( - create_meter_event_calls[0] - .identifier - .starts_with("model_requests/") - ); - assert_eq!(create_meter_event_calls[0].stripe_customer_id, customer_id); - assert_eq!( - create_meter_event_calls[0].event_name.as_ref(), - "some_model/requests" - ); - assert_eq!(create_meter_event_calls[0].value, 73); -} From 794098e5c94dbbd2744ac90cdfdb5e5d1e0659d6 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 6 Aug 2025 11:10:28 -0700 Subject: [PATCH 127/693] Update instructions for local collaboration (#35689) Release Notes: - N/A --- Procfile | 1 + docs/src/development/local-collaboration.md | 49 ++++++++++++++++----- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/Procfile b/Procfile index 5f1231b90a..b3f13f66a6 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,4 @@ collab: RUST_LOG=${RUST_LOG:-info} cargo run --package=collab serve all +cloud: cd ../cloud; cargo make dev livekit: livekit-server --dev blob_store: ./script/run-local-minio diff --git a/docs/src/development/local-collaboration.md b/docs/src/development/local-collaboration.md index 9f0e3ef191..eb7f3dfc43 100644 --- a/docs/src/development/local-collaboration.md +++ b/docs/src/development/local-collaboration.md @@ -1,13 +1,27 @@ # Local Collaboration -First, make sure you've installed Zed's dependencies for your platform: +1. Ensure you have access to our cloud infrastructure. If you don't have access, you can't collaborate locally at this time. -- [macOS](./macos.md#backend-dependencies) -- [Linux](./linux.md#backend-dependencies) -- [Windows](./windows.md#backend-dependencies) +2. Make sure you've installed Zed's dependencies for your platform: + +- [macOS](#macos) +- [Linux](#linux) +- [Windows](#backend-windows) Note that `collab` can be compiled only with MSVC toolchain on Windows +3. Clone down our cloud repository and follow the instructions in the cloud README + +4. Setup the local database for your platform: + +- [macOS & Linux](#database-unix) +- [Windows](#database-windows) + +5. Run collab: + +- [macOS & Linux](#run-collab-unix) +- [Windows](#run-collab-windows) + ## Backend Dependencies If you are developing collaborative features of Zed, you'll need to install the dependencies of zed's `collab` server: @@ -18,7 +32,7 @@ If you are developing collaborative features of Zed, you'll need to install the You can install these dependencies natively or run them under Docker. -### MacOS +### macOS 1. Install [Postgres.app](https://postgresapp.com) or [postgresql via homebrew](https://formulae.brew.sh/formula/postgresql@15): @@ -76,7 +90,7 @@ docker compose up -d Before you can run the `collab` server locally, you'll need to set up a `zed` Postgres database. -### On macOS and Linux +### On macOS and Linux {#database-unix} ```sh script/bootstrap @@ -99,7 +113,7 @@ To use a different set of admin users, you can create your own version of that j } ``` -### On Windows +### On Windows {#database-windows} ```powershell .\script\bootstrap.ps1 @@ -107,7 +121,7 @@ To use a different set of admin users, you can create your own version of that j ## Testing collaborative features locally -### On macOS and Linux +### On macOS and Linux {#run-collab-unix} Ensure that Postgres is configured and running, then run Zed's collaboration server and the `livekit` dev server: @@ -117,12 +131,16 @@ foreman start docker compose up ``` -Alternatively, if you're not testing voice and screenshare, you can just run `collab`, and not the `livekit` dev server: +Alternatively, if you're not testing voice and screenshare, you can just run `collab` and `cloud`, and not the `livekit` dev server: ```sh cargo run -p collab -- serve all ``` +```sh +cd ../cloud; cargo make dev +``` + In a new terminal, run two or more instances of Zed. ```sh @@ -131,7 +149,7 @@ script/zed-local -3 This script starts one to four instances of Zed, depending on the `-2`, `-3` or `-4` flags. Each instance will be connected to the local `collab` server, signed in as a different user from `.admins.json` or `.admins.default.json`. -### On Windows +### On Windows {#run-collab-windows} Since `foreman` is not available on Windows, you can run the following commands in separate terminals: @@ -151,6 +169,12 @@ Otherwise, .\path\to\livekit-serve.exe --dev ``` +You'll also need to start the cloud server: + +```powershell +cd ..\cloud; cargo make dev +``` + In a new terminal, run two or more instances of Zed. ```powershell @@ -161,7 +185,10 @@ Note that this requires `node.exe` to be in your `PATH`. ## Running a local collab server -If you want to run your own version of the zed collaboration service, you can, but note that this is still under development, and there is no good support for authentication nor extensions. +> [!NOTE] +> Because of recent changes to our authentication system, Zed will not be able to authenticate itself with, and therefore use, a local collab server. + +If you want to run your own version of the zed collaboration service, you can, but note that this is still under development, and there is no support for authentication nor extensions. Configuration is done through environment variables. By default it will read the configuration from [`.env.toml`](https://github.com/zed-industries/zed/blob/main/crates/collab/.env.toml) and you should use that as a guide for setting this up. From fb1f9d12127fe3177cc2231b876095c6850512ae Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 6 Aug 2025 20:27:48 +0200 Subject: [PATCH 128/693] lsp: Correctly serialize errors for LSP requests + improve handling of unrecognized methods (#35738) We used to not respond at all to requests that we didn't have a handler for, which is yuck. It may have left the language server waiting for the response for no good reason. The other (worse) finding is that we did not have a full definition of an Error type for LSP, which made it so that a spec-compliant language server would fail to deserialize our response (with an error). This then could lead to all sorts of funkiness, including hangs and crashes on the language server's part. Co-authored-by: Lukas Co-authored-by: Remco Smits Co-authored-by: Anthony Eid Closes #ISSUE Release Notes: - Improved reporting of errors to language servers, which should improve the stability of LSPs ran by Zed. --------- Co-authored-by: Lukas Co-authored-by: Remco Smits Co-authored-by: Anthony Eid --- crates/lsp/src/input_handler.rs | 11 +++--- crates/lsp/src/lsp.rs | 70 ++++++++++++++++++++++++--------- 2 files changed, 57 insertions(+), 24 deletions(-) diff --git a/crates/lsp/src/input_handler.rs b/crates/lsp/src/input_handler.rs index db3f1190fc..001ebf1fc9 100644 --- a/crates/lsp/src/input_handler.rs +++ b/crates/lsp/src/input_handler.rs @@ -13,14 +13,15 @@ use parking_lot::Mutex; use smol::io::BufReader; use crate::{ - AnyNotification, AnyResponse, CONTENT_LEN_HEADER, IoHandler, IoKind, RequestId, ResponseHandler, + AnyResponse, CONTENT_LEN_HEADER, IoHandler, IoKind, NotificationOrRequest, RequestId, + ResponseHandler, }; const HEADER_DELIMITER: &[u8; 4] = b"\r\n\r\n"; /// Handler for stdout of language server. pub struct LspStdoutHandler { pub(super) loop_handle: Task>, - pub(super) notifications_channel: UnboundedReceiver, + pub(super) incoming_messages: UnboundedReceiver, } async fn read_headers(reader: &mut BufReader, buffer: &mut Vec) -> Result<()> @@ -54,13 +55,13 @@ impl LspStdoutHandler { let loop_handle = cx.spawn(Self::handler(stdout, tx, response_handlers, io_handlers)); Self { loop_handle, - notifications_channel, + incoming_messages: notifications_channel, } } async fn handler( stdout: Input, - notifications_sender: UnboundedSender, + notifications_sender: UnboundedSender, response_handlers: Arc>>>, io_handlers: Arc>>, ) -> anyhow::Result<()> @@ -96,7 +97,7 @@ impl LspStdoutHandler { } } - if let Ok(msg) = serde_json::from_slice::(&buffer) { + if let Ok(msg) = serde_json::from_slice::(&buffer) { notifications_sender.unbounded_send(msg)?; } else if let Ok(AnyResponse { id, error, result, .. diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index b9701a83d2..3f45d2e6fc 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -242,7 +242,7 @@ struct Notification<'a, T> { /// Language server RPC notification message before it is deserialized into a concrete type. #[derive(Debug, Clone, Deserialize)] -struct AnyNotification { +struct NotificationOrRequest { #[serde(default)] id: Option, method: String, @@ -252,7 +252,10 @@ struct AnyNotification { #[derive(Debug, Serialize, Deserialize)] struct Error { + code: i64, message: String, + #[serde(default)] + data: Option, } pub trait LspRequestFuture: Future> { @@ -364,6 +367,7 @@ impl LanguageServer { notification.method, serde_json::to_string_pretty(¬ification.params).unwrap(), ); + false }, ); @@ -389,7 +393,7 @@ impl LanguageServer { Stdin: AsyncWrite + Unpin + Send + 'static, Stdout: AsyncRead + Unpin + Send + 'static, Stderr: AsyncRead + Unpin + Send + 'static, - F: FnMut(AnyNotification) + 'static + Send + Sync + Clone, + F: Fn(&NotificationOrRequest) -> bool + 'static + Send + Sync + Clone, { let (outbound_tx, outbound_rx) = channel::unbounded::(); let (output_done_tx, output_done_rx) = barrier::channel(); @@ -400,14 +404,34 @@ impl LanguageServer { let io_handlers = Arc::new(Mutex::new(HashMap::default())); let stdout_input_task = cx.spawn({ - let on_unhandled_notification = on_unhandled_notification.clone(); + let unhandled_notification_wrapper = { + let response_channel = outbound_tx.clone(); + async move |msg: NotificationOrRequest| { + let did_handle = on_unhandled_notification(&msg); + if !did_handle && let Some(message_id) = msg.id { + let response = AnyResponse { + jsonrpc: JSON_RPC_VERSION, + id: message_id, + error: Some(Error { + code: -32601, + message: format!("Unrecognized method `{}`", msg.method), + data: None, + }), + result: None, + }; + if let Ok(response) = serde_json::to_string(&response) { + response_channel.send(response).await.ok(); + } + } + } + }; let notification_handlers = notification_handlers.clone(); let response_handlers = response_handlers.clone(); let io_handlers = io_handlers.clone(); async move |cx| { - Self::handle_input( + Self::handle_incoming_messages( stdout, - on_unhandled_notification, + unhandled_notification_wrapper, notification_handlers, response_handlers, io_handlers, @@ -433,7 +457,7 @@ impl LanguageServer { stdout.or(stderr) }); let output_task = cx.background_spawn({ - Self::handle_output( + Self::handle_outgoing_messages( stdin, outbound_rx, output_done_tx, @@ -479,9 +503,9 @@ impl LanguageServer { self.code_action_kinds.clone() } - async fn handle_input( + async fn handle_incoming_messages( stdout: Stdout, - mut on_unhandled_notification: F, + on_unhandled_notification: impl AsyncFn(NotificationOrRequest) + 'static + Send, notification_handlers: Arc>>, response_handlers: Arc>>>, io_handlers: Arc>>, @@ -489,7 +513,6 @@ impl LanguageServer { ) -> anyhow::Result<()> where Stdout: AsyncRead + Unpin + Send + 'static, - F: FnMut(AnyNotification) + 'static + Send, { use smol::stream::StreamExt; let stdout = BufReader::new(stdout); @@ -506,15 +529,19 @@ impl LanguageServer { cx.background_executor().clone(), ); - while let Some(msg) = input_handler.notifications_channel.next().await { - { + while let Some(msg) = input_handler.incoming_messages.next().await { + let unhandled_message = { let mut notification_handlers = notification_handlers.lock(); if let Some(handler) = notification_handlers.get_mut(msg.method.as_str()) { handler(msg.id, msg.params.unwrap_or(Value::Null), cx); + None } else { - drop(notification_handlers); - on_unhandled_notification(msg); + Some(msg) } + }; + + if let Some(msg) = unhandled_message { + on_unhandled_notification(msg).await; } // Don't starve the main thread when receiving lots of notifications at once. @@ -558,7 +585,7 @@ impl LanguageServer { } } - async fn handle_output( + async fn handle_outgoing_messages( stdin: Stdin, outbound_rx: channel::Receiver, output_done_tx: barrier::Sender, @@ -1036,7 +1063,9 @@ impl LanguageServer { jsonrpc: JSON_RPC_VERSION, id, value: LspResult::Error(Some(Error { + code: lsp_types::error_codes::REQUEST_FAILED, message: error.to_string(), + data: None, })), }, }; @@ -1057,7 +1086,9 @@ impl LanguageServer { id, result: None, error: Some(Error { + code: -32700, // Parse error message: error.to_string(), + data: None, }), }; if let Some(response) = serde_json::to_string(&response).log_err() { @@ -1559,7 +1590,7 @@ impl FakeLanguageServer { root, Some(workspace_folders.clone()), cx, - |_| {}, + |_| false, ); server.process_name = process_name; let fake = FakeLanguageServer { @@ -1582,9 +1613,10 @@ impl FakeLanguageServer { notifications_tx .try_send(( msg.method.to_string(), - msg.params.unwrap_or(Value::Null).to_string(), + msg.params.as_ref().unwrap_or(&Value::Null).to_string(), )) .ok(); + true }, ); server.process_name = name.as_str().into(); @@ -1862,7 +1894,7 @@ mod tests { #[gpui::test] fn test_deserialize_string_digit_id() { let json = r#"{"jsonrpc":"2.0","id":"2","method":"workspace/configuration","params":{"items":[{"scopeUri":"file:///Users/mph/Devel/personal/hello-scala/","section":"metals"}]}}"#; - let notification = serde_json::from_str::(json) + let notification = serde_json::from_str::(json) .expect("message with string id should be parsed"); let expected_id = RequestId::Str("2".to_string()); assert_eq!(notification.id, Some(expected_id)); @@ -1871,7 +1903,7 @@ mod tests { #[gpui::test] fn test_deserialize_string_id() { let json = r#"{"jsonrpc":"2.0","id":"anythingAtAll","method":"workspace/configuration","params":{"items":[{"scopeUri":"file:///Users/mph/Devel/personal/hello-scala/","section":"metals"}]}}"#; - let notification = serde_json::from_str::(json) + let notification = serde_json::from_str::(json) .expect("message with string id should be parsed"); let expected_id = RequestId::Str("anythingAtAll".to_string()); assert_eq!(notification.id, Some(expected_id)); @@ -1880,7 +1912,7 @@ mod tests { #[gpui::test] fn test_deserialize_int_id() { let json = r#"{"jsonrpc":"2.0","id":2,"method":"workspace/configuration","params":{"items":[{"scopeUri":"file:///Users/mph/Devel/personal/hello-scala/","section":"metals"}]}}"#; - let notification = serde_json::from_str::(json) + let notification = serde_json::from_str::(json) .expect("message with string id should be parsed"); let expected_id = RequestId::Int(2); assert_eq!(notification.id, Some(expected_id)); From a80da784b7cbd3355ddfb02c1f74d3d5fb175fdb Mon Sep 17 00:00:00 2001 From: xdBronch <51252236+xdBronch@users.noreply.github.com> Date: Wed, 6 Aug 2025 14:42:29 -0400 Subject: [PATCH 129/693] lsp: Advertise support for markdown in completion documentation (#35727) Release Notes: - N/A --- crates/lsp/src/lsp.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 3f45d2e6fc..a92787cd3e 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -747,6 +747,10 @@ impl LanguageServer { InsertTextMode::ADJUST_INDENTATION, ], }), + documentation_format: Some(vec![ + MarkupKind::Markdown, + MarkupKind::PlainText, + ]), ..Default::default() }), insert_text_mode: Some(InsertTextMode::ADJUST_INDENTATION), From f9038f61898420f8620fd1266872994ff341b190 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Wed, 6 Aug 2025 15:28:18 -0400 Subject: [PATCH 130/693] Add key contexts for Pickers (#35665) Closes: https://github.com/zed-industries/zed/issues/35430 Added: - Workspace > CommandPalette - Workspace > GitBranchSelector - Workspace > GitRepositorySelector - Workspace > RecentProjects - Workspace > LanguageSelector - Workspace > IconThemeSelector - Workspace > ThemeSelector Release Notes: - Added new keymap contexts for various Pickers - CommandPalette, GitBranchSelector, GitRepositorySelector, RecentProjects, LanguageSelector, IconThemeSelector, ThemeSelector --- crates/command_palette/src/command_palette.rs | 5 ++++- crates/git_ui/src/branch_picker.rs | 1 + crates/git_ui/src/repository_selector.rs | 5 ++++- crates/language_selector/src/language_selector.rs | 5 ++++- crates/recent_projects/src/recent_projects.rs | 1 + crates/theme_selector/src/icon_theme_selector.rs | 5 ++++- crates/theme_selector/src/theme_selector.rs | 5 ++++- 7 files changed, 22 insertions(+), 5 deletions(-) diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index dfaede0dc4..b8800ff912 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -136,7 +136,10 @@ impl Focusable for CommandPalette { impl Render for CommandPalette { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - v_flex().w(rems(34.)).child(self.picker.clone()) + v_flex() + .key_context("CommandPalette") + .w(rems(34.)) + .child(self.picker.clone()) } } diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index 1092ba33d1..b74fa649b0 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -180,6 +180,7 @@ impl Focusable for BranchList { impl Render for BranchList { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() + .key_context("GitBranchSelector") .w(self.width) .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) .child(self.picker.clone()) diff --git a/crates/git_ui/src/repository_selector.rs b/crates/git_ui/src/repository_selector.rs index b5865e9a85..db080ab0b4 100644 --- a/crates/git_ui/src/repository_selector.rs +++ b/crates/git_ui/src/repository_selector.rs @@ -109,7 +109,10 @@ impl Focusable for RepositorySelector { impl Render for RepositorySelector { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().w(self.width).child(self.picker.clone()) + div() + .key_context("GitRepositorySelector") + .w(self.width) + .child(self.picker.clone()) } } diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs index 7ef57085bb..f6e2d75015 100644 --- a/crates/language_selector/src/language_selector.rs +++ b/crates/language_selector/src/language_selector.rs @@ -86,7 +86,10 @@ impl LanguageSelector { impl Render for LanguageSelector { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - v_flex().w(rems(34.)).child(self.picker.clone()) + v_flex() + .key_context("LanguageSelector") + .w(rems(34.)) + .child(self.picker.clone()) } } diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 5dbde6496d..2093e96cae 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -141,6 +141,7 @@ impl Focusable for RecentProjects { impl Render for RecentProjects { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() + .key_context("RecentProjects") .w(rems(self.rem_width)) .child(self.picker.clone()) .on_mouse_down_out(cx.listener(|this, _, window, cx| { diff --git a/crates/theme_selector/src/icon_theme_selector.rs b/crates/theme_selector/src/icon_theme_selector.rs index 1adfc4b5d8..2d0b9480d5 100644 --- a/crates/theme_selector/src/icon_theme_selector.rs +++ b/crates/theme_selector/src/icon_theme_selector.rs @@ -40,7 +40,10 @@ impl IconThemeSelector { impl Render for IconThemeSelector { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - v_flex().w(rems(34.)).child(self.picker.clone()) + v_flex() + .key_context("IconThemeSelector") + .w(rems(34.)) + .child(self.picker.clone()) } } diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 022daced7a..ba8bde243b 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -92,7 +92,10 @@ impl Focusable for ThemeSelector { impl Render for ThemeSelector { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - v_flex().w(rems(34.)).child(self.picker.clone()) + v_flex() + .key_context("ThemeSelector") + .w(rems(34.)) + .child(self.picker.clone()) } } From 010441e23bf81cfaacf4b875e9e330d456c53558 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 6 Aug 2025 15:45:22 -0400 Subject: [PATCH 131/693] debugger: Show run to cursor in editor's context menu (#35745) This also fixed a bug where evaluate selected text was an available option when the selected debug session was terminated. Release Notes: - debugger: add Run to Cursor back to Editor's context menu Co-authored-by: Remco Smits --- crates/debugger_ui/src/debugger_ui.rs | 103 ++++++++++++++---------- crates/editor/src/mouse_context_menu.rs | 18 +++-- crates/gpui/src/window.rs | 19 +++++ 3 files changed, 91 insertions(+), 49 deletions(-) diff --git a/crates/debugger_ui/src/debugger_ui.rs b/crates/debugger_ui/src/debugger_ui.rs index 9eac59af83..5f5dfd1a1e 100644 --- a/crates/debugger_ui/src/debugger_ui.rs +++ b/crates/debugger_ui/src/debugger_ui.rs @@ -299,59 +299,76 @@ pub fn init(cx: &mut App) { else { return; }; + + let session = active_session + .read(cx) + .running_state + .read(cx) + .session() + .read(cx); + + if session.is_terminated() { + return; + } + let editor = cx.entity().downgrade(); - window.on_action(TypeId::of::(), { - let editor = editor.clone(); - let active_session = active_session.clone(); - move |_, phase, _, cx| { - if phase != DispatchPhase::Bubble { - return; - } - maybe!({ - let (buffer, position, _) = editor - .update(cx, |editor, cx| { - let cursor_point: language::Point = - editor.selections.newest(cx).head(); - editor - .buffer() - .read(cx) - .point_to_buffer_point(cursor_point, cx) - }) - .ok()??; + window.on_action_when( + session.any_stopped_thread(), + TypeId::of::(), + { + let editor = editor.clone(); + let active_session = active_session.clone(); + move |_, phase, _, cx| { + if phase != DispatchPhase::Bubble { + return; + } + maybe!({ + let (buffer, position, _) = editor + .update(cx, |editor, cx| { + let cursor_point: language::Point = + editor.selections.newest(cx).head(); - let path = + editor + .buffer() + .read(cx) + .point_to_buffer_point(cursor_point, cx) + }) + .ok()??; + + let path = debugger::breakpoint_store::BreakpointStore::abs_path_from_buffer( &buffer, cx, )?; - let source_breakpoint = SourceBreakpoint { - row: position.row, - path, - message: None, - condition: None, - hit_condition: None, - state: debugger::breakpoint_store::BreakpointState::Enabled, - }; + let source_breakpoint = SourceBreakpoint { + row: position.row, + path, + message: None, + condition: None, + hit_condition: None, + state: debugger::breakpoint_store::BreakpointState::Enabled, + }; - active_session.update(cx, |session, cx| { - session.running_state().update(cx, |state, cx| { - if let Some(thread_id) = state.selected_thread_id() { - state.session().update(cx, |session, cx| { - session.run_to_position( - source_breakpoint, - thread_id, - cx, - ); - }) - } + active_session.update(cx, |session, cx| { + session.running_state().update(cx, |state, cx| { + if let Some(thread_id) = state.selected_thread_id() { + state.session().update(cx, |session, cx| { + session.run_to_position( + source_breakpoint, + thread_id, + cx, + ); + }) + } + }); }); - }); - Some(()) - }); - } - }); + Some(()) + }); + } + }, + ); window.on_action( TypeId::of::(), diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index cbb6791a2f..9d5145dec1 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -1,8 +1,8 @@ use crate::{ Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DisplayPoint, DisplaySnapshot, Editor, EvaluateSelectedText, FindAllReferences, GoToDeclaration, GoToDefinition, GoToImplementation, - GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, SelectionEffects, - SelectionExt, ToDisplayPoint, ToggleCodeActions, + GoToTypeDefinition, Paste, Rename, RevealInFileManager, RunToCursor, SelectMode, + SelectionEffects, SelectionExt, ToDisplayPoint, ToggleCodeActions, actions::{Format, FormatSelections}, selections_collection::SelectionsCollection, }; @@ -200,15 +200,21 @@ pub fn deploy_context_menu( }); let evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx); + let run_to_cursor = window.is_action_available(&RunToCursor, cx); ui::ContextMenu::build(window, cx, |menu, _window, _cx| { let builder = menu .on_blur_subscription(Subscription::new(|| {})) - .when(evaluate_selection && has_selections, |builder| { - builder - .action("Evaluate Selection", Box::new(EvaluateSelectedText)) - .separator() + .when(run_to_cursor, |builder| { + builder.action("Run to Cursor", Box::new(RunToCursor)) }) + .when(evaluate_selection && has_selections, |builder| { + builder.action("Evaluate Selection", Box::new(EvaluateSelectedText)) + }) + .when( + run_to_cursor || (evaluate_selection && has_selections), + |builder| builder.separator(), + ) .action("Go to Definition", Box::new(GoToDefinition)) .action("Go to Declaration", Box::new(GoToDeclaration)) .action("Go to Type Definition", Box::new(GoToTypeDefinition)) diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 8610993994..40d3845ff9 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4248,6 +4248,25 @@ impl Window { .on_action(action_type, Rc::new(listener)); } + /// Register an action listener on the window for the next frame if the condition is true. + /// The type of action is determined by the first parameter of the given listener. + /// When the next frame is rendered the listener will be cleared. + /// + /// This is a fairly low-level method, so prefer using action handlers on elements unless you have + /// a specific need to register a global listener. + pub fn on_action_when( + &mut self, + condition: bool, + action_type: TypeId, + listener: impl Fn(&dyn Any, DispatchPhase, &mut Window, &mut App) + 'static, + ) { + if condition { + self.next_frame + .dispatch_tree + .on_action(action_type, Rc::new(listener)); + } + } + /// Read information about the GPU backing this window. /// Currently returns None on Mac and Windows. pub fn gpu_specs(&self) -> Option { From 250c51bb206bb4a20538bae33232685d40d281cf Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 6 Aug 2025 16:53:45 -0300 Subject: [PATCH 132/693] Fix syntax highlighting in ACP diffs (#35748) Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 47 ++++++++++++++--------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 70d9abd731..9febd9c46f 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -411,8 +411,6 @@ impl ToolCallContent { pub struct Diff { pub multibuffer: Entity, pub path: PathBuf, - pub new_buffer: Entity, - pub old_buffer: Entity, _task: Task>, } @@ -433,23 +431,34 @@ impl Diff { let new_buffer = cx.new(|cx| Buffer::local(new_text, cx)); let old_buffer = cx.new(|cx| Buffer::local(old_text.unwrap_or("".into()), cx)); let new_buffer_snapshot = new_buffer.read(cx).text_snapshot(); - let old_buffer_snapshot = old_buffer.read(cx).snapshot(); let buffer_diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot, cx)); - let diff_task = buffer_diff.update(cx, |diff, cx| { - diff.set_base_text( - old_buffer_snapshot, - Some(language_registry.clone()), - new_buffer_snapshot, - cx, - ) - }); let task = cx.spawn({ let multibuffer = multibuffer.clone(); let path = path.clone(); - let new_buffer = new_buffer.clone(); async move |cx| { - diff_task.await?; + let language = language_registry + .language_for_file_path(&path) + .await + .log_err(); + + new_buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?; + + let old_buffer_snapshot = old_buffer.update(cx, |buffer, cx| { + buffer.set_language(language, cx); + buffer.snapshot() + })?; + + buffer_diff + .update(cx, |diff, cx| { + diff.set_base_text( + old_buffer_snapshot, + Some(language_registry), + new_buffer_snapshot, + cx, + ) + })? + .await?; multibuffer .update(cx, |multibuffer, cx| { @@ -468,18 +477,10 @@ impl Diff { editor::DEFAULT_MULTIBUFFER_CONTEXT, cx, ); - multibuffer.add_diff(buffer_diff.clone(), cx); + multibuffer.add_diff(buffer_diff, cx); }) .log_err(); - if let Some(language) = language_registry - .language_for_file_path(&path) - .await - .log_err() - { - new_buffer.update(cx, |buffer, cx| buffer.set_language(Some(language), cx))?; - } - anyhow::Ok(()) } }); @@ -487,8 +488,6 @@ impl Diff { Self { multibuffer, path, - new_buffer, - old_buffer, _task: task, } } From a5dd8d0052255bd8e03e55f6ec10dd54fde114a8 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Wed, 6 Aug 2025 15:56:11 -0400 Subject: [PATCH 133/693] Recognize pixi.lock as YAML (#35747) Release Notes: - N/A --- crates/languages/src/yaml/config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/languages/src/yaml/config.toml b/crates/languages/src/yaml/config.toml index 4dfb890c54..e54bceda1a 100644 --- a/crates/languages/src/yaml/config.toml +++ b/crates/languages/src/yaml/config.toml @@ -1,6 +1,6 @@ name = "YAML" grammar = "yaml" -path_suffixes = ["yml", "yaml"] +path_suffixes = ["yml", "yaml", "pixi.lock"] line_comments = ["# "] autoclose_before = ",]}" brackets = [ From 3ea90e397b84fbeabd01443c37f69319002a9dfd Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 6 Aug 2025 18:10:17 -0400 Subject: [PATCH 134/693] debugger: Filter out debug scenarios with invalid Adapters from debug picker (#35744) I also removed a debug assertion that wasn't true when a debug session was restarting through a request, because there wasn't a booting task Zed needed to run before the session. I renamed SessionState::Building to SessionState::Booting as well, because building implies that we're building code while booting the session covers more cases and is more accurate. Release Notes: - debugger: Filter out more invalid debug configurations from the debug picker Co-authored-by: Remco Smits --- crates/dap/src/adapters.rs | 6 +++ crates/dap/src/registry.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 2 +- crates/debugger_ui/src/new_process_modal.rs | 8 +++- crates/debugger_ui/src/session/running.rs | 2 +- .../src/tests/new_process_modal.rs | 2 +- crates/project/src/debugger/session.rs | 43 ++++++++++--------- 7 files changed, 38 insertions(+), 27 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 0c88f37ff8..687305ae94 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -74,6 +74,12 @@ impl Borrow for DebugAdapterName { } } +impl Borrow for DebugAdapterName { + fn borrow(&self) -> &SharedString { + &self.0 + } +} + impl std::fmt::Display for DebugAdapterName { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(&self.0, f) diff --git a/crates/dap/src/registry.rs b/crates/dap/src/registry.rs index d56e2f8f34..212fa2bc23 100644 --- a/crates/dap/src/registry.rs +++ b/crates/dap/src/registry.rs @@ -87,7 +87,7 @@ impl DapRegistry { self.0.read().adapters.get(name).cloned() } - pub fn enumerate_adapters(&self) -> Vec { + pub fn enumerate_adapters>(&self) -> B { self.0.read().adapters.keys().cloned().collect() } } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index d81c593484..0ac419580b 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -300,7 +300,7 @@ impl DebugPanel { }); session.update(cx, |session, _| match &mut session.mode { - SessionState::Building(state_task) => { + SessionState::Booting(state_task) => { *state_task = Some(boot_task); } SessionState::Running(_) => { diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index 2695941bc0..4ac8e371a1 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -1,5 +1,5 @@ use anyhow::{Context as _, bail}; -use collections::{FxHashMap, HashMap}; +use collections::{FxHashMap, HashMap, HashSet}; use language::LanguageRegistry; use std::{ borrow::Cow, @@ -450,7 +450,7 @@ impl NewProcessModal { .and_then(|buffer| buffer.read(cx).language()) .cloned(); - let mut available_adapters = workspace + let mut available_adapters: Vec<_> = workspace .update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters()) .unwrap_or_default(); if let Some(language) = active_buffer_language { @@ -1054,6 +1054,9 @@ impl DebugDelegate { }) }) }); + + let valid_adapters: HashSet<_> = cx.global::().enumerate_adapters(); + cx.spawn(async move |this, cx| { let (recent, scenarios) = if let Some(task) = task { task.await @@ -1094,6 +1097,7 @@ impl DebugDelegate { } => !(hide_vscode && dir.ends_with(".vscode")), _ => true, }) + .filter(|(_, scenario)| valid_adapters.contains(&scenario.adapter)) .map(|(kind, scenario)| { let (language, scenario) = Self::get_scenario_kind(&languages, &dap_registry, scenario); diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 2651a94520..f2f9e17d89 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -1651,7 +1651,7 @@ impl RunningState { let is_building = self.session.update(cx, |session, cx| { session.shutdown(cx).detach(); - matches!(session.mode, session::SessionState::Building(_)) + matches!(session.mode, session::SessionState::Booting(_)) }); if is_building { diff --git a/crates/debugger_ui/src/tests/new_process_modal.rs b/crates/debugger_ui/src/tests/new_process_modal.rs index 0805060bf4..d6b0dfa004 100644 --- a/crates/debugger_ui/src/tests/new_process_modal.rs +++ b/crates/debugger_ui/src/tests/new_process_modal.rs @@ -298,7 +298,7 @@ async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppConte let adapter_names = cx.update(|cx| { let registry = DapRegistry::global(cx); - registry.enumerate_adapters() + registry.enumerate_adapters::>() }); let zed_config = ZedDebugConfig { diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index f60a7becf7..d9c28df497 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -56,7 +56,7 @@ use std::{ }; use task::TaskContext; use text::{PointUtf16, ToPointUtf16}; -use util::{ResultExt, maybe}; +use util::{ResultExt, debug_panic, maybe}; use worktree::Worktree; #[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd, Ord, Eq)] @@ -141,7 +141,10 @@ pub struct DataBreakpointState { } pub enum SessionState { - Building(Option>>), + /// Represents a session that is building/initializing + /// even if a session doesn't have a pre build task this state + /// is used to run all the async tasks that are required to start the session + Booting(Option>>), Running(RunningMode), } @@ -574,7 +577,7 @@ impl SessionState { { match self { SessionState::Running(debug_adapter_client) => debug_adapter_client.request(request), - SessionState::Building(_) => Task::ready(Err(anyhow!( + SessionState::Booting(_) => Task::ready(Err(anyhow!( "no adapter running to send request: {request:?}" ))), } @@ -583,7 +586,7 @@ impl SessionState { /// Did this debug session stop at least once? pub(crate) fn has_ever_stopped(&self) -> bool { match self { - SessionState::Building(_) => false, + SessionState::Booting(_) => false, SessionState::Running(running_mode) => running_mode.has_ever_stopped, } } @@ -839,7 +842,7 @@ impl Session { .detach(); let this = Self { - mode: SessionState::Building(None), + mode: SessionState::Booting(None), id: session_id, child_session_ids: HashSet::default(), parent_session, @@ -879,7 +882,7 @@ impl Session { pub fn worktree(&self) -> Option> { match &self.mode { - SessionState::Building(_) => None, + SessionState::Booting(_) => None, SessionState::Running(local_mode) => local_mode.worktree.upgrade(), } } @@ -940,14 +943,12 @@ impl Session { .await?; this.update(cx, |this, cx| { match &mut this.mode { - SessionState::Building(task) if task.is_some() => { + SessionState::Booting(task) if task.is_some() => { task.take().unwrap().detach_and_log_err(cx); } - _ => { - debug_assert!( - this.parent_session.is_some(), - "Booting a root debug session without a boot task" - ); + SessionState::Booting(_) => {} + SessionState::Running(_) => { + debug_panic!("Attempting to boot a session that is already running"); } }; this.mode = SessionState::Running(mode); @@ -1043,7 +1044,7 @@ impl Session { pub fn binary(&self) -> Option<&DebugAdapterBinary> { match &self.mode { - SessionState::Building(_) => None, + SessionState::Booting(_) => None, SessionState::Running(running_mode) => Some(&running_mode.binary), } } @@ -1089,26 +1090,26 @@ impl Session { pub fn is_started(&self) -> bool { match &self.mode { - SessionState::Building(_) => false, + SessionState::Booting(_) => false, SessionState::Running(running) => running.is_started, } } pub fn is_building(&self) -> bool { - matches!(self.mode, SessionState::Building(_)) + matches!(self.mode, SessionState::Booting(_)) } pub fn as_running_mut(&mut self) -> Option<&mut RunningMode> { match &mut self.mode { SessionState::Running(local_mode) => Some(local_mode), - SessionState::Building(_) => None, + SessionState::Booting(_) => None, } } pub fn as_running(&self) -> Option<&RunningMode> { match &self.mode { SessionState::Running(local_mode) => Some(local_mode), - SessionState::Building(_) => None, + SessionState::Booting(_) => None, } } @@ -1302,7 +1303,7 @@ impl Session { SessionState::Running(local_mode) => { local_mode.initialize_sequence(&self.capabilities, initialize_rx, dap_store, cx) } - SessionState::Building(_) => { + SessionState::Booting(_) => { Task::ready(Err(anyhow!("cannot initialize, still building"))) } } @@ -1339,7 +1340,7 @@ impl Session { }) .detach(); } - SessionState::Building(_) => {} + SessionState::Booting(_) => {} } } @@ -2145,7 +2146,7 @@ impl Session { ) } } - SessionState::Building(build_task) => { + SessionState::Booting(build_task) => { build_task.take(); Task::ready(Some(())) } @@ -2199,7 +2200,7 @@ impl Session { pub fn adapter_client(&self) -> Option> { match self.mode { SessionState::Running(ref local) => Some(local.client.clone()), - SessionState::Building(_) => None, + SessionState::Booting(_) => None, } } From 9358690337421b580248d3f594ceba47e3c88f32 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 6 Aug 2025 18:38:00 -0400 Subject: [PATCH 135/693] Fix flicker when agent plan updates (#35739) Currently, when the agent updates its plan, there are a few frames where the text after `Current:` in the plan summary is blank, causing a flicker. This is because we treat that field as markdown, and the `MarkdownElement` renders as blank until the raw text has finished parsing in the background. This PR fixes the flicker by changing `Markdown::new_text` to optimistically render the source as a single `MarkdownEvent::Text` span until background parsing has finished. Release Notes: - N/A Co-authored-by: Agus Zubiaga --- crates/acp_thread/src/acp_thread.rs | 33 +++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 9febd9c46f..aa17a80b5b 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -221,7 +221,9 @@ impl ToolCall { } if let Some(title) = title { - self.label = cx.new(|cx| Markdown::new_text(title.into(), cx)); + self.label.update(cx, |label, cx| { + label.replace(title, cx); + }); } if let Some(content) = content { @@ -556,7 +558,7 @@ pub struct PlanEntry { impl PlanEntry { pub fn from_acp(entry: acp::PlanEntry, cx: &mut App) -> Self { Self { - content: cx.new(|cx| Markdown::new_text(entry.content.into(), cx)), + content: cx.new(|cx| Markdown::new(entry.content.into(), None, None, cx)), priority: entry.priority, status: entry.status, } @@ -970,13 +972,26 @@ impl AcpThread { } pub fn update_plan(&mut self, request: acp::Plan, cx: &mut Context) { - self.plan = Plan { - entries: request - .entries - .into_iter() - .map(|entry| PlanEntry::from_acp(entry, cx)) - .collect(), - }; + let new_entries_len = request.entries.len(); + let mut new_entries = request.entries.into_iter(); + + // Reuse existing markdown to prevent flickering + for (old, new) in self.plan.entries.iter_mut().zip(new_entries.by_ref()) { + let PlanEntry { + content, + priority, + status, + } = old; + content.update(cx, |old, cx| { + old.replace(new.content, cx); + }); + *priority = new.priority; + *status = new.status; + } + for new in new_entries { + self.plan.entries.push(PlanEntry::from_acp(new, cx)) + } + self.plan.entries.truncate(new_entries_len); cx.notify(); } From 58392b9c13a3a814ae7a788d5577ca79ab92bd1d Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Aug 2025 19:20:04 -0400 Subject: [PATCH 136/693] cloud_api_types: Add types for WebSocket protocol (#35753) This PR adds types for the Cloud WebSocket protocol to the `cloud_api_types` crate. Release Notes: - N/A --- Cargo.lock | 2 ++ Cargo.toml | 1 + crates/cloud_api_types/Cargo.toml | 2 ++ crates/cloud_api_types/src/cloud_api_types.rs | 1 + .../cloud_api_types/src/websocket_protocol.rs | 28 +++++++++++++++++++ 5 files changed, 34 insertions(+) create mode 100644 crates/cloud_api_types/src/websocket_protocol.rs diff --git a/Cargo.lock b/Cargo.lock index 1eb5669fa2..b73b89bde6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3081,7 +3081,9 @@ dependencies = [ name = "cloud_api_types" version = "0.1.0" dependencies = [ + "anyhow", "chrono", + "ciborium", "cloud_llm_client", "pretty_assertions", "serde", diff --git a/Cargo.toml b/Cargo.toml index 7b82fd1910..a60a65fcd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -461,6 +461,7 @@ bytes = "1.0" cargo_metadata = "0.19" cargo_toml = "0.21" chrono = { version = "0.4", features = ["serde"] } +ciborium = "0.2" circular-buffer = "1.0" clap = { version = "4.4", features = ["derive"] } cocoa = "0.26" diff --git a/crates/cloud_api_types/Cargo.toml b/crates/cloud_api_types/Cargo.toml index 868797df3b..28e0a36a44 100644 --- a/crates/cloud_api_types/Cargo.toml +++ b/crates/cloud_api_types/Cargo.toml @@ -12,7 +12,9 @@ workspace = true path = "src/cloud_api_types.rs" [dependencies] +anyhow.workspace = true chrono.workspace = true +ciborium.workspace = true cloud_llm_client.workspace = true serde.workspace = true workspace-hack.workspace = true diff --git a/crates/cloud_api_types/src/cloud_api_types.rs b/crates/cloud_api_types/src/cloud_api_types.rs index b38b38cde1..fa189cd3b5 100644 --- a/crates/cloud_api_types/src/cloud_api_types.rs +++ b/crates/cloud_api_types/src/cloud_api_types.rs @@ -1,4 +1,5 @@ mod timestamp; +pub mod websocket_protocol; use serde::{Deserialize, Serialize}; diff --git a/crates/cloud_api_types/src/websocket_protocol.rs b/crates/cloud_api_types/src/websocket_protocol.rs new file mode 100644 index 0000000000..c90d09e370 --- /dev/null +++ b/crates/cloud_api_types/src/websocket_protocol.rs @@ -0,0 +1,28 @@ +use anyhow::{Context as _, Result}; +use serde::{Deserialize, Serialize}; + +/// The version of the Cloud WebSocket protocol. +pub const PROTOCOL_VERSION: u32 = 0; + +/// The name of the header used to indicate the protocol version in use. +pub const PROTOCOL_VERSION_HEADER_NAME: &str = "x-zed-protocol-version"; + +/// A message from Cloud to the Zed client. +#[derive(Serialize, Deserialize)] +pub enum MessageToClient { + /// The user was updated and should be refreshed. + UserUpdated, +} + +impl MessageToClient { + pub fn serialize(&self) -> Result> { + let mut buffer = Vec::new(); + ciborium::into_writer(self, &mut buffer).context("failed to serialize message")?; + + Ok(buffer) + } + + pub fn deserialize(data: &[u8]) -> Result { + ciborium::from_reader(data).context("failed to deserialize message") + } +} From 8e290b446e025cea2c4f9d4f3175dd97ee39e1bf Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 6 Aug 2025 20:31:11 -0300 Subject: [PATCH 137/693] thread view: Add UI refinements (#35754) More notably around how we render tool calls. Nothing too drastic, though. Release Notes: - N/A --- .../icons/{tool_bulb.svg => tool_think.svg} | 0 assets/keymaps/default-linux.json | 4 +- assets/keymaps/default-macos.json | 4 +- crates/agent_ui/src/acp/thread_view.rs | 275 ++++++++++++------ crates/agent_ui/src/active_thread.rs | 2 +- crates/assistant_tools/src/thinking_tool.rs | 2 +- crates/icons/src/icons.rs | 2 +- crates/ui/src/components/disclosure.rs | 2 +- 8 files changed, 188 insertions(+), 103 deletions(-) rename assets/icons/{tool_bulb.svg => tool_think.svg} (100%) diff --git a/assets/icons/tool_bulb.svg b/assets/icons/tool_think.svg similarity index 100% rename from assets/icons/tool_bulb.svg rename to assets/icons/tool_think.svg diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 81f5c695a2..2a4c095124 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -332,7 +332,9 @@ "enter": "agent::Chat", "up": "agent::PreviousHistoryMessage", "down": "agent::NextHistoryMessage", - "shift-ctrl-r": "agent::OpenAgentDiff" + "shift-ctrl-r": "agent::OpenAgentDiff", + "ctrl-shift-y": "agent::KeepAll", + "ctrl-shift-n": "agent::RejectAll" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 69958fd1f8..1a6cda4b64 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -384,7 +384,9 @@ "enter": "agent::Chat", "up": "agent::PreviousHistoryMessage", "down": "agent::NextHistoryMessage", - "shift-ctrl-r": "agent::OpenAgentDiff" + "shift-ctrl-r": "agent::OpenAgentDiff", + "cmd-shift-y": "agent::KeepAll", + "cmd-shift-n": "agent::RejectAll" } }, { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 6475b7eeee..a9b39e6cea 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -858,6 +858,7 @@ impl AcpThreadView { .into_any() } AgentThreadEntry::ToolCall(tool_call) => div() + .w_full() .py_1p5() .px_5() .child(self.render_tool_call(index, tool_call, window, cx)) @@ -903,6 +904,7 @@ impl AcpThreadView { cx: &Context, ) -> AnyElement { let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix)); + let card_header_id = SharedString::from("inner-card-header"); let key = (entry_ix, chunk_ix); let is_open = self.expanded_thinking_blocks.contains(&key); @@ -910,41 +912,53 @@ impl AcpThreadView { .child( h_flex() .id(header_id) - .group("disclosure-header") + .group(&card_header_id) + .relative() .w_full() - .justify_between() + .gap_1p5() .opacity(0.8) .hover(|style| style.opacity(1.)) .child( h_flex() - .gap_1p5() - .child( - Icon::new(IconName::ToolBulb) - .size(IconSize::Small) - .color(Color::Muted), - ) + .size_4() + .justify_center() .child( div() - .text_size(self.tool_name_font_size()) - .child("Thinking"), + .group_hover(&card_header_id, |s| s.invisible().w_0()) + .child( + Icon::new(IconName::ToolThink) + .size(IconSize::Small) + .color(Color::Muted), + ), + ) + .child( + h_flex() + .absolute() + .inset_0() + .invisible() + .justify_center() + .group_hover(&card_header_id, |s| s.visible()) + .child( + Disclosure::new(("expand", entry_ix), is_open) + .opened_icon(IconName::ChevronUp) + .closed_icon(IconName::ChevronRight) + .on_click(cx.listener({ + move |this, _event, _window, cx| { + if is_open { + this.expanded_thinking_blocks.remove(&key); + } else { + this.expanded_thinking_blocks.insert(key); + } + cx.notify(); + } + })), + ), ), ) .child( - div().visible_on_hover("disclosure-header").child( - Disclosure::new("thinking-disclosure", is_open) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .on_click(cx.listener({ - move |this, _event, _window, cx| { - if is_open { - this.expanded_thinking_blocks.remove(&key); - } else { - this.expanded_thinking_blocks.insert(key); - } - cx.notify(); - } - })), - ), + div() + .text_size(self.tool_name_font_size()) + .child("Thinking"), ) .on_click(cx.listener({ move |this, _event, _window, cx| { @@ -975,6 +989,67 @@ impl AcpThreadView { .into_any_element() } + fn render_tool_call_icon( + &self, + group_name: SharedString, + entry_ix: usize, + is_collapsible: bool, + is_open: bool, + tool_call: &ToolCall, + cx: &Context, + ) -> Div { + let tool_icon = Icon::new(match tool_call.kind { + acp::ToolKind::Read => IconName::ToolRead, + acp::ToolKind::Edit => IconName::ToolPencil, + acp::ToolKind::Delete => IconName::ToolDeleteFile, + acp::ToolKind::Move => IconName::ArrowRightLeft, + acp::ToolKind::Search => IconName::ToolSearch, + acp::ToolKind::Execute => IconName::ToolTerminal, + acp::ToolKind::Think => IconName::ToolThink, + acp::ToolKind::Fetch => IconName::ToolWeb, + acp::ToolKind::Other => IconName::ToolHammer, + }) + .size(IconSize::Small) + .color(Color::Muted); + + if is_collapsible { + h_flex() + .size_4() + .justify_center() + .child( + div() + .group_hover(&group_name, |s| s.invisible().w_0()) + .child(tool_icon), + ) + .child( + h_flex() + .absolute() + .inset_0() + .invisible() + .justify_center() + .group_hover(&group_name, |s| s.visible()) + .child( + Disclosure::new(("expand", entry_ix), is_open) + .opened_icon(IconName::ChevronUp) + .closed_icon(IconName::ChevronRight) + .on_click(cx.listener({ + let id = tool_call.id.clone(); + move |this: &mut Self, _, _, cx: &mut Context| { + if is_open { + this.expanded_tool_calls.remove(&id); + } else { + this.expanded_tool_calls.insert(id.clone()); + } + cx.notify(); + } + })), + ), + ) + } else { + div().child(tool_icon) + } + } + fn render_tool_call( &self, entry_ix: usize, @@ -982,7 +1057,8 @@ impl AcpThreadView { window: &Window, cx: &Context, ) -> Div { - let header_id = SharedString::from(format!("tool-call-header-{}", entry_ix)); + let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix)); + let card_header_id = SharedString::from("inner-tool-call-header"); let status_icon = match &tool_call.status { ToolCallStatus::Allowed { @@ -1031,6 +1107,21 @@ impl AcpThreadView { let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation; let is_open = !is_collapsible || self.expanded_tool_calls.contains(&tool_call.id); + let gradient_color = cx.theme().colors().panel_background; + let gradient_overlay = { + div() + .absolute() + .top_0() + .right_0() + .w_12() + .h_full() + .bg(linear_gradient( + 90., + linear_color_stop(gradient_color, 1.), + linear_color_stop(gradient_color.opacity(0.2), 0.), + )) + }; + v_flex() .when(needs_confirmation, |this| { this.rounded_lg() @@ -1047,43 +1138,38 @@ impl AcpThreadView { .justify_between() .map(|this| { if needs_confirmation { - this.px_2() + this.pl_2() + .pr_1() .py_1() .rounded_t_md() - .bg(self.tool_card_header_bg(cx)) .border_b_1() .border_color(self.tool_card_border_color(cx)) + .bg(self.tool_card_header_bg(cx)) } else { this.opacity(0.8).hover(|style| style.opacity(1.)) } }) .child( h_flex() - .id("tool-call-header") - .overflow_x_scroll() + .group(&card_header_id) + .relative() + .w_full() .map(|this| { - if needs_confirmation { - this.text_xs() + if tool_call.locations.len() == 1 { + this.gap_0() } else { - this.text_size(self.tool_name_font_size()) + this.gap_1p5() } }) - .gap_1p5() - .child( - Icon::new(match tool_call.kind { - acp::ToolKind::Read => IconName::ToolRead, - acp::ToolKind::Edit => IconName::ToolPencil, - acp::ToolKind::Delete => IconName::ToolDeleteFile, - acp::ToolKind::Move => IconName::ArrowRightLeft, - acp::ToolKind::Search => IconName::ToolSearch, - acp::ToolKind::Execute => IconName::ToolTerminal, - acp::ToolKind::Think => IconName::ToolBulb, - acp::ToolKind::Fetch => IconName::ToolWeb, - acp::ToolKind::Other => IconName::ToolHammer, - }) - .size(IconSize::Small) - .color(Color::Muted), - ) + .text_size(self.tool_name_font_size()) + .child(self.render_tool_call_icon( + card_header_id, + entry_ix, + is_collapsible, + is_open, + tool_call, + cx, + )) .child(if tool_call.locations.len() == 1 { let name = tool_call.locations[0] .path @@ -1094,13 +1180,11 @@ impl AcpThreadView { h_flex() .id(("open-tool-call-location", entry_ix)) - .child(name) .w_full() .max_w_full() - .pr_1() - .gap_0p5() - .cursor_pointer() + .px_1p5() .rounded_sm() + .overflow_x_scroll() .opacity(0.8) .hover(|label| { label.opacity(1.).bg(cx @@ -1109,53 +1193,49 @@ impl AcpThreadView { .element_hover .opacity(0.5)) }) + .child(name) .tooltip(Tooltip::text("Jump to File")) .on_click(cx.listener(move |this, _, window, cx| { this.open_tool_call_location(entry_ix, 0, window, cx); })) .into_any_element() } else { - self.render_markdown( - tool_call.label.clone(), - default_markdown_style(needs_confirmation, window, cx), - ) - .into_any() + h_flex() + .id("non-card-label-container") + .w_full() + .relative() + .overflow_hidden() + .child( + h_flex() + .id("non-card-label") + .pr_8() + .w_full() + .overflow_x_scroll() + .child(self.render_markdown( + tool_call.label.clone(), + default_markdown_style( + needs_confirmation, + window, + cx, + ), + )), + ) + .child(gradient_overlay) + .on_click(cx.listener({ + let id = tool_call.id.clone(); + move |this: &mut Self, _, _, cx: &mut Context| { + if is_open { + this.expanded_tool_calls.remove(&id); + } else { + this.expanded_tool_calls.insert(id.clone()); + } + cx.notify(); + } + })) + .into_any() }), ) - .child( - h_flex() - .gap_0p5() - .when(is_collapsible, |this| { - this.child( - Disclosure::new(("expand", entry_ix), is_open) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .on_click(cx.listener({ - let id = tool_call.id.clone(); - move |this: &mut Self, _, _, cx: &mut Context| { - if is_open { - this.expanded_tool_calls.remove(&id); - } else { - this.expanded_tool_calls.insert(id.clone()); - } - cx.notify(); - } - })), - ) - }) - .children(status_icon), - ) - .on_click(cx.listener({ - let id = tool_call.id.clone(); - move |this: &mut Self, _, _, cx: &mut Context| { - if is_open { - this.expanded_tool_calls.remove(&id); - } else { - this.expanded_tool_calls.insert(id.clone()); - } - cx.notify(); - } - })), + .children(status_icon), ) .when(is_open, |this| { this.child( @@ -1249,8 +1329,7 @@ impl AcpThreadView { cx: &Context, ) -> Div { h_flex() - .py_1p5() - .px_1p5() + .p_1p5() .gap_1() .justify_end() .when(!empty_content, |this| { @@ -1276,6 +1355,7 @@ impl AcpThreadView { }) .icon_position(IconPosition::Start) .icon_size(IconSize::XSmall) + .label_size(LabelSize::Small) .on_click(cx.listener({ let tool_call_id = tool_call_id.clone(); let option_id = option.id.clone(); @@ -1525,7 +1605,7 @@ impl AcpThreadView { }) }) .when(!changed_buffers.is_empty(), |this| { - this.child(Divider::horizontal()) + this.child(Divider::horizontal().color(DividerColor::Border)) .child(self.render_edits_summary( action_log, &changed_buffers, @@ -1555,6 +1635,7 @@ impl AcpThreadView { { h_flex() .w_full() + .cursor_default() .gap_1() .text_xs() .text_color(cx.theme().colors().text_muted) @@ -1584,7 +1665,7 @@ impl AcpThreadView { let status_label = if stats.pending == 0 { "All Done".to_string() } else if stats.completed == 0 { - format!("{}", plan.entries.len()) + format!("{} Tasks", plan.entries.len()) } else { format!("{}/{}", stats.completed, plan.entries.len()) }; @@ -1698,7 +1779,6 @@ impl AcpThreadView { .child( h_flex() .id("edits-container") - .cursor_pointer() .w_full() .gap_1() .child(Disclosure::new("edits-disclosure", expanded)) @@ -2473,6 +2553,7 @@ impl AcpThreadView { })); h_flex() + .w_full() .mr_1() .pb_2() .px(RESPONSE_PADDING_X) diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index c4d4e72252..71526c8fe1 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -2624,7 +2624,7 @@ impl ActiveThread { h_flex() .gap_1p5() .child( - Icon::new(IconName::ToolBulb) + Icon::new(IconName::ToolThink) .size(IconSize::Small) .color(Color::Muted), ) diff --git a/crates/assistant_tools/src/thinking_tool.rs b/crates/assistant_tools/src/thinking_tool.rs index 443c2930be..76c6e6c0ba 100644 --- a/crates/assistant_tools/src/thinking_tool.rs +++ b/crates/assistant_tools/src/thinking_tool.rs @@ -37,7 +37,7 @@ impl Tool for ThinkingTool { } fn icon(&self) -> IconName { - IconName::ToolBulb + IconName::ToolThink } fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index a94d89bdc8..12805e62e0 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -261,7 +261,6 @@ pub enum IconName { TodoComplete, TodoPending, TodoProgress, - ToolBulb, ToolCopy, ToolDeleteFile, ToolDiagnostics, @@ -273,6 +272,7 @@ pub enum IconName { ToolRegex, ToolSearch, ToolTerminal, + ToolThink, ToolWeb, Trash, Triangle, diff --git a/crates/ui/src/components/disclosure.rs b/crates/ui/src/components/disclosure.rs index a1fab02e54..98406cd1e2 100644 --- a/crates/ui/src/components/disclosure.rs +++ b/crates/ui/src/components/disclosure.rs @@ -95,7 +95,7 @@ impl RenderOnce for Disclosure { impl Component for Disclosure { fn scope() -> ComponentScope { - ComponentScope::Navigation + ComponentScope::Input } fn description() -> Option<&'static str> { From c595a7576d5002bdeede53537f0e3d5d26b87a95 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Aug 2025 17:30:36 -0700 Subject: [PATCH 138/693] Fix git hunk staging on windows (#35755) We were failing to flush the Git process's `stdin` before dropping it. Release Notes: - N/A --- crates/git/src/repository.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index b536bed710..dc7ab0af65 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -846,14 +846,12 @@ impl GitRepository for RealGitRepository { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn()?; - child - .stdin - .take() - .unwrap() - .write_all(content.as_bytes()) - .await?; + let mut stdin = child.stdin.take().unwrap(); + stdin.write_all(content.as_bytes()).await?; + stdin.flush().await?; + drop(stdin); let output = child.output().await?.stdout; - let sha = String::from_utf8(output)?; + let sha = str::from_utf8(&output)?.trim(); log::debug!("indexing SHA: {sha}, path {path:?}"); @@ -871,6 +869,7 @@ impl GitRepository for RealGitRepository { String::from_utf8_lossy(&output.stderr) ); } else { + log::debug!("removing path {path:?} from the index"); let output = new_smol_command(&git_binary_path) .current_dir(&working_directory) .envs(env.iter()) @@ -921,6 +920,7 @@ impl GitRepository for RealGitRepository { for rev in &revs { write!(&mut stdin, "{rev}\n")?; } + stdin.flush()?; drop(stdin); let output = process.wait_with_output()?; From 1907b16fe647e7e25f4833fe03e1b34ff18fd73a Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Wed, 6 Aug 2025 21:28:41 -0400 Subject: [PATCH 139/693] Establish WebSocket connection to Cloud (#35734) This PR adds a new WebSocket connection to Cloud. This connection will be used to push down notifications from the server to the client. Release Notes: - N/A --------- Co-authored-by: Marshall Bowers --- Cargo.lock | 48 +++++++++++- Cargo.toml | 3 + crates/client/src/client.rs | 35 +++++++++ crates/cloud_api_client/Cargo.toml | 3 + .../cloud_api_client/src/cloud_api_client.rs | 43 +++++++++++ crates/cloud_api_client/src/websocket.rs | 73 +++++++++++++++++++ .../cloud_api_types/src/websocket_protocol.rs | 2 +- tooling/workspace-hack/Cargo.toml | 24 +++--- 8 files changed, 214 insertions(+), 17 deletions(-) create mode 100644 crates/cloud_api_client/src/websocket.rs diff --git a/Cargo.lock b/Cargo.lock index b73b89bde6..e15026d8d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1411,7 +1411,7 @@ dependencies = [ "anyhow", "arrayvec", "log", - "nom", + "nom 7.1.3", "num-rational", "v_frame", ] @@ -2785,7 +2785,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -3071,10 +3071,13 @@ dependencies = [ "anyhow", "cloud_api_types", "futures 0.3.31", + "gpui", + "gpui_tokio", "http_client", "parking_lot", "serde_json", "workspace-hack", + "yawc", ] [[package]] @@ -10582,6 +10585,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "noop_proc_macro" version = "0.3.0" @@ -15403,7 +15415,7 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" dependencies = [ - "nom", + "nom 7.1.3", "unicode_categories", ] @@ -19979,7 +19991,7 @@ dependencies = [ "naga", "nix 0.28.0", "nix 0.29.0", - "nom", + "nom 7.1.3", "num-bigint", "num-bigint-dig", "num-integer", @@ -20314,6 +20326,34 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yawc" +version = "0.2.4" +source = "git+https://github.com/deviant-forks/yawc?rev=1899688f3e69ace4545aceb97b2a13881cf26142#1899688f3e69ace4545aceb97b2a13881cf26142" +dependencies = [ + "base64 0.22.1", + "bytes 1.10.1", + "flate2", + "futures 0.3.31", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "js-sys", + "nom 8.0.0", + "pin-project", + "rand 0.8.5", + "sha1", + "thiserror 1.0.69", + "tokio", + "tokio-rustls 0.26.2", + "tokio-util", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + [[package]] name = "yazi" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index a60a65fcd8..a04d8f6099 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -661,6 +661,9 @@ which = "6.0.0" windows-core = "0.61" wit-component = "0.221" workspace-hack = "0.1.0" +# We can switch back to the published version once https://github.com/infinitefield/yawc/pull/16 is merged and a new +# version is released. +yawc = { git = "https://github.com/deviant-forks/yawc", rev = "1899688f3e69ace4545aceb97b2a13881cf26142" } zstd = "0.11" [workspace.dependencies.async-stripe] diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index b4894cddcf..0480ed1c3e 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -14,6 +14,7 @@ use async_tungstenite::tungstenite::{ }; use clock::SystemClock; use cloud_api_client::CloudApiClient; +use cloud_api_client::websocket_protocol::MessageToClient; use credentials_provider::CredentialsProvider; use futures::{ AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt, @@ -933,6 +934,32 @@ impl Client { } } + /// Establishes a WebSocket connection with Cloud for receiving updates from the server. + async fn connect_to_cloud(self: &Arc, cx: &AsyncApp) -> Result<()> { + let connect_task = cx.update({ + let cloud_client = self.cloud_client.clone(); + move |cx| cloud_client.connect(cx) + })??; + let connection = connect_task.await?; + + let (mut messages, task) = cx.update(|cx| connection.spawn(cx))?; + task.detach(); + + cx.spawn({ + let this = self.clone(); + async move |cx| { + while let Some(message) = messages.next().await { + if let Some(message) = message.log_err() { + this.handle_message_to_client(message, cx); + } + } + } + }) + .detach(); + + Ok(()) + } + /// Performs a sign-in and also connects to Collab. /// /// This is called in places where we *don't* need to connect in the future. We will replace these calls with calls @@ -944,6 +971,8 @@ impl Client { ) -> Result<()> { let credentials = self.sign_in(try_provider, cx).await?; + self.connect_to_cloud(cx).await.log_err(); + let connect_result = match self.connect_with_credentials(credentials, cx).await { ConnectionResult::Timeout => Err(anyhow!("connection timed out")), ConnectionResult::ConnectionReset => Err(anyhow!("connection reset")), @@ -1622,6 +1651,12 @@ impl Client { } } + fn handle_message_to_client(self: &Arc, message: MessageToClient, _cx: &AsyncApp) { + match message { + MessageToClient::UserUpdated => {} + } + } + pub fn telemetry(&self) -> &Arc { &self.telemetry } diff --git a/crates/cloud_api_client/Cargo.toml b/crates/cloud_api_client/Cargo.toml index d56aa94c6e..8e50ccb191 100644 --- a/crates/cloud_api_client/Cargo.toml +++ b/crates/cloud_api_client/Cargo.toml @@ -15,7 +15,10 @@ path = "src/cloud_api_client.rs" anyhow.workspace = true cloud_api_types.workspace = true futures.workspace = true +gpui.workspace = true +gpui_tokio.workspace = true http_client.workspace = true parking_lot.workspace = true serde_json.workspace = true workspace-hack.workspace = true +yawc.workspace = true diff --git a/crates/cloud_api_client/src/cloud_api_client.rs b/crates/cloud_api_client/src/cloud_api_client.rs index edac051a0e..ef9a1a9a55 100644 --- a/crates/cloud_api_client/src/cloud_api_client.rs +++ b/crates/cloud_api_client/src/cloud_api_client.rs @@ -1,11 +1,19 @@ +mod websocket; + use std::sync::Arc; use anyhow::{Context, Result, anyhow}; +use cloud_api_types::websocket_protocol::{PROTOCOL_VERSION, PROTOCOL_VERSION_HEADER_NAME}; pub use cloud_api_types::*; use futures::AsyncReadExt as _; +use gpui::{App, Task}; +use gpui_tokio::Tokio; use http_client::http::request; use http_client::{AsyncBody, HttpClientWithUrl, Method, Request, StatusCode}; use parking_lot::RwLock; +use yawc::WebSocket; + +use crate::websocket::Connection; struct Credentials { user_id: u32, @@ -78,6 +86,41 @@ impl CloudApiClient { Ok(serde_json::from_str(&body)?) } + pub fn connect(&self, cx: &App) -> Result>> { + let mut connect_url = self + .http_client + .build_zed_cloud_url("/client/users/connect", &[])?; + connect_url + .set_scheme(match connect_url.scheme() { + "https" => "wss", + "http" => "ws", + scheme => Err(anyhow!("invalid URL scheme: {scheme}"))?, + }) + .map_err(|_| anyhow!("failed to set URL scheme"))?; + + let credentials = self.credentials.read(); + let credentials = credentials.as_ref().context("no credentials provided")?; + let authorization_header = format!("{} {}", credentials.user_id, credentials.access_token); + + Ok(cx.spawn(async move |cx| { + let handle = cx + .update(|cx| Tokio::handle(cx)) + .ok() + .context("failed to get Tokio handle")?; + let _guard = handle.enter(); + + let ws = WebSocket::connect(connect_url) + .with_request( + request::Builder::new() + .header("Authorization", authorization_header) + .header(PROTOCOL_VERSION_HEADER_NAME, PROTOCOL_VERSION.to_string()), + ) + .await?; + + Ok(Connection::new(ws)) + })) + } + pub async fn accept_terms_of_service(&self) -> Result { let request = self.build_request( Request::builder().method(Method::POST).uri( diff --git a/crates/cloud_api_client/src/websocket.rs b/crates/cloud_api_client/src/websocket.rs new file mode 100644 index 0000000000..48a628db78 --- /dev/null +++ b/crates/cloud_api_client/src/websocket.rs @@ -0,0 +1,73 @@ +use std::pin::Pin; +use std::time::Duration; + +use anyhow::Result; +use cloud_api_types::websocket_protocol::MessageToClient; +use futures::channel::mpsc::unbounded; +use futures::stream::{SplitSink, SplitStream}; +use futures::{FutureExt as _, SinkExt as _, Stream, StreamExt as _, TryStreamExt as _, pin_mut}; +use gpui::{App, BackgroundExecutor, Task}; +use yawc::WebSocket; +use yawc::frame::{FrameView, OpCode}; + +const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1); + +pub type MessageStream = Pin>>>; + +pub struct Connection { + tx: SplitSink, + rx: SplitStream, +} + +impl Connection { + pub fn new(ws: WebSocket) -> Self { + let (tx, rx) = ws.split(); + + Self { tx, rx } + } + + pub fn spawn(self, cx: &App) -> (MessageStream, Task<()>) { + let (mut tx, rx) = (self.tx, self.rx); + + let (message_tx, message_rx) = unbounded(); + + let handle_io = |executor: BackgroundExecutor| async move { + // Send messages on this frequency so the connection isn't closed. + let keepalive_timer = executor.timer(KEEPALIVE_INTERVAL).fuse(); + futures::pin_mut!(keepalive_timer); + + let rx = rx.fuse(); + pin_mut!(rx); + + loop { + futures::select_biased! { + _ = keepalive_timer => { + let _ = tx.send(FrameView::ping(Vec::new())).await; + + keepalive_timer.set(executor.timer(KEEPALIVE_INTERVAL).fuse()); + } + frame = rx.next() => { + let Some(frame) = frame else { + break; + }; + + match frame.opcode { + OpCode::Binary => { + let message_result = MessageToClient::deserialize(&frame.payload); + message_tx.unbounded_send(message_result).ok(); + } + OpCode::Close => { + break; + } + _ => {} + } + } + } + } + }; + + let task = cx.spawn(async move |cx| handle_io(cx.background_executor().clone()).await); + + (message_rx.into_stream().boxed(), task) + } +} diff --git a/crates/cloud_api_types/src/websocket_protocol.rs b/crates/cloud_api_types/src/websocket_protocol.rs index c90d09e370..75f6a73b43 100644 --- a/crates/cloud_api_types/src/websocket_protocol.rs +++ b/crates/cloud_api_types/src/websocket_protocol.rs @@ -8,7 +8,7 @@ pub const PROTOCOL_VERSION: u32 = 0; pub const PROTOCOL_VERSION_HEADER_NAME: &str = "x-zed-protocol-version"; /// A message from Cloud to the Zed client. -#[derive(Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub enum MessageToClient { /// The user was updated and should be refreshed. UserUpdated, diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index 5678e46236..338985ed95 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -305,7 +305,7 @@ scopeguard = { version = "1" } security-framework = { version = "3", features = ["OSX_10_14"] } security-framework-sys = { version = "2", features = ["OSX_10_14"] } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } +tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } tokio-socks = { version = "0.5", features = ["futures-io"] } tokio-stream = { version = "0.1", features = ["fs"] } tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } @@ -334,7 +334,7 @@ scopeguard = { version = "1" } security-framework = { version = "3", features = ["OSX_10_14"] } security-framework-sys = { version = "2", features = ["OSX_10_14"] } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } +tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } tokio-socks = { version = "0.5", features = ["futures-io"] } tokio-stream = { version = "0.1", features = ["fs"] } tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } @@ -362,7 +362,7 @@ scopeguard = { version = "1" } security-framework = { version = "3", features = ["OSX_10_14"] } security-framework-sys = { version = "2", features = ["OSX_10_14"] } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } +tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } tokio-socks = { version = "0.5", features = ["futures-io"] } tokio-stream = { version = "0.1", features = ["fs"] } tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } @@ -391,7 +391,7 @@ scopeguard = { version = "1" } security-framework = { version = "3", features = ["OSX_10_14"] } security-framework-sys = { version = "2", features = ["OSX_10_14"] } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } +tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } tokio-socks = { version = "0.5", features = ["futures-io"] } tokio-stream = { version = "0.1", features = ["fs"] } tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } @@ -429,7 +429,7 @@ rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", scopeguard = { version = "1" } syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } +tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } tokio-socks = { version = "0.5", features = ["futures-io"] } tokio-stream = { version = "0.1", features = ["fs"] } toml_datetime = { version = "0.6", default-features = false, features = ["serde"] } @@ -468,7 +468,7 @@ rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["ev rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] } scopeguard = { version = "1" } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } +tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } tokio-socks = { version = "0.5", features = ["futures-io"] } tokio-stream = { version = "0.1", features = ["fs"] } toml_datetime = { version = "0.6", default-features = false, features = ["serde"] } @@ -509,7 +509,7 @@ rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", scopeguard = { version = "1" } syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } +tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } tokio-socks = { version = "0.5", features = ["futures-io"] } tokio-stream = { version = "0.1", features = ["fs"] } toml_datetime = { version = "0.6", default-features = false, features = ["serde"] } @@ -548,7 +548,7 @@ rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["ev rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] } scopeguard = { version = "1" } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } +tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } tokio-socks = { version = "0.5", features = ["futures-io"] } tokio-stream = { version = "0.1", features = ["fs"] } toml_datetime = { version = "0.6", default-features = false, features = ["serde"] } @@ -568,7 +568,7 @@ ring = { version = "0.17", features = ["std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] } scopeguard = { version = "1" } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } +tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } tokio-socks = { version = "0.5", features = ["futures-io"] } tokio-stream = { version = "0.1", features = ["fs"] } tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } @@ -592,7 +592,7 @@ ring = { version = "0.17", features = ["std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] } scopeguard = { version = "1" } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } +tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } tokio-socks = { version = "0.5", features = ["futures-io"] } tokio-stream = { version = "0.1", features = ["fs"] } tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } @@ -636,7 +636,7 @@ rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", scopeguard = { version = "1" } syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } +tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } tokio-socks = { version = "0.5", features = ["futures-io"] } tokio-stream = { version = "0.1", features = ["fs"] } toml_datetime = { version = "0.6", default-features = false, features = ["serde"] } @@ -675,7 +675,7 @@ rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["ev rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] } scopeguard = { version = "1" } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } +tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } tokio-socks = { version = "0.5", features = ["futures-io"] } tokio-stream = { version = "0.1", features = ["fs"] } toml_datetime = { version = "0.6", default-features = false, features = ["serde"] } From bd1c26cb5b28e91b3b6c9420afe0cbb6219e16a2 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 6 Aug 2025 22:55:17 -0300 Subject: [PATCH 140/693] Fix interrupting ACP threads and CC cancellation (#35752) Fixes a bug where generation wouldn't continue after interrupting the agent, and improves CC cancellation so we don't display "[Request interrupted by user]" Release Notes: - N/A --------- Co-authored-by: Cole Miller --- crates/acp_thread/src/acp_thread.rs | 54 ++++---- crates/agent_servers/src/claude.rs | 197 ++++++++++++++++++++-------- 2 files changed, 171 insertions(+), 80 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index aa17a80b5b..be9952fd55 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -6,6 +6,7 @@ use anyhow::{Context as _, Result}; use assistant_tool::ActionLog; use buffer_diff::BufferDiff; use editor::{Bias, MultiBuffer, PathKey}; +use futures::future::{Fuse, FusedFuture}; use futures::{FutureExt, channel::oneshot, future::BoxFuture}; use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task}; use itertools::Itertools; @@ -572,7 +573,7 @@ pub struct AcpThread { project: Entity, action_log: Entity, shared_buffers: HashMap, BufferSnapshot>, - send_task: Option>, + send_task: Option>>, connection: Rc, session_id: acp::SessionId, } @@ -662,7 +663,11 @@ impl AcpThread { } pub fn status(&self) -> ThreadStatus { - if self.send_task.is_some() { + if self + .send_task + .as_ref() + .map_or(false, |t| !t.is_terminated()) + { if self.waiting_for_tool_confirmation() { ThreadStatus::WaitingForToolConfirmation } else { @@ -1037,28 +1042,31 @@ impl AcpThread { let (tx, rx) = oneshot::channel(); let cancel_task = self.cancel(cx); - self.send_task = Some(cx.spawn(async move |this, cx| { - async { - cancel_task.await; + self.send_task = Some( + cx.spawn(async move |this, cx| { + async { + cancel_task.await; - let result = this - .update(cx, |this, cx| { - this.connection.prompt( - acp::PromptRequest { - prompt: message, - session_id: this.session_id.clone(), - }, - cx, - ) - })? - .await; - tx.send(result).log_err(); - this.update(cx, |this, _cx| this.send_task.take())?; - anyhow::Ok(()) - } - .await - .log_err(); - })); + let result = this + .update(cx, |this, cx| { + this.connection.prompt( + acp::PromptRequest { + prompt: message, + session_id: this.session_id.clone(), + }, + cx, + ) + })? + .await; + + tx.send(result).log_err(); + anyhow::Ok(()) + } + .await + .log_err(); + }) + .fuse(), + ); cx.spawn(async move |this, cx| match rx.await { Ok(Err(e)) => { diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 59e2e87433..09d08fdcf8 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -6,7 +6,7 @@ use context_server::listener::McpServerTool; use project::Project; use settings::SettingsStore; use smol::process::Child; -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::fmt::Display; use std::path::Path; use std::rc::Rc; @@ -24,7 +24,7 @@ use futures::{ }; use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; use serde::{Deserialize, Serialize}; -use util::ResultExt; +use util::{ResultExt, debug_panic}; use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig}; use crate::claude::tools::ClaudeTool; @@ -153,16 +153,20 @@ impl AgentConnection for ClaudeAgentConnection { }) .detach(); + let pending_cancellation = Rc::new(Cell::new(PendingCancellation::None)); + let end_turn_tx = Rc::new(RefCell::new(None)); let handler_task = cx.spawn({ let end_turn_tx = end_turn_tx.clone(); let mut thread_rx = thread_rx.clone(); + let cancellation_state = pending_cancellation.clone(); async move |cx| { while let Some(message) = incoming_message_rx.next().await { ClaudeAgentSession::handle_message( thread_rx.clone(), message, end_turn_tx.clone(), + cancellation_state.clone(), cx, ) .await @@ -189,6 +193,7 @@ impl AgentConnection for ClaudeAgentConnection { let session = ClaudeAgentSession { outgoing_tx, end_turn_tx, + pending_cancellation, _handler_task: handler_task, _mcp_server: Some(permission_mcp_server), }; @@ -255,7 +260,12 @@ impl AgentConnection for ClaudeAgentConnection { return Task::ready(Err(anyhow!(err))); } - cx.foreground_executor().spawn(async move { rx.await? }) + let cancellation_state = session.pending_cancellation.clone(); + cx.foreground_executor().spawn(async move { + let result = rx.await??; + cancellation_state.set(PendingCancellation::None); + Ok(result) + }) } fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) { @@ -265,18 +275,19 @@ impl AgentConnection for ClaudeAgentConnection { return; }; + let request_id = new_request_id(); + + session.pending_cancellation.set(PendingCancellation::Sent { + request_id: request_id.clone(), + }); + session .outgoing_tx - .unbounded_send(SdkMessage::new_interrupt_message()) + .unbounded_send(SdkMessage::ControlRequest { + request_id, + request: ControlRequest::Interrupt, + }) .log_err(); - - if let Some(end_turn_tx) = session.end_turn_tx.borrow_mut().take() { - end_turn_tx - .send(Ok(acp::PromptResponse { - stop_reason: acp::StopReason::Cancelled, - })) - .ok(); - } } } @@ -339,25 +350,107 @@ fn spawn_claude( struct ClaudeAgentSession { outgoing_tx: UnboundedSender, end_turn_tx: Rc>>>>, + pending_cancellation: Rc>, _mcp_server: Option, _handler_task: Task<()>, } +#[derive(Debug, Default, PartialEq)] +enum PendingCancellation { + #[default] + None, + Sent { + request_id: String, + }, + Confirmed, +} + impl ClaudeAgentSession { async fn handle_message( mut thread_rx: watch::Receiver>, message: SdkMessage, end_turn_tx: Rc>>>>, + pending_cancellation: Rc>, cx: &mut AsyncApp, ) { match message { // we should only be sending these out, they don't need to be in the thread SdkMessage::ControlRequest { .. } => {} - SdkMessage::Assistant { + SdkMessage::User { message, session_id: _, + } => { + let Some(thread) = thread_rx + .recv() + .await + .log_err() + .and_then(|entity| entity.upgrade()) + else { + log::error!("Received an SDK message but thread is gone"); + return; + }; + + for chunk in message.content.chunks() { + match chunk { + ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => { + let state = pending_cancellation.take(); + if state != PendingCancellation::Confirmed { + thread + .update(cx, |thread, cx| { + thread.push_user_content_block(text.into(), cx) + }) + .log_err(); + } + pending_cancellation.set(state); + } + ContentChunk::ToolResult { + content, + tool_use_id, + } => { + let content = content.to_string(); + thread + .update(cx, |thread, cx| { + thread.update_tool_call( + acp::ToolCallUpdate { + id: acp::ToolCallId(tool_use_id.into()), + fields: acp::ToolCallUpdateFields { + status: Some(acp::ToolCallStatus::Completed), + content: (!content.is_empty()) + .then(|| vec![content.into()]), + ..Default::default() + }, + }, + cx, + ) + }) + .log_err(); + } + ContentChunk::Thinking { .. } + | ContentChunk::RedactedThinking + | ContentChunk::ToolUse { .. } => { + debug_panic!( + "Should not get {:?} with role: assistant. should we handle this?", + chunk + ); + } + + ContentChunk::Image + | ContentChunk::Document + | ContentChunk::WebSearchToolResult => { + thread + .update(cx, |thread, cx| { + thread.push_assistant_content_block( + format!("Unsupported content: {:?}", chunk).into(), + false, + cx, + ) + }) + .log_err(); + } + } + } } - | SdkMessage::User { + SdkMessage::Assistant { message, session_id: _, } => { @@ -423,31 +516,12 @@ impl ClaudeAgentSession { }) .log_err(); } - ContentChunk::ToolResult { - content, - tool_use_id, - } => { - let content = content.to_string(); - thread - .update(cx, |thread, cx| { - thread.update_tool_call( - acp::ToolCallUpdate { - id: acp::ToolCallId(tool_use_id.into()), - fields: acp::ToolCallUpdateFields { - status: Some(acp::ToolCallStatus::Completed), - content: (!content.is_empty()) - .then(|| vec![content.into()]), - ..Default::default() - }, - }, - cx, - ) - }) - .log_err(); + ContentChunk::ToolResult { .. } | ContentChunk::WebSearchToolResult => { + debug_panic!( + "Should not get tool results with role: assistant. should we handle this?" + ); } - ContentChunk::Image - | ContentChunk::Document - | ContentChunk::WebSearchToolResult => { + ContentChunk::Image | ContentChunk::Document => { thread .update(cx, |thread, cx| { thread.push_assistant_content_block( @@ -468,7 +542,10 @@ impl ClaudeAgentSession { .. } => { if let Some(end_turn_tx) = end_turn_tx.borrow_mut().take() { - if is_error || subtype == ResultErrorType::ErrorDuringExecution { + if is_error + || (subtype == ResultErrorType::ErrorDuringExecution + && pending_cancellation.take() != PendingCancellation::Confirmed) + { end_turn_tx .send(Err(anyhow!( "Error: {}", @@ -479,7 +556,7 @@ impl ClaudeAgentSession { let stop_reason = match subtype { ResultErrorType::Success => acp::StopReason::EndTurn, ResultErrorType::ErrorMaxTurns => acp::StopReason::MaxTurnRequests, - ResultErrorType::ErrorDuringExecution => unreachable!(), + ResultErrorType::ErrorDuringExecution => acp::StopReason::Cancelled, }; end_turn_tx .send(Ok(acp::PromptResponse { stop_reason })) @@ -487,7 +564,20 @@ impl ClaudeAgentSession { } } } - SdkMessage::System { .. } | SdkMessage::ControlResponse { .. } => {} + SdkMessage::ControlResponse { response } => { + if matches!(response.subtype, ResultErrorType::Success) { + let pending_cancellation_value = pending_cancellation.take(); + + if let PendingCancellation::Sent { request_id } = &pending_cancellation_value + && request_id == &response.request_id + { + pending_cancellation.set(PendingCancellation::Confirmed); + } else { + pending_cancellation.set(pending_cancellation_value); + } + } + } + SdkMessage::System { .. } => {} } } @@ -728,22 +818,15 @@ impl Display for ResultErrorType { } } -impl SdkMessage { - fn new_interrupt_message() -> Self { - use rand::Rng; - // In the Claude Code TS SDK they just generate a random 12 character string, - // `Math.random().toString(36).substring(2, 15)` - let request_id = rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(12) - .map(char::from) - .collect(); - - Self::ControlRequest { - request_id, - request: ControlRequest::Interrupt, - } - } +fn new_request_id() -> String { + use rand::Rng; + // In the Claude Code TS SDK they just generate a random 12 character string, + // `Math.random().toString(36).substring(2, 15)` + rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(12) + .map(char::from) + .collect() } #[derive(Debug, Clone, Serialize, Deserialize)] From f1e69f631171098f89ef0a0f40c6d139a4f1b056 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 6 Aug 2025 23:24:37 -0400 Subject: [PATCH 141/693] gpui: Impl Default for ClickEvent (#35751) While default for ClickEvent shouldn't be used much this is helpful for other projects using gpui besides Zed. Mainly because the orphan rule prevents those projects from implementing their own default trait cc: @huacnlee Release Notes: - N/A --- crates/gpui/src/interactive.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/gpui/src/interactive.rs b/crates/gpui/src/interactive.rs index 46af946e69..218ae5fcdf 100644 --- a/crates/gpui/src/interactive.rs +++ b/crates/gpui/src/interactive.rs @@ -150,7 +150,7 @@ pub struct MouseClickEvent { } /// A click event that was generated by a keyboard button being pressed and released. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct KeyboardClickEvent { /// The keyboard button that was pressed to trigger the click. pub button: KeyboardButton, @@ -168,6 +168,12 @@ pub enum ClickEvent { Keyboard(KeyboardClickEvent), } +impl Default for ClickEvent { + fn default() -> Self { + ClickEvent::Keyboard(KeyboardClickEvent::default()) + } +} + impl ClickEvent { /// Returns the modifiers that were held during the click event /// @@ -256,9 +262,10 @@ impl ClickEvent { } /// An enum representing the keyboard button that was pressed for a click event. -#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)] +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, Default)] pub enum KeyboardButton { /// Enter key was clicked + #[default] Enter, /// Space key was clicked Space, From b4a441f12fad28ec472deade760025b00c1e8671 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 7 Aug 2025 06:52:22 +0200 Subject: [PATCH 142/693] Add UnwrapSyntaxNode action (#31421) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remake of #8967 > Hey there, > > I have started relying on this action, that I've also put into VSCode as [an extension](https://github.com/Gregoor/soy). On some level I don't know how people code (cope?) without it: > > Release Notes: > > * Added UnwrapSyntaxNode action > > https://github.com/zed-industries/zed/assets/4051932/d74c98c0-96d8-4075-9b63-cea55bea42f6 > > Since I had to put it into Zed anyway to make it my daily driver, I thought I'd also check here if there's an interest in shipping it by default (that would ofc also personally make my life better, not having to maintain my personal fork and all). > > If there is interest, I'd be happy to make any changes to make this more mergeable. Two TODOs on my mind are: > > * unwrap multiple into single (e.g. `fn(≤a≥, b)` to `fn(≤a≥)`) > * multi-cursor > * syntax awareness, i.e. only unwrap if it does not break syntax (I added [a coarse version of that for my VSC extension](https://github.com/Gregoor/soy/blob/main/src/actions/unwrap.ts#L29)) > > Somewhat off-topic: I was happy to see that you're [also](https://github.com/Gregoor/soy/blob/main/src/actions/unwrap.test.ts) using rare special chars in test code to denote cursor positions. Release Notes: - Added UnwrapSyntaxNode action --------- Co-authored-by: Peter Tripp --- crates/editor/src/actions.rs | 1 + crates/editor/src/editor.rs | 75 +++++++++++++++++++++++++++++++ crates/editor/src/editor_tests.rs | 32 +++++++++++++ crates/editor/src/element.rs | 1 + 4 files changed, 109 insertions(+) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 3a3a57ca64..39433b3c27 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -745,5 +745,6 @@ actions!( UniqueLinesCaseInsensitive, /// Removes duplicate lines (case-sensitive). UniqueLinesCaseSensitive, + UnwrapSyntaxNode ] ); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 156fda1b37..73a81bea19 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -14711,6 +14711,81 @@ impl Editor { } } + pub fn unwrap_syntax_node( + &mut self, + _: &UnwrapSyntaxNode, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + + let buffer = self.buffer.read(cx).snapshot(cx); + let old_selections: Box<[_]> = self.selections.all::(cx).into(); + + let edits = old_selections + .iter() + // only consider the first selection for now + .take(1) + .map(|selection| { + // Only requires two branches once if-let-chains stabilize (#53667) + let selection_range = if !selection.is_empty() { + selection.range() + } else if let Some((_, ancestor_range)) = + buffer.syntax_ancestor(selection.start..selection.end) + { + match ancestor_range { + MultiOrSingleBufferOffsetRange::Single(range) => range, + MultiOrSingleBufferOffsetRange::Multi(range) => range, + } + } else { + selection.range() + }; + + let mut new_range = selection_range.clone(); + while let Some((_, ancestor_range)) = buffer.syntax_ancestor(new_range.clone()) { + new_range = match ancestor_range { + MultiOrSingleBufferOffsetRange::Single(range) => range, + MultiOrSingleBufferOffsetRange::Multi(range) => range, + }; + if new_range.start < selection_range.start + || new_range.end > selection_range.end + { + break; + } + } + + (selection, selection_range, new_range) + }) + .collect::>(); + + self.transact(window, cx, |editor, window, cx| { + for (_, child, parent) in &edits { + let text = buffer.text_for_range(child.clone()).collect::(); + editor.replace_text_in_range(Some(parent.clone()), &text, window, cx); + } + + editor.change_selections( + SelectionEffects::scroll(Autoscroll::fit()), + window, + cx, + |s| { + s.select( + edits + .iter() + .map(|(s, old, new)| Selection { + id: s.id, + start: new.start, + end: new.start + old.len(), + goal: SelectionGoal::None, + reversed: s.reversed, + }) + .collect(), + ); + }, + ); + }); + } + fn refresh_runnables(&mut self, window: &mut Window, cx: &mut Context) -> Task<()> { if !EditorSettings::get_global(cx).gutter.runnables { self.clear_tasks(); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 1cb3565733..b31963c9c8 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -7969,6 +7969,38 @@ async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppConte }); } +#[gpui::test] +async fn test_unwrap_syntax_node(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let language = Arc::new(Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::LANGUAGE.into()), + )); + + cx.update_buffer(|buffer, cx| { + buffer.set_language(Some(language), cx); + }); + + cx.set_state( + &r#" + use mod1::mod2::{«mod3ˇ», mod4}; + "# + .unindent(), + ); + cx.update_editor(|editor, window, cx| { + editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx); + }); + cx.assert_editor_state( + &r#" + use mod1::mod2::«mod3ˇ»; + "# + .unindent(), + ); +} + #[gpui::test] async fn test_fold_function_bodies(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e1647215bc..17a43f9640 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -357,6 +357,7 @@ impl EditorElement { register_action(editor, window, Editor::toggle_comments); register_action(editor, window, Editor::select_larger_syntax_node); register_action(editor, window, Editor::select_smaller_syntax_node); + register_action(editor, window, Editor::unwrap_syntax_node); register_action(editor, window, Editor::select_enclosing_symbol); register_action(editor, window, Editor::move_to_enclosing_bracket); register_action(editor, window, Editor::undo_selection); From 5b1b3c51d4b3fb524d22cb58fca75a35730dc34a Mon Sep 17 00:00:00 2001 From: smit Date: Thu, 7 Aug 2025 11:39:27 +0530 Subject: [PATCH 143/693] language_models: Fix high memory consumption while using Agent Panel (#35764) Closes #31108 The `num_tokens_from_messages` method we use from `tiktoken-rs` creates new BPE every time that method is called. This creation of BPE is expensive as well as has some underlying issue that keeps memory from releasing once the method is finished, specifically noticeable on Linux. This leads to a gradual increase in memory every time that method is called in my case around +50MB on each call. We call this method with debounce every time user types in Agent Panel to calculate tokens. This can add up really fast. This PR lands quick fix, while I/maintainers figure out underlying issue. See upstream discussion: https://github.com/zurawiki/tiktoken-rs/issues/39. Here on fork https://github.com/zed-industries/tiktoken-rs/pull/1, instead of creating BPE instances every time that method is called, we use singleton BPE instances instead. So, whatever memory it is holding on to, at least that is only once per model. Before: Increase of 700MB+ on extensive use On init: prev-init First message: prev-first-call Extensive use: prev-extensive-use After: Increase of 50MB+ on extensive use On init: now-init First message: now-first-call Extensive use: now-extensive-use Release Notes: - Fixed issue where Agent Panel would cause high memory consumption over prolonged use. --- Cargo.lock | 5 ++--- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e15026d8d1..b74928e05d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16618,9 +16618,8 @@ dependencies = [ [[package]] name = "tiktoken-rs" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25563eeba904d770acf527e8b370fe9a5547bacd20ff84a0b6c3bc41288e5625" +version = "0.8.0" +source = "git+https://github.com/zed-industries/tiktoken-rs?rev=30c32a4522751699adeda0d5840c71c3b75ae73d#30c32a4522751699adeda0d5840c71c3b75ae73d" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/Cargo.toml b/Cargo.toml index a04d8f6099..86f1b8b0a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -601,7 +601,7 @@ sysinfo = "0.31.0" take-until = "0.2.0" tempfile = "3.20.0" thiserror = "2.0.12" -tiktoken-rs = "0.7.0" +tiktoken-rs = { git = "https://github.com/zed-industries/tiktoken-rs", rev = "30c32a4522751699adeda0d5840c71c3b75ae73d" } time = { version = "0.3", features = [ "macros", "parsing", From f5f837d39a61c1f70102ca2daa8ce5fad591258a Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 7 Aug 2025 12:38:58 +0200 Subject: [PATCH 144/693] languages: Fix rust completions not having proper detail labels (#35772) rust-analyzer changed the format here a bit some months ago which partially broke our nice detailed highlighted completion labels. The brings that back while also cleaning up the code a bit. Also fixes a bug where disabling rust-analyzers snippet callable completions would fully break them. Release Notes: - N/A --- crates/languages/src/rust.rs | 202 +++++++++++++++++++---------------- 1 file changed, 112 insertions(+), 90 deletions(-) diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 6545bf64a2..7fb6f44a5b 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -305,66 +305,63 @@ impl LspAdapter for RustLspAdapter { completion: &lsp::CompletionItem, language: &Arc, ) -> Option { - let detail = completion + // rust-analyzer calls these detail left and detail right in terms of where it expects things to be rendered + // this usually contains signatures of the thing to be completed + let detail_right = completion .label_details .as_ref() - .and_then(|detail| detail.detail.as_ref()) + .and_then(|detail| detail.description.as_ref()) .or(completion.detail.as_ref()) .map(|detail| detail.trim()); - let function_signature = completion + // this tends to contain alias and import information + let detail_left = completion .label_details .as_ref() - .and_then(|detail| detail.description.as_deref()) - .or(completion.detail.as_deref()); - match (detail, completion.kind) { - (Some(detail), Some(lsp::CompletionItemKind::FIELD)) => { + .and_then(|detail| detail.detail.as_deref()); + let mk_label = |text: String, runs| { + let filter_range = completion + .filter_text + .as_deref() + .and_then(|filter| { + completion + .label + .find(filter) + .map(|ix| ix..ix + filter.len()) + }) + .unwrap_or(0..completion.label.len()); + + CodeLabel { + text, + runs, + filter_range, + } + }; + let mut label = match (detail_right, completion.kind) { + (Some(signature), Some(lsp::CompletionItemKind::FIELD)) => { let name = &completion.label; - let text = format!("{name}: {detail}"); + let text = format!("{name}: {signature}"); let prefix = "struct S { "; let source = Rope::from(format!("{prefix}{text} }}")); let runs = language.highlight_text(&source, prefix.len()..prefix.len() + text.len()); - let filter_range = completion - .filter_text - .as_deref() - .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len())) - .unwrap_or(0..name.len()); - return Some(CodeLabel { - text, - runs, - filter_range, - }); + mk_label(text, runs) } ( - Some(detail), + Some(signature), Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE), ) if completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) => { let name = &completion.label; - let text = format!( - "{}: {}", - name, - completion.detail.as_deref().unwrap_or(detail) - ); + let text = format!("{name}: {signature}",); let prefix = "let "; let source = Rope::from(format!("{prefix}{text} = ();")); let runs = language.highlight_text(&source, prefix.len()..prefix.len() + text.len()); - let filter_range = completion - .filter_text - .as_deref() - .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len())) - .unwrap_or(0..name.len()); - return Some(CodeLabel { - text, - runs, - filter_range, - }); + mk_label(text, runs) } ( - Some(detail), + function_signature, Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD), ) => { - static REGEX: LazyLock = LazyLock::new(|| Regex::new("\\(…?\\)").unwrap()); const FUNCTION_PREFIXES: [&str; 6] = [ "async fn", "async unsafe fn", @@ -373,34 +370,27 @@ impl LspAdapter for RustLspAdapter { "unsafe fn", "fn", ]; - // Is it function `async`? - let fn_keyword = FUNCTION_PREFIXES.iter().find_map(|prefix| { - function_signature.as_ref().and_then(|signature| { - signature - .strip_prefix(*prefix) - .map(|suffix| (*prefix, suffix)) - }) + let fn_prefixed = FUNCTION_PREFIXES.iter().find_map(|&prefix| { + function_signature? + .strip_prefix(prefix) + .map(|suffix| (prefix, suffix)) }); // fn keyword should be followed by opening parenthesis. - if let Some((prefix, suffix)) = fn_keyword { - let mut text = REGEX.replace(&completion.label, suffix).to_string(); + if let Some((prefix, suffix)) = fn_prefixed { + let label = if let Some(label) = completion + .label + .strip_suffix("(…)") + .or_else(|| completion.label.strip_suffix("()")) + { + label + } else { + &completion.label + }; + let text = format!("{label}{suffix}"); let source = Rope::from(format!("{prefix} {text} {{}}")); let run_start = prefix.len() + 1; let runs = language.highlight_text(&source, run_start..run_start + text.len()); - if detail.starts_with("(") { - text.push(' '); - text.push_str(&detail); - } - let filter_range = completion - .filter_text - .as_deref() - .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len())) - .unwrap_or(0..completion.label.find('(').unwrap_or(text.len())); - return Some(CodeLabel { - filter_range, - text, - runs, - }); + mk_label(text, runs) } else if completion .detail .as_ref() @@ -410,20 +400,13 @@ impl LspAdapter for RustLspAdapter { let len = text.len(); let source = Rope::from(text.as_str()); let runs = language.highlight_text(&source, 0..len); - let filter_range = completion - .filter_text - .as_deref() - .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len())) - .unwrap_or(0..len); - return Some(CodeLabel { - filter_range, - text, - runs, - }); + mk_label(text, runs) + } else { + mk_label(completion.label.clone(), vec![]) } } - (_, Some(kind)) => { - let highlight_name = match kind { + (_, kind) => { + let highlight_name = kind.and_then(|kind| match kind { lsp::CompletionItemKind::STRUCT | lsp::CompletionItemKind::INTERFACE | lsp::CompletionItemKind::ENUM => Some("type"), @@ -433,27 +416,32 @@ impl LspAdapter for RustLspAdapter { Some("constant") } _ => None, - }; + }); - let mut label = completion.label.clone(); - if let Some(detail) = detail.filter(|detail| detail.starts_with("(")) { - label.push(' '); - label.push_str(detail); - } - let mut label = CodeLabel::plain(label, completion.filter_text.as_deref()); + let label = completion.label.clone(); + let mut runs = vec![]; if let Some(highlight_name) = highlight_name { let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name)?; - label.runs.push(( - 0..label.text.rfind('(').unwrap_or(completion.label.len()), + runs.push(( + 0..label.rfind('(').unwrap_or(completion.label.len()), highlight_id, )); } - - return Some(label); + mk_label(label, runs) + } + }; + + if let Some(detail_left) = detail_left { + label.text.push(' '); + if !detail_left.starts_with('(') { + label.text.push('('); + } + label.text.push_str(detail_left); + if !detail_left.ends_with(')') { + label.text.push(')'); } - _ => {} } - None + Some(label) } async fn label_for_symbol( @@ -1169,7 +1157,7 @@ mod tests { .await, Some(CodeLabel { text: "hello(&mut Option) -> Vec (use crate::foo)".to_string(), - filter_range: 0..5, + filter_range: 0..10, runs: vec![ (0..5, highlight_function), (7..10, highlight_keyword), @@ -1187,7 +1175,7 @@ mod tests { kind: Some(lsp::CompletionItemKind::FUNCTION), label: "hello(…)".to_string(), label_details: Some(CompletionItemLabelDetails { - detail: Some(" (use crate::foo)".into()), + detail: Some("(use crate::foo)".into()), description: Some("async fn(&mut Option) -> Vec".to_string()), }), ..Default::default() @@ -1197,7 +1185,7 @@ mod tests { .await, Some(CodeLabel { text: "hello(&mut Option) -> Vec (use crate::foo)".to_string(), - filter_range: 0..5, + filter_range: 0..10, runs: vec![ (0..5, highlight_function), (7..10, highlight_keyword), @@ -1234,7 +1222,7 @@ mod tests { kind: Some(lsp::CompletionItemKind::FUNCTION), label: "hello(…)".to_string(), label_details: Some(CompletionItemLabelDetails { - detail: Some(" (use crate::foo)".to_string()), + detail: Some("(use crate::foo)".to_string()), description: Some("fn(&mut Option) -> Vec".to_string()), }), @@ -1243,6 +1231,35 @@ mod tests { &language ) .await, + Some(CodeLabel { + text: "hello(&mut Option) -> Vec (use crate::foo)".to_string(), + filter_range: 0..10, + runs: vec![ + (0..5, highlight_function), + (7..10, highlight_keyword), + (11..17, highlight_type), + (18..19, highlight_type), + (25..28, highlight_type), + (29..30, highlight_type), + ], + }) + ); + + assert_eq!( + adapter + .label_for_completion( + &lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FUNCTION), + label: "hello".to_string(), + label_details: Some(CompletionItemLabelDetails { + detail: Some("(use crate::foo)".to_string()), + description: Some("fn(&mut Option) -> Vec".to_string()), + }), + ..Default::default() + }, + &language + ) + .await, Some(CodeLabel { text: "hello(&mut Option) -> Vec (use crate::foo)".to_string(), filter_range: 0..5, @@ -1274,9 +1291,14 @@ mod tests { ) .await, Some(CodeLabel { - text: "await.as_deref_mut()".to_string(), + text: "await.as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(), filter_range: 6..18, - runs: vec![], + runs: vec![ + (6..18, HighlightId(2)), + (20..23, HighlightId(1)), + (33..40, HighlightId(0)), + (45..46, HighlightId(0)) + ], }) ); From c397027ec2c143d6a6cba6797a9a709e1600befb Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 7 Aug 2025 15:56:10 +0300 Subject: [PATCH 145/693] Add `release_channel` into the span fields list (#35783) Follow-up of https://github.com/zed-industries/zed/pull/35729 Release Notes: - N/A Co-authored-by: Marshall Bowers --- crates/collab/src/rpc.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index dfa5859f51..078632d397 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -761,7 +761,8 @@ impl Server { login=field::Empty, impersonator=field::Empty, user_agent=field::Empty, - geoip_country_code=field::Empty + geoip_country_code=field::Empty, + release_channel=field::Empty, ); principal.update_span(&span); if let Some(user_agent) = user_agent { From 4dbd24d75f61345996eb3ad0d92b46fb854a3c4c Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 7 Aug 2025 15:24:29 +0200 Subject: [PATCH 146/693] Reduce amount of allocations in RustLsp label handling (#35786) There can be a lot of completions after all Release Notes: - N/A --- crates/diagnostics/src/diagnostics_tests.rs | 2 +- crates/editor/src/display_map/inlay_map.rs | 16 +++--- crates/editor/src/inlay_hint_cache.rs | 2 +- crates/editor/src/signature_help.rs | 2 +- crates/languages/src/rust.rs | 63 +++++---------------- crates/project/src/project.rs | 9 ++- crates/project/src/project_tests.rs | 1 + crates/rope/src/rope.rs | 8 +++ crates/text/src/text.rs | 2 +- 9 files changed, 40 insertions(+), 65 deletions(-) diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 1bb84488e8..8fb223b2cb 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -876,7 +876,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S vec![Inlay::edit_prediction( post_inc(&mut next_inlay_id), snapshot.buffer_snapshot.anchor_before(position), - format!("Test inlay {next_inlay_id}"), + Rope::from_iter(["Test inlay ", "next_inlay_id"]), )], cx, ); diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index fd49c262c6..b296b3e62a 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -48,16 +48,16 @@ pub struct Inlay { impl Inlay { pub fn hint(id: usize, position: Anchor, hint: &project::InlayHint) -> Self { let mut text = hint.text(); - if hint.padding_right && !text.ends_with(' ') { - text.push(' '); + if hint.padding_right && text.chars_at(text.len().saturating_sub(1)).next() != Some(' ') { + text.push(" "); } - if hint.padding_left && !text.starts_with(' ') { - text.insert(0, ' '); + if hint.padding_left && text.chars_at(0).next() != Some(' ') { + text.push_front(" "); } Self { id: InlayId::Hint(id), position, - text: text.into(), + text, color: None, } } @@ -737,13 +737,13 @@ impl InlayMap { Inlay::mock_hint( post_inc(next_inlay_id), snapshot.buffer.anchor_at(position, bias), - text.clone(), + &text, ) } else { Inlay::edit_prediction( post_inc(next_inlay_id), snapshot.buffer.anchor_at(position, bias), - text.clone(), + &text, ) }; let inlay_id = next_inlay.id; @@ -1694,7 +1694,7 @@ mod tests { (offset, inlay.clone()) }) .collect::>(); - let mut expected_text = Rope::from(buffer_snapshot.text()); + let mut expected_text = Rope::from(&buffer_snapshot.text()); for (offset, inlay) in inlays.iter().rev() { expected_text.replace(*offset..*offset, &inlay.text.to_string()); } diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index db01cc7ad1..60ad0e5bf6 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -3546,7 +3546,7 @@ pub mod tests { let excerpt_hints = excerpt_hints.read(); for id in &excerpt_hints.ordered_hints { let hint = &excerpt_hints.hints_by_id[id]; - let mut label = hint.text(); + let mut label = hint.text().to_string(); if hint.padding_left { label.insert(0, ' '); } diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs index 3447e66ccd..e9f8d2dbd3 100644 --- a/crates/editor/src/signature_help.rs +++ b/crates/editor/src/signature_help.rs @@ -191,7 +191,7 @@ impl Editor { if let Some(language) = language { for signature in &mut signature_help.signatures { - let text = Rope::from(signature.label.to_string()); + let text = Rope::from(signature.label.as_ref()); let highlights = language .highlight_text(&text, 0..signature.label.len()) .into_iter() diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 7fb6f44a5b..b6567c6e33 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -341,7 +341,7 @@ impl LspAdapter for RustLspAdapter { let name = &completion.label; let text = format!("{name}: {signature}"); let prefix = "struct S { "; - let source = Rope::from(format!("{prefix}{text} }}")); + let source = Rope::from_iter([prefix, &text, " }"]); let runs = language.highlight_text(&source, prefix.len()..prefix.len() + text.len()); mk_label(text, runs) @@ -353,7 +353,7 @@ impl LspAdapter for RustLspAdapter { let name = &completion.label; let text = format!("{name}: {signature}",); let prefix = "let "; - let source = Rope::from(format!("{prefix}{text} = ();")); + let source = Rope::from_iter([prefix, &text, " = ();"]); let runs = language.highlight_text(&source, prefix.len()..prefix.len() + text.len()); mk_label(text, runs) @@ -387,7 +387,7 @@ impl LspAdapter for RustLspAdapter { &completion.label }; let text = format!("{label}{suffix}"); - let source = Rope::from(format!("{prefix} {text} {{}}")); + let source = Rope::from_iter([prefix, " ", &text, " {}"]); let run_start = prefix.len() + 1; let runs = language.highlight_text(&source, run_start..run_start + text.len()); mk_label(text, runs) @@ -450,55 +450,22 @@ impl LspAdapter for RustLspAdapter { kind: lsp::SymbolKind, language: &Arc, ) -> Option { - let (text, filter_range, display_range) = match kind { - lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => { - let text = format!("fn {} () {{}}", name); - let filter_range = 3..3 + name.len(); - let display_range = 0..filter_range.end; - (text, filter_range, display_range) - } - lsp::SymbolKind::STRUCT => { - let text = format!("struct {} {{}}", name); - let filter_range = 7..7 + name.len(); - let display_range = 0..filter_range.end; - (text, filter_range, display_range) - } - lsp::SymbolKind::ENUM => { - let text = format!("enum {} {{}}", name); - let filter_range = 5..5 + name.len(); - let display_range = 0..filter_range.end; - (text, filter_range, display_range) - } - lsp::SymbolKind::INTERFACE => { - let text = format!("trait {} {{}}", name); - let filter_range = 6..6 + name.len(); - let display_range = 0..filter_range.end; - (text, filter_range, display_range) - } - lsp::SymbolKind::CONSTANT => { - let text = format!("const {}: () = ();", name); - let filter_range = 6..6 + name.len(); - let display_range = 0..filter_range.end; - (text, filter_range, display_range) - } - lsp::SymbolKind::MODULE => { - let text = format!("mod {} {{}}", name); - let filter_range = 4..4 + name.len(); - let display_range = 0..filter_range.end; - (text, filter_range, display_range) - } - lsp::SymbolKind::TYPE_PARAMETER => { - let text = format!("type {} {{}}", name); - let filter_range = 5..5 + name.len(); - let display_range = 0..filter_range.end; - (text, filter_range, display_range) - } + let (prefix, suffix) = match kind { + lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => ("fn ", " () {}"), + lsp::SymbolKind::STRUCT => ("struct ", " {}"), + lsp::SymbolKind::ENUM => ("enum ", " {}"), + lsp::SymbolKind::INTERFACE => ("trait ", " {}"), + lsp::SymbolKind::CONSTANT => ("const ", ": () = ();"), + lsp::SymbolKind::MODULE => ("mod ", " {}"), + lsp::SymbolKind::TYPE_PARAMETER => ("type ", " {}"), _ => return None, }; + let filter_range = prefix.len()..prefix.len() + name.len(); + let display_range = 0..filter_range.end; Some(CodeLabel { - runs: language.highlight_text(&text.as_str().into(), display_range.clone()), - text: text[display_range].to_string(), + runs: language.highlight_text(&Rope::from_iter([prefix, name, suffix]), display_range), + text: format!("{prefix}{name}"), filter_range, }) } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index cca026ec87..614d514cd4 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -73,7 +73,6 @@ use gpui::{ App, AppContext, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Hsla, SharedString, Task, WeakEntity, Window, }; -use itertools::Itertools; use language::{ Buffer, BufferEvent, Capability, CodeLabel, CursorShape, DiagnosticSourceKind, Language, LanguageName, LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList, @@ -113,7 +112,7 @@ use std::{ use task_store::TaskStore; use terminals::Terminals; -use text::{Anchor, BufferId, OffsetRangeExt, Point}; +use text::{Anchor, BufferId, OffsetRangeExt, Point, Rope}; use toolchain_store::EmptyToolchainStore; use util::{ ResultExt as _, @@ -668,10 +667,10 @@ pub enum ResolveState { } impl InlayHint { - pub fn text(&self) -> String { + pub fn text(&self) -> Rope { match &self.label { - InlayHintLabel::String(s) => s.to_owned(), - InlayHintLabel::LabelParts(parts) => parts.iter().map(|part| &part.value).join(""), + InlayHintLabel::String(s) => Rope::from(s), + InlayHintLabel::LabelParts(parts) => parts.iter().map(|part| &*part.value).collect(), } } } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 75ebc8339a..9c6d9ec979 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -18,6 +18,7 @@ use git::{ use git2::RepositoryInitOptions; use gpui::{App, BackgroundExecutor, SemanticVersion, UpdateGlobal}; use http_client::Url; +use itertools::Itertools; use language::{ Diagnostic, DiagnosticEntry, DiagnosticSet, DiskState, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint, diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index aa3ed5db57..d8ed3bfac8 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -471,11 +471,19 @@ impl<'a> FromIterator<&'a str> for Rope { } impl From for Rope { + #[inline(always)] fn from(text: String) -> Self { Rope::from(text.as_str()) } } +impl From<&String> for Rope { + #[inline(always)] + fn from(text: &String) -> Self { + Rope::from(text.as_str()) + } +} + impl fmt::Display for Rope { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for chunk in self.chunks() { diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 68c7b2a2cd..9f7e49d24d 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -713,7 +713,7 @@ impl Buffer { let mut base_text = base_text.into(); let line_ending = LineEnding::detect(&base_text); LineEnding::normalize(&mut base_text); - Self::new_normalized(replica_id, remote_id, line_ending, Rope::from(base_text)) + Self::new_normalized(replica_id, remote_id, line_ending, Rope::from(&*base_text)) } pub fn new_normalized( From 03876d076e9453bff07c07729b9bee39ca5ea7a1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 7 Aug 2025 15:40:12 +0200 Subject: [PATCH 147/693] Add system prompt and tool permission to agent2 (#35781) Release Notes: - N/A --------- Co-authored-by: Ben Brandt Co-authored-by: Max Brunsfeld --- Cargo.lock | 8 +- Cargo.toml | 2 +- crates/acp_thread/src/acp_thread.rs | 2 +- crates/agent2/Cargo.toml | 4 + crates/agent2/src/agent.rs | 397 +++++++++++++++++- crates/agent2/src/agent2.rs | 1 - crates/agent2/src/native_agent_server.rs | 12 +- crates/agent2/src/prompts.rs | 35 -- crates/agent2/src/templates.rs | 75 +++- crates/agent2/src/templates/base.hbs | 56 --- crates/agent2/src/templates/system_prompt.hbs | 178 ++++++++ crates/agent2/src/tests/mod.rs | 211 ++++++++-- crates/agent2/src/tests/test_tools.rs | 41 ++ crates/agent2/src/thread.rs | 372 ++++++++++------ crates/agent2/src/tools/glob.rs | 4 + crates/agent_servers/src/acp/v0.rs | 2 +- crates/agent_servers/src/acp/v1.rs | 2 +- crates/agent_servers/src/claude/mcp_server.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 2 +- crates/assistant_tools/src/assistant_tools.rs | 3 +- crates/prompt_store/src/prompts.rs | 6 +- 21 files changed, 1111 insertions(+), 304 deletions(-) delete mode 100644 crates/agent2/src/prompts.rs delete mode 100644 crates/agent2/src/templates/base.hbs create mode 100644 crates/agent2/src/templates/system_prompt.hbs diff --git a/Cargo.lock b/Cargo.lock index b74928e05d..e6a0b6c75f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,9 +138,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.20" +version = "0.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dbfec3d27680337ed9d3064eecafe97acf0b0f190148bb4e29d96707c9e403" +checksum = "b7ae3c22c23b64a5c3b7fc8a86fcc7c494e989bd2cd66fdce14a58cfc8078381" dependencies = [ "anyhow", "futures 0.3.31", @@ -159,6 +159,7 @@ dependencies = [ "agent-client-protocol", "agent_servers", "anyhow", + "assistant_tool", "client", "clock", "cloud_llm_client", @@ -171,10 +172,12 @@ dependencies = [ "gpui_tokio", "handlebars 4.5.0", "indoc", + "language", "language_model", "language_models", "log", "project", + "prompt_store", "reqwest_client", "rust-embed", "schemars", @@ -185,6 +188,7 @@ dependencies = [ "ui", "util", "uuid", + "watch", "workspace-hack", "worktree", ] diff --git a/Cargo.toml b/Cargo.toml index 86f1b8b0a3..6bff713aaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -425,7 +425,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.20" +agent-client-protocol = "0.0.21" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index be9952fd55..1671003023 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -902,7 +902,7 @@ impl AcpThread { }); } - pub fn request_tool_call_permission( + pub fn request_tool_call_authorization( &mut self, tool_call: acp::ToolCall, options: Vec, diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 74aa2993dd..21a043fd98 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -16,6 +16,7 @@ acp_thread.workspace = true agent-client-protocol.workspace = true agent_servers.workspace = true anyhow.workspace = true +assistant_tool.workspace = true cloud_llm_client.workspace = true collections.workspace = true fs.workspace = true @@ -27,6 +28,7 @@ language_model.workspace = true language_models.workspace = true log.workspace = true project.workspace = true +prompt_store.workspace = true rust-embed.workspace = true schemars.workspace = true serde.workspace = true @@ -36,6 +38,7 @@ smol.workspace = true ui.workspace = true util.workspace = true uuid.workspace = true +watch.workspace = true worktree.workspace = true workspace-hack.workspace = true @@ -47,6 +50,7 @@ env_logger.workspace = true fs = { workspace = true, "features" = ["test-support"] } gpui = { workspace = true, "features" = ["test-support"] } gpui_tokio.workspace = true +language = { workspace = true, "features" = ["test-support"] } language_model = { workspace = true, "features" = ["test-support"] } project = { workspace = true, "features" = ["test-support"] } reqwest_client.workspace = true diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 305a31fc98..5c0acb3fb1 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,16 +1,39 @@ +use crate::ToolCallAuthorization; +use crate::{templates::Templates, AgentResponseEvent, Thread}; use acp_thread::ModelSelector; use agent_client_protocol as acp; -use anyhow::{anyhow, Result}; -use futures::StreamExt; -use gpui::{App, AppContext, AsyncApp, Entity, Subscription, Task, WeakEntity}; +use anyhow::{anyhow, Context as _, Result}; +use futures::{future, StreamExt}; +use gpui::{ + App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity, +}; use language_model::{LanguageModel, LanguageModelRegistry}; -use project::Project; +use project::{Project, ProjectItem, ProjectPath, Worktree}; +use prompt_store::{ + ProjectContext, PromptId, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext, +}; +use std::cell::RefCell; use std::collections::HashMap; use std::path::Path; use std::rc::Rc; use std::sync::Arc; +use util::ResultExt; -use crate::{templates::Templates, AgentResponseEvent, Thread}; +const RULES_FILE_NAMES: [&'static str; 9] = [ + ".rules", + ".cursorrules", + ".windsurfrules", + ".clinerules", + ".github/copilot-instructions.md", + "CLAUDE.md", + "AGENT.md", + "AGENTS.md", + "GEMINI.md", +]; + +pub struct RulesLoadingError { + pub message: SharedString, +} /// Holds both the internal Thread and the AcpThread for a session struct Session { @@ -24,17 +47,247 @@ struct Session { pub struct NativeAgent { /// Session ID -> Session mapping sessions: HashMap, + /// Shared project context for all threads + project_context: Rc>, + project_context_needs_refresh: watch::Sender<()>, + _maintain_project_context: Task>, /// Shared templates for all threads templates: Arc, + project: Entity, + prompt_store: Option>, + _subscriptions: Vec, } impl NativeAgent { - pub fn new(templates: Arc) -> Self { + pub async fn new( + project: Entity, + templates: Arc, + prompt_store: Option>, + cx: &mut AsyncApp, + ) -> Result> { log::info!("Creating new NativeAgent"); - Self { - sessions: HashMap::new(), - templates, + + let project_context = cx + .update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))? + .await; + + cx.new(|cx| { + let mut subscriptions = vec![cx.subscribe(&project, Self::handle_project_event)]; + if let Some(prompt_store) = prompt_store.as_ref() { + subscriptions.push(cx.subscribe(prompt_store, Self::handle_prompts_updated_event)) + } + + let (project_context_needs_refresh_tx, project_context_needs_refresh_rx) = + watch::channel(()); + Self { + sessions: HashMap::new(), + project_context: Rc::new(RefCell::new(project_context)), + project_context_needs_refresh: project_context_needs_refresh_tx, + _maintain_project_context: cx.spawn(async move |this, cx| { + Self::maintain_project_context(this, project_context_needs_refresh_rx, cx).await + }), + templates, + project, + prompt_store, + _subscriptions: subscriptions, + } + }) + } + + async fn maintain_project_context( + this: WeakEntity, + mut needs_refresh: watch::Receiver<()>, + cx: &mut AsyncApp, + ) -> Result<()> { + while needs_refresh.changed().await.is_ok() { + let project_context = this + .update(cx, |this, cx| { + Self::build_project_context(&this.project, this.prompt_store.as_ref(), cx) + })? + .await; + this.update(cx, |this, _| this.project_context.replace(project_context))?; } + + Ok(()) + } + + fn build_project_context( + project: &Entity, + prompt_store: Option<&Entity>, + cx: &mut App, + ) -> Task { + let worktrees = project.read(cx).visible_worktrees(cx).collect::>(); + let worktree_tasks = worktrees + .into_iter() + .map(|worktree| { + Self::load_worktree_info_for_system_prompt(worktree, project.clone(), cx) + }) + .collect::>(); + let default_user_rules_task = if let Some(prompt_store) = prompt_store.as_ref() { + prompt_store.read_with(cx, |prompt_store, cx| { + let prompts = prompt_store.default_prompt_metadata(); + let load_tasks = prompts.into_iter().map(|prompt_metadata| { + let contents = prompt_store.load(prompt_metadata.id, cx); + async move { (contents.await, prompt_metadata) } + }); + cx.background_spawn(future::join_all(load_tasks)) + }) + } else { + Task::ready(vec![]) + }; + + cx.spawn(async move |_cx| { + let (worktrees, default_user_rules) = + future::join(future::join_all(worktree_tasks), default_user_rules_task).await; + + let worktrees = worktrees + .into_iter() + .map(|(worktree, _rules_error)| { + // TODO: show error message + // if let Some(rules_error) = rules_error { + // this.update(cx, |_, cx| cx.emit(rules_error)).ok(); + // } + worktree + }) + .collect::>(); + + let default_user_rules = default_user_rules + .into_iter() + .flat_map(|(contents, prompt_metadata)| match contents { + Ok(contents) => Some(UserRulesContext { + uuid: match prompt_metadata.id { + PromptId::User { uuid } => uuid, + PromptId::EditWorkflow => return None, + }, + title: prompt_metadata.title.map(|title| title.to_string()), + contents, + }), + Err(_err) => { + // TODO: show error message + // this.update(cx, |_, cx| { + // cx.emit(RulesLoadingError { + // message: format!("{err:?}").into(), + // }); + // }) + // .ok(); + None + } + }) + .collect::>(); + + ProjectContext::new(worktrees, default_user_rules) + }) + } + + fn load_worktree_info_for_system_prompt( + worktree: Entity, + project: Entity, + cx: &mut App, + ) -> Task<(WorktreeContext, Option)> { + let tree = worktree.read(cx); + let root_name = tree.root_name().into(); + let abs_path = tree.abs_path(); + + let mut context = WorktreeContext { + root_name, + abs_path, + rules_file: None, + }; + + let rules_task = Self::load_worktree_rules_file(worktree, project, cx); + let Some(rules_task) = rules_task else { + return Task::ready((context, None)); + }; + + cx.spawn(async move |_| { + let (rules_file, rules_file_error) = match rules_task.await { + Ok(rules_file) => (Some(rules_file), None), + Err(err) => ( + None, + Some(RulesLoadingError { + message: format!("{err}").into(), + }), + ), + }; + context.rules_file = rules_file; + (context, rules_file_error) + }) + } + + fn load_worktree_rules_file( + worktree: Entity, + project: Entity, + cx: &mut App, + ) -> Option>> { + let worktree = worktree.read(cx); + let worktree_id = worktree.id(); + let selected_rules_file = RULES_FILE_NAMES + .into_iter() + .filter_map(|name| { + worktree + .entry_for_path(name) + .filter(|entry| entry.is_file()) + .map(|entry| entry.path.clone()) + }) + .next(); + + // Note that Cline supports `.clinerules` being a directory, but that is not currently + // supported. This doesn't seem to occur often in GitHub repositories. + selected_rules_file.map(|path_in_worktree| { + let project_path = ProjectPath { + worktree_id, + path: path_in_worktree.clone(), + }; + let buffer_task = + project.update(cx, |project, cx| project.open_buffer(project_path, cx)); + let rope_task = cx.spawn(async move |cx| { + buffer_task.await?.read_with(cx, |buffer, cx| { + let project_entry_id = buffer.entry_id(cx).context("buffer has no file")?; + anyhow::Ok((project_entry_id, buffer.as_rope().clone())) + })? + }); + // Build a string from the rope on a background thread. + cx.background_spawn(async move { + let (project_entry_id, rope) = rope_task.await?; + anyhow::Ok(RulesFileContext { + path_in_worktree, + text: rope.to_string().trim().to_string(), + project_entry_id: project_entry_id.to_usize(), + }) + }) + }) + } + + fn handle_project_event( + &mut self, + _project: Entity, + event: &project::Event, + _cx: &mut Context, + ) { + match event { + project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => { + self.project_context_needs_refresh.send(()).ok(); + } + project::Event::WorktreeUpdatedEntries(_, items) => { + if items.iter().any(|(path, _, _)| { + RULES_FILE_NAMES + .iter() + .any(|name| path.as_ref() == Path::new(name)) + }) { + self.project_context_needs_refresh.send(()).ok(); + } + } + _ => {} + } + } + + fn handle_prompts_updated_event( + &mut self, + _prompt_store: Entity, + _event: &prompt_store::PromptsUpdatedEvent, + _cx: &mut Context, + ) { + self.project_context_needs_refresh.send(()).ok(); } } @@ -120,8 +373,21 @@ impl acp_thread::AgentConnection for NativeAgentConnection { cx.spawn(async move |cx| { log::debug!("Starting thread creation in async context"); + + // Generate session ID + let session_id = acp::SessionId(uuid::Uuid::new_v4().to_string().into()); + log::info!("Created session with ID: {}", session_id); + + // Create AcpThread + let acp_thread = cx.update(|cx| { + cx.new(|cx| { + acp_thread::AcpThread::new("agent2", self.clone(), project.clone(), session_id.clone(), cx) + }) + })?; + let action_log = cx.update(|cx| acp_thread.read(cx).action_log().clone())?; + // Create Thread - let (session_id, thread) = agent.update( + let thread = agent.update( cx, |agent, cx: &mut gpui::Context| -> Result<_> { // Fetch default model from registry settings @@ -146,22 +412,11 @@ impl acp_thread::AgentConnection for NativeAgentConnection { anyhow!("No default model configured. Please configure a default model in settings.") })?; - let thread = cx.new(|_| Thread::new(project.clone(), agent.templates.clone(), default_model)); - - // Generate session ID - let session_id = acp::SessionId(uuid::Uuid::new_v4().to_string().into()); - log::info!("Created session with ID: {}", session_id); - Ok((session_id, thread)) + let thread = cx.new(|_| Thread::new(project, agent.project_context.clone(), action_log, agent.templates.clone(), default_model)); + Ok(thread) }, )??; - // Create AcpThread - let acp_thread = cx.update(|cx| { - cx.new(|cx| { - acp_thread::AcpThread::new("agent2", self.clone(), project, session_id.clone(), cx) - }) - })?; - // Store the session agent.update(cx, |agent, cx| { agent.sessions.insert( @@ -264,6 +519,28 @@ impl acp_thread::AgentConnection for NativeAgentConnection { ) })??; } + AgentResponseEvent::ToolCallAuthorization(ToolCallAuthorization { + tool_call, + options, + response, + }) => { + let recv = acp_thread.update(cx, |thread, cx| { + thread.request_tool_call_authorization(tool_call, options, cx) + })?; + cx.background_spawn(async move { + if let Some(option) = recv + .await + .context("authorization sender was dropped") + .log_err() + { + response + .send(option) + .map(|_| anyhow!("authorization receiver was dropped")) + .log_err(); + } + }) + .detach(); + } AgentResponseEvent::ToolCall(tool_call) => { acp_thread.update(cx, |thread, cx| { thread.handle_session_update( @@ -343,3 +620,77 @@ fn convert_prompt_to_message(blocks: Vec) -> String { message } + +#[cfg(test)] +mod tests { + use super::*; + use fs::FakeFs; + use gpui::TestAppContext; + use serde_json::json; + use settings::SettingsStore; + + #[gpui::test] + async fn test_maintaining_project_context(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/", + json!({ + "a": {} + }), + ) + .await; + let project = Project::test(fs.clone(), [], cx).await; + let agent = NativeAgent::new(project.clone(), Templates::new(), None, &mut cx.to_async()) + .await + .unwrap(); + agent.read_with(cx, |agent, _| { + assert_eq!(agent.project_context.borrow().worktrees, vec![]) + }); + + let worktree = project + .update(cx, |project, cx| project.create_worktree("/a", true, cx)) + .await + .unwrap(); + cx.run_until_parked(); + agent.read_with(cx, |agent, _| { + assert_eq!( + agent.project_context.borrow().worktrees, + vec![WorktreeContext { + root_name: "a".into(), + abs_path: Path::new("/a").into(), + rules_file: None + }] + ) + }); + + // Creating `/a/.rules` updates the project context. + fs.insert_file("/a/.rules", Vec::new()).await; + cx.run_until_parked(); + agent.read_with(cx, |agent, cx| { + let rules_entry = worktree.read(cx).entry_for_path(".rules").unwrap(); + assert_eq!( + agent.project_context.borrow().worktrees, + vec![WorktreeContext { + root_name: "a".into(), + abs_path: Path::new("/a").into(), + rules_file: Some(RulesFileContext { + path_in_worktree: Path::new(".rules").into(), + text: "".into(), + project_entry_id: rules_entry.id.to_usize() + }) + }] + ) + }); + } + + fn init_test(cx: &mut TestAppContext) { + env_logger::try_init().ok(); + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + Project::init_settings(cx); + language::init(cx); + }); + } +} diff --git a/crates/agent2/src/agent2.rs b/crates/agent2/src/agent2.rs index aa665fe313..d759f63d89 100644 --- a/crates/agent2/src/agent2.rs +++ b/crates/agent2/src/agent2.rs @@ -1,6 +1,5 @@ mod agent; mod native_agent_server; -mod prompts; mod templates; mod thread; mod tools; diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index aafe70a8a2..dd0188b548 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -3,8 +3,9 @@ use std::rc::Rc; use agent_servers::AgentServer; use anyhow::Result; -use gpui::{App, AppContext, Entity, Task}; +use gpui::{App, Entity, Task}; use project::Project; +use prompt_store::PromptStore; use crate::{templates::Templates, NativeAgent, NativeAgentConnection}; @@ -32,21 +33,22 @@ impl AgentServer for NativeAgentServer { fn connect( &self, _root_dir: &Path, - _project: &Entity, + project: &Entity, cx: &mut App, ) -> Task>> { log::info!( "NativeAgentServer::connect called for path: {:?}", _root_dir ); + let project = project.clone(); + let prompt_store = PromptStore::global(cx); cx.spawn(async move |cx| { log::debug!("Creating templates for native agent"); - // Create templates (you might want to load these from files or resources) let templates = Templates::new(); + let prompt_store = prompt_store.await?; - // Create the native agent log::debug!("Creating native agent entity"); - let agent = cx.update(|cx| cx.new(|_| NativeAgent::new(templates)))?; + let agent = NativeAgent::new(project, templates, Some(prompt_store), cx).await?; // Create the connection wrapper let connection = NativeAgentConnection(agent); diff --git a/crates/agent2/src/prompts.rs b/crates/agent2/src/prompts.rs deleted file mode 100644 index 28507f4968..0000000000 --- a/crates/agent2/src/prompts.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::{ - templates::{BaseTemplate, Template, Templates, WorktreeData}, - thread::Prompt, -}; -use anyhow::Result; -use gpui::{App, Entity}; -use project::Project; - -pub struct BasePrompt { - project: Entity, -} - -impl BasePrompt { - pub fn new(project: Entity) -> Self { - Self { project } - } -} - -impl Prompt for BasePrompt { - fn render(&self, templates: &Templates, cx: &App) -> Result { - BaseTemplate { - os: std::env::consts::OS.to_string(), - shell: util::get_system_shell(), - worktrees: self - .project - .read(cx) - .worktrees(cx) - .map(|worktree| WorktreeData { - root_name: worktree.read(cx).root_name().to_string(), - }) - .collect(), - } - .render(templates) - } -} diff --git a/crates/agent2/src/templates.rs b/crates/agent2/src/templates.rs index 04569369be..7d51a626fc 100644 --- a/crates/agent2/src/templates.rs +++ b/crates/agent2/src/templates.rs @@ -1,9 +1,9 @@ -use std::sync::Arc; - use anyhow::Result; +use gpui::SharedString; use handlebars::Handlebars; use rust_embed::RustEmbed; use serde::Serialize; +use std::sync::Arc; #[derive(RustEmbed)] #[folder = "src/templates"] @@ -15,6 +15,8 @@ pub struct Templates(Handlebars<'static>); impl Templates { pub fn new() -> Arc { let mut handlebars = Handlebars::new(); + handlebars.set_strict_mode(true); + handlebars.register_helper("contains", Box::new(contains)); handlebars.register_embed_templates::().unwrap(); Arc::new(Self(handlebars)) } @@ -31,22 +33,6 @@ pub trait Template: Sized { } } -#[derive(Serialize)] -pub struct BaseTemplate { - pub os: String, - pub shell: String, - pub worktrees: Vec, -} - -impl Template for BaseTemplate { - const TEMPLATE_NAME: &'static str = "base.hbs"; -} - -#[derive(Serialize)] -pub struct WorktreeData { - pub root_name: String, -} - #[derive(Serialize)] pub struct GlobTemplate { pub project_roots: String, @@ -55,3 +41,56 @@ pub struct GlobTemplate { impl Template for GlobTemplate { const TEMPLATE_NAME: &'static str = "glob.hbs"; } + +#[derive(Serialize)] +pub struct SystemPromptTemplate<'a> { + #[serde(flatten)] + pub project: &'a prompt_store::ProjectContext, + pub available_tools: Vec, +} + +impl Template for SystemPromptTemplate<'_> { + const TEMPLATE_NAME: &'static str = "system_prompt.hbs"; +} + +/// Handlebars helper for checking if an item is in a list +fn contains( + h: &handlebars::Helper, + _: &handlebars::Handlebars, + _: &handlebars::Context, + _: &mut handlebars::RenderContext, + out: &mut dyn handlebars::Output, +) -> handlebars::HelperResult { + let list = h + .param(0) + .and_then(|v| v.value().as_array()) + .ok_or_else(|| { + handlebars::RenderError::new("contains: missing or invalid list parameter") + })?; + let query = h.param(1).map(|v| v.value()).ok_or_else(|| { + handlebars::RenderError::new("contains: missing or invalid query parameter") + })?; + + if list.contains(&query) { + out.write("true")?; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_system_prompt_template() { + let project = prompt_store::ProjectContext::default(); + let template = SystemPromptTemplate { + project: &project, + available_tools: vec!["echo".into()], + }; + let templates = Templates::new(); + let rendered = template.render(&templates).unwrap(); + assert!(rendered.contains("## Fixing Diagnostics")); + } +} diff --git a/crates/agent2/src/templates/base.hbs b/crates/agent2/src/templates/base.hbs deleted file mode 100644 index 7eef231e32..0000000000 --- a/crates/agent2/src/templates/base.hbs +++ /dev/null @@ -1,56 +0,0 @@ -You are a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. - -## Communication - -1. Be conversational but professional. -2. Refer to the USER in the second person and yourself in the first person. -3. Format your responses in markdown. Use backticks to format file, directory, function, and class names. -4. NEVER lie or make things up. -5. Refrain from apologizing all the time when results are unexpected. Instead, just try your best to proceed or explain the circumstances to the user without apologizing. - -## Tool Use - -1. Make sure to adhere to the tools schema. -2. Provide every required argument. -3. DO NOT use tools to access items that are already available in the context section. -4. Use only the tools that are currently available. -5. DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off. - -## Searching and Reading - -If you are unsure how to fulfill the user's request, gather more information with tool calls and/or clarifying questions. - -If appropriate, use tool calls to explore the current project, which contains the following root directories: - -{{#each worktrees}} -- `{{root_name}}` -{{/each}} - -- When providing paths to tools, the path should always begin with a path that starts with a project root directory listed above. -- When looking for symbols in the project, prefer the `grep` tool. -- As you learn about the structure of the project, use that information to scope `grep` searches to targeted subtrees of the project. -- Bias towards not asking the user for help if you can find the answer yourself. - -## Fixing Diagnostics - -1. Make 1-2 attempts at fixing diagnostics, then defer to the user. -2. Never simplify code you've written just to solve diagnostics. Complete, mostly correct code is more valuable than perfect code that doesn't solve the problem. - -## Debugging - -When debugging, only make code changes if you are certain that you can solve the problem. -Otherwise, follow debugging best practices: -1. Address the root cause instead of the symptoms. -2. Add descriptive logging statements and error messages to track variable and code state. -3. Add test functions and statements to isolate the problem. - -## Calling External APIs - -1. Unless explicitly requested by the user, use the best suited external APIs and packages to solve the task. There is no need to ask the user for permission. -2. When selecting which version of an API or package to use, choose one that is compatible with the user's dependency management file. If no such file exists or if the package is not present, use the latest version that is in your training data. -3. If an external API requires an API Key, be sure to point this out to the user. Adhere to best security practices (e.g. DO NOT hardcode an API key in a place where it can be exposed) - -## System Information - -Operating System: {{os}} -Default Shell: {{shell}} diff --git a/crates/agent2/src/templates/system_prompt.hbs b/crates/agent2/src/templates/system_prompt.hbs new file mode 100644 index 0000000000..a9f67460d8 --- /dev/null +++ b/crates/agent2/src/templates/system_prompt.hbs @@ -0,0 +1,178 @@ +You are a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. + +## Communication + +1. Be conversational but professional. +2. Refer to the user in the second person and yourself in the first person. +3. Format your responses in markdown. Use backticks to format file, directory, function, and class names. +4. NEVER lie or make things up. +5. Refrain from apologizing all the time when results are unexpected. Instead, just try your best to proceed or explain the circumstances to the user without apologizing. + +{{#if (gt (len available_tools) 0)}} +## Tool Use + +1. Make sure to adhere to the tools schema. +2. Provide every required argument. +3. DO NOT use tools to access items that are already available in the context section. +4. Use only the tools that are currently available. +5. DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off. +6. NEVER run commands that don't terminate on their own such as web servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers. +7. Avoid HTML entity escaping - use plain characters instead. + +## Searching and Reading + +If you are unsure how to fulfill the user's request, gather more information with tool calls and/or clarifying questions. + +If appropriate, use tool calls to explore the current project, which contains the following root directories: + +{{#each worktrees}} +- `{{abs_path}}` +{{/each}} + +- Bias towards not asking the user for help if you can find the answer yourself. +- When providing paths to tools, the path should always start with the name of a project root directory listed above. +- Before you read or edit a file, you must first find the full path. DO NOT ever guess a file path! +{{# if (contains available_tools 'grep') }} +- When looking for symbols in the project, prefer the `grep` tool. +- As you learn about the structure of the project, use that information to scope `grep` searches to targeted subtrees of the project. +- The user might specify a partial file path. If you don't know the full path, use `find_path` (not `grep`) before you read the file. +{{/if}} +{{else}} +You are being tasked with providing a response, but you have no ability to use tools or to read or write any aspect of the user's system (other than any context the user might have provided to you). + +As such, if you need the user to perform any actions for you, you must request them explicitly. Bias towards giving a response to the best of your ability, and then making requests for the user to take action (e.g. to give you more context) only optionally. + +The one exception to this is if the user references something you don't know about - for example, the name of a source code file, function, type, or other piece of code that you have no awareness of. In this case, you MUST NOT MAKE SOMETHING UP, or assume you know what that thing is or how it works. Instead, you must ask the user for clarification rather than giving a response. +{{/if}} + +## Code Block Formatting + +Whenever you mention a code block, you MUST use ONLY use the following format: +```path/to/Something.blah#L123-456 +(code goes here) +``` +The `#L123-456` means the line number range 123 through 456, and the path/to/Something.blah +is a path in the project. (If there is no valid path in the project, then you can use +/dev/null/path.extension for its path.) This is the ONLY valid way to format code blocks, because the Markdown parser +does not understand the more common ```language syntax, or bare ``` blocks. It only +understands this path-based syntax, and if the path is missing, then it will error and you will have to do it over again. +Just to be really clear about this, if you ever find yourself writing three backticks followed by a language name, STOP! +You have made a mistake. You can only ever put paths after triple backticks! + +Based on all the information I've gathered, here's a summary of how this system works: +1. The README file is loaded into the system. +2. The system finds the first two headers, including everything in between. In this case, that would be: +```path/to/README.md#L8-12 +# First Header +This is the info under the first header. +## Sub-header +``` +3. Then the system finds the last header in the README: +```path/to/README.md#L27-29 +## Last Header +This is the last header in the README. +``` +4. Finally, it passes this information on to the next process. + + +In Markdown, hash marks signify headings. For example: +```/dev/null/example.md#L1-3 +# Level 1 heading +## Level 2 heading +### Level 3 heading +``` + +Here are examples of ways you must never render code blocks: + +In Markdown, hash marks signify headings. For example: +``` +# Level 1 heading +## Level 2 heading +### Level 3 heading +``` + +This example is unacceptable because it does not include the path. + +In Markdown, hash marks signify headings. For example: +```markdown +# Level 1 heading +## Level 2 heading +### Level 3 heading +``` + +This example is unacceptable because it has the language instead of the path. + +In Markdown, hash marks signify headings. For example: + # Level 1 heading + ## Level 2 heading + ### Level 3 heading + +This example is unacceptable because it uses indentation to mark the code block +instead of backticks with a path. + +In Markdown, hash marks signify headings. For example: +```markdown +/dev/null/example.md#L1-3 +# Level 1 heading +## Level 2 heading +### Level 3 heading +``` + +This example is unacceptable because the path is in the wrong place. The path must be directly after the opening backticks. + +{{#if (gt (len available_tools) 0)}} +## Fixing Diagnostics + +1. Make 1-2 attempts at fixing diagnostics, then defer to the user. +2. Never simplify code you've written just to solve diagnostics. Complete, mostly correct code is more valuable than perfect code that doesn't solve the problem. + +## Debugging + +When debugging, only make code changes if you are certain that you can solve the problem. +Otherwise, follow debugging best practices: +1. Address the root cause instead of the symptoms. +2. Add descriptive logging statements and error messages to track variable and code state. +3. Add test functions and statements to isolate the problem. + +{{/if}} +## Calling External APIs + +1. Unless explicitly requested by the user, use the best suited external APIs and packages to solve the task. There is no need to ask the user for permission. +2. When selecting which version of an API or package to use, choose one that is compatible with the user's dependency management file(s). If no such file exists or if the package is not present, use the latest version that is in your training data. +3. If an external API requires an API Key, be sure to point this out to the user. Adhere to best security practices (e.g. DO NOT hardcode an API key in a place where it can be exposed) + +## System Information + +Operating System: {{os}} +Default Shell: {{shell}} + +{{#if (or has_rules has_user_rules)}} +## User's Custom Instructions + +The following additional instructions are provided by the user, and should be followed to the best of your ability{{#if (gt (len available_tools) 0)}} without interfering with the tool use guidelines{{/if}}. + +{{#if has_rules}} +There are project rules that apply to these root directories: +{{#each worktrees}} +{{#if rules_file}} +`{{root_name}}/{{rules_file.path_in_worktree}}`: +`````` +{{{rules_file.text}}} +`````` +{{/if}} +{{/each}} +{{/if}} + +{{#if has_user_rules}} +The user has specified the following rules that should be applied: +{{#each user_rules}} + +{{#if title}} +Rules title: {{title}} +{{/if}} +`````` +{{contents}}} +`````` +{{/each}} +{{/if}} +{{/if}} diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 330d04b60c..b13b1cbe1a 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1,30 +1,34 @@ use super::*; use crate::templates::Templates; use acp_thread::AgentConnection; -use agent_client_protocol as acp; +use agent_client_protocol::{self as acp}; +use anyhow::Result; +use assistant_tool::ActionLog; use client::{Client, UserStore}; use fs::FakeFs; +use futures::channel::mpsc::UnboundedReceiver; use gpui::{http_client::FakeHttpClient, AppContext, Entity, Task, TestAppContext}; use indoc::indoc; use language_model::{ fake_provider::FakeLanguageModel, LanguageModel, LanguageModelCompletionError, - LanguageModelCompletionEvent, LanguageModelId, LanguageModelRegistry, MessageContent, - StopReason, + LanguageModelCompletionEvent, LanguageModelId, LanguageModelRegistry, LanguageModelToolResult, + LanguageModelToolUse, MessageContent, Role, StopReason, }; use project::Project; +use prompt_store::ProjectContext; use reqwest_client::ReqwestClient; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::json; use smol::stream::StreamExt; -use std::{path::Path, rc::Rc, sync::Arc, time::Duration}; +use std::{cell::RefCell, path::Path, rc::Rc, sync::Arc, time::Duration}; use util::path; mod test_tools; use test_tools::*; #[gpui::test] -#[ignore = "temporarily disabled until it can be run on CI"] +#[ignore = "can't run on CI yet"] async fn test_echo(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -44,7 +48,7 @@ async fn test_echo(cx: &mut TestAppContext) { } #[gpui::test] -#[ignore = "temporarily disabled until it can be run on CI"] +#[ignore = "can't run on CI yet"] async fn test_thinking(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4Thinking).await; @@ -77,7 +81,46 @@ async fn test_thinking(cx: &mut TestAppContext) { } #[gpui::test] -#[ignore = "temporarily disabled until it can be run on CI"] +async fn test_system_prompt(cx: &mut TestAppContext) { + let ThreadTest { + model, + thread, + project_context, + .. + } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + project_context.borrow_mut().shell = "test-shell".into(); + thread.update(cx, |thread, _| thread.add_tool(EchoTool)); + thread.update(cx, |thread, cx| thread.send(model.clone(), "abc", cx)); + cx.run_until_parked(); + let mut pending_completions = fake_model.pending_completions(); + assert_eq!( + pending_completions.len(), + 1, + "unexpected pending completions: {:?}", + pending_completions + ); + + let pending_completion = pending_completions.pop().unwrap(); + assert_eq!(pending_completion.messages[0].role, Role::System); + + let system_message = &pending_completion.messages[0]; + let system_prompt = system_message.content[0].to_str().unwrap(); + assert!( + system_prompt.contains("test-shell"), + "unexpected system message: {:?}", + system_message + ); + assert!( + system_prompt.contains("## Fixing Diagnostics"), + "unexpected system message: {:?}", + system_message + ); +} + +#[gpui::test] +#[ignore = "can't run on CI yet"] async fn test_basic_tool_calls(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -127,7 +170,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) { } #[gpui::test] -#[ignore = "temporarily disabled until it can be run on CI"] +#[ignore = "can't run on CI yet"] async fn test_streaming_tool_calls(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -175,7 +218,104 @@ async fn test_streaming_tool_calls(cx: &mut TestAppContext) { } #[gpui::test] -#[ignore = "temporarily disabled until it can be run on CI"] +async fn test_tool_authorization(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let mut events = thread.update(cx, |thread, cx| { + thread.add_tool(ToolRequiringPermission); + thread.send(model.clone(), "abc", cx) + }); + cx.run_until_parked(); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + LanguageModelToolUse { + id: "tool_id_1".into(), + name: ToolRequiringPermission.name().into(), + raw_input: "{}".into(), + input: json!({}), + is_input_complete: true, + }, + )); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + LanguageModelToolUse { + id: "tool_id_2".into(), + name: ToolRequiringPermission.name().into(), + raw_input: "{}".into(), + input: json!({}), + is_input_complete: true, + }, + )); + fake_model.end_last_completion_stream(); + let tool_call_auth_1 = next_tool_call_authorization(&mut events).await; + let tool_call_auth_2 = next_tool_call_authorization(&mut events).await; + + // Approve the first + tool_call_auth_1 + .response + .send(tool_call_auth_1.options[1].id.clone()) + .unwrap(); + cx.run_until_parked(); + + // Reject the second + tool_call_auth_2 + .response + .send(tool_call_auth_1.options[2].id.clone()) + .unwrap(); + cx.run_until_parked(); + + let completion = fake_model.pending_completions().pop().unwrap(); + let message = completion.messages.last().unwrap(); + assert_eq!( + message.content, + vec![ + MessageContent::ToolResult(LanguageModelToolResult { + tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(), + tool_name: tool_call_auth_1.tool_call.title.into(), + is_error: false, + content: "Allowed".into(), + output: None + }), + MessageContent::ToolResult(LanguageModelToolResult { + tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(), + tool_name: tool_call_auth_2.tool_call.title.into(), + is_error: true, + content: "Permission to run tool denied by user".into(), + output: None + }) + ] + ); +} + +async fn next_tool_call_authorization( + events: &mut UnboundedReceiver>, +) -> ToolCallAuthorization { + loop { + let event = events + .next() + .await + .expect("no tool call authorization event received") + .unwrap(); + if let AgentResponseEvent::ToolCallAuthorization(tool_call_authorization) = event { + let permission_kinds = tool_call_authorization + .options + .iter() + .map(|o| o.kind) + .collect::>(); + assert_eq!( + permission_kinds, + vec![ + acp::PermissionOptionKind::AllowAlways, + acp::PermissionOptionKind::AllowOnce, + acp::PermissionOptionKind::RejectOnce, + ] + ); + return tool_call_authorization; + } + } +} + +#[gpui::test] +#[ignore = "can't run on CI yet"] async fn test_concurrent_tool_calls(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -214,7 +354,7 @@ async fn test_concurrent_tool_calls(cx: &mut TestAppContext) { } #[gpui::test] -#[ignore = "temporarily disabled until it can be run on CI"] +#[ignore = "can't run on CI yet"] async fn test_cancellation(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -281,12 +421,10 @@ async fn test_cancellation(cx: &mut TestAppContext) { #[gpui::test] async fn test_refusal(cx: &mut TestAppContext) { - let fake_model = Arc::new(FakeLanguageModel::default()); - let ThreadTest { thread, .. } = setup(cx, TestModel::Fake(fake_model.clone())).await; + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); - let events = thread.update(cx, |thread, cx| { - thread.send(fake_model.clone(), "Hello", cx) - }); + let events = thread.update(cx, |thread, cx| thread.send(model.clone(), "Hello", cx)); cx.run_until_parked(); thread.read_with(cx, |thread, _| { assert_eq!( @@ -343,8 +481,16 @@ async fn test_agent_connection(cx: &mut TestAppContext) { }); cx.executor().forbid_parking(); + // Create a project for new_thread + let fake_fs = cx.update(|cx| fs::FakeFs::new(cx.background_executor().clone())); + fake_fs.insert_tree(path!("/test"), json!({})).await; + let project = Project::test(fake_fs, [Path::new("/test")], cx).await; + let cwd = Path::new("/test"); + // Create agent and connection - let agent = cx.new(|_| NativeAgent::new(templates.clone())); + let agent = NativeAgent::new(project.clone(), templates.clone(), None, &mut cx.to_async()) + .await + .unwrap(); let connection = NativeAgentConnection(agent.clone()); // Test model_selector returns Some @@ -366,12 +512,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) { assert!(!listed_models.is_empty(), "should have at least one model"); assert_eq!(listed_models[0].id().0, "fake"); - // Create a project for new_thread - let fake_fs = cx.update(|cx| fs::FakeFs::new(cx.background_executor().clone())); - let project = Project::test(fake_fs, [Path::new("/test")], cx).await; - // Create a thread using new_thread - let cwd = Path::new("/test"); let connection_rc = Rc::new(connection.clone()); let acp_thread = cx .update(|cx| { @@ -457,12 +598,13 @@ fn stop_events( struct ThreadTest { model: Arc, thread: Entity, + project_context: Rc>, } enum TestModel { Sonnet4, Sonnet4Thinking, - Fake(Arc), + Fake, } impl TestModel { @@ -470,7 +612,7 @@ impl TestModel { match self { TestModel::Sonnet4 => LanguageModelId("claude-sonnet-4-latest".into()), TestModel::Sonnet4Thinking => LanguageModelId("claude-sonnet-4-thinking-latest".into()), - TestModel::Fake(fake_model) => fake_model.id(), + TestModel::Fake => unreachable!(), } } } @@ -499,8 +641,8 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { language_model::init(client.clone(), cx); language_models::init(user_store.clone(), client.clone(), cx); - if let TestModel::Fake(model) = model { - Task::ready(model as Arc<_>) + if let TestModel::Fake = model { + Task::ready(Arc::new(FakeLanguageModel::default()) as Arc<_>) } else { let model_id = model.id(); let models = LanguageModelRegistry::read_global(cx); @@ -520,9 +662,22 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { }) .await; - let thread = cx.new(|_| Thread::new(project, templates, model.clone())); - - ThreadTest { model, thread } + let project_context = Rc::new(RefCell::new(ProjectContext::default())); + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let thread = cx.new(|_| { + Thread::new( + project, + project_context.clone(), + action_log, + templates, + model.clone(), + ) + }); + ThreadTest { + model, + thread, + project_context, + } } #[cfg(test)] diff --git a/crates/agent2/src/tests/test_tools.rs b/crates/agent2/src/tests/test_tools.rs index 1847a14fee..a066bb982e 100644 --- a/crates/agent2/src/tests/test_tools.rs +++ b/crates/agent2/src/tests/test_tools.rs @@ -19,6 +19,10 @@ impl AgentTool for EchoTool { "echo".into() } + fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool { + false + } + fn run(self: Arc, input: Self::Input, _cx: &mut App) -> Task> { Task::ready(Ok(input.text)) } @@ -40,6 +44,10 @@ impl AgentTool for DelayTool { "delay".into() } + fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool { + false + } + fn run(self: Arc, input: Self::Input, cx: &mut App) -> Task> where Self: Sized, @@ -51,6 +59,31 @@ impl AgentTool for DelayTool { } } +#[derive(JsonSchema, Serialize, Deserialize)] +pub struct ToolRequiringPermissionInput {} + +pub struct ToolRequiringPermission; + +impl AgentTool for ToolRequiringPermission { + type Input = ToolRequiringPermissionInput; + + fn name(&self) -> SharedString { + "tool_requiring_permission".into() + } + + fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool { + true + } + + fn run(self: Arc, _input: Self::Input, cx: &mut App) -> Task> + where + Self: Sized, + { + cx.foreground_executor() + .spawn(async move { Ok("Allowed".to_string()) }) + } +} + #[derive(JsonSchema, Serialize, Deserialize)] pub struct InfiniteToolInput {} @@ -63,6 +96,10 @@ impl AgentTool for InfiniteTool { "infinite".into() } + fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool { + false + } + fn run(self: Arc, _input: Self::Input, cx: &mut App) -> Task> { cx.foreground_executor().spawn(async move { future::pending::<()>().await; @@ -100,6 +137,10 @@ impl AgentTool for WordListTool { "word_list".into() } + fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool { + false + } + fn run(self: Arc, _input: Self::Input, _cx: &mut App) -> Task> { Task::ready(Ok("ok".to_string())) } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index af3aa17ea8..9b17d7e37e 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1,9 +1,13 @@ -use crate::{prompts::BasePrompt, templates::Templates}; +use crate::templates::{SystemPromptTemplate, Template, Templates}; use agent_client_protocol as acp; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context as _, Result}; +use assistant_tool::ActionLog; use cloud_llm_client::{CompletionIntent, CompletionMode}; use collections::HashMap; -use futures::{channel::mpsc, stream::FuturesUnordered}; +use futures::{ + channel::{mpsc, oneshot}, + stream::FuturesUnordered, +}; use gpui::{App, Context, Entity, ImageFormat, SharedString, Task}; use language_model::{ LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage, @@ -13,10 +17,11 @@ use language_model::{ }; use log; use project::Project; +use prompt_store::ProjectContext; use schemars::{JsonSchema, Schema}; use serde::Deserialize; use smol::stream::StreamExt; -use std::{collections::BTreeMap, fmt::Write, sync::Arc}; +use std::{cell::RefCell, collections::BTreeMap, fmt::Write, future::Future, rc::Rc, sync::Arc}; use util::{markdown::MarkdownCodeBlock, ResultExt}; #[derive(Debug, Clone)] @@ -97,11 +102,15 @@ pub enum AgentResponseEvent { Thinking(String), ToolCall(acp::ToolCall), ToolCallUpdate(acp::ToolCallUpdate), + ToolCallAuthorization(ToolCallAuthorization), Stop(acp::StopReason), } -pub trait Prompt { - fn render(&self, prompts: &Templates, cx: &App) -> Result; +#[derive(Debug)] +pub struct ToolCallAuthorization { + pub tool_call: acp::ToolCall, + pub options: Vec, + pub response: oneshot::Sender, } pub struct Thread { @@ -112,28 +121,31 @@ pub struct Thread { /// we run tools, report their results. running_turn: Option>, pending_tool_uses: HashMap, - system_prompts: Vec>, tools: BTreeMap>, + project_context: Rc>, templates: Arc, pub selected_model: Arc, - // action_log: Entity, + _action_log: Entity, } impl Thread { pub fn new( - project: Entity, + _project: Entity, + project_context: Rc>, + action_log: Entity, templates: Arc, default_model: Arc, ) -> Self { Self { messages: Vec::new(), completion_mode: CompletionMode::Normal, - system_prompts: vec![Arc::new(BasePrompt::new(project))], running_turn: None, pending_tool_uses: HashMap::default(), tools: BTreeMap::default(), + project_context, templates, selected_model: default_model, + _action_log: action_log, } } @@ -188,6 +200,7 @@ impl Thread { cx.notify(); let (events_tx, events_rx) = mpsc::unbounded::>(); + let event_stream = AgentResponseEventStream(events_tx); let user_message_ix = self.messages.len(); self.messages.push(AgentMessage { @@ -222,12 +235,7 @@ impl Thread { while let Some(event) = events.next().await { match event { Ok(LanguageModelCompletionEvent::Stop(reason)) => { - if let Some(reason) = to_acp_stop_reason(reason) { - events_tx - .unbounded_send(Ok(AgentResponseEvent::Stop(reason))) - .ok(); - } - + event_stream.send_stop(reason); if reason == StopReason::Refusal { thread.update(cx, |thread, _cx| { thread.messages.truncate(user_message_ix); @@ -240,14 +248,16 @@ impl Thread { thread .update(cx, |thread, cx| { tool_uses.extend(thread.handle_streamed_completion_event( - event, &events_tx, cx, + event, + &event_stream, + cx, )); }) .ok(); } Err(error) => { log::error!("Error in completion stream: {:?}", error); - events_tx.unbounded_send(Err(error)).ok(); + event_stream.send_error(error); break; } } @@ -266,11 +276,7 @@ impl Thread { while let Some(tool_result) = tool_uses.next().await { log::info!("Tool finished {:?}", tool_result); - events_tx - .unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate( - to_acp_tool_call_update(&tool_result), - ))) - .ok(); + event_stream.send_tool_call_result(&tool_result); thread .update(cx, |thread, _cx| { thread.pending_tool_uses.remove(&tool_result.tool_use_id); @@ -291,7 +297,7 @@ impl Thread { if let Err(error) = turn_result { log::error!("Turn execution failed: {:?}", error); - events_tx.unbounded_send(Err(error)).ok(); + event_stream.send_error(error); } else { log::info!("Turn execution completed successfully"); } @@ -299,24 +305,20 @@ impl Thread { events_rx } - pub fn build_system_message(&self, cx: &App) -> Option { + pub fn build_system_message(&self) -> AgentMessage { log::debug!("Building system message"); - let mut system_message = AgentMessage { - role: Role::System, - content: Vec::new(), - }; - - for prompt in &self.system_prompts { - if let Some(rendered_prompt) = prompt.render(&self.templates, cx).log_err() { - system_message - .content - .push(MessageContent::Text(rendered_prompt)); - } + let prompt = SystemPromptTemplate { + project: &self.project_context.borrow(), + available_tools: self.tools.keys().cloned().collect(), + } + .render(&self.templates) + .context("failed to build system prompt") + .expect("Invalid template"); + log::debug!("System message built"); + AgentMessage { + role: Role::System, + content: vec![prompt.into()], } - - let result = (!system_message.content.is_empty()).then_some(system_message); - log::debug!("System message built: {}", result.is_some()); - result } /// A helper method that's called on every streamed completion event. @@ -325,7 +327,7 @@ impl Thread { fn handle_streamed_completion_event( &mut self, event: LanguageModelCompletionEvent, - events_tx: &mpsc::UnboundedSender>, + event_stream: &AgentResponseEventStream, cx: &mut Context, ) -> Option> { log::trace!("Handling streamed completion event: {:?}", event); @@ -338,13 +340,13 @@ impl Thread { content: Vec::new(), }); } - Text(new_text) => self.handle_text_event(new_text, events_tx, cx), + Text(new_text) => self.handle_text_event(new_text, event_stream, cx), Thinking { text, signature } => { - self.handle_thinking_event(text, signature, events_tx, cx) + self.handle_thinking_event(text, signature, event_stream, cx) } RedactedThinking { data } => self.handle_redacted_thinking_event(data, cx), ToolUse(tool_use) => { - return self.handle_tool_use_event(tool_use, events_tx, cx); + return self.handle_tool_use_event(tool_use, event_stream, cx); } ToolUseJsonParseError { id, @@ -369,12 +371,10 @@ impl Thread { fn handle_text_event( &mut self, new_text: String, - events_tx: &mpsc::UnboundedSender>, + events_stream: &AgentResponseEventStream, cx: &mut Context, ) { - events_tx - .unbounded_send(Ok(AgentResponseEvent::Text(new_text.clone()))) - .ok(); + events_stream.send_text(&new_text); let last_message = self.last_assistant_message(); if let Some(MessageContent::Text(text)) = last_message.content.last_mut() { @@ -390,12 +390,10 @@ impl Thread { &mut self, new_text: String, new_signature: Option, - events_tx: &mpsc::UnboundedSender>, + event_stream: &AgentResponseEventStream, cx: &mut Context, ) { - events_tx - .unbounded_send(Ok(AgentResponseEvent::Thinking(new_text.clone()))) - .ok(); + event_stream.send_thinking(&new_text); let last_message = self.last_assistant_message(); if let Some(MessageContent::Thinking { text, signature }) = last_message.content.last_mut() @@ -423,7 +421,7 @@ impl Thread { fn handle_tool_use_event( &mut self, tool_use: LanguageModelToolUse, - events_tx: &mpsc::UnboundedSender>, + event_stream: &AgentResponseEventStream, cx: &mut Context, ) -> Option> { cx.notify(); @@ -446,32 +444,18 @@ impl Thread { } }); if push_new_tool_use { - events_tx - .unbounded_send(Ok(AgentResponseEvent::ToolCall(acp::ToolCall { - id: acp::ToolCallId(tool_use.id.to_string().into()), - title: tool_use.name.to_string(), - kind: acp::ToolKind::Other, - status: acp::ToolCallStatus::Pending, - content: vec![], - locations: vec![], - raw_input: Some(tool_use.input.clone()), - }))) - .ok(); + event_stream.send_tool_call(&tool_use); last_message .content .push(MessageContent::ToolUse(tool_use.clone())); } else { - events_tx - .unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate( - acp::ToolCallUpdate { - id: acp::ToolCallId(tool_use.id.to_string().into()), - fields: acp::ToolCallUpdateFields { - raw_input: Some(tool_use.input.clone()), - ..Default::default() - }, - }, - ))) - .ok(); + event_stream.send_tool_call_update( + &tool_use.id, + acp::ToolCallUpdateFields { + raw_input: Some(tool_use.input.clone()), + ..Default::default() + }, + ); } if !tool_use.is_input_complete { @@ -479,22 +463,10 @@ impl Thread { } if let Some(tool) = self.tools.get(tool_use.name.as_ref()) { - events_tx - .unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate( - acp::ToolCallUpdate { - id: acp::ToolCallId(tool_use.id.to_string().into()), - fields: acp::ToolCallUpdateFields { - status: Some(acp::ToolCallStatus::InProgress), - ..Default::default() - }, - }, - ))) - .ok(); - - let pending_tool_result = tool.clone().run(tool_use.input, cx); - + let tool_result = + self.run_tool(tool.clone(), tool_use.clone(), event_stream.clone(), cx); Some(cx.foreground_executor().spawn(async move { - match pending_tool_result.await { + match tool_result.await { Ok(tool_output) => LanguageModelToolResult { tool_use_id: tool_use.id, tool_name: tool_use.name, @@ -523,6 +495,30 @@ impl Thread { } } + fn run_tool( + &self, + tool: Arc, + tool_use: LanguageModelToolUse, + event_stream: AgentResponseEventStream, + cx: &mut Context, + ) -> Task> { + let needs_authorization = tool.needs_authorization(tool_use.input.clone(), cx); + cx.spawn(async move |_this, cx| { + if needs_authorization? { + event_stream.authorize_tool_call(&tool_use).await?; + } + + event_stream.send_tool_call_update( + &tool_use.id, + acp::ToolCallUpdateFields { + status: Some(acp::ToolCallStatus::InProgress), + ..Default::default() + }, + ); + cx.update(|cx| tool.run(tool_use.input, cx))?.await + }) + } + fn handle_tool_use_json_parse_error_event( &mut self, tool_use_id: LanguageModelToolUseId, @@ -575,7 +571,7 @@ impl Thread { log::debug!("Completion intent: {:?}", completion_intent); log::debug!("Completion mode: {:?}", self.completion_mode); - let messages = self.build_request_messages(cx); + let messages = self.build_request_messages(); log::info!("Request will include {} messages", messages.len()); let tools: Vec = self @@ -613,14 +609,13 @@ impl Thread { request } - fn build_request_messages(&self, cx: &App) -> Vec { + fn build_request_messages(&self) -> Vec { log::trace!( "Building request messages from {} thread messages", self.messages.len() ); - let messages = self - .build_system_message(cx) + let messages = Some(self.build_system_message()) .iter() .chain(self.messages.iter()) .map(|message| { @@ -674,6 +669,10 @@ where schemars::schema_for!(Self::Input) } + /// Returns true if the tool needs the users's authorization + /// before running. + fn needs_authorization(&self, input: Self::Input, cx: &App) -> bool; + /// Runs the tool with the provided input. fn run(self: Arc, input: Self::Input, cx: &mut App) -> Task>; @@ -688,6 +687,7 @@ pub trait AnyAgentTool { fn name(&self) -> SharedString; fn description(&self, cx: &mut App) -> SharedString; fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result; + fn needs_authorization(&self, input: serde_json::Value, cx: &mut App) -> Result; fn run(self: Arc, input: serde_json::Value, cx: &mut App) -> Task>; } @@ -707,6 +707,14 @@ where Ok(serde_json::to_value(self.0.input_schema(format))?) } + fn needs_authorization(&self, input: serde_json::Value, cx: &mut App) -> Result { + let parsed_input: Result = serde_json::from_value(input).map_err(Into::into); + match parsed_input { + Ok(input) => Ok(self.0.needs_authorization(input, cx)), + Err(error) => Err(anyhow!(error)), + } + } + fn run(self: Arc, input: serde_json::Value, cx: &mut App) -> Task> { let parsed_input: Result = serde_json::from_value(input).map_err(Into::into); match parsed_input { @@ -716,39 +724,153 @@ where } } -fn to_acp_stop_reason(reason: StopReason) -> Option { - match reason { - StopReason::EndTurn => Some(acp::StopReason::EndTurn), - StopReason::MaxTokens => Some(acp::StopReason::MaxTokens), - StopReason::Refusal => Some(acp::StopReason::Refusal), - StopReason::ToolUse => None, - } -} +#[derive(Clone)] +struct AgentResponseEventStream( + mpsc::UnboundedSender>, +); -fn to_acp_tool_call_update(tool_result: &LanguageModelToolResult) -> acp::ToolCallUpdate { - let status = if tool_result.is_error { - acp::ToolCallStatus::Failed - } else { - acp::ToolCallStatus::Completed - }; - let content = match &tool_result.content { - LanguageModelToolResultContent::Text(text) => text.to_string().into(), - LanguageModelToolResultContent::Image(LanguageModelImage { source, .. }) => { - acp::ToolCallContent::Content { - content: acp::ContentBlock::Image(acp::ImageContent { - annotations: None, - data: source.to_string(), - mime_type: ImageFormat::Png.mime_type().to_string(), - }), +impl AgentResponseEventStream { + fn send_text(&self, text: &str) { + self.0 + .unbounded_send(Ok(AgentResponseEvent::Text(text.to_string()))) + .ok(); + } + + fn send_thinking(&self, text: &str) { + self.0 + .unbounded_send(Ok(AgentResponseEvent::Thinking(text.to_string()))) + .ok(); + } + + fn authorize_tool_call( + &self, + tool_use: &LanguageModelToolUse, + ) -> impl use<> + Future> { + let (response_tx, response_rx) = oneshot::channel(); + self.0 + .unbounded_send(Ok(AgentResponseEvent::ToolCallAuthorization( + ToolCallAuthorization { + tool_call: acp::ToolCall { + id: acp::ToolCallId(tool_use.id.to_string().into()), + title: tool_use.name.to_string(), + kind: acp::ToolKind::Other, + status: acp::ToolCallStatus::Pending, + content: vec![], + locations: vec![], + raw_input: Some(tool_use.input.clone()), + }, + options: vec![ + acp::PermissionOption { + id: acp::PermissionOptionId("always_allow".into()), + name: "Always Allow".into(), + kind: acp::PermissionOptionKind::AllowAlways, + }, + acp::PermissionOption { + id: acp::PermissionOptionId("allow".into()), + name: "Allow".into(), + kind: acp::PermissionOptionKind::AllowOnce, + }, + acp::PermissionOption { + id: acp::PermissionOptionId("deny".into()), + name: "Deny".into(), + kind: acp::PermissionOptionKind::RejectOnce, + }, + ], + response: response_tx, + }, + ))) + .ok(); + async move { + match response_rx.await?.0.as_ref() { + "allow" | "always_allow" => Ok(()), + _ => Err(anyhow!("Permission to run tool denied by user")), } } - }; - acp::ToolCallUpdate { - id: acp::ToolCallId(tool_result.tool_use_id.to_string().into()), - fields: acp::ToolCallUpdateFields { - status: Some(status), - content: Some(vec![content]), - ..Default::default() - }, + } + + fn send_tool_call(&self, tool_use: &LanguageModelToolUse) { + self.0 + .unbounded_send(Ok(AgentResponseEvent::ToolCall(acp::ToolCall { + id: acp::ToolCallId(tool_use.id.to_string().into()), + title: tool_use.name.to_string(), + kind: acp::ToolKind::Other, + status: acp::ToolCallStatus::Pending, + content: vec![], + locations: vec![], + raw_input: Some(tool_use.input.clone()), + }))) + .ok(); + } + + fn send_tool_call_update( + &self, + tool_use_id: &LanguageModelToolUseId, + fields: acp::ToolCallUpdateFields, + ) { + self.0 + .unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate( + acp::ToolCallUpdate { + id: acp::ToolCallId(tool_use_id.to_string().into()), + fields, + }, + ))) + .ok(); + } + + fn send_tool_call_result(&self, tool_result: &LanguageModelToolResult) { + let status = if tool_result.is_error { + acp::ToolCallStatus::Failed + } else { + acp::ToolCallStatus::Completed + }; + let content = match &tool_result.content { + LanguageModelToolResultContent::Text(text) => text.to_string().into(), + LanguageModelToolResultContent::Image(LanguageModelImage { source, .. }) => { + acp::ToolCallContent::Content { + content: acp::ContentBlock::Image(acp::ImageContent { + annotations: None, + data: source.to_string(), + mime_type: ImageFormat::Png.mime_type().to_string(), + }), + } + } + }; + self.0 + .unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate( + acp::ToolCallUpdate { + id: acp::ToolCallId(tool_result.tool_use_id.to_string().into()), + fields: acp::ToolCallUpdateFields { + status: Some(status), + content: Some(vec![content]), + ..Default::default() + }, + }, + ))) + .ok(); + } + + fn send_stop(&self, reason: StopReason) { + match reason { + StopReason::EndTurn => { + self.0 + .unbounded_send(Ok(AgentResponseEvent::Stop(acp::StopReason::EndTurn))) + .ok(); + } + StopReason::MaxTokens => { + self.0 + .unbounded_send(Ok(AgentResponseEvent::Stop(acp::StopReason::MaxTokens))) + .ok(); + } + StopReason::Refusal => { + self.0 + .unbounded_send(Ok(AgentResponseEvent::Stop(acp::StopReason::Refusal))) + .ok(); + } + StopReason::ToolUse => {} + } + } + + fn send_error(&self, error: LanguageModelCompletionError) { + self.0.unbounded_send(Err(error)).ok(); } } diff --git a/crates/agent2/src/tools/glob.rs b/crates/agent2/src/tools/glob.rs index 9434311aaf..f44ce9f359 100644 --- a/crates/agent2/src/tools/glob.rs +++ b/crates/agent2/src/tools/glob.rs @@ -46,6 +46,10 @@ impl AgentTool for GlobTool { .into() } + fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool { + false + } + fn run(self: Arc, input: Self::Input, cx: &mut App) -> Task> { let path_matcher = match PathMatcher::new([&input.glob]) { Ok(matcher) => matcher, diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index c0b64fcc41..e676b7ee46 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -135,7 +135,7 @@ impl acp_old::Client for OldAcpClientDelegate { let response = cx .update(|cx| { self.thread.borrow().update(cx, |thread, cx| { - thread.request_tool_call_permission(tool_call, acp_options, cx) + thread.request_tool_call_authorization(tool_call, acp_options, cx) }) })? .context("Failed to update thread")? diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index 178796816a..ff71783b48 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -210,7 +210,7 @@ impl acp::Client for ClientDelegate { .context("Failed to get session")? .thread .update(cx, |thread, cx| { - thread.request_tool_call_permission(arguments.tool_call, arguments.options, cx) + thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx) })?; let result = rx.await; diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/claude/mcp_server.rs index c6f8bb5b69..53a8556e74 100644 --- a/crates/agent_servers/src/claude/mcp_server.rs +++ b/crates/agent_servers/src/claude/mcp_server.rs @@ -153,7 +153,7 @@ impl McpServerTool for PermissionTool { let chosen_option = thread .update(cx, |thread, cx| { - thread.request_tool_call_permission( + thread.request_tool_call_authorization( claude_tool.as_acp(tool_call_id), vec![ acp::PermissionOption { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index a9b39e6cea..06e47a11dc 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -3147,7 +3147,7 @@ mod tests { let task = cx.spawn(async move |cx| { if let Some((tool_call, options)) = permission_request { let permission = thread.update(cx, |thread, cx| { - thread.request_tool_call_permission( + thread.request_tool_call_authorization( tool_call.clone(), options.clone(), cx, diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs index 57fdc51336..90bb2e9b7c 100644 --- a/crates/assistant_tools/src/assistant_tools.rs +++ b/crates/assistant_tools/src/assistant_tools.rs @@ -36,13 +36,12 @@ use crate::delete_path_tool::DeletePathTool; use crate::diagnostics_tool::DiagnosticsTool; use crate::edit_file_tool::EditFileTool; use crate::fetch_tool::FetchTool; -use crate::find_path_tool::FindPathTool; use crate::list_directory_tool::ListDirectoryTool; use crate::now_tool::NowTool; use crate::thinking_tool::ThinkingTool; pub use edit_file_tool::{EditFileMode, EditFileToolInput}; -pub use find_path_tool::FindPathToolInput; +pub use find_path_tool::*; pub use grep_tool::{GrepTool, GrepToolInput}; pub use open_tool::OpenTool; pub use project_notifications_tool::ProjectNotificationsTool; diff --git a/crates/prompt_store/src/prompts.rs b/crates/prompt_store/src/prompts.rs index d737ef9246..7eb63eec5e 100644 --- a/crates/prompt_store/src/prompts.rs +++ b/crates/prompt_store/src/prompts.rs @@ -18,7 +18,7 @@ use util::{ResultExt, get_system_shell}; use crate::UserPromptId; -#[derive(Debug, Clone, Serialize)] +#[derive(Default, Debug, Clone, Serialize)] pub struct ProjectContext { pub worktrees: Vec, /// Whether any worktree has a rules_file. Provided as a field because handlebars can't do this. @@ -71,14 +71,14 @@ pub struct UserRulesContext { pub contents: String, } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Eq, PartialEq, Serialize)] pub struct WorktreeContext { pub root_name: String, pub abs_path: Arc, pub rules_file: Option, } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Eq, PartialEq, Serialize)] pub struct RulesFileContext { pub path_in_worktree: Arc, pub text: String, From e227b5ac3029d15d7cc8bbc65046136ddea52439 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 7 Aug 2025 10:42:46 -0300 Subject: [PATCH 148/693] onboarding: Add young account treatment to AI upsell card (#35785) Release Notes: - N/A --- crates/ai_onboarding/src/ai_upsell_card.rs | 213 +++++++++++++-------- crates/onboarding/src/ai_setup_page.rs | 1 + 2 files changed, 139 insertions(+), 75 deletions(-) diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index 4e4833f770..65d3866273 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -1,26 +1,33 @@ use std::{sync::Arc, time::Duration}; -use client::{Client, zed_urls}; +use client::{Client, UserStore, zed_urls}; use cloud_llm_client::Plan; use gpui::{ - Animation, AnimationExt, AnyElement, App, IntoElement, RenderOnce, Transformation, Window, - percentage, + Animation, AnimationExt, AnyElement, App, Entity, IntoElement, RenderOnce, Transformation, + Window, percentage, }; use ui::{Divider, Vector, VectorName, prelude::*}; -use crate::{SignInStatus, plan_definitions::PlanDefinitions}; +use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions}; #[derive(IntoElement, RegisterComponent)] pub struct AiUpsellCard { pub sign_in_status: SignInStatus, pub sign_in: Arc, + pub account_too_young: bool, pub user_plan: Option, pub tab_index: Option, } impl AiUpsellCard { - pub fn new(client: Arc, user_plan: Option) -> Self { + pub fn new( + client: Arc, + user_store: &Entity, + user_plan: Option, + cx: &mut App, + ) -> Self { let status = *client.status().borrow(); + let store = user_store.read(cx); Self { user_plan, @@ -32,6 +39,7 @@ impl AiUpsellCard { }) .detach_and_log_err(cx); }), + account_too_young: store.account_too_young(), tab_index: None, } } @@ -40,6 +48,7 @@ impl AiUpsellCard { impl RenderOnce for AiUpsellCard { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let plan_definitions = PlanDefinitions; + let young_account_banner = YoungAccountBanner; let pro_section = v_flex() .flex_grow() @@ -158,36 +167,70 @@ impl RenderOnce for AiUpsellCard { SignInStatus::SignedIn => match self.user_plan { None | Some(Plan::ZedFree) => card .child(Label::new("Try Zed AI").size(LabelSize::Large)) - .child( - div() - .max_w_3_4() - .mb_2() - .child(Label::new(description).color(Color::Muted)), - ) - .child(plans_section) - .child( - footer_container - .child( - Button::new("start_trial", "Start 14-day Free Pro Trial") - .full_width() - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .when_some(self.tab_index, |this, tab_index| { - this.tab_index(tab_index) - }) - .on_click(move |_, _window, cx| { - telemetry::event!( - "Start Trial Clicked", - state = "post-sign-in" - ); - cx.open_url(&zed_urls::start_trial_url(cx)) - }), + .map(|this| { + if self.account_too_young { + this.child(young_account_banner).child( + v_flex() + .mt_2() + .gap_1() + .child( + h_flex() + .gap_2() + .child( + Label::new("Pro") + .size(LabelSize::Small) + .color(Color::Accent) + .buffer_font(cx), + ) + .child(Divider::horizontal()), + ) + .child(plan_definitions.pro_plan(true)) + .child( + Button::new("pro", "Get Started") + .full_width() + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .on_click(move |_, _window, cx| { + telemetry::event!( + "Upgrade To Pro Clicked", + state = "young-account" + ); + cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)) + }), + ), ) + } else { + this.child( + div() + .max_w_3_4() + .mb_2() + .child(Label::new(description).color(Color::Muted)), + ) + .child(plans_section) .child( - Label::new("No credit card required") - .size(LabelSize::Small) - .color(Color::Muted), - ), - ), + footer_container + .child( + Button::new("start_trial", "Start 14-day Free Pro Trial") + .full_width() + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .when_some(self.tab_index, |this, tab_index| { + this.tab_index(tab_index) + }) + .on_click(move |_, _window, cx| { + telemetry::event!( + "Start Trial Clicked", + state = "post-sign-in" + ); + cx.open_url(&zed_urls::start_trial_url(cx)) + }), + ) + .child( + Label::new("No credit card required") + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + } + }), Some(Plan::ZedProTrial) => card .child(pro_trial_stamp) .child(Label::new("You're in the Zed Pro Trial").size(LabelSize::Large)) @@ -255,48 +298,68 @@ impl Component for AiUpsellCard { Some( v_flex() .gap_4() - .children(vec![example_group(vec![ - single_example( - "Signed Out State", - AiUpsellCard { - sign_in_status: SignInStatus::SignedOut, - sign_in: Arc::new(|_, _| {}), - user_plan: None, - tab_index: Some(0), - } - .into_any_element(), - ), - single_example( - "Free Plan", - AiUpsellCard { - sign_in_status: SignInStatus::SignedIn, - sign_in: Arc::new(|_, _| {}), - user_plan: Some(Plan::ZedFree), - tab_index: Some(1), - } - .into_any_element(), - ), - single_example( - "Pro Trial", - AiUpsellCard { - sign_in_status: SignInStatus::SignedIn, - sign_in: Arc::new(|_, _| {}), - user_plan: Some(Plan::ZedProTrial), - tab_index: Some(1), - } - .into_any_element(), - ), - single_example( - "Pro Plan", - AiUpsellCard { - sign_in_status: SignInStatus::SignedIn, - sign_in: Arc::new(|_, _| {}), - user_plan: Some(Plan::ZedPro), - tab_index: Some(1), - } - .into_any_element(), - ), - ])]) + .items_center() + .max_w_4_5() + .child(single_example( + "Signed Out State", + AiUpsellCard { + sign_in_status: SignInStatus::SignedOut, + sign_in: Arc::new(|_, _| {}), + account_too_young: false, + user_plan: None, + tab_index: Some(0), + } + .into_any_element(), + )) + .child(example_group_with_title( + "Signed In States", + vec![ + single_example( + "Free Plan", + AiUpsellCard { + sign_in_status: SignInStatus::SignedIn, + sign_in: Arc::new(|_, _| {}), + account_too_young: false, + user_plan: Some(Plan::ZedFree), + tab_index: Some(1), + } + .into_any_element(), + ), + single_example( + "Free Plan but Young Account", + AiUpsellCard { + sign_in_status: SignInStatus::SignedIn, + sign_in: Arc::new(|_, _| {}), + account_too_young: true, + user_plan: Some(Plan::ZedFree), + tab_index: Some(1), + } + .into_any_element(), + ), + single_example( + "Pro Trial", + AiUpsellCard { + sign_in_status: SignInStatus::SignedIn, + sign_in: Arc::new(|_, _| {}), + account_too_young: false, + user_plan: Some(Plan::ZedProTrial), + tab_index: Some(1), + } + .into_any_element(), + ), + single_example( + "Pro Plan", + AiUpsellCard { + sign_in_status: SignInStatus::SignedIn, + sign_in: Arc::new(|_, _| {}), + account_too_young: false, + user_plan: Some(Plan::ZedPro), + tab_index: Some(1), + } + .into_any_element(), + ), + ], + )) .into_any_element(), ) } diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index 098907870b..6099745c40 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -286,6 +286,7 @@ pub(crate) fn render_ai_setup_page( .child(AiUpsellCard { sign_in_status: SignInStatus::SignedIn, sign_in: Arc::new(|_, _| {}), + account_too_young: user_store.read(cx).account_too_young(), user_plan: user_store.read(cx).plan(), tab_index: Some({ tab_index += 1; From 305c653c62eacf3895a8c816df4726fee4508a1c Mon Sep 17 00:00:00 2001 From: Tongue_chaude <145228731+Tonguechaude@users.noreply.github.com> Date: Thu, 7 Aug 2025 15:51:29 +0200 Subject: [PATCH 149/693] Add icons for Puppet files (#35778) Release Notes: - Added icon for Puppet (.pp) files Actually puppet icons are available in the extension here : --------- Co-authored-by: Danilo Leal --- assets/icons/file_icons/puppet.svg | 1 + crates/theme/src/icon_theme.rs | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 assets/icons/file_icons/puppet.svg diff --git a/assets/icons/file_icons/puppet.svg b/assets/icons/file_icons/puppet.svg new file mode 100644 index 0000000000..cdf903bc62 --- /dev/null +++ b/assets/icons/file_icons/puppet.svg @@ -0,0 +1 @@ + diff --git a/crates/theme/src/icon_theme.rs b/crates/theme/src/icon_theme.rs index 10fd1e002d..5bd69c1733 100644 --- a/crates/theme/src/icon_theme.rs +++ b/crates/theme/src/icon_theme.rs @@ -183,6 +183,7 @@ const FILE_SUFFIXES_BY_ICON_KEY: &[(&str, &[&str])] = &[ ], ), ("prisma", &["prisma"]), + ("puppet", &["pp"]), ("python", &["py"]), ("r", &["r", "R"]), ("react", &["cjsx", "ctsx", "jsx", "mjsx", "mtsx", "tsx"]), @@ -331,6 +332,7 @@ const FILE_ICONS: &[(&str, &str)] = &[ ("php", "icons/file_icons/php.svg"), ("prettier", "icons/file_icons/prettier.svg"), ("prisma", "icons/file_icons/prisma.svg"), + ("puppet", "icons/file_icons/puppet.svg"), ("python", "icons/file_icons/python.svg"), ("r", "icons/file_icons/r.svg"), ("react", "icons/file_icons/react.svg"), From a5c25e036696fd0519da25a70ed8e07d6672e87c Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 7 Aug 2025 10:53:15 -0300 Subject: [PATCH 150/693] agent: Improve end of trial card display (#35789) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now rendering the backdrop behind the card to clean up the UI, bring focus to the card's content, and direct the user to act on it, either by ignoring it or upgrading. CleanShot 2025-08-07 at 10  30
58@2x Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 60 +++++++++++++++++------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 8f5fff5da3..6b8e36066b 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -2343,6 +2343,16 @@ impl AgentPanel { ) } + fn render_backdrop(&self, cx: &mut Context) -> impl IntoElement { + div() + .size_full() + .absolute() + .inset_0() + .bg(cx.theme().colors().panel_background) + .opacity(0.8) + .block_mouse_except_scroll() + } + fn render_trial_end_upsell( &self, _window: &mut Window, @@ -2352,15 +2362,24 @@ impl AgentPanel { return None; } - Some(EndTrialUpsell::new(Arc::new({ - let this = cx.entity(); - move |_, cx| { - this.update(cx, |_this, cx| { - TrialEndUpsell::set_dismissed(true, cx); - cx.notify(); - }); - } - }))) + Some( + v_flex() + .absolute() + .inset_0() + .size_full() + .bg(cx.theme().colors().panel_background) + .opacity(0.85) + .block_mouse_except_scroll() + .child(EndTrialUpsell::new(Arc::new({ + let this = cx.entity(); + move |_, cx| { + this.update(cx, |_this, cx| { + TrialEndUpsell::set_dismissed(true, cx); + cx.notify(); + }); + } + }))), + ) } fn render_empty_state_section_header( @@ -3210,9 +3229,10 @@ impl Render for AgentPanel { // - Scrolling in all views works as expected // - Files can be dropped into the panel let content = v_flex() - .key_context(self.key_context()) - .justify_between() + .relative() .size_full() + .justify_between() + .key_context(self.key_context()) .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(|this, action: &NewThread, window, cx| { this.new_thread(action, window, cx); @@ -3255,14 +3275,12 @@ impl Render for AgentPanel { .on_action(cx.listener(Self::toggle_burn_mode)) .child(self.render_toolbar(window, cx)) .children(self.render_onboarding(window, cx)) - .children(self.render_trial_end_upsell(window, cx)) .map(|parent| match &self.active_view { ActiveView::Thread { thread, message_editor, .. } => parent - .relative() .child( if thread.read(cx).is_empty() && !self.should_render_onboarding(cx) { self.render_thread_empty_state(window, cx) @@ -3299,21 +3317,10 @@ impl Render for AgentPanel { }) .child(h_flex().relative().child(message_editor.clone()).when( !LanguageModelRegistry::read_global(cx).has_authenticated_provider(cx), - |this| { - this.child( - div() - .size_full() - .absolute() - .inset_0() - .bg(cx.theme().colors().panel_background) - .opacity(0.8) - .block_mouse_except_scroll(), - ) - }, + |this| this.child(self.render_backdrop(cx)), )) .child(self.render_drag_target(cx)), ActiveView::ExternalAgentThread { thread_view, .. } => parent - .relative() .child(thread_view.clone()) .child(self.render_drag_target(cx)), ActiveView::History => parent.child(self.history.clone()), @@ -3352,7 +3359,8 @@ impl Render for AgentPanel { )) } ActiveView::Configuration => parent.children(self.configuration.clone()), - }); + }) + .children(self.render_trial_end_upsell(window, cx)); match self.active_view.which_font_size_used() { WhichFontSize::AgentFont => { From 740686b8830a548b0f2d26b11ddaaefa2484346c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 7 Aug 2025 17:45:41 +0300 Subject: [PATCH 151/693] Batch diagnostics updates (#35794) Diagnostics updates were programmed in Zed based off the r-a LSP push diagnostics, with all related updates happening per file. https://github.com/zed-industries/zed/pull/19230 and especially https://github.com/zed-industries/zed/pull/32269 brought in pull diagnostics that could produce results for thousands files simultaneously. It was noted and addressed on the local side in https://github.com/zed-industries/zed/pull/34022 but the remote side was still not adjusted properly. This PR * removes redundant diagnostics pull updates on remote clients, as buffer diagnostics are updated via buffer sync operations separately * batches all diagnostics-related updates and proto messages, so multiple diagnostic summaries (per file) could be sent at once, specifically, 1 (potentially large) diagnostics summary update instead of N*10^3 small ones. Buffer updates are still sent per buffer and not updated, as happening separately and not offending the collab traffic that much. Release Notes: - Improved diagnostics performance in the collaborative mode --- crates/collab/src/rpc.rs | 36 +- crates/diagnostics/src/diagnostics.rs | 8 +- crates/project/src/lsp_store.rs | 729 +++++++++++++-------- crates/project/src/lsp_store/clangd_ext.rs | 18 +- crates/project/src/project.rs | 50 +- crates/project/src/project_tests.rs | 8 +- crates/proto/proto/lsp.proto | 1 + 7 files changed, 500 insertions(+), 350 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 078632d397..b3603d2619 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1630,15 +1630,15 @@ fn notify_rejoined_projects( } // Stream this worktree's diagnostics. - for summary in worktree.diagnostic_summaries { - session.peer.send( - session.connection_id, - proto::UpdateDiagnosticSummary { - project_id: project.id.to_proto(), - worktree_id: worktree.id, - summary: Some(summary), - }, - )?; + let mut worktree_diagnostics = worktree.diagnostic_summaries.into_iter(); + if let Some(summary) = worktree_diagnostics.next() { + let message = proto::UpdateDiagnosticSummary { + project_id: project.id.to_proto(), + worktree_id: worktree.id, + summary: Some(summary), + more_summaries: worktree_diagnostics.collect(), + }; + session.peer.send(session.connection_id, message)?; } for settings_file in worktree.settings_files { @@ -2060,15 +2060,15 @@ async fn join_project( } // Stream this worktree's diagnostics. - for summary in worktree.diagnostic_summaries { - session.peer.send( - session.connection_id, - proto::UpdateDiagnosticSummary { - project_id: project_id.to_proto(), - worktree_id: worktree.id, - summary: Some(summary), - }, - )?; + let mut worktree_diagnostics = worktree.diagnostic_summaries.into_iter(); + if let Some(summary) = worktree_diagnostics.next() { + let message = proto::UpdateDiagnosticSummary { + project_id: project.id.to_proto(), + worktree_id: worktree.id, + summary: Some(summary), + more_summaries: worktree_diagnostics.collect(), + }; + session.peer.send(session.connection_id, message)?; } for settings_file in worktree.settings_files { diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index ba64ba0eed..e7660920da 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -177,9 +177,9 @@ impl ProjectDiagnosticsEditor { } project::Event::DiagnosticsUpdated { language_server_id, - path, + paths, } => { - this.paths_to_update.insert(path.clone()); + this.paths_to_update.extend(paths.clone()); let project = project.clone(); this.diagnostic_summary_update = cx.spawn(async move |this, cx| { cx.background_executor() @@ -193,9 +193,9 @@ impl ProjectDiagnosticsEditor { cx.emit(EditorEvent::TitleChanged); if this.editor.focus_handle(cx).contains_focused(window, cx) || this.focus_handle.contains_focused(window, cx) { - log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change"); + log::debug!("diagnostics updated for server {language_server_id}, paths {paths:?}. recording change"); } else { - log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts"); + log::debug!("diagnostics updated for server {language_server_id}, paths {paths:?}. updating excerpts"); this.update_stale_excerpts(window, cx); } } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 4489f9f043..b88cf42ff5 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -140,6 +140,20 @@ impl FormatTrigger { } } +#[derive(Debug)] +pub struct DocumentDiagnosticsUpdate<'a, D> { + pub diagnostics: D, + pub result_id: Option, + pub server_id: LanguageServerId, + pub disk_based_sources: Cow<'a, [String]>, +} + +pub struct DocumentDiagnostics { + diagnostics: Vec>>, + document_abs_path: PathBuf, + version: Option, +} + pub struct LocalLspStore { weak: WeakEntity, worktree_store: Entity, @@ -503,12 +517,16 @@ impl LocalLspStore { adapter.process_diagnostics(&mut params, server_id, buffer); } - this.merge_diagnostics( - server_id, - params, - None, + this.merge_lsp_diagnostics( DiagnosticSourceKind::Pushed, - &adapter.disk_based_diagnostic_sources, + vec![DocumentDiagnosticsUpdate { + server_id, + diagnostics: params, + result_id: None, + disk_based_sources: Cow::Borrowed( + &adapter.disk_based_diagnostic_sources, + ), + }], |_, diagnostic, cx| match diagnostic.source_kind { DiagnosticSourceKind::Other | DiagnosticSourceKind::Pushed => { adapter.retain_old_diagnostic(diagnostic, cx) @@ -3610,8 +3628,8 @@ pub enum LspStoreEvent { RefreshInlayHints, RefreshCodeLens, DiagnosticsUpdated { - language_server_id: LanguageServerId, - path: ProjectPath, + server_id: LanguageServerId, + paths: Vec, }, DiskBasedDiagnosticsStarted { language_server_id: LanguageServerId, @@ -4440,17 +4458,24 @@ impl LspStore { pub(crate) fn send_diagnostic_summaries(&self, worktree: &mut Worktree) { if let Some((client, downstream_project_id)) = self.downstream_client.clone() { - if let Some(summaries) = self.diagnostic_summaries.get(&worktree.id()) { - for (path, summaries) in summaries { - for (&server_id, summary) in summaries { - client - .send(proto::UpdateDiagnosticSummary { - project_id: downstream_project_id, - worktree_id: worktree.id().to_proto(), - summary: Some(summary.to_proto(server_id, path)), - }) - .log_err(); - } + if let Some(diangostic_summaries) = self.diagnostic_summaries.get(&worktree.id()) { + let mut summaries = + diangostic_summaries + .into_iter() + .flat_map(|(path, summaries)| { + summaries + .into_iter() + .map(|(server_id, summary)| summary.to_proto(*server_id, path)) + }); + if let Some(summary) = summaries.next() { + client + .send(proto::UpdateDiagnosticSummary { + project_id: downstream_project_id, + worktree_id: worktree.id().to_proto(), + summary: Some(summary), + more_summaries: summaries.collect(), + }) + .log_err(); } } } @@ -6564,7 +6589,7 @@ impl LspStore { &mut self, buffer: Entity, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { let buffer_id = buffer.read(cx).remote_id(); if let Some((client, upstream_project_id)) = self.upstream_client() { @@ -6575,7 +6600,7 @@ impl LspStore { }, cx, ) { - return Task::ready(Ok(Vec::new())); + return Task::ready(Ok(None)); } let request_task = client.request(proto::MultiLspQuery { buffer_id: buffer_id.to_proto(), @@ -6593,7 +6618,7 @@ impl LspStore { )), }); cx.background_spawn(async move { - Ok(request_task + let _proto_responses = request_task .await? .responses .into_iter() @@ -6606,8 +6631,11 @@ impl LspStore { None } }) - .flat_map(GetDocumentDiagnostics::diagnostics_from_proto) - .collect()) + .collect::>(); + // Proto requests cause the diagnostics to be pulled from language server(s) on the local side + // and then, buffer state updated with the diagnostics received, which will be later propagated to the client. + // Do not attempt to further process the dummy responses here. + Ok(None) }) } else { let server_ids = buffer.update(cx, |buffer, cx| { @@ -6635,7 +6663,7 @@ impl LspStore { for diagnostics in join_all(pull_diagnostics).await { responses.extend(diagnostics?); } - Ok(responses) + Ok(Some(responses)) }) } } @@ -6701,75 +6729,93 @@ impl LspStore { buffer: Entity, cx: &mut Context, ) -> Task> { - let buffer_id = buffer.read(cx).remote_id(); let diagnostics = self.pull_diagnostics(buffer, cx); cx.spawn(async move |lsp_store, cx| { - let diagnostics = diagnostics.await.context("pulling diagnostics")?; + let Some(diagnostics) = diagnostics.await.context("pulling diagnostics")? else { + return Ok(()); + }; lsp_store.update(cx, |lsp_store, cx| { if lsp_store.as_local().is_none() { return; } - for diagnostics_set in diagnostics { - let LspPullDiagnostics::Response { - server_id, - uri, - diagnostics, - } = diagnostics_set - else { - continue; - }; - - let adapter = lsp_store.language_server_adapter_for_id(server_id); - let disk_based_sources = adapter - .as_ref() - .map(|adapter| adapter.disk_based_diagnostic_sources.as_slice()) - .unwrap_or(&[]); - match diagnostics { - PulledDiagnostics::Unchanged { result_id } => { - lsp_store - .merge_diagnostics( - server_id, - lsp::PublishDiagnosticsParams { - uri: uri.clone(), - diagnostics: Vec::new(), - version: None, - }, - Some(result_id), - DiagnosticSourceKind::Pulled, - disk_based_sources, - |_, _, _| true, - cx, - ) - .log_err(); - } - PulledDiagnostics::Changed { + let mut unchanged_buffers = HashSet::default(); + let mut changed_buffers = HashSet::default(); + let server_diagnostics_updates = diagnostics + .into_iter() + .filter_map(|diagnostics_set| match diagnostics_set { + LspPullDiagnostics::Response { + server_id, + uri, diagnostics, - result_id, - } => { - lsp_store - .merge_diagnostics( + } => Some((server_id, uri, diagnostics)), + LspPullDiagnostics::Default => None, + }) + .fold( + HashMap::default(), + |mut acc, (server_id, uri, diagnostics)| { + let (result_id, diagnostics) = match diagnostics { + PulledDiagnostics::Unchanged { result_id } => { + unchanged_buffers.insert(uri.clone()); + (Some(result_id), Vec::new()) + } + PulledDiagnostics::Changed { + result_id, + diagnostics, + } => { + changed_buffers.insert(uri.clone()); + (result_id, diagnostics) + } + }; + let disk_based_sources = Cow::Owned( + lsp_store + .language_server_adapter_for_id(server_id) + .as_ref() + .map(|adapter| adapter.disk_based_diagnostic_sources.as_slice()) + .unwrap_or(&[]) + .to_vec(), + ); + acc.entry(server_id).or_insert_with(Vec::new).push( + DocumentDiagnosticsUpdate { server_id, - lsp::PublishDiagnosticsParams { - uri: uri.clone(), + diagnostics: lsp::PublishDiagnosticsParams { + uri, diagnostics, version: None, }, result_id, - DiagnosticSourceKind::Pulled, disk_based_sources, - |buffer, old_diagnostic, _| match old_diagnostic.source_kind { - DiagnosticSourceKind::Pulled => { - buffer.remote_id() != buffer_id - } - DiagnosticSourceKind::Other - | DiagnosticSourceKind::Pushed => true, - }, - cx, - ) - .log_err(); - } - } + }, + ); + acc + }, + ); + + for diagnostic_updates in server_diagnostics_updates.into_values() { + lsp_store + .merge_lsp_diagnostics( + DiagnosticSourceKind::Pulled, + diagnostic_updates, + |buffer, old_diagnostic, cx| { + File::from_dyn(buffer.file()) + .and_then(|file| { + let abs_path = file.as_local()?.abs_path(cx); + lsp::Url::from_file_path(abs_path).ok() + }) + .is_none_or(|buffer_uri| { + unchanged_buffers.contains(&buffer_uri) + || match old_diagnostic.source_kind { + DiagnosticSourceKind::Pulled => { + !changed_buffers.contains(&buffer_uri) + } + DiagnosticSourceKind::Other + | DiagnosticSourceKind::Pushed => true, + } + }) + }, + cx, + ) + .log_err(); } }) }) @@ -7791,88 +7837,135 @@ impl LspStore { cx: &mut Context, ) -> anyhow::Result<()> { self.merge_diagnostic_entries( - server_id, - abs_path, - result_id, - version, - diagnostics, + vec![DocumentDiagnosticsUpdate { + diagnostics: DocumentDiagnostics { + diagnostics, + document_abs_path: abs_path, + version, + }, + result_id, + server_id, + disk_based_sources: Cow::Borrowed(&[]), + }], |_, _, _| false, cx, )?; Ok(()) } - pub fn merge_diagnostic_entries( + pub fn merge_diagnostic_entries<'a>( &mut self, - server_id: LanguageServerId, - abs_path: PathBuf, - result_id: Option, - version: Option, - mut diagnostics: Vec>>, - filter: impl Fn(&Buffer, &Diagnostic, &App) -> bool + Clone, + diagnostic_updates: Vec>, + merge: impl Fn(&Buffer, &Diagnostic, &App) -> bool + Clone, cx: &mut Context, ) -> anyhow::Result<()> { - let Some((worktree, relative_path)) = - self.worktree_store.read(cx).find_worktree(&abs_path, cx) - else { - log::warn!("skipping diagnostics update, no worktree found for path {abs_path:?}"); - return Ok(()); - }; + let mut diagnostics_summary = None::; + let mut updated_diagnostics_paths = HashMap::default(); + for mut update in diagnostic_updates { + let abs_path = &update.diagnostics.document_abs_path; + let server_id = update.server_id; + let Some((worktree, relative_path)) = + self.worktree_store.read(cx).find_worktree(abs_path, cx) + else { + log::warn!("skipping diagnostics update, no worktree found for path {abs_path:?}"); + return Ok(()); + }; - let project_path = ProjectPath { - worktree_id: worktree.read(cx).id(), - path: relative_path.into(), - }; + let worktree_id = worktree.read(cx).id(); + let project_path = ProjectPath { + worktree_id, + path: relative_path.into(), + }; - if let Some(buffer_handle) = self.buffer_store.read(cx).get_by_path(&project_path) { - let snapshot = buffer_handle.read(cx).snapshot(); - let buffer = buffer_handle.read(cx); - let reused_diagnostics = buffer - .get_diagnostics(server_id) - .into_iter() - .flat_map(|diag| { - diag.iter() - .filter(|v| filter(buffer, &v.diagnostic, cx)) - .map(|v| { - let start = Unclipped(v.range.start.to_point_utf16(&snapshot)); - let end = Unclipped(v.range.end.to_point_utf16(&snapshot)); - DiagnosticEntry { - range: start..end, - diagnostic: v.diagnostic.clone(), - } - }) - }) - .collect::>(); + if let Some(buffer_handle) = self.buffer_store.read(cx).get_by_path(&project_path) { + let snapshot = buffer_handle.read(cx).snapshot(); + let buffer = buffer_handle.read(cx); + let reused_diagnostics = buffer + .get_diagnostics(server_id) + .into_iter() + .flat_map(|diag| { + diag.iter() + .filter(|v| merge(buffer, &v.diagnostic, cx)) + .map(|v| { + let start = Unclipped(v.range.start.to_point_utf16(&snapshot)); + let end = Unclipped(v.range.end.to_point_utf16(&snapshot)); + DiagnosticEntry { + range: start..end, + diagnostic: v.diagnostic.clone(), + } + }) + }) + .collect::>(); - self.as_local_mut() - .context("cannot merge diagnostics on a remote LspStore")? - .update_buffer_diagnostics( - &buffer_handle, + self.as_local_mut() + .context("cannot merge diagnostics on a remote LspStore")? + .update_buffer_diagnostics( + &buffer_handle, + server_id, + update.result_id, + update.diagnostics.version, + update.diagnostics.diagnostics.clone(), + reused_diagnostics.clone(), + cx, + )?; + + update.diagnostics.diagnostics.extend(reused_diagnostics); + } + + let updated = worktree.update(cx, |worktree, cx| { + self.update_worktree_diagnostics( + worktree.id(), server_id, - result_id, - version, - diagnostics.clone(), - reused_diagnostics.clone(), + project_path.path.clone(), + update.diagnostics.diagnostics, cx, - )?; - - diagnostics.extend(reused_diagnostics); + ) + })?; + match updated { + ControlFlow::Continue(new_summary) => { + if let Some((project_id, new_summary)) = new_summary { + match &mut diagnostics_summary { + Some(diagnostics_summary) => { + diagnostics_summary + .more_summaries + .push(proto::DiagnosticSummary { + path: project_path.path.as_ref().to_proto(), + language_server_id: server_id.0 as u64, + error_count: new_summary.error_count, + warning_count: new_summary.warning_count, + }) + } + None => { + diagnostics_summary = Some(proto::UpdateDiagnosticSummary { + project_id: project_id, + worktree_id: worktree_id.to_proto(), + summary: Some(proto::DiagnosticSummary { + path: project_path.path.as_ref().to_proto(), + language_server_id: server_id.0 as u64, + error_count: new_summary.error_count, + warning_count: new_summary.warning_count, + }), + more_summaries: Vec::new(), + }) + } + } + } + updated_diagnostics_paths + .entry(server_id) + .or_insert_with(Vec::new) + .push(project_path); + } + ControlFlow::Break(()) => {} + } } - let updated = worktree.update(cx, |worktree, cx| { - self.update_worktree_diagnostics( - worktree.id(), - server_id, - project_path.path.clone(), - diagnostics, - cx, - ) - })?; - if updated { - cx.emit(LspStoreEvent::DiagnosticsUpdated { - language_server_id: server_id, - path: project_path, - }) + if let Some((diagnostics_summary, (downstream_client, _))) = + diagnostics_summary.zip(self.downstream_client.as_ref()) + { + downstream_client.send(diagnostics_summary).log_err(); + } + for (server_id, paths) in updated_diagnostics_paths { + cx.emit(LspStoreEvent::DiagnosticsUpdated { server_id, paths }); } Ok(()) } @@ -7881,10 +7974,10 @@ impl LspStore { &mut self, worktree_id: WorktreeId, server_id: LanguageServerId, - worktree_path: Arc, + path_in_worktree: Arc, diagnostics: Vec>>, _: &mut Context, - ) -> Result { + ) -> Result>> { let local = match &mut self.mode { LspStoreMode::Local(local_lsp_store) => local_lsp_store, _ => anyhow::bail!("update_worktree_diagnostics called on remote"), @@ -7892,7 +7985,9 @@ impl LspStore { let summaries_for_tree = self.diagnostic_summaries.entry(worktree_id).or_default(); let diagnostics_for_tree = local.diagnostics.entry(worktree_id).or_default(); - let summaries_by_server_id = summaries_for_tree.entry(worktree_path.clone()).or_default(); + let summaries_by_server_id = summaries_for_tree + .entry(path_in_worktree.clone()) + .or_default(); let old_summary = summaries_by_server_id .remove(&server_id) @@ -7900,18 +7995,19 @@ impl LspStore { let new_summary = DiagnosticSummary::new(&diagnostics); if new_summary.is_empty() { - if let Some(diagnostics_by_server_id) = diagnostics_for_tree.get_mut(&worktree_path) { + if let Some(diagnostics_by_server_id) = diagnostics_for_tree.get_mut(&path_in_worktree) + { if let Ok(ix) = diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) { diagnostics_by_server_id.remove(ix); } if diagnostics_by_server_id.is_empty() { - diagnostics_for_tree.remove(&worktree_path); + diagnostics_for_tree.remove(&path_in_worktree); } } } else { summaries_by_server_id.insert(server_id, new_summary); let diagnostics_by_server_id = diagnostics_for_tree - .entry(worktree_path.clone()) + .entry(path_in_worktree.clone()) .or_default(); match diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) { Ok(ix) => { @@ -7924,23 +8020,22 @@ impl LspStore { } if !old_summary.is_empty() || !new_summary.is_empty() { - if let Some((downstream_client, project_id)) = &self.downstream_client { - downstream_client - .send(proto::UpdateDiagnosticSummary { - project_id: *project_id, - worktree_id: worktree_id.to_proto(), - summary: Some(proto::DiagnosticSummary { - path: worktree_path.to_proto(), - language_server_id: server_id.0 as u64, - error_count: new_summary.error_count as u32, - warning_count: new_summary.warning_count as u32, - }), - }) - .log_err(); + if let Some((_, project_id)) = &self.downstream_client { + Ok(ControlFlow::Continue(Some(( + *project_id, + proto::DiagnosticSummary { + path: path_in_worktree.to_proto(), + language_server_id: server_id.0 as u64, + error_count: new_summary.error_count as u32, + warning_count: new_summary.warning_count as u32, + }, + )))) + } else { + Ok(ControlFlow::Continue(None)) } + } else { + Ok(ControlFlow::Break(())) } - - Ok(!old_summary.is_empty() || !new_summary.is_empty()) } pub fn open_buffer_for_symbol( @@ -8793,23 +8888,30 @@ impl LspStore { envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result<()> { - this.update(&mut cx, |this, cx| { + this.update(&mut cx, |lsp_store, cx| { let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - if let Some(message) = envelope.payload.summary { + let mut updated_diagnostics_paths = HashMap::default(); + let mut diagnostics_summary = None::; + for message_summary in envelope + .payload + .summary + .into_iter() + .chain(envelope.payload.more_summaries) + { let project_path = ProjectPath { worktree_id, - path: Arc::::from_proto(message.path), + path: Arc::::from_proto(message_summary.path), }; let path = project_path.path.clone(); - let server_id = LanguageServerId(message.language_server_id as usize); + let server_id = LanguageServerId(message_summary.language_server_id as usize); let summary = DiagnosticSummary { - error_count: message.error_count as usize, - warning_count: message.warning_count as usize, + error_count: message_summary.error_count as usize, + warning_count: message_summary.warning_count as usize, }; if summary.is_empty() { if let Some(worktree_summaries) = - this.diagnostic_summaries.get_mut(&worktree_id) + lsp_store.diagnostic_summaries.get_mut(&worktree_id) { if let Some(summaries) = worktree_summaries.get_mut(&path) { summaries.remove(&server_id); @@ -8819,31 +8921,55 @@ impl LspStore { } } } else { - this.diagnostic_summaries + lsp_store + .diagnostic_summaries .entry(worktree_id) .or_default() .entry(path) .or_default() .insert(server_id, summary); } - if let Some((downstream_client, project_id)) = &this.downstream_client { - downstream_client - .send(proto::UpdateDiagnosticSummary { - project_id: *project_id, - worktree_id: worktree_id.to_proto(), - summary: Some(proto::DiagnosticSummary { - path: project_path.path.as_ref().to_proto(), - language_server_id: server_id.0 as u64, - error_count: summary.error_count as u32, - warning_count: summary.warning_count as u32, - }), - }) - .log_err(); + + if let Some((_, project_id)) = &lsp_store.downstream_client { + match &mut diagnostics_summary { + Some(diagnostics_summary) => { + diagnostics_summary + .more_summaries + .push(proto::DiagnosticSummary { + path: project_path.path.as_ref().to_proto(), + language_server_id: server_id.0 as u64, + error_count: summary.error_count as u32, + warning_count: summary.warning_count as u32, + }) + } + None => { + diagnostics_summary = Some(proto::UpdateDiagnosticSummary { + project_id: *project_id, + worktree_id: worktree_id.to_proto(), + summary: Some(proto::DiagnosticSummary { + path: project_path.path.as_ref().to_proto(), + language_server_id: server_id.0 as u64, + error_count: summary.error_count as u32, + warning_count: summary.warning_count as u32, + }), + more_summaries: Vec::new(), + }) + } + } } - cx.emit(LspStoreEvent::DiagnosticsUpdated { - language_server_id: LanguageServerId(message.language_server_id as usize), - path: project_path, - }); + updated_diagnostics_paths + .entry(server_id) + .or_insert_with(Vec::new) + .push(project_path); + } + + if let Some((diagnostics_summary, (downstream_client, _))) = + diagnostics_summary.zip(lsp_store.downstream_client.as_ref()) + { + downstream_client.send(diagnostics_summary).log_err(); + } + for (server_id, paths) in updated_diagnostics_paths { + cx.emit(LspStoreEvent::DiagnosticsUpdated { server_id, paths }); } Ok(()) })? @@ -10361,6 +10487,7 @@ impl LspStore { error_count: 0, warning_count: 0, }), + more_summaries: Vec::new(), }) .log_err(); } @@ -10649,52 +10776,80 @@ impl LspStore { ) } + #[cfg(any(test, feature = "test-support"))] pub fn update_diagnostics( &mut self, - language_server_id: LanguageServerId, - params: lsp::PublishDiagnosticsParams, + server_id: LanguageServerId, + diagnostics: lsp::PublishDiagnosticsParams, result_id: Option, source_kind: DiagnosticSourceKind, disk_based_sources: &[String], cx: &mut Context, ) -> Result<()> { - self.merge_diagnostics( - language_server_id, - params, - result_id, + self.merge_lsp_diagnostics( source_kind, - disk_based_sources, + vec![DocumentDiagnosticsUpdate { + diagnostics, + result_id, + server_id, + disk_based_sources: Cow::Borrowed(disk_based_sources), + }], |_, _, _| false, cx, ) } - pub fn merge_diagnostics( + pub fn merge_lsp_diagnostics( &mut self, - language_server_id: LanguageServerId, - mut params: lsp::PublishDiagnosticsParams, - result_id: Option, source_kind: DiagnosticSourceKind, - disk_based_sources: &[String], - filter: impl Fn(&Buffer, &Diagnostic, &App) -> bool + Clone, + lsp_diagnostics: Vec>, + merge: impl Fn(&Buffer, &Diagnostic, &App) -> bool + Clone, cx: &mut Context, ) -> Result<()> { anyhow::ensure!(self.mode.is_local(), "called update_diagnostics on remote"); - let abs_path = params - .uri - .to_file_path() - .map_err(|()| anyhow!("URI is not a file"))?; + let updates = lsp_diagnostics + .into_iter() + .filter_map(|update| { + let abs_path = update.diagnostics.uri.to_file_path().ok()?; + Some(DocumentDiagnosticsUpdate { + diagnostics: self.lsp_to_document_diagnostics( + abs_path, + source_kind, + update.server_id, + update.diagnostics, + &update.disk_based_sources, + ), + result_id: update.result_id, + server_id: update.server_id, + disk_based_sources: update.disk_based_sources, + }) + }) + .collect(); + self.merge_diagnostic_entries(updates, merge, cx)?; + Ok(()) + } + + fn lsp_to_document_diagnostics( + &mut self, + document_abs_path: PathBuf, + source_kind: DiagnosticSourceKind, + server_id: LanguageServerId, + mut lsp_diagnostics: lsp::PublishDiagnosticsParams, + disk_based_sources: &[String], + ) -> DocumentDiagnostics { let mut diagnostics = Vec::default(); let mut primary_diagnostic_group_ids = HashMap::default(); let mut sources_by_group_id = HashMap::default(); let mut supporting_diagnostics = HashMap::default(); - let adapter = self.language_server_adapter_for_id(language_server_id); + let adapter = self.language_server_adapter_for_id(server_id); // Ensure that primary diagnostics are always the most severe - params.diagnostics.sort_by_key(|item| item.severity); + lsp_diagnostics + .diagnostics + .sort_by_key(|item| item.severity); - for diagnostic in ¶ms.diagnostics { + for diagnostic in &lsp_diagnostics.diagnostics { let source = diagnostic.source.as_ref(); let range = range_from_lsp(diagnostic.range); let is_supporting = diagnostic @@ -10716,7 +10871,7 @@ impl LspStore { .map_or(false, |tags| tags.contains(&DiagnosticTag::UNNECESSARY)); let underline = self - .language_server_adapter_for_id(language_server_id) + .language_server_adapter_for_id(server_id) .map_or(true, |adapter| adapter.underline_diagnostic(diagnostic)); if is_supporting { @@ -10758,7 +10913,7 @@ impl LspStore { }); if let Some(infos) = &diagnostic.related_information { for info in infos { - if info.location.uri == params.uri && !info.message.is_empty() { + if info.location.uri == lsp_diagnostics.uri && !info.message.is_empty() { let range = range_from_lsp(info.location.range); diagnostics.push(DiagnosticEntry { range, @@ -10806,16 +10961,11 @@ impl LspStore { } } - self.merge_diagnostic_entries( - language_server_id, - abs_path, - result_id, - params.version, + DocumentDiagnostics { diagnostics, - filter, - cx, - )?; - Ok(()) + document_abs_path, + version: lsp_diagnostics.version, + } } fn insert_newly_running_language_server( @@ -11571,67 +11721,84 @@ impl LspStore { ) { let workspace_diagnostics = GetDocumentDiagnostics::deserialize_workspace_diagnostics_report(report, server_id); - for workspace_diagnostics in workspace_diagnostics { - let LspPullDiagnostics::Response { - server_id, - uri, - diagnostics, - } = workspace_diagnostics.diagnostics - else { - continue; - }; - - let adapter = self.language_server_adapter_for_id(server_id); - let disk_based_sources = adapter - .as_ref() - .map(|adapter| adapter.disk_based_diagnostic_sources.as_slice()) - .unwrap_or(&[]); - - match diagnostics { - PulledDiagnostics::Unchanged { result_id } => { - self.merge_diagnostics( + let mut unchanged_buffers = HashSet::default(); + let mut changed_buffers = HashSet::default(); + let workspace_diagnostics_updates = workspace_diagnostics + .into_iter() + .filter_map( + |workspace_diagnostics| match workspace_diagnostics.diagnostics { + LspPullDiagnostics::Response { server_id, - lsp::PublishDiagnosticsParams { - uri: uri.clone(), - diagnostics: Vec::new(), - version: None, - }, - Some(result_id), - DiagnosticSourceKind::Pulled, - disk_based_sources, - |_, _, _| true, - cx, - ) - .log_err(); - } - PulledDiagnostics::Changed { - diagnostics, - result_id, - } => { - self.merge_diagnostics( - server_id, - lsp::PublishDiagnosticsParams { - uri: uri.clone(), + uri, + diagnostics, + } => Some((server_id, uri, diagnostics, workspace_diagnostics.version)), + LspPullDiagnostics::Default => None, + }, + ) + .fold( + HashMap::default(), + |mut acc, (server_id, uri, diagnostics, version)| { + let (result_id, diagnostics) = match diagnostics { + PulledDiagnostics::Unchanged { result_id } => { + unchanged_buffers.insert(uri.clone()); + (Some(result_id), Vec::new()) + } + PulledDiagnostics::Changed { + result_id, diagnostics, - version: workspace_diagnostics.version, - }, - result_id, - DiagnosticSourceKind::Pulled, - disk_based_sources, - |buffer, old_diagnostic, cx| match old_diagnostic.source_kind { - DiagnosticSourceKind::Pulled => { - let buffer_url = File::from_dyn(buffer.file()) - .map(|f| f.abs_path(cx)) - .and_then(|abs_path| file_path_to_lsp_url(&abs_path).ok()); - buffer_url.is_none_or(|buffer_url| buffer_url != uri) - } - DiagnosticSourceKind::Other | DiagnosticSourceKind::Pushed => true, - }, - cx, - ) - .log_err(); - } - } + } => { + changed_buffers.insert(uri.clone()); + (result_id, diagnostics) + } + }; + let disk_based_sources = Cow::Owned( + self.language_server_adapter_for_id(server_id) + .as_ref() + .map(|adapter| adapter.disk_based_diagnostic_sources.as_slice()) + .unwrap_or(&[]) + .to_vec(), + ); + acc.entry(server_id) + .or_insert_with(Vec::new) + .push(DocumentDiagnosticsUpdate { + server_id, + diagnostics: lsp::PublishDiagnosticsParams { + uri, + diagnostics, + version, + }, + result_id, + disk_based_sources, + }); + acc + }, + ); + + for diagnostic_updates in workspace_diagnostics_updates.into_values() { + self.merge_lsp_diagnostics( + DiagnosticSourceKind::Pulled, + diagnostic_updates, + |buffer, old_diagnostic, cx| { + File::from_dyn(buffer.file()) + .and_then(|file| { + let abs_path = file.as_local()?.abs_path(cx); + lsp::Url::from_file_path(abs_path).ok() + }) + .is_none_or(|buffer_uri| { + unchanged_buffers.contains(&buffer_uri) + || match old_diagnostic.source_kind { + DiagnosticSourceKind::Pulled => { + !changed_buffers.contains(&buffer_uri) + } + DiagnosticSourceKind::Other | DiagnosticSourceKind::Pushed => { + true + } + } + }) + }, + cx, + ) + .log_err(); } } } diff --git a/crates/project/src/lsp_store/clangd_ext.rs b/crates/project/src/lsp_store/clangd_ext.rs index cf3507352e..274b1b8980 100644 --- a/crates/project/src/lsp_store/clangd_ext.rs +++ b/crates/project/src/lsp_store/clangd_ext.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{borrow::Cow, sync::Arc}; use ::serde::{Deserialize, Serialize}; use gpui::WeakEntity; @@ -6,7 +6,7 @@ use language::{CachedLspAdapter, Diagnostic, DiagnosticSourceKind}; use lsp::{LanguageServer, LanguageServerName}; use util::ResultExt as _; -use crate::LspStore; +use crate::{LspStore, lsp_store::DocumentDiagnosticsUpdate}; pub const CLANGD_SERVER_NAME: LanguageServerName = LanguageServerName::new_static("clangd"); const INACTIVE_REGION_MESSAGE: &str = "inactive region"; @@ -81,12 +81,16 @@ pub fn register_notifications( version: params.text_document.version, diagnostics, }; - this.merge_diagnostics( - server_id, - mapped_diagnostics, - None, + this.merge_lsp_diagnostics( DiagnosticSourceKind::Pushed, - &adapter.disk_based_diagnostic_sources, + vec![DocumentDiagnosticsUpdate { + server_id, + diagnostics: mapped_diagnostics, + result_id: None, + disk_based_sources: Cow::Borrowed( + &adapter.disk_based_diagnostic_sources, + ), + }], |_, diag, _| !is_inactive_region(diag), cx, ) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 614d514cd4..b3a9e6fdf5 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -74,9 +74,9 @@ use gpui::{ Task, WeakEntity, Window, }; use language::{ - Buffer, BufferEvent, Capability, CodeLabel, CursorShape, DiagnosticSourceKind, Language, - LanguageName, LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList, - Transaction, Unclipped, language_settings::InlayHintKind, proto::split_operations, + Buffer, BufferEvent, Capability, CodeLabel, CursorShape, Language, LanguageName, + LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList, Transaction, + Unclipped, language_settings::InlayHintKind, proto::split_operations, }; use lsp::{ CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, InsertTextMode, @@ -305,7 +305,7 @@ pub enum Event { language_server_id: LanguageServerId, }, DiagnosticsUpdated { - path: ProjectPath, + paths: Vec, language_server_id: LanguageServerId, }, RemoteIdChanged(Option), @@ -2895,18 +2895,17 @@ impl Project { cx: &mut Context, ) { match event { - LspStoreEvent::DiagnosticsUpdated { - language_server_id, - path, - } => cx.emit(Event::DiagnosticsUpdated { - path: path.clone(), - language_server_id: *language_server_id, - }), - LspStoreEvent::LanguageServerAdded(language_server_id, name, worktree_id) => cx.emit( - Event::LanguageServerAdded(*language_server_id, name.clone(), *worktree_id), + LspStoreEvent::DiagnosticsUpdated { server_id, paths } => { + cx.emit(Event::DiagnosticsUpdated { + paths: paths.clone(), + language_server_id: *server_id, + }) + } + LspStoreEvent::LanguageServerAdded(server_id, name, worktree_id) => cx.emit( + Event::LanguageServerAdded(*server_id, name.clone(), *worktree_id), ), - LspStoreEvent::LanguageServerRemoved(language_server_id) => { - cx.emit(Event::LanguageServerRemoved(*language_server_id)) + LspStoreEvent::LanguageServerRemoved(server_id) => { + cx.emit(Event::LanguageServerRemoved(*server_id)) } LspStoreEvent::LanguageServerLog(server_id, log_type, string) => cx.emit( Event::LanguageServerLog(*server_id, log_type.clone(), string.clone()), @@ -3829,27 +3828,6 @@ impl Project { }) } - pub fn update_diagnostics( - &mut self, - language_server_id: LanguageServerId, - source_kind: DiagnosticSourceKind, - result_id: Option, - params: lsp::PublishDiagnosticsParams, - disk_based_sources: &[String], - cx: &mut Context, - ) -> Result<(), anyhow::Error> { - self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.update_diagnostics( - language_server_id, - params, - result_id, - source_kind, - disk_based_sources, - cx, - ) - }) - } - pub fn search(&mut self, query: SearchQuery, cx: &mut Context) -> Receiver { let (result_tx, result_rx) = smol::channel::unbounded(); diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 9c6d9ec979..cb3c9efe60 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -20,8 +20,8 @@ use gpui::{App, BackgroundExecutor, SemanticVersion, UpdateGlobal}; use http_client::Url; use itertools::Itertools; use language::{ - Diagnostic, DiagnosticEntry, DiagnosticSet, DiskState, FakeLspAdapter, LanguageConfig, - LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint, + Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, DiskState, FakeLspAdapter, + LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint, language_settings::{AllLanguageSettings, LanguageSettingsContent, language_settings}, tree_sitter_rust, tree_sitter_typescript, }; @@ -1619,7 +1619,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { events.next().await.unwrap(), Event::DiagnosticsUpdated { language_server_id: LanguageServerId(0), - path: (worktree_id, Path::new("a.rs")).into() + paths: vec![(worktree_id, Path::new("a.rs")).into()], } ); @@ -1667,7 +1667,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { events.next().await.unwrap(), Event::DiagnosticsUpdated { language_server_id: LanguageServerId(0), - path: (worktree_id, Path::new("a.rs")).into() + paths: vec![(worktree_id, Path::new("a.rs")).into()], } ); diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index 164a7d3a50..ea9647feff 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -525,6 +525,7 @@ message UpdateDiagnosticSummary { uint64 project_id = 1; uint64 worktree_id = 2; DiagnosticSummary summary = 3; + repeated DiagnosticSummary more_summaries = 4; } message DiagnosticSummary { From 90fa06dd61add35ec4e3a7ff8cc81ee5c5244937 Mon Sep 17 00:00:00 2001 From: localcc Date: Thu, 7 Aug 2025 16:47:19 +0200 Subject: [PATCH 152/693] Fix file unlocking after closing the workspace (#35741) Release Notes: - Fixed folders being locked after closing them in zed --- crates/fs/src/fs_watcher.rs | 189 ++++++++++++++++++++++++++---------- 1 file changed, 137 insertions(+), 52 deletions(-) diff --git a/crates/fs/src/fs_watcher.rs b/crates/fs/src/fs_watcher.rs index 9fdf2ad0b1..c9da4fa957 100644 --- a/crates/fs/src/fs_watcher.rs +++ b/crates/fs/src/fs_watcher.rs @@ -1,6 +1,9 @@ use notify::EventKind; use parking_lot::Mutex; -use std::sync::{Arc, OnceLock}; +use std::{ + collections::HashMap, + sync::{Arc, OnceLock}, +}; use util::{ResultExt, paths::SanitizedPath}; use crate::{PathEvent, PathEventKind, Watcher}; @@ -8,6 +11,7 @@ use crate::{PathEvent, PathEventKind, Watcher}; pub struct FsWatcher { tx: smol::channel::Sender<()>, pending_path_events: Arc>>, + registrations: Mutex, WatcherRegistrationId>>, } impl FsWatcher { @@ -18,10 +22,24 @@ impl FsWatcher { Self { tx, pending_path_events, + registrations: Default::default(), } } } +impl Drop for FsWatcher { + fn drop(&mut self) { + let mut registrations = self.registrations.lock(); + let registrations = registrations.drain(); + + let _ = global(|g| { + for (_, registration) in registrations { + g.remove(registration); + } + }); + } +} + impl Watcher for FsWatcher { fn add(&self, path: &std::path::Path) -> anyhow::Result<()> { let root_path = SanitizedPath::from(path); @@ -29,75 +47,136 @@ impl Watcher for FsWatcher { let tx = self.tx.clone(); let pending_paths = self.pending_path_events.clone(); - use notify::Watcher; + let path: Arc = path.into(); - global({ + if self.registrations.lock().contains_key(&path) { + return Ok(()); + } + + let registration_id = global({ + let path = path.clone(); |g| { - g.add(move |event: ¬ify::Event| { - let kind = match event.kind { - EventKind::Create(_) => Some(PathEventKind::Created), - EventKind::Modify(_) => Some(PathEventKind::Changed), - EventKind::Remove(_) => Some(PathEventKind::Removed), - _ => None, - }; - let mut path_events = event - .paths - .iter() - .filter_map(|event_path| { - let event_path = SanitizedPath::from(event_path); - event_path.starts_with(&root_path).then(|| PathEvent { - path: event_path.as_path().to_path_buf(), - kind, + g.add( + path, + notify::RecursiveMode::NonRecursive, + move |event: ¬ify::Event| { + let kind = match event.kind { + EventKind::Create(_) => Some(PathEventKind::Created), + EventKind::Modify(_) => Some(PathEventKind::Changed), + EventKind::Remove(_) => Some(PathEventKind::Removed), + _ => None, + }; + let mut path_events = event + .paths + .iter() + .filter_map(|event_path| { + let event_path = SanitizedPath::from(event_path); + event_path.starts_with(&root_path).then(|| PathEvent { + path: event_path.as_path().to_path_buf(), + kind, + }) }) - }) - .collect::>(); + .collect::>(); - if !path_events.is_empty() { - path_events.sort(); - let mut pending_paths = pending_paths.lock(); - if pending_paths.is_empty() { - tx.try_send(()).ok(); + if !path_events.is_empty() { + path_events.sort(); + let mut pending_paths = pending_paths.lock(); + if pending_paths.is_empty() { + tx.try_send(()).ok(); + } + util::extend_sorted( + &mut *pending_paths, + path_events, + usize::MAX, + |a, b| a.path.cmp(&b.path), + ); } - util::extend_sorted( - &mut *pending_paths, - path_events, - usize::MAX, - |a, b| a.path.cmp(&b.path), - ); - } - }) + }, + ) } - })?; - - global(|g| { - g.watcher - .lock() - .watch(path, notify::RecursiveMode::NonRecursive) })??; + self.registrations.lock().insert(path, registration_id); + Ok(()) } fn remove(&self, path: &std::path::Path) -> anyhow::Result<()> { - use notify::Watcher; - Ok(global(|w| w.watcher.lock().unwatch(path))??) + let Some(registration) = self.registrations.lock().remove(path) else { + return Ok(()); + }; + + global(|w| w.remove(registration)) } } -pub struct GlobalWatcher { +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct WatcherRegistrationId(u32); + +struct WatcherRegistrationState { + callback: Box, + path: Arc, +} + +struct WatcherState { // two mutexes because calling watcher.add triggers an watcher.event, which needs watchers. #[cfg(target_os = "linux")] - pub(super) watcher: Mutex, + watcher: notify::INotifyWatcher, #[cfg(target_os = "freebsd")] - pub(super) watcher: Mutex, + watcher: notify::KqueueWatcher, #[cfg(target_os = "windows")] - pub(super) watcher: Mutex, - pub(super) watchers: Mutex>>, + watcher: notify::ReadDirectoryChangesWatcher, + + watchers: HashMap, + path_registrations: HashMap, u32>, + last_registration: WatcherRegistrationId, +} + +pub struct GlobalWatcher { + state: Mutex, } impl GlobalWatcher { - pub(super) fn add(&self, cb: impl Fn(¬ify::Event) + Send + Sync + 'static) { - self.watchers.lock().push(Box::new(cb)) + #[must_use] + fn add( + &self, + path: Arc, + mode: notify::RecursiveMode, + cb: impl Fn(¬ify::Event) + Send + Sync + 'static, + ) -> anyhow::Result { + use notify::Watcher; + let mut state = self.state.lock(); + + state.watcher.watch(&path, mode)?; + + let id = state.last_registration; + state.last_registration = WatcherRegistrationId(id.0 + 1); + + let registration_state = WatcherRegistrationState { + callback: Box::new(cb), + path: path.clone(), + }; + state.watchers.insert(id, registration_state); + *state.path_registrations.entry(path.clone()).or_insert(0) += 1; + + Ok(id) + } + + pub fn remove(&self, id: WatcherRegistrationId) { + use notify::Watcher; + let mut state = self.state.lock(); + let Some(registration_state) = state.watchers.remove(&id) else { + return; + }; + + let Some(count) = state.path_registrations.get_mut(®istration_state.path) else { + return; + }; + *count -= 1; + if *count == 0 { + state.watcher.unwatch(®istration_state.path).log_err(); + state.path_registrations.remove(®istration_state.path); + } } } @@ -114,8 +193,10 @@ fn handle_event(event: Result) { return; }; global::<()>(move |watcher| { - for f in watcher.watchers.lock().iter() { - f(&event) + let state = watcher.state.lock(); + for registration in state.watchers.values() { + let callback = ®istration.callback; + callback(&event); } }) .log_err(); @@ -124,8 +205,12 @@ fn handle_event(event: Result) { pub fn global(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result { let result = FS_WATCHER_INSTANCE.get_or_init(|| { notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher { - watcher: Mutex::new(file_watcher), - watchers: Default::default(), + state: Mutex::new(WatcherState { + watcher: file_watcher, + watchers: Default::default(), + path_registrations: Default::default(), + last_registration: Default::default(), + }), }) }); match result { From 262365ca241cafc3008b79bd61724f08e93b676c Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 7 Aug 2025 11:50:11 -0300 Subject: [PATCH 153/693] keymap editor: Refine how we display matching keystrokes (#35796) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit | Before | After | |--------|--------| | CleanShot 2025-08-07 at 10  54
42@2x | CleanShot 2025-08-07 at 11  29
47@2x | Release Notes: - N/A --- crates/settings_ui/src/keybindings.rs | 100 ++++++++++-------- .../src/ui_components/keystroke_input.rs | 2 +- crates/ui_input/src/ui_input.rs | 2 +- 3 files changed, 59 insertions(+), 45 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 60e527677a..599bb0b18f 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -2415,6 +2415,7 @@ impl Render for KeybindingEditorModal { .header( ModalHeader::new().child( v_flex() + .w_full() .pb_1p5() .mb_1() .gap_0p5() @@ -2438,17 +2439,55 @@ impl Render for KeybindingEditorModal { .section( Section::new().child( v_flex() - .gap_2() + .gap_2p5() .child( v_flex() - .child(Label::new("Edit Keystroke")) .gap_1() - .child(self.keybind_editor.clone()), + .child(Label::new("Edit Keystroke")) + .child(self.keybind_editor.clone()) + .child(h_flex().gap_px().when( + matching_bindings_count > 0, + |this| { + let label = format!( + "There {} {} {} with the same keystrokes.", + if matching_bindings_count == 1 { + "is" + } else { + "are" + }, + matching_bindings_count, + if matching_bindings_count == 1 { + "binding" + } else { + "bindings" + } + ); + + this.child( + Label::new(label) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child( + Button::new("show_matching", "View") + .label_size(LabelSize::Small) + .icon(IconName::ArrowUpRight) + .icon_color(Color::Muted) + .icon_size(IconSize::XSmall) + .on_click(cx.listener( + |this, _, window, cx| { + this.show_matching_bindings( + window, cx, + ); + }, + )), + ) + }, + )), ) .when_some(self.action_arguments_editor.clone(), |this, editor| { this.child( v_flex() - .mt_1p5() .gap_1() .child(Label::new("Edit Arguments")) .child(editor), @@ -2459,50 +2498,25 @@ impl Render for KeybindingEditorModal { this.child( Banner::new() .severity(error.severity) - // For some reason, the div overflows its container to the - //right. The padding accounts for that. - .child( - div() - .size_full() - .pr_2() - .child(Label::new(error.content.clone())), - ), + .child(Label::new(error.content.clone())), ) }), ), ) .footer( - ModalFooter::new() - .start_slot( - div().when(matching_bindings_count > 0, |this| { - this.child( - Button::new("show_matching", format!( - "There {} {} {} with the same keystrokes. Click to view", - if matching_bindings_count == 1 { "is" } else { "are" }, - matching_bindings_count, - if matching_bindings_count == 1 { "binding" } else { "bindings" } - )) - .style(ButtonStyle::Transparent) - .color(Color::Accent) - .on_click(cx.listener(|this, _, window, cx| { - this.show_matching_bindings(window, cx); - })) - ) - }) - ) - .end_slot( - h_flex() - .gap_1() - .child( - Button::new("cancel", "Cancel") - .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), - ) - .child(Button::new("save-btn", "Save").on_click(cx.listener( - |this, _event, _window, cx| { - this.save_or_display_error(cx); - }, - ))), - ), + ModalFooter::new().end_slot( + h_flex() + .gap_1() + .child( + Button::new("cancel", "Cancel") + .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), + ) + .child(Button::new("save-btn", "Save").on_click(cx.listener( + |this, _event, _window, cx| { + this.save_or_display_error(cx); + }, + ))), + ), ), ) } diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/settings_ui/src/ui_components/keystroke_input.rs index 03d27d0ab9..ee5c4036ea 100644 --- a/crates/settings_ui/src/ui_components/keystroke_input.rs +++ b/crates/settings_ui/src/ui_components/keystroke_input.rs @@ -529,7 +529,7 @@ impl Render for KeystrokeInput { .w_full() .flex_1() .justify_between() - .rounded_lg() + .rounded_sm() .overflow_hidden() .map(|this| { if is_recording { diff --git a/crates/ui_input/src/ui_input.rs b/crates/ui_input/src/ui_input.rs index 309b3f62f6..1a5bebaf1e 100644 --- a/crates/ui_input/src/ui_input.rs +++ b/crates/ui_input/src/ui_input.rs @@ -168,7 +168,7 @@ impl Render for SingleLineInput { .py_1p5() .flex_grow() .text_color(style.text_color) - .rounded_lg() + .rounded_sm() .bg(style.background_color) .border_1() .border_color(style.border_color) From dd840e4b2776a66bc8b2f6ad8379e7fdb914c0c5 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:01:22 +0200 Subject: [PATCH 154/693] editor: Fix multi-buffer headers spilling over at narrow widths (#35800) Release Notes: - N/A --- crates/editor/src/element.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 17a43f9640..034fff970d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -3682,6 +3682,7 @@ impl EditorElement { .id("path header block") .size_full() .justify_between() + .overflow_hidden() .child( h_flex() .gap_2() From 22342206183005b8dd58ecb6d6cac8efcac54a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Raphael=20L=C3=BCthy?= Date: Thu, 7 Aug 2025 17:27:29 +0200 Subject: [PATCH 155/693] completions: Add subtle/eager behavior to Supermaven and Copilot (#35548) This pull request introduces changes to improve the behavior and consistency of multiple completion providers (`CopilotCompletionProvider`, `SupermavenCompletionProvider`) and their integration with UI elements like menus and inline completion buttons. It now allows to see the prediction with the completion menu open whilst pressing `opt` and also enables the subtle/eager setting that was introduced with zeta. Edit: I managed to get the preview working with correct icons! image CleanShot 2025-08-04 at 01 36 31@2x Correct icons are also displayed: image Edit2: I added some comments, would be very happy to receive feedback (still learning rust) Release Notes: - Added Subtle and Eager edit prediction modes to Copilot and Supermaven --- .../src/copilot_completion_provider.rs | 23 +-- crates/edit_prediction/src/edit_prediction.rs | 9 ++ .../src/edit_prediction_button.rs | 7 +- crates/editor/src/edit_prediction_tests.rs | 152 ++++++++++++++++++ crates/editor/src/editor.rs | 141 +++++++++++----- crates/supermaven/src/supermaven.rs | 92 +++++++++-- .../src/supermaven_completion_provider.rs | 11 +- 7 files changed, 372 insertions(+), 63 deletions(-) diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 2a7225c4e3..2fd6df27b9 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -58,11 +58,19 @@ impl EditPredictionProvider for CopilotCompletionProvider { } fn show_completions_in_menu() -> bool { + true + } + + fn show_tab_accept_marker() -> bool { + true + } + + fn supports_jump_to_edit() -> bool { false } fn is_refreshing(&self) -> bool { - self.pending_refresh.is_some() + self.pending_refresh.is_some() && self.completions.is_empty() } fn is_enabled( @@ -343,8 +351,8 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, window, cx| { assert!(editor.context_menu_visible()); - assert!(!editor.has_active_edit_prediction()); - // Since we have both, the copilot suggestion is not shown inline + assert!(editor.has_active_edit_prediction()); + // Since we have both, the copilot suggestion is existing but does not show up as ghost text assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n"); @@ -934,8 +942,9 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, _, cx| { assert!(editor.context_menu_visible()); - assert!(!editor.has_active_edit_prediction(),); + assert!(editor.has_active_edit_prediction()); assert_eq!(editor.text(cx), "one\ntwo.\nthree\n"); + assert_eq!(editor.display_text(cx), "one\ntwo.\nthree\n"); }); } @@ -1077,8 +1086,6 @@ mod tests { vec![complete_from_marker.clone(), replace_range_marker.clone()], ); - let complete_from_position = - cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start); let replace_range = cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone()); @@ -1087,10 +1094,6 @@ mod tests { let completions = completions.clone(); async move { assert_eq!(params.text_document_position.text_document.uri, url.clone()); - assert_eq!( - params.text_document_position.position, - complete_from_position - ); Ok(Some(lsp::CompletionResponse::Array( completions .iter() diff --git a/crates/edit_prediction/src/edit_prediction.rs b/crates/edit_prediction/src/edit_prediction.rs index fd4e9bb21d..c8502f75de 100644 --- a/crates/edit_prediction/src/edit_prediction.rs +++ b/crates/edit_prediction/src/edit_prediction.rs @@ -61,6 +61,10 @@ pub trait EditPredictionProvider: 'static + Sized { fn show_tab_accept_marker() -> bool { false } + fn supports_jump_to_edit() -> bool { + true + } + fn data_collection_state(&self, _cx: &App) -> DataCollectionState { DataCollectionState::Unsupported } @@ -116,6 +120,7 @@ pub trait EditPredictionProviderHandle { ) -> bool; fn show_completions_in_menu(&self) -> bool; fn show_tab_accept_marker(&self) -> bool; + fn supports_jump_to_edit(&self) -> bool; fn data_collection_state(&self, cx: &App) -> DataCollectionState; fn usage(&self, cx: &App) -> Option; fn toggle_data_collection(&self, cx: &mut App); @@ -166,6 +171,10 @@ where T::show_tab_accept_marker() } + fn supports_jump_to_edit(&self) -> bool { + T::supports_jump_to_edit() + } + fn data_collection_state(&self, cx: &App) -> DataCollectionState { self.read(cx).data_collection_state(cx) } diff --git a/crates/edit_prediction_button/src/edit_prediction_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs index 9ab94a4095..3d3b43d71b 100644 --- a/crates/edit_prediction_button/src/edit_prediction_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -491,7 +491,12 @@ impl EditPredictionButton { let subtle_mode = matches!(current_mode, EditPredictionsMode::Subtle); let eager_mode = matches!(current_mode, EditPredictionsMode::Eager); - if matches!(provider, EditPredictionProvider::Zed) { + if matches!( + provider, + EditPredictionProvider::Zed + | EditPredictionProvider::Copilot + | EditPredictionProvider::Supermaven + ) { menu = menu .separator() .header("Display Modes") diff --git a/crates/editor/src/edit_prediction_tests.rs b/crates/editor/src/edit_prediction_tests.rs index 527dfb8832..7bf51e45d7 100644 --- a/crates/editor/src/edit_prediction_tests.rs +++ b/crates/editor/src/edit_prediction_tests.rs @@ -228,6 +228,49 @@ async fn test_edit_prediction_invalidation_range(cx: &mut gpui::TestAppContext) }); } +#[gpui::test] +async fn test_edit_prediction_jump_disabled_for_non_zed_providers(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let provider = cx.new(|_| FakeNonZedEditPredictionProvider::default()); + assign_editor_completion_provider_non_zed(provider.clone(), &mut cx); + + // Cursor is 2+ lines above the proposed edit + cx.set_state(indoc! {" + line 0 + line ˇ1 + line 2 + line 3 + line + "}); + + propose_edits_non_zed( + &provider, + vec![(Point::new(4, 3)..Point::new(4, 3), " 4")], + &mut cx, + ); + + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); + + // For non-Zed providers, there should be no move completion (jump functionality disabled) + cx.editor(|editor, _, _| { + if let Some(completion_state) = &editor.active_edit_prediction { + // Should be an Edit prediction, not a Move prediction + match &completion_state.completion { + EditPrediction::Edit { .. } => { + // This is expected for non-Zed providers + } + EditPrediction::Move { .. } => { + panic!( + "Non-Zed providers should not show Move predictions (jump functionality)" + ); + } + } + } + }); +} + fn assert_editor_active_edit_completion( cx: &mut EditorTestContext, assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range, String)>), @@ -301,6 +344,37 @@ fn assign_editor_completion_provider( }) } +fn propose_edits_non_zed( + provider: &Entity, + edits: Vec<(Range, &str)>, + cx: &mut EditorTestContext, +) { + let snapshot = cx.buffer_snapshot(); + let edits = edits.into_iter().map(|(range, text)| { + let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end); + (range, text.into()) + }); + + cx.update(|_, cx| { + provider.update(cx, |provider, _| { + provider.set_edit_prediction(Some(edit_prediction::EditPrediction { + id: None, + edits: edits.collect(), + edit_preview: None, + })) + }) + }); +} + +fn assign_editor_completion_provider_non_zed( + provider: Entity, + cx: &mut EditorTestContext, +) { + cx.update_editor(|editor, window, cx| { + editor.set_edit_prediction_provider(Some(provider), window, cx); + }) +} + #[derive(Default, Clone)] pub struct FakeEditPredictionProvider { pub completion: Option, @@ -325,6 +399,84 @@ impl EditPredictionProvider for FakeEditPredictionProvider { false } + fn supports_jump_to_edit() -> bool { + true + } + + fn is_enabled( + &self, + _buffer: &gpui::Entity, + _cursor_position: language::Anchor, + _cx: &gpui::App, + ) -> bool { + true + } + + fn is_refreshing(&self) -> bool { + false + } + + fn refresh( + &mut self, + _project: Option>, + _buffer: gpui::Entity, + _cursor_position: language::Anchor, + _debounce: bool, + _cx: &mut gpui::Context, + ) { + } + + fn cycle( + &mut self, + _buffer: gpui::Entity, + _cursor_position: language::Anchor, + _direction: edit_prediction::Direction, + _cx: &mut gpui::Context, + ) { + } + + fn accept(&mut self, _cx: &mut gpui::Context) {} + + fn discard(&mut self, _cx: &mut gpui::Context) {} + + fn suggest<'a>( + &mut self, + _buffer: &gpui::Entity, + _cursor_position: language::Anchor, + _cx: &mut gpui::Context, + ) -> Option { + self.completion.clone() + } +} + +#[derive(Default, Clone)] +pub struct FakeNonZedEditPredictionProvider { + pub completion: Option, +} + +impl FakeNonZedEditPredictionProvider { + pub fn set_edit_prediction(&mut self, completion: Option) { + self.completion = completion; + } +} + +impl EditPredictionProvider for FakeNonZedEditPredictionProvider { + fn name() -> &'static str { + "fake-non-zed-provider" + } + + fn display_name() -> &'static str { + "Fake Non-Zed Provider" + } + + fn show_completions_in_menu() -> bool { + false + } + + fn supports_jump_to_edit() -> bool { + false + } + fn is_enabled( &self, _buffer: &gpui::Entity, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 73a81bea19..677acd9fd8 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7760,8 +7760,14 @@ impl Editor { } else { None }; - let is_move = - move_invalidation_row_range.is_some() || self.edit_predictions_hidden_for_vim_mode; + let supports_jump = self + .edit_prediction_provider + .as_ref() + .map(|provider| provider.provider.supports_jump_to_edit()) + .unwrap_or(true); + + let is_move = supports_jump + && (move_invalidation_row_range.is_some() || self.edit_predictions_hidden_for_vim_mode); let completion = if is_move { invalidation_row_range = move_invalidation_row_range.unwrap_or(edit_start_row..edit_end_row); @@ -8799,8 +8805,12 @@ impl Editor { return None; } - let highlighted_edits = - crate::edit_prediction_edit_text(&snapshot, edits, edit_preview.as_ref()?, false, cx); + let highlighted_edits = if let Some(edit_preview) = edit_preview.as_ref() { + crate::edit_prediction_edit_text(&snapshot, edits, edit_preview, false, cx) + } else { + // Fallback for providers without edit_preview + crate::edit_prediction_fallback_text(edits, cx) + }; let styled_text = highlighted_edits.to_styled_text(&style.text); let line_count = highlighted_edits.text.lines().count(); @@ -9068,6 +9078,18 @@ impl Editor { let editor_bg_color = cx.theme().colors().editor_background; editor_bg_color.blend(accent_color.opacity(0.6)) } + fn get_prediction_provider_icon_name( + provider: &Option, + ) -> IconName { + match provider { + Some(provider) => match provider.provider.name() { + "copilot" => IconName::Copilot, + "supermaven" => IconName::Supermaven, + _ => IconName::ZedPredict, + }, + None => IconName::ZedPredict, + } + } fn render_edit_prediction_cursor_popover( &self, @@ -9080,6 +9102,7 @@ impl Editor { cx: &mut Context, ) -> Option { let provider = self.edit_prediction_provider.as_ref()?; + let provider_icon = Self::get_prediction_provider_icon_name(&self.edit_prediction_provider); if provider.provider.needs_terms_acceptance(cx) { return Some( @@ -9106,7 +9129,7 @@ impl Editor { h_flex() .flex_1() .gap_2() - .child(Icon::new(IconName::ZedPredict)) + .child(Icon::new(provider_icon)) .child(Label::new("Accept Terms of Service")) .child(div().w_full()) .child( @@ -9122,12 +9145,8 @@ impl Editor { let is_refreshing = provider.provider.is_refreshing(cx); - fn pending_completion_container() -> Div { - h_flex() - .h_full() - .flex_1() - .gap_2() - .child(Icon::new(IconName::ZedPredict)) + fn pending_completion_container(icon: IconName) -> Div { + h_flex().h_full().flex_1().gap_2().child(Icon::new(icon)) } let completion = match &self.active_edit_prediction { @@ -9157,7 +9176,7 @@ impl Editor { Icon::new(IconName::ZedPredictUp) } } - EditPrediction::Edit { .. } => Icon::new(IconName::ZedPredict), + EditPrediction::Edit { .. } => Icon::new(provider_icon), })) .child( h_flex() @@ -9224,15 +9243,15 @@ impl Editor { cx, )?, - None => { - pending_completion_container().child(Label::new("...").size(LabelSize::Small)) - } + None => pending_completion_container(provider_icon) + .child(Label::new("...").size(LabelSize::Small)), }, - None => pending_completion_container().child(Label::new("No Prediction")), + None => pending_completion_container(provider_icon) + .child(Label::new("...").size(LabelSize::Small)), }; - let completion = if is_refreshing { + let completion = if is_refreshing || self.active_edit_prediction.is_none() { completion .with_animation( "loading-completion", @@ -9332,23 +9351,35 @@ impl Editor { .child(Icon::new(arrow).color(Color::Muted).size(IconSize::Small)) } + let supports_jump = self + .edit_prediction_provider + .as_ref() + .map(|provider| provider.provider.supports_jump_to_edit()) + .unwrap_or(true); + match &completion.completion { EditPrediction::Move { target, snapshot, .. - } => Some( - h_flex() - .px_2() - .gap_2() - .flex_1() - .child( - if target.text_anchor.to_point(&snapshot).row > cursor_point.row { - Icon::new(IconName::ZedPredictDown) - } else { - Icon::new(IconName::ZedPredictUp) - }, - ) - .child(Label::new("Jump to Edit")), - ), + } => { + if !supports_jump { + return None; + } + + Some( + h_flex() + .px_2() + .gap_2() + .flex_1() + .child( + if target.text_anchor.to_point(&snapshot).row > cursor_point.row { + Icon::new(IconName::ZedPredictDown) + } else { + Icon::new(IconName::ZedPredictUp) + }, + ) + .child(Label::new("Jump to Edit")), + ) + } EditPrediction::Edit { edits, @@ -9358,14 +9389,13 @@ impl Editor { } => { let first_edit_row = edits.first()?.0.start.text_anchor.to_point(&snapshot).row; - let (highlighted_edits, has_more_lines) = crate::edit_prediction_edit_text( - &snapshot, - &edits, - edit_preview.as_ref()?, - true, - cx, - ) - .first_line_preview(); + let (highlighted_edits, has_more_lines) = + if let Some(edit_preview) = edit_preview.as_ref() { + crate::edit_prediction_edit_text(&snapshot, &edits, edit_preview, true, cx) + .first_line_preview() + } else { + crate::edit_prediction_fallback_text(&edits, cx).first_line_preview() + }; let styled_text = gpui::StyledText::new(highlighted_edits.text) .with_default_highlights(&style.text, highlighted_edits.highlights); @@ -9376,11 +9406,13 @@ impl Editor { .child(styled_text) .when(has_more_lines, |parent| parent.child("…")); - let left = if first_edit_row != cursor_point.row { + let left = if supports_jump && first_edit_row != cursor_point.row { render_relative_row_jump("", cursor_point.row, first_edit_row) .into_any_element() } else { - Icon::new(IconName::ZedPredict).into_any_element() + let icon_name = + Editor::get_prediction_provider_icon_name(&self.edit_prediction_provider); + Icon::new(icon_name).into_any_element() }; Some( @@ -23270,6 +23302,33 @@ fn edit_prediction_edit_text( edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx) } +fn edit_prediction_fallback_text(edits: &[(Range, String)], cx: &App) -> HighlightedText { + // Fallback for providers that don't provide edit_preview (like Copilot/Supermaven) + // Just show the raw edit text with basic styling + let mut text = String::new(); + let mut highlights = Vec::new(); + + let insertion_highlight_style = HighlightStyle { + color: Some(cx.theme().colors().text), + ..Default::default() + }; + + for (_, edit_text) in edits { + let start_offset = text.len(); + text.push_str(edit_text); + let end_offset = text.len(); + + if start_offset < end_offset { + highlights.push((start_offset..end_offset, insertion_highlight_style)); + } + } + + HighlightedText { + text: text.into(), + highlights, + } +} + pub fn diagnostic_style(severity: lsp::DiagnosticSeverity, colors: &StatusColors) -> Hsla { match severity { lsp::DiagnosticSeverity::ERROR => colors.error, diff --git a/crates/supermaven/src/supermaven.rs b/crates/supermaven/src/supermaven.rs index ab500fb79d..a31b96d882 100644 --- a/crates/supermaven/src/supermaven.rs +++ b/crates/supermaven/src/supermaven.rs @@ -234,16 +234,14 @@ fn find_relevant_completion<'a>( } let original_cursor_offset = buffer.clip_offset(state.prefix_offset, text::Bias::Left); - let text_inserted_since_completion_request = - buffer.text_for_range(original_cursor_offset..current_cursor_offset); - let mut trimmed_completion = state_completion; - for chunk in text_inserted_since_completion_request { - if let Some(suffix) = trimmed_completion.strip_prefix(chunk) { - trimmed_completion = suffix; - } else { - continue 'completions; - } - } + let text_inserted_since_completion_request: String = buffer + .text_for_range(original_cursor_offset..current_cursor_offset) + .collect(); + let trimmed_completion = + match state_completion.strip_prefix(&text_inserted_since_completion_request) { + Some(suffix) => suffix, + None => continue 'completions, + }; if best_completion.map_or(false, |best| best.len() > trimmed_completion.len()) { continue; @@ -439,3 +437,77 @@ pub struct SupermavenCompletion { pub id: SupermavenCompletionStateId, pub updates: watch::Receiver<()>, } + +#[cfg(test)] +mod tests { + use super::*; + use collections::BTreeMap; + use gpui::TestAppContext; + use language::Buffer; + + #[gpui::test] + async fn test_find_relevant_completion_no_first_letter_skip(cx: &mut TestAppContext) { + let buffer = cx.new(|cx| Buffer::local("hello world", cx)); + let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); + + let mut states = BTreeMap::new(); + let state_id = SupermavenCompletionStateId(1); + let (updates_tx, _) = watch::channel(); + + states.insert( + state_id, + SupermavenCompletionState { + buffer_id: buffer.entity_id(), + prefix_anchor: buffer_snapshot.anchor_before(0), // Start of buffer + prefix_offset: 0, + text: "hello".to_string(), + dedent: String::new(), + updates_tx, + }, + ); + + let cursor_position = buffer_snapshot.anchor_after(1); + + let result = find_relevant_completion( + &states, + buffer.entity_id(), + &buffer_snapshot, + cursor_position, + ); + + assert_eq!(result, Some("ello")); + } + + #[gpui::test] + async fn test_find_relevant_completion_with_multiple_chars(cx: &mut TestAppContext) { + let buffer = cx.new(|cx| Buffer::local("hello world", cx)); + let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); + + let mut states = BTreeMap::new(); + let state_id = SupermavenCompletionStateId(1); + let (updates_tx, _) = watch::channel(); + + states.insert( + state_id, + SupermavenCompletionState { + buffer_id: buffer.entity_id(), + prefix_anchor: buffer_snapshot.anchor_before(0), // Start of buffer + prefix_offset: 0, + text: "hello".to_string(), + dedent: String::new(), + updates_tx, + }, + ); + + let cursor_position = buffer_snapshot.anchor_after(3); + + let result = find_relevant_completion( + &states, + buffer.entity_id(), + &buffer_snapshot, + cursor_position, + ); + + assert_eq!(result, Some("lo")); + } +} diff --git a/crates/supermaven/src/supermaven_completion_provider.rs b/crates/supermaven/src/supermaven_completion_provider.rs index 2660a03e6f..1b1fc54a7a 100644 --- a/crates/supermaven/src/supermaven_completion_provider.rs +++ b/crates/supermaven/src/supermaven_completion_provider.rs @@ -108,6 +108,14 @@ impl EditPredictionProvider for SupermavenCompletionProvider { } fn show_completions_in_menu() -> bool { + true + } + + fn show_tab_accept_marker() -> bool { + true + } + + fn supports_jump_to_edit() -> bool { false } @@ -116,7 +124,7 @@ impl EditPredictionProvider for SupermavenCompletionProvider { } fn is_refreshing(&self) -> bool { - self.pending_refresh.is_some() + self.pending_refresh.is_some() && self.completion_id.is_none() } fn refresh( @@ -197,6 +205,7 @@ impl EditPredictionProvider for SupermavenCompletionProvider { let mut point = cursor_position.to_point(&snapshot); point.column = snapshot.line_len(point.row); let range = cursor_position..snapshot.anchor_after(point); + Some(completion_from_diff( snapshot, completion_text, From efba2cbfd371bdd85dc3bfdd6b98d1d405ad9a89 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:32:06 +0200 Subject: [PATCH 156/693] chore: Bump Rust to 1.89 (#35788) Release Notes: - N/A --------- Co-authored-by: Julia Ryan --- Dockerfile-collab | 2 +- crates/agent2/src/templates.rs | 4 +++ crates/agent2/src/tools/glob.rs | 4 +++ crates/fs/src/fake_git_repo.rs | 4 +-- crates/git/src/repository.rs | 8 +++--- crates/gpui/src/keymap/context.rs | 30 +++++++++++--------- crates/gpui/src/platform/windows/wrapper.rs | 24 +--------------- crates/terminal_view/src/terminal_element.rs | 2 +- flake.lock | 18 ++++++------ rust-toolchain.toml | 2 +- 10 files changed, 43 insertions(+), 55 deletions(-) diff --git a/Dockerfile-collab b/Dockerfile-collab index 2dafe296c7..c1621d6ee6 100644 --- a/Dockerfile-collab +++ b/Dockerfile-collab @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.2 -FROM rust:1.88-bookworm as builder +FROM rust:1.89-bookworm as builder WORKDIR app COPY . . diff --git a/crates/agent2/src/templates.rs b/crates/agent2/src/templates.rs index 7d51a626fc..e634d414d6 100644 --- a/crates/agent2/src/templates.rs +++ b/crates/agent2/src/templates.rs @@ -33,6 +33,10 @@ pub trait Template: Sized { } } +#[expect( + dead_code, + reason = "Marked as unused by Rust 1.89 and left as is as of 07 Aug 2025 to let AI team address it." +)] #[derive(Serialize)] pub struct GlobTemplate { pub project_roots: String, diff --git a/crates/agent2/src/tools/glob.rs b/crates/agent2/src/tools/glob.rs index f44ce9f359..4dace7c074 100644 --- a/crates/agent2/src/tools/glob.rs +++ b/crates/agent2/src/tools/glob.rs @@ -19,6 +19,10 @@ struct GlobInput { glob: SharedString, } +#[expect( + dead_code, + reason = "Marked as unused by Rust 1.89 and left as is as of 07 Aug 2025 to let AI team address it." +)] struct GlobTool { project: Entity, templates: Arc, diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 04ba656232..73da63fd47 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -402,11 +402,11 @@ impl GitRepository for FakeGitRepository { &self, _paths: Vec, _env: Arc>, - ) -> BoxFuture> { + ) -> BoxFuture<'_, Result<()>> { unimplemented!() } - fn stash_pop(&self, _env: Arc>) -> BoxFuture> { + fn stash_pop(&self, _env: Arc>) -> BoxFuture<'_, Result<()>> { unimplemented!() } diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index dc7ab0af65..518b6c4f46 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -399,9 +399,9 @@ pub trait GitRepository: Send + Sync { &self, paths: Vec, env: Arc>, - ) -> BoxFuture>; + ) -> BoxFuture<'_, Result<()>>; - fn stash_pop(&self, env: Arc>) -> BoxFuture>; + fn stash_pop(&self, env: Arc>) -> BoxFuture<'_, Result<()>>; fn push( &self, @@ -1203,7 +1203,7 @@ impl GitRepository for RealGitRepository { &self, paths: Vec, env: Arc>, - ) -> BoxFuture> { + ) -> BoxFuture<'_, Result<()>> { let working_directory = self.working_directory(); self.executor .spawn(async move { @@ -1227,7 +1227,7 @@ impl GitRepository for RealGitRepository { .boxed() } - fn stash_pop(&self, env: Arc>) -> BoxFuture> { + fn stash_pop(&self, env: Arc>) -> BoxFuture<'_, Result<()>> { let working_directory = self.working_directory(); self.executor .spawn(async move { diff --git a/crates/gpui/src/keymap/context.rs b/crates/gpui/src/keymap/context.rs index f4b878ae77..281035fe97 100644 --- a/crates/gpui/src/keymap/context.rs +++ b/crates/gpui/src/keymap/context.rs @@ -461,6 +461,8 @@ fn skip_whitespace(source: &str) -> &str { #[cfg(test)] mod tests { + use core::slice; + use super::*; use crate as gpui; use KeyBindingContextPredicate::*; @@ -674,11 +676,11 @@ mod tests { assert!(predicate.eval(&contexts)); assert!(!predicate.eval(&[])); - assert!(!predicate.eval(&[child_context.clone()])); + assert!(!predicate.eval(slice::from_ref(&child_context))); assert!(!predicate.eval(&[parent_context])); let zany_predicate = KeyBindingContextPredicate::parse("child > child").unwrap(); - assert!(!zany_predicate.eval(&[child_context.clone()])); + assert!(!zany_predicate.eval(slice::from_ref(&child_context))); assert!(zany_predicate.eval(&[child_context.clone(), child_context.clone()])); } @@ -690,13 +692,13 @@ mod tests { let parent_context = KeyContext::try_from("parent").unwrap(); let child_context = KeyContext::try_from("child").unwrap(); - assert!(not_predicate.eval(&[workspace_context.clone()])); - assert!(!not_predicate.eval(&[editor_context.clone()])); + assert!(not_predicate.eval(slice::from_ref(&workspace_context))); + assert!(!not_predicate.eval(slice::from_ref(&editor_context))); assert!(!not_predicate.eval(&[editor_context.clone(), workspace_context.clone()])); assert!(!not_predicate.eval(&[workspace_context.clone(), editor_context.clone()])); let complex_not = KeyBindingContextPredicate::parse("!editor && workspace").unwrap(); - assert!(complex_not.eval(&[workspace_context.clone()])); + assert!(complex_not.eval(slice::from_ref(&workspace_context))); assert!(!complex_not.eval(&[editor_context.clone(), workspace_context.clone()])); let not_mode_predicate = KeyBindingContextPredicate::parse("!(mode == full)").unwrap(); @@ -709,18 +711,18 @@ mod tests { assert!(not_mode_predicate.eval(&[other_mode_context])); let not_descendant = KeyBindingContextPredicate::parse("!(parent > child)").unwrap(); - assert!(not_descendant.eval(&[parent_context.clone()])); - assert!(not_descendant.eval(&[child_context.clone()])); + assert!(not_descendant.eval(slice::from_ref(&parent_context))); + assert!(not_descendant.eval(slice::from_ref(&child_context))); assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); let not_descendant = KeyBindingContextPredicate::parse("parent > !child").unwrap(); - assert!(!not_descendant.eval(&[parent_context.clone()])); - assert!(!not_descendant.eval(&[child_context.clone()])); + assert!(!not_descendant.eval(slice::from_ref(&parent_context))); + assert!(!not_descendant.eval(slice::from_ref(&child_context))); assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); let double_not = KeyBindingContextPredicate::parse("!!editor").unwrap(); - assert!(double_not.eval(&[editor_context.clone()])); - assert!(!double_not.eval(&[workspace_context.clone()])); + assert!(double_not.eval(slice::from_ref(&editor_context))); + assert!(!double_not.eval(slice::from_ref(&workspace_context))); // Test complex descendant cases let workspace_context = KeyContext::try_from("Workspace").unwrap(); @@ -754,9 +756,9 @@ mod tests { // !Workspace - shouldn't match when Workspace is in the context let not_workspace = KeyBindingContextPredicate::parse("!Workspace").unwrap(); - assert!(!not_workspace.eval(&[workspace_context.clone()])); - assert!(not_workspace.eval(&[pane_context.clone()])); - assert!(not_workspace.eval(&[editor_context.clone()])); + assert!(!not_workspace.eval(slice::from_ref(&workspace_context))); + assert!(not_workspace.eval(slice::from_ref(&pane_context))); + assert!(not_workspace.eval(slice::from_ref(&editor_context))); assert!(!not_workspace.eval(&workspace_pane_editor)); } } diff --git a/crates/gpui/src/platform/windows/wrapper.rs b/crates/gpui/src/platform/windows/wrapper.rs index 6015dffdab..a1fe98a392 100644 --- a/crates/gpui/src/platform/windows/wrapper.rs +++ b/crates/gpui/src/platform/windows/wrapper.rs @@ -1,28 +1,6 @@ use std::ops::Deref; -use windows::Win32::{Foundation::HANDLE, UI::WindowsAndMessaging::HCURSOR}; - -#[derive(Debug, Clone, Copy)] -pub(crate) struct SafeHandle { - raw: HANDLE, -} - -unsafe impl Send for SafeHandle {} -unsafe impl Sync for SafeHandle {} - -impl From for SafeHandle { - fn from(value: HANDLE) -> Self { - SafeHandle { raw: value } - } -} - -impl Deref for SafeHandle { - type Target = HANDLE; - - fn deref(&self) -> &Self::Target { - &self.raw - } -} +use windows::Win32::UI::WindowsAndMessaging::HCURSOR; #[derive(Debug, Clone, Copy)] pub(crate) struct SafeCursor { diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 083c07de9c..6c1be9d5e7 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -136,7 +136,7 @@ impl BatchedTextRun { .shape_line( self.text.clone().into(), self.font_size.to_pixels(window.rem_size()), - &[self.style.clone()], + std::slice::from_ref(&self.style), Some(dimensions.cell_width), ) .paint(pos, dimensions.line_height, window, cx); diff --git a/flake.lock b/flake.lock index fa0d51d90d..80022f7b55 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1750266157, - "narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=", + "lastModified": 1754269165, + "narHash": "sha256-0tcS8FHd4QjbCVoxN9jI+PjHgA4vc/IjkUSp+N3zy0U=", "owner": "ipetkov", "repo": "crane", - "rev": "e37c943371b73ed87faf33f7583860f81f1d5a48", + "rev": "444e81206df3f7d92780680e45858e31d2f07a08", "type": "github" }, "original": { @@ -33,10 +33,10 @@ "nixpkgs": { "locked": { "lastModified": 315532800, - "narHash": "sha256-j+zO+IHQ7VwEam0pjPExdbLT2rVioyVS3iq4bLO3GEc=", - "rev": "61c0f513911459945e2cb8bf333dc849f1b976ff", + "narHash": "sha256-5VYevX3GccubYeccRGAXvCPA1ktrGmIX1IFC0icX07g=", + "rev": "a683adc19ff5228af548c6539dbc3440509bfed3", "type": "tarball", - "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre821324.61c0f5139114/nixexprs.tar.xz" + "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre840248.a683adc19ff5/nixexprs.tar.xz" }, "original": { "type": "tarball", @@ -58,11 +58,11 @@ ] }, "locked": { - "lastModified": 1750964660, - "narHash": "sha256-YQ6EyFetjH1uy5JhdhRdPe6cuNXlYpMAQePFfZj4W7M=", + "lastModified": 1754575663, + "narHash": "sha256-afOx8AG0KYtw7mlt6s6ahBBy7eEHZwws3iCRoiuRQS4=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "04f0fcfb1a50c63529805a798b4b5c21610ff390", + "rev": "6db0fb0e9cec2e9729dc52bf4898e6c135bb8a0f", "type": "github" }, "original": { diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f80eab8fbc..3d87025a27 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.88" +channel = "1.89" profile = "minimal" components = [ "rustfmt", "clippy" ] targets = [ From c1d1d1cff6709b51b9bb3f234dc5c21475b13c57 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:43:37 +0200 Subject: [PATCH 157/693] chore: Bump to taffy 0.9 (#35802) Co-authored-by: Anthony Eid Co-authored-by: Lukas Wirth Co-authored-by: Ben Kunkle Release Notes: - N/A Co-authored-by: Anthony Eid Co-authored-by: Lukas Wirth Co-authored-by: Ben Kunkle --- Cargo.lock | 8 ++++---- crates/gpui/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e6a0b6c75f..8b4bc8752d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7506,9 +7506,9 @@ dependencies = [ [[package]] name = "grid" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71b01d27060ad58be4663b9e4ac9e2d4806918e8876af8912afbddd1a91d5eaa" +checksum = "12101ecc8225ea6d675bc70263074eab6169079621c2186fe0c66590b2df9681" [[package]] name = "group" @@ -16219,9 +16219,9 @@ dependencies = [ [[package]] name = "taffy" -version = "0.8.3" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aaef0ac998e6527d6d0d5582f7e43953bb17221ac75bb8eb2fcc2db3396db1c" +checksum = "a13e5d13f79d558b5d353a98072ca8ca0e99da429467804de959aa8c83c9a004" dependencies = [ "arrayvec", "grid", diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 2bf49fa7d8..6e5a76d441 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -121,7 +121,7 @@ smallvec.workspace = true smol.workspace = true strum.workspace = true sum_tree.workspace = true -taffy = "=0.8.3" +taffy = "=0.9.0" thiserror.workspace = true util.workspace = true uuid.workspace = true From fa2ff3ce1c6e46fd23c2623a0827b7e537ce19cc Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 7 Aug 2025 13:26:08 -0400 Subject: [PATCH 158/693] collab: Increase `DATABASE_MAX_CONNECTIONS` for Collab server (#35818) This PR increases the `DATABASE_MAX_CONNECTIONS` limit for the Collab server to 850 (up from 250). Release Notes: - N/A Co-authored-by: Nathan Co-authored-by: Mikayla --- .github/workflows/deploy_collab.yml | 2 ++ crates/collab/k8s/environments/production.sh | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy_collab.yml b/.github/workflows/deploy_collab.yml index f7348a1069..ea264b7a66 100644 --- a/.github/workflows/deploy_collab.yml +++ b/.github/workflows/deploy_collab.yml @@ -137,12 +137,14 @@ jobs: export ZED_SERVICE_NAME=collab export ZED_LOAD_BALANCER_SIZE_UNIT=$ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT + export DATABASE_MAX_CONNECTIONS=850 envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f - kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}" export ZED_SERVICE_NAME=api export ZED_LOAD_BALANCER_SIZE_UNIT=$ZED_API_LOAD_BALANCER_SIZE_UNIT + export DATABASE_MAX_CONNECTIONS=60 envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f - kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}" diff --git a/crates/collab/k8s/environments/production.sh b/crates/collab/k8s/environments/production.sh index e9e68849b8..2861f37896 100644 --- a/crates/collab/k8s/environments/production.sh +++ b/crates/collab/k8s/environments/production.sh @@ -2,5 +2,6 @@ ZED_ENVIRONMENT=production RUST_LOG=info INVITE_LINK_PREFIX=https://zed.dev/invites/ AUTO_JOIN_CHANNEL_ID=283 -DATABASE_MAX_CONNECTIONS=250 +# Set DATABASE_MAX_CONNECTIONS max connections in the `deploy_collab.yml`: +# https://github.com/zed-industries/zed/blob/main/.github/workflows/deploy_collab.yml LLM_DATABASE_MAX_CONNECTIONS=25 From e2e147ab0e7dbf6775f726230fef302b032fedd1 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Thu, 7 Aug 2025 12:52:54 -0500 Subject: [PATCH 159/693] Add OS specific settings (#35756) Release Notes: - Settings can now be configured per operating system with the new top-level fields: `"macos"`/`"windows"`/`"linux"`. These will override user level settings, but are lower precedence than _release channel_ settings. --- crates/auto_update/src/auto_update.rs | 13 +++++++---- crates/go_to_line/src/cursor_position.rs | 12 ++++++---- crates/settings/src/settings_store.rs | 29 ++++++++++++++++++++++-- crates/theme/src/settings.rs | 1 + 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index d62a9cdbe3..074aaa6fea 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -134,10 +134,15 @@ impl Settings for AutoUpdateSetting { type FileContent = Option; fn load(sources: SettingsSources, _: &mut App) -> Result { - let auto_update = [sources.server, sources.release_channel, sources.user] - .into_iter() - .find_map(|value| value.copied().flatten()) - .unwrap_or(sources.default.ok_or_else(Self::missing_default)?); + let auto_update = [ + sources.server, + sources.release_channel, + sources.operating_system, + sources.user, + ] + .into_iter() + .find_map(|value| value.copied().flatten()) + .unwrap_or(sources.default.ok_or_else(Self::missing_default)?); Ok(Self(auto_update.0)) } diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index 322a791b13..29064eb29c 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -308,10 +308,14 @@ impl Settings for LineIndicatorFormat { type FileContent = Option; fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { - let format = [sources.release_channel, sources.user] - .into_iter() - .find_map(|value| value.copied().flatten()) - .unwrap_or(sources.default.ok_or_else(Self::missing_default)?); + let format = [ + sources.release_channel, + sources.operating_system, + sources.user, + ] + .into_iter() + .find_map(|value| value.copied().flatten()) + .unwrap_or(sources.default.ok_or_else(Self::missing_default)?); Ok(format.0) } diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index bc42d2c886..fbc10f5860 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -16,6 +16,7 @@ use serde_json::{Value, json}; use smallvec::SmallVec; use std::{ any::{Any, TypeId, type_name}, + env, fmt::Debug, ops::Range, path::{Path, PathBuf}, @@ -126,6 +127,8 @@ pub struct SettingsSources<'a, T> { pub user: Option<&'a T>, /// The user settings for the current release channel. pub release_channel: Option<&'a T>, + /// The user settings for the current operating system. + pub operating_system: Option<&'a T>, /// The settings associated with an enabled settings profile pub profile: Option<&'a T>, /// The server's settings. @@ -147,6 +150,7 @@ impl<'a, T: Serialize> SettingsSources<'a, T> { .chain(self.extensions) .chain(self.user) .chain(self.release_channel) + .chain(self.operating_system) .chain(self.profile) .chain(self.server) .chain(self.project.iter().copied()) @@ -336,6 +340,11 @@ impl SettingsStore { .log_err(); } + let mut os_settings_value = None; + if let Some(os_settings) = &self.raw_user_settings.get(env::consts::OS) { + os_settings_value = setting_value.deserialize_setting(os_settings).log_err(); + } + let mut profile_value = None; if let Some(active_profile) = cx.try_global::() { if let Some(profiles) = self.raw_user_settings.get("profiles") { @@ -366,6 +375,7 @@ impl SettingsStore { extensions: extension_value.as_ref(), user: user_value.as_ref(), release_channel: release_channel_value.as_ref(), + operating_system: os_settings_value.as_ref(), profile: profile_value.as_ref(), server: server_value.as_ref(), project: &[], @@ -1092,7 +1102,7 @@ impl SettingsStore { "$schema": meta_schema, "title": "Zed Settings", "unevaluatedProperties": false, - // ZedSettings + settings overrides for each release stage / profiles + // ZedSettings + settings overrides for each release stage / OS / profiles "allOf": [ zed_settings_ref, { @@ -1101,6 +1111,9 @@ impl SettingsStore { "nightly": zed_settings_override_ref, "stable": zed_settings_override_ref, "preview": zed_settings_override_ref, + "linux": zed_settings_override_ref, + "macos": zed_settings_override_ref, + "windows": zed_settings_override_ref, "profiles": { "type": "object", "description": "Configures any number of settings profiles.", @@ -1164,6 +1177,13 @@ impl SettingsStore { } } + let mut os_settings = None; + if let Some(settings) = &self.raw_user_settings.get(env::consts::OS) { + if let Some(settings) = setting_value.deserialize_setting(settings).log_err() { + os_settings = Some(settings); + } + } + let mut profile_settings = None; if let Some(active_profile) = cx.try_global::() { if let Some(profiles) = self.raw_user_settings.get("profiles") { @@ -1183,7 +1203,8 @@ impl SettingsStore { global: global_settings.as_ref(), extensions: extension_settings.as_ref(), user: user_settings.as_ref(), - release_channel: release_channel_settings.as_ref(), + release_channel: os_settings.as_ref(), + operating_system: os_settings.as_ref(), profile: profile_settings.as_ref(), server: server_settings.as_ref(), project: &[], @@ -1237,6 +1258,7 @@ impl SettingsStore { extensions: extension_settings.as_ref(), user: user_settings.as_ref(), release_channel: release_channel_settings.as_ref(), + operating_system: os_settings.as_ref(), profile: profile_settings.as_ref(), server: server_settings.as_ref(), project: &project_settings_stack.iter().collect::>(), @@ -1363,6 +1385,9 @@ impl AnySettingValue for SettingValue { release_channel: values .release_channel .map(|value| value.0.downcast_ref::().unwrap()), + operating_system: values + .operating_system + .map(|value| value.0.downcast_ref::().unwrap()), profile: values .profile .map(|value| value.0.downcast_ref::().unwrap()), diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 20c837f287..6d19494f40 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -867,6 +867,7 @@ impl settings::Settings for ThemeSettings { .user .into_iter() .chain(sources.release_channel) + .chain(sources.operating_system) .chain(sources.profile) .chain(sources.server) { From 53b69d29c5f46fc99556fd8da147386286953b3f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 7 Aug 2025 20:58:33 +0300 Subject: [PATCH 160/693] Actually update remote collab capabilities (#35809) Follow-up of https://github.com/zed-industries/zed/pull/35682 Release Notes: - N/A --- crates/collab/src/db/queries/projects.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 31635575a8..82f74d910b 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -699,7 +699,10 @@ impl Database { language_server::Column::ProjectId, language_server::Column::Id, ]) - .update_column(language_server::Column::Name) + .update_columns([ + language_server::Column::Name, + language_server::Column::Capabilities, + ]) .to_owned(), ) .exec(&*tx) From e8db429d240bca5b12e027dd505212d0e40ad56f Mon Sep 17 00:00:00 2001 From: mcwindy <40684304+mcwindy@users.noreply.github.com> Date: Fri, 8 Aug 2025 02:34:12 +0800 Subject: [PATCH 161/693] project_panel: Add file comparison function, supports selecting files for comparison (#35255) Closes https://github.com/zed-industries/zed/discussions/35010 Closes https://github.com/zed-industries/zed/issues/17100 Closes https://github.com/zed-industries/zed/issues/4523 Release Notes: - Added file comparison function in project panel --------- Co-authored-by: Kirill Bulatov --- Cargo.lock | 1 + assets/keymaps/default-linux.json | 1 + assets/keymaps/default-macos.json | 1 + assets/keymaps/vim.json | 1 + crates/project_panel/Cargo.toml | 1 + crates/project_panel/src/project_panel.rs | 139 +++++++++---- .../project_panel/src/project_panel_tests.rs | 194 +++++++++++++++++- crates/workspace/src/pane.rs | 2 +- 8 files changed, 295 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b4bc8752d..fe0c7a1b23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12636,6 +12636,7 @@ dependencies = [ "editor", "file_icons", "git", + "git_ui", "gpui", "indexmap", "language", diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 2a4c095124..567580a9c6 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -848,6 +848,7 @@ "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }], "alt-ctrl-r": "project_panel::RevealInFileManager", "ctrl-shift-enter": "project_panel::OpenWithSystem", + "alt-d": "project_panel::CompareMarkedFiles", "shift-find": "project_panel::NewSearchInDirectory", "ctrl-alt-shift-f": "project_panel::NewSearchInDirectory", "shift-down": "menu::SelectNext", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 1a6cda4b64..1c2ad3a006 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -907,6 +907,7 @@ "cmd-delete": ["project_panel::Delete", { "skip_prompt": false }], "alt-cmd-r": "project_panel::RevealInFileManager", "ctrl-shift-enter": "project_panel::OpenWithSystem", + "alt-d": "project_panel::CompareMarkedFiles", "cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }], "cmd-alt-shift-f": "project_panel::NewSearchInDirectory", "shift-down": "menu::SelectNext", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 6458ac1510..57edb1e4c1 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -813,6 +813,7 @@ "p": "project_panel::Open", "x": "project_panel::RevealInFileManager", "s": "project_panel::OpenWithSystem", + "z d": "project_panel::CompareMarkedFiles", "] c": "project_panel::SelectNextGitEntry", "[ c": "project_panel::SelectPrevGitEntry", "] d": "project_panel::SelectNextDiagnostic", diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index ce5fec0b13..b9d43d9873 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -19,6 +19,7 @@ command_palette_hooks.workspace = true db.workspace = true editor.workspace = true file_icons.workspace = true +git_ui.workspace = true indexmap.workspace = true git.workspace = true gpui.workspace = true diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 048b9e73d0..45581a97c4 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -16,6 +16,7 @@ use editor::{ }; use file_icons::FileIcons; use git::status::GitSummary; +use git_ui::file_diff_view::FileDiffView; use gpui::{ Action, AnyElement, App, ArcCow, AsyncWindowContext, Bounds, ClipboardItem, Context, CursorStyle, DismissEvent, Div, DragMoveEvent, Entity, EventEmitter, ExternalPaths, @@ -93,7 +94,7 @@ pub struct ProjectPanel { unfolded_dir_ids: HashSet, // Currently selected leaf entry (see auto-folding for a definition of that) in a file tree selection: Option, - marked_entries: BTreeSet, + marked_entries: Vec, context_menu: Option<(Entity, Point, Subscription)>, edit_state: Option, filename_editor: Entity, @@ -280,6 +281,8 @@ actions!( SelectNextDirectory, /// Selects the previous directory. SelectPrevDirectory, + /// Opens a diff view to compare two marked files. + CompareMarkedFiles, ] ); @@ -376,7 +379,7 @@ struct DraggedProjectEntryView { selection: SelectedEntry, details: EntryDetails, click_offset: Point, - selections: Arc>, + selections: Arc<[SelectedEntry]>, } struct ItemColors { @@ -442,7 +445,15 @@ impl ProjectPanel { } } project::Event::ActiveEntryChanged(None) => { - this.marked_entries.clear(); + let is_active_item_file_diff_view = this + .workspace + .upgrade() + .and_then(|ws| ws.read(cx).active_item(cx)) + .map(|item| item.act_as_type(TypeId::of::(), cx).is_some()) + .unwrap_or(false); + if !is_active_item_file_diff_view { + this.marked_entries.clear(); + } } project::Event::RevealInProjectPanel(entry_id) => { if let Some(()) = this @@ -676,7 +687,7 @@ impl ProjectPanel { project_panel.update(cx, |project_panel, _| { let entry = SelectedEntry { worktree_id, entry_id }; project_panel.marked_entries.clear(); - project_panel.marked_entries.insert(entry); + project_panel.marked_entries.push(entry); project_panel.selection = Some(entry); }); if !focus_opened_item { @@ -887,6 +898,7 @@ impl ProjectPanel { let should_hide_rename = is_root && (cfg!(target_os = "windows") || (settings.hide_root && visible_worktrees_count == 1)); + let should_show_compare = !is_dir && self.file_abs_paths_to_diff(cx).is_some(); let context_menu = ContextMenu::build(window, cx, |menu, _, _| { menu.context(self.focus_handle.clone()).map(|menu| { @@ -918,6 +930,10 @@ impl ProjectPanel { .when(is_foldable, |menu| { menu.action("Fold Directory", Box::new(FoldDirectory)) }) + .when(should_show_compare, |menu| { + menu.separator() + .action("Compare marked files", Box::new(CompareMarkedFiles)) + }) .separator() .action("Cut", Box::new(Cut)) .action("Copy", Box::new(Copy)) @@ -1262,7 +1278,7 @@ impl ProjectPanel { }; self.selection = Some(selection); if window.modifiers().shift { - self.marked_entries.insert(selection); + self.marked_entries.push(selection); } self.autoscroll(cx); cx.notify(); @@ -2007,7 +2023,7 @@ impl ProjectPanel { }; self.selection = Some(selection); if window.modifiers().shift { - self.marked_entries.insert(selection); + self.marked_entries.push(selection); } self.autoscroll(cx); @@ -2244,7 +2260,7 @@ impl ProjectPanel { }; self.selection = Some(selection); if window.modifiers().shift { - self.marked_entries.insert(selection); + self.marked_entries.push(selection); } self.autoscroll(cx); cx.notify(); @@ -2572,6 +2588,43 @@ impl ProjectPanel { } } + fn file_abs_paths_to_diff(&self, cx: &Context) -> Option<(PathBuf, PathBuf)> { + let mut selections_abs_path = self + .marked_entries + .iter() + .filter_map(|entry| { + let project = self.project.read(cx); + let worktree = project.worktree_for_id(entry.worktree_id, cx)?; + let entry = worktree.read(cx).entry_for_id(entry.entry_id)?; + if !entry.is_file() { + return None; + } + worktree.read(cx).absolutize(&entry.path).ok() + }) + .rev(); + + let last_path = selections_abs_path.next()?; + let previous_to_last = selections_abs_path.next()?; + Some((previous_to_last, last_path)) + } + + fn compare_marked_files( + &mut self, + _: &CompareMarkedFiles, + window: &mut Window, + cx: &mut Context, + ) { + let selected_files = self.file_abs_paths_to_diff(cx); + if let Some((file_path1, file_path2)) = selected_files { + self.workspace + .update(cx, |workspace, cx| { + FileDiffView::open(file_path1, file_path2, workspace, window, cx) + .detach_and_log_err(cx); + }) + .ok(); + } + } + fn open_system(&mut self, _: &OpenWithSystem, _: &mut Window, cx: &mut Context) { if let Some((worktree, entry)) = self.selected_entry(cx) { let abs_path = worktree.abs_path().join(&entry.path); @@ -3914,11 +3967,9 @@ impl ProjectPanel { let depth = details.depth; let worktree_id = details.worktree_id; - let selections = Arc::new(self.marked_entries.clone()); - let dragged_selection = DraggedSelection { active_selection: selection, - marked_selections: selections, + marked_selections: Arc::from(self.marked_entries.clone()), }; let bg_color = if is_marked { @@ -4089,7 +4140,7 @@ impl ProjectPanel { }); if drag_state.items().count() == 1 { this.marked_entries.clear(); - this.marked_entries.insert(drag_state.active_selection); + this.marked_entries.push(drag_state.active_selection); } this.hover_expand_task.take(); @@ -4156,65 +4207,69 @@ impl ProjectPanel { }), ) .on_click( - cx.listener(move |this, event: &gpui::ClickEvent, window, cx| { + cx.listener(move |project_panel, event: &gpui::ClickEvent, window, cx| { if event.is_right_click() || event.first_focus() || show_editor { return; } if event.standard_click() { - this.mouse_down = false; + project_panel.mouse_down = false; } cx.stop_propagation(); - if let Some(selection) = this.selection.filter(|_| event.modifiers().shift) { - let current_selection = this.index_for_selection(selection); + if let Some(selection) = project_panel.selection.filter(|_| event.modifiers().shift) { + let current_selection = project_panel.index_for_selection(selection); let clicked_entry = SelectedEntry { entry_id, worktree_id, }; - let target_selection = this.index_for_selection(clicked_entry); + let target_selection = project_panel.index_for_selection(clicked_entry); if let Some(((_, _, source_index), (_, _, target_index))) = current_selection.zip(target_selection) { let range_start = source_index.min(target_index); let range_end = source_index.max(target_index) + 1; - let mut new_selections = BTreeSet::new(); - this.for_each_visible_entry( + let mut new_selections = Vec::new(); + project_panel.for_each_visible_entry( range_start..range_end, window, cx, |entry_id, details, _, _| { - new_selections.insert(SelectedEntry { + new_selections.push(SelectedEntry { entry_id, worktree_id: details.worktree_id, }); }, ); - this.marked_entries = this - .marked_entries - .union(&new_selections) - .cloned() - .collect(); + for selection in &new_selections { + if !project_panel.marked_entries.contains(selection) { + project_panel.marked_entries.push(*selection); + } + } - this.selection = Some(clicked_entry); - this.marked_entries.insert(clicked_entry); + project_panel.selection = Some(clicked_entry); + if !project_panel.marked_entries.contains(&clicked_entry) { + project_panel.marked_entries.push(clicked_entry); + } } } else if event.modifiers().secondary() { if event.click_count() > 1 { - this.split_entry(entry_id, cx); + project_panel.split_entry(entry_id, cx); } else { - this.selection = Some(selection); - if !this.marked_entries.insert(selection) { - this.marked_entries.remove(&selection); + project_panel.selection = Some(selection); + if let Some(position) = project_panel.marked_entries.iter().position(|e| *e == selection) { + project_panel.marked_entries.remove(position); + } else { + project_panel.marked_entries.push(selection); } } } else if kind.is_dir() { - this.marked_entries.clear(); + project_panel.marked_entries.clear(); if is_sticky { - if let Some((_, _, index)) = this.index_for_entry(entry_id, worktree_id) { - this.scroll_handle.scroll_to_item_with_offset(index, ScrollStrategy::Top, sticky_index.unwrap_or(0)); + if let Some((_, _, index)) = project_panel.index_for_entry(entry_id, worktree_id) { + project_panel.scroll_handle.scroll_to_item_with_offset(index, ScrollStrategy::Top, sticky_index.unwrap_or(0)); cx.notify(); // move down by 1px so that clicked item // don't count as sticky anymore @@ -4230,16 +4285,16 @@ impl ProjectPanel { } } if event.modifiers().alt { - this.toggle_expand_all(entry_id, window, cx); + project_panel.toggle_expand_all(entry_id, window, cx); } else { - this.toggle_expanded(entry_id, window, cx); + project_panel.toggle_expanded(entry_id, window, cx); } } else { let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled; let click_count = event.click_count(); let focus_opened_item = !preview_tabs_enabled || click_count > 1; let allow_preview = preview_tabs_enabled && click_count == 1; - this.open_entry(entry_id, focus_opened_item, allow_preview, cx); + project_panel.open_entry(entry_id, focus_opened_item, allow_preview, cx); } }), ) @@ -4810,12 +4865,21 @@ impl ProjectPanel { { anyhow::bail!("can't reveal an ignored entry in the project panel"); } + let is_active_item_file_diff_view = self + .workspace + .upgrade() + .and_then(|ws| ws.read(cx).active_item(cx)) + .map(|item| item.act_as_type(TypeId::of::(), cx).is_some()) + .unwrap_or(false); + if is_active_item_file_diff_view { + return Ok(()); + } let worktree_id = worktree.id(); self.expand_entry(worktree_id, entry_id, cx); self.update_visible_entries(Some((worktree_id, entry_id)), cx); self.marked_entries.clear(); - self.marked_entries.insert(SelectedEntry { + self.marked_entries.push(SelectedEntry { worktree_id, entry_id, }); @@ -5170,6 +5234,7 @@ impl Render for ProjectPanel { .on_action(cx.listener(Self::unfold_directory)) .on_action(cx.listener(Self::fold_directory)) .on_action(cx.listener(Self::remove_from_project)) + .on_action(cx.listener(Self::compare_marked_files)) .when(!project.is_read_only(cx), |el| { el.on_action(cx.listener(Self::new_file)) .on_action(cx.listener(Self::new_directory)) diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index 7699256bc9..6c62c8db93 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -8,7 +8,7 @@ use settings::SettingsStore; use std::path::{Path, PathBuf}; use util::path; use workspace::{ - AppState, Pane, + AppState, ItemHandle, Pane, item::{Item, ProjectItem}, register_project_item, }; @@ -3068,7 +3068,7 @@ async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) { panel.update(cx, |this, cx| { let drag = DraggedSelection { active_selection: this.selection.unwrap(), - marked_selections: Arc::new(this.marked_entries.clone()), + marked_selections: this.marked_entries.clone().into(), }; let target_entry = this .project @@ -5562,10 +5562,10 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext) worktree_id, entry_id: child_file.id, }, - marked_selections: Arc::new(BTreeSet::from([SelectedEntry { + marked_selections: Arc::new([SelectedEntry { worktree_id, entry_id: child_file.id, - }])), + }]), }; let result = panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx); @@ -5604,7 +5604,7 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext) worktree_id, entry_id: child_file.id, }, - marked_selections: Arc::new(BTreeSet::from([ + marked_selections: Arc::new([ SelectedEntry { worktree_id, entry_id: child_file.id, @@ -5613,7 +5613,7 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext) worktree_id, entry_id: sibling_file.id, }, - ])), + ]), }; let result = panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx); @@ -5821,6 +5821,186 @@ async fn test_hide_root(cx: &mut gpui::TestAppContext) { } } +#[gpui::test] +async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root", + json!({ + "file1.txt": "content of file1", + "file2.txt": "content of file2", + "dir1": { + "file3.txt": "content of file3" + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; + let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + let file1_path = path!("root/file1.txt"); + let file2_path = path!("root/file2.txt"); + select_path_with_mark(&panel, file1_path, cx); + select_path_with_mark(&panel, file2_path, cx); + + panel.update_in(cx, |panel, window, cx| { + panel.compare_marked_files(&CompareMarkedFiles, window, cx); + }); + cx.executor().run_until_parked(); + + workspace + .update(cx, |workspace, _, cx| { + let active_items = workspace + .panes() + .iter() + .filter_map(|pane| pane.read(cx).active_item()) + .collect::>(); + assert_eq!(active_items.len(), 1); + let diff_view = active_items + .into_iter() + .next() + .unwrap() + .downcast::() + .expect("Open item should be an FileDiffView"); + assert_eq!(diff_view.tab_content_text(0, cx), "file1.txt ↔ file2.txt"); + assert_eq!( + diff_view.tab_tooltip_text(cx).unwrap(), + format!("{} ↔ {}", file1_path, file2_path) + ); + }) + .unwrap(); + + let file1_entry_id = find_project_entry(&panel, file1_path, cx).unwrap(); + let file2_entry_id = find_project_entry(&panel, file2_path, cx).unwrap(); + let worktree_id = panel.update(cx, |panel, cx| { + panel + .project + .read(cx) + .worktrees(cx) + .next() + .unwrap() + .read(cx) + .id() + }); + + let expected_entries = [ + SelectedEntry { + worktree_id, + entry_id: file1_entry_id, + }, + SelectedEntry { + worktree_id, + entry_id: file2_entry_id, + }, + ]; + panel.update(cx, |panel, _cx| { + assert_eq!( + &panel.marked_entries, &expected_entries, + "Should keep marked entries after comparison" + ); + }); + + panel.update(cx, |panel, cx| { + panel.project.update(cx, |_, cx| { + cx.emit(project::Event::RevealInProjectPanel(file2_entry_id)) + }) + }); + + panel.update(cx, |panel, _cx| { + assert_eq!( + &panel.marked_entries, &expected_entries, + "Marked entries should persist after focusing back on the project panel" + ); + }); +} + +#[gpui::test] +async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root", + json!({ + "file1.txt": "content of file1", + "file2.txt": "content of file2", + "dir1": {}, + "dir2": { + "file3.txt": "content of file3" + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; + let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + // Test 1: When only one file is selected, there should be no compare option + select_path(&panel, "root/file1.txt", cx); + + let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx)); + assert_eq!( + selected_files, None, + "Should not have compare option when only one file is selected" + ); + + // Test 2: When multiple files are selected, there should be a compare option + select_path_with_mark(&panel, "root/file1.txt", cx); + select_path_with_mark(&panel, "root/file2.txt", cx); + + let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx)); + assert!( + selected_files.is_some(), + "Should have files selected for comparison" + ); + if let Some((file1, file2)) = selected_files { + assert!( + file1.to_string_lossy().ends_with("file1.txt") + && file2.to_string_lossy().ends_with("file2.txt"), + "Should have file1.txt and file2.txt as the selected files when multi-selecting" + ); + } + + // Test 3: Selecting a directory shouldn't count as a comparable file + select_path_with_mark(&panel, "root/dir1", cx); + + let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx)); + assert!( + selected_files.is_some(), + "Directory selection should not affect comparable files" + ); + if let Some((file1, file2)) = selected_files { + assert!( + file1.to_string_lossy().ends_with("file1.txt") + && file2.to_string_lossy().ends_with("file2.txt"), + "Selecting a directory should not affect the number of comparable files" + ); + } + + // Test 4: Selecting one more file + select_path_with_mark(&panel, "root/dir2/file3.txt", cx); + + let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx)); + assert!( + selected_files.is_some(), + "Directory selection should not affect comparable files" + ); + if let Some((file1, file2)) = selected_files { + assert!( + file1.to_string_lossy().ends_with("file2.txt") + && file2.to_string_lossy().ends_with("file3.txt"), + "Selecting a directory should not affect the number of comparable files" + ); + } +} + fn select_path(panel: &Entity, path: impl AsRef, cx: &mut VisualTestContext) { let path = path.as_ref(); panel.update(cx, |panel, cx| { @@ -5855,7 +6035,7 @@ fn select_path_with_mark( entry_id, }; if !panel.marked_entries.contains(&entry) { - panel.marked_entries.insert(entry); + panel.marked_entries.push(entry); } panel.selection = Some(entry); return; diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index fff15d2b52..a9e7304e47 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -62,7 +62,7 @@ pub struct SelectedEntry { #[derive(Debug)] pub struct DraggedSelection { pub active_selection: SelectedEntry, - pub marked_selections: Arc>, + pub marked_selections: Arc<[SelectedEntry]>, } impl DraggedSelection { From 9ade399756c6224f6858ed5ea7a1ca212cbd5d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Bergstr=C3=B6m?= Date: Thu, 7 Aug 2025 21:13:51 +0200 Subject: [PATCH 162/693] workspace: Don't update platform window title if title has not changed (#34753) Closes #34749 #34715 Release Notes: - Fixed window title X event spam --- crates/workspace/src/workspace.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6572574ce1..aab8a36f45 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1086,6 +1086,7 @@ pub struct Workspace { follower_states: HashMap, last_leaders_by_pane: HashMap, CollaboratorId>, window_edited: bool, + last_window_title: Option, dirty_items: HashMap, active_call: Option<(Entity, Vec)>, leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, @@ -1418,6 +1419,7 @@ impl Workspace { last_leaders_by_pane: Default::default(), dispatching_keystrokes: Default::default(), window_edited: false, + last_window_title: None, dirty_items: Default::default(), active_call, database_id: workspace_id, @@ -4403,7 +4405,13 @@ impl Workspace { title.push_str(" ↗"); } + if let Some(last_title) = self.last_window_title.as_ref() { + if &title == last_title { + return; + } + } window.set_window_title(&title); + self.last_window_title = Some(title); } fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) { From 7679db99acae10c45e9ce723ca50d67008bfc110 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Thu, 7 Aug 2025 16:59:11 -0400 Subject: [PATCH 163/693] ci: Switch from BuildJet to GitHub runners (#35826) In response to an ongoing BuildJet outage, consider migrating CI to GitHub hosted runners. Also includes revert of (causing flaky tests): - https://github.com/zed-industries/zed/pull/35741 Downsides: - Cost (2x) - Force migration to Ubuntu 22.04 from 20.04 will bump our glibc minimum from 2.31 to 2.35. Which would break RHEL 9.x (glibc 2.34), Ubuntu 20.04 (EOL) and derivatives. Release Notes: - N/A --- .github/actionlint.yml | 5 + .github/actions/build_docs/action.yml | 2 +- .github/workflows/bump_patch_version.yml | 2 +- .github/workflows/ci.yml | 19 +-- .github/workflows/deploy_cloudflare.yml | 2 +- .github/workflows/deploy_collab.yml | 4 +- .github/workflows/eval.yml | 4 +- .github/workflows/nix.yml | 2 +- .github/workflows/randomized_tests.yml | 2 +- .github/workflows/release_nightly.yml | 5 +- .github/workflows/unit_evals.yml | 4 +- crates/fs/src/fs_watcher.rs | 195 +++++++---------------- 12 files changed, 84 insertions(+), 162 deletions(-) diff --git a/.github/actionlint.yml b/.github/actionlint.yml index 6bfbc27705..06b48b9b54 100644 --- a/.github/actionlint.yml +++ b/.github/actionlint.yml @@ -5,6 +5,11 @@ self-hosted-runner: # GitHub-hosted Runners - github-8vcpu-ubuntu-2404 - github-16vcpu-ubuntu-2404 + - github-32vcpu-ubuntu-2404 + - github-8vcpu-ubuntu-2204 + - github-16vcpu-ubuntu-2204 + - github-32vcpu-ubuntu-2204 + - github-16vcpu-ubuntu-2204-arm - windows-2025-16 - windows-2025-32 - windows-2025-64 diff --git a/.github/actions/build_docs/action.yml b/.github/actions/build_docs/action.yml index a7effad247..d2e62d5b22 100644 --- a/.github/actions/build_docs/action.yml +++ b/.github/actions/build_docs/action.yml @@ -13,7 +13,7 @@ runs: uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - cache-provider: "buildjet" + # cache-provider: "buildjet" - name: Install Linux dependencies shell: bash -euxo pipefail {0} diff --git a/.github/workflows/bump_patch_version.yml b/.github/workflows/bump_patch_version.yml index 8a48ff96f1..bc44066ea6 100644 --- a/.github/workflows/bump_patch_version.yml +++ b/.github/workflows/bump_patch_version.yml @@ -16,7 +16,7 @@ jobs: bump_patch_version: if: github.repository_owner == 'zed-industries' runs-on: - - buildjet-16vcpu-ubuntu-2204 + - github-16vcpu-ubuntu-2204 steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43d305faae..8f4f4d2b11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -137,7 +137,7 @@ jobs: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_tests == 'true' runs-on: - - buildjet-8vcpu-ubuntu-2204 + - github-8vcpu-ubuntu-2204 steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -168,7 +168,7 @@ jobs: needs: [job_spec] if: github.repository_owner == 'zed-industries' runs-on: - - buildjet-8vcpu-ubuntu-2204 + - github-8vcpu-ubuntu-2204 steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -221,7 +221,7 @@ jobs: github.repository_owner == 'zed-industries' && (needs.job_spec.outputs.run_tests == 'true' || needs.job_spec.outputs.run_docs == 'true') runs-on: - - buildjet-8vcpu-ubuntu-2204 + - github-8vcpu-ubuntu-2204 steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -328,7 +328,7 @@ jobs: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_tests == 'true' runs-on: - - buildjet-16vcpu-ubuntu-2204 + - github-16vcpu-ubuntu-2204 steps: - name: Add Rust to the PATH run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" @@ -342,7 +342,7 @@ jobs: uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - cache-provider: "buildjet" + # cache-provider: "buildjet" - name: Install Linux dependencies run: ./script/linux @@ -380,7 +380,7 @@ jobs: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_tests == 'true' runs-on: - - buildjet-8vcpu-ubuntu-2204 + - github-8vcpu-ubuntu-2204 steps: - name: Add Rust to the PATH run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" @@ -394,7 +394,7 @@ jobs: uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - cache-provider: "buildjet" + # cache-provider: "buildjet" - name: Install Clang & Mold run: ./script/remote-server && ./script/install-mold 2.34.0 @@ -597,7 +597,8 @@ jobs: timeout-minutes: 60 name: Linux x86_x64 release bundle runs-on: - - buildjet-16vcpu-ubuntu-2004 # ubuntu 20.04 for minimal glibc + - github-16vcpu-ubuntu-2204 + # - buildjet-16vcpu-ubuntu-2004 # ubuntu 20.04 for minimal glibc if: | startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') @@ -650,7 +651,7 @@ jobs: timeout-minutes: 60 name: Linux arm64 release bundle runs-on: - - buildjet-32vcpu-ubuntu-2204-arm + - github-16vcpu-ubuntu-2204-arm if: | startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') diff --git a/.github/workflows/deploy_cloudflare.yml b/.github/workflows/deploy_cloudflare.yml index fe443d493e..3a294fdc17 100644 --- a/.github/workflows/deploy_cloudflare.yml +++ b/.github/workflows/deploy_cloudflare.yml @@ -9,7 +9,7 @@ jobs: deploy-docs: name: Deploy Docs if: github.repository_owner == 'zed-industries' - runs-on: buildjet-16vcpu-ubuntu-2204 + runs-on: github-16vcpu-ubuntu-2204 steps: - name: Checkout repo diff --git a/.github/workflows/deploy_collab.yml b/.github/workflows/deploy_collab.yml index ea264b7a66..d1a68a6280 100644 --- a/.github/workflows/deploy_collab.yml +++ b/.github/workflows/deploy_collab.yml @@ -61,7 +61,7 @@ jobs: - style - tests runs-on: - - buildjet-16vcpu-ubuntu-2204 + - github-16vcpu-ubuntu-2204 steps: - name: Install doctl uses: digitalocean/action-doctl@v2 @@ -94,7 +94,7 @@ jobs: needs: - publish runs-on: - - buildjet-16vcpu-ubuntu-2204 + - github-16vcpu-ubuntu-2204 steps: - name: Checkout repo diff --git a/.github/workflows/eval.yml b/.github/workflows/eval.yml index 2ad302a602..196e00519b 100644 --- a/.github/workflows/eval.yml +++ b/.github/workflows/eval.yml @@ -32,7 +32,7 @@ jobs: github.repository_owner == 'zed-industries' && (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-eval')) runs-on: - - buildjet-16vcpu-ubuntu-2204 + - github-16vcpu-ubuntu-2204 steps: - name: Add Rust to the PATH run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" @@ -46,7 +46,7 @@ jobs: uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - cache-provider: "buildjet" + # cache-provider: "buildjet" - name: Install Linux dependencies run: ./script/linux diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 6c3a97c163..913d6cfe9f 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -20,7 +20,7 @@ jobs: matrix: system: - os: x86 Linux - runner: buildjet-16vcpu-ubuntu-2204 + runner: github-16vcpu-ubuntu-2204 install_nix: true - os: arm Mac runner: [macOS, ARM64, test] diff --git a/.github/workflows/randomized_tests.yml b/.github/workflows/randomized_tests.yml index db4d44318e..3a7b476ba0 100644 --- a/.github/workflows/randomized_tests.yml +++ b/.github/workflows/randomized_tests.yml @@ -20,7 +20,7 @@ jobs: name: Run randomized tests if: github.repository_owner == 'zed-industries' runs-on: - - buildjet-16vcpu-ubuntu-2204 + - github-16vcpu-ubuntu-2204 steps: - name: Install Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index c847149984..c5be72fca2 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -128,7 +128,8 @@ jobs: name: Create a Linux *.tar.gz bundle for x86 if: github.repository_owner == 'zed-industries' runs-on: - - buildjet-16vcpu-ubuntu-2004 + - github-16vcpu-ubuntu-2204 + # - buildjet-16vcpu-ubuntu-2004 needs: tests steps: - name: Checkout repo @@ -168,7 +169,7 @@ jobs: name: Create a Linux *.tar.gz bundle for ARM if: github.repository_owner == 'zed-industries' runs-on: - - buildjet-32vcpu-ubuntu-2204-arm + - github-16vcpu-ubuntu-2204-arm needs: tests steps: - name: Checkout repo diff --git a/.github/workflows/unit_evals.yml b/.github/workflows/unit_evals.yml index cb4e39d151..225fca558f 100644 --- a/.github/workflows/unit_evals.yml +++ b/.github/workflows/unit_evals.yml @@ -23,7 +23,7 @@ jobs: timeout-minutes: 60 name: Run unit evals runs-on: - - buildjet-16vcpu-ubuntu-2204 + - github-16vcpu-ubuntu-2204 steps: - name: Add Rust to the PATH run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" @@ -37,7 +37,7 @@ jobs: uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - cache-provider: "buildjet" + # cache-provider: "buildjet" - name: Install Linux dependencies run: ./script/linux diff --git a/crates/fs/src/fs_watcher.rs b/crates/fs/src/fs_watcher.rs index c9da4fa957..9fdf2ad0b1 100644 --- a/crates/fs/src/fs_watcher.rs +++ b/crates/fs/src/fs_watcher.rs @@ -1,9 +1,6 @@ use notify::EventKind; use parking_lot::Mutex; -use std::{ - collections::HashMap, - sync::{Arc, OnceLock}, -}; +use std::sync::{Arc, OnceLock}; use util::{ResultExt, paths::SanitizedPath}; use crate::{PathEvent, PathEventKind, Watcher}; @@ -11,7 +8,6 @@ use crate::{PathEvent, PathEventKind, Watcher}; pub struct FsWatcher { tx: smol::channel::Sender<()>, pending_path_events: Arc>>, - registrations: Mutex, WatcherRegistrationId>>, } impl FsWatcher { @@ -22,24 +18,10 @@ impl FsWatcher { Self { tx, pending_path_events, - registrations: Default::default(), } } } -impl Drop for FsWatcher { - fn drop(&mut self) { - let mut registrations = self.registrations.lock(); - let registrations = registrations.drain(); - - let _ = global(|g| { - for (_, registration) in registrations { - g.remove(registration); - } - }); - } -} - impl Watcher for FsWatcher { fn add(&self, path: &std::path::Path) -> anyhow::Result<()> { let root_path = SanitizedPath::from(path); @@ -47,136 +29,75 @@ impl Watcher for FsWatcher { let tx = self.tx.clone(); let pending_paths = self.pending_path_events.clone(); - let path: Arc = path.into(); + use notify::Watcher; - if self.registrations.lock().contains_key(&path) { - return Ok(()); - } - - let registration_id = global({ - let path = path.clone(); + global({ |g| { - g.add( - path, - notify::RecursiveMode::NonRecursive, - move |event: ¬ify::Event| { - let kind = match event.kind { - EventKind::Create(_) => Some(PathEventKind::Created), - EventKind::Modify(_) => Some(PathEventKind::Changed), - EventKind::Remove(_) => Some(PathEventKind::Removed), - _ => None, - }; - let mut path_events = event - .paths - .iter() - .filter_map(|event_path| { - let event_path = SanitizedPath::from(event_path); - event_path.starts_with(&root_path).then(|| PathEvent { - path: event_path.as_path().to_path_buf(), - kind, - }) + g.add(move |event: ¬ify::Event| { + let kind = match event.kind { + EventKind::Create(_) => Some(PathEventKind::Created), + EventKind::Modify(_) => Some(PathEventKind::Changed), + EventKind::Remove(_) => Some(PathEventKind::Removed), + _ => None, + }; + let mut path_events = event + .paths + .iter() + .filter_map(|event_path| { + let event_path = SanitizedPath::from(event_path); + event_path.starts_with(&root_path).then(|| PathEvent { + path: event_path.as_path().to_path_buf(), + kind, }) - .collect::>(); + }) + .collect::>(); - if !path_events.is_empty() { - path_events.sort(); - let mut pending_paths = pending_paths.lock(); - if pending_paths.is_empty() { - tx.try_send(()).ok(); - } - util::extend_sorted( - &mut *pending_paths, - path_events, - usize::MAX, - |a, b| a.path.cmp(&b.path), - ); + if !path_events.is_empty() { + path_events.sort(); + let mut pending_paths = pending_paths.lock(); + if pending_paths.is_empty() { + tx.try_send(()).ok(); } - }, - ) + util::extend_sorted( + &mut *pending_paths, + path_events, + usize::MAX, + |a, b| a.path.cmp(&b.path), + ); + } + }) } - })??; + })?; - self.registrations.lock().insert(path, registration_id); + global(|g| { + g.watcher + .lock() + .watch(path, notify::RecursiveMode::NonRecursive) + })??; Ok(()) } fn remove(&self, path: &std::path::Path) -> anyhow::Result<()> { - let Some(registration) = self.registrations.lock().remove(path) else { - return Ok(()); - }; - - global(|w| w.remove(registration)) + use notify::Watcher; + Ok(global(|w| w.watcher.lock().unwatch(path))??) } } -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub struct WatcherRegistrationId(u32); - -struct WatcherRegistrationState { - callback: Box, - path: Arc, -} - -struct WatcherState { - // two mutexes because calling watcher.add triggers an watcher.event, which needs watchers. - #[cfg(target_os = "linux")] - watcher: notify::INotifyWatcher, - #[cfg(target_os = "freebsd")] - watcher: notify::KqueueWatcher, - #[cfg(target_os = "windows")] - watcher: notify::ReadDirectoryChangesWatcher, - - watchers: HashMap, - path_registrations: HashMap, u32>, - last_registration: WatcherRegistrationId, -} - pub struct GlobalWatcher { - state: Mutex, + // two mutexes because calling watcher.add triggers an watcher.event, which needs watchers. + #[cfg(target_os = "linux")] + pub(super) watcher: Mutex, + #[cfg(target_os = "freebsd")] + pub(super) watcher: Mutex, + #[cfg(target_os = "windows")] + pub(super) watcher: Mutex, + pub(super) watchers: Mutex>>, } impl GlobalWatcher { - #[must_use] - fn add( - &self, - path: Arc, - mode: notify::RecursiveMode, - cb: impl Fn(¬ify::Event) + Send + Sync + 'static, - ) -> anyhow::Result { - use notify::Watcher; - let mut state = self.state.lock(); - - state.watcher.watch(&path, mode)?; - - let id = state.last_registration; - state.last_registration = WatcherRegistrationId(id.0 + 1); - - let registration_state = WatcherRegistrationState { - callback: Box::new(cb), - path: path.clone(), - }; - state.watchers.insert(id, registration_state); - *state.path_registrations.entry(path.clone()).or_insert(0) += 1; - - Ok(id) - } - - pub fn remove(&self, id: WatcherRegistrationId) { - use notify::Watcher; - let mut state = self.state.lock(); - let Some(registration_state) = state.watchers.remove(&id) else { - return; - }; - - let Some(count) = state.path_registrations.get_mut(®istration_state.path) else { - return; - }; - *count -= 1; - if *count == 0 { - state.watcher.unwatch(®istration_state.path).log_err(); - state.path_registrations.remove(®istration_state.path); - } + pub(super) fn add(&self, cb: impl Fn(¬ify::Event) + Send + Sync + 'static) { + self.watchers.lock().push(Box::new(cb)) } } @@ -193,10 +114,8 @@ fn handle_event(event: Result) { return; }; global::<()>(move |watcher| { - let state = watcher.state.lock(); - for registration in state.watchers.values() { - let callback = ®istration.callback; - callback(&event); + for f in watcher.watchers.lock().iter() { + f(&event) } }) .log_err(); @@ -205,12 +124,8 @@ fn handle_event(event: Result) { pub fn global(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result { let result = FS_WATCHER_INSTANCE.get_or_init(|| { notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher { - state: Mutex::new(WatcherState { - watcher: file_watcher, - watchers: Default::default(), - path_registrations: Default::default(), - last_registration: Default::default(), - }), + watcher: Mutex::new(file_watcher), + watchers: Default::default(), }) }); match result { From a1080a0411542aca95ac780c142578ec6f3bed60 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 7 Aug 2025 17:31:30 -0400 Subject: [PATCH 164/693] Update diff editor font size when agent_font_size setting changes (#35834) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 37 +++++++++++++++++++------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 06e47a11dc..5c6ae091cd 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -31,7 +31,7 @@ use language::{Buffer, Language}; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; use parking_lot::Mutex; use project::Project; -use settings::Settings as _; +use settings::{Settings as _, SettingsStore}; use text::{Anchor, BufferSnapshot}; use theme::ThemeSettings; use ui::{ @@ -80,6 +80,7 @@ pub struct AcpThreadView { editor_expanded: bool, message_history: Rc>>>, _cancel_task: Option>, + _subscriptions: [Subscription; 1], } enum ThreadState { @@ -178,6 +179,8 @@ impl AcpThreadView { let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0)); + let subscription = cx.observe_global_in::(window, Self::settings_changed); + Self { agent: agent.clone(), workspace: workspace.clone(), @@ -200,6 +203,7 @@ impl AcpThreadView { plan_expanded: false, editor_expanded: false, message_history, + _subscriptions: [subscription], _cancel_task: None, } } @@ -704,15 +708,7 @@ impl AcpThreadView { editor.set_show_code_actions(false, cx); editor.set_show_git_diff_gutter(false, cx); editor.set_expand_all_diff_hunks(cx); - editor.set_text_style_refinement(TextStyleRefinement { - font_size: Some( - TextSize::Small - .rems(cx) - .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx)) - .into(), - ), - ..Default::default() - }); + editor.set_text_style_refinement(diff_editor_text_style_refinement(cx)); editor }); let entity_id = multibuffer.entity_id(); @@ -2597,6 +2593,15 @@ impl AcpThreadView { .cursor_default() .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx))) } + + fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context) { + for diff_editor in self.diff_editors.values() { + diff_editor.update(cx, |diff_editor, cx| { + diff_editor.set_text_style_refinement(diff_editor_text_style_refinement(cx)); + cx.notify(); + }) + } + } } impl Focusable for AcpThreadView { @@ -2874,6 +2879,18 @@ fn plan_label_markdown_style( } } +fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement { + TextStyleRefinement { + font_size: Some( + TextSize::Small + .rems(cx) + .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx)) + .into(), + ), + ..Default::default() + } +} + #[cfg(test)] mod tests { use agent_client_protocol::SessionId; From 106d4cfce949aa53c725c78f766e3e076225f0c0 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 7 Aug 2025 17:44:53 -0400 Subject: [PATCH 165/693] client: Re-fetch the authenticated user when receiving a `UserUpdated` message from Cloud (#35807) This PR wires up handling for the new `UserUpdated` message coming from Cloud over the WebSocket connection. When we receive this message we will refresh the authenticated user. Release Notes: - N/A Co-authored-by: Richard --- crates/client/src/client.rs | 24 ++++++++++++++++++++---- crates/client/src/user.rs | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 0480ed1c3e..9d58692c0d 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -192,6 +192,8 @@ pub fn init(client: &Arc, cx: &mut App) { }); } +pub type MessageToClientHandler = Box; + struct GlobalClient(Arc); impl Global for GlobalClient {} @@ -205,6 +207,7 @@ pub struct Client { credentials_provider: ClientCredentialsProvider, state: RwLock, handler_set: parking_lot::Mutex, + message_to_client_handlers: parking_lot::Mutex>, #[allow(clippy::type_complexity)] #[cfg(any(test, feature = "test-support"))] @@ -554,6 +557,7 @@ impl Client { credentials_provider: ClientCredentialsProvider::new(cx), state: Default::default(), handler_set: Default::default(), + message_to_client_handlers: parking_lot::Mutex::new(Vec::new()), #[cfg(any(test, feature = "test-support"))] authenticate: Default::default(), @@ -1651,10 +1655,22 @@ impl Client { } } - fn handle_message_to_client(self: &Arc, message: MessageToClient, _cx: &AsyncApp) { - match message { - MessageToClient::UserUpdated => {} - } + pub fn add_message_to_client_handler( + self: &Arc, + handler: impl Fn(&MessageToClient, &App) + Send + Sync + 'static, + ) { + self.message_to_client_handlers + .lock() + .push(Box::new(handler)); + } + + fn handle_message_to_client(self: &Arc, message: MessageToClient, cx: &AsyncApp) { + cx.update(|cx| { + for handler in self.message_to_client_handlers.lock().iter() { + handler(&message, cx); + } + }) + .ok(); } pub fn telemetry(&self) -> &Arc { diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 3c125a0882..9f76dd7ad0 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,6 +1,7 @@ use super::{Client, Status, TypedEnvelope, proto}; use anyhow::{Context as _, Result, anyhow}; use chrono::{DateTime, Utc}; +use cloud_api_client::websocket_protocol::MessageToClient; use cloud_api_client::{GetAuthenticatedUserResponse, PlanInfo}; use cloud_llm_client::{ EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME, EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME, @@ -181,6 +182,12 @@ impl UserStore { client.add_message_handler(cx.weak_entity(), Self::handle_update_invite_info), client.add_message_handler(cx.weak_entity(), Self::handle_show_contacts), ]; + + client.add_message_to_client_handler({ + let this = cx.weak_entity(); + move |message, cx| Self::handle_message_to_client(this.clone(), message, cx) + }); + Self { users: Default::default(), by_github_login: Default::default(), @@ -813,6 +820,32 @@ impl UserStore { cx.emit(Event::PrivateUserInfoUpdated); } + fn handle_message_to_client(this: WeakEntity, message: &MessageToClient, cx: &App) { + cx.spawn(async move |cx| { + match message { + MessageToClient::UserUpdated => { + let cloud_client = cx + .update(|cx| { + this.read_with(cx, |this, _cx| { + this.client.upgrade().map(|client| client.cloud_client()) + }) + })?? + .ok_or(anyhow::anyhow!("Failed to get Cloud client"))?; + + let response = cloud_client.get_authenticated_user().await?; + cx.update(|cx| { + this.update(cx, |this, cx| { + this.update_authenticated_user(response, cx); + }) + })??; + } + } + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + pub fn watch_current_user(&self) -> watch::Receiver>> { self.current_user.clone() } From 070f7dbe1a283cc6d765f5fc8a8c3d78ba5ed425 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 7 Aug 2025 19:01:52 -0300 Subject: [PATCH 166/693] onboarding: Add fast-follow adjustments (#35814) Release Notes: - N/A --- assets/images/certified_user_stamp.svg | 1 - assets/images/pro_trial_stamp.svg | 2 +- assets/images/pro_user_stamp.svg | 1 + assets/keymaps/default-linux.json | 10 ++- assets/keymaps/default-macos.json | 10 ++- crates/ai_onboarding/src/ai_upsell_card.rs | 2 +- crates/onboarding/src/ai_setup_page.rs | 65 +++++++++++-------- crates/onboarding/src/basics_page.rs | 12 ++-- crates/onboarding/src/editing_page.rs | 17 +++-- crates/onboarding/src/onboarding.rs | 61 +++++++++-------- .../ui/src/components/button/toggle_button.rs | 44 ++++++++++++- crates/ui/src/components/image.rs | 2 +- 12 files changed, 155 insertions(+), 72 deletions(-) delete mode 100644 assets/images/certified_user_stamp.svg create mode 100644 assets/images/pro_user_stamp.svg diff --git a/assets/images/certified_user_stamp.svg b/assets/images/certified_user_stamp.svg deleted file mode 100644 index 7e65c4fc9d..0000000000 --- a/assets/images/certified_user_stamp.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/images/pro_trial_stamp.svg b/assets/images/pro_trial_stamp.svg index 501de88a48..a3f9095120 100644 --- a/assets/images/pro_trial_stamp.svg +++ b/assets/images/pro_trial_stamp.svg @@ -1 +1 @@ - + diff --git a/assets/images/pro_user_stamp.svg b/assets/images/pro_user_stamp.svg new file mode 100644 index 0000000000..d037a9e833 --- /dev/null +++ b/assets/images/pro_user_stamp.svg @@ -0,0 +1 @@ + diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 567580a9c6..c436b1a8fb 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1103,6 +1103,13 @@ "ctrl-enter": "menu::Confirm" } }, + { + "context": "OnboardingAiConfigurationModal", + "use_key_equivalents": true, + "bindings": { + "escape": "menu::Cancel" + } + }, { "context": "Diagnostics", "use_key_equivalents": true, @@ -1179,7 +1186,8 @@ "ctrl-1": "onboarding::ActivateBasicsPage", "ctrl-2": "onboarding::ActivateEditingPage", "ctrl-3": "onboarding::ActivateAISetupPage", - "ctrl-escape": "onboarding::Finish" + "ctrl-escape": "onboarding::Finish", + "alt-tab": "onboarding::SignIn" } } ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 1c2ad3a006..960bac1479 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1205,6 +1205,13 @@ "cmd-enter": "menu::Confirm" } }, + { + "context": "OnboardingAiConfigurationModal", + "use_key_equivalents": true, + "bindings": { + "escape": "menu::Cancel" + } + }, { "context": "Diagnostics", "use_key_equivalents": true, @@ -1281,7 +1288,8 @@ "cmd-1": "onboarding::ActivateBasicsPage", "cmd-2": "onboarding::ActivateEditingPage", "cmd-3": "onboarding::ActivateAISetupPage", - "cmd-escape": "onboarding::Finish" + "cmd-escape": "onboarding::Finish", + "alt-tab": "onboarding::SignIn" } } ] diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index 65d3866273..e9639ca075 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -137,7 +137,7 @@ impl RenderOnce for AiUpsellCard { .size(rems_from_px(72.)) .child( Vector::new( - VectorName::CertifiedUserStamp, + VectorName::ProUserStamp, rems_from_px(72.), rems_from_px(72.), ) diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index 6099745c40..00f2d5fc8b 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -1,7 +1,7 @@ use std::sync::Arc; -use ai_onboarding::{AiUpsellCard, SignInStatus}; -use client::UserStore; +use ai_onboarding::AiUpsellCard; +use client::{Client, UserStore}; use fs::Fs; use gpui::{ Action, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, @@ -12,8 +12,8 @@ use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageMod use project::DisableAiSettings; use settings::{Settings, update_settings_file}; use ui::{ - Badge, ButtonLike, Divider, Modal, ModalFooter, ModalHeader, Section, SwitchField, ToggleState, - prelude::*, tooltip_container, + Badge, ButtonLike, Divider, KeyBinding, Modal, ModalFooter, ModalHeader, Section, SwitchField, + ToggleState, prelude::*, tooltip_container, }; use util::ResultExt; use workspace::{ModalView, Workspace}; @@ -88,7 +88,7 @@ fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> i h_flex() .gap_2() .justify_between() - .child(Label::new("We don't train models using your data")) + .child(Label::new("Privacy is the default for Zed")) .child( h_flex().gap_1().child(privacy_badge()).child( Button::new("learn_more", "Learn More") @@ -109,7 +109,7 @@ fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> i ) .child( Label::new( - "Feel confident in the security and privacy of your projects using Zed.", + "Any use or storage of your data is with your explicit, single-use, opt-in consent.", ) .size(LabelSize::Small) .color(Color::Muted), @@ -240,6 +240,7 @@ fn render_llm_provider_card( pub(crate) fn render_ai_setup_page( workspace: WeakEntity, user_store: Entity, + client: Arc, window: &mut Window, cx: &mut App, ) -> impl IntoElement { @@ -283,15 +284,16 @@ pub(crate) fn render_ai_setup_page( v_flex() .mt_2() .gap_6() - .child(AiUpsellCard { - sign_in_status: SignInStatus::SignedIn, - sign_in: Arc::new(|_, _| {}), - account_too_young: user_store.read(cx).account_too_young(), - user_plan: user_store.read(cx).plan(), - tab_index: Some({ + .child({ + let mut ai_upsell_card = + AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx); + + ai_upsell_card.tab_index = Some({ tab_index += 1; tab_index - 1 - }), + }); + + ai_upsell_card }) .child(render_llm_provider_section( &mut tab_index, @@ -336,6 +338,10 @@ impl AiConfigurationModal { selected_provider, } } + + fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context) { + cx.emit(DismissEvent); + } } impl ModalView for AiConfigurationModal {} @@ -349,11 +355,15 @@ impl Focusable for AiConfigurationModal { } impl Render for AiConfigurationModal { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() + .key_context("OnboardingAiConfigurationModal") .w(rems(34.)) .elevation_3(cx) .track_focus(&self.focus_handle) + .on_action( + cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)), + ) .child( Modal::new("onboarding-ai-setup-modal", None) .header( @@ -368,18 +378,19 @@ impl Render for AiConfigurationModal { .section(Section::new().child(self.configuration_view.clone())) .footer( ModalFooter::new().end_slot( - h_flex() - .gap_1() - .child( - Button::new("onboarding-closing-cancel", "Cancel") - .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), + Button::new("ai-onb-modal-Done", "Done") + .key_binding( + KeyBinding::for_action_in( + &menu::Cancel, + &self.focus_handle.clone(), + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(12.))), ) - .child(Button::new("save-btn", "Done").on_click(cx.listener( - |_, _, window, cx| { - window.dispatch_action(menu::Confirm.boxed_clone(), cx); - cx.emit(DismissEvent); - }, - ))), + .on_click(cx.listener(|this, _event, _window, cx| { + this.cancel(&menu::Cancel, cx) + })), ), ), ) @@ -396,7 +407,7 @@ impl AiPrivacyTooltip { impl Render for AiPrivacyTooltip { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - const DESCRIPTION: &'static str = "One of Zed's most important principles is transparency. This is why we are and value open-source so much. And it wouldn't be any different with AI."; + const DESCRIPTION: &'static str = "We believe in opt-in data sharing as the default for building AI products, rather than opt-out. We'll only use or store your data if you affirmatively send it to us. "; tooltip_container(window, cx, move |this, _, _| { this.child( @@ -407,7 +418,7 @@ impl Render for AiPrivacyTooltip { .size(IconSize::Small) .color(Color::Muted), ) - .child(Label::new("Privacy Principle")), + .child(Label::new("Privacy First")), ) .child( div().max_w_64().child( diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index a4e4028051..a19a21fddf 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -201,12 +201,15 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement let fs = ::global(cx); v_flex() + .pt_6() .gap_4() + .border_t_1() + .border_color(cx.theme().colors().border_variant.opacity(0.5)) .child(Label::new("Telemetry").size(LabelSize::Large)) .child(SwitchField::new( "onboarding-telemetry-metrics", "Help Improve Zed", - Some("Sending anonymous usage data helps us build the right features and create the best experience.".into()), + Some("Anonymous usage data helps us build the right features and improve your experience.".into()), if TelemetrySettings::get_global(cx).metrics { ui::ToggleState::Selected } else { @@ -294,7 +297,7 @@ fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoE ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| { write_keymap_base(BaseKeymap::Emacs, cx); }), - ToggleButtonWithIcon::new("Cursor (Beta)", IconName::EditorCursor, |_, _, cx| { + ToggleButtonWithIcon::new("Cursor", IconName::EditorCursor, |_, _, cx| { write_keymap_base(BaseKeymap::Cursor, cx); }), ], @@ -326,10 +329,7 @@ fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoEleme SwitchField::new( "onboarding-vim-mode", "Vim Mode", - Some( - "Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back." - .into(), - ), + Some("Coming from Neovim? Use our first-class implementation of Vim Mode.".into()), toggle_state, { let fs = ::global(cx); diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index a8f0265b6b..8b4293db0d 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -584,11 +584,15 @@ fn render_popular_settings_section( window: &mut Window, cx: &mut App, ) -> impl IntoElement { - const LIGATURE_TOOLTIP: &'static str = "Ligatures are when a font creates a special character out of combining two characters into one. For example, with ligatures turned on, =/= would become ≠."; + const LIGATURE_TOOLTIP: &'static str = + "Font ligatures combine two characters into one. For example, turning =/= into ≠."; v_flex() - .gap_5() - .child(Label::new("Popular Settings").size(LabelSize::Large).mt_8()) + .pt_6() + .gap_4() + .border_t_1() + .border_color(cx.theme().colors().border_variant.opacity(0.5)) + .child(Label::new("Popular Settings").size(LabelSize::Large)) .child(render_font_customization_section(tab_index, window, cx)) .child( SwitchField::new( @@ -683,7 +687,10 @@ fn render_popular_settings_section( [ ToggleButtonSimple::new("Auto", |_, _, cx| { write_show_mini_map(ShowMinimap::Auto, cx); - }), + }) + .tooltip(Tooltip::text( + "Show the minimap if the editor's scrollbar is visible.", + )), ToggleButtonSimple::new("Always", |_, _, cx| { write_show_mini_map(ShowMinimap::Always, cx); }), @@ -707,7 +714,7 @@ fn render_popular_settings_section( pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement { let mut tab_index = 0; v_flex() - .gap_4() + .gap_6() .child(render_import_settings_section(&mut tab_index, cx)) .child(render_popular_settings_section(&mut tab_index, window, cx)) } diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index c4d2b6847c..98f61df97b 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -77,6 +77,8 @@ actions!( ActivateAISetupPage, /// Finish the onboarding process. Finish, + /// Sign in while in the onboarding flow. + SignIn ] ); @@ -376,6 +378,7 @@ impl Onboarding { cx, ) .map(|kb| kb.size(rems_from_px(12.))); + if ai_setup_page { this.child( ButtonLike::new("start_building") @@ -387,14 +390,7 @@ impl Onboarding { .w_full() .justify_between() .child(Label::new("Start Building")) - .child(keybinding.map_or_else( - || { - Icon::new(IconName::Check) - .size(IconSize::Small) - .into_any_element() - }, - IntoElement::into_any_element, - )), + .children(keybinding), ) .on_click(|_, window, cx| { window.dispatch_action(Finish.boxed_clone(), cx); @@ -409,11 +405,10 @@ impl Onboarding { .ml_1() .w_full() .justify_between() - .child(Label::new("Skip All")) - .child(keybinding.map_or_else( - || gpui::Empty.into_any_element(), - IntoElement::into_any_element, - )), + .child( + Label::new("Skip All").color(Color::Muted), + ) + .children(keybinding), ) .on_click(|_, window, cx| { window.dispatch_action(Finish.boxed_clone(), cx); @@ -435,23 +430,39 @@ impl Onboarding { Button::new("sign_in", "Sign In") .full_width() .style(ButtonStyle::Outlined) + .size(ButtonSize::Medium) + .key_binding( + KeyBinding::for_action_in(&SignIn, &self.focus_handle, window, cx) + .map(|kb| kb.size(rems_from_px(12.))), + ) .on_click(|_, window, cx| { - let client = Client::global(cx); - window - .spawn(cx, async move |cx| { - client - .sign_in_with_optional_connect(true, &cx) - .await - .notify_async_err(cx); - }) - .detach(); + window.dispatch_action(SignIn.boxed_clone(), cx); }) .into_any_element() }, ) } + fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) { + go_to_welcome_page(cx); + } + + fn handle_sign_in(_: &SignIn, window: &mut Window, cx: &mut App) { + let client = Client::global(cx); + + window + .spawn(cx, async move |cx| { + client + .sign_in_with_optional_connect(true, &cx) + .await + .notify_async_err(cx); + }) + .detach(); + } + fn render_page(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { + let client = Client::global(cx); + match self.selected_page { SelectedPage::Basics => crate::basics_page::render_basics_page(cx).into_any_element(), SelectedPage::Editing => { @@ -460,16 +471,13 @@ impl Onboarding { SelectedPage::AiSetup => crate::ai_setup_page::render_ai_setup_page( self.workspace.clone(), self.user_store.clone(), + client, window, cx, ) .into_any_element(), } } - - fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) { - go_to_welcome_page(cx); - } } impl Render for Onboarding { @@ -486,6 +494,7 @@ impl Render for Onboarding { .size_full() .bg(cx.theme().colors().editor_background) .on_action(Self::on_finish) + .on_action(Self::handle_sign_in) .on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| { this.set_page(SelectedPage::Basics, cx); })) diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs index 6fbf834667..91defa730b 100644 --- a/crates/ui/src/components/button/toggle_button.rs +++ b/crates/ui/src/components/button/toggle_button.rs @@ -1,6 +1,8 @@ +use std::rc::Rc; + use gpui::{AnyView, ClickEvent}; -use crate::{ButtonLike, ButtonLikeRounding, ElevationIndex, TintColor, prelude::*}; +use crate::{ButtonLike, ButtonLikeRounding, ElevationIndex, TintColor, Tooltip, prelude::*}; /// The position of a [`ToggleButton`] within a group of buttons. #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -301,6 +303,7 @@ pub struct ButtonConfiguration { icon: Option, on_click: Box, selected: bool, + tooltip: Option AnyView>>, } mod private { @@ -315,6 +318,7 @@ pub struct ToggleButtonSimple { label: SharedString, on_click: Box, selected: bool, + tooltip: Option AnyView>>, } impl ToggleButtonSimple { @@ -326,6 +330,7 @@ impl ToggleButtonSimple { label: label.into(), on_click: Box::new(on_click), selected: false, + tooltip: None, } } @@ -333,6 +338,11 @@ impl ToggleButtonSimple { self.selected = selected; self } + + pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { + self.tooltip = Some(Rc::new(tooltip)); + self + } } impl private::ToggleButtonStyle for ToggleButtonSimple {} @@ -344,6 +354,7 @@ impl ButtonBuilder for ToggleButtonSimple { icon: None, on_click: self.on_click, selected: self.selected, + tooltip: self.tooltip, } } } @@ -353,6 +364,7 @@ pub struct ToggleButtonWithIcon { icon: IconName, on_click: Box, selected: bool, + tooltip: Option AnyView>>, } impl ToggleButtonWithIcon { @@ -366,6 +378,7 @@ impl ToggleButtonWithIcon { icon, on_click: Box::new(on_click), selected: false, + tooltip: None, } } @@ -373,6 +386,11 @@ impl ToggleButtonWithIcon { self.selected = selected; self } + + pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { + self.tooltip = Some(Rc::new(tooltip)); + self + } } impl private::ToggleButtonStyle for ToggleButtonWithIcon {} @@ -384,6 +402,7 @@ impl ButtonBuilder for ToggleButtonWithIcon { icon: Some(self.icon), on_click: self.on_click, selected: self.selected, + tooltip: self.tooltip, } } } @@ -486,11 +505,13 @@ impl RenderOnce icon, on_click, selected, + tooltip, } = button.into_configuration(); let entry_index = row_index * COLS + col_index; ButtonLike::new((self.group_name, entry_index)) + .rounding(None) .when_some(self.tab_index, |this, tab_index| { this.tab_index(tab_index + entry_index as isize) }) @@ -498,7 +519,6 @@ impl RenderOnce this.toggle_state(true) .selected_style(ButtonStyle::Tinted(TintColor::Accent)) }) - .rounding(None) .when(self.style == ToggleButtonGroupStyle::Filled, |button| { button.style(ButtonStyle::Filled) }) @@ -527,6 +547,9 @@ impl RenderOnce |this| this.color(Color::Accent), )), ) + .when_some(tooltip, |this, tooltip| { + this.tooltip(move |window, cx| tooltip(window, cx)) + }) .on_click(on_click) .into_any_element() }) @@ -920,6 +943,23 @@ impl Component ), ], )]) + .children(vec![single_example( + "With Tooltips", + ToggleButtonGroup::single_row( + "with_tooltips", + [ + ToggleButtonSimple::new("First", |_, _, _| {}) + .tooltip(Tooltip::text("This is a tooltip. Hello!")), + ToggleButtonSimple::new("Second", |_, _, _| {}) + .tooltip(Tooltip::text("This is a tooltip. Hey?")), + ToggleButtonSimple::new("Third", |_, _, _| {}) + .tooltip(Tooltip::text("This is a tooltip. Get out of here now!")), + ], + ) + .selected_index(1) + .button_width(rems_from_px(100.)) + .into_any_element(), + )]) .into_any_element(), ) } diff --git a/crates/ui/src/components/image.rs b/crates/ui/src/components/image.rs index 18f804abe9..09c3bbeb94 100644 --- a/crates/ui/src/components/image.rs +++ b/crates/ui/src/components/image.rs @@ -14,10 +14,10 @@ use crate::prelude::*; #[strum(serialize_all = "snake_case")] pub enum VectorName { AiGrid, - CertifiedUserStamp, DebuggerGrid, Grid, ProTrialStamp, + ProUserStamp, ZedLogo, ZedXCopilot, } From e6dc6faccf8949ad2f122afb549a112d0f622170 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 7 Aug 2025 18:10:29 -0400 Subject: [PATCH 167/693] Don't insert resource links for @mentions that have been removed from the message editor (#35831) Release Notes: - N/A --- crates/agent_ui/src/acp/message_history.rs | 5 + crates/agent_ui/src/acp/thread_view.rs | 112 +++++++++++++++++++++ crates/editor/src/editor.rs | 5 + 3 files changed, 122 insertions(+) diff --git a/crates/agent_ui/src/acp/message_history.rs b/crates/agent_ui/src/acp/message_history.rs index d0fb1f0990..c6106c7578 100644 --- a/crates/agent_ui/src/acp/message_history.rs +++ b/crates/agent_ui/src/acp/message_history.rs @@ -45,6 +45,11 @@ impl MessageHistory { None }) } + + #[cfg(test)] + pub fn items(&self) -> &[T] { + &self.items + } } #[cfg(test)] mod tests { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 5c6ae091cd..ff6da43299 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -381,6 +381,11 @@ impl AcpThreadView { editor.display_map.update(cx, |map, cx| { let snapshot = map.snapshot(cx); for (crease_id, crease) in snapshot.crease_snapshot.creases() { + // Skip creases that have been edited out of the message buffer. + if !crease.range().start.is_valid(&snapshot.buffer_snapshot) { + continue; + } + if let Some(project_path) = self.mention_set.lock().path_for_crease_id(crease_id) { @@ -2898,8 +2903,12 @@ mod tests { use fs::FakeFs; use futures::future::try_join_all; use gpui::{SemanticVersion, TestAppContext, VisualTestContext}; + use lsp::{CompletionContext, CompletionTriggerKind}; + use project::CompletionIntent; use rand::Rng; + use serde_json::json; use settings::SettingsStore; + use util::path; use super::*; @@ -3011,6 +3020,109 @@ mod tests { ); } + #[gpui::test] + async fn test_crease_removal(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/project", json!({"file": ""})).await; + let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; + let agent = StubAgentServer::default(); + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let thread_view = cx.update(|window, cx| { + cx.new(|cx| { + AcpThreadView::new( + Rc::new(agent), + workspace.downgrade(), + project, + Rc::new(RefCell::new(MessageHistory::default())), + 1, + None, + window, + cx, + ) + }) + }); + + cx.run_until_parked(); + + let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); + let excerpt_id = message_editor.update(cx, |editor, cx| { + editor + .buffer() + .read(cx) + .excerpt_ids() + .into_iter() + .next() + .unwrap() + }); + let completions = message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Hello @", window, cx); + let buffer = editor.buffer().read(cx).as_singleton().unwrap(); + let completion_provider = editor.completion_provider().unwrap(); + completion_provider.completions( + excerpt_id, + &buffer, + Anchor::MAX, + CompletionContext { + trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER, + trigger_character: Some("@".into()), + }, + window, + cx, + ) + }); + let [_, completion]: [_; 2] = completions + .await + .unwrap() + .into_iter() + .flat_map(|response| response.completions) + .collect::>() + .try_into() + .unwrap(); + + message_editor.update_in(cx, |editor, window, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let start = snapshot + .anchor_in_excerpt(excerpt_id, completion.replace_range.start) + .unwrap(); + let end = snapshot + .anchor_in_excerpt(excerpt_id, completion.replace_range.end) + .unwrap(); + editor.edit([(start..end, completion.new_text)], cx); + (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx); + }); + + cx.run_until_parked(); + + // Backspace over the inserted crease (and the following space). + message_editor.update_in(cx, |editor, window, cx| { + editor.backspace(&Default::default(), window, cx); + editor.backspace(&Default::default(), window, cx); + }); + + thread_view.update_in(cx, |thread_view, window, cx| { + thread_view.chat(&Chat, window, cx); + }); + + cx.run_until_parked(); + + let content = thread_view.update_in(cx, |thread_view, _window, _cx| { + thread_view + .message_history + .borrow() + .items() + .iter() + .flatten() + .cloned() + .collect::>() + }); + + // We don't send a resource link for the deleted crease. + pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]); + } + async fn setup_thread_view( agent: impl AgentServer + 'static, cx: &mut TestAppContext, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 677acd9fd8..bd7963a2e2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2705,6 +2705,11 @@ impl Editor { self.completion_provider = provider; } + #[cfg(any(test, feature = "test-support"))] + pub fn completion_provider(&self) -> Option> { + self.completion_provider.clone() + } + pub fn semantics_provider(&self) -> Option> { self.semantics_provider.clone() } From 11efa32fa798c7a3ed2775e1db62da1f2e5dca68 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 7 Aug 2025 18:14:25 -0400 Subject: [PATCH 168/693] client: Only connect to Collab automatically for Zed staff (#35827) This PR makes it so that only Zed staff connect to Collab automatically. Anyone else can connect to Collab manually when they want to collaborate (but this is not required for using Zed's LLM features). Release Notes: - N/A --------- Co-authored-by: Richard --- crates/client/src/client.rs | 45 ++++++++++++++++++----- crates/feature_flags/src/feature_flags.rs | 24 ++++++++++++ 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 9d58692c0d..12ea4bcd3e 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -16,6 +16,7 @@ use clock::SystemClock; use cloud_api_client::CloudApiClient; use cloud_api_client::websocket_protocol::MessageToClient; use credentials_provider::CredentialsProvider; +use feature_flags::FeatureFlagAppExt as _; use futures::{ AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt, channel::oneshot, future::BoxFuture, @@ -964,25 +965,51 @@ impl Client { Ok(()) } - /// Performs a sign-in and also connects to Collab. + /// Performs a sign-in and also (optionally) connects to Collab. /// - /// This is called in places where we *don't* need to connect in the future. We will replace these calls with calls - /// to `sign_in` when we're ready to remove auto-connection to Collab. + /// Only Zed staff automatically connect to Collab. pub async fn sign_in_with_optional_connect( self: &Arc, try_provider: bool, cx: &AsyncApp, ) -> Result<()> { + let (is_staff_tx, is_staff_rx) = oneshot::channel::(); + let mut is_staff_tx = Some(is_staff_tx); + cx.update(|cx| { + cx.on_flags_ready(move |state, _cx| { + if let Some(is_staff_tx) = is_staff_tx.take() { + is_staff_tx.send(state.is_staff).log_err(); + } + }) + .detach(); + }) + .log_err(); + let credentials = self.sign_in(try_provider, cx).await?; self.connect_to_cloud(cx).await.log_err(); - let connect_result = match self.connect_with_credentials(credentials, cx).await { - ConnectionResult::Timeout => Err(anyhow!("connection timed out")), - ConnectionResult::ConnectionReset => Err(anyhow!("connection reset")), - ConnectionResult::Result(result) => result.context("client auth and connect"), - }; - connect_result.log_err(); + cx.update(move |cx| { + cx.spawn({ + let client = self.clone(); + async move |cx| { + let is_staff = is_staff_rx.await?; + if is_staff { + match client.connect_with_credentials(credentials, cx).await { + ConnectionResult::Timeout => Err(anyhow!("connection timed out")), + ConnectionResult::ConnectionReset => Err(anyhow!("connection reset")), + ConnectionResult::Result(result) => { + result.context("client auth and connect") + } + } + } else { + Ok(()) + } + } + }) + .detach_and_log_err(cx); + }) + .log_err(); Ok(()) } diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index 631bafc841..ef357adf35 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -158,6 +158,11 @@ where } } +#[derive(Debug)] +pub struct OnFlagsReady { + pub is_staff: bool, +} + pub trait FeatureFlagAppExt { fn wait_for_flag(&mut self) -> WaitForFlag; @@ -169,6 +174,10 @@ pub trait FeatureFlagAppExt { fn has_flag(&self) -> bool; fn is_staff(&self) -> bool; + fn on_flags_ready(&mut self, callback: F) -> Subscription + where + F: FnMut(OnFlagsReady, &mut App) + 'static; + fn observe_flag(&mut self, callback: F) -> Subscription where F: FnMut(bool, &mut App) + 'static; @@ -198,6 +207,21 @@ impl FeatureFlagAppExt for App { .unwrap_or(false) } + fn on_flags_ready(&mut self, mut callback: F) -> Subscription + where + F: FnMut(OnFlagsReady, &mut App) + 'static, + { + self.observe_global::(move |cx| { + let feature_flags = cx.global::(); + callback( + OnFlagsReady { + is_staff: feature_flags.staff, + }, + cx, + ); + }) + } + fn observe_flag(&mut self, mut callback: F) -> Subscription where F: FnMut(bool, &mut App) + 'static, From 90fa9217563b2ca79cbfd1b1c2deb11aff2fc551 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 8 Aug 2025 00:21:26 +0200 Subject: [PATCH 169/693] Wire up find_path tool in agent2 (#35799) Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra --- Cargo.lock | 4 +- Cargo.toml | 2 +- crates/acp_thread/src/acp_thread.rs | 9 + crates/agent2/Cargo.toml | 1 - crates/agent2/src/agent.rs | 10 +- crates/agent2/src/agent2.rs | 1 + crates/agent2/src/templates.rs | 13 - crates/agent2/src/templates/glob.hbs | 8 - crates/agent2/src/tests/mod.rs | 132 +++++++++- crates/agent2/src/tests/test_tools.rs | 82 +++++-- crates/agent2/src/thread.rs | 282 +++++++++++++--------- crates/agent2/src/tools.rs | 6 +- crates/agent2/src/tools/find_path_tool.rs | 231 ++++++++++++++++++ crates/agent2/src/tools/glob.rs | 84 ------- crates/agent2/src/tools/thinking_tool.rs | 48 ++++ crates/agent_servers/src/acp/v0.rs | 1 + crates/agent_servers/src/claude/tools.rs | 1 + crates/agent_ui/src/acp/thread_view.rs | 1 + 18 files changed, 669 insertions(+), 247 deletions(-) delete mode 100644 crates/agent2/src/templates/glob.hbs create mode 100644 crates/agent2/src/tools/find_path_tool.rs delete mode 100644 crates/agent2/src/tools/glob.rs create mode 100644 crates/agent2/src/tools/thinking_tool.rs diff --git a/Cargo.lock b/Cargo.lock index fe0c7a1b23..8c1f1d00ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,9 +138,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.21" +version = "0.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ae3c22c23b64a5c3b7fc8a86fcc7c494e989bd2cd66fdce14a58cfc8078381" +checksum = "3fad72b7b8ee4331b3a4c8d43c107e982a4725564b4ee658ae5c4e79d2b486e8" dependencies = [ "anyhow", "futures 0.3.31", diff --git a/Cargo.toml b/Cargo.toml index 6bff713aaa..d547110bb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -425,7 +425,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.21" +agent-client-protocol = { version = "0.0.23" } aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 1671003023..71827d6948 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -167,6 +167,7 @@ pub struct ToolCall { pub status: ToolCallStatus, pub locations: Vec, pub raw_input: Option, + pub raw_output: Option, } impl ToolCall { @@ -195,6 +196,7 @@ impl ToolCall { locations: tool_call.locations, status, raw_input: tool_call.raw_input, + raw_output: tool_call.raw_output, } } @@ -211,6 +213,7 @@ impl ToolCall { content, locations, raw_input, + raw_output, } = fields; if let Some(kind) = kind { @@ -241,6 +244,10 @@ impl ToolCall { if let Some(raw_input) = raw_input { self.raw_input = Some(raw_input); } + + if let Some(raw_output) = raw_output { + self.raw_output = Some(raw_output); + } } pub fn diffs(&self) -> impl Iterator { @@ -1547,6 +1554,7 @@ mod tests { content: vec![], locations: vec![], raw_input: None, + raw_output: None, }), cx, ) @@ -1659,6 +1667,7 @@ mod tests { }], locations: vec![], raw_input: None, + raw_output: None, }), cx, ) diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 21a043fd98..884378fbcc 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -39,7 +39,6 @@ ui.workspace = true util.workspace = true uuid.workspace = true watch.workspace = true -worktree.workspace = true workspace-hack.workspace = true [dev-dependencies] diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 5c0acb3fb1..cb568f04c2 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,5 +1,5 @@ -use crate::ToolCallAuthorization; use crate::{templates::Templates, AgentResponseEvent, Thread}; +use crate::{FindPathTool, ThinkingTool, ToolCallAuthorization}; use acp_thread::ModelSelector; use agent_client_protocol as acp; use anyhow::{anyhow, Context as _, Result}; @@ -412,7 +412,13 @@ impl acp_thread::AgentConnection for NativeAgentConnection { anyhow!("No default model configured. Please configure a default model in settings.") })?; - let thread = cx.new(|_| Thread::new(project, agent.project_context.clone(), action_log, agent.templates.clone(), default_model)); + let thread = cx.new(|_| { + let mut thread = Thread::new(project.clone(), agent.project_context.clone(), action_log, agent.templates.clone(), default_model); + thread.add_tool(ThinkingTool); + thread.add_tool(FindPathTool::new(project.clone())); + thread + }); + Ok(thread) }, )??; diff --git a/crates/agent2/src/agent2.rs b/crates/agent2/src/agent2.rs index d759f63d89..db743c8429 100644 --- a/crates/agent2/src/agent2.rs +++ b/crates/agent2/src/agent2.rs @@ -10,3 +10,4 @@ mod tests; pub use agent::*; pub use native_agent_server::NativeAgentServer; pub use thread::*; +pub use tools::*; diff --git a/crates/agent2/src/templates.rs b/crates/agent2/src/templates.rs index e634d414d6..a63f0ad206 100644 --- a/crates/agent2/src/templates.rs +++ b/crates/agent2/src/templates.rs @@ -33,19 +33,6 @@ pub trait Template: Sized { } } -#[expect( - dead_code, - reason = "Marked as unused by Rust 1.89 and left as is as of 07 Aug 2025 to let AI team address it." -)] -#[derive(Serialize)] -pub struct GlobTemplate { - pub project_roots: String, -} - -impl Template for GlobTemplate { - const TEMPLATE_NAME: &'static str = "glob.hbs"; -} - #[derive(Serialize)] pub struct SystemPromptTemplate<'a> { #[serde(flatten)] diff --git a/crates/agent2/src/templates/glob.hbs b/crates/agent2/src/templates/glob.hbs deleted file mode 100644 index 3bf992b093..0000000000 --- a/crates/agent2/src/templates/glob.hbs +++ /dev/null @@ -1,8 +0,0 @@ -Find paths on disk with glob patterns. - -Assume that all glob patterns are matched in a project directory with the following entries. - -{{project_roots}} - -When searching with patterns that begin with literal path components, e.g. `foo/bar/**/*.rs`, be -sure to anchor them with one of the directories listed above. diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index b13b1cbe1a..7913f9a24c 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -270,14 +270,14 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { vec![ MessageContent::ToolResult(LanguageModelToolResult { tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(), - tool_name: tool_call_auth_1.tool_call.title.into(), + tool_name: ToolRequiringPermission.name().into(), is_error: false, content: "Allowed".into(), output: None }), MessageContent::ToolResult(LanguageModelToolResult { tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(), - tool_name: tool_call_auth_2.tool_call.title.into(), + tool_name: ToolRequiringPermission.name().into(), is_error: true, content: "Permission to run tool denied by user".into(), output: None @@ -286,6 +286,63 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { ); } +#[gpui::test] +async fn test_tool_hallucination(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let mut events = thread.update(cx, |thread, cx| thread.send(model.clone(), "abc", cx)); + cx.run_until_parked(); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + LanguageModelToolUse { + id: "tool_id_1".into(), + name: "nonexistent_tool".into(), + raw_input: "{}".into(), + input: json!({}), + is_input_complete: true, + }, + )); + fake_model.end_last_completion_stream(); + + let tool_call = expect_tool_call(&mut events).await; + assert_eq!(tool_call.title, "nonexistent_tool"); + assert_eq!(tool_call.status, acp::ToolCallStatus::Pending); + let update = expect_tool_call_update(&mut events).await; + assert_eq!(update.fields.status, Some(acp::ToolCallStatus::Failed)); +} + +async fn expect_tool_call( + events: &mut UnboundedReceiver>, +) -> acp::ToolCall { + let event = events + .next() + .await + .expect("no tool call authorization event received") + .unwrap(); + match event { + AgentResponseEvent::ToolCall(tool_call) => return tool_call, + event => { + panic!("Unexpected event {event:?}"); + } + } +} + +async fn expect_tool_call_update( + events: &mut UnboundedReceiver>, +) -> acp::ToolCallUpdate { + let event = events + .next() + .await + .expect("no tool call authorization event received") + .unwrap(); + match event { + AgentResponseEvent::ToolCallUpdate(tool_call_update) => return tool_call_update, + event => { + panic!("Unexpected event {event:?}"); + } + } +} + async fn next_tool_call_authorization( events: &mut UnboundedReceiver>, ) -> ToolCallAuthorization { @@ -582,6 +639,77 @@ async fn test_agent_connection(cx: &mut TestAppContext) { ); } +#[gpui::test] +async fn test_tool_updates_to_completion(cx: &mut TestAppContext) { + let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; + thread.update(cx, |thread, _cx| thread.add_tool(ThinkingTool)); + let fake_model = model.as_fake(); + + let mut events = thread.update(cx, |thread, cx| thread.send(model.clone(), "Think", cx)); + cx.run_until_parked(); + + let input = json!({ "content": "Thinking hard!" }); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + LanguageModelToolUse { + id: "1".into(), + name: ThinkingTool.name().into(), + raw_input: input.to_string(), + input, + is_input_complete: true, + }, + )); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + let tool_call = expect_tool_call(&mut events).await; + assert_eq!( + tool_call, + acp::ToolCall { + id: acp::ToolCallId("1".into()), + title: "Thinking".into(), + kind: acp::ToolKind::Think, + status: acp::ToolCallStatus::Pending, + content: vec![], + locations: vec![], + raw_input: Some(json!({ "content": "Thinking hard!" })), + raw_output: None, + } + ); + let update = expect_tool_call_update(&mut events).await; + assert_eq!( + update, + acp::ToolCallUpdate { + id: acp::ToolCallId("1".into()), + fields: acp::ToolCallUpdateFields { + status: Some(acp::ToolCallStatus::InProgress,), + ..Default::default() + }, + } + ); + let update = expect_tool_call_update(&mut events).await; + assert_eq!( + update, + acp::ToolCallUpdate { + id: acp::ToolCallId("1".into()), + fields: acp::ToolCallUpdateFields { + content: Some(vec!["Thinking hard!".into()]), + ..Default::default() + }, + } + ); + let update = expect_tool_call_update(&mut events).await; + assert_eq!( + update, + acp::ToolCallUpdate { + id: acp::ToolCallId("1".into()), + fields: acp::ToolCallUpdateFields { + status: Some(acp::ToolCallStatus::Completed), + ..Default::default() + }, + } + ); +} + /// Filters out the stop events for asserting against in tests fn stop_events( result_events: Vec>, diff --git a/crates/agent2/src/tests/test_tools.rs b/crates/agent2/src/tests/test_tools.rs index a066bb982e..fd6e7e941f 100644 --- a/crates/agent2/src/tests/test_tools.rs +++ b/crates/agent2/src/tests/test_tools.rs @@ -19,11 +19,20 @@ impl AgentTool for EchoTool { "echo".into() } - fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool { - false + fn kind(&self) -> acp::ToolKind { + acp::ToolKind::Other } - fn run(self: Arc, input: Self::Input, _cx: &mut App) -> Task> { + fn initial_title(&self, _: Self::Input) -> SharedString { + "Echo".into() + } + + fn run( + self: Arc, + input: Self::Input, + _event_stream: ToolCallEventStream, + _cx: &mut App, + ) -> Task> { Task::ready(Ok(input.text)) } } @@ -44,11 +53,20 @@ impl AgentTool for DelayTool { "delay".into() } - fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool { - false + fn initial_title(&self, input: Self::Input) -> SharedString { + format!("Delay {}ms", input.ms).into() } - fn run(self: Arc, input: Self::Input, cx: &mut App) -> Task> + fn kind(&self) -> acp::ToolKind { + acp::ToolKind::Other + } + + fn run( + self: Arc, + input: Self::Input, + _event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> where Self: Sized, { @@ -71,16 +89,28 @@ impl AgentTool for ToolRequiringPermission { "tool_requiring_permission".into() } - fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool { - true + fn kind(&self) -> acp::ToolKind { + acp::ToolKind::Other } - fn run(self: Arc, _input: Self::Input, cx: &mut App) -> Task> + fn initial_title(&self, _input: Self::Input) -> SharedString { + "This tool requires permission".into() + } + + fn run( + self: Arc, + input: Self::Input, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> where Self: Sized, { - cx.foreground_executor() - .spawn(async move { Ok("Allowed".to_string()) }) + let auth_check = self.authorize(input, event_stream); + cx.foreground_executor().spawn(async move { + auth_check.await?; + Ok("Allowed".to_string()) + }) } } @@ -96,11 +126,20 @@ impl AgentTool for InfiniteTool { "infinite".into() } - fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool { - false + fn kind(&self) -> acp::ToolKind { + acp::ToolKind::Other } - fn run(self: Arc, _input: Self::Input, cx: &mut App) -> Task> { + fn initial_title(&self, _input: Self::Input) -> SharedString { + "This is the tool that never ends... it just goes on and on my friends!".into() + } + + fn run( + self: Arc, + _input: Self::Input, + _event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> { cx.foreground_executor().spawn(async move { future::pending::<()>().await; unreachable!() @@ -137,11 +176,20 @@ impl AgentTool for WordListTool { "word_list".into() } - fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool { - false + fn initial_title(&self, _input: Self::Input) -> SharedString { + "List of random words".into() } - fn run(self: Arc, _input: Self::Input, _cx: &mut App) -> Task> { + fn kind(&self) -> acp::ToolKind { + acp::ToolKind::Other + } + + fn run( + self: Arc, + _input: Self::Input, + _event_stream: ToolCallEventStream, + _cx: &mut App, + ) -> Task> { Task::ready(Ok("ok".to_string())) } } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 9b17d7e37e..805ffff1c0 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1,16 +1,16 @@ use crate::templates::{SystemPromptTemplate, Template, Templates}; use agent_client_protocol as acp; use anyhow::{anyhow, Context as _, Result}; -use assistant_tool::ActionLog; +use assistant_tool::{adapt_schema_to_format, ActionLog}; use cloud_llm_client::{CompletionIntent, CompletionMode}; use collections::HashMap; use futures::{ channel::{mpsc, oneshot}, stream::FuturesUnordered, }; -use gpui::{App, Context, Entity, ImageFormat, SharedString, Task}; +use gpui::{App, Context, Entity, SharedString, Task}; use language_model::{ - LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage, + LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role, StopReason, @@ -19,7 +19,7 @@ use log; use project::Project; use prompt_store::ProjectContext; use schemars::{JsonSchema, Schema}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use smol::stream::StreamExt; use std::{cell::RefCell, collections::BTreeMap, fmt::Write, future::Future, rc::Rc, sync::Arc}; use util::{markdown::MarkdownCodeBlock, ResultExt}; @@ -276,7 +276,17 @@ impl Thread { while let Some(tool_result) = tool_uses.next().await { log::info!("Tool finished {:?}", tool_result); - event_stream.send_tool_call_result(&tool_result); + event_stream.send_tool_call_update( + &tool_result.tool_use_id, + acp::ToolCallUpdateFields { + status: Some(if tool_result.is_error { + acp::ToolCallStatus::Failed + } else { + acp::ToolCallStatus::Completed + }), + ..Default::default() + }, + ); thread .update(cx, |thread, _cx| { thread.pending_tool_uses.remove(&tool_result.tool_use_id); @@ -426,6 +436,8 @@ impl Thread { ) -> Option> { cx.notify(); + let tool = self.tools.get(tool_use.name.as_ref()).cloned(); + self.pending_tool_uses .insert(tool_use.id.clone(), tool_use.clone()); let last_message = self.last_assistant_message(); @@ -443,8 +455,9 @@ impl Thread { true } }); + if push_new_tool_use { - event_stream.send_tool_call(&tool_use); + event_stream.send_tool_call(tool.as_ref(), &tool_use); last_message .content .push(MessageContent::ToolUse(tool_use.clone())); @@ -462,37 +475,36 @@ impl Thread { return None; } - if let Some(tool) = self.tools.get(tool_use.name.as_ref()) { - let tool_result = - self.run_tool(tool.clone(), tool_use.clone(), event_stream.clone(), cx); - Some(cx.foreground_executor().spawn(async move { - match tool_result.await { - Ok(tool_output) => LanguageModelToolResult { - tool_use_id: tool_use.id, - tool_name: tool_use.name, - is_error: false, - content: LanguageModelToolResultContent::Text(Arc::from(tool_output)), - output: None, - }, - Err(error) => LanguageModelToolResult { - tool_use_id: tool_use.id, - tool_name: tool_use.name, - is_error: true, - content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())), - output: None, - }, - } - })) - } else { + let Some(tool) = tool else { let content = format!("No tool named {} exists", tool_use.name); - Some(Task::ready(LanguageModelToolResult { + return Some(Task::ready(LanguageModelToolResult { content: LanguageModelToolResultContent::Text(Arc::from(content)), tool_use_id: tool_use.id, tool_name: tool_use.name, is_error: true, output: None, - })) - } + })); + }; + + let tool_result = self.run_tool(tool, tool_use.clone(), event_stream.clone(), cx); + Some(cx.foreground_executor().spawn(async move { + match tool_result.await { + Ok(tool_output) => LanguageModelToolResult { + tool_use_id: tool_use.id, + tool_name: tool_use.name, + is_error: false, + content: LanguageModelToolResultContent::Text(Arc::from(tool_output)), + output: None, + }, + Err(error) => LanguageModelToolResult { + tool_use_id: tool_use.id, + tool_name: tool_use.name, + is_error: true, + content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())), + output: None, + }, + } + })) } fn run_tool( @@ -502,20 +514,14 @@ impl Thread { event_stream: AgentResponseEventStream, cx: &mut Context, ) -> Task> { - let needs_authorization = tool.needs_authorization(tool_use.input.clone(), cx); cx.spawn(async move |_this, cx| { - if needs_authorization? { - event_stream.authorize_tool_call(&tool_use).await?; - } - - event_stream.send_tool_call_update( - &tool_use.id, - acp::ToolCallUpdateFields { - status: Some(acp::ToolCallStatus::InProgress), - ..Default::default() - }, - ); - cx.update(|cx| tool.run(tool_use.input, cx))?.await + let tool_event_stream = ToolCallEventStream::new(tool_use.id, event_stream); + tool_event_stream.send_update(acp::ToolCallUpdateFields { + status: Some(acp::ToolCallStatus::InProgress), + ..Default::default() + }); + cx.update(|cx| tool.run(tool_use.input, tool_event_stream, cx))? + .await }) } @@ -584,7 +590,7 @@ impl Thread { name: tool_name, description: tool.description(cx).to_string(), input_schema: tool - .input_schema(LanguageModelToolSchemaFormat::JsonSchema) + .input_schema(self.selected_model.tool_input_format()) .log_err()?, }) }) @@ -651,9 +657,10 @@ pub trait AgentTool where Self: 'static + Sized, { - type Input: for<'de> Deserialize<'de> + JsonSchema; + type Input: for<'de> Deserialize<'de> + Serialize + JsonSchema; fn name(&self) -> SharedString; + fn description(&self, _cx: &mut App) -> SharedString { let schema = schemars::schema_for!(Self::Input); SharedString::new( @@ -664,17 +671,33 @@ where ) } + fn kind(&self) -> acp::ToolKind; + + /// The initial tool title to display. Can be updated during the tool run. + fn initial_title(&self, input: Self::Input) -> SharedString; + /// Returns the JSON schema that describes the tool's input. - fn input_schema(&self, _format: LanguageModelToolSchemaFormat) -> Schema { + fn input_schema(&self) -> Schema { schemars::schema_for!(Self::Input) } - /// Returns true if the tool needs the users's authorization - /// before running. - fn needs_authorization(&self, input: Self::Input, cx: &App) -> bool; + /// Allows the tool to authorize a given tool call with the user if necessary + fn authorize( + &self, + input: Self::Input, + event_stream: ToolCallEventStream, + ) -> impl use + Future> { + let json_input = serde_json::json!(&input); + event_stream.authorize(self.initial_title(input).into(), self.kind(), json_input) + } /// Runs the tool with the provided input. - fn run(self: Arc, input: Self::Input, cx: &mut App) -> Task>; + fn run( + self: Arc, + input: Self::Input, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task>; fn erase(self) -> Arc { Arc::new(Erased(Arc::new(self))) @@ -686,9 +709,15 @@ pub struct Erased(T); pub trait AnyAgentTool { fn name(&self) -> SharedString; fn description(&self, cx: &mut App) -> SharedString; + fn kind(&self) -> acp::ToolKind; + fn initial_title(&self, input: serde_json::Value) -> Result; fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result; - fn needs_authorization(&self, input: serde_json::Value, cx: &mut App) -> Result; - fn run(self: Arc, input: serde_json::Value, cx: &mut App) -> Task>; + fn run( + self: Arc, + input: serde_json::Value, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task>; } impl AnyAgentTool for Erased> @@ -703,22 +732,30 @@ where self.0.description(cx) } + fn kind(&self) -> agent_client_protocol::ToolKind { + self.0.kind() + } + + fn initial_title(&self, input: serde_json::Value) -> Result { + let parsed_input = serde_json::from_value(input)?; + Ok(self.0.initial_title(parsed_input)) + } + fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - Ok(serde_json::to_value(self.0.input_schema(format))?) + let mut json = serde_json::to_value(self.0.input_schema())?; + adapt_schema_to_format(&mut json, format)?; + Ok(json) } - fn needs_authorization(&self, input: serde_json::Value, cx: &mut App) -> Result { + fn run( + self: Arc, + input: serde_json::Value, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> { let parsed_input: Result = serde_json::from_value(input).map_err(Into::into); match parsed_input { - Ok(input) => Ok(self.0.needs_authorization(input, cx)), - Err(error) => Err(anyhow!(error)), - } - } - - fn run(self: Arc, input: serde_json::Value, cx: &mut App) -> Task> { - let parsed_input: Result = serde_json::from_value(input).map_err(Into::into); - match parsed_input { - Ok(input) => self.0.clone().run(input, cx), + Ok(input) => self.0.clone().run(input, event_stream, cx), Err(error) => Task::ready(Err(anyhow!(error))), } } @@ -744,21 +781,16 @@ impl AgentResponseEventStream { fn authorize_tool_call( &self, - tool_use: &LanguageModelToolUse, + id: &LanguageModelToolUseId, + title: String, + kind: acp::ToolKind, + input: serde_json::Value, ) -> impl use<> + Future> { let (response_tx, response_rx) = oneshot::channel(); self.0 .unbounded_send(Ok(AgentResponseEvent::ToolCallAuthorization( ToolCallAuthorization { - tool_call: acp::ToolCall { - id: acp::ToolCallId(tool_use.id.to_string().into()), - title: tool_use.name.to_string(), - kind: acp::ToolKind::Other, - status: acp::ToolCallStatus::Pending, - content: vec![], - locations: vec![], - raw_input: Some(tool_use.input.clone()), - }, + tool_call: Self::initial_tool_call(id, title, kind, input), options: vec![ acp::PermissionOption { id: acp::PermissionOptionId("always_allow".into()), @@ -788,20 +820,41 @@ impl AgentResponseEventStream { } } - fn send_tool_call(&self, tool_use: &LanguageModelToolUse) { + fn send_tool_call( + &self, + tool: Option<&Arc>, + tool_use: &LanguageModelToolUse, + ) { self.0 - .unbounded_send(Ok(AgentResponseEvent::ToolCall(acp::ToolCall { - id: acp::ToolCallId(tool_use.id.to_string().into()), - title: tool_use.name.to_string(), - kind: acp::ToolKind::Other, - status: acp::ToolCallStatus::Pending, - content: vec![], - locations: vec![], - raw_input: Some(tool_use.input.clone()), - }))) + .unbounded_send(Ok(AgentResponseEvent::ToolCall(Self::initial_tool_call( + &tool_use.id, + tool.and_then(|t| t.initial_title(tool_use.input.clone()).ok()) + .map(|i| i.into()) + .unwrap_or_else(|| tool_use.name.to_string()), + tool.map(|t| t.kind()).unwrap_or(acp::ToolKind::Other), + tool_use.input.clone(), + )))) .ok(); } + fn initial_tool_call( + id: &LanguageModelToolUseId, + title: String, + kind: acp::ToolKind, + input: serde_json::Value, + ) -> acp::ToolCall { + acp::ToolCall { + id: acp::ToolCallId(id.to_string().into()), + title, + kind, + status: acp::ToolCallStatus::Pending, + content: vec![], + locations: vec![], + raw_input: Some(input), + raw_output: None, + } + } + fn send_tool_call_update( &self, tool_use_id: &LanguageModelToolUseId, @@ -817,38 +870,6 @@ impl AgentResponseEventStream { .ok(); } - fn send_tool_call_result(&self, tool_result: &LanguageModelToolResult) { - let status = if tool_result.is_error { - acp::ToolCallStatus::Failed - } else { - acp::ToolCallStatus::Completed - }; - let content = match &tool_result.content { - LanguageModelToolResultContent::Text(text) => text.to_string().into(), - LanguageModelToolResultContent::Image(LanguageModelImage { source, .. }) => { - acp::ToolCallContent::Content { - content: acp::ContentBlock::Image(acp::ImageContent { - annotations: None, - data: source.to_string(), - mime_type: ImageFormat::Png.mime_type().to_string(), - }), - } - } - }; - self.0 - .unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate( - acp::ToolCallUpdate { - id: acp::ToolCallId(tool_result.tool_use_id.to_string().into()), - fields: acp::ToolCallUpdateFields { - status: Some(status), - content: Some(vec![content]), - ..Default::default() - }, - }, - ))) - .ok(); - } - fn send_stop(&self, reason: StopReason) { match reason { StopReason::EndTurn => { @@ -874,3 +895,32 @@ impl AgentResponseEventStream { self.0.unbounded_send(Err(error)).ok(); } } + +#[derive(Clone)] +pub struct ToolCallEventStream { + tool_use_id: LanguageModelToolUseId, + stream: AgentResponseEventStream, +} + +impl ToolCallEventStream { + fn new(tool_use_id: LanguageModelToolUseId, stream: AgentResponseEventStream) -> Self { + Self { + tool_use_id, + stream, + } + } + + pub fn send_update(&self, fields: acp::ToolCallUpdateFields) { + self.stream.send_tool_call_update(&self.tool_use_id, fields); + } + + pub fn authorize( + &self, + title: String, + kind: acp::ToolKind, + input: serde_json::Value, + ) -> impl use<> + Future> { + self.stream + .authorize_tool_call(&self.tool_use_id, title, kind, input) + } +} diff --git a/crates/agent2/src/tools.rs b/crates/agent2/src/tools.rs index cf3162abfa..848fe552ed 100644 --- a/crates/agent2/src/tools.rs +++ b/crates/agent2/src/tools.rs @@ -1 +1,5 @@ -mod glob; +mod find_path_tool; +mod thinking_tool; + +pub use find_path_tool::*; +pub use thinking_tool::*; diff --git a/crates/agent2/src/tools/find_path_tool.rs b/crates/agent2/src/tools/find_path_tool.rs new file mode 100644 index 0000000000..e840fec78c --- /dev/null +++ b/crates/agent2/src/tools/find_path_tool.rs @@ -0,0 +1,231 @@ +use agent_client_protocol as acp; +use anyhow::{anyhow, Result}; +use gpui::{App, AppContext, Entity, SharedString, Task}; +use project::Project; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::fmt::Write; +use std::{cmp, path::PathBuf, sync::Arc}; +use util::paths::PathMatcher; + +use crate::{AgentTool, ToolCallEventStream}; + +/// Fast file path pattern matching tool that works with any codebase size +/// +/// - Supports glob patterns like "**/*.js" or "src/**/*.ts" +/// - Returns matching file paths sorted alphabetically +/// - Prefer the `grep` tool to this tool when searching for symbols unless you have specific information about paths. +/// - Use this tool when you need to find files by name patterns +/// - Results are paginated with 50 matches per page. Use the optional 'offset' parameter to request subsequent pages. +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct FindPathToolInput { + /// The glob to match against every path in the project. + /// + /// + /// If the project has the following root directories: + /// + /// - directory1/a/something.txt + /// - directory2/a/things.txt + /// - directory3/a/other.txt + /// + /// You can get back the first two paths by providing a glob of "*thing*.txt" + /// + pub glob: String, + + /// Optional starting position for paginated results (0-based). + /// When not provided, starts from the beginning. + #[serde(default)] + pub offset: usize, +} + +#[derive(Debug, Serialize, Deserialize)] +struct FindPathToolOutput { + paths: Vec, +} + +const RESULTS_PER_PAGE: usize = 50; + +pub struct FindPathTool { + project: Entity, +} + +impl FindPathTool { + pub fn new(project: Entity) -> Self { + Self { project } + } +} + +impl AgentTool for FindPathTool { + type Input = FindPathToolInput; + + fn name(&self) -> SharedString { + "find_path".into() + } + + fn kind(&self) -> acp::ToolKind { + acp::ToolKind::Search + } + + fn initial_title(&self, input: Self::Input) -> SharedString { + format!("Find paths matching “`{}`”", input.glob).into() + } + + fn run( + self: Arc, + input: Self::Input, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> { + let search_paths_task = search_paths(&input.glob, self.project.clone(), cx); + + cx.background_spawn(async move { + let matches = search_paths_task.await?; + let paginated_matches: &[PathBuf] = &matches[cmp::min(input.offset, matches.len()) + ..cmp::min(input.offset + RESULTS_PER_PAGE, matches.len())]; + + event_stream.send_update(acp::ToolCallUpdateFields { + title: Some(if paginated_matches.len() == 0 { + "No matches".into() + } else if paginated_matches.len() == 1 { + "1 match".into() + } else { + format!("{} matches", paginated_matches.len()) + }), + content: Some( + paginated_matches + .iter() + .map(|path| acp::ToolCallContent::Content { + content: acp::ContentBlock::ResourceLink(acp::ResourceLink { + uri: format!("file://{}", path.display()), + name: path.to_string_lossy().into(), + annotations: None, + description: None, + mime_type: None, + size: None, + title: None, + }), + }) + .collect(), + ), + raw_output: Some(serde_json::json!({ + "paths": &matches, + })), + ..Default::default() + }); + + if matches.is_empty() { + Ok("No matches found".into()) + } else { + let mut message = format!("Found {} total matches.", matches.len()); + if matches.len() > RESULTS_PER_PAGE { + write!( + &mut message, + "\nShowing results {}-{} (provide 'offset' parameter for more results):", + input.offset + 1, + input.offset + paginated_matches.len() + ) + .unwrap(); + } + + for mat in matches.iter().skip(input.offset).take(RESULTS_PER_PAGE) { + write!(&mut message, "\n{}", mat.display()).unwrap(); + } + + Ok(message) + } + }) + } +} + +fn search_paths(glob: &str, project: Entity, cx: &mut App) -> Task>> { + let path_matcher = match PathMatcher::new([ + // Sometimes models try to search for "". In this case, return all paths in the project. + if glob.is_empty() { "*" } else { glob }, + ]) { + Ok(matcher) => matcher, + Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))), + }; + let snapshots: Vec<_> = project + .read(cx) + .worktrees(cx) + .map(|worktree| worktree.read(cx).snapshot()) + .collect(); + + cx.background_spawn(async move { + Ok(snapshots + .iter() + .flat_map(|snapshot| { + let root_name = PathBuf::from(snapshot.root_name()); + snapshot + .entries(false, 0) + .map(move |entry| root_name.join(&entry.path)) + .filter(|path| path_matcher.is_match(&path)) + }) + .collect()) + }) +} + +#[cfg(test)] +mod test { + use super::*; + use gpui::TestAppContext; + use project::{FakeFs, Project}; + use settings::SettingsStore; + use util::path; + + #[gpui::test] + async fn test_find_path_tool(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/root", + serde_json::json!({ + "apple": { + "banana": { + "carrot": "1", + }, + "bandana": { + "carbonara": "2", + }, + "endive": "3" + } + }), + ) + .await; + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + + let matches = cx + .update(|cx| search_paths("root/**/car*", project.clone(), cx)) + .await + .unwrap(); + assert_eq!( + matches, + &[ + PathBuf::from("root/apple/banana/carrot"), + PathBuf::from("root/apple/bandana/carbonara") + ] + ); + + let matches = cx + .update(|cx| search_paths("**/car*", project.clone(), cx)) + .await + .unwrap(); + assert_eq!( + matches, + &[ + PathBuf::from("root/apple/banana/carrot"), + PathBuf::from("root/apple/bandana/carbonara") + ] + ); + } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + Project::init_settings(cx); + }); + } +} diff --git a/crates/agent2/src/tools/glob.rs b/crates/agent2/src/tools/glob.rs deleted file mode 100644 index 4dace7c074..0000000000 --- a/crates/agent2/src/tools/glob.rs +++ /dev/null @@ -1,84 +0,0 @@ -use anyhow::{anyhow, Result}; -use gpui::{App, AppContext, Entity, SharedString, Task}; -use project::Project; -use schemars::JsonSchema; -use serde::Deserialize; -use std::{path::PathBuf, sync::Arc}; -use util::paths::PathMatcher; -use worktree::Snapshot as WorktreeSnapshot; - -use crate::{ - templates::{GlobTemplate, Template, Templates}, - thread::AgentTool, -}; - -// Description is dynamic, see `fn description` below -#[derive(Deserialize, JsonSchema)] -struct GlobInput { - /// A POSIX glob pattern - glob: SharedString, -} - -#[expect( - dead_code, - reason = "Marked as unused by Rust 1.89 and left as is as of 07 Aug 2025 to let AI team address it." -)] -struct GlobTool { - project: Entity, - templates: Arc, -} - -impl AgentTool for GlobTool { - type Input = GlobInput; - - fn name(&self) -> SharedString { - "glob".into() - } - - fn description(&self, cx: &mut App) -> SharedString { - let project_roots = self - .project - .read(cx) - .worktrees(cx) - .map(|worktree| worktree.read(cx).root_name().into()) - .collect::>() - .join("\n"); - - GlobTemplate { project_roots } - .render(&self.templates) - .expect("template failed to render") - .into() - } - - fn needs_authorization(&self, _input: Self::Input, _cx: &App) -> bool { - false - } - - fn run(self: Arc, input: Self::Input, cx: &mut App) -> Task> { - let path_matcher = match PathMatcher::new([&input.glob]) { - Ok(matcher) => matcher, - Err(error) => return Task::ready(Err(anyhow!(error))), - }; - - let snapshots: Vec = self - .project - .read(cx) - .worktrees(cx) - .map(|worktree| worktree.read(cx).snapshot()) - .collect(); - - cx.background_spawn(async move { - let paths = snapshots.iter().flat_map(|snapshot| { - let root_name = PathBuf::from(snapshot.root_name()); - snapshot - .entries(false, 0) - .map(move |entry| root_name.join(&entry.path)) - .filter(|path| path_matcher.is_match(&path)) - }); - let output = paths - .map(|path| format!("{}\n", path.display())) - .collect::(); - Ok(output) - }) - } -} diff --git a/crates/agent2/src/tools/thinking_tool.rs b/crates/agent2/src/tools/thinking_tool.rs new file mode 100644 index 0000000000..bb85d8eceb --- /dev/null +++ b/crates/agent2/src/tools/thinking_tool.rs @@ -0,0 +1,48 @@ +use agent_client_protocol as acp; +use anyhow::Result; +use gpui::{App, SharedString, Task}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +use crate::{AgentTool, ToolCallEventStream}; + +/// A tool for thinking through problems, brainstorming ideas, or planning without executing any actions. +/// Use this tool when you need to work through complex problems, develop strategies, or outline approaches before taking action. +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct ThinkingToolInput { + /// Content to think about. This should be a description of what to think about or + /// a problem to solve. + content: String, +} + +pub struct ThinkingTool; + +impl AgentTool for ThinkingTool { + type Input = ThinkingToolInput; + + fn name(&self) -> SharedString { + "thinking".into() + } + + fn kind(&self) -> acp::ToolKind { + acp::ToolKind::Think + } + + fn initial_title(&self, _input: Self::Input) -> SharedString { + "Thinking".into() + } + + fn run( + self: Arc, + input: Self::Input, + event_stream: ToolCallEventStream, + _cx: &mut App, + ) -> Task> { + event_stream.send_update(acp::ToolCallUpdateFields { + content: Some(vec![input.content.into()]), + ..Default::default() + }); + Task::ready(Ok("Finished thinking.".to_string())) + } +} diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index e676b7ee46..8d85435f92 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -280,6 +280,7 @@ fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) .map(into_new_tool_call_location) .collect(), raw_input: None, + raw_output: None, } } diff --git a/crates/agent_servers/src/claude/tools.rs b/crates/agent_servers/src/claude/tools.rs index 85b9a13642..7ca150c0bd 100644 --- a/crates/agent_servers/src/claude/tools.rs +++ b/crates/agent_servers/src/claude/tools.rs @@ -297,6 +297,7 @@ impl ClaudeTool { content: self.content(), locations: self.locations(), raw_input: None, + raw_output: None, } } } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index ff6da43299..3d1fbba45d 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2988,6 +2988,7 @@ mod tests { content: vec!["hi".into()], locations: vec![], raw_input: None, + raw_output: None, }; let connection = StubAgentConnection::new(vec![acp::SessionUpdate::ToolCall(tool_call)]) .with_permission_requests(HashMap::from_iter([( From d693f02c6341ffa7625e710a80167c1d53d321ba Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 7 Aug 2025 18:27:50 -0400 Subject: [PATCH 170/693] Settings: fix release channel settings not being respected (#35838) Typo in #35756 Release Notes: - N/A --- crates/settings/src/settings_store.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index fbc10f5860..bfdafbffe8 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -1203,7 +1203,7 @@ impl SettingsStore { global: global_settings.as_ref(), extensions: extension_settings.as_ref(), user: user_settings.as_ref(), - release_channel: os_settings.as_ref(), + release_channel: release_channel_settings.as_ref(), operating_system: os_settings.as_ref(), profile: profile_settings.as_ref(), server: server_settings.as_ref(), From d110459ef861fb2079ded3e39e086589f9a13bb1 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 7 Aug 2025 18:29:59 -0400 Subject: [PATCH 171/693] collab_ui: Show signed-out state when not connected to Collab (#35832) This PR updates signed-out state of the Collab panel to show when not connected to Collab, as opposed to just when the user is signed-out. Release Notes: - N/A --- crates/collab_ui/src/collab_panel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index bb7c2ba1cd..51e4ff8965 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -3053,7 +3053,7 @@ impl Render for CollabPanel { .on_action(cx.listener(CollabPanel::move_channel_down)) .track_focus(&self.focus_handle) .size_full() - .child(if self.user_store.read(cx).current_user().is_none() { + .child(if !self.client.status().borrow().is_connected() { self.render_signed_out(cx) } else { self.render_signed_in(window, cx) From 50482a6bc2c5274634f9f1f2446b05e425a142ad Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 7 Aug 2025 19:00:45 -0400 Subject: [PATCH 172/693] language_model: Refresh the LLM token upon receiving a `UserUpdated` message from Cloud (#35839) This PR makes it so we refresh the LLM token upon receiving a `UserUpdated` message from Cloud over the WebSocket connection. Release Notes: - N/A --- Cargo.lock | 1 + crates/client/src/client.rs | 4 +-- crates/language_model/Cargo.toml | 1 + .../language_model/src/model/cloud_model.rs | 34 +++++++++---------- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c1f1d00ba..39ee75f6dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9127,6 +9127,7 @@ dependencies = [ "anyhow", "base64 0.22.1", "client", + "cloud_api_types", "cloud_llm_client", "collections", "futures 0.3.31", diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 12ea4bcd3e..f09c012a85 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -193,7 +193,7 @@ pub fn init(client: &Arc, cx: &mut App) { }); } -pub type MessageToClientHandler = Box; +pub type MessageToClientHandler = Box; struct GlobalClient(Arc); @@ -1684,7 +1684,7 @@ impl Client { pub fn add_message_to_client_handler( self: &Arc, - handler: impl Fn(&MessageToClient, &App) + Send + Sync + 'static, + handler: impl Fn(&MessageToClient, &mut App) + Send + Sync + 'static, ) { self.message_to_client_handlers .lock() diff --git a/crates/language_model/Cargo.toml b/crates/language_model/Cargo.toml index 841be60b0e..f9920623b5 100644 --- a/crates/language_model/Cargo.toml +++ b/crates/language_model/Cargo.toml @@ -20,6 +20,7 @@ anthropic = { workspace = true, features = ["schemars"] } anyhow.workspace = true base64.workspace = true client.workspace = true +cloud_api_types.workspace = true cloud_llm_client.workspace = true collections.workspace = true futures.workspace = true diff --git a/crates/language_model/src/model/cloud_model.rs b/crates/language_model/src/model/cloud_model.rs index 8ae5893410..3b4c1fa269 100644 --- a/crates/language_model/src/model/cloud_model.rs +++ b/crates/language_model/src/model/cloud_model.rs @@ -3,11 +3,9 @@ use std::sync::Arc; use anyhow::Result; use client::Client; +use cloud_api_types::websocket_protocol::MessageToClient; use cloud_llm_client::Plan; -use gpui::{ - App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, ReadGlobal as _, -}; -use proto::TypedEnvelope; +use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, ReadGlobal as _}; use smol::lock::{RwLock, RwLockUpgradableReadGuard, RwLockWriteGuard}; use thiserror::Error; @@ -82,9 +80,7 @@ impl Global for GlobalRefreshLlmTokenListener {} pub struct RefreshLlmTokenEvent; -pub struct RefreshLlmTokenListener { - _llm_token_subscription: client::Subscription, -} +pub struct RefreshLlmTokenListener; impl EventEmitter for RefreshLlmTokenListener {} @@ -99,17 +95,21 @@ impl RefreshLlmTokenListener { } fn new(client: Arc, cx: &mut Context) -> Self { - Self { - _llm_token_subscription: client - .add_message_handler(cx.weak_entity(), Self::handle_refresh_llm_token), - } + client.add_message_to_client_handler({ + let this = cx.entity(); + move |message, cx| { + Self::handle_refresh_llm_token(this.clone(), message, cx); + } + }); + + Self } - async fn handle_refresh_llm_token( - this: Entity, - _: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |_this, cx| cx.emit(RefreshLlmTokenEvent)) + fn handle_refresh_llm_token(this: Entity, message: &MessageToClient, cx: &mut App) { + match message { + MessageToClient::UserUpdated => { + this.update(cx, |_this, cx| cx.emit(RefreshLlmTokenEvent)); + } + } } } From 913e9adf90aef94071059a5ed2c7f635c0a37e92 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 7 Aug 2025 16:07:33 -0700 Subject: [PATCH 173/693] Move timing fields into span (#35833) Release Notes: - N/A --- crates/collab/src/rpc.rs | 256 +++++++++++++++++++++++---------------- crates/rpc/src/peer.rs | 15 --- 2 files changed, 149 insertions(+), 122 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index b3603d2619..ec1105b138 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -41,9 +41,11 @@ use chrono::Utc; use collections::{HashMap, HashSet}; pub use connection_pool::{ConnectionPool, ZedVersion}; use core::fmt::{self, Debug, Formatter}; +use futures::TryFutureExt as _; use reqwest_client::ReqwestClient; use rpc::proto::{MultiLspQuery, split_repository_update}; use supermaven_api::{CreateExternalUserRequest, SupermavenAdminApi}; +use tracing::Span; use futures::{ FutureExt, SinkExt, StreamExt, TryStreamExt, channel::oneshot, future::BoxFuture, @@ -94,8 +96,13 @@ const MAX_CONCURRENT_CONNECTIONS: usize = 512; static CONCURRENT_CONNECTIONS: AtomicUsize = AtomicUsize::new(0); +const TOTAL_DURATION_MS: &str = "total_duration_ms"; +const PROCESSING_DURATION_MS: &str = "processing_duration_ms"; +const QUEUE_DURATION_MS: &str = "queue_duration_ms"; +const HOST_WAITING_MS: &str = "host_waiting_ms"; + type MessageHandler = - Box, Session) -> BoxFuture<'static, ()>>; + Box, Session, Span) -> BoxFuture<'static, ()>>; pub struct ConnectionGuard; @@ -163,6 +170,42 @@ impl Principal { } } +#[derive(Clone)] +struct MessageContext { + session: Session, + span: tracing::Span, +} + +impl Deref for MessageContext { + type Target = Session; + + fn deref(&self) -> &Self::Target { + &self.session + } +} + +impl MessageContext { + pub fn forward_request( + &self, + receiver_id: ConnectionId, + request: T, + ) -> impl Future> { + let request_start_time = Instant::now(); + let span = self.span.clone(); + tracing::info!("start forwarding request"); + self.peer + .forward_request(self.connection_id, receiver_id, request) + .inspect(move |_| { + span.record( + HOST_WAITING_MS, + request_start_time.elapsed().as_micros() as f64 / 1000.0, + ); + }) + .inspect_err(|_| tracing::error!("error forwarding request")) + .inspect_ok(|_| tracing::info!("finished forwarding request")) + } +} + #[derive(Clone)] struct Session { principal: Principal, @@ -646,40 +689,37 @@ impl Server { fn add_handler(&mut self, handler: F) -> &mut Self where - F: 'static + Send + Sync + Fn(TypedEnvelope, Session) -> Fut, + F: 'static + Send + Sync + Fn(TypedEnvelope, MessageContext) -> Fut, Fut: 'static + Send + Future>, M: EnvelopedMessage, { let prev_handler = self.handlers.insert( TypeId::of::(), - Box::new(move |envelope, session| { + Box::new(move |envelope, session, span| { let envelope = envelope.into_any().downcast::>().unwrap(); let received_at = envelope.received_at; tracing::info!("message received"); let start_time = Instant::now(); - let future = (handler)(*envelope, session); + let future = (handler)( + *envelope, + MessageContext { + session, + span: span.clone(), + }, + ); async move { let result = future.await; let total_duration_ms = received_at.elapsed().as_micros() as f64 / 1000.0; let processing_duration_ms = start_time.elapsed().as_micros() as f64 / 1000.0; let queue_duration_ms = total_duration_ms - processing_duration_ms; - + span.record(TOTAL_DURATION_MS, total_duration_ms); + span.record(PROCESSING_DURATION_MS, processing_duration_ms); + span.record(QUEUE_DURATION_MS, queue_duration_ms); match result { Err(error) => { - tracing::error!( - ?error, - total_duration_ms, - processing_duration_ms, - queue_duration_ms, - "error handling message" - ) + tracing::error!(?error, "error handling message") } - Ok(()) => tracing::info!( - total_duration_ms, - processing_duration_ms, - queue_duration_ms, - "finished handling message" - ), + Ok(()) => tracing::info!("finished handling message"), } } .boxed() @@ -693,7 +733,7 @@ impl Server { fn add_message_handler(&mut self, handler: F) -> &mut Self where - F: 'static + Send + Sync + Fn(M, Session) -> Fut, + F: 'static + Send + Sync + Fn(M, MessageContext) -> Fut, Fut: 'static + Send + Future>, M: EnvelopedMessage, { @@ -703,7 +743,7 @@ impl Server { fn add_request_handler(&mut self, handler: F) -> &mut Self where - F: 'static + Send + Sync + Fn(M, Response, Session) -> Fut, + F: 'static + Send + Sync + Fn(M, Response, MessageContext) -> Fut, Fut: Send + Future>, M: RequestMessage, { @@ -889,12 +929,16 @@ impl Server { login=field::Empty, impersonator=field::Empty, multi_lsp_query_request=field::Empty, + { TOTAL_DURATION_MS }=field::Empty, + { PROCESSING_DURATION_MS }=field::Empty, + { QUEUE_DURATION_MS }=field::Empty, + { HOST_WAITING_MS }=field::Empty ); principal.update_span(&span); let span_enter = span.enter(); if let Some(handler) = this.handlers.get(&message.payload_type_id()) { let is_background = message.is_background(); - let handle_message = (handler)(message, session.clone()); + let handle_message = (handler)(message, session.clone(), span.clone()); drop(span_enter); let handle_message = async move { @@ -1386,7 +1430,11 @@ async fn connection_lost( } /// Acknowledges a ping from a client, used to keep the connection alive. -async fn ping(_: proto::Ping, response: Response, _session: Session) -> Result<()> { +async fn ping( + _: proto::Ping, + response: Response, + _session: MessageContext, +) -> Result<()> { response.send(proto::Ack {})?; Ok(()) } @@ -1395,7 +1443,7 @@ async fn ping(_: proto::Ping, response: Response, _session: Session async fn create_room( _request: proto::CreateRoom, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let livekit_room = nanoid::nanoid!(30); @@ -1435,7 +1483,7 @@ async fn create_room( async fn join_room( request: proto::JoinRoom, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let room_id = RoomId::from_proto(request.id); @@ -1502,7 +1550,7 @@ async fn join_room( async fn rejoin_room( request: proto::RejoinRoom, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let room; let channel; @@ -1679,7 +1727,7 @@ fn notify_rejoined_projects( async fn leave_room( _: proto::LeaveRoom, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { leave_room_for_session(&session, session.connection_id).await?; response.send(proto::Ack {})?; @@ -1690,7 +1738,7 @@ async fn leave_room( async fn set_room_participant_role( request: proto::SetRoomParticipantRole, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let user_id = UserId::from_proto(request.user_id); let role = ChannelRole::from(request.role()); @@ -1738,7 +1786,7 @@ async fn set_room_participant_role( async fn call( request: proto::Call, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let room_id = RoomId::from_proto(request.room_id); let calling_user_id = session.user_id(); @@ -1807,7 +1855,7 @@ async fn call( async fn cancel_call( request: proto::CancelCall, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let called_user_id = UserId::from_proto(request.called_user_id); let room_id = RoomId::from_proto(request.room_id); @@ -1842,7 +1890,7 @@ async fn cancel_call( } /// Decline an incoming call. -async fn decline_call(message: proto::DeclineCall, session: Session) -> Result<()> { +async fn decline_call(message: proto::DeclineCall, session: MessageContext) -> Result<()> { let room_id = RoomId::from_proto(message.room_id); { let room = session @@ -1877,7 +1925,7 @@ async fn decline_call(message: proto::DeclineCall, session: Session) -> Result<( async fn update_participant_location( request: proto::UpdateParticipantLocation, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let room_id = RoomId::from_proto(request.room_id); let location = request.location.context("invalid location")?; @@ -1896,7 +1944,7 @@ async fn update_participant_location( async fn share_project( request: proto::ShareProject, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let (project_id, room) = &*session .db() @@ -1917,7 +1965,7 @@ async fn share_project( } /// Unshare a project from the room. -async fn unshare_project(message: proto::UnshareProject, session: Session) -> Result<()> { +async fn unshare_project(message: proto::UnshareProject, session: MessageContext) -> Result<()> { let project_id = ProjectId::from_proto(message.project_id); unshare_project_internal(project_id, session.connection_id, &session).await } @@ -1964,7 +2012,7 @@ async fn unshare_project_internal( async fn join_project( request: proto::JoinProject, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let project_id = ProjectId::from_proto(request.project_id); @@ -2111,7 +2159,7 @@ async fn join_project( } /// Leave someone elses shared project. -async fn leave_project(request: proto::LeaveProject, session: Session) -> Result<()> { +async fn leave_project(request: proto::LeaveProject, session: MessageContext) -> Result<()> { let sender_id = session.connection_id; let project_id = ProjectId::from_proto(request.project_id); let db = session.db().await; @@ -2134,7 +2182,7 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result async fn update_project( request: proto::UpdateProject, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let project_id = ProjectId::from_proto(request.project_id); let (room, guest_connection_ids) = &*session @@ -2163,7 +2211,7 @@ async fn update_project( async fn update_worktree( request: proto::UpdateWorktree, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let guest_connection_ids = session .db() @@ -2187,7 +2235,7 @@ async fn update_worktree( async fn update_repository( request: proto::UpdateRepository, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let guest_connection_ids = session .db() @@ -2211,7 +2259,7 @@ async fn update_repository( async fn remove_repository( request: proto::RemoveRepository, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let guest_connection_ids = session .db() @@ -2235,7 +2283,7 @@ async fn remove_repository( /// Updates other participants with changes to the diagnostics async fn update_diagnostic_summary( message: proto::UpdateDiagnosticSummary, - session: Session, + session: MessageContext, ) -> Result<()> { let guest_connection_ids = session .db() @@ -2259,7 +2307,7 @@ async fn update_diagnostic_summary( /// Updates other participants with changes to the worktree settings async fn update_worktree_settings( message: proto::UpdateWorktreeSettings, - session: Session, + session: MessageContext, ) -> Result<()> { let guest_connection_ids = session .db() @@ -2283,7 +2331,7 @@ async fn update_worktree_settings( /// Notify other participants that a language server has started. async fn start_language_server( request: proto::StartLanguageServer, - session: Session, + session: MessageContext, ) -> Result<()> { let guest_connection_ids = session .db() @@ -2306,7 +2354,7 @@ async fn start_language_server( /// Notify other participants that a language server has changed. async fn update_language_server( request: proto::UpdateLanguageServer, - session: Session, + session: MessageContext, ) -> Result<()> { let project_id = ProjectId::from_proto(request.project_id); let db = session.db().await; @@ -2339,7 +2387,7 @@ async fn update_language_server( async fn forward_read_only_project_request( request: T, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> where T: EntityMessage + RequestMessage, @@ -2350,10 +2398,7 @@ where .await .host_for_read_only_project_request(project_id, session.connection_id) .await?; - let payload = session - .peer - .forward_request(session.connection_id, host_connection_id, request) - .await?; + let payload = session.forward_request(host_connection_id, request).await?; response.send(payload)?; Ok(()) } @@ -2363,7 +2408,7 @@ where async fn forward_mutating_project_request( request: T, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> where T: EntityMessage + RequestMessage, @@ -2375,10 +2420,7 @@ where .await .host_for_mutating_project_request(project_id, session.connection_id) .await?; - let payload = session - .peer - .forward_request(session.connection_id, host_connection_id, request) - .await?; + let payload = session.forward_request(host_connection_id, request).await?; response.send(payload)?; Ok(()) } @@ -2386,7 +2428,7 @@ where async fn multi_lsp_query( request: MultiLspQuery, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { tracing::Span::current().record("multi_lsp_query_request", request.request_str()); tracing::info!("multi_lsp_query message received"); @@ -2396,7 +2438,7 @@ async fn multi_lsp_query( /// Notify other participants that a new buffer has been created async fn create_buffer_for_peer( request: proto::CreateBufferForPeer, - session: Session, + session: MessageContext, ) -> Result<()> { session .db() @@ -2418,7 +2460,7 @@ async fn create_buffer_for_peer( async fn update_buffer( request: proto::UpdateBuffer, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let project_id = ProjectId::from_proto(request.project_id); let mut capability = Capability::ReadOnly; @@ -2453,17 +2495,14 @@ async fn update_buffer( }; if host != session.connection_id { - session - .peer - .forward_request(session.connection_id, host, request.clone()) - .await?; + session.forward_request(host, request.clone()).await?; } response.send(proto::Ack {})?; Ok(()) } -async fn update_context(message: proto::UpdateContext, session: Session) -> Result<()> { +async fn update_context(message: proto::UpdateContext, session: MessageContext) -> Result<()> { let project_id = ProjectId::from_proto(message.project_id); let operation = message.operation.as_ref().context("invalid operation")?; @@ -2508,7 +2547,7 @@ async fn update_context(message: proto::UpdateContext, session: Session) -> Resu /// Notify other participants that a project has been updated. async fn broadcast_project_message_from_host>( request: T, - session: Session, + session: MessageContext, ) -> Result<()> { let project_id = ProjectId::from_proto(request.remote_entity_id()); let project_connection_ids = session @@ -2533,7 +2572,7 @@ async fn broadcast_project_message_from_host, - session: Session, + session: MessageContext, ) -> Result<()> { let room_id = RoomId::from_proto(request.room_id); let project_id = request.project_id.map(ProjectId::from_proto); @@ -2546,10 +2585,7 @@ async fn follow( .check_room_participants(room_id, leader_id, session.connection_id) .await?; - let response_payload = session - .peer - .forward_request(session.connection_id, leader_id, request) - .await?; + let response_payload = session.forward_request(leader_id, request).await?; response.send(response_payload)?; if let Some(project_id) = project_id { @@ -2565,7 +2601,7 @@ async fn follow( } /// Stop following another user in a call. -async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> { +async fn unfollow(request: proto::Unfollow, session: MessageContext) -> Result<()> { let room_id = RoomId::from_proto(request.room_id); let project_id = request.project_id.map(ProjectId::from_proto); let leader_id = request.leader_id.context("invalid leader id")?.into(); @@ -2594,7 +2630,7 @@ async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> { } /// Notify everyone following you of your current location. -async fn update_followers(request: proto::UpdateFollowers, session: Session) -> Result<()> { +async fn update_followers(request: proto::UpdateFollowers, session: MessageContext) -> Result<()> { let room_id = RoomId::from_proto(request.room_id); let database = session.db.lock().await; @@ -2629,7 +2665,7 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) -> async fn get_users( request: proto::GetUsers, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let user_ids = request .user_ids @@ -2657,7 +2693,7 @@ async fn get_users( async fn fuzzy_search_users( request: proto::FuzzySearchUsers, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let query = request.query; let users = match query.len() { @@ -2689,7 +2725,7 @@ async fn fuzzy_search_users( async fn request_contact( request: proto::RequestContact, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let requester_id = session.user_id(); let responder_id = UserId::from_proto(request.responder_id); @@ -2736,7 +2772,7 @@ async fn request_contact( async fn respond_to_contact_request( request: proto::RespondToContactRequest, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let responder_id = session.user_id(); let requester_id = UserId::from_proto(request.requester_id); @@ -2794,7 +2830,7 @@ async fn respond_to_contact_request( async fn remove_contact( request: proto::RemoveContact, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let requester_id = session.user_id(); let responder_id = UserId::from_proto(request.user_id); @@ -3053,7 +3089,10 @@ async fn update_user_plan(session: &Session) -> Result<()> { Ok(()) } -async fn subscribe_to_channels(_: proto::SubscribeToChannels, session: Session) -> Result<()> { +async fn subscribe_to_channels( + _: proto::SubscribeToChannels, + session: MessageContext, +) -> Result<()> { subscribe_user_to_channels(session.user_id(), &session).await?; Ok(()) } @@ -3079,7 +3118,7 @@ async fn subscribe_user_to_channels(user_id: UserId, session: &Session) -> Resul async fn create_channel( request: proto::CreateChannel, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let db = session.db().await; @@ -3134,7 +3173,7 @@ async fn create_channel( async fn delete_channel( request: proto::DeleteChannel, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let db = session.db().await; @@ -3162,7 +3201,7 @@ async fn delete_channel( async fn invite_channel_member( request: proto::InviteChannelMember, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); @@ -3199,7 +3238,7 @@ async fn invite_channel_member( async fn remove_channel_member( request: proto::RemoveChannelMember, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); @@ -3243,7 +3282,7 @@ async fn remove_channel_member( async fn set_channel_visibility( request: proto::SetChannelVisibility, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); @@ -3288,7 +3327,7 @@ async fn set_channel_visibility( async fn set_channel_member_role( request: proto::SetChannelMemberRole, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); @@ -3336,7 +3375,7 @@ async fn set_channel_member_role( async fn rename_channel( request: proto::RenameChannel, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); @@ -3368,7 +3407,7 @@ async fn rename_channel( async fn move_channel( request: proto::MoveChannel, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let channel_id = ChannelId::from_proto(request.channel_id); let to = ChannelId::from_proto(request.to); @@ -3410,7 +3449,7 @@ async fn move_channel( async fn reorder_channel( request: proto::ReorderChannel, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let channel_id = ChannelId::from_proto(request.channel_id); let direction = request.direction(); @@ -3456,7 +3495,7 @@ async fn reorder_channel( async fn get_channel_members( request: proto::GetChannelMembers, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); @@ -3476,7 +3515,7 @@ async fn get_channel_members( async fn respond_to_channel_invite( request: proto::RespondToChannelInvite, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); @@ -3517,7 +3556,7 @@ async fn respond_to_channel_invite( async fn join_channel( request: proto::JoinChannel, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let channel_id = ChannelId::from_proto(request.channel_id); join_channel_internal(channel_id, Box::new(response), session).await @@ -3540,7 +3579,7 @@ impl JoinChannelInternalResponse for Response { async fn join_channel_internal( channel_id: ChannelId, response: Box, - session: Session, + session: MessageContext, ) -> Result<()> { let joined_room = { let mut db = session.db().await; @@ -3635,7 +3674,7 @@ async fn join_channel_internal( async fn join_channel_buffer( request: proto::JoinChannelBuffer, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); @@ -3666,7 +3705,7 @@ async fn join_channel_buffer( /// Edit the channel notes async fn update_channel_buffer( request: proto::UpdateChannelBuffer, - session: Session, + session: MessageContext, ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); @@ -3718,7 +3757,7 @@ async fn update_channel_buffer( async fn rejoin_channel_buffers( request: proto::RejoinChannelBuffers, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let db = session.db().await; let buffers = db @@ -3753,7 +3792,7 @@ async fn rejoin_channel_buffers( async fn leave_channel_buffer( request: proto::LeaveChannelBuffer, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); @@ -3815,7 +3854,7 @@ fn send_notifications( async fn send_channel_message( request: proto::SendChannelMessage, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { // Validate the message body. let body = request.body.trim().to_string(); @@ -3908,7 +3947,7 @@ async fn send_channel_message( async fn remove_channel_message( request: proto::RemoveChannelMessage, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let channel_id = ChannelId::from_proto(request.channel_id); let message_id = MessageId::from_proto(request.message_id); @@ -3943,7 +3982,7 @@ async fn remove_channel_message( async fn update_channel_message( request: proto::UpdateChannelMessage, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let channel_id = ChannelId::from_proto(request.channel_id); let message_id = MessageId::from_proto(request.message_id); @@ -4027,7 +4066,7 @@ async fn update_channel_message( /// Mark a channel message as read async fn acknowledge_channel_message( request: proto::AckChannelMessage, - session: Session, + session: MessageContext, ) -> Result<()> { let channel_id = ChannelId::from_proto(request.channel_id); let message_id = MessageId::from_proto(request.message_id); @@ -4047,7 +4086,7 @@ async fn acknowledge_channel_message( /// Mark a buffer version as synced async fn acknowledge_buffer_version( request: proto::AckBufferOperation, - session: Session, + session: MessageContext, ) -> Result<()> { let buffer_id = BufferId::from_proto(request.buffer_id); session @@ -4067,7 +4106,7 @@ async fn acknowledge_buffer_version( async fn get_supermaven_api_key( _request: proto::GetSupermavenApiKey, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let user_id: String = session.user_id().to_string(); if !session.is_staff() { @@ -4096,7 +4135,7 @@ async fn get_supermaven_api_key( async fn join_channel_chat( request: proto::JoinChannelChat, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let channel_id = ChannelId::from_proto(request.channel_id); @@ -4114,7 +4153,10 @@ async fn join_channel_chat( } /// Stop receiving chat updates for a channel -async fn leave_channel_chat(request: proto::LeaveChannelChat, session: Session) -> Result<()> { +async fn leave_channel_chat( + request: proto::LeaveChannelChat, + session: MessageContext, +) -> Result<()> { let channel_id = ChannelId::from_proto(request.channel_id); session .db() @@ -4128,7 +4170,7 @@ async fn leave_channel_chat(request: proto::LeaveChannelChat, session: Session) async fn get_channel_messages( request: proto::GetChannelMessages, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let channel_id = ChannelId::from_proto(request.channel_id); let messages = session @@ -4152,7 +4194,7 @@ async fn get_channel_messages( async fn get_channel_messages_by_id( request: proto::GetChannelMessagesById, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let message_ids = request .message_ids @@ -4175,7 +4217,7 @@ async fn get_channel_messages_by_id( async fn get_notifications( request: proto::GetNotifications, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let notifications = session .db() @@ -4197,7 +4239,7 @@ async fn get_notifications( async fn mark_notification_as_read( request: proto::MarkNotificationRead, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let database = &session.db().await; let notifications = database @@ -4219,7 +4261,7 @@ async fn mark_notification_as_read( async fn get_private_user_info( _request: proto::GetPrivateUserInfo, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let db = session.db().await; @@ -4243,7 +4285,7 @@ async fn get_private_user_info( async fn accept_terms_of_service( _request: proto::AcceptTermsOfService, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let db = session.db().await; @@ -4267,7 +4309,7 @@ async fn accept_terms_of_service( async fn get_llm_api_token( _request: proto::GetLlmToken, response: Response, - session: Session, + session: MessageContext, ) -> Result<()> { let db = session.db().await; diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 8ddebfb269..80a104641f 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -422,23 +422,8 @@ impl Peer { receiver_id: ConnectionId, request: T, ) -> impl Future> { - let request_start_time = Instant::now(); - let elapsed_time = move || request_start_time.elapsed().as_millis(); - tracing::info!("start forwarding request"); self.request_internal(Some(sender_id), receiver_id, request) .map_ok(|envelope| envelope.payload) - .inspect_err(move |_| { - tracing::error!( - waiting_for_host_ms = elapsed_time(), - "error forwarding request" - ) - }) - .inspect_ok(move |_| { - tracing::info!( - waiting_for_host_ms = elapsed_time(), - "finished forwarding request" - ) - }) } fn request_internal( From 952e3713d7f2675c45cd99aa65bf0e61cb556168 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Thu, 7 Aug 2025 19:16:25 -0400 Subject: [PATCH 174/693] ci: Switch to Namespace (#35835) Follow-up to: - https://github.com/zed-industries/zed/pull/35826 Release Notes: - N/A --- .github/actionlint.yml | 27 ++++++++++-------------- .github/workflows/bump_patch_version.yml | 2 +- .github/workflows/ci.yml | 15 ++++++------- .github/workflows/deploy_cloudflare.yml | 2 +- .github/workflows/deploy_collab.yml | 4 ++-- .github/workflows/eval.yml | 2 +- .github/workflows/nix.yml | 2 +- .github/workflows/randomized_tests.yml | 2 +- .github/workflows/release_nightly.yml | 5 ++--- .github/workflows/unit_evals.yml | 2 +- 10 files changed, 28 insertions(+), 35 deletions(-) diff --git a/.github/actionlint.yml b/.github/actionlint.yml index 06b48b9b54..ad09545902 100644 --- a/.github/actionlint.yml +++ b/.github/actionlint.yml @@ -13,22 +13,17 @@ self-hosted-runner: - windows-2025-16 - windows-2025-32 - windows-2025-64 - # Buildjet Ubuntu 20.04 - AMD x86_64 - - buildjet-2vcpu-ubuntu-2004 - - buildjet-4vcpu-ubuntu-2004 - - buildjet-8vcpu-ubuntu-2004 - - buildjet-16vcpu-ubuntu-2004 - - buildjet-32vcpu-ubuntu-2004 - # Buildjet Ubuntu 22.04 - AMD x86_64 - - buildjet-2vcpu-ubuntu-2204 - - buildjet-4vcpu-ubuntu-2204 - - buildjet-8vcpu-ubuntu-2204 - - buildjet-16vcpu-ubuntu-2204 - - buildjet-32vcpu-ubuntu-2204 - # Buildjet Ubuntu 22.04 - Graviton aarch64 - - buildjet-8vcpu-ubuntu-2204-arm - - buildjet-16vcpu-ubuntu-2204-arm - - buildjet-32vcpu-ubuntu-2204-arm + # Namespace Ubuntu 20.04 (Release builds) + - namespace-profile-16x32-ubuntu-2004 + - namespace-profile-32x64-ubuntu-2004 + - namespace-profile-16x32-ubuntu-2004-arm + - namespace-profile-32x64-ubuntu-2004-arm + # Namespace Ubuntu 22.04 (Everything else) + - namespace-profile-2x4-ubuntu-2204 + - namespace-profile-4x8-ubuntu-2204 + - namespace-profile-8x16-ubuntu-2204 + - namespace-profile-16x32-ubuntu-2204 + - namespace-profile-32x64-ubuntu-2204 # Self Hosted Runners - self-mini-macos - self-32vcpu-windows-2022 diff --git a/.github/workflows/bump_patch_version.yml b/.github/workflows/bump_patch_version.yml index bc44066ea6..bfaf7a271b 100644 --- a/.github/workflows/bump_patch_version.yml +++ b/.github/workflows/bump_patch_version.yml @@ -16,7 +16,7 @@ jobs: bump_patch_version: if: github.repository_owner == 'zed-industries' runs-on: - - github-16vcpu-ubuntu-2204 + - namespace-profile-16x32-ubuntu-2204 steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f4f4d2b11..84907351fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -137,7 +137,7 @@ jobs: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_tests == 'true' runs-on: - - github-8vcpu-ubuntu-2204 + - namespace-profile-8x16-ubuntu-2204 steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -168,7 +168,7 @@ jobs: needs: [job_spec] if: github.repository_owner == 'zed-industries' runs-on: - - github-8vcpu-ubuntu-2204 + - namespace-profile-4x8-ubuntu-2204 steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -221,7 +221,7 @@ jobs: github.repository_owner == 'zed-industries' && (needs.job_spec.outputs.run_tests == 'true' || needs.job_spec.outputs.run_docs == 'true') runs-on: - - github-8vcpu-ubuntu-2204 + - namespace-profile-8x16-ubuntu-2204 steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -328,7 +328,7 @@ jobs: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_tests == 'true' runs-on: - - github-16vcpu-ubuntu-2204 + - namespace-profile-16x32-ubuntu-2204 steps: - name: Add Rust to the PATH run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" @@ -380,7 +380,7 @@ jobs: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_tests == 'true' runs-on: - - github-8vcpu-ubuntu-2204 + - namespace-profile-16x32-ubuntu-2204 steps: - name: Add Rust to the PATH run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" @@ -597,8 +597,7 @@ jobs: timeout-minutes: 60 name: Linux x86_x64 release bundle runs-on: - - github-16vcpu-ubuntu-2204 - # - buildjet-16vcpu-ubuntu-2004 # ubuntu 20.04 for minimal glibc + - namespace-profile-16x32-ubuntu-2004 # ubuntu 20.04 for minimal glibc if: | startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') @@ -651,7 +650,7 @@ jobs: timeout-minutes: 60 name: Linux arm64 release bundle runs-on: - - github-16vcpu-ubuntu-2204-arm + - namespace-profile-32x64-ubuntu-2004-arm # ubuntu 20.04 for minimal glibc if: | startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') diff --git a/.github/workflows/deploy_cloudflare.yml b/.github/workflows/deploy_cloudflare.yml index 3a294fdc17..df35d44ca9 100644 --- a/.github/workflows/deploy_cloudflare.yml +++ b/.github/workflows/deploy_cloudflare.yml @@ -9,7 +9,7 @@ jobs: deploy-docs: name: Deploy Docs if: github.repository_owner == 'zed-industries' - runs-on: github-16vcpu-ubuntu-2204 + runs-on: namespace-profile-16x32-ubuntu-2204 steps: - name: Checkout repo diff --git a/.github/workflows/deploy_collab.yml b/.github/workflows/deploy_collab.yml index d1a68a6280..ff2a3589e4 100644 --- a/.github/workflows/deploy_collab.yml +++ b/.github/workflows/deploy_collab.yml @@ -61,7 +61,7 @@ jobs: - style - tests runs-on: - - github-16vcpu-ubuntu-2204 + - namespace-profile-16x32-ubuntu-2204 steps: - name: Install doctl uses: digitalocean/action-doctl@v2 @@ -94,7 +94,7 @@ jobs: needs: - publish runs-on: - - github-16vcpu-ubuntu-2204 + - namespace-profile-16x32-ubuntu-2204 steps: - name: Checkout repo diff --git a/.github/workflows/eval.yml b/.github/workflows/eval.yml index 196e00519b..b5da9e7b7c 100644 --- a/.github/workflows/eval.yml +++ b/.github/workflows/eval.yml @@ -32,7 +32,7 @@ jobs: github.repository_owner == 'zed-industries' && (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-eval')) runs-on: - - github-16vcpu-ubuntu-2204 + - namespace-profile-16x32-ubuntu-2204 steps: - name: Add Rust to the PATH run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 913d6cfe9f..e682ce5890 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -20,7 +20,7 @@ jobs: matrix: system: - os: x86 Linux - runner: github-16vcpu-ubuntu-2204 + runner: namespace-profile-16x32-ubuntu-2204 install_nix: true - os: arm Mac runner: [macOS, ARM64, test] diff --git a/.github/workflows/randomized_tests.yml b/.github/workflows/randomized_tests.yml index 3a7b476ba0..de96c3df78 100644 --- a/.github/workflows/randomized_tests.yml +++ b/.github/workflows/randomized_tests.yml @@ -20,7 +20,7 @@ jobs: name: Run randomized tests if: github.repository_owner == 'zed-industries' runs-on: - - github-16vcpu-ubuntu-2204 + - namespace-profile-16x32-ubuntu-2204 steps: - name: Install Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index c5be72fca2..b3500a085b 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -128,8 +128,7 @@ jobs: name: Create a Linux *.tar.gz bundle for x86 if: github.repository_owner == 'zed-industries' runs-on: - - github-16vcpu-ubuntu-2204 - # - buildjet-16vcpu-ubuntu-2004 + - namespace-profile-16x32-ubuntu-2004 # ubuntu 20.04 for minimal glibc needs: tests steps: - name: Checkout repo @@ -169,7 +168,7 @@ jobs: name: Create a Linux *.tar.gz bundle for ARM if: github.repository_owner == 'zed-industries' runs-on: - - github-16vcpu-ubuntu-2204-arm + - namespace-profile-32x64-ubuntu-2004-arm # ubuntu 20.04 for minimal glibc needs: tests steps: - name: Checkout repo diff --git a/.github/workflows/unit_evals.yml b/.github/workflows/unit_evals.yml index 225fca558f..2e03fb028f 100644 --- a/.github/workflows/unit_evals.yml +++ b/.github/workflows/unit_evals.yml @@ -23,7 +23,7 @@ jobs: timeout-minutes: 60 name: Run unit evals runs-on: - - github-16vcpu-ubuntu-2204 + - namespace-profile-16x32-ubuntu-2204 steps: - name: Add Rust to the PATH run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" From 6912dc8399148dd0caf951ce0bba711de7279f01 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Thu, 7 Aug 2025 20:26:19 -0300 Subject: [PATCH 175/693] Fix CC tool state on cancel (#35763) When we stop the generation, CC tells us the tool completed, but it was actually cancelled. Release Notes: - N/A --- crates/agent_servers/src/claude.rs | 148 +++++++++++++++----------- crates/agent_servers/src/e2e_tests.rs | 7 +- 2 files changed, 91 insertions(+), 64 deletions(-) diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 09d08fdcf8..c65508f152 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -6,7 +6,7 @@ use context_server::listener::McpServerTool; use project::Project; use settings::SettingsStore; use smol::process::Child; -use std::cell::{Cell, RefCell}; +use std::cell::RefCell; use std::fmt::Display; use std::path::Path; use std::rc::Rc; @@ -153,20 +153,17 @@ impl AgentConnection for ClaudeAgentConnection { }) .detach(); - let pending_cancellation = Rc::new(Cell::new(PendingCancellation::None)); + let turn_state = Rc::new(RefCell::new(TurnState::None)); - let end_turn_tx = Rc::new(RefCell::new(None)); let handler_task = cx.spawn({ - let end_turn_tx = end_turn_tx.clone(); + let turn_state = turn_state.clone(); let mut thread_rx = thread_rx.clone(); - let cancellation_state = pending_cancellation.clone(); async move |cx| { while let Some(message) = incoming_message_rx.next().await { ClaudeAgentSession::handle_message( thread_rx.clone(), message, - end_turn_tx.clone(), - cancellation_state.clone(), + turn_state.clone(), cx, ) .await @@ -192,8 +189,7 @@ impl AgentConnection for ClaudeAgentConnection { let session = ClaudeAgentSession { outgoing_tx, - end_turn_tx, - pending_cancellation, + turn_state, _handler_task: handler_task, _mcp_server: Some(permission_mcp_server), }; @@ -225,8 +221,8 @@ impl AgentConnection for ClaudeAgentConnection { ))); }; - let (tx, rx) = oneshot::channel(); - session.end_turn_tx.borrow_mut().replace(tx); + let (end_tx, end_rx) = oneshot::channel(); + session.turn_state.replace(TurnState::InProgress { end_tx }); let mut content = String::new(); for chunk in params.prompt { @@ -260,12 +256,7 @@ impl AgentConnection for ClaudeAgentConnection { return Task::ready(Err(anyhow!(err))); } - let cancellation_state = session.pending_cancellation.clone(); - cx.foreground_executor().spawn(async move { - let result = rx.await??; - cancellation_state.set(PendingCancellation::None); - Ok(result) - }) + cx.foreground_executor().spawn(async move { end_rx.await? }) } fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) { @@ -277,7 +268,15 @@ impl AgentConnection for ClaudeAgentConnection { let request_id = new_request_id(); - session.pending_cancellation.set(PendingCancellation::Sent { + let turn_state = session.turn_state.take(); + let TurnState::InProgress { end_tx } = turn_state else { + // Already cancelled or idle, put it back + session.turn_state.replace(turn_state); + return; + }; + + session.turn_state.replace(TurnState::CancelRequested { + end_tx, request_id: request_id.clone(), }); @@ -349,28 +348,56 @@ fn spawn_claude( struct ClaudeAgentSession { outgoing_tx: UnboundedSender, - end_turn_tx: Rc>>>>, - pending_cancellation: Rc>, + turn_state: Rc>, _mcp_server: Option, _handler_task: Task<()>, } -#[derive(Debug, Default, PartialEq)] -enum PendingCancellation { +#[derive(Debug, Default)] +enum TurnState { #[default] None, - Sent { + InProgress { + end_tx: oneshot::Sender>, + }, + CancelRequested { + end_tx: oneshot::Sender>, request_id: String, }, - Confirmed, + CancelConfirmed { + end_tx: oneshot::Sender>, + }, +} + +impl TurnState { + fn is_cancelled(&self) -> bool { + matches!(self, TurnState::CancelConfirmed { .. }) + } + + fn end_tx(self) -> Option>> { + match self { + TurnState::None => None, + TurnState::InProgress { end_tx, .. } => Some(end_tx), + TurnState::CancelRequested { end_tx, .. } => Some(end_tx), + TurnState::CancelConfirmed { end_tx } => Some(end_tx), + } + } + + fn confirm_cancellation(self, id: &str) -> Self { + match self { + TurnState::CancelRequested { request_id, end_tx } if request_id == id => { + TurnState::CancelConfirmed { end_tx } + } + _ => self, + } + } } impl ClaudeAgentSession { async fn handle_message( mut thread_rx: watch::Receiver>, message: SdkMessage, - end_turn_tx: Rc>>>>, - pending_cancellation: Rc>, + turn_state: Rc>, cx: &mut AsyncApp, ) { match message { @@ -393,15 +420,13 @@ impl ClaudeAgentSession { for chunk in message.content.chunks() { match chunk { ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => { - let state = pending_cancellation.take(); - if state != PendingCancellation::Confirmed { + if !turn_state.borrow().is_cancelled() { thread .update(cx, |thread, cx| { thread.push_user_content_block(text.into(), cx) }) .log_err(); } - pending_cancellation.set(state); } ContentChunk::ToolResult { content, @@ -414,7 +439,12 @@ impl ClaudeAgentSession { acp::ToolCallUpdate { id: acp::ToolCallId(tool_use_id.into()), fields: acp::ToolCallUpdateFields { - status: Some(acp::ToolCallStatus::Completed), + status: if turn_state.borrow().is_cancelled() { + // Do not set to completed if turn was cancelled + None + } else { + Some(acp::ToolCallStatus::Completed) + }, content: (!content.is_empty()) .then(|| vec![content.into()]), ..Default::default() @@ -541,40 +571,38 @@ impl ClaudeAgentSession { result, .. } => { - if let Some(end_turn_tx) = end_turn_tx.borrow_mut().take() { - if is_error - || (subtype == ResultErrorType::ErrorDuringExecution - && pending_cancellation.take() != PendingCancellation::Confirmed) - { - end_turn_tx - .send(Err(anyhow!( - "Error: {}", - result.unwrap_or_else(|| subtype.to_string()) - ))) - .ok(); - } else { - let stop_reason = match subtype { - ResultErrorType::Success => acp::StopReason::EndTurn, - ResultErrorType::ErrorMaxTurns => acp::StopReason::MaxTurnRequests, - ResultErrorType::ErrorDuringExecution => acp::StopReason::Cancelled, - }; - end_turn_tx - .send(Ok(acp::PromptResponse { stop_reason })) - .ok(); - } + let turn_state = turn_state.take(); + let was_cancelled = turn_state.is_cancelled(); + let Some(end_turn_tx) = turn_state.end_tx() else { + debug_panic!("Received `SdkMessage::Result` but there wasn't an active turn"); + return; + }; + + if is_error || (!was_cancelled && subtype == ResultErrorType::ErrorDuringExecution) + { + end_turn_tx + .send(Err(anyhow!( + "Error: {}", + result.unwrap_or_else(|| subtype.to_string()) + ))) + .ok(); + } else { + let stop_reason = match subtype { + ResultErrorType::Success => acp::StopReason::EndTurn, + ResultErrorType::ErrorMaxTurns => acp::StopReason::MaxTurnRequests, + ResultErrorType::ErrorDuringExecution => acp::StopReason::Cancelled, + }; + end_turn_tx + .send(Ok(acp::PromptResponse { stop_reason })) + .ok(); } } SdkMessage::ControlResponse { response } => { if matches!(response.subtype, ResultErrorType::Success) { - let pending_cancellation_value = pending_cancellation.take(); - - if let PendingCancellation::Sent { request_id } = &pending_cancellation_value - && request_id == &response.request_id - { - pending_cancellation.set(PendingCancellation::Confirmed); - } else { - pending_cancellation.set(pending_cancellation_value); - } + let new_state = turn_state.take().confirm_cancellation(&response.request_id); + turn_state.replace(new_state); + } else { + log::error!("Control response error: {:?}", response); } } SdkMessage::System { .. } => {} diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 05f874bd30..ec6ca29b9d 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -246,7 +246,7 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; - let full_turn = thread.update(cx, |thread, cx| { + let _ = thread.update(cx, |thread, cx| { thread.send_raw( r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#, cx, @@ -285,9 +285,8 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon id.clone() }); - let _ = thread.update(cx, |thread, cx| thread.cancel(cx)); - full_turn.await.unwrap(); - thread.read_with(cx, |thread, _| { + thread.update(cx, |thread, cx| thread.cancel(cx)).await; + thread.read_with(cx, |thread, _cx| { let AgentThreadEntry::ToolCall(ToolCall { status: ToolCallStatus::Canceled, .. From 7d4d8b8398b03dedc6eabd71f3474300b947ae68 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 7 Aug 2025 19:35:41 -0400 Subject: [PATCH 176/693] Add GPT-5 support through OpenAI API (#35822) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (This PR does not add GPT-5 to Zed Pro, but rather adds access if you're using your own OpenAI API key.) Screenshot 2025-08-07 at 2 23 18 PM --- **NOTE:** If your API key is not through a verified organization, you may see this error: Screenshot 2025-08-07 at 2 04 54 PM Even if your org is verified, you still may not have access to GPT-5, in which case you could see this error: Screenshot 2025-08-07 at 2 09 18 PM One way to test if you're in this situation is to visit https://platform.openai.com/chat/edit?models=gpt-5 and see if you get the same "you don't have access to GPT-5" error on OpenAI's official playground. It looks like this: Screenshot 2025-08-07 at 2 15 25 PM Release Notes: - Added GPT-5, as well as its mini and nano variants. To use this, you need to have an OpenAI API key configured via the `OPENAI_API_KEY` environment variable. --- .../language_models/src/provider/open_ai.rs | 4 +++ crates/open_ai/src/open_ai.rs | 26 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 5185e979b7..ee74562687 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -674,6 +674,10 @@ pub fn count_open_ai_tokens( | Model::O3 | Model::O3Mini | Model::O4Mini => tiktoken_rs::num_tokens_from_messages(model.id(), &messages), + // GPT-5 models don't have tiktoken support yet; fall back on gpt-4o tokenizer + Model::Five | Model::FiveMini | Model::FiveNano => { + tiktoken_rs::num_tokens_from_messages("gpt-4o", &messages) + } } .map(|tokens| tokens as u64) }) diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 12a5cf52d2..4697d71ed3 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -74,6 +74,12 @@ pub enum Model { O3, #[serde(rename = "o4-mini")] O4Mini, + #[serde(rename = "gpt-5")] + Five, + #[serde(rename = "gpt-5-mini")] + FiveMini, + #[serde(rename = "gpt-5-nano")] + FiveNano, #[serde(rename = "custom")] Custom { @@ -105,6 +111,9 @@ impl Model { "o3-mini" => Ok(Self::O3Mini), "o3" => Ok(Self::O3), "o4-mini" => Ok(Self::O4Mini), + "gpt-5" => Ok(Self::Five), + "gpt-5-mini" => Ok(Self::FiveMini), + "gpt-5-nano" => Ok(Self::FiveNano), invalid_id => anyhow::bail!("invalid model id '{invalid_id}'"), } } @@ -123,6 +132,9 @@ impl Model { Self::O3Mini => "o3-mini", Self::O3 => "o3", Self::O4Mini => "o4-mini", + Self::Five => "gpt-5", + Self::FiveMini => "gpt-5-mini", + Self::FiveNano => "gpt-5-nano", Self::Custom { name, .. } => name, } } @@ -141,6 +153,9 @@ impl Model { Self::O3Mini => "o3-mini", Self::O3 => "o3", Self::O4Mini => "o4-mini", + Self::Five => "gpt-5", + Self::FiveMini => "gpt-5-mini", + Self::FiveNano => "gpt-5-nano", Self::Custom { name, display_name, .. } => display_name.as_ref().unwrap_or(name), @@ -161,6 +176,9 @@ impl Model { Self::O3Mini => 200_000, Self::O3 => 200_000, Self::O4Mini => 200_000, + Self::Five => 272_000, + Self::FiveMini => 272_000, + Self::FiveNano => 272_000, Self::Custom { max_tokens, .. } => *max_tokens, } } @@ -182,6 +200,9 @@ impl Model { Self::O3Mini => Some(100_000), Self::O3 => Some(100_000), Self::O4Mini => Some(100_000), + Self::Five => Some(128_000), + Self::FiveMini => Some(128_000), + Self::FiveNano => Some(128_000), } } @@ -197,7 +218,10 @@ impl Model { | Self::FourOmniMini | Self::FourPointOne | Self::FourPointOneMini - | Self::FourPointOneNano => true, + | Self::FourPointOneNano + | Self::Five + | Self::FiveMini + | Self::FiveNano => true, Self::O1 | Self::O3 | Self::O3Mini | Self::O4Mini | Model::Custom { .. } => false, } } From 3d662ee2828ab022058a60dde74d4f99d3f1a15d Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Thu, 7 Aug 2025 20:46:47 -0300 Subject: [PATCH 177/693] agent2: Port read_file tool (#35840) Ports the read_file tool from `assistant_tools` to `agent2`. Note: Image support not implemented. Release Notes: - N/A --- Cargo.lock | 2 + crates/agent2/Cargo.toml | 3 + crates/agent2/src/agent.rs | 5 +- crates/agent2/src/thread.rs | 33 +- crates/agent2/src/tools.rs | 2 + crates/agent2/src/tools/read_file_tool.rs | 970 ++++++++++++++++++++++ 6 files changed, 1011 insertions(+), 4 deletions(-) create mode 100644 crates/agent2/src/tools/read_file_tool.rs diff --git a/Cargo.lock b/Cargo.lock index 39ee75f6dd..e63c5e2acf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,10 +172,12 @@ dependencies = [ "gpui_tokio", "handlebars 4.5.0", "indoc", + "itertools 0.14.0", "language", "language_model", "language_models", "log", + "pretty_assertions", "project", "prompt_store", "reqwest_client", diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 884378fbcc..a75011a671 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -24,6 +24,8 @@ futures.workspace = true gpui.workspace = true handlebars = { workspace = true, features = ["rust-embed"] } indoc.workspace = true +itertools.workspace = true +language.workspace = true language_model.workspace = true language_models.workspace = true log.workspace = true @@ -55,3 +57,4 @@ project = { workspace = true, "features" = ["test-support"] } reqwest_client.workspace = true settings = { workspace = true, "features" = ["test-support"] } worktree = { workspace = true, "features" = ["test-support"] } +pretty_assertions.workspace = true diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index cb568f04c2..2014d86fb7 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,5 +1,5 @@ use crate::{templates::Templates, AgentResponseEvent, Thread}; -use crate::{FindPathTool, ThinkingTool, ToolCallAuthorization}; +use crate::{FindPathTool, ReadFileTool, ThinkingTool, ToolCallAuthorization}; use acp_thread::ModelSelector; use agent_client_protocol as acp; use anyhow::{anyhow, Context as _, Result}; @@ -413,9 +413,10 @@ impl acp_thread::AgentConnection for NativeAgentConnection { })?; let thread = cx.new(|_| { - let mut thread = Thread::new(project.clone(), agent.project_context.clone(), action_log, agent.templates.clone(), default_model); + let mut thread = Thread::new(project.clone(), agent.project_context.clone(), action_log.clone(), agent.templates.clone(), default_model); thread.add_tool(ThinkingTool); thread.add_tool(FindPathTool::new(project.clone())); + thread.add_tool(ReadFileTool::new(project.clone(), action_log)); thread }); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 805ffff1c0..4b8a65655f 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -125,7 +125,7 @@ pub struct Thread { project_context: Rc>, templates: Arc, pub selected_model: Arc, - _action_log: Entity, + action_log: Entity, } impl Thread { @@ -145,7 +145,7 @@ impl Thread { project_context, templates, selected_model: default_model, - _action_log: action_log, + action_log, } } @@ -315,6 +315,10 @@ impl Thread { events_rx } + pub fn action_log(&self) -> &Entity { + &self.action_log + } + pub fn build_system_message(&self) -> AgentMessage { log::debug!("Building system message"); let prompt = SystemPromptTemplate { @@ -924,3 +928,28 @@ impl ToolCallEventStream { .authorize_tool_call(&self.tool_use_id, title, kind, input) } } + +#[cfg(test)] +pub struct TestToolCallEventStream { + stream: ToolCallEventStream, + _events_rx: mpsc::UnboundedReceiver>, +} + +#[cfg(test)] +impl TestToolCallEventStream { + pub fn new() -> Self { + let (events_tx, events_rx) = + mpsc::unbounded::>(); + + let stream = ToolCallEventStream::new("test".into(), AgentResponseEventStream(events_tx)); + + Self { + stream, + _events_rx: events_rx, + } + } + + pub fn stream(&self) -> ToolCallEventStream { + self.stream.clone() + } +} diff --git a/crates/agent2/src/tools.rs b/crates/agent2/src/tools.rs index 848fe552ed..240614c263 100644 --- a/crates/agent2/src/tools.rs +++ b/crates/agent2/src/tools.rs @@ -1,5 +1,7 @@ mod find_path_tool; +mod read_file_tool; mod thinking_tool; pub use find_path_tool::*; +pub use read_file_tool::*; pub use thinking_tool::*; diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs new file mode 100644 index 0000000000..30794ccdad --- /dev/null +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -0,0 +1,970 @@ +use agent_client_protocol::{self as acp}; +use anyhow::{anyhow, Result}; +use assistant_tool::{outline, ActionLog}; +use gpui::{Entity, Task}; +use indoc::formatdoc; +use language::{Anchor, Point}; +use project::{AgentLocation, Project, WorktreeSettings}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Settings; +use std::sync::Arc; +use ui::{App, SharedString}; + +use crate::{AgentTool, ToolCallEventStream}; + +/// Reads the content of the given file in the project. +/// +/// - Never attempt to read a path that hasn't been previously mentioned. +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct ReadFileToolInput { + /// The relative path of the file to read. + /// + /// This path should never be absolute, and the first component + /// of the path should always be a root directory in a project. + /// + /// + /// If the project has the following root directories: + /// + /// - /a/b/directory1 + /// - /c/d/directory2 + /// + /// If you want to access `file.txt` in `directory1`, you should use the path `directory1/file.txt`. + /// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`. + /// + pub path: String, + + /// Optional line number to start reading on (1-based index) + #[serde(default)] + pub start_line: Option, + + /// Optional line number to end reading on (1-based index, inclusive) + #[serde(default)] + pub end_line: Option, +} + +pub struct ReadFileTool { + project: Entity, + action_log: Entity, +} + +impl ReadFileTool { + pub fn new(project: Entity, action_log: Entity) -> Self { + Self { + project, + action_log, + } + } +} + +impl AgentTool for ReadFileTool { + type Input = ReadFileToolInput; + + fn name(&self) -> SharedString { + "read_file".into() + } + + fn kind(&self) -> acp::ToolKind { + acp::ToolKind::Read + } + + fn initial_title(&self, input: Self::Input) -> SharedString { + let path = &input.path; + match (input.start_line, input.end_line) { + (Some(start), Some(end)) => { + format!( + "[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))", + path, start, end, path, start, end + ) + } + (Some(start), None) => { + format!( + "[Read file `{}` (from line {})](@selection:{}:({}-{}))", + path, start, path, start, start + ) + } + _ => format!("[Read file `{}`](@file:{})", path, path), + } + .into() + } + + fn run( + self: Arc, + input: Self::Input, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> { + let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) else { + return Task::ready(Err(anyhow!("Path {} not found in project", &input.path))); + }; + + // Error out if this path is either excluded or private in global settings + let global_settings = WorktreeSettings::get_global(cx); + if global_settings.is_path_excluded(&project_path.path) { + return Task::ready(Err(anyhow!( + "Cannot read file because its path matches the global `file_scan_exclusions` setting: {}", + &input.path + ))); + } + + if global_settings.is_path_private(&project_path.path) { + return Task::ready(Err(anyhow!( + "Cannot read file because its path matches the global `private_files` setting: {}", + &input.path + ))); + } + + // Error out if this path is either excluded or private in worktree settings + let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx); + if worktree_settings.is_path_excluded(&project_path.path) { + return Task::ready(Err(anyhow!( + "Cannot read file because its path matches the worktree `file_scan_exclusions` setting: {}", + &input.path + ))); + } + + if worktree_settings.is_path_private(&project_path.path) { + return Task::ready(Err(anyhow!( + "Cannot read file because its path matches the worktree `private_files` setting: {}", + &input.path + ))); + } + + let file_path = input.path.clone(); + + event_stream.send_update(acp::ToolCallUpdateFields { + locations: Some(vec![acp::ToolCallLocation { + path: project_path.path.to_path_buf(), + line: input.start_line, + // TODO (tracked): use full range + }]), + ..Default::default() + }); + + // TODO (tracked): images + // if image_store::is_image_file(&self.project, &project_path, cx) { + // let model = &self.thread.read(cx).selected_model; + + // if !model.supports_images() { + // return Task::ready(Err(anyhow!( + // "Attempted to read an image, but Zed doesn't currently support sending images to {}.", + // model.name().0 + // ))) + // .into(); + // } + + // return cx.spawn(async move |cx| -> Result { + // let image_entity: Entity = cx + // .update(|cx| { + // self.project.update(cx, |project, cx| { + // project.open_image(project_path.clone(), cx) + // }) + // })? + // .await?; + + // let image = + // image_entity.read_with(cx, |image_item, _| Arc::clone(&image_item.image))?; + + // let language_model_image = cx + // .update(|cx| LanguageModelImage::from_image(image, cx))? + // .await + // .context("processing image")?; + + // Ok(ToolResultOutput { + // content: ToolResultContent::Image(language_model_image), + // output: None, + // }) + // }); + // } + // + + let project = self.project.clone(); + let action_log = self.action_log.clone(); + + cx.spawn(async move |cx| { + let buffer = cx + .update(|cx| { + project.update(cx, |project, cx| project.open_buffer(project_path, cx)) + })? + .await?; + if buffer.read_with(cx, |buffer, _| { + buffer + .file() + .as_ref() + .map_or(true, |file| !file.disk_state().exists()) + })? { + anyhow::bail!("{file_path} not found"); + } + + project.update(cx, |project, cx| { + project.set_agent_location( + Some(AgentLocation { + buffer: buffer.downgrade(), + position: Anchor::MIN, + }), + cx, + ); + })?; + + // Check if specific line ranges are provided + if input.start_line.is_some() || input.end_line.is_some() { + let mut anchor = None; + let result = buffer.read_with(cx, |buffer, _cx| { + let text = buffer.text(); + // .max(1) because despite instructions to be 1-indexed, sometimes the model passes 0. + let start = input.start_line.unwrap_or(1).max(1); + let start_row = start - 1; + if start_row <= buffer.max_point().row { + let column = buffer.line_indent_for_row(start_row).raw_len(); + anchor = Some(buffer.anchor_before(Point::new(start_row, column))); + } + + let lines = text.split('\n').skip(start_row as usize); + if let Some(end) = input.end_line { + let count = end.saturating_sub(start).saturating_add(1); // Ensure at least 1 line + itertools::intersperse(lines.take(count as usize), "\n").collect::() + } else { + itertools::intersperse(lines, "\n").collect::() + } + })?; + + action_log.update(cx, |log, cx| { + log.buffer_read(buffer.clone(), cx); + })?; + + if let Some(anchor) = anchor { + project.update(cx, |project, cx| { + project.set_agent_location( + Some(AgentLocation { + buffer: buffer.downgrade(), + position: anchor, + }), + cx, + ); + })?; + } + + Ok(result) + } else { + // No line ranges specified, so check file size to see if it's too big. + let file_size = buffer.read_with(cx, |buffer, _cx| buffer.text().len())?; + + if file_size <= outline::AUTO_OUTLINE_SIZE { + // File is small enough, so return its contents. + let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?; + + action_log.update(cx, |log, cx| { + log.buffer_read(buffer, cx); + })?; + + Ok(result) + } else { + // File is too big, so return the outline + // and a suggestion to read again with line numbers. + let outline = + outline::file_outline(project, file_path, action_log, None, cx).await?; + Ok(formatdoc! {" + This file was too big to read all at once. + + Here is an outline of its symbols: + + {outline} + + Using the line numbers in this outline, you can call this tool again + while specifying the start_line and end_line fields to see the + implementations of symbols in the outline. + + Alternatively, you can fall back to the `grep` tool (if available) + to search the file for specific content." + }) + } + } + }) + } +} + +#[cfg(test)] +mod test { + use crate::TestToolCallEventStream; + + use super::*; + use gpui::{AppContext, TestAppContext, UpdateGlobal as _}; + use language::{tree_sitter_rust, Language, LanguageConfig, LanguageMatcher}; + use project::{FakeFs, Project}; + use serde_json::json; + use settings::SettingsStore; + use util::path; + + #[gpui::test] + async fn test_read_nonexistent_file(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree(path!("/root"), json!({})).await; + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let tool = Arc::new(ReadFileTool::new(project, action_log)); + let event_stream = TestToolCallEventStream::new(); + + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "root/nonexistent_file.txt".to_string(), + start_line: None, + end_line: None, + }; + tool.run(input, event_stream.stream(), cx) + }) + .await; + assert_eq!( + result.unwrap_err().to_string(), + "root/nonexistent_file.txt not found" + ); + } + #[gpui::test] + async fn test_read_small_file(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/root"), + json!({ + "small_file.txt": "This is a small file content" + }), + ) + .await; + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let tool = Arc::new(ReadFileTool::new(project, action_log)); + let event_stream = TestToolCallEventStream::new(); + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "root/small_file.txt".into(), + start_line: None, + end_line: None, + }; + tool.run(input, event_stream.stream(), cx) + }) + .await; + assert_eq!(result.unwrap(), "This is a small file content"); + } + + #[gpui::test] + async fn test_read_large_file(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/root"), + json!({ + "large_file.rs": (0..1000).map(|i| format!("struct Test{} {{\n a: u32,\n b: usize,\n}}", i)).collect::>().join("\n") + }), + ) + .await; + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(Arc::new(rust_lang())); + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let tool = Arc::new(ReadFileTool::new(project, action_log)); + let event_stream = TestToolCallEventStream::new(); + let content = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "root/large_file.rs".into(), + start_line: None, + end_line: None, + }; + tool.clone().run(input, event_stream.stream(), cx) + }) + .await + .unwrap(); + + assert_eq!( + content.lines().skip(4).take(6).collect::>(), + vec![ + "struct Test0 [L1-4]", + " a [L2]", + " b [L3]", + "struct Test1 [L5-8]", + " a [L6]", + " b [L7]", + ] + ); + + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "root/large_file.rs".into(), + start_line: None, + end_line: None, + }; + tool.run(input, event_stream.stream(), cx) + }) + .await; + let content = result.unwrap(); + let expected_content = (0..1000) + .flat_map(|i| { + vec![ + format!("struct Test{} [L{}-{}]", i, i * 4 + 1, i * 4 + 4), + format!(" a [L{}]", i * 4 + 2), + format!(" b [L{}]", i * 4 + 3), + ] + }) + .collect::>(); + pretty_assertions::assert_eq!( + content + .lines() + .skip(4) + .take(expected_content.len()) + .collect::>(), + expected_content + ); + } + + #[gpui::test] + async fn test_read_file_with_line_range(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/root"), + json!({ + "multiline.txt": "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" + }), + ) + .await; + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let tool = Arc::new(ReadFileTool::new(project, action_log)); + let event_stream = TestToolCallEventStream::new(); + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "root/multiline.txt".to_string(), + start_line: Some(2), + end_line: Some(4), + }; + tool.run(input, event_stream.stream(), cx) + }) + .await; + assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4"); + } + + #[gpui::test] + async fn test_read_file_line_range_edge_cases(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/root"), + json!({ + "multiline.txt": "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" + }), + ) + .await; + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let tool = Arc::new(ReadFileTool::new(project, action_log)); + let event_stream = TestToolCallEventStream::new(); + + // start_line of 0 should be treated as 1 + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "root/multiline.txt".to_string(), + start_line: Some(0), + end_line: Some(2), + }; + tool.clone().run(input, event_stream.stream(), cx) + }) + .await; + assert_eq!(result.unwrap(), "Line 1\nLine 2"); + + // end_line of 0 should result in at least 1 line + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "root/multiline.txt".to_string(), + start_line: Some(1), + end_line: Some(0), + }; + tool.clone().run(input, event_stream.stream(), cx) + }) + .await; + assert_eq!(result.unwrap(), "Line 1"); + + // when start_line > end_line, should still return at least 1 line + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "root/multiline.txt".to_string(), + start_line: Some(3), + end_line: Some(2), + }; + tool.clone().run(input, event_stream.stream(), cx) + }) + .await; + assert_eq!(result.unwrap(), "Line 3"); + } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + Project::init_settings(cx); + }); + } + + fn rust_lang() -> Language { + Language::new( + LanguageConfig { + name: "Rust".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + ) + .with_outline_query( + r#" + (line_comment) @annotation + + (struct_item + "struct" @context + name: (_) @name) @item + (enum_item + "enum" @context + name: (_) @name) @item + (enum_variant + name: (_) @name) @item + (field_declaration + name: (_) @name) @item + (impl_item + "impl" @context + trait: (_)? @name + "for"? @context + type: (_) @name + body: (_ "{" (_)* "}")) @item + (function_item + "fn" @context + name: (_) @name) @item + (mod_item + "mod" @context + name: (_) @name) @item + "#, + ) + .unwrap() + } + + #[gpui::test] + async fn test_read_file_security(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + + fs.insert_tree( + path!("/"), + json!({ + "project_root": { + "allowed_file.txt": "This file is in the project", + ".mysecrets": "SECRET_KEY=abc123", + ".secretdir": { + "config": "special configuration" + }, + ".mymetadata": "custom metadata", + "subdir": { + "normal_file.txt": "Normal file content", + "special.privatekey": "private key content", + "data.mysensitive": "sensitive data" + } + }, + "outside_project": { + "sensitive_file.txt": "This file is outside the project" + } + }), + ) + .await; + + cx.update(|cx| { + use gpui::UpdateGlobal; + use project::WorktreeSettings; + use settings::SettingsStore; + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings::(cx, |settings| { + settings.file_scan_exclusions = Some(vec![ + "**/.secretdir".to_string(), + "**/.mymetadata".to_string(), + ]); + settings.private_files = Some(vec![ + "**/.mysecrets".to_string(), + "**/*.privatekey".to_string(), + "**/*.mysensitive".to_string(), + ]); + }); + }); + }); + + let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let tool = Arc::new(ReadFileTool::new(project, action_log)); + let event_stream = TestToolCallEventStream::new(); + + // Reading a file outside the project worktree should fail + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "/outside_project/sensitive_file.txt".to_string(), + start_line: None, + end_line: None, + }; + tool.clone().run(input, event_stream.stream(), cx) + }) + .await; + assert!( + result.is_err(), + "read_file_tool should error when attempting to read an absolute path outside a worktree" + ); + + // Reading a file within the project should succeed + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "project_root/allowed_file.txt".to_string(), + start_line: None, + end_line: None, + }; + tool.clone().run(input, event_stream.stream(), cx) + }) + .await; + assert!( + result.is_ok(), + "read_file_tool should be able to read files inside worktrees" + ); + + // Reading files that match file_scan_exclusions should fail + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "project_root/.secretdir/config".to_string(), + start_line: None, + end_line: None, + }; + tool.clone().run(input, event_stream.stream(), cx) + }) + .await; + assert!( + result.is_err(), + "read_file_tool should error when attempting to read files in .secretdir (file_scan_exclusions)" + ); + + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "project_root/.mymetadata".to_string(), + start_line: None, + end_line: None, + }; + tool.clone().run(input, event_stream.stream(), cx) + }) + .await; + assert!( + result.is_err(), + "read_file_tool should error when attempting to read .mymetadata files (file_scan_exclusions)" + ); + + // Reading private files should fail + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "project_root/.mysecrets".to_string(), + start_line: None, + end_line: None, + }; + tool.clone().run(input, event_stream.stream(), cx) + }) + .await; + assert!( + result.is_err(), + "read_file_tool should error when attempting to read .mysecrets (private_files)" + ); + + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "project_root/subdir/special.privatekey".to_string(), + start_line: None, + end_line: None, + }; + tool.clone().run(input, event_stream.stream(), cx) + }) + .await; + assert!( + result.is_err(), + "read_file_tool should error when attempting to read .privatekey files (private_files)" + ); + + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "project_root/subdir/data.mysensitive".to_string(), + start_line: None, + end_line: None, + }; + tool.clone().run(input, event_stream.stream(), cx) + }) + .await; + assert!( + result.is_err(), + "read_file_tool should error when attempting to read .mysensitive files (private_files)" + ); + + // Reading a normal file should still work, even with private_files configured + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "project_root/subdir/normal_file.txt".to_string(), + start_line: None, + end_line: None, + }; + tool.clone().run(input, event_stream.stream(), cx) + }) + .await; + assert!(result.is_ok(), "Should be able to read normal files"); + assert_eq!(result.unwrap(), "Normal file content"); + + // Path traversal attempts with .. should fail + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "project_root/../outside_project/sensitive_file.txt".to_string(), + start_line: None, + end_line: None, + }; + tool.run(input, event_stream.stream(), cx) + }) + .await; + assert!( + result.is_err(), + "read_file_tool should error when attempting to read a relative path that resolves to outside a worktree" + ); + } + + #[gpui::test] + async fn test_read_file_with_multiple_worktree_settings(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + + // Create first worktree with its own private_files setting + fs.insert_tree( + path!("/worktree1"), + json!({ + "src": { + "main.rs": "fn main() { println!(\"Hello from worktree1\"); }", + "secret.rs": "const API_KEY: &str = \"secret_key_1\";", + "config.toml": "[database]\nurl = \"postgres://localhost/db1\"" + }, + "tests": { + "test.rs": "mod tests { fn test_it() {} }", + "fixture.sql": "CREATE TABLE users (id INT, name VARCHAR(255));" + }, + ".zed": { + "settings.json": r#"{ + "file_scan_exclusions": ["**/fixture.*"], + "private_files": ["**/secret.rs", "**/config.toml"] + }"# + } + }), + ) + .await; + + // Create second worktree with different private_files setting + fs.insert_tree( + path!("/worktree2"), + json!({ + "lib": { + "public.js": "export function greet() { return 'Hello from worktree2'; }", + "private.js": "const SECRET_TOKEN = \"private_token_2\";", + "data.json": "{\"api_key\": \"json_secret_key\"}" + }, + "docs": { + "README.md": "# Public Documentation", + "internal.md": "# Internal Secrets and Configuration" + }, + ".zed": { + "settings.json": r#"{ + "file_scan_exclusions": ["**/internal.*"], + "private_files": ["**/private.js", "**/data.json"] + }"# + } + }), + ) + .await; + + // Set global settings + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings::(cx, |settings| { + settings.file_scan_exclusions = + Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); + settings.private_files = Some(vec!["**/.env".to_string()]); + }); + }); + }); + + let project = Project::test( + fs.clone(), + [path!("/worktree1").as_ref(), path!("/worktree2").as_ref()], + cx, + ) + .await; + + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let tool = Arc::new(ReadFileTool::new(project.clone(), action_log.clone())); + let event_stream = TestToolCallEventStream::new(); + + // Test reading allowed files in worktree1 + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "worktree1/src/main.rs".to_string(), + start_line: None, + end_line: None, + }; + tool.clone().run(input, event_stream.stream(), cx) + }) + .await + .unwrap(); + + assert_eq!(result, "fn main() { println!(\"Hello from worktree1\"); }"); + + // Test reading private file in worktree1 should fail + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "worktree1/src/secret.rs".to_string(), + start_line: None, + end_line: None, + }; + tool.clone().run(input, event_stream.stream(), cx) + }) + .await; + + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("worktree `private_files` setting"), + "Error should mention worktree private_files setting" + ); + + // Test reading excluded file in worktree1 should fail + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "worktree1/tests/fixture.sql".to_string(), + start_line: None, + end_line: None, + }; + tool.clone().run(input, event_stream.stream(), cx) + }) + .await; + + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("worktree `file_scan_exclusions` setting"), + "Error should mention worktree file_scan_exclusions setting" + ); + + // Test reading allowed files in worktree2 + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "worktree2/lib/public.js".to_string(), + start_line: None, + end_line: None, + }; + tool.clone().run(input, event_stream.stream(), cx) + }) + .await + .unwrap(); + + assert_eq!( + result, + "export function greet() { return 'Hello from worktree2'; }" + ); + + // Test reading private file in worktree2 should fail + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "worktree2/lib/private.js".to_string(), + start_line: None, + end_line: None, + }; + tool.clone().run(input, event_stream.stream(), cx) + }) + .await; + + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("worktree `private_files` setting"), + "Error should mention worktree private_files setting" + ); + + // Test reading excluded file in worktree2 should fail + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "worktree2/docs/internal.md".to_string(), + start_line: None, + end_line: None, + }; + tool.clone().run(input, event_stream.stream(), cx) + }) + .await; + + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("worktree `file_scan_exclusions` setting"), + "Error should mention worktree file_scan_exclusions setting" + ); + + // Test that files allowed in one worktree but not in another are handled correctly + // (e.g., config.toml is private in worktree1 but doesn't exist in worktree2) + let result = cx + .update(|cx| { + let input = ReadFileToolInput { + path: "worktree1/src/config.toml".to_string(), + start_line: None, + end_line: None, + }; + tool.clone().run(input, event_stream.stream(), cx) + }) + .await; + + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("worktree `private_files` setting"), + "Config.toml should be blocked by worktree1's private_files setting" + ); + } +} From c7d641ecb88c3323f10a6b585252b727d374e613 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 7 Aug 2025 16:55:15 -0700 Subject: [PATCH 178/693] Revert "chore: Bump Rust to 1.89 (#35788)" (#35843) This reverts commit efba2cbfd371bdd85dc3bfdd6b98d1d405ad9a89. Unfortunately, the Docker image for 1.89 has not shown up yet. Once it has, we should re-land this. Release Notes: - N/A --- Dockerfile-collab | 2 +- crates/fs/src/fake_git_repo.rs | 4 +-- crates/git/src/repository.rs | 8 +++--- crates/gpui/src/keymap/context.rs | 30 +++++++++----------- crates/gpui/src/platform/windows/wrapper.rs | 24 +++++++++++++++- crates/terminal_view/src/terminal_element.rs | 2 +- flake.lock | 18 ++++++------ rust-toolchain.toml | 2 +- 8 files changed, 55 insertions(+), 35 deletions(-) diff --git a/Dockerfile-collab b/Dockerfile-collab index c1621d6ee6..2dafe296c7 100644 --- a/Dockerfile-collab +++ b/Dockerfile-collab @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.2 -FROM rust:1.89-bookworm as builder +FROM rust:1.88-bookworm as builder WORKDIR app COPY . . diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 73da63fd47..04ba656232 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -402,11 +402,11 @@ impl GitRepository for FakeGitRepository { &self, _paths: Vec, _env: Arc>, - ) -> BoxFuture<'_, Result<()>> { + ) -> BoxFuture> { unimplemented!() } - fn stash_pop(&self, _env: Arc>) -> BoxFuture<'_, Result<()>> { + fn stash_pop(&self, _env: Arc>) -> BoxFuture> { unimplemented!() } diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 518b6c4f46..dc7ab0af65 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -399,9 +399,9 @@ pub trait GitRepository: Send + Sync { &self, paths: Vec, env: Arc>, - ) -> BoxFuture<'_, Result<()>>; + ) -> BoxFuture>; - fn stash_pop(&self, env: Arc>) -> BoxFuture<'_, Result<()>>; + fn stash_pop(&self, env: Arc>) -> BoxFuture>; fn push( &self, @@ -1203,7 +1203,7 @@ impl GitRepository for RealGitRepository { &self, paths: Vec, env: Arc>, - ) -> BoxFuture<'_, Result<()>> { + ) -> BoxFuture> { let working_directory = self.working_directory(); self.executor .spawn(async move { @@ -1227,7 +1227,7 @@ impl GitRepository for RealGitRepository { .boxed() } - fn stash_pop(&self, env: Arc>) -> BoxFuture<'_, Result<()>> { + fn stash_pop(&self, env: Arc>) -> BoxFuture> { let working_directory = self.working_directory(); self.executor .spawn(async move { diff --git a/crates/gpui/src/keymap/context.rs b/crates/gpui/src/keymap/context.rs index 281035fe97..f4b878ae77 100644 --- a/crates/gpui/src/keymap/context.rs +++ b/crates/gpui/src/keymap/context.rs @@ -461,8 +461,6 @@ fn skip_whitespace(source: &str) -> &str { #[cfg(test)] mod tests { - use core::slice; - use super::*; use crate as gpui; use KeyBindingContextPredicate::*; @@ -676,11 +674,11 @@ mod tests { assert!(predicate.eval(&contexts)); assert!(!predicate.eval(&[])); - assert!(!predicate.eval(slice::from_ref(&child_context))); + assert!(!predicate.eval(&[child_context.clone()])); assert!(!predicate.eval(&[parent_context])); let zany_predicate = KeyBindingContextPredicate::parse("child > child").unwrap(); - assert!(!zany_predicate.eval(slice::from_ref(&child_context))); + assert!(!zany_predicate.eval(&[child_context.clone()])); assert!(zany_predicate.eval(&[child_context.clone(), child_context.clone()])); } @@ -692,13 +690,13 @@ mod tests { let parent_context = KeyContext::try_from("parent").unwrap(); let child_context = KeyContext::try_from("child").unwrap(); - assert!(not_predicate.eval(slice::from_ref(&workspace_context))); - assert!(!not_predicate.eval(slice::from_ref(&editor_context))); + assert!(not_predicate.eval(&[workspace_context.clone()])); + assert!(!not_predicate.eval(&[editor_context.clone()])); assert!(!not_predicate.eval(&[editor_context.clone(), workspace_context.clone()])); assert!(!not_predicate.eval(&[workspace_context.clone(), editor_context.clone()])); let complex_not = KeyBindingContextPredicate::parse("!editor && workspace").unwrap(); - assert!(complex_not.eval(slice::from_ref(&workspace_context))); + assert!(complex_not.eval(&[workspace_context.clone()])); assert!(!complex_not.eval(&[editor_context.clone(), workspace_context.clone()])); let not_mode_predicate = KeyBindingContextPredicate::parse("!(mode == full)").unwrap(); @@ -711,18 +709,18 @@ mod tests { assert!(not_mode_predicate.eval(&[other_mode_context])); let not_descendant = KeyBindingContextPredicate::parse("!(parent > child)").unwrap(); - assert!(not_descendant.eval(slice::from_ref(&parent_context))); - assert!(not_descendant.eval(slice::from_ref(&child_context))); + assert!(not_descendant.eval(&[parent_context.clone()])); + assert!(not_descendant.eval(&[child_context.clone()])); assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); let not_descendant = KeyBindingContextPredicate::parse("parent > !child").unwrap(); - assert!(!not_descendant.eval(slice::from_ref(&parent_context))); - assert!(!not_descendant.eval(slice::from_ref(&child_context))); + assert!(!not_descendant.eval(&[parent_context.clone()])); + assert!(!not_descendant.eval(&[child_context.clone()])); assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); let double_not = KeyBindingContextPredicate::parse("!!editor").unwrap(); - assert!(double_not.eval(slice::from_ref(&editor_context))); - assert!(!double_not.eval(slice::from_ref(&workspace_context))); + assert!(double_not.eval(&[editor_context.clone()])); + assert!(!double_not.eval(&[workspace_context.clone()])); // Test complex descendant cases let workspace_context = KeyContext::try_from("Workspace").unwrap(); @@ -756,9 +754,9 @@ mod tests { // !Workspace - shouldn't match when Workspace is in the context let not_workspace = KeyBindingContextPredicate::parse("!Workspace").unwrap(); - assert!(!not_workspace.eval(slice::from_ref(&workspace_context))); - assert!(not_workspace.eval(slice::from_ref(&pane_context))); - assert!(not_workspace.eval(slice::from_ref(&editor_context))); + assert!(!not_workspace.eval(&[workspace_context.clone()])); + assert!(not_workspace.eval(&[pane_context.clone()])); + assert!(not_workspace.eval(&[editor_context.clone()])); assert!(!not_workspace.eval(&workspace_pane_editor)); } } diff --git a/crates/gpui/src/platform/windows/wrapper.rs b/crates/gpui/src/platform/windows/wrapper.rs index a1fe98a392..6015dffdab 100644 --- a/crates/gpui/src/platform/windows/wrapper.rs +++ b/crates/gpui/src/platform/windows/wrapper.rs @@ -1,6 +1,28 @@ use std::ops::Deref; -use windows::Win32::UI::WindowsAndMessaging::HCURSOR; +use windows::Win32::{Foundation::HANDLE, UI::WindowsAndMessaging::HCURSOR}; + +#[derive(Debug, Clone, Copy)] +pub(crate) struct SafeHandle { + raw: HANDLE, +} + +unsafe impl Send for SafeHandle {} +unsafe impl Sync for SafeHandle {} + +impl From for SafeHandle { + fn from(value: HANDLE) -> Self { + SafeHandle { raw: value } + } +} + +impl Deref for SafeHandle { + type Target = HANDLE; + + fn deref(&self) -> &Self::Target { + &self.raw + } +} #[derive(Debug, Clone, Copy)] pub(crate) struct SafeCursor { diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 6c1be9d5e7..083c07de9c 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -136,7 +136,7 @@ impl BatchedTextRun { .shape_line( self.text.clone().into(), self.font_size.to_pixels(window.rem_size()), - std::slice::from_ref(&self.style), + &[self.style.clone()], Some(dimensions.cell_width), ) .paint(pos, dimensions.line_height, window, cx); diff --git a/flake.lock b/flake.lock index 80022f7b55..fa0d51d90d 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1754269165, - "narHash": "sha256-0tcS8FHd4QjbCVoxN9jI+PjHgA4vc/IjkUSp+N3zy0U=", + "lastModified": 1750266157, + "narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=", "owner": "ipetkov", "repo": "crane", - "rev": "444e81206df3f7d92780680e45858e31d2f07a08", + "rev": "e37c943371b73ed87faf33f7583860f81f1d5a48", "type": "github" }, "original": { @@ -33,10 +33,10 @@ "nixpkgs": { "locked": { "lastModified": 315532800, - "narHash": "sha256-5VYevX3GccubYeccRGAXvCPA1ktrGmIX1IFC0icX07g=", - "rev": "a683adc19ff5228af548c6539dbc3440509bfed3", + "narHash": "sha256-j+zO+IHQ7VwEam0pjPExdbLT2rVioyVS3iq4bLO3GEc=", + "rev": "61c0f513911459945e2cb8bf333dc849f1b976ff", "type": "tarball", - "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre840248.a683adc19ff5/nixexprs.tar.xz" + "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre821324.61c0f5139114/nixexprs.tar.xz" }, "original": { "type": "tarball", @@ -58,11 +58,11 @@ ] }, "locked": { - "lastModified": 1754575663, - "narHash": "sha256-afOx8AG0KYtw7mlt6s6ahBBy7eEHZwws3iCRoiuRQS4=", + "lastModified": 1750964660, + "narHash": "sha256-YQ6EyFetjH1uy5JhdhRdPe6cuNXlYpMAQePFfZj4W7M=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "6db0fb0e9cec2e9729dc52bf4898e6c135bb8a0f", + "rev": "04f0fcfb1a50c63529805a798b4b5c21610ff390", "type": "github" }, "original": { diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 3d87025a27..f80eab8fbc 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.89" +channel = "1.88" profile = "minimal" components = [ "rustfmt", "clippy" ] targets = [ From bd402fdc7daf81fa0672c04b1e0777c86c02916d Mon Sep 17 00:00:00 2001 From: smit Date: Fri, 8 Aug 2025 06:17:37 +0530 Subject: [PATCH 179/693] editor: Fix Follow Agent unexpectedly stopping during edits (#35845) Closes #34881 For horizontal scroll, we weren't keeping track of the `local` bool, so whenever the agent tries to autoscroll horizontally, it would be seen as a user scroll event resulting in unfollow. Release Notes: - Fixed an issue where the Follow Agent could unexpectedly stop following during edits. --- crates/editor/src/element.rs | 19 ++++++++++++++++--- crates/editor/src/scroll.rs | 4 ++-- crates/editor/src/scroll/autoscroll.rs | 14 +++++++------- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 034fff970d..428a30471d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -8030,12 +8030,20 @@ impl Element for EditorElement { autoscroll_containing_element, needs_horizontal_autoscroll, ) = self.editor.update(cx, |editor, cx| { - let autoscroll_request = editor.autoscroll_request(); + let autoscroll_request = editor.scroll_manager.take_autoscroll_request(); + let autoscroll_containing_element = autoscroll_request.is_some() || editor.has_pending_selection(); let (needs_horizontal_autoscroll, was_scrolled) = editor - .autoscroll_vertically(bounds, line_height, max_scroll_top, window, cx); + .autoscroll_vertically( + bounds, + line_height, + max_scroll_top, + autoscroll_request, + window, + cx, + ); if was_scrolled.0 { snapshot = editor.snapshot(window, cx); } @@ -8425,7 +8433,11 @@ impl Element for EditorElement { Ok(blocks) => blocks, Err(resized_blocks) => { self.editor.update(cx, |editor, cx| { - editor.resize_blocks(resized_blocks, autoscroll_request, cx) + editor.resize_blocks( + resized_blocks, + autoscroll_request.map(|(autoscroll, _)| autoscroll), + cx, + ) }); return self.prepaint(None, _inspector_id, bounds, &mut (), window, cx); } @@ -8470,6 +8482,7 @@ impl Element for EditorElement { scroll_width, em_advance, &line_layouts, + autoscroll_request, window, cx, ) diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index ecaf7c11e4..08ff23f8f7 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -348,8 +348,8 @@ impl ScrollManager { self.show_scrollbars } - pub fn autoscroll_request(&self) -> Option { - self.autoscroll_request.map(|(autoscroll, _)| autoscroll) + pub fn take_autoscroll_request(&mut self) -> Option<(Autoscroll, bool)> { + self.autoscroll_request.take() } pub fn active_scrollbar_state(&self) -> Option<&ActiveScrollbarState> { diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index e8a1f8da73..88d3b52d76 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -102,15 +102,12 @@ impl AutoscrollStrategy { pub(crate) struct NeedsHorizontalAutoscroll(pub(crate) bool); impl Editor { - pub fn autoscroll_request(&self) -> Option { - self.scroll_manager.autoscroll_request() - } - pub(crate) fn autoscroll_vertically( &mut self, bounds: Bounds, line_height: Pixels, max_scroll_top: f32, + autoscroll_request: Option<(Autoscroll, bool)>, window: &mut Window, cx: &mut Context, ) -> (NeedsHorizontalAutoscroll, WasScrolled) { @@ -137,7 +134,7 @@ impl Editor { WasScrolled(false) }; - let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else { + let Some((autoscroll, local)) = autoscroll_request else { return (NeedsHorizontalAutoscroll(false), editor_was_scrolled); }; @@ -284,9 +281,12 @@ impl Editor { scroll_width: Pixels, em_advance: Pixels, layouts: &[LineWithInvisibles], + autoscroll_request: Option<(Autoscroll, bool)>, window: &mut Window, cx: &mut Context, ) -> Option> { + let (_, local) = autoscroll_request?; + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let selections = self.selections.all::(cx); let mut scroll_position = self.scroll_manager.scroll_position(&display_map); @@ -335,10 +335,10 @@ impl Editor { let was_scrolled = if target_left < scroll_left { scroll_position.x = target_left / em_advance; - self.set_scroll_position_internal(scroll_position, true, true, window, cx) + self.set_scroll_position_internal(scroll_position, local, true, window, cx) } else if target_right > scroll_right { scroll_position.x = (target_right - viewport_width) / em_advance; - self.set_scroll_position_internal(scroll_position, true, true, window, cx) + self.set_scroll_position_internal(scroll_position, local, true, window, cx) } else { WasScrolled(false) }; From 35cd1b9ae15dcc528567b5983b85a5f5a4fe928a Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 7 Aug 2025 18:01:46 -0700 Subject: [PATCH 180/693] filter out comments in deploy helper env vars (#35847) Turns out a `.sh` file isn't actually a shell script :( Release Notes: - N/A --- script/lib/deploy-helpers.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/lib/deploy-helpers.sh b/script/lib/deploy-helpers.sh index c0feb2f861..bd7b3c4d6f 100644 --- a/script/lib/deploy-helpers.sh +++ b/script/lib/deploy-helpers.sh @@ -5,7 +5,7 @@ function export_vars_for_environment { echo "Invalid environment name '${environment}'" >&2 exit 1 fi - export $(cat $env_file) + export $(grep -v '^#' $env_file | grep -v '^[[:space:]]*$') } function target_zed_kube_cluster { From cdfb3348ea0adf96b273a2ffa43feef234f34cc9 Mon Sep 17 00:00:00 2001 From: Abdelhakim Qbaich Date: Thu, 7 Aug 2025 21:35:07 -0400 Subject: [PATCH 181/693] git: Make inline blame padding configurable (#33631) Just like with diagnostics, adding a configurable padding to inline blame Release Notes: - Added configurable padding to inline blame --------- Co-authored-by: Cole Miller Co-authored-by: Peter Tripp --- assets/settings/default.json | 3 ++ crates/collab/src/tests/editor_tests.rs | 4 +-- crates/editor/src/element.rs | 19 +++++++++---- crates/project/src/project_settings.rs | 37 ++++++++++++++++++++----- docs/src/configuring-zed.md | 15 ++++++++-- docs/src/visual-customization.md | 1 + 6 files changed, 60 insertions(+), 19 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 4734b5d118..d69fd58009 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1171,6 +1171,9 @@ // Sets a delay after which the inline blame information is shown. // Delay is restarted with every cursor movement. "delay_ms": 0, + // The amount of padding between the end of the source line and the start + // of the inline blame in units of em widths. + "padding": 7, // Whether or not to display the git commit summary on the same line. "show_commit_summary": false, // The minimum column number to show the inline blame information at diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 8754b53f6e..7b95fdd458 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -3101,9 +3101,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA // Turn inline-blame-off by default so no state is transferred without us explicitly doing so let inline_blame_off_settings = Some(InlineBlameSettings { enabled: false, - delay_ms: None, - min_column: None, - show_commit_summary: false, + ..Default::default() }); cx_a.update(|cx| { SettingsStore::update_global(cx, |store, cx| { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 428a30471d..a7fd0abf88 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -86,8 +86,6 @@ use util::post_inc; use util::{RangeExt, ResultExt, debug_panic}; use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt}; -const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.; - /// Determines what kinds of highlights should be applied to a lines background. #[derive(Clone, Copy, Default)] struct LineHighlightSpec { @@ -2428,10 +2426,13 @@ impl EditorElement { let editor = self.editor.read(cx); let blame = editor.blame.clone()?; let padding = { - const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.; const INLINE_ACCEPT_SUGGESTION_EM_WIDTHS: f32 = 14.; - let mut padding = INLINE_BLAME_PADDING_EM_WIDTHS; + let mut padding = ProjectSettings::get_global(cx) + .git + .inline_blame + .unwrap_or_default() + .padding as f32; if let Some(edit_prediction) = editor.active_edit_prediction.as_ref() { match &edit_prediction.completion { @@ -2469,7 +2470,7 @@ impl EditorElement { let min_column_in_pixels = ProjectSettings::get_global(cx) .git .inline_blame - .and_then(|settings| settings.min_column) + .map(|settings| settings.min_column) .map(|col| self.column_pixels(col as usize, window)) .unwrap_or(px(0.)); let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels; @@ -8365,7 +8366,13 @@ impl Element for EditorElement { }) .flatten()?; let mut element = render_inline_blame_entry(blame_entry, &style, cx)?; - let inline_blame_padding = INLINE_BLAME_PADDING_EM_WIDTHS * em_advance; + let inline_blame_padding = ProjectSettings::get_global(cx) + .git + .inline_blame + .unwrap_or_default() + .padding + as f32 + * em_advance; Some( element .layout_as_root(AvailableSpace::min_size(), window, cx) diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 20be7fef85..12e3aa88ad 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -431,10 +431,9 @@ impl GitSettings { pub fn inline_blame_delay(&self) -> Option { match self.inline_blame { - Some(InlineBlameSettings { - delay_ms: Some(delay_ms), - .. - }) if delay_ms > 0 => Some(Duration::from_millis(delay_ms)), + Some(InlineBlameSettings { delay_ms, .. }) if delay_ms > 0 => { + Some(Duration::from_millis(delay_ms)) + } _ => None, } } @@ -470,7 +469,7 @@ pub enum GitGutterSetting { Hide, } -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct InlineBlameSettings { /// Whether or not to show git blame data inline in @@ -483,11 +482,19 @@ pub struct InlineBlameSettings { /// after a delay once the cursor stops moving. /// /// Default: 0 - pub delay_ms: Option, + #[serde(default)] + pub delay_ms: u64, + /// The amount of padding between the end of the source line and the start + /// of the inline blame in units of columns. + /// + /// Default: 7 + #[serde(default = "default_inline_blame_padding")] + pub padding: u32, /// The minimum column number to show the inline blame information at /// /// Default: 0 - pub min_column: Option, + #[serde(default)] + pub min_column: u32, /// Whether to show commit summary as part of the inline blame. /// /// Default: false @@ -495,6 +502,22 @@ pub struct InlineBlameSettings { pub show_commit_summary: bool, } +fn default_inline_blame_padding() -> u32 { + 7 +} + +impl Default for InlineBlameSettings { + fn default() -> Self { + Self { + enabled: true, + delay_ms: 0, + padding: default_inline_blame_padding(), + min_column: 0, + show_commit_summary: false, + } + } +} + #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct BinarySettings { pub path: Option, diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 5fd27abad6..d2ca0e0604 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1795,7 +1795,6 @@ Example: { "git": { "inline_blame": { - "enabled": true, "delay_ms": 500 } } @@ -1808,7 +1807,6 @@ Example: { "git": { "inline_blame": { - "enabled": true, "show_commit_summary": true } } @@ -1821,13 +1819,24 @@ Example: { "git": { "inline_blame": { - "enabled": true, "min_column": 80 } } } ``` +5. Set the padding between the end of the line and the inline blame hint, in ems: + +```json +{ + "git": { + "inline_blame": { + "padding": 10 + } + } +} +``` + ### Hunk Style - Description: What styling we should use for the diff hunks. diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 8b307d97d5..34ce067eba 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -223,6 +223,7 @@ TBD: Centered layout related settings "enabled": true, // Show/hide inline blame "delay": 0, // Show after delay (ms) "min_column": 0, // Minimum column to inline display blame + "padding": 7, // Padding between code and inline blame (em) "show_commit_summary": false // Show/hide commit summary }, "hunk_style": "staged_hollow" // staged_hollow, unstaged_hollow From 00701b5e99b795a461b884c49a7a9d88bb51b6d3 Mon Sep 17 00:00:00 2001 From: Neo Nie Date: Fri, 8 Aug 2025 02:39:32 +0100 Subject: [PATCH 182/693] git_hosting_providers: Extract Bitbucket pull request number (#34584) git: Extract Bitbucket pull request number Release Notes: - git: Extract Bitbucket pull request number --------- Co-authored-by: Peter Tripp --- .../src/providers/bitbucket.rs | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/crates/git_hosting_providers/src/providers/bitbucket.rs b/crates/git_hosting_providers/src/providers/bitbucket.rs index 074a169135..26df7b567a 100644 --- a/crates/git_hosting_providers/src/providers/bitbucket.rs +++ b/crates/git_hosting_providers/src/providers/bitbucket.rs @@ -1,12 +1,22 @@ use std::str::FromStr; +use std::sync::LazyLock; +use regex::Regex; use url::Url; use git::{ BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote, - RemoteUrl, + PullRequest, RemoteUrl, }; +fn pull_request_regex() -> &'static Regex { + static PULL_REQUEST_REGEX: LazyLock = LazyLock::new(|| { + // This matches Bitbucket PR reference pattern: (pull request #xxx) + Regex::new(r"\(pull request #(\d+)\)").unwrap() + }); + &PULL_REQUEST_REGEX +} + pub struct Bitbucket { name: String, base_url: Url, @@ -96,6 +106,22 @@ impl GitHostingProvider for Bitbucket { ); permalink } + + fn extract_pull_request(&self, remote: &ParsedGitRemote, message: &str) -> Option { + // Check first line of commit message for PR references + let first_line = message.lines().next()?; + + // Try to match against our PR patterns + let capture = pull_request_regex().captures(first_line)?; + let number = capture.get(1)?.as_str().parse::().ok()?; + + // Construct the PR URL in Bitbucket format + let mut url = self.base_url(); + let path = format!("/{}/{}/pull-requests/{}", remote.owner, remote.repo, number); + url.set_path(&path); + + Some(PullRequest { number, url }) + } } #[cfg(test)] @@ -203,4 +229,34 @@ mod tests { "https://bitbucket.org/zed-industries/zed/src/f00b4r/main.rs#lines-24:48"; assert_eq!(permalink.to_string(), expected_url.to_string()) } + + #[test] + fn test_bitbucket_pull_requests() { + use indoc::indoc; + + let remote = ParsedGitRemote { + owner: "zed-industries".into(), + repo: "zed".into(), + }; + + let bitbucket = Bitbucket::public_instance(); + + // Test message without PR reference + let message = "This does not contain a pull request"; + assert!(bitbucket.extract_pull_request(&remote, message).is_none()); + + // Pull request number at end of first line + let message = indoc! {r#" + Merged in feature-branch (pull request #123) + + Some detailed description of the changes. + "#}; + + let pr = bitbucket.extract_pull_request(&remote, message).unwrap(); + assert_eq!(pr.number, 123); + assert_eq!( + pr.url.as_str(), + "https://bitbucket.org/zed-industries/zed/pull-requests/123" + ); + } } From 34fc2fd9d0b760dfc6c8518c66eaa933a37b8c96 Mon Sep 17 00:00:00 2001 From: Phoenix Himself Date: Fri, 8 Aug 2025 03:54:42 +0200 Subject: [PATCH 183/693] Treat Arduino files as C++ (#35467) Closes https://github.com/zed-industries/zed/discussions/35466 Release Notes: - N/A --- crates/languages/src/cpp/config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/languages/src/cpp/config.toml b/crates/languages/src/cpp/config.toml index fab88266d7..7e24415f9d 100644 --- a/crates/languages/src/cpp/config.toml +++ b/crates/languages/src/cpp/config.toml @@ -1,6 +1,6 @@ name = "C++" grammar = "cpp" -path_suffixes = ["cc", "hh", "cpp", "h", "hpp", "cxx", "hxx", "c++", "ipp", "inl", "ixx", "cu", "cuh", "C", "H"] +path_suffixes = ["cc", "hh", "cpp", "h", "hpp", "cxx", "hxx", "c++", "ipp", "inl", "ino", "ixx", "cu", "cuh", "C", "H"] line_comments = ["// ", "/// ", "//! "] decrease_indent_patterns = [ { pattern = "^\\s*\\{.*\\}?\\s*$", valid_after = ["if", "for", "while", "do", "switch", "else"] }, From 0dd480d475f7e8141e560785033b8e39411ca775 Mon Sep 17 00:00:00 2001 From: Dan Wood Date: Fri, 8 Aug 2025 03:58:26 +0200 Subject: [PATCH 184/693] Add spread operator to the @operator list for ECMAScript languages (#35360) Previously, this was the one thing that could not be styled properly in ecmascript languages in the zed config, because it was not able to be targeted. Now, it is added alongside other operators. This has been tested and works as expected. Release Notes: - N/A --- crates/languages/src/javascript/highlights.scm | 1 + crates/languages/src/tsx/highlights.scm | 1 + crates/languages/src/typescript/highlights.scm | 1 + 3 files changed, 3 insertions(+) diff --git a/crates/languages/src/javascript/highlights.scm b/crates/languages/src/javascript/highlights.scm index 73cb1a5e45..9d5ebbaf71 100644 --- a/crates/languages/src/javascript/highlights.scm +++ b/crates/languages/src/javascript/highlights.scm @@ -146,6 +146,7 @@ "&&=" "||=" "??=" + "..." ] @operator (regex "/" @string.regex) diff --git a/crates/languages/src/tsx/highlights.scm b/crates/languages/src/tsx/highlights.scm index e2837c61fd..5e2fbbf63a 100644 --- a/crates/languages/src/tsx/highlights.scm +++ b/crates/languages/src/tsx/highlights.scm @@ -146,6 +146,7 @@ "&&=" "||=" "??=" + "..." ] @operator (regex "/" @string.regex) diff --git a/crates/languages/src/typescript/highlights.scm b/crates/languages/src/typescript/highlights.scm index 486e5a7684..af37ef6415 100644 --- a/crates/languages/src/typescript/highlights.scm +++ b/crates/languages/src/typescript/highlights.scm @@ -167,6 +167,7 @@ "&&=" "||=" "??=" + "..." ] @operator (regex "/" @string.regex) From d6022dc87ceb2fd83ffbee062db82610901c8c7b Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 8 Aug 2025 04:50:54 +0200 Subject: [PATCH 185/693] emmet: Enable in Vue.js files (#35599) Resolves part of #34337 Actually I need also to add: ``` "languages": { "Vue.js": { "language_servers": [ "vue-language-server", "emmet-language-server", "..." ] } }, ``` not sure how to resolve fully, happy to continue only little guidance needed. Release Notes: - allow emmet in Vue.js files --------- Co-authored-by: Marshall Bowers --- extensions/emmet/extension.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/emmet/extension.toml b/extensions/emmet/extension.toml index 99aa80a2d4..9fa14d091f 100644 --- a/extensions/emmet/extension.toml +++ b/extensions/emmet/extension.toml @@ -9,7 +9,7 @@ repository = "https://github.com/zed-industries/zed" [language_servers.emmet-language-server] name = "Emmet Language Server" language = "HTML" -languages = ["HTML", "PHP", "ERB", "HTML/ERB", "JavaScript", "TSX", "CSS", "HEEX", "Elixir"] +languages = ["HTML", "PHP", "ERB", "HTML/ERB", "JavaScript", "TSX", "CSS", "HEEX", "Elixir", "Vue.js"] [language_servers.emmet-language-server.language_ids] "HTML" = "html" @@ -21,3 +21,4 @@ languages = ["HTML", "PHP", "ERB", "HTML/ERB", "JavaScript", "TSX", "CSS", "HEEX "CSS" = "css" "HEEX" = "heex" "Elixir" = "heex" +"Vue.js" = "vue" From 9edc01d9a528e06d9fb643b9bf61ae5bd4e52f4e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 7 Aug 2025 21:47:16 -0700 Subject: [PATCH 186/693] Update nightly icon on windows (#35812) Release Notes: - N/A --- .../resources/windows/app-icon-nightly.ico | Bin 185900 -> 149224 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/crates/zed/resources/windows/app-icon-nightly.ico b/crates/zed/resources/windows/app-icon-nightly.ico index 15e06a6e17631ddd9aed52679b328a46ffe482ff..165e4ce1f7ccbff084fbbf8131fb3c8e27853f4b 100644 GIT binary patch literal 149224 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

x8_y)-@rqrF+7QAFlcfxBWFdqUzM6QMXRK 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ΠKz3mwEJf{tphjPapdaT&>C8mv6bhe|Oc#pZPbR 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�^VKc%wt|I~_- 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

Fe+Ermhr^929fhzR|+5E4MapgJBDS9Zbz|7}5Q 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

iNpP9s0yzxIz{(1xc+;z7> z=kS60fYP8n)SxqJs<-|ZEZYP*e+Sfms@wwZ+kwxnsow!U*GRB>>wkf&ZMQ*Z{P7iR z_%96VM}y9Bs@(dYt9vXD#3QpRH`me~yZ+|9L@uNKhXLbgmJozgG`B z2WRVl!3xkBIpFiHzqI7Zrk;ruW{#pp7y=}LHl?^m!JFZzx?cfk42~cJIz1&Uv=`~|Dm9@ zzo5O)p#5nR4ubkBf22G2{RQnI1??y0?b!RjeD|&Y!ae){KmGFaf5fIM|DBhg{~x*G z%73%@r~bRIIR9T5w2yw$VbESw5dN(&?dV_7{v*X%$NuwAIr6{#_=EpGYcKwHTz%pH z``>@T`)m~EpZc%8=*)l1<>&uP&pq)U#0T%koPYAS%KTG*LG44;MW_FZ%sutL^Yo+t z343n-588hH|G_68{{Q^__rK)A)Bj}_o%ye`;{1O_&>pGf=l+A*d#WqW{nlJ@{;$sJ z^Z&I#>(rK<{oi}}+5eQocm7Yj^8A0)!8`vmkKOI?rBLHjU3>%l?oFbMvw zv;Oj5(Aq)$O;`Ve+60qtzWVR5>&E}o6A%BVo_zQ}<;27P8K)lozw`Mg_%1zdP}^tQ zwg0-?K<$(3e?V(0jX>)*w?Wo1>utRHf5x3R|3T(Vxbf{txJdWPK?R(;Z0qFjt K1`0|hkX``1cXoCF From 0169bddb592e3563f8b36f0f8fddf997ad9cf062 Mon Sep 17 00:00:00 2001 From: maan2003 <49202620+maan2003@users.noreply.github.com> Date: Fri, 8 Aug 2025 10:32:11 +0530 Subject: [PATCH 187/693] project panel: Add setting to disable auto opening project panel (#34752) Release Notes: - Add `project_panel.starts_open` to control opening project panel in new projects. --- assets/settings/default.json | 2 ++ crates/project_panel/src/project_panel.rs | 4 ++++ crates/project_panel/src/project_panel_settings.rs | 5 +++++ docs/src/configuring-zed.md | 3 ++- 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index d69fd58009..295bebe23a 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -596,6 +596,8 @@ // when a corresponding project entry becomes active. // Gitignored entries are never auto revealed. "auto_reveal_entries": true, + // Whether the project panel should open on startup. + "starts_open": true, // Whether to fold directories automatically and show compact folders // (e.g. "a/b/c" ) when a directory has only one subdirectory inside. "auto_fold_dirs": true, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 45581a97c4..94d543ed0c 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -5625,6 +5625,10 @@ impl Panel for ProjectPanel { } fn starts_open(&self, _: &Window, cx: &App) -> bool { + if !ProjectPanelSettings::get_global(cx).starts_open { + return false; + } + let project = &self.project.read(cx); project.visible_worktrees(cx).any(|tree| { tree.read(cx) diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index 9057480972..8a243589ed 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -43,6 +43,7 @@ pub struct ProjectPanelSettings { pub sticky_scroll: bool, pub auto_reveal_entries: bool, pub auto_fold_dirs: bool, + pub starts_open: bool, pub scrollbar: ScrollbarSettings, pub show_diagnostics: ShowDiagnostics, pub hide_root: bool, @@ -139,6 +140,10 @@ pub struct ProjectPanelSettingsContent { /// /// Default: true pub auto_fold_dirs: Option, + /// Whether the project panel should open on startup. + /// + /// Default: true + pub starts_open: Option, /// Scrollbar-related settings pub scrollbar: Option, /// Which files containing diagnostic errors/warnings to mark in the project panel. diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index d2ca0e0604..67f1cd000b 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -3213,7 +3213,8 @@ Run the `theme selector: toggle` action in the command palette to see a current "indent_guides": { "show": "always" }, - "hide_root": false + "hide_root": false, + "starts_open": true } } ``` From 2c7251e4f9177e047b58f53f7b3f8fed9e928fa1 Mon Sep 17 00:00:00 2001 From: zumbalogy Date: Thu, 7 Aug 2025 22:04:30 -0700 Subject: [PATCH 188/693] Add setting to hide active language button in the status bar (#33977) Release Notes: - Added settings status_bar.show_active_language_button to show/hide the language button in the status bar. The motivation for this is visual, I have had zero issues with its functionality. The language switcher can still be accessed by the command palette, menu, or a keyboard shortcut. ------ This is my first Zed and first Rust PR, so criticism is very welcome. I know there has been discussion around how the status bar settings are structured and named, and I am happy to change it to whatever is best. I was also not sure what order to put it in in the settings default.json. Feedback welcome. Here is a picture of it in action: ![image](https://github.com/user-attachments/assets/c50131e2-71aa-4fab-8db0-8b2aae586e71) --------- Co-authored-by: zumbalogy <3770982+zumbalogy@users.noreply.github.com> Co-authored-by: Kirill Bulatov --- assets/settings/default.json | 5 +++++ crates/editor/src/editor_settings.rs | 20 +++++++++++++++++++ .../src/active_buffer_language.rs | 10 +++++++++- docs/src/configuring-zed.md | 12 +++++++++++ docs/src/visual-customization.md | 11 ++++++++++ 5 files changed, 57 insertions(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 295bebe23a..9c579b858d 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1238,6 +1238,11 @@ // 2. hour24 "hour_format": "hour12" }, + // Status bar-related settings. + "status_bar": { + // Whether to show the active language button in the status bar. + "active_language_button": true + }, // Settings specific to the terminal "terminal": { // What shell to use when opening a terminal. May take 3 values: diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 14f46c0e60..3d132651b8 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -20,6 +20,7 @@ pub struct EditorSettings { pub lsp_highlight_debounce: u64, pub hover_popover_enabled: bool, pub hover_popover_delay: u64, + pub status_bar: StatusBar, pub toolbar: Toolbar, pub scrollbar: Scrollbar, pub minimap: Minimap, @@ -125,6 +126,14 @@ pub struct JupyterContent { pub enabled: Option, } +#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct StatusBar { + /// Whether to display the active language button in the status bar. + /// + /// Default: true + pub active_language_button: bool, +} + #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct Toolbar { pub breadcrumbs: bool, @@ -440,6 +449,8 @@ pub struct EditorSettingsContent { /// /// Default: 300 pub hover_popover_delay: Option, + /// Status bar related settings + pub status_bar: Option, /// Toolbar related settings pub toolbar: Option, /// Scrollbar related settings @@ -567,6 +578,15 @@ pub struct EditorSettingsContent { pub lsp_document_colors: Option, } +// Status bar related settings +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct StatusBarContent { + /// Whether to display the active language button in the status bar. + /// + /// Default: true + pub active_language_button: Option, +} + // Toolbar related settings #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct ToolbarContent { diff --git a/crates/language_selector/src/active_buffer_language.rs b/crates/language_selector/src/active_buffer_language.rs index 250d0c23d8..c5c5eceab5 100644 --- a/crates/language_selector/src/active_buffer_language.rs +++ b/crates/language_selector/src/active_buffer_language.rs @@ -1,8 +1,9 @@ -use editor::Editor; +use editor::{Editor, EditorSettings}; use gpui::{ Context, Entity, IntoElement, ParentElement, Render, Subscription, WeakEntity, Window, div, }; use language::LanguageName; +use settings::Settings as _; use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, Tooltip}; use workspace::{StatusItemView, Workspace, item::ItemHandle}; @@ -39,6 +40,13 @@ impl ActiveBufferLanguage { impl Render for ActiveBufferLanguage { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + if !EditorSettings::get_global(cx) + .status_bar + .active_language_button + { + return div(); + } + div().when_some(self.active_language.as_ref(), |el, active_language| { let active_language_text = if let Some(active_language_text) = active_language { active_language_text.to_string() diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 67f1cd000b..1996e1c4ee 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1275,6 +1275,18 @@ Each option controls displaying of a particular toolbar element. If all elements `boolean` values +## Status Bar + +- Description: Control various elements in the status bar. Note that some items in the status bar have their own settings set elsewhere. +- Setting: `status_bar` +- Default: + +```json +"status_bar": { + "active_language_button": true, +}, +``` + ## LSP - Description: Configuration for language servers. diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 34ce067eba..46de078d89 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -306,6 +306,17 @@ TBD: Centered layout related settings } ``` +### Status Bar + +```json + "status_bar": { + // Show/hide a button that displays the active buffer's language. + // Clicking the button brings up the language selector. + // Defaults to true. + "active_language_button": true, + }, +``` + ### Multibuffer ```json From 3bee803b518ee2a8d82bf63cb2dacbe98366103d Mon Sep 17 00:00:00 2001 From: Anne Schuth Date: Fri, 8 Aug 2025 07:05:56 +0200 Subject: [PATCH 189/693] Use TMPDIR environment variable in install script (#35636) ## Summary This PR updates the install script to respect the `TMPDIR` environment variable when creating temporary directories. ## Motivation Some environments have non-standard temporary directory locations or restrictions on `/tmp`. This change allows users to specify an alternative temporary directory by setting the `TMPDIR` environment variable. ## Changes - Check if `TMPDIR` is set and points to a valid directory - Use `$TMPDIR` for temporary files if available - Fall back to `/tmp` if `TMPDIR` is not set or invalid ## Testing Tested the script with: - `TMPDIR` unset (uses `/tmp` as before) - `TMPDIR` set to a valid directory (uses specified directory) - `TMPDIR` set to an invalid path (falls back to `/tmp`) This change maintains backward compatibility while adding flexibility for environments with non-standard temporary directory requirements. Release Notes: - N/A --- script/install.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/script/install.sh b/script/install.sh index 9cd21119b7..feb140c984 100755 --- a/script/install.sh +++ b/script/install.sh @@ -9,7 +9,12 @@ main() { platform="$(uname -s)" arch="$(uname -m)" channel="${ZED_CHANNEL:-stable}" - temp="$(mktemp -d "/tmp/zed-XXXXXX")" + # Use TMPDIR if available (for environments with non-standard temp directories) + if [ -n "${TMPDIR:-}" ] && [ -d "${TMPDIR}" ]; then + temp="$(mktemp -d "$TMPDIR/zed-XXXXXX")" + else + temp="$(mktemp -d "/tmp/zed-XXXXXX")" + fi if [ "$platform" = "Darwin" ]; then platform="macos" From edef1f1470e3219a2f858e0b8296d31c9fb4e16e Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Fri, 8 Aug 2025 02:26:53 -0300 Subject: [PATCH 190/693] Fix acp generating status after stop (#35852) Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 71 ++++++++++++++++------------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 71827d6948..443375a51b 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -6,7 +6,6 @@ use anyhow::{Context as _, Result}; use assistant_tool::ActionLog; use buffer_diff::BufferDiff; use editor::{Bias, MultiBuffer, PathKey}; -use futures::future::{Fuse, FusedFuture}; use futures::{FutureExt, channel::oneshot, future::BoxFuture}; use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task}; use itertools::Itertools; @@ -580,7 +579,7 @@ pub struct AcpThread { project: Entity, action_log: Entity, shared_buffers: HashMap, BufferSnapshot>, - send_task: Option>>, + send_task: Option>, connection: Rc, session_id: acp::SessionId, } @@ -670,11 +669,7 @@ impl AcpThread { } pub fn status(&self) -> ThreadStatus { - if self - .send_task - .as_ref() - .map_or(false, |t| !t.is_terminated()) - { + if self.send_task.is_some() { if self.waiting_for_tool_confirmation() { ThreadStatus::WaitingForToolConfirmation } else { @@ -1049,31 +1044,29 @@ impl AcpThread { let (tx, rx) = oneshot::channel(); let cancel_task = self.cancel(cx); - self.send_task = Some( - cx.spawn(async move |this, cx| { - async { - cancel_task.await; + self.send_task = Some(cx.spawn(async move |this, cx| { + async { + cancel_task.await; - let result = this - .update(cx, |this, cx| { - this.connection.prompt( - acp::PromptRequest { - prompt: message, - session_id: this.session_id.clone(), - }, - cx, - ) - })? - .await; + let result = this + .update(cx, |this, cx| { + this.connection.prompt( + acp::PromptRequest { + prompt: message, + session_id: this.session_id.clone(), + }, + cx, + ) + })? + .await; - tx.send(result).log_err(); - anyhow::Ok(()) - } - .await - .log_err(); - }) - .fuse(), - ); + tx.send(result).log_err(); + + anyhow::Ok(()) + } + .await + .log_err(); + })); cx.spawn(async move |this, cx| match rx.await { Ok(Err(e)) => { @@ -1081,7 +1074,23 @@ impl AcpThread { .log_err(); Err(e)? } - _ => { + result => { + let cancelled = matches!( + result, + Ok(Ok(acp::PromptResponse { + stop_reason: acp::StopReason::Cancelled + })) + ); + + // We only take the task if the current prompt wasn't cancelled. + // + // This prompt may have been cancelled because another one was sent + // while it was still generating. In these cases, dropping `send_task` + // would cause the next generation to be cancelled. + if !cancelled { + this.update(cx, |this, _cx| this.send_task.take()).ok(); + } + this.update(cx, |_, cx| cx.emit(AcpThreadEvent::Stopped)) .log_err(); Ok(()) From 738968e90cf139b61bea6c827bbf40169bca1a37 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 8 Aug 2025 07:32:51 +0200 Subject: [PATCH 191/693] editor: Consider mixed hover link kinds when navigating to multibuffer (#35828) Previously when handling multiple hover links we filtered non-location links out which may end up with a single location entry only, resulting in us opening a multi buffer for a single location. This changes the logic to do the filtering first, then deciding on whether to open a single buffer or multi buffer. Closes https://github.com/zed-industries/zed/issues/6730 Release Notes: - N/A --- crates/editor/src/editor.rs | 215 ++++++++++++++++-------------------- 1 file changed, 93 insertions(+), 122 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bd7963a2e2..d1bf95c794 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -118,7 +118,7 @@ use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file}; use hover_popover::{HoverState, hide_hover}; use indent_guides::ActiveIndentGuidesState; use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; -use itertools::Itertools; +use itertools::{Either, Itertools}; use language::{ AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow, BufferSnapshot, Capability, CharClassifier, CharKind, CodeLabel, CursorShape, DiagnosticEntry, @@ -15666,12 +15666,9 @@ impl Editor { }; let head = self.selections.newest::(cx).head(); let buffer = self.buffer.read(cx); - let (buffer, head) = if let Some(text_anchor) = buffer.text_anchor_for_position(head, cx) { - text_anchor - } else { + let Some((buffer, head)) = buffer.text_anchor_for_position(head, cx) else { return Task::ready(Ok(Navigated::No)); }; - let Some(definitions) = provider.definitions(&buffer, head, kind, cx) else { return Task::ready(Ok(Navigated::No)); }; @@ -15776,62 +15773,109 @@ impl Editor { pub(crate) fn navigate_to_hover_links( &mut self, kind: Option, - mut definitions: Vec, + definitions: Vec, split: bool, window: &mut Window, cx: &mut Context, ) -> Task> { - // If there is one definition, just open it directly - if definitions.len() == 1 { - let definition = definitions.pop().unwrap(); - - enum TargetTaskResult { - Location(Option), - AlreadyNavigated, - } - - let target_task = match definition { - HoverLink::Text(link) => { - Task::ready(anyhow::Ok(TargetTaskResult::Location(Some(link.target)))) - } + // Separate out url and file links, we can only handle one of them at most or an arbitrary number of locations + let mut first_url_or_file = None; + let definitions: Vec<_> = definitions + .into_iter() + .filter_map(|def| match def { + HoverLink::Text(link) => Some(Task::ready(anyhow::Ok(Some(link.target)))), HoverLink::InlayHint(lsp_location, server_id) => { let computation = self.compute_target_location(lsp_location, server_id, window, cx); - cx.background_spawn(async move { - let location = computation.await?; - Ok(TargetTaskResult::Location(location)) - }) + Some(cx.background_spawn(computation)) } HoverLink::Url(url) => { - cx.open_url(&url); - Task::ready(Ok(TargetTaskResult::AlreadyNavigated)) + first_url_or_file = Some(Either::Left(url)); + None } HoverLink::File(path) => { - if let Some(workspace) = self.workspace() { - cx.spawn_in(window, async move |_, cx| { - workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_resolved_path(path, window, cx) - })? - .await - .map(|_| TargetTaskResult::AlreadyNavigated) - }) - } else { - Task::ready(Ok(TargetTaskResult::Location(None))) - } + first_url_or_file = Some(Either::Right(path)); + None } - }; - cx.spawn_in(window, async move |editor, cx| { - let target = match target_task.await.context("target resolution task")? { - TargetTaskResult::AlreadyNavigated => return Ok(Navigated::Yes), - TargetTaskResult::Location(None) => return Ok(Navigated::No), - TargetTaskResult::Location(Some(target)) => target, + }) + .collect(); + + let workspace = self.workspace(); + + cx.spawn_in(window, async move |editor, acx| { + let mut locations: Vec = future::join_all(definitions) + .await + .into_iter() + .filter_map(|location| location.transpose()) + .collect::>() + .context("location tasks")?; + + if locations.len() > 1 { + let Some(workspace) = workspace else { + return Ok(Navigated::No); }; - editor.update_in(cx, |editor, window, cx| { - let Some(workspace) = editor.workspace() else { - return Navigated::No; - }; + let tab_kind = match kind { + Some(GotoDefinitionKind::Implementation) => "Implementations", + _ => "Definitions", + }; + let title = editor + .update_in(acx, |_, _, cx| { + let origin = locations.first().unwrap(); + let buffer = origin.buffer.read(cx); + format!( + "{} for {}", + tab_kind, + buffer + .text_for_range(origin.range.clone()) + .collect::() + ) + }) + .context("buffer title")?; + + let opened = workspace + .update_in(acx, |workspace, window, cx| { + Self::open_locations_in_multibuffer( + workspace, + locations, + title, + split, + MultibufferSelectionMode::First, + window, + cx, + ) + }) + .is_ok(); + + anyhow::Ok(Navigated::from_bool(opened)) + } else if locations.is_empty() { + // If there is one definition, just open it directly + match first_url_or_file { + Some(Either::Left(url)) => { + acx.update(|_, cx| cx.open_url(&url))?; + Ok(Navigated::Yes) + } + Some(Either::Right(path)) => { + let Some(workspace) = workspace else { + return Ok(Navigated::No); + }; + + workspace + .update_in(acx, |workspace, window, cx| { + workspace.open_resolved_path(path, window, cx) + })? + .await?; + Ok(Navigated::Yes) + } + None => Ok(Navigated::No), + } + } else { + let Some(workspace) = workspace else { + return Ok(Navigated::No); + }; + + let target = locations.pop().unwrap(); + editor.update_in(acx, |editor, window, cx| { let pane = workspace.read(cx).active_pane().clone(); let range = target.range.to_point(target.buffer.read(cx)); @@ -15872,81 +15916,8 @@ impl Editor { } Navigated::Yes }) - }) - } else if !definitions.is_empty() { - cx.spawn_in(window, async move |editor, cx| { - let (title, location_tasks, workspace) = editor - .update_in(cx, |editor, window, cx| { - let tab_kind = match kind { - Some(GotoDefinitionKind::Implementation) => "Implementations", - _ => "Definitions", - }; - let title = definitions - .iter() - .find_map(|definition| match definition { - HoverLink::Text(link) => link.origin.as_ref().map(|origin| { - let buffer = origin.buffer.read(cx); - format!( - "{} for {}", - tab_kind, - buffer - .text_for_range(origin.range.clone()) - .collect::() - ) - }), - HoverLink::InlayHint(_, _) => None, - HoverLink::Url(_) => None, - HoverLink::File(_) => None, - }) - .unwrap_or(tab_kind.to_string()); - let location_tasks = definitions - .into_iter() - .map(|definition| match definition { - HoverLink::Text(link) => Task::ready(Ok(Some(link.target))), - HoverLink::InlayHint(lsp_location, server_id) => editor - .compute_target_location(lsp_location, server_id, window, cx), - HoverLink::Url(_) => Task::ready(Ok(None)), - HoverLink::File(_) => Task::ready(Ok(None)), - }) - .collect::>(); - (title, location_tasks, editor.workspace().clone()) - }) - .context("location tasks preparation")?; - - let locations: Vec = future::join_all(location_tasks) - .await - .into_iter() - .filter_map(|location| location.transpose()) - .collect::>() - .context("location tasks")?; - - if locations.is_empty() { - return Ok(Navigated::No); - } - - let Some(workspace) = workspace else { - return Ok(Navigated::No); - }; - - let opened = workspace - .update_in(cx, |workspace, window, cx| { - Self::open_locations_in_multibuffer( - workspace, - locations, - title, - split, - MultibufferSelectionMode::First, - window, - cx, - ) - }) - .ok(); - - anyhow::Ok(Navigated::from_bool(opened.is_some())) - }) - } else { - Task::ready(Ok(Navigated::No)) - } + } + }) } fn compute_target_location( From eb22639dff0fea60e9e14435b7f65277f02e7f3a Mon Sep 17 00:00:00 2001 From: Jakub Panek Date: Fri, 8 Aug 2025 08:49:36 +0200 Subject: [PATCH 192/693] cli: Use existing release channel name (#34771) Remove the local `RELEASE_CHANNEL` source that seems to be used only for Linux as opposed to `channel_release::CHANNEL_RELEASE_NAME` for other platform Windows: https://github.com/zed-industries/zed/blob/eee1b1f8a8ba47a14efc524a21b63d896b03feff/crates/cli/src/main.rs#L681-L685 Release Notes: - N/A --- crates/cli/src/main.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 287c62b753..8d6cd2544a 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -400,7 +400,6 @@ mod linux { os::unix::net::{SocketAddr, UnixDatagram}, path::{Path, PathBuf}, process::{self, ExitStatus}, - sync::LazyLock, thread, time::Duration, }; @@ -411,9 +410,6 @@ mod linux { use crate::{Detect, InstalledApp}; - static RELEASE_CHANNEL: LazyLock = - LazyLock::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string()); - struct App(PathBuf); impl Detect { @@ -444,10 +440,10 @@ mod linux { fn zed_version_string(&self) -> String { format!( "Zed {}{}{} – {}", - if *RELEASE_CHANNEL == "stable" { + if *release_channel::RELEASE_CHANNEL_NAME == "stable" { "".to_string() } else { - format!("{} ", *RELEASE_CHANNEL) + format!("{} ", *release_channel::RELEASE_CHANNEL_NAME) }, option_env!("RELEASE_VERSION").unwrap_or_default(), match option_env!("ZED_COMMIT_SHA") { @@ -459,7 +455,10 @@ mod linux { } fn launch(&self, ipc_url: String) -> anyhow::Result<()> { - let sock_path = paths::data_dir().join(format!("zed-{}.sock", *RELEASE_CHANNEL)); + let sock_path = paths::data_dir().join(format!( + "zed-{}.sock", + *release_channel::RELEASE_CHANNEL_NAME + )); let sock = UnixDatagram::unbound()?; if sock.connect(&sock_path).is_err() { self.boot_background(ipc_url)?; From 0097d896729ac4d5b3c215f61836bb10c8e7b41b Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 8 Aug 2025 09:43:49 +0200 Subject: [PATCH 193/693] language: Fix rust completion labels with `fullFunctionSignature` config (#35823) Release Notes: - N/A --- crates/languages/src/rust.rs | 108 ++++++++++++++++++++++++++--------- 1 file changed, 81 insertions(+), 27 deletions(-) diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index b6567c6e33..b52b1e7d55 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -15,6 +15,7 @@ use serde_json::json; use settings::Settings as _; use smol::fs::{self}; use std::fmt::Display; +use std::ops::Range; use std::{ any::Any, borrow::Cow, @@ -318,17 +319,16 @@ impl LspAdapter for RustLspAdapter { .label_details .as_ref() .and_then(|detail| detail.detail.as_deref()); - let mk_label = |text: String, runs| { + let mk_label = |text: String, filter_range: &dyn Fn() -> Range, runs| { let filter_range = completion .filter_text .as_deref() - .and_then(|filter| { - completion - .label - .find(filter) - .map(|ix| ix..ix + filter.len()) + .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len())) + .or_else(|| { + text.find(&completion.label) + .map(|ix| ix..ix + completion.label.len()) }) - .unwrap_or(0..completion.label.len()); + .unwrap_or_else(filter_range); CodeLabel { text, @@ -344,7 +344,7 @@ impl LspAdapter for RustLspAdapter { let source = Rope::from_iter([prefix, &text, " }"]); let runs = language.highlight_text(&source, prefix.len()..prefix.len() + text.len()); - mk_label(text, runs) + mk_label(text, &|| 0..completion.label.len(), runs) } ( Some(signature), @@ -356,7 +356,7 @@ impl LspAdapter for RustLspAdapter { let source = Rope::from_iter([prefix, &text, " = ();"]); let runs = language.highlight_text(&source, prefix.len()..prefix.len() + text.len()); - mk_label(text, runs) + mk_label(text, &|| 0..completion.label.len(), runs) } ( function_signature, @@ -375,22 +375,35 @@ impl LspAdapter for RustLspAdapter { .strip_prefix(prefix) .map(|suffix| (prefix, suffix)) }); - // fn keyword should be followed by opening parenthesis. - if let Some((prefix, suffix)) = fn_prefixed { - let label = if let Some(label) = completion - .label - .strip_suffix("(…)") - .or_else(|| completion.label.strip_suffix("()")) - { - label - } else { - &completion.label - }; + let label = if let Some(label) = completion + .label + .strip_suffix("(…)") + .or_else(|| completion.label.strip_suffix("()")) + { + label + } else { + &completion.label + }; + + static FULL_SIGNATURE_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"fn (.?+)\(").expect("Failed to create REGEX")); + if let Some((function_signature, match_)) = function_signature + .filter(|it| it.contains(&label)) + .and_then(|it| Some((it, FULL_SIGNATURE_REGEX.find(it)?))) + { + let source = Rope::from(function_signature); + let runs = language.highlight_text(&source, 0..function_signature.len()); + mk_label( + function_signature.to_owned(), + &|| match_.range().start - 3..match_.range().end - 1, + runs, + ) + } else if let Some((prefix, suffix)) = fn_prefixed { let text = format!("{label}{suffix}"); let source = Rope::from_iter([prefix, " ", &text, " {}"]); let run_start = prefix.len() + 1; let runs = language.highlight_text(&source, run_start..run_start + text.len()); - mk_label(text, runs) + mk_label(text, &|| 0..label.len(), runs) } else if completion .detail .as_ref() @@ -400,9 +413,15 @@ impl LspAdapter for RustLspAdapter { let len = text.len(); let source = Rope::from(text.as_str()); let runs = language.highlight_text(&source, 0..len); - mk_label(text, runs) + mk_label(text, &|| 0..completion.label.len(), runs) + } else if detail_left.is_none() { + return None; } else { - mk_label(completion.label.clone(), vec![]) + mk_label( + completion.label.clone(), + &|| 0..completion.label.len(), + vec![], + ) } } (_, kind) => { @@ -426,8 +445,11 @@ impl LspAdapter for RustLspAdapter { 0..label.rfind('(').unwrap_or(completion.label.len()), highlight_id, )); + } else if detail_left.is_none() { + return None; } - mk_label(label, runs) + + mk_label(label, &|| 0..completion.label.len(), runs) } }; @@ -1124,7 +1146,7 @@ mod tests { .await, Some(CodeLabel { text: "hello(&mut Option) -> Vec (use crate::foo)".to_string(), - filter_range: 0..10, + filter_range: 0..5, runs: vec![ (0..5, highlight_function), (7..10, highlight_keyword), @@ -1152,7 +1174,7 @@ mod tests { .await, Some(CodeLabel { text: "hello(&mut Option) -> Vec (use crate::foo)".to_string(), - filter_range: 0..10, + filter_range: 0..5, runs: vec![ (0..5, highlight_function), (7..10, highlight_keyword), @@ -1200,7 +1222,7 @@ mod tests { .await, Some(CodeLabel { text: "hello(&mut Option) -> Vec (use crate::foo)".to_string(), - filter_range: 0..10, + filter_range: 0..5, runs: vec![ (0..5, highlight_function), (7..10, highlight_keyword), @@ -1269,6 +1291,38 @@ mod tests { }) ); + assert_eq!( + adapter + .label_for_completion( + &lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::METHOD), + label: "as_deref_mut()".to_string(), + filter_text: Some("as_deref_mut".to_string()), + label_details: Some(CompletionItemLabelDetails { + detail: None, + description: Some( + "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string() + ), + }), + ..Default::default() + }, + &language + ) + .await, + Some(CodeLabel { + text: "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(), + filter_range: 7..19, + runs: vec![ + (0..3, HighlightId(1)), + (4..6, HighlightId(1)), + (7..19, HighlightId(2)), + (21..24, HighlightId(1)), + (34..41, HighlightId(0)), + (46..47, HighlightId(0)) + ], + }) + ); + assert_eq!( adapter .label_for_completion( From bc32b5a976436f941b7ba9a38f64e80dbcebb268 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 8 Aug 2025 13:32:58 +0100 Subject: [PATCH 194/693] Project panel faster (#35634) - **Use a struct instead of a thruple for visible worktree entries** - **Try some telemetry** Closes #ISSUE Release Notes: - N/A --------- Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> --- Cargo.lock | 1 + crates/project/src/git_store/git_traversal.rs | 6 +- crates/project_panel/Cargo.toml | 1 + crates/project_panel/src/project_panel.rs | 192 +++++++++++------- 4 files changed, 122 insertions(+), 78 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e63c5e2acf..0f0e78bb48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12653,6 +12653,7 @@ dependencies = [ "serde_json", "settings", "smallvec", + "telemetry", "theme", "ui", "util", diff --git a/crates/project/src/git_store/git_traversal.rs b/crates/project/src/git_store/git_traversal.rs index 777042cb02..bbcffe046d 100644 --- a/crates/project/src/git_store/git_traversal.rs +++ b/crates/project/src/git_store/git_traversal.rs @@ -110,11 +110,7 @@ impl<'a> GitTraversal<'a> { } pub fn advance(&mut self) -> bool { - self.advance_by(1) - } - - pub fn advance_by(&mut self, count: usize) -> bool { - let found = self.traversal.advance_by(count); + let found = self.traversal.advance_by(1); self.synchronize_statuses(false); found } diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index b9d43d9873..6ad3c4c2cd 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -41,6 +41,7 @@ worktree.workspace = true workspace.workspace = true language.workspace = true zed_actions.workspace = true +telemetry.workspace = true workspace-hack.workspace = true [dev-dependencies] diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 94d543ed0c..967df41e23 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -44,7 +44,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore, update_settings_file}; use smallvec::SmallVec; -use std::any::TypeId; +use std::{any::TypeId, time::Instant}; use std::{ cell::OnceCell, cmp, @@ -74,6 +74,12 @@ use zed_actions::OpenRecent; const PROJECT_PANEL_KEY: &str = "ProjectPanel"; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; +struct VisibleEntriesForWorktree { + worktree_id: WorktreeId, + entries: Vec, + index: OnceCell>>, +} + pub struct ProjectPanel { project: Entity, fs: Arc, @@ -82,7 +88,7 @@ pub struct ProjectPanel { // An update loop that keeps incrementing/decrementing scroll offset while there is a dragged entry that's // hovered over the start/end of a list. hover_scroll_task: Option>, - visible_entries: Vec<(WorktreeId, Vec, OnceCell>>)>, + visible_entries: Vec, /// Maps from leaf project entry ID to the currently selected ancestor. /// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several /// project entries (and all non-leaf nodes are guaranteed to be directories). @@ -116,6 +122,7 @@ pub struct ProjectPanel { hover_expand_task: Option>, previous_drag_position: Option>, sticky_items_count: usize, + last_reported_update: Instant, } struct DragTargetEntry { @@ -631,6 +638,7 @@ impl ProjectPanel { hover_expand_task: None, previous_drag_position: None, sticky_items_count: 0, + last_reported_update: Instant::now(), }; this.update_visible_entries(None, cx); @@ -1266,15 +1274,19 @@ impl ProjectPanel { entry_ix -= 1; } else if worktree_ix > 0 { worktree_ix -= 1; - entry_ix = self.visible_entries[worktree_ix].1.len() - 1; + entry_ix = self.visible_entries[worktree_ix].entries.len() - 1; } else { return; } - let (worktree_id, worktree_entries, _) = &self.visible_entries[worktree_ix]; + let VisibleEntriesForWorktree { + worktree_id, + entries, + .. + } = &self.visible_entries[worktree_ix]; let selection = SelectedEntry { worktree_id: *worktree_id, - entry_id: worktree_entries[entry_ix].id, + entry_id: entries[entry_ix].id, }; self.selection = Some(selection); if window.modifiers().shift { @@ -2005,7 +2017,9 @@ impl ProjectPanel { if let Some(selection) = self.selection { let (mut worktree_ix, mut entry_ix, _) = self.index_for_selection(selection).unwrap_or_default(); - if let Some((_, worktree_entries, _)) = self.visible_entries.get(worktree_ix) { + if let Some(worktree_entries) = + self.visible_entries.get(worktree_ix).map(|v| &v.entries) + { if entry_ix + 1 < worktree_entries.len() { entry_ix += 1; } else { @@ -2014,9 +2028,13 @@ impl ProjectPanel { } } - if let Some((worktree_id, worktree_entries, _)) = self.visible_entries.get(worktree_ix) + if let Some(VisibleEntriesForWorktree { + worktree_id, + entries, + .. + }) = self.visible_entries.get(worktree_ix) { - if let Some(entry) = worktree_entries.get(entry_ix) { + if let Some(entry) = entries.get(entry_ix) { let selection = SelectedEntry { worktree_id: *worktree_id, entry_id: entry.id, @@ -2252,8 +2270,13 @@ impl ProjectPanel { } fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context) { - if let Some((worktree_id, visible_worktree_entries, _)) = self.visible_entries.first() { - if let Some(entry) = visible_worktree_entries.first() { + if let Some(VisibleEntriesForWorktree { + worktree_id, + entries, + .. + }) = self.visible_entries.first() + { + if let Some(entry) = entries.first() { let selection = SelectedEntry { worktree_id: *worktree_id, entry_id: entry.id, @@ -2269,9 +2292,14 @@ impl ProjectPanel { } fn select_last(&mut self, _: &SelectLast, _: &mut Window, cx: &mut Context) { - if let Some((worktree_id, visible_worktree_entries, _)) = self.visible_entries.last() { + if let Some(VisibleEntriesForWorktree { + worktree_id, + entries, + .. + }) = self.visible_entries.last() + { let worktree = self.project.read(cx).worktree_for_id(*worktree_id, cx); - if let (Some(worktree), Some(entry)) = (worktree, visible_worktree_entries.last()) { + if let (Some(worktree), Some(entry)) = (worktree, entries.last()) { let worktree = worktree.read(cx); if let Some(entry) = worktree.entry_for_id(entry.id) { let selection = SelectedEntry { @@ -2960,6 +2988,7 @@ impl ProjectPanel { new_selected_entry: Option<(WorktreeId, ProjectEntryId)>, cx: &mut Context, ) { + let now = Instant::now(); let settings = ProjectPanelSettings::get_global(cx); let auto_collapse_dirs = settings.auto_fold_dirs; let hide_gitignore = settings.hide_gitignore; @@ -3157,19 +3186,23 @@ impl ProjectPanel { project::sort_worktree_entries(&mut visible_worktree_entries); - self.visible_entries - .push((worktree_id, visible_worktree_entries, OnceCell::new())); + self.visible_entries.push(VisibleEntriesForWorktree { + worktree_id, + entries: visible_worktree_entries, + index: OnceCell::new(), + }) } if let Some((project_entry_id, worktree_id, _)) = max_width_item { let mut visited_worktrees_length = 0; - let index = self.visible_entries.iter().find_map(|(id, entries, _)| { - if worktree_id == *id { - entries + let index = self.visible_entries.iter().find_map(|visible_entries| { + if worktree_id == visible_entries.worktree_id { + visible_entries + .entries .iter() .position(|entry| entry.id == project_entry_id) } else { - visited_worktrees_length += entries.len(); + visited_worktrees_length += visible_entries.entries.len(); None } }); @@ -3183,6 +3216,18 @@ impl ProjectPanel { entry_id, }); } + let elapsed = now.elapsed(); + if self.last_reported_update.elapsed() > Duration::from_secs(3600) { + telemetry::event!( + "Project Panel Updated", + elapsed_ms = elapsed.as_millis() as u64, + worktree_entries = self + .visible_entries + .iter() + .map(|worktree| worktree.entries.len()) + .sum::(), + ) + } } fn expand_entry( @@ -3396,15 +3441,14 @@ impl ProjectPanel { worktree_id: WorktreeId, ) -> Option<(usize, usize, usize)> { let mut total_ix = 0; - for (worktree_ix, (current_worktree_id, visible_worktree_entries, _)) in - self.visible_entries.iter().enumerate() - { - if worktree_id != *current_worktree_id { - total_ix += visible_worktree_entries.len(); + for (worktree_ix, visible) in self.visible_entries.iter().enumerate() { + if worktree_id != visible.worktree_id { + total_ix += visible.entries.len(); continue; } - return visible_worktree_entries + return visible + .entries .iter() .enumerate() .find(|(_, entry)| entry.id == entry_id) @@ -3415,12 +3459,13 @@ impl ProjectPanel { fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef<'_>)> { let mut offset = 0; - for (worktree_id, visible_worktree_entries, _) in &self.visible_entries { - let current_len = visible_worktree_entries.len(); + for worktree in &self.visible_entries { + let current_len = worktree.entries.len(); if index < offset + current_len { - return visible_worktree_entries + return worktree + .entries .get(index - offset) - .map(|entry| (*worktree_id, entry.to_ref())); + .map(|entry| (worktree.worktree_id, entry.to_ref())); } offset += current_len; } @@ -3441,26 +3486,23 @@ impl ProjectPanel { ), ) { let mut ix = 0; - for (_, visible_worktree_entries, entries_paths) in &self.visible_entries { + for visible in &self.visible_entries { if ix >= range.end { return; } - if ix + visible_worktree_entries.len() <= range.start { - ix += visible_worktree_entries.len(); + if ix + visible.entries.len() <= range.start { + ix += visible.entries.len(); continue; } - let end_ix = range.end.min(ix + visible_worktree_entries.len()); + let end_ix = range.end.min(ix + visible.entries.len()); let entry_range = range.start.saturating_sub(ix)..end_ix - ix; - let entries = entries_paths.get_or_init(|| { - visible_worktree_entries - .iter() - .map(|e| (e.path.clone())) - .collect() - }); + let entries = visible + .index + .get_or_init(|| visible.entries.iter().map(|e| (e.path.clone())).collect()); let base_index = ix + entry_range.start; - for (i, entry) in visible_worktree_entries[entry_range].iter().enumerate() { + for (i, entry) in visible.entries[entry_range].iter().enumerate() { let global_index = base_index + i; callback(&entry, global_index, entries, window, cx); } @@ -3476,40 +3518,41 @@ impl ProjectPanel { mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut Window, &mut Context), ) { let mut ix = 0; - for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries { + for visible in &self.visible_entries { if ix >= range.end { return; } - if ix + visible_worktree_entries.len() <= range.start { - ix += visible_worktree_entries.len(); + if ix + visible.entries.len() <= range.start { + ix += visible.entries.len(); continue; } - let end_ix = range.end.min(ix + visible_worktree_entries.len()); + let end_ix = range.end.min(ix + visible.entries.len()); let git_status_setting = { let settings = ProjectPanelSettings::get_global(cx); settings.git_status }; - if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) { + if let Some(worktree) = self + .project + .read(cx) + .worktree_for_id(visible.worktree_id, cx) + { let snapshot = worktree.read(cx).snapshot(); let root_name = OsStr::new(snapshot.root_name()); let entry_range = range.start.saturating_sub(ix)..end_ix - ix; - let entries = entries_paths.get_or_init(|| { - visible_worktree_entries - .iter() - .map(|e| (e.path.clone())) - .collect() - }); - for entry in visible_worktree_entries[entry_range].iter() { + let entries = visible + .index + .get_or_init(|| visible.entries.iter().map(|e| (e.path.clone())).collect()); + for entry in visible.entries[entry_range].iter() { let status = git_status_setting .then_some(entry.git_summary) .unwrap_or_default(); let mut details = self.details_for_entry( entry, - *worktree_id, + visible.worktree_id, root_name, entries, status, @@ -3595,9 +3638,9 @@ impl ProjectPanel { let entries = self .visible_entries .iter() - .find_map(|(tree_id, entries, _)| { - if worktree_id == *tree_id { - Some(entries) + .find_map(|visible| { + if worktree_id == visible.worktree_id { + Some(&visible.entries) } else { None } @@ -3636,7 +3679,7 @@ impl ProjectPanel { let mut worktree_ids: Vec<_> = self .visible_entries .iter() - .map(|(worktree_id, _, _)| *worktree_id) + .map(|worktree| worktree.worktree_id) .collect(); let repo_snapshots = self .project @@ -3752,7 +3795,7 @@ impl ProjectPanel { let mut worktree_ids: Vec<_> = self .visible_entries .iter() - .map(|(worktree_id, _, _)| *worktree_id) + .map(|worktree| worktree.worktree_id) .collect(); let mut last_found: Option = None; @@ -3761,8 +3804,8 @@ impl ProjectPanel { let entries = self .visible_entries .iter() - .find(|(worktree_id, _, _)| *worktree_id == start.worktree_id) - .map(|(_, entries, _)| entries)?; + .find(|worktree| worktree.worktree_id == start.worktree_id) + .map(|worktree| &worktree.entries)?; let mut start_idx = entries .iter() @@ -4914,7 +4957,7 @@ impl ProjectPanel { let (active_indent_range, depth) = { let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?; - let child_paths = &self.visible_entries[worktree_ix].1; + let child_paths = &self.visible_entries[worktree_ix].entries; let mut child_count = 0; let depth = entry.path.ancestors().count(); while let Some(entry) = child_paths.get(child_offset + child_count + 1) { @@ -4927,9 +4970,14 @@ impl ProjectPanel { let start = ix + 1; let end = start + child_count; - let (_, entries, paths) = &self.visible_entries[worktree_ix]; - let visible_worktree_entries = - paths.get_or_init(|| entries.iter().map(|e| (e.path.clone())).collect()); + let visible_worktree = &self.visible_entries[worktree_ix]; + let visible_worktree_entries = visible_worktree.index.get_or_init(|| { + visible_worktree + .entries + .iter() + .map(|e| (e.path.clone())) + .collect() + }); // Calculate the actual depth of the entry, taking into account that directories can be auto-folded. let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries); @@ -4964,10 +5012,10 @@ impl ProjectPanel { return SmallVec::new(); }; - let Some((_, visible_worktree_entries, entries_paths)) = self + let Some(visible) = self .visible_entries .iter() - .find(|(id, _, _)| *id == worktree_id) + .find(|worktree| worktree.worktree_id == worktree_id) else { return SmallVec::new(); }; @@ -4977,12 +5025,9 @@ impl ProjectPanel { }; let worktree = worktree.read(cx).snapshot(); - let paths = entries_paths.get_or_init(|| { - visible_worktree_entries - .iter() - .map(|e| e.path.clone()) - .collect() - }); + let paths = visible + .index + .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect()); let mut sticky_parents = Vec::new(); let mut current_path = entry_ref.path.clone(); @@ -5012,7 +5057,8 @@ impl ProjectPanel { let root_name = OsStr::new(worktree.root_name()); let git_summaries_by_id = if git_status_enabled { - visible_worktree_entries + visible + .entries .iter() .map(|e| (e.id, e.git_summary)) .collect::>() @@ -5110,7 +5156,7 @@ impl Render for ProjectPanel { let item_count = self .visible_entries .iter() - .map(|(_, worktree_entries, _)| worktree_entries.len()) + .map(|worktree| worktree.entries.len()) .sum(); fn handle_drag_move( From d705585a2e566a0399673303deb8a934e7ae058c Mon Sep 17 00:00:00 2001 From: localcc Date: Fri, 8 Aug 2025 14:39:08 +0200 Subject: [PATCH 195/693] Fix file unlocking after closing the workspace (#35865) Release Notes: - Fixed folders being locked after closing them in zed --- crates/fs/src/fs_watcher.rs | 198 +++++++++++++++++++++++++++--------- 1 file changed, 148 insertions(+), 50 deletions(-) diff --git a/crates/fs/src/fs_watcher.rs b/crates/fs/src/fs_watcher.rs index 9fdf2ad0b1..a5ce21294f 100644 --- a/crates/fs/src/fs_watcher.rs +++ b/crates/fs/src/fs_watcher.rs @@ -1,6 +1,9 @@ use notify::EventKind; use parking_lot::Mutex; -use std::sync::{Arc, OnceLock}; +use std::{ + collections::HashMap, + sync::{Arc, OnceLock}, +}; use util::{ResultExt, paths::SanitizedPath}; use crate::{PathEvent, PathEventKind, Watcher}; @@ -8,6 +11,7 @@ use crate::{PathEvent, PathEventKind, Watcher}; pub struct FsWatcher { tx: smol::channel::Sender<()>, pending_path_events: Arc>>, + registrations: Mutex, WatcherRegistrationId>>, } impl FsWatcher { @@ -18,10 +22,24 @@ impl FsWatcher { Self { tx, pending_path_events, + registrations: Default::default(), } } } +impl Drop for FsWatcher { + fn drop(&mut self) { + let mut registrations = self.registrations.lock(); + let registrations = registrations.drain(); + + let _ = global(|g| { + for (_, registration) in registrations { + g.remove(registration); + } + }); + } +} + impl Watcher for FsWatcher { fn add(&self, path: &std::path::Path) -> anyhow::Result<()> { let root_path = SanitizedPath::from(path); @@ -29,75 +47,143 @@ impl Watcher for FsWatcher { let tx = self.tx.clone(); let pending_paths = self.pending_path_events.clone(); - use notify::Watcher; + let path: Arc = path.into(); - global({ + if self.registrations.lock().contains_key(&path) { + return Ok(()); + } + + let registration_id = global({ + let path = path.clone(); |g| { - g.add(move |event: ¬ify::Event| { - let kind = match event.kind { - EventKind::Create(_) => Some(PathEventKind::Created), - EventKind::Modify(_) => Some(PathEventKind::Changed), - EventKind::Remove(_) => Some(PathEventKind::Removed), - _ => None, - }; - let mut path_events = event - .paths - .iter() - .filter_map(|event_path| { - let event_path = SanitizedPath::from(event_path); - event_path.starts_with(&root_path).then(|| PathEvent { - path: event_path.as_path().to_path_buf(), - kind, + g.add( + path, + notify::RecursiveMode::NonRecursive, + move |event: ¬ify::Event| { + let kind = match event.kind { + EventKind::Create(_) => Some(PathEventKind::Created), + EventKind::Modify(_) => Some(PathEventKind::Changed), + EventKind::Remove(_) => Some(PathEventKind::Removed), + _ => None, + }; + let mut path_events = event + .paths + .iter() + .filter_map(|event_path| { + let event_path = SanitizedPath::from(event_path); + event_path.starts_with(&root_path).then(|| PathEvent { + path: event_path.as_path().to_path_buf(), + kind, + }) }) - }) - .collect::>(); + .collect::>(); - if !path_events.is_empty() { - path_events.sort(); - let mut pending_paths = pending_paths.lock(); - if pending_paths.is_empty() { - tx.try_send(()).ok(); + if !path_events.is_empty() { + path_events.sort(); + let mut pending_paths = pending_paths.lock(); + if pending_paths.is_empty() { + tx.try_send(()).ok(); + } + util::extend_sorted( + &mut *pending_paths, + path_events, + usize::MAX, + |a, b| a.path.cmp(&b.path), + ); } - util::extend_sorted( - &mut *pending_paths, - path_events, - usize::MAX, - |a, b| a.path.cmp(&b.path), - ); - } - }) + }, + ) } - })?; - - global(|g| { - g.watcher - .lock() - .watch(path, notify::RecursiveMode::NonRecursive) })??; + self.registrations.lock().insert(path, registration_id); + Ok(()) } fn remove(&self, path: &std::path::Path) -> anyhow::Result<()> { - use notify::Watcher; - Ok(global(|w| w.watcher.lock().unwatch(path))??) + let Some(registration) = self.registrations.lock().remove(path) else { + return Ok(()); + }; + + global(|w| w.remove(registration)) } } +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct WatcherRegistrationId(u32); + +struct WatcherRegistrationState { + callback: Arc, + path: Arc, +} + +struct WatcherState { + watchers: HashMap, + path_registrations: HashMap, u32>, + last_registration: WatcherRegistrationId, +} + pub struct GlobalWatcher { + state: Mutex, + + // DANGER: never keep the state lock while holding the watcher lock // two mutexes because calling watcher.add triggers an watcher.event, which needs watchers. #[cfg(target_os = "linux")] - pub(super) watcher: Mutex, + watcher: Mutex, #[cfg(target_os = "freebsd")] - pub(super) watcher: Mutex, + watcher: Mutex, #[cfg(target_os = "windows")] - pub(super) watcher: Mutex, - pub(super) watchers: Mutex>>, + watcher: Mutex, } impl GlobalWatcher { - pub(super) fn add(&self, cb: impl Fn(¬ify::Event) + Send + Sync + 'static) { - self.watchers.lock().push(Box::new(cb)) + #[must_use] + fn add( + &self, + path: Arc, + mode: notify::RecursiveMode, + cb: impl Fn(¬ify::Event) + Send + Sync + 'static, + ) -> anyhow::Result { + use notify::Watcher; + + self.watcher.lock().watch(&path, mode)?; + + let mut state = self.state.lock(); + + let id = state.last_registration; + state.last_registration = WatcherRegistrationId(id.0 + 1); + + let registration_state = WatcherRegistrationState { + callback: Arc::new(cb), + path: path.clone(), + }; + state.watchers.insert(id, registration_state); + *state.path_registrations.entry(path.clone()).or_insert(0) += 1; + + Ok(id) + } + + pub fn remove(&self, id: WatcherRegistrationId) { + use notify::Watcher; + let mut state = self.state.lock(); + let Some(registration_state) = state.watchers.remove(&id) else { + return; + }; + + let Some(count) = state.path_registrations.get_mut(®istration_state.path) else { + return; + }; + *count -= 1; + if *count == 0 { + state.path_registrations.remove(®istration_state.path); + + drop(state); + self.watcher + .lock() + .unwatch(®istration_state.path) + .log_err(); + } } } @@ -114,8 +200,16 @@ fn handle_event(event: Result) { return; }; global::<()>(move |watcher| { - for f in watcher.watchers.lock().iter() { - f(&event) + let callbacks = { + let state = watcher.state.lock(); + state + .watchers + .values() + .map(|r| r.callback.clone()) + .collect::>() + }; + for callback in callbacks { + callback(&event); } }) .log_err(); @@ -124,8 +218,12 @@ fn handle_event(event: Result) { pub fn global(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result { let result = FS_WATCHER_INSTANCE.get_or_init(|| { notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher { + state: Mutex::new(WatcherState { + watchers: Default::default(), + path_registrations: Default::default(), + last_registration: Default::default(), + }), watcher: Mutex::new(file_watcher), - watchers: Default::default(), }) }); match result { From 2526dcb5a54019977ba69a86f4b1f8e214c58399 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Fri, 8 Aug 2025 09:43:53 -0300 Subject: [PATCH 196/693] agent2: Port `edit_file` tool (#35844) TODO: - [x] Authorization - [x] Restore tests Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra Co-authored-by: Ben Brandt --- Cargo.lock | 4 + Cargo.toml | 2 +- crates/acp_thread/src/acp_thread.rs | 133 +- crates/acp_thread/src/diff.rs | 388 +++++ crates/agent2/Cargo.toml | 4 + crates/agent2/src/agent.rs | 14 +- crates/agent2/src/agent2.rs | 1 + crates/agent2/src/tests/mod.rs | 3 +- crates/agent2/src/tests/test_tools.rs | 22 +- crates/agent2/src/thread.rs | 212 ++- crates/agent2/src/tools.rs | 2 + crates/agent2/src/tools/edit_file_tool.rs | 1361 +++++++++++++++++ crates/agent2/src/tools/find_path_tool.rs | 63 +- crates/agent2/src/tools/read_file_tool.rs | 163 +- crates/agent2/src/tools/thinking_tool.rs | 1 + crates/agent_ui/src/acp/thread_view.rs | 15 +- crates/assistant_tools/src/assistant_tools.rs | 4 +- crates/assistant_tools/src/edit_agent.rs | 6 - crates/assistant_tools/src/edit_file_tool.rs | 85 - crates/language_model/src/request.rs | 6 + 20 files changed, 2075 insertions(+), 414 deletions(-) create mode 100644 crates/acp_thread/src/diff.rs create mode 100644 crates/agent2/src/tools/edit_file_tool.rs diff --git a/Cargo.lock b/Cargo.lock index 0f0e78bb48..6f434e8685 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,8 +158,10 @@ dependencies = [ "acp_thread", "agent-client-protocol", "agent_servers", + "agent_settings", "anyhow", "assistant_tool", + "assistant_tools", "client", "clock", "cloud_llm_client", @@ -177,6 +179,8 @@ dependencies = [ "language_model", "language_models", "log", + "lsp", + "paths", "pretty_assertions", "project", "prompt_store", diff --git a/Cargo.toml b/Cargo.toml index d547110bb4..998e727602 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -425,7 +425,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = { version = "0.0.23" } +agent-client-protocol = "0.0.23" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 443375a51b..54bfe56a15 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1,18 +1,17 @@ mod connection; +mod diff; + pub use connection::*; +pub use diff::*; use agent_client_protocol as acp; use anyhow::{Context as _, Result}; use assistant_tool::ActionLog; -use buffer_diff::BufferDiff; -use editor::{Bias, MultiBuffer, PathKey}; +use editor::Bias; use futures::{FutureExt, channel::oneshot, future::BoxFuture}; use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task}; use itertools::Itertools; -use language::{ - Anchor, Buffer, BufferSnapshot, Capability, LanguageRegistry, OffsetRangeExt as _, Point, - text_diff, -}; +use language::{Anchor, Buffer, BufferSnapshot, LanguageRegistry, Point, text_diff}; use markdown::Markdown; use project::{AgentLocation, Project}; use std::collections::HashMap; @@ -140,7 +139,7 @@ impl AgentThreadEntry { } } - pub fn diffs(&self) -> impl Iterator { + pub fn diffs(&self) -> impl Iterator> { if let AgentThreadEntry::ToolCall(call) = self { itertools::Either::Left(call.diffs()) } else { @@ -249,7 +248,7 @@ impl ToolCall { } } - pub fn diffs(&self) -> impl Iterator { + pub fn diffs(&self) -> impl Iterator> { self.content.iter().filter_map(|content| match content { ToolCallContent::ContentBlock { .. } => None, ToolCallContent::Diff { diff } => Some(diff), @@ -389,7 +388,7 @@ impl ContentBlock { #[derive(Debug)] pub enum ToolCallContent { ContentBlock { content: ContentBlock }, - Diff { diff: Diff }, + Diff { diff: Entity }, } impl ToolCallContent { @@ -403,7 +402,7 @@ impl ToolCallContent { content: ContentBlock::new(content, &language_registry, cx), }, acp::ToolCallContent::Diff { diff } => Self::Diff { - diff: Diff::from_acp(diff, language_registry, cx), + diff: cx.new(|cx| Diff::from_acp(diff, language_registry, cx)), }, } } @@ -411,108 +410,11 @@ impl ToolCallContent { pub fn to_markdown(&self, cx: &App) -> String { match self { Self::ContentBlock { content } => content.to_markdown(cx).to_string(), - Self::Diff { diff } => diff.to_markdown(cx), + Self::Diff { diff } => diff.read(cx).to_markdown(cx), } } } -#[derive(Debug)] -pub struct Diff { - pub multibuffer: Entity, - pub path: PathBuf, - _task: Task>, -} - -impl Diff { - pub fn from_acp( - diff: acp::Diff, - language_registry: Arc, - cx: &mut App, - ) -> Self { - let acp::Diff { - path, - old_text, - new_text, - } = diff; - - let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly)); - - let new_buffer = cx.new(|cx| Buffer::local(new_text, cx)); - let old_buffer = cx.new(|cx| Buffer::local(old_text.unwrap_or("".into()), cx)); - let new_buffer_snapshot = new_buffer.read(cx).text_snapshot(); - let buffer_diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot, cx)); - - let task = cx.spawn({ - let multibuffer = multibuffer.clone(); - let path = path.clone(); - async move |cx| { - let language = language_registry - .language_for_file_path(&path) - .await - .log_err(); - - new_buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?; - - let old_buffer_snapshot = old_buffer.update(cx, |buffer, cx| { - buffer.set_language(language, cx); - buffer.snapshot() - })?; - - buffer_diff - .update(cx, |diff, cx| { - diff.set_base_text( - old_buffer_snapshot, - Some(language_registry), - new_buffer_snapshot, - cx, - ) - })? - .await?; - - multibuffer - .update(cx, |multibuffer, cx| { - let hunk_ranges = { - let buffer = new_buffer.read(cx); - let diff = buffer_diff.read(cx); - diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, cx) - .map(|diff_hunk| diff_hunk.buffer_range.to_point(&buffer)) - .collect::>() - }; - - multibuffer.set_excerpts_for_path( - PathKey::for_buffer(&new_buffer, cx), - new_buffer.clone(), - hunk_ranges, - editor::DEFAULT_MULTIBUFFER_CONTEXT, - cx, - ); - multibuffer.add_diff(buffer_diff, cx); - }) - .log_err(); - - anyhow::Ok(()) - } - }); - - Self { - multibuffer, - path, - _task: task, - } - } - - fn to_markdown(&self, cx: &App) -> String { - let buffer_text = self - .multibuffer - .read(cx) - .all_buffers() - .iter() - .map(|buffer| buffer.read(cx).text()) - .join("\n"); - format!("Diff: {}\n```\n{}\n```\n", self.path.display(), buffer_text) - } -} - #[derive(Debug, Default)] pub struct Plan { pub entries: Vec, @@ -823,6 +725,21 @@ impl AcpThread { Ok(()) } + pub fn set_tool_call_diff( + &mut self, + tool_call_id: &acp::ToolCallId, + diff: Entity, + cx: &mut Context, + ) -> Result<()> { + let (ix, current_call) = self + .tool_call_mut(tool_call_id) + .context("Tool call not found")?; + current_call.content.clear(); + current_call.content.push(ToolCallContent::Diff { diff }); + cx.emit(AcpThreadEvent::EntryUpdated(ix)); + Ok(()) + } + /// Updates a tool call if id matches an existing entry, otherwise inserts a new one. pub fn upsert_tool_call(&mut self, tool_call: acp::ToolCall, cx: &mut Context) { let status = ToolCallStatus::Allowed { diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs new file mode 100644 index 0000000000..9cc6271360 --- /dev/null +++ b/crates/acp_thread/src/diff.rs @@ -0,0 +1,388 @@ +use agent_client_protocol as acp; +use anyhow::Result; +use buffer_diff::{BufferDiff, BufferDiffSnapshot}; +use editor::{MultiBuffer, PathKey}; +use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task}; +use itertools::Itertools; +use language::{ + Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, Rope, TextBuffer, +}; +use std::{ + cmp::Reverse, + ops::Range, + path::{Path, PathBuf}, + sync::Arc, +}; +use util::ResultExt; + +pub enum Diff { + Pending(PendingDiff), + Finalized(FinalizedDiff), +} + +impl Diff { + pub fn from_acp( + diff: acp::Diff, + language_registry: Arc, + cx: &mut Context, + ) -> Self { + let acp::Diff { + path, + old_text, + new_text, + } = diff; + + let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly)); + + let new_buffer = cx.new(|cx| Buffer::local(new_text, cx)); + let old_buffer = cx.new(|cx| Buffer::local(old_text.unwrap_or("".into()), cx)); + let new_buffer_snapshot = new_buffer.read(cx).text_snapshot(); + let buffer_diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot, cx)); + + let task = cx.spawn({ + let multibuffer = multibuffer.clone(); + let path = path.clone(); + async move |_, cx| { + let language = language_registry + .language_for_file_path(&path) + .await + .log_err(); + + new_buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?; + + let old_buffer_snapshot = old_buffer.update(cx, |buffer, cx| { + buffer.set_language(language, cx); + buffer.snapshot() + })?; + + buffer_diff + .update(cx, |diff, cx| { + diff.set_base_text( + old_buffer_snapshot, + Some(language_registry), + new_buffer_snapshot, + cx, + ) + })? + .await?; + + multibuffer + .update(cx, |multibuffer, cx| { + let hunk_ranges = { + let buffer = new_buffer.read(cx); + let diff = buffer_diff.read(cx); + diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, cx) + .map(|diff_hunk| diff_hunk.buffer_range.to_point(&buffer)) + .collect::>() + }; + + multibuffer.set_excerpts_for_path( + PathKey::for_buffer(&new_buffer, cx), + new_buffer.clone(), + hunk_ranges, + editor::DEFAULT_MULTIBUFFER_CONTEXT, + cx, + ); + multibuffer.add_diff(buffer_diff, cx); + }) + .log_err(); + + anyhow::Ok(()) + } + }); + + Self::Finalized(FinalizedDiff { + multibuffer, + path, + _update_diff: task, + }) + } + + pub fn new(buffer: Entity, cx: &mut Context) -> Self { + let buffer_snapshot = buffer.read(cx).snapshot(); + let base_text = buffer_snapshot.text(); + let language_registry = buffer.read(cx).language_registry(); + let text_snapshot = buffer.read(cx).text_snapshot(); + let buffer_diff = cx.new(|cx| { + let mut diff = BufferDiff::new(&text_snapshot, cx); + let _ = diff.set_base_text( + buffer_snapshot.clone(), + language_registry, + text_snapshot, + cx, + ); + diff + }); + + let multibuffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::without_headers(Capability::ReadOnly); + multibuffer.add_diff(buffer_diff.clone(), cx); + multibuffer + }); + + Self::Pending(PendingDiff { + multibuffer, + base_text: Arc::new(base_text), + _subscription: cx.observe(&buffer, |this, _, cx| { + if let Diff::Pending(diff) = this { + diff.update(cx); + } + }), + buffer, + diff: buffer_diff, + revealed_ranges: Vec::new(), + update_diff: Task::ready(Ok(())), + }) + } + + pub fn reveal_range(&mut self, range: Range, cx: &mut Context) { + if let Self::Pending(diff) = self { + diff.reveal_range(range, cx); + } + } + + pub fn finalize(&mut self, cx: &mut Context) { + if let Self::Pending(diff) = self { + *self = Self::Finalized(diff.finalize(cx)); + } + } + + pub fn multibuffer(&self) -> &Entity { + match self { + Self::Pending(PendingDiff { multibuffer, .. }) => multibuffer, + Self::Finalized(FinalizedDiff { multibuffer, .. }) => multibuffer, + } + } + + pub fn to_markdown(&self, cx: &App) -> String { + let buffer_text = self + .multibuffer() + .read(cx) + .all_buffers() + .iter() + .map(|buffer| buffer.read(cx).text()) + .join("\n"); + let path = match self { + Diff::Pending(PendingDiff { buffer, .. }) => { + buffer.read(cx).file().map(|file| file.path().as_ref()) + } + Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_path()), + }; + format!( + "Diff: {}\n```\n{}\n```\n", + path.unwrap_or(Path::new("untitled")).display(), + buffer_text + ) + } +} + +pub struct PendingDiff { + multibuffer: Entity, + base_text: Arc, + buffer: Entity, + diff: Entity, + revealed_ranges: Vec>, + _subscription: Subscription, + update_diff: Task>, +} + +impl PendingDiff { + pub fn update(&mut self, cx: &mut Context) { + let buffer = self.buffer.clone(); + let buffer_diff = self.diff.clone(); + let base_text = self.base_text.clone(); + self.update_diff = cx.spawn(async move |diff, cx| { + let text_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot())?; + let diff_snapshot = BufferDiff::update_diff( + buffer_diff.clone(), + text_snapshot.clone(), + Some(base_text), + false, + false, + None, + None, + cx, + ) + .await?; + buffer_diff.update(cx, |diff, cx| { + diff.set_snapshot(diff_snapshot, &text_snapshot, cx) + })?; + diff.update(cx, |diff, cx| { + if let Diff::Pending(diff) = diff { + diff.update_visible_ranges(cx); + } + }) + }); + } + + pub fn reveal_range(&mut self, range: Range, cx: &mut Context) { + self.revealed_ranges.push(range); + self.update_visible_ranges(cx); + } + + fn finalize(&self, cx: &mut Context) -> FinalizedDiff { + let ranges = self.excerpt_ranges(cx); + let base_text = self.base_text.clone(); + let language_registry = self.buffer.read(cx).language_registry().clone(); + + let path = self + .buffer + .read(cx) + .file() + .map(|file| file.path().as_ref()) + .unwrap_or(Path::new("untitled")) + .into(); + + // Replace the buffer in the multibuffer with the snapshot + let buffer = cx.new(|cx| { + let language = self.buffer.read(cx).language().cloned(); + let buffer = TextBuffer::new_normalized( + 0, + cx.entity_id().as_non_zero_u64().into(), + self.buffer.read(cx).line_ending(), + self.buffer.read(cx).as_rope().clone(), + ); + let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite); + buffer.set_language(language, cx); + buffer + }); + + let buffer_diff = cx.spawn({ + let buffer = buffer.clone(); + let language_registry = language_registry.clone(); + async move |_this, cx| { + build_buffer_diff(base_text, &buffer, language_registry, cx).await + } + }); + + let update_diff = cx.spawn(async move |this, cx| { + let buffer_diff = buffer_diff.await?; + this.update(cx, |this, cx| { + this.multibuffer().update(cx, |multibuffer, cx| { + let path_key = PathKey::for_buffer(&buffer, cx); + multibuffer.clear(cx); + multibuffer.set_excerpts_for_path( + path_key, + buffer, + ranges, + editor::DEFAULT_MULTIBUFFER_CONTEXT, + cx, + ); + multibuffer.add_diff(buffer_diff.clone(), cx); + }); + + cx.notify(); + }) + }); + + FinalizedDiff { + path, + multibuffer: self.multibuffer.clone(), + _update_diff: update_diff, + } + } + + fn update_visible_ranges(&mut self, cx: &mut Context) { + let ranges = self.excerpt_ranges(cx); + self.multibuffer.update(cx, |multibuffer, cx| { + multibuffer.set_excerpts_for_path( + PathKey::for_buffer(&self.buffer, cx), + self.buffer.clone(), + ranges, + editor::DEFAULT_MULTIBUFFER_CONTEXT, + cx, + ); + let end = multibuffer.len(cx); + Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1) + }); + cx.notify(); + } + + fn excerpt_ranges(&self, cx: &App) -> Vec> { + let buffer = self.buffer.read(cx); + let diff = self.diff.read(cx); + let mut ranges = diff + .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, cx) + .map(|diff_hunk| diff_hunk.buffer_range.to_point(&buffer)) + .collect::>(); + ranges.extend( + self.revealed_ranges + .iter() + .map(|range| range.to_point(&buffer)), + ); + ranges.sort_unstable_by_key(|range| (range.start, Reverse(range.end))); + + // Merge adjacent ranges + let mut ranges = ranges.into_iter().peekable(); + let mut merged_ranges = Vec::new(); + while let Some(mut range) = ranges.next() { + while let Some(next_range) = ranges.peek() { + if range.end >= next_range.start { + range.end = range.end.max(next_range.end); + ranges.next(); + } else { + break; + } + } + + merged_ranges.push(range); + } + merged_ranges + } +} + +pub struct FinalizedDiff { + path: PathBuf, + multibuffer: Entity, + _update_diff: Task>, +} + +async fn build_buffer_diff( + old_text: Arc, + buffer: &Entity, + language_registry: Option>, + cx: &mut AsyncApp, +) -> Result> { + let buffer = cx.update(|cx| buffer.read(cx).snapshot())?; + + let old_text_rope = cx + .background_spawn({ + let old_text = old_text.clone(); + async move { Rope::from(old_text.as_str()) } + }) + .await; + let base_buffer = cx + .update(|cx| { + Buffer::build_snapshot( + old_text_rope, + buffer.language().cloned(), + language_registry, + cx, + ) + })? + .await; + + let diff_snapshot = cx + .update(|cx| { + BufferDiffSnapshot::new_with_base_buffer( + buffer.text.clone(), + Some(old_text), + base_buffer, + cx, + ) + })? + .await; + + let secondary_diff = cx.new(|cx| { + let mut diff = BufferDiff::new(&buffer, cx); + diff.set_snapshot(diff_snapshot.clone(), &buffer, cx); + diff + })?; + + cx.new(|cx| { + let mut diff = BufferDiff::new(&buffer.text, cx); + diff.set_snapshot(diff_snapshot, &buffer, cx); + diff.set_secondary_diff(secondary_diff); + diff + }) +} diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index a75011a671..3e19895a31 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -15,8 +15,10 @@ workspace = true acp_thread.workspace = true agent-client-protocol.workspace = true agent_servers.workspace = true +agent_settings.workspace = true anyhow.workspace = true assistant_tool.workspace = true +assistant_tools.workspace = true cloud_llm_client.workspace = true collections.workspace = true fs.workspace = true @@ -29,6 +31,7 @@ language.workspace = true language_model.workspace = true language_models.workspace = true log.workspace = true +paths.workspace = true project.workspace = true prompt_store.workspace = true rust-embed.workspace = true @@ -53,6 +56,7 @@ gpui = { workspace = true, "features" = ["test-support"] } gpui_tokio.workspace = true language = { workspace = true, "features" = ["test-support"] } language_model = { workspace = true, "features" = ["test-support"] } +lsp = { workspace = true, "features" = ["test-support"] } project = { workspace = true, "features" = ["test-support"] } reqwest_client.workspace = true settings = { workspace = true, "features" = ["test-support"] } diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 2014d86fb7..e7920e7891 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,5 +1,5 @@ use crate::{templates::Templates, AgentResponseEvent, Thread}; -use crate::{FindPathTool, ReadFileTool, ThinkingTool, ToolCallAuthorization}; +use crate::{EditFileTool, FindPathTool, ReadFileTool, ThinkingTool, ToolCallAuthorization}; use acp_thread::ModelSelector; use agent_client_protocol as acp; use anyhow::{anyhow, Context as _, Result}; @@ -412,11 +412,12 @@ impl acp_thread::AgentConnection for NativeAgentConnection { anyhow!("No default model configured. Please configure a default model in settings.") })?; - let thread = cx.new(|_| { + let thread = cx.new(|cx| { let mut thread = Thread::new(project.clone(), agent.project_context.clone(), action_log.clone(), agent.templates.clone(), default_model); thread.add_tool(ThinkingTool); thread.add_tool(FindPathTool::new(project.clone())); thread.add_tool(ReadFileTool::new(project.clone(), action_log)); + thread.add_tool(EditFileTool::new(cx.entity())); thread }); @@ -564,6 +565,15 @@ impl acp_thread::AgentConnection for NativeAgentConnection { ) })??; } + AgentResponseEvent::ToolCallDiff(tool_call_diff) => { + acp_thread.update(cx, |thread, cx| { + thread.set_tool_call_diff( + &tool_call_diff.tool_call_id, + tool_call_diff.diff, + cx, + ) + })??; + } AgentResponseEvent::Stop(stop_reason) => { log::debug!("Assistant message complete: {:?}", stop_reason); return Ok(acp::PromptResponse { stop_reason }); diff --git a/crates/agent2/src/agent2.rs b/crates/agent2/src/agent2.rs index db743c8429..f13cd1bd67 100644 --- a/crates/agent2/src/agent2.rs +++ b/crates/agent2/src/agent2.rs @@ -9,5 +9,6 @@ mod tests; pub use agent::*; pub use native_agent_server::NativeAgentServer; +pub use templates::*; pub use thread::*; pub use tools::*; diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 7913f9a24c..b70f54ac0a 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1,5 +1,4 @@ use super::*; -use crate::templates::Templates; use acp_thread::AgentConnection; use agent_client_protocol::{self as acp}; use anyhow::Result; @@ -273,7 +272,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { tool_name: ToolRequiringPermission.name().into(), is_error: false, content: "Allowed".into(), - output: None + output: Some("Allowed".into()) }), MessageContent::ToolResult(LanguageModelToolResult { tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(), diff --git a/crates/agent2/src/tests/test_tools.rs b/crates/agent2/src/tests/test_tools.rs index fd6e7e941f..d22ff6ace8 100644 --- a/crates/agent2/src/tests/test_tools.rs +++ b/crates/agent2/src/tests/test_tools.rs @@ -14,6 +14,7 @@ pub struct EchoTool; impl AgentTool for EchoTool { type Input = EchoToolInput; + type Output = String; fn name(&self) -> SharedString { "echo".into() @@ -48,6 +49,7 @@ pub struct DelayTool; impl AgentTool for DelayTool { type Input = DelayToolInput; + type Output = String; fn name(&self) -> SharedString { "delay".into() @@ -84,6 +86,7 @@ pub struct ToolRequiringPermission; impl AgentTool for ToolRequiringPermission { type Input = ToolRequiringPermissionInput; + type Output = String; fn name(&self) -> SharedString { "tool_requiring_permission".into() @@ -99,14 +102,11 @@ impl AgentTool for ToolRequiringPermission { fn run( self: Arc, - input: Self::Input, + _input: Self::Input, event_stream: ToolCallEventStream, cx: &mut App, - ) -> Task> - where - Self: Sized, - { - let auth_check = self.authorize(input, event_stream); + ) -> Task> { + let auth_check = event_stream.authorize("Authorize?".into()); cx.foreground_executor().spawn(async move { auth_check.await?; Ok("Allowed".to_string()) @@ -121,6 +121,7 @@ pub struct InfiniteTool; impl AgentTool for InfiniteTool { type Input = InfiniteToolInput; + type Output = String; fn name(&self) -> SharedString { "infinite".into() @@ -171,19 +172,20 @@ pub struct WordListTool; impl AgentTool for WordListTool { type Input = WordListInput; + type Output = String; fn name(&self) -> SharedString { "word_list".into() } - fn initial_title(&self, _input: Self::Input) -> SharedString { - "List of random words".into() - } - fn kind(&self) -> acp::ToolKind { acp::ToolKind::Other } + fn initial_title(&self, _input: Self::Input) -> SharedString { + "List of random words".into() + } + fn run( self: Arc, _input: Self::Input, diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 4b8a65655f..98f2d0651d 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1,4 +1,5 @@ -use crate::templates::{SystemPromptTemplate, Template, Templates}; +use crate::{SystemPromptTemplate, Template, Templates}; +use acp_thread::Diff; use agent_client_protocol as acp; use anyhow::{anyhow, Context as _, Result}; use assistant_tool::{adapt_schema_to_format, ActionLog}; @@ -103,6 +104,7 @@ pub enum AgentResponseEvent { ToolCall(acp::ToolCall), ToolCallUpdate(acp::ToolCallUpdate), ToolCallAuthorization(ToolCallAuthorization), + ToolCallDiff(ToolCallDiff), Stop(acp::StopReason), } @@ -113,6 +115,12 @@ pub struct ToolCallAuthorization { pub response: oneshot::Sender, } +#[derive(Debug)] +pub struct ToolCallDiff { + pub tool_call_id: acp::ToolCallId, + pub diff: Entity, +} + pub struct Thread { messages: Vec, completion_mode: CompletionMode, @@ -125,12 +133,13 @@ pub struct Thread { project_context: Rc>, templates: Arc, pub selected_model: Arc, + project: Entity, action_log: Entity, } impl Thread { pub fn new( - _project: Entity, + project: Entity, project_context: Rc>, action_log: Entity, templates: Arc, @@ -145,10 +154,19 @@ impl Thread { project_context, templates, selected_model: default_model, + project, action_log, } } + pub fn project(&self) -> &Entity { + &self.project + } + + pub fn action_log(&self) -> &Entity { + &self.action_log + } + pub fn set_mode(&mut self, mode: CompletionMode) { self.completion_mode = mode; } @@ -315,10 +333,6 @@ impl Thread { events_rx } - pub fn action_log(&self) -> &Entity { - &self.action_log - } - pub fn build_system_message(&self) -> AgentMessage { log::debug!("Building system message"); let prompt = SystemPromptTemplate { @@ -490,15 +504,33 @@ impl Thread { })); }; - let tool_result = self.run_tool(tool, tool_use.clone(), event_stream.clone(), cx); + let tool_event_stream = + ToolCallEventStream::new(&tool_use, tool.kind(), event_stream.clone()); + tool_event_stream.send_update(acp::ToolCallUpdateFields { + status: Some(acp::ToolCallStatus::InProgress), + ..Default::default() + }); + let supports_images = self.selected_model.supports_images(); + let tool_result = tool.run(tool_use.input, tool_event_stream, cx); Some(cx.foreground_executor().spawn(async move { - match tool_result.await { - Ok(tool_output) => LanguageModelToolResult { + let tool_result = tool_result.await.and_then(|output| { + if let LanguageModelToolResultContent::Image(_) = &output.llm_output { + if !supports_images { + return Err(anyhow!( + "Attempted to read an image, but this model doesn't support it.", + )); + } + } + Ok(output) + }); + + match tool_result { + Ok(output) => LanguageModelToolResult { tool_use_id: tool_use.id, tool_name: tool_use.name, is_error: false, - content: LanguageModelToolResultContent::Text(Arc::from(tool_output)), - output: None, + content: output.llm_output, + output: Some(output.raw_output), }, Err(error) => LanguageModelToolResult { tool_use_id: tool_use.id, @@ -511,24 +543,6 @@ impl Thread { })) } - fn run_tool( - &self, - tool: Arc, - tool_use: LanguageModelToolUse, - event_stream: AgentResponseEventStream, - cx: &mut Context, - ) -> Task> { - cx.spawn(async move |_this, cx| { - let tool_event_stream = ToolCallEventStream::new(tool_use.id, event_stream); - tool_event_stream.send_update(acp::ToolCallUpdateFields { - status: Some(acp::ToolCallStatus::InProgress), - ..Default::default() - }); - cx.update(|cx| tool.run(tool_use.input, tool_event_stream, cx))? - .await - }) - } - fn handle_tool_use_json_parse_error_event( &mut self, tool_use_id: LanguageModelToolUseId, @@ -572,7 +586,7 @@ impl Thread { self.messages.last_mut().unwrap() } - fn build_completion_request( + pub(crate) fn build_completion_request( &self, completion_intent: CompletionIntent, cx: &mut App, @@ -662,6 +676,7 @@ where Self: 'static + Sized, { type Input: for<'de> Deserialize<'de> + Serialize + JsonSchema; + type Output: for<'de> Deserialize<'de> + Serialize + Into; fn name(&self) -> SharedString; @@ -685,23 +700,13 @@ where schemars::schema_for!(Self::Input) } - /// Allows the tool to authorize a given tool call with the user if necessary - fn authorize( - &self, - input: Self::Input, - event_stream: ToolCallEventStream, - ) -> impl use + Future> { - let json_input = serde_json::json!(&input); - event_stream.authorize(self.initial_title(input).into(), self.kind(), json_input) - } - /// Runs the tool with the provided input. fn run( self: Arc, input: Self::Input, event_stream: ToolCallEventStream, cx: &mut App, - ) -> Task>; + ) -> Task>; fn erase(self) -> Arc { Arc::new(Erased(Arc::new(self))) @@ -710,6 +715,11 @@ where pub struct Erased(T); +pub struct AgentToolOutput { + llm_output: LanguageModelToolResultContent, + raw_output: serde_json::Value, +} + pub trait AnyAgentTool { fn name(&self) -> SharedString; fn description(&self, cx: &mut App) -> SharedString; @@ -721,7 +731,7 @@ pub trait AnyAgentTool { input: serde_json::Value, event_stream: ToolCallEventStream, cx: &mut App, - ) -> Task>; + ) -> Task>; } impl AnyAgentTool for Erased> @@ -756,12 +766,18 @@ where input: serde_json::Value, event_stream: ToolCallEventStream, cx: &mut App, - ) -> Task> { - let parsed_input: Result = serde_json::from_value(input).map_err(Into::into); - match parsed_input { - Ok(input) => self.0.clone().run(input, event_stream, cx), - Err(error) => Task::ready(Err(anyhow!(error))), - } + ) -> Task> { + cx.spawn(async move |cx| { + let input = serde_json::from_value(input)?; + let output = cx + .update(|cx| self.0.clone().run(input, event_stream, cx))? + .await?; + let raw_output = serde_json::to_value(&output)?; + Ok(AgentToolOutput { + llm_output: output.into(), + raw_output, + }) + }) } } @@ -874,6 +890,12 @@ impl AgentResponseEventStream { .ok(); } + fn send_tool_call_diff(&self, tool_call_diff: ToolCallDiff) { + self.0 + .unbounded_send(Ok(AgentResponseEvent::ToolCallDiff(tool_call_diff))) + .ok(); + } + fn send_stop(&self, reason: StopReason) { match reason { StopReason::EndTurn => { @@ -903,13 +925,41 @@ impl AgentResponseEventStream { #[derive(Clone)] pub struct ToolCallEventStream { tool_use_id: LanguageModelToolUseId, + kind: acp::ToolKind, + input: serde_json::Value, stream: AgentResponseEventStream, } impl ToolCallEventStream { - fn new(tool_use_id: LanguageModelToolUseId, stream: AgentResponseEventStream) -> Self { + #[cfg(test)] + pub fn test() -> (Self, ToolCallEventStreamReceiver) { + let (events_tx, events_rx) = + mpsc::unbounded::>(); + + let stream = ToolCallEventStream::new( + &LanguageModelToolUse { + id: "test_id".into(), + name: "test_tool".into(), + raw_input: String::new(), + input: serde_json::Value::Null, + is_input_complete: true, + }, + acp::ToolKind::Other, + AgentResponseEventStream(events_tx), + ); + + (stream, ToolCallEventStreamReceiver(events_rx)) + } + + fn new( + tool_use: &LanguageModelToolUse, + kind: acp::ToolKind, + stream: AgentResponseEventStream, + ) -> Self { Self { - tool_use_id, + tool_use_id: tool_use.id.clone(), + kind, + input: tool_use.input.clone(), stream, } } @@ -918,38 +968,52 @@ impl ToolCallEventStream { self.stream.send_tool_call_update(&self.tool_use_id, fields); } - pub fn authorize( - &self, - title: String, - kind: acp::ToolKind, - input: serde_json::Value, - ) -> impl use<> + Future> { - self.stream - .authorize_tool_call(&self.tool_use_id, title, kind, input) + pub fn send_diff(&self, diff: Entity) { + self.stream.send_tool_call_diff(ToolCallDiff { + tool_call_id: acp::ToolCallId(self.tool_use_id.to_string().into()), + diff, + }); + } + + pub fn authorize(&self, title: String) -> impl use<> + Future> { + self.stream.authorize_tool_call( + &self.tool_use_id, + title, + self.kind.clone(), + self.input.clone(), + ) } } #[cfg(test)] -pub struct TestToolCallEventStream { - stream: ToolCallEventStream, - _events_rx: mpsc::UnboundedReceiver>, -} +pub struct ToolCallEventStreamReceiver( + mpsc::UnboundedReceiver>, +); #[cfg(test)] -impl TestToolCallEventStream { - pub fn new() -> Self { - let (events_tx, events_rx) = - mpsc::unbounded::>(); - - let stream = ToolCallEventStream::new("test".into(), AgentResponseEventStream(events_tx)); - - Self { - stream, - _events_rx: events_rx, +impl ToolCallEventStreamReceiver { + pub async fn expect_tool_authorization(&mut self) -> ToolCallAuthorization { + let event = self.0.next().await; + if let Some(Ok(AgentResponseEvent::ToolCallAuthorization(auth))) = event { + auth + } else { + panic!("Expected ToolCallAuthorization but got: {:?}", event); } } +} - pub fn stream(&self) -> ToolCallEventStream { - self.stream.clone() +#[cfg(test)] +impl std::ops::Deref for ToolCallEventStreamReceiver { + type Target = mpsc::UnboundedReceiver>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(test)] +impl std::ops::DerefMut for ToolCallEventStreamReceiver { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 } } diff --git a/crates/agent2/src/tools.rs b/crates/agent2/src/tools.rs index 240614c263..5fe13db854 100644 --- a/crates/agent2/src/tools.rs +++ b/crates/agent2/src/tools.rs @@ -1,7 +1,9 @@ +mod edit_file_tool; mod find_path_tool; mod read_file_tool; mod thinking_tool; +pub use edit_file_tool::*; pub use find_path_tool::*; pub use read_file_tool::*; pub use thinking_tool::*; diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs new file mode 100644 index 0000000000..0dbe0be217 --- /dev/null +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -0,0 +1,1361 @@ +use acp_thread::Diff; +use agent_client_protocol as acp; +use anyhow::{anyhow, Context as _, Result}; +use assistant_tools::edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat}; +use cloud_llm_client::CompletionIntent; +use collections::HashSet; +use gpui::{App, AppContext, AsyncApp, Entity, Task}; +use indoc::formatdoc; +use language::language_settings::{self, FormatOnSave}; +use language_model::LanguageModelToolResultContent; +use paths; +use project::lsp_store::{FormatTrigger, LspFormatTarget}; +use project::{Project, ProjectPath}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Settings; +use smol::stream::StreamExt as _; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use ui::SharedString; +use util::ResultExt; + +use crate::{AgentTool, Thread, ToolCallEventStream}; + +/// This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead. +/// +/// Before using this tool: +/// +/// 1. Use the `read_file` tool to understand the file's contents and context +/// +/// 2. Verify the directory path is correct (only applicable when creating new files): +/// - Use the `list_directory` tool to verify the parent directory exists and is the correct location +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct EditFileToolInput { + /// A one-line, user-friendly markdown description of the edit. This will be + /// shown in the UI and also passed to another model to perform the edit. + /// + /// Be terse, but also descriptive in what you want to achieve with this + /// edit. Avoid generic instructions. + /// + /// NEVER mention the file path in this description. + /// + /// Fix API endpoint URLs + /// Update copyright year in `page_footer` + /// + /// Make sure to include this field before all the others in the input object + /// so that we can display it immediately. + pub display_description: String, + + /// The full path of the file to create or modify in the project. + /// + /// WARNING: When specifying which file path need changing, you MUST + /// start each path with one of the project's root directories. + /// + /// The following examples assume we have two root directories in the project: + /// - /a/b/backend + /// - /c/d/frontend + /// + /// + /// `backend/src/main.rs` + /// + /// Notice how the file path starts with `backend`. Without that, the path + /// would be ambiguous and the call would fail! + /// + /// + /// + /// `frontend/db.js` + /// + pub path: PathBuf, + + /// The mode of operation on the file. Possible values: + /// - 'edit': Make granular edits to an existing file. + /// - 'create': Create a new file if it doesn't exist. + /// - 'overwrite': Replace the entire contents of an existing file. + /// + /// When a file already exists or you just created it, prefer editing + /// it as opposed to recreating it from scratch. + pub mode: EditFileMode, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum EditFileMode { + Edit, + Create, + Overwrite, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct EditFileToolOutput { + input_path: PathBuf, + project_path: PathBuf, + new_text: String, + old_text: Arc, + diff: String, + edit_agent_output: EditAgentOutput, +} + +impl From for LanguageModelToolResultContent { + fn from(output: EditFileToolOutput) -> Self { + if output.diff.is_empty() { + "No edits were made.".into() + } else { + format!( + "Edited {}:\n\n```diff\n{}\n```", + output.input_path.display(), + output.diff + ) + .into() + } + } +} + +pub struct EditFileTool { + thread: Entity, +} + +impl EditFileTool { + pub fn new(thread: Entity) -> Self { + Self { thread } + } + + fn authorize( + &self, + input: &EditFileToolInput, + event_stream: &ToolCallEventStream, + cx: &App, + ) -> Task> { + if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions { + return Task::ready(Ok(())); + } + + // If any path component matches the local settings folder, then this could affect + // the editor in ways beyond the project source, so prompt. + let local_settings_folder = paths::local_settings_folder_relative_path(); + let path = Path::new(&input.path); + if path + .components() + .any(|component| component.as_os_str() == local_settings_folder.as_os_str()) + { + return cx.foreground_executor().spawn( + event_stream.authorize(format!("{} (local settings)", input.display_description)), + ); + } + + // It's also possible that the global config dir is configured to be inside the project, + // so check for that edge case too. + if let Ok(canonical_path) = std::fs::canonicalize(&input.path) { + if canonical_path.starts_with(paths::config_dir()) { + return cx.foreground_executor().spawn( + event_stream + .authorize(format!("{} (global settings)", input.display_description)), + ); + } + } + + // Check if path is inside the global config directory + // First check if it's already inside project - if not, try to canonicalize + let thread = self.thread.read(cx); + let project_path = thread.project().read(cx).find_project_path(&input.path, cx); + + // If the path is inside the project, and it's not one of the above edge cases, + // then no confirmation is necessary. Otherwise, confirmation is necessary. + if project_path.is_some() { + Task::ready(Ok(())) + } else { + cx.foreground_executor() + .spawn(event_stream.authorize(input.display_description.clone())) + } + } +} + +impl AgentTool for EditFileTool { + type Input = EditFileToolInput; + type Output = EditFileToolOutput; + + fn name(&self) -> SharedString { + "edit_file".into() + } + + fn kind(&self) -> acp::ToolKind { + acp::ToolKind::Edit + } + + fn initial_title(&self, input: Self::Input) -> SharedString { + input.display_description.into() + } + + fn run( + self: Arc, + input: Self::Input, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> { + let project = self.thread.read(cx).project().clone(); + let project_path = match resolve_path(&input, project.clone(), cx) { + Ok(path) => path, + Err(err) => return Task::ready(Err(anyhow!(err))), + }; + + let request = self.thread.update(cx, |thread, cx| { + thread.build_completion_request(CompletionIntent::ToolResults, cx) + }); + let thread = self.thread.read(cx); + let model = thread.selected_model.clone(); + let action_log = thread.action_log().clone(); + + let authorize = self.authorize(&input, &event_stream, cx); + cx.spawn(async move |cx: &mut AsyncApp| { + authorize.await?; + + let edit_format = EditFormat::from_model(model.clone())?; + let edit_agent = EditAgent::new( + model, + project.clone(), + action_log.clone(), + // TODO: move edit agent to this crate so we can use our templates + assistant_tools::templates::Templates::new(), + edit_format, + ); + + let buffer = project + .update(cx, |project, cx| { + project.open_buffer(project_path.clone(), cx) + })? + .await?; + + let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?; + event_stream.send_diff(diff.clone()); + + let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; + let old_text = cx + .background_spawn({ + let old_snapshot = old_snapshot.clone(); + async move { Arc::new(old_snapshot.text()) } + }) + .await; + + + let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) { + edit_agent.edit( + buffer.clone(), + input.display_description.clone(), + &request, + cx, + ) + } else { + edit_agent.overwrite( + buffer.clone(), + input.display_description.clone(), + &request, + cx, + ) + }; + + let mut hallucinated_old_text = false; + let mut ambiguous_ranges = Vec::new(); + while let Some(event) = events.next().await { + match event { + EditAgentOutputEvent::Edited => {}, + EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true, + EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges, + EditAgentOutputEvent::ResolvingEditRange(range) => { + diff.update(cx, |card, cx| card.reveal_range(range, cx))?; + } + } + } + + // If format_on_save is enabled, format the buffer + let format_on_save_enabled = buffer + .read_with(cx, |buffer, cx| { + let settings = language_settings::language_settings( + buffer.language().map(|l| l.name()), + buffer.file(), + cx, + ); + settings.format_on_save != FormatOnSave::Off + }) + .unwrap_or(false); + + let edit_agent_output = output.await?; + + if format_on_save_enabled { + action_log.update(cx, |log, cx| { + log.buffer_edited(buffer.clone(), cx); + })?; + + let format_task = project.update(cx, |project, cx| { + project.format( + HashSet::from_iter([buffer.clone()]), + LspFormatTarget::Buffers, + false, // Don't push to history since the tool did it. + FormatTrigger::Save, + cx, + ) + })?; + format_task.await.log_err(); + } + + project + .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))? + .await?; + + action_log.update(cx, |log, cx| { + log.buffer_edited(buffer.clone(), cx); + })?; + + let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; + let (new_text, unified_diff) = cx + .background_spawn({ + let new_snapshot = new_snapshot.clone(); + let old_text = old_text.clone(); + async move { + let new_text = new_snapshot.text(); + let diff = language::unified_diff(&old_text, &new_text); + (new_text, diff) + } + }) + .await; + + diff.update(cx, |diff, cx| diff.finalize(cx)).ok(); + + let input_path = input.path.display(); + if unified_diff.is_empty() { + anyhow::ensure!( + !hallucinated_old_text, + formatdoc! {" + Some edits were produced but none of them could be applied. + Read the relevant sections of {input_path} again so that + I can perform the requested edits. + "} + ); + anyhow::ensure!( + ambiguous_ranges.is_empty(), + { + let line_numbers = ambiguous_ranges + .iter() + .map(|range| range.start.to_string()) + .collect::>() + .join(", "); + formatdoc! {" + matches more than one position in the file (lines: {line_numbers}). Read the + relevant sections of {input_path} again and extend so + that I can perform the requested edits. + "} + } + ); + } + + Ok(EditFileToolOutput { + input_path: input.path, + project_path: project_path.path.to_path_buf(), + new_text: new_text.clone(), + old_text, + diff: unified_diff, + edit_agent_output, + }) + }) + } +} + +/// Validate that the file path is valid, meaning: +/// +/// - For `edit` and `overwrite`, the path must point to an existing file. +/// - For `create`, the file must not already exist, but it's parent dir must exist. +fn resolve_path( + input: &EditFileToolInput, + project: Entity, + cx: &mut App, +) -> Result { + let project = project.read(cx); + + match input.mode { + EditFileMode::Edit | EditFileMode::Overwrite => { + let path = project + .find_project_path(&input.path, cx) + .context("Can't edit file: path not found")?; + + let entry = project + .entry_for_path(&path, cx) + .context("Can't edit file: path not found")?; + + anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory"); + Ok(path) + } + + EditFileMode::Create => { + if let Some(path) = project.find_project_path(&input.path, cx) { + anyhow::ensure!( + project.entry_for_path(&path, cx).is_none(), + "Can't create file: file already exists" + ); + } + + let parent_path = input + .path + .parent() + .context("Can't create file: incorrect path")?; + + let parent_project_path = project.find_project_path(&parent_path, cx); + + let parent_entry = parent_project_path + .as_ref() + .and_then(|path| project.entry_for_path(&path, cx)) + .context("Can't create file: parent directory doesn't exist")?; + + anyhow::ensure!( + parent_entry.is_dir(), + "Can't create file: parent is not a directory" + ); + + let file_name = input + .path + .file_name() + .context("Can't create file: invalid filename")?; + + let new_file_path = parent_project_path.map(|parent| ProjectPath { + path: Arc::from(parent.path.join(file_name)), + ..parent + }); + + new_file_path.context("Can't create file") + } + } +} + +#[cfg(test)] +mod tests { + use crate::Templates; + + use super::*; + use assistant_tool::ActionLog; + use client::TelemetrySettings; + use fs::Fs; + use gpui::{TestAppContext, UpdateGlobal}; + use language_model::fake_provider::FakeLanguageModel; + use serde_json::json; + use settings::SettingsStore; + use std::rc::Rc; + use util::path; + + #[gpui::test] + async fn test_edit_nonexistent_file(cx: &mut TestAppContext) { + init_test(cx); + + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree("/root", json!({})).await; + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let model = Arc::new(FakeLanguageModel::default()); + let thread = + cx.new(|_| Thread::new(project, Rc::default(), action_log, Templates::new(), model)); + let result = cx + .update(|cx| { + let input = EditFileToolInput { + display_description: "Some edit".into(), + path: "root/nonexistent_file.txt".into(), + mode: EditFileMode::Edit, + }; + Arc::new(EditFileTool { thread }).run(input, ToolCallEventStream::test().0, cx) + }) + .await; + assert_eq!( + result.unwrap_err().to_string(), + "Can't edit file: path not found" + ); + } + + #[gpui::test] + async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) { + let mode = &EditFileMode::Create; + + let result = test_resolve_path(mode, "root/new.txt", cx); + assert_resolved_path_eq(result.await, "new.txt"); + + let result = test_resolve_path(mode, "new.txt", cx); + assert_resolved_path_eq(result.await, "new.txt"); + + let result = test_resolve_path(mode, "dir/new.txt", cx); + assert_resolved_path_eq(result.await, "dir/new.txt"); + + let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx); + assert_eq!( + result.await.unwrap_err().to_string(), + "Can't create file: file already exists" + ); + + let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx); + assert_eq!( + result.await.unwrap_err().to_string(), + "Can't create file: parent directory doesn't exist" + ); + } + + #[gpui::test] + async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) { + let mode = &EditFileMode::Edit; + + let path_with_root = "root/dir/subdir/existing.txt"; + let path_without_root = "dir/subdir/existing.txt"; + let result = test_resolve_path(mode, path_with_root, cx); + assert_resolved_path_eq(result.await, path_without_root); + + let result = test_resolve_path(mode, path_without_root, cx); + assert_resolved_path_eq(result.await, path_without_root); + + let result = test_resolve_path(mode, "root/nonexistent.txt", cx); + assert_eq!( + result.await.unwrap_err().to_string(), + "Can't edit file: path not found" + ); + + let result = test_resolve_path(mode, "root/dir", cx); + assert_eq!( + result.await.unwrap_err().to_string(), + "Can't edit file: path is a directory" + ); + } + + async fn test_resolve_path( + mode: &EditFileMode, + path: &str, + cx: &mut TestAppContext, + ) -> anyhow::Result { + init_test(cx); + + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree( + "/root", + json!({ + "dir": { + "subdir": { + "existing.txt": "hello" + } + } + }), + ) + .await; + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + + let input = EditFileToolInput { + display_description: "Some edit".into(), + path: path.into(), + mode: mode.clone(), + }; + + let result = cx.update(|cx| resolve_path(&input, project, cx)); + result + } + + fn assert_resolved_path_eq(path: anyhow::Result, expected: &str) { + let actual = path + .expect("Should return valid path") + .path + .to_str() + .unwrap() + .replace("\\", "/"); // Naive Windows paths normalization + assert_eq!(actual, expected); + } + + #[gpui::test] + async fn test_format_on_save(cx: &mut TestAppContext) { + init_test(cx); + + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree("/root", json!({"src": {}})).await; + + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + + // Set up a Rust language with LSP formatting support + let rust_language = Arc::new(language::Language::new( + language::LanguageConfig { + name: "Rust".into(), + matcher: language::LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + ..Default::default() + }, + None, + )); + + // Register the language and fake LSP + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_language); + + let mut fake_language_servers = language_registry.register_fake_lsp( + "Rust", + language::FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + }, + ); + + // Create the file + fs.save( + path!("/root/src/main.rs").as_ref(), + &"initial content".into(), + language::LineEnding::Unix, + ) + .await + .unwrap(); + + // Open the buffer to trigger LSP initialization + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer(path!("/root/src/main.rs"), cx) + }) + .await + .unwrap(); + + // Register the buffer with language servers + let _handle = project.update(cx, |project, cx| { + project.register_buffer_with_language_servers(&buffer, cx) + }); + + const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n"; + const FORMATTED_CONTENT: &str = + "This file was formatted by the fake formatter in the test.\n"; + + // Get the fake language server and set up formatting handler + let fake_language_server = fake_language_servers.next().await.unwrap(); + fake_language_server.set_request_handler::({ + |_, _| async move { + Ok(Some(vec![lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)), + new_text: FORMATTED_CONTENT.to_string(), + }])) + } + }); + + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let model = Arc::new(FakeLanguageModel::default()); + let thread = cx.new(|_| { + Thread::new( + project, + Rc::default(), + action_log.clone(), + Templates::new(), + model.clone(), + ) + }); + + // First, test with format_on_save enabled + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings::( + cx, + |settings| { + settings.defaults.format_on_save = Some(FormatOnSave::On); + settings.defaults.formatter = + Some(language::language_settings::SelectedFormatter::Auto); + }, + ); + }); + }); + + // Have the model stream unformatted content + let edit_result = { + let edit_task = cx.update(|cx| { + let input = EditFileToolInput { + display_description: "Create main function".into(), + path: "root/src/main.rs".into(), + mode: EditFileMode::Overwrite, + }; + Arc::new(EditFileTool { + thread: thread.clone(), + }) + .run(input, ToolCallEventStream::test().0, cx) + }); + + // Stream the unformatted content + cx.executor().run_until_parked(); + model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); + model.end_last_completion_stream(); + + edit_task.await + }; + assert!(edit_result.is_ok()); + + // Wait for any async operations (e.g. formatting) to complete + cx.executor().run_until_parked(); + + // Read the file to verify it was formatted automatically + let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); + assert_eq!( + // Ignore carriage returns on Windows + new_content.replace("\r\n", "\n"), + FORMATTED_CONTENT, + "Code should be formatted when format_on_save is enabled" + ); + + let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count()); + + assert_eq!( + stale_buffer_count, 0, + "BUG: Buffer is incorrectly marked as stale after format-on-save. Found {} stale buffers. \ + This causes the agent to think the file was modified externally when it was just formatted.", + stale_buffer_count + ); + + // Next, test with format_on_save disabled + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings::( + cx, + |settings| { + settings.defaults.format_on_save = Some(FormatOnSave::Off); + }, + ); + }); + }); + + // Stream unformatted edits again + let edit_result = { + let edit_task = cx.update(|cx| { + let input = EditFileToolInput { + display_description: "Update main function".into(), + path: "root/src/main.rs".into(), + mode: EditFileMode::Overwrite, + }; + Arc::new(EditFileTool { thread }).run(input, ToolCallEventStream::test().0, cx) + }); + + // Stream the unformatted content + cx.executor().run_until_parked(); + model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); + model.end_last_completion_stream(); + + edit_task.await + }; + assert!(edit_result.is_ok()); + + // Wait for any async operations (e.g. formatting) to complete + cx.executor().run_until_parked(); + + // Verify the file was not formatted + let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); + assert_eq!( + // Ignore carriage returns on Windows + new_content.replace("\r\n", "\n"), + UNFORMATTED_CONTENT, + "Code should not be formatted when format_on_save is disabled" + ); + } + + #[gpui::test] + async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) { + init_test(cx); + + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree("/root", json!({"src": {}})).await; + + // Create a simple file with trailing whitespace + fs.save( + path!("/root/src/main.rs").as_ref(), + &"initial content".into(), + language::LineEnding::Unix, + ) + .await + .unwrap(); + + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let model = Arc::new(FakeLanguageModel::default()); + let thread = cx.new(|_| { + Thread::new( + project, + Rc::default(), + action_log.clone(), + Templates::new(), + model.clone(), + ) + }); + + // First, test with remove_trailing_whitespace_on_save enabled + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings::( + cx, + |settings| { + settings.defaults.remove_trailing_whitespace_on_save = Some(true); + }, + ); + }); + }); + + const CONTENT_WITH_TRAILING_WHITESPACE: &str = + "fn main() { \n println!(\"Hello!\"); \n}\n"; + + // Have the model stream content that contains trailing whitespace + let edit_result = { + let edit_task = cx.update(|cx| { + let input = EditFileToolInput { + display_description: "Create main function".into(), + path: "root/src/main.rs".into(), + mode: EditFileMode::Overwrite, + }; + Arc::new(EditFileTool { + thread: thread.clone(), + }) + .run(input, ToolCallEventStream::test().0, cx) + }); + + // Stream the content with trailing whitespace + cx.executor().run_until_parked(); + model.send_last_completion_stream_text_chunk( + CONTENT_WITH_TRAILING_WHITESPACE.to_string(), + ); + model.end_last_completion_stream(); + + edit_task.await + }; + assert!(edit_result.is_ok()); + + // Wait for any async operations (e.g. formatting) to complete + cx.executor().run_until_parked(); + + // Read the file to verify trailing whitespace was removed automatically + assert_eq!( + // Ignore carriage returns on Windows + fs.load(path!("/root/src/main.rs").as_ref()) + .await + .unwrap() + .replace("\r\n", "\n"), + "fn main() {\n println!(\"Hello!\");\n}\n", + "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled" + ); + + // Next, test with remove_trailing_whitespace_on_save disabled + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings::( + cx, + |settings| { + settings.defaults.remove_trailing_whitespace_on_save = Some(false); + }, + ); + }); + }); + + // Stream edits again with trailing whitespace + let edit_result = { + let edit_task = cx.update(|cx| { + let input = EditFileToolInput { + display_description: "Update main function".into(), + path: "root/src/main.rs".into(), + mode: EditFileMode::Overwrite, + }; + Arc::new(EditFileTool { + thread: thread.clone(), + }) + .run(input, ToolCallEventStream::test().0, cx) + }); + + // Stream the content with trailing whitespace + cx.executor().run_until_parked(); + model.send_last_completion_stream_text_chunk( + CONTENT_WITH_TRAILING_WHITESPACE.to_string(), + ); + model.end_last_completion_stream(); + + edit_task.await + }; + assert!(edit_result.is_ok()); + + // Wait for any async operations (e.g. formatting) to complete + cx.executor().run_until_parked(); + + // Verify the file still has trailing whitespace + // Read the file again - it should still have trailing whitespace + let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); + assert_eq!( + // Ignore carriage returns on Windows + final_content.replace("\r\n", "\n"), + CONTENT_WITH_TRAILING_WHITESPACE, + "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled" + ); + } + + #[gpui::test] + async fn test_authorize(cx: &mut TestAppContext) { + init_test(cx); + let fs = project::FakeFs::new(cx.executor()); + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let model = Arc::new(FakeLanguageModel::default()); + let thread = cx.new(|_| { + Thread::new( + project, + Rc::default(), + action_log.clone(), + Templates::new(), + model.clone(), + ) + }); + let tool = Arc::new(EditFileTool { thread }); + fs.insert_tree("/root", json!({})).await; + + // Test 1: Path with .zed component should require confirmation + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let _auth = cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "test 1".into(), + path: ".zed/settings.json".into(), + mode: EditFileMode::Edit, + }, + &stream_tx, + cx, + ) + }); + + let event = stream_rx.expect_tool_authorization().await; + assert_eq!(event.tool_call.title, "test 1 (local settings)"); + + // Test 2: Path outside project should require confirmation + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let _auth = cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "test 2".into(), + path: "/etc/hosts".into(), + mode: EditFileMode::Edit, + }, + &stream_tx, + cx, + ) + }); + + let event = stream_rx.expect_tool_authorization().await; + assert_eq!(event.tool_call.title, "test 2"); + + // Test 3: Relative path without .zed should not require confirmation + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "test 3".into(), + path: "root/src/main.rs".into(), + mode: EditFileMode::Edit, + }, + &stream_tx, + cx, + ) + }) + .await + .unwrap(); + assert!(stream_rx.try_next().is_err()); + + // Test 4: Path with .zed in the middle should require confirmation + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let _auth = cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "test 4".into(), + path: "root/.zed/tasks.json".into(), + mode: EditFileMode::Edit, + }, + &stream_tx, + cx, + ) + }); + let event = stream_rx.expect_tool_authorization().await; + assert_eq!(event.tool_call.title, "test 4 (local settings)"); + + // Test 5: When always_allow_tool_actions is enabled, no confirmation needed + cx.update(|cx| { + let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); + settings.always_allow_tool_actions = true; + agent_settings::AgentSettings::override_global(settings, cx); + }); + + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "test 5.1".into(), + path: ".zed/settings.json".into(), + mode: EditFileMode::Edit, + }, + &stream_tx, + cx, + ) + }) + .await + .unwrap(); + assert!(stream_rx.try_next().is_err()); + + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "test 5.2".into(), + path: "/etc/hosts".into(), + mode: EditFileMode::Edit, + }, + &stream_tx, + cx, + ) + }) + .await + .unwrap(); + assert!(stream_rx.try_next().is_err()); + } + + #[gpui::test] + async fn test_authorize_global_config(cx: &mut TestAppContext) { + init_test(cx); + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree("/project", json!({})).await; + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let model = Arc::new(FakeLanguageModel::default()); + let thread = cx.new(|_| { + Thread::new( + project, + Rc::default(), + action_log.clone(), + Templates::new(), + model.clone(), + ) + }); + let tool = Arc::new(EditFileTool { thread }); + + // Test global config paths - these should require confirmation if they exist and are outside the project + let test_cases = vec![ + ( + "/etc/hosts", + true, + "System file should require confirmation", + ), + ( + "/usr/local/bin/script", + true, + "System bin file should require confirmation", + ), + ( + "project/normal_file.rs", + false, + "Normal project file should not require confirmation", + ), + ]; + + for (path, should_confirm, description) in test_cases { + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let auth = cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "Edit file".into(), + path: path.into(), + mode: EditFileMode::Edit, + }, + &stream_tx, + cx, + ) + }); + + if should_confirm { + stream_rx.expect_tool_authorization().await; + } else { + auth.await.unwrap(); + assert!( + stream_rx.try_next().is_err(), + "Failed for case: {} - path: {} - expected no confirmation but got one", + description, + path + ); + } + } + } + + #[gpui::test] + async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) { + init_test(cx); + let fs = project::FakeFs::new(cx.executor()); + + // Create multiple worktree directories + fs.insert_tree( + "/workspace/frontend", + json!({ + "src": { + "main.js": "console.log('frontend');" + } + }), + ) + .await; + fs.insert_tree( + "/workspace/backend", + json!({ + "src": { + "main.rs": "fn main() {}" + } + }), + ) + .await; + fs.insert_tree( + "/workspace/shared", + json!({ + ".zed": { + "settings.json": "{}" + } + }), + ) + .await; + + // Create project with multiple worktrees + let project = Project::test( + fs.clone(), + [ + path!("/workspace/frontend").as_ref(), + path!("/workspace/backend").as_ref(), + path!("/workspace/shared").as_ref(), + ], + cx, + ) + .await; + + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let model = Arc::new(FakeLanguageModel::default()); + let thread = cx.new(|_| { + Thread::new( + project.clone(), + Rc::default(), + action_log.clone(), + Templates::new(), + model.clone(), + ) + }); + let tool = Arc::new(EditFileTool { thread }); + + // Test files in different worktrees + let test_cases = vec![ + ("frontend/src/main.js", false, "File in first worktree"), + ("backend/src/main.rs", false, "File in second worktree"), + ( + "shared/.zed/settings.json", + true, + ".zed file in third worktree", + ), + ("/etc/hosts", true, "Absolute path outside all worktrees"), + ( + "../outside/file.txt", + true, + "Relative path outside worktrees", + ), + ]; + + for (path, should_confirm, description) in test_cases { + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let auth = cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "Edit file".into(), + path: path.into(), + mode: EditFileMode::Edit, + }, + &stream_tx, + cx, + ) + }); + + if should_confirm { + stream_rx.expect_tool_authorization().await; + } else { + auth.await.unwrap(); + assert!( + stream_rx.try_next().is_err(), + "Failed for case: {} - path: {} - expected no confirmation but got one", + description, + path + ); + } + } + } + + #[gpui::test] + async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) { + init_test(cx); + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree( + "/project", + json!({ + ".zed": { + "settings.json": "{}" + }, + "src": { + ".zed": { + "local.json": "{}" + } + } + }), + ) + .await; + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let model = Arc::new(FakeLanguageModel::default()); + let thread = cx.new(|_| { + Thread::new( + project.clone(), + Rc::default(), + action_log.clone(), + Templates::new(), + model.clone(), + ) + }); + let tool = Arc::new(EditFileTool { thread }); + + // Test edge cases + let test_cases = vec![ + // Empty path - find_project_path returns Some for empty paths + ("", false, "Empty path is treated as project root"), + // Root directory + ("/", true, "Root directory should be outside project"), + // Parent directory references - find_project_path resolves these + ( + "project/../other", + false, + "Path with .. is resolved by find_project_path", + ), + ( + "project/./src/file.rs", + false, + "Path with . should work normally", + ), + // Windows-style paths (if on Windows) + #[cfg(target_os = "windows")] + ("C:\\Windows\\System32\\hosts", true, "Windows system path"), + #[cfg(target_os = "windows")] + ("project\\src\\main.rs", false, "Windows-style project path"), + ]; + + for (path, should_confirm, description) in test_cases { + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let auth = cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "Edit file".into(), + path: path.into(), + mode: EditFileMode::Edit, + }, + &stream_tx, + cx, + ) + }); + + if should_confirm { + stream_rx.expect_tool_authorization().await; + } else { + auth.await.unwrap(); + assert!( + stream_rx.try_next().is_err(), + "Failed for case: {} - path: {} - expected no confirmation but got one", + description, + path + ); + } + } + } + + #[gpui::test] + async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) { + init_test(cx); + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree( + "/project", + json!({ + "existing.txt": "content", + ".zed": { + "settings.json": "{}" + } + }), + ) + .await; + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let model = Arc::new(FakeLanguageModel::default()); + let thread = cx.new(|_| { + Thread::new( + project.clone(), + Rc::default(), + action_log.clone(), + Templates::new(), + model.clone(), + ) + }); + let tool = Arc::new(EditFileTool { thread }); + + // Test different EditFileMode values + let modes = vec![ + EditFileMode::Edit, + EditFileMode::Create, + EditFileMode::Overwrite, + ]; + + for mode in modes { + // Test .zed path with different modes + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let _auth = cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "Edit settings".into(), + path: "project/.zed/settings.json".into(), + mode: mode.clone(), + }, + &stream_tx, + cx, + ) + }); + + stream_rx.expect_tool_authorization().await; + + // Test outside path with different modes + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let _auth = cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "Edit file".into(), + path: "/outside/file.txt".into(), + mode: mode.clone(), + }, + &stream_tx, + cx, + ) + }); + + stream_rx.expect_tool_authorization().await; + + // Test normal path with different modes + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "Edit file".into(), + path: "project/normal.txt".into(), + mode: mode.clone(), + }, + &stream_tx, + cx, + ) + }) + .await + .unwrap(); + assert!(stream_rx.try_next().is_err()); + } + } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + TelemetrySettings::register(cx); + agent_settings::AgentSettings::register(cx); + Project::init_settings(cx); + }); + } +} diff --git a/crates/agent2/src/tools/find_path_tool.rs b/crates/agent2/src/tools/find_path_tool.rs index e840fec78c..24bdcded8c 100644 --- a/crates/agent2/src/tools/find_path_tool.rs +++ b/crates/agent2/src/tools/find_path_tool.rs @@ -1,6 +1,8 @@ +use crate::{AgentTool, ToolCallEventStream}; use agent_client_protocol as acp; use anyhow::{anyhow, Result}; use gpui::{App, AppContext, Entity, SharedString, Task}; +use language_model::LanguageModelToolResultContent; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -8,8 +10,6 @@ use std::fmt::Write; use std::{cmp, path::PathBuf, sync::Arc}; use util::paths::PathMatcher; -use crate::{AgentTool, ToolCallEventStream}; - /// Fast file path pattern matching tool that works with any codebase size /// /// - Supports glob patterns like "**/*.js" or "src/**/*.ts" @@ -39,8 +39,35 @@ pub struct FindPathToolInput { } #[derive(Debug, Serialize, Deserialize)] -struct FindPathToolOutput { - paths: Vec, +pub struct FindPathToolOutput { + offset: usize, + current_matches_page: Vec, + all_matches_len: usize, +} + +impl From for LanguageModelToolResultContent { + fn from(output: FindPathToolOutput) -> Self { + if output.current_matches_page.is_empty() { + "No matches found".into() + } else { + let mut llm_output = format!("Found {} total matches.", output.all_matches_len); + if output.all_matches_len > RESULTS_PER_PAGE { + write!( + &mut llm_output, + "\nShowing results {}-{} (provide 'offset' parameter for more results):", + output.offset + 1, + output.offset + output.current_matches_page.len() + ) + .unwrap(); + } + + for mat in output.current_matches_page { + write!(&mut llm_output, "\n{}", mat.display()).unwrap(); + } + + llm_output.into() + } + } } const RESULTS_PER_PAGE: usize = 50; @@ -57,6 +84,7 @@ impl FindPathTool { impl AgentTool for FindPathTool { type Input = FindPathToolInput; + type Output = FindPathToolOutput; fn name(&self) -> SharedString { "find_path".into() @@ -75,7 +103,7 @@ impl AgentTool for FindPathTool { input: Self::Input, event_stream: ToolCallEventStream, cx: &mut App, - ) -> Task> { + ) -> Task> { let search_paths_task = search_paths(&input.glob, self.project.clone(), cx); cx.background_spawn(async move { @@ -113,26 +141,11 @@ impl AgentTool for FindPathTool { ..Default::default() }); - if matches.is_empty() { - Ok("No matches found".into()) - } else { - let mut message = format!("Found {} total matches.", matches.len()); - if matches.len() > RESULTS_PER_PAGE { - write!( - &mut message, - "\nShowing results {}-{} (provide 'offset' parameter for more results):", - input.offset + 1, - input.offset + paginated_matches.len() - ) - .unwrap(); - } - - for mat in matches.iter().skip(input.offset).take(RESULTS_PER_PAGE) { - write!(&mut message, "\n{}", mat.display()).unwrap(); - } - - Ok(message) - } + Ok(FindPathToolOutput { + offset: input.offset, + current_matches_page: paginated_matches.to_vec(), + all_matches_len: matches.len(), + }) }) } } diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index 30794ccdad..3d91e3dc74 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -1,10 +1,11 @@ use agent_client_protocol::{self as acp}; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use assistant_tool::{outline, ActionLog}; use gpui::{Entity, Task}; use indoc::formatdoc; use language::{Anchor, Point}; -use project::{AgentLocation, Project, WorktreeSettings}; +use language_model::{LanguageModelImage, LanguageModelToolResultContent}; +use project::{image_store, AgentLocation, ImageItem, Project, WorktreeSettings}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; @@ -59,6 +60,7 @@ impl ReadFileTool { impl AgentTool for ReadFileTool { type Input = ReadFileToolInput; + type Output = LanguageModelToolResultContent; fn name(&self) -> SharedString { "read_file".into() @@ -91,9 +93,9 @@ impl AgentTool for ReadFileTool { fn run( self: Arc, input: Self::Input, - event_stream: ToolCallEventStream, + _event_stream: ToolCallEventStream, cx: &mut App, - ) -> Task> { + ) -> Task> { let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) else { return Task::ready(Err(anyhow!("Path {} not found in project", &input.path))); }; @@ -132,51 +134,27 @@ impl AgentTool for ReadFileTool { let file_path = input.path.clone(); - event_stream.send_update(acp::ToolCallUpdateFields { - locations: Some(vec![acp::ToolCallLocation { - path: project_path.path.to_path_buf(), - line: input.start_line, - // TODO (tracked): use full range - }]), - ..Default::default() - }); + if image_store::is_image_file(&self.project, &project_path, cx) { + return cx.spawn(async move |cx| { + let image_entity: Entity = cx + .update(|cx| { + self.project.update(cx, |project, cx| { + project.open_image(project_path.clone(), cx) + }) + })? + .await?; - // TODO (tracked): images - // if image_store::is_image_file(&self.project, &project_path, cx) { - // let model = &self.thread.read(cx).selected_model; + let image = + image_entity.read_with(cx, |image_item, _| Arc::clone(&image_item.image))?; - // if !model.supports_images() { - // return Task::ready(Err(anyhow!( - // "Attempted to read an image, but Zed doesn't currently support sending images to {}.", - // model.name().0 - // ))) - // .into(); - // } + let language_model_image = cx + .update(|cx| LanguageModelImage::from_image(image, cx))? + .await + .context("processing image")?; - // return cx.spawn(async move |cx| -> Result { - // let image_entity: Entity = cx - // .update(|cx| { - // self.project.update(cx, |project, cx| { - // project.open_image(project_path.clone(), cx) - // }) - // })? - // .await?; - - // let image = - // image_entity.read_with(cx, |image_item, _| Arc::clone(&image_item.image))?; - - // let language_model_image = cx - // .update(|cx| LanguageModelImage::from_image(image, cx))? - // .await - // .context("processing image")?; - - // Ok(ToolResultOutput { - // content: ToolResultContent::Image(language_model_image), - // output: None, - // }) - // }); - // } - // + Ok(language_model_image.into()) + }); + } let project = self.project.clone(); let action_log = self.action_log.clone(); @@ -244,7 +222,7 @@ impl AgentTool for ReadFileTool { })?; } - Ok(result) + Ok(result.into()) } else { // No line ranges specified, so check file size to see if it's too big. let file_size = buffer.read_with(cx, |buffer, _cx| buffer.text().len())?; @@ -257,7 +235,7 @@ impl AgentTool for ReadFileTool { log.buffer_read(buffer, cx); })?; - Ok(result) + Ok(result.into()) } else { // File is too big, so return the outline // and a suggestion to read again with line numbers. @@ -276,7 +254,8 @@ impl AgentTool for ReadFileTool { Alternatively, you can fall back to the `grep` tool (if available) to search the file for specific content." - }) + } + .into()) } } }) @@ -285,8 +264,6 @@ impl AgentTool for ReadFileTool { #[cfg(test)] mod test { - use crate::TestToolCallEventStream; - use super::*; use gpui::{AppContext, TestAppContext, UpdateGlobal as _}; use language::{tree_sitter_rust, Language, LanguageConfig, LanguageMatcher}; @@ -304,7 +281,7 @@ mod test { let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let action_log = cx.new(|_| ActionLog::new(project.clone())); let tool = Arc::new(ReadFileTool::new(project, action_log)); - let event_stream = TestToolCallEventStream::new(); + let (event_stream, _) = ToolCallEventStream::test(); let result = cx .update(|cx| { @@ -313,7 +290,7 @@ mod test { start_line: None, end_line: None, }; - tool.run(input, event_stream.stream(), cx) + tool.run(input, event_stream, cx) }) .await; assert_eq!( @@ -321,6 +298,7 @@ mod test { "root/nonexistent_file.txt not found" ); } + #[gpui::test] async fn test_read_small_file(cx: &mut TestAppContext) { init_test(cx); @@ -336,7 +314,6 @@ mod test { let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let action_log = cx.new(|_| ActionLog::new(project.clone())); let tool = Arc::new(ReadFileTool::new(project, action_log)); - let event_stream = TestToolCallEventStream::new(); let result = cx .update(|cx| { let input = ReadFileToolInput { @@ -344,10 +321,10 @@ mod test { start_line: None, end_line: None, }; - tool.run(input, event_stream.stream(), cx) + tool.run(input, ToolCallEventStream::test().0, cx) }) .await; - assert_eq!(result.unwrap(), "This is a small file content"); + assert_eq!(result.unwrap(), "This is a small file content".into()); } #[gpui::test] @@ -367,18 +344,18 @@ mod test { language_registry.add(Arc::new(rust_lang())); let action_log = cx.new(|_| ActionLog::new(project.clone())); let tool = Arc::new(ReadFileTool::new(project, action_log)); - let event_stream = TestToolCallEventStream::new(); - let content = cx + let result = cx .update(|cx| { let input = ReadFileToolInput { path: "root/large_file.rs".into(), start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await .unwrap(); + let content = result.to_str().unwrap(); assert_eq!( content.lines().skip(4).take(6).collect::>(), @@ -399,10 +376,11 @@ mod test { start_line: None, end_line: None, }; - tool.run(input, event_stream.stream(), cx) + tool.run(input, ToolCallEventStream::test().0, cx) }) - .await; - let content = result.unwrap(); + .await + .unwrap(); + let content = result.to_str().unwrap(); let expected_content = (0..1000) .flat_map(|i| { vec![ @@ -438,7 +416,6 @@ mod test { let action_log = cx.new(|_| ActionLog::new(project.clone())); let tool = Arc::new(ReadFileTool::new(project, action_log)); - let event_stream = TestToolCallEventStream::new(); let result = cx .update(|cx| { let input = ReadFileToolInput { @@ -446,10 +423,10 @@ mod test { start_line: Some(2), end_line: Some(4), }; - tool.run(input, event_stream.stream(), cx) + tool.run(input, ToolCallEventStream::test().0, cx) }) .await; - assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4"); + assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4".into()); } #[gpui::test] @@ -467,7 +444,6 @@ mod test { let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let action_log = cx.new(|_| ActionLog::new(project.clone())); let tool = Arc::new(ReadFileTool::new(project, action_log)); - let event_stream = TestToolCallEventStream::new(); // start_line of 0 should be treated as 1 let result = cx @@ -477,10 +453,10 @@ mod test { start_line: Some(0), end_line: Some(2), }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; - assert_eq!(result.unwrap(), "Line 1\nLine 2"); + assert_eq!(result.unwrap(), "Line 1\nLine 2".into()); // end_line of 0 should result in at least 1 line let result = cx @@ -490,10 +466,10 @@ mod test { start_line: Some(1), end_line: Some(0), }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; - assert_eq!(result.unwrap(), "Line 1"); + assert_eq!(result.unwrap(), "Line 1".into()); // when start_line > end_line, should still return at least 1 line let result = cx @@ -503,10 +479,10 @@ mod test { start_line: Some(3), end_line: Some(2), }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; - assert_eq!(result.unwrap(), "Line 3"); + assert_eq!(result.unwrap(), "Line 3".into()); } fn init_test(cx: &mut TestAppContext) { @@ -612,7 +588,6 @@ mod test { let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await; let action_log = cx.new(|_| ActionLog::new(project.clone())); let tool = Arc::new(ReadFileTool::new(project, action_log)); - let event_stream = TestToolCallEventStream::new(); // Reading a file outside the project worktree should fail let result = cx @@ -622,7 +597,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; assert!( @@ -638,7 +613,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; assert!( @@ -654,7 +629,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; assert!( @@ -669,7 +644,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; assert!( @@ -685,7 +660,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; assert!( @@ -700,7 +675,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; assert!( @@ -715,7 +690,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; assert!( @@ -731,11 +706,11 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; assert!(result.is_ok(), "Should be able to read normal files"); - assert_eq!(result.unwrap(), "Normal file content"); + assert_eq!(result.unwrap(), "Normal file content".into()); // Path traversal attempts with .. should fail let result = cx @@ -745,7 +720,7 @@ mod test { start_line: None, end_line: None, }; - tool.run(input, event_stream.stream(), cx) + tool.run(input, ToolCallEventStream::test().0, cx) }) .await; assert!( @@ -826,7 +801,6 @@ mod test { let action_log = cx.new(|_| ActionLog::new(project.clone())); let tool = Arc::new(ReadFileTool::new(project.clone(), action_log.clone())); - let event_stream = TestToolCallEventStream::new(); // Test reading allowed files in worktree1 let result = cx @@ -836,12 +810,15 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await .unwrap(); - assert_eq!(result, "fn main() { println!(\"Hello from worktree1\"); }"); + assert_eq!( + result, + "fn main() { println!(\"Hello from worktree1\"); }".into() + ); // Test reading private file in worktree1 should fail let result = cx @@ -851,7 +828,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; @@ -872,7 +849,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; @@ -893,14 +870,14 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await .unwrap(); assert_eq!( result, - "export function greet() { return 'Hello from worktree2'; }" + "export function greet() { return 'Hello from worktree2'; }".into() ); // Test reading private file in worktree2 should fail @@ -911,7 +888,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; @@ -932,7 +909,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; @@ -954,7 +931,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; diff --git a/crates/agent2/src/tools/thinking_tool.rs b/crates/agent2/src/tools/thinking_tool.rs index bb85d8eceb..d85370e7e5 100644 --- a/crates/agent2/src/tools/thinking_tool.rs +++ b/crates/agent2/src/tools/thinking_tool.rs @@ -20,6 +20,7 @@ pub struct ThinkingTool; impl AgentTool for ThinkingTool { type Input = ThinkingToolInput; + type Output = String; fn name(&self) -> SharedString { "thinking".into() diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 3d1fbba45d..7f4e7e7208 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -42,7 +42,7 @@ use workspace::{CollaboratorId, Workspace}; use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage}; use ::acp_thread::{ - AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, Diff, + AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, }; @@ -732,7 +732,11 @@ impl AcpThreadView { cx: &App, ) -> Option>> { let entry = self.thread()?.read(cx).entries().get(entry_ix)?; - Some(entry.diffs().map(|diff| diff.multibuffer.clone())) + Some( + entry + .diffs() + .map(|diff| diff.read(cx).multibuffer().clone()), + ) } fn authenticate( @@ -1314,10 +1318,9 @@ impl AcpThreadView { Empty.into_any_element() } } - ToolCallContent::Diff { - diff: Diff { multibuffer, .. }, - .. - } => self.render_diff_editor(multibuffer), + ToolCallContent::Diff { diff, .. } => { + self.render_diff_editor(&diff.read(cx).multibuffer()) + } } } diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs index 90bb2e9b7c..bf668e6918 100644 --- a/crates/assistant_tools/src/assistant_tools.rs +++ b/crates/assistant_tools/src/assistant_tools.rs @@ -2,7 +2,7 @@ mod copy_path_tool; mod create_directory_tool; mod delete_path_tool; mod diagnostics_tool; -mod edit_agent; +pub mod edit_agent; mod edit_file_tool; mod fetch_tool; mod find_path_tool; @@ -14,7 +14,7 @@ mod open_tool; mod project_notifications_tool; mod read_file_tool; mod schema; -mod templates; +pub mod templates; mod terminal_tool; mod thinking_tool; mod ui; diff --git a/crates/assistant_tools/src/edit_agent.rs b/crates/assistant_tools/src/edit_agent.rs index 715d106a26..dcb14a48f3 100644 --- a/crates/assistant_tools/src/edit_agent.rs +++ b/crates/assistant_tools/src/edit_agent.rs @@ -29,7 +29,6 @@ use serde::{Deserialize, Serialize}; use std::{cmp, iter, mem, ops::Range, path::PathBuf, pin::Pin, sync::Arc, task::Poll}; use streaming_diff::{CharOperation, StreamingDiff}; use streaming_fuzzy_matcher::StreamingFuzzyMatcher; -use util::debug_panic; #[derive(Serialize)] struct CreateFilePromptTemplate { @@ -682,11 +681,6 @@ impl EditAgent { if last_message.content.is_empty() { conversation.messages.pop(); } - } else { - debug_panic!( - "Last message must be an Assistant tool calling! Got {:?}", - last_message.content - ); } } diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index dce9f49abd..311521019d 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -120,8 +120,6 @@ struct PartialInput { display_description: String, } -const DEFAULT_UI_TEXT: &str = "Editing file"; - impl Tool for EditFileTool { fn name(&self) -> String { "edit_file".into() @@ -211,22 +209,6 @@ impl Tool for EditFileTool { } } - fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String { - if let Some(input) = serde_json::from_value::(input.clone()).ok() { - let description = input.display_description.trim(); - if !description.is_empty() { - return description.to_string(); - } - - let path = input.path.trim(); - if !path.is_empty() { - return path.to_string(); - } - } - - DEFAULT_UI_TEXT.to_string() - } - fn run( self: Arc, input: serde_json::Value, @@ -1370,73 +1352,6 @@ mod tests { assert_eq!(actual, expected); } - #[test] - fn still_streaming_ui_text_with_path() { - let input = json!({ - "path": "src/main.rs", - "display_description": "", - "old_string": "old code", - "new_string": "new code" - }); - - assert_eq!(EditFileTool.still_streaming_ui_text(&input), "src/main.rs"); - } - - #[test] - fn still_streaming_ui_text_with_description() { - let input = json!({ - "path": "", - "display_description": "Fix error handling", - "old_string": "old code", - "new_string": "new code" - }); - - assert_eq!( - EditFileTool.still_streaming_ui_text(&input), - "Fix error handling", - ); - } - - #[test] - fn still_streaming_ui_text_with_path_and_description() { - let input = json!({ - "path": "src/main.rs", - "display_description": "Fix error handling", - "old_string": "old code", - "new_string": "new code" - }); - - assert_eq!( - EditFileTool.still_streaming_ui_text(&input), - "Fix error handling", - ); - } - - #[test] - fn still_streaming_ui_text_no_path_or_description() { - let input = json!({ - "path": "", - "display_description": "", - "old_string": "old code", - "new_string": "new code" - }); - - assert_eq!( - EditFileTool.still_streaming_ui_text(&input), - DEFAULT_UI_TEXT, - ); - } - - #[test] - fn still_streaming_ui_text_with_null() { - let input = serde_json::Value::Null; - - assert_eq!( - EditFileTool.still_streaming_ui_text(&input), - DEFAULT_UI_TEXT, - ); - } - fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); diff --git a/crates/language_model/src/request.rs b/crates/language_model/src/request.rs index dc485e9937..edce3d03b7 100644 --- a/crates/language_model/src/request.rs +++ b/crates/language_model/src/request.rs @@ -297,6 +297,12 @@ impl From for LanguageModelToolResultContent { } } +impl From for LanguageModelToolResultContent { + fn from(image: LanguageModelImage) -> Self { + Self::Image(image) + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)] pub enum MessageContent { Text(String), From d5c4e4b7b2cb8e9bef1bdc955ffd630d4230e192 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 8 Aug 2025 15:54:26 +0200 Subject: [PATCH 197/693] languages: Fix digest check on downloaded artifact for clangd (#35870) Closes 35864 Release Notes: - N/A --- crates/languages/src/c.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/languages/src/c.rs b/crates/languages/src/c.rs index a55d8ff998..df93e51760 100644 --- a/crates/languages/src/c.rs +++ b/crates/languages/src/c.rs @@ -75,6 +75,9 @@ impl super::LspAdapter for CLspAdapter { &*version.downcast::().unwrap(); let version_dir = container_dir.join(format!("clangd_{name}")); let binary_path = version_dir.join("bin/clangd"); + let expected_digest = digest + .as_ref() + .and_then(|digest| digest.strip_prefix("sha256:")); let binary = LanguageServerBinary { path: binary_path.clone(), @@ -99,7 +102,9 @@ impl super::LspAdapter for CLspAdapter { log::warn!("Unable to run {binary_path:?} asset, redownloading: {err}",) }) }; - if let (Some(actual_digest), Some(expected_digest)) = (&metadata.digest, digest) { + if let (Some(actual_digest), Some(expected_digest)) = + (&metadata.digest, expected_digest) + { if actual_digest == expected_digest { if validity_check().await.is_ok() { return Ok(binary); From 8430197df0ffde444c5f4286fc7c22875368709c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 8 Aug 2025 15:56:07 +0200 Subject: [PATCH 198/693] Restore accidentally deleted `EditFileTool::still_streaming_ui_text` (#35871) This was accidentally removed in #35844. Release Notes: - N/A Co-authored-by: Ben Brandt --- crates/assistant_tools/src/edit_file_tool.rs | 85 ++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 311521019d..dce9f49abd 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -120,6 +120,8 @@ struct PartialInput { display_description: String, } +const DEFAULT_UI_TEXT: &str = "Editing file"; + impl Tool for EditFileTool { fn name(&self) -> String { "edit_file".into() @@ -209,6 +211,22 @@ impl Tool for EditFileTool { } } + fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String { + if let Some(input) = serde_json::from_value::(input.clone()).ok() { + let description = input.display_description.trim(); + if !description.is_empty() { + return description.to_string(); + } + + let path = input.path.trim(); + if !path.is_empty() { + return path.to_string(); + } + } + + DEFAULT_UI_TEXT.to_string() + } + fn run( self: Arc, input: serde_json::Value, @@ -1352,6 +1370,73 @@ mod tests { assert_eq!(actual, expected); } + #[test] + fn still_streaming_ui_text_with_path() { + let input = json!({ + "path": "src/main.rs", + "display_description": "", + "old_string": "old code", + "new_string": "new code" + }); + + assert_eq!(EditFileTool.still_streaming_ui_text(&input), "src/main.rs"); + } + + #[test] + fn still_streaming_ui_text_with_description() { + let input = json!({ + "path": "", + "display_description": "Fix error handling", + "old_string": "old code", + "new_string": "new code" + }); + + assert_eq!( + EditFileTool.still_streaming_ui_text(&input), + "Fix error handling", + ); + } + + #[test] + fn still_streaming_ui_text_with_path_and_description() { + let input = json!({ + "path": "src/main.rs", + "display_description": "Fix error handling", + "old_string": "old code", + "new_string": "new code" + }); + + assert_eq!( + EditFileTool.still_streaming_ui_text(&input), + "Fix error handling", + ); + } + + #[test] + fn still_streaming_ui_text_no_path_or_description() { + let input = json!({ + "path": "", + "display_description": "", + "old_string": "old code", + "new_string": "new code" + }); + + assert_eq!( + EditFileTool.still_streaming_ui_text(&input), + DEFAULT_UI_TEXT, + ); + } + + #[test] + fn still_streaming_ui_text_with_null() { + let input = serde_json::Value::Null; + + assert_eq!( + EditFileTool.still_streaming_ui_text(&input), + DEFAULT_UI_TEXT, + ); + } + fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); From f0782aa243a2d61f1e4a83eebabcc5f4354f34c2 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 8 Aug 2025 16:01:48 +0200 Subject: [PATCH 199/693] agent: Don't error when the agent navigation history hasn't been persisted (#35863) This causes us to log an unrecognizable error on every startup otherwise Release Notes: - N/A --- crates/agent/src/history_store.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/agent/src/history_store.rs b/crates/agent/src/history_store.rs index 89f75a72bd..eb39c3e454 100644 --- a/crates/agent/src/history_store.rs +++ b/crates/agent/src/history_store.rs @@ -212,7 +212,16 @@ impl HistoryStore { fn load_recently_opened_entries(cx: &AsyncApp) -> Task>> { cx.background_spawn(async move { let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH); - let contents = smol::fs::read_to_string(path).await?; + let contents = match smol::fs::read_to_string(path).await { + Ok(it) => it, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + return Ok(Vec::new()); + } + Err(e) => { + return Err(e) + .context("deserializing persisted agent panel navigation history"); + } + }; let entries = serde_json::from_str::>(&contents) .context("deserializing persisted agent panel navigation history")? .into_iter() From 95547f099c0872b2c83a90d8406c99838a878929 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 8 Aug 2025 17:17:18 +0300 Subject: [PATCH 200/693] Add release_channel data to request child spans (#35874) Follow-up of https://github.com/zed-industries/zed/pull/35729 Release Notes: - N/A --- crates/collab/src/rpc.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index ec1105b138..18eb1457dc 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -929,6 +929,7 @@ impl Server { login=field::Empty, impersonator=field::Empty, multi_lsp_query_request=field::Empty, + release_channel=field::Empty, { TOTAL_DURATION_MS }=field::Empty, { PROCESSING_DURATION_MS }=field::Empty, { QUEUE_DURATION_MS }=field::Empty, From 51298b691229b71544eef117739922ab5a556ae7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 8 Aug 2025 16:30:49 +0200 Subject: [PATCH 201/693] Use `Project`'s EntityId as the "window id" for Alacritty PTYs (#35876) It's unfortunate to need to have access to a GPUI window in order to create a terminal, because it forces to take a `Window` parameter in entities that otherwise would have been pure models. This pull request changes it so that we pass the `Project`'s entity id, which is equally stable as the window id. Release Notes: - N/A Co-authored-by: Ben Brandt --- crates/assistant_tools/src/terminal_tool.rs | 1 - crates/debugger_ui/src/session/running.rs | 7 ++----- crates/project/src/terminals.rs | 8 +++----- crates/terminal/src/terminal.rs | 14 +++++--------- crates/terminal_view/src/persistence.rs | 5 ++--- crates/terminal_view/src/terminal_panel.rs | 16 ++++------------ crates/terminal_view/src/terminal_view.rs | 5 +---- 7 files changed, 17 insertions(+), 39 deletions(-) diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index 58833c5208..8add60f09a 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -225,7 +225,6 @@ impl Tool for TerminalTool { env, ..Default::default() }), - window, cx, ) })? diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index f2f9e17d89..a3e2805e2b 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -1014,10 +1014,9 @@ impl RunningState { ..task.resolved.clone() }; let terminal = project - .update_in(cx, |project, window, cx| { + .update(cx, |project, cx| { project.create_terminal( TerminalKind::Task(task_with_shell.clone()), - window.window_handle(), cx, ) })? @@ -1189,9 +1188,7 @@ impl RunningState { let workspace = self.workspace.clone(); let weak_project = project.downgrade(); - let terminal_task = project.update(cx, |project, cx| { - project.create_terminal(kind, window.window_handle(), cx) - }); + let terminal_task = project.update(cx, |project, cx| project.create_terminal(kind, cx)); let terminal_task = cx.spawn_in(window, async move |_, cx| { let terminal = terminal_task.await?; diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 973d4e8811..41d8c4b2fd 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -1,7 +1,7 @@ use crate::{Project, ProjectPath}; use anyhow::{Context as _, Result}; use collections::HashMap; -use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, Task, WeakEntity}; +use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity}; use itertools::Itertools; use language::LanguageName; use remote::ssh_session::SshArgs; @@ -98,7 +98,6 @@ impl Project { pub fn create_terminal( &mut self, kind: TerminalKind, - window: AnyWindowHandle, cx: &mut Context, ) -> Task>> { let path: Option> = match &kind { @@ -134,7 +133,7 @@ impl Project { None }; project.update(cx, |project, cx| { - project.create_terminal_with_venv(kind, python_venv_directory, window, cx) + project.create_terminal_with_venv(kind, python_venv_directory, cx) })? }) } @@ -209,7 +208,6 @@ impl Project { &mut self, kind: TerminalKind, python_venv_directory: Option, - window: AnyWindowHandle, cx: &mut Context, ) -> Result> { let this = &mut *self; @@ -396,7 +394,7 @@ impl Project { settings.alternate_scroll, settings.max_scroll_history_lines, is_ssh_terminal, - window, + cx.entity_id().as_u64(), completion_tx, cx, ) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 6e359414d7..d6a09a590f 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -63,9 +63,9 @@ use std::{ use thiserror::Error; use gpui::{ - AnyWindowHandle, App, AppContext as _, Bounds, ClipboardItem, Context, EventEmitter, Hsla, - Keystroke, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, - Rgba, ScrollWheelEvent, SharedString, Size, Task, TouchPhase, Window, actions, black, px, + App, AppContext as _, Bounds, ClipboardItem, Context, EventEmitter, Hsla, Keystroke, Modifiers, + MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Rgba, + ScrollWheelEvent, SharedString, Size, Task, TouchPhase, Window, actions, black, px, }; use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str}; @@ -351,7 +351,7 @@ impl TerminalBuilder { alternate_scroll: AlternateScroll, max_scroll_history_lines: Option, is_ssh_terminal: bool, - window: AnyWindowHandle, + window_id: u64, completion_tx: Sender>, cx: &App, ) -> Result { @@ -463,11 +463,7 @@ impl TerminalBuilder { let term = Arc::new(FairMutex::new(term)); //Setup the pty... - let pty = match tty::new( - &pty_options, - TerminalBounds::default().into(), - window.window_id().as_u64(), - ) { + let pty = match tty::new(&pty_options, TerminalBounds::default().into(), window_id) { Ok(pty) => pty, Err(error) => { bail!(TerminalError { diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index 056365ab8c..b93b267f58 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -245,9 +245,8 @@ async fn deserialize_pane_group( let kind = TerminalKind::Shell( working_directory.as_deref().map(Path::to_path_buf), ); - let window = window.window_handle(); - let terminal = project - .update(cx, |project, cx| project.create_terminal(kind, window, cx)); + let terminal = + project.update(cx, |project, cx| project.create_terminal(kind, cx)); Some(Some(terminal)) } else { Some(None) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index cb1e362884..c9528c39b9 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -432,10 +432,9 @@ impl TerminalPanel { }) .unwrap_or((None, None)); let kind = TerminalKind::Shell(working_directory); - let window_handle = window.window_handle(); let terminal = project .update(cx, |project, cx| { - project.create_terminal_with_venv(kind, python_venv_directory, window_handle, cx) + project.create_terminal_with_venv(kind, python_venv_directory, cx) }) .ok()?; @@ -666,13 +665,10 @@ impl TerminalPanel { "terminal not yet supported for remote projects" ))); } - let window_handle = window.window_handle(); let project = workspace.project().downgrade(); cx.spawn_in(window, async move |workspace, cx| { let terminal = project - .update(cx, |project, cx| { - project.create_terminal(kind, window_handle, cx) - })? + .update(cx, |project, cx| project.create_terminal(kind, cx))? .await?; workspace.update_in(cx, |workspace, window, cx| { @@ -709,11 +705,8 @@ impl TerminalPanel { terminal_panel.active_pane.clone() })?; let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?; - let window_handle = cx.window_handle(); let terminal = project - .update(cx, |project, cx| { - project.create_terminal(kind, window_handle, cx) - })? + .update(cx, |project, cx| project.create_terminal(kind, cx))? .await?; let result = workspace.update_in(cx, |workspace, window, cx| { let terminal_view = Box::new(cx.new(|cx| { @@ -814,7 +807,6 @@ impl TerminalPanel { ) -> Task>> { let reveal = spawn_task.reveal; let reveal_target = spawn_task.reveal_target; - let window_handle = window.window_handle(); let task_workspace = self.workspace.clone(); cx.spawn_in(window, async move |terminal_panel, cx| { let project = terminal_panel.update(cx, |this, cx| { @@ -823,7 +815,7 @@ impl TerminalPanel { })??; let new_terminal = project .update(cx, |project, cx| { - project.create_terminal(TerminalKind::Task(spawn_task), window_handle, cx) + project.create_terminal(TerminalKind::Task(spawn_task), cx) })? .await?; terminal_to_replace.update_in(cx, |terminal_to_replace, window, cx| { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 2e6be5aaf4..361cdd0b1c 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1654,7 +1654,6 @@ impl Item for TerminalView { window: &mut Window, cx: &mut Context, ) -> Option> { - let window_handle = window.window_handle(); let terminal = self .project .update(cx, |project, cx| { @@ -1666,7 +1665,6 @@ impl Item for TerminalView { project.create_terminal_with_venv( TerminalKind::Shell(working_directory), python_venv_directory, - window_handle, cx, ) }) @@ -1802,7 +1800,6 @@ impl SerializableItem for TerminalView { window: &mut Window, cx: &mut App, ) -> Task>> { - let window_handle = window.window_handle(); window.spawn(cx, async move |cx| { let cwd = cx .update(|_window, cx| { @@ -1826,7 +1823,7 @@ impl SerializableItem for TerminalView { let terminal = project .update(cx, |project, cx| { - project.create_terminal(TerminalKind::Shell(cwd), window_handle, cx) + project.create_terminal(TerminalKind::Shell(cwd), cx) })? .await?; cx.update(|window, cx| { From db901278f2a7fd166b7820f1c71b69919cb8315e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 8 Aug 2025 16:39:40 +0200 Subject: [PATCH 202/693] Lay the groundwork to create terminals in `AcpThread` (#35872) This just prepares the types so that it will be easy later to update a tool call with a terminal entity. We paused because we realized we want to simplify how terminals are created in zed, and so that warrants a dedicated pull request that can be reviewed in isolation. Release Notes: - N/A --------- Co-authored-by: Ben Brandt --- crates/acp_thread/src/acp_thread.rs | 67 +++++++++++----- crates/agent2/src/agent.rs | 53 +++++-------- crates/agent2/src/tests/mod.rs | 72 ++++++++++++----- crates/agent2/src/tests/test_tools.rs | 18 +++-- crates/agent2/src/thread.rs | 79 ++++++++++--------- crates/agent2/src/tools/edit_file_tool.rs | 96 ++++++++++++++++++++++- crates/agent2/src/tools/find_path_tool.rs | 10 ++- crates/agent2/src/tools/read_file_tool.rs | 36 +++++---- crates/agent2/src/tools/thinking_tool.rs | 4 +- 9 files changed, 292 insertions(+), 143 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 54bfe56a15..1df0e1def7 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -198,7 +198,7 @@ impl ToolCall { } } - fn update( + fn update_fields( &mut self, fields: acp::ToolCallUpdateFields, language_registry: Arc, @@ -415,6 +415,39 @@ impl ToolCallContent { } } +#[derive(Debug, PartialEq)] +pub enum ToolCallUpdate { + UpdateFields(acp::ToolCallUpdate), + UpdateDiff(ToolCallUpdateDiff), +} + +impl ToolCallUpdate { + fn id(&self) -> &acp::ToolCallId { + match self { + Self::UpdateFields(update) => &update.id, + Self::UpdateDiff(diff) => &diff.id, + } + } +} + +impl From for ToolCallUpdate { + fn from(update: acp::ToolCallUpdate) -> Self { + Self::UpdateFields(update) + } +} + +impl From for ToolCallUpdate { + fn from(diff: ToolCallUpdateDiff) -> Self { + Self::UpdateDiff(diff) + } +} + +#[derive(Debug, PartialEq)] +pub struct ToolCallUpdateDiff { + pub id: acp::ToolCallId, + pub diff: Entity, +} + #[derive(Debug, Default)] pub struct Plan { pub entries: Vec, @@ -710,36 +743,32 @@ impl AcpThread { pub fn update_tool_call( &mut self, - update: acp::ToolCallUpdate, + update: impl Into, cx: &mut Context, ) -> Result<()> { + let update = update.into(); let languages = self.project.read(cx).languages().clone(); let (ix, current_call) = self - .tool_call_mut(&update.id) + .tool_call_mut(update.id()) .context("Tool call not found")?; - current_call.update(update.fields, languages, cx); + match update { + ToolCallUpdate::UpdateFields(update) => { + current_call.update_fields(update.fields, languages, cx); + } + ToolCallUpdate::UpdateDiff(update) => { + current_call.content.clear(); + current_call + .content + .push(ToolCallContent::Diff { diff: update.diff }); + } + } cx.emit(AcpThreadEvent::EntryUpdated(ix)); Ok(()) } - pub fn set_tool_call_diff( - &mut self, - tool_call_id: &acp::ToolCallId, - diff: Entity, - cx: &mut Context, - ) -> Result<()> { - let (ix, current_call) = self - .tool_call_mut(tool_call_id) - .context("Tool call not found")?; - current_call.content.clear(); - current_call.content.push(ToolCallContent::Diff { diff }); - cx.emit(AcpThreadEvent::EntryUpdated(ix)); - Ok(()) - } - /// Updates a tool call if id matches an existing entry, otherwise inserts a new one. pub fn upsert_tool_call(&mut self, tool_call: acp::ToolCall, cx: &mut Context) { let status = ToolCallStatus::Allowed { diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index e7920e7891..df061cd5ed 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -503,29 +503,27 @@ impl acp_thread::AgentConnection for NativeAgentConnection { match event { AgentResponseEvent::Text(text) => { acp_thread.update(cx, |thread, cx| { - thread.handle_session_update( - acp::SessionUpdate::AgentMessageChunk { - content: acp::ContentBlock::Text(acp::TextContent { - text, - annotations: None, - }), - }, + thread.push_assistant_content_block( + acp::ContentBlock::Text(acp::TextContent { + text, + annotations: None, + }), + false, cx, ) - })??; + })?; } AgentResponseEvent::Thinking(text) => { acp_thread.update(cx, |thread, cx| { - thread.handle_session_update( - acp::SessionUpdate::AgentThoughtChunk { - content: acp::ContentBlock::Text(acp::TextContent { - text, - annotations: None, - }), - }, + thread.push_assistant_content_block( + acp::ContentBlock::Text(acp::TextContent { + text, + annotations: None, + }), + true, cx, ) - })??; + })?; } AgentResponseEvent::ToolCallAuthorization(ToolCallAuthorization { tool_call, @@ -551,27 +549,12 @@ impl acp_thread::AgentConnection for NativeAgentConnection { } AgentResponseEvent::ToolCall(tool_call) => { acp_thread.update(cx, |thread, cx| { - thread.handle_session_update( - acp::SessionUpdate::ToolCall(tool_call), - cx, - ) - })??; + thread.upsert_tool_call(tool_call, cx) + })?; } - AgentResponseEvent::ToolCallUpdate(tool_call_update) => { + AgentResponseEvent::ToolCallUpdate(update) => { acp_thread.update(cx, |thread, cx| { - thread.handle_session_update( - acp::SessionUpdate::ToolCallUpdate(tool_call_update), - cx, - ) - })??; - } - AgentResponseEvent::ToolCallDiff(tool_call_diff) => { - acp_thread.update(cx, |thread, cx| { - thread.set_tool_call_diff( - &tool_call_diff.tool_call_id, - tool_call_diff.diff, - cx, - ) + thread.update_tool_call(update, cx) })??; } AgentResponseEvent::Stop(stop_reason) => { diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index b70f54ac0a..273da1dae5 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -306,7 +306,7 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) { let tool_call = expect_tool_call(&mut events).await; assert_eq!(tool_call.title, "nonexistent_tool"); assert_eq!(tool_call.status, acp::ToolCallStatus::Pending); - let update = expect_tool_call_update(&mut events).await; + let update = expect_tool_call_update_fields(&mut events).await; assert_eq!(update.fields.status, Some(acp::ToolCallStatus::Failed)); } @@ -326,7 +326,7 @@ async fn expect_tool_call( } } -async fn expect_tool_call_update( +async fn expect_tool_call_update_fields( events: &mut UnboundedReceiver>, ) -> acp::ToolCallUpdate { let event = events @@ -335,7 +335,9 @@ async fn expect_tool_call_update( .expect("no tool call authorization event received") .unwrap(); match event { - AgentResponseEvent::ToolCallUpdate(tool_call_update) => return tool_call_update, + AgentResponseEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => { + return update + } event => { panic!("Unexpected event {event:?}"); } @@ -425,31 +427,33 @@ async fn test_cancellation(cx: &mut TestAppContext) { }); // Wait until both tools are called. - let mut expected_tool_calls = vec!["echo", "infinite"]; + let mut expected_tools = vec!["Echo", "Infinite Tool"]; let mut echo_id = None; let mut echo_completed = false; while let Some(event) = events.next().await { match event.unwrap() { AgentResponseEvent::ToolCall(tool_call) => { - assert_eq!(tool_call.title, expected_tool_calls.remove(0)); - if tool_call.title == "echo" { + assert_eq!(tool_call.title, expected_tools.remove(0)); + if tool_call.title == "Echo" { echo_id = Some(tool_call.id); } } - AgentResponseEvent::ToolCallUpdate(acp::ToolCallUpdate { - id, - fields: - acp::ToolCallUpdateFields { - status: Some(acp::ToolCallStatus::Completed), - .. - }, - }) if Some(&id) == echo_id.as_ref() => { + AgentResponseEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields( + acp::ToolCallUpdate { + id, + fields: + acp::ToolCallUpdateFields { + status: Some(acp::ToolCallStatus::Completed), + .. + }, + }, + )) if Some(&id) == echo_id.as_ref() => { echo_completed = true; } _ => {} } - if expected_tool_calls.is_empty() && echo_completed { + if expected_tools.is_empty() && echo_completed { break; } } @@ -647,13 +651,26 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) { let mut events = thread.update(cx, |thread, cx| thread.send(model.clone(), "Think", cx)); cx.run_until_parked(); - let input = json!({ "content": "Thinking hard!" }); + // Simulate streaming partial input. + let input = json!({}); fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { id: "1".into(), name: ThinkingTool.name().into(), raw_input: input.to_string(), input, + is_input_complete: false, + }, + )); + + // Input streaming completed + let input = json!({ "content": "Thinking hard!" }); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + LanguageModelToolUse { + id: "1".into(), + name: "thinking".into(), + raw_input: input.to_string(), + input, is_input_complete: true, }, )); @@ -670,22 +687,35 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) { status: acp::ToolCallStatus::Pending, content: vec![], locations: vec![], - raw_input: Some(json!({ "content": "Thinking hard!" })), + raw_input: Some(json!({})), raw_output: None, } ); - let update = expect_tool_call_update(&mut events).await; + let update = expect_tool_call_update_fields(&mut events).await; assert_eq!( update, acp::ToolCallUpdate { id: acp::ToolCallId("1".into()), fields: acp::ToolCallUpdateFields { - status: Some(acp::ToolCallStatus::InProgress,), + title: Some("Thinking".into()), + kind: Some(acp::ToolKind::Think), + raw_input: Some(json!({ "content": "Thinking hard!" })), ..Default::default() }, } ); - let update = expect_tool_call_update(&mut events).await; + let update = expect_tool_call_update_fields(&mut events).await; + assert_eq!( + update, + acp::ToolCallUpdate { + id: acp::ToolCallId("1".into()), + fields: acp::ToolCallUpdateFields { + status: Some(acp::ToolCallStatus::InProgress), + ..Default::default() + }, + } + ); + let update = expect_tool_call_update_fields(&mut events).await; assert_eq!( update, acp::ToolCallUpdate { @@ -696,7 +726,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) { }, } ); - let update = expect_tool_call_update(&mut events).await; + let update = expect_tool_call_update_fields(&mut events).await; assert_eq!( update, acp::ToolCallUpdate { diff --git a/crates/agent2/src/tests/test_tools.rs b/crates/agent2/src/tests/test_tools.rs index d22ff6ace8..d06614f3fe 100644 --- a/crates/agent2/src/tests/test_tools.rs +++ b/crates/agent2/src/tests/test_tools.rs @@ -24,7 +24,7 @@ impl AgentTool for EchoTool { acp::ToolKind::Other } - fn initial_title(&self, _: Self::Input) -> SharedString { + fn initial_title(&self, _input: Result) -> SharedString { "Echo".into() } @@ -55,8 +55,12 @@ impl AgentTool for DelayTool { "delay".into() } - fn initial_title(&self, input: Self::Input) -> SharedString { - format!("Delay {}ms", input.ms).into() + fn initial_title(&self, input: Result) -> SharedString { + if let Ok(input) = input { + format!("Delay {}ms", input.ms).into() + } else { + "Delay".into() + } } fn kind(&self) -> acp::ToolKind { @@ -96,7 +100,7 @@ impl AgentTool for ToolRequiringPermission { acp::ToolKind::Other } - fn initial_title(&self, _input: Self::Input) -> SharedString { + fn initial_title(&self, _input: Result) -> SharedString { "This tool requires permission".into() } @@ -131,8 +135,8 @@ impl AgentTool for InfiniteTool { acp::ToolKind::Other } - fn initial_title(&self, _input: Self::Input) -> SharedString { - "This is the tool that never ends... it just goes on and on my friends!".into() + fn initial_title(&self, _input: Result) -> SharedString { + "Infinite Tool".into() } fn run( @@ -182,7 +186,7 @@ impl AgentTool for WordListTool { acp::ToolKind::Other } - fn initial_title(&self, _input: Self::Input) -> SharedString { + fn initial_title(&self, _input: Result) -> SharedString { "List of random words".into() } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 98f2d0651d..f664e0f5d2 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -102,9 +102,8 @@ pub enum AgentResponseEvent { Text(String), Thinking(String), ToolCall(acp::ToolCall), - ToolCallUpdate(acp::ToolCallUpdate), + ToolCallUpdate(acp_thread::ToolCallUpdate), ToolCallAuthorization(ToolCallAuthorization), - ToolCallDiff(ToolCallDiff), Stop(acp::StopReason), } @@ -115,12 +114,6 @@ pub struct ToolCallAuthorization { pub response: oneshot::Sender, } -#[derive(Debug)] -pub struct ToolCallDiff { - pub tool_call_id: acp::ToolCallId, - pub diff: Entity, -} - pub struct Thread { messages: Vec, completion_mode: CompletionMode, @@ -294,7 +287,7 @@ impl Thread { while let Some(tool_result) = tool_uses.next().await { log::info!("Tool finished {:?}", tool_result); - event_stream.send_tool_call_update( + event_stream.update_tool_call_fields( &tool_result.tool_use_id, acp::ToolCallUpdateFields { status: Some(if tool_result.is_error { @@ -474,15 +467,24 @@ impl Thread { } }); + let mut title = SharedString::from(&tool_use.name); + let mut kind = acp::ToolKind::Other; + if let Some(tool) = tool.as_ref() { + title = tool.initial_title(tool_use.input.clone()); + kind = tool.kind(); + } + if push_new_tool_use { - event_stream.send_tool_call(tool.as_ref(), &tool_use); + event_stream.send_tool_call(&tool_use.id, title, kind, tool_use.input.clone()); last_message .content .push(MessageContent::ToolUse(tool_use.clone())); } else { - event_stream.send_tool_call_update( + event_stream.update_tool_call_fields( &tool_use.id, acp::ToolCallUpdateFields { + title: Some(title.into()), + kind: Some(kind), raw_input: Some(tool_use.input.clone()), ..Default::default() }, @@ -506,7 +508,7 @@ impl Thread { let tool_event_stream = ToolCallEventStream::new(&tool_use, tool.kind(), event_stream.clone()); - tool_event_stream.send_update(acp::ToolCallUpdateFields { + tool_event_stream.update_fields(acp::ToolCallUpdateFields { status: Some(acp::ToolCallStatus::InProgress), ..Default::default() }); @@ -693,7 +695,7 @@ where fn kind(&self) -> acp::ToolKind; /// The initial tool title to display. Can be updated during the tool run. - fn initial_title(&self, input: Self::Input) -> SharedString; + fn initial_title(&self, input: Result) -> SharedString; /// Returns the JSON schema that describes the tool's input. fn input_schema(&self) -> Schema { @@ -724,7 +726,7 @@ pub trait AnyAgentTool { fn name(&self) -> SharedString; fn description(&self, cx: &mut App) -> SharedString; fn kind(&self) -> acp::ToolKind; - fn initial_title(&self, input: serde_json::Value) -> Result; + fn initial_title(&self, input: serde_json::Value) -> SharedString; fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result; fn run( self: Arc, @@ -750,9 +752,9 @@ where self.0.kind() } - fn initial_title(&self, input: serde_json::Value) -> Result { - let parsed_input = serde_json::from_value(input)?; - Ok(self.0.initial_title(parsed_input)) + fn initial_title(&self, input: serde_json::Value) -> SharedString { + let parsed_input = serde_json::from_value(input.clone()).map_err(|_| input); + self.0.initial_title(parsed_input) } fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { @@ -842,17 +844,17 @@ impl AgentResponseEventStream { fn send_tool_call( &self, - tool: Option<&Arc>, - tool_use: &LanguageModelToolUse, + id: &LanguageModelToolUseId, + title: SharedString, + kind: acp::ToolKind, + input: serde_json::Value, ) { self.0 .unbounded_send(Ok(AgentResponseEvent::ToolCall(Self::initial_tool_call( - &tool_use.id, - tool.and_then(|t| t.initial_title(tool_use.input.clone()).ok()) - .map(|i| i.into()) - .unwrap_or_else(|| tool_use.name.to_string()), - tool.map(|t| t.kind()).unwrap_or(acp::ToolKind::Other), - tool_use.input.clone(), + id, + title.to_string(), + kind, + input, )))) .ok(); } @@ -875,7 +877,7 @@ impl AgentResponseEventStream { } } - fn send_tool_call_update( + fn update_tool_call_fields( &self, tool_use_id: &LanguageModelToolUseId, fields: acp::ToolCallUpdateFields, @@ -885,14 +887,21 @@ impl AgentResponseEventStream { acp::ToolCallUpdate { id: acp::ToolCallId(tool_use_id.to_string().into()), fields, - }, + } + .into(), ))) .ok(); } - fn send_tool_call_diff(&self, tool_call_diff: ToolCallDiff) { + fn update_tool_call_diff(&self, tool_use_id: &LanguageModelToolUseId, diff: Entity) { self.0 - .unbounded_send(Ok(AgentResponseEvent::ToolCallDiff(tool_call_diff))) + .unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate( + acp_thread::ToolCallUpdateDiff { + id: acp::ToolCallId(tool_use_id.to_string().into()), + diff, + } + .into(), + ))) .ok(); } @@ -964,15 +973,13 @@ impl ToolCallEventStream { } } - pub fn send_update(&self, fields: acp::ToolCallUpdateFields) { - self.stream.send_tool_call_update(&self.tool_use_id, fields); + pub fn update_fields(&self, fields: acp::ToolCallUpdateFields) { + self.stream + .update_tool_call_fields(&self.tool_use_id, fields); } - pub fn send_diff(&self, diff: Entity) { - self.stream.send_tool_call_diff(ToolCallDiff { - tool_call_id: acp::ToolCallId(self.tool_use_id.to_string().into()), - diff, - }); + pub fn update_diff(&self, diff: Entity) { + self.stream.update_tool_call_diff(&self.tool_use_id, diff); } pub fn authorize(&self, title: String) -> impl use<> + Future> { diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 0dbe0be217..0858bb501c 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -1,3 +1,4 @@ +use crate::{AgentTool, Thread, ToolCallEventStream}; use acp_thread::Diff; use agent_client_protocol as acp; use anyhow::{anyhow, Context as _, Result}; @@ -20,7 +21,7 @@ use std::sync::Arc; use ui::SharedString; use util::ResultExt; -use crate::{AgentTool, Thread, ToolCallEventStream}; +const DEFAULT_UI_TEXT: &str = "Editing file"; /// This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead. /// @@ -78,6 +79,14 @@ pub struct EditFileToolInput { pub mode: EditFileMode, } +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +struct EditFileToolPartialInput { + #[serde(default)] + path: String, + #[serde(default)] + display_description: String, +} + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum EditFileMode { @@ -182,8 +191,27 @@ impl AgentTool for EditFileTool { acp::ToolKind::Edit } - fn initial_title(&self, input: Self::Input) -> SharedString { - input.display_description.into() + fn initial_title(&self, input: Result) -> SharedString { + match input { + Ok(input) => input.display_description.into(), + Err(raw_input) => { + if let Some(input) = + serde_json::from_value::(raw_input).ok() + { + let description = input.display_description.trim(); + if !description.is_empty() { + return description.to_string().into(); + } + + let path = input.path.trim().to_string(); + if !path.is_empty() { + return path.into(); + } + } + + DEFAULT_UI_TEXT.into() + } + } } fn run( @@ -226,7 +254,7 @@ impl AgentTool for EditFileTool { .await?; let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?; - event_stream.send_diff(diff.clone()); + event_stream.update_diff(diff.clone()); let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; let old_text = cx @@ -1348,6 +1376,66 @@ mod tests { } } + #[gpui::test] + async fn test_initial_title_with_partial_input(cx: &mut TestAppContext) { + init_test(cx); + let fs = project::FakeFs::new(cx.executor()); + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let model = Arc::new(FakeLanguageModel::default()); + let thread = cx.new(|_| { + Thread::new( + project.clone(), + Rc::default(), + action_log.clone(), + Templates::new(), + model.clone(), + ) + }); + let tool = Arc::new(EditFileTool { thread }); + + assert_eq!( + tool.initial_title(Err(json!({ + "path": "src/main.rs", + "display_description": "", + "old_string": "old code", + "new_string": "new code" + }))), + "src/main.rs" + ); + assert_eq!( + tool.initial_title(Err(json!({ + "path": "", + "display_description": "Fix error handling", + "old_string": "old code", + "new_string": "new code" + }))), + "Fix error handling" + ); + assert_eq!( + tool.initial_title(Err(json!({ + "path": "src/main.rs", + "display_description": "Fix error handling", + "old_string": "old code", + "new_string": "new code" + }))), + "Fix error handling" + ); + assert_eq!( + tool.initial_title(Err(json!({ + "path": "", + "display_description": "", + "old_string": "old code", + "new_string": "new code" + }))), + DEFAULT_UI_TEXT + ); + assert_eq!( + tool.initial_title(Err(serde_json::Value::Null)), + DEFAULT_UI_TEXT + ); + } + fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); diff --git a/crates/agent2/src/tools/find_path_tool.rs b/crates/agent2/src/tools/find_path_tool.rs index 24bdcded8c..f4589e5600 100644 --- a/crates/agent2/src/tools/find_path_tool.rs +++ b/crates/agent2/src/tools/find_path_tool.rs @@ -94,8 +94,12 @@ impl AgentTool for FindPathTool { acp::ToolKind::Search } - fn initial_title(&self, input: Self::Input) -> SharedString { - format!("Find paths matching “`{}`”", input.glob).into() + fn initial_title(&self, input: Result) -> SharedString { + let mut title = "Find paths".to_string(); + if let Ok(input) = input { + title.push_str(&format!(" matching “`{}`”", input.glob)); + } + title.into() } fn run( @@ -111,7 +115,7 @@ impl AgentTool for FindPathTool { let paginated_matches: &[PathBuf] = &matches[cmp::min(input.offset, matches.len()) ..cmp::min(input.offset + RESULTS_PER_PAGE, matches.len())]; - event_stream.send_update(acp::ToolCallUpdateFields { + event_stream.update_fields(acp::ToolCallUpdateFields { title: Some(if paginated_matches.len() == 0 { "No matches".into() } else if paginated_matches.len() == 1 { diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index 3d91e3dc74..7bbe3ac4c1 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -70,24 +70,28 @@ impl AgentTool for ReadFileTool { acp::ToolKind::Read } - fn initial_title(&self, input: Self::Input) -> SharedString { - let path = &input.path; - match (input.start_line, input.end_line) { - (Some(start), Some(end)) => { - format!( - "[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))", - path, start, end, path, start, end - ) + fn initial_title(&self, input: Result) -> SharedString { + if let Ok(input) = input { + let path = &input.path; + match (input.start_line, input.end_line) { + (Some(start), Some(end)) => { + format!( + "[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))", + path, start, end, path, start, end + ) + } + (Some(start), None) => { + format!( + "[Read file `{}` (from line {})](@selection:{}:({}-{}))", + path, start, path, start, start + ) + } + _ => format!("[Read file `{}`](@file:{})", path, path), } - (Some(start), None) => { - format!( - "[Read file `{}` (from line {})](@selection:{}:({}-{}))", - path, start, path, start, start - ) - } - _ => format!("[Read file `{}`](@file:{})", path, path), + .into() + } else { + "Read file".into() } - .into() } fn run( diff --git a/crates/agent2/src/tools/thinking_tool.rs b/crates/agent2/src/tools/thinking_tool.rs index d85370e7e5..43647bb468 100644 --- a/crates/agent2/src/tools/thinking_tool.rs +++ b/crates/agent2/src/tools/thinking_tool.rs @@ -30,7 +30,7 @@ impl AgentTool for ThinkingTool { acp::ToolKind::Think } - fn initial_title(&self, _input: Self::Input) -> SharedString { + fn initial_title(&self, _input: Result) -> SharedString { "Thinking".into() } @@ -40,7 +40,7 @@ impl AgentTool for ThinkingTool { event_stream: ToolCallEventStream, _cx: &mut App, ) -> Task> { - event_stream.send_update(acp::ToolCallUpdateFields { + event_stream.update_fields(acp::ToolCallUpdateFields { content: Some(vec![input.content.into()]), ..Default::default() }); From 2a310d78e1d6008884b5b347e5db01fb939073eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Fri, 8 Aug 2025 22:42:20 +0800 Subject: [PATCH 203/693] =?UTF-8?q?windows:=20Fix=20the=20issue=20where=20?= =?UTF-8?q?`ags.dll`=20couldn=E2=80=99t=20be=20replaced=20during=20update?= =?UTF-8?q?=20(#35877)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release Notes: - N/A --------- Co-authored-by: Kirill Bulatov --- .../src/platform/windows/directx_renderer.rs | 66 +++++++++++-------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/crates/gpui/src/platform/windows/directx_renderer.rs b/crates/gpui/src/platform/windows/directx_renderer.rs index ac285b79ac..585b1dab1c 100644 --- a/crates/gpui/src/platform/windows/directx_renderer.rs +++ b/crates/gpui/src/platform/windows/directx_renderer.rs @@ -4,15 +4,16 @@ use ::util::ResultExt; use anyhow::{Context, Result}; use windows::{ Win32::{ - Foundation::{HMODULE, HWND}, + Foundation::{FreeLibrary, HMODULE, HWND}, Graphics::{ Direct3D::*, Direct3D11::*, DirectComposition::*, Dxgi::{Common::*, *}, }, + System::LibraryLoader::LoadLibraryA, }, - core::Interface, + core::{Interface, PCSTR}, }; use crate::{ @@ -1618,17 +1619,32 @@ pub(crate) mod shader_resources { } } +fn with_dll_library(dll_name: PCSTR, f: F) -> Result +where + F: FnOnce(HMODULE) -> Result, +{ + let library = unsafe { + LoadLibraryA(dll_name).with_context(|| format!("Loading dll: {}", dll_name.display()))? + }; + let result = f(library); + unsafe { + FreeLibrary(library) + .with_context(|| format!("Freeing dll: {}", dll_name.display())) + .log_err(); + } + result +} + mod nvidia { use std::{ ffi::CStr, os::raw::{c_char, c_int, c_uint}, }; - use anyhow::{Context, Result}; - use windows::{ - Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA}, - core::s, - }; + use anyhow::Result; + use windows::{Win32::System::LibraryLoader::GetProcAddress, core::s}; + + use crate::platform::windows::directx_renderer::with_dll_library; // https://github.com/NVIDIA/nvapi/blob/7cb76fce2f52de818b3da497af646af1ec16ce27/nvapi_lite_common.h#L180 const NVAPI_SHORT_STRING_MAX: usize = 64; @@ -1645,13 +1661,12 @@ mod nvidia { ) -> c_int; pub(super) fn get_driver_version() -> Result { - unsafe { - // Try to load the NVIDIA driver DLL - #[cfg(target_pointer_width = "64")] - let nvidia_dll = LoadLibraryA(s!("nvapi64.dll")).context("Can't load nvapi64.dll")?; - #[cfg(target_pointer_width = "32")] - let nvidia_dll = LoadLibraryA(s!("nvapi.dll")).context("Can't load nvapi.dll")?; + #[cfg(target_pointer_width = "64")] + let nvidia_dll_name = s!("nvapi64.dll"); + #[cfg(target_pointer_width = "32")] + let nvidia_dll_name = s!("nvapi.dll"); + with_dll_library(nvidia_dll_name, |nvidia_dll| unsafe { let nvapi_query_addr = GetProcAddress(nvidia_dll, s!("nvapi_QueryInterface")) .ok_or_else(|| anyhow::anyhow!("Failed to get nvapi_QueryInterface address"))?; let nvapi_query: extern "C" fn(u32) -> *mut () = std::mem::transmute(nvapi_query_addr); @@ -1686,18 +1701,17 @@ mod nvidia { minor, branch_string.to_string_lossy() )) - } + }) } } mod amd { use std::os::raw::{c_char, c_int, c_void}; - use anyhow::{Context, Result}; - use windows::{ - Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA}, - core::s, - }; + use anyhow::Result; + use windows::{Win32::System::LibraryLoader::GetProcAddress, core::s}; + + use crate::platform::windows::directx_renderer::with_dll_library; // https://github.com/GPUOpen-LibrariesAndSDKs/AGS_SDK/blob/5d8812d703d0335741b6f7ffc37838eeb8b967f7/ags_lib/inc/amd_ags.h#L145 const AGS_CURRENT_VERSION: i32 = (6 << 22) | (3 << 12); @@ -1731,14 +1745,12 @@ mod amd { type agsDeInitialize_t = unsafe extern "C" fn(context: *mut AGSContext) -> c_int; pub(super) fn get_driver_version() -> Result { - unsafe { - #[cfg(target_pointer_width = "64")] - let amd_dll = - LoadLibraryA(s!("amd_ags_x64.dll")).context("Failed to load AMD AGS library")?; - #[cfg(target_pointer_width = "32")] - let amd_dll = - LoadLibraryA(s!("amd_ags_x86.dll")).context("Failed to load AMD AGS library")?; + #[cfg(target_pointer_width = "64")] + let amd_dll_name = s!("amd_ags_x64.dll"); + #[cfg(target_pointer_width = "32")] + let amd_dll_name = s!("amd_ags_x86.dll"); + with_dll_library(amd_dll_name, |amd_dll| unsafe { let ags_initialize_addr = GetProcAddress(amd_dll, s!("agsInitialize")) .ok_or_else(|| anyhow::anyhow!("Failed to get agsInitialize address"))?; let ags_deinitialize_addr = GetProcAddress(amd_dll, s!("agsDeInitialize")) @@ -1784,7 +1796,7 @@ mod amd { ags_deinitialize(context); Ok(format!("{} ({})", software_version, driver_version)) - } + }) } } From 327456d1d2ff748797bd2a0f93f79c3b099599ba Mon Sep 17 00:00:00 2001 From: Alvaro Parker <64918109+AlvaroParker@users.noreply.github.com> Date: Fri, 8 Aug 2025 10:47:00 -0400 Subject: [PATCH 204/693] context menu: Fix go to first element on context menu (#35875) Closes #35873 Release Notes: - Fixed bug where context menu doesn't circle back to the first item when the last item is not selectable --- crates/ui/src/components/context_menu.rs | 29 +++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 77468fd295..21ab283d88 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -679,18 +679,18 @@ impl ContextMenu { let next_index = ix + 1; if self.items.len() <= next_index { self.select_first(&SelectFirst, window, cx); + return; } else { for (ix, item) in self.items.iter().enumerate().skip(next_index) { if item.is_selectable() { self.select_index(ix, window, cx); cx.notify(); - break; + return; } } } - } else { - self.select_first(&SelectFirst, window, cx); } + self.select_first(&SelectFirst, window, cx); } pub fn select_previous( @@ -1203,6 +1203,7 @@ mod tests { .separator() .separator() .entry("Last entry", None, |_, _| {}) + .header("Last header") }) }); @@ -1255,5 +1256,27 @@ mod tests { "Should go back to previous selectable entry (first)" ); }); + + context_menu.update_in(cx, |context_menu, window, cx| { + context_menu.select_first(&SelectFirst, window, cx); + assert_eq!( + Some(2), + context_menu.selected_index, + "Should start from the first selectable entry" + ); + + context_menu.select_previous(&SelectPrevious, window, cx); + assert_eq!( + Some(5), + context_menu.selected_index, + "Should wrap around to last selectable entry" + ); + context_menu.select_next(&SelectNext, window, cx); + assert_eq!( + Some(2), + context_menu.selected_index, + "Should wrap around to first selectable entry" + ); + }); } } From f2435f7284a4195dbf46439ce7a95b9e53ec44b5 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 8 Aug 2025 13:02:17 -0400 Subject: [PATCH 205/693] onboarding: Fix a double lease panic caused by Onboarding::clone_on_split (#35815) Release Notes: - N/A --- crates/onboarding/src/onboarding.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 98f61df97b..342b52bdda 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -565,9 +565,13 @@ impl Item for Onboarding { _: &mut Window, cx: &mut Context, ) -> Option> { - self.workspace - .update(cx, |workspace, cx| Onboarding::new(workspace, cx)) - .ok() + Some(cx.new(|cx| Onboarding { + workspace: self.workspace.clone(), + user_store: self.user_store.clone(), + selected_page: self.selected_page, + focus_handle: cx.focus_handle(), + _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), + })) } fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { From 315a92091b91b4448b4c95ecf9e3dc3fa1bd7a62 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 8 Aug 2025 14:10:09 -0400 Subject: [PATCH 206/693] Ensure Edit Prediction provider is properly assigned on sign-in (#35885) This PR fixes an issue where Edit Predictions would not be available in buffers that were opened when the workspace loaded. The issue was that there was a race condition between fetching/setting the authenticated user state and when we assigned the Edit Prediction provider to buffers that were already opened. We now wait for the event that we emit when we have successfully loaded the user in order to assign the Edit Prediction provider, as we'll know the user has been loaded into the `UserStore` by that point. Closes https://github.com/zed-industries/zed/issues/35883 Release Notes: - Fixed an issue where Edit Predictions were not working in buffers that were open when the workspace initially loaded. Co-authored-by: Richard Feldman --- crates/client/src/user.rs | 39 +++++++++++++------ .../zed/src/zed/edit_prediction_registry.rs | 29 ++++++-------- 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 9f76dd7ad0..faf46945d8 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -226,17 +226,35 @@ impl UserStore { match status { Status::Authenticated | Status::Connected { .. } => { if let Some(user_id) = client.user_id() { - let response = client.cloud_client().get_authenticated_user().await; - let mut current_user = None; + let response = client + .cloud_client() + .get_authenticated_user() + .await + .log_err(); + + let current_user_and_response = if let Some(response) = response { + let user = Arc::new(User { + id: user_id, + github_login: response.user.github_login.clone().into(), + avatar_uri: response.user.avatar_url.clone().into(), + name: response.user.name.clone(), + }); + + Some((user, response)) + } else { + None + }; + current_user_tx + .send( + current_user_and_response + .as_ref() + .map(|(user, _)| user.clone()), + ) + .await + .ok(); + cx.update(|cx| { - if let Some(response) = response.log_err() { - let user = Arc::new(User { - id: user_id, - github_login: response.user.github_login.clone().into(), - avatar_uri: response.user.avatar_url.clone().into(), - name: response.user.name.clone(), - }); - current_user = Some(user.clone()); + if let Some((user, response)) = current_user_and_response { this.update(cx, |this, cx| { this.by_github_login .insert(user.github_login.clone(), user_id); @@ -247,7 +265,6 @@ impl UserStore { anyhow::Ok(()) } })??; - current_user_tx.send(current_user).await.ok(); this.update(cx, |_, cx| cx.notify())?; } diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index b9f561c0e7..da4b6e78c6 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -5,11 +5,9 @@ use editor::Editor; use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, WeakEntity}; use language::language_settings::{EditPredictionProvider, all_language_settings}; use settings::SettingsStore; -use smol::stream::StreamExt; use std::{cell::RefCell, rc::Rc, sync::Arc}; use supermaven::{Supermaven, SupermavenCompletionProvider}; use ui::Window; -use util::ResultExt; use workspace::Workspace; use zeta::{ProviderDataCollection, ZetaEditPredictionProvider}; @@ -59,25 +57,20 @@ pub fn init(client: Arc, user_store: Entity, cx: &mut App) { cx.on_action(clear_zeta_edit_history); let mut provider = all_language_settings(None, cx).edit_predictions.provider; - cx.spawn({ - let user_store = user_store.clone(); + cx.subscribe(&user_store, { let editors = editors.clone(); let client = client.clone(); - - async move |cx| { - let mut status = client.status(); - while let Some(_status) = status.next().await { - cx.update(|cx| { - assign_edit_prediction_providers( - &editors, - provider, - &client, - user_store.clone(), - cx, - ); - }) - .log_err(); + move |user_store, event, cx| match event { + client::user::Event::PrivateUserInfoUpdated => { + assign_edit_prediction_providers( + &editors, + provider, + &client, + user_store.clone(), + cx, + ); } + _ => {} } }) .detach(); From 530f5075d0c1e1084a27ab6705bfe3458c9d8e72 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:34:25 -0300 Subject: [PATCH 207/693] ui: Fix switch field info tooltip (#35882) Passing an empty on_click handler so that clicking on the info icon doesn't actually trigger the switch itself, which happens if you click anywhere in the general switch field surface area. Release Notes: - N/A --- crates/ui/src/components/toggle.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index 4b985fd2c2..59c056859d 100644 --- a/crates/ui/src/components/toggle.rs +++ b/crates/ui/src/components/toggle.rs @@ -659,10 +659,12 @@ impl RenderOnce for SwitchField { .icon_size(IconSize::XSmall) .icon_color(Color::Muted) .shape(crate::IconButtonShape::Square) + .style(ButtonStyle::Transparent) .tooltip({ let tooltip = tooltip_fn.clone(); move |window, cx| tooltip(window, cx) - }), + }) + .on_click(|_, _, _| {}), // Intentional empty on click handler so that clicking on the info tooltip icon doesn't trigger the switch toggle ) }); From 2cde6da5ffb1960d919e53904d0b50a33c432975 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:34:36 -0300 Subject: [PATCH 208/693] Redesign and clean up all icons across Zed (#35856) - [x] Clean up unused and old icons - [x] Swap SVG for all in-use icons with the redesigned version - [x] Document guidelines Release Notes: - N/A --- assets/icons/arrow_circle.svg | 8 +-- assets/icons/arrow_down.svg | 2 +- assets/icons/arrow_down10.svg | 2 +- assets/icons/arrow_down_from_line.svg | 1 - assets/icons/arrow_down_right.svg | 5 +- assets/icons/arrow_left.svg | 2 +- assets/icons/arrow_right.svg | 2 +- assets/icons/arrow_right_left.svg | 7 ++- assets/icons/arrow_up.svg | 2 +- assets/icons/arrow_up_alt.svg | 3 - assets/icons/arrow_up_from_line.svg | 1 - assets/icons/arrow_up_right.svg | 5 +- assets/icons/arrow_up_right_alt.svg | 3 - assets/icons/backspace.svg | 6 +- assets/icons/binary.svg | 2 +- assets/icons/blocks.svg | 2 +- assets/icons/book.svg | 2 +- assets/icons/book_copy.svg | 2 +- assets/icons/bug_off.svg | 1 - assets/icons/caret_down.svg | 8 --- assets/icons/caret_up.svg | 8 --- assets/icons/case_sensitive.svg | 9 +-- assets/icons/check.svg | 2 +- assets/icons/check_circle.svg | 6 +- assets/icons/check_double.svg | 5 +- assets/icons/chevron_down.svg | 4 +- assets/icons/chevron_down_small.svg | 3 - assets/icons/chevron_left.svg | 4 +- assets/icons/chevron_right.svg | 4 +- assets/icons/chevron_up.svg | 4 +- assets/icons/chevron_up_down.svg | 5 +- assets/icons/circle.svg | 4 +- assets/icons/circle_check.svg | 2 +- assets/icons/circle_help.svg | 6 +- assets/icons/circle_off.svg | 1 - assets/icons/close.svg | 4 +- assets/icons/cloud.svg | 1 - assets/icons/cloud_download.svg | 2 +- assets/icons/code.svg | 2 +- assets/icons/cog.svg | 2 +- assets/icons/command.svg | 2 +- assets/icons/context.svg | 6 -- assets/icons/control.svg | 2 +- assets/icons/copilot.svg | 16 ++--- assets/icons/copilot_disabled.svg | 10 ++-- assets/icons/copilot_error.svg | 6 +- assets/icons/copilot_init.svg | 4 +- assets/icons/copy.svg | 5 +- assets/icons/countdown_timer.svg | 2 +- assets/icons/crosshair.svg | 12 ++-- assets/icons/dash.svg | 2 +- assets/icons/database_zap.svg | 2 +- assets/icons/debug.svg | 20 +++---- assets/icons/debug_breakpoint.svg | 4 +- assets/icons/debug_continue.svg | 2 +- assets/icons/debug_detach.svg | 2 +- assets/icons/debug_disabled_breakpoint.svg | 4 +- .../icons/debug_disabled_log_breakpoint.svg | 4 +- assets/icons/debug_ignore_breakpoints.svg | 2 +- assets/icons/debug_log_breakpoint.svg | 4 +- assets/icons/debug_pause.svg | 5 +- assets/icons/debug_restart.svg | 1 - assets/icons/debug_step_back.svg | 2 +- assets/icons/debug_step_into.svg | 6 +- assets/icons/debug_step_out.svg | 6 +- assets/icons/debug_step_over.svg | 6 +- assets/icons/debug_stop.svg | 1 - assets/icons/delete.svg | 1 - assets/icons/diff.svg | 2 +- assets/icons/disconnected.svg | 4 +- assets/icons/document_text.svg | 3 - assets/icons/download.svg | 2 +- assets/icons/ellipsis.svg | 8 +-- assets/icons/ellipsis_vertical.svg | 6 +- assets/icons/equal.svg | 1 - assets/icons/eraser.svg | 5 +- assets/icons/escape.svg | 2 +- assets/icons/expand_down.svg | 6 +- assets/icons/expand_up.svg | 6 +- assets/icons/expand_vertical.svg | 2 +- assets/icons/external_link.svg | 5 -- assets/icons/eye.svg | 5 +- assets/icons/file.svg | 5 +- assets/icons/file_code.svg | 2 +- assets/icons/file_create.svg | 5 -- assets/icons/file_diff.svg | 2 +- assets/icons/file_markdown.svg | 1 + assets/icons/file_search.svg | 5 -- assets/icons/file_text.svg | 6 -- assets/icons/file_text_filled.svg | 3 + assets/icons/file_text_outlined.svg | 6 ++ assets/icons/file_tree.svg | 6 +- assets/icons/flame.svg | 2 +- assets/icons/folder.svg | 2 +- assets/icons/folder_search.svg | 5 ++ assets/icons/folder_x.svg | 5 -- assets/icons/font.svg | 2 +- assets/icons/font_size.svg | 2 +- assets/icons/font_weight.svg | 2 +- assets/icons/forward_arrow.svg | 5 +- assets/icons/function.svg | 1 - assets/icons/generic_maximize.svg | 2 +- assets/icons/generic_restore.svg | 4 +- assets/icons/git_branch.svg | 2 +- assets/icons/git_branch_alt.svg | 7 +++ assets/icons/git_branch_small.svg | 7 --- assets/icons/github.svg | 2 +- assets/icons/globe.svg | 12 ---- assets/icons/hammer.svg | 1 - assets/icons/hash.svg | 7 +-- assets/icons/image.svg | 2 +- assets/icons/inlay_hint.svg | 5 -- assets/icons/keyboard.svg | 2 +- assets/icons/layout.svg | 5 -- assets/icons/library.svg | 7 ++- assets/icons/light_bulb.svg | 3 - assets/icons/line_height.svg | 7 +-- assets/icons/link.svg | 3 - assets/icons/list_collapse.svg | 2 +- assets/icons/list_todo.svg | 2 +- assets/icons/list_x.svg | 10 ++-- assets/icons/load_circle.svg | 2 +- assets/icons/location_edit.svg | 2 +- assets/icons/logo_96.svg | 3 - assets/icons/lsp_debug.svg | 12 ---- assets/icons/lsp_restart.svg | 4 -- assets/icons/lsp_stop.svg | 4 -- assets/icons/magnifying_glass.svg | 1 + assets/icons/mail_open.svg | 1 - assets/icons/maximize.svg | 7 ++- assets/icons/menu.svg | 2 +- assets/icons/menu_alt.svg | 6 +- assets/icons/minimize.svg | 7 ++- assets/icons/notepad.svg | 1 + assets/icons/option.svg | 3 +- assets/icons/panel_left.svg | 1 - assets/icons/panel_right.svg | 1 - assets/icons/pencil.svg | 5 +- assets/icons/person.svg | 5 +- assets/icons/person_circle.svg | 1 - assets/icons/phone_incoming.svg | 1 - assets/icons/pocket_knife.svg | 1 - assets/icons/power.svg | 2 +- assets/icons/public.svg | 4 +- assets/icons/pull_request.svg | 2 +- assets/icons/quote.svg | 2 +- assets/icons/reader.svg | 5 ++ assets/icons/refresh_title.svg | 6 +- assets/icons/regex.svg | 6 +- assets/icons/repl_neutral.svg | 15 ++--- assets/icons/repl_off.svg | 29 ++++----- assets/icons/repl_pause.svg | 21 +++---- assets/icons/repl_play.svg | 19 ++---- assets/icons/rerun.svg | 8 +-- assets/icons/return.svg | 5 +- assets/icons/rotate_ccw.svg | 2 +- assets/icons/rotate_cw.svg | 5 +- assets/icons/route.svg | 1 - assets/icons/save.svg | 1 - assets/icons/scissors.svg | 2 +- assets/icons/scroll_text.svg | 1 - assets/icons/search_selection.svg | 1 - assets/icons/select_all.svg | 6 +- assets/icons/send.svg | 5 +- assets/icons/server.svg | 20 ++----- assets/icons/settings.svg | 2 +- assets/icons/settings_alt.svg | 6 -- assets/icons/shift.svg | 2 +- assets/icons/slash.svg | 4 +- assets/icons/slash_square.svg | 1 - assets/icons/sliders_alt.svg | 6 -- assets/icons/sliders_vertical.svg | 11 ---- assets/icons/snip.svg | 1 - assets/icons/space.svg | 4 +- assets/icons/sparkle.svg | 2 +- assets/icons/sparkle_alt.svg | 3 - assets/icons/sparkle_filled.svg | 3 - assets/icons/speaker_loud.svg | 8 --- assets/icons/split.svg | 8 +-- assets/icons/split_alt.svg | 2 +- assets/icons/square_dot.svg | 5 +- assets/icons/square_minus.svg | 5 +- assets/icons/square_plus.svg | 6 +- assets/icons/star_filled.svg | 2 +- assets/icons/stop.svg | 4 +- assets/icons/stop_filled.svg | 3 - assets/icons/supermaven.svg | 14 ++--- assets/icons/supermaven_disabled.svg | 16 +---- assets/icons/supermaven_error.svg | 14 ++--- assets/icons/supermaven_init.svg | 14 ++--- assets/icons/swatch_book.svg | 2 +- assets/icons/tab.svg | 6 +- assets/icons/terminal_alt.svg | 6 +- assets/icons/text_snippet.svg | 2 +- assets/icons/thumbs_down.svg | 4 +- assets/icons/thumbs_up.svg | 4 +- assets/icons/todo_complete.svg | 5 +- assets/icons/tool_folder.svg | 2 +- assets/icons/tool_terminal.svg | 6 +- assets/icons/tool_think.svg | 2 +- assets/icons/triangle.svg | 4 +- assets/icons/triangle_right.svg | 4 +- assets/icons/undo.svg | 2 +- assets/icons/update.svg | 8 --- assets/icons/user_check.svg | 2 +- assets/icons/user_round_pen.svg | 2 +- assets/icons/visible.svg | 1 - assets/icons/wand.svg | 1 - assets/icons/warning.svg | 2 +- assets/icons/whole_word.svg | 6 +- assets/icons/x.svg | 3 - assets/icons/x_circle.svg | 5 +- assets/icons/zed_assistant_filled.svg | 5 -- assets/icons/zed_burn_mode.svg | 4 +- assets/icons/zed_burn_mode_on.svg | 14 +---- assets/icons/zed_x_copilot.svg | 14 ----- crates/agent/src/context.rs | 6 +- crates/agent_ui/src/acp/thread_view.rs | 15 ++--- crates/agent_ui/src/active_thread.rs | 31 ++++++---- crates/agent_ui/src/agent_configuration.rs | 2 +- .../configure_context_server_modal.rs | 4 +- .../manage_profiles_modal.rs | 4 +- crates/agent_ui/src/context_picker.rs | 4 +- .../src/context_picker/completion_provider.rs | 6 +- crates/agent_ui/src/inline_prompt_editor.rs | 2 +- crates/agent_ui/src/message_editor.rs | 6 +- crates/agent_ui/src/slash_command_picker.rs | 2 +- crates/agent_ui/src/text_thread_editor.rs | 4 +- crates/agent_ui/src/ui/onboarding_modal.rs | 2 +- .../agent_ui/src/ui/preview/usage_callouts.rs | 2 +- crates/ai_onboarding/src/ai_onboarding.rs | 2 +- .../src/fetch_command.rs | 4 +- crates/assistant_tools/src/edit_file_tool.rs | 2 +- crates/assistant_tools/src/find_path_tool.rs | 2 +- crates/assistant_tools/src/web_search_tool.rs | 4 +- crates/collab_ui/src/collab_panel.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 6 +- crates/debugger_ui/src/onboarding_modal.rs | 2 +- .../src/session/running/breakpoint_list.rs | 4 +- .../src/session/running/console.rs | 2 +- .../src/session/running/stack_frame_list.rs | 2 +- crates/diagnostics/src/toolbar_controls.rs | 4 +- crates/editor/src/items.rs | 2 +- .../src/components/feature_upsell.rs | 2 +- crates/git_ui/src/branch_picker.rs | 2 +- crates/git_ui/src/commit_modal.rs | 2 +- crates/git_ui/src/git_panel.rs | 12 ++-- crates/git_ui/src/git_ui.rs | 6 +- crates/git_ui/src/onboarding.rs | 2 +- crates/icons/README.md | 29 +++++++++ crates/icons/src/icons.rs | 60 +++---------------- crates/language_models/src/provider/cloud.rs | 2 +- .../language_models/src/provider/lmstudio.rs | 8 +-- crates/language_models/src/provider/ollama.rs | 8 +-- .../language_models/src/provider/open_ai.rs | 2 +- .../src/ui/instruction_list_item.rs | 2 +- crates/notifications/src/status_toast.rs | 2 +- crates/onboarding/src/ai_setup_page.rs | 2 +- crates/onboarding/src/onboarding.rs | 2 +- crates/recent_projects/src/ssh_connections.rs | 2 +- crates/repl/src/components/kernel_options.rs | 4 +- crates/repl/src/notebook/cell.rs | 2 +- crates/repl/src/notebook/notebook_ui.rs | 2 +- crates/repl/src/outputs.rs | 2 +- crates/search/src/buffer_search.rs | 2 +- crates/search/src/project_search.rs | 2 +- crates/settings_ui/src/keybindings.rs | 2 +- .../src/ui_components/keystroke_input.rs | 4 +- crates/snippets_ui/src/snippets_ui.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 2 +- .../theme_selector/src/icon_theme_selector.rs | 2 +- crates/theme_selector/src/theme_selector.rs | 2 +- crates/title_bar/src/collab.rs | 2 +- crates/title_bar/src/title_bar.rs | 2 +- crates/ui/src/components/banner.rs | 4 +- crates/ui/src/components/callout.rs | 2 +- crates/ui/src/components/context_menu.rs | 2 +- crates/ui/src/components/indicator.rs | 2 +- crates/ui/src/components/keybinding.rs | 2 +- .../ui/src/components/stories/icon_button.rs | 2 +- crates/welcome/src/multibuffer_hint.rs | 2 +- crates/zed/src/zed/component_preview.rs | 2 +- .../zed/src/zed/quick_action_bar/repl_menu.rs | 2 +- crates/zeta/src/onboarding_modal.rs | 2 +- 284 files changed, 535 insertions(+), 791 deletions(-) delete mode 100644 assets/icons/arrow_down_from_line.svg delete mode 100644 assets/icons/arrow_up_alt.svg delete mode 100644 assets/icons/arrow_up_from_line.svg delete mode 100644 assets/icons/arrow_up_right_alt.svg delete mode 100644 assets/icons/bug_off.svg delete mode 100644 assets/icons/caret_down.svg delete mode 100644 assets/icons/caret_up.svg delete mode 100644 assets/icons/chevron_down_small.svg delete mode 100644 assets/icons/circle_off.svg delete mode 100644 assets/icons/cloud.svg delete mode 100644 assets/icons/context.svg delete mode 100644 assets/icons/debug_restart.svg delete mode 100644 assets/icons/debug_stop.svg delete mode 100644 assets/icons/delete.svg delete mode 100644 assets/icons/document_text.svg delete mode 100644 assets/icons/equal.svg delete mode 100644 assets/icons/external_link.svg delete mode 100644 assets/icons/file_create.svg create mode 100644 assets/icons/file_markdown.svg delete mode 100644 assets/icons/file_search.svg delete mode 100644 assets/icons/file_text.svg create mode 100644 assets/icons/file_text_filled.svg create mode 100644 assets/icons/file_text_outlined.svg create mode 100644 assets/icons/folder_search.svg delete mode 100644 assets/icons/folder_x.svg delete mode 100644 assets/icons/function.svg create mode 100644 assets/icons/git_branch_alt.svg delete mode 100644 assets/icons/git_branch_small.svg delete mode 100644 assets/icons/globe.svg delete mode 100644 assets/icons/hammer.svg delete mode 100644 assets/icons/inlay_hint.svg delete mode 100644 assets/icons/layout.svg delete mode 100644 assets/icons/light_bulb.svg delete mode 100644 assets/icons/link.svg delete mode 100644 assets/icons/logo_96.svg delete mode 100644 assets/icons/lsp_debug.svg delete mode 100644 assets/icons/lsp_restart.svg delete mode 100644 assets/icons/lsp_stop.svg delete mode 100644 assets/icons/mail_open.svg create mode 100644 assets/icons/notepad.svg delete mode 100644 assets/icons/panel_left.svg delete mode 100644 assets/icons/panel_right.svg delete mode 100644 assets/icons/person_circle.svg delete mode 100644 assets/icons/phone_incoming.svg delete mode 100644 assets/icons/pocket_knife.svg create mode 100644 assets/icons/reader.svg delete mode 100644 assets/icons/route.svg delete mode 100644 assets/icons/save.svg delete mode 100644 assets/icons/scroll_text.svg delete mode 100644 assets/icons/search_selection.svg delete mode 100644 assets/icons/settings_alt.svg delete mode 100644 assets/icons/slash_square.svg delete mode 100644 assets/icons/sliders_alt.svg delete mode 100644 assets/icons/sliders_vertical.svg delete mode 100644 assets/icons/snip.svg delete mode 100644 assets/icons/sparkle_alt.svg delete mode 100644 assets/icons/sparkle_filled.svg delete mode 100644 assets/icons/speaker_loud.svg delete mode 100644 assets/icons/stop_filled.svg delete mode 100644 assets/icons/update.svg delete mode 100644 assets/icons/visible.svg delete mode 100644 assets/icons/wand.svg delete mode 100644 assets/icons/x.svg delete mode 100644 assets/icons/zed_assistant_filled.svg delete mode 100644 assets/icons/zed_x_copilot.svg create mode 100644 crates/icons/README.md diff --git a/assets/icons/arrow_circle.svg b/assets/icons/arrow_circle.svg index 90e352bdea..790428702e 100644 --- a/assets/icons/arrow_circle.svg +++ b/assets/icons/arrow_circle.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/arrow_down.svg b/assets/icons/arrow_down.svg index 7d78497e6d..c71e5437f8 100644 --- a/assets/icons/arrow_down.svg +++ b/assets/icons/arrow_down.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_down10.svg b/assets/icons/arrow_down10.svg index 97ce967a8b..8eed82276c 100644 --- a/assets/icons/arrow_down10.svg +++ b/assets/icons/arrow_down10.svg @@ -1 +1 @@ - + diff --git a/assets/icons/arrow_down_from_line.svg b/assets/icons/arrow_down_from_line.svg deleted file mode 100644 index 89316973a0..0000000000 --- a/assets/icons/arrow_down_from_line.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/arrow_down_right.svg b/assets/icons/arrow_down_right.svg index b9c10263d0..73f72a2c38 100644 --- a/assets/icons/arrow_down_right.svg +++ b/assets/icons/arrow_down_right.svg @@ -1 +1,4 @@ - + + + + diff --git a/assets/icons/arrow_left.svg b/assets/icons/arrow_left.svg index 57ee750490..ca441497a0 100644 --- a/assets/icons/arrow_left.svg +++ b/assets/icons/arrow_left.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_right.svg b/assets/icons/arrow_right.svg index 7a5b1174eb..ae14888563 100644 --- a/assets/icons/arrow_right.svg +++ b/assets/icons/arrow_right.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_right_left.svg b/assets/icons/arrow_right_left.svg index 30331960c9..cfeee0cc24 100644 --- a/assets/icons/arrow_right_left.svg +++ b/assets/icons/arrow_right_left.svg @@ -1 +1,6 @@ - + + + + + + diff --git a/assets/icons/arrow_up.svg b/assets/icons/arrow_up.svg index 81dfee8042..b98c710374 100644 --- a/assets/icons/arrow_up.svg +++ b/assets/icons/arrow_up.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_up_alt.svg b/assets/icons/arrow_up_alt.svg deleted file mode 100644 index c8cf286a8c..0000000000 --- a/assets/icons/arrow_up_alt.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/arrow_up_from_line.svg b/assets/icons/arrow_up_from_line.svg deleted file mode 100644 index 50a075e42b..0000000000 --- a/assets/icons/arrow_up_from_line.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/arrow_up_right.svg b/assets/icons/arrow_up_right.svg index 9fbafba4ec..fb065bc9ce 100644 --- a/assets/icons/arrow_up_right.svg +++ b/assets/icons/arrow_up_right.svg @@ -1 +1,4 @@ - + + + + diff --git a/assets/icons/arrow_up_right_alt.svg b/assets/icons/arrow_up_right_alt.svg deleted file mode 100644 index 4e923c6867..0000000000 --- a/assets/icons/arrow_up_right_alt.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/backspace.svg b/assets/icons/backspace.svg index f7f1cf107a..679ef1ade1 100644 --- a/assets/icons/backspace.svg +++ b/assets/icons/backspace.svg @@ -1,3 +1,5 @@ - - + + + + diff --git a/assets/icons/binary.svg b/assets/icons/binary.svg index 8f5e456d16..bbc375617f 100644 --- a/assets/icons/binary.svg +++ b/assets/icons/binary.svg @@ -1 +1 @@ - + diff --git a/assets/icons/blocks.svg b/assets/icons/blocks.svg index 588d49abbc..128ca84ef1 100644 --- a/assets/icons/blocks.svg +++ b/assets/icons/blocks.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/icons/book.svg b/assets/icons/book.svg index d30f81f32e..8b0f89e82d 100644 --- a/assets/icons/book.svg +++ b/assets/icons/book.svg @@ -1 +1 @@ - + diff --git a/assets/icons/book_copy.svg b/assets/icons/book_copy.svg index b055d47b5f..f509beffe6 100644 --- a/assets/icons/book_copy.svg +++ b/assets/icons/book_copy.svg @@ -1 +1 @@ - + diff --git a/assets/icons/bug_off.svg b/assets/icons/bug_off.svg deleted file mode 100644 index 23f4ef06df..0000000000 --- a/assets/icons/bug_off.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/caret_down.svg b/assets/icons/caret_down.svg deleted file mode 100644 index ff8b8c3b88..0000000000 --- a/assets/icons/caret_down.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/caret_up.svg b/assets/icons/caret_up.svg deleted file mode 100644 index 53026b83d8..0000000000 --- a/assets/icons/caret_up.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/case_sensitive.svg b/assets/icons/case_sensitive.svg index 8c943e7509..015e241416 100644 --- a/assets/icons/case_sensitive.svg +++ b/assets/icons/case_sensitive.svg @@ -1,8 +1 @@ - - - - - - - - + diff --git a/assets/icons/check.svg b/assets/icons/check.svg index 39352682c9..4563505aaa 100644 --- a/assets/icons/check.svg +++ b/assets/icons/check.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/check_circle.svg b/assets/icons/check_circle.svg index b48fe34631..e6ec5d11ef 100644 --- a/assets/icons/check_circle.svg +++ b/assets/icons/check_circle.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/assets/icons/check_double.svg b/assets/icons/check_double.svg index 5c17d95a6b..b52bef81a4 100644 --- a/assets/icons/check_double.svg +++ b/assets/icons/check_double.svg @@ -1 +1,4 @@ - + + + + diff --git a/assets/icons/chevron_down.svg b/assets/icons/chevron_down.svg index b971555cfa..7894aae764 100644 --- a/assets/icons/chevron_down.svg +++ b/assets/icons/chevron_down.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/chevron_down_small.svg b/assets/icons/chevron_down_small.svg deleted file mode 100644 index 8f8a99d4b9..0000000000 --- a/assets/icons/chevron_down_small.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_left.svg b/assets/icons/chevron_left.svg index 8e61beed5d..4be4c95dca 100644 --- a/assets/icons/chevron_left.svg +++ b/assets/icons/chevron_left.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/chevron_right.svg b/assets/icons/chevron_right.svg index fcd9d83fc2..c8ff847177 100644 --- a/assets/icons/chevron_right.svg +++ b/assets/icons/chevron_right.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/chevron_up.svg b/assets/icons/chevron_up.svg index 171cdd61c0..8e575e2e8d 100644 --- a/assets/icons/chevron_up.svg +++ b/assets/icons/chevron_up.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/chevron_up_down.svg b/assets/icons/chevron_up_down.svg index a7414ec8a0..c7af01d4a3 100644 --- a/assets/icons/chevron_up_down.svg +++ b/assets/icons/chevron_up_down.svg @@ -1 +1,4 @@ - + + + + diff --git a/assets/icons/circle.svg b/assets/icons/circle.svg index 67306cb12a..1d80edac09 100644 --- a/assets/icons/circle.svg +++ b/assets/icons/circle.svg @@ -1 +1,3 @@ - + + + diff --git a/assets/icons/circle_check.svg b/assets/icons/circle_check.svg index adfc8cecca..8950aa7a0e 100644 --- a/assets/icons/circle_check.svg +++ b/assets/icons/circle_check.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/circle_help.svg b/assets/icons/circle_help.svg index 1a004bfff8..4e2890d3e1 100644 --- a/assets/icons/circle_help.svg +++ b/assets/icons/circle_help.svg @@ -1 +1,5 @@ - + + + + + diff --git a/assets/icons/circle_off.svg b/assets/icons/circle_off.svg deleted file mode 100644 index be1bf29225..0000000000 --- a/assets/icons/circle_off.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/close.svg b/assets/icons/close.svg index 31c5aa31a6..ad487e0a4f 100644 --- a/assets/icons/close.svg +++ b/assets/icons/close.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/cloud.svg b/assets/icons/cloud.svg deleted file mode 100644 index 73a9618067..0000000000 --- a/assets/icons/cloud.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/cloud_download.svg b/assets/icons/cloud_download.svg index bc7a8376d1..0efcbe10f1 100644 --- a/assets/icons/cloud_download.svg +++ b/assets/icons/cloud_download.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/icons/code.svg b/assets/icons/code.svg index 757c5a1cb6..6a1795b59c 100644 --- a/assets/icons/code.svg +++ b/assets/icons/code.svg @@ -1 +1 @@ - + diff --git a/assets/icons/cog.svg b/assets/icons/cog.svg index 03c0a290b7..4f3ada11a6 100644 --- a/assets/icons/cog.svg +++ b/assets/icons/cog.svg @@ -1 +1 @@ - + diff --git a/assets/icons/command.svg b/assets/icons/command.svg index d38389aea4..6602af8e1f 100644 --- a/assets/icons/command.svg +++ b/assets/icons/command.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/context.svg b/assets/icons/context.svg deleted file mode 100644 index 837b3aadd9..0000000000 --- a/assets/icons/context.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/control.svg b/assets/icons/control.svg index 94189dc07d..e831968df6 100644 --- a/assets/icons/control.svg +++ b/assets/icons/control.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/copilot.svg b/assets/icons/copilot.svg index 06dbf178ae..57c0a5f91a 100644 --- a/assets/icons/copilot.svg +++ b/assets/icons/copilot.svg @@ -1,9 +1,9 @@ - - - - - - - - + + + + + + + + diff --git a/assets/icons/copilot_disabled.svg b/assets/icons/copilot_disabled.svg index eba36a2b69..90afa84966 100644 --- a/assets/icons/copilot_disabled.svg +++ b/assets/icons/copilot_disabled.svg @@ -1,9 +1,9 @@ - - - - + + + + - + diff --git a/assets/icons/copilot_error.svg b/assets/icons/copilot_error.svg index 6069c554f1..77744e7529 100644 --- a/assets/icons/copilot_error.svg +++ b/assets/icons/copilot_error.svg @@ -1,7 +1,7 @@ - - + + - + diff --git a/assets/icons/copilot_init.svg b/assets/icons/copilot_init.svg index 6cbf63fb49..754d159584 100644 --- a/assets/icons/copilot_init.svg +++ b/assets/icons/copilot_init.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg index 7a3cdcf6da..dfd8d9dbb9 100644 --- a/assets/icons/copy.svg +++ b/assets/icons/copy.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/countdown_timer.svg b/assets/icons/countdown_timer.svg index b9b7479228..5e69f1bfb4 100644 --- a/assets/icons/countdown_timer.svg +++ b/assets/icons/countdown_timer.svg @@ -1 +1 @@ - + diff --git a/assets/icons/crosshair.svg b/assets/icons/crosshair.svg index 006c6362aa..1492bf9245 100644 --- a/assets/icons/crosshair.svg +++ b/assets/icons/crosshair.svg @@ -1,7 +1,7 @@ - - - - - - + + + + + + diff --git a/assets/icons/dash.svg b/assets/icons/dash.svg index efff9eab5e..9270f80781 100644 --- a/assets/icons/dash.svg +++ b/assets/icons/dash.svg @@ -1 +1 @@ - + diff --git a/assets/icons/database_zap.svg b/assets/icons/database_zap.svg index 06241b35f4..160ffa5041 100644 --- a/assets/icons/database_zap.svg +++ b/assets/icons/database_zap.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug.svg b/assets/icons/debug.svg index ff51e42b1a..900caf4b98 100644 --- a/assets/icons/debug.svg +++ b/assets/icons/debug.svg @@ -1,12 +1,12 @@ - - - - - - - - - - + + + + + + + + + + diff --git a/assets/icons/debug_breakpoint.svg b/assets/icons/debug_breakpoint.svg index f6a7b35658..9cab42eecd 100644 --- a/assets/icons/debug_breakpoint.svg +++ b/assets/icons/debug_breakpoint.svg @@ -1 +1,3 @@ - + + + diff --git a/assets/icons/debug_continue.svg b/assets/icons/debug_continue.svg index e2a99c38d0..f663a5a041 100644 --- a/assets/icons/debug_continue.svg +++ b/assets/icons/debug_continue.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug_detach.svg b/assets/icons/debug_detach.svg index 0eb2537152..a34a0e8171 100644 --- a/assets/icons/debug_detach.svg +++ b/assets/icons/debug_detach.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug_disabled_breakpoint.svg b/assets/icons/debug_disabled_breakpoint.svg index a7260ec04b..8b80623b02 100644 --- a/assets/icons/debug_disabled_breakpoint.svg +++ b/assets/icons/debug_disabled_breakpoint.svg @@ -1 +1,3 @@ - + + + diff --git a/assets/icons/debug_disabled_log_breakpoint.svg b/assets/icons/debug_disabled_log_breakpoint.svg index d0bb2c8e2b..a028ead3a0 100644 --- a/assets/icons/debug_disabled_log_breakpoint.svg +++ b/assets/icons/debug_disabled_log_breakpoint.svg @@ -1 +1,3 @@ - + + + diff --git a/assets/icons/debug_ignore_breakpoints.svg b/assets/icons/debug_ignore_breakpoints.svg index ba7074e083..a0bbabfb26 100644 --- a/assets/icons/debug_ignore_breakpoints.svg +++ b/assets/icons/debug_ignore_breakpoints.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug_log_breakpoint.svg b/assets/icons/debug_log_breakpoint.svg index a878ce3e04..7c652db1e9 100644 --- a/assets/icons/debug_log_breakpoint.svg +++ b/assets/icons/debug_log_breakpoint.svg @@ -1 +1,3 @@ - + + + diff --git a/assets/icons/debug_pause.svg b/assets/icons/debug_pause.svg index bea531bc5a..65e1949581 100644 --- a/assets/icons/debug_pause.svg +++ b/assets/icons/debug_pause.svg @@ -1 +1,4 @@ - + + + + diff --git a/assets/icons/debug_restart.svg b/assets/icons/debug_restart.svg deleted file mode 100644 index 4eff13b94b..0000000000 --- a/assets/icons/debug_restart.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/debug_step_back.svg b/assets/icons/debug_step_back.svg index bc7c9b8444..d1112d6b8e 100644 --- a/assets/icons/debug_step_back.svg +++ b/assets/icons/debug_step_back.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug_step_into.svg b/assets/icons/debug_step_into.svg index 69e5cff3f1..02bdd63cb4 100644 --- a/assets/icons/debug_step_into.svg +++ b/assets/icons/debug_step_into.svg @@ -1,5 +1 @@ - - - - - + diff --git a/assets/icons/debug_step_out.svg b/assets/icons/debug_step_out.svg index 680e13e65e..48190b704b 100644 --- a/assets/icons/debug_step_out.svg +++ b/assets/icons/debug_step_out.svg @@ -1,5 +1 @@ - - - - - + diff --git a/assets/icons/debug_step_over.svg b/assets/icons/debug_step_over.svg index 005b901da3..54afac001f 100644 --- a/assets/icons/debug_step_over.svg +++ b/assets/icons/debug_step_over.svg @@ -1,5 +1 @@ - - - - - + diff --git a/assets/icons/debug_stop.svg b/assets/icons/debug_stop.svg deleted file mode 100644 index fef651c586..0000000000 --- a/assets/icons/debug_stop.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/delete.svg b/assets/icons/delete.svg deleted file mode 100644 index a7edbb6158..0000000000 --- a/assets/icons/delete.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/diff.svg b/assets/icons/diff.svg index ca43c379da..61aa617f5b 100644 --- a/assets/icons/diff.svg +++ b/assets/icons/diff.svg @@ -1 +1 @@ - + diff --git a/assets/icons/disconnected.svg b/assets/icons/disconnected.svg index 37d0ee904c..f3069798d0 100644 --- a/assets/icons/disconnected.svg +++ b/assets/icons/disconnected.svg @@ -1,3 +1 @@ - - - + diff --git a/assets/icons/document_text.svg b/assets/icons/document_text.svg deleted file mode 100644 index 78c08d92f9..0000000000 --- a/assets/icons/document_text.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/download.svg b/assets/icons/download.svg index 2ffa65e8ac..6ddcb1e100 100644 --- a/assets/icons/download.svg +++ b/assets/icons/download.svg @@ -1 +1 @@ - + diff --git a/assets/icons/ellipsis.svg b/assets/icons/ellipsis.svg index 1858c65520..22b5a8fd46 100644 --- a/assets/icons/ellipsis.svg +++ b/assets/icons/ellipsis.svg @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/assets/icons/ellipsis_vertical.svg b/assets/icons/ellipsis_vertical.svg index 077dbe8778..c38437667e 100644 --- a/assets/icons/ellipsis_vertical.svg +++ b/assets/icons/ellipsis_vertical.svg @@ -1 +1,5 @@ - + + + + + diff --git a/assets/icons/equal.svg b/assets/icons/equal.svg deleted file mode 100644 index 9b3a151a12..0000000000 --- a/assets/icons/equal.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/eraser.svg b/assets/icons/eraser.svg index edb893a8c6..601f2b9b90 100644 --- a/assets/icons/eraser.svg +++ b/assets/icons/eraser.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/escape.svg b/assets/icons/escape.svg index 00c772a2ad..a87f03d2fa 100644 --- a/assets/icons/escape.svg +++ b/assets/icons/escape.svg @@ -1 +1 @@ - + diff --git a/assets/icons/expand_down.svg b/assets/icons/expand_down.svg index a17b9e285c..07390aad18 100644 --- a/assets/icons/expand_down.svg +++ b/assets/icons/expand_down.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/assets/icons/expand_up.svg b/assets/icons/expand_up.svg index 30f9af92e3..73c1358b99 100644 --- a/assets/icons/expand_up.svg +++ b/assets/icons/expand_up.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/assets/icons/expand_vertical.svg b/assets/icons/expand_vertical.svg index e278911478..e2a6dd227e 100644 --- a/assets/icons/expand_vertical.svg +++ b/assets/icons/expand_vertical.svg @@ -1 +1 @@ - + diff --git a/assets/icons/external_link.svg b/assets/icons/external_link.svg deleted file mode 100644 index 561f012452..0000000000 --- a/assets/icons/external_link.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/eye.svg b/assets/icons/eye.svg index 21e3d3ba63..7f10f73801 100644 --- a/assets/icons/eye.svg +++ b/assets/icons/eye.svg @@ -1 +1,4 @@ - + + + + diff --git a/assets/icons/file.svg b/assets/icons/file.svg index 5b1b892756..85f3f543a5 100644 --- a/assets/icons/file.svg +++ b/assets/icons/file.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/file_code.svg b/assets/icons/file_code.svg index 0a15da7705..b0e632b67f 100644 --- a/assets/icons/file_code.svg +++ b/assets/icons/file_code.svg @@ -1 +1 @@ - + diff --git a/assets/icons/file_create.svg b/assets/icons/file_create.svg deleted file mode 100644 index bd7f88a7ec..0000000000 --- a/assets/icons/file_create.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/file_diff.svg b/assets/icons/file_diff.svg index ff20f16c60..d6cb4440ea 100644 --- a/assets/icons/file_diff.svg +++ b/assets/icons/file_diff.svg @@ -1 +1 @@ - + diff --git a/assets/icons/file_markdown.svg b/assets/icons/file_markdown.svg new file mode 100644 index 0000000000..e26d7a532d --- /dev/null +++ b/assets/icons/file_markdown.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/file_search.svg b/assets/icons/file_search.svg deleted file mode 100644 index ddf5b14770..0000000000 --- a/assets/icons/file_search.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/file_text.svg b/assets/icons/file_text.svg deleted file mode 100644 index a9b8f971e0..0000000000 --- a/assets/icons/file_text.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/file_text_filled.svg b/assets/icons/file_text_filled.svg new file mode 100644 index 0000000000..15c81cca62 --- /dev/null +++ b/assets/icons/file_text_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/file_text_outlined.svg b/assets/icons/file_text_outlined.svg new file mode 100644 index 0000000000..bb9b85d62f --- /dev/null +++ b/assets/icons/file_text_outlined.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/file_tree.svg b/assets/icons/file_tree.svg index a140cd70b1..74acb1fc25 100644 --- a/assets/icons/file_tree.svg +++ b/assets/icons/file_tree.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/flame.svg b/assets/icons/flame.svg index 075e027a5c..3215f0d5ae 100644 --- a/assets/icons/flame.svg +++ b/assets/icons/flame.svg @@ -1 +1 @@ - + diff --git a/assets/icons/folder.svg b/assets/icons/folder.svg index 1a40805a70..0d76b7e3f8 100644 --- a/assets/icons/folder.svg +++ b/assets/icons/folder.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/folder_search.svg b/assets/icons/folder_search.svg new file mode 100644 index 0000000000..15b0705dd6 --- /dev/null +++ b/assets/icons/folder_search.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/folder_x.svg b/assets/icons/folder_x.svg deleted file mode 100644 index b0f06f68eb..0000000000 --- a/assets/icons/folder_x.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/font.svg b/assets/icons/font.svg index 861ab1a415..1cc569ecb7 100644 --- a/assets/icons/font.svg +++ b/assets/icons/font.svg @@ -1 +1 @@ - + diff --git a/assets/icons/font_size.svg b/assets/icons/font_size.svg index cfba2deb6c..fd983cb5d3 100644 --- a/assets/icons/font_size.svg +++ b/assets/icons/font_size.svg @@ -1 +1 @@ - + diff --git a/assets/icons/font_weight.svg b/assets/icons/font_weight.svg index 3ebbfa77bc..73b9852e2f 100644 --- a/assets/icons/font_weight.svg +++ b/assets/icons/font_weight.svg @@ -1 +1 @@ - + diff --git a/assets/icons/forward_arrow.svg b/assets/icons/forward_arrow.svg index 0a7b71993f..503b0b309b 100644 --- a/assets/icons/forward_arrow.svg +++ b/assets/icons/forward_arrow.svg @@ -1 +1,4 @@ - + + + + diff --git a/assets/icons/function.svg b/assets/icons/function.svg deleted file mode 100644 index 5d0b9d58ef..0000000000 --- a/assets/icons/function.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/generic_maximize.svg b/assets/icons/generic_maximize.svg index e44abd8f06..f1d7da44ef 100644 --- a/assets/icons/generic_maximize.svg +++ b/assets/icons/generic_maximize.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/generic_restore.svg b/assets/icons/generic_restore.svg index 3bf581f2cd..d8a3d72bcd 100644 --- a/assets/icons/generic_restore.svg +++ b/assets/icons/generic_restore.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/git_branch.svg b/assets/icons/git_branch.svg index db6190a9c8..811bc74762 100644 --- a/assets/icons/git_branch.svg +++ b/assets/icons/git_branch.svg @@ -1 +1 @@ - + diff --git a/assets/icons/git_branch_alt.svg b/assets/icons/git_branch_alt.svg new file mode 100644 index 0000000000..d18b072512 --- /dev/null +++ b/assets/icons/git_branch_alt.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/git_branch_small.svg b/assets/icons/git_branch_small.svg deleted file mode 100644 index 22832d6fed..0000000000 --- a/assets/icons/git_branch_small.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/github.svg b/assets/icons/github.svg index 28148b9894..fe9186872b 100644 --- a/assets/icons/github.svg +++ b/assets/icons/github.svg @@ -1 +1 @@ - + diff --git a/assets/icons/globe.svg b/assets/icons/globe.svg deleted file mode 100644 index 545b83aa71..0000000000 --- a/assets/icons/globe.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/assets/icons/hammer.svg b/assets/icons/hammer.svg deleted file mode 100644 index ccc0d30e3d..0000000000 --- a/assets/icons/hammer.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/hash.svg b/assets/icons/hash.svg index f685245ed3..9e4dd7c068 100644 --- a/assets/icons/hash.svg +++ b/assets/icons/hash.svg @@ -1,6 +1 @@ - - - - - - + diff --git a/assets/icons/image.svg b/assets/icons/image.svg index 4b17300f47..0a26c35182 100644 --- a/assets/icons/image.svg +++ b/assets/icons/image.svg @@ -1 +1 @@ - + diff --git a/assets/icons/inlay_hint.svg b/assets/icons/inlay_hint.svg deleted file mode 100644 index c8e6bb2d36..0000000000 --- a/assets/icons/inlay_hint.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/keyboard.svg b/assets/icons/keyboard.svg index 8bdc054a65..de9afd9561 100644 --- a/assets/icons/keyboard.svg +++ b/assets/icons/keyboard.svg @@ -1 +1 @@ - + diff --git a/assets/icons/layout.svg b/assets/icons/layout.svg deleted file mode 100644 index 79464013b1..0000000000 --- a/assets/icons/layout.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/library.svg b/assets/icons/library.svg index 95f8c710c8..ed59e1818b 100644 --- a/assets/icons/library.svg +++ b/assets/icons/library.svg @@ -1 +1,6 @@ - + + + + + + diff --git a/assets/icons/light_bulb.svg b/assets/icons/light_bulb.svg deleted file mode 100644 index 61a8f04211..0000000000 --- a/assets/icons/light_bulb.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/line_height.svg b/assets/icons/line_height.svg index 904cfad8a8..7afa70f767 100644 --- a/assets/icons/line_height.svg +++ b/assets/icons/line_height.svg @@ -1,6 +1 @@ - - - - - - + diff --git a/assets/icons/link.svg b/assets/icons/link.svg deleted file mode 100644 index 4925bd8e00..0000000000 --- a/assets/icons/link.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/list_collapse.svg b/assets/icons/list_collapse.svg index a0e0ed604d..938799b151 100644 --- a/assets/icons/list_collapse.svg +++ b/assets/icons/list_collapse.svg @@ -1 +1 @@ - + diff --git a/assets/icons/list_todo.svg b/assets/icons/list_todo.svg index 1f50219418..019af95734 100644 --- a/assets/icons/list_todo.svg +++ b/assets/icons/list_todo.svg @@ -1 +1 @@ - + diff --git a/assets/icons/list_x.svg b/assets/icons/list_x.svg index 683f38ab5d..206faf2ce4 100644 --- a/assets/icons/list_x.svg +++ b/assets/icons/list_x.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/load_circle.svg b/assets/icons/load_circle.svg index c4de36b1ff..825aa335b0 100644 --- a/assets/icons/load_circle.svg +++ b/assets/icons/load_circle.svg @@ -1 +1 @@ - + diff --git a/assets/icons/location_edit.svg b/assets/icons/location_edit.svg index de82e8db4e..02cd6f3389 100644 --- a/assets/icons/location_edit.svg +++ b/assets/icons/location_edit.svg @@ -1 +1 @@ - + diff --git a/assets/icons/logo_96.svg b/assets/icons/logo_96.svg deleted file mode 100644 index dc98bb8bc2..0000000000 --- a/assets/icons/logo_96.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/lsp_debug.svg b/assets/icons/lsp_debug.svg deleted file mode 100644 index aa49fcb6a2..0000000000 --- a/assets/icons/lsp_debug.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/assets/icons/lsp_restart.svg b/assets/icons/lsp_restart.svg deleted file mode 100644 index dfc68e7a9e..0000000000 --- a/assets/icons/lsp_restart.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/lsp_stop.svg b/assets/icons/lsp_stop.svg deleted file mode 100644 index c6311d2155..0000000000 --- a/assets/icons/lsp_stop.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/magnifying_glass.svg b/assets/icons/magnifying_glass.svg index 75c3e76c80..b7c22e64bd 100644 --- a/assets/icons/magnifying_glass.svg +++ b/assets/icons/magnifying_glass.svg @@ -1,3 +1,4 @@ + diff --git a/assets/icons/mail_open.svg b/assets/icons/mail_open.svg deleted file mode 100644 index b857037b86..0000000000 --- a/assets/icons/mail_open.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/maximize.svg b/assets/icons/maximize.svg index b3504b5701..c51b71aaf0 100644 --- a/assets/icons/maximize.svg +++ b/assets/icons/maximize.svg @@ -1 +1,6 @@ - + + + + + + diff --git a/assets/icons/menu.svg b/assets/icons/menu.svg index 6598697ff8..0724fb2816 100644 --- a/assets/icons/menu.svg +++ b/assets/icons/menu.svg @@ -1 +1 @@ - + diff --git a/assets/icons/menu_alt.svg b/assets/icons/menu_alt.svg index ae3581ba01..b605e094e3 100644 --- a/assets/icons/menu_alt.svg +++ b/assets/icons/menu_alt.svg @@ -1,5 +1 @@ - - - - - + diff --git a/assets/icons/minimize.svg b/assets/icons/minimize.svg index 0451233cc9..97d4699687 100644 --- a/assets/icons/minimize.svg +++ b/assets/icons/minimize.svg @@ -1 +1,6 @@ - + + + + + + diff --git a/assets/icons/notepad.svg b/assets/icons/notepad.svg new file mode 100644 index 0000000000..48875eedee --- /dev/null +++ b/assets/icons/notepad.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/option.svg b/assets/icons/option.svg index 9d54a6f34b..676c10c93b 100644 --- a/assets/icons/option.svg +++ b/assets/icons/option.svg @@ -1,3 +1,4 @@ - + + diff --git a/assets/icons/panel_left.svg b/assets/icons/panel_left.svg deleted file mode 100644 index 2eed26673e..0000000000 --- a/assets/icons/panel_left.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/panel_right.svg b/assets/icons/panel_right.svg deleted file mode 100644 index d29a4a519e..0000000000 --- a/assets/icons/panel_right.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/pencil.svg b/assets/icons/pencil.svg index d90dcda10d..b913015c08 100644 --- a/assets/icons/pencil.svg +++ b/assets/icons/pencil.svg @@ -1,3 +1,4 @@ - - + + + diff --git a/assets/icons/person.svg b/assets/icons/person.svg index 93bee97a5f..c641678303 100644 --- a/assets/icons/person.svg +++ b/assets/icons/person.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/person_circle.svg b/assets/icons/person_circle.svg deleted file mode 100644 index 7e22682e0e..0000000000 --- a/assets/icons/person_circle.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/phone_incoming.svg b/assets/icons/phone_incoming.svg deleted file mode 100644 index 4577df47ad..0000000000 --- a/assets/icons/phone_incoming.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/pocket_knife.svg b/assets/icons/pocket_knife.svg deleted file mode 100644 index fb2d078e20..0000000000 --- a/assets/icons/pocket_knife.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/power.svg b/assets/icons/power.svg index 787d1a3519..23f6f48f30 100644 --- a/assets/icons/power.svg +++ b/assets/icons/power.svg @@ -1 +1 @@ - + diff --git a/assets/icons/public.svg b/assets/icons/public.svg index 38278cdaba..574ee1010d 100644 --- a/assets/icons/public.svg +++ b/assets/icons/public.svg @@ -1,3 +1 @@ - - - + diff --git a/assets/icons/pull_request.svg b/assets/icons/pull_request.svg index 150a532cc6..ccfaaacfdc 100644 --- a/assets/icons/pull_request.svg +++ b/assets/icons/pull_request.svg @@ -1 +1 @@ - + diff --git a/assets/icons/quote.svg b/assets/icons/quote.svg index b970db1430..5564a60f95 100644 --- a/assets/icons/quote.svg +++ b/assets/icons/quote.svg @@ -1 +1 @@ - + diff --git a/assets/icons/reader.svg b/assets/icons/reader.svg new file mode 100644 index 0000000000..2ccc37623d --- /dev/null +++ b/assets/icons/reader.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/refresh_title.svg b/assets/icons/refresh_title.svg index bd3657d48c..8a8fdb04f3 100644 --- a/assets/icons/refresh_title.svg +++ b/assets/icons/refresh_title.svg @@ -1,5 +1 @@ - - - - - + diff --git a/assets/icons/regex.svg b/assets/icons/regex.svg index 1b24398cc1..0432cd570f 100644 --- a/assets/icons/regex.svg +++ b/assets/icons/regex.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/assets/icons/repl_neutral.svg b/assets/icons/repl_neutral.svg index db647fe40b..d9c8b001df 100644 --- a/assets/icons/repl_neutral.svg +++ b/assets/icons/repl_neutral.svg @@ -1,13 +1,6 @@ - - - - - - - - - - - + + + + diff --git a/assets/icons/repl_off.svg b/assets/icons/repl_off.svg index 51ada0db46..ac249ad5ff 100644 --- a/assets/icons/repl_off.svg +++ b/assets/icons/repl_off.svg @@ -1,20 +1,11 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + diff --git a/assets/icons/repl_pause.svg b/assets/icons/repl_pause.svg index 2ac327df3b..5273ed60bb 100644 --- a/assets/icons/repl_pause.svg +++ b/assets/icons/repl_pause.svg @@ -1,15 +1,8 @@ - - - - - - - - - - - - - - + + + + + + + diff --git a/assets/icons/repl_play.svg b/assets/icons/repl_play.svg index d23b899112..76c292a382 100644 --- a/assets/icons/repl_play.svg +++ b/assets/icons/repl_play.svg @@ -1,14 +1,7 @@ - - - - - - - - - - - - - + + + + + + diff --git a/assets/icons/rerun.svg b/assets/icons/rerun.svg index 4d22f924f5..a5daa5de1d 100644 --- a/assets/icons/rerun.svg +++ b/assets/icons/rerun.svg @@ -1,7 +1 @@ - - - - - - - + diff --git a/assets/icons/return.svg b/assets/icons/return.svg index 16cfeeda2e..aed9242a95 100644 --- a/assets/icons/return.svg +++ b/assets/icons/return.svg @@ -1 +1,4 @@ - + + + + diff --git a/assets/icons/rotate_ccw.svg b/assets/icons/rotate_ccw.svg index 4eff13b94b..8f6bd6346a 100644 --- a/assets/icons/rotate_ccw.svg +++ b/assets/icons/rotate_ccw.svg @@ -1 +1 @@ - + diff --git a/assets/icons/rotate_cw.svg b/assets/icons/rotate_cw.svg index 2098de38c2..b082096ee4 100644 --- a/assets/icons/rotate_cw.svg +++ b/assets/icons/rotate_cw.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/route.svg b/assets/icons/route.svg deleted file mode 100644 index 7d2a5621ff..0000000000 --- a/assets/icons/route.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/save.svg b/assets/icons/save.svg deleted file mode 100644 index f83d035331..0000000000 --- a/assets/icons/save.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/scissors.svg b/assets/icons/scissors.svg index e7fb6005f4..89d246841e 100644 --- a/assets/icons/scissors.svg +++ b/assets/icons/scissors.svg @@ -1 +1 @@ - + diff --git a/assets/icons/scroll_text.svg b/assets/icons/scroll_text.svg deleted file mode 100644 index f066c8a84e..0000000000 --- a/assets/icons/scroll_text.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/search_selection.svg b/assets/icons/search_selection.svg deleted file mode 100644 index b970db1430..0000000000 --- a/assets/icons/search_selection.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/select_all.svg b/assets/icons/select_all.svg index 78c3ee6399..c15973c419 100644 --- a/assets/icons/select_all.svg +++ b/assets/icons/select_all.svg @@ -1,5 +1 @@ - - - - - + diff --git a/assets/icons/send.svg b/assets/icons/send.svg index 0d6ad36341..1403a43ff5 100644 --- a/assets/icons/send.svg +++ b/assets/icons/send.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/server.svg b/assets/icons/server.svg index a8b6ad92b3..bde19efd75 100644 --- a/assets/icons/server.svg +++ b/assets/icons/server.svg @@ -1,16 +1,6 @@ - - - - - + + + + + diff --git a/assets/icons/settings.svg b/assets/icons/settings.svg index a82cf03398..617b14b3cd 100644 --- a/assets/icons/settings.svg +++ b/assets/icons/settings.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/settings_alt.svg b/assets/icons/settings_alt.svg deleted file mode 100644 index a5fb4171d5..0000000000 --- a/assets/icons/settings_alt.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/shift.svg b/assets/icons/shift.svg index 0232114777..35dc2f144c 100644 --- a/assets/icons/shift.svg +++ b/assets/icons/shift.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/slash.svg b/assets/icons/slash.svg index 792c405bb0..e2313f0099 100644 --- a/assets/icons/slash.svg +++ b/assets/icons/slash.svg @@ -1 +1,3 @@ - + + + diff --git a/assets/icons/slash_square.svg b/assets/icons/slash_square.svg deleted file mode 100644 index 8f269ddeb5..0000000000 --- a/assets/icons/slash_square.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/sliders_alt.svg b/assets/icons/sliders_alt.svg deleted file mode 100644 index 36c3feccfe..0000000000 --- a/assets/icons/sliders_alt.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/sliders_vertical.svg b/assets/icons/sliders_vertical.svg deleted file mode 100644 index ab61037a51..0000000000 --- a/assets/icons/sliders_vertical.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/snip.svg b/assets/icons/snip.svg deleted file mode 100644 index 03ae4ce039..0000000000 --- a/assets/icons/snip.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/space.svg b/assets/icons/space.svg index 63718fb4aa..86bd55cd53 100644 --- a/assets/icons/space.svg +++ b/assets/icons/space.svg @@ -1 +1,3 @@ - + + + diff --git a/assets/icons/sparkle.svg b/assets/icons/sparkle.svg index f420f527f1..e5cce9fafd 100644 --- a/assets/icons/sparkle.svg +++ b/assets/icons/sparkle.svg @@ -1 +1 @@ - + diff --git a/assets/icons/sparkle_alt.svg b/assets/icons/sparkle_alt.svg deleted file mode 100644 index d5c227b105..0000000000 --- a/assets/icons/sparkle_alt.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/sparkle_filled.svg b/assets/icons/sparkle_filled.svg deleted file mode 100644 index 96837f618d..0000000000 --- a/assets/icons/sparkle_filled.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/speaker_loud.svg b/assets/icons/speaker_loud.svg deleted file mode 100644 index 68982ee5e9..0000000000 --- a/assets/icons/speaker_loud.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/split.svg b/assets/icons/split.svg index 4c131466c2..eb031ab790 100644 --- a/assets/icons/split.svg +++ b/assets/icons/split.svg @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/assets/icons/split_alt.svg b/assets/icons/split_alt.svg index 3f7622701d..5b99b7a26a 100644 --- a/assets/icons/split_alt.svg +++ b/assets/icons/split_alt.svg @@ -1 +1 @@ - + diff --git a/assets/icons/square_dot.svg b/assets/icons/square_dot.svg index 2c1d8afdcb..4bb684afb2 100644 --- a/assets/icons/square_dot.svg +++ b/assets/icons/square_dot.svg @@ -1 +1,4 @@ - + + + + diff --git a/assets/icons/square_minus.svg b/assets/icons/square_minus.svg index a9ab42c408..4b8fc4d982 100644 --- a/assets/icons/square_minus.svg +++ b/assets/icons/square_minus.svg @@ -1 +1,4 @@ - + + + + diff --git a/assets/icons/square_plus.svg b/assets/icons/square_plus.svg index 8cbe3dc0e7..e0ee106b52 100644 --- a/assets/icons/square_plus.svg +++ b/assets/icons/square_plus.svg @@ -1 +1,5 @@ - + + + + + diff --git a/assets/icons/star_filled.svg b/assets/icons/star_filled.svg index 89b03ded29..d7de9939db 100644 --- a/assets/icons/star_filled.svg +++ b/assets/icons/star_filled.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/stop.svg b/assets/icons/stop.svg index 6291a34c08..41e4fd35e9 100644 --- a/assets/icons/stop.svg +++ b/assets/icons/stop.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/stop_filled.svg b/assets/icons/stop_filled.svg deleted file mode 100644 index caf40d197e..0000000000 --- a/assets/icons/stop_filled.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/supermaven.svg b/assets/icons/supermaven.svg index 19837fbf56..af778c70b7 100644 --- a/assets/icons/supermaven.svg +++ b/assets/icons/supermaven.svg @@ -1,8 +1,8 @@ - - - - - - - + + + + + + + diff --git a/assets/icons/supermaven_disabled.svg b/assets/icons/supermaven_disabled.svg index 39ff8a6122..25eea54cde 100644 --- a/assets/icons/supermaven_disabled.svg +++ b/assets/icons/supermaven_disabled.svg @@ -1,15 +1 @@ - - - - - - - - - - - - - - - + diff --git a/assets/icons/supermaven_error.svg b/assets/icons/supermaven_error.svg index 669322b97d..a0a12e17c3 100644 --- a/assets/icons/supermaven_error.svg +++ b/assets/icons/supermaven_error.svg @@ -1,11 +1,11 @@ - - - - - - + + + + + + - + diff --git a/assets/icons/supermaven_init.svg b/assets/icons/supermaven_init.svg index b919d5559b..6851aad49d 100644 --- a/assets/icons/supermaven_init.svg +++ b/assets/icons/supermaven_init.svg @@ -1,11 +1,11 @@ - - - - - - + + + + + + - + diff --git a/assets/icons/swatch_book.svg b/assets/icons/swatch_book.svg index 985994ffcf..99a1c88bd5 100644 --- a/assets/icons/swatch_book.svg +++ b/assets/icons/swatch_book.svg @@ -1 +1 @@ - + diff --git a/assets/icons/tab.svg b/assets/icons/tab.svg index 49a3536bed..f16d51ccf5 100644 --- a/assets/icons/tab.svg +++ b/assets/icons/tab.svg @@ -1 +1,5 @@ - + + + + + diff --git a/assets/icons/terminal_alt.svg b/assets/icons/terminal_alt.svg index 7afb89db21..82d88167b2 100644 --- a/assets/icons/terminal_alt.svg +++ b/assets/icons/terminal_alt.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/text_snippet.svg b/assets/icons/text_snippet.svg index 255635de6a..12f131fdd5 100644 --- a/assets/icons/text_snippet.svg +++ b/assets/icons/text_snippet.svg @@ -1 +1 @@ - + diff --git a/assets/icons/thumbs_down.svg b/assets/icons/thumbs_down.svg index 2edc09acd1..334115a014 100644 --- a/assets/icons/thumbs_down.svg +++ b/assets/icons/thumbs_down.svg @@ -1,3 +1 @@ - - - + diff --git a/assets/icons/thumbs_up.svg b/assets/icons/thumbs_up.svg index ff4406034d..b1e435936b 100644 --- a/assets/icons/thumbs_up.svg +++ b/assets/icons/thumbs_up.svg @@ -1,3 +1 @@ - - - + diff --git a/assets/icons/todo_complete.svg b/assets/icons/todo_complete.svg index 9fa2e818bb..d50044e435 100644 --- a/assets/icons/todo_complete.svg +++ b/assets/icons/todo_complete.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/tool_folder.svg b/assets/icons/tool_folder.svg index 9d3ac299d2..0d76b7e3f8 100644 --- a/assets/icons/tool_folder.svg +++ b/assets/icons/tool_folder.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/tool_terminal.svg b/assets/icons/tool_terminal.svg index 5154fa8e70..3c4ab42a4d 100644 --- a/assets/icons/tool_terminal.svg +++ b/assets/icons/tool_terminal.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/tool_think.svg b/assets/icons/tool_think.svg index 54d5ac5fd7..595f8070d8 100644 --- a/assets/icons/tool_think.svg +++ b/assets/icons/tool_think.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/triangle.svg b/assets/icons/triangle.svg index 0ecf071e24..c36d382e73 100644 --- a/assets/icons/triangle.svg +++ b/assets/icons/triangle.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/triangle_right.svg b/assets/icons/triangle_right.svg index 2c78a316f7..bb82d8e637 100644 --- a/assets/icons/triangle_right.svg +++ b/assets/icons/triangle_right.svg @@ -1 +1,3 @@ - + + + diff --git a/assets/icons/undo.svg b/assets/icons/undo.svg index 907cc77195..b2407456dc 100644 --- a/assets/icons/undo.svg +++ b/assets/icons/undo.svg @@ -1 +1 @@ - + diff --git a/assets/icons/update.svg b/assets/icons/update.svg deleted file mode 100644 index b529b2b08b..0000000000 --- a/assets/icons/update.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/user_check.svg b/assets/icons/user_check.svg index e5f13feeb4..cd682b5eda 100644 --- a/assets/icons/user_check.svg +++ b/assets/icons/user_check.svg @@ -1 +1 @@ - + diff --git a/assets/icons/user_round_pen.svg b/assets/icons/user_round_pen.svg index e25bf10469..eb75517323 100644 --- a/assets/icons/user_round_pen.svg +++ b/assets/icons/user_round_pen.svg @@ -1 +1 @@ - + diff --git a/assets/icons/visible.svg b/assets/icons/visible.svg deleted file mode 100644 index 0a7e65d60d..0000000000 --- a/assets/icons/visible.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/wand.svg b/assets/icons/wand.svg deleted file mode 100644 index a6704b1c42..0000000000 --- a/assets/icons/wand.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/warning.svg b/assets/icons/warning.svg index c48a575a90..456799fa5a 100644 --- a/assets/icons/warning.svg +++ b/assets/icons/warning.svg @@ -1 +1 @@ - + diff --git a/assets/icons/whole_word.svg b/assets/icons/whole_word.svg index beca4cbe82..77cecce38c 100644 --- a/assets/icons/whole_word.svg +++ b/assets/icons/whole_word.svg @@ -1,5 +1 @@ - - - - - + diff --git a/assets/icons/x.svg b/assets/icons/x.svg deleted file mode 100644 index 5d91a9edd9..0000000000 --- a/assets/icons/x.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/x_circle.svg b/assets/icons/x_circle.svg index 593629beee..69aaa3f6a1 100644 --- a/assets/icons/x_circle.svg +++ b/assets/icons/x_circle.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/zed_assistant_filled.svg b/assets/icons/zed_assistant_filled.svg deleted file mode 100644 index 8d16fd9849..0000000000 --- a/assets/icons/zed_assistant_filled.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/zed_burn_mode.svg b/assets/icons/zed_burn_mode.svg index 544368d8e0..f6192d16e7 100644 --- a/assets/icons/zed_burn_mode.svg +++ b/assets/icons/zed_burn_mode.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/zed_burn_mode_on.svg b/assets/icons/zed_burn_mode_on.svg index 94230b6fd6..29a74a3e63 100644 --- a/assets/icons/zed_burn_mode_on.svg +++ b/assets/icons/zed_burn_mode_on.svg @@ -1,13 +1 @@ - - - - - - - - - - - - - + diff --git a/assets/icons/zed_x_copilot.svg b/assets/icons/zed_x_copilot.svg deleted file mode 100644 index d024678c50..0000000000 --- a/assets/icons/zed_x_copilot.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/crates/agent/src/context.rs b/crates/agent/src/context.rs index cd366b8308..8cdb87ef8d 100644 --- a/crates/agent/src/context.rs +++ b/crates/agent/src/context.rs @@ -20,7 +20,7 @@ use text::{Anchor, OffsetRangeExt as _}; use util::markdown::MarkdownCodeBlock; use util::{ResultExt as _, post_inc}; -pub const RULES_ICON: IconName = IconName::Context; +pub const RULES_ICON: IconName = IconName::Reader; pub enum ContextKind { File, @@ -40,8 +40,8 @@ impl ContextKind { ContextKind::File => IconName::File, ContextKind::Directory => IconName::Folder, ContextKind::Symbol => IconName::Code, - ContextKind::Selection => IconName::Context, - ContextKind::FetchedUrl => IconName::Globe, + ContextKind::Selection => IconName::Reader, + ContextKind::FetchedUrl => IconName::ToolWeb, ContextKind::Thread => IconName::Thread, ContextKind::TextThread => IconName::TextThread, ContextKind::Rules => RULES_ICON, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 7f4e7e7208..74bbac2b1c 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1094,7 +1094,7 @@ impl AcpThreadView { status: acp::ToolCallStatus::Failed, .. } => Some( - Icon::new(IconName::X) + Icon::new(IconName::Close) .color(Color::Error) .size(IconSize::Small) .into_any_element(), @@ -1351,10 +1351,10 @@ impl AcpThreadView { this.icon(IconName::CheckDouble).icon_color(Color::Success) } acp::PermissionOptionKind::RejectOnce => { - this.icon(IconName::X).icon_color(Color::Error) + this.icon(IconName::Close).icon_color(Color::Error) } acp::PermissionOptionKind::RejectAlways => { - this.icon(IconName::X).icon_color(Color::Error) + this.icon(IconName::Close).icon_color(Color::Error) } }) .icon_position(IconPosition::Start) @@ -2118,7 +2118,7 @@ impl AcpThreadView { .hover(|this| this.opacity(1.0)) .child( IconButton::new("toggle-height", expand_icon) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .icon_color(Color::Muted) .tooltip({ let focus_handle = focus_handle.clone(); @@ -2168,7 +2168,7 @@ impl AcpThreadView { })) .into_any_element() } else { - IconButton::new("stop-generation", IconName::StopFilled) + IconButton::new("stop-generation", IconName::Stop) .icon_color(Color::Error) .style(ButtonStyle::Tinted(ui::TintColor::Error)) .tooltip(move |window, cx| { @@ -2537,7 +2537,7 @@ impl AcpThreadView { } fn render_thread_controls(&self, cx: &Context) -> impl IntoElement { - let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileText) + let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown) .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) .tooltip(Tooltip::text("Open Thread as Markdown")) @@ -2548,7 +2548,7 @@ impl AcpThreadView { } })); - let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUpAlt) + let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUp) .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) .tooltip(Tooltip::text("Scroll To Top")) @@ -2560,6 +2560,7 @@ impl AcpThreadView { .w_full() .mr_1() .pb_2() + .gap_1() .px(RESPONSE_PADDING_X) .opacity(0.4) .hover(|style| style.opacity(1.)) diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 71526c8fe1..ffed62d41f 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -434,7 +434,7 @@ fn render_markdown_code_block( .child(content) .child( Icon::new(IconName::ArrowUpRight) - .size(IconSize::XSmall) + .size(IconSize::Small) .color(Color::Ignored), ), ) @@ -1896,8 +1896,9 @@ impl ActiveThread { (colors.editor_background, colors.panel_background) }; - let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::DocumentText) - .icon_size(IconSize::XSmall) + let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::FileMarkdown) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) .icon_color(Color::Ignored) .tooltip(Tooltip::text("Open Thread as Markdown")) .on_click({ @@ -1911,8 +1912,9 @@ impl ActiveThread { } }); - let scroll_to_top = IconButton::new(("scroll_to_top", ix), IconName::ArrowUpAlt) - .icon_size(IconSize::XSmall) + let scroll_to_top = IconButton::new(("scroll_to_top", ix), IconName::ArrowUp) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) .icon_color(Color::Ignored) .tooltip(Tooltip::text("Scroll To Top")) .on_click(cx.listener(move |this, _, _, cx| { @@ -1926,6 +1928,7 @@ impl ActiveThread { .py_2() .px(RESPONSE_PADDING_X) .mr_1() + .gap_1() .opacity(0.4) .hover(|style| style.opacity(1.)) .gap_1p5() @@ -1949,7 +1952,8 @@ impl ActiveThread { h_flex() .child( IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp) - .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) .icon_color(match feedback { ThreadFeedback::Positive => Color::Accent, ThreadFeedback::Negative => Color::Ignored, @@ -1966,7 +1970,8 @@ impl ActiveThread { ) .child( IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown) - .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) .icon_color(match feedback { ThreadFeedback::Positive => Color::Ignored, ThreadFeedback::Negative => Color::Accent, @@ -1999,7 +2004,8 @@ impl ActiveThread { h_flex() .child( IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp) - .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) .icon_color(Color::Ignored) .tooltip(Tooltip::text("Helpful Response")) .on_click(cx.listener(move |this, _, window, cx| { @@ -2013,7 +2019,8 @@ impl ActiveThread { ) .child( IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown) - .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) .icon_color(Color::Ignored) .tooltip(Tooltip::text("Not Helpful")) .on_click(cx.listener(move |this, _, window, cx| { @@ -2750,7 +2757,7 @@ impl ActiveThread { h_flex() .gap_1p5() .child( - Icon::new(IconName::LightBulb) + Icon::new(IconName::ToolThink) .size(IconSize::XSmall) .color(Color::Muted), ) @@ -3362,7 +3369,7 @@ impl ActiveThread { .mr_0p5(), ) .child( - IconButton::new("open-prompt-library", IconName::ArrowUpRightAlt) + IconButton::new("open-prompt-library", IconName::ArrowUpRight) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) @@ -3397,7 +3404,7 @@ impl ActiveThread { .mr_0p5(), ) .child( - IconButton::new("open-rule", IconName::ArrowUpRightAlt) + IconButton::new("open-rule", IconName::ArrowUpRight) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 02c15b7e41..5f72fa58c8 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -573,7 +573,7 @@ impl AgentConfiguration { .style(ButtonStyle::Filled) .layer(ElevationIndex::ModalSurface) .full_width() - .icon(IconName::Hammer) + .icon(IconName::ToolHammer) .icon_size(IconSize::Small) .icon_position(IconPosition::Start) .on_click(|_event, window, cx| { diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs index 06d035d836..32360dd56e 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs @@ -438,7 +438,7 @@ impl ConfigureContextServerModal { format!("{} configured successfully.", id.0), cx, |this, _cx| { - this.icon(ToastIcon::new(IconName::Hammer).color(Color::Muted)) + this.icon(ToastIcon::new(IconName::ToolHammer).color(Color::Muted)) .action("Dismiss", |_, _| {}) }, ); @@ -567,7 +567,7 @@ impl ConfigureContextServerModal { Button::new("open-repository", "Open Repository") .icon(IconName::ArrowUpRight) .icon_color(Color::Muted) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .tooltip({ let repository_url = repository_url.clone(); move |window, cx| { diff --git a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs index 5d44bb2d92..09ad013d1c 100644 --- a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs +++ b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs @@ -594,7 +594,7 @@ impl ManageProfilesModal { .inset(true) .spacing(ListItemSpacing::Sparse) .start_slot( - Icon::new(IconName::Hammer) + Icon::new(IconName::ToolHammer) .size(IconSize::Small) .color(Color::Muted), ) @@ -763,7 +763,7 @@ impl Render for ManageProfilesModal { .pb_1() .child(ProfileModalHeader::new( format!("{profile_name} — Configure MCP Tools"), - Some(IconName::Hammer), + Some(IconName::ToolHammer), )) .child(ListSeparator) .child(tool_picker.clone()) diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index 32f9a096d9..58f11313e6 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -102,7 +102,7 @@ impl ContextPickerAction { pub fn icon(&self) -> IconName { match self { - Self::AddSelections => IconName::Context, + Self::AddSelections => IconName::Reader, } } } @@ -147,7 +147,7 @@ impl ContextPickerMode { match self { Self::File => IconName::File, Self::Symbol => IconName::Code, - Self::Fetch => IconName::Globe, + Self::Fetch => IconName::ToolWeb, Self::Thread => IconName::Thread, Self::Rules => RULES_ICON, } diff --git a/crates/agent_ui/src/context_picker/completion_provider.rs b/crates/agent_ui/src/context_picker/completion_provider.rs index 5ca0913be7..8123b3437d 100644 --- a/crates/agent_ui/src/context_picker/completion_provider.rs +++ b/crates/agent_ui/src/context_picker/completion_provider.rs @@ -371,7 +371,7 @@ impl ContextPickerCompletionProvider { line_range.end.row + 1 ) .into(), - IconName::Context.path().into(), + IconName::Reader.path().into(), range, editor.downgrade(), ); @@ -539,10 +539,10 @@ impl ContextPickerCompletionProvider { label: CodeLabel::plain(url_to_fetch.to_string(), None), documentation: None, source: project::CompletionSource::Custom, - icon_path: Some(IconName::Globe.path().into()), + icon_path: Some(IconName::ToolWeb.path().into()), insert_text_mode: None, confirm: Some(confirm_completion_callback( - IconName::Globe.path().into(), + IconName::ToolWeb.path().into(), url_to_fetch.clone(), excerpt_id, source_range.start, diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index a5f90edb57..e6fca16984 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -541,7 +541,7 @@ impl PromptEditor { match &self.mode { PromptEditorMode::Terminal { .. } => vec![ accept, - IconButton::new("confirm", IconName::PlayOutlined) + IconButton::new("confirm", IconName::PlayFilled) .icon_color(Color::Info) .shape(IconButtonShape::Square) .tooltip(|window, cx| { diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 2185885347..4b6d51c4c1 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -725,7 +725,7 @@ impl MessageEditor { .when(focus_handle.is_focused(window), |this| { this.child( IconButton::new("toggle-height", expand_icon) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .icon_color(Color::Muted) .tooltip({ let focus_handle = focus_handle.clone(); @@ -831,7 +831,7 @@ impl MessageEditor { parent.child( IconButton::new( "stop-generation", - IconName::StopFilled, + IconName::Stop, ) .icon_color(Color::Error) .style(ButtonStyle::Tinted( @@ -1305,7 +1305,7 @@ impl MessageEditor { cx: &mut Context, ) -> Option

{ let icon = if token_usage_ratio == TokenUsageRatio::Exceeded { - Icon::new(IconName::X) + Icon::new(IconName::Close) .color(Color::Error) .size(IconSize::XSmall) } else { diff --git a/crates/agent_ui/src/slash_command_picker.rs b/crates/agent_ui/src/slash_command_picker.rs index a757a2f50a..678562e059 100644 --- a/crates/agent_ui/src/slash_command_picker.rs +++ b/crates/agent_ui/src/slash_command_picker.rs @@ -306,7 +306,7 @@ where ) .child( Icon::new(IconName::ArrowUpRight) - .size(IconSize::XSmall) + .size(IconSize::Small) .color(Color::Muted), ), ) diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 4836a95c8e..49a37002f7 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -2233,7 +2233,7 @@ fn render_thought_process_fold_icon_button( let button = match status { ThoughtProcessStatus::Pending => button .child( - Icon::new(IconName::LightBulb) + Icon::new(IconName::ToolThink) .size(IconSize::Small) .color(Color::Muted), ) @@ -2248,7 +2248,7 @@ fn render_thought_process_fold_icon_button( ), ThoughtProcessStatus::Completed => button .style(ButtonStyle::Filled) - .child(Icon::new(IconName::LightBulb).size(IconSize::Small)) + .child(Icon::new(IconName::ToolThink).size(IconSize::Small)) .child(Label::new("Thought Process").single_line()), }; diff --git a/crates/agent_ui/src/ui/onboarding_modal.rs b/crates/agent_ui/src/ui/onboarding_modal.rs index 9e04171ec9..b8b038bdfc 100644 --- a/crates/agent_ui/src/ui/onboarding_modal.rs +++ b/crates/agent_ui/src/ui/onboarding_modal.rs @@ -139,7 +139,7 @@ impl Render for AgentOnboardingModal { .child(Headline::new("Agentic Editing in Zed").size(HeadlineSize::Large)), ) .child(h_flex().absolute().top_2().right_2().child( - IconButton::new("cancel", IconName::X).on_click(cx.listener( + IconButton::new("cancel", IconName::Close).on_click(cx.listener( |_, _: &ClickEvent, _window, cx| { agent_onboarding_event!("Cancelled", trigger = "X click"); cx.emit(DismissEvent); diff --git a/crates/agent_ui/src/ui/preview/usage_callouts.rs b/crates/agent_ui/src/ui/preview/usage_callouts.rs index 64869a6ec7..eef878a9d1 100644 --- a/crates/agent_ui/src/ui/preview/usage_callouts.rs +++ b/crates/agent_ui/src/ui/preview/usage_callouts.rs @@ -81,7 +81,7 @@ impl RenderOnce for UsageCallout { }; let icon = if is_limit_reached { - Icon::new(IconName::X) + Icon::new(IconName::Close) .color(Color::Error) .size(IconSize::XSmall) } else { diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index b9a1e49a4a..75177d4bd2 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -110,7 +110,7 @@ impl ZedAiOnboarding { .style(ButtonStyle::Outlined) .icon(IconName::ArrowUpRight) .icon_color(Color::Muted) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .on_click(move |_, _window, cx| { telemetry::event!("Review Terms of Service Clicked"); cx.open_url(&zed_urls::terms_of_service(cx)) diff --git a/crates/assistant_slash_commands/src/fetch_command.rs b/crates/assistant_slash_commands/src/fetch_command.rs index 5e586d4f23..4e0bb3d05a 100644 --- a/crates/assistant_slash_commands/src/fetch_command.rs +++ b/crates/assistant_slash_commands/src/fetch_command.rs @@ -112,7 +112,7 @@ impl SlashCommand for FetchSlashCommand { } fn icon(&self) -> IconName { - IconName::Globe + IconName::ToolWeb } fn menu_text(&self) -> String { @@ -171,7 +171,7 @@ impl SlashCommand for FetchSlashCommand { text, sections: vec![SlashCommandOutputSection { range, - icon: IconName::Globe, + icon: IconName::ToolWeb, label: format!("fetch {}", url).into(), metadata: None, }], diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index dce9f49abd..54431ee1d7 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -857,7 +857,7 @@ impl ToolCard for EditFileToolCard { ) .child( Icon::new(IconName::ArrowUpRight) - .size(IconSize::XSmall) + .size(IconSize::Small) .color(Color::Ignored), ), ) diff --git a/crates/assistant_tools/src/find_path_tool.rs b/crates/assistant_tools/src/find_path_tool.rs index affc019417..6cdf58eac8 100644 --- a/crates/assistant_tools/src/find_path_tool.rs +++ b/crates/assistant_tools/src/find_path_tool.rs @@ -257,7 +257,7 @@ impl ToolCard for FindPathToolCard { Button::new(("path", index), button_label) .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .icon_position(IconPosition::End) .label_size(LabelSize::Small) .color(Color::Muted) diff --git a/crates/assistant_tools/src/web_search_tool.rs b/crates/assistant_tools/src/web_search_tool.rs index d4a12f22c5..c6c37de472 100644 --- a/crates/assistant_tools/src/web_search_tool.rs +++ b/crates/assistant_tools/src/web_search_tool.rs @@ -45,7 +45,7 @@ impl Tool for WebSearchTool { } fn icon(&self) -> IconName { - IconName::Globe + IconName::ToolWeb } fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { @@ -177,7 +177,7 @@ impl ToolCard for WebSearchToolCard { .label_size(LabelSize::Small) .color(Color::Muted) .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .icon_position(IconPosition::End) .truncate(true) .tooltip({ diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 51e4ff8965..430b447580 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2931,7 +2931,7 @@ impl CollabPanel { .visible_on_hover(""), ) .child( - IconButton::new("channel_notes", IconName::FileText) + IconButton::new("channel_notes", IconName::Reader) .style(ButtonStyle::Filled) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::Small) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 0ac419580b..91382c74ae 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -649,7 +649,7 @@ impl DebugPanel { .tooltip(Tooltip::text("Open Documentation")) }; let logs_button = || { - IconButton::new("debug-open-logs", IconName::ScrollText) + IconButton::new("debug-open-logs", IconName::Notepad) .icon_size(IconSize::Small) .on_click(move |_, window, cx| { window.dispatch_action(debugger_tools::OpenDebugAdapterLogs.boxed_clone(), cx) @@ -788,7 +788,7 @@ impl DebugPanel { ) .child( IconButton::new("debug-step-out", IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .shape(ui::IconButtonShape::Square) .on_click(window.listener_for( &running_state, @@ -812,7 +812,7 @@ impl DebugPanel { ) .child(Divider::vertical()) .child( - IconButton::new("debug-restart", IconName::DebugRestart) + IconButton::new("debug-restart", IconName::RotateCcw) .icon_size(IconSize::XSmall) .on_click(window.listener_for( &running_state, diff --git a/crates/debugger_ui/src/onboarding_modal.rs b/crates/debugger_ui/src/onboarding_modal.rs index c9fa009940..2a9f68d0c9 100644 --- a/crates/debugger_ui/src/onboarding_modal.rs +++ b/crates/debugger_ui/src/onboarding_modal.rs @@ -131,7 +131,7 @@ impl Render for DebuggerOnboardingModal { .child(Headline::new("Zed's Debugger").size(HeadlineSize::Large)), ) .child(h_flex().absolute().top_2().right_2().child( - IconButton::new("cancel", IconName::X).on_click(cx.listener( + IconButton::new("cancel", IconName::Close).on_click(cx.listener( |_, _: &ClickEvent, _window, cx| { debugger_onboarding_event!("Cancelled", trigger = "X click"); cx.emit(DismissEvent); diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index a6defbbf35..326fb84e20 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -681,7 +681,7 @@ impl BreakpointList { }), ) .child( - IconButton::new("remove-breakpoint-breakpoint-list", IconName::X) + IconButton::new("remove-breakpoint-breakpoint-list", IconName::Close) .icon_size(IconSize::XSmall) .icon_color(ui::Color::Error) .when_some(remove_breakpoint_tooltip, |this, tooltip| { @@ -1439,7 +1439,7 @@ impl RenderOnce for BreakpointOptionsStrip { .child( IconButton::new( SharedString::from(format!("{id}-log-toggle")), - IconName::ScrollText, + IconName::Notepad, ) .icon_size(IconSize::XSmall) .style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs)) diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index 1385bec54e..daf4486f81 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -352,7 +352,7 @@ impl Console { .child( div() .px_1() - .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)), + .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)), ), ) .when( diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index 2149502f4a..8b44c231c3 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -493,7 +493,7 @@ impl StackFrameList { .child( IconButton::new( ("restart-stack-frame", stack_frame.id), - IconName::DebugRestart, + IconName::RotateCcw, ) .icon_size(IconSize::Small) .on_click(cx.listener({ diff --git a/crates/diagnostics/src/toolbar_controls.rs b/crates/diagnostics/src/toolbar_controls.rs index 9a7dcbe62f..e77b80115f 100644 --- a/crates/diagnostics/src/toolbar_controls.rs +++ b/crates/diagnostics/src/toolbar_controls.rs @@ -54,7 +54,7 @@ impl Render for ToolbarControls { .map(|div| { if is_updating { div.child( - IconButton::new("stop-updating", IconName::StopFilled) + IconButton::new("stop-updating", IconName::Stop) .icon_color(Color::Info) .shape(IconButtonShape::Square) .tooltip(Tooltip::for_action_title( @@ -73,7 +73,7 @@ impl Render for ToolbarControls { ) } else { div.child( - IconButton::new("refresh-diagnostics", IconName::Update) + IconButton::new("refresh-diagnostics", IconName::ArrowCircle) .icon_color(Color::Info) .shape(IconButtonShape::Square) .disabled(!has_stale_excerpts && !fetch_cargo_diagnostics) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index ca635a2132..231aaa1d00 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1825,7 +1825,7 @@ pub fn entry_diagnostic_aware_icon_name_and_color( diagnostic_severity: Option, ) -> Option<(IconName, Color)> { match diagnostic_severity { - Some(DiagnosticSeverity::ERROR) => Some((IconName::X, Color::Error)), + Some(DiagnosticSeverity::ERROR) => Some((IconName::Close, Color::Error)), Some(DiagnosticSeverity::WARNING) => Some((IconName::Triangle, Color::Warning)), _ => None, } diff --git a/crates/extensions_ui/src/components/feature_upsell.rs b/crates/extensions_ui/src/components/feature_upsell.rs index e2e65f1598..573b0b992d 100644 --- a/crates/extensions_ui/src/components/feature_upsell.rs +++ b/crates/extensions_ui/src/components/feature_upsell.rs @@ -58,7 +58,7 @@ impl RenderOnce for FeatureUpsell { el.child( Button::new("open_docs", "View Documentation") .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .icon_position(IconPosition::End) .on_click({ let docs_url = docs_url.clone(); diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index b74fa649b0..6bb84db834 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -473,7 +473,7 @@ impl PickerDelegate for BranchListDelegate { && entry.is_new { Some( - IconButton::new("branch-from-default", IconName::GitBranchSmall) + IconButton::new("branch-from-default", IconName::GitBranchAlt) .on_click(cx.listener(move |this, _, window, cx| { this.delegate.set_selected_index(ix, window, cx); this.delegate.confirm(true, window, cx); diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index 5dfa800ae5..5e7430ebc6 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -272,7 +272,7 @@ impl CommitModal { .child( div() .px_1() - .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)), + .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)), ), ) .menu({ diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 44222b8299..e4f445858d 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -2900,10 +2900,10 @@ impl GitPanel { use remote_output::SuccessStyle::*; match style { Toast { .. } => { - this.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted)) + this.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)) } ToastWithLog { output } => this - .icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted)) + .icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)) .action("View Log", move |window, cx| { let output = output.clone(); let output = @@ -2915,7 +2915,7 @@ impl GitPanel { .ok(); }), PushPrLink { text, link } => this - .icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted)) + .icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)) .action(text, move |_, cx| cx.open_url(&link)), } }); @@ -3109,7 +3109,7 @@ impl GitPanel { .justify_center() .border_l_1() .border_color(cx.theme().colors().border) - .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)), + .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)), ), ) .menu({ @@ -4561,7 +4561,7 @@ impl Panel for GitPanel { } fn icon(&self, _: &Window, cx: &App) -> Option { - Some(ui::IconName::GitBranchSmall).filter(|_| GitPanelSettings::get_global(cx).button) + Some(ui::IconName::GitBranchAlt).filter(|_| GitPanelSettings::get_global(cx).button) } fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> { @@ -4808,7 +4808,7 @@ impl RenderOnce for PanelRepoFooter { .items_center() .child( div().child( - Icon::new(IconName::GitBranchSmall) + Icon::new(IconName::GitBranchAlt) .size(IconSize::Small) .color(if single_repo { Color::Disabled diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 0163175eda..bde867bcd2 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -356,7 +356,7 @@ mod remote_button { "Publish", 0, 0, - Some(IconName::ArrowUpFromLine), + Some(IconName::ExpandUp), keybinding_target.clone(), move |_, window, cx| { window.dispatch_action(Box::new(git::Push), cx); @@ -383,7 +383,7 @@ mod remote_button { "Republish", 0, 0, - Some(IconName::ArrowUpFromLine), + Some(IconName::ExpandUp), keybinding_target.clone(), move |_, window, cx| { window.dispatch_action(Box::new(git::Push), cx); @@ -438,7 +438,7 @@ mod remote_button { .child( div() .px_1() - .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)), + .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)), ), ) .menu(move |window, cx| { diff --git a/crates/git_ui/src/onboarding.rs b/crates/git_ui/src/onboarding.rs index d721b21a2a..d1709e043b 100644 --- a/crates/git_ui/src/onboarding.rs +++ b/crates/git_ui/src/onboarding.rs @@ -110,7 +110,7 @@ impl Render for GitOnboardingModal { .child(Headline::new("Native Git Support").size(HeadlineSize::Large)), ) .child(h_flex().absolute().top_2().right_2().child( - IconButton::new("cancel", IconName::X).on_click(cx.listener( + IconButton::new("cancel", IconName::Close).on_click(cx.listener( |_, _: &ClickEvent, _window, cx| { git_onboarding_event!("Cancelled", trigger = "X click"); cx.emit(DismissEvent); diff --git a/crates/icons/README.md b/crates/icons/README.md new file mode 100644 index 0000000000..5fbd6d4948 --- /dev/null +++ b/crates/icons/README.md @@ -0,0 +1,29 @@ +# Zed Icons + +## Guidelines + +Icons are a big part of Zed, and they're how we convey hundreds of actions without relying on labeled buttons. +When introducing a new icon to the set, it's important to ensure it is consistent with the whole set, which follows a few guidelines: + +1. The SVG view box should be 16x16. +2. For outlined icons, use a 1.5px stroke width. +3. Not all icons are mathematically aligned; there's quite a bit of optical adjustment. But try to keep the icon within an internal 12x12 bounding box as much as possible while ensuring proper visibility. +4. Use the `filled` and `outlined` terminology when introducing icons that will have the two variants. +5. Icons that are deeply contextual may have the feature context as their name prefix. For example, `ToolWeb`, `ReplPlay`, `DebugStepInto`, etc. +6. Avoid complex layer structure in the icon SVG, like clipping masks and whatnot. When the shape ends up too complex, we recommend running the SVG in [SVGOMG](https://jakearchibald.github.io/svgomg/) to clean it up a bit. + +## Sourcing + +Most icons are created by sourcing them from [Lucide](https://lucide.dev/). +Then, they're modified, adjusted, cleaned up, and simplified depending on their use and overall fit with Zed. + +Sometimes, we may use other sources like [Phosphor](https://phosphoricons.com/), but we also design many of them completely from scratch. + +## Contributing + +To introduce a new icon, add the `.svg` file in the `assets/icon` directory and then add its corresponding item in the `icons.rs` file within the `crates` directory. + +- SVG files in the assets folder follow a snake case name format. +- Icons in the `icons.rs` file follow the pascal case name format. + +Ensure you tag a member of Zed's design team so we can adjust and double-check any newly introduced icon. diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 12805e62e0..f5c2a83fec 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -28,16 +28,12 @@ pub enum IconName { ArrowCircle, ArrowDown, ArrowDown10, - ArrowDownFromLine, ArrowDownRight, ArrowLeft, ArrowRight, ArrowRightLeft, ArrowUp, - ArrowUpAlt, - ArrowUpFromLine, ArrowUpRight, - ArrowUpRightAlt, AudioOff, AudioOn, Backspace, @@ -51,28 +47,22 @@ pub enum IconName { BoltFilled, Book, BookCopy, - BugOff, CaseSensitive, Chat, Check, CheckDouble, ChevronDown, - /// This chevron indicates a popover menu. - ChevronDownSmall, ChevronLeft, ChevronRight, ChevronUp, ChevronUpDown, Circle, - CircleOff, CircleHelp, Close, - Cloud, CloudDownload, Code, Cog, Command, - Context, Control, Copilot, CopilotDisabled, @@ -93,16 +83,12 @@ pub enum IconName { DebugIgnoreBreakpoints, DebugLogBreakpoint, DebugPause, - DebugRestart, DebugStepBack, DebugStepInto, DebugStepOut, DebugStepOver, - DebugStop, - Delete, Diff, Disconnected, - DocumentText, Download, EditorAtom, EditorCursor, @@ -113,59 +99,50 @@ pub enum IconName { Ellipsis, EllipsisVertical, Envelope, - Equal, Eraser, Escape, Exit, ExpandDown, ExpandUp, ExpandVertical, - ExternalLink, Eye, File, FileCode, - FileCreate, FileDiff, FileDoc, FileGeneric, FileGit, FileLock, + FileMarkdown, FileRust, - FileSearch, - FileText, + FileTextFilled, + FileTextOutlined, FileToml, FileTree, Filter, Flame, Folder, FolderOpen, - FolderX, + FolderSearch, Font, FontSize, FontWeight, ForwardArrow, - Function, GenericClose, GenericMaximize, GenericMinimize, GenericRestore, GitBranch, - GitBranchSmall, + GitBranchAlt, Github, - Globe, - Hammer, Hash, HistoryRerun, Image, Indicator, Info, - InlayHint, Keyboard, - Layout, Library, - LightBulb, LineHeight, - Link, ListCollapse, ListTodo, ListTree, @@ -173,35 +150,28 @@ pub enum IconName { LoadCircle, LocationEdit, LockOutlined, - LspDebug, - LspRestart, - LspStop, MagnifyingGlass, - MailOpen, Maximize, Menu, MenuAlt, Mic, MicMute, Minimize, + Notepad, Option, PageDown, PageUp, - PanelLeft, - PanelRight, Pencil, Person, - PersonCircle, - PhoneIncoming, Pin, PlayOutlined, PlayFilled, Plus, - PocketKnife, Power, Public, PullRequest, Quote, + Reader, RefreshTitle, Regex, ReplNeutral, @@ -213,28 +183,18 @@ pub enum IconName { Return, RotateCcw, RotateCw, - Route, - Save, Scissors, Screen, - ScrollText, - SearchSelection, SelectAll, Send, Server, Settings, - SettingsAlt, ShieldCheck, Shift, Slash, - SlashSquare, Sliders, - SlidersVertical, - Snip, Space, Sparkle, - SparkleAlt, - SparkleFilled, Split, SplitAlt, SquareDot, @@ -243,7 +203,6 @@ pub enum IconName { Star, StarFilled, Stop, - StopFilled, Supermaven, SupermavenDisabled, SupermavenError, @@ -279,18 +238,13 @@ pub enum IconName { TriangleRight, Undo, Unpin, - Update, UserCheck, UserGroup, UserRoundPen, - Visible, - Wand, Warning, WholeWord, - X, XCircle, ZedAssistant, - ZedAssistantFilled, ZedBurnMode, ZedBurnModeOn, ZedMcpCustom, diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 40dd120761..ba110be9c5 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -437,7 +437,7 @@ fn render_accept_terms( .style(ButtonStyle::Subtle) .icon(IconName::ArrowUpRight) .icon_color(Color::Muted) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .when(thread_empty_state, |this| this.label_size(LabelSize::Small)) .on_click(move |_, _window, cx| cx.open_url("https://zed.dev/terms-of-service")); diff --git a/crates/language_models/src/provider/lmstudio.rs b/crates/language_models/src/provider/lmstudio.rs index 9792b4f27b..36a32ab941 100644 --- a/crates/language_models/src/provider/lmstudio.rs +++ b/crates/language_models/src/provider/lmstudio.rs @@ -690,7 +690,7 @@ impl Render for ConfigurationView { Button::new("lmstudio-site", "LM Studio") .style(ButtonStyle::Subtle) .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .icon_color(Color::Muted) .on_click(move |_, _window, cx| { cx.open_url(LMSTUDIO_SITE) @@ -705,7 +705,7 @@ impl Render for ConfigurationView { ) .style(ButtonStyle::Subtle) .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .icon_color(Color::Muted) .on_click(move |_, _window, cx| { cx.open_url(LMSTUDIO_DOWNLOAD_URL) @@ -718,7 +718,7 @@ impl Render for ConfigurationView { Button::new("view-models", "Model Catalog") .style(ButtonStyle::Subtle) .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .icon_color(Color::Muted) .on_click(move |_, _window, cx| { cx.open_url(LMSTUDIO_CATALOG_URL) @@ -744,7 +744,7 @@ impl Render for ConfigurationView { Button::new("retry_lmstudio_models", "Connect") .icon_position(IconPosition::Start) .icon_size(IconSize::XSmall) - .icon(IconName::PlayOutlined) + .icon(IconName::PlayFilled) .on_click(cx.listener(move |this, _, _window, cx| { this.retry_connection(cx) })), diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index c845c97b09..0c2b1107b1 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -608,7 +608,7 @@ impl Render for ConfigurationView { Button::new("ollama-site", "Ollama") .style(ButtonStyle::Subtle) .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .icon_color(Color::Muted) .on_click(move |_, _, cx| cx.open_url(OLLAMA_SITE)) .into_any_element(), @@ -621,7 +621,7 @@ impl Render for ConfigurationView { ) .style(ButtonStyle::Subtle) .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .icon_color(Color::Muted) .on_click(move |_, _, cx| { cx.open_url(OLLAMA_DOWNLOAD_URL) @@ -634,7 +634,7 @@ impl Render for ConfigurationView { Button::new("view-models", "View All Models") .style(ButtonStyle::Subtle) .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .icon_color(Color::Muted) .on_click(move |_, _, cx| cx.open_url(OLLAMA_LIBRARY_URL)), ), @@ -658,7 +658,7 @@ impl Render for ConfigurationView { Button::new("retry_ollama_models", "Connect") .icon_position(IconPosition::Start) .icon_size(IconSize::XSmall) - .icon(IconName::PlayOutlined) + .icon(IconName::PlayFilled) .on_click(cx.listener(move |this, _, _, cx| { this.retry_connection(cx) })), diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index ee74562687..7a6c8e09ed 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -869,7 +869,7 @@ impl Render for ConfigurationView { .child( Button::new("docs", "Learn More") .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .icon_color(Color::Muted) .on_click(move |_, _window, cx| { cx.open_url("https://zed.dev/docs/ai/llm-providers#openai-api-compatible") diff --git a/crates/language_models/src/ui/instruction_list_item.rs b/crates/language_models/src/ui/instruction_list_item.rs index 794a85b400..3dee97aff6 100644 --- a/crates/language_models/src/ui/instruction_list_item.rs +++ b/crates/language_models/src/ui/instruction_list_item.rs @@ -47,7 +47,7 @@ impl IntoElement for InstructionListItem { Button::new(unique_id, button_label) .style(ButtonStyle::Subtle) .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .icon_color(Color::Muted) .on_click(move |_, _window, cx| cx.open_url(&link)), ) diff --git a/crates/notifications/src/status_toast.rs b/crates/notifications/src/status_toast.rs index ffd87e0b8b..7affa93f5a 100644 --- a/crates/notifications/src/status_toast.rs +++ b/crates/notifications/src/status_toast.rs @@ -205,7 +205,7 @@ impl Component for StatusToast { let pr_example = StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| { - this.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted)) + this.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)) .action("Open Pull Request", |_, cx| { cx.open_url("https://github.com/") }) diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index 00f2d5fc8b..0397bcbd9b 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -95,7 +95,7 @@ fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> i .style(ButtonStyle::Outlined) .label_size(LabelSize::Small) .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .icon_color(Color::Muted) .on_click(|_, _, cx| { cx.open_url("https://zed.dev/docs/ai/privacy-and-security"); diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 342b52bdda..145cb07a1c 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -694,7 +694,7 @@ pub async fn handle_import_vscode_settings( "Failed to import settings. See log for details", cx, |this, _| { - this.icon(ToastIcon::new(IconName::X).color(Color::Error)) + this.icon(ToastIcon::new(IconName::Close).color(Color::Error)) .action("Open Log", |window, cx| { window.dispatch_action(workspace::OpenLog.boxed_clone(), cx) }) diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index 5a38e1aadb..7b58792178 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -357,7 +357,7 @@ impl RenderOnce for SshConnectionHeader { .rounded_t_sm() .w_full() .gap_1p5() - .child(Icon::new(IconName::Server).size(IconSize::XSmall)) + .child(Icon::new(IconName::Server).size(IconSize::Small)) .child( h_flex() .gap_1() diff --git a/crates/repl/src/components/kernel_options.rs b/crates/repl/src/components/kernel_options.rs index 0623fd7ea5..cd73783b4c 100644 --- a/crates/repl/src/components/kernel_options.rs +++ b/crates/repl/src/components/kernel_options.rs @@ -235,8 +235,8 @@ impl PickerDelegate for KernelPickerDelegate { .gap_4() .child( Button::new("kernel-docs", "Kernel Docs") - .icon(IconName::ExternalLink) - .icon_size(IconSize::XSmall) + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::Small) .icon_color(Color::Muted) .icon_position(IconPosition::End) .on_click(move |_, _, cx| cx.open_url(KERNEL_DOCS_URL)), diff --git a/crates/repl/src/notebook/cell.rs b/crates/repl/src/notebook/cell.rs index 18851417c0..15179a632c 100644 --- a/crates/repl/src/notebook/cell.rs +++ b/crates/repl/src/notebook/cell.rs @@ -38,7 +38,7 @@ pub enum CellControlType { impl CellControlType { fn icon_name(&self) -> IconName { match self { - CellControlType::RunCell => IconName::PlayOutlined, + CellControlType::RunCell => IconName::PlayFilled, CellControlType::RerunCell => IconName::ArrowCircle, CellControlType::ClearCell => IconName::ListX, CellControlType::CellOptions => IconName::Ellipsis, diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index 2efa51e0cc..b53809dff0 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -321,7 +321,7 @@ impl NotebookEditor { .child( Self::render_notebook_control( "run-all-cells", - IconName::PlayOutlined, + IconName::PlayFilled, window, cx, ) diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs index e13e569c2a..ed252b239f 100644 --- a/crates/repl/src/outputs.rs +++ b/crates/repl/src/outputs.rs @@ -163,7 +163,7 @@ impl Output { el.child( IconButton::new( ElementId::Name("open-in-buffer".into()), - IconName::FileText, + IconName::FileTextOutlined, ) .style(ButtonStyle::Transparent) .tooltip(Tooltip::text("Open in Buffer")) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 5d77a95027..14703be7a2 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -336,7 +336,7 @@ impl Render for BufferSearchBar { this.child( IconButton::new( "buffer-search-bar-toggle-search-selection-button", - IconName::SearchSelection, + IconName::Quote, ) .style(ButtonStyle::Subtle) .shape(IconButtonShape::Square) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 15c1099aec..96194cdad2 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -2268,7 +2268,7 @@ impl Render for ProjectSearchBar { .min_w_64() .gap_1() .child( - IconButton::new("project-search-opened-only", IconName::FileSearch) + IconButton::new("project-search-opened-only", IconName::FolderSearch) .shape(IconButtonShape::Square) .toggle_state(self.is_opened_only_enabled(cx)) .tooltip(Tooltip::text("Only Search Open Files")) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 599bb0b18f..a62c669488 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -2473,7 +2473,7 @@ impl Render for KeybindingEditorModal { .label_size(LabelSize::Small) .icon(IconName::ArrowUpRight) .icon_color(Color::Muted) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .on_click(cx.listener( |this, _, window, cx| { this.show_matching_bindings( diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/settings_ui/src/ui_components/keystroke_input.rs index ee5c4036ea..f23d80931c 100644 --- a/crates/settings_ui/src/ui_components/keystroke_input.rs +++ b/crates/settings_ui/src/ui_components/keystroke_input.rs @@ -590,7 +590,7 @@ impl Render for KeystrokeInput { .map(|this| { if is_recording { this.child( - IconButton::new("stop-record-btn", IconName::StopFilled) + IconButton::new("stop-record-btn", IconName::Stop) .shape(IconButtonShape::Square) .map(|this| { this.tooltip(Tooltip::for_action_title( @@ -629,7 +629,7 @@ impl Render for KeystrokeInput { } }) .child( - IconButton::new("clear-btn", IconName::Delete) + IconButton::new("clear-btn", IconName::Backspace) .shape(IconButtonShape::Square) .tooltip(Tooltip::for_action_title( "Clear Keystrokes", diff --git a/crates/snippets_ui/src/snippets_ui.rs b/crates/snippets_ui/src/snippets_ui.rs index a8710d1672..bf0ef63bff 100644 --- a/crates/snippets_ui/src/snippets_ui.rs +++ b/crates/snippets_ui/src/snippets_ui.rs @@ -330,7 +330,7 @@ impl PickerDelegate for ScopeSelectorDelegate { .and_then(|available_language| self.scope_icon(available_language.matcher(), cx)) .or_else(|| { Some( - Icon::from_path(IconName::Globe.path()) + Icon::from_path(IconName::ToolWeb.path()) .map(|icon| icon.color(Color::Muted)), ) }) diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 361cdd0b1c..0c05aec85e 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1591,7 +1591,7 @@ impl Item for TerminalView { let (icon, icon_color, rerun_button) = match terminal.task() { Some(terminal_task) => match &terminal_task.status { TaskStatus::Running => ( - IconName::PlayOutlined, + IconName::PlayFilled, Color::Disabled, TerminalView::rerun_button(&terminal_task), ), diff --git a/crates/theme_selector/src/icon_theme_selector.rs b/crates/theme_selector/src/icon_theme_selector.rs index 2d0b9480d5..af7abdee62 100644 --- a/crates/theme_selector/src/icon_theme_selector.rs +++ b/crates/theme_selector/src/icon_theme_selector.rs @@ -318,7 +318,7 @@ impl PickerDelegate for IconThemeSelectorDelegate { Button::new("docs", "View Icon Theme Docs") .icon(IconName::ArrowUpRight) .icon_position(IconPosition::End) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .icon_color(Color::Muted) .on_click(|_event, _window, cx| { cx.open_url("https://zed.dev/docs/icon-themes"); diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index ba8bde243b..8c48f295dd 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -376,7 +376,7 @@ impl PickerDelegate for ThemeSelectorDelegate { Button::new("docs", "View Theme Docs") .icon(IconName::ArrowUpRight) .icon_position(IconPosition::End) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .icon_color(Color::Muted) .on_click(cx.listener(|_, _, _, cx| { cx.open_url("https://zed.dev/docs/themes"); diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index d026b4de14..74d60a6d66 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -518,7 +518,7 @@ impl TitleBar { .mx_neg_0p5() .h_full() .justify_center() - .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)), + .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)), ) .toggle_state(self.screen_share_popover_handle.is_deployed()), ) diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index a8b16d881f..d11d3b7081 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -344,7 +344,7 @@ impl TitleBar { .child( IconWithIndicator::new( Icon::new(IconName::Server) - .size(IconSize::XSmall) + .size(IconSize::Small) .color(icon_color), Some(Indicator::dot().color(indicator_color)), ) diff --git a/crates/ui/src/components/banner.rs b/crates/ui/src/components/banner.rs index d88905d466..d493e8a0d3 100644 --- a/crates/ui/src/components/banner.rs +++ b/crates/ui/src/components/banner.rs @@ -24,7 +24,7 @@ pub enum Severity { /// .action_slot( /// Button::new("learn-more", "Learn More") /// .icon(IconName::ArrowUpRight) -/// .icon_size(IconSize::XSmall) +/// .icon_size(IconSize::Small) /// .icon_position(IconPosition::End), /// ) /// ``` @@ -150,7 +150,7 @@ impl Component for Banner { .action_slot( Button::new("learn-more", "Learn More") .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .icon_position(IconPosition::End), ) .into_any_element(), diff --git a/crates/ui/src/components/callout.rs b/crates/ui/src/components/callout.rs index 9c1c9fb1a9..abb03198ab 100644 --- a/crates/ui/src/components/callout.rs +++ b/crates/ui/src/components/callout.rs @@ -207,7 +207,7 @@ impl Component for Callout { "Error with Multiple Actions", Callout::new() .icon( - Icon::new(IconName::X) + Icon::new(IconName::Close) .color(Color::Error) .size(IconSize::Small), ) diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 21ab283d88..25575c4f1e 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -561,7 +561,7 @@ impl ContextMenu { action: Some(action.boxed_clone()), handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)), icon: Some(IconName::ArrowUpRight), - icon_size: IconSize::XSmall, + icon_size: IconSize::Small, icon_position: IconPosition::End, icon_color: None, disabled: false, diff --git a/crates/ui/src/components/indicator.rs b/crates/ui/src/components/indicator.rs index d319547bed..59d69a068b 100644 --- a/crates/ui/src/components/indicator.rs +++ b/crates/ui/src/components/indicator.rs @@ -164,7 +164,7 @@ impl Component for Indicator { ), single_example( "Error", - Indicator::icon(Icon::new(IconName::X)) + Indicator::icon(Icon::new(IconName::Close)) .color(Color::Error) .into_any_element(), ), diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index 5779093ccc..56be867796 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -188,7 +188,7 @@ fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option< "up" => Some(IconName::ArrowUp), "down" => Some(IconName::ArrowDown), "backspace" => Some(IconName::Backspace), - "delete" => Some(IconName::Delete), + "delete" => Some(IconName::Backspace), "return" => Some(IconName::Return), "enter" => Some(IconName::Return), "tab" => Some(IconName::Tab), diff --git a/crates/ui/src/components/stories/icon_button.rs b/crates/ui/src/components/stories/icon_button.rs index ad6886252d..166297eabc 100644 --- a/crates/ui/src/components/stories/icon_button.rs +++ b/crates/ui/src/components/stories/icon_button.rs @@ -90,7 +90,7 @@ impl Render for IconButtonStory { let selected_with_tooltip_button = StoryItem::new( "Selected with `tooltip`", - IconButton::new("selected_with_tooltip_button", IconName::InlayHint) + IconButton::new("selected_with_tooltip_button", IconName::CaseSensitive) .toggle_state(true) .tooltip(Tooltip::text("Toggle inlay hints")), ) diff --git a/crates/welcome/src/multibuffer_hint.rs b/crates/welcome/src/multibuffer_hint.rs index ea64cab9df..3a20cbb6bd 100644 --- a/crates/welcome/src/multibuffer_hint.rs +++ b/crates/welcome/src/multibuffer_hint.rs @@ -159,7 +159,7 @@ impl Render for MultibufferHint { .child( Button::new("open_docs", "Learn More") .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .icon_color(Color::Muted) .icon_position(IconPosition::End) .on_click(move |_event, _, cx| { diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index db75b544f6..ac889a7ad9 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/crates/zed/src/zed/component_preview.rs @@ -697,7 +697,7 @@ impl ComponentPreview { workspace.update(cx, |workspace, cx| { let status_toast = StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| { - this.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted)) + this.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)) .action("Open Pull Request", |_, cx| { cx.open_url("https://github.com/") }) diff --git a/crates/zed/src/zed/quick_action_bar/repl_menu.rs b/crates/zed/src/zed/quick_action_bar/repl_menu.rs index 12e5cf1b76..5d1a6c8887 100644 --- a/crates/zed/src/zed/quick_action_bar/repl_menu.rs +++ b/crates/zed/src/zed/quick_action_bar/repl_menu.rs @@ -212,7 +212,7 @@ impl QuickActionBar { .trigger_with_tooltip( ButtonLike::new_rounded_right(element_id("dropdown")) .child( - Icon::new(IconName::ChevronDownSmall) + Icon::new(IconName::ChevronDown) .size(IconSize::XSmall) .color(Color::Muted), ) diff --git a/crates/zeta/src/onboarding_modal.rs b/crates/zeta/src/onboarding_modal.rs index 1d59f36b05..c2886f2864 100644 --- a/crates/zeta/src/onboarding_modal.rs +++ b/crates/zeta/src/onboarding_modal.rs @@ -141,7 +141,7 @@ impl Render for ZedPredictModal { )), ) .child(h_flex().absolute().top_2().right_2().child( - IconButton::new("cancel", IconName::X).on_click(cx.listener( + IconButton::new("cancel", IconName::Close).on_click(cx.listener( |_, _: &ClickEvent, _window, cx| { onboarding_event!("Cancelled", trigger = "X click"); cx.emit(DismissEvent); From f3a58b50c4e44a088831033e0e1e791785a53298 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 8 Aug 2025 15:03:50 -0400 Subject: [PATCH 209/693] Handle drag and drop in new agent threads (#35879) This is a bit simpler than for the original agent thread view, since we don't have to deal with opening buffers or a context store. Release Notes: - N/A --- .../agent_ui/src/acp/completion_provider.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 57 ++++++++++++++++++- crates/agent_ui/src/agent_panel.rs | 6 +- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index fca4ae0300..d8f452afa5 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -59,7 +59,7 @@ impl ContextPickerCompletionProvider { } } - fn completion_for_path( + pub(crate) fn completion_for_path( project_path: ProjectPath, path_prefix: &str, is_recent: bool, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 74bbac2b1c..6411abb84f 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -30,7 +30,7 @@ use language::language_settings::SoftWrap; use language::{Buffer, Language}; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; use parking_lot::Mutex; -use project::Project; +use project::{CompletionIntent, Project}; use settings::{Settings as _, SettingsStore}; use text::{Anchor, BufferSnapshot}; use theme::ThemeSettings; @@ -2611,6 +2611,61 @@ impl AcpThreadView { }) } } + + pub(crate) fn insert_dragged_files( + &self, + paths: Vec, + _added_worktrees: Vec>, + window: &mut Window, + cx: &mut Context<'_, Self>, + ) { + let buffer = self.message_editor.read(cx).buffer().clone(); + let Some((&excerpt_id, _, _)) = buffer.read(cx).snapshot(cx).as_singleton() else { + return; + }; + let Some(buffer) = buffer.read(cx).as_singleton() else { + return; + }; + for path in paths { + let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else { + continue; + }; + let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else { + continue; + }; + + let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len())); + let path_prefix = abs_path + .file_name() + .unwrap_or(path.path.as_os_str()) + .display() + .to_string(); + let completion = ContextPickerCompletionProvider::completion_for_path( + path, + &path_prefix, + false, + entry.is_dir(), + excerpt_id, + anchor..anchor, + self.message_editor.clone(), + self.mention_set.clone(), + cx, + ); + + self.message_editor.update(cx, |message_editor, cx| { + message_editor.edit( + [( + multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), + completion.new_text, + )], + cx, + ); + }); + if let Some(confirm) = completion.confirm.clone() { + confirm(CompletionIntent::Complete, window, cx); + } + } + } } impl Focusable for AcpThreadView { diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 6b8e36066b..87e4dd822c 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -3187,8 +3187,10 @@ impl AgentPanel { .detach(); }); } - ActiveView::ExternalAgentThread { .. } => { - unimplemented!() + ActiveView::ExternalAgentThread { thread_view } => { + thread_view.update(cx, |thread_view, cx| { + thread_view.insert_dragged_files(paths, added_worktrees, window, cx); + }); } ActiveView::TextThread { context_editor, .. } => { context_editor.update(cx, |context_editor, cx| { From b77a15d53a881d1c08df15e3f881cc56c67d91d5 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Fri, 8 Aug 2025 15:20:10 -0400 Subject: [PATCH 210/693] ci: Use faster Linux ARM runners (#35880) Switch our Linux aarch_64 release builds from Linux on Graviton (32 vCPU, 64GB) to Linux running on Apple M4 Pro (8vCPU, 32GB). Builds are faster (20mins vs 30mins) for the same cost (960 unit minutes; ~$0.96/ea). Screenshot 2025-08-08 at 13 14 41 Release Notes: - N/A --- .github/actionlint.yml | 3 +++ .github/workflows/ci.yml | 16 +++++++--------- .github/workflows/release_nightly.yml | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/actionlint.yml b/.github/actionlint.yml index ad09545902..0ee6af8a1d 100644 --- a/.github/actionlint.yml +++ b/.github/actionlint.yml @@ -24,6 +24,9 @@ self-hosted-runner: - namespace-profile-8x16-ubuntu-2204 - namespace-profile-16x32-ubuntu-2204 - namespace-profile-32x64-ubuntu-2204 + # Namespace Limited Preview + - namespace-profile-8x16-ubuntu-2004-arm-m4 + - namespace-profile-8x32-ubuntu-2004-arm-m4 # Self Hosted Runners - self-mini-macos - self-32vcpu-windows-2022 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84907351fe..02edc99824 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -511,8 +511,8 @@ jobs: runs-on: - self-mini-macos if: | - startsWith(github.ref, 'refs/tags/v') - || contains(github.event.pull_request.labels.*.name, 'run-bundling') + ( startsWith(github.ref, 'refs/tags/v') + || contains(github.event.pull_request.labels.*.name, 'run-bundling') ) needs: [macos_tests] env: MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} @@ -599,8 +599,8 @@ jobs: runs-on: - namespace-profile-16x32-ubuntu-2004 # ubuntu 20.04 for minimal glibc if: | - startsWith(github.ref, 'refs/tags/v') - || contains(github.event.pull_request.labels.*.name, 'run-bundling') + ( startsWith(github.ref, 'refs/tags/v') + || contains(github.event.pull_request.labels.*.name, 'run-bundling') ) needs: [linux_tests] steps: - name: Checkout repo @@ -650,7 +650,7 @@ jobs: timeout-minutes: 60 name: Linux arm64 release bundle runs-on: - - namespace-profile-32x64-ubuntu-2004-arm # ubuntu 20.04 for minimal glibc + - namespace-profile-8x32-ubuntu-2004-arm-m4 # ubuntu 20.04 for minimal glibc if: | startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') @@ -703,10 +703,8 @@ jobs: timeout-minutes: 60 runs-on: github-8vcpu-ubuntu-2404 if: | - false && ( - startsWith(github.ref, 'refs/tags/v') - || contains(github.event.pull_request.labels.*.name, 'run-bundling') - ) + ( startsWith(github.ref, 'refs/tags/v') + || contains(github.event.pull_request.labels.*.name, 'run-bundling') ) needs: [linux_tests] name: Build Zed on FreeBSD steps: diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index b3500a085b..ed9f4c8450 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -168,7 +168,7 @@ jobs: name: Create a Linux *.tar.gz bundle for ARM if: github.repository_owner == 'zed-industries' runs-on: - - namespace-profile-32x64-ubuntu-2004-arm # ubuntu 20.04 for minimal glibc + - namespace-profile-8x32-ubuntu-2004-arm-m4 # ubuntu 20.04 for minimal glibc needs: tests steps: - name: Checkout repo From 024a5bbcd0f40dc7c9c762a207ef49964b0ec8b4 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 8 Aug 2025 16:40:41 -0300 Subject: [PATCH 211/693] onboarding: Add some adjustments (#35887) Release Notes: - N/A --- assets/keymaps/default-linux.json | 3 +- assets/keymaps/default-macos.json | 3 +- crates/client/src/zed_urls.rs | 8 +++ crates/onboarding/src/ai_setup_page.rs | 97 +++++++++++--------------- crates/onboarding/src/editing_page.rs | 2 +- crates/onboarding/src/onboarding.rs | 50 +++++++++++-- 6 files changed, 97 insertions(+), 66 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index c436b1a8fb..708432393c 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1187,7 +1187,8 @@ "ctrl-2": "onboarding::ActivateEditingPage", "ctrl-3": "onboarding::ActivateAISetupPage", "ctrl-escape": "onboarding::Finish", - "alt-tab": "onboarding::SignIn" + "alt-tab": "onboarding::SignIn", + "alt-shift-a": "onboarding::OpenAccount" } } ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 960bac1479..abb741af29 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1289,7 +1289,8 @@ "cmd-2": "onboarding::ActivateEditingPage", "cmd-3": "onboarding::ActivateAISetupPage", "cmd-escape": "onboarding::Finish", - "alt-tab": "onboarding::SignIn" + "alt-tab": "onboarding::SignIn", + "alt-shift-a": "onboarding::OpenAccount" } } ] diff --git a/crates/client/src/zed_urls.rs b/crates/client/src/zed_urls.rs index 693c7bf836..9df41906d7 100644 --- a/crates/client/src/zed_urls.rs +++ b/crates/client/src/zed_urls.rs @@ -35,3 +35,11 @@ pub fn upgrade_to_zed_pro_url(cx: &App) -> String { pub fn terms_of_service(cx: &App) -> String { format!("{server_url}/terms-of-service", server_url = server_url(cx)) } + +/// Returns the URL to Zed AI's privacy and security docs. +pub fn ai_privacy_and_security(cx: &App) -> String { + format!( + "{server_url}/docs/ai/privacy-and-security", + server_url = server_url(cx) + ) +} diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index 0397bcbd9b..8203f96479 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use ai_onboarding::AiUpsellCard; -use client::{Client, UserStore}; +use client::{Client, UserStore, zed_urls}; use fs::Fs; use gpui::{ Action, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, @@ -42,10 +42,16 @@ fn render_llm_provider_section( } fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> impl IntoElement { - let privacy_badge = || { - Badge::new("Privacy") - .icon(IconName::ShieldCheck) - .tooltip(move |_, cx| cx.new(|_| AiPrivacyTooltip::new()).into()) + let (title, description) = if disabled { + ( + "AI is disabled across Zed", + "Re-enable it any time in Settings.", + ) + } else { + ( + "Privacy is the default for Zed", + "Any use or storage of your data is with your explicit, single-use, opt-in consent.", + ) }; v_flex() @@ -60,62 +66,41 @@ fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> i .bg(cx.theme().colors().surface_background.opacity(0.3)) .rounded_lg() .overflow_hidden() - .map(|this| { - if disabled { - this.child( + .child( + h_flex() + .gap_2() + .justify_between() + .child(Label::new(title)) + .child( h_flex() - .gap_2() - .justify_between() + .gap_1() .child( - h_flex() - .gap_1() - .child(Label::new("AI is disabled across Zed")) - .child( - Icon::new(IconName::Check) - .color(Color::Success) - .size(IconSize::XSmall), - ), + Badge::new("Privacy") + .icon(IconName::ShieldCheck) + .tooltip(move |_, cx| cx.new(|_| AiPrivacyTooltip::new()).into()), ) - .child(privacy_badge()), - ) - .child( - Label::new("Re-enable it any time in Settings.") - .size(LabelSize::Small) - .color(Color::Muted), - ) - } else { - this.child( - h_flex() - .gap_2() - .justify_between() - .child(Label::new("Privacy is the default for Zed")) .child( - h_flex().gap_1().child(privacy_badge()).child( - Button::new("learn_more", "Learn More") - .style(ButtonStyle::Outlined) - .label_size(LabelSize::Small) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .on_click(|_, _, cx| { - cx.open_url("https://zed.dev/docs/ai/privacy-and-security"); - }) - .tab_index({ - *tab_index += 1; - *tab_index - 1 - }), - ), + Button::new("learn_more", "Learn More") + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .on_click(|_, _, cx| { + cx.open_url(&zed_urls::ai_privacy_and_security(cx)) + }) + .tab_index({ + *tab_index += 1; + *tab_index - 1 + }), ), - ) - .child( - Label::new( - "Any use or storage of your data is with your explicit, single-use, opt-in consent.", - ) - .size(LabelSize::Small) - .color(Color::Muted), - ) - } - }) + ), + ) + .child( + Label::new(description) + .size(LabelSize::Small) + .color(Color::Muted), + ) } fn render_llm_provider_card( diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 8b4293db0d..13b4f6a5c1 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -655,7 +655,7 @@ fn render_popular_settings_section( .child( SwitchField::new( "onboarding-git-blame-switch", - "Git Blame", + "Inline Git Blame", Some("See who committed each line on a given file.".into()), if read_git_blame(cx) { ui::ToggleState::Selected diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 145cb07a1c..7ba7ba60cb 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -1,5 +1,5 @@ use crate::welcome::{ShowWelcome, WelcomePage}; -use client::{Client, UserStore}; +use client::{Client, UserStore, zed_urls}; use command_palette_hooks::CommandPaletteFilter; use db::kvp::KEY_VALUE_STORE; use feature_flags::{FeatureFlag, FeatureFlagViewExt as _}; @@ -78,7 +78,9 @@ actions!( /// Finish the onboarding process. Finish, /// Sign in while in the onboarding flow. - SignIn + SignIn, + /// Open the user account in zed.dev while in the onboarding flow. + OpenAccount ] ); @@ -420,11 +422,40 @@ impl Onboarding { ) .child( if let Some(user) = self.user_store.read(cx).current_user() { - h_flex() - .pl_1p5() - .gap_2() - .child(Avatar::new(user.avatar_uri.clone())) - .child(Label::new(user.github_login.clone())) + v_flex() + .gap_1() + .child( + h_flex() + .ml_2() + .gap_2() + .max_w_full() + .w_full() + .child(Avatar::new(user.avatar_uri.clone())) + .child(Label::new(user.github_login.clone()).truncate()), + ) + .child( + ButtonLike::new("open_account") + .size(ButtonSize::Medium) + .child( + h_flex() + .ml_1() + .w_full() + .justify_between() + .child(Label::new("Open Account")) + .children( + KeyBinding::for_action_in( + &OpenAccount, + &self.focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(12.))), + ), + ) + .on_click(|_, window, cx| { + window.dispatch_action(OpenAccount.boxed_clone(), cx); + }), + ) .into_any_element() } else { Button::new("sign_in", "Sign In") @@ -460,6 +491,10 @@ impl Onboarding { .detach(); } + fn handle_open_account(_: &OpenAccount, _: &mut Window, cx: &mut App) { + cx.open_url(&zed_urls::account_url(cx)) + } + fn render_page(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { let client = Client::global(cx); @@ -495,6 +530,7 @@ impl Render for Onboarding { .bg(cx.theme().colors().editor_background) .on_action(Self::on_finish) .on_action(Self::handle_sign_in) + .on_action(Self::handle_open_account) .on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| { this.set_page(SelectedPage::Basics, cx); })) From e0fc32009f061f3c767effa0936c7da83086edcd Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 8 Aug 2025 23:12:41 +0300 Subject: [PATCH 212/693] Fill capabilities on project (re)join (#35892) Follow-up of https://github.com/zed-industries/zed/pull/35682 Release Notes: - N/A Co-authored-by: Smit Barmase --- crates/project/src/lsp_store.rs | 11 +++++++++-- crates/project/src/project.rs | 10 ++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index b88cf42ff5..d3843bc4ea 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -7768,12 +7768,19 @@ impl LspStore { pub(crate) fn set_language_server_statuses_from_proto( &mut self, language_servers: Vec, + server_capabilities: Vec, ) { self.language_server_statuses = language_servers .into_iter() - .map(|server| { + .zip(server_capabilities) + .map(|(server, server_capabilities)| { + let server_id = LanguageServerId(server.id as usize); + if let Ok(server_capabilities) = serde_json::from_str(&server_capabilities) { + self.lsp_server_capabilities + .insert(server_id, server_capabilities); + } ( - LanguageServerId(server.id as usize), + server_id, LanguageServerStatus { name: LanguageServerName::from_proto(server.name), pending_work: Default::default(), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b3a9e6fdf5..d543e6bf25 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1487,7 +1487,10 @@ impl Project { fs.clone(), cx, ); - lsp_store.set_language_server_statuses_from_proto(response.payload.language_servers); + lsp_store.set_language_server_statuses_from_proto( + response.payload.language_servers, + response.payload.language_server_capabilities, + ); lsp_store })?; @@ -2318,7 +2321,10 @@ impl Project { self.set_worktrees_from_proto(message.worktrees, cx)?; self.set_collaborators_from_proto(message.collaborators, cx)?; self.lsp_store.update(cx, |lsp_store, _| { - lsp_store.set_language_server_statuses_from_proto(message.language_servers) + lsp_store.set_language_server_statuses_from_proto( + message.language_servers, + message.language_server_capabilities, + ) }); self.enqueue_buffer_ordered_message(BufferOrderedMessage::Resync) .unwrap(); From fd1beedb16fcc5444e4389d412df78691fe8150e Mon Sep 17 00:00:00 2001 From: ddoemonn <109994179+ddoemonn@users.noreply.github.com> Date: Fri, 8 Aug 2025 23:46:31 +0300 Subject: [PATCH 213/693] Prevent scrollbar from covering bottom right text in terminal (#33636) Closes https://github.com/zed-industries/zed/issues/27241 Release Notes: - Fixed terminal scrollbar covering bottom right text by adding proper content padding when scrollbar is visible --------- Co-authored-by: Danilo Leal --- crates/terminal_view/src/terminal_view.rs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 0c05aec85e..0ec5f816d5 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -64,8 +64,8 @@ use std::{ }; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); - const GIT_DIFF_PATH_PREFIXES: &[&str] = &["a", "b"]; +const TERMINAL_SCROLLBAR_WIDTH: Pixels = px(12.); /// Event to transmit the scroll from the element to the view #[derive(Clone, Debug, PartialEq)] @@ -956,13 +956,12 @@ impl TerminalView { .on_scroll_wheel(cx.listener(|_, _, _window, cx| { cx.notify(); })) - .h_full() .absolute() - .right_1() - .top_1() + .top_0() .bottom_0() - .w(px(12.)) - .cursor_default() + .right_0() + .h_full() + .w(TERMINAL_SCROLLBAR_WIDTH) .children(Scrollbar::vertical(self.scrollbar_state.clone())), ) } @@ -1496,6 +1495,16 @@ impl Render for TerminalView { let focused = self.focus_handle.is_focused(window); + // Always calculate scrollbar width to prevent layout shift + let scrollbar_width = if Self::should_show_scrollbar(cx) + && self.content_mode(window, cx).is_scrollable() + && self.terminal.read(cx).total_lines() > self.terminal.read(cx).viewport_lines() + { + TERMINAL_SCROLLBAR_WIDTH + } else { + px(0.) + }; + div() .id("terminal-view") .size_full() @@ -1545,6 +1554,8 @@ impl Render for TerminalView { // TODO: Oddly this wrapper div is needed for TerminalElement to not steal events from the context menu div() .size_full() + .bg(cx.theme().colors().editor_background) + .when(scrollbar_width > px(0.), |div| div.pr(scrollbar_width)) .child(TerminalElement::new( terminal_handle, terminal_view_handle, From 91474e247f188a761a4f1adba9fead7f1b54cb96 Mon Sep 17 00:00:00 2001 From: Daniel Sauble Date: Fri, 8 Aug 2025 14:04:32 -0700 Subject: [PATCH 214/693] Make close tab and pin tab buttons slightly larger for better usability (#34428) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #6817 Increases the size of tab buttons from 16px to 18px so they're easier to click. For comparison, tab buttons in VSCode have a click target size of 20px, so we're still a bit smaller than that. Before: before_tab_buttons After: after_tab_buttons VSCode (for comparison): Screenshot 2025-07-14 at 1 43 03 PM Release Notes: - Improve usability of close tab and pin tab buttons by making them slightly larger --------- Co-authored-by: Danilo Leal --- crates/ui/src/components/tab.rs | 7 +++++-- crates/workspace/src/pane.rs | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/ui/src/components/tab.rs b/crates/ui/src/components/tab.rs index d704846a68..e6823f46b7 100644 --- a/crates/ui/src/components/tab.rs +++ b/crates/ui/src/components/tab.rs @@ -5,6 +5,9 @@ use smallvec::SmallVec; use crate::prelude::*; +const START_TAB_SLOT_SIZE: Pixels = px(12.); +const END_TAB_SLOT_SIZE: Pixels = px(14.); + /// The position of a [`Tab`] within a list of tabs. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum TabPosition { @@ -123,12 +126,12 @@ impl RenderOnce for Tab { let (start_slot, end_slot) = { let start_slot = h_flex() - .size(px(12.)) // use px over rem from size_3 + .size(START_TAB_SLOT_SIZE) .justify_center() .children(self.start_slot); let end_slot = h_flex() - .size(px(12.)) // use px over rem from size_3 + .size(END_TAB_SLOT_SIZE) .justify_center() .children(self.end_slot); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index a9e7304e47..0c35752165 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2519,7 +2519,7 @@ impl Pane { .shape(IconButtonShape::Square) .icon_color(Color::Muted) .size(ButtonSize::None) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .on_click(cx.listener(move |pane, _, window, cx| { pane.unpin_tab_at(ix, window, cx); })) @@ -2539,7 +2539,7 @@ impl Pane { .shape(IconButtonShape::Square) .icon_color(Color::Muted) .size(ButtonSize::None) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .on_click(cx.listener(move |pane, _, window, cx| { pane.close_item_by_id(item_id, SaveIntent::Close, window, cx) .detach_and_log_err(cx); From c6ef35ba378cce9c3d8801ce8aede114dfd90179 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Fri, 8 Aug 2025 17:05:28 -0400 Subject: [PATCH 215/693] Disable edit predictions in Zed settings by default (#34401) In Zed settings, json schema based LSP autocomplete is very good, edit predictions are not. Disable the latter by default. Release Notes: - N/A --- assets/settings/default.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 9c579b858d..28cf591ee7 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1210,7 +1210,18 @@ // Any addition to this list will be merged with the default list. // Globs are matched relative to the worktree root, // except when starting with a slash (/) or equivalent in Windows. - "disabled_globs": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/.dev.vars", "**/secrets.yml"], + "disabled_globs": [ + "**/.env*", + "**/*.pem", + "**/*.key", + "**/*.cert", + "**/*.crt", + "**/.dev.vars", + "**/secrets.yml", + "**/.zed/settings.json", // zed project settings + "/**/zed/settings.json", // zed user settings + "/**/zed/keymap.json" + ], // When to show edit predictions previews in buffer. // This setting takes two possible values: // 1. Display predictions inline when there are no language server completions available. From 2be6f9d17bfdc0687f0b227139f74bbbe7028897 Mon Sep 17 00:00:00 2001 From: Aleksei Gusev Date: Sat, 9 Aug 2025 00:17:19 +0300 Subject: [PATCH 216/693] theme: Add support for per-theme overrides (#30860) Closes #14050 Release Notes: - Added the ability to set theme-specific overrides via the `theme_overrides` setting. --------- Co-authored-by: Peter Tripp Co-authored-by: Marshall Bowers --- crates/theme/src/settings.rs | 73 +++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 6d19494f40..f5f1fd5547 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -4,6 +4,7 @@ use crate::{ ThemeNotFoundError, ThemeRegistry, ThemeStyleContent, }; use anyhow::Result; +use collections::HashMap; use derive_more::{Deref, DerefMut}; use gpui::{ App, Context, Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, Global, Pixels, @@ -117,7 +118,9 @@ pub struct ThemeSettings { /// Manual overrides for the active theme. /// /// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078) - pub theme_overrides: Option, + pub experimental_theme_overrides: Option, + /// Manual overrides per theme + pub theme_overrides: HashMap, /// The current icon theme selection. pub icon_theme_selection: Option, /// The active icon theme. @@ -425,7 +428,13 @@ pub struct ThemeSettingsContent { /// /// These values will override the ones on the current theme specified in `theme`. #[serde(rename = "experimental.theme_overrides", default)] - pub theme_overrides: Option, + pub experimental_theme_overrides: Option, + + /// Overrides per theme + /// + /// These values will override the ones on the specified theme + #[serde(default)] + pub theme_overrides: HashMap, } fn default_font_features() -> Option { @@ -647,30 +656,39 @@ impl ThemeSettings { /// Applies the theme overrides, if there are any, to the current theme. pub fn apply_theme_overrides(&mut self) { - if let Some(theme_overrides) = &self.theme_overrides { - let mut base_theme = (*self.active_theme).clone(); - - if let Some(window_background_appearance) = theme_overrides.window_background_appearance - { - base_theme.styles.window_background_appearance = - window_background_appearance.into(); - } - - base_theme - .styles - .colors - .refine(&theme_overrides.theme_colors_refinement()); - base_theme - .styles - .status - .refine(&theme_overrides.status_colors_refinement()); - base_theme.styles.player.merge(&theme_overrides.players); - base_theme.styles.accents.merge(&theme_overrides.accents); - base_theme.styles.syntax = - SyntaxTheme::merge(base_theme.styles.syntax, theme_overrides.syntax_overrides()); - - self.active_theme = Arc::new(base_theme); + // Apply the old overrides setting first, so that the new setting can override those. + if let Some(experimental_theme_overrides) = &self.experimental_theme_overrides { + let mut theme = (*self.active_theme).clone(); + ThemeSettings::modify_theme(&mut theme, experimental_theme_overrides); + self.active_theme = Arc::new(theme); } + + if let Some(theme_overrides) = self.theme_overrides.get(self.active_theme.name.as_ref()) { + let mut theme = (*self.active_theme).clone(); + ThemeSettings::modify_theme(&mut theme, theme_overrides); + self.active_theme = Arc::new(theme); + } + } + + fn modify_theme(base_theme: &mut Theme, theme_overrides: &ThemeStyleContent) { + if let Some(window_background_appearance) = theme_overrides.window_background_appearance { + base_theme.styles.window_background_appearance = window_background_appearance.into(); + } + + base_theme + .styles + .colors + .refine(&theme_overrides.theme_colors_refinement()); + base_theme + .styles + .status + .refine(&theme_overrides.status_colors_refinement()); + base_theme.styles.player.merge(&theme_overrides.players); + base_theme.styles.accents.merge(&theme_overrides.accents); + base_theme.styles.syntax = SyntaxTheme::merge( + base_theme.styles.syntax.clone(), + theme_overrides.syntax_overrides(), + ); } /// Switches to the icon theme with the given name, if it exists. @@ -848,7 +866,8 @@ impl settings::Settings for ThemeSettings { .get(defaults.theme.as_ref().unwrap().theme(*system_appearance)) .or(themes.get(&zed_default_dark().name)) .unwrap(), - theme_overrides: None, + experimental_theme_overrides: None, + theme_overrides: HashMap::default(), icon_theme_selection: defaults.icon_theme.clone(), active_icon_theme: defaults .icon_theme @@ -918,6 +937,8 @@ impl settings::Settings for ThemeSettings { } } + this.experimental_theme_overrides + .clone_from(&value.experimental_theme_overrides); this.theme_overrides.clone_from(&value.theme_overrides); this.apply_theme_overrides(); From f3399daf6c7d48adf9c07e3d9df3349495c8c0f0 Mon Sep 17 00:00:00 2001 From: Alvaro Parker <64918109+AlvaroParker@users.noreply.github.com> Date: Fri, 8 Aug 2025 17:32:13 -0400 Subject: [PATCH 217/693] file_finder: Fix right border not rendering (#35684) Closes #35683 Release Notes: - Fixed file finder borders not rendering properly Before: image After: image --- crates/file_finder/src/file_finder.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index e5ac70bb58..c6997ccdc0 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -17,7 +17,7 @@ use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, WeakEntity, - Window, actions, + Window, actions, rems, }; use open_path_prompt::OpenPathPrompt; use picker::{Picker, PickerDelegate}; @@ -350,7 +350,7 @@ impl FileFinder { pub fn modal_max_width(width_setting: Option, window: &mut Window) -> Pixels { let window_width = window.viewport_size().width; - let small_width = Pixels(545.); + let small_width = rems(34.).to_pixels(window.rem_size()); match width_setting { None | Some(FileFinderWidth::Small) => small_width, From d7db03443a4a27e24c95be106e6ce10624c01f68 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Fri, 8 Aug 2025 16:32:36 -0500 Subject: [PATCH 218/693] Upload debug info for preview/stable builds (#35895) This should fix all the unsymbolicated backtraces we're seeing on preview builds Release Notes: - N/A --- .github/workflows/ci.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02edc99824..928c47a4a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -526,6 +526,11 @@ jobs: with: node-version: "18" + - name: Setup Sentry CLI + uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2 + with: + token: ${{ SECRETS.SENTRY_AUTH_TOKEN }} + - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: @@ -611,6 +616,11 @@ jobs: - name: Install Linux dependencies run: ./script/linux && ./script/install-mold 2.34.0 + - name: Setup Sentry CLI + uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2 + with: + token: ${{ SECRETS.SENTRY_AUTH_TOKEN }} + - name: Determine version and release channel if: startsWith(github.ref, 'refs/tags/v') run: | @@ -664,6 +674,11 @@ jobs: - name: Install Linux dependencies run: ./script/linux + - name: Setup Sentry CLI + uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2 + with: + token: ${{ SECRETS.SENTRY_AUTH_TOKEN }} + - name: Determine version and release channel if: startsWith(github.ref, 'refs/tags/v') run: | @@ -789,6 +804,11 @@ jobs: with: clean: false + - name: Setup Sentry CLI + uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2 + with: + token: ${{ SECRETS.SENTRY_AUTH_TOKEN }} + - name: Determine version and release channel working-directory: ${{ env.ZED_WORKSPACE }} if: ${{ startsWith(github.ref, 'refs/tags/v') }} From a4f7747c7382e2116ed2f29ffefd028d3cd043f0 Mon Sep 17 00:00:00 2001 From: Phileas Lebada Date: Fri, 8 Aug 2025 22:44:03 +0100 Subject: [PATCH 219/693] Improve extension development docs (#33646) I'm installing an extension for the first time from source and assumed that the sentence > If you already have a published extension with the same name installed, your dev extension will override it. also means that it would override the already installed extension. Besides that I've had to use `--foreground` mode to also get more meaningful error messages under NixOS without using `programs.nix-ld.enabled = true;`. Release Notes: - Improved Zed documentation for extension development --------- Co-authored-by: Peter Tripp --- docs/src/extensions/developing-extensions.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/src/extensions/developing-extensions.md b/docs/src/extensions/developing-extensions.md index 97af1f2673..947956f5b7 100644 --- a/docs/src/extensions/developing-extensions.md +++ b/docs/src/extensions/developing-extensions.md @@ -19,10 +19,16 @@ Before starting to develop an extension for Zed, be sure to [install Rust via ru When developing an extension, you can use it in Zed without needing to publish it by installing it as a _dev extension_. -From the extensions page, click the `Install Dev Extension` button and select the directory containing your extension. +From the extensions page, click the `Install Dev Extension` button (or the {#action zed::InstallDevExtension} action) and select the directory containing your extension. + +If you need to troubleshoot, you can check the Zed.log ({#action zed::OpenLog}) for additional output. For debug output, close and relaunch zed with the `zed --foreground` from the command line which show more verbose INFO level logging. If you already have a published extension with the same name installed, your dev extension will override it. +After installing the `Extensions` page will indicate that that the upstream extension is "Overridden by dev extension". + +Pre-installed extensions with the same name have to be uninstalled before installing the dev extension. See [#31106](https://github.com/zed-industries/zed/issues/31106) for more. + ## Directory Structure of a Zed Extension A Zed extension is a Git repository that contains an `extension.toml`. This file must contain some From a1bc6ee75e9371121caac1b115518cc1396085a3 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Fri, 8 Aug 2025 16:16:13 -0600 Subject: [PATCH 220/693] zeta: Only send outline and diagnostics when data collection is enabled (#35896) This data is not currently used by edit predictions - it is only useful when `can_collect_data == true`. Release Notes: - N/A --- crates/zeta/src/zeta.rs | 8 ++++++-- crates/zeta_cli/src/main.rs | 38 +++++++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index b1bd737dbf..828310a3bd 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -1211,7 +1211,7 @@ pub fn gather_context( let local_lsp_store = project.and_then(|project| project.read(cx).lsp_store().read(cx).as_local()); let diagnostic_groups: Vec<(String, serde_json::Value)> = - if let Some(local_lsp_store) = local_lsp_store { + if can_collect_data && let Some(local_lsp_store) = local_lsp_store { snapshot .diagnostic_groups(None) .into_iter() @@ -1245,7 +1245,11 @@ pub fn gather_context( MAX_CONTEXT_TOKENS, ); let input_events = make_events_prompt(); - let input_outline = prompt_for_outline(&snapshot); + let input_outline = if can_collect_data { + prompt_for_outline(&snapshot) + } else { + String::new() + }; let editable_range = input_excerpt.editable_range.to_offset(&snapshot); let body = PredictEditsBody { diff --git a/crates/zeta_cli/src/main.rs b/crates/zeta_cli/src/main.rs index adf7683152..d78035bc9d 100644 --- a/crates/zeta_cli/src/main.rs +++ b/crates/zeta_cli/src/main.rs @@ -171,21 +171,31 @@ async fn get_context( Some(events) => events.read_to_string().await?, None => String::new(), }; - let can_collect_data = false; + // Enable gathering extra data not currently needed for edit predictions + let can_collect_data = true; let git_info = None; - cx.update(|cx| { - gather_context( - project.as_ref(), - full_path_str, - &snapshot, - clipped_cursor, - move || events, - can_collect_data, - git_info, - cx, - ) - })? - .await + let mut gather_context_output = cx + .update(|cx| { + gather_context( + project.as_ref(), + full_path_str, + &snapshot, + clipped_cursor, + move || events, + can_collect_data, + git_info, + cx, + ) + })? + .await; + + // Disable data collection for these requests, as this is currently just used for evals + match gather_context_output.as_mut() { + Ok(gather_context_output) => gather_context_output.body.can_collect_data = false, + Err(_) => {} + } + + gather_context_output } pub async fn open_buffer_with_language_server( From 9443c930de078bc6ebdc7c82099b05816ec68aa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Sat, 9 Aug 2025 00:50:39 +0200 Subject: [PATCH 221/693] Make One Dark's `ansi.*magenta` colors more magenta-y (#35423) Tweak the `ansi.*magenta` colours so they are not confused with `ansi.*red`. This matches how "One Light" behaves, where `ansi.*magenta` uses the same purple as for keyword. This change helps distinguish anything that the terminal might use magenta for from errors, and helps make more readable the output of certain tools. For maintainers: The color for `ansi.magenta` is the same as for `syntax.keyword`. The others are modifications on that colour to taste. If you have some specific shades that need to be used please tell me, or feel free to take over the PR. Before: `jj log` and `difftastic` output Screenshot 2025-07-31 at 19 32 11 After: Screenshot 2025-07-31 at 19 35 33 Release Notes: - N/A --------- Co-authored-by: Danilo Leal --- assets/themes/one/one.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/themes/one/one.json b/assets/themes/one/one.json index 384ad28272..23ebbcc67e 100644 --- a/assets/themes/one/one.json +++ b/assets/themes/one/one.json @@ -86,9 +86,9 @@ "terminal.ansi.blue": "#74ade8ff", "terminal.ansi.bright_blue": "#385378ff", "terminal.ansi.dim_blue": "#bed5f4ff", - "terminal.ansi.magenta": "#be5046ff", - "terminal.ansi.bright_magenta": "#5e2b26ff", - "terminal.ansi.dim_magenta": "#e6a79eff", + "terminal.ansi.magenta": "#b477cfff", + "terminal.ansi.bright_magenta": "#d6b4e4ff", + "terminal.ansi.dim_magenta": "#612a79ff", "terminal.ansi.cyan": "#6eb4bfff", "terminal.ansi.bright_cyan": "#3a565bff", "terminal.ansi.dim_cyan": "#b9d9dfff", From aedf195e978cb2312d66249f1c5d798c2d138d72 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Fri, 8 Aug 2025 17:26:38 -0600 Subject: [PATCH 222/693] Use distinct user agents in agent eval and zeta-cli (#35897) Agent eval now also uses a proper Zed version Release Notes: - N/A --- crates/eval/build.rs | 14 ++++++++++++++ crates/eval/src/eval.rs | 4 ++-- crates/zeta_cli/build.rs | 2 +- crates/zeta_cli/src/headless.rs | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 crates/eval/build.rs diff --git a/crates/eval/build.rs b/crates/eval/build.rs new file mode 100644 index 0000000000..9ab40da0fb --- /dev/null +++ b/crates/eval/build.rs @@ -0,0 +1,14 @@ +fn main() { + let cargo_toml = + std::fs::read_to_string("../zed/Cargo.toml").expect("Failed to read crates/zed/Cargo.toml"); + let version = cargo_toml + .lines() + .find(|line| line.starts_with("version = ")) + .expect("Version not found in crates/zed/Cargo.toml") + .split('=') + .nth(1) + .expect("Invalid version format") + .trim() + .trim_matches('"'); + println!("cargo:rustc-env=ZED_PKG_VERSION={}", version); +} diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index d638ac171f..6558222d89 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -337,7 +337,7 @@ pub struct AgentAppState { } pub fn init(cx: &mut App) -> Arc { - let app_version = AppVersion::global(cx); + let app_version = AppVersion::load(env!("ZED_PKG_VERSION")); release_channel::init(app_version, cx); gpui_tokio::init(cx); @@ -350,7 +350,7 @@ pub fn init(cx: &mut App) -> Arc { // Set User-Agent so we can download language servers from GitHub let user_agent = format!( - "Zed/{} ({}; {})", + "Zed Agent Eval/{} ({}; {})", app_version, std::env::consts::OS, std::env::consts::ARCH diff --git a/crates/zeta_cli/build.rs b/crates/zeta_cli/build.rs index ccbb54c5b4..9ab40da0fb 100644 --- a/crates/zeta_cli/build.rs +++ b/crates/zeta_cli/build.rs @@ -1,6 +1,6 @@ fn main() { let cargo_toml = - std::fs::read_to_string("../zed/Cargo.toml").expect("Failed to read Cargo.toml"); + std::fs::read_to_string("../zed/Cargo.toml").expect("Failed to read crates/zed/Cargo.toml"); let version = cargo_toml .lines() .find(|line| line.starts_with("version = ")) diff --git a/crates/zeta_cli/src/headless.rs b/crates/zeta_cli/src/headless.rs index 959bb91a8f..d6ee085d18 100644 --- a/crates/zeta_cli/src/headless.rs +++ b/crates/zeta_cli/src/headless.rs @@ -40,7 +40,7 @@ pub fn init(cx: &mut App) -> ZetaCliAppState { // Set User-Agent so we can download language servers from GitHub let user_agent = format!( - "Zed/{} ({}; {})", + "Zeta CLI/{} ({}; {})", app_version, std::env::consts::OS, std::env::consts::ARCH From c0539230154356f948a858bc1ccf75af2b70b6e5 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 8 Aug 2025 19:50:59 -0400 Subject: [PATCH 223/693] thread_view: Trim only trailing whitespace from last chunk of user message (#35902) This fixes internal whitespace after the last @mention going missing from the user message as displayed in history. Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 6411abb84f..c811878c21 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -410,7 +410,7 @@ impl AcpThreadView { } if ix < text.len() { - let last_chunk = text[ix..].trim(); + let last_chunk = text[ix..].trim_end(); if !last_chunk.is_empty() { chunks.push(last_chunk.into()); } From 4e97968bcb6963ed4c474d12477e181899b4decc Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Fri, 8 Aug 2025 18:38:54 -0600 Subject: [PATCH 224/693] zeta: Update data collection eligibility when license file contents change + add Apache 2.0 (#35900) Closes #35070 Release Notes: - Edit Prediction: Made license detection update eligibility for data collection when license files change. - Edit Prediction: Added Apache 2.0 license to opensource licenses eligible for data collection. - Edit Prediction: Made license detection less sensitive to whitespace differences and check more files. --- crates/zeta/src/license_detection.rs | 654 +++++++++++++----- crates/zeta/src/license_detection/apache-text | 174 +++++ .../zeta/src/license_detection/apache.regex | 201 ++++++ crates/zeta/src/license_detection/isc.regex | 15 + crates/zeta/src/license_detection/mit-text | 21 + crates/zeta/src/license_detection/mit.regex | 21 + crates/zeta/src/license_detection/upl.regex | 35 + crates/zeta/src/zeta.rs | 66 +- 8 files changed, 955 insertions(+), 232 deletions(-) create mode 100644 crates/zeta/src/license_detection/apache-text create mode 100644 crates/zeta/src/license_detection/apache.regex create mode 100644 crates/zeta/src/license_detection/isc.regex create mode 100644 crates/zeta/src/license_detection/mit-text create mode 100644 crates/zeta/src/license_detection/mit.regex create mode 100644 crates/zeta/src/license_detection/upl.regex diff --git a/crates/zeta/src/license_detection.rs b/crates/zeta/src/license_detection.rs index e27ef8918d..c55f8d5d08 100644 --- a/crates/zeta/src/license_detection.rs +++ b/crates/zeta/src/license_detection.rs @@ -1,204 +1,213 @@ +use std::{ + collections::BTreeSet, + path::{Path, PathBuf}, + sync::{Arc, LazyLock}, +}; + +use fs::Fs; +use futures::StreamExt as _; +use gpui::{App, AppContext as _, Entity, Subscription, Task}; +use postage::watch; +use project::Worktree; use regex::Regex; +use util::ResultExt as _; +use worktree::ChildEntriesOptions; -/// The most common license locations, with US and UK English spelling. -pub const LICENSE_FILES_TO_CHECK: &[&str] = &[ - "LICENSE", - "LICENCE", - "LICENSE.txt", - "LICENCE.txt", - "LICENSE.md", - "LICENCE.md", -]; +/// Matches the most common license locations, with US and UK English spelling. +const LICENSE_FILE_NAME_REGEX: LazyLock = LazyLock::new(|| { + regex::bytes::RegexBuilder::new( + "^ \ + (?: license | licence) \ + (?: [\\-._] (?: apache | isc | mit | upl))? \ + (?: \\.txt | \\.md)? \ + $", + ) + .ignore_whitespace(true) + .case_insensitive(true) + .build() + .unwrap() +}); -pub fn is_license_eligible_for_data_collection(license: &str) -> bool { - // TODO: Include more licenses later (namely, Apache) - for pattern in [MIT_LICENSE_REGEX, ISC_LICENSE_REGEX, UPL_LICENSE_REGEX] { - let regex = Regex::new(pattern.trim()).unwrap(); - if regex.is_match(license.trim()) { - return true; - } - } - false +fn is_license_eligible_for_data_collection(license: &str) -> bool { + const LICENSE_REGEXES: LazyLock> = LazyLock::new(|| { + [ + include_str!("license_detection/apache.regex"), + include_str!("license_detection/isc.regex"), + include_str!("license_detection/mit.regex"), + include_str!("license_detection/upl.regex"), + ] + .into_iter() + .map(|pattern| Regex::new(&canonicalize_license_text(pattern)).unwrap()) + .collect() + }); + + let license = canonicalize_license_text(license); + LICENSE_REGEXES.iter().any(|regex| regex.is_match(&license)) } -const MIT_LICENSE_REGEX: &str = r#" -^.*MIT License.* +/// Canonicalizes the whitespace of license text and license regexes. +fn canonicalize_license_text(license: &str) -> String { + const PARAGRAPH_SEPARATOR_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"\s*\n\s*\n\s*").unwrap()); -Copyright.*? + PARAGRAPH_SEPARATOR_REGEX + .split(license) + .filter(|paragraph| !paragraph.trim().is_empty()) + .map(|paragraph| { + paragraph + .trim() + .split_whitespace() + .collect::>() + .join(" ") + }) + .collect::>() + .join("\n\n") +} -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files \(the "Software"\), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +pub enum LicenseDetectionWatcher { + Local { + is_open_source_rx: watch::Receiver, + _is_open_source_task: Task<()>, + _worktree_subscription: Subscription, + }, + SingleFile, + Remote, +} -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software\. +impl LicenseDetectionWatcher { + pub fn new(worktree: &Entity, cx: &mut App) -> Self { + let worktree_ref = worktree.read(cx); + if worktree_ref.is_single_file() { + return Self::SingleFile; + } -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE\.$ -"#; + let (files_to_check_tx, mut files_to_check_rx) = futures::channel::mpsc::unbounded(); -const ISC_LICENSE_REGEX: &str = r#" -^ISC License + let Worktree::Local(local_worktree) = worktree_ref else { + return Self::Remote; + }; + let fs = local_worktree.fs().clone(); + let worktree_abs_path = local_worktree.abs_path().clone(); -Copyright.*? + let options = ChildEntriesOptions { + include_files: true, + include_dirs: false, + include_ignored: true, + }; + for top_file in local_worktree.child_entries_with_options(Path::new(""), options) { + let path_bytes = top_file.path.as_os_str().as_encoded_bytes(); + if top_file.is_created() && LICENSE_FILE_NAME_REGEX.is_match(path_bytes) { + let rel_path = top_file.path.clone(); + files_to_check_tx.unbounded_send(rel_path).ok(); + } + } -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies\. + let _worktree_subscription = + cx.subscribe(worktree, move |_worktree, event, _cx| match event { + worktree::Event::UpdatedEntries(updated_entries) => { + for updated_entry in updated_entries.iter() { + let rel_path = &updated_entry.0; + let path_bytes = rel_path.as_os_str().as_encoded_bytes(); + if LICENSE_FILE_NAME_REGEX.is_match(path_bytes) { + files_to_check_tx.unbounded_send(rel_path.clone()).ok(); + } + } + } + worktree::Event::DeletedEntry(_) | worktree::Event::UpdatedGitRepositories(_) => {} + }); -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS\. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE\.$ -"#; + let (mut is_open_source_tx, is_open_source_rx) = watch::channel_with::(false); -const UPL_LICENSE_REGEX: &str = r#" -Copyright.*? + let _is_open_source_task = cx.background_spawn(async move { + let mut eligible_licenses = BTreeSet::new(); + while let Some(rel_path) = files_to_check_rx.next().await { + let abs_path = worktree_abs_path.join(&rel_path); + let was_open_source = !eligible_licenses.is_empty(); + if Self::is_path_eligible(&fs, abs_path).await.unwrap_or(false) { + eligible_licenses.insert(rel_path); + } else { + eligible_licenses.remove(&rel_path); + } + let is_open_source = !eligible_licenses.is_empty(); + if is_open_source != was_open_source { + *is_open_source_tx.borrow_mut() = is_open_source; + } + } + }); -The Universal Permissive License.*? + Self::Local { + is_open_source_rx, + _is_open_source_task, + _worktree_subscription, + } + } -Subject to the condition set forth below, permission is hereby granted to any person -obtaining a copy of this software, associated documentation and/or data \(collectively -the "Software"\), free of charge and under any and all copyright rights in the -Software, and any and all patent rights owned or freely licensable by each licensor -hereunder covering either \(i\) the unmodified Software as contributed to or provided -by such licensor, or \(ii\) the Larger Works \(as defined below\), to deal in both + async fn is_path_eligible(fs: &Arc, abs_path: PathBuf) -> Option { + log::info!("checking if `{abs_path:?}` is an open source license"); + // Resolve symlinks so that the file size from metadata is correct. + let Some(abs_path) = fs.canonicalize(&abs_path).await.ok() else { + log::info!( + "`{abs_path:?}` license file probably deleted (error canonicalizing the path)" + ); + return None; + }; + let metadata = fs.metadata(&abs_path).await.log_err()??; + // If the license file is >32kb it's unlikely to legitimately match any eligible license. + if metadata.len > 32768 { + return None; + } + let text = fs.load(&abs_path).await.log_err()?; + let is_eligible = is_license_eligible_for_data_collection(&text); + if is_eligible { + log::info!( + "`{abs_path:?}` matches a license that is eligible for data collection (if enabled)" + ); + } else { + log::info!( + "`{abs_path:?}` does not match a license that is eligible for data collection" + ); + } + Some(is_eligible) + } -\(a\) the Software, and - -\(b\) any piece of software and/or hardware listed in the lrgrwrks\.txt file if one is - included with the Software \(each a "Larger Work" to which the Software is - contributed by such licensors\), - -without restriction, including without limitation the rights to copy, create -derivative works of, display, perform, and distribute the Software and make, use, -sell, offer for sale, import, export, have made, and have sold the Software and the -Larger Work\(s\), and to sublicense the foregoing rights on either these or other -terms\. - -This license is subject to the following condition: - -The above copyright notice and either this complete permission notice or at a minimum -a reference to the UPL must be included in all copies or substantial portions of the -Software\. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT\. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE -OR THE USE OR OTHER DEALINGS IN THE SOFTWARE\.$ -"#; + /// Answers false until we find out it's open source + pub fn is_project_open_source(&self) -> bool { + match self { + Self::Local { + is_open_source_rx, .. + } => *is_open_source_rx.borrow(), + Self::SingleFile | Self::Remote => false, + } + } +} #[cfg(test)] mod tests { - use unindent::unindent; - use crate::is_license_eligible_for_data_collection; + use fs::FakeFs; + use gpui::TestAppContext; + use serde_json::json; + use settings::{Settings as _, SettingsStore}; + use unindent::unindent; + use worktree::WorktreeSettings; + + use super::*; + + const MIT_LICENSE: &str = include_str!("license_detection/mit-text"); + const APACHE_LICENSE: &str = include_str!("license_detection/apache-text"); #[test] fn test_mit_positive_detection() { - let example_license = unindent( - r#" - MIT License - - Copyright (c) 2024 John Doe - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - "# - .trim(), - ); - - assert!(is_license_eligible_for_data_collection(&example_license)); - - let example_license = unindent( - r#" - The MIT License (MIT) - - Copyright (c) 2019 John Doe - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - "# - .trim(), - ); - - assert!(is_license_eligible_for_data_collection(&example_license)); + assert!(is_license_eligible_for_data_collection(&MIT_LICENSE)); } #[test] fn test_mit_negative_detection() { - let example_license = unindent( - r#" - MIT License + let example_license = format!( + r#"{MIT_LICENSE} - Copyright (c) 2024 John Doe - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - - This project is dual licensed under the MIT License and the Apache License, Version 2.0. - "# - .trim(), + This project is dual licensed under the MIT License and the Apache License, Version 2.0."# ); - assert!(!is_license_eligible_for_data_collection(&example_license)); } @@ -351,4 +360,307 @@ mod tests { assert!(!is_license_eligible_for_data_collection(&example_license)); } + + #[test] + fn test_apache_positive_detection() { + assert!(is_license_eligible_for_data_collection(APACHE_LICENSE)); + + let license_with_appendix = format!( + r#"{APACHE_LICENSE} + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License."# + ); + assert!(is_license_eligible_for_data_collection( + &license_with_appendix + )); + + // Sometimes people fill in the appendix with copyright info. + let license_with_copyright = license_with_appendix.replace( + "Copyright [yyyy] [name of copyright owner]", + "Copyright 2025 John Doe", + ); + assert!(license_with_copyright != license_with_appendix); + assert!(is_license_eligible_for_data_collection( + &license_with_copyright + )); + } + + #[test] + fn test_apache_negative_detection() { + assert!(!is_license_eligible_for_data_collection(&format!( + "{APACHE_LICENSE}\n\nThe terms in this license are void if P=NP." + ))); + } + + #[test] + fn test_license_file_name_regex() { + // Test basic license file names + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"license")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"licence")); + + // Test with extensions + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.txt")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.md")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE.txt")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE.md")); + + // Test with specific license types + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-APACHE")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-MIT")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.MIT")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE_MIT")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-ISC")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-UPL")); + + // Test combinations + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-MIT.txt")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE.ISC.md")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"license_upl")); + + // Test case insensitive + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"License")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"license-mit.TXT")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"LICENCE_isc.MD")); + + // Test edge cases that should match + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"license.mit")); + assert!(LICENSE_FILE_NAME_REGEX.is_match(b"licence-upl.txt")); + + // Test non-matching patterns + assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"COPYING")); + assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.html")); + assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"MYLICENSE")); + assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"src/LICENSE")); + assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE.old")); + assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"LICENSE-GPL")); + assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"LICENSEABC")); + assert!(!LICENSE_FILE_NAME_REGEX.is_match(b"")); + } + + #[test] + fn test_canonicalize_license_text() { + // Test basic whitespace normalization + let input = "Line 1\n Line 2 \n\n\n Line 3 "; + let expected = "Line 1 Line 2\n\nLine 3"; + assert_eq!(canonicalize_license_text(input), expected); + + // Test paragraph separation + let input = "Paragraph 1\nwith multiple lines\n\n\n\nParagraph 2\nwith more lines"; + let expected = "Paragraph 1 with multiple lines\n\nParagraph 2 with more lines"; + assert_eq!(canonicalize_license_text(input), expected); + + // Test empty paragraphs are filtered out + let input = "\n\n\nParagraph 1\n\n\n \n\n\nParagraph 2\n\n\n"; + let expected = "Paragraph 1\n\nParagraph 2"; + assert_eq!(canonicalize_license_text(input), expected); + + // Test single line + let input = " Single line with spaces "; + let expected = "Single line with spaces"; + assert_eq!(canonicalize_license_text(input), expected); + + // Test multiple consecutive spaces within lines + let input = "Word1 Word2\n\nWord3 Word4"; + let expected = "Word1 Word2\n\nWord3 Word4"; + assert_eq!(canonicalize_license_text(input), expected); + + // Test tabs and mixed whitespace + let input = "Word1\t\tWord2\n\n Word3\r\n\r\n\r\nWord4 "; + let expected = "Word1 Word2\n\nWord3\n\nWord4"; + assert_eq!(canonicalize_license_text(input), expected); + } + + #[test] + fn test_license_detection_canonicalizes_whitespace() { + let mit_with_weird_spacing = unindent( + r#" + MIT License + + + Copyright (c) 2024 John Doe + + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + "# + .trim(), + ); + + assert!(is_license_eligible_for_data_collection( + &mit_with_weird_spacing + )); + } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + WorktreeSettings::register(cx); + }); + } + + #[gpui::test] + async fn test_watcher_single_file(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree("/root", json!({ "main.rs": "fn main() {}" })) + .await; + + let worktree = Worktree::local( + Path::new("/root/main.rs"), + true, + fs.clone(), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + let watcher = cx.update(|cx| LicenseDetectionWatcher::new(&worktree, cx)); + assert!(matches!(watcher, LicenseDetectionWatcher::SingleFile)); + assert!(!watcher.is_project_open_source()); + } + + #[gpui::test] + async fn test_watcher_updates_on_changes(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree("/root", json!({ "main.rs": "fn main() {}" })) + .await; + + let worktree = Worktree::local( + Path::new("/root"), + true, + fs.clone(), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + let watcher = cx.update(|cx| LicenseDetectionWatcher::new(&worktree, cx)); + assert!(matches!(watcher, LicenseDetectionWatcher::Local { .. })); + assert!(!watcher.is_project_open_source()); + + fs.write(Path::new("/root/LICENSE-MIT"), MIT_LICENSE.as_bytes()) + .await + .unwrap(); + + cx.background_executor.run_until_parked(); + assert!(watcher.is_project_open_source()); + + fs.write(Path::new("/root/LICENSE-APACHE"), APACHE_LICENSE.as_bytes()) + .await + .unwrap(); + + cx.background_executor.run_until_parked(); + assert!(watcher.is_project_open_source()); + + fs.write(Path::new("/root/LICENSE-MIT"), "Nevermind".as_bytes()) + .await + .unwrap(); + + // Still considered open source as LICENSE-APACHE is present + cx.background_executor.run_until_parked(); + assert!(watcher.is_project_open_source()); + + fs.write( + Path::new("/root/LICENSE-APACHE"), + "Also nevermind".as_bytes(), + ) + .await + .unwrap(); + + cx.background_executor.run_until_parked(); + assert!(!watcher.is_project_open_source()); + } + + #[gpui::test] + async fn test_watcher_initially_opensource_and_then_deleted(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/root", + json!({ "main.rs": "fn main() {}", "LICENSE-MIT": MIT_LICENSE }), + ) + .await; + + let worktree = Worktree::local( + Path::new("/root"), + true, + fs.clone(), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + let watcher = cx.update(|cx| LicenseDetectionWatcher::new(&worktree, cx)); + assert!(matches!(watcher, LicenseDetectionWatcher::Local { .. })); + + cx.background_executor.run_until_parked(); + assert!(watcher.is_project_open_source()); + + fs.remove_file( + Path::new("/root/LICENSE-MIT"), + fs::RemoveOptions { + recursive: false, + ignore_if_not_exists: false, + }, + ) + .await + .unwrap(); + + cx.background_executor.run_until_parked(); + assert!(!watcher.is_project_open_source()); + } } diff --git a/crates/zeta/src/license_detection/apache-text b/crates/zeta/src/license_detection/apache-text new file mode 100644 index 0000000000..dd5b3a58aa --- /dev/null +++ b/crates/zeta/src/license_detection/apache-text @@ -0,0 +1,174 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/crates/zeta/src/license_detection/apache.regex b/crates/zeta/src/license_detection/apache.regex new file mode 100644 index 0000000000..e200e063c9 --- /dev/null +++ b/crates/zeta/src/license_detection/apache.regex @@ -0,0 +1,201 @@ + ^Apache License + Version 2\.0, January 2004 + http://www\.apache\.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1\. Definitions\. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document\. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License\. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity\. For the purposes of this definition, + "control" means \(i\) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or \(ii\) ownership of fifty percent \(50%\) or more of the + outstanding shares, or \(iii\) beneficial ownership of such entity\. + + "You" \(or "Your"\) shall mean an individual or Legal Entity + exercising permissions granted by this License\. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files\. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types\. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + \(an example is provided in the Appendix below\)\. + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on \(or derived from\) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship\. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link \(or bind by name\) to the interfaces of, + the Work and Derivative Works thereof\. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner\. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution\." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work\. + + 2\. Grant of Copyright License\. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non\-exclusive, no\-charge, royalty\-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form\. + + 3\. Grant of Patent License\. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non\-exclusive, no\-charge, royalty\-free, irrevocable + \(except as stated in this section\) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution\(s\) alone or by combination of their Contribution\(s\) + with the Work to which such Contribution\(s\) was submitted\. If You + institute patent litigation against any entity \(including a + cross\-claim or counterclaim in a lawsuit\) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed\. + + 4\. Redistribution\. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + \(a\) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + \(b\) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + \(c\) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + \(d\) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third\-party notices normally appear\. The contents + of the NOTICE file are for informational purposes only and + do not modify the License\. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License\. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License\. + + 5\. Submission of Contributions\. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions\. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions\. + + 6\. Trademarks\. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file\. + + 7\. Disclaimer of Warranty\. Unless required by applicable law or + agreed to in writing, Licensor provides the Work \(and each + Contributor provides its Contributions\) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON\-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE\. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License\. + + 8\. Limitation of Liability\. In no event and under no legal theory, + whether in tort \(including negligence\), contract, or otherwise, + unless required by applicable law \(such as deliberate and grossly + negligent acts\) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work \(including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses\), even if such Contributor + has been advised of the possibility of such damages\. + + 9\. Accepting Warranty or Additional Liability\. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License\. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability\.(:? + + END OF TERMS AND CONDITIONS)?(:? + + APPENDIX: How to apply the Apache License to your work\. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "\[\]" + replaced with your own identifying information\. \(Don't include + the brackets!\) The text should be enclosed in the appropriate + comment syntax for the file format\. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third\-party archives\.)?(:? + + Copyright .*)?(:? + + Licensed under the Apache License, Version 2\.0 \(the "License"\); + you may not use this file except in compliance with the License\. + You may obtain a copy of the License at + + http://www\.apache\.org/licenses/LICENSE\-2\.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\. + See the License for the specific language governing permissions and + limitations under the License\.)?$ diff --git a/crates/zeta/src/license_detection/isc.regex b/crates/zeta/src/license_detection/isc.regex new file mode 100644 index 0000000000..63c6126bce --- /dev/null +++ b/crates/zeta/src/license_detection/isc.regex @@ -0,0 +1,15 @@ +^.*ISC License.* + +Copyright.* + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies\. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS\. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE\.$ diff --git a/crates/zeta/src/license_detection/mit-text b/crates/zeta/src/license_detection/mit-text new file mode 100644 index 0000000000..2b8f73ab0d --- /dev/null +++ b/crates/zeta/src/license_detection/mit-text @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 John Doe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/zeta/src/license_detection/mit.regex b/crates/zeta/src/license_detection/mit.regex new file mode 100644 index 0000000000..deda8f0352 --- /dev/null +++ b/crates/zeta/src/license_detection/mit.regex @@ -0,0 +1,21 @@ +^.*MIT License.* + +Copyright.* + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files \(the "Software"\), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software\. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE\.$ diff --git a/crates/zeta/src/license_detection/upl.regex b/crates/zeta/src/license_detection/upl.regex new file mode 100644 index 0000000000..34ba2a64c6 --- /dev/null +++ b/crates/zeta/src/license_detection/upl.regex @@ -0,0 +1,35 @@ +^Copyright.* + +The Universal Permissive License.* + +Subject to the condition set forth below, permission is hereby granted to any person +obtaining a copy of this software, associated documentation and/or data \(collectively +the "Software"\), free of charge and under any and all copyright rights in the +Software, and any and all patent rights owned or freely licensable by each licensor +hereunder covering either \(i\) the unmodified Software as contributed to or provided +by such licensor, or \(ii\) the Larger Works \(as defined below\), to deal in both + +\(a\) the Software, and + +\(b\) any piece of software and/or hardware listed in the lrgrwrks\.txt file if one is + included with the Software \(each a "Larger Work" to which the Software is + contributed by such licensors\), + +without restriction, including without limitation the rights to copy, create +derivative works of, display, perform, and distribute the Software and make, use, +sell, offer for sale, import, export, have made, and have sold the Software and the +Larger Work\(s\), and to sublicense the foregoing rights on either these or other +terms\. + +This license is subject to the following condition: + +The above copyright notice and either this complete permission notice or at a minimum +a reference to the UPL must be included in all copies or substantial portions of the +Software\. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT\. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE\.$ diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 828310a3bd..1ddbd25cb8 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -10,8 +10,7 @@ pub(crate) use completion_diff_element::*; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use edit_prediction::DataCollectionState; pub use init::*; -use license_detection::LICENSE_FILES_TO_CHECK; -pub use license_detection::is_license_eligible_for_data_collection; +use license_detection::LicenseDetectionWatcher; pub use rate_completion_modal::*; use anyhow::{Context as _, Result, anyhow}; @@ -33,7 +32,6 @@ use language::{ Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, ToOffset, ToPoint, text_diff, }; use language_model::{LlmApiToken, RefreshLlmTokenListener}; -use postage::watch; use project::{Project, ProjectPath}; use release_channel::AppVersion; use settings::WorktreeId; @@ -253,11 +251,10 @@ impl Zeta { this.update(cx, move |this, cx| { if let Some(worktree) = worktree { - worktree.update(cx, |worktree, cx| { - this.license_detection_watchers - .entry(worktree.id()) - .or_insert_with(|| Rc::new(LicenseDetectionWatcher::new(worktree, cx))); - }); + let worktree_id = worktree.read(cx).id(); + this.license_detection_watchers + .entry(worktree_id) + .or_insert_with(|| Rc::new(LicenseDetectionWatcher::new(&worktree, cx))); } }); @@ -1104,59 +1101,6 @@ pub struct ZedUpdateRequiredError { minimum_version: SemanticVersion, } -struct LicenseDetectionWatcher { - is_open_source_rx: watch::Receiver, - _is_open_source_task: Task<()>, -} - -impl LicenseDetectionWatcher { - pub fn new(worktree: &Worktree, cx: &mut Context) -> Self { - let (mut is_open_source_tx, is_open_source_rx) = watch::channel_with::(false); - - // Check if worktree is a single file, if so we do not need to check for a LICENSE file - let task = if worktree.abs_path().is_file() { - Task::ready(()) - } else { - let loaded_files = LICENSE_FILES_TO_CHECK - .iter() - .map(Path::new) - .map(|file| worktree.load_file(file, cx)) - .collect::>(); - - cx.background_spawn(async move { - for loaded_file in loaded_files.into_iter() { - let Ok(loaded_file) = loaded_file.await else { - continue; - }; - - let path = &loaded_file.file.path; - if is_license_eligible_for_data_collection(&loaded_file.text) { - log::info!("detected '{path:?}' as open source license"); - *is_open_source_tx.borrow_mut() = true; - } else { - log::info!("didn't detect '{path:?}' as open source license"); - } - - // stop on the first license that successfully read - return; - } - - log::debug!("didn't find a license file to check, assuming closed source"); - }) - }; - - Self { - is_open_source_rx, - _is_open_source_task: task, - } - } - - /// Answers false until we find out it's open source - pub fn is_project_open_source(&self) -> bool { - *self.is_open_source_rx.borrow() - } -} - fn common_prefix, T2: Iterator>(a: T1, b: T2) -> usize { a.zip(b) .take_while(|(a, b)| a == b) From 4c5058c077981d20c886332480a9e861de63d430 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Sat, 9 Aug 2025 05:28:36 -0500 Subject: [PATCH 225/693] Fix uploading mac dsyms (#35904) I'm not sure we actually want to be using `debug-info=unpacked` and then running `dsymutil` with `--flat`, but for now the minimal change to get this working is to manually specify the flattened, uncompressed debug info file for upload, which in turn will cause `sentry-cli` to pick up on source-info for the zed binary. I think in the future we should switch to `packed` debug info, both for the zed binary _and_ the remote server, and then we can tar up the better supported `dSYM` folder format rather than the flat dwarf version. Release Notes: - N/A --- script/bundle-mac | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/script/bundle-mac b/script/bundle-mac index b2be573235..f2a5bf313d 100755 --- a/script/bundle-mac +++ b/script/bundle-mac @@ -207,7 +207,7 @@ function prepare_binaries() { rm -f target/${architecture}/${target_dir}/Zed.dwarf.gz echo "Gzipping dSYMs for $architecture" - gzip -f target/${architecture}/${target_dir}/Zed.dwarf + gzip -kf target/${architecture}/${target_dir}/Zed.dwarf echo "Uploading dSYMs${architecture} for $architecture to by-uuid/${uuid}.dwarf.gz" upload_to_blob_store_public \ @@ -367,19 +367,25 @@ else gzip -f --stdout --best target/aarch64-apple-darwin/release/remote_server > target/zed-remote-server-macos-aarch64.gz fi -# Upload debug info to sentry.io -if ! command -v sentry-cli >/dev/null 2>&1; then - echo "sentry-cli not found. skipping sentry upload." - echo "install with: 'curl -sL https://sentry.io/get-cli | bash'" -else +function upload_debug_info() { + architecture=$1 if [[ -n "${SENTRY_AUTH_TOKEN:-}" ]]; then echo "Uploading zed debug symbols to sentry..." # note: this uploads the unstripped binary which is needed because it contains # .eh_frame data for stack unwinindg. see https://github.com/getsentry/symbolic/issues/783 sentry-cli debug-files upload --include-sources --wait -p zed -o zed-dev \ - "target/x86_64-apple-darwin/${target_dir}/" \ - "target/aarch64-apple-darwin/${target_dir}/" + "target/${architecture}/${target_dir}/zed" \ + "target/${architecture}/${target_dir}/remote_server" \ + "target/${architecture}/${target_dir}/zed.dwarf" else echo "missing SENTRY_AUTH_TOKEN. skipping sentry upload." fi +} + +if command -v sentry-cli >/dev/null 2>&1; then + upload_debug_info "aarch64-apple-darwin" + upload_debug_info "x86_64-apple-darwin" +else + echo "sentry-cli not found. skipping sentry upload." + echo "install with: 'curl -sL https://sentry.io/get-cli | bash'" fi From c91fb4caf4c8a221a5e17b7b18d86ba203311c04 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Sat, 9 Aug 2025 05:37:28 -0500 Subject: [PATCH 226/693] Add sentry release step to ci (#35911) This should allow us to associate sha's from crashes and generate links to github source in sentry. Release Notes: - N/A --- .github/workflows/ci.yml | 9 +++++++++ .github/workflows/release_nightly.yml | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 928c47a4a7..3b70271e57 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -851,3 +851,12 @@ jobs: run: gh release edit "$GITHUB_REF_NAME" --draft=false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create Sentry release + uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c # v3 + env: + SENTRY_ORG: zed-dev + SENTRY_PROJECT: zed + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + with: + environment: production diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index ed9f4c8450..0cc6737a45 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -316,3 +316,12 @@ jobs: git config user.email github-actions@github.com git tag -f nightly git push origin nightly --force + + - name: Create Sentry release + uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c # v3 + env: + SENTRY_ORG: zed-dev + SENTRY_PROJECT: zed + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + with: + environment: production From 7862c0c94588a85809e5e31e06ca1dc69de0afe3 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Sat, 9 Aug 2025 06:20:38 -0500 Subject: [PATCH 227/693] Add more info to crash reports (#35914) None of this is new info, we're just pulling more things out of the panic message to send with the minidump. We do want to add more fields like gpu version which will come in a subsequent change. Release Notes: - N/A --- crates/zed/src/reliability.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index 53539699cc..fde44344b1 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -603,11 +603,31 @@ async fn upload_minidump( .text("platform", "rust"); if let Some(panic) = panic { form = form + .text("sentry[tags][channel]", panic.release_channel.clone()) + .text("sentry[tags][version]", panic.app_version.clone()) + .text("sentry[context][os][name]", panic.os_name.clone()) .text( + "sentry[context][device][architecture]", + panic.architecture.clone(), + ) + .text("sentry[logentry][formatted]", panic.payload.clone()); + + if let Some(sha) = panic.app_commit_sha.clone() { + form = form.text("sentry[release]", sha) + } else { + form = form.text( "sentry[release]", format!("{}-{}", panic.release_channel, panic.app_version), ) - .text("sentry[logentry][formatted]", panic.payload.clone()); + } + if let Some(v) = panic.os_version.clone() { + form = form.text("sentry[context][os][release]", v); + } + if let Some(location) = panic.location_data.as_ref() { + form = form.text("span", format!("{}:{}", location.file, location.line)) + } + // TODO: add gpu-context, feature-flag-context, and more of device-context like gpu + // name, screen resolution, available ram, device model, etc } let mut response_text = String::new(); From 021681d4563df41cdc04a40cf4ef03c5afe0645a Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Sat, 9 Aug 2025 06:42:30 -0500 Subject: [PATCH 228/693] Don't generate crash reports on the Dev channel (#35915) We only want minidumps to be generated on actual release builds. Now we avoid spawning crash handler processes for dev builds. To test minidumping you can still set the `ZED_GENERATE_MINIDUMPS` env var which force-enable the feature. Release Notes: - N/A --- Cargo.lock | 1 + crates/crashes/Cargo.toml | 1 + crates/crashes/src/crashes.rs | 13 ++++++++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 6f434e8685..1ae4303c71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4014,6 +4014,7 @@ dependencies = [ "log", "minidumper", "paths", + "release_channel", "smol", "workspace-hack", ] diff --git a/crates/crashes/Cargo.toml b/crates/crashes/Cargo.toml index 641a97765a..afb4936b63 100644 --- a/crates/crashes/Cargo.toml +++ b/crates/crashes/Cargo.toml @@ -10,6 +10,7 @@ crash-handler.workspace = true log.workspace = true minidumper.workspace = true paths.workspace = true +release_channel.workspace = true smol.workspace = true workspace-hack.workspace = true diff --git a/crates/crashes/src/crashes.rs b/crates/crashes/src/crashes.rs index cfb4b57d5d..5b9ae0b546 100644 --- a/crates/crashes/src/crashes.rs +++ b/crates/crashes/src/crashes.rs @@ -1,6 +1,7 @@ use crash_handler::CrashHandler; use log::info; use minidumper::{Client, LoopAction, MinidumpBinary}; +use release_channel::{RELEASE_CHANNEL, ReleaseChannel}; use std::{ env, @@ -9,7 +10,7 @@ use std::{ path::{Path, PathBuf}, process::{self, Command}, sync::{ - OnceLock, + LazyLock, OnceLock, atomic::{AtomicBool, Ordering}, }, thread, @@ -22,7 +23,14 @@ pub static CRASH_HANDLER: AtomicBool = AtomicBool::new(false); pub static REQUESTED_MINIDUMP: AtomicBool = AtomicBool::new(false); const CRASH_HANDLER_TIMEOUT: Duration = Duration::from_secs(60); +pub static GENERATE_MINIDUMPS: LazyLock = LazyLock::new(|| { + *RELEASE_CHANNEL != ReleaseChannel::Dev || env::var("ZED_GENERATE_MINIDUMPS").is_ok() +}); + pub async fn init(id: String) { + if !*GENERATE_MINIDUMPS { + return; + } let exe = env::current_exe().expect("unable to find ourselves"); let zed_pid = process::id(); // TODO: we should be able to get away with using 1 crash-handler process per machine, @@ -138,6 +146,9 @@ impl minidumper::ServerHandler for CrashServer { } pub fn handle_panic() { + if !*GENERATE_MINIDUMPS { + return; + } // wait 500ms for the crash handler process to start up // if it's still not there just write panic info and no minidump let retry_frequency = Duration::from_millis(100); From ce39644cbd5e52efa80dfe4d320927afea13ec4b Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Sun, 10 Aug 2025 00:55:47 +0530 Subject: [PATCH 229/693] language_models: Add thinking to Mistral Provider (#32476) Tested prompt: John is one of 4 children. The first sister is 4 years old. Next year, the second sister will be twice as old as the first sister. The third sister is two years older than the second sister. The third sister is half the age of her older brother. How old is John? Return your thinking inside Release Notes: - Add thinking to Mistral Provider --------- Signed-off-by: Umesh Yadav Co-authored-by: Peter Tripp --- .../language_models/src/provider/mistral.rs | 135 +++++++++++------- crates/mistral/src/mistral.rs | 50 +++++-- 2 files changed, 126 insertions(+), 59 deletions(-) diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index 02e53cb99a..4a0d740334 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/crates/language_models/src/provider/mistral.rs @@ -47,6 +47,7 @@ pub struct AvailableModel { pub max_completion_tokens: Option, pub supports_tools: Option, pub supports_images: Option, + pub supports_thinking: Option, } pub struct MistralLanguageModelProvider { @@ -215,6 +216,7 @@ impl LanguageModelProvider for MistralLanguageModelProvider { max_completion_tokens: model.max_completion_tokens, supports_tools: model.supports_tools, supports_images: model.supports_images, + supports_thinking: model.supports_thinking, }, ); } @@ -366,11 +368,7 @@ impl LanguageModel for MistralLanguageModel { LanguageModelCompletionError, >, > { - let request = into_mistral( - request, - self.model.id().to_string(), - self.max_output_tokens(), - ); + let request = into_mistral(request, self.model.clone(), self.max_output_tokens()); let stream = self.stream_completion(request, cx); async move { @@ -384,7 +382,7 @@ impl LanguageModel for MistralLanguageModel { pub fn into_mistral( request: LanguageModelRequest, - model: String, + model: mistral::Model, max_output_tokens: Option, ) -> mistral::Request { let stream = true; @@ -401,13 +399,20 @@ pub fn into_mistral( .push_part(mistral::MessagePart::Text { text: text.clone() }); } MessageContent::Image(image_content) => { - message_content.push_part(mistral::MessagePart::ImageUrl { - image_url: image_content.to_base64_url(), - }); + if model.supports_images() { + message_content.push_part(mistral::MessagePart::ImageUrl { + image_url: image_content.to_base64_url(), + }); + } } MessageContent::Thinking { text, .. } => { - message_content - .push_part(mistral::MessagePart::Text { text: text.clone() }); + if model.supports_thinking() { + message_content.push_part(mistral::MessagePart::Thinking { + thinking: vec![mistral::ThinkingPart::Text { + text: text.clone(), + }], + }); + } } MessageContent::RedactedThinking(_) => {} MessageContent::ToolUse(_) => { @@ -437,12 +442,28 @@ pub fn into_mistral( Role::Assistant => { for content in &message.content { match content { - MessageContent::Text(text) | MessageContent::Thinking { text, .. } => { + MessageContent::Text(text) => { messages.push(mistral::RequestMessage::Assistant { - content: Some(text.clone()), + content: Some(mistral::MessageContent::Plain { + content: text.clone(), + }), tool_calls: Vec::new(), }); } + MessageContent::Thinking { text, .. } => { + if model.supports_thinking() { + messages.push(mistral::RequestMessage::Assistant { + content: Some(mistral::MessageContent::Multipart { + content: vec![mistral::MessagePart::Thinking { + thinking: vec![mistral::ThinkingPart::Text { + text: text.clone(), + }], + }], + }), + tool_calls: Vec::new(), + }); + } + } MessageContent::RedactedThinking(_) => {} MessageContent::Image(_) => {} MessageContent::ToolUse(tool_use) => { @@ -477,11 +498,26 @@ pub fn into_mistral( Role::System => { for content in &message.content { match content { - MessageContent::Text(text) | MessageContent::Thinking { text, .. } => { + MessageContent::Text(text) => { messages.push(mistral::RequestMessage::System { - content: text.clone(), + content: mistral::MessageContent::Plain { + content: text.clone(), + }, }); } + MessageContent::Thinking { text, .. } => { + if model.supports_thinking() { + messages.push(mistral::RequestMessage::System { + content: mistral::MessageContent::Multipart { + content: vec![mistral::MessagePart::Thinking { + thinking: vec![mistral::ThinkingPart::Text { + text: text.clone(), + }], + }], + }, + }); + } + } MessageContent::RedactedThinking(_) => {} MessageContent::Image(_) | MessageContent::ToolUse(_) @@ -494,37 +530,8 @@ pub fn into_mistral( } } - // The Mistral API requires that tool messages be followed by assistant messages, - // not user messages. When we have a tool->user sequence in the conversation, - // we need to insert a placeholder assistant message to maintain proper conversation - // flow and prevent API errors. This is a Mistral-specific requirement that differs - // from other language model APIs. - let messages = { - let mut fixed_messages = Vec::with_capacity(messages.len()); - let mut messages_iter = messages.into_iter().peekable(); - - while let Some(message) = messages_iter.next() { - let is_tool_message = matches!(message, mistral::RequestMessage::Tool { .. }); - fixed_messages.push(message); - - // Insert assistant message between tool and user messages - if is_tool_message { - if let Some(next_msg) = messages_iter.peek() { - if matches!(next_msg, mistral::RequestMessage::User { .. }) { - fixed_messages.push(mistral::RequestMessage::Assistant { - content: Some(" ".to_string()), - tool_calls: Vec::new(), - }); - } - } - } - } - - fixed_messages - }; - mistral::Request { - model, + model: model.id().to_string(), messages, stream, max_tokens: max_output_tokens, @@ -595,8 +602,38 @@ impl MistralEventMapper { }; let mut events = Vec::new(); - if let Some(content) = choice.delta.content.clone() { - events.push(Ok(LanguageModelCompletionEvent::Text(content))); + if let Some(content) = choice.delta.content.as_ref() { + match content { + mistral::MessageContentDelta::Text(text) => { + events.push(Ok(LanguageModelCompletionEvent::Text(text.clone()))); + } + mistral::MessageContentDelta::Parts(parts) => { + for part in parts { + match part { + mistral::MessagePart::Text { text } => { + events.push(Ok(LanguageModelCompletionEvent::Text(text.clone()))); + } + mistral::MessagePart::Thinking { thinking } => { + for tp in thinking.iter().cloned() { + match tp { + mistral::ThinkingPart::Text { text } => { + events.push(Ok( + LanguageModelCompletionEvent::Thinking { + text, + signature: None, + }, + )); + } + } + } + } + mistral::MessagePart::ImageUrl { .. } => { + // We currently don't emit a separate event for images in responses. + } + } + } + } + } } if let Some(tool_calls) = choice.delta.tool_calls.as_ref() { @@ -908,7 +945,7 @@ mod tests { thinking_allowed: true, }; - let mistral_request = into_mistral(request, "mistral-small-latest".into(), None); + let mistral_request = into_mistral(request, mistral::Model::MistralSmallLatest, None); assert_eq!(mistral_request.model, "mistral-small-latest"); assert_eq!(mistral_request.temperature, Some(0.5)); @@ -941,7 +978,7 @@ mod tests { thinking_allowed: true, }; - let mistral_request = into_mistral(request, "pixtral-12b-latest".into(), None); + let mistral_request = into_mistral(request, mistral::Model::Pixtral12BLatest, None); assert_eq!(mistral_request.messages.len(), 1); assert!(matches!( diff --git a/crates/mistral/src/mistral.rs b/crates/mistral/src/mistral.rs index c466a598a0..5b4d05377c 100644 --- a/crates/mistral/src/mistral.rs +++ b/crates/mistral/src/mistral.rs @@ -86,6 +86,7 @@ pub enum Model { max_completion_tokens: Option, supports_tools: Option, supports_images: Option, + supports_thinking: Option, }, } @@ -214,6 +215,16 @@ impl Model { } => supports_images.unwrap_or(false), } } + + pub fn supports_thinking(&self) -> bool { + match self { + Self::MagistralMediumLatest | Self::MagistralSmallLatest => true, + Self::Custom { + supports_thinking, .. + } => supports_thinking.unwrap_or(false), + _ => false, + } + } } #[derive(Debug, Serialize, Deserialize)] @@ -288,7 +299,9 @@ pub enum ToolChoice { #[serde(tag = "role", rename_all = "lowercase")] pub enum RequestMessage { Assistant { - content: Option, + #[serde(flatten)] + #[serde(default, skip_serializing_if = "Option::is_none")] + content: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] tool_calls: Vec, }, @@ -297,7 +310,8 @@ pub enum RequestMessage { content: MessageContent, }, System { - content: String, + #[serde(flatten)] + content: MessageContent, }, Tool { content: String, @@ -305,7 +319,7 @@ pub enum RequestMessage { }, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] #[serde(untagged)] pub enum MessageContent { #[serde(rename = "content")] @@ -346,11 +360,21 @@ impl MessageContent { } } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] #[serde(tag = "type", rename_all = "snake_case")] pub enum MessagePart { Text { text: String }, ImageUrl { image_url: String }, + Thinking { thinking: Vec }, +} + +// Backwards-compatibility alias for provider code that refers to ContentPart +pub type ContentPart = MessagePart; + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ThinkingPart { + Text { text: String }, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] @@ -418,24 +442,30 @@ pub struct StreamChoice { pub finish_reason: Option, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct StreamDelta { pub role: Option, - pub content: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub content: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub tool_calls: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub reasoning_content: Option, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +#[serde(untagged)] +pub enum MessageContentDelta { + Text(String), + Parts(Vec), +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct ToolCallChunk { pub index: usize, pub id: Option, pub function: Option, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct FunctionChunk { pub name: Option, pub arguments: Option, From 5901aec40a154990451e7a0d5b5752695c78581a Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Sat, 9 Aug 2025 23:40:44 +0200 Subject: [PATCH 230/693] agent2: Remove model param from Thread::send method (#35936) It instead uses the currently selected model Release Notes: - N/A --- crates/agent2/src/agent.rs | 3 +-- crates/agent2/src/tests/mod.rs | 33 ++++++++++++++------------------- crates/agent2/src/thread.rs | 2 +- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index df061cd5ed..892469db47 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -491,8 +491,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { // Send to thread log::info!("Sending message to thread with model: {:?}", model.name()); - let mut response_stream = - thread.update(cx, |thread, cx| thread.send(model, message, cx))?; + let mut response_stream = thread.update(cx, |thread, cx| thread.send(message, cx))?; // Handle response stream and forward to session.acp_thread while let Some(result) = response_stream.next().await { diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 273da1dae5..6e0dc86091 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -29,11 +29,11 @@ use test_tools::*; #[gpui::test] #[ignore = "can't run on CI yet"] async fn test_echo(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await; + let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; let events = thread .update(cx, |thread, cx| { - thread.send(model.clone(), "Testing: Reply with 'Hello'", cx) + thread.send("Testing: Reply with 'Hello'", cx) }) .collect() .await; @@ -49,12 +49,11 @@ async fn test_echo(cx: &mut TestAppContext) { #[gpui::test] #[ignore = "can't run on CI yet"] async fn test_thinking(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4Thinking).await; + let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4Thinking).await; let events = thread .update(cx, |thread, cx| { thread.send( - model.clone(), indoc! {" Testing: @@ -91,7 +90,7 @@ async fn test_system_prompt(cx: &mut TestAppContext) { project_context.borrow_mut().shell = "test-shell".into(); thread.update(cx, |thread, _| thread.add_tool(EchoTool)); - thread.update(cx, |thread, cx| thread.send(model.clone(), "abc", cx)); + thread.update(cx, |thread, cx| thread.send("abc", cx)); cx.run_until_parked(); let mut pending_completions = fake_model.pending_completions(); assert_eq!( @@ -121,14 +120,13 @@ async fn test_system_prompt(cx: &mut TestAppContext) { #[gpui::test] #[ignore = "can't run on CI yet"] async fn test_basic_tool_calls(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await; + let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; // Test a tool call that's likely to complete *before* streaming stops. let events = thread .update(cx, |thread, cx| { thread.add_tool(EchoTool); thread.send( - model.clone(), "Now test the echo tool with 'Hello'. Does it work? Say 'Yes' or 'No'.", cx, ) @@ -143,7 +141,6 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) { thread.remove_tool(&AgentTool::name(&EchoTool)); thread.add_tool(DelayTool); thread.send( - model.clone(), "Now call the delay tool with 200ms. When the timer goes off, then you echo the output of the tool.", cx, ) @@ -171,12 +168,12 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) { #[gpui::test] #[ignore = "can't run on CI yet"] async fn test_streaming_tool_calls(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await; + let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; // Test a tool call that's likely to complete *before* streaming stops. let mut events = thread.update(cx, |thread, cx| { thread.add_tool(WordListTool); - thread.send(model.clone(), "Test the word_list tool.", cx) + thread.send("Test the word_list tool.", cx) }); let mut saw_partial_tool_use = false; @@ -223,7 +220,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { let mut events = thread.update(cx, |thread, cx| { thread.add_tool(ToolRequiringPermission); - thread.send(model.clone(), "abc", cx) + thread.send("abc", cx) }); cx.run_until_parked(); fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( @@ -290,7 +287,7 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); - let mut events = thread.update(cx, |thread, cx| thread.send(model.clone(), "abc", cx)); + let mut events = thread.update(cx, |thread, cx| thread.send("abc", cx)); cx.run_until_parked(); fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { @@ -375,14 +372,13 @@ async fn next_tool_call_authorization( #[gpui::test] #[ignore = "can't run on CI yet"] async fn test_concurrent_tool_calls(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await; + let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; // Test concurrent tool calls with different delay times let events = thread .update(cx, |thread, cx| { thread.add_tool(DelayTool); thread.send( - model.clone(), "Call the delay tool twice in the same message. Once with 100ms. Once with 300ms. When both timers are complete, describe the outputs.", cx, ) @@ -414,13 +410,12 @@ async fn test_concurrent_tool_calls(cx: &mut TestAppContext) { #[gpui::test] #[ignore = "can't run on CI yet"] async fn test_cancellation(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await; + let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; let mut events = thread.update(cx, |thread, cx| { thread.add_tool(InfiniteTool); thread.add_tool(EchoTool); thread.send( - model.clone(), "Call the echo tool and then call the infinite tool, then explain their output", cx, ) @@ -466,7 +461,7 @@ async fn test_cancellation(cx: &mut TestAppContext) { // Ensure we can still send a new message after cancellation. let events = thread .update(cx, |thread, cx| { - thread.send(model.clone(), "Testing: reply with 'Hello' then stop.", cx) + thread.send("Testing: reply with 'Hello' then stop.", cx) }) .collect::>() .await; @@ -484,7 +479,7 @@ async fn test_refusal(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); - let events = thread.update(cx, |thread, cx| thread.send(model.clone(), "Hello", cx)); + let events = thread.update(cx, |thread, cx| thread.send("Hello", cx)); cx.run_until_parked(); thread.read_with(cx, |thread, _| { assert_eq!( @@ -648,7 +643,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) { thread.update(cx, |thread, _cx| thread.add_tool(ThinkingTool)); let fake_model = model.as_fake(); - let mut events = thread.update(cx, |thread, cx| thread.send(model.clone(), "Think", cx)); + let mut events = thread.update(cx, |thread, cx| thread.send("Think", cx)); cx.run_until_parked(); // Simulate streaming partial input. diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index f664e0f5d2..8ed200b56b 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -200,11 +200,11 @@ impl Thread { /// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn. pub fn send( &mut self, - model: Arc, content: impl Into, cx: &mut Context, ) -> mpsc::UnboundedReceiver> { let content = content.into(); + let model = self.selected_model.clone(); log::info!("Thread::send called with model: {:?}", model.name()); log::debug!("Thread::send content: {:?}", content); From daa53f276148b6ddb265a67f95de5f9ed7d45e65 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sat, 9 Aug 2025 23:48:58 +0200 Subject: [PATCH 231/693] Revert "Revert "chore: Bump Rust to 1.89 (#35788)"" (#35937) Reverts zed-industries/zed#35843 Docker image for 1.89 is now up. --- Dockerfile-collab | 2 +- crates/fs/src/fake_git_repo.rs | 4 +-- crates/git/src/repository.rs | 8 +++--- crates/gpui/src/keymap/context.rs | 30 +++++++++++--------- crates/gpui/src/platform/windows/wrapper.rs | 24 +--------------- crates/terminal_view/src/terminal_element.rs | 2 +- flake.lock | 18 ++++++------ rust-toolchain.toml | 2 +- 8 files changed, 35 insertions(+), 55 deletions(-) diff --git a/Dockerfile-collab b/Dockerfile-collab index 2dafe296c7..c1621d6ee6 100644 --- a/Dockerfile-collab +++ b/Dockerfile-collab @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.2 -FROM rust:1.88-bookworm as builder +FROM rust:1.89-bookworm as builder WORKDIR app COPY . . diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 04ba656232..73da63fd47 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -402,11 +402,11 @@ impl GitRepository for FakeGitRepository { &self, _paths: Vec, _env: Arc>, - ) -> BoxFuture> { + ) -> BoxFuture<'_, Result<()>> { unimplemented!() } - fn stash_pop(&self, _env: Arc>) -> BoxFuture> { + fn stash_pop(&self, _env: Arc>) -> BoxFuture<'_, Result<()>> { unimplemented!() } diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index dc7ab0af65..518b6c4f46 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -399,9 +399,9 @@ pub trait GitRepository: Send + Sync { &self, paths: Vec, env: Arc>, - ) -> BoxFuture>; + ) -> BoxFuture<'_, Result<()>>; - fn stash_pop(&self, env: Arc>) -> BoxFuture>; + fn stash_pop(&self, env: Arc>) -> BoxFuture<'_, Result<()>>; fn push( &self, @@ -1203,7 +1203,7 @@ impl GitRepository for RealGitRepository { &self, paths: Vec, env: Arc>, - ) -> BoxFuture> { + ) -> BoxFuture<'_, Result<()>> { let working_directory = self.working_directory(); self.executor .spawn(async move { @@ -1227,7 +1227,7 @@ impl GitRepository for RealGitRepository { .boxed() } - fn stash_pop(&self, env: Arc>) -> BoxFuture> { + fn stash_pop(&self, env: Arc>) -> BoxFuture<'_, Result<()>> { let working_directory = self.working_directory(); self.executor .spawn(async move { diff --git a/crates/gpui/src/keymap/context.rs b/crates/gpui/src/keymap/context.rs index f4b878ae77..281035fe97 100644 --- a/crates/gpui/src/keymap/context.rs +++ b/crates/gpui/src/keymap/context.rs @@ -461,6 +461,8 @@ fn skip_whitespace(source: &str) -> &str { #[cfg(test)] mod tests { + use core::slice; + use super::*; use crate as gpui; use KeyBindingContextPredicate::*; @@ -674,11 +676,11 @@ mod tests { assert!(predicate.eval(&contexts)); assert!(!predicate.eval(&[])); - assert!(!predicate.eval(&[child_context.clone()])); + assert!(!predicate.eval(slice::from_ref(&child_context))); assert!(!predicate.eval(&[parent_context])); let zany_predicate = KeyBindingContextPredicate::parse("child > child").unwrap(); - assert!(!zany_predicate.eval(&[child_context.clone()])); + assert!(!zany_predicate.eval(slice::from_ref(&child_context))); assert!(zany_predicate.eval(&[child_context.clone(), child_context.clone()])); } @@ -690,13 +692,13 @@ mod tests { let parent_context = KeyContext::try_from("parent").unwrap(); let child_context = KeyContext::try_from("child").unwrap(); - assert!(not_predicate.eval(&[workspace_context.clone()])); - assert!(!not_predicate.eval(&[editor_context.clone()])); + assert!(not_predicate.eval(slice::from_ref(&workspace_context))); + assert!(!not_predicate.eval(slice::from_ref(&editor_context))); assert!(!not_predicate.eval(&[editor_context.clone(), workspace_context.clone()])); assert!(!not_predicate.eval(&[workspace_context.clone(), editor_context.clone()])); let complex_not = KeyBindingContextPredicate::parse("!editor && workspace").unwrap(); - assert!(complex_not.eval(&[workspace_context.clone()])); + assert!(complex_not.eval(slice::from_ref(&workspace_context))); assert!(!complex_not.eval(&[editor_context.clone(), workspace_context.clone()])); let not_mode_predicate = KeyBindingContextPredicate::parse("!(mode == full)").unwrap(); @@ -709,18 +711,18 @@ mod tests { assert!(not_mode_predicate.eval(&[other_mode_context])); let not_descendant = KeyBindingContextPredicate::parse("!(parent > child)").unwrap(); - assert!(not_descendant.eval(&[parent_context.clone()])); - assert!(not_descendant.eval(&[child_context.clone()])); + assert!(not_descendant.eval(slice::from_ref(&parent_context))); + assert!(not_descendant.eval(slice::from_ref(&child_context))); assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); let not_descendant = KeyBindingContextPredicate::parse("parent > !child").unwrap(); - assert!(!not_descendant.eval(&[parent_context.clone()])); - assert!(!not_descendant.eval(&[child_context.clone()])); + assert!(!not_descendant.eval(slice::from_ref(&parent_context))); + assert!(!not_descendant.eval(slice::from_ref(&child_context))); assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); let double_not = KeyBindingContextPredicate::parse("!!editor").unwrap(); - assert!(double_not.eval(&[editor_context.clone()])); - assert!(!double_not.eval(&[workspace_context.clone()])); + assert!(double_not.eval(slice::from_ref(&editor_context))); + assert!(!double_not.eval(slice::from_ref(&workspace_context))); // Test complex descendant cases let workspace_context = KeyContext::try_from("Workspace").unwrap(); @@ -754,9 +756,9 @@ mod tests { // !Workspace - shouldn't match when Workspace is in the context let not_workspace = KeyBindingContextPredicate::parse("!Workspace").unwrap(); - assert!(!not_workspace.eval(&[workspace_context.clone()])); - assert!(not_workspace.eval(&[pane_context.clone()])); - assert!(not_workspace.eval(&[editor_context.clone()])); + assert!(!not_workspace.eval(slice::from_ref(&workspace_context))); + assert!(not_workspace.eval(slice::from_ref(&pane_context))); + assert!(not_workspace.eval(slice::from_ref(&editor_context))); assert!(!not_workspace.eval(&workspace_pane_editor)); } } diff --git a/crates/gpui/src/platform/windows/wrapper.rs b/crates/gpui/src/platform/windows/wrapper.rs index 6015dffdab..a1fe98a392 100644 --- a/crates/gpui/src/platform/windows/wrapper.rs +++ b/crates/gpui/src/platform/windows/wrapper.rs @@ -1,28 +1,6 @@ use std::ops::Deref; -use windows::Win32::{Foundation::HANDLE, UI::WindowsAndMessaging::HCURSOR}; - -#[derive(Debug, Clone, Copy)] -pub(crate) struct SafeHandle { - raw: HANDLE, -} - -unsafe impl Send for SafeHandle {} -unsafe impl Sync for SafeHandle {} - -impl From for SafeHandle { - fn from(value: HANDLE) -> Self { - SafeHandle { raw: value } - } -} - -impl Deref for SafeHandle { - type Target = HANDLE; - - fn deref(&self) -> &Self::Target { - &self.raw - } -} +use windows::Win32::UI::WindowsAndMessaging::HCURSOR; #[derive(Debug, Clone, Copy)] pub(crate) struct SafeCursor { diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 083c07de9c..6c1be9d5e7 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -136,7 +136,7 @@ impl BatchedTextRun { .shape_line( self.text.clone().into(), self.font_size.to_pixels(window.rem_size()), - &[self.style.clone()], + std::slice::from_ref(&self.style), Some(dimensions.cell_width), ) .paint(pos, dimensions.line_height, window, cx); diff --git a/flake.lock b/flake.lock index fa0d51d90d..80022f7b55 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1750266157, - "narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=", + "lastModified": 1754269165, + "narHash": "sha256-0tcS8FHd4QjbCVoxN9jI+PjHgA4vc/IjkUSp+N3zy0U=", "owner": "ipetkov", "repo": "crane", - "rev": "e37c943371b73ed87faf33f7583860f81f1d5a48", + "rev": "444e81206df3f7d92780680e45858e31d2f07a08", "type": "github" }, "original": { @@ -33,10 +33,10 @@ "nixpkgs": { "locked": { "lastModified": 315532800, - "narHash": "sha256-j+zO+IHQ7VwEam0pjPExdbLT2rVioyVS3iq4bLO3GEc=", - "rev": "61c0f513911459945e2cb8bf333dc849f1b976ff", + "narHash": "sha256-5VYevX3GccubYeccRGAXvCPA1ktrGmIX1IFC0icX07g=", + "rev": "a683adc19ff5228af548c6539dbc3440509bfed3", "type": "tarball", - "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre821324.61c0f5139114/nixexprs.tar.xz" + "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre840248.a683adc19ff5/nixexprs.tar.xz" }, "original": { "type": "tarball", @@ -58,11 +58,11 @@ ] }, "locked": { - "lastModified": 1750964660, - "narHash": "sha256-YQ6EyFetjH1uy5JhdhRdPe6cuNXlYpMAQePFfZj4W7M=", + "lastModified": 1754575663, + "narHash": "sha256-afOx8AG0KYtw7mlt6s6ahBBy7eEHZwws3iCRoiuRQS4=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "04f0fcfb1a50c63529805a798b4b5c21610ff390", + "rev": "6db0fb0e9cec2e9729dc52bf4898e6c135bb8a0f", "type": "github" }, "original": { diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f80eab8fbc..3d87025a27 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.88" +channel = "1.89" profile = "minimal" components = [ "rustfmt", "clippy" ] targets = [ From 2d9cd2ac8888a144ef41e59c9820ffbecee66ed1 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Sat, 9 Aug 2025 22:12:23 -0300 Subject: [PATCH 232/693] Update and refine some icons (#35938) Follow up to https://github.com/zed-industries/zed/pull/35856. Release Notes: - N/A --- assets/icons/arrow_circle.svg | 8 ++++---- assets/icons/blocks.svg | 4 +++- assets/icons/folder_search.svg | 2 +- assets/icons/maximize.svg | 4 ++-- assets/icons/minimize.svg | 8 ++++---- assets/icons/scissors.svg | 4 +++- crates/icons/README.md | 18 +++++++++--------- 7 files changed, 26 insertions(+), 22 deletions(-) diff --git a/assets/icons/arrow_circle.svg b/assets/icons/arrow_circle.svg index 790428702e..76363c6270 100644 --- a/assets/icons/arrow_circle.svg +++ b/assets/icons/arrow_circle.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/blocks.svg b/assets/icons/blocks.svg index 128ca84ef1..e1690e2642 100644 --- a/assets/icons/blocks.svg +++ b/assets/icons/blocks.svg @@ -1 +1,3 @@ - + + + diff --git a/assets/icons/folder_search.svg b/assets/icons/folder_search.svg index 15b0705dd6..d1bc537c98 100644 --- a/assets/icons/folder_search.svg +++ b/assets/icons/folder_search.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/maximize.svg b/assets/icons/maximize.svg index c51b71aaf0..ee03a2c021 100644 --- a/assets/icons/maximize.svg +++ b/assets/icons/maximize.svg @@ -1,6 +1,6 @@ - - + + diff --git a/assets/icons/minimize.svg b/assets/icons/minimize.svg index 97d4699687..ea825f054e 100644 --- a/assets/icons/minimize.svg +++ b/assets/icons/minimize.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/scissors.svg b/assets/icons/scissors.svg index 89d246841e..430293f913 100644 --- a/assets/icons/scissors.svg +++ b/assets/icons/scissors.svg @@ -1 +1,3 @@ - + + + diff --git a/crates/icons/README.md b/crates/icons/README.md index 5fbd6d4948..71bc5c8545 100644 --- a/crates/icons/README.md +++ b/crates/icons/README.md @@ -3,27 +3,27 @@ ## Guidelines Icons are a big part of Zed, and they're how we convey hundreds of actions without relying on labeled buttons. -When introducing a new icon to the set, it's important to ensure it is consistent with the whole set, which follows a few guidelines: +When introducing a new icon, it's important to ensure consistency with the existing set, which follows these guidelines: 1. The SVG view box should be 16x16. 2. For outlined icons, use a 1.5px stroke width. -3. Not all icons are mathematically aligned; there's quite a bit of optical adjustment. But try to keep the icon within an internal 12x12 bounding box as much as possible while ensuring proper visibility. -4. Use the `filled` and `outlined` terminology when introducing icons that will have the two variants. +3. Not all icons are mathematically aligned; there's quite a bit of optical adjustment. However, try to keep the icon within an internal 12x12 bounding box as much as possible while ensuring proper visibility. +4. Use the `filled` and `outlined` terminology when introducing icons that will have these two variants. 5. Icons that are deeply contextual may have the feature context as their name prefix. For example, `ToolWeb`, `ReplPlay`, `DebugStepInto`, etc. -6. Avoid complex layer structure in the icon SVG, like clipping masks and whatnot. When the shape ends up too complex, we recommend running the SVG in [SVGOMG](https://jakearchibald.github.io/svgomg/) to clean it up a bit. +6. Avoid complex layer structures in the icon SVG, like clipping masks and similar elements. When the shape becomes too complex, we recommend running the SVG through [SVGOMG](https://jakearchibald.github.io/svgomg/) to clean it up. ## Sourcing Most icons are created by sourcing them from [Lucide](https://lucide.dev/). Then, they're modified, adjusted, cleaned up, and simplified depending on their use and overall fit with Zed. -Sometimes, we may use other sources like [Phosphor](https://phosphoricons.com/), but we also design many of them completely from scratch. +Sometimes, we may use other sources like [Phosphor](https://phosphoricons.com/), but we also design many icons completely from scratch. ## Contributing -To introduce a new icon, add the `.svg` file in the `assets/icon` directory and then add its corresponding item in the `icons.rs` file within the `crates` directory. +To introduce a new icon, add the `.svg` file to the `assets/icon` directory and then add its corresponding item to the `icons.rs` file within the `crates` directory. -- SVG files in the assets folder follow a snake case name format. -- Icons in the `icons.rs` file follow the pascal case name format. +- SVG files in the assets folder follow a snake_case name format. +- Icons in the `icons.rs` file follow the PascalCase name format. -Ensure you tag a member of Zed's design team so we can adjust and double-check any newly introduced icon. +Make sure to tag a member of Zed's design team so we can review and adjust any newly introduced icon. From 8382afb2ba6f60ddd8d61a150bc97d92baeb209b Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Sun, 10 Aug 2025 17:43:48 +0300 Subject: [PATCH 233/693] evals: Run unit evals CI weekly (#35950) Release Notes: - N/A --- .github/workflows/unit_evals.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit_evals.yml b/.github/workflows/unit_evals.yml index 2e03fb028f..c03cf8b087 100644 --- a/.github/workflows/unit_evals.yml +++ b/.github/workflows/unit_evals.yml @@ -3,7 +3,7 @@ name: Run Unit Evals on: schedule: # GitHub might drop jobs at busy times, so we choose a random time in the middle of the night. - - cron: "47 1 * * *" + - cron: "47 1 * * 2" workflow_dispatch: concurrency: From 9cd5c3656e831a30fe8ef606aff04adf4bba4a60 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sun, 10 Aug 2025 17:19:06 +0200 Subject: [PATCH 234/693] util: Fix crate name extraction for `log_error_with_caller` (#35944) The paths can be absolute, meaning they would just log the initial segment of where the repo was cloned. Release Notes: - N/A --- crates/util/src/util.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 932b519b18..b526f53ce4 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -669,9 +669,12 @@ where let file = caller.file(); #[cfg(target_os = "windows")] let file = caller.file().replace('\\', "/"); - // In this codebase, the first segment of the file path is - // the 'crates' folder, followed by the crate name. - let target = file.split('/').nth(1); + // In this codebase all crates reside in a `crates` directory, + // so discard the prefix up to that segment to find the crate name + let target = file + .split_once("crates/") + .and_then(|(_, s)| s.split_once('/')) + .map(|(p, _)| p); log::logger().log( &log::Record::builder() From 95e302fa68722b8af29e99ed8d3256e1585a8ede Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 10 Aug 2025 21:01:54 +0300 Subject: [PATCH 235/693] Properly use `static` instead of `const` for global types that need a single init (#35955) Release Notes: - N/A --- Cargo.toml | 1 + .../src/edit_agent/create_file_parser.rs | 13 ++++--- crates/docs_preprocessor/src/main.rs | 13 ++++--- crates/gpui/src/platform/mac/platform.rs | 34 +++++++++---------- crates/onboarding/src/theme_preview.rs | 29 ++++++++++------ crates/zeta/src/license_detection.rs | 6 ++-- 6 files changed, 55 insertions(+), 41 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 998e727602..d6ca4c664d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -839,6 +839,7 @@ style = { level = "allow", priority = -1 } module_inception = { level = "deny" } question_mark = { level = "deny" } redundant_closure = { level = "deny" } +declare_interior_mutable_const = { level = "deny" } # Individual rules that have violations in the codebase: type_complexity = "allow" # We often return trait objects from `new` functions. diff --git a/crates/assistant_tools/src/edit_agent/create_file_parser.rs b/crates/assistant_tools/src/edit_agent/create_file_parser.rs index 07c8fac7b9..0aad9ecb87 100644 --- a/crates/assistant_tools/src/edit_agent/create_file_parser.rs +++ b/crates/assistant_tools/src/edit_agent/create_file_parser.rs @@ -1,10 +1,11 @@ +use std::sync::OnceLock; + use regex::Regex; use smallvec::SmallVec; -use std::cell::LazyCell; use util::debug_panic; -const START_MARKER: LazyCell = LazyCell::new(|| Regex::new(r"\n?```\S*\n").unwrap()); -const END_MARKER: LazyCell = LazyCell::new(|| Regex::new(r"(^|\n)```\s*$").unwrap()); +static START_MARKER: OnceLock = OnceLock::new(); +static END_MARKER: OnceLock = OnceLock::new(); #[derive(Debug)] pub enum CreateFileParserEvent { @@ -43,10 +44,12 @@ impl CreateFileParser { self.buffer.push_str(chunk); let mut edit_events = SmallVec::new(); + let start_marker_regex = START_MARKER.get_or_init(|| Regex::new(r"\n?```\S*\n").unwrap()); + let end_marker_regex = END_MARKER.get_or_init(|| Regex::new(r"(^|\n)```\s*$").unwrap()); loop { match &mut self.state { ParserState::Pending => { - if let Some(m) = START_MARKER.find(&self.buffer) { + if let Some(m) = start_marker_regex.find(&self.buffer) { self.buffer.drain(..m.end()); self.state = ParserState::WithinText; } else { @@ -65,7 +68,7 @@ impl CreateFileParser { break; } ParserState::Finishing => { - if let Some(m) = END_MARKER.find(&self.buffer) { + if let Some(m) = end_marker_regex.find(&self.buffer) { self.buffer.drain(m.start()..); } if !self.buffer.is_empty() { diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index 1448f4cb52..17804b4281 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -8,7 +8,7 @@ use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use std::io::{self, Read}; use std::process; -use std::sync::LazyLock; +use std::sync::{LazyLock, OnceLock}; use util::paths::PathExt; static KEYMAP_MACOS: LazyLock = LazyLock::new(|| { @@ -388,7 +388,7 @@ fn handle_postprocessing() -> Result<()> { let meta_title = format!("{} | {}", page_title, meta_title); zlog::trace!(logger => "Updating {:?}", pretty_path(&file, &root_dir)); let contents = contents.replace("#description#", meta_description); - let contents = TITLE_REGEX + let contents = title_regex() .replace(&contents, |_: ®ex::Captures| { format!("{}", meta_title) }) @@ -404,10 +404,8 @@ fn handle_postprocessing() -> Result<()> { ) -> &'a std::path::Path { &path.strip_prefix(&root).unwrap_or(&path) } - const TITLE_REGEX: std::cell::LazyCell = - std::cell::LazyCell::new(|| Regex::new(r"\s*(.*?)\s*").unwrap()); fn extract_title_from_page(contents: &str, pretty_path: &std::path::Path) -> String { - let title_tag_contents = &TITLE_REGEX + let title_tag_contents = &title_regex() .captures(&contents) .with_context(|| format!("Failed to find title in {:?}", pretty_path)) .expect("Page has element")[1]; @@ -420,3 +418,8 @@ fn handle_postprocessing() -> Result<()> { title } } + +fn title_regex() -> &'static Regex { + static TITLE_REGEX: OnceLock<Regex> = OnceLock::new(); + TITLE_REGEX.get_or_init(|| Regex::new(r"<title>\s*(.*?)\s*").unwrap()) +} diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 1d2146cf73..c71eb448c4 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -47,7 +47,7 @@ use objc::{ use parking_lot::Mutex; use ptr::null_mut; use std::{ - cell::{Cell, LazyCell}, + cell::Cell, convert::TryInto, ffi::{CStr, OsStr, c_void}, os::{raw::c_char, unix::ffi::OsStrExt}, @@ -56,7 +56,7 @@ use std::{ ptr, rc::Rc, slice, str, - sync::Arc, + sync::{Arc, OnceLock}, }; use strum::IntoEnumIterator; use util::ResultExt; @@ -296,18 +296,7 @@ impl MacPlatform { actions: &mut Vec>, keymap: &Keymap, ) -> id { - const DEFAULT_CONTEXT: LazyCell> = LazyCell::new(|| { - let mut workspace_context = KeyContext::new_with_defaults(); - workspace_context.add("Workspace"); - let mut pane_context = KeyContext::new_with_defaults(); - pane_context.add("Pane"); - let mut editor_context = KeyContext::new_with_defaults(); - editor_context.add("Editor"); - - pane_context.extend(&editor_context); - workspace_context.extend(&pane_context); - vec![workspace_context] - }); + static DEFAULT_CONTEXT: OnceLock> = OnceLock::new(); unsafe { match item { @@ -323,9 +312,20 @@ impl MacPlatform { let keystrokes = keymap .bindings_for_action(action.as_ref()) .find_or_first(|binding| { - binding - .predicate() - .is_none_or(|predicate| predicate.eval(&DEFAULT_CONTEXT)) + binding.predicate().is_none_or(|predicate| { + predicate.eval(DEFAULT_CONTEXT.get_or_init(|| { + let mut workspace_context = KeyContext::new_with_defaults(); + workspace_context.add("Workspace"); + let mut pane_context = KeyContext::new_with_defaults(); + pane_context.add("Pane"); + let mut editor_context = KeyContext::new_with_defaults(); + editor_context.add("Editor"); + + pane_context.extend(&editor_context); + workspace_context.extend(&pane_context); + vec![workspace_context] + })) + }) }) .map(|binding| binding.keystrokes()); diff --git a/crates/onboarding/src/theme_preview.rs b/crates/onboarding/src/theme_preview.rs index 81eb14ec4b..9d86137b0b 100644 --- a/crates/onboarding/src/theme_preview.rs +++ b/crates/onboarding/src/theme_preview.rs @@ -1,6 +1,9 @@ #![allow(unused, dead_code)] use gpui::{Hsla, Length}; -use std::sync::Arc; +use std::{ + cell::LazyCell, + sync::{Arc, OnceLock}, +}; use theme::{Theme, ThemeColors, ThemeRegistry}; use ui::{ IntoElement, RenderOnce, component_prelude::Documented, prelude::*, utils::inner_corner_radius, @@ -22,6 +25,18 @@ pub struct ThemePreviewTile { style: ThemePreviewStyle, } +fn child_radius() -> Pixels { + static CHILD_RADIUS: OnceLock = OnceLock::new(); + *CHILD_RADIUS.get_or_init(|| { + inner_corner_radius( + ThemePreviewTile::ROOT_RADIUS, + ThemePreviewTile::ROOT_BORDER, + ThemePreviewTile::ROOT_PADDING, + ThemePreviewTile::CHILD_BORDER, + ) + }) +} + impl ThemePreviewTile { pub const SKELETON_HEIGHT_DEFAULT: Pixels = px(2.); pub const SIDEBAR_SKELETON_ITEM_COUNT: usize = 8; @@ -30,14 +45,6 @@ impl ThemePreviewTile { pub const ROOT_BORDER: Pixels = px(2.0); pub const ROOT_PADDING: Pixels = px(2.0); pub const CHILD_BORDER: Pixels = px(1.0); - pub const CHILD_RADIUS: std::cell::LazyCell = std::cell::LazyCell::new(|| { - inner_corner_radius( - Self::ROOT_RADIUS, - Self::ROOT_BORDER, - Self::ROOT_PADDING, - Self::CHILD_BORDER, - ) - }); pub fn new(theme: Arc, seed: f32) -> Self { Self { @@ -222,7 +229,7 @@ impl ThemePreviewTile { .child( div() .size_full() - .rounded(*Self::CHILD_RADIUS) + .rounded(child_radius()) .border(Self::CHILD_BORDER) .border_color(theme.colors().border) .child(Self::render_editor( @@ -250,7 +257,7 @@ impl ThemePreviewTile { h_flex() .size_full() .relative() - .rounded(*Self::CHILD_RADIUS) + .rounded(child_radius()) .border(Self::CHILD_BORDER) .border_color(border_color) .overflow_hidden() diff --git a/crates/zeta/src/license_detection.rs b/crates/zeta/src/license_detection.rs index c55f8d5d08..fa1eabf524 100644 --- a/crates/zeta/src/license_detection.rs +++ b/crates/zeta/src/license_detection.rs @@ -14,7 +14,7 @@ use util::ResultExt as _; use worktree::ChildEntriesOptions; /// Matches the most common license locations, with US and UK English spelling. -const LICENSE_FILE_NAME_REGEX: LazyLock = LazyLock::new(|| { +static LICENSE_FILE_NAME_REGEX: LazyLock = LazyLock::new(|| { regex::bytes::RegexBuilder::new( "^ \ (?: license | licence) \ @@ -29,7 +29,7 @@ const LICENSE_FILE_NAME_REGEX: LazyLock = LazyLock::new(|| }); fn is_license_eligible_for_data_collection(license: &str) -> bool { - const LICENSE_REGEXES: LazyLock> = LazyLock::new(|| { + static LICENSE_REGEXES: LazyLock> = LazyLock::new(|| { [ include_str!("license_detection/apache.regex"), include_str!("license_detection/isc.regex"), @@ -47,7 +47,7 @@ fn is_license_eligible_for_data_collection(license: &str) -> bool { /// Canonicalizes the whitespace of license text and license regexes. fn canonicalize_license_text(license: &str) -> String { - const PARAGRAPH_SEPARATOR_REGEX: LazyLock = + static PARAGRAPH_SEPARATOR_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"\s*\n\s*\n\s*").unwrap()); PARAGRAPH_SEPARATOR_REGEX From f3d6deb5a319af86a68b797a37be86f2f4c288a9 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Sun, 10 Aug 2025 15:23:27 -0300 Subject: [PATCH 236/693] debugger: Add refinements to the UI (#35940) Took a little bit of time to add just a handful of small tweaks to the debugger UI so it looks slightly more polished. This PR includes adjustments to size, focus styles, and more in icon buttons, overall spacing nudges in each section pane, making tooltip labels title case (for overall consistency), and some icon SVG iteration. Release Notes: - N/A --- .../icons/debug_disabled_log_breakpoint.svg | 4 +- assets/icons/debug_ignore_breakpoints.svg | 4 +- assets/icons/debug_log_breakpoint.svg | 2 +- crates/debugger_ui/src/debugger_panel.rs | 143 +++++----- crates/debugger_ui/src/session/running.rs | 27 +- .../src/session/running/breakpoint_list.rs | 266 ++++++++++-------- .../src/session/running/console.rs | 15 +- .../src/session/running/memory_view.rs | 6 +- 8 files changed, 263 insertions(+), 204 deletions(-) diff --git a/assets/icons/debug_disabled_log_breakpoint.svg b/assets/icons/debug_disabled_log_breakpoint.svg index a028ead3a0..2ccc37623d 100644 --- a/assets/icons/debug_disabled_log_breakpoint.svg +++ b/assets/icons/debug_disabled_log_breakpoint.svg @@ -1,3 +1,5 @@ - + + + diff --git a/assets/icons/debug_ignore_breakpoints.svg b/assets/icons/debug_ignore_breakpoints.svg index a0bbabfb26..b2a345d314 100644 --- a/assets/icons/debug_ignore_breakpoints.svg +++ b/assets/icons/debug_ignore_breakpoints.svg @@ -1 +1,3 @@ - + + + diff --git a/assets/icons/debug_log_breakpoint.svg b/assets/icons/debug_log_breakpoint.svg index 7c652db1e9..22eae9d029 100644 --- a/assets/icons/debug_log_breakpoint.svg +++ b/assets/icons/debug_log_breakpoint.svg @@ -1,3 +1,3 @@ - + diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 91382c74ae..1d44c5c244 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -36,7 +36,7 @@ use settings::Settings; use std::sync::{Arc, LazyLock}; use task::{DebugScenario, TaskContext}; use tree_sitter::{Query, StreamingIterator as _}; -use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*}; +use ui::{ContextMenu, Divider, PopoverMenuHandle, Tab, Tooltip, prelude::*}; use util::{ResultExt, debug_panic, maybe}; use workspace::SplitDirection; use workspace::item::SaveOptions; @@ -642,12 +642,14 @@ impl DebugPanel { } }) }; + let documentation_button = || { IconButton::new("debug-open-documentation", IconName::CircleHelp) .icon_size(IconSize::Small) .on_click(move |_, _, cx| cx.open_url("https://zed.dev/docs/debugger")) .tooltip(Tooltip::text("Open Documentation")) }; + let logs_button = || { IconButton::new("debug-open-logs", IconName::Notepad) .icon_size(IconSize::Small) @@ -658,16 +660,18 @@ impl DebugPanel { }; Some( - div.border_b_1() - .border_color(cx.theme().colors().border) - .p_1() + div.w_full() + .py_1() + .px_1p5() .justify_between() - .w_full() + .border_b_1() + .border_color(cx.theme().colors().border) .when(is_side, |this| this.gap_1()) .child( h_flex() + .justify_between() .child( - h_flex().gap_2().w_full().when_some( + h_flex().gap_1().w_full().when_some( active_session .as_ref() .map(|session| session.read(cx).running_state()), @@ -679,6 +683,7 @@ impl DebugPanel { let capabilities = running_state.read(cx).capabilities(cx); let supports_detach = running_state.read(cx).session().read(cx).is_attached(); + this.map(|this| { if thread_status == ThreadStatus::Running { this.child( @@ -686,8 +691,7 @@ impl DebugPanel { "debug-pause", IconName::DebugPause, ) - .icon_size(IconSize::XSmall) - .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) .on_click(window.listener_for( &running_state, |this, _, _window, cx| { @@ -698,7 +702,7 @@ impl DebugPanel { let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( - "Pause program", + "Pause Program", &Pause, &focus_handle, window, @@ -713,8 +717,7 @@ impl DebugPanel { "debug-continue", IconName::DebugContinue, ) - .icon_size(IconSize::XSmall) - .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) .on_click(window.listener_for( &running_state, |this, _, _window, cx| this.continue_thread(cx), @@ -724,7 +727,7 @@ impl DebugPanel { let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( - "Continue program", + "Continue Program", &Continue, &focus_handle, window, @@ -737,8 +740,7 @@ impl DebugPanel { }) .child( IconButton::new("debug-step-over", IconName::ArrowRight) - .icon_size(IconSize::XSmall) - .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) .on_click(window.listener_for( &running_state, |this, _, _window, cx| { @@ -750,7 +752,7 @@ impl DebugPanel { let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( - "Step over", + "Step Over", &StepOver, &focus_handle, window, @@ -764,8 +766,7 @@ impl DebugPanel { "debug-step-into", IconName::ArrowDownRight, ) - .icon_size(IconSize::XSmall) - .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) .on_click(window.listener_for( &running_state, |this, _, _window, cx| { @@ -777,7 +778,7 @@ impl DebugPanel { let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( - "Step in", + "Step In", &StepInto, &focus_handle, window, @@ -789,7 +790,6 @@ impl DebugPanel { .child( IconButton::new("debug-step-out", IconName::ArrowUpRight) .icon_size(IconSize::Small) - .shape(ui::IconButtonShape::Square) .on_click(window.listener_for( &running_state, |this, _, _window, cx| { @@ -801,7 +801,7 @@ impl DebugPanel { let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( - "Step out", + "Step Out", &StepOut, &focus_handle, window, @@ -813,7 +813,7 @@ impl DebugPanel { .child(Divider::vertical()) .child( IconButton::new("debug-restart", IconName::RotateCcw) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .on_click(window.listener_for( &running_state, |this, _, window, cx| { @@ -835,7 +835,7 @@ impl DebugPanel { ) .child( IconButton::new("debug-stop", IconName::Power) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .on_click(window.listener_for( &running_state, |this, _, _window, cx| { @@ -890,7 +890,7 @@ impl DebugPanel { thread_status != ThreadStatus::Stopped && thread_status != ThreadStatus::Running, ) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .on_click(window.listener_for( &running_state, |this, _, _, cx| { @@ -915,7 +915,6 @@ impl DebugPanel { }, ), ) - .justify_around() .when(is_side, |this| { this.child(new_session_button()) .child(logs_button()) @@ -924,7 +923,7 @@ impl DebugPanel { ) .child( h_flex() - .gap_2() + .gap_0p5() .when(is_side, |this| this.justify_between()) .child( h_flex().when_some( @@ -954,12 +953,15 @@ impl DebugPanel { ) }) }) - .when(!is_side, |this| this.gap_2().child(Divider::vertical())) + .when(!is_side, |this| { + this.gap_0p5().child(Divider::vertical()) + }) }, ), ) .child( h_flex() + .gap_0p5() .children(self.render_session_menu( self.active_session(), self.running_state(cx), @@ -1702,6 +1704,7 @@ impl Render for DebugPanel { this.child(active_session) } else { let docked_to_bottom = self.position(window, cx) == DockPosition::Bottom; + let welcome_experience = v_flex() .when_else( docked_to_bottom, @@ -1767,54 +1770,58 @@ impl Render for DebugPanel { ); }), ); - let breakpoint_list = - v_flex() - .group("base-breakpoint-list") - .items_start() - .when_else( - docked_to_bottom, - |this| this.min_w_1_3().h_full(), - |this| this.w_full().h_2_3(), - ) - .p_1() - .child( - h_flex() - .pl_1() - .w_full() - .justify_between() - .child(Label::new("Breakpoints").size(LabelSize::Small)) - .child(h_flex().visible_on_hover("base-breakpoint-list").child( + + let breakpoint_list = v_flex() + .group("base-breakpoint-list") + .when_else( + docked_to_bottom, + |this| this.min_w_1_3().h_full(), + |this| this.size_full().h_2_3(), + ) + .child( + h_flex() + .track_focus(&self.breakpoint_list.focus_handle(cx)) + .h(Tab::container_height(cx)) + .p_1p5() + .w_full() + .justify_between() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .child(Label::new("Breakpoints").size(LabelSize::Small)) + .child( + h_flex().visible_on_hover("base-breakpoint-list").child( self.breakpoint_list.read(cx).render_control_strip(), - )) - .track_focus(&self.breakpoint_list.focus_handle(cx)), - ) - .child(Divider::horizontal()) - .child(self.breakpoint_list.clone()); + ), + ), + ) + .child(self.breakpoint_list.clone()); + this.child( v_flex() - .h_full() + .size_full() .gap_1() .items_center() .justify_center() - .child( - div() - .when_else(docked_to_bottom, Div::h_flex, Div::v_flex) - .size_full() - .map(|this| { - if docked_to_bottom { - this.items_start() - .child(breakpoint_list) - .child(Divider::vertical()) - .child(welcome_experience) - .child(Divider::vertical()) - } else { - this.items_end() - .child(welcome_experience) - .child(Divider::horizontal()) - .child(breakpoint_list) - } - }), - ), + .map(|this| { + if docked_to_bottom { + this.child( + h_flex() + .size_full() + .child(breakpoint_list) + .child(Divider::vertical()) + .child(welcome_experience) + .child(Divider::vertical()), + ) + } else { + this.child( + v_flex() + .size_full() + .child(welcome_experience) + .child(Divider::horizontal()) + .child(breakpoint_list), + ) + } + }), ) } }) diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index a3e2805e2b..c8bee42039 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -48,10 +48,8 @@ use task::{ }; use terminal_view::TerminalView; use ui::{ - ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, FluentBuilder, - IconButton, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon as _, - ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Tab, Tooltip, - VisibleOnHover, VisualContext, Window, div, h_flex, v_flex, + FluentBuilder, IntoElement, Render, StatefulInteractiveElement, Tab, Tooltip, VisibleOnHover, + VisualContext, prelude::*, }; use util::ResultExt; use variable_list::VariableList; @@ -419,13 +417,14 @@ pub(crate) fn new_debugger_pane( .map_or(false, |item| item.read(cx).hovered); h_flex() - .group(pane_group_id.clone()) - .justify_between() - .bg(cx.theme().colors().tab_bar_background) - .border_b_1() - .px_2() - .border_color(cx.theme().colors().border) .track_focus(&focus_handle) + .group(pane_group_id.clone()) + .pl_1p5() + .pr_1() + .justify_between() + .border_b_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().tab_bar_background) .on_action(|_: &menu::Cancel, window, cx| { if cx.stop_active_drag(window) { return; @@ -514,6 +513,7 @@ pub(crate) fn new_debugger_pane( ) .child({ let zoomed = pane.is_zoomed(); + h_flex() .visible_on_hover(pane_group_id) .when(is_hovered, |this| this.visible()) @@ -537,7 +537,7 @@ pub(crate) fn new_debugger_pane( IconName::Maximize }, ) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .on_click(cx.listener(move |pane, _, _, cx| { let is_zoomed = pane.is_zoomed(); pane.set_zoomed(!is_zoomed, cx); @@ -592,10 +592,11 @@ impl DebugTerminal { } impl gpui::Render for DebugTerminal { - fn render(&mut self, _window: &mut Window, _: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { div() - .size_full() .track_focus(&self.focus_handle) + .size_full() + .bg(cx.theme().colors().editor_background) .children(self.terminal.clone()) } } diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index 326fb84e20..38108dbfbc 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -23,11 +23,8 @@ use project::{ worktree_store::WorktreeStore, }; use ui::{ - ActiveTheme, AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div, - Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, InteractiveElement, - IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, Render, RenderOnce, - Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Toggleable, - Tooltip, Window, div, h_flex, px, v_flex, + Divider, DividerColor, FluentBuilder as _, Indicator, IntoElement, ListItem, Render, Scrollbar, + ScrollbarState, StatefulInteractiveElement, Tooltip, prelude::*, }; use workspace::Workspace; use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint}; @@ -569,6 +566,7 @@ impl BreakpointList { .map(|session| SupportedBreakpointProperties::from(session.read(cx).capabilities())) .unwrap_or_else(SupportedBreakpointProperties::empty); let strip_mode = self.strip_mode; + uniform_list( "breakpoint-list", self.breakpoints.len(), @@ -591,7 +589,7 @@ impl BreakpointList { }), ) .track_scroll(self.scroll_handle.clone()) - .flex_grow() + .flex_1() } fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ @@ -630,6 +628,7 @@ impl BreakpointList { pub(crate) fn render_control_strip(&self) -> AnyElement { let selection_kind = self.selection_kind(); let focus_handle = self.focus_handle.clone(); + let remove_breakpoint_tooltip = selection_kind.map(|(kind, _)| match kind { SelectedBreakpointKind::Source => "Remove breakpoint from a breakpoint list", SelectedBreakpointKind::Exception => { @@ -637,6 +636,7 @@ impl BreakpointList { } SelectedBreakpointKind::Data => "Remove data breakpoint from a breakpoint list", }); + let toggle_label = selection_kind.map(|(_, is_enabled)| { if is_enabled { ( @@ -649,13 +649,12 @@ impl BreakpointList { }); h_flex() - .gap_2() .child( IconButton::new( "disable-breakpoint-breakpoint-list", IconName::DebugDisabledBreakpoint, ) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .when_some(toggle_label, |this, (label, meta)| { this.tooltip({ let focus_handle = focus_handle.clone(); @@ -681,9 +680,8 @@ impl BreakpointList { }), ) .child( - IconButton::new("remove-breakpoint-breakpoint-list", IconName::Close) - .icon_size(IconSize::XSmall) - .icon_color(ui::Color::Error) + IconButton::new("remove-breakpoint-breakpoint-list", IconName::Trash) + .icon_size(IconSize::Small) .when_some(remove_breakpoint_tooltip, |this, tooltip| { this.tooltip({ let focus_handle = focus_handle.clone(); @@ -710,7 +708,6 @@ impl BreakpointList { } }), ) - .mr_2() .into_any_element() } } @@ -791,6 +788,7 @@ impl Render for BreakpointList { .chain(data_breakpoints) .chain(exception_breakpoints), ); + v_flex() .id("breakpoint-list") .key_context("BreakpointList") @@ -806,35 +804,33 @@ impl Render for BreakpointList { .on_action(cx.listener(Self::next_breakpoint_property)) .on_action(cx.listener(Self::previous_breakpoint_property)) .size_full() - .m_0p5() - .child( - v_flex() - .size_full() - .child(self.render_list(cx)) - .child(self.render_vertical_scrollbar(cx)), - ) + .pt_1() + .child(self.render_list(cx)) + .child(self.render_vertical_scrollbar(cx)) .when_some(self.strip_mode, |this, _| { - this.child(Divider::horizontal()).child( - h_flex() - // .w_full() - .m_0p5() - .p_0p5() - .border_1() - .rounded_sm() - .when( - self.input.focus_handle(cx).contains_focused(window, cx), - |this| { - let colors = cx.theme().colors(); - let border = if self.input.read(cx).read_only(cx) { - colors.border_disabled - } else { - colors.border_focused - }; - this.border_color(border) - }, - ) - .child(self.input.clone()), - ) + this.child(Divider::horizontal().color(DividerColor::Border)) + .child( + h_flex() + .p_1() + .rounded_sm() + .bg(cx.theme().colors().editor_background) + .border_1() + .when( + self.input.focus_handle(cx).contains_focused(window, cx), + |this| { + let colors = cx.theme().colors(); + + let border_color = if self.input.read(cx).read_only(cx) { + colors.border_disabled + } else { + colors.border_transparent + }; + + this.border_color(border_color) + }, + ) + .child(self.input.clone()), + ) }) } } @@ -865,12 +861,17 @@ impl LineBreakpoint { let path = self.breakpoint.path.clone(); let row = self.breakpoint.row; let is_enabled = self.breakpoint.state.is_enabled(); + let indicator = div() .id(SharedString::from(format!( "breakpoint-ui-toggle-{:?}/{}:{}", self.dir, self.name, self.line ))) - .cursor_pointer() + .child( + Icon::new(icon_name) + .color(Color::Debugger) + .size(IconSize::XSmall), + ) .tooltip({ let focus_handle = focus_handle.clone(); move |window, cx| { @@ -902,17 +903,14 @@ impl LineBreakpoint { .ok(); } }) - .child( - Icon::new(icon_name) - .color(Color::Debugger) - .size(IconSize::XSmall), - ) .on_mouse_down(MouseButton::Left, move |_, _, _| {}); ListItem::new(SharedString::from(format!( "breakpoint-ui-item-{:?}/{}:{}", self.dir, self.name, self.line ))) + .toggle_state(is_selected) + .inset(true) .on_click({ let weak = weak.clone(); move |_, window, cx| { @@ -922,23 +920,20 @@ impl LineBreakpoint { .ok(); } }) - .start_slot(indicator) - .rounded() .on_secondary_mouse_down(|_, _, cx| { cx.stop_propagation(); }) + .start_slot(indicator) .child( h_flex() - .w_full() - .mr_4() - .py_0p5() - .gap_1() - .min_h(px(26.)) - .justify_between() .id(SharedString::from(format!( "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}", self.dir, self.name, self.line ))) + .w_full() + .gap_1() + .min_h(rems_from_px(26.)) + .justify_between() .on_click({ let weak = weak.clone(); move |_, window, cx| { @@ -949,9 +944,9 @@ impl LineBreakpoint { .ok(); } }) - .cursor_pointer() .child( h_flex() + .id("label-container") .gap_0p5() .child( Label::new(format!("{}:{}", self.name, self.line)) @@ -971,11 +966,13 @@ impl LineBreakpoint { .line_height_style(ui::LineHeightStyle::UiLabel) .truncate(), ) - })), + })) + .when_some(self.dir.as_ref(), |this, parent_dir| { + this.tooltip(Tooltip::text(format!( + "Worktree parent path: {parent_dir}" + ))) + }), ) - .when_some(self.dir.as_ref(), |this, parent_dir| { - this.tooltip(Tooltip::text(format!("Worktree parent path: {parent_dir}"))) - }) .child(BreakpointOptionsStrip { props, breakpoint: BreakpointEntry { @@ -988,15 +985,16 @@ impl LineBreakpoint { index: ix, }), ) - .toggle_state(is_selected) } } + #[derive(Clone, Debug)] struct ExceptionBreakpoint { id: String, data: ExceptionBreakpointsFilter, is_enabled: bool, } + #[derive(Clone, Debug)] struct DataBreakpoint(project::debugger::session::DataBreakpointState); @@ -1017,17 +1015,24 @@ impl DataBreakpoint { }; let is_enabled = self.0.is_enabled; let id = self.0.dap.data_id.clone(); + ListItem::new(SharedString::from(format!( "data-breakpoint-ui-item-{}", self.0.dap.data_id ))) - .rounded() + .toggle_state(is_selected) + .inset(true) .start_slot( div() .id(SharedString::from(format!( "data-breakpoint-ui-item-{}-click-handler", self.0.dap.data_id ))) + .child( + Icon::new(IconName::Binary) + .color(color) + .size(IconSize::Small), + ) .tooltip({ let focus_handle = focus_handle.clone(); move |window, cx| { @@ -1052,25 +1057,18 @@ impl DataBreakpoint { }) .ok(); } - }) - .cursor_pointer() - .child( - Icon::new(IconName::Binary) - .color(color) - .size(IconSize::Small), - ), + }), ) .child( h_flex() .w_full() - .mr_4() - .py_0p5() + .gap_1() + .min_h(rems_from_px(26.)) .justify_between() .child( v_flex() .py_1() .gap_1() - .min_h(px(26.)) .justify_center() .id(("data-breakpoint-label", ix)) .child( @@ -1091,7 +1089,6 @@ impl DataBreakpoint { index: ix, }), ) - .toggle_state(is_selected) } } @@ -1113,10 +1110,13 @@ impl ExceptionBreakpoint { let id = SharedString::from(&self.id); let is_enabled = self.is_enabled; let weak = list.clone(); + ListItem::new(SharedString::from(format!( "exception-breakpoint-ui-item-{}", self.id ))) + .toggle_state(is_selected) + .inset(true) .on_click({ let list = list.clone(); move |_, window, cx| { @@ -1124,7 +1124,6 @@ impl ExceptionBreakpoint { .ok(); } }) - .rounded() .on_secondary_mouse_down(|_, _, cx| { cx.stop_propagation(); }) @@ -1134,6 +1133,11 @@ impl ExceptionBreakpoint { "exception-breakpoint-ui-item-{}-click-handler", self.id ))) + .child( + Icon::new(IconName::Flame) + .color(color) + .size(IconSize::Small), + ) .tooltip({ let focus_handle = focus_handle.clone(); move |window, cx| { @@ -1158,25 +1162,18 @@ impl ExceptionBreakpoint { }) .ok(); } - }) - .cursor_pointer() - .child( - Icon::new(IconName::Flame) - .color(color) - .size(IconSize::Small), - ), + }), ) .child( h_flex() .w_full() - .mr_4() - .py_0p5() + .gap_1() + .min_h(rems_from_px(26.)) .justify_between() .child( v_flex() .py_1() .gap_1() - .min_h(px(26.)) .justify_center() .id(("exception-breakpoint-label", ix)) .child( @@ -1200,7 +1197,6 @@ impl ExceptionBreakpoint { index: ix, }), ) - .toggle_state(is_selected) } } #[derive(Clone, Debug)] @@ -1302,6 +1298,7 @@ impl BreakpointEntry { } } } + bitflags::bitflags! { #[derive(Clone, Copy)] pub struct SupportedBreakpointProperties: u32 { @@ -1360,6 +1357,7 @@ impl BreakpointOptionsStrip { fn is_toggled(&self, expected_mode: ActiveBreakpointStripMode) -> bool { self.is_selected && self.strip_mode == Some(expected_mode) } + fn on_click_callback( &self, mode: ActiveBreakpointStripMode, @@ -1379,7 +1377,8 @@ impl BreakpointOptionsStrip { .ok(); } } - fn add_border( + + fn add_focus_styles( &self, kind: ActiveBreakpointStripMode, available: bool, @@ -1388,22 +1387,25 @@ impl BreakpointOptionsStrip { ) -> impl Fn(Div) -> Div { move |this: Div| { // Avoid layout shifts in case there's no colored border - let this = this.border_2().rounded_sm(); + let this = this.border_1().rounded_sm(); + let color = cx.theme().colors(); + if self.is_selected && self.strip_mode == Some(kind) { - let theme = cx.theme().colors(); if self.focus_handle.is_focused(window) { - this.border_color(theme.border_selected) + this.bg(color.editor_background) + .border_color(color.border_focused) } else { - this.border_color(theme.border_disabled) + this.border_color(color.border) } } else if !available { - this.border_color(cx.theme().colors().border_disabled) + this.border_color(color.border_transparent) } else { this } } } } + impl RenderOnce for BreakpointOptionsStrip { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let id = self.breakpoint.id(); @@ -1426,73 +1428,117 @@ impl RenderOnce for BreakpointOptionsStrip { }; let color_for_toggle = |is_enabled| { if is_enabled { - ui::Color::Default + Color::Default } else { - ui::Color::Muted + Color::Muted } }; h_flex() - .gap_1() + .gap_px() + .mr_3() // Space to avoid overlapping with the scrollbar .child( - div().map(self.add_border(ActiveBreakpointStripMode::Log, supports_logs, window, cx)) + div() + .map(self.add_focus_styles( + ActiveBreakpointStripMode::Log, + supports_logs, + window, + cx, + )) .child( IconButton::new( SharedString::from(format!("{id}-log-toggle")), IconName::Notepad, ) - .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) .style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs)) + .icon_size(IconSize::Small) .icon_color(color_for_toggle(has_logs)) + .when(has_logs, |this| this.indicator(Indicator::dot().color(Color::Info))) .disabled(!supports_logs) .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log)) - .on_click(self.on_click_callback(ActiveBreakpointStripMode::Log)).tooltip(|window, cx| Tooltip::with_meta("Set Log Message", None, "Set log message to display (instead of stopping) when a breakpoint is hit", window, cx)) + .on_click(self.on_click_callback(ActiveBreakpointStripMode::Log)) + .tooltip(|window, cx| { + Tooltip::with_meta( + "Set Log Message", + None, + "Set log message to display (instead of stopping) when a breakpoint is hit.", + window, + cx, + ) + }), ) .when(!has_logs && !self.is_selected, |this| this.invisible()), ) .child( - div().map(self.add_border( - ActiveBreakpointStripMode::Condition, - supports_condition, - window, cx - )) + div() + .map(self.add_focus_styles( + ActiveBreakpointStripMode::Condition, + supports_condition, + window, + cx, + )) .child( IconButton::new( SharedString::from(format!("{id}-condition-toggle")), IconName::SplitAlt, ) - .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) .style(style_for_toggle( ActiveBreakpointStripMode::Condition, - has_condition + has_condition, )) + .icon_size(IconSize::Small) .icon_color(color_for_toggle(has_condition)) + .when(has_condition, |this| this.indicator(Indicator::dot().color(Color::Info))) .disabled(!supports_condition) .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition)) .on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition)) - .tooltip(|window, cx| Tooltip::with_meta("Set Condition", None, "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met", window, cx)) + .tooltip(|window, cx| { + Tooltip::with_meta( + "Set Condition", + None, + "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met.", + window, + cx, + ) + }), ) .when(!has_condition && !self.is_selected, |this| this.invisible()), ) .child( - div().map(self.add_border( - ActiveBreakpointStripMode::HitCondition, - supports_hit_condition,window, cx - )) + div() + .map(self.add_focus_styles( + ActiveBreakpointStripMode::HitCondition, + supports_hit_condition, + window, + cx, + )) .child( IconButton::new( SharedString::from(format!("{id}-hit-condition-toggle")), IconName::ArrowDown10, ) - .icon_size(IconSize::XSmall) .style(style_for_toggle( ActiveBreakpointStripMode::HitCondition, has_hit_condition, )) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) .icon_color(color_for_toggle(has_hit_condition)) + .when(has_hit_condition, |this| this.indicator(Indicator::dot().color(Color::Info))) .disabled(!supports_hit_condition) .toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition)) - .on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition)).tooltip(|window, cx| Tooltip::with_meta("Set Hit Condition", None, "Set expression that controls how many hits of the breakpoint are ignored.", window, cx)) + .on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition)) + .tooltip(|window, cx| { + Tooltip::with_meta( + "Set Hit Condition", + None, + "Set expression that controls how many hits of the breakpoint are ignored.", + window, + cx, + ) + }), ) .when(!has_hit_condition && !self.is_selected, |this| { this.invisible() diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index daf4486f81..e6308518e4 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -367,7 +367,7 @@ impl Console { .when_some(keybinding_target.clone(), |el, keybinding_target| { el.context(keybinding_target.clone()) }) - .action("Watch expression", WatchExpression.boxed_clone()) + .action("Watch Expression", WatchExpression.boxed_clone()) })) }) }, @@ -452,18 +452,22 @@ impl Render for Console { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let query_focus_handle = self.query_bar.focus_handle(cx); self.update_output(window, cx); + v_flex() .track_focus(&self.focus_handle) .key_context("DebugConsole") .on_action(cx.listener(Self::evaluate)) .on_action(cx.listener(Self::watch_expression)) .size_full() + .border_2() + .bg(cx.theme().colors().editor_background) .child(self.render_console(cx)) .when(self.is_running(cx), |this| { this.child(Divider::horizontal()).child( h_flex() .on_action(cx.listener(Self::previous_query)) .on_action(cx.listener(Self::next_query)) + .p_1() .gap_1() .bg(cx.theme().colors().editor_background) .child(self.render_query_bar(cx)) @@ -474,6 +478,9 @@ impl Render for Console { .on_click(move |_, window, cx| { window.dispatch_action(Box::new(Confirm), cx) }) + .layer(ui::ElevationIndex::ModalSurface) + .size(ui::ButtonSize::Compact) + .child(Label::new("Evaluate")) .tooltip({ let query_focus_handle = query_focus_handle.clone(); @@ -486,10 +493,7 @@ impl Render for Console { cx, ) } - }) - .layer(ui::ElevationIndex::ModalSurface) - .size(ui::ButtonSize::Compact) - .child(Label::new("Evaluate")), + }), self.render_submit_menu( ElementId::Name("split-button-right-confirm-button".into()), Some(query_focus_handle.clone()), @@ -499,7 +503,6 @@ impl Render for Console { )), ) }) - .border_2() } } diff --git a/crates/debugger_ui/src/session/running/memory_view.rs b/crates/debugger_ui/src/session/running/memory_view.rs index 75b8938371..f936d908b1 100644 --- a/crates/debugger_ui/src/session/running/memory_view.rs +++ b/crates/debugger_ui/src/session/running/memory_view.rs @@ -18,10 +18,8 @@ use project::debugger::{MemoryCell, dap_command::DataBreakpointContext, session: use settings::Settings; use theme::ThemeSettings; use ui::{ - ActiveTheme, AnyElement, App, Color, Context, ContextMenu, Div, Divider, DropdownMenu, Element, - FluentBuilder, Icon, IconName, InteractiveElement, IntoElement, Label, LabelCommon, - ParentElement, Pixels, PopoverMenuHandle, Render, Scrollbar, ScrollbarState, SharedString, - StatefulInteractiveElement, Styled, TextSize, Tooltip, Window, div, h_flex, px, v_flex, + ContextMenu, Divider, DropdownMenu, FluentBuilder, IntoElement, PopoverMenuHandle, Render, + Scrollbar, ScrollbarState, StatefulInteractiveElement, Tooltip, prelude::*, }; use workspace::Workspace; From 6bd2f8758ee0d81aa6f31e0590f1f2270847ba9c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 10 Aug 2025 22:32:25 +0300 Subject: [PATCH 237/693] Simplify the lock usage (#35957) Follow-up of https://github.com/zed-industries/zed/pull/35955 Release Notes: - N/A Co-authored-by: Piotr Osiewicz --- crates/onboarding/src/theme_preview.rs | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/crates/onboarding/src/theme_preview.rs b/crates/onboarding/src/theme_preview.rs index 9d86137b0b..9f299eb6ea 100644 --- a/crates/onboarding/src/theme_preview.rs +++ b/crates/onboarding/src/theme_preview.rs @@ -2,7 +2,7 @@ use gpui::{Hsla, Length}; use std::{ cell::LazyCell, - sync::{Arc, OnceLock}, + sync::{Arc, LazyLock, OnceLock}, }; use theme::{Theme, ThemeColors, ThemeRegistry}; use ui::{ @@ -25,17 +25,14 @@ pub struct ThemePreviewTile { style: ThemePreviewStyle, } -fn child_radius() -> Pixels { - static CHILD_RADIUS: OnceLock = OnceLock::new(); - *CHILD_RADIUS.get_or_init(|| { - inner_corner_radius( - ThemePreviewTile::ROOT_RADIUS, - ThemePreviewTile::ROOT_BORDER, - ThemePreviewTile::ROOT_PADDING, - ThemePreviewTile::CHILD_BORDER, - ) - }) -} +static CHILD_RADIUS: LazyLock = LazyLock::new(|| { + inner_corner_radius( + ThemePreviewTile::ROOT_RADIUS, + ThemePreviewTile::ROOT_BORDER, + ThemePreviewTile::ROOT_PADDING, + ThemePreviewTile::CHILD_BORDER, + ) +}); impl ThemePreviewTile { pub const SKELETON_HEIGHT_DEFAULT: Pixels = px(2.); @@ -229,7 +226,7 @@ impl ThemePreviewTile { .child( div() .size_full() - .rounded(child_radius()) + .rounded(*CHILD_RADIUS) .border(Self::CHILD_BORDER) .border_color(theme.colors().border) .child(Self::render_editor( @@ -257,7 +254,7 @@ impl ThemePreviewTile { h_flex() .size_full() .relative() - .rounded(child_radius()) + .rounded(*CHILD_RADIUS) .border(Self::CHILD_BORDER) .border_color(border_color) .overflow_hidden() From 72761797a25ced34a73c171d64d15378f8219914 Mon Sep 17 00:00:00 2001 From: jingyuexing <19589872+jingyuexing@users.noreply.github.com> Date: Mon, 11 Aug 2025 03:40:14 +0800 Subject: [PATCH 238/693] Fix SHA-256 verification mismatch when downloading language servers (#35953) Closes #35642 Release Notes: - Fixed: when the expected digest included a "sha256:" prefix while the computed digest has no prefix. --- crates/languages/src/github_download.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/languages/src/github_download.rs b/crates/languages/src/github_download.rs index a3cd0a964b..04f5ecfa08 100644 --- a/crates/languages/src/github_download.rs +++ b/crates/languages/src/github_download.rs @@ -62,6 +62,12 @@ pub(crate) async fn download_server_binary( format!("saving archive contents into the temporary file for {url}",) })?; let asset_sha_256 = format!("{:x}", writer.hasher.finalize()); + + // Strip "sha256:" prefix for comparison + let expected_sha_256 = expected_sha_256 + .strip_prefix("sha256:") + .unwrap_or(expected_sha_256); + anyhow::ensure!( asset_sha_256 == expected_sha_256, "{url} asset got SHA-256 mismatch. Expected: {expected_sha_256}, Got: {asset_sha_256}", From 308cb9e537eda81b35bfccef00e2ef7be8d070d1 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Sun, 10 Aug 2025 23:57:55 +0200 Subject: [PATCH 239/693] Pull action_log into its own crate (#35959) Release Notes: - N/A --- Cargo.lock | 36 +++++++++++++-- Cargo.toml | 2 + crates/acp_thread/Cargo.toml | 2 +- crates/acp_thread/src/acp_thread.rs | 2 +- crates/action_log/Cargo.toml | 45 +++++++++++++++++++ crates/action_log/LICENSE-GPL | 1 + .../src/action_log.rs | 0 crates/agent/Cargo.toml | 1 + crates/agent/src/agent_profile.rs | 2 +- crates/agent/src/context_server_tool.rs | 3 +- crates/agent/src/thread.rs | 3 +- crates/agent2/Cargo.toml | 5 ++- crates/agent2/src/agent.rs | 6 +-- crates/agent2/src/native_agent_server.rs | 2 +- crates/agent2/src/tests/mod.rs | 40 +++++++++-------- crates/agent2/src/thread.rs | 7 +-- crates/agent2/src/tools/edit_file_tool.rs | 4 +- crates/agent2/src/tools/find_path_tool.rs | 2 +- crates/agent2/src/tools/read_file_tool.rs | 12 ++--- crates/agent_ui/Cargo.toml | 1 + crates/agent_ui/src/acp/thread_view.rs | 2 +- crates/agent_ui/src/agent_diff.rs | 2 +- crates/assistant_tool/Cargo.toml | 5 +-- crates/assistant_tool/src/assistant_tool.rs | 3 +- crates/assistant_tool/src/outline.rs | 2 +- crates/assistant_tools/Cargo.toml | 1 + crates/assistant_tools/src/copy_path_tool.rs | 3 +- .../src/create_directory_tool.rs | 3 +- .../assistant_tools/src/delete_path_tool.rs | 3 +- .../assistant_tools/src/diagnostics_tool.rs | 3 +- crates/assistant_tools/src/edit_agent.rs | 2 +- crates/assistant_tools/src/edit_file_tool.rs | 4 +- crates/assistant_tools/src/fetch_tool.rs | 3 +- crates/assistant_tools/src/find_path_tool.rs | 3 +- crates/assistant_tools/src/grep_tool.rs | 3 +- .../src/list_directory_tool.rs | 3 +- crates/assistant_tools/src/move_path_tool.rs | 3 +- crates/assistant_tools/src/now_tool.rs | 3 +- crates/assistant_tools/src/open_tool.rs | 3 +- .../src/project_notifications_tool.rs | 3 +- crates/assistant_tools/src/read_file_tool.rs | 5 ++- crates/assistant_tools/src/terminal_tool.rs | 3 +- crates/assistant_tools/src/thinking_tool.rs | 3 +- crates/assistant_tools/src/web_search_tool.rs | 3 +- crates/remote_server/Cargo.toml | 1 + .../remote_server/src/remote_editing_tests.rs | 2 +- tooling/workspace-hack/Cargo.toml | 4 +- 47 files changed, 177 insertions(+), 77 deletions(-) create mode 100644 crates/action_log/Cargo.toml create mode 120000 crates/action_log/LICENSE-GPL rename crates/{assistant_tool => action_log}/src/action_log.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 1ae4303c71..4bb36fdeee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,9 +6,9 @@ version = 4 name = "acp_thread" version = "0.1.0" dependencies = [ + "action_log", "agent-client-protocol", "anyhow", - "assistant_tool", "buffer_diff", "editor", "env_logger 0.11.8", @@ -32,6 +32,32 @@ dependencies = [ "workspace-hack", ] +[[package]] +name = "action_log" +version = "0.1.0" +dependencies = [ + "anyhow", + "buffer_diff", + "clock", + "collections", + "ctor", + "futures 0.3.31", + "gpui", + "indoc", + "language", + "log", + "pretty_assertions", + "project", + "rand 0.8.5", + "serde_json", + "settings", + "text", + "util", + "watch", + "workspace-hack", + "zlog", +] + [[package]] name = "activity_indicator" version = "0.1.0" @@ -84,6 +110,7 @@ dependencies = [ name = "agent" version = "0.1.0" dependencies = [ + "action_log", "agent_settings", "anyhow", "assistant_context", @@ -156,6 +183,7 @@ name = "agent2" version = "0.1.0" dependencies = [ "acp_thread", + "action_log", "agent-client-protocol", "agent_servers", "agent_settings", @@ -261,6 +289,7 @@ name = "agent_ui" version = "0.1.0" dependencies = [ "acp_thread", + "action_log", "agent", "agent-client-protocol", "agent2", @@ -842,13 +871,13 @@ dependencies = [ name = "assistant_tool" version = "0.1.0" dependencies = [ + "action_log", "anyhow", "buffer_diff", "clock", "collections", "ctor", "derive_more 0.99.19", - "futures 0.3.31", "gpui", "icons", "indoc", @@ -865,7 +894,6 @@ dependencies = [ "settings", "text", "util", - "watch", "workspace", "workspace-hack", "zlog", @@ -875,6 +903,7 @@ dependencies = [ name = "assistant_tools" version = "0.1.0" dependencies = [ + "action_log", "agent_settings", "anyhow", "assistant_tool", @@ -13523,6 +13552,7 @@ dependencies = [ name = "remote_server" version = "0.1.0" dependencies = [ + "action_log", "anyhow", "askpass", "assistant_tool", diff --git a/Cargo.toml b/Cargo.toml index d6ca4c664d..48a11c27da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "crates/acp_thread", + "crates/action_log", "crates/activity_indicator", "crates/agent", "crates/agent2", @@ -229,6 +230,7 @@ edition = "2024" # acp_thread = { path = "crates/acp_thread" } +action_log = { path = "crates/action_log" } agent = { path = "crates/agent" } agent2 = { path = "crates/agent2" } activity_indicator = { path = "crates/activity_indicator" } diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index 1831c7e473..37d2920045 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -16,9 +16,9 @@ doctest = false test-support = ["gpui/test-support", "project/test-support"] [dependencies] +action_log.workspace = true agent-client-protocol.workspace = true anyhow.workspace = true -assistant_tool.workspace = true buffer_diff.workspace = true editor.workspace = true futures.workspace = true diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 1df0e1def7..f2bebf7391 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -4,9 +4,9 @@ mod diff; pub use connection::*; pub use diff::*; +use action_log::ActionLog; use agent_client_protocol as acp; use anyhow::{Context as _, Result}; -use assistant_tool::ActionLog; use editor::Bias; use futures::{FutureExt, channel::oneshot, future::BoxFuture}; use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task}; diff --git a/crates/action_log/Cargo.toml b/crates/action_log/Cargo.toml new file mode 100644 index 0000000000..1a389e8859 --- /dev/null +++ b/crates/action_log/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "action_log" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lib] +path = "src/action_log.rs" + +[lints] +workspace = true + +[dependencies] +anyhow.workspace = true +buffer_diff.workspace = true +clock.workspace = true +collections.workspace = true +futures.workspace = true +gpui.workspace = true +language.workspace = true +project.workspace = true +text.workspace = true +util.workspace = true +watch.workspace = true +workspace-hack.workspace = true + + +[dev-dependencies] +buffer_diff = { workspace = true, features = ["test-support"] } +collections = { workspace = true, features = ["test-support"] } +clock = { workspace = true, features = ["test-support"] } +ctor.workspace = true +gpui = { workspace = true, features = ["test-support"] } +indoc.workspace = true +language = { workspace = true, features = ["test-support"] } +log.workspace = true +pretty_assertions.workspace = true +project = { workspace = true, features = ["test-support"] } +rand.workspace = true +serde_json.workspace = true +settings = { workspace = true, features = ["test-support"] } +text = { workspace = true, features = ["test-support"] } +util = { workspace = true, features = ["test-support"] } +zlog.workspace = true diff --git a/crates/action_log/LICENSE-GPL b/crates/action_log/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/action_log/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/assistant_tool/src/action_log.rs b/crates/action_log/src/action_log.rs similarity index 100% rename from crates/assistant_tool/src/action_log.rs rename to crates/action_log/src/action_log.rs diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index 7bc0e82cad..53ad2f4967 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -19,6 +19,7 @@ test-support = [ ] [dependencies] +action_log.workspace = true agent_settings.workspace = true anyhow.workspace = true assistant_context.workspace = true diff --git a/crates/agent/src/agent_profile.rs b/crates/agent/src/agent_profile.rs index 34ea1c8df7..38e697dd9b 100644 --- a/crates/agent/src/agent_profile.rs +++ b/crates/agent/src/agent_profile.rs @@ -326,7 +326,7 @@ mod tests { _input: serde_json::Value, _request: Arc, _project: Entity, - _action_log: Entity, + _action_log: Entity, _model: Arc, _window: Option, _cx: &mut App, diff --git a/crates/agent/src/context_server_tool.rs b/crates/agent/src/context_server_tool.rs index 85e8ac7451..22d1a72bf5 100644 --- a/crates/agent/src/context_server_tool.rs +++ b/crates/agent/src/context_server_tool.rs @@ -1,7 +1,8 @@ use std::sync::Arc; +use action_log::ActionLog; use anyhow::{Result, anyhow, bail}; -use assistant_tool::{ActionLog, Tool, ToolResult, ToolSource}; +use assistant_tool::{Tool, ToolResult, ToolSource}; use context_server::{ContextServerId, types}; use gpui::{AnyWindowHandle, App, Entity, Task}; use icons::IconName; diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 048aa4245d..20d482f60d 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -8,9 +8,10 @@ use crate::{ }, tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState}, }; +use action_log::ActionLog; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_PROMPT}; use anyhow::{Result, anyhow}; -use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet}; +use assistant_tool::{AnyToolCard, Tool, ToolWorkingSet}; use chrono::{DateTime, Utc}; use client::{ModelRequestUsage, RequestUsage}; use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit}; diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 3e19895a31..c1c3f2d459 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "agent2" version = "0.1.0" -edition = "2021" +edition.workspace = true +publish.workspace = true license = "GPL-3.0-or-later" -publish = false [lib] path = "src/agent2.rs" @@ -13,6 +13,7 @@ workspace = true [dependencies] acp_thread.workspace = true +action_log.workspace = true agent-client-protocol.workspace = true agent_servers.workspace = true agent_settings.workspace = true diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 892469db47..5be3892d60 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,9 +1,9 @@ -use crate::{templates::Templates, AgentResponseEvent, Thread}; +use crate::{AgentResponseEvent, Thread, templates::Templates}; use crate::{EditFileTool, FindPathTool, ReadFileTool, ThinkingTool, ToolCallAuthorization}; use acp_thread::ModelSelector; use agent_client_protocol as acp; -use anyhow::{anyhow, Context as _, Result}; -use futures::{future, StreamExt}; +use anyhow::{Context as _, Result, anyhow}; +use futures::{StreamExt, future}; use gpui::{ App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity, }; diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index dd0188b548..58f6d37c54 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -7,7 +7,7 @@ use gpui::{App, Entity, Task}; use project::Project; use prompt_store::PromptStore; -use crate::{templates::Templates, NativeAgent, NativeAgentConnection}; +use crate::{NativeAgent, NativeAgentConnection, templates::Templates}; #[derive(Clone)] pub struct NativeAgentServer; diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 6e0dc86091..b47816f35c 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1,17 +1,17 @@ use super::*; use acp_thread::AgentConnection; +use action_log::ActionLog; use agent_client_protocol::{self as acp}; use anyhow::Result; -use assistant_tool::ActionLog; use client::{Client, UserStore}; use fs::FakeFs; use futures::channel::mpsc::UnboundedReceiver; -use gpui::{http_client::FakeHttpClient, AppContext, Entity, Task, TestAppContext}; +use gpui::{AppContext, Entity, Task, TestAppContext, http_client::FakeHttpClient}; use indoc::indoc; use language_model::{ - fake_provider::FakeLanguageModel, LanguageModel, LanguageModelCompletionError, - LanguageModelCompletionEvent, LanguageModelId, LanguageModelRegistry, LanguageModelToolResult, - LanguageModelToolUse, MessageContent, Role, StopReason, + LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, + LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role, + StopReason, fake_provider::FakeLanguageModel, }; use project::Project; use prompt_store::ProjectContext; @@ -149,19 +149,21 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) { .await; assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); thread.update(cx, |thread, _cx| { - assert!(thread - .messages() - .last() - .unwrap() - .content - .iter() - .any(|content| { - if let MessageContent::Text(text) = content { - text.contains("Ding") - } else { - false - } - })); + assert!( + thread + .messages() + .last() + .unwrap() + .content + .iter() + .any(|content| { + if let MessageContent::Text(text) = content { + text.contains("Ding") + } else { + false + } + }) + ); }); } @@ -333,7 +335,7 @@ async fn expect_tool_call_update_fields( .unwrap(); match event { AgentResponseEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => { - return update + return update; } event => { panic!("Unexpected event {event:?}"); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 8ed200b56b..a0a2a3a2b0 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1,8 +1,9 @@ use crate::{SystemPromptTemplate, Template, Templates}; use acp_thread::Diff; +use action_log::ActionLog; use agent_client_protocol as acp; -use anyhow::{anyhow, Context as _, Result}; -use assistant_tool::{adapt_schema_to_format, ActionLog}; +use anyhow::{Context as _, Result, anyhow}; +use assistant_tool::adapt_schema_to_format; use cloud_llm_client::{CompletionIntent, CompletionMode}; use collections::HashMap; use futures::{ @@ -23,7 +24,7 @@ use schemars::{JsonSchema, Schema}; use serde::{Deserialize, Serialize}; use smol::stream::StreamExt; use std::{cell::RefCell, collections::BTreeMap, fmt::Write, future::Future, rc::Rc, sync::Arc}; -use util::{markdown::MarkdownCodeBlock, ResultExt}; +use util::{ResultExt, markdown::MarkdownCodeBlock}; #[derive(Debug, Clone)] pub struct AgentMessage { diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 0858bb501c..48e5d37586 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -1,7 +1,7 @@ use crate::{AgentTool, Thread, ToolCallEventStream}; use acp_thread::Diff; use agent_client_protocol as acp; -use anyhow::{anyhow, Context as _, Result}; +use anyhow::{Context as _, Result, anyhow}; use assistant_tools::edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat}; use cloud_llm_client::CompletionIntent; use collections::HashSet; @@ -457,7 +457,7 @@ mod tests { use crate::Templates; use super::*; - use assistant_tool::ActionLog; + use action_log::ActionLog; use client::TelemetrySettings; use fs::Fs; use gpui::{TestAppContext, UpdateGlobal}; diff --git a/crates/agent2/src/tools/find_path_tool.rs b/crates/agent2/src/tools/find_path_tool.rs index f4589e5600..611d34e701 100644 --- a/crates/agent2/src/tools/find_path_tool.rs +++ b/crates/agent2/src/tools/find_path_tool.rs @@ -1,6 +1,6 @@ use crate::{AgentTool, ToolCallEventStream}; use agent_client_protocol as acp; -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use gpui::{App, AppContext, Entity, SharedString, Task}; use language_model::LanguageModelToolResultContent; use project::Project; diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index 7bbe3ac4c1..fac637d838 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -1,16 +1,16 @@ +use action_log::ActionLog; use agent_client_protocol::{self as acp}; -use anyhow::{anyhow, Context, Result}; -use assistant_tool::{outline, ActionLog}; -use gpui::{Entity, Task}; +use anyhow::{Context as _, Result, anyhow}; +use assistant_tool::outline; +use gpui::{App, Entity, SharedString, Task}; use indoc::formatdoc; use language::{Anchor, Point}; use language_model::{LanguageModelImage, LanguageModelToolResultContent}; -use project::{image_store, AgentLocation, ImageItem, Project, WorktreeSettings}; +use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; use std::sync::Arc; -use ui::{App, SharedString}; use crate::{AgentTool, ToolCallEventStream}; @@ -270,7 +270,7 @@ impl AgentTool for ReadFileTool { mod test { use super::*; use gpui::{AppContext, TestAppContext, UpdateGlobal as _}; - use language::{tree_sitter_rust, Language, LanguageConfig, LanguageMatcher}; + use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust}; use project::{FakeFs, Project}; use serde_json::json; use settings::SettingsStore; diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index c145df0eae..de0a27c2cb 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -17,6 +17,7 @@ test-support = ["gpui/test-support", "language/test-support"] [dependencies] acp_thread.workspace = true +action_log.workspace = true agent-client-protocol.workspace = true agent.workspace = true agent2.workspace = true diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index c811878c21..01980b8fb7 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -10,8 +10,8 @@ use std::rc::Rc; use std::sync::Arc; use std::time::Duration; +use action_log::ActionLog; use agent_client_protocol as acp; -use assistant_tool::ActionLog; use buffer_diff::BufferDiff; use collections::{HashMap, HashSet}; use editor::{ diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index e1ceaf761d..0abc5280f4 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1,9 +1,9 @@ use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll}; use acp_thread::{AcpThread, AcpThreadEvent}; +use action_log::ActionLog; use agent::{Thread, ThreadEvent, ThreadSummary}; use agent_settings::AgentSettings; use anyhow::Result; -use assistant_tool::ActionLog; use buffer_diff::DiffHunkStatus; use collections::{HashMap, HashSet}; use editor::{ diff --git a/crates/assistant_tool/Cargo.toml b/crates/assistant_tool/Cargo.toml index acbe674b02..c95695052a 100644 --- a/crates/assistant_tool/Cargo.toml +++ b/crates/assistant_tool/Cargo.toml @@ -12,12 +12,10 @@ workspace = true path = "src/assistant_tool.rs" [dependencies] +action_log.workspace = true anyhow.workspace = true -buffer_diff.workspace = true -clock.workspace = true collections.workspace = true derive_more.workspace = true -futures.workspace = true gpui.workspace = true icons.workspace = true language.workspace = true @@ -30,7 +28,6 @@ serde.workspace = true serde_json.workspace = true text.workspace = true util.workspace = true -watch.workspace = true workspace.workspace = true workspace-hack.workspace = true diff --git a/crates/assistant_tool/src/assistant_tool.rs b/crates/assistant_tool/src/assistant_tool.rs index 22cbaac3f8..9c5825d0f0 100644 --- a/crates/assistant_tool/src/assistant_tool.rs +++ b/crates/assistant_tool/src/assistant_tool.rs @@ -1,4 +1,3 @@ -mod action_log; pub mod outline; mod tool_registry; mod tool_schema; @@ -10,6 +9,7 @@ use std::fmt::Formatter; use std::ops::Deref; use std::sync::Arc; +use action_log::ActionLog; use anyhow::Result; use gpui::AnyElement; use gpui::AnyWindowHandle; @@ -25,7 +25,6 @@ use language_model::LanguageModelToolSchemaFormat; use project::Project; use workspace::Workspace; -pub use crate::action_log::*; pub use crate::tool_registry::*; pub use crate::tool_schema::*; pub use crate::tool_working_set::*; diff --git a/crates/assistant_tool/src/outline.rs b/crates/assistant_tool/src/outline.rs index 6af204d79a..4f8bde5456 100644 --- a/crates/assistant_tool/src/outline.rs +++ b/crates/assistant_tool/src/outline.rs @@ -1,4 +1,4 @@ -use crate::ActionLog; +use action_log::ActionLog; use anyhow::{Context as _, Result}; use gpui::{AsyncApp, Entity}; use language::{OutlineItem, ParseStatus}; diff --git a/crates/assistant_tools/Cargo.toml b/crates/assistant_tools/Cargo.toml index d4b8fa3afc..5a8ca8a5e9 100644 --- a/crates/assistant_tools/Cargo.toml +++ b/crates/assistant_tools/Cargo.toml @@ -15,6 +15,7 @@ path = "src/assistant_tools.rs" eval = [] [dependencies] +action_log.workspace = true agent_settings.workspace = true anyhow.workspace = true assistant_tool.workspace = true diff --git a/crates/assistant_tools/src/copy_path_tool.rs b/crates/assistant_tools/src/copy_path_tool.rs index e34ae9ff93..c56a864bd4 100644 --- a/crates/assistant_tools/src/copy_path_tool.rs +++ b/crates/assistant_tools/src/copy_path_tool.rs @@ -1,6 +1,7 @@ use crate::schema::json_schema_for; +use action_log::ActionLog; use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{ActionLog, Tool, ToolResult}; +use assistant_tool::{Tool, ToolResult}; use gpui::AnyWindowHandle; use gpui::{App, AppContext, Entity, Task}; use language_model::LanguageModel; diff --git a/crates/assistant_tools/src/create_directory_tool.rs b/crates/assistant_tools/src/create_directory_tool.rs index 11d969d234..85eea463dc 100644 --- a/crates/assistant_tools/src/create_directory_tool.rs +++ b/crates/assistant_tools/src/create_directory_tool.rs @@ -1,6 +1,7 @@ use crate::schema::json_schema_for; +use action_log::ActionLog; use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{ActionLog, Tool, ToolResult}; +use assistant_tool::{Tool, ToolResult}; use gpui::AnyWindowHandle; use gpui::{App, Entity, Task}; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; diff --git a/crates/assistant_tools/src/delete_path_tool.rs b/crates/assistant_tools/src/delete_path_tool.rs index 9e69c18b65..b181eeff5c 100644 --- a/crates/assistant_tools/src/delete_path_tool.rs +++ b/crates/assistant_tools/src/delete_path_tool.rs @@ -1,6 +1,7 @@ use crate::schema::json_schema_for; +use action_log::ActionLog; use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{ActionLog, Tool, ToolResult}; +use assistant_tool::{Tool, ToolResult}; use futures::{SinkExt, StreamExt, channel::mpsc}; use gpui::{AnyWindowHandle, App, AppContext, Entity, Task}; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; diff --git a/crates/assistant_tools/src/diagnostics_tool.rs b/crates/assistant_tools/src/diagnostics_tool.rs index 12ab97f820..bc479eb596 100644 --- a/crates/assistant_tools/src/diagnostics_tool.rs +++ b/crates/assistant_tools/src/diagnostics_tool.rs @@ -1,6 +1,7 @@ use crate::schema::json_schema_for; +use action_log::ActionLog; use anyhow::{Result, anyhow}; -use assistant_tool::{ActionLog, Tool, ToolResult}; +use assistant_tool::{Tool, ToolResult}; use gpui::{AnyWindowHandle, App, Entity, Task}; use language::{DiagnosticSeverity, OffsetRangeExt}; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; diff --git a/crates/assistant_tools/src/edit_agent.rs b/crates/assistant_tools/src/edit_agent.rs index dcb14a48f3..9305f584cb 100644 --- a/crates/assistant_tools/src/edit_agent.rs +++ b/crates/assistant_tools/src/edit_agent.rs @@ -5,8 +5,8 @@ mod evals; mod streaming_fuzzy_matcher; use crate::{Template, Templates}; +use action_log::ActionLog; use anyhow::Result; -use assistant_tool::ActionLog; use cloud_llm_client::CompletionIntent; use create_file_parser::{CreateFileParser, CreateFileParserEvent}; pub use edit_parser::EditFormat; diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 54431ee1d7..b5712415ec 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -4,11 +4,11 @@ use crate::{ schema::json_schema_for, ui::{COLLAPSED_LINES, ToolOutputPreview}, }; +use action_log::ActionLog; use agent_settings; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::{ - ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, - ToolUseStatus, + AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus, }; use buffer_diff::{BufferDiff, BufferDiffSnapshot}; use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey}; diff --git a/crates/assistant_tools/src/fetch_tool.rs b/crates/assistant_tools/src/fetch_tool.rs index a31ec39268..79e205f205 100644 --- a/crates/assistant_tools/src/fetch_tool.rs +++ b/crates/assistant_tools/src/fetch_tool.rs @@ -3,8 +3,9 @@ use std::sync::Arc; use std::{borrow::Cow, cell::RefCell}; use crate::schema::json_schema_for; +use action_log::ActionLog; use anyhow::{Context as _, Result, anyhow, bail}; -use assistant_tool::{ActionLog, Tool, ToolResult}; +use assistant_tool::{Tool, ToolResult}; use futures::AsyncReadExt as _; use gpui::{AnyWindowHandle, App, AppContext as _, Entity, Task}; use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown}; diff --git a/crates/assistant_tools/src/find_path_tool.rs b/crates/assistant_tools/src/find_path_tool.rs index 6cdf58eac8..6b62638a4c 100644 --- a/crates/assistant_tools/src/find_path_tool.rs +++ b/crates/assistant_tools/src/find_path_tool.rs @@ -1,7 +1,8 @@ use crate::{schema::json_schema_for, ui::ToolCallCardHeader}; +use action_log::ActionLog; use anyhow::{Result, anyhow}; use assistant_tool::{ - ActionLog, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus, + Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus, }; use editor::Editor; use futures::channel::oneshot::{self, Receiver}; diff --git a/crates/assistant_tools/src/grep_tool.rs b/crates/assistant_tools/src/grep_tool.rs index 43c3d1d990..a5ce07823f 100644 --- a/crates/assistant_tools/src/grep_tool.rs +++ b/crates/assistant_tools/src/grep_tool.rs @@ -1,6 +1,7 @@ use crate::schema::json_schema_for; +use action_log::ActionLog; use anyhow::{Result, anyhow}; -use assistant_tool::{ActionLog, Tool, ToolResult}; +use assistant_tool::{Tool, ToolResult}; use futures::StreamExt; use gpui::{AnyWindowHandle, App, Entity, Task}; use language::{OffsetRangeExt, ParseStatus, Point}; diff --git a/crates/assistant_tools/src/list_directory_tool.rs b/crates/assistant_tools/src/list_directory_tool.rs index b1980615d6..5471d8923b 100644 --- a/crates/assistant_tools/src/list_directory_tool.rs +++ b/crates/assistant_tools/src/list_directory_tool.rs @@ -1,6 +1,7 @@ use crate::schema::json_schema_for; +use action_log::ActionLog; use anyhow::{Result, anyhow}; -use assistant_tool::{ActionLog, Tool, ToolResult}; +use assistant_tool::{Tool, ToolResult}; use gpui::{AnyWindowHandle, App, Entity, Task}; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; use project::{Project, WorktreeSettings}; diff --git a/crates/assistant_tools/src/move_path_tool.rs b/crates/assistant_tools/src/move_path_tool.rs index c1cbbf848d..2c065488ce 100644 --- a/crates/assistant_tools/src/move_path_tool.rs +++ b/crates/assistant_tools/src/move_path_tool.rs @@ -1,6 +1,7 @@ use crate::schema::json_schema_for; +use action_log::ActionLog; use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{ActionLog, Tool, ToolResult}; +use assistant_tool::{Tool, ToolResult}; use gpui::{AnyWindowHandle, App, AppContext, Entity, Task}; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; use project::Project; diff --git a/crates/assistant_tools/src/now_tool.rs b/crates/assistant_tools/src/now_tool.rs index b51b91d3d5..f50ad065d1 100644 --- a/crates/assistant_tools/src/now_tool.rs +++ b/crates/assistant_tools/src/now_tool.rs @@ -1,8 +1,9 @@ use std::sync::Arc; use crate::schema::json_schema_for; +use action_log::ActionLog; use anyhow::{Result, anyhow}; -use assistant_tool::{ActionLog, Tool, ToolResult}; +use assistant_tool::{Tool, ToolResult}; use chrono::{Local, Utc}; use gpui::{AnyWindowHandle, App, Entity, Task}; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; diff --git a/crates/assistant_tools/src/open_tool.rs b/crates/assistant_tools/src/open_tool.rs index 8fddbb0431..6dbf66749b 100644 --- a/crates/assistant_tools/src/open_tool.rs +++ b/crates/assistant_tools/src/open_tool.rs @@ -1,6 +1,7 @@ use crate::schema::json_schema_for; +use action_log::ActionLog; use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{ActionLog, Tool, ToolResult}; +use assistant_tool::{Tool, ToolResult}; use gpui::{AnyWindowHandle, App, AppContext, Entity, Task}; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; use project::Project; diff --git a/crates/assistant_tools/src/project_notifications_tool.rs b/crates/assistant_tools/src/project_notifications_tool.rs index 03487e5419..c65cfd0ca7 100644 --- a/crates/assistant_tools/src/project_notifications_tool.rs +++ b/crates/assistant_tools/src/project_notifications_tool.rs @@ -1,6 +1,7 @@ use crate::schema::json_schema_for; +use action_log::ActionLog; use anyhow::Result; -use assistant_tool::{ActionLog, Tool, ToolResult}; +use assistant_tool::{Tool, ToolResult}; use gpui::{AnyWindowHandle, App, Entity, Task}; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; use project::Project; diff --git a/crates/assistant_tools/src/read_file_tool.rs b/crates/assistant_tools/src/read_file_tool.rs index ee38273cc0..68b870e40f 100644 --- a/crates/assistant_tools/src/read_file_tool.rs +++ b/crates/assistant_tools/src/read_file_tool.rs @@ -1,6 +1,7 @@ use crate::schema::json_schema_for; +use action_log::ActionLog; use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{ActionLog, Tool, ToolResult}; +use assistant_tool::{Tool, ToolResult}; use assistant_tool::{ToolResultContent, outline}; use gpui::{AnyWindowHandle, App, Entity, Task}; use project::{ImageItem, image_store}; @@ -286,7 +287,7 @@ impl Tool for ReadFileTool { Using the line numbers in this outline, you can call this tool again while specifying the start_line and end_line fields to see the implementations of symbols in the outline. - + Alternatively, you can fall back to the `grep` tool (if available) to search the file for specific content." } diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index 8add60f09a..46227f130d 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -2,9 +2,10 @@ use crate::{ schema::json_schema_for, ui::{COLLAPSED_LINES, ToolOutputPreview}, }; +use action_log::ActionLog; use agent_settings; use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus}; +use assistant_tool::{Tool, ToolCard, ToolResult, ToolUseStatus}; use futures::{FutureExt as _, future::Shared}; use gpui::{ Animation, AnimationExt, AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, diff --git a/crates/assistant_tools/src/thinking_tool.rs b/crates/assistant_tools/src/thinking_tool.rs index 76c6e6c0ba..17ce4afc2e 100644 --- a/crates/assistant_tools/src/thinking_tool.rs +++ b/crates/assistant_tools/src/thinking_tool.rs @@ -1,8 +1,9 @@ use std::sync::Arc; use crate::schema::json_schema_for; +use action_log::ActionLog; use anyhow::{Result, anyhow}; -use assistant_tool::{ActionLog, Tool, ToolResult}; +use assistant_tool::{Tool, ToolResult}; use gpui::{AnyWindowHandle, App, Entity, Task}; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; use project::Project; diff --git a/crates/assistant_tools/src/web_search_tool.rs b/crates/assistant_tools/src/web_search_tool.rs index c6c37de472..47a6958b7a 100644 --- a/crates/assistant_tools/src/web_search_tool.rs +++ b/crates/assistant_tools/src/web_search_tool.rs @@ -2,9 +2,10 @@ use std::{sync::Arc, time::Duration}; use crate::schema::json_schema_for; use crate::ui::ToolCallCardHeader; +use action_log::ActionLog; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::{ - ActionLog, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus, + Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus, }; use cloud_llm_client::{WebSearchResponse, WebSearchResult}; use futures::{Future, FutureExt, TryFutureExt}; diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index c6a546f345..dcec9f6fe0 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -74,6 +74,7 @@ libc.workspace = true minidumper.workspace = true [dev-dependencies] +action_log.workspace = true assistant_tool.workspace = true assistant_tools.workspace = true client = { workspace = true, features = ["test-support"] } diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 9730984f26..514e5ce4c0 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -1724,7 +1724,7 @@ async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mu .await .unwrap(); - let action_log = cx.new(|_| assistant_tool::ActionLog::new(project.clone())); + let action_log = cx.new(|_| action_log::ActionLog::new(project.clone())); let model = Arc::new(FakeLanguageModel::default()); let request = Arc::new(LanguageModelRequest::default()); diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index 338985ed95..054e757056 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -6,9 +6,9 @@ [package] name = "workspace-hack" version = "0.1.0" -edition = "2021" description = "workspace-hack package, managed by hakari" -publish = false +edition.workspace = true +publish.workspace = true # The parts of the file between the BEGIN HAKARI SECTION and END HAKARI SECTION comments # are managed by hakari. From c82cd0c6b1939a8638ef2a9d1e085d1584313509 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Sun, 10 Aug 2025 23:28:28 -0300 Subject: [PATCH 240/693] docs: Clarify storage of AI API keys (#35963) Previous docs was inaccurate as Zed doesn't store LLM API keys in the `settings.json`. Release Notes: - N/A --- docs/src/ai/llm-providers.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/src/ai/llm-providers.md b/docs/src/ai/llm-providers.md index 8fdb7ea325..64995e6eb8 100644 --- a/docs/src/ai/llm-providers.md +++ b/docs/src/ai/llm-providers.md @@ -6,29 +6,29 @@ You can do that by either subscribing to [one of Zed's plans](./plans-and-usage. ## Use Your Own Keys {#use-your-own-keys} -If you already have an API key for an existing LLM provider—say Anthropic or OpenAI, for example—you can insert them in Zed and use the Agent Panel **_for free_**. +If you already have an API key for an existing LLM provider—say Anthropic or OpenAI, for example—you can insert them into Zed and use the full power of the Agent Panel **_for free_**. -You can add your API key to a given provider either via the Agent Panel's settings UI or directly via the `settings.json` through the `language_models` key. +To add an existing API key to a given provider, go to the Agent Panel settings (`agent: open settings`), look for the desired provider, paste the key into the input, and hit enter. + +> Note: API keys are _not_ stored as plain text in your `settings.json`, but rather in your OS's secure credential storage. ## Supported Providers Here's all the supported LLM providers for which you can use your own API keys: -| Provider | -| ----------------------------------------------- | -| [Amazon Bedrock](#amazon-bedrock) | -| [Anthropic](#anthropic) | -| [DeepSeek](#deepseek) | -| [GitHub Copilot Chat](#github-copilot-chat) | -| [Google AI](#google-ai) | -| [LM Studio](#lmstudio) | -| [Mistral](#mistral) | -| [Ollama](#ollama) | -| [OpenAI](#openai) | -| [OpenAI API Compatible](#openai-api-compatible) | -| [OpenRouter](#openrouter) | -| [Vercel](#vercel-v0) | -| [xAI](#xai) | +- [Amazon Bedrock](#amazon-bedrock) +- [Anthropic](#anthropic) +- [DeepSeek](#deepseek) +- [GitHub Copilot Chat](#github-copilot-chat) +- [Google AI](#google-ai) +- [LM Studio](#lmstudio) +- [Mistral](#mistral) +- [Ollama](#ollama) +- [OpenAI](#openai) +- [OpenAI API Compatible](#openai-api-compatible) +- [OpenRouter](#openrouter) +- [Vercel](#vercel-v0) +- [xAI](#xai) ### Amazon Bedrock {#amazon-bedrock} From 8d332da4c5d61c11ab64667d2dad5c4199217119 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 11 Aug 2025 09:20:03 +0200 Subject: [PATCH 241/693] languages: Don't remove old artifacts on download failure (#35967) Release Notes: - N/A --- crates/http_client/src/github.rs | 12 +++++++++-- crates/languages/src/c.rs | 18 ++++++++-------- crates/languages/src/github_download.rs | 10 ++------- crates/languages/src/rust.rs | 28 ++++++++++++------------- 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/crates/http_client/src/github.rs b/crates/http_client/src/github.rs index a19c13b0ff..89309ff344 100644 --- a/crates/http_client/src/github.rs +++ b/crates/http_client/src/github.rs @@ -71,11 +71,19 @@ pub async fn latest_github_release( } }; - releases + let mut release = releases .into_iter() .filter(|release| !require_assets || !release.assets.is_empty()) .find(|release| release.pre_release == pre_release) - .context("finding a prerelease") + .context("finding a prerelease")?; + release.assets.iter_mut().for_each(|asset| { + if let Some(digest) = &mut asset.digest { + if let Some(stripped) = digest.strip_prefix("sha256:") { + *digest = stripped.to_owned(); + } + } + }); + Ok(release) } pub async fn get_release_by_tag_name( diff --git a/crates/languages/src/c.rs b/crates/languages/src/c.rs index df93e51760..aee1abee95 100644 --- a/crates/languages/src/c.rs +++ b/crates/languages/src/c.rs @@ -71,13 +71,13 @@ impl super::LspAdapter for CLspAdapter { container_dir: PathBuf, delegate: &dyn LspAdapterDelegate, ) -> Result { - let GitHubLspBinaryVersion { name, url, digest } = - &*version.downcast::().unwrap(); + let GitHubLspBinaryVersion { + name, + url, + digest: expected_digest, + } = *version.downcast::().unwrap(); let version_dir = container_dir.join(format!("clangd_{name}")); let binary_path = version_dir.join("bin/clangd"); - let expected_digest = digest - .as_ref() - .and_then(|digest| digest.strip_prefix("sha256:")); let binary = LanguageServerBinary { path: binary_path.clone(), @@ -103,7 +103,7 @@ impl super::LspAdapter for CLspAdapter { }) }; if let (Some(actual_digest), Some(expected_digest)) = - (&metadata.digest, expected_digest) + (&metadata.digest, &expected_digest) { if actual_digest == expected_digest { if validity_check().await.is_ok() { @@ -120,8 +120,8 @@ impl super::LspAdapter for CLspAdapter { } download_server_binary( delegate, - url, - digest.as_deref(), + &url, + expected_digest.as_deref(), &container_dir, AssetKind::Zip, ) @@ -130,7 +130,7 @@ impl super::LspAdapter for CLspAdapter { GithubBinaryMetadata::write_to_file( &GithubBinaryMetadata { metadata_version: 1, - digest: digest.clone(), + digest: expected_digest, }, &metadata_path, ) diff --git a/crates/languages/src/github_download.rs b/crates/languages/src/github_download.rs index 04f5ecfa08..5b0f1d0729 100644 --- a/crates/languages/src/github_download.rs +++ b/crates/languages/src/github_download.rs @@ -18,9 +18,8 @@ impl GithubBinaryMetadata { let metadata_content = async_fs::read_to_string(metadata_path) .await .with_context(|| format!("reading metadata file at {metadata_path:?}"))?; - let metadata: GithubBinaryMetadata = serde_json::from_str(&metadata_content) - .with_context(|| format!("parsing metadata file at {metadata_path:?}"))?; - Ok(metadata) + serde_json::from_str(&metadata_content) + .with_context(|| format!("parsing metadata file at {metadata_path:?}")) } pub(crate) async fn write_to_file(&self, metadata_path: &Path) -> Result<()> { @@ -63,11 +62,6 @@ pub(crate) async fn download_server_binary( })?; let asset_sha_256 = format!("{:x}", writer.hasher.finalize()); - // Strip "sha256:" prefix for comparison - let expected_sha_256 = expected_sha_256 - .strip_prefix("sha256:") - .unwrap_or(expected_sha_256); - anyhow::ensure!( asset_sha_256 == expected_sha_256, "{url} asset got SHA-256 mismatch. Expected: {expected_sha_256}, Got: {asset_sha_256}", diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index b52b1e7d55..1d489052e6 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -23,7 +23,7 @@ use std::{ sync::{Arc, LazyLock}, }; use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName}; -use util::fs::make_file_executable; +use util::fs::{make_file_executable, remove_matching}; use util::merge_json_value_into; use util::{ResultExt, maybe}; @@ -162,13 +162,13 @@ impl LspAdapter for RustLspAdapter { let asset_name = Self::build_asset_name(); let asset = release .assets - .iter() + .into_iter() .find(|asset| asset.name == asset_name) .with_context(|| format!("no asset found matching `{asset_name:?}`"))?; Ok(Box::new(GitHubLspBinaryVersion { name: release.tag_name, - url: asset.browser_download_url.clone(), - digest: asset.digest.clone(), + url: asset.browser_download_url, + digest: asset.digest, })) } @@ -178,11 +178,11 @@ impl LspAdapter for RustLspAdapter { container_dir: PathBuf, delegate: &dyn LspAdapterDelegate, ) -> Result { - let GitHubLspBinaryVersion { name, url, digest } = - &*version.downcast::().unwrap(); - let expected_digest = digest - .as_ref() - .and_then(|digest| digest.strip_prefix("sha256:")); + let GitHubLspBinaryVersion { + name, + url, + digest: expected_digest, + } = *version.downcast::().unwrap(); let destination_path = container_dir.join(format!("rust-analyzer-{name}")); let server_path = match Self::GITHUB_ASSET_KIND { AssetKind::TarGz | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place. @@ -213,7 +213,7 @@ impl LspAdapter for RustLspAdapter { }) }; if let (Some(actual_digest), Some(expected_digest)) = - (&metadata.digest, expected_digest) + (&metadata.digest, &expected_digest) { if actual_digest == expected_digest { if validity_check().await.is_ok() { @@ -229,20 +229,20 @@ impl LspAdapter for RustLspAdapter { } } - _ = fs::remove_dir_all(&destination_path).await; download_server_binary( delegate, - url, - expected_digest, + &url, + expected_digest.as_deref(), &destination_path, Self::GITHUB_ASSET_KIND, ) .await?; make_file_executable(&server_path).await?; + remove_matching(&container_dir, |path| server_path == path).await; GithubBinaryMetadata::write_to_file( &GithubBinaryMetadata { metadata_version: 1, - digest: expected_digest.map(ToString::to_string), + digest: expected_digest, }, &metadata_path, ) From e132c7cad9728ee1b82eaa801a750e207dfa7212 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 11 Aug 2025 10:15:59 +0200 Subject: [PATCH 242/693] dap_adapters: Log CodeLldb version fetching errors (#35943) Release Notes: - N/A --- crates/dap_adapters/src/codelldb.rs | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/crates/dap_adapters/src/codelldb.rs b/crates/dap_adapters/src/codelldb.rs index 5b88db4432..842bb264a8 100644 --- a/crates/dap_adapters/src/codelldb.rs +++ b/crates/dap_adapters/src/codelldb.rs @@ -338,8 +338,8 @@ impl DebugAdapter for CodeLldbDebugAdapter { if command.is_none() { delegate.output_to_console(format!("Checking latest version of {}...", self.name())); let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME); - let version_path = - if let Ok(version) = self.fetch_latest_adapter_version(delegate).await { + let version_path = match self.fetch_latest_adapter_version(delegate).await { + Ok(version) => { adapters::download_adapter_from_github( self.name(), version.clone(), @@ -351,10 +351,26 @@ impl DebugAdapter for CodeLldbDebugAdapter { adapter_path.join(format!("{}_{}", Self::ADAPTER_NAME, version.tag_name)); remove_matching(&adapter_path, |entry| entry != version_path).await; version_path - } else { - let mut paths = delegate.fs().read_dir(&adapter_path).await?; - paths.next().await.context("No adapter found")?? - }; + } + Err(e) => { + delegate.output_to_console("Unable to fetch latest version".to_string()); + log::error!("Error fetching latest version of {}: {}", self.name(), e); + delegate.output_to_console(format!( + "Searching for adapters in: {}", + adapter_path.display() + )); + let mut paths = delegate + .fs() + .read_dir(&adapter_path) + .await + .context("No cached adapter directory")?; + paths + .next() + .await + .context("No cached adapter found")? + .context("No cached adapter found")? + } + }; let adapter_dir = version_path.join("extension").join("adapter"); let path = adapter_dir.join("codelldb").to_string_lossy().to_string(); self.path_to_codelldb.set(path.clone()).ok(); From 422e0a2eb74eb5ca86d1864c2ef28add2949133c Mon Sep 17 00:00:00 2001 From: smit Date: Mon, 11 Aug 2025 15:29:41 +0530 Subject: [PATCH 243/693] project: Add more dynamic capability registrations for LSP (#35306) Closes #34204 Adds the ability to dynamically register and unregister code actions for language servers such as Biome. See more: https://github.com/zed-industries/zed/issues/34204#issuecomment-3134227856 Release Notes: - Fixed an issue where the Biome formatter was always used even when `require_config_file` was set to true and the project had no config file. --------- Co-authored-by: Kirill Bulatov --- crates/lsp/src/lsp.rs | 44 ++- crates/project/src/lsp_store.rs | 614 +++++++++++++++++++++----------- 2 files changed, 435 insertions(+), 223 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index a92787cd3e..22a227c231 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -651,7 +651,7 @@ impl LanguageServer { capabilities: ClientCapabilities { general: Some(GeneralClientCapabilities { position_encodings: Some(vec![PositionEncodingKind::UTF16]), - ..Default::default() + ..GeneralClientCapabilities::default() }), workspace: Some(WorkspaceClientCapabilities { configuration: Some(true), @@ -665,6 +665,7 @@ impl LanguageServer { workspace_folders: Some(true), symbol: Some(WorkspaceSymbolClientCapabilities { resolve_support: None, + dynamic_registration: Some(true), ..WorkspaceSymbolClientCapabilities::default() }), inlay_hint: Some(InlayHintWorkspaceClientCapabilities { @@ -688,21 +689,21 @@ impl LanguageServer { ..WorkspaceEditClientCapabilities::default() }), file_operations: Some(WorkspaceFileOperationsClientCapabilities { - dynamic_registration: Some(false), + dynamic_registration: Some(true), did_rename: Some(true), will_rename: Some(true), - ..Default::default() + ..WorkspaceFileOperationsClientCapabilities::default() }), apply_edit: Some(true), execute_command: Some(ExecuteCommandClientCapabilities { - dynamic_registration: Some(false), + dynamic_registration: Some(true), }), - ..Default::default() + ..WorkspaceClientCapabilities::default() }), text_document: Some(TextDocumentClientCapabilities { definition: Some(GotoCapability { link_support: Some(true), - dynamic_registration: None, + dynamic_registration: Some(true), }), code_action: Some(CodeActionClientCapabilities { code_action_literal_support: Some(CodeActionLiteralSupport { @@ -725,7 +726,8 @@ impl LanguageServer { "command".to_string(), ], }), - ..Default::default() + dynamic_registration: Some(true), + ..CodeActionClientCapabilities::default() }), completion: Some(CompletionClientCapabilities { completion_item: Some(CompletionItemCapability { @@ -751,7 +753,7 @@ impl LanguageServer { MarkupKind::Markdown, MarkupKind::PlainText, ]), - ..Default::default() + ..CompletionItemCapability::default() }), insert_text_mode: Some(InsertTextMode::ADJUST_INDENTATION), completion_list: Some(CompletionListCapability { @@ -764,18 +766,20 @@ impl LanguageServer { ]), }), context_support: Some(true), - ..Default::default() + dynamic_registration: Some(true), + ..CompletionClientCapabilities::default() }), rename: Some(RenameClientCapabilities { prepare_support: Some(true), prepare_support_default_behavior: Some( PrepareSupportDefaultBehavior::IDENTIFIER, ), - ..Default::default() + dynamic_registration: Some(true), + ..RenameClientCapabilities::default() }), hover: Some(HoverClientCapabilities { content_format: Some(vec![MarkupKind::Markdown]), - dynamic_registration: None, + dynamic_registration: Some(true), }), inlay_hint: Some(InlayHintClientCapabilities { resolve_support: Some(InlayHintResolveClientCapabilities { @@ -787,7 +791,7 @@ impl LanguageServer { "label.command".to_string(), ], }), - dynamic_registration: Some(false), + dynamic_registration: Some(true), }), publish_diagnostics: Some(PublishDiagnosticsClientCapabilities { related_information: Some(true), @@ -818,26 +822,29 @@ impl LanguageServer { }), active_parameter_support: Some(true), }), + dynamic_registration: Some(true), ..SignatureHelpClientCapabilities::default() }), synchronization: Some(TextDocumentSyncClientCapabilities { did_save: Some(true), + dynamic_registration: Some(true), ..TextDocumentSyncClientCapabilities::default() }), code_lens: Some(CodeLensClientCapabilities { - dynamic_registration: Some(false), + dynamic_registration: Some(true), }), document_symbol: Some(DocumentSymbolClientCapabilities { hierarchical_document_symbol_support: Some(true), + dynamic_registration: Some(true), ..DocumentSymbolClientCapabilities::default() }), diagnostic: Some(DiagnosticClientCapabilities { - dynamic_registration: Some(false), + dynamic_registration: Some(true), related_document_support: Some(true), }) .filter(|_| pull_diagnostics), color_provider: Some(DocumentColorClientCapabilities { - dynamic_registration: Some(false), + dynamic_registration: Some(true), }), ..TextDocumentClientCapabilities::default() }), @@ -850,7 +857,7 @@ impl LanguageServer { show_message: Some(ShowMessageRequestClientCapabilities { message_action_item: None, }), - ..Default::default() + ..WindowClientCapabilities::default() }), }, trace: None, @@ -862,8 +869,7 @@ impl LanguageServer { } }), locale: None, - - ..Default::default() + ..InitializeParams::default() } } @@ -1672,7 +1678,7 @@ impl LanguageServer { workspace_symbol_provider: Some(OneOf::Left(true)), implementation_provider: Some(ImplementationProviderCapability::Simple(true)), type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)), - ..Default::default() + ..ServerCapabilities::default() } } } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index d3843bc4ea..de6544f5a2 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -638,139 +638,27 @@ impl LocalLspStore { language_server .on_request::({ - let this = this.clone(); + let lsp_store = this.clone(); move |params, cx| { - let lsp_store = this.clone(); + let lsp_store = lsp_store.clone(); let mut cx = cx.clone(); async move { - for reg in params.registrations { - match reg.method.as_str() { - "workspace/didChangeWatchedFiles" => { - if let Some(options) = reg.register_options { - let options = serde_json::from_value(options)?; - lsp_store.update(&mut cx, |this, cx| { - this.as_local_mut()?.on_lsp_did_change_watched_files( - server_id, ®.id, options, cx, + lsp_store + .update(&mut cx, |lsp_store, cx| { + if lsp_store.as_local().is_some() { + match lsp_store + .register_server_capabilities(server_id, params, cx) + { + Ok(()) => {} + Err(e) => { + log::error!( + "Failed to register server capabilities: {e:#}" ); - Some(()) - })?; - } - } - "textDocument/rangeFormatting" => { - lsp_store.update(&mut cx, |lsp_store, cx| { - if let Some(server) = - lsp_store.language_server_for_id(server_id) - { - let options = reg - .register_options - .map(|options| { - serde_json::from_value::< - lsp::DocumentRangeFormattingOptions, - >( - options - ) - }) - .transpose()?; - let provider = match options { - None => OneOf::Left(true), - Some(options) => OneOf::Right(options), - }; - server.update_capabilities(|capabilities| { - capabilities.document_range_formatting_provider = - Some(provider); - }); - notify_server_capabilities_updated(&server, cx); } - anyhow::Ok(()) - })??; + }; } - "textDocument/onTypeFormatting" => { - lsp_store.update(&mut cx, |lsp_store, cx| { - if let Some(server) = - lsp_store.language_server_for_id(server_id) - { - let options = reg - .register_options - .map(|options| { - serde_json::from_value::< - lsp::DocumentOnTypeFormattingOptions, - >( - options - ) - }) - .transpose()?; - if let Some(options) = options { - server.update_capabilities(|capabilities| { - capabilities - .document_on_type_formatting_provider = - Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } - } - anyhow::Ok(()) - })??; - } - "textDocument/formatting" => { - lsp_store.update(&mut cx, |lsp_store, cx| { - if let Some(server) = - lsp_store.language_server_for_id(server_id) - { - let options = reg - .register_options - .map(|options| { - serde_json::from_value::< - lsp::DocumentFormattingOptions, - >( - options - ) - }) - .transpose()?; - let provider = match options { - None => OneOf::Left(true), - Some(options) => OneOf::Right(options), - }; - server.update_capabilities(|capabilities| { - capabilities.document_formatting_provider = - Some(provider); - }); - notify_server_capabilities_updated(&server, cx); - } - anyhow::Ok(()) - })??; - } - "workspace/didChangeConfiguration" => { - // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. - } - "textDocument/rename" => { - lsp_store.update(&mut cx, |lsp_store, cx| { - if let Some(server) = - lsp_store.language_server_for_id(server_id) - { - let options = reg - .register_options - .map(|options| { - serde_json::from_value::( - options, - ) - }) - .transpose()?; - let options = match options { - None => OneOf::Left(true), - Some(options) => OneOf::Right(options), - }; - - server.update_capabilities(|capabilities| { - capabilities.rename_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } - anyhow::Ok(()) - })??; - } - _ => log::warn!("unhandled capability registration: {reg:?}"), - } - } + }) + .ok(); Ok(()) } } @@ -779,79 +667,27 @@ impl LocalLspStore { language_server .on_request::({ - let this = this.clone(); + let lsp_store = this.clone(); move |params, cx| { - let lsp_store = this.clone(); + let lsp_store = lsp_store.clone(); let mut cx = cx.clone(); async move { - for unreg in params.unregisterations.iter() { - match unreg.method.as_str() { - "workspace/didChangeWatchedFiles" => { - lsp_store.update(&mut cx, |lsp_store, cx| { - lsp_store - .as_local_mut()? - .on_lsp_unregister_did_change_watched_files( - server_id, &unreg.id, cx, + lsp_store + .update(&mut cx, |lsp_store, cx| { + if lsp_store.as_local().is_some() { + match lsp_store + .unregister_server_capabilities(server_id, params, cx) + { + Ok(()) => {} + Err(e) => { + log::error!( + "Failed to unregister server capabilities: {e:#}" ); - Some(()) - })?; - } - "workspace/didChangeConfiguration" => { - // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. - } - "textDocument/rename" => { - lsp_store.update(&mut cx, |lsp_store, cx| { - if let Some(server) = - lsp_store.language_server_for_id(server_id) - { - server.update_capabilities(|capabilities| { - capabilities.rename_provider = None - }); - notify_server_capabilities_updated(&server, cx); } - })?; + } } - "textDocument/rangeFormatting" => { - lsp_store.update(&mut cx, |lsp_store, cx| { - if let Some(server) = - lsp_store.language_server_for_id(server_id) - { - server.update_capabilities(|capabilities| { - capabilities.document_range_formatting_provider = - None - }); - notify_server_capabilities_updated(&server, cx); - } - })?; - } - "textDocument/onTypeFormatting" => { - lsp_store.update(&mut cx, |lsp_store, cx| { - if let Some(server) = - lsp_store.language_server_for_id(server_id) - { - server.update_capabilities(|capabilities| { - capabilities.document_on_type_formatting_provider = - None; - }); - notify_server_capabilities_updated(&server, cx); - } - })?; - } - "textDocument/formatting" => { - lsp_store.update(&mut cx, |lsp_store, cx| { - if let Some(server) = - lsp_store.language_server_for_id(server_id) - { - server.update_capabilities(|capabilities| { - capabilities.document_formatting_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - })?; - } - _ => log::warn!("unhandled capability unregistration: {unreg:?}"), - } - } + }) + .ok(); Ok(()) } } @@ -3519,6 +3355,30 @@ impl LocalLspStore { Ok(workspace_config) } + + fn language_server_for_id(&self, id: LanguageServerId) -> Option> { + if let Some(LanguageServerState::Running { server, .. }) = self.language_servers.get(&id) { + Some(server.clone()) + } else if let Some((_, server)) = self.supplementary_language_servers.get(&id) { + Some(Arc::clone(server)) + } else { + None + } + } +} + +fn parse_register_capabilities( + reg: lsp::Registration, +) -> anyhow::Result> { + let caps = match reg + .register_options + .map(|options| serde_json::from_value::(options)) + .transpose()? + { + None => OneOf::Left(true), + Some(options) => OneOf::Right(options), + }; + Ok(caps) } fn notify_server_capabilities_updated(server: &LanguageServer, cx: &mut Context) { @@ -9434,16 +9294,7 @@ impl LspStore { } pub fn language_server_for_id(&self, id: LanguageServerId) -> Option> { - let local_lsp_store = self.as_local()?; - if let Some(LanguageServerState::Running { server, .. }) = - local_lsp_store.language_servers.get(&id) - { - Some(server.clone()) - } else if let Some((_, server)) = local_lsp_store.supplementary_language_servers.get(&id) { - Some(Arc::clone(server)) - } else { - None - } + self.as_local()?.language_server_for_id(id) } fn on_lsp_progress( @@ -11808,6 +11659,361 @@ impl LspStore { .log_err(); } } + + fn register_server_capabilities( + &mut self, + server_id: LanguageServerId, + params: lsp::RegistrationParams, + cx: &mut Context, + ) -> anyhow::Result<()> { + let server = self + .language_server_for_id(server_id) + .with_context(|| format!("no server {server_id} found"))?; + for reg in params.registrations { + match reg.method.as_str() { + "workspace/didChangeWatchedFiles" => { + if let Some(options) = reg.register_options { + let notify = if let Some(local_lsp_store) = self.as_local_mut() { + let caps = serde_json::from_value(options)?; + local_lsp_store + .on_lsp_did_change_watched_files(server_id, ®.id, caps, cx); + true + } else { + false + }; + if notify { + notify_server_capabilities_updated(&server, cx); + } + } + } + "workspace/didChangeConfiguration" => { + // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. + } + "workspace/symbol" => { + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.workspace_symbol_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } + "workspace/fileOperations" => { + let caps = reg + .register_options + .map(serde_json::from_value) + .transpose()? + .unwrap_or_default(); + server.update_capabilities(|capabilities| { + capabilities + .workspace + .get_or_insert_default() + .file_operations = Some(caps); + }); + notify_server_capabilities_updated(&server, cx); + } + "workspace/executeCommand" => { + let options = reg + .register_options + .map(serde_json::from_value) + .transpose()? + .unwrap_or_default(); + server.update_capabilities(|capabilities| { + capabilities.execute_command_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/rangeFormatting" => { + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.document_range_formatting_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/onTypeFormatting" => { + let options = reg + .register_options + .map(serde_json::from_value) + .transpose()? + .unwrap_or_default(); + server.update_capabilities(|capabilities| { + capabilities.document_on_type_formatting_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/formatting" => { + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.document_formatting_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/rename" => { + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.rename_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/inlayHint" => { + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.inlay_hint_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/documentSymbol" => { + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.document_symbol_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/codeAction" => { + let options = reg + .register_options + .map(serde_json::from_value) + .transpose()?; + let provider_capability = match options { + None => lsp::CodeActionProviderCapability::Simple(true), + Some(options) => lsp::CodeActionProviderCapability::Options(options), + }; + server.update_capabilities(|capabilities| { + capabilities.code_action_provider = Some(provider_capability); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/definition" => { + let caps = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.definition_provider = Some(caps); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/completion" => { + let caps = reg + .register_options + .map(serde_json::from_value) + .transpose()? + .unwrap_or_default(); + server.update_capabilities(|capabilities| { + capabilities.completion_provider = Some(caps); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/hover" => { + let caps = reg + .register_options + .map(serde_json::from_value) + .transpose()? + .unwrap_or_else(|| lsp::HoverProviderCapability::Simple(true)); + server.update_capabilities(|capabilities| { + capabilities.hover_provider = Some(caps); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/signatureHelp" => { + let caps = reg + .register_options + .map(serde_json::from_value) + .transpose()? + .unwrap_or_default(); + server.update_capabilities(|capabilities| { + capabilities.signature_help_provider = Some(caps); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/synchronization" => { + let caps = reg + .register_options + .map(serde_json::from_value) + .transpose()? + .unwrap_or_else(|| { + lsp::TextDocumentSyncCapability::Options( + lsp::TextDocumentSyncOptions::default(), + ) + }); + server.update_capabilities(|capabilities| { + capabilities.text_document_sync = Some(caps); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/codeLens" => { + let caps = reg + .register_options + .map(serde_json::from_value) + .transpose()? + .unwrap_or_else(|| lsp::CodeLensOptions { + resolve_provider: None, + }); + server.update_capabilities(|capabilities| { + capabilities.code_lens_provider = Some(caps); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/diagnostic" => { + let caps = reg + .register_options + .map(serde_json::from_value) + .transpose()? + .unwrap_or_else(|| { + lsp::DiagnosticServerCapabilities::RegistrationOptions( + lsp::DiagnosticRegistrationOptions::default(), + ) + }); + server.update_capabilities(|capabilities| { + capabilities.diagnostic_provider = Some(caps); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/colorProvider" => { + let caps = reg + .register_options + .map(serde_json::from_value) + .transpose()? + .unwrap_or_else(|| lsp::ColorProviderCapability::Simple(true)); + server.update_capabilities(|capabilities| { + capabilities.color_provider = Some(caps); + }); + notify_server_capabilities_updated(&server, cx); + } + _ => log::warn!("unhandled capability registration: {reg:?}"), + } + } + + Ok(()) + } + + fn unregister_server_capabilities( + &mut self, + server_id: LanguageServerId, + params: lsp::UnregistrationParams, + cx: &mut Context, + ) -> anyhow::Result<()> { + let server = self + .language_server_for_id(server_id) + .with_context(|| format!("no server {server_id} found"))?; + for unreg in params.unregisterations.iter() { + match unreg.method.as_str() { + "workspace/didChangeWatchedFiles" => { + let notify = if let Some(local_lsp_store) = self.as_local_mut() { + local_lsp_store + .on_lsp_unregister_did_change_watched_files(server_id, &unreg.id, cx); + true + } else { + false + }; + if notify { + notify_server_capabilities_updated(&server, cx); + } + } + "workspace/didChangeConfiguration" => { + // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. + } + "workspace/symbol" => { + server.update_capabilities(|capabilities| { + capabilities.workspace_symbol_provider = None + }); + notify_server_capabilities_updated(&server, cx); + } + "workspace/fileOperations" => { + server.update_capabilities(|capabilities| { + capabilities + .workspace + .get_or_insert_with(|| lsp::WorkspaceServerCapabilities { + workspace_folders: None, + file_operations: None, + }) + .file_operations = None; + }); + notify_server_capabilities_updated(&server, cx); + } + "workspace/executeCommand" => { + server.update_capabilities(|capabilities| { + capabilities.execute_command_provider = None; + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/rangeFormatting" => { + server.update_capabilities(|capabilities| { + capabilities.document_range_formatting_provider = None + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/onTypeFormatting" => { + server.update_capabilities(|capabilities| { + capabilities.document_on_type_formatting_provider = None; + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/formatting" => { + server.update_capabilities(|capabilities| { + capabilities.document_formatting_provider = None; + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/rename" => { + server.update_capabilities(|capabilities| capabilities.rename_provider = None); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/codeAction" => { + server.update_capabilities(|capabilities| { + capabilities.code_action_provider = None; + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/definition" => { + server.update_capabilities(|capabilities| { + capabilities.definition_provider = None; + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/completion" => { + server.update_capabilities(|capabilities| { + capabilities.completion_provider = None; + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/hover" => { + server.update_capabilities(|capabilities| { + capabilities.hover_provider = None; + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/signatureHelp" => { + server.update_capabilities(|capabilities| { + capabilities.signature_help_provider = None; + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/synchronization" => { + server.update_capabilities(|capabilities| { + capabilities.text_document_sync = None; + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/codeLens" => { + server.update_capabilities(|capabilities| { + capabilities.code_lens_provider = None; + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/diagnostic" => { + server.update_capabilities(|capabilities| { + capabilities.diagnostic_provider = None; + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/colorProvider" => { + server.update_capabilities(|capabilities| { + capabilities.color_provider = None; + }); + notify_server_capabilities_updated(&server, cx); + } + _ => log::warn!("unhandled capability unregistration: {unreg:?}"), + } + } + + Ok(()) + } } fn subscribe_to_binary_statuses( From 086ea3c61939f1329473cc9d0537f4b78bfacc0a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 11 Aug 2025 12:31:13 +0200 Subject: [PATCH 244/693] Port `terminal` tool to agent2 (#35918) Release Notes: - N/A --------- Co-authored-by: Ben Brandt --- Cargo.lock | 8 + crates/acp_thread/Cargo.toml | 1 + crates/acp_thread/src/acp_thread.rs | 67 ++- crates/acp_thread/src/terminal.rs | 87 ++++ crates/agent2/Cargo.toml | 10 +- crates/agent2/src/agent.rs | 5 +- crates/agent2/src/thread.rs | 138 +++--- crates/agent2/src/tools.rs | 2 + crates/agent2/src/tools/edit_file_tool.rs | 16 +- crates/agent2/src/tools/terminal_tool.rs | 489 ++++++++++++++++++++++ crates/agent_ui/src/acp/thread_view.rs | 106 ++++- crates/terminal/Cargo.toml | 8 + crates/terminal/src/terminal.rs | 57 ++- 13 files changed, 882 insertions(+), 112 deletions(-) create mode 100644 crates/acp_thread/src/terminal.rs create mode 100644 crates/agent2/src/tools/terminal_tool.rs diff --git a/Cargo.lock b/Cargo.lock index 4bb36fdeee..634bacd0f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,6 +27,7 @@ dependencies = [ "settings", "smol", "tempfile", + "terminal", "ui", "util", "workspace-hack", @@ -195,6 +196,7 @@ dependencies = [ "cloud_llm_client", "collections", "ctor", + "editor", "env_logger 0.11.8", "fs", "futures 0.3.31", @@ -209,6 +211,7 @@ dependencies = [ "log", "lsp", "paths", + "portable-pty", "pretty_assertions", "project", "prompt_store", @@ -219,12 +222,17 @@ dependencies = [ "serde_json", "settings", "smol", + "task", + "terminal", + "theme", "ui", "util", "uuid", "watch", + "which 6.0.3", "workspace-hack", "worktree", + "zlog", ] [[package]] diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index 37d2920045..33e88df761 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -32,6 +32,7 @@ serde.workspace = true serde_json.workspace = true settings.workspace = true smol.workspace = true +terminal.workspace = true ui.workspace = true util.workspace = true workspace-hack.workspace = true diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index f2bebf7391..d632e6e570 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1,8 +1,10 @@ mod connection; mod diff; +mod terminal; pub use connection::*; pub use diff::*; +pub use terminal::*; use action_log::ActionLog; use agent_client_protocol as acp; @@ -147,6 +149,14 @@ impl AgentThreadEntry { } } + pub fn terminals(&self) -> impl Iterator> { + if let AgentThreadEntry::ToolCall(call) = self { + itertools::Either::Left(call.terminals()) + } else { + itertools::Either::Right(std::iter::empty()) + } + } + pub fn locations(&self) -> Option<&[acp::ToolCallLocation]> { if let AgentThreadEntry::ToolCall(ToolCall { locations, .. }) = self { Some(locations) @@ -250,8 +260,17 @@ impl ToolCall { pub fn diffs(&self) -> impl Iterator> { self.content.iter().filter_map(|content| match content { - ToolCallContent::ContentBlock { .. } => None, - ToolCallContent::Diff { diff } => Some(diff), + ToolCallContent::Diff(diff) => Some(diff), + ToolCallContent::ContentBlock(_) => None, + ToolCallContent::Terminal(_) => None, + }) + } + + pub fn terminals(&self) -> impl Iterator> { + self.content.iter().filter_map(|content| match content { + ToolCallContent::Terminal(terminal) => Some(terminal), + ToolCallContent::ContentBlock(_) => None, + ToolCallContent::Diff(_) => None, }) } @@ -387,8 +406,9 @@ impl ContentBlock { #[derive(Debug)] pub enum ToolCallContent { - ContentBlock { content: ContentBlock }, - Diff { diff: Entity }, + ContentBlock(ContentBlock), + Diff(Entity), + Terminal(Entity), } impl ToolCallContent { @@ -398,19 +418,20 @@ impl ToolCallContent { cx: &mut App, ) -> Self { match content { - acp::ToolCallContent::Content { content } => Self::ContentBlock { - content: ContentBlock::new(content, &language_registry, cx), - }, - acp::ToolCallContent::Diff { diff } => Self::Diff { - diff: cx.new(|cx| Diff::from_acp(diff, language_registry, cx)), - }, + acp::ToolCallContent::Content { content } => { + Self::ContentBlock(ContentBlock::new(content, &language_registry, cx)) + } + acp::ToolCallContent::Diff { diff } => { + Self::Diff(cx.new(|cx| Diff::from_acp(diff, language_registry, cx))) + } } } pub fn to_markdown(&self, cx: &App) -> String { match self { - Self::ContentBlock { content } => content.to_markdown(cx).to_string(), - Self::Diff { diff } => diff.read(cx).to_markdown(cx), + Self::ContentBlock(content) => content.to_markdown(cx).to_string(), + Self::Diff(diff) => diff.read(cx).to_markdown(cx), + Self::Terminal(terminal) => terminal.read(cx).to_markdown(cx), } } } @@ -419,6 +440,7 @@ impl ToolCallContent { pub enum ToolCallUpdate { UpdateFields(acp::ToolCallUpdate), UpdateDiff(ToolCallUpdateDiff), + UpdateTerminal(ToolCallUpdateTerminal), } impl ToolCallUpdate { @@ -426,6 +448,7 @@ impl ToolCallUpdate { match self { Self::UpdateFields(update) => &update.id, Self::UpdateDiff(diff) => &diff.id, + Self::UpdateTerminal(terminal) => &terminal.id, } } } @@ -448,6 +471,18 @@ pub struct ToolCallUpdateDiff { pub diff: Entity, } +impl From for ToolCallUpdate { + fn from(terminal: ToolCallUpdateTerminal) -> Self { + Self::UpdateTerminal(terminal) + } +} + +#[derive(Debug, PartialEq)] +pub struct ToolCallUpdateTerminal { + pub id: acp::ToolCallId, + pub terminal: Entity, +} + #[derive(Debug, Default)] pub struct Plan { pub entries: Vec, @@ -760,7 +795,13 @@ impl AcpThread { current_call.content.clear(); current_call .content - .push(ToolCallContent::Diff { diff: update.diff }); + .push(ToolCallContent::Diff(update.diff)); + } + ToolCallUpdate::UpdateTerminal(update) => { + current_call.content.clear(); + current_call + .content + .push(ToolCallContent::Terminal(update.terminal)); } } diff --git a/crates/acp_thread/src/terminal.rs b/crates/acp_thread/src/terminal.rs new file mode 100644 index 0000000000..b800873737 --- /dev/null +++ b/crates/acp_thread/src/terminal.rs @@ -0,0 +1,87 @@ +use gpui::{App, AppContext, Context, Entity}; +use language::LanguageRegistry; +use markdown::Markdown; +use std::{path::PathBuf, process::ExitStatus, sync::Arc, time::Instant}; + +pub struct Terminal { + command: Entity, + working_dir: Option, + terminal: Entity, + started_at: Instant, + output: Option, +} + +pub struct TerminalOutput { + pub ended_at: Instant, + pub exit_status: Option, + pub was_content_truncated: bool, + pub original_content_len: usize, + pub content_line_count: usize, + pub finished_with_empty_output: bool, +} + +impl Terminal { + pub fn new( + command: String, + working_dir: Option, + terminal: Entity, + language_registry: Arc, + cx: &mut Context, + ) -> Self { + Self { + command: cx + .new(|cx| Markdown::new(command.into(), Some(language_registry.clone()), None, cx)), + working_dir, + terminal, + started_at: Instant::now(), + output: None, + } + } + + pub fn finish( + &mut self, + exit_status: Option, + original_content_len: usize, + truncated_content_len: usize, + content_line_count: usize, + finished_with_empty_output: bool, + cx: &mut Context, + ) { + self.output = Some(TerminalOutput { + ended_at: Instant::now(), + exit_status, + was_content_truncated: truncated_content_len < original_content_len, + original_content_len, + content_line_count, + finished_with_empty_output, + }); + cx.notify(); + } + + pub fn command(&self) -> &Entity { + &self.command + } + + pub fn working_dir(&self) -> &Option { + &self.working_dir + } + + pub fn started_at(&self) -> Instant { + self.started_at + } + + pub fn output(&self) -> Option<&TerminalOutput> { + self.output.as_ref() + } + + pub fn inner(&self) -> &Entity { + &self.terminal + } + + pub fn to_markdown(&self, cx: &App) -> String { + format!( + "Terminal:\n```\n{}\n```\n", + self.terminal.read(cx).get_content() + ) + } +} diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index c1c3f2d459..65452f60fc 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -33,6 +33,7 @@ language_model.workspace = true language_models.workspace = true log.workspace = true paths.workspace = true +portable-pty.workspace = true project.workspace = true prompt_store.workspace = true rust-embed.workspace = true @@ -41,16 +42,20 @@ serde.workspace = true serde_json.workspace = true settings.workspace = true smol.workspace = true +task.workspace = true +terminal.workspace = true ui.workspace = true util.workspace = true uuid.workspace = true watch.workspace = true +which.workspace = true workspace-hack.workspace = true [dev-dependencies] ctor.workspace = true client = { workspace = true, "features" = ["test-support"] } clock = { workspace = true, "features" = ["test-support"] } +editor = { workspace = true, "features" = ["test-support"] } env_logger.workspace = true fs = { workspace = true, "features" = ["test-support"] } gpui = { workspace = true, "features" = ["test-support"] } @@ -58,8 +63,11 @@ gpui_tokio.workspace = true language = { workspace = true, "features" = ["test-support"] } language_model = { workspace = true, "features" = ["test-support"] } lsp = { workspace = true, "features" = ["test-support"] } +pretty_assertions.workspace = true project = { workspace = true, "features" = ["test-support"] } reqwest_client.workspace = true settings = { workspace = true, "features" = ["test-support"] } +terminal = { workspace = true, "features" = ["test-support"] } +theme = { workspace = true, "features" = ["test-support"] } worktree = { workspace = true, "features" = ["test-support"] } -pretty_assertions.workspace = true +zlog.workspace = true diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 5be3892d60..edb79003b4 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,5 +1,7 @@ use crate::{AgentResponseEvent, Thread, templates::Templates}; -use crate::{EditFileTool, FindPathTool, ReadFileTool, ThinkingTool, ToolCallAuthorization}; +use crate::{ + EditFileTool, FindPathTool, ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, +}; use acp_thread::ModelSelector; use agent_client_protocol as acp; use anyhow::{Context as _, Result, anyhow}; @@ -418,6 +420,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { thread.add_tool(FindPathTool::new(project.clone())); thread.add_tool(ReadFileTool::new(project.clone(), action_log)); thread.add_tool(EditFileTool::new(cx.entity())); + thread.add_tool(TerminalTool::new(project.clone(), cx)); thread }); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index a0a2a3a2b0..dd8e5476ab 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1,5 +1,4 @@ use crate::{SystemPromptTemplate, Template, Templates}; -use acp_thread::Diff; use action_log::ActionLog; use agent_client_protocol as acp; use anyhow::{Context as _, Result, anyhow}; @@ -802,47 +801,6 @@ impl AgentResponseEventStream { .ok(); } - fn authorize_tool_call( - &self, - id: &LanguageModelToolUseId, - title: String, - kind: acp::ToolKind, - input: serde_json::Value, - ) -> impl use<> + Future> { - let (response_tx, response_rx) = oneshot::channel(); - self.0 - .unbounded_send(Ok(AgentResponseEvent::ToolCallAuthorization( - ToolCallAuthorization { - tool_call: Self::initial_tool_call(id, title, kind, input), - options: vec![ - acp::PermissionOption { - id: acp::PermissionOptionId("always_allow".into()), - name: "Always Allow".into(), - kind: acp::PermissionOptionKind::AllowAlways, - }, - acp::PermissionOption { - id: acp::PermissionOptionId("allow".into()), - name: "Allow".into(), - kind: acp::PermissionOptionKind::AllowOnce, - }, - acp::PermissionOption { - id: acp::PermissionOptionId("deny".into()), - name: "Deny".into(), - kind: acp::PermissionOptionKind::RejectOnce, - }, - ], - response: response_tx, - }, - ))) - .ok(); - async move { - match response_rx.await?.0.as_ref() { - "allow" | "always_allow" => Ok(()), - _ => Err(anyhow!("Permission to run tool denied by user")), - } - } - } - fn send_tool_call( &self, id: &LanguageModelToolUseId, @@ -894,18 +852,6 @@ impl AgentResponseEventStream { .ok(); } - fn update_tool_call_diff(&self, tool_use_id: &LanguageModelToolUseId, diff: Entity) { - self.0 - .unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate( - acp_thread::ToolCallUpdateDiff { - id: acp::ToolCallId(tool_use_id.to_string().into()), - diff, - } - .into(), - ))) - .ok(); - } - fn send_stop(&self, reason: StopReason) { match reason { StopReason::EndTurn => { @@ -979,17 +925,71 @@ impl ToolCallEventStream { .update_tool_call_fields(&self.tool_use_id, fields); } - pub fn update_diff(&self, diff: Entity) { - self.stream.update_tool_call_diff(&self.tool_use_id, diff); + pub fn update_diff(&self, diff: Entity) { + self.stream + .0 + .unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate( + acp_thread::ToolCallUpdateDiff { + id: acp::ToolCallId(self.tool_use_id.to_string().into()), + diff, + } + .into(), + ))) + .ok(); + } + + pub fn update_terminal(&self, terminal: Entity) { + self.stream + .0 + .unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate( + acp_thread::ToolCallUpdateTerminal { + id: acp::ToolCallId(self.tool_use_id.to_string().into()), + terminal, + } + .into(), + ))) + .ok(); } pub fn authorize(&self, title: String) -> impl use<> + Future> { - self.stream.authorize_tool_call( - &self.tool_use_id, - title, - self.kind.clone(), - self.input.clone(), - ) + let (response_tx, response_rx) = oneshot::channel(); + self.stream + .0 + .unbounded_send(Ok(AgentResponseEvent::ToolCallAuthorization( + ToolCallAuthorization { + tool_call: AgentResponseEventStream::initial_tool_call( + &self.tool_use_id, + title, + self.kind.clone(), + self.input.clone(), + ), + options: vec![ + acp::PermissionOption { + id: acp::PermissionOptionId("always_allow".into()), + name: "Always Allow".into(), + kind: acp::PermissionOptionKind::AllowAlways, + }, + acp::PermissionOption { + id: acp::PermissionOptionId("allow".into()), + name: "Allow".into(), + kind: acp::PermissionOptionKind::AllowOnce, + }, + acp::PermissionOption { + id: acp::PermissionOptionId("deny".into()), + name: "Deny".into(), + kind: acp::PermissionOptionKind::RejectOnce, + }, + ], + response: response_tx, + }, + ))) + .ok(); + async move { + match response_rx.await?.0.as_ref() { + "allow" | "always_allow" => Ok(()), + _ => Err(anyhow!("Permission to run tool denied by user")), + } + } } } @@ -1000,7 +1000,7 @@ pub struct ToolCallEventStreamReceiver( #[cfg(test)] impl ToolCallEventStreamReceiver { - pub async fn expect_tool_authorization(&mut self) -> ToolCallAuthorization { + pub async fn expect_authorization(&mut self) -> ToolCallAuthorization { let event = self.0.next().await; if let Some(Ok(AgentResponseEvent::ToolCallAuthorization(auth))) = event { auth @@ -1008,6 +1008,18 @@ impl ToolCallEventStreamReceiver { panic!("Expected ToolCallAuthorization but got: {:?}", event); } } + + pub async fn expect_terminal(&mut self) -> Entity { + let event = self.0.next().await; + if let Some(Ok(AgentResponseEvent::ToolCallUpdate( + acp_thread::ToolCallUpdate::UpdateTerminal(update), + ))) = event + { + update.terminal + } else { + panic!("Expected terminal but got: {:?}", event); + } + } } #[cfg(test)] diff --git a/crates/agent2/src/tools.rs b/crates/agent2/src/tools.rs index 5fe13db854..df4a7a9580 100644 --- a/crates/agent2/src/tools.rs +++ b/crates/agent2/src/tools.rs @@ -1,9 +1,11 @@ mod edit_file_tool; mod find_path_tool; mod read_file_tool; +mod terminal_tool; mod thinking_tool; pub use edit_file_tool::*; pub use find_path_tool::*; pub use read_file_tool::*; +pub use terminal_tool::*; pub use thinking_tool::*; diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 48e5d37586..d9a4cdf8ba 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -942,7 +942,7 @@ mod tests { ) }); - let event = stream_rx.expect_tool_authorization().await; + let event = stream_rx.expect_authorization().await; assert_eq!(event.tool_call.title, "test 1 (local settings)"); // Test 2: Path outside project should require confirmation @@ -959,7 +959,7 @@ mod tests { ) }); - let event = stream_rx.expect_tool_authorization().await; + let event = stream_rx.expect_authorization().await; assert_eq!(event.tool_call.title, "test 2"); // Test 3: Relative path without .zed should not require confirmation @@ -992,7 +992,7 @@ mod tests { cx, ) }); - let event = stream_rx.expect_tool_authorization().await; + let event = stream_rx.expect_authorization().await; assert_eq!(event.tool_call.title, "test 4 (local settings)"); // Test 5: When always_allow_tool_actions is enabled, no confirmation needed @@ -1088,7 +1088,7 @@ mod tests { }); if should_confirm { - stream_rx.expect_tool_authorization().await; + stream_rx.expect_authorization().await; } else { auth.await.unwrap(); assert!( @@ -1192,7 +1192,7 @@ mod tests { }); if should_confirm { - stream_rx.expect_tool_authorization().await; + stream_rx.expect_authorization().await; } else { auth.await.unwrap(); assert!( @@ -1276,7 +1276,7 @@ mod tests { }); if should_confirm { - stream_rx.expect_tool_authorization().await; + stream_rx.expect_authorization().await; } else { auth.await.unwrap(); assert!( @@ -1339,7 +1339,7 @@ mod tests { ) }); - stream_rx.expect_tool_authorization().await; + stream_rx.expect_authorization().await; // Test outside path with different modes let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); @@ -1355,7 +1355,7 @@ mod tests { ) }); - stream_rx.expect_tool_authorization().await; + stream_rx.expect_authorization().await; // Test normal path with different modes let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs new file mode 100644 index 0000000000..c0b34444dd --- /dev/null +++ b/crates/agent2/src/tools/terminal_tool.rs @@ -0,0 +1,489 @@ +use agent_client_protocol as acp; +use anyhow::Result; +use futures::{FutureExt as _, future::Shared}; +use gpui::{App, AppContext, Entity, SharedString, Task}; +use project::{Project, terminals::TerminalKind}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Settings; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; +use util::{ResultExt, get_system_shell, markdown::MarkdownInlineCode}; + +use crate::{AgentTool, ToolCallEventStream}; + +const COMMAND_OUTPUT_LIMIT: usize = 16 * 1024; + +/// Executes a shell one-liner and returns the combined output. +/// +/// This tool spawns a process using the user's shell, reads from stdout and stderr (preserving the order of writes), and returns a string with the combined output result. +/// +/// The output results will be shown to the user already, only list it again if necessary, avoid being redundant. +/// +/// Make sure you use the `cd` parameter to navigate to one of the root directories of the project. NEVER do it as part of the `command` itself, otherwise it will error. +/// +/// Do not use this tool for commands that run indefinitely, such as servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers that don't terminate on their own. +/// +/// Remember that each invocation of this tool will spawn a new shell process, so you can't rely on any state from previous invocations. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct TerminalToolInput { + /// The one-liner command to execute. + command: String, + /// Working directory for the command. This must be one of the root directories of the project. + cd: String, +} + +pub struct TerminalTool { + project: Entity, + determine_shell: Shared>, +} + +impl TerminalTool { + pub fn new(project: Entity, cx: &mut App) -> Self { + let determine_shell = cx.background_spawn(async move { + if cfg!(windows) { + return get_system_shell(); + } + + if which::which("bash").is_ok() { + log::info!("agent selected bash for terminal tool"); + "bash".into() + } else { + let shell = get_system_shell(); + log::info!("agent selected {shell} for terminal tool"); + shell + } + }); + Self { + project, + determine_shell: determine_shell.shared(), + } + } + + fn authorize( + &self, + input: &TerminalToolInput, + event_stream: &ToolCallEventStream, + cx: &App, + ) -> Task> { + if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions { + return Task::ready(Ok(())); + } + + // TODO: do we want to have a special title here? + cx.foreground_executor() + .spawn(event_stream.authorize(self.initial_title(Ok(input.clone())).to_string())) + } +} + +impl AgentTool for TerminalTool { + type Input = TerminalToolInput; + type Output = String; + + fn name(&self) -> SharedString { + "terminal".into() + } + + fn kind(&self) -> acp::ToolKind { + acp::ToolKind::Execute + } + + fn initial_title(&self, input: Result) -> SharedString { + if let Ok(input) = input { + let mut lines = input.command.lines(); + let first_line = lines.next().unwrap_or_default(); + let remaining_line_count = lines.count(); + match remaining_line_count { + 0 => MarkdownInlineCode(&first_line).to_string().into(), + 1 => MarkdownInlineCode(&format!( + "{} - {} more line", + first_line, remaining_line_count + )) + .to_string() + .into(), + n => MarkdownInlineCode(&format!("{} - {} more lines", first_line, n)) + .to_string() + .into(), + } + } else { + "Run terminal command".into() + } + } + + fn run( + self: Arc, + input: Self::Input, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> { + let language_registry = self.project.read(cx).languages().clone(); + let working_dir = match working_dir(&input, &self.project, cx) { + Ok(dir) => dir, + Err(err) => return Task::ready(Err(err)), + }; + let program = self.determine_shell.clone(); + let command = if cfg!(windows) { + format!("$null | & {{{}}}", input.command.replace("\"", "'")) + } else if let Some(cwd) = working_dir + .as_ref() + .and_then(|cwd| cwd.as_os_str().to_str()) + { + // Make sure once we're *inside* the shell, we cd into `cwd` + format!("(cd {cwd}; {}) self.project.update(cx, |project, cx| { + project.directory_environment(dir.as_path().into(), cx) + }), + None => Task::ready(None).shared(), + }; + + let env = cx.spawn(async move |_| { + let mut env = env.await.unwrap_or_default(); + if cfg!(unix) { + env.insert("PAGER".into(), "cat".into()); + } + env + }); + + let authorize = self.authorize(&input, &event_stream, cx); + + cx.spawn({ + async move |cx| { + authorize.await?; + + let program = program.await; + let env = env.await; + let terminal = self + .project + .update(cx, |project, cx| { + project.create_terminal( + TerminalKind::Task(task::SpawnInTerminal { + command: Some(program), + args, + cwd: working_dir.clone(), + env, + ..Default::default() + }), + cx, + ) + })? + .await?; + let acp_terminal = cx.new(|cx| { + acp_thread::Terminal::new( + input.command.clone(), + working_dir.clone(), + terminal.clone(), + language_registry, + cx, + ) + })?; + event_stream.update_terminal(acp_terminal.clone()); + + let exit_status = terminal + .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))? + .await; + let (content, content_line_count) = terminal.read_with(cx, |terminal, _| { + (terminal.get_content(), terminal.total_lines()) + })?; + + let (processed_content, finished_with_empty_output) = process_content( + &content, + &input.command, + exit_status.map(portable_pty::ExitStatus::from), + ); + + acp_terminal + .update(cx, |terminal, cx| { + terminal.finish( + exit_status, + content.len(), + processed_content.len(), + content_line_count, + finished_with_empty_output, + cx, + ); + }) + .log_err(); + + Ok(processed_content) + } + }) + } +} + +fn process_content( + content: &str, + command: &str, + exit_status: Option, +) -> (String, bool) { + let should_truncate = content.len() > COMMAND_OUTPUT_LIMIT; + + let content = if should_truncate { + let mut end_ix = COMMAND_OUTPUT_LIMIT.min(content.len()); + while !content.is_char_boundary(end_ix) { + end_ix -= 1; + } + // Don't truncate mid-line, clear the remainder of the last line + end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix); + &content[..end_ix] + } else { + content + }; + let content = content.trim(); + let is_empty = content.is_empty(); + let content = format!("```\n{content}\n```"); + let content = if should_truncate { + format!( + "Command output too long. The first {} bytes:\n\n{content}", + content.len(), + ) + } else { + content + }; + + let content = match exit_status { + Some(exit_status) if exit_status.success() => { + if is_empty { + "Command executed successfully.".to_string() + } else { + content.to_string() + } + } + Some(exit_status) => { + if is_empty { + format!( + "Command \"{command}\" failed with exit code {}.", + exit_status.exit_code() + ) + } else { + format!( + "Command \"{command}\" failed with exit code {}.\n\n{content}", + exit_status.exit_code() + ) + } + } + None => { + format!( + "Command failed or was interrupted.\nPartial output captured:\n\n{}", + content, + ) + } + }; + (content, is_empty) +} + +fn working_dir( + input: &TerminalToolInput, + project: &Entity, + cx: &mut App, +) -> Result> { + let project = project.read(cx); + let cd = &input.cd; + + if cd == "." || cd == "" { + // Accept "." or "" as meaning "the one worktree" if we only have one worktree. + let mut worktrees = project.worktrees(cx); + + match worktrees.next() { + Some(worktree) => { + anyhow::ensure!( + worktrees.next().is_none(), + "'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly.", + ); + Ok(Some(worktree.read(cx).abs_path().to_path_buf())) + } + None => Ok(None), + } + } else { + let input_path = Path::new(cd); + + if input_path.is_absolute() { + // Absolute paths are allowed, but only if they're in one of the project's worktrees. + if project + .worktrees(cx) + .any(|worktree| input_path.starts_with(&worktree.read(cx).abs_path())) + { + return Ok(Some(input_path.into())); + } + } else { + if let Some(worktree) = project.worktree_for_root_name(cd, cx) { + return Ok(Some(worktree.read(cx).abs_path().to_path_buf())); + } + } + + anyhow::bail!("`cd` directory {cd:?} was not in any of the project's worktrees."); + } +} + +#[cfg(test)] +mod tests { + use agent_settings::AgentSettings; + use editor::EditorSettings; + use fs::RealFs; + use gpui::{BackgroundExecutor, TestAppContext}; + use pretty_assertions::assert_eq; + use serde_json::json; + use settings::{Settings, SettingsStore}; + use terminal::terminal_settings::TerminalSettings; + use theme::ThemeSettings; + use util::test::TempTree; + + use crate::AgentResponseEvent; + + use super::*; + + fn init_test(executor: &BackgroundExecutor, cx: &mut TestAppContext) { + zlog::init_test(); + + executor.allow_parking(); + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + Project::init_settings(cx); + ThemeSettings::register(cx); + TerminalSettings::register(cx); + EditorSettings::register(cx); + AgentSettings::register(cx); + }); + } + + #[gpui::test] + async fn test_interactive_command(executor: BackgroundExecutor, cx: &mut TestAppContext) { + if cfg!(windows) { + return; + } + + init_test(&executor, cx); + + let fs = Arc::new(RealFs::new(None, executor)); + let tree = TempTree::new(json!({ + "project": {}, + })); + let project: Entity = + Project::test(fs, [tree.path().join("project").as_path()], cx).await; + + let input = TerminalToolInput { + command: "cat".to_owned(), + cd: tree + .path() + .join("project") + .as_path() + .to_string_lossy() + .to_string(), + }; + let (event_stream_tx, mut event_stream_rx) = ToolCallEventStream::test(); + let result = cx + .update(|cx| Arc::new(TerminalTool::new(project, cx)).run(input, event_stream_tx, cx)); + + let auth = event_stream_rx.expect_authorization().await; + auth.response.send(auth.options[0].id.clone()).unwrap(); + event_stream_rx.expect_terminal().await; + assert_eq!(result.await.unwrap(), "Command executed successfully."); + } + + #[gpui::test] + async fn test_working_directory(executor: BackgroundExecutor, cx: &mut TestAppContext) { + if cfg!(windows) { + return; + } + + init_test(&executor, cx); + + let fs = Arc::new(RealFs::new(None, executor)); + let tree = TempTree::new(json!({ + "project": {}, + "other-project": {}, + })); + let project: Entity = + Project::test(fs, [tree.path().join("project").as_path()], cx).await; + + let check = |input, expected, cx: &mut TestAppContext| { + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let result = cx.update(|cx| { + Arc::new(TerminalTool::new(project.clone(), cx)).run(input, stream_tx, cx) + }); + cx.run_until_parked(); + let event = stream_rx.try_next(); + if let Ok(Some(Ok(AgentResponseEvent::ToolCallAuthorization(auth)))) = event { + auth.response.send(auth.options[0].id.clone()).unwrap(); + } + + cx.spawn(async move |_| { + let output = result.await; + assert_eq!(output.ok(), expected); + }) + }; + + check( + TerminalToolInput { + command: "pwd".into(), + cd: ".".into(), + }, + Some(format!( + "```\n{}\n```", + tree.path().join("project").display() + )), + cx, + ) + .await; + + check( + TerminalToolInput { + command: "pwd".into(), + cd: "other-project".into(), + }, + None, // other-project is a dir, but *not* a worktree (yet) + cx, + ) + .await; + + // Absolute path above the worktree root + check( + TerminalToolInput { + command: "pwd".into(), + cd: tree.path().to_string_lossy().into(), + }, + None, + cx, + ) + .await; + + project + .update(cx, |project, cx| { + project.create_worktree(tree.path().join("other-project"), true, cx) + }) + .await + .unwrap(); + + check( + TerminalToolInput { + command: "pwd".into(), + cd: "other-project".into(), + }, + Some(format!( + "```\n{}\n```", + tree.path().join("other-project").display() + )), + cx, + ) + .await; + + check( + TerminalToolInput { + command: "pwd".into(), + cd: ".".into(), + }, + None, + cx, + ) + .await; + } +} diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 01980b8fb7..2536612ece 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1,17 +1,13 @@ +use acp_thread::{ + AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, + LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, +}; use acp_thread::{AgentConnection, Plan}; +use action_log::ActionLog; +use agent_client_protocol as acp; use agent_servers::AgentServer; use agent_settings::{AgentSettings, NotifyWhenAgentWaiting}; use audio::{Audio, Sound}; -use std::cell::RefCell; -use std::collections::BTreeMap; -use std::path::Path; -use std::process::ExitStatus; -use std::rc::Rc; -use std::sync::Arc; -use std::time::Duration; - -use action_log::ActionLog; -use agent_client_protocol as acp; use buffer_diff::BufferDiff; use collections::{HashMap, HashSet}; use editor::{ @@ -32,6 +28,11 @@ use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; use parking_lot::Mutex; use project::{CompletionIntent, Project}; use settings::{Settings as _, SettingsStore}; +use std::{ + cell::RefCell, collections::BTreeMap, path::Path, process::ExitStatus, rc::Rc, sync::Arc, + time::Duration, +}; +use terminal_view::TerminalView; use text::{Anchor, BufferSnapshot}; use theme::ThemeSettings; use ui::{ @@ -41,11 +42,6 @@ use util::ResultExt; use workspace::{CollaboratorId, Workspace}; use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage}; -use ::acp_thread::{ - AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, - LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, -}; - use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet}; use crate::acp::message_history::MessageHistory; use crate::agent_diff::AgentDiff; @@ -63,6 +59,7 @@ pub struct AcpThreadView { project: Entity, thread_state: ThreadState, diff_editors: HashMap>, + terminal_views: HashMap>, message_editor: Entity, message_set_from_history: Option, _message_editor_subscription: Subscription, @@ -193,6 +190,7 @@ impl AcpThreadView { notifications: Vec::new(), notification_subscriptions: HashMap::default(), diff_editors: Default::default(), + terminal_views: Default::default(), list_state: list_state.clone(), scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), last_error: None, @@ -676,6 +674,16 @@ impl AcpThreadView { entry_ix: usize, window: &mut Window, cx: &mut Context, + ) { + self.sync_diff_multibuffers(entry_ix, window, cx); + self.sync_terminals(entry_ix, window, cx); + } + + fn sync_diff_multibuffers( + &mut self, + entry_ix: usize, + window: &mut Window, + cx: &mut Context, ) { let Some(multibuffers) = self.entry_diff_multibuffers(entry_ix, cx) else { return; @@ -739,6 +747,50 @@ impl AcpThreadView { ) } + fn sync_terminals(&mut self, entry_ix: usize, window: &mut Window, cx: &mut Context) { + let Some(terminals) = self.entry_terminals(entry_ix, cx) else { + return; + }; + + let terminals = terminals.collect::>(); + + for terminal in terminals { + if self.terminal_views.contains_key(&terminal.entity_id()) { + return; + } + + let terminal_view = cx.new(|cx| { + let mut view = TerminalView::new( + terminal.read(cx).inner().clone(), + self.workspace.clone(), + None, + self.project.downgrade(), + window, + cx, + ); + view.set_embedded_mode(None, cx); + view + }); + + let entity_id = terminal.entity_id(); + cx.observe_release(&terminal, move |this, _, _| { + this.terminal_views.remove(&entity_id); + }) + .detach(); + + self.terminal_views.insert(entity_id, terminal_view); + } + } + + fn entry_terminals( + &self, + entry_ix: usize, + cx: &App, + ) -> Option>> { + let entry = self.thread()?.read(cx).entries().get(entry_ix)?; + Some(entry.terminals().map(|terminal| terminal.clone())) + } + fn authenticate( &mut self, method: acp::AuthMethodId, @@ -1106,7 +1158,7 @@ impl AcpThreadView { _ => tool_call .content .iter() - .any(|content| matches!(content, ToolCallContent::Diff { .. })), + .any(|content| matches!(content, ToolCallContent::Diff(_))), }; let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation; @@ -1303,7 +1355,7 @@ impl AcpThreadView { cx: &Context, ) -> AnyElement { match content { - ToolCallContent::ContentBlock { content } => { + ToolCallContent::ContentBlock(content) => { if let Some(md) = content.markdown() { div() .p_2() @@ -1318,9 +1370,8 @@ impl AcpThreadView { Empty.into_any_element() } } - ToolCallContent::Diff { diff, .. } => { - self.render_diff_editor(&diff.read(cx).multibuffer()) - } + ToolCallContent::Diff(diff) => self.render_diff_editor(&diff.read(cx).multibuffer()), + ToolCallContent::Terminal(terminal) => self.render_terminal(terminal), } } @@ -1389,6 +1440,21 @@ impl AcpThreadView { .into_any() } + fn render_terminal(&self, terminal: &Entity) -> AnyElement { + v_flex() + .h_72() + .child( + if let Some(terminal_view) = self.terminal_views.get(&terminal.entity_id()) { + // TODO: terminal has all the state we need to reproduce + // what we had in the terminal card. + terminal_view.clone().into_any_element() + } else { + Empty.into_any() + }, + ) + .into_any() + } + fn render_agent_logo(&self) -> AnyElement { Icon::new(self.agent.logo()) .color(Color::Muted) diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 93f61622c8..b1c0dd693f 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -5,6 +5,13 @@ edition.workspace = true publish.workspace = true license = "GPL-3.0-or-later" +[features] +test-support = [ + "collections/test-support", + "gpui/test-support", + "settings/test-support", +] + [lints] workspace = true @@ -39,5 +46,6 @@ workspace-hack.workspace = true windows.workspace = true [dev-dependencies] +gpui = { workspace = true, features = ["test-support"] } rand.workspace = true url.workspace = true diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index d6a09a590f..3e7d9c0ad4 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -58,7 +58,7 @@ use std::{ path::PathBuf, process::ExitStatus, sync::Arc, - time::{Duration, Instant}, + time::Instant, }; use thiserror::Error; @@ -534,10 +534,15 @@ impl TerminalBuilder { 'outer: loop { let mut events = Vec::new(); + + #[cfg(any(test, feature = "test-support"))] + let mut timer = cx.background_executor().simulate_random_delay().fuse(); + #[cfg(not(any(test, feature = "test-support")))] let mut timer = cx .background_executor() - .timer(Duration::from_millis(4)) + .timer(std::time::Duration::from_millis(4)) .fuse(); + let mut wakeup = false; loop { futures::select_biased! { @@ -2104,16 +2109,56 @@ pub fn rgba_color(r: u8, g: u8, b: u8) -> Hsla { #[cfg(test)] mod tests { + use super::*; + use crate::{ + IndexedCell, TerminalBounds, TerminalBuilder, TerminalContent, content_index_for_mouse, + rgb_for_index, + }; use alacritty_terminal::{ index::{Column, Line, Point as AlacPoint}, term::cell::Cell, }; - use gpui::{Pixels, Point, bounds, point, size}; + use collections::HashMap; + use gpui::{Pixels, Point, TestAppContext, bounds, point, size}; use rand::{Rng, distributions::Alphanumeric, rngs::ThreadRng, thread_rng}; - use crate::{ - IndexedCell, TerminalBounds, TerminalContent, content_index_for_mouse, rgb_for_index, - }; + #[cfg_attr(windows, ignore = "TODO: fix on windows")] + #[gpui::test] + async fn test_basic_terminal(cx: &mut TestAppContext) { + cx.executor().allow_parking(); + + let (completion_tx, completion_rx) = smol::channel::unbounded(); + let terminal = cx.new(|cx| { + TerminalBuilder::new( + None, + None, + None, + task::Shell::WithArguments { + program: "echo".into(), + args: vec!["hello".into()], + title_override: None, + }, + HashMap::default(), + CursorShape::default(), + AlternateScroll::On, + None, + false, + 0, + completion_tx, + cx, + ) + .unwrap() + .subscribe(cx) + }); + assert_eq!( + completion_rx.recv().await.unwrap(), + Some(ExitStatus::default()) + ); + assert_eq!( + terminal.update(cx, |term, _| term.get_content()).trim(), + "hello" + ); + } #[test] fn test_rgb_for_index() { From 702a95ffb22fc0d36b74ca1fad683defd8dbc54e Mon Sep 17 00:00:00 2001 From: localcc Date: Mon, 11 Aug 2025 13:57:30 +0200 Subject: [PATCH 245/693] Fix underline DPI (#35816) Release Notes: - Fixed wavy underlines looking inconsistent on different displays --- crates/gpui/src/platform/blade/shaders.wgsl | 9 +++++++-- crates/gpui/src/platform/mac/shaders.metal | 9 +++++++-- crates/gpui/src/platform/windows/shaders.hlsl | 11 ++++++++--- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/crates/gpui/src/platform/blade/shaders.wgsl b/crates/gpui/src/platform/blade/shaders.wgsl index b1ffb1812e..95980b54fe 100644 --- a/crates/gpui/src/platform/blade/shaders.wgsl +++ b/crates/gpui/src/platform/blade/shaders.wgsl @@ -1057,6 +1057,9 @@ fn vs_underline(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) @fragment fn fs_underline(input: UnderlineVarying) -> @location(0) vec4 { + const WAVE_FREQUENCY: f32 = 2.0; + const WAVE_HEIGHT_RATIO: f32 = 0.8; + // Alpha clip first, since we don't have `clip_distance`. if (any(input.clip_distances < vec4(0.0))) { return vec4(0.0); @@ -1069,9 +1072,11 @@ fn fs_underline(input: UnderlineVarying) -> @location(0) vec4 { } let half_thickness = underline.thickness * 0.5; + let st = (input.position.xy - underline.bounds.origin) / underline.bounds.size.y - vec2(0.0, 0.5); - let frequency = M_PI_F * 3.0 * underline.thickness / 3.0; - let amplitude = 1.0 / (4.0 * underline.thickness); + let frequency = M_PI_F * WAVE_FREQUENCY * underline.thickness / underline.bounds.size.y; + let amplitude = (underline.thickness * WAVE_HEIGHT_RATIO) / underline.bounds.size.y; + let sine = sin(st.x * frequency) * amplitude; let dSine = cos(st.x * frequency) * amplitude * frequency; let distance = (st.y - sine) / sqrt(1.0 + dSine * dSine); diff --git a/crates/gpui/src/platform/mac/shaders.metal b/crates/gpui/src/platform/mac/shaders.metal index f9d5bdbf4c..83c978b853 100644 --- a/crates/gpui/src/platform/mac/shaders.metal +++ b/crates/gpui/src/platform/mac/shaders.metal @@ -567,15 +567,20 @@ vertex UnderlineVertexOutput underline_vertex( fragment float4 underline_fragment(UnderlineFragmentInput input [[stage_in]], constant Underline *underlines [[buffer(UnderlineInputIndex_Underlines)]]) { + const float WAVE_FREQUENCY = 2.0; + const float WAVE_HEIGHT_RATIO = 0.8; + Underline underline = underlines[input.underline_id]; if (underline.wavy) { float half_thickness = underline.thickness * 0.5; float2 origin = float2(underline.bounds.origin.x, underline.bounds.origin.y); + float2 st = ((input.position.xy - origin) / underline.bounds.size.height) - float2(0., 0.5); - float frequency = (M_PI_F * (3. * underline.thickness)) / 8.; - float amplitude = 1. / (2. * underline.thickness); + float frequency = (M_PI_F * WAVE_FREQUENCY * underline.thickness) / underline.bounds.size.height; + float amplitude = (underline.thickness * WAVE_HEIGHT_RATIO) / underline.bounds.size.height; + float sine = sin(st.x * frequency) * amplitude; float dSine = cos(st.x * frequency) * amplitude * frequency; float distance = (st.y - sine) / sqrt(1. + dSine * dSine); diff --git a/crates/gpui/src/platform/windows/shaders.hlsl b/crates/gpui/src/platform/windows/shaders.hlsl index 25830e4b6c..6fabe859e3 100644 --- a/crates/gpui/src/platform/windows/shaders.hlsl +++ b/crates/gpui/src/platform/windows/shaders.hlsl @@ -914,7 +914,7 @@ float4 path_rasterization_fragment(PathFragmentInput input): SV_Target { float2 dx = ddx(input.st_position); float2 dy = ddy(input.st_position); PathRasterizationSprite sprite = path_rasterization_sprites[input.vertex_id]; - + Background background = sprite.color; Bounds bounds = sprite.bounds; @@ -1021,13 +1021,18 @@ UnderlineVertexOutput underline_vertex(uint vertex_id: SV_VertexID, uint underli } float4 underline_fragment(UnderlineFragmentInput input): SV_Target { + const float WAVE_FREQUENCY = 2.0; + const float WAVE_HEIGHT_RATIO = 0.8; + Underline underline = underlines[input.underline_id]; if (underline.wavy) { float half_thickness = underline.thickness * 0.5; float2 origin = underline.bounds.origin; + float2 st = ((input.position.xy - origin) / underline.bounds.size.y) - float2(0., 0.5); - float frequency = (M_PI_F * (3. * underline.thickness)) / 8.; - float amplitude = 1. / (2. * underline.thickness); + float frequency = (M_PI_F * WAVE_FREQUENCY * underline.thickness) / underline.bounds.size.y; + float amplitude = (underline.thickness * WAVE_HEIGHT_RATIO) / underline.bounds.size.y; + float sine = sin(st.x * frequency) * amplitude; float dSine = cos(st.x * frequency) * amplitude * frequency; float distance = (st.y - sine) / sqrt(1. + dSine * dSine); From a88c533ffc4563ccd2403ace36884753cd3a8db2 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 11 Aug 2025 14:24:53 +0200 Subject: [PATCH 246/693] language: Fix rust-analyzer removing itself on download (#35971) Release Notes: - N/A\ --- crates/languages/src/rust.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 1d489052e6..e79f0c9e8e 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -238,7 +238,7 @@ impl LspAdapter for RustLspAdapter { ) .await?; make_file_executable(&server_path).await?; - remove_matching(&container_dir, |path| server_path == path).await; + remove_matching(&container_dir, |path| server_path != path).await; GithubBinaryMetadata::write_to_file( &GithubBinaryMetadata { metadata_version: 1, From d5ed569fad878c838753c2ad11f868afc0eaa893 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Mon, 11 Aug 2025 15:33:16 +0300 Subject: [PATCH 247/693] zeta: Reduce request payload (#35968) 1. Don't send diagnostics if there are more than 10 of them. This fixes an issue with sending 100kb requests for projects with many warnings. 2. Don't send speculated_output and outline, as those are currently unused. Release Notes: - Improved edit prediction latency --- crates/zeta/src/input_excerpt.rs | 9 ------- crates/zeta/src/zeta.rs | 41 +++++--------------------------- 2 files changed, 6 insertions(+), 44 deletions(-) diff --git a/crates/zeta/src/input_excerpt.rs b/crates/zeta/src/input_excerpt.rs index 5949e713e9..8ca6d39407 100644 --- a/crates/zeta/src/input_excerpt.rs +++ b/crates/zeta/src/input_excerpt.rs @@ -9,7 +9,6 @@ use std::{fmt::Write, ops::Range}; pub struct InputExcerpt { pub editable_range: Range, pub prompt: String, - pub speculated_output: String, } pub fn excerpt_for_cursor_position( @@ -46,7 +45,6 @@ pub fn excerpt_for_cursor_position( let context_range = expand_range(snapshot, editable_range.clone(), context_token_limit); let mut prompt = String::new(); - let mut speculated_output = String::new(); writeln!(&mut prompt, "```{path}").unwrap(); if context_range.start == Point::zero() { @@ -58,12 +56,6 @@ pub fn excerpt_for_cursor_position( } push_editable_range(position, snapshot, editable_range.clone(), &mut prompt); - push_editable_range( - position, - snapshot, - editable_range.clone(), - &mut speculated_output, - ); for chunk in snapshot.chunks(editable_range.end..context_range.end, false) { prompt.push_str(chunk.text); @@ -73,7 +65,6 @@ pub fn excerpt_for_cursor_position( InputExcerpt { editable_range, prompt, - speculated_output, } } diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 1ddbd25cb8..6900082003 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -37,7 +37,6 @@ use release_channel::AppVersion; use settings::WorktreeId; use std::str::FromStr; use std::{ - borrow::Cow, cmp, fmt::Write, future::Future, @@ -66,6 +65,7 @@ const ZED_PREDICT_DATA_COLLECTION_CHOICE: &str = "zed_predict_data_collection_ch const MAX_CONTEXT_TOKENS: usize = 150; const MAX_REWRITE_TOKENS: usize = 350; const MAX_EVENT_TOKENS: usize = 500; +const MAX_DIAGNOSTIC_GROUPS: usize = 10; /// Maximum number of events to track. const MAX_EVENT_COUNT: usize = 16; @@ -1175,7 +1175,9 @@ pub fn gather_context( cx.background_spawn({ let snapshot = snapshot.clone(); async move { - let diagnostic_groups = if diagnostic_groups.is_empty() { + let diagnostic_groups = if diagnostic_groups.is_empty() + || diagnostic_groups.len() >= MAX_DIAGNOSTIC_GROUPS + { None } else { Some(diagnostic_groups) @@ -1189,21 +1191,16 @@ pub fn gather_context( MAX_CONTEXT_TOKENS, ); let input_events = make_events_prompt(); - let input_outline = if can_collect_data { - prompt_for_outline(&snapshot) - } else { - String::new() - }; let editable_range = input_excerpt.editable_range.to_offset(&snapshot); let body = PredictEditsBody { input_events, input_excerpt: input_excerpt.prompt, - speculated_output: Some(input_excerpt.speculated_output), - outline: Some(input_outline), can_collect_data, diagnostic_groups, git_info, + outline: None, + speculated_output: None, }; Ok(GatherContextOutput { @@ -1214,32 +1211,6 @@ pub fn gather_context( }) } -fn prompt_for_outline(snapshot: &BufferSnapshot) -> String { - let mut input_outline = String::new(); - - writeln!( - input_outline, - "```{}", - snapshot - .file() - .map_or(Cow::Borrowed("untitled"), |file| file - .path() - .to_string_lossy()) - ) - .unwrap(); - - if let Some(outline) = snapshot.outline(None) { - for item in &outline.items { - let spacing = " ".repeat(item.depth); - writeln!(input_outline, "{}{}", spacing, item.text).unwrap(); - } - } - - writeln!(input_outline, "```").unwrap(); - - input_outline -} - fn prompt_for_events(events: &VecDeque, mut remaining_tokens: usize) -> String { let mut result = String::new(); for event in events.iter().rev() { From ebcce8730dee6f611f729c922d1a8f5793d68257 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 11 Aug 2025 15:10:46 +0200 Subject: [PATCH 248/693] Port some more tools to `agent2` (#35973) Release Notes: - N/A --- Cargo.lock | 2 + crates/agent2/Cargo.toml | 2 + crates/agent2/src/agent.rs | 8 +- crates/agent2/src/tools.rs | 12 + crates/agent2/src/tools/copy_path_tool.rs | 118 ++++ .../agent2/src/tools/create_directory_tool.rs | 89 +++ crates/agent2/src/tools/delete_path_tool.rs | 137 ++++ .../agent2/src/tools/list_directory_tool.rs | 664 ++++++++++++++++++ crates/agent2/src/tools/move_path_tool.rs | 123 ++++ crates/agent2/src/tools/open_tool.rs | 170 +++++ 10 files changed, 1324 insertions(+), 1 deletion(-) create mode 100644 crates/agent2/src/tools/copy_path_tool.rs create mode 100644 crates/agent2/src/tools/create_directory_tool.rs create mode 100644 crates/agent2/src/tools/delete_path_tool.rs create mode 100644 crates/agent2/src/tools/list_directory_tool.rs create mode 100644 crates/agent2/src/tools/move_path_tool.rs create mode 100644 crates/agent2/src/tools/open_tool.rs diff --git a/Cargo.lock b/Cargo.lock index 634bacd0f3..f0d21381fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -210,6 +210,7 @@ dependencies = [ "language_models", "log", "lsp", + "open", "paths", "portable-pty", "pretty_assertions", @@ -223,6 +224,7 @@ dependencies = [ "settings", "smol", "task", + "tempfile", "terminal", "theme", "ui", diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 65452f60fc..a288ff30b2 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -32,6 +32,7 @@ language.workspace = true language_model.workspace = true language_models.workspace = true log.workspace = true +open.workspace = true paths.workspace = true portable-pty.workspace = true project.workspace = true @@ -67,6 +68,7 @@ pretty_assertions.workspace = true project = { workspace = true, "features" = ["test-support"] } reqwest_client.workspace = true settings = { workspace = true, "features" = ["test-support"] } +tempfile.workspace = true terminal = { workspace = true, "features" = ["test-support"] } theme = { workspace = true, "features" = ["test-support"] } worktree = { workspace = true, "features" = ["test-support"] } diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index edb79003b4..398ea6ad50 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,6 +1,7 @@ use crate::{AgentResponseEvent, Thread, templates::Templates}; use crate::{ - EditFileTool, FindPathTool, ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, + CopyPathTool, CreateDirectoryTool, EditFileTool, FindPathTool, ListDirectoryTool, MovePathTool, + OpenTool, ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, }; use acp_thread::ModelSelector; use agent_client_protocol as acp; @@ -416,6 +417,11 @@ impl acp_thread::AgentConnection for NativeAgentConnection { let thread = cx.new(|cx| { let mut thread = Thread::new(project.clone(), agent.project_context.clone(), action_log.clone(), agent.templates.clone(), default_model); + thread.add_tool(CreateDirectoryTool::new(project.clone())); + thread.add_tool(CopyPathTool::new(project.clone())); + thread.add_tool(MovePathTool::new(project.clone())); + thread.add_tool(ListDirectoryTool::new(project.clone())); + thread.add_tool(OpenTool::new(project.clone())); thread.add_tool(ThinkingTool); thread.add_tool(FindPathTool::new(project.clone())); thread.add_tool(ReadFileTool::new(project.clone(), action_log)); diff --git a/crates/agent2/src/tools.rs b/crates/agent2/src/tools.rs index df4a7a9580..5c3920fcbb 100644 --- a/crates/agent2/src/tools.rs +++ b/crates/agent2/src/tools.rs @@ -1,11 +1,23 @@ +mod copy_path_tool; +mod create_directory_tool; +mod delete_path_tool; mod edit_file_tool; mod find_path_tool; +mod list_directory_tool; +mod move_path_tool; +mod open_tool; mod read_file_tool; mod terminal_tool; mod thinking_tool; +pub use copy_path_tool::*; +pub use create_directory_tool::*; +pub use delete_path_tool::*; pub use edit_file_tool::*; pub use find_path_tool::*; +pub use list_directory_tool::*; +pub use move_path_tool::*; +pub use open_tool::*; pub use read_file_tool::*; pub use terminal_tool::*; pub use thinking_tool::*; diff --git a/crates/agent2/src/tools/copy_path_tool.rs b/crates/agent2/src/tools/copy_path_tool.rs new file mode 100644 index 0000000000..f973b86990 --- /dev/null +++ b/crates/agent2/src/tools/copy_path_tool.rs @@ -0,0 +1,118 @@ +use crate::{AgentTool, ToolCallEventStream}; +use agent_client_protocol::ToolKind; +use anyhow::{Context as _, Result, anyhow}; +use gpui::{App, AppContext, Entity, SharedString, Task}; +use project::Project; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use util::markdown::MarkdownInlineCode; + +/// Copies a file or directory in the project, and returns confirmation that the +/// copy succeeded. +/// +/// Directory contents will be copied recursively (like `cp -r`). +/// +/// This tool should be used when it's desirable to create a copy of a file or +/// directory without modifying the original. It's much more efficient than +/// doing this by separately reading and then writing the file or directory's +/// contents, so this tool should be preferred over that approach whenever +/// copying is the goal. +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct CopyPathToolInput { + /// The source path of the file or directory to copy. + /// If a directory is specified, its contents will be copied recursively (like `cp -r`). + /// + /// + /// If the project has the following files: + /// + /// - directory1/a/something.txt + /// - directory2/a/things.txt + /// - directory3/a/other.txt + /// + /// You can copy the first file by providing a source_path of "directory1/a/something.txt" + /// + pub source_path: String, + + /// The destination path where the file or directory should be copied to. + /// + /// + /// To copy "directory1/a/something.txt" to "directory2/b/copy.txt", + /// provide a destination_path of "directory2/b/copy.txt" + /// + pub destination_path: String, +} + +pub struct CopyPathTool { + project: Entity, +} + +impl CopyPathTool { + pub fn new(project: Entity) -> Self { + Self { project } + } +} + +impl AgentTool for CopyPathTool { + type Input = CopyPathToolInput; + type Output = String; + + fn name(&self) -> SharedString { + "copy_path".into() + } + + fn kind(&self) -> ToolKind { + ToolKind::Move + } + + fn initial_title(&self, input: Result) -> ui::SharedString { + if let Ok(input) = input { + let src = MarkdownInlineCode(&input.source_path); + let dest = MarkdownInlineCode(&input.destination_path); + format!("Copy {src} to {dest}").into() + } else { + "Copy path".into() + } + } + + fn run( + self: Arc, + input: Self::Input, + _event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> { + let copy_task = self.project.update(cx, |project, cx| { + match project + .find_project_path(&input.source_path, cx) + .and_then(|project_path| project.entry_for_path(&project_path, cx)) + { + Some(entity) => match project.find_project_path(&input.destination_path, cx) { + Some(project_path) => { + project.copy_entry(entity.id, None, project_path.path, cx) + } + None => Task::ready(Err(anyhow!( + "Destination path {} was outside the project.", + input.destination_path + ))), + }, + None => Task::ready(Err(anyhow!( + "Source path {} was not found in the project.", + input.source_path + ))), + } + }); + + cx.background_spawn(async move { + let _ = copy_task.await.with_context(|| { + format!( + "Copying {} to {}", + input.source_path, input.destination_path + ) + })?; + Ok(format!( + "Copied {} to {}", + input.source_path, input.destination_path + )) + }) + } +} diff --git a/crates/agent2/src/tools/create_directory_tool.rs b/crates/agent2/src/tools/create_directory_tool.rs new file mode 100644 index 0000000000..c173c5ae67 --- /dev/null +++ b/crates/agent2/src/tools/create_directory_tool.rs @@ -0,0 +1,89 @@ +use agent_client_protocol::ToolKind; +use anyhow::{Context as _, Result, anyhow}; +use gpui::{App, Entity, SharedString, Task}; +use project::Project; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use util::markdown::MarkdownInlineCode; + +use crate::{AgentTool, ToolCallEventStream}; + +/// Creates a new directory at the specified path within the project. Returns +/// confirmation that the directory was created. +/// +/// This tool creates a directory and all necessary parent directories (similar +/// to `mkdir -p`). It should be used whenever you need to create new +/// directories within the project. +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct CreateDirectoryToolInput { + /// The path of the new directory. + /// + /// + /// If the project has the following structure: + /// + /// - directory1/ + /// - directory2/ + /// + /// You can create a new directory by providing a path of "directory1/new_directory" + /// + pub path: String, +} + +pub struct CreateDirectoryTool { + project: Entity, +} + +impl CreateDirectoryTool { + pub fn new(project: Entity) -> Self { + Self { project } + } +} + +impl AgentTool for CreateDirectoryTool { + type Input = CreateDirectoryToolInput; + type Output = String; + + fn name(&self) -> SharedString { + "create_directory".into() + } + + fn kind(&self) -> ToolKind { + ToolKind::Read + } + + fn initial_title(&self, input: Result) -> SharedString { + if let Ok(input) = input { + format!("Create directory {}", MarkdownInlineCode(&input.path)).into() + } else { + "Create directory".into() + } + } + + fn run( + self: Arc, + input: Self::Input, + _event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> { + let project_path = match self.project.read(cx).find_project_path(&input.path, cx) { + Some(project_path) => project_path, + None => { + return Task::ready(Err(anyhow!("Path to create was outside the project"))); + } + }; + let destination_path: Arc = input.path.as_str().into(); + + let create_entry = self.project.update(cx, |project, cx| { + project.create_entry(project_path.clone(), true, cx) + }); + + cx.spawn(async move |_cx| { + create_entry + .await + .with_context(|| format!("Creating directory {destination_path}"))?; + + Ok(format!("Created directory {destination_path}")) + }) + } +} diff --git a/crates/agent2/src/tools/delete_path_tool.rs b/crates/agent2/src/tools/delete_path_tool.rs new file mode 100644 index 0000000000..e013b3a3e7 --- /dev/null +++ b/crates/agent2/src/tools/delete_path_tool.rs @@ -0,0 +1,137 @@ +use crate::{AgentTool, ToolCallEventStream}; +use action_log::ActionLog; +use agent_client_protocol::ToolKind; +use anyhow::{Context as _, Result, anyhow}; +use futures::{SinkExt, StreamExt, channel::mpsc}; +use gpui::{App, AppContext, Entity, SharedString, Task}; +use project::{Project, ProjectPath}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +/// Deletes the file or directory (and the directory's contents, recursively) at +/// the specified path in the project, and returns confirmation of the deletion. +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct DeletePathToolInput { + /// The path of the file or directory to delete. + /// + /// + /// If the project has the following files: + /// + /// - directory1/a/something.txt + /// - directory2/a/things.txt + /// - directory3/a/other.txt + /// + /// You can delete the first file by providing a path of "directory1/a/something.txt" + /// + pub path: String, +} + +pub struct DeletePathTool { + project: Entity, + action_log: Entity, +} + +impl DeletePathTool { + pub fn new(project: Entity, action_log: Entity) -> Self { + Self { + project, + action_log, + } + } +} + +impl AgentTool for DeletePathTool { + type Input = DeletePathToolInput; + type Output = String; + + fn name(&self) -> SharedString { + "delete_path".into() + } + + fn kind(&self) -> ToolKind { + ToolKind::Delete + } + + fn initial_title(&self, input: Result) -> SharedString { + if let Ok(input) = input { + format!("Delete “`{}`”", input.path).into() + } else { + "Delete path".into() + } + } + + fn run( + self: Arc, + input: Self::Input, + _event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> { + let path = input.path; + let Some(project_path) = self.project.read(cx).find_project_path(&path, cx) else { + return Task::ready(Err(anyhow!( + "Couldn't delete {path} because that path isn't in this project." + ))); + }; + + let Some(worktree) = self + .project + .read(cx) + .worktree_for_id(project_path.worktree_id, cx) + else { + return Task::ready(Err(anyhow!( + "Couldn't delete {path} because that path isn't in this project." + ))); + }; + + let worktree_snapshot = worktree.read(cx).snapshot(); + let (mut paths_tx, mut paths_rx) = mpsc::channel(256); + cx.background_spawn({ + let project_path = project_path.clone(); + async move { + for entry in + worktree_snapshot.traverse_from_path(true, false, false, &project_path.path) + { + if !entry.path.starts_with(&project_path.path) { + break; + } + paths_tx + .send(ProjectPath { + worktree_id: project_path.worktree_id, + path: entry.path.clone(), + }) + .await?; + } + anyhow::Ok(()) + } + }) + .detach(); + + let project = self.project.clone(); + let action_log = self.action_log.clone(); + cx.spawn(async move |cx| { + while let Some(path) = paths_rx.next().await { + if let Ok(buffer) = project + .update(cx, |project, cx| project.open_buffer(path, cx))? + .await + { + action_log.update(cx, |action_log, cx| { + action_log.will_delete_buffer(buffer.clone(), cx) + })?; + } + } + + let deletion_task = project + .update(cx, |project, cx| { + project.delete_file(project_path, false, cx) + })? + .with_context(|| { + format!("Couldn't delete {path} because that path isn't in this project.") + })?; + deletion_task + .await + .with_context(|| format!("Deleting {path}"))?; + Ok(format!("Deleted {path}")) + }) + } +} diff --git a/crates/agent2/src/tools/list_directory_tool.rs b/crates/agent2/src/tools/list_directory_tool.rs new file mode 100644 index 0000000000..61f21d8f95 --- /dev/null +++ b/crates/agent2/src/tools/list_directory_tool.rs @@ -0,0 +1,664 @@ +use crate::{AgentTool, ToolCallEventStream}; +use agent_client_protocol::ToolKind; +use anyhow::{Result, anyhow}; +use gpui::{App, Entity, SharedString, Task}; +use project::{Project, WorktreeSettings}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Settings; +use std::fmt::Write; +use std::{path::Path, sync::Arc}; +use util::markdown::MarkdownInlineCode; + +/// Lists files and directories in a given path. Prefer the `grep` or +/// `find_path` tools when searching the codebase. +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct ListDirectoryToolInput { + /// The fully-qualified path of the directory to list in the project. + /// + /// This path should never be absolute, and the first component + /// of the path should always be a root directory in a project. + /// + /// + /// If the project has the following root directories: + /// + /// - directory1 + /// - directory2 + /// + /// You can list the contents of `directory1` by using the path `directory1`. + /// + /// + /// + /// If the project has the following root directories: + /// + /// - foo + /// - bar + /// + /// If you wanna list contents in the directory `foo/baz`, you should use the path `foo/baz`. + /// + pub path: String, +} + +pub struct ListDirectoryTool { + project: Entity, +} + +impl ListDirectoryTool { + pub fn new(project: Entity) -> Self { + Self { project } + } +} + +impl AgentTool for ListDirectoryTool { + type Input = ListDirectoryToolInput; + type Output = String; + + fn name(&self) -> SharedString { + "list_directory".into() + } + + fn kind(&self) -> ToolKind { + ToolKind::Read + } + + fn initial_title(&self, input: Result) -> SharedString { + if let Ok(input) = input { + let path = MarkdownInlineCode(&input.path); + format!("List the {path} directory's contents").into() + } else { + "List directory".into() + } + } + + fn run( + self: Arc, + input: Self::Input, + _event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> { + // Sometimes models will return these even though we tell it to give a path and not a glob. + // When this happens, just list the root worktree directories. + if matches!(input.path.as_str(), "." | "" | "./" | "*") { + let output = self + .project + .read(cx) + .worktrees(cx) + .filter_map(|worktree| { + worktree.read(cx).root_entry().and_then(|entry| { + if entry.is_dir() { + entry.path.to_str() + } else { + None + } + }) + }) + .collect::>() + .join("\n"); + + return Task::ready(Ok(output)); + } + + let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) else { + return Task::ready(Err(anyhow!("Path {} not found in project", input.path))); + }; + let Some(worktree) = self + .project + .read(cx) + .worktree_for_id(project_path.worktree_id, cx) + else { + return Task::ready(Err(anyhow!("Worktree not found"))); + }; + + // Check if the directory whose contents we're listing is itself excluded or private + let global_settings = WorktreeSettings::get_global(cx); + if global_settings.is_path_excluded(&project_path.path) { + return Task::ready(Err(anyhow!( + "Cannot list directory because its path matches the user's global `file_scan_exclusions` setting: {}", + &input.path + ))); + } + + if global_settings.is_path_private(&project_path.path) { + return Task::ready(Err(anyhow!( + "Cannot list directory because its path matches the user's global `private_files` setting: {}", + &input.path + ))); + } + + let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx); + if worktree_settings.is_path_excluded(&project_path.path) { + return Task::ready(Err(anyhow!( + "Cannot list directory because its path matches the user's worktree`file_scan_exclusions` setting: {}", + &input.path + ))); + } + + if worktree_settings.is_path_private(&project_path.path) { + return Task::ready(Err(anyhow!( + "Cannot list directory because its path matches the user's worktree `private_paths` setting: {}", + &input.path + ))); + } + + let worktree_snapshot = worktree.read(cx).snapshot(); + let worktree_root_name = worktree.read(cx).root_name().to_string(); + + let Some(entry) = worktree_snapshot.entry_for_path(&project_path.path) else { + return Task::ready(Err(anyhow!("Path not found: {}", input.path))); + }; + + if !entry.is_dir() { + return Task::ready(Err(anyhow!("{} is not a directory.", input.path))); + } + let worktree_snapshot = worktree.read(cx).snapshot(); + + let mut folders = Vec::new(); + let mut files = Vec::new(); + + for entry in worktree_snapshot.child_entries(&project_path.path) { + // Skip private and excluded files and directories + if global_settings.is_path_private(&entry.path) + || global_settings.is_path_excluded(&entry.path) + { + continue; + } + + if self + .project + .read(cx) + .find_project_path(&entry.path, cx) + .map(|project_path| { + let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx); + + worktree_settings.is_path_excluded(&project_path.path) + || worktree_settings.is_path_private(&project_path.path) + }) + .unwrap_or(false) + { + continue; + } + + let full_path = Path::new(&worktree_root_name) + .join(&entry.path) + .display() + .to_string(); + if entry.is_dir() { + folders.push(full_path); + } else { + files.push(full_path); + } + } + + let mut output = String::new(); + + if !folders.is_empty() { + writeln!(output, "# Folders:\n{}", folders.join("\n")).unwrap(); + } + + if !files.is_empty() { + writeln!(output, "\n# Files:\n{}", files.join("\n")).unwrap(); + } + + if output.is_empty() { + writeln!(output, "{} is empty.", input.path).unwrap(); + } + + Task::ready(Ok(output)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::{TestAppContext, UpdateGlobal}; + use indoc::indoc; + use project::{FakeFs, Project, WorktreeSettings}; + use serde_json::json; + use settings::SettingsStore; + use util::path; + + fn platform_paths(path_str: &str) -> String { + if cfg!(target_os = "windows") { + path_str.replace("/", "\\") + } else { + path_str.to_string() + } + } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + Project::init_settings(cx); + }); + } + + #[gpui::test] + async fn test_list_directory_separates_files_and_dirs(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/project"), + json!({ + "src": { + "main.rs": "fn main() {}", + "lib.rs": "pub fn hello() {}", + "models": { + "user.rs": "struct User {}", + "post.rs": "struct Post {}" + }, + "utils": { + "helper.rs": "pub fn help() {}" + } + }, + "tests": { + "integration_test.rs": "#[test] fn test() {}" + }, + "README.md": "# Project", + "Cargo.toml": "[package]" + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let tool = Arc::new(ListDirectoryTool::new(project)); + + // Test listing root directory + let input = ListDirectoryToolInput { + path: "project".into(), + }; + let output = cx + .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) + .await + .unwrap(); + assert_eq!( + output, + platform_paths(indoc! {" + # Folders: + project/src + project/tests + + # Files: + project/Cargo.toml + project/README.md + "}) + ); + + // Test listing src directory + let input = ListDirectoryToolInput { + path: "project/src".into(), + }; + let output = cx + .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) + .await + .unwrap(); + assert_eq!( + output, + platform_paths(indoc! {" + # Folders: + project/src/models + project/src/utils + + # Files: + project/src/lib.rs + project/src/main.rs + "}) + ); + + // Test listing directory with only files + let input = ListDirectoryToolInput { + path: "project/tests".into(), + }; + let output = cx + .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) + .await + .unwrap(); + assert!(!output.contains("# Folders:")); + assert!(output.contains("# Files:")); + assert!(output.contains(&platform_paths("project/tests/integration_test.rs"))); + } + + #[gpui::test] + async fn test_list_directory_empty_directory(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/project"), + json!({ + "empty_dir": {} + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let tool = Arc::new(ListDirectoryTool::new(project)); + + let input = ListDirectoryToolInput { + path: "project/empty_dir".into(), + }; + let output = cx + .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) + .await + .unwrap(); + assert_eq!(output, "project/empty_dir is empty.\n"); + } + + #[gpui::test] + async fn test_list_directory_error_cases(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/project"), + json!({ + "file.txt": "content" + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let tool = Arc::new(ListDirectoryTool::new(project)); + + // Test non-existent path + let input = ListDirectoryToolInput { + path: "project/nonexistent".into(), + }; + let output = cx + .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) + .await; + assert!(output.unwrap_err().to_string().contains("Path not found")); + + // Test trying to list a file instead of directory + let input = ListDirectoryToolInput { + path: "project/file.txt".into(), + }; + let output = cx + .update(|cx| tool.run(input, ToolCallEventStream::test().0, cx)) + .await; + assert!( + output + .unwrap_err() + .to_string() + .contains("is not a directory") + ); + } + + #[gpui::test] + async fn test_list_directory_security(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/project"), + json!({ + "normal_dir": { + "file1.txt": "content", + "file2.txt": "content" + }, + ".mysecrets": "SECRET_KEY=abc123", + ".secretdir": { + "config": "special configuration", + "secret.txt": "secret content" + }, + ".mymetadata": "custom metadata", + "visible_dir": { + "normal.txt": "normal content", + "special.privatekey": "private key content", + "data.mysensitive": "sensitive data", + ".hidden_subdir": { + "hidden_file.txt": "hidden content" + } + } + }), + ) + .await; + + // Configure settings explicitly + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings::(cx, |settings| { + settings.file_scan_exclusions = Some(vec![ + "**/.secretdir".to_string(), + "**/.mymetadata".to_string(), + "**/.hidden_subdir".to_string(), + ]); + settings.private_files = Some(vec![ + "**/.mysecrets".to_string(), + "**/*.privatekey".to_string(), + "**/*.mysensitive".to_string(), + ]); + }); + }); + }); + + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let tool = Arc::new(ListDirectoryTool::new(project)); + + // Listing root directory should exclude private and excluded files + let input = ListDirectoryToolInput { + path: "project".into(), + }; + let output = cx + .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) + .await + .unwrap(); + + // Should include normal directories + assert!(output.contains("normal_dir"), "Should list normal_dir"); + assert!(output.contains("visible_dir"), "Should list visible_dir"); + + // Should NOT include excluded or private files + assert!( + !output.contains(".secretdir"), + "Should not list .secretdir (file_scan_exclusions)" + ); + assert!( + !output.contains(".mymetadata"), + "Should not list .mymetadata (file_scan_exclusions)" + ); + assert!( + !output.contains(".mysecrets"), + "Should not list .mysecrets (private_files)" + ); + + // Trying to list an excluded directory should fail + let input = ListDirectoryToolInput { + path: "project/.secretdir".into(), + }; + let output = cx + .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) + .await; + assert!( + output + .unwrap_err() + .to_string() + .contains("file_scan_exclusions"), + "Error should mention file_scan_exclusions" + ); + + // Listing a directory should exclude private files within it + let input = ListDirectoryToolInput { + path: "project/visible_dir".into(), + }; + let output = cx + .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) + .await + .unwrap(); + + // Should include normal files + assert!(output.contains("normal.txt"), "Should list normal.txt"); + + // Should NOT include private files + assert!( + !output.contains("privatekey"), + "Should not list .privatekey files (private_files)" + ); + assert!( + !output.contains("mysensitive"), + "Should not list .mysensitive files (private_files)" + ); + + // Should NOT include subdirectories that match exclusions + assert!( + !output.contains(".hidden_subdir"), + "Should not list .hidden_subdir (file_scan_exclusions)" + ); + } + + #[gpui::test] + async fn test_list_directory_with_multiple_worktree_settings(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + + // Create first worktree with its own private files + fs.insert_tree( + path!("/worktree1"), + json!({ + ".zed": { + "settings.json": r#"{ + "file_scan_exclusions": ["**/fixture.*"], + "private_files": ["**/secret.rs", "**/config.toml"] + }"# + }, + "src": { + "main.rs": "fn main() { println!(\"Hello from worktree1\"); }", + "secret.rs": "const API_KEY: &str = \"secret_key_1\";", + "config.toml": "[database]\nurl = \"postgres://localhost/db1\"" + }, + "tests": { + "test.rs": "mod tests { fn test_it() {} }", + "fixture.sql": "CREATE TABLE users (id INT, name VARCHAR(255));" + } + }), + ) + .await; + + // Create second worktree with different private files + fs.insert_tree( + path!("/worktree2"), + json!({ + ".zed": { + "settings.json": r#"{ + "file_scan_exclusions": ["**/internal.*"], + "private_files": ["**/private.js", "**/data.json"] + }"# + }, + "lib": { + "public.js": "export function greet() { return 'Hello from worktree2'; }", + "private.js": "const SECRET_TOKEN = \"private_token_2\";", + "data.json": "{\"api_key\": \"json_secret_key\"}" + }, + "docs": { + "README.md": "# Public Documentation", + "internal.md": "# Internal Secrets and Configuration" + } + }), + ) + .await; + + // Set global settings + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings::(cx, |settings| { + settings.file_scan_exclusions = + Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); + settings.private_files = Some(vec!["**/.env".to_string()]); + }); + }); + }); + + let project = Project::test( + fs.clone(), + [path!("/worktree1").as_ref(), path!("/worktree2").as_ref()], + cx, + ) + .await; + + // Wait for worktrees to be fully scanned + cx.executor().run_until_parked(); + + let tool = Arc::new(ListDirectoryTool::new(project)); + + // Test listing worktree1/src - should exclude secret.rs and config.toml based on local settings + let input = ListDirectoryToolInput { + path: "worktree1/src".into(), + }; + let output = cx + .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) + .await + .unwrap(); + assert!(output.contains("main.rs"), "Should list main.rs"); + assert!( + !output.contains("secret.rs"), + "Should not list secret.rs (local private_files)" + ); + assert!( + !output.contains("config.toml"), + "Should not list config.toml (local private_files)" + ); + + // Test listing worktree1/tests - should exclude fixture.sql based on local settings + let input = ListDirectoryToolInput { + path: "worktree1/tests".into(), + }; + let output = cx + .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) + .await + .unwrap(); + assert!(output.contains("test.rs"), "Should list test.rs"); + assert!( + !output.contains("fixture.sql"), + "Should not list fixture.sql (local file_scan_exclusions)" + ); + + // Test listing worktree2/lib - should exclude private.js and data.json based on local settings + let input = ListDirectoryToolInput { + path: "worktree2/lib".into(), + }; + let output = cx + .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) + .await + .unwrap(); + assert!(output.contains("public.js"), "Should list public.js"); + assert!( + !output.contains("private.js"), + "Should not list private.js (local private_files)" + ); + assert!( + !output.contains("data.json"), + "Should not list data.json (local private_files)" + ); + + // Test listing worktree2/docs - should exclude internal.md based on local settings + let input = ListDirectoryToolInput { + path: "worktree2/docs".into(), + }; + let output = cx + .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) + .await + .unwrap(); + assert!(output.contains("README.md"), "Should list README.md"); + assert!( + !output.contains("internal.md"), + "Should not list internal.md (local file_scan_exclusions)" + ); + + // Test trying to list an excluded directory directly + let input = ListDirectoryToolInput { + path: "worktree1/src/secret.rs".into(), + }; + let output = cx + .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) + .await; + assert!( + output + .unwrap_err() + .to_string() + .contains("Cannot list directory"), + ); + } +} diff --git a/crates/agent2/src/tools/move_path_tool.rs b/crates/agent2/src/tools/move_path_tool.rs new file mode 100644 index 0000000000..f8d5d0d176 --- /dev/null +++ b/crates/agent2/src/tools/move_path_tool.rs @@ -0,0 +1,123 @@ +use crate::{AgentTool, ToolCallEventStream}; +use agent_client_protocol::ToolKind; +use anyhow::{Context as _, Result, anyhow}; +use gpui::{App, AppContext, Entity, SharedString, Task}; +use project::Project; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::{path::Path, sync::Arc}; +use util::markdown::MarkdownInlineCode; + +/// Moves or rename a file or directory in the project, and returns confirmation +/// that the move succeeded. +/// +/// If the source and destination directories are the same, but the filename is +/// different, this performs a rename. Otherwise, it performs a move. +/// +/// This tool should be used when it's desirable to move or rename a file or +/// directory without changing its contents at all. +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct MovePathToolInput { + /// The source path of the file or directory to move/rename. + /// + /// + /// If the project has the following files: + /// + /// - directory1/a/something.txt + /// - directory2/a/things.txt + /// - directory3/a/other.txt + /// + /// You can move the first file by providing a source_path of "directory1/a/something.txt" + /// + pub source_path: String, + + /// The destination path where the file or directory should be moved/renamed to. + /// If the paths are the same except for the filename, then this will be a rename. + /// + /// + /// To move "directory1/a/something.txt" to "directory2/b/renamed.txt", + /// provide a destination_path of "directory2/b/renamed.txt" + /// + pub destination_path: String, +} + +pub struct MovePathTool { + project: Entity, +} + +impl MovePathTool { + pub fn new(project: Entity) -> Self { + Self { project } + } +} + +impl AgentTool for MovePathTool { + type Input = MovePathToolInput; + type Output = String; + + fn name(&self) -> SharedString { + "move_path".into() + } + + fn kind(&self) -> ToolKind { + ToolKind::Move + } + + fn initial_title(&self, input: Result) -> SharedString { + if let Ok(input) = input { + let src = MarkdownInlineCode(&input.source_path); + let dest = MarkdownInlineCode(&input.destination_path); + let src_path = Path::new(&input.source_path); + let dest_path = Path::new(&input.destination_path); + + match dest_path + .file_name() + .and_then(|os_str| os_str.to_os_string().into_string().ok()) + { + Some(filename) if src_path.parent() == dest_path.parent() => { + let filename = MarkdownInlineCode(&filename); + format!("Rename {src} to {filename}").into() + } + _ => format!("Move {src} to {dest}").into(), + } + } else { + "Move path".into() + } + } + + fn run( + self: Arc, + input: Self::Input, + _event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> { + let rename_task = self.project.update(cx, |project, cx| { + match project + .find_project_path(&input.source_path, cx) + .and_then(|project_path| project.entry_for_path(&project_path, cx)) + { + Some(entity) => match project.find_project_path(&input.destination_path, cx) { + Some(project_path) => project.rename_entry(entity.id, project_path.path, cx), + None => Task::ready(Err(anyhow!( + "Destination path {} was outside the project.", + input.destination_path + ))), + }, + None => Task::ready(Err(anyhow!( + "Source path {} was not found in the project.", + input.source_path + ))), + } + }); + + cx.background_spawn(async move { + let _ = rename_task.await.with_context(|| { + format!("Moving {} to {}", input.source_path, input.destination_path) + })?; + Ok(format!( + "Moved {} to {}", + input.source_path, input.destination_path + )) + }) + } +} diff --git a/crates/agent2/src/tools/open_tool.rs b/crates/agent2/src/tools/open_tool.rs new file mode 100644 index 0000000000..0860b62a51 --- /dev/null +++ b/crates/agent2/src/tools/open_tool.rs @@ -0,0 +1,170 @@ +use crate::AgentTool; +use agent_client_protocol::ToolKind; +use anyhow::{Context as _, Result}; +use gpui::{App, AppContext, Entity, SharedString, Task}; +use project::Project; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::{path::PathBuf, sync::Arc}; +use util::markdown::MarkdownEscaped; + +/// This tool opens a file or URL with the default application associated with +/// it on the user's operating system: +/// +/// - On macOS, it's equivalent to the `open` command +/// - On Windows, it's equivalent to `start` +/// - On Linux, it uses something like `xdg-open`, `gio open`, `gnome-open`, `kde-open`, `wslview` as appropriate +/// +/// For example, it can open a web browser with a URL, open a PDF file with the +/// default PDF viewer, etc. +/// +/// You MUST ONLY use this tool when the user has explicitly requested opening +/// something. You MUST NEVER assume that the user would like for you to use +/// this tool. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct OpenToolInput { + /// The path or URL to open with the default application. + path_or_url: String, +} + +pub struct OpenTool { + project: Entity, +} + +impl OpenTool { + pub fn new(project: Entity) -> Self { + Self { project } + } +} + +impl AgentTool for OpenTool { + type Input = OpenToolInput; + type Output = String; + + fn name(&self) -> SharedString { + "open".into() + } + + fn kind(&self) -> ToolKind { + ToolKind::Execute + } + + fn initial_title(&self, input: Result) -> SharedString { + if let Ok(input) = input { + format!("Open `{}`", MarkdownEscaped(&input.path_or_url)).into() + } else { + "Open file or URL".into() + } + } + + fn run( + self: Arc, + input: Self::Input, + event_stream: crate::ToolCallEventStream, + cx: &mut App, + ) -> Task> { + // If path_or_url turns out to be a path in the project, make it absolute. + let abs_path = to_absolute_path(&input.path_or_url, self.project.clone(), cx); + let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())).to_string()); + cx.background_spawn(async move { + authorize.await?; + + match abs_path { + Some(path) => open::that(path), + None => open::that(&input.path_or_url), + } + .context("Failed to open URL or file path")?; + + Ok(format!("Successfully opened {}", input.path_or_url)) + }) + } +} + +fn to_absolute_path( + potential_path: &str, + project: Entity, + cx: &mut App, +) -> Option { + let project = project.read(cx); + project + .find_project_path(PathBuf::from(potential_path), cx) + .and_then(|project_path| project.absolute_path(&project_path, cx)) +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::TestAppContext; + use project::{FakeFs, Project}; + use settings::SettingsStore; + use std::path::Path; + use tempfile::TempDir; + + #[gpui::test] + async fn test_to_absolute_path(cx: &mut TestAppContext) { + init_test(cx); + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let temp_path = temp_dir.path().to_string_lossy().to_string(); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + &temp_path, + serde_json::json!({ + "src": { + "main.rs": "fn main() {}", + "lib.rs": "pub fn lib_fn() {}" + }, + "docs": { + "readme.md": "# Project Documentation" + } + }), + ) + .await; + + // Use the temp_path as the root directory, not just its filename + let project = Project::test(fs.clone(), [temp_dir.path()], cx).await; + + // Test cases where the function should return Some + cx.update(|cx| { + // Project-relative paths should return Some + // Create paths using the last segment of the temp path to simulate a project-relative path + let root_dir_name = Path::new(&temp_path) + .file_name() + .unwrap_or_else(|| std::ffi::OsStr::new("temp")) + .to_string_lossy(); + + assert!( + to_absolute_path(&format!("{root_dir_name}/src/main.rs"), project.clone(), cx) + .is_some(), + "Failed to resolve main.rs path" + ); + + assert!( + to_absolute_path( + &format!("{root_dir_name}/docs/readme.md",), + project.clone(), + cx, + ) + .is_some(), + "Failed to resolve readme.md path" + ); + + // External URL should return None + let result = to_absolute_path("https://example.com", project.clone(), cx); + assert_eq!(result, None, "External URLs should return None"); + + // Path outside project + let result = to_absolute_path("../invalid/path", project.clone(), cx); + assert_eq!(result, None, "Paths outside the project should return None"); + }); + } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + Project::init_settings(cx); + }); + } +} From 8dbded46d8b28c80d2948088afa19db3188a6b34 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Mon, 11 Aug 2025 15:34:34 +0200 Subject: [PATCH 249/693] agent2: Add now, grep, and web search tools (#35974) Release Notes: - N/A --------- Co-authored-by: Bennet Bo Fenner Co-authored-by: Antonio Scandurra --- Cargo.lock | 4 + crates/agent2/Cargo.toml | 4 + crates/agent2/src/agent.rs | 9 +- crates/agent2/src/tools.rs | 6 + crates/agent2/src/tools/grep_tool.rs | 1196 +++++++++++++++++ crates/agent2/src/tools/now_tool.rs | 66 + crates/agent2/src/tools/web_search_tool.rs | 105 ++ .../cloud_llm_client/src/cloud_llm_client.rs | 4 +- 8 files changed, 1390 insertions(+), 4 deletions(-) create mode 100644 crates/agent2/src/tools/grep_tool.rs create mode 100644 crates/agent2/src/tools/now_tool.rs create mode 100644 crates/agent2/src/tools/web_search_tool.rs diff --git a/Cargo.lock b/Cargo.lock index f0d21381fa..7b5e82a312 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,6 +191,7 @@ dependencies = [ "anyhow", "assistant_tool", "assistant_tools", + "chrono", "client", "clock", "cloud_llm_client", @@ -227,10 +228,13 @@ dependencies = [ "tempfile", "terminal", "theme", + "tree-sitter-rust", "ui", + "unindent", "util", "uuid", "watch", + "web_search", "which 6.0.3", "workspace-hack", "worktree", diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index a288ff30b2..622b08016a 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -20,6 +20,7 @@ agent_settings.workspace = true anyhow.workspace = true assistant_tool.workspace = true assistant_tools.workspace = true +chrono.workspace = true cloud_llm_client.workspace = true collections.workspace = true fs.workspace = true @@ -49,6 +50,7 @@ ui.workspace = true util.workspace = true uuid.workspace = true watch.workspace = true +web_search.workspace = true which.workspace = true workspace-hack.workspace = true @@ -71,5 +73,7 @@ settings = { workspace = true, "features" = ["test-support"] } tempfile.workspace = true terminal = { workspace = true, "features" = ["test-support"] } theme = { workspace = true, "features" = ["test-support"] } +tree-sitter-rust.workspace = true +unindent = { workspace = true } worktree = { workspace = true, "features" = ["test-support"] } zlog.workspace = true diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 398ea6ad50..b1cefd2864 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,7 +1,8 @@ use crate::{AgentResponseEvent, Thread, templates::Templates}; use crate::{ - CopyPathTool, CreateDirectoryTool, EditFileTool, FindPathTool, ListDirectoryTool, MovePathTool, - OpenTool, ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, + CopyPathTool, CreateDirectoryTool, EditFileTool, FindPathTool, GrepTool, ListDirectoryTool, + MovePathTool, NowTool, OpenTool, ReadFileTool, TerminalTool, ThinkingTool, + ToolCallAuthorization, WebSearchTool, }; use acp_thread::ModelSelector; use agent_client_protocol as acp; @@ -424,9 +425,13 @@ impl acp_thread::AgentConnection for NativeAgentConnection { thread.add_tool(OpenTool::new(project.clone())); thread.add_tool(ThinkingTool); thread.add_tool(FindPathTool::new(project.clone())); + thread.add_tool(GrepTool::new(project.clone())); thread.add_tool(ReadFileTool::new(project.clone(), action_log)); thread.add_tool(EditFileTool::new(cx.entity())); + thread.add_tool(NowTool); thread.add_tool(TerminalTool::new(project.clone(), cx)); + // TODO: Needs to be conditional based on zed model or not + thread.add_tool(WebSearchTool); thread }); diff --git a/crates/agent2/src/tools.rs b/crates/agent2/src/tools.rs index 5c3920fcbb..29ba6780b8 100644 --- a/crates/agent2/src/tools.rs +++ b/crates/agent2/src/tools.rs @@ -3,21 +3,27 @@ mod create_directory_tool; mod delete_path_tool; mod edit_file_tool; mod find_path_tool; +mod grep_tool; mod list_directory_tool; mod move_path_tool; +mod now_tool; mod open_tool; mod read_file_tool; mod terminal_tool; mod thinking_tool; +mod web_search_tool; pub use copy_path_tool::*; pub use create_directory_tool::*; pub use delete_path_tool::*; pub use edit_file_tool::*; pub use find_path_tool::*; +pub use grep_tool::*; pub use list_directory_tool::*; pub use move_path_tool::*; +pub use now_tool::*; pub use open_tool::*; pub use read_file_tool::*; pub use terminal_tool::*; pub use thinking_tool::*; +pub use web_search_tool::*; diff --git a/crates/agent2/src/tools/grep_tool.rs b/crates/agent2/src/tools/grep_tool.rs new file mode 100644 index 0000000000..3266cb5734 --- /dev/null +++ b/crates/agent2/src/tools/grep_tool.rs @@ -0,0 +1,1196 @@ +use crate::{AgentTool, ToolCallEventStream}; +use agent_client_protocol as acp; +use anyhow::{Result, anyhow}; +use futures::StreamExt; +use gpui::{App, Entity, SharedString, Task}; +use language::{OffsetRangeExt, ParseStatus, Point}; +use project::{ + Project, WorktreeSettings, + search::{SearchQuery, SearchResult}, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Settings; +use std::{cmp, fmt::Write, sync::Arc}; +use util::RangeExt; +use util::markdown::MarkdownInlineCode; +use util::paths::PathMatcher; + +/// Searches the contents of files in the project with a regular expression +/// +/// - Prefer this tool to path search when searching for symbols in the project, because you won't need to guess what path it's in. +/// - Supports full regex syntax (eg. "log.*Error", "function\\s+\\w+", etc.) +/// - Pass an `include_pattern` if you know how to narrow your search on the files system +/// - Never use this tool to search for paths. Only search file contents with this tool. +/// - Use this tool when you need to find files containing specific patterns +/// - Results are paginated with 20 matches per page. Use the optional 'offset' parameter to request subsequent pages. +/// - DO NOT use HTML entities solely to escape characters in the tool parameters. +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct GrepToolInput { + /// A regex pattern to search for in the entire project. Note that the regex + /// will be parsed by the Rust `regex` crate. + /// + /// Do NOT specify a path here! This will only be matched against the code **content**. + pub regex: String, + /// A glob pattern for the paths of files to include in the search. + /// Supports standard glob patterns like "**/*.rs" or "src/**/*.ts". + /// If omitted, all files in the project will be searched. + pub include_pattern: Option, + /// Optional starting position for paginated results (0-based). + /// When not provided, starts from the beginning. + #[serde(default)] + pub offset: u32, + /// Whether the regex is case-sensitive. Defaults to false (case-insensitive). + #[serde(default)] + pub case_sensitive: bool, +} + +impl GrepToolInput { + /// Which page of search results this is. + pub fn page(&self) -> u32 { + 1 + (self.offset / RESULTS_PER_PAGE) + } +} + +const RESULTS_PER_PAGE: u32 = 20; + +pub struct GrepTool { + project: Entity, +} + +impl GrepTool { + pub fn new(project: Entity) -> Self { + Self { project } + } +} + +impl AgentTool for GrepTool { + type Input = GrepToolInput; + type Output = String; + + fn name(&self) -> SharedString { + "grep".into() + } + + fn kind(&self) -> acp::ToolKind { + acp::ToolKind::Search + } + + fn initial_title(&self, input: Result) -> SharedString { + match input { + Ok(input) => { + let page = input.page(); + let regex_str = MarkdownInlineCode(&input.regex); + let case_info = if input.case_sensitive { + " (case-sensitive)" + } else { + "" + }; + + if page > 1 { + format!("Get page {page} of search results for regex {regex_str}{case_info}") + } else { + format!("Search files for regex {regex_str}{case_info}") + } + } + Err(_) => "Search with regex".into(), + } + .into() + } + + fn run( + self: Arc, + input: Self::Input, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> { + const CONTEXT_LINES: u32 = 2; + const MAX_ANCESTOR_LINES: u32 = 10; + + let include_matcher = match PathMatcher::new( + input + .include_pattern + .as_ref() + .into_iter() + .collect::>(), + ) { + Ok(matcher) => matcher, + Err(error) => { + return Task::ready(Err(anyhow!("invalid include glob pattern: {error}"))); + } + }; + + // Exclude global file_scan_exclusions and private_files settings + let exclude_matcher = { + let global_settings = WorktreeSettings::get_global(cx); + let exclude_patterns = global_settings + .file_scan_exclusions + .sources() + .iter() + .chain(global_settings.private_files.sources().iter()); + + match PathMatcher::new(exclude_patterns) { + Ok(matcher) => matcher, + Err(error) => { + return Task::ready(Err(anyhow!("invalid exclude pattern: {error}"))); + } + } + }; + + let query = match SearchQuery::regex( + &input.regex, + false, + input.case_sensitive, + false, + false, + include_matcher, + exclude_matcher, + true, // Always match file include pattern against *full project paths* that start with a project root. + None, + ) { + Ok(query) => query, + Err(error) => return Task::ready(Err(error)), + }; + + let results = self + .project + .update(cx, |project, cx| project.search(query, cx)); + + let project = self.project.downgrade(); + cx.spawn(async move |cx| { + futures::pin_mut!(results); + + let mut output = String::new(); + let mut skips_remaining = input.offset; + let mut matches_found = 0; + let mut has_more_matches = false; + + 'outer: while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await { + if ranges.is_empty() { + continue; + } + + let Ok((Some(path), mut parse_status)) = buffer.read_with(cx, |buffer, cx| { + (buffer.file().map(|file| file.full_path(cx)), buffer.parse_status()) + }) else { + continue; + }; + + // Check if this file should be excluded based on its worktree settings + if let Ok(Some(project_path)) = project.read_with(cx, |project, cx| { + project.find_project_path(&path, cx) + }) { + if cx.update(|cx| { + let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx); + worktree_settings.is_path_excluded(&project_path.path) + || worktree_settings.is_path_private(&project_path.path) + }).unwrap_or(false) { + continue; + } + } + + while *parse_status.borrow() != ParseStatus::Idle { + parse_status.changed().await?; + } + + let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; + + let mut ranges = ranges + .into_iter() + .map(|range| { + let matched = range.to_point(&snapshot); + let matched_end_line_len = snapshot.line_len(matched.end.row); + let full_lines = Point::new(matched.start.row, 0)..Point::new(matched.end.row, matched_end_line_len); + let symbols = snapshot.symbols_containing(matched.start, None); + + if let Some(ancestor_node) = snapshot.syntax_ancestor(full_lines.clone()) { + let full_ancestor_range = ancestor_node.byte_range().to_point(&snapshot); + let end_row = full_ancestor_range.end.row.min(full_ancestor_range.start.row + MAX_ANCESTOR_LINES); + let end_col = snapshot.line_len(end_row); + let capped_ancestor_range = Point::new(full_ancestor_range.start.row, 0)..Point::new(end_row, end_col); + + if capped_ancestor_range.contains_inclusive(&full_lines) { + return (capped_ancestor_range, Some(full_ancestor_range), symbols) + } + } + + let mut matched = matched; + matched.start.column = 0; + matched.start.row = + matched.start.row.saturating_sub(CONTEXT_LINES); + matched.end.row = cmp::min( + snapshot.max_point().row, + matched.end.row + CONTEXT_LINES, + ); + matched.end.column = snapshot.line_len(matched.end.row); + + (matched, None, symbols) + }) + .peekable(); + + let mut file_header_written = false; + + while let Some((mut range, ancestor_range, parent_symbols)) = ranges.next(){ + if skips_remaining > 0 { + skips_remaining -= 1; + continue; + } + + // We'd already found a full page of matches, and we just found one more. + if matches_found >= RESULTS_PER_PAGE { + has_more_matches = true; + break 'outer; + } + + while let Some((next_range, _, _)) = ranges.peek() { + if range.end.row >= next_range.start.row { + range.end = next_range.end; + ranges.next(); + } else { + break; + } + } + + if !file_header_written { + writeln!(output, "\n## Matches in {}", path.display())?; + file_header_written = true; + } + + let end_row = range.end.row; + output.push_str("\n### "); + + if let Some(parent_symbols) = &parent_symbols { + for symbol in parent_symbols { + write!(output, "{} › ", symbol.text)?; + } + } + + if range.start.row == end_row { + writeln!(output, "L{}", range.start.row + 1)?; + } else { + writeln!(output, "L{}-{}", range.start.row + 1, end_row + 1)?; + } + + output.push_str("```\n"); + output.extend(snapshot.text_for_range(range)); + output.push_str("\n```\n"); + + if let Some(ancestor_range) = ancestor_range { + if end_row < ancestor_range.end.row { + let remaining_lines = ancestor_range.end.row - end_row; + writeln!(output, "\n{} lines remaining in ancestor node. Read the file to see all.", remaining_lines)?; + } + } + + event_stream.update_fields(acp::ToolCallUpdateFields { + content: Some(vec![output.clone().into()]), + ..Default::default() + }); + matches_found += 1; + } + } + + let output = if matches_found == 0 { + "No matches found".to_string() + } else if has_more_matches { + format!( + "Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}", + input.offset + 1, + input.offset + matches_found, + input.offset + RESULTS_PER_PAGE, + ) + } else { + format!("Found {matches_found} matches:\n{output}") + }; + + event_stream.update_fields(acp::ToolCallUpdateFields { + content: Some(vec![output.clone().into()]), + ..Default::default() + }); + + Ok(output) + }) + } +} + +#[cfg(test)] +mod tests { + use crate::ToolCallEventStream; + + use super::*; + use gpui::{TestAppContext, UpdateGlobal}; + use language::{Language, LanguageConfig, LanguageMatcher}; + use project::{FakeFs, Project, WorktreeSettings}; + use serde_json::json; + use settings::SettingsStore; + use unindent::Unindent; + use util::path; + + #[gpui::test] + async fn test_grep_tool_with_include_pattern(cx: &mut TestAppContext) { + init_test(cx); + cx.executor().allow_parking(); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + path!("/root"), + serde_json::json!({ + "src": { + "main.rs": "fn main() {\n println!(\"Hello, world!\");\n}", + "utils": { + "helper.rs": "fn helper() {\n println!(\"I'm a helper!\");\n}", + }, + }, + "tests": { + "test_main.rs": "fn test_main() {\n assert!(true);\n}", + } + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + + // Test with include pattern for Rust files inside the root of the project + let input = GrepToolInput { + regex: "println".to_string(), + include_pattern: Some("root/**/*.rs".to_string()), + offset: 0, + case_sensitive: false, + }; + + let result = run_grep_tool(input, project.clone(), cx).await; + assert!(result.contains("main.rs"), "Should find matches in main.rs"); + assert!( + result.contains("helper.rs"), + "Should find matches in helper.rs" + ); + assert!( + !result.contains("test_main.rs"), + "Should not include test_main.rs even though it's a .rs file (because it doesn't have the pattern)" + ); + + // Test with include pattern for src directory only + let input = GrepToolInput { + regex: "fn".to_string(), + include_pattern: Some("root/**/src/**".to_string()), + offset: 0, + case_sensitive: false, + }; + + let result = run_grep_tool(input, project.clone(), cx).await; + assert!( + result.contains("main.rs"), + "Should find matches in src/main.rs" + ); + assert!( + result.contains("helper.rs"), + "Should find matches in src/utils/helper.rs" + ); + assert!( + !result.contains("test_main.rs"), + "Should not include test_main.rs as it's not in src directory" + ); + + // Test with empty include pattern (should default to all files) + let input = GrepToolInput { + regex: "fn".to_string(), + include_pattern: None, + offset: 0, + case_sensitive: false, + }; + + let result = run_grep_tool(input, project.clone(), cx).await; + assert!(result.contains("main.rs"), "Should find matches in main.rs"); + assert!( + result.contains("helper.rs"), + "Should find matches in helper.rs" + ); + assert!( + result.contains("test_main.rs"), + "Should include test_main.rs" + ); + } + + #[gpui::test] + async fn test_grep_tool_with_case_sensitivity(cx: &mut TestAppContext) { + init_test(cx); + cx.executor().allow_parking(); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + path!("/root"), + serde_json::json!({ + "case_test.txt": "This file has UPPERCASE and lowercase text.\nUPPERCASE patterns should match only with case_sensitive: true", + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + + // Test case-insensitive search (default) + let input = GrepToolInput { + regex: "uppercase".to_string(), + include_pattern: Some("**/*.txt".to_string()), + offset: 0, + case_sensitive: false, + }; + + let result = run_grep_tool(input, project.clone(), cx).await; + assert!( + result.contains("UPPERCASE"), + "Case-insensitive search should match uppercase" + ); + + // Test case-sensitive search + let input = GrepToolInput { + regex: "uppercase".to_string(), + include_pattern: Some("**/*.txt".to_string()), + offset: 0, + case_sensitive: true, + }; + + let result = run_grep_tool(input, project.clone(), cx).await; + assert!( + !result.contains("UPPERCASE"), + "Case-sensitive search should not match uppercase" + ); + + // Test case-sensitive search + let input = GrepToolInput { + regex: "LOWERCASE".to_string(), + include_pattern: Some("**/*.txt".to_string()), + offset: 0, + case_sensitive: true, + }; + + let result = run_grep_tool(input, project.clone(), cx).await; + + assert!( + !result.contains("lowercase"), + "Case-sensitive search should match lowercase" + ); + + // Test case-sensitive search for lowercase pattern + let input = GrepToolInput { + regex: "lowercase".to_string(), + include_pattern: Some("**/*.txt".to_string()), + offset: 0, + case_sensitive: true, + }; + + let result = run_grep_tool(input, project.clone(), cx).await; + assert!( + result.contains("lowercase"), + "Case-sensitive search should match lowercase text" + ); + } + + /// Helper function to set up a syntax test environment + async fn setup_syntax_test(cx: &mut TestAppContext) -> Entity { + use unindent::Unindent; + init_test(cx); + cx.executor().allow_parking(); + + let fs = FakeFs::new(cx.executor().clone()); + + // Create test file with syntax structures + fs.insert_tree( + path!("/root"), + serde_json::json!({ + "test_syntax.rs": r#" + fn top_level_function() { + println!("This is at the top level"); + } + + mod feature_module { + pub mod nested_module { + pub fn nested_function( + first_arg: String, + second_arg: i32, + ) { + println!("Function in nested module"); + println!("{first_arg}"); + println!("{second_arg}"); + } + } + } + + struct MyStruct { + field1: String, + field2: i32, + } + + impl MyStruct { + fn method_with_block() { + let condition = true; + if condition { + println!("Inside if block"); + } + } + + fn long_function() { + println!("Line 1"); + println!("Line 2"); + println!("Line 3"); + println!("Line 4"); + println!("Line 5"); + println!("Line 6"); + println!("Line 7"); + println!("Line 8"); + println!("Line 9"); + println!("Line 10"); + println!("Line 11"); + println!("Line 12"); + } + } + + trait Processor { + fn process(&self, input: &str) -> String; + } + + impl Processor for MyStruct { + fn process(&self, input: &str) -> String { + format!("Processed: {}", input) + } + } + "#.unindent().trim(), + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + + project.update(cx, |project, _cx| { + project.languages().add(rust_lang().into()) + }); + + project + } + + #[gpui::test] + async fn test_grep_top_level_function(cx: &mut TestAppContext) { + let project = setup_syntax_test(cx).await; + + // Test: Line at the top level of the file + let input = GrepToolInput { + regex: "This is at the top level".to_string(), + include_pattern: Some("**/*.rs".to_string()), + offset: 0, + case_sensitive: false, + }; + + let result = run_grep_tool(input, project.clone(), cx).await; + let expected = r#" + Found 1 matches: + + ## Matches in root/test_syntax.rs + + ### fn top_level_function › L1-3 + ``` + fn top_level_function() { + println!("This is at the top level"); + } + ``` + "# + .unindent(); + assert_eq!(result, expected); + } + + #[gpui::test] + async fn test_grep_function_body(cx: &mut TestAppContext) { + let project = setup_syntax_test(cx).await; + + // Test: Line inside a function body + let input = GrepToolInput { + regex: "Function in nested module".to_string(), + include_pattern: Some("**/*.rs".to_string()), + offset: 0, + case_sensitive: false, + }; + + let result = run_grep_tool(input, project.clone(), cx).await; + let expected = r#" + Found 1 matches: + + ## Matches in root/test_syntax.rs + + ### mod feature_module › pub mod nested_module › pub fn nested_function › L10-14 + ``` + ) { + println!("Function in nested module"); + println!("{first_arg}"); + println!("{second_arg}"); + } + ``` + "# + .unindent(); + assert_eq!(result, expected); + } + + #[gpui::test] + async fn test_grep_function_args_and_body(cx: &mut TestAppContext) { + let project = setup_syntax_test(cx).await; + + // Test: Line with a function argument + let input = GrepToolInput { + regex: "second_arg".to_string(), + include_pattern: Some("**/*.rs".to_string()), + offset: 0, + case_sensitive: false, + }; + + let result = run_grep_tool(input, project.clone(), cx).await; + let expected = r#" + Found 1 matches: + + ## Matches in root/test_syntax.rs + + ### mod feature_module › pub mod nested_module › pub fn nested_function › L7-14 + ``` + pub fn nested_function( + first_arg: String, + second_arg: i32, + ) { + println!("Function in nested module"); + println!("{first_arg}"); + println!("{second_arg}"); + } + ``` + "# + .unindent(); + assert_eq!(result, expected); + } + + #[gpui::test] + async fn test_grep_if_block(cx: &mut TestAppContext) { + use unindent::Unindent; + let project = setup_syntax_test(cx).await; + + // Test: Line inside an if block + let input = GrepToolInput { + regex: "Inside if block".to_string(), + include_pattern: Some("**/*.rs".to_string()), + offset: 0, + case_sensitive: false, + }; + + let result = run_grep_tool(input, project.clone(), cx).await; + let expected = r#" + Found 1 matches: + + ## Matches in root/test_syntax.rs + + ### impl MyStruct › fn method_with_block › L26-28 + ``` + if condition { + println!("Inside if block"); + } + ``` + "# + .unindent(); + assert_eq!(result, expected); + } + + #[gpui::test] + async fn test_grep_long_function_top(cx: &mut TestAppContext) { + use unindent::Unindent; + let project = setup_syntax_test(cx).await; + + // Test: Line in the middle of a long function - should show message about remaining lines + let input = GrepToolInput { + regex: "Line 5".to_string(), + include_pattern: Some("**/*.rs".to_string()), + offset: 0, + case_sensitive: false, + }; + + let result = run_grep_tool(input, project.clone(), cx).await; + let expected = r#" + Found 1 matches: + + ## Matches in root/test_syntax.rs + + ### impl MyStruct › fn long_function › L31-41 + ``` + fn long_function() { + println!("Line 1"); + println!("Line 2"); + println!("Line 3"); + println!("Line 4"); + println!("Line 5"); + println!("Line 6"); + println!("Line 7"); + println!("Line 8"); + println!("Line 9"); + println!("Line 10"); + ``` + + 3 lines remaining in ancestor node. Read the file to see all. + "# + .unindent(); + assert_eq!(result, expected); + } + + #[gpui::test] + async fn test_grep_long_function_bottom(cx: &mut TestAppContext) { + use unindent::Unindent; + let project = setup_syntax_test(cx).await; + + // Test: Line in the long function + let input = GrepToolInput { + regex: "Line 12".to_string(), + include_pattern: Some("**/*.rs".to_string()), + offset: 0, + case_sensitive: false, + }; + + let result = run_grep_tool(input, project.clone(), cx).await; + let expected = r#" + Found 1 matches: + + ## Matches in root/test_syntax.rs + + ### impl MyStruct › fn long_function › L41-45 + ``` + println!("Line 10"); + println!("Line 11"); + println!("Line 12"); + } + } + ``` + "# + .unindent(); + assert_eq!(result, expected); + } + + async fn run_grep_tool( + input: GrepToolInput, + project: Entity, + cx: &mut TestAppContext, + ) -> String { + let tool = Arc::new(GrepTool { project }); + let task = cx.update(|cx| tool.run(input, ToolCallEventStream::test().0, cx)); + + match task.await { + Ok(result) => { + if cfg!(windows) { + result.replace("root\\", "root/") + } else { + result.to_string() + } + } + Err(e) => panic!("Failed to run grep tool: {}", e), + } + } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + Project::init_settings(cx); + }); + } + + fn rust_lang() -> Language { + Language::new( + LanguageConfig { + name: "Rust".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + ) + .with_outline_query(include_str!("../../../languages/src/rust/outline.scm")) + .unwrap() + } + + #[gpui::test] + async fn test_grep_security_boundaries(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + + fs.insert_tree( + path!("/"), + json!({ + "project_root": { + "allowed_file.rs": "fn main() { println!(\"This file is in the project\"); }", + ".mysecrets": "SECRET_KEY=abc123\nfn secret() { /* private */ }", + ".secretdir": { + "config": "fn special_configuration() { /* excluded */ }" + }, + ".mymetadata": "fn custom_metadata() { /* excluded */ }", + "subdir": { + "normal_file.rs": "fn normal_file_content() { /* Normal */ }", + "special.privatekey": "fn private_key_content() { /* private */ }", + "data.mysensitive": "fn sensitive_data() { /* private */ }" + } + }, + "outside_project": { + "sensitive_file.rs": "fn outside_function() { /* This file is outside the project */ }" + } + }), + ) + .await; + + cx.update(|cx| { + use gpui::UpdateGlobal; + use project::WorktreeSettings; + use settings::SettingsStore; + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings::(cx, |settings| { + settings.file_scan_exclusions = Some(vec![ + "**/.secretdir".to_string(), + "**/.mymetadata".to_string(), + ]); + settings.private_files = Some(vec![ + "**/.mysecrets".to_string(), + "**/*.privatekey".to_string(), + "**/*.mysensitive".to_string(), + ]); + }); + }); + }); + + let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await; + + // Searching for files outside the project worktree should return no results + let result = run_grep_tool( + GrepToolInput { + regex: "outside_function".to_string(), + include_pattern: None, + offset: 0, + case_sensitive: false, + }, + project.clone(), + cx, + ) + .await; + let paths = extract_paths_from_results(&result); + assert!( + paths.is_empty(), + "grep_tool should not find files outside the project worktree" + ); + + // Searching within the project should succeed + let result = run_grep_tool( + GrepToolInput { + regex: "main".to_string(), + include_pattern: None, + offset: 0, + case_sensitive: false, + }, + project.clone(), + cx, + ) + .await; + let paths = extract_paths_from_results(&result); + assert!( + paths.iter().any(|p| p.contains("allowed_file.rs")), + "grep_tool should be able to search files inside worktrees" + ); + + // Searching files that match file_scan_exclusions should return no results + let result = run_grep_tool( + GrepToolInput { + regex: "special_configuration".to_string(), + include_pattern: None, + offset: 0, + case_sensitive: false, + }, + project.clone(), + cx, + ) + .await; + let paths = extract_paths_from_results(&result); + assert!( + paths.is_empty(), + "grep_tool should not search files in .secretdir (file_scan_exclusions)" + ); + + let result = run_grep_tool( + GrepToolInput { + regex: "custom_metadata".to_string(), + include_pattern: None, + offset: 0, + case_sensitive: false, + }, + project.clone(), + cx, + ) + .await; + let paths = extract_paths_from_results(&result); + assert!( + paths.is_empty(), + "grep_tool should not search .mymetadata files (file_scan_exclusions)" + ); + + // Searching private files should return no results + let result = run_grep_tool( + GrepToolInput { + regex: "SECRET_KEY".to_string(), + include_pattern: None, + offset: 0, + case_sensitive: false, + }, + project.clone(), + cx, + ) + .await; + let paths = extract_paths_from_results(&result); + assert!( + paths.is_empty(), + "grep_tool should not search .mysecrets (private_files)" + ); + + let result = run_grep_tool( + GrepToolInput { + regex: "private_key_content".to_string(), + include_pattern: None, + offset: 0, + case_sensitive: false, + }, + project.clone(), + cx, + ) + .await; + let paths = extract_paths_from_results(&result); + + assert!( + paths.is_empty(), + "grep_tool should not search .privatekey files (private_files)" + ); + + let result = run_grep_tool( + GrepToolInput { + regex: "sensitive_data".to_string(), + include_pattern: None, + offset: 0, + case_sensitive: false, + }, + project.clone(), + cx, + ) + .await; + let paths = extract_paths_from_results(&result); + assert!( + paths.is_empty(), + "grep_tool should not search .mysensitive files (private_files)" + ); + + // Searching a normal file should still work, even with private_files configured + let result = run_grep_tool( + GrepToolInput { + regex: "normal_file_content".to_string(), + include_pattern: None, + offset: 0, + case_sensitive: false, + }, + project.clone(), + cx, + ) + .await; + let paths = extract_paths_from_results(&result); + assert!( + paths.iter().any(|p| p.contains("normal_file.rs")), + "Should be able to search normal files" + ); + + // Path traversal attempts with .. in include_pattern should not escape project + let result = run_grep_tool( + GrepToolInput { + regex: "outside_function".to_string(), + include_pattern: Some("../outside_project/**/*.rs".to_string()), + offset: 0, + case_sensitive: false, + }, + project.clone(), + cx, + ) + .await; + let paths = extract_paths_from_results(&result); + assert!( + paths.is_empty(), + "grep_tool should not allow escaping project boundaries with relative paths" + ); + } + + #[gpui::test] + async fn test_grep_with_multiple_worktree_settings(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + + // Create first worktree with its own private files + fs.insert_tree( + path!("/worktree1"), + json!({ + ".zed": { + "settings.json": r#"{ + "file_scan_exclusions": ["**/fixture.*"], + "private_files": ["**/secret.rs"] + }"# + }, + "src": { + "main.rs": "fn main() { let secret_key = \"hidden\"; }", + "secret.rs": "const API_KEY: &str = \"secret_value\";", + "utils.rs": "pub fn get_config() -> String { \"config\".to_string() }" + }, + "tests": { + "test.rs": "fn test_secret() { assert!(true); }", + "fixture.sql": "SELECT * FROM secret_table;" + } + }), + ) + .await; + + // Create second worktree with different private files + fs.insert_tree( + path!("/worktree2"), + json!({ + ".zed": { + "settings.json": r#"{ + "file_scan_exclusions": ["**/internal.*"], + "private_files": ["**/private.js", "**/data.json"] + }"# + }, + "lib": { + "public.js": "export function getSecret() { return 'public'; }", + "private.js": "const SECRET_KEY = \"private_value\";", + "data.json": "{\"secret_data\": \"hidden\"}" + }, + "docs": { + "README.md": "# Documentation with secret info", + "internal.md": "Internal secret documentation" + } + }), + ) + .await; + + // Set global settings + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings::(cx, |settings| { + settings.file_scan_exclusions = + Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); + settings.private_files = Some(vec!["**/.env".to_string()]); + }); + }); + }); + + let project = Project::test( + fs.clone(), + [path!("/worktree1").as_ref(), path!("/worktree2").as_ref()], + cx, + ) + .await; + + // Wait for worktrees to be fully scanned + cx.executor().run_until_parked(); + + // Search for "secret" - should exclude files based on worktree-specific settings + let result = run_grep_tool( + GrepToolInput { + regex: "secret".to_string(), + include_pattern: None, + offset: 0, + case_sensitive: false, + }, + project.clone(), + cx, + ) + .await; + let paths = extract_paths_from_results(&result); + + // Should find matches in non-private files + assert!( + paths.iter().any(|p| p.contains("main.rs")), + "Should find 'secret' in worktree1/src/main.rs" + ); + assert!( + paths.iter().any(|p| p.contains("test.rs")), + "Should find 'secret' in worktree1/tests/test.rs" + ); + assert!( + paths.iter().any(|p| p.contains("public.js")), + "Should find 'secret' in worktree2/lib/public.js" + ); + assert!( + paths.iter().any(|p| p.contains("README.md")), + "Should find 'secret' in worktree2/docs/README.md" + ); + + // Should NOT find matches in private/excluded files based on worktree settings + assert!( + !paths.iter().any(|p| p.contains("secret.rs")), + "Should not search in worktree1/src/secret.rs (local private_files)" + ); + assert!( + !paths.iter().any(|p| p.contains("fixture.sql")), + "Should not search in worktree1/tests/fixture.sql (local file_scan_exclusions)" + ); + assert!( + !paths.iter().any(|p| p.contains("private.js")), + "Should not search in worktree2/lib/private.js (local private_files)" + ); + assert!( + !paths.iter().any(|p| p.contains("data.json")), + "Should not search in worktree2/lib/data.json (local private_files)" + ); + assert!( + !paths.iter().any(|p| p.contains("internal.md")), + "Should not search in worktree2/docs/internal.md (local file_scan_exclusions)" + ); + + // Test with `include_pattern` specific to one worktree + let result = run_grep_tool( + GrepToolInput { + regex: "secret".to_string(), + include_pattern: Some("worktree1/**/*.rs".to_string()), + offset: 0, + case_sensitive: false, + }, + project.clone(), + cx, + ) + .await; + + let paths = extract_paths_from_results(&result); + + // Should only find matches in worktree1 *.rs files (excluding private ones) + assert!( + paths.iter().any(|p| p.contains("main.rs")), + "Should find match in worktree1/src/main.rs" + ); + assert!( + paths.iter().any(|p| p.contains("test.rs")), + "Should find match in worktree1/tests/test.rs" + ); + assert!( + !paths.iter().any(|p| p.contains("secret.rs")), + "Should not find match in excluded worktree1/src/secret.rs" + ); + assert!( + paths.iter().all(|p| !p.contains("worktree2")), + "Should not find any matches in worktree2" + ); + } + + // Helper function to extract file paths from grep results + fn extract_paths_from_results(results: &str) -> Vec { + results + .lines() + .filter(|line| line.starts_with("## Matches in ")) + .map(|line| { + line.strip_prefix("## Matches in ") + .unwrap() + .trim() + .to_string() + }) + .collect() + } +} diff --git a/crates/agent2/src/tools/now_tool.rs b/crates/agent2/src/tools/now_tool.rs new file mode 100644 index 0000000000..71698b8275 --- /dev/null +++ b/crates/agent2/src/tools/now_tool.rs @@ -0,0 +1,66 @@ +use std::sync::Arc; + +use agent_client_protocol as acp; +use anyhow::Result; +use chrono::{Local, Utc}; +use gpui::{App, SharedString, Task}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{AgentTool, ToolCallEventStream}; + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum Timezone { + /// Use UTC for the datetime. + Utc, + /// Use local time for the datetime. + Local, +} + +/// Returns the current datetime in RFC 3339 format. +/// Only use this tool when the user specifically asks for it or the current task would benefit from knowing the current datetime. +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct NowToolInput { + /// The timezone to use for the datetime. + timezone: Timezone, +} + +pub struct NowTool; + +impl AgentTool for NowTool { + type Input = NowToolInput; + type Output = String; + + fn name(&self) -> SharedString { + "now".into() + } + + fn kind(&self) -> acp::ToolKind { + acp::ToolKind::Other + } + + fn initial_title(&self, _input: Result) -> SharedString { + "Get current time".into() + } + + fn run( + self: Arc, + input: Self::Input, + event_stream: ToolCallEventStream, + _cx: &mut App, + ) -> Task> { + let now = match input.timezone { + Timezone::Utc => Utc::now().to_rfc3339(), + Timezone::Local => Local::now().to_rfc3339(), + }; + let content = format!("The current datetime is {now}."); + + event_stream.update_fields(acp::ToolCallUpdateFields { + content: Some(vec![content.clone().into()]), + ..Default::default() + }); + + Task::ready(Ok(content)) + } +} diff --git a/crates/agent2/src/tools/web_search_tool.rs b/crates/agent2/src/tools/web_search_tool.rs new file mode 100644 index 0000000000..12587c2f67 --- /dev/null +++ b/crates/agent2/src/tools/web_search_tool.rs @@ -0,0 +1,105 @@ +use std::sync::Arc; + +use crate::{AgentTool, ToolCallEventStream}; +use agent_client_protocol as acp; +use anyhow::{Result, anyhow}; +use cloud_llm_client::WebSearchResponse; +use gpui::{App, AppContext, Task}; +use language_model::LanguageModelToolResultContent; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use ui::prelude::*; +use web_search::WebSearchRegistry; + +/// Search the web for information using your query. +/// Use this when you need real-time information, facts, or data that might not be in your training. \ +/// Results will include snippets and links from relevant web pages. +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct WebSearchToolInput { + /// The search term or question to query on the web. + query: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(transparent)] +pub struct WebSearchToolOutput(WebSearchResponse); + +impl From for LanguageModelToolResultContent { + fn from(value: WebSearchToolOutput) -> Self { + serde_json::to_string(&value.0) + .expect("Failed to serialize WebSearchResponse") + .into() + } +} + +pub struct WebSearchTool; + +impl AgentTool for WebSearchTool { + type Input = WebSearchToolInput; + type Output = WebSearchToolOutput; + + fn name(&self) -> SharedString { + "web_search".into() + } + + fn kind(&self) -> acp::ToolKind { + acp::ToolKind::Fetch + } + + fn initial_title(&self, _input: Result) -> SharedString { + "Searching the Web".into() + } + + fn run( + self: Arc, + input: Self::Input, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> { + let Some(provider) = WebSearchRegistry::read_global(cx).active_provider() else { + return Task::ready(Err(anyhow!("Web search is not available."))); + }; + + let search_task = provider.search(input.query, cx); + cx.background_spawn(async move { + let response = match search_task.await { + Ok(response) => response, + Err(err) => { + event_stream.update_fields(acp::ToolCallUpdateFields { + title: Some("Web Search Failed".to_string()), + ..Default::default() + }); + return Err(err); + } + }; + + let result_text = if response.results.len() == 1 { + "1 result".to_string() + } else { + format!("{} results", response.results.len()) + }; + event_stream.update_fields(acp::ToolCallUpdateFields { + title: Some(format!("Searched the web: {result_text}")), + content: Some( + response + .results + .iter() + .map(|result| acp::ToolCallContent::Content { + content: acp::ContentBlock::ResourceLink(acp::ResourceLink { + name: result.title.clone(), + uri: result.url.clone(), + title: Some(result.title.clone()), + description: Some(result.text.clone()), + mime_type: None, + annotations: None, + size: None, + }), + }) + .collect(), + ), + ..Default::default() + }); + Ok(WebSearchToolOutput(response)) + }) + } +} diff --git a/crates/cloud_llm_client/src/cloud_llm_client.rs b/crates/cloud_llm_client/src/cloud_llm_client.rs index e78957ec49..741945af10 100644 --- a/crates/cloud_llm_client/src/cloud_llm_client.rs +++ b/crates/cloud_llm_client/src/cloud_llm_client.rs @@ -263,12 +263,12 @@ pub struct WebSearchBody { pub query: String, } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct WebSearchResponse { pub results: Vec, } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct WebSearchResult { pub title: String, pub url: String, From abb64d2320e77bd3c3e6e0f46c0dbad9f2e25c17 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Mon, 11 Aug 2025 10:09:25 -0400 Subject: [PATCH 250/693] Ignore project-local settings for always_allow_tool_actions (#35976) Now `always_allow_tool_actions` is only respected as the user's global setting, not as an overridable project-local setting. This way, you don't have to worry about switching into a project (or switching branches within a project) and discovering that suddenly your tool calls no longer require confirmation. Release Notes: - Removed always_allow_tool_actions from project-local settings (it is now global-only) Co-authored-by: David Kleingeld --- crates/agent_settings/src/agent_settings.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index e6a79963d6..d9557c5d00 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -442,10 +442,6 @@ impl Settings for AgentSettings { &mut settings.inline_alternatives, value.inline_alternatives.clone(), ); - merge( - &mut settings.always_allow_tool_actions, - value.always_allow_tool_actions, - ); merge( &mut settings.notify_when_agent_waiting, value.notify_when_agent_waiting, @@ -507,6 +503,20 @@ impl Settings for AgentSettings { } } + debug_assert_eq!( + sources.default.always_allow_tool_actions.unwrap_or(false), + false, + "For security, agent.always_allow_tool_actions should always be false in default.json. If it's true, that is a bug that should be fixed!" + ); + + // For security reasons, only trust the user's global settings for whether to always allow tool actions. + // If this could be overridden locally, an attacker could (e.g. by committing to source control and + // convincing you to switch branches) modify your project-local settings to disable the agent's safety checks. + settings.always_allow_tool_actions = sources + .user + .and_then(|setting| setting.always_allow_tool_actions) + .unwrap_or(false); + Ok(settings) } From 6478e66e7a6e0c2c580190c674e1f9f1db92f764 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Mon, 11 Aug 2025 10:56:45 -0400 Subject: [PATCH 251/693] Stricter `disable_ai` overrides (#35977) Settings overrides (e.g. local project settings, server settings) can no longer change `disable_ai` to `false` if it was `true`; they can only change it to `true`. In other words, settings can only cause AI to be *more* disabled, they can't undo the user's preference for no AI (or the project's requirement not to use AI). Release Notes: - Settings overrides (such as local project settings) can now only override `disable_ai` to become `true`; they can no longer cause otherwise-disabled AI to become re-enabled. --------- Co-authored-by: Assistant Co-authored-by: David Kleingeld --- crates/project/src/project.rs | 171 ++++++++++++++++++++++++++++++++-- 1 file changed, 163 insertions(+), 8 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d543e6bf25..27ab55d53e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -962,14 +962,19 @@ impl settings::Settings for DisableAiSettings { type FileContent = Option; fn load(sources: SettingsSources, _: &mut App) -> Result { - Ok(Self { - disable_ai: sources - .user - .or(sources.server) - .copied() - .flatten() - .unwrap_or(sources.default.ok_or_else(Self::missing_default)?), - }) + // For security reasons, settings can only make AI restrictions MORE strict, not less. + // (For example, if someone is working on a project that contractually + // requires no AI use, that should override the user's setting which + // permits AI use.) + // This also prevents an attacker from using project or server settings to enable AI when it should be disabled. + let disable_ai = sources + .project + .iter() + .chain(sources.user.iter()) + .chain(sources.server.iter()) + .any(|disabled| **disabled == Some(true)); + + Ok(Self { disable_ai }) } fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} @@ -5508,3 +5513,153 @@ fn provide_inline_values( variables } + +#[cfg(test)] +mod disable_ai_settings_tests { + use super::*; + use gpui::TestAppContext; + use settings::{Settings, SettingsSources}; + + #[gpui::test] + async fn test_disable_ai_settings_security(cx: &mut TestAppContext) { + cx.update(|cx| { + // Test 1: Default is false (AI enabled) + let sources = SettingsSources { + default: &Some(false), + global: None, + extensions: None, + user: None, + release_channel: None, + operating_system: None, + profile: None, + server: None, + project: &[], + }; + let settings = DisableAiSettings::load(sources, cx).unwrap(); + assert_eq!(settings.disable_ai, false, "Default should allow AI"); + + // Test 2: Global true, local false -> still disabled (local cannot re-enable) + let global_true = Some(true); + let local_false = Some(false); + let sources = SettingsSources { + default: &Some(false), + global: None, + extensions: None, + user: Some(&global_true), + release_channel: None, + operating_system: None, + profile: None, + server: None, + project: &[&local_false], + }; + let settings = DisableAiSettings::load(sources, cx).unwrap(); + assert_eq!( + settings.disable_ai, true, + "Local false cannot override global true" + ); + + // Test 3: Global false, local true -> disabled (local can make more restrictive) + let global_false = Some(false); + let local_true = Some(true); + let sources = SettingsSources { + default: &Some(false), + global: None, + extensions: None, + user: Some(&global_false), + release_channel: None, + operating_system: None, + profile: None, + server: None, + project: &[&local_true], + }; + let settings = DisableAiSettings::load(sources, cx).unwrap(); + assert_eq!( + settings.disable_ai, true, + "Local true can override global false" + ); + + // Test 4: Server can only make more restrictive (set to true) + let user_false = Some(false); + let server_true = Some(true); + let sources = SettingsSources { + default: &Some(false), + global: None, + extensions: None, + user: Some(&user_false), + release_channel: None, + operating_system: None, + profile: None, + server: Some(&server_true), + project: &[], + }; + let settings = DisableAiSettings::load(sources, cx).unwrap(); + assert_eq!( + settings.disable_ai, true, + "Server can set to true even if user is false" + ); + + // Test 5: Server false cannot override user true + let user_true = Some(true); + let server_false = Some(false); + let sources = SettingsSources { + default: &Some(false), + global: None, + extensions: None, + user: Some(&user_true), + release_channel: None, + operating_system: None, + profile: None, + server: Some(&server_false), + project: &[], + }; + let settings = DisableAiSettings::load(sources, cx).unwrap(); + assert_eq!( + settings.disable_ai, true, + "Server false cannot override user true" + ); + + // Test 6: Multiple local settings, any true disables AI + let global_false = Some(false); + let local_false3 = Some(false); + let local_true2 = Some(true); + let local_false4 = Some(false); + let sources = SettingsSources { + default: &Some(false), + global: None, + extensions: None, + user: Some(&global_false), + release_channel: None, + operating_system: None, + profile: None, + server: None, + project: &[&local_false3, &local_true2, &local_false4], + }; + let settings = DisableAiSettings::load(sources, cx).unwrap(); + assert_eq!( + settings.disable_ai, true, + "Any local true should disable AI" + ); + + // Test 7: All three sources can independently disable AI + let user_false2 = Some(false); + let server_false2 = Some(false); + let local_true3 = Some(true); + let sources = SettingsSources { + default: &Some(false), + global: None, + extensions: None, + user: Some(&user_false2), + release_channel: None, + operating_system: None, + profile: None, + server: Some(&server_false2), + project: &[&local_true3], + }; + let settings = DisableAiSettings::load(sources, cx).unwrap(); + assert_eq!( + settings.disable_ai, true, + "Local can disable even if user and server are false" + ); + }); + } +} From 12084b667784e3a1fae4b1bc4b9abf5c7640f55e Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 11 Aug 2025 16:07:32 +0100 Subject: [PATCH 252/693] Fix keys not being sent to terminal (#35979) Fixes #35057 Release Notes: - Fix input being sent to editor/terminal when pending keystrokes are resolved --- crates/gpui/src/key_dispatch.rs | 173 +++++++++++++++++++++++++++++++- crates/gpui/src/window.rs | 3 +- 2 files changed, 173 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index cc6ebb9b08..c3f5d18603 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -611,9 +611,17 @@ impl DispatchTree { #[cfg(test)] mod tests { - use std::{cell::RefCell, rc::Rc}; + use crate::{ + self as gpui, Element, ElementId, GlobalElementId, InspectorElementId, LayoutId, Style, + }; + use core::panic; + use std::{cell::RefCell, ops::Range, rc::Rc}; - use crate::{Action, ActionRegistry, DispatchTree, KeyBinding, KeyContext, Keymap}; + use crate::{ + Action, ActionRegistry, App, Bounds, Context, DispatchTree, FocusHandle, InputHandler, + IntoElement, KeyBinding, KeyContext, Keymap, Pixels, Point, Render, TestAppContext, + UTF16Selection, Window, + }; #[derive(PartialEq, Eq)] struct TestAction; @@ -674,4 +682,165 @@ mod tests { assert!(keybinding[0].action.partial_eq(&TestAction)) } + + #[crate::test] + fn test_input_handler_pending(cx: &mut TestAppContext) { + #[derive(Clone)] + struct CustomElement { + focus_handle: FocusHandle, + text: Rc>, + } + impl CustomElement { + fn new(cx: &mut Context) -> Self { + Self { + focus_handle: cx.focus_handle(), + text: Rc::default(), + } + } + } + impl Element for CustomElement { + type RequestLayoutState = (); + + type PrepaintState = (); + + fn id(&self) -> Option { + Some("custom".into()) + } + fn source_location(&self) -> Option<&'static panic::Location<'static>> { + None + } + fn request_layout( + &mut self, + _: Option<&GlobalElementId>, + _: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + (window.request_layout(Style::default(), [], cx), ()) + } + fn prepaint( + &mut self, + _: Option<&GlobalElementId>, + _: Option<&InspectorElementId>, + _: Bounds, + _: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState { + window.set_focus_handle(&self.focus_handle, cx); + } + fn paint( + &mut self, + _: Option<&GlobalElementId>, + _: Option<&InspectorElementId>, + _: Bounds, + _: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + let mut key_context = KeyContext::default(); + key_context.add("Terminal"); + window.set_key_context(key_context); + window.handle_input(&self.focus_handle, self.clone(), cx); + window.on_action(std::any::TypeId::of::(), |_, _, _, _| {}); + } + } + impl IntoElement for CustomElement { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } + } + + impl InputHandler for CustomElement { + fn selected_text_range( + &mut self, + _: bool, + _: &mut Window, + _: &mut App, + ) -> Option { + None + } + + fn marked_text_range(&mut self, _: &mut Window, _: &mut App) -> Option> { + None + } + + fn text_for_range( + &mut self, + _: Range, + _: &mut Option>, + _: &mut Window, + _: &mut App, + ) -> Option { + None + } + + fn replace_text_in_range( + &mut self, + replacement_range: Option>, + text: &str, + _: &mut Window, + _: &mut App, + ) { + if replacement_range.is_some() { + unimplemented!() + } + self.text.borrow_mut().push_str(text) + } + + fn replace_and_mark_text_in_range( + &mut self, + replacement_range: Option>, + new_text: &str, + _: Option>, + _: &mut Window, + _: &mut App, + ) { + if replacement_range.is_some() { + unimplemented!() + } + self.text.borrow_mut().push_str(new_text) + } + + fn unmark_text(&mut self, _: &mut Window, _: &mut App) {} + + fn bounds_for_range( + &mut self, + _: Range, + _: &mut Window, + _: &mut App, + ) -> Option> { + None + } + + fn character_index_for_point( + &mut self, + _: Point, + _: &mut Window, + _: &mut App, + ) -> Option { + None + } + } + impl Render for CustomElement { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + self.clone() + } + } + + cx.update(|cx| { + cx.bind_keys([KeyBinding::new("ctrl-b", TestAction, Some("Terminal"))]); + cx.bind_keys([KeyBinding::new("ctrl-b h", TestAction, Some("Terminal"))]); + }); + let (test, cx) = cx.add_window_view(|_, cx| CustomElement::new(cx)); + cx.update(|window, cx| { + window.focus(&test.read(cx).focus_handle); + window.activate_window(); + }); + cx.simulate_keystrokes("ctrl-b ["); + test.update(cx, |test, _| assert_eq!(test.text.borrow().as_str(), "[")) + } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 40d3845ff9..3a430b806d 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -3688,7 +3688,8 @@ impl Window { ); if !match_result.to_replay.is_empty() { - self.replay_pending_input(match_result.to_replay, cx) + self.replay_pending_input(match_result.to_replay, cx); + cx.propagate_event = true; } if !match_result.pending.is_empty() { From 62270b33c24e64763c5330d69f8ebc3931d49ae8 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Mon, 11 Aug 2025 11:09:38 -0400 Subject: [PATCH 253/693] git: Add ability to clone remote repositories from Zed (#35606) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds preliminary git clone support through using the new `GitClone` action. This works with SSH connections too. - [x] Get backend working - [x] Add a UI to interact with this Future follow-ups: - Polish the UI - Have the path select prompt say "Select Repository clone target" instead of “Open” - Use Zed path prompt if the user has that as a setting - Add support for cloning from a user's GitHub repositories directly Release Notes: - Add the ability to clone remote git repositories through the `git: Clone` action --------- Co-authored-by: hpmcdona --- crates/fs/src/fs.rs | 24 ++++- crates/git/src/git.rs | 2 + crates/git_ui/src/git_panel.rs | 93 ++++++++++++++++++ crates/git_ui/src/git_ui.rs | 120 ++++++++++++++++++++++- crates/language/src/language_settings.rs | 2 +- crates/project/src/git_store.rs | 56 +++++++++++ crates/proto/proto/git.proto | 10 ++ crates/proto/proto/zed.proto | 5 +- crates/proto/src/proto.rs | 6 +- 9 files changed, 310 insertions(+), 8 deletions(-) diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index a76ccee2bf..af8fe129ab 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -12,7 +12,7 @@ use gpui::BackgroundExecutor; use gpui::Global; use gpui::ReadGlobal as _; use std::borrow::Cow; -use util::command::new_std_command; +use util::command::{new_smol_command, new_std_command}; #[cfg(unix)] use std::os::fd::{AsFd, AsRawFd}; @@ -134,6 +134,7 @@ pub trait Fs: Send + Sync { fn home_dir(&self) -> Option; fn open_repo(&self, abs_dot_git: &Path) -> Option>; fn git_init(&self, abs_work_directory: &Path, fallback_branch_name: String) -> Result<()>; + async fn git_clone(&self, repo_url: &str, abs_work_directory: &Path) -> Result<()>; fn is_fake(&self) -> bool; async fn is_case_sensitive(&self) -> Result; @@ -839,6 +840,23 @@ impl Fs for RealFs { Ok(()) } + async fn git_clone(&self, repo_url: &str, abs_work_directory: &Path) -> Result<()> { + let output = new_smol_command("git") + .current_dir(abs_work_directory) + .args(&["clone", repo_url]) + .output() + .await?; + + if !output.status.success() { + anyhow::bail!( + "git clone failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + Ok(()) + } + fn is_fake(&self) -> bool { false } @@ -2352,6 +2370,10 @@ impl Fs for FakeFs { smol::block_on(self.create_dir(&abs_work_directory_path.join(".git"))) } + async fn git_clone(&self, _repo_url: &str, _abs_work_directory: &Path) -> Result<()> { + anyhow::bail!("Git clone is not supported in fake Fs") + } + fn is_fake(&self) -> bool { true } diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index 553361e673..e6336eb656 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -93,6 +93,8 @@ actions!( Init, /// Opens all modified files in the editor. OpenModifiedFiles, + /// Clones a repository. + Clone, ] ); diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index e4f445858d..75fac114d2 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -2081,6 +2081,99 @@ impl GitPanel { .detach_and_log_err(cx); } + pub(crate) fn git_clone(&mut self, repo: String, window: &mut Window, cx: &mut Context) { + let path = cx.prompt_for_paths(gpui::PathPromptOptions { + files: false, + directories: true, + multiple: false, + }); + + let workspace = self.workspace.clone(); + + cx.spawn_in(window, async move |this, cx| { + let mut paths = path.await.ok()?.ok()??; + let mut path = paths.pop()?; + let repo_name = repo + .split(std::path::MAIN_SEPARATOR_STR) + .last()? + .strip_suffix(".git")? + .to_owned(); + + let fs = this.read_with(cx, |this, _| this.fs.clone()).ok()?; + + let prompt_answer = match fs.git_clone(&repo, path.as_path()).await { + Ok(_) => cx.update(|window, cx| { + window.prompt( + PromptLevel::Info, + "Git Clone", + None, + &["Add repo to project", "Open repo in new project"], + cx, + ) + }), + Err(e) => { + this.update(cx, |this: &mut GitPanel, cx| { + let toast = StatusToast::new(e.to_string(), cx, |this, _| { + this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error)) + .dismiss_button(true) + }); + + this.workspace + .update(cx, |workspace, cx| { + workspace.toggle_status_toast(toast, cx); + }) + .ok(); + }) + .ok()?; + + return None; + } + } + .ok()?; + + path.push(repo_name); + match prompt_answer.await.ok()? { + 0 => { + workspace + .update(cx, |workspace, cx| { + workspace + .project() + .update(cx, |project, cx| { + project.create_worktree(path.as_path(), true, cx) + }) + .detach(); + }) + .ok(); + } + 1 => { + workspace + .update(cx, move |workspace, cx| { + workspace::open_new( + Default::default(), + workspace.app_state().clone(), + cx, + move |workspace, _, cx| { + cx.activate(true); + workspace + .project() + .update(cx, |project, cx| { + project.create_worktree(&path, true, cx) + }) + .detach(); + }, + ) + .detach(); + }) + .ok(); + } + _ => {} + } + + Some(()) + }) + .detach(); + } + pub(crate) fn git_init(&mut self, window: &mut Window, cx: &mut Context) { let worktrees = self .project diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index bde867bcd2..7d5207dfb6 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -3,21 +3,25 @@ use std::any::Any; use ::settings::Settings; use command_palette_hooks::CommandPaletteFilter; use commit_modal::CommitModal; -use editor::{Editor, actions::DiffClipboardWithSelectionData}; +use editor::{Editor, EditorElement, EditorStyle, actions::DiffClipboardWithSelectionData}; mod blame_ui; use git::{ repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus}, status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode}, }; use git_panel_settings::GitPanelSettings; -use gpui::{Action, App, Context, FocusHandle, Window, actions}; +use gpui::{ + Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, TextStyle, + Window, actions, +}; use onboarding::GitOnboardingModal; use project_diff::ProjectDiff; +use theme::ThemeSettings; use ui::prelude::*; -use workspace::Workspace; +use workspace::{ModalView, Workspace}; use zed_actions; -use crate::text_diff_view::TextDiffView; +use crate::{git_panel::GitPanel, text_diff_view::TextDiffView}; mod askpass_modal; pub mod branch_picker; @@ -169,6 +173,19 @@ pub fn init(cx: &mut App) { panel.git_init(window, cx); }); }); + workspace.register_action(|workspace, _action: &git::Clone, window, cx| { + let Some(panel) = workspace.panel::(cx) else { + return; + }; + + workspace.toggle_modal(window, cx, |window, cx| { + GitCloneModal::show(panel, window, cx) + }); + + // panel.update(cx, |panel, cx| { + // panel.git_clone(window, cx); + // }); + }); workspace.register_action(|workspace, _: &git::OpenModifiedFiles, window, cx| { open_modified_files(workspace, window, cx); }); @@ -613,3 +630,98 @@ impl Component for GitStatusIcon { ) } } + +struct GitCloneModal { + panel: Entity, + repo_input: Entity, + focus_handle: FocusHandle, +} + +impl GitCloneModal { + pub fn show(panel: Entity, window: &mut Window, cx: &mut Context) -> Self { + let repo_input = cx.new(|cx| { + let mut editor = Editor::single_line(window, cx); + editor.set_placeholder_text("Enter repository", cx); + editor + }); + let focus_handle = repo_input.focus_handle(cx); + + window.focus(&focus_handle); + + Self { + panel, + repo_input, + focus_handle, + } + } + + fn render_editor(&self, window: &Window, cx: &App) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let theme = cx.theme(); + + let text_style = TextStyle { + color: cx.theme().colors().text, + font_family: settings.buffer_font.family.clone(), + font_features: settings.buffer_font.features.clone(), + font_size: settings.buffer_font_size(cx).into(), + font_weight: settings.buffer_font.weight, + line_height: relative(settings.buffer_line_height.value()), + background_color: Some(theme.colors().editor_background), + ..Default::default() + }; + + let element = EditorElement::new( + &self.repo_input, + EditorStyle { + background: theme.colors().editor_background, + local_player: theme.players().local(), + text: text_style, + ..Default::default() + }, + ); + + div() + .rounded_md() + .p_1() + .border_1() + .border_color(theme.colors().border_variant) + .when( + self.repo_input + .focus_handle(cx) + .contains_focused(window, cx), + |this| this.border_color(theme.colors().border_focused), + ) + .child(element) + .bg(theme.colors().editor_background) + } +} + +impl Focusable for GitCloneModal { + fn focus_handle(&self, _: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for GitCloneModal { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .size_full() + .w(rems(34.)) + .elevation_3(cx) + .child(self.render_editor(window, cx)) + .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| { + cx.emit(DismissEvent); + })) + .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| { + let repo = this.repo_input.read(cx).text(cx); + this.panel.update(cx, |panel, cx| { + panel.git_clone(repo, window, cx); + }); + cx.emit(DismissEvent); + })) + } +} + +impl EventEmitter for GitCloneModal {} + +impl ModalView for GitCloneModal {} diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 9b0abb1537..1aae0b2f7e 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -987,7 +987,7 @@ pub struct InlayHintSettings { /// Default: false #[serde(default)] pub enabled: bool, - /// Global switch to toggle inline values on and off. + /// Global switch to toggle inline values on and off when debugging. /// /// Default: true #[serde(default = "default_true")] diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 01fc987816..5d48c833ab 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -441,6 +441,7 @@ impl GitStore { client.add_entity_request_handler(Self::handle_blame_buffer); client.add_entity_message_handler(Self::handle_update_repository); client.add_entity_message_handler(Self::handle_remove_repository); + client.add_entity_request_handler(Self::handle_git_clone); } pub fn is_local(&self) -> bool { @@ -1464,6 +1465,45 @@ impl GitStore { } } + pub fn git_clone( + &self, + repo: String, + path: impl Into>, + cx: &App, + ) -> Task> { + let path = path.into(); + match &self.state { + GitStoreState::Local { fs, .. } => { + let fs = fs.clone(); + cx.background_executor() + .spawn(async move { fs.git_clone(&repo, &path).await }) + } + GitStoreState::Ssh { + upstream_client, + upstream_project_id, + .. + } => { + let request = upstream_client.request(proto::GitClone { + project_id: upstream_project_id.0, + abs_path: path.to_string_lossy().to_string(), + remote_repo: repo, + }); + + cx.background_spawn(async move { + let result = request.await?; + + match result.success { + true => Ok(()), + false => Err(anyhow!("Git Clone failed")), + } + }) + } + GitStoreState::Remote { .. } => { + Task::ready(Err(anyhow!("Git Clone isn't supported for remote users"))) + } + } + } + async fn handle_update_repository( this: Entity, envelope: TypedEnvelope, @@ -1550,6 +1590,22 @@ impl GitStore { Ok(proto::Ack {}) } + async fn handle_git_clone( + this: Entity, + envelope: TypedEnvelope, + cx: AsyncApp, + ) -> Result { + let path: Arc = PathBuf::from(envelope.payload.abs_path).into(); + let repo_name = envelope.payload.remote_repo; + let result = cx + .update(|cx| this.read(cx).git_clone(repo_name, path, cx))? + .await; + + Ok(proto::GitCloneResponse { + success: result.is_ok(), + }) + } + async fn handle_fetch( this: Entity, envelope: TypedEnvelope, diff --git a/crates/proto/proto/git.proto b/crates/proto/proto/git.proto index c32da9b110..f2c388a3a3 100644 --- a/crates/proto/proto/git.proto +++ b/crates/proto/proto/git.proto @@ -202,6 +202,16 @@ message GitInit { string fallback_branch_name = 3; } +message GitClone { + uint64 project_id = 1; + string abs_path = 2; + string remote_repo = 3; +} + +message GitCloneResponse { + bool success = 1; +} + message CheckForPushedCommits { uint64 project_id = 1; reserved 2; diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index bb97bd500a..856a793c2f 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -399,7 +399,10 @@ message Envelope { GetDefaultBranchResponse get_default_branch_response = 360; GetCrashFiles get_crash_files = 361; - GetCrashFilesResponse get_crash_files_response = 362; // current max + GetCrashFilesResponse get_crash_files_response = 362; + + GitClone git_clone = 363; + GitCloneResponse git_clone_response = 364; // current max } reserved 87 to 88; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 9edb041b4b..a5dd97661f 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -316,6 +316,8 @@ messages!( (PullWorkspaceDiagnostics, Background), (GetDefaultBranch, Background), (GetDefaultBranchResponse, Background), + (GitClone, Background), + (GitCloneResponse, Background) ); request_messages!( @@ -484,6 +486,7 @@ request_messages!( (GetDocumentDiagnostics, GetDocumentDiagnosticsResponse), (PullWorkspaceDiagnostics, Ack), (GetDefaultBranch, GetDefaultBranchResponse), + (GitClone, GitCloneResponse) ); entity_messages!( @@ -615,7 +618,8 @@ entity_messages!( LogToDebugConsole, GetDocumentDiagnostics, PullWorkspaceDiagnostics, - GetDefaultBranch + GetDefaultBranch, + GitClone ); entity_messages!( From 7965052757f2ce235eea72cf40d9d992f00b5527 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Mon, 11 Aug 2025 12:33:21 -0400 Subject: [PATCH 254/693] Make SwitchField component clickable from the keyboard when focused (#35830) Release Notes: - N/A --- crates/ui/src/components/toggle.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index 59c056859d..e5f28e3b25 100644 --- a/crates/ui/src/components/toggle.rs +++ b/crates/ui/src/components/toggle.rs @@ -420,7 +420,7 @@ pub struct Switch { id: ElementId, toggle_state: ToggleState, disabled: bool, - on_click: Option>, + on_click: Option>, label: Option, key_binding: Option, color: SwitchColor, @@ -459,7 +459,7 @@ impl Switch { mut self, handler: impl Fn(&ToggleState, &mut Window, &mut App) + 'static, ) -> Self { - self.on_click = Some(Box::new(handler)); + self.on_click = Some(Rc::new(handler)); self } @@ -513,10 +513,16 @@ impl RenderOnce for Switch { .when_some( self.tab_index.filter(|_| !self.disabled), |this, tab_index| { - this.tab_index(tab_index).focus(|mut style| { - style.border_color = Some(cx.theme().colors().border_focused); - style - }) + this.tab_index(tab_index) + .focus(|mut style| { + style.border_color = Some(cx.theme().colors().border_focused); + style + }) + .when_some(self.on_click.clone(), |this, on_click| { + this.on_click(move |_, window, cx| { + on_click(&self.toggle_state.inverse(), window, cx) + }) + }) }, ) .child( From 42bf5a17b969bd33335b18ad12e0773b07a198f6 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 11 Aug 2025 12:49:46 -0400 Subject: [PATCH 255/693] Delay rendering tool call diff editor until it has a revealed range (#35901) Release Notes: - N/A --- crates/acp_thread/src/diff.rs | 4 +++ crates/agent_ui/src/acp/thread_view.rs | 35 ++++++++++++++++---------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs index 9cc6271360..a2c2d6c322 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -174,6 +174,10 @@ impl Diff { buffer_text ) } + + pub fn has_revealed_range(&self, cx: &App) -> bool { + self.multibuffer().read(cx).excerpt_paths().next().is_some() + } } pub struct PendingDiff { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 2536612ece..32f9948d97 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1153,16 +1153,25 @@ impl AcpThreadView { ), }; - let needs_confirmation = match &tool_call.status { - ToolCallStatus::WaitingForConfirmation { .. } => true, - _ => tool_call - .content - .iter() - .any(|content| matches!(content, ToolCallContent::Diff(_))), - }; - - let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation; - let is_open = !is_collapsible || self.expanded_tool_calls.contains(&tool_call.id); + let needs_confirmation = matches!( + tool_call.status, + ToolCallStatus::WaitingForConfirmation { .. } + ); + let is_edit = matches!(tool_call.kind, acp::ToolKind::Edit); + let has_diff = tool_call + .content + .iter() + .any(|content| matches!(content, ToolCallContent::Diff { .. })); + let has_nonempty_diff = tool_call.content.iter().any(|content| match content { + ToolCallContent::Diff(diff) => diff.read(cx).has_revealed_range(cx), + _ => false, + }); + let is_collapsible = + !tool_call.content.is_empty() && !needs_confirmation && !is_edit && !has_diff; + let is_open = tool_call.content.is_empty() + || needs_confirmation + || has_nonempty_diff + || self.expanded_tool_calls.contains(&tool_call.id); let gradient_color = cx.theme().colors().panel_background; let gradient_overlay = { @@ -1180,7 +1189,7 @@ impl AcpThreadView { }; v_flex() - .when(needs_confirmation, |this| { + .when(needs_confirmation || is_edit || has_diff, |this| { this.rounded_lg() .border_1() .border_color(self.tool_card_border_color(cx)) @@ -1194,7 +1203,7 @@ impl AcpThreadView { .gap_1() .justify_between() .map(|this| { - if needs_confirmation { + if needs_confirmation || is_edit || has_diff { this.pl_2() .pr_1() .py_1() @@ -1271,7 +1280,7 @@ impl AcpThreadView { .child(self.render_markdown( tool_call.label.clone(), default_markdown_style( - needs_confirmation, + needs_confirmation || is_edit || has_diff, window, cx, ), From 39dfd52d041cf33f6270b1deebc854c749fb4a58 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 11 Aug 2025 18:50:24 +0200 Subject: [PATCH 256/693] python: Create DAP download directory sooner (#35986) Closes #35980 Release Notes: - Fixed Python Debug sessions not starting up when a session is started up for the first time. --- crates/dap_adapters/src/python.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 461ce6fbb3..a2bd934311 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -152,6 +152,9 @@ impl PythonDebugAdapter { maybe!(async move { let response = latest_release.filter(|response| response.status().is_success())?; + let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME); + std::fs::create_dir_all(&download_dir).ok()?; + let mut output = String::new(); response .into_body() From 76b95d4f671ac04b2af25385004dc58cff95ff72 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 11 Aug 2025 13:06:31 -0400 Subject: [PATCH 257/693] Try to diagnose memory access violation in Windows tests (#35926) Release Notes: - N/A --- .github/actions/run_tests_windows/action.yml | 163 ++++++++++++++++++- 1 file changed, 162 insertions(+), 1 deletion(-) diff --git a/.github/actions/run_tests_windows/action.yml b/.github/actions/run_tests_windows/action.yml index cbe95e82c1..e3e3b7142e 100644 --- a/.github/actions/run_tests_windows/action.yml +++ b/.github/actions/run_tests_windows/action.yml @@ -20,7 +20,168 @@ runs: with: node-version: "18" + - name: Configure crash dumps + shell: powershell + run: | + # Record the start time for this CI run + $runStartTime = Get-Date + $runStartTimeStr = $runStartTime.ToString("yyyy-MM-dd HH:mm:ss") + Write-Host "CI run started at: $runStartTimeStr" + + # Save the timestamp for later use + echo "CI_RUN_START_TIME=$($runStartTime.Ticks)" >> $env:GITHUB_ENV + + # Create crash dump directory in workspace (non-persistent) + $dumpPath = "$env:GITHUB_WORKSPACE\crash_dumps" + New-Item -ItemType Directory -Force -Path $dumpPath | Out-Null + + Write-Host "Setting up crash dump detection..." + Write-Host "Workspace dump path: $dumpPath" + + # Note: We're NOT modifying registry on stateful runners + # Instead, we'll check default Windows crash locations after tests + - name: Run tests shell: powershell working-directory: ${{ inputs.working-directory }} - run: cargo nextest run --workspace --no-fail-fast + run: | + $env:RUST_BACKTRACE = "full" + + # Enable Windows debugging features + $env:_NT_SYMBOL_PATH = "srv*https://msdl.microsoft.com/download/symbols" + + # .NET crash dump environment variables (ephemeral) + $env:COMPlus_DbgEnableMiniDump = "1" + $env:COMPlus_DbgMiniDumpType = "4" + $env:COMPlus_CreateDumpDiagnostics = "1" + + cargo nextest run --workspace --no-fail-fast + continue-on-error: true + + - name: Analyze crash dumps + if: always() + shell: powershell + run: | + Write-Host "Checking for crash dumps..." + + # Get the CI run start time from the environment + $runStartTime = [DateTime]::new([long]$env:CI_RUN_START_TIME) + Write-Host "Only analyzing dumps created after: $($runStartTime.ToString('yyyy-MM-dd HH:mm:ss'))" + + # Check all possible crash dump locations + $searchPaths = @( + "$env:GITHUB_WORKSPACE\crash_dumps", + "$env:LOCALAPPDATA\CrashDumps", + "$env:TEMP", + "$env:GITHUB_WORKSPACE", + "$env:USERPROFILE\AppData\Local\CrashDumps", + "C:\Windows\System32\config\systemprofile\AppData\Local\CrashDumps" + ) + + $dumps = @() + foreach ($path in $searchPaths) { + if (Test-Path $path) { + Write-Host "Searching in: $path" + $found = Get-ChildItem "$path\*.dmp" -ErrorAction SilentlyContinue | Where-Object { + $_.CreationTime -gt $runStartTime + } + if ($found) { + $dumps += $found + Write-Host " Found $($found.Count) dump(s) from this CI run" + } + } + } + + if ($dumps) { + Write-Host "Found $($dumps.Count) crash dump(s)" + + # Install debugging tools if not present + $cdbPath = "C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe" + if (-not (Test-Path $cdbPath)) { + Write-Host "Installing Windows Debugging Tools..." + $url = "https://go.microsoft.com/fwlink/?linkid=2237387" + Invoke-WebRequest -Uri $url -OutFile winsdksetup.exe + Start-Process -Wait winsdksetup.exe -ArgumentList "/features OptionId.WindowsDesktopDebuggers /quiet" + } + + foreach ($dump in $dumps) { + Write-Host "`n==================================" + Write-Host "Analyzing crash dump: $($dump.Name)" + Write-Host "Size: $([math]::Round($dump.Length / 1MB, 2)) MB" + Write-Host "Time: $($dump.CreationTime)" + Write-Host "==================================" + + # Set symbol path + $env:_NT_SYMBOL_PATH = "srv*C:\symbols*https://msdl.microsoft.com/download/symbols" + + # Run analysis + $analysisOutput = & $cdbPath -z $dump.FullName -c "!analyze -v; ~*k; lm; q" 2>&1 | Out-String + + # Extract key information + if ($analysisOutput -match "ExceptionCode:\s*([\w]+)") { + Write-Host "Exception Code: $($Matches[1])" + if ($Matches[1] -eq "c0000005") { + Write-Host "Exception Type: ACCESS VIOLATION" + } + } + + if ($analysisOutput -match "EXCEPTION_RECORD:\s*(.+)") { + Write-Host "Exception Record: $($Matches[1])" + } + + if ($analysisOutput -match "FAULTING_IP:\s*\n(.+)") { + Write-Host "Faulting Instruction: $($Matches[1])" + } + + # Save full analysis + $analysisFile = "$($dump.FullName).analysis.txt" + $analysisOutput | Out-File -FilePath $analysisFile + Write-Host "`nFull analysis saved to: $analysisFile" + + # Print stack trace section + Write-Host "`n--- Stack Trace Preview ---" + $stackSection = $analysisOutput -split "STACK_TEXT:" | Select-Object -Last 1 + $stackLines = $stackSection -split "`n" | Select-Object -First 20 + $stackLines | ForEach-Object { Write-Host $_ } + Write-Host "--- End Stack Trace Preview ---" + } + + Write-Host "`n⚠️ Crash dumps detected! Download the 'crash-dumps' artifact for detailed analysis." + + # Copy dumps to workspace for artifact upload + $artifactPath = "$env:GITHUB_WORKSPACE\crash_dumps_collected" + New-Item -ItemType Directory -Force -Path $artifactPath | Out-Null + + foreach ($dump in $dumps) { + $destName = "$($dump.Directory.Name)_$($dump.Name)" + Copy-Item $dump.FullName -Destination "$artifactPath\$destName" + if (Test-Path "$($dump.FullName).analysis.txt") { + Copy-Item "$($dump.FullName).analysis.txt" -Destination "$artifactPath\$destName.analysis.txt" + } + } + + Write-Host "Copied $($dumps.Count) dump(s) to artifact directory" + } else { + Write-Host "No crash dumps from this CI run found" + } + + - name: Upload crash dumps + if: always() + uses: actions/upload-artifact@v4 + with: + name: crash-dumps-${{ github.run_id }}-${{ github.run_attempt }} + path: | + crash_dumps_collected/*.dmp + crash_dumps_collected/*.txt + if-no-files-found: ignore + retention-days: 7 + + - name: Check test results + shell: powershell + working-directory: ${{ inputs.working-directory }} + run: | + # Re-check test results to fail the job if tests failed + if ($LASTEXITCODE -ne 0) { + Write-Host "Tests failed with exit code: $LASTEXITCODE" + exit $LASTEXITCODE + } From 56c4992b9ac5c63534fb8cf63fb6536d9abe9a0f Mon Sep 17 00:00:00 2001 From: localcc Date: Mon, 11 Aug 2025 19:17:48 +0200 Subject: [PATCH 258/693] Fix underline flickering (#35989) Closes #35559 Release Notes: - Fixed underline flickering --- crates/gpui/src/scene.rs | 2 +- crates/gpui/src/window.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index c527dfe750..758d06e597 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -476,7 +476,7 @@ pub(crate) struct Underline { pub content_mask: ContentMask, pub color: Hsla, pub thickness: ScaledPixels, - pub wavy: bool, + pub wavy: u32, } impl From for Primitive { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 3a430b806d..c0ffd34a0d 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -2814,7 +2814,7 @@ impl Window { content_mask: content_mask.scale(scale_factor), color: style.color.unwrap_or_default().opacity(element_opacity), thickness: style.thickness.scale(scale_factor), - wavy: style.wavy, + wavy: if style.wavy { 1 } else { 0 }, }); } @@ -2845,7 +2845,7 @@ impl Window { content_mask: content_mask.scale(scale_factor), thickness: style.thickness.scale(scale_factor), color: style.color.unwrap_or_default().opacity(opacity), - wavy: false, + wavy: 0, }); } From 365b5aa31d606f8ecac440de98a81f405f751d67 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 11 Aug 2025 19:22:19 +0200 Subject: [PATCH 259/693] Centralize `always_allow` logic when authorizing agent2 tools (#35988) Release Notes: - N/A --------- Co-authored-by: Cole Miller Co-authored-by: Bennet Bo Fenner Co-authored-by: Agus Zubiaga Co-authored-by: Ben Brandt --- crates/agent2/src/tests/mod.rs | 93 ++++++++++++++++++++++- crates/agent2/src/tests/test_tools.rs | 4 +- crates/agent2/src/thread.rs | 40 +++++++--- crates/agent2/src/tools/edit_file_tool.rs | 16 ++-- crates/agent2/src/tools/open_tool.rs | 2 +- crates/agent2/src/tools/terminal_tool.rs | 18 +---- crates/fs/src/fs.rs | 3 + 7 files changed, 136 insertions(+), 40 deletions(-) diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index b47816f35c..d6aaddf2c2 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -4,9 +4,11 @@ use action_log::ActionLog; use agent_client_protocol::{self as acp}; use anyhow::Result; use client::{Client, UserStore}; -use fs::FakeFs; +use fs::{FakeFs, Fs}; use futures::channel::mpsc::UnboundedReceiver; -use gpui::{AppContext, Entity, Task, TestAppContext, http_client::FakeHttpClient}; +use gpui::{ + App, AppContext, Entity, Task, TestAppContext, UpdateGlobal, http_client::FakeHttpClient, +}; use indoc::indoc; use language_model::{ LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, @@ -19,6 +21,7 @@ use reqwest_client::ReqwestClient; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::json; +use settings::SettingsStore; use smol::stream::StreamExt; use std::{cell::RefCell, path::Path, rc::Rc, sync::Arc, time::Duration}; use util::path; @@ -282,6 +285,63 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { }) ] ); + + // Simulate yet another tool call. + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + LanguageModelToolUse { + id: "tool_id_3".into(), + name: ToolRequiringPermission.name().into(), + raw_input: "{}".into(), + input: json!({}), + is_input_complete: true, + }, + )); + fake_model.end_last_completion_stream(); + + // Respond by always allowing tools. + let tool_call_auth_3 = next_tool_call_authorization(&mut events).await; + tool_call_auth_3 + .response + .send(tool_call_auth_3.options[0].id.clone()) + .unwrap(); + cx.run_until_parked(); + let completion = fake_model.pending_completions().pop().unwrap(); + let message = completion.messages.last().unwrap(); + assert_eq!( + message.content, + vec![MessageContent::ToolResult(LanguageModelToolResult { + tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(), + tool_name: ToolRequiringPermission.name().into(), + is_error: false, + content: "Allowed".into(), + output: Some("Allowed".into()) + })] + ); + + // Simulate a final tool call, ensuring we don't trigger authorization. + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + LanguageModelToolUse { + id: "tool_id_4".into(), + name: ToolRequiringPermission.name().into(), + raw_input: "{}".into(), + input: json!({}), + is_input_complete: true, + }, + )); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + let completion = fake_model.pending_completions().pop().unwrap(); + let message = completion.messages.last().unwrap(); + assert_eq!( + message.content, + vec![MessageContent::ToolResult(LanguageModelToolResult { + tool_use_id: "tool_id_4".into(), + tool_name: ToolRequiringPermission.name().into(), + is_error: false, + content: "Allowed".into(), + output: Some("Allowed".into()) + })] + ); } #[gpui::test] @@ -773,13 +833,17 @@ impl TestModel { async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { cx.executor().allow_parking(); + + let fs = FakeFs::new(cx.background_executor.clone()); + cx.update(|cx| { settings::init(cx); + watch_settings(fs.clone(), cx); Project::init_settings(cx); + agent_settings::init(cx); }); let templates = Templates::new(); - let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree(path!("/test"), json!({})).await; let project = Project::test(fs, [path!("/test").as_ref()], cx).await; @@ -841,3 +905,26 @@ fn init_logger() { env_logger::init(); } } + +fn watch_settings(fs: Arc, cx: &mut App) { + let fs = fs.clone(); + cx.spawn({ + async move |cx| { + let mut new_settings_content_rx = settings::watch_config_file( + cx.background_executor(), + fs, + paths::settings_file().clone(), + ); + + while let Some(new_settings_content) = new_settings_content_rx.next().await { + cx.update(|cx| { + SettingsStore::update_global(cx, |settings, cx| { + settings.set_user_settings(&new_settings_content, cx) + }) + }) + .ok(); + } + } + }) + .detach(); +} diff --git a/crates/agent2/src/tests/test_tools.rs b/crates/agent2/src/tests/test_tools.rs index d06614f3fe..7c7b81f52f 100644 --- a/crates/agent2/src/tests/test_tools.rs +++ b/crates/agent2/src/tests/test_tools.rs @@ -110,9 +110,9 @@ impl AgentTool for ToolRequiringPermission { event_stream: ToolCallEventStream, cx: &mut App, ) -> Task> { - let auth_check = event_stream.authorize("Authorize?".into()); + let authorize = event_stream.authorize("Authorize?", cx); cx.foreground_executor().spawn(async move { - auth_check.await?; + authorize.await?; Ok("Allowed".to_string()) }) } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index dd8e5476ab..23a0f7972d 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1,10 +1,12 @@ use crate::{SystemPromptTemplate, Template, Templates}; use action_log::ActionLog; use agent_client_protocol as acp; +use agent_settings::AgentSettings; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::adapt_schema_to_format; use cloud_llm_client::{CompletionIntent, CompletionMode}; use collections::HashMap; +use fs::Fs; use futures::{ channel::{mpsc, oneshot}, stream::FuturesUnordered, @@ -21,8 +23,9 @@ use project::Project; use prompt_store::ProjectContext; use schemars::{JsonSchema, Schema}; use serde::{Deserialize, Serialize}; +use settings::{Settings, update_settings_file}; use smol::stream::StreamExt; -use std::{cell::RefCell, collections::BTreeMap, fmt::Write, future::Future, rc::Rc, sync::Arc}; +use std::{cell::RefCell, collections::BTreeMap, fmt::Write, rc::Rc, sync::Arc}; use util::{ResultExt, markdown::MarkdownCodeBlock}; #[derive(Debug, Clone)] @@ -506,8 +509,9 @@ impl Thread { })); }; + let fs = self.project.read(cx).fs().clone(); let tool_event_stream = - ToolCallEventStream::new(&tool_use, tool.kind(), event_stream.clone()); + ToolCallEventStream::new(&tool_use, tool.kind(), event_stream.clone(), Some(fs)); tool_event_stream.update_fields(acp::ToolCallUpdateFields { status: Some(acp::ToolCallStatus::InProgress), ..Default::default() @@ -884,6 +888,7 @@ pub struct ToolCallEventStream { kind: acp::ToolKind, input: serde_json::Value, stream: AgentResponseEventStream, + fs: Option>, } impl ToolCallEventStream { @@ -902,6 +907,7 @@ impl ToolCallEventStream { }, acp::ToolKind::Other, AgentResponseEventStream(events_tx), + None, ); (stream, ToolCallEventStreamReceiver(events_rx)) @@ -911,12 +917,14 @@ impl ToolCallEventStream { tool_use: &LanguageModelToolUse, kind: acp::ToolKind, stream: AgentResponseEventStream, + fs: Option>, ) -> Self { Self { tool_use_id: tool_use.id.clone(), kind, input: tool_use.input.clone(), stream, + fs, } } @@ -951,7 +959,11 @@ impl ToolCallEventStream { .ok(); } - pub fn authorize(&self, title: String) -> impl use<> + Future> { + pub fn authorize(&self, title: impl Into, cx: &mut App) -> Task> { + if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions { + return Task::ready(Ok(())); + } + let (response_tx, response_rx) = oneshot::channel(); self.stream .0 @@ -959,7 +971,7 @@ impl ToolCallEventStream { ToolCallAuthorization { tool_call: AgentResponseEventStream::initial_tool_call( &self.tool_use_id, - title, + title.into(), self.kind.clone(), self.input.clone(), ), @@ -984,12 +996,22 @@ impl ToolCallEventStream { }, ))) .ok(); - async move { - match response_rx.await?.0.as_ref() { - "allow" | "always_allow" => Ok(()), - _ => Err(anyhow!("Permission to run tool denied by user")), + let fs = self.fs.clone(); + cx.spawn(async move |cx| match response_rx.await?.0.as_ref() { + "always_allow" => { + if let Some(fs) = fs.clone() { + cx.update(|cx| { + update_settings_file::(fs, cx, |settings, _| { + settings.set_always_allow_tool_actions(true); + }); + })?; + } + + Ok(()) } - } + "allow" => Ok(()), + _ => Err(anyhow!("Permission to run tool denied by user")), + }) } } diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index d9a4cdf8ba..88764d1953 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -133,7 +133,7 @@ impl EditFileTool { &self, input: &EditFileToolInput, event_stream: &ToolCallEventStream, - cx: &App, + cx: &mut App, ) -> Task> { if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions { return Task::ready(Ok(())); @@ -147,8 +147,9 @@ impl EditFileTool { .components() .any(|component| component.as_os_str() == local_settings_folder.as_os_str()) { - return cx.foreground_executor().spawn( - event_stream.authorize(format!("{} (local settings)", input.display_description)), + return event_stream.authorize( + format!("{} (local settings)", input.display_description), + cx, ); } @@ -156,9 +157,9 @@ impl EditFileTool { // so check for that edge case too. if let Ok(canonical_path) = std::fs::canonicalize(&input.path) { if canonical_path.starts_with(paths::config_dir()) { - return cx.foreground_executor().spawn( - event_stream - .authorize(format!("{} (global settings)", input.display_description)), + return event_stream.authorize( + format!("{} (global settings)", input.display_description), + cx, ); } } @@ -173,8 +174,7 @@ impl EditFileTool { if project_path.is_some() { Task::ready(Ok(())) } else { - cx.foreground_executor() - .spawn(event_stream.authorize(input.display_description.clone())) + event_stream.authorize(&input.display_description, cx) } } } diff --git a/crates/agent2/src/tools/open_tool.rs b/crates/agent2/src/tools/open_tool.rs index 0860b62a51..36420560c1 100644 --- a/crates/agent2/src/tools/open_tool.rs +++ b/crates/agent2/src/tools/open_tool.rs @@ -65,7 +65,7 @@ impl AgentTool for OpenTool { ) -> Task> { // If path_or_url turns out to be a path in the project, make it absolute. let abs_path = to_absolute_path(&input.path_or_url, self.project.clone(), cx); - let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())).to_string()); + let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx); cx.background_spawn(async move { authorize.await?; diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs index c0b34444dd..ecb855ac34 100644 --- a/crates/agent2/src/tools/terminal_tool.rs +++ b/crates/agent2/src/tools/terminal_tool.rs @@ -5,7 +5,6 @@ use gpui::{App, AppContext, Entity, SharedString, Task}; use project::{Project, terminals::TerminalKind}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::Settings; use std::{ path::{Path, PathBuf}, sync::Arc, @@ -61,21 +60,6 @@ impl TerminalTool { determine_shell: determine_shell.shared(), } } - - fn authorize( - &self, - input: &TerminalToolInput, - event_stream: &ToolCallEventStream, - cx: &App, - ) -> Task> { - if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions { - return Task::ready(Ok(())); - } - - // TODO: do we want to have a special title here? - cx.foreground_executor() - .spawn(event_stream.authorize(self.initial_title(Ok(input.clone())).to_string())) - } } impl AgentTool for TerminalTool { @@ -152,7 +136,7 @@ impl AgentTool for TerminalTool { env }); - let authorize = self.authorize(&input, &event_stream, cx); + let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx); cx.spawn({ async move |cx| { diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index af8fe129ab..a2b75ac6a7 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -2172,6 +2172,9 @@ impl Fs for FakeFs { async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> { self.simulate_random_delay().await; let path = normalize_path(path.as_path()); + if let Some(path) = path.parent() { + self.create_dir(path).await?; + } self.write_file_internal(path, data.into_bytes(), true)?; Ok(()) } From bb6ea2294430b96aacebb58c696d57f3e9ef8ba8 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 11 Aug 2025 19:24:48 +0200 Subject: [PATCH 260/693] agent2: Port more tools (#35987) Release Notes: - N/A --------- Co-authored-by: Ben Brandt Co-authored-by: Antonio Scandurra --- Cargo.lock | 2 + crates/action_log/src/action_log.rs | 16 -- crates/agent2/Cargo.toml | 2 + crates/agent2/src/agent.rs | 8 +- crates/agent2/src/tools.rs | 4 + crates/agent2/src/tools/diagnostics_tool.rs | 177 ++++++++++++++++++ crates/agent2/src/tools/fetch_tool.rs | 161 ++++++++++++++++ .../assistant_tools/src/diagnostics_tool.rs | 6 +- 8 files changed, 352 insertions(+), 24 deletions(-) create mode 100644 crates/agent2/src/tools/diagnostics_tool.rs create mode 100644 crates/agent2/src/tools/fetch_tool.rs diff --git a/Cargo.lock b/Cargo.lock index 7b5e82a312..8a3e319a57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -204,6 +204,8 @@ dependencies = [ "gpui", "gpui_tokio", "handlebars 4.5.0", + "html_to_markdown", + "http_client", "indoc", "itertools 0.14.0", "language", diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs index 025aba060d..c4eaffc228 100644 --- a/crates/action_log/src/action_log.rs +++ b/crates/action_log/src/action_log.rs @@ -17,8 +17,6 @@ use util::{ pub struct ActionLog { /// Buffers that we want to notify the model about when they change. tracked_buffers: BTreeMap, TrackedBuffer>, - /// Has the model edited a file since it last checked diagnostics? - edited_since_project_diagnostics_check: bool, /// The project this action log is associated with project: Entity, } @@ -28,7 +26,6 @@ impl ActionLog { pub fn new(project: Entity) -> Self { Self { tracked_buffers: BTreeMap::default(), - edited_since_project_diagnostics_check: false, project, } } @@ -37,16 +34,6 @@ impl ActionLog { &self.project } - /// Notifies a diagnostics check - pub fn checked_project_diagnostics(&mut self) { - self.edited_since_project_diagnostics_check = false; - } - - /// Returns true if any files have been edited since the last project diagnostics check - pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool { - self.edited_since_project_diagnostics_check - } - pub fn latest_snapshot(&self, buffer: &Entity) -> Option { Some(self.tracked_buffers.get(buffer)?.snapshot.clone()) } @@ -543,14 +530,11 @@ impl ActionLog { /// Mark a buffer as created by agent, so we can refresh it in the context pub fn buffer_created(&mut self, buffer: Entity, cx: &mut Context) { - self.edited_since_project_diagnostics_check = true; self.track_buffer_internal(buffer.clone(), true, cx); } /// Mark a buffer as edited by agent, so we can refresh it in the context pub fn buffer_edited(&mut self, buffer: Entity, cx: &mut Context) { - self.edited_since_project_diagnostics_check = true; - let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx); if let TrackedBufferStatus::Deleted = tracked_buffer.status { tracked_buffer.status = TrackedBufferStatus::Modified; diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 622b08016a..7ee48aca04 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -27,6 +27,8 @@ fs.workspace = true futures.workspace = true gpui.workspace = true handlebars = { workspace = true, features = ["rust-embed"] } +html_to_markdown.workspace = true +http_client.workspace = true indoc.workspace = true itertools.workspace = true language.workspace = true diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index b1cefd2864..66893f49f9 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,8 +1,8 @@ use crate::{AgentResponseEvent, Thread, templates::Templates}; use crate::{ - CopyPathTool, CreateDirectoryTool, EditFileTool, FindPathTool, GrepTool, ListDirectoryTool, - MovePathTool, NowTool, OpenTool, ReadFileTool, TerminalTool, ThinkingTool, - ToolCallAuthorization, WebSearchTool, + CopyPathTool, CreateDirectoryTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, + GrepTool, ListDirectoryTool, MovePathTool, NowTool, OpenTool, ReadFileTool, TerminalTool, + ThinkingTool, ToolCallAuthorization, WebSearchTool, }; use acp_thread::ModelSelector; use agent_client_protocol as acp; @@ -420,11 +420,13 @@ impl acp_thread::AgentConnection for NativeAgentConnection { let mut thread = Thread::new(project.clone(), agent.project_context.clone(), action_log.clone(), agent.templates.clone(), default_model); thread.add_tool(CreateDirectoryTool::new(project.clone())); thread.add_tool(CopyPathTool::new(project.clone())); + thread.add_tool(DiagnosticsTool::new(project.clone())); thread.add_tool(MovePathTool::new(project.clone())); thread.add_tool(ListDirectoryTool::new(project.clone())); thread.add_tool(OpenTool::new(project.clone())); thread.add_tool(ThinkingTool); thread.add_tool(FindPathTool::new(project.clone())); + thread.add_tool(FetchTool::new(project.read(cx).client().http_client())); thread.add_tool(GrepTool::new(project.clone())); thread.add_tool(ReadFileTool::new(project.clone(), action_log)); thread.add_tool(EditFileTool::new(cx.entity())); diff --git a/crates/agent2/src/tools.rs b/crates/agent2/src/tools.rs index 29ba6780b8..8896b14538 100644 --- a/crates/agent2/src/tools.rs +++ b/crates/agent2/src/tools.rs @@ -1,7 +1,9 @@ mod copy_path_tool; mod create_directory_tool; mod delete_path_tool; +mod diagnostics_tool; mod edit_file_tool; +mod fetch_tool; mod find_path_tool; mod grep_tool; mod list_directory_tool; @@ -16,7 +18,9 @@ mod web_search_tool; pub use copy_path_tool::*; pub use create_directory_tool::*; pub use delete_path_tool::*; +pub use diagnostics_tool::*; pub use edit_file_tool::*; +pub use fetch_tool::*; pub use find_path_tool::*; pub use grep_tool::*; pub use list_directory_tool::*; diff --git a/crates/agent2/src/tools/diagnostics_tool.rs b/crates/agent2/src/tools/diagnostics_tool.rs new file mode 100644 index 0000000000..bd0b20df5a --- /dev/null +++ b/crates/agent2/src/tools/diagnostics_tool.rs @@ -0,0 +1,177 @@ +use crate::{AgentTool, ToolCallEventStream}; +use agent_client_protocol as acp; +use anyhow::{Result, anyhow}; +use gpui::{App, Entity, Task}; +use language::{DiagnosticSeverity, OffsetRangeExt}; +use project::Project; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::{fmt::Write, path::Path, sync::Arc}; +use ui::SharedString; +use util::markdown::MarkdownInlineCode; + +/// Get errors and warnings for the project or a specific file. +/// +/// This tool can be invoked after a series of edits to determine if further edits are necessary, or if the user asks to fix errors or warnings in their codebase. +/// +/// When a path is provided, shows all diagnostics for that specific file. +/// When no path is provided, shows a summary of error and warning counts for all files in the project. +/// +/// +/// To get diagnostics for a specific file: +/// { +/// "path": "src/main.rs" +/// } +/// +/// To get a project-wide diagnostic summary: +/// {} +/// +/// +/// +/// - If you think you can fix a diagnostic, make 1-2 attempts and then give up. +/// - Don't remove code you've generated just because you can't fix an error. The user can help you fix it. +/// +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct DiagnosticsToolInput { + /// The path to get diagnostics for. If not provided, returns a project-wide summary. + /// + /// This path should never be absolute, and the first component + /// of the path should always be a root directory in a project. + /// + /// + /// If the project has the following root directories: + /// + /// - lorem + /// - ipsum + /// + /// If you wanna access diagnostics for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`. + /// + pub path: Option, +} + +pub struct DiagnosticsTool { + project: Entity, +} + +impl DiagnosticsTool { + pub fn new(project: Entity) -> Self { + Self { project } + } +} + +impl AgentTool for DiagnosticsTool { + type Input = DiagnosticsToolInput; + type Output = String; + + fn name(&self) -> SharedString { + "diagnostics".into() + } + + fn kind(&self) -> acp::ToolKind { + acp::ToolKind::Read + } + + fn initial_title(&self, input: Result) -> SharedString { + if let Some(path) = input.ok().and_then(|input| match input.path { + Some(path) if !path.is_empty() => Some(path), + _ => None, + }) { + format!("Check diagnostics for {}", MarkdownInlineCode(&path)).into() + } else { + "Check project diagnostics".into() + } + } + + fn run( + self: Arc, + input: Self::Input, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> { + match input.path { + Some(path) if !path.is_empty() => { + let Some(project_path) = self.project.read(cx).find_project_path(&path, cx) else { + return Task::ready(Err(anyhow!("Could not find path {path} in project",))); + }; + + let buffer = self + .project + .update(cx, |project, cx| project.open_buffer(project_path, cx)); + + cx.spawn(async move |cx| { + let mut output = String::new(); + let buffer = buffer.await?; + let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; + + for (_, group) in snapshot.diagnostic_groups(None) { + let entry = &group.entries[group.primary_ix]; + let range = entry.range.to_point(&snapshot); + let severity = match entry.diagnostic.severity { + DiagnosticSeverity::ERROR => "error", + DiagnosticSeverity::WARNING => "warning", + _ => continue, + }; + + writeln!( + output, + "{} at line {}: {}", + severity, + range.start.row + 1, + entry.diagnostic.message + )?; + + event_stream.update_fields(acp::ToolCallUpdateFields { + content: Some(vec![output.clone().into()]), + ..Default::default() + }); + } + + if output.is_empty() { + Ok("File doesn't have errors or warnings!".to_string()) + } else { + Ok(output) + } + }) + } + _ => { + let project = self.project.read(cx); + let mut output = String::new(); + let mut has_diagnostics = false; + + for (project_path, _, summary) in project.diagnostic_summaries(true, cx) { + if summary.error_count > 0 || summary.warning_count > 0 { + let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx) + else { + continue; + }; + + has_diagnostics = true; + output.push_str(&format!( + "{}: {} error(s), {} warning(s)\n", + Path::new(worktree.read(cx).root_name()) + .join(project_path.path) + .display(), + summary.error_count, + summary.warning_count + )); + } + } + + if has_diagnostics { + event_stream.update_fields(acp::ToolCallUpdateFields { + content: Some(vec![output.clone().into()]), + ..Default::default() + }); + Task::ready(Ok(output)) + } else { + let text = "No errors or warnings found in the project."; + event_stream.update_fields(acp::ToolCallUpdateFields { + content: Some(vec![text.into()]), + ..Default::default() + }); + Task::ready(Ok(text.into())) + } + } + } + } +} diff --git a/crates/agent2/src/tools/fetch_tool.rs b/crates/agent2/src/tools/fetch_tool.rs new file mode 100644 index 0000000000..7f3752843c --- /dev/null +++ b/crates/agent2/src/tools/fetch_tool.rs @@ -0,0 +1,161 @@ +use std::rc::Rc; +use std::sync::Arc; +use std::{borrow::Cow, cell::RefCell}; + +use agent_client_protocol as acp; +use anyhow::{Context as _, Result, bail}; +use futures::AsyncReadExt as _; +use gpui::{App, AppContext as _, Task}; +use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown}; +use http_client::{AsyncBody, HttpClientWithUrl}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use ui::SharedString; +use util::markdown::MarkdownEscaped; + +use crate::{AgentTool, ToolCallEventStream}; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +enum ContentType { + Html, + Plaintext, + Json, +} + +/// Fetches a URL and returns the content as Markdown. +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct FetchToolInput { + /// The URL to fetch. + url: String, +} + +pub struct FetchTool { + http_client: Arc, +} + +impl FetchTool { + pub fn new(http_client: Arc) -> Self { + Self { http_client } + } + + async fn build_message(http_client: Arc, url: &str) -> Result { + let url = if !url.starts_with("https://") && !url.starts_with("http://") { + Cow::Owned(format!("https://{url}")) + } else { + Cow::Borrowed(url) + }; + + let mut response = http_client.get(&url, AsyncBody::default(), true).await?; + + let mut body = Vec::new(); + response + .body_mut() + .read_to_end(&mut body) + .await + .context("error reading response body")?; + + if response.status().is_client_error() { + let text = String::from_utf8_lossy(body.as_slice()); + bail!( + "status error {}, response: {text:?}", + response.status().as_u16() + ); + } + + let Some(content_type) = response.headers().get("content-type") else { + bail!("missing Content-Type header"); + }; + let content_type = content_type + .to_str() + .context("invalid Content-Type header")?; + + let content_type = if content_type.starts_with("text/plain") { + ContentType::Plaintext + } else if content_type.starts_with("application/json") { + ContentType::Json + } else { + ContentType::Html + }; + + match content_type { + ContentType::Html => { + let mut handlers: Vec = vec![ + Rc::new(RefCell::new(markdown::WebpageChromeRemover)), + Rc::new(RefCell::new(markdown::ParagraphHandler)), + Rc::new(RefCell::new(markdown::HeadingHandler)), + Rc::new(RefCell::new(markdown::ListHandler)), + Rc::new(RefCell::new(markdown::TableHandler::new())), + Rc::new(RefCell::new(markdown::StyledTextHandler)), + ]; + if url.contains("wikipedia.org") { + use html_to_markdown::structure::wikipedia; + + handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover))); + handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler))); + handlers.push(Rc::new( + RefCell::new(wikipedia::WikipediaCodeHandler::new()), + )); + } else { + handlers.push(Rc::new(RefCell::new(markdown::CodeHandler))); + } + + convert_html_to_markdown(&body[..], &mut handlers) + } + ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()), + ContentType::Json => { + let json: serde_json::Value = serde_json::from_slice(&body)?; + + Ok(format!( + "```json\n{}\n```", + serde_json::to_string_pretty(&json)? + )) + } + } + } +} + +impl AgentTool for FetchTool { + type Input = FetchToolInput; + type Output = String; + + fn name(&self) -> SharedString { + "fetch".into() + } + + fn kind(&self) -> acp::ToolKind { + acp::ToolKind::Fetch + } + + fn initial_title(&self, input: Result) -> SharedString { + match input { + Ok(input) => format!("Fetch {}", MarkdownEscaped(&input.url)).into(), + Err(_) => "Fetch URL".into(), + } + } + + fn run( + self: Arc, + input: Self::Input, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> { + let text = cx.background_spawn({ + let http_client = self.http_client.clone(); + async move { Self::build_message(http_client, &input.url).await } + }); + + cx.foreground_executor().spawn(async move { + let text = text.await?; + if text.trim().is_empty() { + bail!("no textual content found"); + } + + event_stream.update_fields(acp::ToolCallUpdateFields { + content: Some(vec![text.clone().into()]), + ..Default::default() + }); + + Ok(text) + }) + } +} diff --git a/crates/assistant_tools/src/diagnostics_tool.rs b/crates/assistant_tools/src/diagnostics_tool.rs index bc479eb596..4ec794e127 100644 --- a/crates/assistant_tools/src/diagnostics_tool.rs +++ b/crates/assistant_tools/src/diagnostics_tool.rs @@ -86,7 +86,7 @@ impl Tool for DiagnosticsTool { input: serde_json::Value, _request: Arc, project: Entity, - action_log: Entity, + _action_log: Entity, _model: Arc, _window: Option, cx: &mut App, @@ -159,10 +159,6 @@ impl Tool for DiagnosticsTool { } } - action_log.update(cx, |action_log, _cx| { - action_log.checked_project_diagnostics(); - }); - if has_diagnostics { Task::ready(Ok(output.into())).into() } else { From 2c84e33b7b7a6e5338221f8bf1d5b60365060566 Mon Sep 17 00:00:00 2001 From: localcc Date: Mon, 11 Aug 2025 19:57:39 +0200 Subject: [PATCH 261/693] Fix icon padding (#35990) Release Notes: - N/A --- .../zed/resources/windows/app-icon-nightly.ico | Bin 149224 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/crates/zed/resources/windows/app-icon-nightly.ico b/crates/zed/resources/windows/app-icon-nightly.ico index 165e4ce1f7ccbff084fbbf8131fb3c8e27853f4b..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 149224 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

x8_y)-@rqrF+7QAFlcfxBWFdqUzM6QMXRK zB4!1n$3^?N2?yPthH%R@ePb5Un`Oo-KiAd6FhRn7N7=5Rs`MW3Ls?TNxJNhl9b!Ld zkahTB-sX!>{ghRFI! z7v@!TG#vakx4CtN^yzBJLvePmY!)697X9~y!}1~%hv4p?G9TWvg))0x31UiR((*X1 zm#)i|#~S5e(9FEBFLg!Rq3ZYE;ukB<-fjuhl8)Z{{QaIELi2t%Zv}bhL*~#Eglbqrf9fvPG~s% zlvyI^$jf)8E>d+>yaz%KSLti+RGF-Bo=N=pm!psFRh(nJY@*D|%^;X);U@L&a9Qk! zx$oyG^|efJ)8Ugpv?Wm_uDLuocME4?XY*0z8-Dr8=64&LO&%{etm3&vnb+X%{#B<| zmA-phcKPXbIlsj#AFYTq_g;Us_3ba21rfzQ`!`r}AK*FH^4+dbcK(kem(M<4`2E#a zy^@8-7f!jn_*7#qu~TY!?M)r~9z)Jmf)UBj8C$)-SH>;A7TK-$XW{#86=!;;p1<-T zU3!8+vD?F)t0&#~Zcu(eA*<_HY}V1O{}mrEeZGI`(tWwxepz4dXk)#RSN2VZ>saD} zPYD+*6#Z5S+Wx;=);)*2=wsNsxD~=P(iC9LzS*iDR9;>m&=bXJ#ZT2r`OD>;n&b#?$(}vwAbgModcARVV z`kuq*gw!-v;h70#mFutNtzH=6B-7{kxxt}5V*dV4iF1a}n*`P?EY)66kRx%Xa%!&F z%#HWYJ)^T zaUlQ6qtP>3KYloTz3_i+_#2y?g#o)w8>?Jp?$`5e+L@@pk}1)`p{YHQwNZ&B=mwLsiS=y(hl5>Af5c|)un%9Cb23fj z*Yy>58W;XOzrSPszi;M++wO(%9FIW_NC{nr*1m=;yI1J$mWMON%b<)o$Pph)_DFed1@E-%74m2j_0M za^QGhF%v^VO9?|$=e@8Pr`6gi`e|?8_^y5P_i{GFt}TIwm%7bQb&shny&?UIZ7Rzl zE|-==+XH`yovO*p_{tZvjpHGM;-NkLrmZJ(ZaAK{_&sM^+tN3iA2Q8Qzq9xAuC9zL zd%8pC9uxPmy&jZ#YoBMbheR9W<(M88qX^!c+249rhxbij6V=J&^}8o~vf}sKXaPSC z5yPjS{a87!ayTrxJ>lNu3;h2-G)~iNdL?&pHCv1(pK0q#x&6-nTC<Vsh zm(VDmH=A=c(xeKDzOpoI{Pm#A>BYO{mH))!4qe~(OLxxN>*pqBi%v61@@To6*BKt<< z`u(0*L$lYtqhYGEGgwZu#rP?v@=ock{L0ZH8Iu>i|7XX9KA&ISUvD>Q9=zgdpm@bF zxM7--ro_}KA-2!|{4uqTRZnCIZp%Go_xGiF+jIrq=B+<(WxRT_bbr`2DRJc(#%%{H zyE^aeDU|+yu>OPm5FT z`pq@#)UW_g+f|AWE?3>^VfeZz&%!6wJ>fH_jEAU!R>BjE#$oW$(e4FK9L|yJ3 zN$K-7^+`JKKGuCpzL+?7{l6EbHSep}uh5#1uc4XIYg?<@`f9D~+^H8trq^8P$d7o{ z%g4H=?YH67;4^!+%H03@GLU_Cl}1mLZ9&3m{bSizW%q_QEPl*f^XX>)!aToCI1==f&YbuFd4 z)lYW?XRm0hu6yscu>E66amI<83?@gJlcvtQ)c#&RUr9YJK|{c4gJfr+!PdnoCq6z% z;re?=D>yY#ZRK`Oj;oUeq$kCn5A1gk4Cm`F+%=W|3{mDywI-;*~tFw(gO3nE7sgJxqqzt{QidL`#;)TdtDrIpf~0G z^=9BGhHTK49zWMBP_FZTqp zjot?(-CMx=BE~U(c2HHV^Kv)u+J9S*J^ot%T0kWG->gr*Z$9N7J^o_31dGnm%qdHM zYu_bT~VyxSfn-wSNuTy1b&qvfe~%dE}c(y9F;Yc7L?% z5GcExtu@irVeS**+3%`5IBi}ok#m3cFmBy4W;@vvlTE`tSOlw{=PA8^Olh{^vwa{NG_;0sQ(eGDO-?$Xu!I_Idry{nP& z^mxMDYCTIf#=BQxZj^phXoF}S~=c2x3e>(K~&_eQRL+#3nn(6T2xB*yh}@VzP`Y@$-v0D zK$>Yq8pDx2%~RD^yYh=|OsVj1tXtmf!>5%wH}bZfq{?1{<-ZL-tMA!-F@$$Q_5!7E zVj^qO{H}%bcPm|4Y&rAj?P6KqGParbFIStsyt11^{Ouwh&XAWE^sJZdDi>|r`e(}B z-`Cxi^kr##y|vw0#XJA^AN4DruQPP5VN!Ybp8(Mi_b?~IRV#|qln?VF|+`#8|XOH1HL^|{CKe+oDH z95A?Xc*BI{J8rGKEdST7y=?MMhDJr3#?;k2l_i!2xUPDwp*#DJlVU{v!4z}B35QIN zUs@{>!f5WZHB?yiNHOz+rFNfruAO+uQU1YEGOxvE}wPTW$EV#v92%Y1RA@(MQ{W>Tem4Ny0Q9IPC#K_VB@Jt zw>@%0BVTEmv1D6aGJJOY`0;H&=Nn3{SDUUo*HA#O;^oi$$Jz1!)XrV^iaYf=hBqTS zz^E}e(6)i=RF@HxGOt7%qll1Gj8coFCfk|IJp%09FNG)QKGHDmP+IoMjO+cENtWfW z{Y!qZFOa*%n?L=q`TP9WSATKaxTaiB{W9%=*1JoLTNN8)RIW3oX+%UmX9?+f({%OM zn+o=j#*0gPw-xSr!*rwd*HJE(ox#s9+V6}wakbK@cITF`nELvAS@C-p@68OcwKeJ2 zsy3g#+o-qF(OhHe<^UcGC+)L0+4NGksfm2I*{D}$E_AC+OH8!t)a9p_IyZ+Q0Cbt_}Z zXZeSp<9~_Ree3Mc%Oy^tyto!nAKDy zcI1J}LP^#40zoc47vBjxytr87;4!!B`Q14?8<)m$zWDTQU$|?%$%7Xk0(~x9F1yQX zap6TV3+IwgpZdNvKWJL)_1m{?ZN>kF2&)TAn+guU$-L2Tf4lzT@uOwTo2IAiYR_Vb zliqM~&Ag~{YUOXwH^)e=-L_!SZH{?TU!+!vX*sv+R~&QHWOOyZzhj5|=bGbeDYF7U zM(L>D{SvbJ{?*Yiae=b~OYqL- zXAl6X@~fF=OuZi>W_^2!((doaPS5z< zv(cp?_uqmJ&fZlYau(zKUsw@v>(s zDC8Xe?eVj2?eBW2k2_2DoYPa7Jgqo6#4-II`&|83J8p<17}S=|J6P$~6E^$3n}IN6 z_fgX)dyd-NcQh8+8{~hwf9tb5pH|y82L4{yCpr6Tn{oudxnh%{@oR>Q1`!)CG zEiqiqNAu;C#W!cWf7vB+VM&%rsYr8}cv~UM?IWqnQzlEV-s^0h+#{&T`1#CsXN)$uU#7+ym$J%dQfI8p9kA%$2~Y(Ke@gJ*&$W`{tDQ zHxH?v{1Ydbqrk1)($ajidctX4fy$2e2}^JMU6hq#(=MLKvikIE-C!RFW%2Bg!#&a4 z_wi2^zs%K?9Pz|y7AQ&iwluvqctKt=y=4eM>+3IqDxV;;!uJ;DEcj;V`Y z-DR6EThlY=y-z6ZNQ{VhvFa9spC4!A1g5MV5yf%QDlF1(pIl=xU^t(8^vhmF)#|y6 z*1Z0D`kTPpl$z^zZ|kPtzwDw_y~4M8pXqV_S=+1i;w+zQ6+e5r`q-=Nmky8FUi>~X z;nUeKX4{^Yy;Zq=n?uhzZ|TKKwjCDH8}8oT7Ra&Up?dS2fYf5iDE5TxtGRzz4{o`g zIIra1!;>cs_kCFEzxsK5SANLHOY6cNU+%qc#Jj(jsV#_MveV)T6M~w&N?%tlt6b? z%T@06Ef#u@4~Na3>HK^5UPIoMZj<*d;ox-MwQSQHhtFU7?naa@kTPSpU%7f$%!R^- zj}HdsPu;%q)me3UPQRwx=Jg$`7_^h#Sl_;*81mpk=G+B*dWN;zqRLhWUC3SBVUThz zL0XZoUuoA%y{~4wKex4?s9mvcetK=;gsg`*D>wuXMGM<;vT;=YSZkSZe=&bF-_|25 zv<0`uu&rjZIDVi<=BJu@kWXS`_kybG#<(esbH&QonB%u{9o3z>=~ASz{#UgZN}b*U zN=Eqw6B;C=lz%0r#0(;%s1f z;%^;-HzuAfUq5HfhaA$UwQ9Xsp+yr*_y;PM>A)=j+$tc zczFHxdS=l%IrgiUMXJlQzI`GbSQOx8m8~COdim?UuYtYuo?G6i5Xd``wK(9YzLdc7 zmNkMRB|g!;?z<bKNcB)9f(U+~f9c#YC2+a*Qh%q%f_D+uZV1n@S zFRX{mjwrL7U(LR__gt??@^h7CO*yX?hzf{mKNMtm#K0F~o^D(JIi`2fX|`jmEfN#D z6^=A4pZ_oN&(Z(y^dB?J*KnPF%6P-UE%)ZsTQ3$f%q!`#{q;MDWe)cY$)g+kzDXX7 zJiw6Xs3#n<@%qzsD}wi{@}9MPd>h<$y5(IOHh=S{Zy{5UGQ_y=_DtSrb??lIn^Km` zxW25~K2O^3Kn9;m)=ASH#R6A0>6C`f+9p@?^+q|v2bulT;$BAuo%K4wvx(8<7thTC z5z`+#&nSHA-M1$CjBLAmwcOsF0V_WXhFq-NP<@5@ws>*O3dVvYcT<))rZ2PBA76ck z!DM5nZfNFFW@YJeM?i6tSRc>}Y9=-XEu-}eJsZk{&Rtjg2>fH7AnLO=* z-JgZ?kKX@f{$JGQF3X;BnzoxZ~b$$8tA(OSq<;UeRwK>b$(`XoJFR zL6gH5%jJuhf7`tIe=9uhz|CE}*FrvaJGSRJe9Pl6t&6hFTQ2(Y`?=*WBr3vqQ%fc0 zY27O0y~yGCkN541r#t`Fv7JAjo7VbBsd?c?<2l3PMKNo|tW2F}w~%nono zPM+&z>gBMdH1$d@S9RtU>w~O29&jYopDEJrj@-;*Eq*KR>CSDOjvcpUlPMRXNt@_=a^KUOFPJDjDH~)Ib;q`mk zN^1EVr#59puYdY^p&LV+Y{;W-#ea8LG(vL13ckKF{P!vTr>V`?f8P`5aWk=9xwSo< zXXSq1bDjzunq`Ih0xMl+S4BVE)p&=q{mSAG$GF$?fJ>%^f|N=QM7bwYEQslPhaDvsg9o=FS;W;<6nAhXPdhYCTUm zKYLq9Z_l#V5&W$w7yTDo9G<^m`Bb^euur15pKs|1sbJe|&mb~FooZ1)c|XU6}h$5&KrvP-^IkB^_lr&2Grqboj^SQ<6FzPYn#!g5Br zDPMa1LoK!})A{8SU-K^7YH9i2=Kj{}4NV(nzkhIcc0J2PNp^Pbn4Lu}mf0t)`>Z5q zx=si>aPy{A`Mb2&%bLGt9jUq}@c7K_;^h*8Z@-JO@cN0VzL9*O_sZ*|grbh&S@V*$ ztY2>x&%3xoQhE@a-3jgYa)l$WMLTEtI_`P7ytT>D zHT834hEGeo+^)W(NiLc>Uu$E-WH_GOoXQ{~Vv!@t6qL-RtbK9s={13KzwX@flWXmf z7vCGBZv20mpR{}K)^vO4_MQlqP>aQH^0Mn?duKf3EPpqr+37%$;K2@!)MFPPUh=B^ z{oEzc(JnFc)XR85g#{c-5?5|XGDs5PIl1La4s%#?{&$;8LYi_3^I~fa!+5$@*-W?J zCDz!s;!|vlnXZt4@dmeoE02@oW~o z8j~6K7EG(>>hygWn)UkA1FN##(N4S32MQMy}9sS@^46hq$uL=YLUH)ONYg z%-JJ-cW|^_)`SbatBTf^Pkr8X@wBt52)%D*ud=+qT zGM~7D*^sqKf+>h+#+v9ejaw1}&h1j@($rH0vY2b#c)CY?e)T@HZ!HoADw74)i)SQFKh|6x)8H|YJ(NpbxU=`` ztLWSM=1Io>6B0F#VL$E&EH1&8Lhz z9$2Z#C7ikH5FlZ|$G_l7vB5Q|XSd_d?K!%3c74y}$;;&4YV1EMGHCcDYjCwg8x z4m^HT&3G%%dF|8PH(=SJ?Eg*cdG7NR-I6=`ZxM0n*iEEpfXY?(Se3rnp{+45g+l8kQf6N&-ZCX4b^rY<8X^8mPjI&er|#2nnycRv4k z^}bn~`~qngE-$fw7f-hP?pb_~UsB+4$BeY(TDH}W9gj4ozRPi#Q~bR5q{qIxH;L!Vd6TqXtmR3vD#lNirk~LmLIrwQNVx7;)B*dE(dOl+vvAH(vsm)N9cr}%bf9YH!F{^8x+Hv=suWs6=xNzR}m%Eot*3GUgxP0TbG@IAE z3vd7IHJ6WRRrziozkb;=;Wd^C+qS4nxz5N<+GO4#5EH&(YU>HNI&CX{-OrJdJkRbH zpW9LUTg3d{zNJ6Ymy1VaE44ΠKz3mwEJf{tphjPapdaT&>C8mv6bhe|Oc#pZPbR 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�^VKc%wt|I~_- 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

Fe+Ermhr^929fhzR|+5E4MapgJBDS9Zbz|7}5Q zi9z$$pfwGkwF`WF|LwSW|2XjQZgA)0S9Ac4U2-GR9>{JGwiX24y~oL5#m@yA59hbx z=ZUrA)@-c`8VRLTIv!+}eXN|Zx&Kh%b zoHgU(IAhJtal(q5bFVEo&jJ@-{#X|tK7Kboeg+pleg-=p?CX_4MWe69TZ^O+cV8X>IV9L!YV9vuS zV8+eKZ^OgM>&VN^=_km`=*A}i9zO@&#|*lw5#(Qd*cN=BAt(4AL(n~iAPkx-0AcVL zFAo>!&TQ~Kg`ja^(A|KbHFzf6oD62*y9GHIY`8hW{BKfu7i48jcz42-nEAhSUjWEaRzklmnh3IPTN@JW{NaS9g9aSBks7}O_M zVi#bLW9Mg(WaDNKW8-8HVP$6!W(D=9*cgOaSs6rFSsBFHSQw;XeQxM^S(5Caa}szM zRM-T$<=FY$CE0nh#M!tCMAEZt0N?93vZUpGu z81NY(?CcCe94w$dPJtvJXpb-Ve?wW>{|O!*|HJGZ{s-GT{EzhT_#f-%_dn3l;eV)u zJ-E-J2HFSA!Cn9wPXX;8mf&P9l;a1Tlfd=g)kyEZv$ppCG;i*|CwP1R z54N}a?`>@S-+&Kvb_36UWe$!)MGkfb=>E$BRRPHUvOrtQ{~@+k|NV`P!R?ux;NbtB zI@;j<1uh!u;C-*4_L>GKM}ZnA2ZJ&fJA*1Wdx4H1Xm2d{|0pNB|H-~y|Dzlo{+GqX z{;!CN{O_u)^xsNa;=i4Y^nWXU9`IUK9WKrSElv&wO>TAuZ61z7Q2h>SS0#D4{Lc;w z_@5UN@;}4V>wkf-&;QEMp#QE4vi~hXx97YBnb4+n!jFGqo`sNjEF zLH_^Qem?&*ygmOH1PA@k_xJsu>*4;t$jjq@MUdZr4_PVjdTr2Jc4Ka?0s}5iQ2UO- zn2$3Hw1){)p9X1b{m=9F`=90M@jucemu>V6Yb8 zVzB1t;I`oBo^C55@WWn2@V~9F;D39NIw3*u8c26=y(RSDRY>5!lc3-aJ6_)DHr(vo zHawtufs?_KkCVZQmy6qim)qT(mpjXhm%G4}hr7^(hpWJhhpWJphpW((hqJ(%hbzmD zo5$Uio1fbjJO{?XV8P1;nrjDR(EOMw4>yA`4;O Date: Mon, 11 Aug 2025 15:25:18 -0400 Subject: [PATCH 262/693] Add windows issue template (#35998) Release Notes: - N/A --- .../ISSUE_TEMPLATE/07_bug_windows_alpha.yml | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml diff --git a/.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml b/.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml new file mode 100644 index 0000000000..bf39560a3c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml @@ -0,0 +1,35 @@ +name: Bug Report (Windows) +description: Zed Windows-Related Bugs +type: "Bug" +labels: ["windows"] +title: "Windows: " +body: + - type: textarea + attributes: + label: Summary + description: Describe the bug with a one line summary, and provide detailed reproduction steps + value: | + + SUMMARY_SENTENCE_HERE + + ### Description + + Steps to trigger the problem: + 1. + 2. + 3. + + **Expected Behavior**: + **Actual Behavior**: + + validations: + required: true + - type: textarea + id: environment + attributes: + label: Zed Version and System Specs + description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"' + placeholder: | + Output of "zed: copy system specs into clipboard" + validations: + required: true From 094e878ccf639968c525294cc02f207061af88c5 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 11 Aug 2025 17:50:47 -0300 Subject: [PATCH 263/693] agent2: Refine terminal tool call display (#35984) Release Notes: - N/A --- crates/acp_thread/src/terminal.rs | 10 +- crates/agent_ui/src/acp/thread_view.rs | 339 ++++++++++++++++++++++--- 2 files changed, 309 insertions(+), 40 deletions(-) diff --git a/crates/acp_thread/src/terminal.rs b/crates/acp_thread/src/terminal.rs index b800873737..41d7fb89bb 100644 --- a/crates/acp_thread/src/terminal.rs +++ b/crates/acp_thread/src/terminal.rs @@ -29,8 +29,14 @@ impl Terminal { cx: &mut Context, ) -> Self { Self { - command: cx - .new(|cx| Markdown::new(command.into(), Some(language_registry.clone()), None, cx)), + command: cx.new(|cx| { + Markdown::new( + format!("```\n{}\n```", command).into(), + Some(language_registry.clone()), + None, + cx, + ) + }), working_dir, terminal, started_at: Instant::now(), diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 32f9948d97..f37deac26e 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -38,7 +38,7 @@ use theme::ThemeSettings; use ui::{ Disclosure, Divider, DividerColor, KeyBinding, Scrollbar, ScrollbarState, Tooltip, prelude::*, }; -use util::ResultExt; +use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, Workspace}; use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage}; @@ -75,6 +75,7 @@ pub struct AcpThreadView { edits_expanded: bool, plan_expanded: bool, editor_expanded: bool, + terminal_expanded: bool, message_history: Rc>>>, _cancel_task: Option>, _subscriptions: [Subscription; 1], @@ -200,6 +201,7 @@ impl AcpThreadView { edits_expanded: false, plan_expanded: false, editor_expanded: false, + terminal_expanded: true, message_history, _subscriptions: [subscription], _cancel_task: None, @@ -768,7 +770,7 @@ impl AcpThreadView { window, cx, ); - view.set_embedded_mode(None, cx); + view.set_embedded_mode(Some(1000), cx); view }); @@ -914,17 +916,26 @@ impl AcpThreadView { .child(message_body) .into_any() } - AgentThreadEntry::ToolCall(tool_call) => div() - .w_full() - .py_1p5() - .px_5() - .child(self.render_tool_call(index, tool_call, window, cx)) - .into_any(), + AgentThreadEntry::ToolCall(tool_call) => { + let has_terminals = tool_call.terminals().next().is_some(); + + div().w_full().py_1p5().px_5().map(|this| { + if has_terminals { + this.children(tool_call.terminals().map(|terminal| { + self.render_terminal_tool_call(terminal, tool_call, window, cx) + })) + } else { + this.child(self.render_tool_call(index, tool_call, window, cx)) + } + }) + } + .into_any(), }; let Some(thread) = self.thread() else { return primary; }; + let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); if index == total_entries - 1 && !is_generating { v_flex() @@ -1173,8 +1184,7 @@ impl AcpThreadView { || has_nonempty_diff || self.expanded_tool_calls.contains(&tool_call.id); - let gradient_color = cx.theme().colors().panel_background; - let gradient_overlay = { + let gradient_overlay = |color: Hsla| { div() .absolute() .top_0() @@ -1183,8 +1193,8 @@ impl AcpThreadView { .h_full() .bg(linear_gradient( 90., - linear_color_stop(gradient_color, 1.), - linear_color_stop(gradient_color.opacity(0.2), 0.), + linear_color_stop(color, 1.), + linear_color_stop(color.opacity(0.2), 0.), )) }; @@ -1286,7 +1296,17 @@ impl AcpThreadView { ), )), ) - .child(gradient_overlay) + .map(|this| { + if needs_confirmation { + this.child(gradient_overlay( + self.tool_card_header_bg(cx), + )) + } else { + this.child(gradient_overlay( + cx.theme().colors().panel_background, + )) + } + }) .on_click(cx.listener({ let id = tool_call.id.clone(); move |this: &mut Self, _, _, cx: &mut Context| { @@ -1321,11 +1341,9 @@ impl AcpThreadView { .children(tool_call.content.iter().map(|content| { div() .py_1p5() - .child( - self.render_tool_call_content( - content, window, cx, - ), - ) + .child(self.render_tool_call_content( + content, tool_call, window, cx, + )) .into_any_element() })) .child(self.render_permission_buttons( @@ -1339,11 +1357,9 @@ impl AcpThreadView { this.children(tool_call.content.iter().map(|content| { div() .py_1p5() - .child( - self.render_tool_call_content( - content, window, cx, - ), - ) + .child(self.render_tool_call_content( + content, tool_call, window, cx, + )) .into_any_element() })) } @@ -1360,6 +1376,7 @@ impl AcpThreadView { fn render_tool_call_content( &self, content: &ToolCallContent, + tool_call: &ToolCall, window: &Window, cx: &Context, ) -> AnyElement { @@ -1380,7 +1397,9 @@ impl AcpThreadView { } } ToolCallContent::Diff(diff) => self.render_diff_editor(&diff.read(cx).multibuffer()), - ToolCallContent::Terminal(terminal) => self.render_terminal(terminal), + ToolCallContent::Terminal(terminal) => { + self.render_terminal_tool_call(terminal, tool_call, window, cx) + } } } @@ -1393,14 +1412,22 @@ impl AcpThreadView { cx: &Context, ) -> Div { h_flex() - .p_1p5() + .py_1() + .pl_2() + .pr_1() .gap_1() - .justify_end() + .justify_between() + .flex_wrap() .when(!empty_content, |this| { this.border_t_1() .border_color(self.tool_card_border_color(cx)) }) - .children(options.iter().map(|option| { + .child( + div() + .min_w(rems_from_px(145.)) + .child(LoadingLabel::new("Waiting for Confirmation").size(LabelSize::Small)), + ) + .child(h_flex().gap_0p5().children(options.iter().map(|option| { let option_id = SharedString::from(option.id.0.clone()); Button::new((option_id, entry_ix), option.name.clone()) .map(|this| match option.kind { @@ -1433,7 +1460,7 @@ impl AcpThreadView { ); } })) - })) + }))) } fn render_diff_editor(&self, multibuffer: &Entity) -> AnyElement { @@ -1449,18 +1476,242 @@ impl AcpThreadView { .into_any() } - fn render_terminal(&self, terminal: &Entity) -> AnyElement { - v_flex() - .h_72() + fn render_terminal_tool_call( + &self, + terminal: &Entity, + tool_call: &ToolCall, + window: &Window, + cx: &Context, + ) -> AnyElement { + let terminal_data = terminal.read(cx); + let working_dir = terminal_data.working_dir(); + let command = terminal_data.command(); + let started_at = terminal_data.started_at(); + + let tool_failed = matches!( + &tool_call.status, + ToolCallStatus::Rejected + | ToolCallStatus::Canceled + | ToolCallStatus::Allowed { + status: acp::ToolCallStatus::Failed, + .. + } + ); + + let output = terminal_data.output(); + let command_finished = output.is_some(); + let truncated_output = output.is_some_and(|output| output.was_content_truncated); + let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0); + + let command_failed = command_finished + && output.is_some_and(|o| o.exit_status.is_none_or(|status| !status.success())); + + let time_elapsed = if let Some(output) = output { + output.ended_at.duration_since(started_at) + } else { + started_at.elapsed() + }; + + let header_bg = cx + .theme() + .colors() + .element_background + .blend(cx.theme().colors().editor_foreground.opacity(0.025)); + let border_color = cx.theme().colors().border.opacity(0.6); + + let working_dir = working_dir + .as_ref() + .map(|path| format!("{}", path.display())) + .unwrap_or_else(|| "current directory".to_string()); + + let header = h_flex() + .id(SharedString::from(format!( + "terminal-tool-header-{}", + terminal.entity_id() + ))) + .flex_none() + .gap_1() + .justify_between() + .rounded_t_md() .child( - if let Some(terminal_view) = self.terminal_views.get(&terminal.entity_id()) { - // TODO: terminal has all the state we need to reproduce - // what we had in the terminal card. - terminal_view.clone().into_any_element() - } else { - Empty.into_any() - }, + div() + .id(("command-target-path", terminal.entity_id())) + .w_full() + .max_w_full() + .overflow_x_scroll() + .child( + Label::new(working_dir) + .buffer_font(cx) + .size(LabelSize::XSmall) + .color(Color::Muted), + ), ) + .when(!command_finished, |header| { + header + .gap_1p5() + .child( + Button::new( + SharedString::from(format!("stop-terminal-{}", terminal.entity_id())), + "Stop", + ) + .icon(IconName::Stop) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .icon_color(Color::Error) + .label_size(LabelSize::Small) + .tooltip(move |window, cx| { + Tooltip::with_meta( + "Stop This Command", + None, + "Also possible by placing your cursor inside the terminal and using regular terminal bindings.", + window, + cx, + ) + }) + .on_click({ + let terminal = terminal.clone(); + cx.listener(move |_this, _event, _window, cx| { + let inner_terminal = terminal.read(cx).inner().clone(); + inner_terminal.update(cx, |inner_terminal, _cx| { + inner_terminal.kill_active_task(); + }); + }) + }), + ) + .child(Divider::vertical()) + .child( + Icon::new(IconName::ArrowCircle) + .size(IconSize::XSmall) + .color(Color::Info) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate(percentage(delta))) + }, + ), + ) + }) + .when(tool_failed || command_failed, |header| { + header.child( + div() + .id(("terminal-tool-error-code-indicator", terminal.entity_id())) + .child( + Icon::new(IconName::Close) + .size(IconSize::Small) + .color(Color::Error), + ) + .when_some(output.and_then(|o| o.exit_status), |this, status| { + this.tooltip(Tooltip::text(format!( + "Exited with code {}", + status.code().unwrap_or(-1), + ))) + }), + ) + }) + .when(truncated_output, |header| { + let tooltip = if let Some(output) = output { + if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES { + "Output exceeded terminal max lines and was \ + truncated, the model received the first 16 KB." + .to_string() + } else { + format!( + "Output is {} long—to avoid unexpected token usage, \ + only 16 KB was sent back to the model.", + format_file_size(output.original_content_len as u64, true), + ) + } + } else { + "Output was truncated".to_string() + }; + + header.child( + h_flex() + .id(("terminal-tool-truncated-label", terminal.entity_id())) + .gap_1() + .child( + Icon::new(IconName::Info) + .size(IconSize::XSmall) + .color(Color::Ignored), + ) + .child( + Label::new("Truncated") + .color(Color::Muted) + .size(LabelSize::XSmall), + ) + .tooltip(Tooltip::text(tooltip)), + ) + }) + .when(time_elapsed > Duration::from_secs(10), |header| { + header.child( + Label::new(format!("({})", duration_alt_display(time_elapsed))) + .buffer_font(cx) + .color(Color::Muted) + .size(LabelSize::XSmall), + ) + }) + .child( + Disclosure::new( + SharedString::from(format!( + "terminal-tool-disclosure-{}", + terminal.entity_id() + )), + self.terminal_expanded, + ) + .opened_icon(IconName::ChevronUp) + .closed_icon(IconName::ChevronDown) + .on_click(cx.listener(move |this, _event, _window, _cx| { + this.terminal_expanded = !this.terminal_expanded; + })), + ); + + let show_output = + self.terminal_expanded && self.terminal_views.contains_key(&terminal.entity_id()); + + v_flex() + .mb_2() + .border_1() + .when(tool_failed || command_failed, |card| card.border_dashed()) + .border_color(border_color) + .rounded_lg() + .overflow_hidden() + .child( + v_flex() + .p_2() + .gap_0p5() + .bg(header_bg) + .text_xs() + .child(header) + .child( + MarkdownElement::new( + command.clone(), + terminal_command_markdown_style(window, cx), + ) + .code_block_renderer( + markdown::CodeBlockRenderer::Default { + copy_button: false, + copy_button_on_hover: true, + border: false, + }, + ), + ), + ) + .when(show_output, |this| { + let terminal_view = self.terminal_views.get(&terminal.entity_id()).unwrap(); + + this.child( + div() + .pt_2() + .border_t_1() + .when(tool_failed || command_failed, |card| card.border_dashed()) + .border_color(border_color) + .bg(cx.theme().colors().editor_background) + .rounded_b_md() + .text_ui_sm(cx) + .child(terminal_view.clone()), + ) + }) .into_any() } @@ -3030,6 +3281,18 @@ fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement { } } +fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { + let default_md_style = default_markdown_style(true, window, cx); + + MarkdownStyle { + base_text_style: TextStyle { + ..default_md_style.base_text_style + }, + selection_background_color: cx.theme().colors().element_selection_background, + ..Default::default() + } +} + #[cfg(test)] mod tests { use agent_client_protocol::SessionId; From fa3d0aaed444027387c3021c9cd4022910cb0638 Mon Sep 17 00:00:00 2001 From: Victor Tran Date: Tue, 12 Aug 2025 07:10:14 +1000 Subject: [PATCH 264/693] gpui: Allow selection of "Services" menu independent of menu title (#34115) Release Notes: - N/A --- In the same vein as #29538, the "Services" menu on macOS depended on the text being exactly "Services", not allowing for i18n of the menu name. This PR introduces a new menu type called `OsMenu` that defines a special menu that can be populated by the system. Currently, it takes one enum value, `ServicesMenu` that tells the system to populate its contents with the items it would usually populate the "Services" menu with. An example of this being used has been implemented in the `set_menus` example: `cargo run -p gpui --example set_menus` --- Point to consider: In `mac/platform.rs:414` the existing code for setting the "Services" menu remains for backwards compatibility. Should this remain now that this new method exists to set the menu, or should it be removed? --------- Co-authored-by: Mikayla Maki --- crates/gpui/examples/set_menus.rs | 9 +++- crates/gpui/src/platform/app_menu.rs | 56 ++++++++++++++++++++++++ crates/gpui/src/platform/mac/platform.rs | 23 +++++++--- crates/title_bar/src/application_menu.rs | 8 ++++ crates/zed/src/zed/app_menus.rs | 5 +-- 5 files changed, 89 insertions(+), 12 deletions(-) diff --git a/crates/gpui/examples/set_menus.rs b/crates/gpui/examples/set_menus.rs index f53fff7c7f..8a97a8d8a2 100644 --- a/crates/gpui/examples/set_menus.rs +++ b/crates/gpui/examples/set_menus.rs @@ -1,5 +1,6 @@ use gpui::{ - App, Application, Context, Menu, MenuItem, Window, WindowOptions, actions, div, prelude::*, rgb, + App, Application, Context, Menu, MenuItem, SystemMenuType, Window, WindowOptions, actions, div, + prelude::*, rgb, }; struct SetMenus; @@ -27,7 +28,11 @@ fn main() { // Add menu items cx.set_menus(vec![Menu { name: "set_menus".into(), - items: vec![MenuItem::action("Quit", Quit)], + items: vec![ + MenuItem::os_submenu("Services", SystemMenuType::Services), + MenuItem::separator(), + MenuItem::action("Quit", Quit), + ], }]); cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| SetMenus {})) .unwrap(); diff --git a/crates/gpui/src/platform/app_menu.rs b/crates/gpui/src/platform/app_menu.rs index 2815cbdd7f..4069fee726 100644 --- a/crates/gpui/src/platform/app_menu.rs +++ b/crates/gpui/src/platform/app_menu.rs @@ -20,6 +20,34 @@ impl Menu { } } +/// OS menus are menus that are recognized by the operating system +/// This allows the operating system to provide specialized items for +/// these menus +pub struct OsMenu { + /// The name of the menu + pub name: SharedString, + + /// The type of menu + pub menu_type: SystemMenuType, +} + +impl OsMenu { + /// Create an OwnedOsMenu from this OsMenu + pub fn owned(self) -> OwnedOsMenu { + OwnedOsMenu { + name: self.name.to_string().into(), + menu_type: self.menu_type, + } + } +} + +/// The type of system menu +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum SystemMenuType { + /// The 'Services' menu in the Application menu on macOS + Services, +} + /// The different kinds of items that can be in a menu pub enum MenuItem { /// A separator between items @@ -28,6 +56,9 @@ pub enum MenuItem { /// A submenu Submenu(Menu), + /// A menu, managed by the system (for example, the Services menu on macOS) + SystemMenu(OsMenu), + /// An action that can be performed Action { /// The name of this menu item @@ -53,6 +84,14 @@ impl MenuItem { Self::Submenu(menu) } + /// Creates a new submenu that is populated by the OS + pub fn os_submenu(name: impl Into, menu_type: SystemMenuType) -> Self { + Self::SystemMenu(OsMenu { + name: name.into(), + menu_type, + }) + } + /// Creates a new menu item that invokes an action pub fn action(name: impl Into, action: impl Action) -> Self { Self::Action { @@ -89,10 +128,23 @@ impl MenuItem { action, os_action, }, + MenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.owned()), } } } +/// OS menus are menus that are recognized by the operating system +/// This allows the operating system to provide specialized items for +/// these menus +#[derive(Clone)] +pub struct OwnedOsMenu { + /// The name of the menu + pub name: SharedString, + + /// The type of menu + pub menu_type: SystemMenuType, +} + /// A menu of the application, either a main menu or a submenu #[derive(Clone)] pub struct OwnedMenu { @@ -111,6 +163,9 @@ pub enum OwnedMenuItem { /// A submenu Submenu(OwnedMenu), + /// A menu, managed by the system (for example, the Services menu on macOS) + SystemMenu(OwnedOsMenu), + /// An action that can be performed Action { /// The name of this menu item @@ -139,6 +194,7 @@ impl Clone for OwnedMenuItem { action: action.boxed_clone(), os_action: *os_action, }, + OwnedMenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.clone()), } } } diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index c71eb448c4..c573131799 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -7,9 +7,9 @@ use super::{ use crate::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString, CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher, - MacDisplay, MacWindow, Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform, PlatformDisplay, - PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task, - WindowAppearance, WindowParams, hash, + MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform, + PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, + SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, hash, }; use anyhow::{Context as _, anyhow}; use block::ConcreteBlock; @@ -413,9 +413,20 @@ impl MacPlatform { } item.setSubmenu_(submenu); item.setTitle_(ns_string(&name)); - if name == "Services" { - let app: id = msg_send![APP_CLASS, sharedApplication]; - app.setServicesMenu_(item); + item + } + MenuItem::SystemMenu(OsMenu { name, menu_type }) => { + let item = NSMenuItem::new(nil).autorelease(); + let submenu = NSMenu::new(nil).autorelease(); + submenu.setDelegate_(delegate); + item.setSubmenu_(submenu); + item.setTitle_(ns_string(&name)); + + match menu_type { + SystemMenuType::Services => { + let app: id = msg_send![APP_CLASS, sharedApplication]; + app.setServicesMenu_(item); + } } item diff --git a/crates/title_bar/src/application_menu.rs b/crates/title_bar/src/application_menu.rs index a5d5f154c9..98f0eeb6cc 100644 --- a/crates/title_bar/src/application_menu.rs +++ b/crates/title_bar/src/application_menu.rs @@ -121,8 +121,16 @@ impl ApplicationMenu { menu.action(name, action) } OwnedMenuItem::Submenu(_) => menu, + OwnedMenuItem::SystemMenu(_) => { + // A system menu doesn't make sense in this context, so ignore it + menu + } }) } + OwnedMenuItem::SystemMenu(_) => { + // A system menu doesn't make sense in this context, so ignore it + menu + } }) }) } diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index 15d5659f03..53eec42ba0 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -35,10 +35,7 @@ pub fn app_menus() -> Vec

{ ], }), MenuItem::separator(), - MenuItem::submenu(Menu { - name: "Services".into(), - items: vec![], - }), + MenuItem::os_submenu("Services", gpui::SystemMenuType::Services), MenuItem::separator(), MenuItem::action("Extensions", zed_actions::Extensions::default()), MenuItem::action("Install CLI", install_cli::Install), From add67bde43aec927dfb74d3db6fdaa362deaff45 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 11 Aug 2025 16:10:06 -0600 Subject: [PATCH 265/693] Remove unnecessary argument from Vim#update_editor (#36001) Release Notes: - N/A --- crates/vim/src/change_list.rs | 4 +- crates/vim/src/command.rs | 49 +++++++++++----------- crates/vim/src/digraph.rs | 8 +--- crates/vim/src/helix.rs | 16 +++---- crates/vim/src/indent.rs | 10 ++--- crates/vim/src/insert.rs | 2 +- crates/vim/src/motion.rs | 2 +- crates/vim/src/normal.rs | 40 +++++++++--------- crates/vim/src/normal/change.rs | 4 +- crates/vim/src/normal/convert.rs | 6 +-- crates/vim/src/normal/delete.rs | 4 +- crates/vim/src/normal/increment.rs | 2 +- crates/vim/src/normal/mark.rs | 10 ++--- crates/vim/src/normal/paste.rs | 6 +-- crates/vim/src/normal/scroll.rs | 2 +- crates/vim/src/normal/search.rs | 4 +- crates/vim/src/normal/substitute.rs | 2 +- crates/vim/src/normal/toggle_comments.rs | 4 +- crates/vim/src/normal/yank.rs | 4 +- crates/vim/src/replace.rs | 12 +++--- crates/vim/src/rewrap.rs | 6 +-- crates/vim/src/surrounds.rs | 8 ++-- crates/vim/src/vim.rs | 53 +++++++++++------------- crates/vim/src/visual.rs | 36 ++++++++-------- 24 files changed, 142 insertions(+), 152 deletions(-) diff --git a/crates/vim/src/change_list.rs b/crates/vim/src/change_list.rs index a59083f7ab..c92ce4720e 100644 --- a/crates/vim/src/change_list.rs +++ b/crates/vim/src/change_list.rs @@ -31,7 +31,7 @@ impl Vim { ) { let count = Vim::take_count(cx).unwrap_or(1); Vim::take_forced_motion(cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { if let Some(selections) = editor .change_list .next_change(count, direction) @@ -49,7 +49,7 @@ impl Vim { } pub(crate) fn push_to_change_list(&mut self, window: &mut Window, cx: &mut Context) { - let Some((new_positions, buffer)) = self.update_editor(window, cx, |vim, editor, _, cx| { + let Some((new_positions, buffer)) = self.update_editor(cx, |vim, editor, cx| { let (map, selections) = editor.selections.all_adjusted_display(cx); let buffer = editor.buffer().clone(); diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 7963db3571..f7889d8cd8 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -241,9 +241,9 @@ impl Deref for WrappedAction { pub fn register(editor: &mut Editor, cx: &mut Context) { // Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| { - Vim::action(editor, cx, |vim, action: &VimSet, window, cx| { + Vim::action(editor, cx, |vim, action: &VimSet, _, cx| { for option in action.options.iter() { - vim.update_editor(window, cx, |_, editor, _, cx| match option { + vim.update_editor(cx, |_, editor, cx| match option { VimOption::Wrap(true) => { editor .set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); @@ -298,7 +298,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { }); Vim::action(editor, cx, |vim, action: &VimSave, window, cx| { - vim.update_editor(window, cx, |_, editor, window, cx| { + vim.update_editor(cx, |_, editor, cx| { let Some(project) = editor.project.clone() else { return; }; @@ -375,7 +375,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { cx, ); } - vim.update_editor(window, cx, |vim, editor, window, cx| match action { + vim.update_editor(cx, |vim, editor, cx| match action { DeleteMarks::Marks(s) => { if s.starts_with('-') || s.ends_with('-') || s.contains(['\'', '`']) { err(s.clone(), window, cx); @@ -432,7 +432,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { }); Vim::action(editor, cx, |vim, action: &VimEdit, window, cx| { - vim.update_editor(window, cx, |vim, editor, window, cx| { + vim.update_editor(cx, |vim, editor, cx| { let Some(workspace) = vim.workspace(window) else { return; }; @@ -462,11 +462,10 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { .map(|c| Keystroke::parse(&c.to_string()).unwrap()) .collect(); vim.switch_mode(Mode::Normal, true, window, cx); - let initial_selections = vim.update_editor(window, cx, |_, editor, _, _| { - editor.selections.disjoint_anchors() - }); + let initial_selections = + vim.update_editor(cx, |_, editor, _| editor.selections.disjoint_anchors()); if let Some(range) = &action.range { - let result = vim.update_editor(window, cx, |vim, editor, window, cx| { + let result = vim.update_editor(cx, |vim, editor, cx| { let range = range.buffer_range(vim, editor, window, cx)?; editor.change_selections( SelectionEffects::no_scroll().nav_history(false), @@ -498,7 +497,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { cx.spawn_in(window, async move |vim, cx| { task.await; vim.update_in(cx, |vim, window, cx| { - vim.update_editor(window, cx, |_, editor, window, cx| { + vim.update_editor(cx, |_, editor, cx| { if had_range { editor.change_selections(SelectionEffects::default(), window, cx, |s| { s.select_anchor_ranges([s.newest_anchor().range()]); @@ -510,7 +509,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { } else { vim.switch_mode(Mode::Normal, true, window, cx); } - vim.update_editor(window, cx, |_, editor, _, cx| { + vim.update_editor(cx, |_, editor, cx| { if let Some(first_sel) = initial_selections { if let Some(tx_id) = editor .buffer() @@ -548,7 +547,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, action: &GoToLine, window, cx| { vim.switch_mode(Mode::Normal, false, window, cx); - let result = vim.update_editor(window, cx, |vim, editor, window, cx| { + let result = vim.update_editor(cx, |vim, editor, cx| { let snapshot = editor.snapshot(window, cx); let buffer_row = action.range.head().buffer_row(vim, editor, window, cx)?; let current = editor.selections.newest::(cx); @@ -573,7 +572,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { }); Vim::action(editor, cx, |vim, action: &YankCommand, window, cx| { - vim.update_editor(window, cx, |vim, editor, window, cx| { + vim.update_editor(cx, |vim, editor, cx| { let snapshot = editor.snapshot(window, cx); if let Ok(range) = action.range.buffer_range(vim, editor, window, cx) { let end = if range.end < snapshot.buffer_snapshot.max_row() { @@ -600,7 +599,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { }); Vim::action(editor, cx, |vim, action: &WithRange, window, cx| { - let result = vim.update_editor(window, cx, |vim, editor, window, cx| { + let result = vim.update_editor(cx, |vim, editor, cx| { action.range.buffer_range(vim, editor, window, cx) }); @@ -619,7 +618,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { }; let previous_selections = vim - .update_editor(window, cx, |_, editor, window, cx| { + .update_editor(cx, |_, editor, cx| { let selections = action.restore_selection.then(|| { editor .selections @@ -635,7 +634,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { .flatten(); window.dispatch_action(action.action.boxed_clone(), cx); cx.defer_in(window, move |vim, window, cx| { - vim.update_editor(window, cx, |_, editor, window, cx| { + vim.update_editor(cx, |_, editor, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { if let Some(previous_selections) = previous_selections { s.select_ranges(previous_selections); @@ -1536,7 +1535,7 @@ impl OnMatchingLines { } pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context) { - let result = vim.update_editor(window, cx, |vim, editor, window, cx| { + let result = vim.update_editor(cx, |vim, editor, cx| { self.range.buffer_range(vim, editor, window, cx) }); @@ -1600,7 +1599,7 @@ impl OnMatchingLines { }); }; - vim.update_editor(window, cx, |_, editor, window, cx| { + vim.update_editor(cx, |_, editor, cx| { let snapshot = editor.snapshot(window, cx); let mut row = range.start.0; @@ -1680,7 +1679,7 @@ pub struct ShellExec { impl Vim { pub fn cancel_running_command(&mut self, window: &mut Window, cx: &mut Context) { if self.running_command.take().is_some() { - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, _window, _cx| { editor.clear_row_highlights::(); }) @@ -1691,7 +1690,7 @@ impl Vim { fn prepare_shell_command( &mut self, command: &str, - window: &mut Window, + _: &mut Window, cx: &mut Context, ) -> String { let mut ret = String::new(); @@ -1711,7 +1710,7 @@ impl Vim { } match c { '%' => { - self.update_editor(window, cx, |_, editor, _window, cx| { + self.update_editor(cx, |_, editor, cx| { if let Some((_, buffer, _)) = editor.active_excerpt(cx) { if let Some(file) = buffer.read(cx).file() { if let Some(local) = file.as_local() { @@ -1747,7 +1746,7 @@ impl Vim { let Some(workspace) = self.workspace(window) else { return; }; - let command = self.update_editor(window, cx, |_, editor, window, cx| { + let command = self.update_editor(cx, |_, editor, cx| { let snapshot = editor.snapshot(window, cx); let start = editor.selections.newest_display(cx); let text_layout_details = editor.text_layout_details(window); @@ -1794,7 +1793,7 @@ impl Vim { let Some(workspace) = self.workspace(window) else { return; }; - let command = self.update_editor(window, cx, |_, editor, window, cx| { + let command = self.update_editor(cx, |_, editor, cx| { let snapshot = editor.snapshot(window, cx); let start = editor.selections.newest_display(cx); let range = object @@ -1896,7 +1895,7 @@ impl ShellExec { let mut input_snapshot = None; let mut input_range = None; let mut needs_newline_prefix = false; - vim.update_editor(window, cx, |vim, editor, window, cx| { + vim.update_editor(cx, |vim, editor, cx| { let snapshot = editor.buffer().read(cx).snapshot(cx); let range = if let Some(range) = self.range.clone() { let Some(range) = range.buffer_range(vim, editor, window, cx).log_err() else { @@ -1990,7 +1989,7 @@ impl ShellExec { } vim.update_in(cx, |vim, window, cx| { - vim.update_editor(window, cx, |_, editor, window, cx| { + vim.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { editor.edit([(range.clone(), text)], cx); let snapshot = editor.buffer().read(cx).snapshot(cx); diff --git a/crates/vim/src/digraph.rs b/crates/vim/src/digraph.rs index 881454392a..c555b781b1 100644 --- a/crates/vim/src/digraph.rs +++ b/crates/vim/src/digraph.rs @@ -56,9 +56,7 @@ impl Vim { self.pop_operator(window, cx); if self.editor_input_enabled() { - self.update_editor(window, cx, |_, editor, window, cx| { - editor.insert(&text, window, cx) - }); + self.update_editor(cx, |_, editor, cx| editor.insert(&text, window, cx)); } else { self.input_ignored(text, window, cx); } @@ -214,9 +212,7 @@ impl Vim { text.push_str(suffix); if self.editor_input_enabled() { - self.update_editor(window, cx, |_, editor, window, cx| { - editor.insert(&text, window, cx) - }); + self.update_editor(cx, |_, editor, cx| editor.insert(&text, window, cx)); } else { self.input_ignored(text.into(), window, cx); } diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index ca93c9c1de..686c74f65e 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -62,7 +62,7 @@ impl Vim { cx: &mut Context, mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool, ) { - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { let times = times.unwrap_or(1); @@ -115,7 +115,7 @@ impl Vim { cx: &mut Context, mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool, ) { - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { let times = times.unwrap_or(1); @@ -175,7 +175,7 @@ impl Vim { window: &mut Window, cx: &mut Context, ) { - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(window); editor.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { @@ -253,7 +253,7 @@ impl Vim { }) } Motion::FindForward { .. } => { - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(window); editor.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { @@ -280,7 +280,7 @@ impl Vim { }); } Motion::FindBackward { .. } => { - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(window); editor.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { @@ -312,7 +312,7 @@ impl Vim { fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context) { self.start_recording(cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.change_selections(Default::default(), window, cx, |s| { s.move_with(|_map, selection| { // In helix normal mode, move cursor to start of selection and collapse @@ -328,7 +328,7 @@ impl Vim { fn helix_append(&mut self, _: &HelixAppend, window: &mut Window, cx: &mut Context) { self.start_recording(cx); self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { let point = if selection.is_empty() { @@ -343,7 +343,7 @@ impl Vim { } pub fn helix_replace(&mut self, text: &str, window: &mut Window, cx: &mut Context) { - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { let (map, selections) = editor.selections.all_display(cx); diff --git a/crates/vim/src/indent.rs b/crates/vim/src/indent.rs index 75b1857a5b..7ef204de0f 100644 --- a/crates/vim/src/indent.rs +++ b/crates/vim/src/indent.rs @@ -31,7 +31,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { let count = Vim::take_count(cx).unwrap_or(1); Vim::take_forced_motion(cx); vim.store_visual_marks(window, cx); - vim.update_editor(window, cx, |vim, editor, window, cx| { + vim.update_editor(cx, |vim, editor, cx| { editor.transact(window, cx, |editor, window, cx| { let original_positions = vim.save_selection_starts(editor, cx); for _ in 0..count { @@ -50,7 +50,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { let count = Vim::take_count(cx).unwrap_or(1); Vim::take_forced_motion(cx); vim.store_visual_marks(window, cx); - vim.update_editor(window, cx, |vim, editor, window, cx| { + vim.update_editor(cx, |vim, editor, cx| { editor.transact(window, cx, |editor, window, cx| { let original_positions = vim.save_selection_starts(editor, cx); for _ in 0..count { @@ -69,7 +69,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { let count = Vim::take_count(cx).unwrap_or(1); Vim::take_forced_motion(cx); vim.store_visual_marks(window, cx); - vim.update_editor(window, cx, |vim, editor, window, cx| { + vim.update_editor(cx, |vim, editor, cx| { editor.transact(window, cx, |editor, window, cx| { let original_positions = vim.save_selection_starts(editor, cx); for _ in 0..count { @@ -95,7 +95,7 @@ impl Vim { cx: &mut Context, ) { self.stop_recording(cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(window); editor.transact(window, cx, |editor, window, cx| { let mut selection_starts: HashMap<_, _> = Default::default(); @@ -137,7 +137,7 @@ impl Vim { cx: &mut Context, ) { self.stop_recording(cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { let mut original_positions: HashMap<_, _> = Default::default(); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index 0a370e16ba..584057a8c0 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -38,7 +38,7 @@ impl Vim { if count <= 1 || Vim::globals(cx).dot_replaying { self.create_mark("^".into(), window, cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.dismiss_menus_and_popups(false, window, cx); if !HelixModeSetting::get_global(cx).0 { diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 0e487f4410..7ef883f406 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -679,7 +679,7 @@ impl Vim { match self.mode { Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { if !prior_selections.is_empty() { - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.change_selections(Default::default(), window, cx, |s| { s.select_ranges(prior_selections.iter().cloned()) }) diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 13128e7b40..b74d85b7c5 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -132,7 +132,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &HelixDelete, window, cx| { vim.record_current_action(cx); - vim.update_editor(window, cx, |_, editor, window, cx| { + vim.update_editor(cx, |_, editor, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.move_with(|map, selection| { if selection.is_empty() { @@ -146,7 +146,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { }); Vim::action(editor, cx, |vim, _: &HelixCollapseSelection, window, cx| { - vim.update_editor(window, cx, |_, editor, window, cx| { + vim.update_editor(cx, |_, editor, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.move_with(|map, selection| { let mut point = selection.head(); @@ -198,7 +198,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &Undo, window, cx| { let times = Vim::take_count(cx); Vim::take_forced_motion(cx); - vim.update_editor(window, cx, |_, editor, window, cx| { + vim.update_editor(cx, |_, editor, cx| { for _ in 0..times.unwrap_or(1) { editor.undo(&editor::actions::Undo, window, cx); } @@ -207,7 +207,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &Redo, window, cx| { let times = Vim::take_count(cx); Vim::take_forced_motion(cx); - vim.update_editor(window, cx, |_, editor, window, cx| { + vim.update_editor(cx, |_, editor, cx| { for _ in 0..times.unwrap_or(1) { editor.redo(&editor::actions::Redo, window, cx); } @@ -215,7 +215,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { }); Vim::action(editor, cx, |vim, _: &UndoLastLine, window, cx| { Vim::take_forced_motion(cx); - vim.update_editor(window, cx, |vim, editor, window, cx| { + vim.update_editor(cx, |vim, editor, cx| { let snapshot = editor.buffer().read(cx).snapshot(cx); let Some(last_change) = editor.change_list.last_before_grouping() else { return; @@ -526,7 +526,7 @@ impl Vim { window: &mut Window, cx: &mut Context, ) { - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(window); editor.change_selections( SelectionEffects::default().nav_history(motion.push_to_jump_list()), @@ -546,7 +546,7 @@ impl Vim { fn insert_after(&mut self, _: &InsertAfter, window: &mut Window, cx: &mut Context) { self.start_recording(cx); self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None)); }); @@ -557,7 +557,7 @@ impl Vim { self.start_recording(cx); if self.mode.is_visual() { let current_mode = self.mode; - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { if current_mode == Mode::VisualLine { @@ -581,7 +581,7 @@ impl Vim { ) { self.start_recording(cx); self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(|map, cursor, _| { ( @@ -601,7 +601,7 @@ impl Vim { ) { self.start_recording(cx); self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(|map, cursor, _| { (next_line_end(map, cursor, 1), SelectionGoal::None) @@ -618,7 +618,7 @@ impl Vim { ) { self.start_recording(cx); self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { let Some(Mark::Local(marks)) = vim.get_mark("^", editor, window, cx) else { return; }; @@ -637,7 +637,7 @@ impl Vim { ) { self.start_recording(cx); self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { let selections = editor.selections.all::(cx); let snapshot = editor.buffer().read(cx).snapshot(cx); @@ -678,7 +678,7 @@ impl Vim { ) { self.start_recording(cx); self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(window); editor.transact(window, cx, |editor, window, cx| { let selections = editor.selections.all::(cx); @@ -725,7 +725,7 @@ impl Vim { self.record_current_action(cx); let count = Vim::take_count(cx).unwrap_or(1); Vim::take_forced_motion(cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, _, cx| { let selections = editor.selections.all::(cx); @@ -754,7 +754,7 @@ impl Vim { self.record_current_action(cx); let count = Vim::take_count(cx).unwrap_or(1); Vim::take_forced_motion(cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { let selections = editor.selections.all::(cx); let snapshot = editor.buffer().read(cx).snapshot(cx); @@ -804,7 +804,7 @@ impl Vim { times -= 1; } - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { for _ in 0..times { editor.join_lines_impl(insert_whitespace, window, cx) @@ -828,10 +828,10 @@ impl Vim { ) } - fn show_location(&mut self, _: &ShowLocation, window: &mut Window, cx: &mut Context) { + fn show_location(&mut self, _: &ShowLocation, _: &mut Window, cx: &mut Context) { let count = Vim::take_count(cx); Vim::take_forced_motion(cx); - self.update_editor(window, cx, |vim, editor, _window, cx| { + self.update_editor(cx, |vim, editor, cx| { let selection = editor.selections.newest_anchor(); let Some((buffer, point, _)) = editor .buffer() @@ -875,7 +875,7 @@ impl Vim { fn toggle_comments(&mut self, _: &ToggleComments, window: &mut Window, cx: &mut Context) { self.record_current_action(cx); self.store_visual_marks(window, cx); - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { editor.transact(window, cx, |editor, window, cx| { let original_positions = vim.save_selection_starts(editor, cx); editor.toggle_comments(&Default::default(), window, cx); @@ -897,7 +897,7 @@ impl Vim { let count = Vim::take_count(cx).unwrap_or(1); Vim::take_forced_motion(cx); self.stop_recording(cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); let (map, display_selections) = editor.selections.all_display(cx); diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index c1bc7a70ae..fcd36dd7ee 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -34,7 +34,7 @@ impl Vim { } else { None }; - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { let text_layout_details = editor.text_layout_details(window); editor.transact(window, cx, |editor, window, cx| { // We are swapping to insert mode anyway. Just set the line end clipping behavior now @@ -111,7 +111,7 @@ impl Vim { cx: &mut Context, ) { let mut objects_found = false; - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { // We are swapping to insert mode anyway. Just set the line end clipping behavior now editor.set_clip_at_line_ends(false, cx); editor.transact(window, cx, |editor, window, cx| { diff --git a/crates/vim/src/normal/convert.rs b/crates/vim/src/normal/convert.rs index cf9498bec9..4b9c3fc8f7 100644 --- a/crates/vim/src/normal/convert.rs +++ b/crates/vim/src/normal/convert.rs @@ -31,7 +31,7 @@ impl Vim { cx: &mut Context, ) { self.stop_recording(cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.set_clip_at_line_ends(false, cx); let text_layout_details = editor.text_layout_details(window); editor.transact(window, cx, |editor, window, cx| { @@ -87,7 +87,7 @@ impl Vim { cx: &mut Context, ) { self.stop_recording(cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); let mut original_positions: HashMap<_, _> = Default::default(); @@ -195,7 +195,7 @@ impl Vim { let count = Vim::take_count(cx).unwrap_or(1) as u32; Vim::take_forced_motion(cx); - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { let mut ranges = Vec::new(); let mut cursor_positions = Vec::new(); let snapshot = editor.buffer().read(cx).snapshot(cx); diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index 2cf40292cf..1b7557371a 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -22,7 +22,7 @@ impl Vim { cx: &mut Context, ) { self.stop_recording(cx); - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { let text_layout_details = editor.text_layout_details(window); editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); @@ -96,7 +96,7 @@ impl Vim { cx: &mut Context, ) { self.stop_recording(cx); - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); // Emulates behavior in vim where if we expanded backwards to include a newline diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index 51f6e4a0f9..007514e472 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -53,7 +53,7 @@ impl Vim { cx: &mut Context, ) { self.store_visual_marks(window, cx); - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { let mut edits = Vec::new(); let mut new_anchors = Vec::new(); diff --git a/crates/vim/src/normal/mark.rs b/crates/vim/src/normal/mark.rs index 57a6108841..1d6264d593 100644 --- a/crates/vim/src/normal/mark.rs +++ b/crates/vim/src/normal/mark.rs @@ -19,7 +19,7 @@ use crate::{ impl Vim { pub fn create_mark(&mut self, text: Arc, window: &mut Window, cx: &mut Context) { - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { let anchors = editor .selections .disjoint_anchors() @@ -49,7 +49,7 @@ impl Vim { let mut ends = vec![]; let mut reversed = vec![]; - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { let (map, selections) = editor.selections.all_display(cx); for selection in selections { let end = movement::saturating_left(&map, selection.end); @@ -190,7 +190,7 @@ impl Vim { self.pop_operator(window, cx); } let mark = self - .update_editor(window, cx, |vim, editor, window, cx| { + .update_editor(cx, |vim, editor, cx| { vim.get_mark(&text, editor, window, cx) }) .flatten(); @@ -209,7 +209,7 @@ impl Vim { let Some(mut anchors) = anchors else { return }; - self.update_editor(window, cx, |_, editor, _, cx| { + self.update_editor(cx, |_, editor, cx| { editor.create_nav_history_entry(cx); }); let is_active_operator = self.active_operator().is_some(); @@ -231,7 +231,7 @@ impl Vim { || self.mode == Mode::VisualLine || self.mode == Mode::VisualBlock; - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { let map = editor.snapshot(window, cx); let mut ranges: Vec> = Vec::new(); for mut anchor in anchors { diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 07712fbedd..0fd17f310e 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -32,7 +32,7 @@ impl Vim { let count = Vim::take_count(cx).unwrap_or(1); Vim::take_forced_motion(cx); - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { let text_layout_details = editor.text_layout_details(window); editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); @@ -236,7 +236,7 @@ impl Vim { ) { self.stop_recording(cx); let selected_register = self.selected_register.take(); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { @@ -273,7 +273,7 @@ impl Vim { ) { self.stop_recording(cx); let selected_register = self.selected_register.take(); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(window); editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index e2ae74b52b..af13bc0fd0 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -97,7 +97,7 @@ impl Vim { let amount = by(Vim::take_count(cx).map(|c| c as f32)); Vim::take_forced_motion(cx); self.exit_temporary_normal(window, cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { scroll_editor(editor, move_cursor, &amount, window, cx) }); } diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 24f2cf751f..e4e95ca48e 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -251,7 +251,7 @@ impl Vim { // If the active editor has changed during a search, don't panic. if prior_selections.iter().any(|s| { - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { !s.start .is_valid(&editor.snapshot(window, cx).buffer_snapshot) }) @@ -457,7 +457,7 @@ impl Vim { else { return; }; - if let Some(result) = self.update_editor(window, cx, |vim, editor, window, cx| { + if let Some(result) = self.update_editor(cx, |vim, editor, cx| { let range = action.range.buffer_range(vim, editor, window, cx)?; let snapshot = &editor.snapshot(window, cx).buffer_snapshot; let end_point = Point::new(range.end.0, snapshot.line_len(range.end)); diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index a9752f2887..889d487170 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -45,7 +45,7 @@ impl Vim { cx: &mut Context, ) { self.store_visual_marks(window, cx); - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { editor.set_clip_at_line_ends(false, cx); editor.transact(window, cx, |editor, window, cx| { let text_layout_details = editor.text_layout_details(window); diff --git a/crates/vim/src/normal/toggle_comments.rs b/crates/vim/src/normal/toggle_comments.rs index 636ea9eec2..17c3b2d363 100644 --- a/crates/vim/src/normal/toggle_comments.rs +++ b/crates/vim/src/normal/toggle_comments.rs @@ -14,7 +14,7 @@ impl Vim { cx: &mut Context, ) { self.stop_recording(cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(window); editor.transact(window, cx, |editor, window, cx| { let mut selection_starts: HashMap<_, _> = Default::default(); @@ -51,7 +51,7 @@ impl Vim { cx: &mut Context, ) { self.stop_recording(cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { let mut original_positions: HashMap<_, _> = Default::default(); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index 847eba3143..fe8180ffff 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -25,7 +25,7 @@ impl Vim { window: &mut Window, cx: &mut Context, ) { - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { let text_layout_details = editor.text_layout_details(window); editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); @@ -70,7 +70,7 @@ impl Vim { window: &mut Window, cx: &mut Context, ) { - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); let mut start_positions: HashMap<_, _> = Default::default(); diff --git a/crates/vim/src/replace.rs b/crates/vim/src/replace.rs index aa857ef73e..eaa9fd5062 100644 --- a/crates/vim/src/replace.rs +++ b/crates/vim/src/replace.rs @@ -49,7 +49,7 @@ impl Vim { window: &mut Window, cx: &mut Context, ) { - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); let map = editor.snapshot(window, cx); @@ -94,7 +94,7 @@ impl Vim { window: &mut Window, cx: &mut Context, ) { - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); let map = editor.snapshot(window, cx); @@ -148,7 +148,7 @@ impl Vim { cx: &mut Context, ) { self.stop_recording(cx); - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { editor.set_clip_at_line_ends(false, cx); let mut selection = editor.selections.newest_display(cx); let snapshot = editor.snapshot(window, cx); @@ -167,7 +167,7 @@ impl Vim { pub fn exchange_visual(&mut self, window: &mut Window, cx: &mut Context) { self.stop_recording(cx); - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { let selection = editor.selections.newest_anchor(); let new_range = selection.start..selection.end; let snapshot = editor.snapshot(window, cx); @@ -178,7 +178,7 @@ impl Vim { pub fn clear_exchange(&mut self, window: &mut Window, cx: &mut Context) { self.stop_recording(cx); - self.update_editor(window, cx, |_, editor, _, cx| { + self.update_editor(cx, |_, editor, cx| { editor.clear_background_highlights::(cx); }); self.clear_operator(window, cx); @@ -193,7 +193,7 @@ impl Vim { cx: &mut Context, ) { self.stop_recording(cx); - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { editor.set_clip_at_line_ends(false, cx); let text_layout_details = editor.text_layout_details(window); let mut selection = editor.selections.newest_display(cx); diff --git a/crates/vim/src/rewrap.rs b/crates/vim/src/rewrap.rs index 4cd9449bfa..85e1967af0 100644 --- a/crates/vim/src/rewrap.rs +++ b/crates/vim/src/rewrap.rs @@ -18,7 +18,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::take_count(cx); Vim::take_forced_motion(cx); vim.store_visual_marks(window, cx); - vim.update_editor(window, cx, |vim, editor, window, cx| { + vim.update_editor(cx, |vim, editor, cx| { editor.transact(window, cx, |editor, window, cx| { let mut positions = vim.save_selection_starts(editor, cx); editor.rewrap_impl( @@ -55,7 +55,7 @@ impl Vim { cx: &mut Context, ) { self.stop_recording(cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(window); editor.transact(window, cx, |editor, window, cx| { let mut selection_starts: HashMap<_, _> = Default::default(); @@ -100,7 +100,7 @@ impl Vim { cx: &mut Context, ) { self.stop_recording(cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { let mut original_positions: HashMap<_, _> = Default::default(); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { diff --git a/crates/vim/src/surrounds.rs b/crates/vim/src/surrounds.rs index 1f77ebda4a..63cd21e88c 100644 --- a/crates/vim/src/surrounds.rs +++ b/crates/vim/src/surrounds.rs @@ -29,7 +29,7 @@ impl Vim { let count = Vim::take_count(cx); let forced_motion = Vim::take_forced_motion(cx); let mode = self.mode; - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(window); editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); @@ -140,7 +140,7 @@ impl Vim { }; let surround = pair.end != *text; - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); @@ -228,7 +228,7 @@ impl Vim { ) { if let Some(will_replace_pair) = object_to_bracket_pair(target) { self.stop_recording(cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); @@ -344,7 +344,7 @@ impl Vim { ) -> bool { let mut valid = false; if let Some(pair) = object_to_bracket_pair(object) { - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); let (display_map, selections) = editor.selections.all_adjusted_display(cx); diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 72edbe77ed..661bb71c91 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -748,7 +748,7 @@ impl Vim { editor, cx, |vim, action: &editor::actions::AcceptEditPrediction, window, cx| { - vim.update_editor(window, cx, |_, editor, window, cx| { + vim.update_editor(cx, |_, editor, cx| { editor.accept_edit_prediction(action, window, cx); }); // In non-insertion modes, predictions will be hidden and instead a jump will be @@ -847,7 +847,7 @@ impl Vim { if let Some(action) = keystroke_event.action.as_ref() { // Keystroke is handled by the vim system, so continue forward if action.name().starts_with("vim::") { - self.update_editor(window, cx, |_, editor, _, cx| { + self.update_editor(cx, |_, editor, cx| { editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx) }); return; @@ -909,7 +909,7 @@ impl Vim { anchor, is_deactivate, } => { - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { let mark = if *is_deactivate { "\"".to_string() } else { @@ -972,7 +972,7 @@ impl Vim { if mode == Mode::Normal || mode != last_mode { self.current_tx.take(); self.current_anchor.take(); - self.update_editor(window, cx, |_, editor, _, _| { + self.update_editor(cx, |_, editor, _| { editor.clear_selection_drag_state(); }); } @@ -988,7 +988,7 @@ impl Vim { && self.mode != self.last_mode && (self.mode == Mode::Insert || self.last_mode == Mode::Insert) { - self.update_editor(window, cx, |vim, editor, _, cx| { + self.update_editor(cx, |vim, editor, cx| { let is_relative = vim.mode != Mode::Insert; editor.set_relative_line_number(Some(is_relative), cx) }); @@ -1003,7 +1003,7 @@ impl Vim { } // Adjust selections - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { if last_mode != Mode::VisualBlock && last_mode.is_visual() && mode == Mode::VisualBlock { vim.visual_block_motion(true, editor, window, cx, |_, point, goal| { @@ -1214,7 +1214,7 @@ impl Vim { if preserve_selection { self.switch_mode(Mode::Visual, true, window, cx); } else { - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.set_clip_at_line_ends(false, cx); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.move_with(|_, selection| { @@ -1232,18 +1232,18 @@ impl Vim { if let Some(old_vim) = Vim::globals(cx).focused_vim() { if old_vim.entity_id() != cx.entity().entity_id() { old_vim.update(cx, |vim, cx| { - vim.update_editor(window, cx, |_, editor, _, cx| { + vim.update_editor(cx, |_, editor, cx| { editor.set_relative_line_number(None, cx) }); }); - self.update_editor(window, cx, |vim, editor, _, cx| { + self.update_editor(cx, |vim, editor, cx| { let is_relative = vim.mode != Mode::Insert; editor.set_relative_line_number(Some(is_relative), cx) }); } } else { - self.update_editor(window, cx, |vim, editor, _, cx| { + self.update_editor(cx, |vim, editor, cx| { let is_relative = vim.mode != Mode::Insert; editor.set_relative_line_number(Some(is_relative), cx) }); @@ -1256,35 +1256,30 @@ impl Vim { self.stop_recording_immediately(NormalBefore.boxed_clone(), cx); self.store_visual_marks(window, cx); self.clear_operator(window, cx); - self.update_editor(window, cx, |vim, editor, _, cx| { + self.update_editor(cx, |vim, editor, cx| { if vim.cursor_shape(cx) == CursorShape::Block { editor.set_cursor_shape(CursorShape::Hollow, cx); } }); } - fn cursor_shape_changed(&mut self, window: &mut Window, cx: &mut Context) { - self.update_editor(window, cx, |vim, editor, _, cx| { + fn cursor_shape_changed(&mut self, _: &mut Window, cx: &mut Context) { + self.update_editor(cx, |vim, editor, cx| { editor.set_cursor_shape(vim.cursor_shape(cx), cx); }); } fn update_editor( &mut self, - window: &mut Window, cx: &mut Context, - update: impl FnOnce(&mut Self, &mut Editor, &mut Window, &mut Context) -> S, + update: impl FnOnce(&mut Self, &mut Editor, &mut Context) -> S, ) -> Option { let editor = self.editor.upgrade()?; - Some(editor.update(cx, |editor, cx| update(self, editor, window, cx))) + Some(editor.update(cx, |editor, cx| update(self, editor, cx))) } - fn editor_selections( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Vec> { - self.update_editor(window, cx, |_, editor, _, _| { + fn editor_selections(&mut self, _: &mut Window, cx: &mut Context) -> Vec> { + self.update_editor(cx, |_, editor, _| { editor .selections .disjoint_anchors() @@ -1300,7 +1295,7 @@ impl Vim { window: &mut Window, cx: &mut Context, ) -> Option { - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { let selection = editor.selections.newest::(cx); let snapshot = &editor.snapshot(window, cx).buffer_snapshot; @@ -1489,7 +1484,7 @@ impl Vim { ) { match self.mode { Mode::VisualLine | Mode::VisualBlock | Mode::Visual => { - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { let original_mode = vim.undo_modes.get(transaction_id); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { match original_mode { @@ -1520,7 +1515,7 @@ impl Vim { self.switch_mode(Mode::Normal, true, window, cx) } Mode::Normal => { - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.move_with(|map, selection| { selection @@ -1547,7 +1542,7 @@ impl Vim { self.current_anchor = Some(newest); } else if self.current_anchor.as_ref().unwrap() != &newest { if let Some(tx_id) = self.current_tx.take() { - self.update_editor(window, cx, |_, editor, _, cx| { + self.update_editor(cx, |_, editor, cx| { editor.group_until_transaction(tx_id, cx) }); } @@ -1694,7 +1689,7 @@ impl Vim { } Some(Operator::Register) => match self.mode { Mode::Insert => { - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { if let Some(register) = Vim::update_globals(cx, |globals, cx| { globals.read_register(text.chars().next(), Some(editor), cx) }) { @@ -1720,7 +1715,7 @@ impl Vim { } if self.mode == Mode::Normal { - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.accept_edit_prediction( &editor::actions::AcceptEditPrediction {}, window, @@ -1733,7 +1728,7 @@ impl Vim { } fn sync_vim_settings(&mut self, window: &mut Window, cx: &mut Context) { - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { editor.set_cursor_shape(vim.cursor_shape(cx), cx); editor.set_clip_at_line_ends(vim.clip_at_line_ends(), cx); editor.set_collapse_matches(true); diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index ca8734ba8b..7bfd8dc8be 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -104,7 +104,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { let count = Vim::take_count(cx).unwrap_or(1); Vim::take_forced_motion(cx); for _ in 0..count { - vim.update_editor(window, cx, |_, editor, window, cx| { + vim.update_editor(cx, |_, editor, cx| { editor.select_larger_syntax_node(&Default::default(), window, cx); }); } @@ -117,7 +117,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { let count = Vim::take_count(cx).unwrap_or(1); Vim::take_forced_motion(cx); for _ in 0..count { - vim.update_editor(window, cx, |_, editor, window, cx| { + vim.update_editor(cx, |_, editor, cx| { editor.select_smaller_syntax_node(&Default::default(), window, cx); }); } @@ -129,7 +129,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { return; }; let marks = vim - .update_editor(window, cx, |vim, editor, window, cx| { + .update_editor(cx, |vim, editor, cx| { vim.get_mark("<", editor, window, cx) .zip(vim.get_mark(">", editor, window, cx)) }) @@ -148,7 +148,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { vim.create_visual_marks(vim.mode, window, cx); } - vim.update_editor(window, cx, |_, editor, window, cx| { + vim.update_editor(cx, |_, editor, cx| { editor.set_clip_at_line_ends(false, cx); editor.change_selections(Default::default(), window, cx, |s| { let map = s.display_map(); @@ -189,7 +189,7 @@ impl Vim { window: &mut Window, cx: &mut Context, ) { - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { let text_layout_details = editor.text_layout_details(window); if vim.mode == Mode::VisualBlock && !matches!( @@ -397,7 +397,7 @@ impl Vim { self.switch_mode(target_mode, true, window, cx); } - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { let mut mut_selection = selection.clone(); @@ -475,7 +475,7 @@ impl Vim { window: &mut Window, cx: &mut Context, ) { - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.split_selection_into_lines(&Default::default(), window, cx); editor.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(|map, cursor, _| { @@ -493,7 +493,7 @@ impl Vim { window: &mut Window, cx: &mut Context, ) { - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.split_selection_into_lines(&Default::default(), window, cx); editor.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(|map, cursor, _| { @@ -517,7 +517,7 @@ impl Vim { } pub fn other_end(&mut self, _: &OtherEnd, window: &mut Window, cx: &mut Context) { - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.change_selections(Default::default(), window, cx, |s| { s.move_with(|_, selection| { selection.reversed = !selection.reversed; @@ -533,7 +533,7 @@ impl Vim { cx: &mut Context, ) { let mode = self.mode; - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.change_selections(Default::default(), window, cx, |s| { s.move_with(|_, selection| { selection.reversed = !selection.reversed; @@ -547,7 +547,7 @@ impl Vim { pub fn visual_delete(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context) { self.store_visual_marks(window, cx); - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { let mut original_columns: HashMap<_, _> = Default::default(); let line_mode = line_mode || editor.selections.line_mode; editor.selections.line_mode = false; @@ -631,7 +631,7 @@ impl Vim { pub fn visual_yank(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context) { self.store_visual_marks(window, cx); - self.update_editor(window, cx, |vim, editor, window, cx| { + self.update_editor(cx, |vim, editor, cx| { let line_mode = line_mode || editor.selections.line_mode; // For visual line mode, adjust selections to avoid yanking the next line when on \n @@ -679,7 +679,7 @@ impl Vim { cx: &mut Context, ) { self.stop_recording(cx); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { let (display_map, selections) = editor.selections.all_adjusted_display(cx); @@ -722,7 +722,7 @@ impl Vim { Vim::take_forced_motion(cx); let count = Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 }); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { editor.set_clip_at_line_ends(false, cx); for _ in 0..count { if editor @@ -745,7 +745,7 @@ impl Vim { Vim::take_forced_motion(cx); let count = Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 }); - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { for _ in 0..count { if editor .select_previous(&Default::default(), window, cx) @@ -773,7 +773,7 @@ impl Vim { let mut start_selection = 0usize; let mut end_selection = 0usize; - self.update_editor(window, cx, |_, editor, _, _| { + self.update_editor(cx, |_, editor, _| { editor.set_collapse_matches(false); }); if vim_is_normal { @@ -791,7 +791,7 @@ impl Vim { } }); } - self.update_editor(window, cx, |_, editor, _, cx| { + self.update_editor(cx, |_, editor, cx| { let latest = editor.selections.newest::(cx); start_selection = latest.start; end_selection = latest.end; @@ -812,7 +812,7 @@ impl Vim { self.stop_replaying(cx); return; } - self.update_editor(window, cx, |_, editor, window, cx| { + self.update_editor(cx, |_, editor, cx| { let latest = editor.selections.newest::(cx); if vim_is_normal { start_selection = latest.start; From b35e69692de2a5bd3c04e04d047ac1e9b29b12d8 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 11 Aug 2025 22:06:02 -0500 Subject: [PATCH 266/693] docs: Add a missing comma in Rust debugging JSON (#36007) Update the Rust debugging doc to include a missing comma in one of the example JSON's. --- docs/src/languages/rust.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/languages/rust.md b/docs/src/languages/rust.md index 1ee25a37b5..7695280275 100644 --- a/docs/src/languages/rust.md +++ b/docs/src/languages/rust.md @@ -326,7 +326,7 @@ When you use `cargo build` or `cargo test` as the build command, Zed can infer t [ { "label": "Build & Debug native binary", - "adapter": "CodeLLDB" + "adapter": "CodeLLDB", "build": { "command": "cargo", "args": ["build"] From 481e3e5092511222376a9fa1dcf2254e09a29a85 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 12 Aug 2025 07:53:20 +0300 Subject: [PATCH 267/693] Ignore capability registrations with empty capabilities (#36000) --- crates/project/src/lsp_store.rs | 268 ++++++++++++++++---------------- 1 file changed, 133 insertions(+), 135 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index de6544f5a2..827341d60d 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3367,20 +3367,6 @@ impl LocalLspStore { } } -fn parse_register_capabilities( - reg: lsp::Registration, -) -> anyhow::Result> { - let caps = match reg - .register_options - .map(|options| serde_json::from_value::(options)) - .transpose()? - { - None => OneOf::Left(true), - Some(options) => OneOf::Right(options), - }; - Ok(caps) -} - fn notify_server_capabilities_updated(server: &LanguageServer, cx: &mut Context) { if let Some(capabilities) = serde_json::to_string(&server.capabilities()).ok() { cx.emit(LspStoreEvent::LanguageServerUpdate { @@ -11690,190 +11676,190 @@ impl LspStore { // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. } "workspace/symbol" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.workspace_symbol_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.workspace_symbol_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "workspace/fileOperations" => { - let caps = reg - .register_options - .map(serde_json::from_value) - .transpose()? - .unwrap_or_default(); - server.update_capabilities(|capabilities| { - capabilities - .workspace - .get_or_insert_default() - .file_operations = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = reg.register_options { + let caps = serde_json::from_value(options)?; + server.update_capabilities(|capabilities| { + capabilities + .workspace + .get_or_insert_default() + .file_operations = Some(caps); + }); + notify_server_capabilities_updated(&server, cx); + } } "workspace/executeCommand" => { - let options = reg - .register_options - .map(serde_json::from_value) - .transpose()? - .unwrap_or_default(); - server.update_capabilities(|capabilities| { - capabilities.execute_command_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = reg.register_options { + let options = serde_json::from_value(options)?; + server.update_capabilities(|capabilities| { + capabilities.execute_command_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/rangeFormatting" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.document_range_formatting_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.document_range_formatting_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/onTypeFormatting" => { - let options = reg + if let Some(options) = reg .register_options .map(serde_json::from_value) .transpose()? - .unwrap_or_default(); - server.update_capabilities(|capabilities| { - capabilities.document_on_type_formatting_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + { + server.update_capabilities(|capabilities| { + capabilities.document_on_type_formatting_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/formatting" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.document_formatting_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.document_formatting_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/rename" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.rename_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.rename_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/inlayHint" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.inlay_hint_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.inlay_hint_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/documentSymbol" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.document_symbol_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.document_symbol_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/codeAction" => { - let options = reg + if let Some(options) = reg .register_options .map(serde_json::from_value) - .transpose()?; - let provider_capability = match options { - None => lsp::CodeActionProviderCapability::Simple(true), - Some(options) => lsp::CodeActionProviderCapability::Options(options), - }; - server.update_capabilities(|capabilities| { - capabilities.code_action_provider = Some(provider_capability); - }); - notify_server_capabilities_updated(&server, cx); + .transpose()? + { + server.update_capabilities(|capabilities| { + capabilities.code_action_provider = + Some(lsp::CodeActionProviderCapability::Options(options)); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/definition" => { - let caps = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.definition_provider = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.definition_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/completion" => { - let caps = reg + if let Some(caps) = reg .register_options .map(serde_json::from_value) .transpose()? - .unwrap_or_default(); - server.update_capabilities(|capabilities| { - capabilities.completion_provider = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); + { + server.update_capabilities(|capabilities| { + capabilities.completion_provider = Some(caps); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/hover" => { - let caps = reg + if let Some(caps) = reg .register_options .map(serde_json::from_value) .transpose()? - .unwrap_or_else(|| lsp::HoverProviderCapability::Simple(true)); - server.update_capabilities(|capabilities| { - capabilities.hover_provider = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); + { + server.update_capabilities(|capabilities| { + capabilities.hover_provider = Some(caps); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/signatureHelp" => { - let caps = reg + if let Some(caps) = reg .register_options .map(serde_json::from_value) .transpose()? - .unwrap_or_default(); - server.update_capabilities(|capabilities| { - capabilities.signature_help_provider = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); + { + server.update_capabilities(|capabilities| { + capabilities.signature_help_provider = Some(caps); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/synchronization" => { - let caps = reg + if let Some(caps) = reg .register_options .map(serde_json::from_value) .transpose()? - .unwrap_or_else(|| { - lsp::TextDocumentSyncCapability::Options( - lsp::TextDocumentSyncOptions::default(), - ) + { + server.update_capabilities(|capabilities| { + capabilities.text_document_sync = Some(caps); }); - server.update_capabilities(|capabilities| { - capabilities.text_document_sync = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/codeLens" => { - let caps = reg + if let Some(caps) = reg .register_options .map(serde_json::from_value) .transpose()? - .unwrap_or_else(|| lsp::CodeLensOptions { - resolve_provider: None, + { + server.update_capabilities(|capabilities| { + capabilities.code_lens_provider = Some(caps); }); - server.update_capabilities(|capabilities| { - capabilities.code_lens_provider = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/diagnostic" => { - let caps = reg + if let Some(caps) = reg .register_options .map(serde_json::from_value) .transpose()? - .unwrap_or_else(|| { - lsp::DiagnosticServerCapabilities::RegistrationOptions( - lsp::DiagnosticRegistrationOptions::default(), - ) + { + server.update_capabilities(|capabilities| { + capabilities.diagnostic_provider = Some(caps); }); - server.update_capabilities(|capabilities| { - capabilities.diagnostic_provider = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/colorProvider" => { - let caps = reg + if let Some(caps) = reg .register_options .map(serde_json::from_value) .transpose()? - .unwrap_or_else(|| lsp::ColorProviderCapability::Simple(true)); - server.update_capabilities(|capabilities| { - capabilities.color_provider = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); + { + server.update_capabilities(|capabilities| { + capabilities.color_provider = Some(caps); + }); + notify_server_capabilities_updated(&server, cx); + } } _ => log::warn!("unhandled capability registration: {reg:?}"), } @@ -12016,6 +12002,18 @@ impl LspStore { } } +// Registration with empty capabilities should be ignored. +// https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/formatting.ts#L67-L70 +fn parse_register_capabilities( + reg: lsp::Registration, +) -> anyhow::Result>> { + Ok(reg + .register_options + .map(|options| serde_json::from_value::(options)) + .transpose()? + .map(OneOf::Right)) +} + fn subscribe_to_binary_statuses( languages: &Arc, cx: &mut Context<'_, LspStore>, From 1a798830cb23586183f9a08048ac1d769cbbed8b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 11 Aug 2025 23:08:58 -0600 Subject: [PATCH 268/693] Fix running vim tests with --features neovim (#36014) This was broken incidentally in https://github.com/zed-industries/zed/pull/33417 A better fix would be to fix app shutdown to take control of the executor so that we *can* run foreground tasks; but that is a bit fiddly (draft #36015) Release Notes: - N/A --- Cargo.lock | 1 + crates/gpui_macros/src/test.rs | 4 +++- crates/vim/Cargo.toml | 1 + crates/vim/src/test/vim_test_context.rs | 1 + 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 8a3e319a57..8d22eeafab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18025,6 +18025,7 @@ dependencies = [ "command_palette_hooks", "db", "editor", + "env_logger 0.11.8", "futures 0.3.31", "git_ui", "gpui", diff --git a/crates/gpui_macros/src/test.rs b/crates/gpui_macros/src/test.rs index 2c52149897..adb27f42ea 100644 --- a/crates/gpui_macros/src/test.rs +++ b/crates/gpui_macros/src/test.rs @@ -167,6 +167,7 @@ fn generate_test_function( )); cx_teardowns.extend(quote!( dispatcher.run_until_parked(); + #cx_varname.executor().forbid_parking(); #cx_varname.quit(); dispatcher.run_until_parked(); )); @@ -232,7 +233,7 @@ fn generate_test_function( cx_teardowns.extend(quote!( drop(#cx_varname_lock); dispatcher.run_until_parked(); - #cx_varname.update(|cx| { cx.quit() }); + #cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); }); dispatcher.run_until_parked(); )); continue; @@ -247,6 +248,7 @@ fn generate_test_function( )); cx_teardowns.extend(quote!( dispatcher.run_until_parked(); + #cx_varname.executor().forbid_parking(); #cx_varname.quit(); dispatcher.run_until_parked(); )); diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 9fb5c46564..434b14b07c 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -24,6 +24,7 @@ command_palette.workspace = true command_palette_hooks.workspace = true db.workspace = true editor.workspace = true +env_logger.workspace = true futures.workspace = true gpui.workspace = true itertools.workspace = true diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index b8988b1d1f..904e48e5a3 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -15,6 +15,7 @@ impl VimTestContext { if cx.has_global::() { return; } + env_logger::try_init().ok(); cx.update(|cx| { let settings = SettingsStore::test(cx); cx.set_global(settings); From 52a9101970bc2994945445b8b7bdecb1ac43f35d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 11 Aug 2025 23:20:09 -0600 Subject: [PATCH 269/693] vim: Add ctrl-y/e in insert mode (#36017) Closes #17292 Release Notes: - vim: Added ctrl-y/ctrl-e in insert mode to copy the next character from the line above or below --- assets/keymaps/vim.json | 4 ++ crates/vim/src/insert.rs | 46 +++++++++++++++++++- crates/vim/test_data/test_insert_ctrl_y.json | 5 +++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 crates/vim/test_data/test_insert_ctrl_y.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 57edb1e4c1..98f9cafc40 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -333,10 +333,14 @@ "ctrl-x ctrl-c": "editor::ShowEditPrediction", // zed specific "ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific "ctrl-x ctrl-z": "editor::Cancel", + "ctrl-x ctrl-e": "vim::LineDown", + "ctrl-x ctrl-y": "vim::LineUp", "ctrl-w": "editor::DeleteToPreviousWordStart", "ctrl-u": "editor::DeleteToBeginningOfLine", "ctrl-t": "vim::Indent", "ctrl-d": "vim::Outdent", + "ctrl-y": "vim::InsertFromAbove", + "ctrl-e": "vim::InsertFromBelow", "ctrl-k": ["vim::PushDigraph", {}], "ctrl-v": ["vim::PushLiteral", {}], "ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use. diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index 584057a8c0..8ef1cd7811 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -3,7 +3,9 @@ use editor::{Bias, Editor}; use gpui::{Action, Context, Window, actions}; use language::SelectionGoal; use settings::Settings; +use text::Point; use vim_mode_setting::HelixModeSetting; +use workspace::searchable::Direction; actions!( vim, @@ -11,13 +13,23 @@ actions!( /// Switches to normal mode with cursor positioned before the current character. NormalBefore, /// Temporarily switches to normal mode for one command. - TemporaryNormal + TemporaryNormal, + /// Inserts the next character from the line above into the current line. + InsertFromAbove, + /// Inserts the next character from the line below into the current line. + InsertFromBelow ] ); pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, Vim::normal_before); Vim::action(editor, cx, Vim::temporary_normal); + Vim::action(editor, cx, |vim, _: &InsertFromAbove, window, cx| { + vim.insert_around(Direction::Prev, window, cx) + }); + Vim::action(editor, cx, |vim, _: &InsertFromBelow, window, cx| { + vim.insert_around(Direction::Next, window, cx) + }) } impl Vim { @@ -71,6 +83,29 @@ impl Vim { self.switch_mode(Mode::Normal, true, window, cx); self.temp_mode = true; } + + fn insert_around(&mut self, direction: Direction, _: &mut Window, cx: &mut Context) { + self.update_editor(cx, |_, editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let mut edits = Vec::new(); + for selection in editor.selections.all::(cx) { + let point = selection.head(); + let new_row = match direction { + Direction::Next => point.row + 1, + Direction::Prev if point.row > 0 => point.row - 1, + _ => continue, + }; + let source = snapshot.clip_point(Point::new(new_row, point.column), Bias::Left); + if let Some(c) = snapshot.chars_at(source).next() + && c != '\n' + { + edits.push((point..point, c.to_string())) + } + } + + editor.edit(edits, cx); + }); + } } #[cfg(test)] @@ -156,4 +191,13 @@ mod test { .await; cx.shared_state().await.assert_eq("hehello\nˇllo\n"); } + + #[gpui::test] + async fn test_insert_ctrl_y(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("hello\nˇ\nworld").await; + cx.simulate_shared_keystrokes("i ctrl-y ctrl-e").await; + cx.shared_state().await.assert_eq("hello\nhoˇ\nworld"); + } } diff --git a/crates/vim/test_data/test_insert_ctrl_y.json b/crates/vim/test_data/test_insert_ctrl_y.json new file mode 100644 index 0000000000..09b707a198 --- /dev/null +++ b/crates/vim/test_data/test_insert_ctrl_y.json @@ -0,0 +1,5 @@ +{"Put":{"state":"hello\nˇ\nworld"}} +{"Key":"i"} +{"Key":"ctrl-y"} +{"Key":"ctrl-e"} +{"Get":{"state":"hello\nhoˇ\nworld","mode":"Insert"}} From cc5eb2406691765ff624d217bc32b07519941280 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Tue, 12 Aug 2025 00:47:54 -0600 Subject: [PATCH 270/693] zeta: Add latency telemetry for 1% of edit predictions (#36020) Release Notes: - N/A Co-authored-by: Oleksiy --- Cargo.lock | 1 + crates/zeta/Cargo.toml | 3 ++- crates/zeta/src/zeta.rs | 24 ++++++++++++++++++++++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8d22eeafab..79bce189e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20923,6 +20923,7 @@ dependencies = [ "menu", "postage", "project", + "rand 0.8.5", "regex", "release_channel", "reqwest_client", diff --git a/crates/zeta/Cargo.toml b/crates/zeta/Cargo.toml index 9f1d02b790..ee76308ff3 100644 --- a/crates/zeta/Cargo.toml +++ b/crates/zeta/Cargo.toml @@ -26,6 +26,7 @@ collections.workspace = true command_palette_hooks.workspace = true copilot.workspace = true db.workspace = true +edit_prediction.workspace = true editor.workspace = true feature_flags.workspace = true fs.workspace = true @@ -33,13 +34,13 @@ futures.workspace = true gpui.workspace = true http_client.workspace = true indoc.workspace = true -edit_prediction.workspace = true language.workspace = true language_model.workspace = true log.workspace = true menu.workspace = true postage.workspace = true project.workspace = true +rand.workspace = true regex.workspace = true release_channel.workspace = true serde.workspace = true diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 6900082003..1a6a8c2934 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -429,6 +429,7 @@ impl Zeta { body, editable_range, } = gather_task.await?; + let done_gathering_context_at = Instant::now(); log::debug!( "Events:\n{}\nExcerpt:\n{:?}", @@ -481,6 +482,7 @@ impl Zeta { } }; + let received_response_at = Instant::now(); log::debug!("completion response: {}", &response.output_excerpt); if let Some(usage) = usage { @@ -492,7 +494,7 @@ impl Zeta { .ok(); } - Self::process_completion_response( + let edit_prediction = Self::process_completion_response( response, buffer, &snapshot, @@ -505,7 +507,25 @@ impl Zeta { buffer_snapshotted_at, &cx, ) - .await + .await; + + let finished_at = Instant::now(); + + // record latency for ~1% of requests + if rand::random::() <= 2 { + telemetry::event!( + "Edit Prediction Request", + context_latency = done_gathering_context_at + .duration_since(buffer_snapshotted_at) + .as_millis(), + request_latency = received_response_at + .duration_since(done_gathering_context_at) + .as_millis(), + process_latency = finished_at.duration_since(received_response_at).as_millis() + ); + } + + edit_prediction }) } From b61b71405d4a2d7725642ccbdda6c387efcc9693 Mon Sep 17 00:00:00 2001 From: Lukas Spiss <35728419+Spissable@users.noreply.github.com> Date: Tue, 12 Aug 2025 09:56:33 +0100 Subject: [PATCH 271/693] go: Add support for running sub-tests in table tests (#35657) One killer feature for the Go runner is to execute individual subtests within a table-test easily. Goland has had this feature forever, while in VSCode this has been notably missing. https://github.com/user-attachments/assets/363417a2-d1b1-43ca-8377-08ce062d6104 Release Notes: - Added support to run Go table-test subtests. --- crates/languages/src/go.rs | 345 +++++++++++++++++++++++++- crates/languages/src/go/runnables.scm | 100 ++++++++ 2 files changed, 439 insertions(+), 6 deletions(-) diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 16c1b67203..14f646133b 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -487,6 +487,8 @@ const GO_MODULE_ROOT_TASK_VARIABLE: VariableName = VariableName::Custom(Cow::Borrowed("GO_MODULE_ROOT")); const GO_SUBTEST_NAME_TASK_VARIABLE: VariableName = VariableName::Custom(Cow::Borrowed("GO_SUBTEST_NAME")); +const GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE: VariableName = + VariableName::Custom(Cow::Borrowed("GO_TABLE_TEST_CASE_NAME")); impl ContextProvider for GoContextProvider { fn build_context( @@ -545,10 +547,19 @@ impl ContextProvider for GoContextProvider { let go_subtest_variable = extract_subtest_name(_subtest_name.unwrap_or("")) .map(|subtest_name| (GO_SUBTEST_NAME_TASK_VARIABLE.clone(), subtest_name)); + let table_test_case_name = variables.get(&VariableName::Custom(Cow::Borrowed( + "_table_test_case_name", + ))); + + let go_table_test_case_variable = table_test_case_name + .and_then(extract_subtest_name) + .map(|case_name| (GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE.clone(), case_name)); + Task::ready(Ok(TaskVariables::from_iter( [ go_package_variable, go_subtest_variable, + go_table_test_case_variable, go_module_root_variable, ] .into_iter() @@ -570,6 +581,28 @@ impl ContextProvider for GoContextProvider { let module_cwd = Some(GO_MODULE_ROOT_TASK_VARIABLE.template_value()); Task::ready(Some(TaskTemplates(vec![ + TaskTemplate { + label: format!( + "go test {} -v -run {}/{}", + GO_PACKAGE_TASK_VARIABLE.template_value(), + VariableName::Symbol.template_value(), + GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE.template_value(), + ), + command: "go".into(), + args: vec![ + "test".into(), + "-v".into(), + "-run".into(), + format!( + "\\^{}\\$/\\^{}\\$", + VariableName::Symbol.template_value(), + GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE.template_value(), + ), + ], + cwd: package_cwd.clone(), + tags: vec!["go-table-test-case".to_owned()], + ..TaskTemplate::default() + }, TaskTemplate { label: format!( "go test {} -run {}", @@ -842,10 +875,21 @@ mod tests { .collect() }); + let tag_strings: Vec = runnables + .iter() + .flat_map(|r| &r.runnable.tags) + .map(|tag| tag.0.to_string()) + .collect(); + assert!( - runnables.len() == 2, - "Should find test function and subtest with double quotes, found: {}", - runnables.len() + tag_strings.contains(&"go-test".to_string()), + "Should find go-test tag, found: {:?}", + tag_strings + ); + assert!( + tag_strings.contains(&"go-subtest".to_string()), + "Should find go-subtest tag, found: {:?}", + tag_strings ); let buffer = cx.new(|cx| { @@ -860,10 +904,299 @@ mod tests { .collect() }); + let tag_strings: Vec = runnables + .iter() + .flat_map(|r| &r.runnable.tags) + .map(|tag| tag.0.to_string()) + .collect(); + assert!( - runnables.len() == 2, - "Should find test function and subtest with backticks, found: {}", - runnables.len() + tag_strings.contains(&"go-test".to_string()), + "Should find go-test tag, found: {:?}", + tag_strings + ); + assert!( + tag_strings.contains(&"go-subtest".to_string()), + "Should find go-subtest tag, found: {:?}", + tag_strings + ); + } + + #[gpui::test] + fn test_go_table_test_slice_detection(cx: &mut TestAppContext) { + let language = language("go", tree_sitter_go::LANGUAGE.into()); + + let table_test = r#" + package main + + import "testing" + + func TestExample(t *testing.T) { + _ = "some random string" + + testCases := []struct{ + name string + anotherStr string + }{ + { + name: "test case 1", + anotherStr: "foo", + }, + { + name: "test case 2", + anotherStr: "bar", + }, + } + + notATableTest := []struct{ + name string + }{ + { + name: "some string", + }, + { + name: "some other string", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // test code here + }) + } + } + "#; + + let buffer = + cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx)); + cx.executor().run_until_parked(); + + let runnables: Vec<_> = buffer.update(cx, |buffer, _| { + let snapshot = buffer.snapshot(); + snapshot.runnable_ranges(0..table_test.len()).collect() + }); + + let tag_strings: Vec = runnables + .iter() + .flat_map(|r| &r.runnable.tags) + .map(|tag| tag.0.to_string()) + .collect(); + + assert!( + tag_strings.contains(&"go-test".to_string()), + "Should find go-test tag, found: {:?}", + tag_strings + ); + assert!( + tag_strings.contains(&"go-table-test-case".to_string()), + "Should find go-table-test-case tag, found: {:?}", + tag_strings + ); + + let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count(); + let go_table_test_count = tag_strings + .iter() + .filter(|&tag| tag == "go-table-test-case") + .count(); + + assert!( + go_test_count == 1, + "Should find exactly 1 go-test, found: {}", + go_test_count + ); + assert!( + go_table_test_count == 2, + "Should find exactly 2 go-table-test-case, found: {}", + go_table_test_count + ); + } + + #[gpui::test] + fn test_go_table_test_slice_ignored(cx: &mut TestAppContext) { + let language = language("go", tree_sitter_go::LANGUAGE.into()); + + let table_test = r#" + package main + + func Example() { + _ = "some random string" + + notATableTest := []struct{ + name string + }{ + { + name: "some string", + }, + { + name: "some other string", + }, + } + } + "#; + + let buffer = + cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx)); + cx.executor().run_until_parked(); + + let runnables: Vec<_> = buffer.update(cx, |buffer, _| { + let snapshot = buffer.snapshot(); + snapshot.runnable_ranges(0..table_test.len()).collect() + }); + + let tag_strings: Vec = runnables + .iter() + .flat_map(|r| &r.runnable.tags) + .map(|tag| tag.0.to_string()) + .collect(); + + assert!( + !tag_strings.contains(&"go-test".to_string()), + "Should find go-test tag, found: {:?}", + tag_strings + ); + assert!( + !tag_strings.contains(&"go-table-test-case".to_string()), + "Should find go-table-test-case tag, found: {:?}", + tag_strings + ); + } + + #[gpui::test] + fn test_go_table_test_map_detection(cx: &mut TestAppContext) { + let language = language("go", tree_sitter_go::LANGUAGE.into()); + + let table_test = r#" + package main + + import "testing" + + func TestExample(t *testing.T) { + _ = "some random string" + + testCases := map[string]struct { + someStr string + fail bool + }{ + "test failure": { + someStr: "foo", + fail: true, + }, + "test success": { + someStr: "bar", + fail: false, + }, + } + + notATableTest := map[string]struct { + someStr string + }{ + "some string": { + someStr: "foo", + }, + "some other string": { + someStr: "bar", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + // test code here + }) + } + } + "#; + + let buffer = + cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx)); + cx.executor().run_until_parked(); + + let runnables: Vec<_> = buffer.update(cx, |buffer, _| { + let snapshot = buffer.snapshot(); + snapshot.runnable_ranges(0..table_test.len()).collect() + }); + + let tag_strings: Vec = runnables + .iter() + .flat_map(|r| &r.runnable.tags) + .map(|tag| tag.0.to_string()) + .collect(); + + assert!( + tag_strings.contains(&"go-test".to_string()), + "Should find go-test tag, found: {:?}", + tag_strings + ); + assert!( + tag_strings.contains(&"go-table-test-case".to_string()), + "Should find go-table-test-case tag, found: {:?}", + tag_strings + ); + + let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count(); + let go_table_test_count = tag_strings + .iter() + .filter(|&tag| tag == "go-table-test-case") + .count(); + + assert!( + go_test_count == 1, + "Should find exactly 1 go-test, found: {}", + go_test_count + ); + assert!( + go_table_test_count == 2, + "Should find exactly 2 go-table-test-case, found: {}", + go_table_test_count + ); + } + + #[gpui::test] + fn test_go_table_test_map_ignored(cx: &mut TestAppContext) { + let language = language("go", tree_sitter_go::LANGUAGE.into()); + + let table_test = r#" + package main + + func Example() { + _ = "some random string" + + notATableTest := map[string]struct { + someStr string + }{ + "some string": { + someStr: "foo", + }, + "some other string": { + someStr: "bar", + }, + } + } + "#; + + let buffer = + cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx)); + cx.executor().run_until_parked(); + + let runnables: Vec<_> = buffer.update(cx, |buffer, _| { + let snapshot = buffer.snapshot(); + snapshot.runnable_ranges(0..table_test.len()).collect() + }); + + let tag_strings: Vec = runnables + .iter() + .flat_map(|r| &r.runnable.tags) + .map(|tag| tag.0.to_string()) + .collect(); + + assert!( + !tag_strings.contains(&"go-test".to_string()), + "Should find go-test tag, found: {:?}", + tag_strings + ); + assert!( + !tag_strings.contains(&"go-table-test-case".to_string()), + "Should find go-table-test-case tag, found: {:?}", + tag_strings ); } diff --git a/crates/languages/src/go/runnables.scm b/crates/languages/src/go/runnables.scm index 6418cd04d8..f56262f799 100644 --- a/crates/languages/src/go/runnables.scm +++ b/crates/languages/src/go/runnables.scm @@ -91,3 +91,103 @@ ) @_ (#set! tag go-main) ) + +; Table test cases - slice and map +( + (short_var_declaration + left: (expression_list (identifier) @_collection_var) + right: (expression_list + (composite_literal + type: [ + (slice_type) + (map_type + key: (type_identifier) @_key_type + (#eq? @_key_type "string") + ) + ] + body: (literal_value + [ + (literal_element + (literal_value + (keyed_element + (literal_element + (identifier) @_field_name + ) + (literal_element + [ + (interpreted_string_literal) @run @_table_test_case_name + (raw_string_literal) @run @_table_test_case_name + ] + ) + ) + ) + ) + (keyed_element + (literal_element + [ + (interpreted_string_literal) @run @_table_test_case_name + (raw_string_literal) @run @_table_test_case_name + ] + ) + ) + ] + ) + ) + ) + ) + (for_statement + (range_clause + left: (expression_list + [ + ( + (identifier) + (identifier) @_loop_var + ) + (identifier) @_loop_var + ] + ) + right: (identifier) @_range_var + (#eq? @_range_var @_collection_var) + ) + body: (block + (expression_statement + (call_expression + function: (selector_expression + operand: (identifier) @_t_var + field: (field_identifier) @_run_method + (#eq? @_run_method "Run") + ) + arguments: (argument_list + . + [ + (selector_expression + operand: (identifier) @_tc_var + (#eq? @_tc_var @_loop_var) + field: (field_identifier) @_field_check + (#eq? @_field_check @_field_name) + ) + (identifier) @_arg_var + (#eq? @_arg_var @_loop_var) + ] + . + (func_literal + parameters: (parameter_list + (parameter_declaration + type: (pointer_type + (qualified_type + package: (package_identifier) @_pkg + name: (type_identifier) @_type + (#eq? @_pkg "testing") + (#eq? @_type "T") + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) @_ + (#set! tag go-table-test-case) +) From 13bf45dd4a773bd31a907698d0498a5ee745729f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 12 Aug 2025 14:10:53 +0200 Subject: [PATCH 272/693] python: Fix toolchain serialization not working with multiple venvs in a single worktree (#36035) Our database did not allow more than entry for a given toolchain for a single worktree (due to incorrect primary key) Co-authored-by: Lukas Wirth Release Notes: - Python: Fixed toolchain selector not working with multiple venvs in a single worktree. Co-authored-by: Lukas Wirth --- crates/workspace/src/persistence.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 6fa5c969e7..b2d1340a7b 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -542,6 +542,20 @@ define_connection! { ALTER TABLE breakpoints ADD COLUMN condition TEXT; ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT; ), + sql!(CREATE TABLE toolchains2 ( + workspace_id INTEGER, + worktree_id INTEGER, + language_name TEXT NOT NULL, + name TEXT NOT NULL, + path TEXT NOT NULL, + raw_json TEXT NOT NULL, + relative_worktree_path TEXT NOT NULL, + PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT; + INSERT INTO toolchains2 + SELECT * FROM toolchains; + DROP TABLE toolchains; + ALTER TABLE toolchains2 RENAME TO toolchains; + ) ]; } @@ -1428,12 +1442,12 @@ impl WorkspaceDb { self.write(move |conn| { let mut insert = conn .exec_bound(sql!( - INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path) VALUES (?, ?, ?, ?, ?, ?) + INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT DO UPDATE SET name = ?5, - path = ?6 - + path = ?6, + raw_json = ?7 )) .context("Preparing insertion")?; @@ -1444,6 +1458,7 @@ impl WorkspaceDb { toolchain.language_name.as_ref(), toolchain.name.as_ref(), toolchain.path.as_ref(), + toolchain.as_json.to_string(), ))?; Ok(()) From 244432175669cf3bc4c1c49c794692e8f0947fd3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 12 Aug 2025 14:17:48 +0200 Subject: [PATCH 273/693] Support profiles in agent2 (#36034) We still need a profile selector. Release Notes: - N/A --------- Co-authored-by: Ben Brandt --- Cargo.lock | 1 + crates/acp_thread/src/acp_thread.rs | 51 ++++ crates/agent2/Cargo.toml | 2 + crates/agent2/src/agent.rs | 34 ++- crates/agent2/src/tests/mod.rs | 142 +++++++++-- crates/agent2/src/thread.rs | 87 +++++-- crates/agent2/src/tools.rs | 2 + .../src/tools/context_server_registry.rs | 231 ++++++++++++++++++ crates/agent2/src/tools/diagnostics_tool.rs | 18 +- crates/agent2/src/tools/edit_file_tool.rs | 66 ++++- crates/agent2/src/tools/fetch_tool.rs | 8 +- crates/agent2/src/tools/find_path_tool.rs | 3 - crates/agent2/src/tools/grep_tool.rs | 25 +- crates/agent2/src/tools/now_tool.rs | 11 +- crates/agent_settings/src/agent_profile.rs | 14 ++ 15 files changed, 587 insertions(+), 108 deletions(-) create mode 100644 crates/agent2/src/tools/context_server_registry.rs diff --git a/Cargo.lock b/Cargo.lock index 79bce189e2..dc28a1cb44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -196,6 +196,7 @@ dependencies = [ "clock", "cloud_llm_client", "collections", + "context_server", "ctor", "editor", "env_logger 0.11.8", diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index d632e6e570..1c0a9479df 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -254,6 +254,15 @@ impl ToolCall { } if let Some(raw_output) = raw_output { + if self.content.is_empty() { + if let Some(markdown) = markdown_for_raw_output(&raw_output, &language_registry, cx) + { + self.content + .push(ToolCallContent::ContentBlock(ContentBlock::Markdown { + markdown, + })); + } + } self.raw_output = Some(raw_output); } } @@ -1266,6 +1275,48 @@ impl AcpThread { } } +fn markdown_for_raw_output( + raw_output: &serde_json::Value, + language_registry: &Arc, + cx: &mut App, +) -> Option> { + match raw_output { + serde_json::Value::Null => None, + serde_json::Value::Bool(value) => Some(cx.new(|cx| { + Markdown::new( + value.to_string().into(), + Some(language_registry.clone()), + None, + cx, + ) + })), + serde_json::Value::Number(value) => Some(cx.new(|cx| { + Markdown::new( + value.to_string().into(), + Some(language_registry.clone()), + None, + cx, + ) + })), + serde_json::Value::String(value) => Some(cx.new(|cx| { + Markdown::new( + value.clone().into(), + Some(language_registry.clone()), + None, + cx, + ) + })), + value => Some(cx.new(|cx| { + Markdown::new( + format!("```json\n{}\n```", value).into(), + Some(language_registry.clone()), + None, + cx, + ) + })), + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 7ee48aca04..1030380dc0 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -23,6 +23,7 @@ assistant_tools.workspace = true chrono.workspace = true cloud_llm_client.workspace = true collections.workspace = true +context_server.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true @@ -60,6 +61,7 @@ workspace-hack.workspace = true ctor.workspace = true client = { workspace = true, "features" = ["test-support"] } clock = { workspace = true, "features" = ["test-support"] } +context_server = { workspace = true, "features" = ["test-support"] } editor = { workspace = true, "features" = ["test-support"] } env_logger.workspace = true fs = { workspace = true, "features" = ["test-support"] } diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 66893f49f9..18a830b978 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,8 +1,8 @@ use crate::{AgentResponseEvent, Thread, templates::Templates}; use crate::{ - CopyPathTool, CreateDirectoryTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, - GrepTool, ListDirectoryTool, MovePathTool, NowTool, OpenTool, ReadFileTool, TerminalTool, - ThinkingTool, ToolCallAuthorization, WebSearchTool, + ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DiagnosticsTool, EditFileTool, + FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MovePathTool, NowTool, OpenTool, + ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, WebSearchTool, }; use acp_thread::ModelSelector; use agent_client_protocol as acp; @@ -55,6 +55,7 @@ pub struct NativeAgent { project_context: Rc>, project_context_needs_refresh: watch::Sender<()>, _maintain_project_context: Task>, + context_server_registry: Entity, /// Shared templates for all threads templates: Arc, project: Entity, @@ -90,6 +91,9 @@ impl NativeAgent { _maintain_project_context: cx.spawn(async move |this, cx| { Self::maintain_project_context(this, project_context_needs_refresh_rx, cx).await }), + context_server_registry: cx.new(|cx| { + ContextServerRegistry::new(project.read(cx).context_server_store(), cx) + }), templates, project, prompt_store, @@ -385,7 +389,13 @@ impl acp_thread::AgentConnection for NativeAgentConnection { // Create AcpThread let acp_thread = cx.update(|cx| { cx.new(|cx| { - acp_thread::AcpThread::new("agent2", self.clone(), project.clone(), session_id.clone(), cx) + acp_thread::AcpThread::new( + "agent2", + self.clone(), + project.clone(), + session_id.clone(), + cx, + ) }) })?; let action_log = cx.update(|cx| acp_thread.read(cx).action_log().clone())?; @@ -413,11 +423,21 @@ impl acp_thread::AgentConnection for NativeAgentConnection { }) .ok_or_else(|| { log::warn!("No default model configured in settings"); - anyhow!("No default model configured. Please configure a default model in settings.") + anyhow!( + "No default model. Please configure a default model in settings." + ) })?; let thread = cx.new(|cx| { - let mut thread = Thread::new(project.clone(), agent.project_context.clone(), action_log.clone(), agent.templates.clone(), default_model); + let mut thread = Thread::new( + project.clone(), + agent.project_context.clone(), + agent.context_server_registry.clone(), + action_log.clone(), + agent.templates.clone(), + default_model, + cx, + ); thread.add_tool(CreateDirectoryTool::new(project.clone())); thread.add_tool(CopyPathTool::new(project.clone())); thread.add_tool(DiagnosticsTool::new(project.clone())); @@ -450,7 +470,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { acp_thread: acp_thread.downgrade(), _subscription: cx.observe_release(&acp_thread, |this, acp_thread, _cx| { this.sessions.remove(acp_thread.session_id()); - }) + }), }, ); })?; diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index d6aaddf2c2..7f4b934c08 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -2,6 +2,7 @@ use super::*; use acp_thread::AgentConnection; use action_log::ActionLog; use agent_client_protocol::{self as acp}; +use agent_settings::AgentProfileId; use anyhow::Result; use client::{Client, UserStore}; use fs::{FakeFs, Fs}; @@ -165,7 +166,9 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) { } else { false } - }) + }), + "{}", + thread.to_markdown() ); }); } @@ -469,6 +472,82 @@ async fn test_concurrent_tool_calls(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_profiles(cx: &mut TestAppContext) { + let ThreadTest { + model, thread, fs, .. + } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + thread.update(cx, |thread, _cx| { + thread.add_tool(DelayTool); + thread.add_tool(EchoTool); + thread.add_tool(InfiniteTool); + }); + + // Override profiles and wait for settings to be loaded. + fs.insert_file( + paths::settings_file(), + json!({ + "agent": { + "profiles": { + "test-1": { + "name": "Test Profile 1", + "tools": { + EchoTool.name(): true, + DelayTool.name(): true, + } + }, + "test-2": { + "name": "Test Profile 2", + "tools": { + InfiniteTool.name(): true, + } + } + } + } + }) + .to_string() + .into_bytes(), + ) + .await; + cx.run_until_parked(); + + // Test that test-1 profile (default) has echo and delay tools + thread.update(cx, |thread, cx| { + thread.set_profile(AgentProfileId("test-1".into())); + thread.send("test", cx); + }); + cx.run_until_parked(); + + let mut pending_completions = fake_model.pending_completions(); + assert_eq!(pending_completions.len(), 1); + let completion = pending_completions.pop().unwrap(); + let tool_names: Vec = completion + .tools + .iter() + .map(|tool| tool.name.clone()) + .collect(); + assert_eq!(tool_names, vec![DelayTool.name(), EchoTool.name()]); + fake_model.end_last_completion_stream(); + + // Switch to test-2 profile, and verify that it has only the infinite tool. + thread.update(cx, |thread, cx| { + thread.set_profile(AgentProfileId("test-2".into())); + thread.send("test2", cx) + }); + cx.run_until_parked(); + let mut pending_completions = fake_model.pending_completions(); + assert_eq!(pending_completions.len(), 1); + let completion = pending_completions.pop().unwrap(); + let tool_names: Vec = completion + .tools + .iter() + .map(|tool| tool.name.clone()) + .collect(); + assert_eq!(tool_names, vec![InfiniteTool.name()]); +} + #[gpui::test] #[ignore = "can't run on CI yet"] async fn test_cancellation(cx: &mut TestAppContext) { @@ -595,6 +674,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) { language_models::init(user_store.clone(), client.clone(), cx); Project::init_settings(cx); LanguageModelRegistry::test(cx); + agent_settings::init(cx); }); cx.executor().forbid_parking(); @@ -790,6 +870,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) { id: acp::ToolCallId("1".into()), fields: acp::ToolCallUpdateFields { status: Some(acp::ToolCallStatus::Completed), + raw_output: Some("Finished thinking.".into()), ..Default::default() }, } @@ -813,6 +894,7 @@ struct ThreadTest { model: Arc, thread: Entity, project_context: Rc>, + fs: Arc, } enum TestModel { @@ -835,30 +917,57 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { cx.executor().allow_parking(); let fs = FakeFs::new(cx.background_executor.clone()); + fs.create_dir(paths::settings_file().parent().unwrap()) + .await + .unwrap(); + fs.insert_file( + paths::settings_file(), + json!({ + "agent": { + "default_profile": "test-profile", + "profiles": { + "test-profile": { + "name": "Test Profile", + "tools": { + EchoTool.name(): true, + DelayTool.name(): true, + WordListTool.name(): true, + ToolRequiringPermission.name(): true, + InfiniteTool.name(): true, + } + } + } + } + }) + .to_string() + .into_bytes(), + ) + .await; cx.update(|cx| { settings::init(cx); - watch_settings(fs.clone(), cx); Project::init_settings(cx); agent_settings::init(cx); + gpui_tokio::init(cx); + let http_client = ReqwestClient::user_agent("agent tests").unwrap(); + cx.set_http_client(Arc::new(http_client)); + + client::init_settings(cx); + let client = Client::production(cx); + let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); + language_model::init(client.clone(), cx); + language_models::init(user_store.clone(), client.clone(), cx); + + watch_settings(fs.clone(), cx); }); + let templates = Templates::new(); fs.insert_tree(path!("/test"), json!({})).await; - let project = Project::test(fs, [path!("/test").as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; let model = cx .update(|cx| { - gpui_tokio::init(cx); - let http_client = ReqwestClient::user_agent("agent tests").unwrap(); - cx.set_http_client(Arc::new(http_client)); - - client::init_settings(cx); - let client = Client::production(cx); - let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - language_model::init(client.clone(), cx); - language_models::init(user_store.clone(), client.clone(), cx); - if let TestModel::Fake = model { Task::ready(Arc::new(FakeLanguageModel::default()) as Arc<_>) } else { @@ -881,20 +990,25 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { .await; let project_context = Rc::new(RefCell::new(ProjectContext::default())); + let context_server_registry = + cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let action_log = cx.new(|_| ActionLog::new(project.clone())); - let thread = cx.new(|_| { + let thread = cx.new(|cx| { Thread::new( project, project_context.clone(), + context_server_registry, action_log, templates, model.clone(), + cx, ) }); ThreadTest { model, thread, project_context, + fs, } } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 23a0f7972d..231f83ce20 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1,7 +1,7 @@ -use crate::{SystemPromptTemplate, Template, Templates}; +use crate::{ContextServerRegistry, SystemPromptTemplate, Template, Templates}; use action_log::ActionLog; use agent_client_protocol as acp; -use agent_settings::AgentSettings; +use agent_settings::{AgentProfileId, AgentSettings}; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::adapt_schema_to_format; use cloud_llm_client::{CompletionIntent, CompletionMode}; @@ -126,6 +126,8 @@ pub struct Thread { running_turn: Option>, pending_tool_uses: HashMap, tools: BTreeMap>, + context_server_registry: Entity, + profile_id: AgentProfileId, project_context: Rc>, templates: Arc, pub selected_model: Arc, @@ -137,16 +139,21 @@ impl Thread { pub fn new( project: Entity, project_context: Rc>, + context_server_registry: Entity, action_log: Entity, templates: Arc, default_model: Arc, + cx: &mut Context, ) -> Self { + let profile_id = AgentSettings::get_global(cx).default_profile.clone(); Self { messages: Vec::new(), completion_mode: CompletionMode::Normal, running_turn: None, pending_tool_uses: HashMap::default(), tools: BTreeMap::default(), + context_server_registry, + profile_id, project_context, templates, selected_model: default_model, @@ -179,6 +186,10 @@ impl Thread { self.tools.remove(name).is_some() } + pub fn set_profile(&mut self, profile_id: AgentProfileId) { + self.profile_id = profile_id; + } + pub fn cancel(&mut self) { self.running_turn.take(); @@ -298,6 +309,7 @@ impl Thread { } else { acp::ToolCallStatus::Completed }), + raw_output: tool_result.output.clone(), ..Default::default() }, ); @@ -604,21 +616,23 @@ impl Thread { let messages = self.build_request_messages(); log::info!("Request will include {} messages", messages.len()); - let tools: Vec = self - .tools - .values() - .filter_map(|tool| { - let tool_name = tool.name().to_string(); - log::trace!("Including tool: {}", tool_name); - Some(LanguageModelRequestTool { - name: tool_name, - description: tool.description(cx).to_string(), - input_schema: tool - .input_schema(self.selected_model.tool_input_format()) - .log_err()?, + let tools = if let Some(tools) = self.tools(cx).log_err() { + tools + .filter_map(|tool| { + let tool_name = tool.name().to_string(); + log::trace!("Including tool: {}", tool_name); + Some(LanguageModelRequestTool { + name: tool_name, + description: tool.description().to_string(), + input_schema: tool + .input_schema(self.selected_model.tool_input_format()) + .log_err()?, + }) }) - }) - .collect(); + .collect() + } else { + Vec::new() + }; log::info!("Request includes {} tools", tools.len()); @@ -639,6 +653,35 @@ impl Thread { request } + fn tools<'a>(&'a self, cx: &'a App) -> Result>> { + let profile = AgentSettings::get_global(cx) + .profiles + .get(&self.profile_id) + .context("profile not found")?; + + Ok(self + .tools + .iter() + .filter_map(|(tool_name, tool)| { + if profile.is_tool_enabled(tool_name) { + Some(tool) + } else { + None + } + }) + .chain(self.context_server_registry.read(cx).servers().flat_map( + |(server_id, tools)| { + tools.iter().filter_map(|(tool_name, tool)| { + if profile.is_context_server_tool_enabled(&server_id.0, tool_name) { + Some(tool) + } else { + None + } + }) + }, + ))) + } + fn build_request_messages(&self) -> Vec { log::trace!( "Building request messages from {} thread messages", @@ -686,7 +729,7 @@ where fn name(&self) -> SharedString; - fn description(&self, _cx: &mut App) -> SharedString { + fn description(&self) -> SharedString { let schema = schemars::schema_for!(Self::Input); SharedString::new( schema @@ -722,13 +765,13 @@ where pub struct Erased(T); pub struct AgentToolOutput { - llm_output: LanguageModelToolResultContent, - raw_output: serde_json::Value, + pub llm_output: LanguageModelToolResultContent, + pub raw_output: serde_json::Value, } pub trait AnyAgentTool { fn name(&self) -> SharedString; - fn description(&self, cx: &mut App) -> SharedString; + fn description(&self) -> SharedString; fn kind(&self) -> acp::ToolKind; fn initial_title(&self, input: serde_json::Value) -> SharedString; fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result; @@ -748,8 +791,8 @@ where self.0.name() } - fn description(&self, cx: &mut App) -> SharedString { - self.0.description(cx) + fn description(&self) -> SharedString { + self.0.description() } fn kind(&self) -> agent_client_protocol::ToolKind { diff --git a/crates/agent2/src/tools.rs b/crates/agent2/src/tools.rs index 8896b14538..d1f2b3b1c7 100644 --- a/crates/agent2/src/tools.rs +++ b/crates/agent2/src/tools.rs @@ -1,3 +1,4 @@ +mod context_server_registry; mod copy_path_tool; mod create_directory_tool; mod delete_path_tool; @@ -15,6 +16,7 @@ mod terminal_tool; mod thinking_tool; mod web_search_tool; +pub use context_server_registry::*; pub use copy_path_tool::*; pub use create_directory_tool::*; pub use delete_path_tool::*; diff --git a/crates/agent2/src/tools/context_server_registry.rs b/crates/agent2/src/tools/context_server_registry.rs new file mode 100644 index 0000000000..db39e9278c --- /dev/null +++ b/crates/agent2/src/tools/context_server_registry.rs @@ -0,0 +1,231 @@ +use crate::{AgentToolOutput, AnyAgentTool, ToolCallEventStream}; +use agent_client_protocol::ToolKind; +use anyhow::{Result, anyhow, bail}; +use collections::{BTreeMap, HashMap}; +use context_server::ContextServerId; +use gpui::{App, Context, Entity, SharedString, Task}; +use project::context_server_store::{ContextServerStatus, ContextServerStore}; +use std::sync::Arc; +use util::ResultExt; + +pub struct ContextServerRegistry { + server_store: Entity, + registered_servers: HashMap, + _subscription: gpui::Subscription, +} + +struct RegisteredContextServer { + tools: BTreeMap>, + load_tools: Task>, +} + +impl ContextServerRegistry { + pub fn new(server_store: Entity, cx: &mut Context) -> Self { + let mut this = Self { + server_store: server_store.clone(), + registered_servers: HashMap::default(), + _subscription: cx.subscribe(&server_store, Self::handle_context_server_store_event), + }; + for server in server_store.read(cx).running_servers() { + this.reload_tools_for_server(server.id(), cx); + } + this + } + + pub fn servers( + &self, + ) -> impl Iterator< + Item = ( + &ContextServerId, + &BTreeMap>, + ), + > { + self.registered_servers + .iter() + .map(|(id, server)| (id, &server.tools)) + } + + fn reload_tools_for_server(&mut self, server_id: ContextServerId, cx: &mut Context) { + let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else { + return; + }; + let Some(client) = server.client() else { + return; + }; + if !client.capable(context_server::protocol::ServerCapability::Tools) { + return; + } + + let registered_server = + self.registered_servers + .entry(server_id.clone()) + .or_insert(RegisteredContextServer { + tools: BTreeMap::default(), + load_tools: Task::ready(Ok(())), + }); + registered_server.load_tools = cx.spawn(async move |this, cx| { + let response = client + .request::(()) + .await; + + this.update(cx, |this, cx| { + let Some(registered_server) = this.registered_servers.get_mut(&server_id) else { + return; + }; + + registered_server.tools.clear(); + if let Some(response) = response.log_err() { + for tool in response.tools { + let tool = Arc::new(ContextServerTool::new( + this.server_store.clone(), + server.id(), + tool, + )); + registered_server.tools.insert(tool.name(), tool); + } + cx.notify(); + } + }) + }); + } + + fn handle_context_server_store_event( + &mut self, + _: Entity, + event: &project::context_server_store::Event, + cx: &mut Context, + ) { + match event { + project::context_server_store::Event::ServerStatusChanged { server_id, status } => { + match status { + ContextServerStatus::Starting => {} + ContextServerStatus::Running => { + self.reload_tools_for_server(server_id.clone(), cx); + } + ContextServerStatus::Stopped | ContextServerStatus::Error(_) => { + self.registered_servers.remove(&server_id); + cx.notify(); + } + } + } + } + } +} + +struct ContextServerTool { + store: Entity, + server_id: ContextServerId, + tool: context_server::types::Tool, +} + +impl ContextServerTool { + fn new( + store: Entity, + server_id: ContextServerId, + tool: context_server::types::Tool, + ) -> Self { + Self { + store, + server_id, + tool, + } + } +} + +impl AnyAgentTool for ContextServerTool { + fn name(&self) -> SharedString { + self.tool.name.clone().into() + } + + fn description(&self) -> SharedString { + self.tool.description.clone().unwrap_or_default().into() + } + + fn kind(&self) -> ToolKind { + ToolKind::Other + } + + fn initial_title(&self, _input: serde_json::Value) -> SharedString { + format!("Run MCP tool `{}`", self.tool.name).into() + } + + fn input_schema( + &self, + format: language_model::LanguageModelToolSchemaFormat, + ) -> Result { + let mut schema = self.tool.input_schema.clone(); + assistant_tool::adapt_schema_to_format(&mut schema, format)?; + Ok(match schema { + serde_json::Value::Null => { + serde_json::json!({ "type": "object", "properties": [] }) + } + serde_json::Value::Object(map) if map.is_empty() => { + serde_json::json!({ "type": "object", "properties": [] }) + } + _ => schema, + }) + } + + fn run( + self: Arc, + input: serde_json::Value, + _event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> { + let Some(server) = self.store.read(cx).get_running_server(&self.server_id) else { + return Task::ready(Err(anyhow!("Context server not found"))); + }; + let tool_name = self.tool.name.clone(); + let server_clone = server.clone(); + let input_clone = input.clone(); + + cx.spawn(async move |_cx| { + let Some(protocol) = server_clone.client() else { + bail!("Context server not initialized"); + }; + + let arguments = if let serde_json::Value::Object(map) = input_clone { + Some(map.into_iter().collect()) + } else { + None + }; + + log::trace!( + "Running tool: {} with arguments: {:?}", + tool_name, + arguments + ); + let response = protocol + .request::( + context_server::types::CallToolParams { + name: tool_name, + arguments, + meta: None, + }, + ) + .await?; + + let mut result = String::new(); + for content in response.content { + match content { + context_server::types::ToolResponseContent::Text { text } => { + result.push_str(&text); + } + context_server::types::ToolResponseContent::Image { .. } => { + log::warn!("Ignoring image content from tool response"); + } + context_server::types::ToolResponseContent::Audio { .. } => { + log::warn!("Ignoring audio content from tool response"); + } + context_server::types::ToolResponseContent::Resource { .. } => { + log::warn!("Ignoring resource content from tool response"); + } + } + } + Ok(AgentToolOutput { + raw_output: result.clone().into(), + llm_output: result.into(), + }) + }) + } +} diff --git a/crates/agent2/src/tools/diagnostics_tool.rs b/crates/agent2/src/tools/diagnostics_tool.rs index bd0b20df5a..6ba8b7b377 100644 --- a/crates/agent2/src/tools/diagnostics_tool.rs +++ b/crates/agent2/src/tools/diagnostics_tool.rs @@ -85,7 +85,7 @@ impl AgentTool for DiagnosticsTool { fn run( self: Arc, input: Self::Input, - event_stream: ToolCallEventStream, + _event_stream: ToolCallEventStream, cx: &mut App, ) -> Task> { match input.path { @@ -119,11 +119,6 @@ impl AgentTool for DiagnosticsTool { range.start.row + 1, entry.diagnostic.message )?; - - event_stream.update_fields(acp::ToolCallUpdateFields { - content: Some(vec![output.clone().into()]), - ..Default::default() - }); } if output.is_empty() { @@ -158,18 +153,9 @@ impl AgentTool for DiagnosticsTool { } if has_diagnostics { - event_stream.update_fields(acp::ToolCallUpdateFields { - content: Some(vec![output.clone().into()]), - ..Default::default() - }); Task::ready(Ok(output)) } else { - let text = "No errors or warnings found in the project."; - event_stream.update_fields(acp::ToolCallUpdateFields { - content: Some(vec![text.into()]), - ..Default::default() - }); - Task::ready(Ok(text.into())) + Task::ready(Ok("No errors or warnings found in the project.".into())) } } } diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 88764d1953..134bc5e5e4 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -454,9 +454,8 @@ fn resolve_path( #[cfg(test)] mod tests { - use crate::Templates; - use super::*; + use crate::{ContextServerRegistry, Templates}; use action_log::ActionLog; use client::TelemetrySettings; use fs::Fs; @@ -475,9 +474,20 @@ mod tests { fs.insert_tree("/root", json!({})).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let action_log = cx.new(|_| ActionLog::new(project.clone())); + let context_server_registry = + cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); - let thread = - cx.new(|_| Thread::new(project, Rc::default(), action_log, Templates::new(), model)); + let thread = cx.new(|cx| { + Thread::new( + project, + Rc::default(), + context_server_registry, + action_log, + Templates::new(), + model, + cx, + ) + }); let result = cx .update(|cx| { let input = EditFileToolInput { @@ -661,14 +671,18 @@ mod tests { }); let action_log = cx.new(|_| ActionLog::new(project.clone())); + let context_server_registry = + cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|_| { + let thread = cx.new(|cx| { Thread::new( project, Rc::default(), + context_server_registry, action_log.clone(), Templates::new(), model.clone(), + cx, ) }); @@ -792,15 +806,19 @@ mod tests { .unwrap(); let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let context_server_registry = + cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let action_log = cx.new(|_| ActionLog::new(project.clone())); let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|_| { + let thread = cx.new(|cx| { Thread::new( project, Rc::default(), + context_server_registry, action_log.clone(), Templates::new(), model.clone(), + cx, ) }); @@ -914,15 +932,19 @@ mod tests { init_test(cx); let fs = project::FakeFs::new(cx.executor()); let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let context_server_registry = + cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let action_log = cx.new(|_| ActionLog::new(project.clone())); let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|_| { + let thread = cx.new(|cx| { Thread::new( project, Rc::default(), + context_server_registry, action_log.clone(), Templates::new(), model.clone(), + cx, ) }); let tool = Arc::new(EditFileTool { thread }); @@ -1041,15 +1063,19 @@ mod tests { let fs = project::FakeFs::new(cx.executor()); fs.insert_tree("/project", json!({})).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let context_server_registry = + cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let action_log = cx.new(|_| ActionLog::new(project.clone())); let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|_| { + let thread = cx.new(|cx| { Thread::new( project, Rc::default(), + context_server_registry, action_log.clone(), Templates::new(), model.clone(), + cx, ) }); let tool = Arc::new(EditFileTool { thread }); @@ -1148,14 +1174,18 @@ mod tests { .await; let action_log = cx.new(|_| ActionLog::new(project.clone())); + let context_server_registry = + cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|_| { + let thread = cx.new(|cx| { Thread::new( project.clone(), Rc::default(), + context_server_registry.clone(), action_log.clone(), Templates::new(), model.clone(), + cx, ) }); let tool = Arc::new(EditFileTool { thread }); @@ -1225,14 +1255,18 @@ mod tests { .await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let action_log = cx.new(|_| ActionLog::new(project.clone())); + let context_server_registry = + cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|_| { + let thread = cx.new(|cx| { Thread::new( project.clone(), Rc::default(), + context_server_registry.clone(), action_log.clone(), Templates::new(), model.clone(), + cx, ) }); let tool = Arc::new(EditFileTool { thread }); @@ -1305,14 +1339,18 @@ mod tests { .await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let action_log = cx.new(|_| ActionLog::new(project.clone())); + let context_server_registry = + cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|_| { + let thread = cx.new(|cx| { Thread::new( project.clone(), Rc::default(), + context_server_registry.clone(), action_log.clone(), Templates::new(), model.clone(), + cx, ) }); let tool = Arc::new(EditFileTool { thread }); @@ -1382,14 +1420,18 @@ mod tests { let fs = project::FakeFs::new(cx.executor()); let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let action_log = cx.new(|_| ActionLog::new(project.clone())); + let context_server_registry = + cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|_| { + let thread = cx.new(|cx| { Thread::new( project.clone(), Rc::default(), + context_server_registry, action_log.clone(), Templates::new(), model.clone(), + cx, ) }); let tool = Arc::new(EditFileTool { thread }); diff --git a/crates/agent2/src/tools/fetch_tool.rs b/crates/agent2/src/tools/fetch_tool.rs index 7f3752843c..ae26c5fe19 100644 --- a/crates/agent2/src/tools/fetch_tool.rs +++ b/crates/agent2/src/tools/fetch_tool.rs @@ -136,7 +136,7 @@ impl AgentTool for FetchTool { fn run( self: Arc, input: Self::Input, - event_stream: ToolCallEventStream, + _event_stream: ToolCallEventStream, cx: &mut App, ) -> Task> { let text = cx.background_spawn({ @@ -149,12 +149,6 @@ impl AgentTool for FetchTool { if text.trim().is_empty() { bail!("no textual content found"); } - - event_stream.update_fields(acp::ToolCallUpdateFields { - content: Some(vec![text.clone().into()]), - ..Default::default() - }); - Ok(text) }) } diff --git a/crates/agent2/src/tools/find_path_tool.rs b/crates/agent2/src/tools/find_path_tool.rs index 611d34e701..552de144a7 100644 --- a/crates/agent2/src/tools/find_path_tool.rs +++ b/crates/agent2/src/tools/find_path_tool.rs @@ -139,9 +139,6 @@ impl AgentTool for FindPathTool { }) .collect(), ), - raw_output: Some(serde_json::json!({ - "paths": &matches, - })), ..Default::default() }); diff --git a/crates/agent2/src/tools/grep_tool.rs b/crates/agent2/src/tools/grep_tool.rs index 3266cb5734..e5d92b3c1d 100644 --- a/crates/agent2/src/tools/grep_tool.rs +++ b/crates/agent2/src/tools/grep_tool.rs @@ -101,7 +101,7 @@ impl AgentTool for GrepTool { fn run( self: Arc, input: Self::Input, - event_stream: ToolCallEventStream, + _event_stream: ToolCallEventStream, cx: &mut App, ) -> Task> { const CONTEXT_LINES: u32 = 2; @@ -282,33 +282,22 @@ impl AgentTool for GrepTool { } } - event_stream.update_fields(acp::ToolCallUpdateFields { - content: Some(vec![output.clone().into()]), - ..Default::default() - }); matches_found += 1; } } - let output = if matches_found == 0 { - "No matches found".to_string() + if matches_found == 0 { + Ok("No matches found".into()) } else if has_more_matches { - format!( + Ok(format!( "Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}", input.offset + 1, input.offset + matches_found, input.offset + RESULTS_PER_PAGE, - ) + )) } else { - format!("Found {matches_found} matches:\n{output}") - }; - - event_stream.update_fields(acp::ToolCallUpdateFields { - content: Some(vec![output.clone().into()]), - ..Default::default() - }); - - Ok(output) + Ok(format!("Found {matches_found} matches:\n{output}")) + } }) } } diff --git a/crates/agent2/src/tools/now_tool.rs b/crates/agent2/src/tools/now_tool.rs index 71698b8275..a72ede26fe 100644 --- a/crates/agent2/src/tools/now_tool.rs +++ b/crates/agent2/src/tools/now_tool.rs @@ -47,20 +47,13 @@ impl AgentTool for NowTool { fn run( self: Arc, input: Self::Input, - event_stream: ToolCallEventStream, + _event_stream: ToolCallEventStream, _cx: &mut App, ) -> Task> { let now = match input.timezone { Timezone::Utc => Utc::now().to_rfc3339(), Timezone::Local => Local::now().to_rfc3339(), }; - let content = format!("The current datetime is {now}."); - - event_stream.update_fields(acp::ToolCallUpdateFields { - content: Some(vec![content.clone().into()]), - ..Default::default() - }); - - Task::ready(Ok(content)) + Task::ready(Ok(format!("The current datetime is {now}."))) } } diff --git a/crates/agent_settings/src/agent_profile.rs b/crates/agent_settings/src/agent_profile.rs index a6b8633b34..402cf81678 100644 --- a/crates/agent_settings/src/agent_profile.rs +++ b/crates/agent_settings/src/agent_profile.rs @@ -48,6 +48,20 @@ pub struct AgentProfileSettings { pub context_servers: IndexMap, ContextServerPreset>, } +impl AgentProfileSettings { + pub fn is_tool_enabled(&self, tool_name: &str) -> bool { + self.tools.get(tool_name) == Some(&true) + } + + pub fn is_context_server_tool_enabled(&self, server_id: &str, tool_name: &str) -> bool { + self.enable_all_context_servers + || self + .context_servers + .get(server_id) + .map_or(false, |preset| preset.tools.get(tool_name) == Some(&true)) + } +} + #[derive(Debug, Clone, Default)] pub struct ContextServerPreset { pub tools: IndexMap, bool>, From 44953375cc9c9829ae43a686f1112fb331bcaa38 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Tue, 12 Aug 2025 10:12:58 -0300 Subject: [PATCH 274/693] Include mention context in acp-based native agent (#36006) Also adds data-layer support for symbols, thread, and rules. Release Notes: - N/A --------- Co-authored-by: Cole Miller --- Cargo.lock | 1 + crates/acp_thread/Cargo.toml | 1 + crates/acp_thread/src/acp_thread.rs | 63 ++--- crates/acp_thread/src/mention.rs | 122 ++++++++ crates/agent2/src/agent.rs | 46 +-- crates/agent2/src/tests/mod.rs | 41 +-- crates/agent2/src/thread.rs | 261 +++++++++++++++++- .../agent_ui/src/acp/completion_provider.rs | 77 +++++- crates/agent_ui/src/acp/thread_view.rs | 249 ++++++++++------- 9 files changed, 630 insertions(+), 231 deletions(-) create mode 100644 crates/acp_thread/src/mention.rs diff --git a/Cargo.lock b/Cargo.lock index dc28a1cb44..5ee4e94281 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,7 @@ dependencies = [ "tempfile", "terminal", "ui", + "url", "util", "workspace-hack", ] diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index 33e88df761..1fef342c01 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -34,6 +34,7 @@ settings.workspace = true smol.workspace = true terminal.workspace = true ui.workspace = true +url.workspace = true util.workspace = true workspace-hack.workspace = true diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 1c0a9479df..eccbef96b8 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1,13 +1,15 @@ mod connection; mod diff; +mod mention; mod terminal; pub use connection::*; pub use diff::*; +pub use mention::*; pub use terminal::*; use action_log::ActionLog; -use agent_client_protocol as acp; +use agent_client_protocol::{self as acp}; use anyhow::{Context as _, Result}; use editor::Bias; use futures::{FutureExt, channel::oneshot, future::BoxFuture}; @@ -21,12 +23,7 @@ use std::error::Error; use std::fmt::Formatter; use std::process::ExitStatus; use std::rc::Rc; -use std::{ - fmt::Display, - mem, - path::{Path, PathBuf}, - sync::Arc, -}; +use std::{fmt::Display, mem, path::PathBuf, sync::Arc}; use ui::App; use util::ResultExt; @@ -53,38 +50,6 @@ impl UserMessage { } } -#[derive(Debug)] -pub struct MentionPath<'a>(&'a Path); - -impl<'a> MentionPath<'a> { - const PREFIX: &'static str = "@file:"; - - pub fn new(path: &'a Path) -> Self { - MentionPath(path) - } - - pub fn try_parse(url: &'a str) -> Option { - let path = url.strip_prefix(Self::PREFIX)?; - Some(MentionPath(Path::new(path))) - } - - pub fn path(&self) -> &Path { - self.0 - } -} - -impl Display for MentionPath<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "[@{}]({}{})", - self.0.file_name().unwrap_or_default().display(), - Self::PREFIX, - self.0.display() - ) - } -} - #[derive(Debug, PartialEq)] pub struct AssistantMessage { pub chunks: Vec, @@ -367,16 +332,24 @@ impl ContentBlock { ) { let new_content = match block { acp::ContentBlock::Text(text_content) => text_content.text.clone(), - acp::ContentBlock::ResourceLink(resource_link) => { - if let Some(path) = resource_link.uri.strip_prefix("file://") { - format!("{}", MentionPath(path.as_ref())) + acp::ContentBlock::Resource(acp::EmbeddedResource { + resource: + acp::EmbeddedResourceResource::TextResourceContents(acp::TextResourceContents { + uri, + .. + }), + .. + }) => { + if let Some(uri) = MentionUri::parse(&uri).log_err() { + uri.to_link() } else { - resource_link.uri.clone() + uri.clone() } } acp::ContentBlock::Image(_) | acp::ContentBlock::Audio(_) - | acp::ContentBlock::Resource(_) => String::new(), + | acp::ContentBlock::Resource(acp::EmbeddedResource { .. }) + | acp::ContentBlock::ResourceLink(_) => String::new(), }; match self { @@ -1329,7 +1302,7 @@ mod tests { use serde_json::json; use settings::SettingsStore; use smol::stream::StreamExt as _; - use std::{cell::RefCell, rc::Rc, time::Duration}; + use std::{cell::RefCell, path::Path, rc::Rc, time::Duration}; use util::path; diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs new file mode 100644 index 0000000000..1fcd27ad4c --- /dev/null +++ b/crates/acp_thread/src/mention.rs @@ -0,0 +1,122 @@ +use agent_client_protocol as acp; +use anyhow::{Result, bail}; +use std::path::PathBuf; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum MentionUri { + File(PathBuf), + Symbol(PathBuf, String), + Thread(acp::SessionId), + Rule(String), +} + +impl MentionUri { + pub fn parse(input: &str) -> Result { + let url = url::Url::parse(input)?; + let path = url.path(); + match url.scheme() { + "file" => { + if let Some(fragment) = url.fragment() { + Ok(Self::Symbol(path.into(), fragment.into())) + } else { + Ok(Self::File(path.into())) + } + } + "zed" => { + if let Some(thread) = path.strip_prefix("/agent/thread/") { + Ok(Self::Thread(acp::SessionId(thread.into()))) + } else if let Some(rule) = path.strip_prefix("/agent/rule/") { + Ok(Self::Rule(rule.into())) + } else { + bail!("invalid zed url: {:?}", input); + } + } + other => bail!("unrecognized scheme {:?}", other), + } + } + + pub fn name(&self) -> String { + match self { + MentionUri::File(path) => path.file_name().unwrap().to_string_lossy().into_owned(), + MentionUri::Symbol(_path, name) => name.clone(), + MentionUri::Thread(thread) => thread.to_string(), + MentionUri::Rule(rule) => rule.clone(), + } + } + + pub fn to_link(&self) -> String { + let name = self.name(); + let uri = self.to_uri(); + format!("[{name}]({uri})") + } + + pub fn to_uri(&self) -> String { + match self { + MentionUri::File(path) => { + format!("file://{}", path.display()) + } + MentionUri::Symbol(path, name) => { + format!("file://{}#{}", path.display(), name) + } + MentionUri::Thread(thread) => { + format!("zed:///agent/thread/{}", thread.0) + } + MentionUri::Rule(rule) => { + format!("zed:///agent/rule/{}", rule) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mention_uri_parse_and_display() { + // Test file URI + let file_uri = "file:///path/to/file.rs"; + let parsed = MentionUri::parse(file_uri).unwrap(); + match &parsed { + MentionUri::File(path) => assert_eq!(path.to_str().unwrap(), "/path/to/file.rs"), + _ => panic!("Expected File variant"), + } + assert_eq!(parsed.to_uri(), file_uri); + + // Test symbol URI + let symbol_uri = "file:///path/to/file.rs#MySymbol"; + let parsed = MentionUri::parse(symbol_uri).unwrap(); + match &parsed { + MentionUri::Symbol(path, symbol) => { + assert_eq!(path.to_str().unwrap(), "/path/to/file.rs"); + assert_eq!(symbol, "MySymbol"); + } + _ => panic!("Expected Symbol variant"), + } + assert_eq!(parsed.to_uri(), symbol_uri); + + // Test thread URI + let thread_uri = "zed:///agent/thread/session123"; + let parsed = MentionUri::parse(thread_uri).unwrap(); + match &parsed { + MentionUri::Thread(session_id) => assert_eq!(session_id.0.as_ref(), "session123"), + _ => panic!("Expected Thread variant"), + } + assert_eq!(parsed.to_uri(), thread_uri); + + // Test rule URI + let rule_uri = "zed:///agent/rule/my_rule"; + let parsed = MentionUri::parse(rule_uri).unwrap(); + match &parsed { + MentionUri::Rule(rule) => assert_eq!(rule, "my_rule"), + _ => panic!("Expected Rule variant"), + } + assert_eq!(parsed.to_uri(), rule_uri); + + // Test invalid scheme + assert!(MentionUri::parse("http://example.com").is_err()); + + // Test invalid zed path + assert!(MentionUri::parse("zed:///invalid/path").is_err()); + } +} diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 18a830b978..7439b2a088 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,8 +1,8 @@ use crate::{AgentResponseEvent, Thread, templates::Templates}; use crate::{ ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DiagnosticsTool, EditFileTool, - FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MovePathTool, NowTool, OpenTool, - ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, WebSearchTool, + FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MessageContent, MovePathTool, NowTool, + OpenTool, ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, WebSearchTool, }; use acp_thread::ModelSelector; use agent_client_protocol as acp; @@ -516,10 +516,13 @@ impl acp_thread::AgentConnection for NativeAgentConnection { })?; log::debug!("Found session for: {}", session_id); - // Convert prompt to message - let message = convert_prompt_to_message(params.prompt); + let message: Vec = params + .prompt + .into_iter() + .map(Into::into) + .collect::>(); log::info!("Converted prompt to message: {} chars", message.len()); - log::debug!("Message content: {}", message); + log::debug!("Message content: {:?}", message); // Get model using the ModelSelector capability (always available for agent2) // Get the selected model from the thread directly @@ -623,39 +626,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection { } } -/// Convert ACP content blocks to a message string -fn convert_prompt_to_message(blocks: Vec) -> String { - log::debug!("Converting {} content blocks to message", blocks.len()); - let mut message = String::new(); - - for block in blocks { - match block { - acp::ContentBlock::Text(text) => { - log::trace!("Processing text block: {} chars", text.text.len()); - message.push_str(&text.text); - } - acp::ContentBlock::ResourceLink(link) => { - log::trace!("Processing resource link: {}", link.uri); - message.push_str(&format!(" @{} ", link.uri)); - } - acp::ContentBlock::Image(_) => { - log::trace!("Processing image block"); - message.push_str(" [image] "); - } - acp::ContentBlock::Audio(_) => { - log::trace!("Processing audio block"); - message.push_str(" [audio] "); - } - acp::ContentBlock::Resource(resource) => { - log::trace!("Processing resource block: {:?}", resource.resource); - message.push_str(&format!(" [resource: {:?}] ", resource.resource)); - } - } - } - - message -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 7f4b934c08..88cf92836b 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1,4 +1,5 @@ use super::*; +use crate::MessageContent; use acp_thread::AgentConnection; use action_log::ActionLog; use agent_client_protocol::{self as acp}; @@ -13,8 +14,8 @@ use gpui::{ use indoc::indoc; use language_model::{ LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, - LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role, - StopReason, fake_provider::FakeLanguageModel, + LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolUse, Role, StopReason, + fake_provider::FakeLanguageModel, }; use project::Project; use prompt_store::ProjectContext; @@ -272,14 +273,14 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { assert_eq!( message.content, vec![ - MessageContent::ToolResult(LanguageModelToolResult { + language_model::MessageContent::ToolResult(LanguageModelToolResult { tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(), tool_name: ToolRequiringPermission.name().into(), is_error: false, content: "Allowed".into(), output: Some("Allowed".into()) }), - MessageContent::ToolResult(LanguageModelToolResult { + language_model::MessageContent::ToolResult(LanguageModelToolResult { tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(), tool_name: ToolRequiringPermission.name().into(), is_error: true, @@ -312,13 +313,15 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { let message = completion.messages.last().unwrap(); assert_eq!( message.content, - vec![MessageContent::ToolResult(LanguageModelToolResult { - tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(), - tool_name: ToolRequiringPermission.name().into(), - is_error: false, - content: "Allowed".into(), - output: Some("Allowed".into()) - })] + vec![language_model::MessageContent::ToolResult( + LanguageModelToolResult { + tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(), + tool_name: ToolRequiringPermission.name().into(), + is_error: false, + content: "Allowed".into(), + output: Some("Allowed".into()) + } + )] ); // Simulate a final tool call, ensuring we don't trigger authorization. @@ -337,13 +340,15 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { let message = completion.messages.last().unwrap(); assert_eq!( message.content, - vec![MessageContent::ToolResult(LanguageModelToolResult { - tool_use_id: "tool_id_4".into(), - tool_name: ToolRequiringPermission.name().into(), - is_error: false, - content: "Allowed".into(), - output: Some("Allowed".into()) - })] + vec![language_model::MessageContent::ToolResult( + LanguageModelToolResult { + tool_use_id: "tool_id_4".into(), + tool_name: ToolRequiringPermission.name().into(), + is_error: false, + content: "Allowed".into(), + output: Some("Allowed".into()) + } + )] ); } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 231f83ce20..678e4cb5d2 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1,4 +1,5 @@ use crate::{ContextServerRegistry, SystemPromptTemplate, Template, Templates}; +use acp_thread::MentionUri; use action_log::ActionLog; use agent_client_protocol as acp; use agent_settings::{AgentProfileId, AgentSettings}; @@ -13,10 +14,10 @@ use futures::{ }; use gpui::{App, Context, Entity, SharedString, Task}; use language_model::{ - LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, + LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, - LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role, StopReason, + LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason, }; use log; use project::Project; @@ -25,7 +26,8 @@ use schemars::{JsonSchema, Schema}; use serde::{Deserialize, Serialize}; use settings::{Settings, update_settings_file}; use smol::stream::StreamExt; -use std::{cell::RefCell, collections::BTreeMap, fmt::Write, rc::Rc, sync::Arc}; +use std::fmt::Write; +use std::{cell::RefCell, collections::BTreeMap, path::Path, rc::Rc, sync::Arc}; use util::{ResultExt, markdown::MarkdownCodeBlock}; #[derive(Debug, Clone)] @@ -34,6 +36,23 @@ pub struct AgentMessage { pub content: Vec, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MessageContent { + Text(String), + Thinking { + text: String, + signature: Option, + }, + Mention { + uri: MentionUri, + content: String, + }, + RedactedThinking(String), + Image(LanguageModelImage), + ToolUse(LanguageModelToolUse), + ToolResult(LanguageModelToolResult), +} + impl AgentMessage { pub fn to_markdown(&self) -> String { let mut markdown = format!("## {}\n", self.role); @@ -93,6 +112,9 @@ impl AgentMessage { .unwrap(); } } + MessageContent::Mention { uri, .. } => { + write!(markdown, "{}", uri.to_link()).ok(); + } } } @@ -214,10 +236,11 @@ impl Thread { /// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn. pub fn send( &mut self, - content: impl Into, + content: impl Into, cx: &mut Context, ) -> mpsc::UnboundedReceiver> { - let content = content.into(); + let content = content.into().0; + let model = self.selected_model.clone(); log::info!("Thread::send called with model: {:?}", model.name()); log::debug!("Thread::send content: {:?}", content); @@ -230,7 +253,7 @@ impl Thread { let user_message_ix = self.messages.len(); self.messages.push(AgentMessage { role: Role::User, - content: vec![content], + content, }); log::info!("Total messages in thread: {}", self.messages.len()); self.running_turn = Some(cx.spawn(async move |thread, cx| { @@ -353,7 +376,7 @@ impl Thread { log::debug!("System message built"); AgentMessage { role: Role::System, - content: vec![prompt.into()], + content: vec![prompt.as_str().into()], } } @@ -701,11 +724,7 @@ impl Thread { }, message.content.len() ); - LanguageModelRequestMessage { - role: message.role, - content: message.content.clone(), - cache: false, - } + message.to_request() }) .collect(); messages @@ -720,6 +739,20 @@ impl Thread { } } +pub struct UserMessage(Vec); + +impl From> for UserMessage { + fn from(content: Vec) -> Self { + UserMessage(content) + } +} + +impl> From for UserMessage { + fn from(content: T) -> Self { + UserMessage(vec![content.into()]) + } +} + pub trait AgentTool where Self: 'static + Sized, @@ -1102,3 +1135,207 @@ impl std::ops::DerefMut for ToolCallEventStreamReceiver { &mut self.0 } } + +impl AgentMessage { + fn to_request(&self) -> language_model::LanguageModelRequestMessage { + let mut message = LanguageModelRequestMessage { + role: self.role, + content: Vec::with_capacity(self.content.len()), + cache: false, + }; + + const OPEN_CONTEXT: &str = "\n\ + The following items were attached by the user. \ + They are up-to-date and don't need to be re-read.\n\n"; + + const OPEN_FILES_TAG: &str = ""; + const OPEN_SYMBOLS_TAG: &str = ""; + const OPEN_THREADS_TAG: &str = ""; + const OPEN_RULES_TAG: &str = + "\nThe user has specified the following rules that should be applied:\n"; + + let mut file_context = OPEN_FILES_TAG.to_string(); + let mut symbol_context = OPEN_SYMBOLS_TAG.to_string(); + let mut thread_context = OPEN_THREADS_TAG.to_string(); + let mut rules_context = OPEN_RULES_TAG.to_string(); + + for chunk in &self.content { + let chunk = match chunk { + MessageContent::Text(text) => language_model::MessageContent::Text(text.clone()), + MessageContent::Thinking { text, signature } => { + language_model::MessageContent::Thinking { + text: text.clone(), + signature: signature.clone(), + } + } + MessageContent::RedactedThinking(value) => { + language_model::MessageContent::RedactedThinking(value.clone()) + } + MessageContent::ToolUse(value) => { + language_model::MessageContent::ToolUse(value.clone()) + } + MessageContent::ToolResult(value) => { + language_model::MessageContent::ToolResult(value.clone()) + } + MessageContent::Image(value) => { + language_model::MessageContent::Image(value.clone()) + } + MessageContent::Mention { uri, content } => { + match uri { + MentionUri::File(path) | MentionUri::Symbol(path, _) => { + write!( + &mut symbol_context, + "\n{}", + MarkdownCodeBlock { + tag: &codeblock_tag(&path), + text: &content.to_string(), + } + ) + .ok(); + } + MentionUri::Thread(_session_id) => { + write!(&mut thread_context, "\n{}\n", content).ok(); + } + MentionUri::Rule(_user_prompt_id) => { + write!( + &mut rules_context, + "\n{}", + MarkdownCodeBlock { + tag: "", + text: &content + } + ) + .ok(); + } + } + + language_model::MessageContent::Text(uri.to_link()) + } + }; + + message.content.push(chunk); + } + + let len_before_context = message.content.len(); + + if file_context.len() > OPEN_FILES_TAG.len() { + file_context.push_str("\n"); + message + .content + .push(language_model::MessageContent::Text(file_context)); + } + + if symbol_context.len() > OPEN_SYMBOLS_TAG.len() { + symbol_context.push_str("\n"); + message + .content + .push(language_model::MessageContent::Text(symbol_context)); + } + + if thread_context.len() > OPEN_THREADS_TAG.len() { + thread_context.push_str("\n"); + message + .content + .push(language_model::MessageContent::Text(thread_context)); + } + + if rules_context.len() > OPEN_RULES_TAG.len() { + rules_context.push_str("\n"); + message + .content + .push(language_model::MessageContent::Text(rules_context)); + } + + if message.content.len() > len_before_context { + message.content.insert( + len_before_context, + language_model::MessageContent::Text(OPEN_CONTEXT.into()), + ); + message + .content + .push(language_model::MessageContent::Text("".into())); + } + + message + } +} + +fn codeblock_tag(full_path: &Path) -> String { + let mut result = String::new(); + + if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) { + let _ = write!(result, "{} ", extension); + } + + let _ = write!(result, "{}", full_path.display()); + + result +} + +impl From for MessageContent { + fn from(value: acp::ContentBlock) -> Self { + match value { + acp::ContentBlock::Text(text_content) => MessageContent::Text(text_content.text), + acp::ContentBlock::Image(image_content) => { + MessageContent::Image(convert_image(image_content)) + } + acp::ContentBlock::Audio(_) => { + // TODO + MessageContent::Text("[audio]".to_string()) + } + acp::ContentBlock::ResourceLink(resource_link) => { + match MentionUri::parse(&resource_link.uri) { + Ok(uri) => Self::Mention { + uri, + content: String::new(), + }, + Err(err) => { + log::error!("Failed to parse mention link: {}", err); + MessageContent::Text(format!( + "[{}]({})", + resource_link.name, resource_link.uri + )) + } + } + } + acp::ContentBlock::Resource(resource) => match resource.resource { + acp::EmbeddedResourceResource::TextResourceContents(resource) => { + match MentionUri::parse(&resource.uri) { + Ok(uri) => Self::Mention { + uri, + content: resource.text, + }, + Err(err) => { + log::error!("Failed to parse mention link: {}", err); + MessageContent::Text( + MarkdownCodeBlock { + tag: &resource.uri, + text: &resource.text, + } + .to_string(), + ) + } + } + } + acp::EmbeddedResourceResource::BlobResourceContents(_) => { + // TODO + MessageContent::Text("[blob]".to_string()) + } + }, + } + } +} + +fn convert_image(image_content: acp::ImageContent) -> LanguageModelImage { + LanguageModelImage { + source: image_content.data.into(), + // TODO: make this optional? + size: gpui::Size::new(0.into(), 0.into()), + } +} + +impl From<&str> for MessageContent { + fn from(text: &str) -> Self { + MessageContent::Text(text.into()) + } +} diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index d8f452afa5..3c2bea53a7 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -1,18 +1,20 @@ use std::ops::Range; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::AtomicBool; -use anyhow::Result; +use acp_thread::MentionUri; +use anyhow::{Context as _, Result}; use collections::HashMap; use editor::display_map::CreaseId; use editor::{CompletionProvider, Editor, ExcerptId}; use file_icons::FileIcons; +use futures::future::try_join_all; use gpui::{App, Entity, Task, WeakEntity}; use language::{Buffer, CodeLabel, HighlightId}; use lsp::CompletionContext; use parking_lot::Mutex; -use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, WorktreeId}; +use project::{Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, WorktreeId}; use rope::Point; use text::{Anchor, ToPoint}; use ui::prelude::*; @@ -23,21 +25,63 @@ use crate::context_picker::file_context_picker::{extract_file_name_and_directory #[derive(Default)] pub struct MentionSet { - paths_by_crease_id: HashMap, + paths_by_crease_id: HashMap, } impl MentionSet { - pub fn insert(&mut self, crease_id: CreaseId, path: ProjectPath) { - self.paths_by_crease_id.insert(crease_id, path); - } - - pub fn path_for_crease_id(&self, crease_id: CreaseId) -> Option { - self.paths_by_crease_id.get(&crease_id).cloned() + pub fn insert(&mut self, crease_id: CreaseId, path: PathBuf) { + self.paths_by_crease_id + .insert(crease_id, MentionUri::File(path)); } pub fn drain(&mut self) -> impl Iterator { self.paths_by_crease_id.drain().map(|(id, _)| id) } + + pub fn contents( + &self, + project: Entity, + cx: &mut App, + ) -> Task>> { + let contents = self + .paths_by_crease_id + .iter() + .map(|(crease_id, uri)| match uri { + MentionUri::File(path) => { + let crease_id = *crease_id; + let uri = uri.clone(); + let path = path.to_path_buf(); + let buffer_task = project.update(cx, |project, cx| { + let path = project + .find_project_path(path, cx) + .context("Failed to find project path")?; + anyhow::Ok(project.open_buffer(path, cx)) + }); + + cx.spawn(async move |cx| { + let buffer = buffer_task?.await?; + let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; + + anyhow::Ok((crease_id, Mention { uri, content })) + }) + } + _ => { + // TODO + unimplemented!() + } + }) + .collect::>(); + + cx.spawn(async move |_cx| { + let contents = try_join_all(contents).await?.into_iter().collect(); + anyhow::Ok(contents) + }) + } +} + +pub struct Mention { + pub uri: MentionUri, + pub content: String, } pub struct ContextPickerCompletionProvider { @@ -68,6 +112,7 @@ impl ContextPickerCompletionProvider { source_range: Range, editor: Entity, mention_set: Arc>, + project: Entity, cx: &App, ) -> Completion { let (file_name, directory) = @@ -112,6 +157,7 @@ impl ContextPickerCompletionProvider { new_text_len - 1, editor, mention_set, + project, )), } } @@ -159,6 +205,7 @@ impl CompletionProvider for ContextPickerCompletionProvider { return Task::ready(Ok(Vec::new())); }; + let project = workspace.read(cx).project().clone(); let snapshot = buffer.read(cx).snapshot(); let source_range = snapshot.anchor_before(state.source_range.start) ..snapshot.anchor_after(state.source_range.end); @@ -195,6 +242,7 @@ impl CompletionProvider for ContextPickerCompletionProvider { source_range.clone(), editor.clone(), mention_set.clone(), + project.clone(), cx, ) }) @@ -254,6 +302,7 @@ fn confirm_completion_callback( content_len: usize, editor: Entity, mention_set: Arc>, + project: Entity, ) -> Arc bool + Send + Sync> { Arc::new(move |_, window, cx| { let crease_text = crease_text.clone(); @@ -261,6 +310,7 @@ fn confirm_completion_callback( let editor = editor.clone(); let project_path = project_path.clone(); let mention_set = mention_set.clone(); + let project = project.clone(); window.defer(cx, move |window, cx| { let crease_id = crate::context_picker::insert_crease_for_mention( excerpt_id, @@ -272,8 +322,13 @@ fn confirm_completion_callback( window, cx, ); + + let Some(path) = project.read(cx).absolute_path(&project_path, cx) else { + return; + }; + if let Some(crease_id) = crease_id { - mention_set.lock().insert(crease_id, project_path); + mention_set.lock().insert(crease_id, path); } }); false diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index f37deac26e..6d8dccd18f 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1,6 +1,6 @@ use acp_thread::{ AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, - LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, + LoadError, MentionUri, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, }; use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; @@ -28,6 +28,7 @@ use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; use parking_lot::Mutex; use project::{CompletionIntent, Project}; use settings::{Settings as _, SettingsStore}; +use std::path::PathBuf; use std::{ cell::RefCell, collections::BTreeMap, path::Path, process::ExitStatus, rc::Rc, sync::Arc, time::Duration, @@ -376,81 +377,101 @@ impl AcpThreadView { let mut ix = 0; let mut chunks: Vec = Vec::new(); let project = self.project.clone(); - self.message_editor.update(cx, |editor, cx| { - let text = editor.text(cx); - editor.display_map.update(cx, |map, cx| { - let snapshot = map.snapshot(cx); - for (crease_id, crease) in snapshot.crease_snapshot.creases() { - // Skip creases that have been edited out of the message buffer. - if !crease.range().start.is_valid(&snapshot.buffer_snapshot) { - continue; - } - if let Some(project_path) = - self.mention_set.lock().path_for_crease_id(crease_id) - { - let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot); - if crease_range.start > ix { - chunks.push(text[ix..crease_range.start].into()); + let contents = self.mention_set.lock().contents(project, cx); + + cx.spawn_in(window, async move |this, cx| { + let contents = match contents.await { + Ok(contents) => contents, + Err(e) => { + this.update(cx, |this, cx| { + this.last_error = + Some(cx.new(|cx| Markdown::new(e.to_string().into(), None, None, cx))); + }) + .ok(); + return; + } + }; + + this.update_in(cx, |this, window, cx| { + this.message_editor.update(cx, |editor, cx| { + let text = editor.text(cx); + editor.display_map.update(cx, |map, cx| { + let snapshot = map.snapshot(cx); + for (crease_id, crease) in snapshot.crease_snapshot.creases() { + // Skip creases that have been edited out of the message buffer. + if !crease.range().start.is_valid(&snapshot.buffer_snapshot) { + continue; + } + + if let Some(mention) = contents.get(&crease_id) { + let crease_range = + crease.range().to_offset(&snapshot.buffer_snapshot); + if crease_range.start > ix { + chunks.push(text[ix..crease_range.start].into()); + } + chunks.push(acp::ContentBlock::Resource(acp::EmbeddedResource { + annotations: None, + resource: acp::EmbeddedResourceResource::TextResourceContents( + acp::TextResourceContents { + mime_type: None, + text: mention.content.clone(), + uri: mention.uri.to_uri(), + }, + ), + })); + ix = crease_range.end; + } } - if let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) { - let path_str = abs_path.display().to_string(); - chunks.push(acp::ContentBlock::ResourceLink(acp::ResourceLink { - uri: path_str.clone(), - name: path_str, - annotations: None, - description: None, - mime_type: None, - size: None, - title: None, - })); + + if ix < text.len() { + let last_chunk = text[ix..].trim_end(); + if !last_chunk.is_empty() { + chunks.push(last_chunk.into()); + } } - ix = crease_range.end; - } + }) + }); + + if chunks.is_empty() { + return; } - if ix < text.len() { - let last_chunk = text[ix..].trim_end(); - if !last_chunk.is_empty() { - chunks.push(last_chunk.into()); - } - } - }) - }); - - if chunks.is_empty() { - return; - } - - let Some(thread) = self.thread() else { - return; - }; - let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx)); - - cx.spawn(async move |this, cx| { - let result = task.await; - - this.update(cx, |this, cx| { - if let Err(err) = result { - this.last_error = - Some(cx.new(|cx| Markdown::new(err.to_string().into(), None, None, cx))) - } + let Some(thread) = this.thread() else { + return; + }; + let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx)); + + cx.spawn(async move |this, cx| { + let result = task.await; + + this.update(cx, |this, cx| { + if let Err(err) = result { + this.last_error = + Some(cx.new(|cx| { + Markdown::new(err.to_string().into(), None, None, cx) + })) + } + }) + }) + .detach(); + + let mention_set = this.mention_set.clone(); + + this.set_editor_is_expanded(false, cx); + + this.message_editor.update(cx, |editor, cx| { + editor.clear(window, cx); + editor.remove_creases(mention_set.lock().drain(), cx) + }); + + this.scroll_to_bottom(cx); + + this.message_history.borrow_mut().push(chunks); }) + .ok(); }) .detach(); - - let mention_set = self.mention_set.clone(); - - self.set_editor_is_expanded(false, cx); - - self.message_editor.update(cx, |editor, cx| { - editor.clear(window, cx); - editor.remove_creases(mention_set.lock().drain(), cx) - }); - - self.scroll_to_bottom(cx); - - self.message_history.borrow_mut().push(chunks); } fn previous_history_message( @@ -563,16 +584,19 @@ impl AcpThreadView { acp::ContentBlock::Text(text_content) => { text.push_str(&text_content.text); } - acp::ContentBlock::ResourceLink(resource_link) => { - let path = Path::new(&resource_link.uri); + acp::ContentBlock::Resource(acp::EmbeddedResource { + resource: acp::EmbeddedResourceResource::TextResourceContents(resource), + .. + }) => { + let path = PathBuf::from(&resource.uri); + let project_path = project.read(cx).project_path_for_absolute_path(&path, cx); let start = text.len(); - let content = MentionPath::new(&path).to_string(); + let content = MentionUri::File(path).to_uri(); text.push_str(&content); let end = text.len(); - if let Some(project_path) = - project.read(cx).project_path_for_absolute_path(&path, cx) - { - let filename: SharedString = path + if let Some(project_path) = project_path { + let filename: SharedString = project_path + .path .file_name() .unwrap_or_default() .to_string_lossy() @@ -583,7 +607,8 @@ impl AcpThreadView { } acp::ContentBlock::Image(_) | acp::ContentBlock::Audio(_) - | acp::ContentBlock::Resource(_) => {} + | acp::ContentBlock::Resource(_) + | acp::ContentBlock::ResourceLink(_) => {} } } @@ -602,18 +627,21 @@ impl AcpThreadView { }; let anchor = snapshot.anchor_before(range.start); - let crease_id = crate::context_picker::insert_crease_for_mention( - anchor.excerpt_id, - anchor.text_anchor, - range.end - range.start, - filename, - crease_icon_path, - message_editor.clone(), - window, - cx, - ); - if let Some(crease_id) = crease_id { - mention_set.lock().insert(crease_id, project_path); + if let Some(project_path) = project.read(cx).absolute_path(&project_path, cx) { + let crease_id = crate::context_picker::insert_crease_for_mention( + anchor.excerpt_id, + anchor.text_anchor, + range.end - range.start, + filename, + crease_icon_path, + message_editor.clone(), + window, + cx, + ); + + if let Some(crease_id) = crease_id { + mention_set.lock().insert(crease_id, project_path); + } } } @@ -2562,25 +2590,31 @@ impl AcpThreadView { return; }; - if let Some(mention_path) = MentionPath::try_parse(&url) { - workspace.update(cx, |workspace, cx| { - let project = workspace.project(); - let Some((path, entry)) = project.update(cx, |project, cx| { - let path = project.find_project_path(mention_path.path(), cx)?; - let entry = project.entry_for_path(&path, cx)?; - Some((path, entry)) - }) else { - return; - }; + if let Some(mention) = MentionUri::parse(&url).log_err() { + workspace.update(cx, |workspace, cx| match mention { + MentionUri::File(path) => { + let project = workspace.project(); + let Some((path, entry)) = project.update(cx, |project, cx| { + let path = project.find_project_path(path, cx)?; + let entry = project.entry_for_path(&path, cx)?; + Some((path, entry)) + }) else { + return; + }; - if entry.is_dir() { - project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(entry.id)); - }); - } else { - workspace - .open_path(path, None, true, window, cx) - .detach_and_log_err(cx); + if entry.is_dir() { + project.update(cx, |_, cx| { + cx.emit(project::Event::RevealInProjectPanel(entry.id)); + }); + } else { + workspace + .open_path(path, None, true, window, cx) + .detach_and_log_err(cx); + } + } + _ => { + // TODO + unimplemented!() } }) } else { @@ -2975,6 +3009,7 @@ impl AcpThreadView { anchor..anchor, self.message_editor.clone(), self.mention_set.clone(), + self.project.clone(), cx, ); @@ -3117,7 +3152,7 @@ fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { style.base_text_style = text_style; style.link_callback = Some(Rc::new(move |url, cx| { - if MentionPath::try_parse(url).is_some() { + if MentionUri::parse(url).is_ok() { let colors = cx.theme().colors(); Some(TextStyleRefinement { background_color: Some(colors.element_background), From 360d4db87c9ae1072ee92dcb286a4056fa23102e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 12 Aug 2025 15:36:28 +0200 Subject: [PATCH 275/693] python: Fix flickering in the status bar (#36039) - **util: Have maybe! use async closures instead of async blocks** - **python: Fix flickering of virtual environment indicator in status bar** Closes #30723 Release Notes: - Python: Fixed flickering of the status bar virtual environment indicator --------- Co-authored-by: Lukas Wirth --- .../src/active_toolchain.rs | 105 +++++++++++------- crates/util/src/util.rs | 4 +- 2 files changed, 66 insertions(+), 43 deletions(-) diff --git a/crates/toolchain_selector/src/active_toolchain.rs b/crates/toolchain_selector/src/active_toolchain.rs index 631f66a83c..01bd7b0a9c 100644 --- a/crates/toolchain_selector/src/active_toolchain.rs +++ b/crates/toolchain_selector/src/active_toolchain.rs @@ -8,6 +8,7 @@ use gpui::{ use language::{Buffer, BufferEvent, LanguageName, Toolchain}; use project::{Project, ProjectPath, WorktreeId, toolchain_store::ToolchainStoreEvent}; use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, SharedString, Tooltip}; +use util::maybe; use workspace::{StatusItemView, Workspace, item::ItemHandle}; use crate::ToolchainSelector; @@ -55,49 +56,61 @@ impl ActiveToolchain { } fn spawn_tracker_task(window: &mut Window, cx: &mut Context) -> Task> { cx.spawn_in(window, async move |this, cx| { - let active_file = this - .read_with(cx, |this, _| { - this.active_buffer - .as_ref() - .map(|(_, buffer, _)| buffer.clone()) - }) - .ok() - .flatten()?; - let workspace = this.read_with(cx, |this, _| this.workspace.clone()).ok()?; - let language_name = active_file - .read_with(cx, |this, _| Some(this.language()?.name())) - .ok() - .flatten()?; - let term = workspace - .update(cx, |workspace, cx| { - let languages = workspace.project().read(cx).languages(); - Project::toolchain_term(languages.clone(), language_name.clone()) - }) - .ok()? - .await?; - let _ = this.update(cx, |this, cx| { - this.term = term; - cx.notify(); - }); - let (worktree_id, path) = active_file - .update(cx, |this, cx| { - this.file().and_then(|file| { - Some(( - file.worktree_id(cx), - Arc::::from(file.path().parent()?), - )) + let did_set_toolchain = maybe!(async { + let active_file = this + .read_with(cx, |this, _| { + this.active_buffer + .as_ref() + .map(|(_, buffer, _)| buffer.clone()) }) + .ok() + .flatten()?; + let workspace = this.read_with(cx, |this, _| this.workspace.clone()).ok()?; + let language_name = active_file + .read_with(cx, |this, _| Some(this.language()?.name())) + .ok() + .flatten()?; + let term = workspace + .update(cx, |workspace, cx| { + let languages = workspace.project().read(cx).languages(); + Project::toolchain_term(languages.clone(), language_name.clone()) + }) + .ok()? + .await?; + let _ = this.update(cx, |this, cx| { + this.term = term; + cx.notify(); + }); + let (worktree_id, path) = active_file + .update(cx, |this, cx| { + this.file().and_then(|file| { + Some(( + file.worktree_id(cx), + Arc::::from(file.path().parent()?), + )) + }) + }) + .ok() + .flatten()?; + let toolchain = + Self::active_toolchain(workspace, worktree_id, path, language_name, cx).await?; + this.update(cx, |this, cx| { + this.active_toolchain = Some(toolchain); + + cx.notify(); }) .ok() - .flatten()?; - let toolchain = - Self::active_toolchain(workspace, worktree_id, path, language_name, cx).await?; - let _ = this.update(cx, |this, cx| { - this.active_toolchain = Some(toolchain); - - cx.notify(); - }); - Some(()) + }) + .await + .is_some(); + if !did_set_toolchain { + this.update(cx, |this, cx| { + this.active_toolchain = None; + cx.notify(); + }) + .ok(); + } + did_set_toolchain.then_some(()) }) } @@ -110,6 +123,17 @@ impl ActiveToolchain { let editor = editor.read(cx); if let Some((_, buffer, _)) = editor.active_excerpt(cx) { if let Some(worktree_id) = buffer.read(cx).file().map(|file| file.worktree_id(cx)) { + if self + .active_buffer + .as_ref() + .is_some_and(|(old_worktree_id, old_buffer, _)| { + (old_worktree_id, old_buffer.entity_id()) + == (&worktree_id, buffer.entity_id()) + }) + { + return; + } + let subscription = cx.subscribe_in( &buffer, window, @@ -231,7 +255,6 @@ impl StatusItemView for ActiveToolchain { cx: &mut Context, ) { if let Some(editor) = active_pane_item.and_then(|item| item.downcast::()) { - self.active_toolchain.take(); self.update_lister(editor, window, cx); } cx.notify(); diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index b526f53ce4..e1b25f4dba 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -887,10 +887,10 @@ macro_rules! maybe { (|| $block)() }; (async $block:block) => { - (|| async $block)() + (async || $block)() }; (async move $block:block) => { - (|| async move $block)() + (async move || $block)() }; } From d2162446d0bb6c4b3a3ba5cb1f77889c8100aff8 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 12 Aug 2025 16:33:46 +0200 Subject: [PATCH 276/693] python: Fix venv activation in remote projects (#36043) Crux of the issue was that we were checking whether a venv activation script exists on local filesystem, which is obviously wrong for remote projects. This PR also does away with `source` for venv activation in favor of `.`, which is compliant with `sh` Co-authored-by: Lukas Wirth Closes #34648 Release Notes: - Python: fixed activation of virtual environments in terminals for remote projects Co-authored-by: Lukas Wirth --- crates/project/src/terminals.rs | 59 ++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 41d8c4b2fd..5ea7b87fbe 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -256,7 +256,7 @@ impl Project { let local_path = if is_ssh_terminal { None } else { path.clone() }; - let mut python_venv_activate_command = None; + let mut python_venv_activate_command = Task::ready(None); let (spawn_task, shell) = match kind { TerminalKind::Shell(_) => { @@ -265,6 +265,7 @@ impl Project { python_venv_directory, &settings.detect_venv, &settings.shell, + cx, ); } @@ -419,9 +420,12 @@ impl Project { }) .detach(); - if let Some(activate_command) = python_venv_activate_command { - this.activate_python_virtual_environment(activate_command, &terminal_handle, cx); - } + this.activate_python_virtual_environment( + python_venv_activate_command, + &terminal_handle, + cx, + ); + terminal_handle }) } @@ -539,12 +543,15 @@ impl Project { venv_base_directory: &Path, venv_settings: &VenvSettings, shell: &Shell, - ) -> Option { - let venv_settings = venv_settings.as_option()?; + cx: &mut App, + ) -> Task> { + let Some(venv_settings) = venv_settings.as_option() else { + return Task::ready(None); + }; let activate_keyword = match venv_settings.activate_script { terminal_settings::ActivateScript::Default => match std::env::consts::OS { "windows" => ".", - _ => "source", + _ => ".", }, terminal_settings::ActivateScript::Nushell => "overlay use", terminal_settings::ActivateScript::PowerShell => ".", @@ -589,30 +596,44 @@ impl Project { .join(activate_script_name) .to_string_lossy() .to_string(); - let quoted = shlex::try_quote(&path).ok()?; - smol::block_on(self.fs.metadata(path.as_ref())) - .ok() - .flatten()?; - Some(format!( - "{} {} ; clear{}", - activate_keyword, quoted, line_ending - )) + let is_valid_path = self.resolve_abs_path(path.as_ref(), cx); + cx.background_spawn(async move { + let quoted = shlex::try_quote(&path).ok()?; + if is_valid_path.await.is_some_and(|meta| meta.is_file()) { + Some(format!( + "{} {} ; clear{}", + activate_keyword, quoted, line_ending + )) + } else { + None + } + }) } else { - Some(format!( + Task::ready(Some(format!( "{activate_keyword} {activate_script_name} {name}; clear{line_ending}", name = venv_settings.venv_name - )) + ))) } } fn activate_python_virtual_environment( &self, - command: String, + command: Task>, terminal_handle: &Entity, cx: &mut App, ) { - terminal_handle.update(cx, |terminal, _| terminal.input(command.into_bytes())); + terminal_handle.update(cx, |_, cx| { + cx.spawn(async move |this, cx| { + if let Some(command) = command.await { + this.update(cx, |this, _| { + this.input(command.into_bytes()); + }) + .ok(); + } + }) + .detach() + }); } pub fn local_terminal_handles(&self) -> &Vec> { From b105028c058c3333e9866b8d6a20325c42312d1b Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 12 Aug 2025 12:39:27 -0300 Subject: [PATCH 277/693] agent2: Add custom UI for resource link content blocks (#36005) Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga --- crates/acp_thread/src/acp_thread.rs | 89 +++++++---- crates/acp_thread/src/mention.rs | 5 +- crates/agent_ui/src/acp/thread_view.rs | 212 +++++++++++++++---------- 3 files changed, 192 insertions(+), 114 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index eccbef96b8..cadab3d62c 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -299,6 +299,7 @@ impl Display for ToolCallStatus { pub enum ContentBlock { Empty, Markdown { markdown: Entity }, + ResourceLink { resource_link: acp::ResourceLink }, } impl ContentBlock { @@ -330,8 +331,56 @@ impl ContentBlock { language_registry: &Arc, cx: &mut App, ) { - let new_content = match block { + if matches!(self, ContentBlock::Empty) { + if let acp::ContentBlock::ResourceLink(resource_link) = block { + *self = ContentBlock::ResourceLink { resource_link }; + return; + } + } + + let new_content = self.extract_content_from_block(block); + + match self { + ContentBlock::Empty => { + *self = Self::create_markdown_block(new_content, language_registry, cx); + } + ContentBlock::Markdown { markdown } => { + markdown.update(cx, |markdown, cx| markdown.append(&new_content, cx)); + } + ContentBlock::ResourceLink { resource_link } => { + let existing_content = Self::resource_link_to_content(&resource_link.uri); + let combined = format!("{}\n{}", existing_content, new_content); + + *self = Self::create_markdown_block(combined, language_registry, cx); + } + } + } + + fn resource_link_to_content(uri: &str) -> String { + if let Some(uri) = MentionUri::parse(&uri).log_err() { + uri.to_link() + } else { + uri.to_string().clone() + } + } + + fn create_markdown_block( + content: String, + language_registry: &Arc, + cx: &mut App, + ) -> ContentBlock { + ContentBlock::Markdown { + markdown: cx + .new(|cx| Markdown::new(content.into(), Some(language_registry.clone()), None, cx)), + } + } + + fn extract_content_from_block(&self, block: acp::ContentBlock) -> String { + match block { acp::ContentBlock::Text(text_content) => text_content.text.clone(), + acp::ContentBlock::ResourceLink(resource_link) => { + Self::resource_link_to_content(&resource_link.uri) + } acp::ContentBlock::Resource(acp::EmbeddedResource { resource: acp::EmbeddedResourceResource::TextResourceContents(acp::TextResourceContents { @@ -339,35 +388,10 @@ impl ContentBlock { .. }), .. - }) => { - if let Some(uri) = MentionUri::parse(&uri).log_err() { - uri.to_link() - } else { - uri.clone() - } - } + }) => Self::resource_link_to_content(&uri), acp::ContentBlock::Image(_) | acp::ContentBlock::Audio(_) - | acp::ContentBlock::Resource(acp::EmbeddedResource { .. }) - | acp::ContentBlock::ResourceLink(_) => String::new(), - }; - - match self { - ContentBlock::Empty => { - *self = ContentBlock::Markdown { - markdown: cx.new(|cx| { - Markdown::new( - new_content.into(), - Some(language_registry.clone()), - None, - cx, - ) - }), - }; - } - ContentBlock::Markdown { markdown } => { - markdown.update(cx, |markdown, cx| markdown.append(&new_content, cx)); - } + | acp::ContentBlock::Resource(_) => String::new(), } } @@ -375,6 +399,7 @@ impl ContentBlock { match self { ContentBlock::Empty => "", ContentBlock::Markdown { markdown } => markdown.read(cx).source(), + ContentBlock::ResourceLink { resource_link } => &resource_link.uri, } } @@ -382,6 +407,14 @@ impl ContentBlock { match self { ContentBlock::Empty => None, ContentBlock::Markdown { markdown } => Some(markdown), + ContentBlock::ResourceLink { .. } => None, + } + } + + pub fn resource_link(&self) -> Option<&acp::ResourceLink> { + match self { + ContentBlock::ResourceLink { resource_link } => Some(resource_link), + _ => None, } } } diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 1fcd27ad4c..59c479d87b 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -19,7 +19,10 @@ impl MentionUri { if let Some(fragment) = url.fragment() { Ok(Self::Symbol(path.into(), fragment.into())) } else { - Ok(Self::File(path.into())) + let file_path = + PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path)); + + Ok(Self::File(file_path)) } } "zed" => { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 6d8dccd18f..791542cf26 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1108,10 +1108,10 @@ impl AcpThreadView { .size(IconSize::Small) .color(Color::Muted); + let base_container = h_flex().size_4().justify_center(); + if is_collapsible { - h_flex() - .size_4() - .justify_center() + base_container .child( div() .group_hover(&group_name, |s| s.invisible().w_0()) @@ -1142,7 +1142,7 @@ impl AcpThreadView { ), ) } else { - div().child(tool_icon) + base_container.child(tool_icon) } } @@ -1205,8 +1205,10 @@ impl AcpThreadView { ToolCallContent::Diff(diff) => diff.read(cx).has_revealed_range(cx), _ => false, }); - let is_collapsible = - !tool_call.content.is_empty() && !needs_confirmation && !is_edit && !has_diff; + let use_card_layout = needs_confirmation || is_edit || has_diff; + + let is_collapsible = !tool_call.content.is_empty() && !use_card_layout; + let is_open = tool_call.content.is_empty() || needs_confirmation || has_nonempty_diff @@ -1225,9 +1227,39 @@ impl AcpThreadView { linear_color_stop(color.opacity(0.2), 0.), )) }; + let gradient_color = if use_card_layout { + self.tool_card_header_bg(cx) + } else { + cx.theme().colors().panel_background + }; + + let tool_output_display = match &tool_call.status { + ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex() + .w_full() + .children(tool_call.content.iter().map(|content| { + div() + .child(self.render_tool_call_content(content, tool_call, window, cx)) + .into_any_element() + })) + .child(self.render_permission_buttons( + options, + entry_ix, + tool_call.id.clone(), + tool_call.content.is_empty(), + cx, + )), + ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => v_flex() + .w_full() + .children(tool_call.content.iter().map(|content| { + div() + .child(self.render_tool_call_content(content, tool_call, window, cx)) + .into_any_element() + })), + ToolCallStatus::Rejected => v_flex().size_0(), + }; v_flex() - .when(needs_confirmation || is_edit || has_diff, |this| { + .when(use_card_layout, |this| { this.rounded_lg() .border_1() .border_color(self.tool_card_border_color(cx)) @@ -1241,7 +1273,7 @@ impl AcpThreadView { .gap_1() .justify_between() .map(|this| { - if needs_confirmation || is_edit || has_diff { + if use_card_layout { this.pl_2() .pr_1() .py_1() @@ -1258,13 +1290,6 @@ impl AcpThreadView { .group(&card_header_id) .relative() .w_full() - .map(|this| { - if tool_call.locations.len() == 1 { - this.gap_0() - } else { - this.gap_1p5() - } - }) .text_size(self.tool_name_font_size()) .child(self.render_tool_call_icon( card_header_id, @@ -1308,6 +1333,7 @@ impl AcpThreadView { .id("non-card-label-container") .w_full() .relative() + .ml_1p5() .overflow_hidden() .child( h_flex() @@ -1324,17 +1350,7 @@ impl AcpThreadView { ), )), ) - .map(|this| { - if needs_confirmation { - this.child(gradient_overlay( - self.tool_card_header_bg(cx), - )) - } else { - this.child(gradient_overlay( - cx.theme().colors().panel_background, - )) - } - }) + .child(gradient_overlay(gradient_color)) .on_click(cx.listener({ let id = tool_call.id.clone(); move |this: &mut Self, _, _, cx: &mut Context| { @@ -1351,54 +1367,7 @@ impl AcpThreadView { ) .children(status_icon), ) - .when(is_open, |this| { - this.child( - v_flex() - .text_xs() - .when(is_collapsible, |this| { - this.mt_1() - .border_1() - .border_color(self.tool_card_border_color(cx)) - .bg(cx.theme().colors().editor_background) - .rounded_lg() - }) - .map(|this| { - if is_open { - match &tool_call.status { - ToolCallStatus::WaitingForConfirmation { options, .. } => this - .children(tool_call.content.iter().map(|content| { - div() - .py_1p5() - .child(self.render_tool_call_content( - content, tool_call, window, cx, - )) - .into_any_element() - })) - .child(self.render_permission_buttons( - options, - entry_ix, - tool_call.id.clone(), - tool_call.content.is_empty(), - cx, - )), - ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => { - this.children(tool_call.content.iter().map(|content| { - div() - .py_1p5() - .child(self.render_tool_call_content( - content, tool_call, window, cx, - )) - .into_any_element() - })) - } - ToolCallStatus::Rejected => this, - } - } else { - this - } - }), - ) - }) + .when(is_open, |this| this.child(tool_output_display)) } fn render_tool_call_content( @@ -1410,16 +1379,10 @@ impl AcpThreadView { ) -> AnyElement { match content { ToolCallContent::ContentBlock(content) => { - if let Some(md) = content.markdown() { - div() - .p_2() - .child( - self.render_markdown( - md.clone(), - default_markdown_style(false, window, cx), - ), - ) - .into_any_element() + if let Some(resource_link) = content.resource_link() { + self.render_resource_link(resource_link, cx) + } else if let Some(markdown) = content.markdown() { + self.render_markdown_output(markdown.clone(), tool_call.id.clone(), window, cx) } else { Empty.into_any_element() } @@ -1431,6 +1394,83 @@ impl AcpThreadView { } } + fn render_markdown_output( + &self, + markdown: Entity, + tool_call_id: acp::ToolCallId, + window: &Window, + cx: &Context, + ) -> AnyElement { + let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id.clone())); + + v_flex() + .mt_1p5() + .ml(px(7.)) + .px_3p5() + .gap_2() + .border_l_1() + .border_color(self.tool_card_border_color(cx)) + .text_sm() + .text_color(cx.theme().colors().text_muted) + .child(self.render_markdown(markdown, default_markdown_style(false, window, cx))) + .child( + Button::new(button_id, "Collapse Output") + .full_width() + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .icon(IconName::ChevronUp) + .icon_color(Color::Muted) + .icon_position(IconPosition::Start) + .on_click(cx.listener({ + let id = tool_call_id.clone(); + move |this: &mut Self, _, _, cx: &mut Context| { + this.expanded_tool_calls.remove(&id); + cx.notify(); + } + })), + ) + .into_any_element() + } + + fn render_resource_link( + &self, + resource_link: &acp::ResourceLink, + cx: &Context, + ) -> AnyElement { + let uri: SharedString = resource_link.uri.clone().into(); + + let label: SharedString = if let Some(path) = resource_link.uri.strip_prefix("file://") { + path.to_string().into() + } else { + uri.clone() + }; + + let button_id = SharedString::from(format!("item-{}", uri.clone())); + + div() + .ml(px(7.)) + .pl_2p5() + .border_l_1() + .border_color(self.tool_card_border_color(cx)) + .overflow_hidden() + .child( + Button::new(button_id, label) + .label_size(LabelSize::Small) + .color(Color::Muted) + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .truncate(true) + .on_click(cx.listener({ + let workspace = self.workspace.clone(); + move |_, _, window, cx: &mut Context| { + Self::open_link(uri.clone(), &workspace, window, cx); + } + })), + ) + .into_any_element() + } + fn render_permission_buttons( &self, options: &[acp::PermissionOption], @@ -1706,7 +1746,9 @@ impl AcpThreadView { .overflow_hidden() .child( v_flex() - .p_2() + .pt_1() + .pb_2() + .px_2() .gap_0p5() .bg(header_bg) .text_xs() From 39c19abdfdb7f64226afdcc688eb74cd26de7f4e Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 12 Aug 2025 11:55:10 -0400 Subject: [PATCH 278/693] Update windows alpha GitHub Issue template (#36049) Release Notes: - N/A --- .github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml b/.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml index bf39560a3c..826c2b8027 100644 --- a/.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml +++ b/.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml @@ -1,15 +1,15 @@ -name: Bug Report (Windows) -description: Zed Windows-Related Bugs +name: Bug Report (Windows Alpha) +description: Zed Windows Alpha Related Bugs type: "Bug" labels: ["windows"] -title: "Windows: " +title: "Windows Alpha: " body: - type: textarea attributes: label: Summary - description: Describe the bug with a one line summary, and provide detailed reproduction steps + description: Describe the bug with a one-line summary, and provide detailed reproduction steps value: | - + SUMMARY_SENTENCE_HERE ### Description From d8fc53608ec8cac89ef3caa7d16f3919308dfc61 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Tue, 12 Aug 2025 19:03:13 +0300 Subject: [PATCH 279/693] docs: Update OpenAI models list (#36050) Closes #ISSUE Release Notes: - N/A --- docs/src/ai/llm-providers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/ai/llm-providers.md b/docs/src/ai/llm-providers.md index 64995e6eb8..21ff2a8a51 100644 --- a/docs/src/ai/llm-providers.md +++ b/docs/src/ai/llm-providers.md @@ -391,7 +391,7 @@ Zed will also use the `OPENAI_API_KEY` environment variable if it's defined. #### Custom Models {#openai-custom-models} -The Zed agent comes pre-configured to use the latest version for common models (GPT-3.5 Turbo, GPT-4, GPT-4 Turbo, GPT-4o, GPT-4o mini). +The Zed agent comes pre-configured to use the latest version for common models (GPT-5, GPT-5 mini, o4-mini, GPT-4.1, and others). To use alternate models, perhaps a preview release or a dated model release, or if you wish to control the request parameters, you can do so by adding the following to your Zed `settings.json`: ```json From 9de04ce21528d23ac81e5292b4d963579539abf6 Mon Sep 17 00:00:00 2001 From: Rishabh Bothra <37180068+07rjain@users.noreply.github.com> Date: Tue, 12 Aug 2025 21:34:51 +0530 Subject: [PATCH 280/693] language_models: Add vision support for OpenAI gpt-5, gpt-5-mini, and gpt-5-nano models (#36047) ## Summary Enable image processing capabilities for GPT-5 series models by updating the `supports_images()` method. ## Changes - Add vision support for `gpt-5`, `gpt-5-mini`, and `gpt-5-nano` models - Update `supports_images()` method in `crates/language_models/src/provider/open_ai.rs` ## Models with Vision Support (after this PR) - gpt-4o - gpt-4o-mini - gpt-4.1 - gpt-4.1-mini - gpt-4.1-nano - gpt-5 (new) - gpt-5-mini (new) - gpt-5-nano (new) - o1 - o3 - o4-mini This brings GPT-5 vision capabilities in line with other OpenAI models that support image processing. Release Notes: - Added vision support for OpenAI models --- .../language_models/src/provider/open_ai.rs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 7a6c8e09ed..2879b01ff3 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -301,7 +301,25 @@ impl LanguageModel for OpenAiLanguageModel { } fn supports_images(&self) -> bool { - false + use open_ai::Model; + match &self.model { + Model::FourOmni + | Model::FourOmniMini + | Model::FourPointOne + | Model::FourPointOneMini + | Model::FourPointOneNano + | Model::Five + | Model::FiveMini + | Model::FiveNano + | Model::O1 + | Model::O3 + | Model::O4Mini => true, + Model::ThreePointFiveTurbo + | Model::Four + | Model::FourTurbo + | Model::O3Mini + | Model::Custom { .. } => false, + } } fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { From 1f20d5bf54e2b69759b669abde8b2896dab983e0 Mon Sep 17 00:00:00 2001 From: localcc Date: Tue, 12 Aug 2025 18:18:42 +0200 Subject: [PATCH 281/693] Fix nightly icon (#36051) Release Notes: - N/A --- .../zed/resources/windows/app-icon-nightly.ico | Bin 0 -> 193385 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/crates/zed/resources/windows/app-icon-nightly.ico b/crates/zed/resources/windows/app-icon-nightly.ico index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..875c0d7b3546ad22f626a3b1d9236de5ea244594 100644 GIT binary patch literal 193385 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 z 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%0Dy+wN5tp8J#<^QX*gYG2d{-5RM{NGJl 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 z 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∾`0pwq^k0*W?Y{~K%l{Zfng6;h4F9wIz5hp8Tl}Bj+w*^6Z_oePjSc@N z7Z?7YTUGXdeP8>3P&sJ8&ivnom;JxHr0{>x{aK(pnBAl#{+n~K|IhLB`9HU(>wmP3 z)&EEf)Bh2sM*qVN_5X+K>io}kaQMG};mrT$9BlusdD;GZ%ZP#7&$?W!zdfWS|J(BN z{WoA{`k&+H`#(Rx|9_5;_y24!&;J>2&i~UK?f)m)*!<77w)yYK4xQhY68~?&$@<%b zhxM|flo)uP)(+GM1l@53YJ;$V*5NS!H)3J=Z_CR1--m6Mo>>3uvP0%ij6ipH iv9tU) Date: Tue, 12 Aug 2025 18:13:36 +0100 Subject: [PATCH 282/693] vim: Support filename in :tabedit and :tabnew commands (#35775) Update both `:tabedit` and `:tabnew` commands in order to support a single argument, a filename, that, when provided, ensures that the new tab either opens an existing file or associates the new tab with the filename, so that when saving the buffer's content, the file is created. Relates to #21112 Release Notes: - vim: Added support for filenames in both `:tabnew` and `:tabedit` commands --- crates/vim/src/command.rs | 112 +++++++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index f7889d8cd8..264fa4bf2f 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1175,8 +1175,10 @@ fn generate_commands(_: &App) -> Vec { VimCommand::str(("ls", ""), "tab_switcher::ToggleAll"), VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal), VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical), - VimCommand::new(("tabe", "dit"), workspace::NewFile), - VimCommand::new(("tabnew", ""), workspace::NewFile), + VimCommand::new(("tabe", "dit"), workspace::NewFile) + .args(|_action, args| Some(VimEdit { filename: args }.boxed_clone())), + VimCommand::new(("tabnew", ""), workspace::NewFile) + .args(|_action, args| Some(VimEdit { filename: args }.boxed_clone())), VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(), VimCommand::new(("tabp", "revious"), workspace::ActivatePreviousItem).count(), VimCommand::new(("tabN", "ext"), workspace::ActivatePreviousItem).count(), @@ -2476,4 +2478,110 @@ mod test { "}); // Once ctrl-v to input character literals is added there should be a test for redo } + + #[gpui::test] + async fn test_command_tabnew(cx: &mut TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // Create a new file to ensure that, when the filename is used with + // `:tabnew`, it opens the existing file in a new tab. + let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone()); + fs.as_fake() + .insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec()) + .await; + + cx.simulate_keystrokes(": tabnew"); + cx.simulate_keystrokes("enter"); + cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2)); + + // Assert that the new tab is empty and not associated with any file, as + // no file path was provided to the `:tabnew` command. + cx.workspace(|workspace, _window, cx| { + let active_editor = workspace.active_item_as::(cx).unwrap(); + let buffer = active_editor + .read(cx) + .buffer() + .read(cx) + .as_singleton() + .unwrap(); + + assert!(&buffer.read(cx).file().is_none()); + }); + + // Leverage the filename as an argument to the `:tabnew` command, + // ensuring that the file, instead of an empty buffer, is opened in a + // new tab. + cx.simulate_keystrokes(": tabnew space dir/file_2.rs"); + cx.simulate_keystrokes("enter"); + + cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3)); + cx.workspace(|workspace, _, cx| { + assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx); + }); + + // If the `filename` argument provided to the `:tabnew` command is for a + // file that doesn't yet exist, it should still associate the buffer + // with that file path, so that when the buffer contents are saved, the + // file is created. + cx.simulate_keystrokes(": tabnew space dir/file_3.rs"); + cx.simulate_keystrokes("enter"); + + cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4)); + cx.workspace(|workspace, _, cx| { + assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx); + }); + } + + #[gpui::test] + async fn test_command_tabedit(cx: &mut TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // Create a new file to ensure that, when the filename is used with + // `:tabedit`, it opens the existing file in a new tab. + let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone()); + fs.as_fake() + .insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec()) + .await; + + cx.simulate_keystrokes(": tabedit"); + cx.simulate_keystrokes("enter"); + cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2)); + + // Assert that the new tab is empty and not associated with any file, as + // no file path was provided to the `:tabedit` command. + cx.workspace(|workspace, _window, cx| { + let active_editor = workspace.active_item_as::(cx).unwrap(); + let buffer = active_editor + .read(cx) + .buffer() + .read(cx) + .as_singleton() + .unwrap(); + + assert!(&buffer.read(cx).file().is_none()); + }); + + // Leverage the filename as an argument to the `:tabedit` command, + // ensuring that the file, instead of an empty buffer, is opened in a + // new tab. + cx.simulate_keystrokes(": tabedit space dir/file_2.rs"); + cx.simulate_keystrokes("enter"); + + cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3)); + cx.workspace(|workspace, _, cx| { + assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx); + }); + + // If the `filename` argument provided to the `:tabedit` command is for a + // file that doesn't yet exist, it should still associate the buffer + // with that file path, so that when the buffer contents are saved, the + // file is created. + cx.simulate_keystrokes(": tabedit space dir/file_3.rs"); + cx.simulate_keystrokes("enter"); + + cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4)); + cx.workspace(|workspace, _, cx| { + assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx); + }); + } } From bfbb18476f73c2aa912bb1deb8fe12d28f93ee8f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 12 Aug 2025 10:26:56 -0700 Subject: [PATCH 283/693] Fix management of rust-analyzer binaries on windows (#36056) Closes https://github.com/zed-industries/zed/issues/34472 * Avoid removing the just-downloaded exe * Invoke exe within nested version directory Release Notes: - Fix issue where Rust-analyzer was not installed correctly on windows Co-authored-by: Lukas Wirth --- crates/languages/src/rust.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index e79f0c9e8e..3baaec1842 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -238,7 +238,7 @@ impl LspAdapter for RustLspAdapter { ) .await?; make_file_executable(&server_path).await?; - remove_matching(&container_dir, |path| server_path != path).await; + remove_matching(&container_dir, |path| path != destination_path).await; GithubBinaryMetadata::write_to_file( &GithubBinaryMetadata { metadata_version: 1, @@ -1023,8 +1023,14 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option path.clone(), // Tar and gzip extract in place. + AssetKind::Zip => path.clone().join("rust-analyzer.exe"), // zip contains a .exe + }; + anyhow::Ok(LanguageServerBinary { - path: last.context("no cached binary")?, + path, env: None, arguments: Default::default(), }) From 42b7dbeaeee8182c96c09102239e44ceacf055a3 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 12 Aug 2025 10:53:19 -0700 Subject: [PATCH 284/693] Remove beta tag from cursor keymap (#36061) Release Notes: - N/A Co-authored-by: Anthony Eid --- crates/settings/src/base_keymap_setting.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/settings/src/base_keymap_setting.rs b/crates/settings/src/base_keymap_setting.rs index 6916d98ae3..91dda03d00 100644 --- a/crates/settings/src/base_keymap_setting.rs +++ b/crates/settings/src/base_keymap_setting.rs @@ -44,7 +44,7 @@ impl BaseKeymap { ("Sublime Text", Self::SublimeText), ("Emacs (beta)", Self::Emacs), ("TextMate", Self::TextMate), - ("Cursor (beta)", Self::Cursor), + ("Cursor", Self::Cursor), ]; #[cfg(not(target_os = "macos"))] @@ -54,7 +54,7 @@ impl BaseKeymap { ("JetBrains", Self::JetBrains), ("Sublime Text", Self::SublimeText), ("Emacs (beta)", Self::Emacs), - ("Cursor (beta)", Self::Cursor), + ("Cursor", Self::Cursor), ]; pub fn asset_path(&self) -> Option<&'static str> { From 3a0465773050d0ce8319cc4a2276f688d72e7635 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 12 Aug 2025 14:24:25 -0400 Subject: [PATCH 285/693] emmet: Add workaround for leading `/` on Windows paths (#36064) This PR adds a workaround for the leading `/` on Windows paths (https://github.com/zed-industries/zed/issues/20559). Release Notes: - N/A --- extensions/emmet/src/emmet.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/extensions/emmet/src/emmet.rs b/extensions/emmet/src/emmet.rs index 83fe809c34..e4fb3cf814 100644 --- a/extensions/emmet/src/emmet.rs +++ b/extensions/emmet/src/emmet.rs @@ -70,8 +70,7 @@ impl zed::Extension for EmmetExtension { Ok(zed::Command { command: zed::node_binary_path()?, args: vec![ - env::current_dir() - .unwrap() + zed_ext::sanitize_windows_path(env::current_dir().unwrap()) .join(&server_path) .to_string_lossy() .to_string(), @@ -83,3 +82,25 @@ impl zed::Extension for EmmetExtension { } zed::register_extension!(EmmetExtension); + +/// Extensions to the Zed extension API that have not yet stabilized. +mod zed_ext { + /// Sanitizes the given path to remove the leading `/` on Windows. + /// + /// On macOS and Linux this is a no-op. + /// + /// This is a workaround for https://github.com/bytecodealliance/wasmtime/issues/10415. + pub fn sanitize_windows_path(path: std::path::PathBuf) -> std::path::PathBuf { + use zed_extension_api::{Os, current_platform}; + + let (os, _arch) = current_platform(); + match os { + Os::Mac | Os::Linux => path, + Os::Windows => path + .to_string_lossy() + .to_string() + .trim_start_matches('/') + .into(), + } + } +} From b62f9595286d322e0a78daa73f58921c23a51d03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Wed, 13 Aug 2025 02:28:47 +0800 Subject: [PATCH 286/693] windows: Fix message loop using too much CPU (#35969) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #34374 This is a leftover issue from #34374. Back in #34374, I wanted to use DirectX to handle vsync, after all, that’s how 99% of Windows apps do it. But after discussing with @maxbrunsfeld , we decided to stick with the original vsync approach given gpui’s architecture. In my tests, there’s no noticeable performance difference between this PR’s approach and DirectX vsync. That said, this PR’s method does have a theoretical advantage, it doesn’t block the main thread while waiting for vsync. The only difference is that in this PR, on Windows 11 we use a newer API instead of `DwmFlush`, since Chrome’s tests have shown that `DwmFlush` has some problems. This PR also removes the use of `MsgWaitForMultipleObjects`. Release Notes: - N/A --------- Co-authored-by: Max Brunsfeld --- Cargo.toml | 1 + crates/gpui/src/platform/windows.rs | 2 + .../src/platform/windows/directx_renderer.rs | 27 +-- crates/gpui/src/platform/windows/platform.rs | 67 +++---- crates/gpui/src/platform/windows/util.rs | 24 ++- crates/gpui/src/platform/windows/vsync.rs | 174 ++++++++++++++++++ crates/gpui/src/platform/windows/wrapper.rs | 30 ++- 7 files changed, 269 insertions(+), 56 deletions(-) create mode 100644 crates/gpui/src/platform/windows/vsync.rs diff --git a/Cargo.toml b/Cargo.toml index 48a11c27da..dd14078dd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -714,6 +714,7 @@ features = [ "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Ole", + "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_SystemInformation", "Win32_System_SystemServices", diff --git a/crates/gpui/src/platform/windows.rs b/crates/gpui/src/platform/windows.rs index 5268d3ccba..77e0ca41bf 100644 --- a/crates/gpui/src/platform/windows.rs +++ b/crates/gpui/src/platform/windows.rs @@ -10,6 +10,7 @@ mod keyboard; mod platform; mod system_settings; mod util; +mod vsync; mod window; mod wrapper; @@ -25,6 +26,7 @@ pub(crate) use keyboard::*; pub(crate) use platform::*; pub(crate) use system_settings::*; pub(crate) use util::*; +pub(crate) use vsync::*; pub(crate) use window::*; pub(crate) use wrapper::*; diff --git a/crates/gpui/src/platform/windows/directx_renderer.rs b/crates/gpui/src/platform/windows/directx_renderer.rs index 585b1dab1c..4e72ded534 100644 --- a/crates/gpui/src/platform/windows/directx_renderer.rs +++ b/crates/gpui/src/platform/windows/directx_renderer.rs @@ -4,16 +4,15 @@ use ::util::ResultExt; use anyhow::{Context, Result}; use windows::{ Win32::{ - Foundation::{FreeLibrary, HMODULE, HWND}, + Foundation::{HMODULE, HWND}, Graphics::{ Direct3D::*, Direct3D11::*, DirectComposition::*, Dxgi::{Common::*, *}, }, - System::LibraryLoader::LoadLibraryA, }, - core::{Interface, PCSTR}, + core::Interface, }; use crate::{ @@ -208,7 +207,7 @@ impl DirectXRenderer { fn present(&mut self) -> Result<()> { unsafe { - let result = self.resources.swap_chain.Present(1, DXGI_PRESENT(0)); + let result = self.resources.swap_chain.Present(0, DXGI_PRESENT(0)); // Presenting the swap chain can fail if the DirectX device was removed or reset. if result == DXGI_ERROR_DEVICE_REMOVED || result == DXGI_ERROR_DEVICE_RESET { let reason = self.devices.device.GetDeviceRemovedReason(); @@ -1619,22 +1618,6 @@ pub(crate) mod shader_resources { } } -fn with_dll_library(dll_name: PCSTR, f: F) -> Result -where - F: FnOnce(HMODULE) -> Result, -{ - let library = unsafe { - LoadLibraryA(dll_name).with_context(|| format!("Loading dll: {}", dll_name.display()))? - }; - let result = f(library); - unsafe { - FreeLibrary(library) - .with_context(|| format!("Freeing dll: {}", dll_name.display())) - .log_err(); - } - result -} - mod nvidia { use std::{ ffi::CStr, @@ -1644,7 +1627,7 @@ mod nvidia { use anyhow::Result; use windows::{Win32::System::LibraryLoader::GetProcAddress, core::s}; - use crate::platform::windows::directx_renderer::with_dll_library; + use crate::with_dll_library; // https://github.com/NVIDIA/nvapi/blob/7cb76fce2f52de818b3da497af646af1ec16ce27/nvapi_lite_common.h#L180 const NVAPI_SHORT_STRING_MAX: usize = 64; @@ -1711,7 +1694,7 @@ mod amd { use anyhow::Result; use windows::{Win32::System::LibraryLoader::GetProcAddress, core::s}; - use crate::platform::windows::directx_renderer::with_dll_library; + use crate::with_dll_library; // https://github.com/GPUOpen-LibrariesAndSDKs/AGS_SDK/blob/5d8812d703d0335741b6f7ffc37838eeb8b967f7/ags_lib/inc/amd_ags.h#L145 const AGS_CURRENT_VERSION: i32 = (6 << 22) | (3 << 12); diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 01b043a755..9e5d359e43 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -32,7 +32,7 @@ use crate::*; pub(crate) struct WindowsPlatform { state: RefCell, - raw_window_handles: RwLock>, + raw_window_handles: Arc>>, // The below members will never change throughout the entire lifecycle of the app. icon: HICON, main_receiver: flume::Receiver, @@ -114,7 +114,7 @@ impl WindowsPlatform { }; let icon = load_icon().unwrap_or_default(); let state = RefCell::new(WindowsPlatformState::new()); - let raw_window_handles = RwLock::new(SmallVec::new()); + let raw_window_handles = Arc::new(RwLock::new(SmallVec::new())); let windows_version = WindowsVersion::new().context("Error retrieve windows version")?; Ok(Self { @@ -134,22 +134,12 @@ impl WindowsPlatform { }) } - fn redraw_all(&self) { - for handle in self.raw_window_handles.read().iter() { - unsafe { - RedrawWindow(Some(*handle), None, None, RDW_INVALIDATE | RDW_UPDATENOW) - .ok() - .log_err(); - } - } - } - pub fn window_from_hwnd(&self, hwnd: HWND) -> Option> { self.raw_window_handles .read() .iter() - .find(|entry| *entry == &hwnd) - .and_then(|hwnd| window_from_hwnd(*hwnd)) + .find(|entry| entry.as_raw() == hwnd) + .and_then(|hwnd| window_from_hwnd(hwnd.as_raw())) } #[inline] @@ -158,7 +148,7 @@ impl WindowsPlatform { .read() .iter() .for_each(|handle| unsafe { - PostMessageW(Some(*handle), message, wparam, lparam).log_err(); + PostMessageW(Some(handle.as_raw()), message, wparam, lparam).log_err(); }); } @@ -166,7 +156,7 @@ impl WindowsPlatform { let mut lock = self.raw_window_handles.write(); let index = lock .iter() - .position(|handle| *handle == target_window) + .position(|handle| handle.as_raw() == target_window) .unwrap(); lock.remove(index); @@ -226,19 +216,19 @@ impl WindowsPlatform { } } - // Returns true if the app should quit. - fn handle_events(&self) -> bool { + // Returns if the app should quit. + fn handle_events(&self) { let mut msg = MSG::default(); unsafe { - while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() { + while GetMessageW(&mut msg, None, 0, 0).as_bool() { match msg.message { - WM_QUIT => return true, + WM_QUIT => return, WM_INPUTLANGCHANGE | WM_GPUI_CLOSE_ONE_WINDOW | WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD | WM_GPUI_DOCK_MENU_ACTION => { if self.handle_gpui_evnets(msg.message, msg.wParam, msg.lParam, &msg) { - return true; + return; } } _ => { @@ -247,7 +237,6 @@ impl WindowsPlatform { } } } - false } // Returns true if the app should quit. @@ -315,8 +304,28 @@ impl WindowsPlatform { self.raw_window_handles .read() .iter() - .find(|&&hwnd| hwnd == active_window_hwnd) - .copied() + .find(|hwnd| hwnd.as_raw() == active_window_hwnd) + .map(|hwnd| hwnd.as_raw()) + } + + fn begin_vsync_thread(&self) { + let all_windows = Arc::downgrade(&self.raw_window_handles); + std::thread::spawn(move || { + let vsync_provider = VSyncProvider::new(); + loop { + vsync_provider.wait_for_vsync(); + let Some(all_windows) = all_windows.upgrade() else { + break; + }; + for hwnd in all_windows.read().iter() { + unsafe { + RedrawWindow(Some(hwnd.as_raw()), None, None, RDW_INVALIDATE) + .ok() + .log_err(); + } + } + } + }); } } @@ -347,12 +356,8 @@ impl Platform for WindowsPlatform { fn run(&self, on_finish_launching: Box) { on_finish_launching(); - loop { - if self.handle_events() { - break; - } - self.redraw_all(); - } + self.begin_vsync_thread(); + self.handle_events(); if let Some(ref mut callback) = self.state.borrow_mut().callbacks.quit { callback(); @@ -445,7 +450,7 @@ impl Platform for WindowsPlatform { ) -> Result> { let window = WindowsWindow::new(handle, options, self.generate_creation_info())?; let handle = window.get_raw_handle(); - self.raw_window_handles.write().push(handle); + self.raw_window_handles.write().push(handle.into()); Ok(Box::new(window)) } diff --git a/crates/gpui/src/platform/windows/util.rs b/crates/gpui/src/platform/windows/util.rs index 5fb8febe3b..af71dfe4a1 100644 --- a/crates/gpui/src/platform/windows/util.rs +++ b/crates/gpui/src/platform/windows/util.rs @@ -1,14 +1,18 @@ use std::sync::OnceLock; use ::util::ResultExt; +use anyhow::Context; use windows::{ UI::{ Color, ViewManagement::{UIColorType, UISettings}, }, Wdk::System::SystemServices::RtlGetVersion, - Win32::{Foundation::*, Graphics::Dwm::*, UI::WindowsAndMessaging::*}, - core::{BOOL, HSTRING}, + Win32::{ + Foundation::*, Graphics::Dwm::*, System::LibraryLoader::LoadLibraryA, + UI::WindowsAndMessaging::*, + }, + core::{BOOL, HSTRING, PCSTR}, }; use crate::*; @@ -197,3 +201,19 @@ pub(crate) fn show_error(title: &str, content: String) { ) }; } + +pub(crate) fn with_dll_library(dll_name: PCSTR, f: F) -> Result +where + F: FnOnce(HMODULE) -> Result, +{ + let library = unsafe { + LoadLibraryA(dll_name).with_context(|| format!("Loading dll: {}", dll_name.display()))? + }; + let result = f(library); + unsafe { + FreeLibrary(library) + .with_context(|| format!("Freeing dll: {}", dll_name.display())) + .log_err(); + } + result +} diff --git a/crates/gpui/src/platform/windows/vsync.rs b/crates/gpui/src/platform/windows/vsync.rs new file mode 100644 index 0000000000..09dbfd0231 --- /dev/null +++ b/crates/gpui/src/platform/windows/vsync.rs @@ -0,0 +1,174 @@ +use std::{ + sync::LazyLock, + time::{Duration, Instant}, +}; + +use anyhow::{Context, Result}; +use util::ResultExt; +use windows::{ + Win32::{ + Foundation::{HANDLE, HWND}, + Graphics::{ + DirectComposition::{ + COMPOSITION_FRAME_ID_COMPLETED, COMPOSITION_FRAME_ID_TYPE, COMPOSITION_FRAME_STATS, + COMPOSITION_TARGET_ID, + }, + Dwm::{DWM_TIMING_INFO, DwmFlush, DwmGetCompositionTimingInfo}, + }, + System::{ + LibraryLoader::{GetModuleHandleA, GetProcAddress}, + Performance::QueryPerformanceFrequency, + Threading::INFINITE, + }, + }, + core::{HRESULT, s}, +}; + +static QPC_TICKS_PER_SECOND: LazyLock = LazyLock::new(|| { + let mut frequency = 0; + // On systems that run Windows XP or later, the function will always succeed and + // will thus never return zero. + unsafe { QueryPerformanceFrequency(&mut frequency).unwrap() }; + frequency as u64 +}); + +const VSYNC_INTERVAL_THRESHOLD: Duration = Duration::from_millis(1); +const DEFAULT_VSYNC_INTERVAL: Duration = Duration::from_micros(16_666); // ~60Hz + +// Here we are using dynamic loading of DirectComposition functions, +// or the app will refuse to start on windows systems that do not support DirectComposition. +type DCompositionGetFrameId = + unsafe extern "system" fn(frameidtype: COMPOSITION_FRAME_ID_TYPE, frameid: *mut u64) -> HRESULT; +type DCompositionGetStatistics = unsafe extern "system" fn( + frameid: u64, + framestats: *mut COMPOSITION_FRAME_STATS, + targetidcount: u32, + targetids: *mut COMPOSITION_TARGET_ID, + actualtargetidcount: *mut u32, +) -> HRESULT; +type DCompositionWaitForCompositorClock = + unsafe extern "system" fn(count: u32, handles: *const HANDLE, timeoutinms: u32) -> u32; + +pub(crate) struct VSyncProvider { + interval: Duration, + f: Box bool>, +} + +impl VSyncProvider { + pub(crate) fn new() -> Self { + if let Some((get_frame_id, get_statistics, wait_for_comp_clock)) = + initialize_direct_composition() + .context("Retrieving DirectComposition functions") + .log_with_level(log::Level::Warn) + { + let interval = get_dwm_interval_from_direct_composition(get_frame_id, get_statistics) + .context("Failed to get DWM interval from DirectComposition") + .log_err() + .unwrap_or(DEFAULT_VSYNC_INTERVAL); + log::info!( + "DirectComposition is supported for VSync, interval: {:?}", + interval + ); + let f = Box::new(move || unsafe { + wait_for_comp_clock(0, std::ptr::null(), INFINITE) == 0 + }); + Self { interval, f } + } else { + let interval = get_dwm_interval() + .context("Failed to get DWM interval") + .log_err() + .unwrap_or(DEFAULT_VSYNC_INTERVAL); + log::info!( + "DirectComposition is not supported for VSync, falling back to DWM, interval: {:?}", + interval + ); + let f = Box::new(|| unsafe { DwmFlush().is_ok() }); + Self { interval, f } + } + } + + pub(crate) fn wait_for_vsync(&self) { + let vsync_start = Instant::now(); + let wait_succeeded = (self.f)(); + let elapsed = vsync_start.elapsed(); + // DwmFlush and DCompositionWaitForCompositorClock returns very early + // instead of waiting until vblank when the monitor goes to sleep or is + // unplugged (nothing to present due to desktop occlusion). We use 1ms as + // a threshhold for the duration of the wait functions and fallback to + // Sleep() if it returns before that. This could happen during normal + // operation for the first call after the vsync thread becomes non-idle, + // but it shouldn't happen often. + if !wait_succeeded || elapsed < VSYNC_INTERVAL_THRESHOLD { + log::warn!("VSyncProvider::wait_for_vsync() took shorter than expected"); + std::thread::sleep(self.interval); + } + } +} + +fn initialize_direct_composition() -> Result<( + DCompositionGetFrameId, + DCompositionGetStatistics, + DCompositionWaitForCompositorClock, +)> { + unsafe { + // Load DLL at runtime since older Windows versions don't have dcomp. + let hmodule = GetModuleHandleA(s!("dcomp.dll")).context("Loading dcomp.dll")?; + let get_frame_id_addr = GetProcAddress(hmodule, s!("DCompositionGetFrameId")) + .context("Function DCompositionGetFrameId not found")?; + let get_statistics_addr = GetProcAddress(hmodule, s!("DCompositionGetStatistics")) + .context("Function DCompositionGetStatistics not found")?; + let wait_for_compositor_clock_addr = + GetProcAddress(hmodule, s!("DCompositionWaitForCompositorClock")) + .context("Function DCompositionWaitForCompositorClock not found")?; + let get_frame_id: DCompositionGetFrameId = std::mem::transmute(get_frame_id_addr); + let get_statistics: DCompositionGetStatistics = std::mem::transmute(get_statistics_addr); + let wait_for_compositor_clock: DCompositionWaitForCompositorClock = + std::mem::transmute(wait_for_compositor_clock_addr); + Ok((get_frame_id, get_statistics, wait_for_compositor_clock)) + } +} + +fn get_dwm_interval_from_direct_composition( + get_frame_id: DCompositionGetFrameId, + get_statistics: DCompositionGetStatistics, +) -> Result { + let mut frame_id = 0; + unsafe { get_frame_id(COMPOSITION_FRAME_ID_COMPLETED, &mut frame_id) }.ok()?; + let mut stats = COMPOSITION_FRAME_STATS::default(); + unsafe { + get_statistics( + frame_id, + &mut stats, + 0, + std::ptr::null_mut(), + std::ptr::null_mut(), + ) + } + .ok()?; + Ok(retrieve_duration(stats.framePeriod, *QPC_TICKS_PER_SECOND)) +} + +fn get_dwm_interval() -> Result { + let mut timing_info = DWM_TIMING_INFO { + cbSize: std::mem::size_of::() as u32, + ..Default::default() + }; + unsafe { DwmGetCompositionTimingInfo(HWND::default(), &mut timing_info) }?; + let interval = retrieve_duration(timing_info.qpcRefreshPeriod, *QPC_TICKS_PER_SECOND); + // Check for interval values that are impossibly low. A 29 microsecond + // interval was seen (from a qpcRefreshPeriod of 60). + if interval < VSYNC_INTERVAL_THRESHOLD { + Ok(retrieve_duration( + timing_info.rateRefresh.uiDenominator as u64, + timing_info.rateRefresh.uiNumerator as u64, + )) + } else { + Ok(interval) + } +} + +#[inline] +fn retrieve_duration(counts: u64, ticks_per_second: u64) -> Duration { + let ticks_per_microsecond = ticks_per_second / 1_000_000; + Duration::from_micros(counts / ticks_per_microsecond) +} diff --git a/crates/gpui/src/platform/windows/wrapper.rs b/crates/gpui/src/platform/windows/wrapper.rs index a1fe98a392..60bbc433ca 100644 --- a/crates/gpui/src/platform/windows/wrapper.rs +++ b/crates/gpui/src/platform/windows/wrapper.rs @@ -1,6 +1,6 @@ use std::ops::Deref; -use windows::Win32::UI::WindowsAndMessaging::HCURSOR; +use windows::Win32::{Foundation::HWND, UI::WindowsAndMessaging::HCURSOR}; #[derive(Debug, Clone, Copy)] pub(crate) struct SafeCursor { @@ -23,3 +23,31 @@ impl Deref for SafeCursor { &self.raw } } + +#[derive(Debug, Clone, Copy)] +pub(crate) struct SafeHwnd { + raw: HWND, +} + +impl SafeHwnd { + pub(crate) fn as_raw(&self) -> HWND { + self.raw + } +} + +unsafe impl Send for SafeHwnd {} +unsafe impl Sync for SafeHwnd {} + +impl From for SafeHwnd { + fn from(value: HWND) -> Self { + SafeHwnd { raw: value } + } +} + +impl Deref for SafeHwnd { + type Target = HWND; + + fn deref(&self) -> &Self::Target { + &self.raw + } +} From d030bb62817ef073ce96a743b7cbc9121d9980b1 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 12 Aug 2025 14:41:26 -0400 Subject: [PATCH 287/693] emmet: Bump to v0.0.5 (#36066) This PR bumps the Emmet extension to v0.0.5. Changes: - https://github.com/zed-industries/zed/pull/35599 - https://github.com/zed-industries/zed/pull/36064 Release Notes: - N/A --- Cargo.lock | 2 +- extensions/emmet/Cargo.toml | 2 +- extensions/emmet/extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ee4e94281..72c5da3a34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20686,7 +20686,7 @@ dependencies = [ [[package]] name = "zed_emmet" -version = "0.0.4" +version = "0.0.5" dependencies = [ "zed_extension_api 0.1.0", ] diff --git a/extensions/emmet/Cargo.toml b/extensions/emmet/Cargo.toml index 9d72a6c5c4..ff9debdea9 100644 --- a/extensions/emmet/Cargo.toml +++ b/extensions/emmet/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_emmet" -version = "0.0.4" +version = "0.0.5" edition.workspace = true publish.workspace = true license = "Apache-2.0" diff --git a/extensions/emmet/extension.toml b/extensions/emmet/extension.toml index 9fa14d091f..0ebb801f9d 100644 --- a/extensions/emmet/extension.toml +++ b/extensions/emmet/extension.toml @@ -1,7 +1,7 @@ id = "emmet" name = "Emmet" description = "Emmet support" -version = "0.0.4" +version = "0.0.5" schema_version = 1 authors = ["Piotr Osiewicz "] repository = "https://github.com/zed-industries/zed" From 7df8e05ad946b01a12bbcf0f71ac573c89f119b5 Mon Sep 17 00:00:00 2001 From: Filip Binkiewicz Date: Tue, 12 Aug 2025 19:47:15 +0100 Subject: [PATCH 288/693] Ignore whitespace in git blame invocation (#35960) This works around a bug wherein inline git blame is unavailable for files with CRLF line endings. At the same time, this prevents users from seeing whitespace-only changes in the editor's git blame Closes #35836 Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/git/src/blame.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/git/src/blame.rs b/crates/git/src/blame.rs index 2128fa55c3..6f12681ea0 100644 --- a/crates/git/src/blame.rs +++ b/crates/git/src/blame.rs @@ -73,6 +73,7 @@ async fn run_git_blame( .current_dir(working_directory) .arg("blame") .arg("--incremental") + .arg("-w") .arg("--contents") .arg("-") .arg(path.as_os_str()) From 7ff0f1525e42f948495970a0bb6227d4c3dfac43 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Tue, 12 Aug 2025 21:49:19 +0300 Subject: [PATCH 289/693] open_ai: Log inputs that caused parsing errors (#36063) Release Notes: - N/A Co-authored-by: Michael Sloan --- Cargo.lock | 1 + crates/open_ai/Cargo.toml | 1 + crates/open_ai/src/open_ai.rs | 10 +++++++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 72c5da3a34..d24a399c1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11242,6 +11242,7 @@ dependencies = [ "anyhow", "futures 0.3.31", "http_client", + "log", "schemars", "serde", "serde_json", diff --git a/crates/open_ai/Cargo.toml b/crates/open_ai/Cargo.toml index 2d40cd2735..bae00f0a8e 100644 --- a/crates/open_ai/Cargo.toml +++ b/crates/open_ai/Cargo.toml @@ -20,6 +20,7 @@ anyhow.workspace = true futures.workspace = true http_client.workspace = true schemars = { workspace = true, optional = true } +log.workspace = true serde.workspace = true serde_json.workspace = true strum.workspace = true diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 4697d71ed3..a6fd03a296 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -445,7 +445,15 @@ pub async fn stream_completion( Ok(ResponseStreamResult::Err { error }) => { Some(Err(anyhow!(error))) } - Err(error) => Some(Err(anyhow!(error))), + Err(error) => { + log::error!( + "Failed to parse OpenAI response into ResponseStreamResult: `{}`\n\ + Response: `{}`", + error, + line, + ); + Some(Err(anyhow!(error))) + } } } } From 7167f193c02520440420a8e88099620fc81b8470 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Tue, 12 Aug 2025 21:51:23 +0300 Subject: [PATCH 290/693] open_ai: Send `prompt_cache_key` to improve caching (#36065) Release Notes: - N/A Co-authored-by: Michael Sloan --- crates/language_models/src/provider/open_ai.rs | 1 + crates/open_ai/src/open_ai.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 2879b01ff3..9eac58c880 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -473,6 +473,7 @@ pub fn into_open_ai( } else { None }, + prompt_cache_key: request.thread_id, tools: request .tools .into_iter() diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index a6fd03a296..919b1d9ebf 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -244,6 +244,8 @@ pub struct Request { pub parallel_tool_calls: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub tools: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub prompt_cache_key: Option, } #[derive(Debug, Serialize, Deserialize)] From 628b1058bee19e6d5093b13826e7942654fbab35 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 12 Aug 2025 16:31:54 -0300 Subject: [PATCH 291/693] agent2: Fix some UI glitches (#36067) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 88 ++++++++++++++++---------- crates/markdown/src/markdown.rs | 5 +- 2 files changed, 58 insertions(+), 35 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 791542cf26..f47c7a0bc5 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1278,8 +1278,6 @@ impl AcpThreadView { .pr_1() .py_1() .rounded_t_md() - .border_b_1() - .border_color(self.tool_card_border_color(cx)) .bg(self.tool_card_header_bg(cx)) } else { this.opacity(0.8).hover(|style| style.opacity(1.)) @@ -1387,7 +1385,9 @@ impl AcpThreadView { Empty.into_any_element() } } - ToolCallContent::Diff(diff) => self.render_diff_editor(&diff.read(cx).multibuffer()), + ToolCallContent::Diff(diff) => { + self.render_diff_editor(&diff.read(cx).multibuffer(), cx) + } ToolCallContent::Terminal(terminal) => { self.render_terminal_tool_call(terminal, tool_call, window, cx) } @@ -1531,9 +1531,15 @@ impl AcpThreadView { }))) } - fn render_diff_editor(&self, multibuffer: &Entity) -> AnyElement { + fn render_diff_editor( + &self, + multibuffer: &Entity, + cx: &Context, + ) -> AnyElement { v_flex() .h_full() + .border_t_1() + .border_color(self.tool_card_border_color(cx)) .child( if let Some(editor) = self.diff_editors.get(&multibuffer.entity_id()) { editor.clone().into_any_element() @@ -1746,9 +1752,9 @@ impl AcpThreadView { .overflow_hidden() .child( v_flex() - .pt_1() - .pb_2() - .px_2() + .py_1p5() + .pl_2() + .pr_1p5() .gap_0p5() .bg(header_bg) .text_xs() @@ -2004,24 +2010,26 @@ impl AcpThreadView { parent.child(self.render_plan_entries(plan, window, cx)) }) }) - .when(!changed_buffers.is_empty(), |this| { + .when(!plan.is_empty() && !changed_buffers.is_empty(), |this| { this.child(Divider::horizontal().color(DividerColor::Border)) - .child(self.render_edits_summary( + }) + .when(!changed_buffers.is_empty(), |this| { + this.child(self.render_edits_summary( + action_log, + &changed_buffers, + self.edits_expanded, + pending_edits, + window, + cx, + )) + .when(self.edits_expanded, |parent| { + parent.child(self.render_edited_files( action_log, &changed_buffers, - self.edits_expanded, pending_edits, - window, cx, )) - .when(self.edits_expanded, |parent| { - parent.child(self.render_edited_files( - action_log, - &changed_buffers, - pending_edits, - cx, - )) - }) + }) }) .into_any() .into() @@ -2940,7 +2948,8 @@ impl AcpThreadView { fn render_thread_controls(&self, cx: &Context) -> impl IntoElement { let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown) - .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) .icon_color(Color::Ignored) .tooltip(Tooltip::text("Open Thread as Markdown")) .on_click(cx.listener(move |this, _, window, cx| { @@ -2951,7 +2960,8 @@ impl AcpThreadView { })); let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUp) - .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) .icon_color(Color::Ignored) .tooltip(Tooltip::text("Scroll To Top")) .on_click(cx.listener(move |this, _, _, cx| { @@ -2962,7 +2972,6 @@ impl AcpThreadView { .w_full() .mr_1() .pb_2() - .gap_1() .px(RESPONSE_PADDING_X) .opacity(0.4) .hover(|style| style.opacity(1.)) @@ -3079,6 +3088,8 @@ impl Focusable for AcpThreadView { impl Render for AcpThreadView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let has_messages = self.list_state.item_count() > 0; + v_flex() .size_full() .key_context("AcpThread") @@ -3125,7 +3136,7 @@ impl Render for AcpThreadView { let thread_clone = thread.clone(); v_flex().flex_1().map(|this| { - if self.list_state.item_count() > 0 { + if has_messages { this.child( list( self.list_state.clone(), @@ -3144,23 +3155,32 @@ impl Render for AcpThreadView { .into_any(), ) .child(self.render_vertical_scrollbar(cx)) - .children(match thread_clone.read(cx).status() { - ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => { - None - } - ThreadStatus::Generating => div() - .px_5() - .py_2() - .child(LoadingLabel::new("").size(LabelSize::Small)) - .into(), - }) - .children(self.render_activity_bar(&thread_clone, window, cx)) + .children( + match thread_clone.read(cx).status() { + ThreadStatus::Idle + | ThreadStatus::WaitingForToolConfirmation => None, + ThreadStatus::Generating => div() + .px_5() + .py_2() + .child(LoadingLabel::new("").size(LabelSize::Small)) + .into(), + }, + ) } else { this.child(self.render_empty_state(cx)) } }) } }) + // The activity bar is intentionally rendered outside of the ThreadState::Ready match + // above so that the scrollbar doesn't render behind it. The current setup allows + // the scrollbar to stop exactly at the activity bar start. + .when(has_messages, |this| match &self.thread_state { + ThreadState::Ready { thread, .. } => { + this.children(self.render_activity_bar(thread, window, cx)) + } + _ => this, + }) .when_some(self.last_error.clone(), |el, error| { el.child( div() diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index dba4bc64b1..a3235a9773 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -1084,7 +1084,9 @@ impl Element for MarkdownElement { self.markdown.clone(), cx, ); - el.child(div().absolute().top_1().right_1().w_5().child(codeblock)) + el.child( + div().absolute().top_1().right_0p5().w_5().child(codeblock), + ) }); } @@ -1312,6 +1314,7 @@ fn render_copy_code_block_button( }, ) .icon_color(Color::Muted) + .icon_size(IconSize::Small) .shape(ui::IconButtonShape::Square) .tooltip(Tooltip::text("Copy Code")) .on_click({ From 255bb0a3f87563cb2162a620bb069e31c7fa3b0b Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Tue, 12 Aug 2025 15:56:27 -0400 Subject: [PATCH 292/693] telemetry: Reduce the amount of telemetry events fired (#36060) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Extension loaded events are now condensed into a single event with a Vec of (extension_id, extension_version) called id_and_versions. 2. Editor Saved & AutoSaved are merged into a singular event with a type field that is either "manual" or "autosave”. 3. Editor Edited event will only fire once every 10 minutes now. 4. Editor Closed event is fired when an editor item (tab) is removed from a pane cc: @katie-z-geer Release Notes: - N/A --------- Co-authored-by: Marshall Bowers --- crates/client/src/telemetry.rs | 35 +++++++++---- crates/editor/src/editor.rs | 57 ++++++++++++++++----- crates/editor/src/items.rs | 34 ++++++++---- crates/extension_host/src/extension_host.rs | 20 ++++---- crates/workspace/src/item.rs | 6 +++ crates/workspace/src/pane.rs | 1 + 6 files changed, 110 insertions(+), 43 deletions(-) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 43a1a0b7a4..54b3d3f801 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -340,22 +340,35 @@ impl Telemetry { } pub fn log_edit_event(self: &Arc, environment: &'static str, is_via_ssh: bool) { + static LAST_EVENT_TIME: Mutex> = Mutex::new(None); + let mut state = self.state.lock(); let period_data = state.event_coalescer.log_event(environment); drop(state); - if let Some((start, end, environment)) = period_data { - let duration = end - .saturating_duration_since(start) - .min(Duration::from_secs(60 * 60 * 24)) - .as_millis() as i64; + if let Some(mut last_event) = LAST_EVENT_TIME.try_lock() { + let current_time = std::time::Instant::now(); + let last_time = last_event.get_or_insert(current_time); - telemetry::event!( - "Editor Edited", - duration = duration, - environment = environment, - is_via_ssh = is_via_ssh - ); + if current_time.duration_since(*last_time) > Duration::from_secs(60 * 10) { + *last_time = current_time; + } else { + return; + } + + if let Some((start, end, environment)) = period_data { + let duration = end + .saturating_duration_since(start) + .min(Duration::from_secs(60 * 60 * 24)) + .as_millis() as i64; + + telemetry::event!( + "Editor Edited", + duration = duration, + environment = environment, + is_via_ssh = is_via_ssh + ); + } } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d1bf95c794..8a9398e71f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -250,6 +250,24 @@ pub type RenderDiffHunkControlsFn = Arc< ) -> AnyElement, >; +enum ReportEditorEvent { + Saved { auto_saved: bool }, + EditorOpened, + ZetaTosClicked, + Closed, +} + +impl ReportEditorEvent { + pub fn event_type(&self) -> &'static str { + match self { + Self::Saved { .. } => "Editor Saved", + Self::EditorOpened => "Editor Opened", + Self::ZetaTosClicked => "Edit Prediction Provider ToS Clicked", + Self::Closed => "Editor Closed", + } + } +} + struct InlineValueCache { enabled: bool, inlays: Vec, @@ -2325,7 +2343,7 @@ impl Editor { } if editor.mode.is_full() { - editor.report_editor_event("Editor Opened", None, cx); + editor.report_editor_event(ReportEditorEvent::EditorOpened, None, cx); } editor @@ -9124,7 +9142,7 @@ impl Editor { .on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default()) .on_click(cx.listener(|this, _event, window, cx| { cx.stop_propagation(); - this.report_editor_event("Edit Prediction Provider ToS Clicked", None, cx); + this.report_editor_event(ReportEditorEvent::ZetaTosClicked, None, cx); window.dispatch_action( zed_actions::OpenZedPredictOnboarding.boxed_clone(), cx, @@ -20547,7 +20565,7 @@ impl Editor { fn report_editor_event( &self, - event_type: &'static str, + reported_event: ReportEditorEvent, file_extension: Option, cx: &App, ) { @@ -20581,15 +20599,30 @@ impl Editor { .show_edit_predictions; let project = project.read(cx); - telemetry::event!( - event_type, - file_extension, - vim_mode, - copilot_enabled, - copilot_enabled_for_language, - edit_predictions_provider, - is_via_ssh = project.is_via_ssh(), - ); + let event_type = reported_event.event_type(); + + if let ReportEditorEvent::Saved { auto_saved } = reported_event { + telemetry::event!( + event_type, + type = if auto_saved {"autosave"} else {"manual"}, + file_extension, + vim_mode, + copilot_enabled, + copilot_enabled_for_language, + edit_predictions_provider, + is_via_ssh = project.is_via_ssh(), + ); + } else { + telemetry::event!( + event_type, + file_extension, + vim_mode, + copilot_enabled, + copilot_enabled_for_language, + edit_predictions_provider, + is_via_ssh = project.is_via_ssh(), + ); + }; } /// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 231aaa1d00..1da82c605d 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,7 +1,7 @@ use crate::{ Anchor, Autoscroll, Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, FormatTarget, - MultiBuffer, MultiBufferSnapshot, NavigationData, SearchWithinRange, SelectionEffects, - ToPoint as _, + MultiBuffer, MultiBufferSnapshot, NavigationData, ReportEditorEvent, SearchWithinRange, + SelectionEffects, ToPoint as _, display_map::HighlightKey, editor_settings::SeedQuerySetting, persistence::{DB, SerializedEditor}, @@ -776,6 +776,10 @@ impl Item for Editor { } } + fn on_removed(&self, cx: &App) { + self.report_editor_event(ReportEditorEvent::Closed, None, cx); + } + fn deactivated(&mut self, _: &mut Window, cx: &mut Context) { let selection = self.selections.newest_anchor(); self.push_to_nav_history(selection.head(), None, true, false, cx); @@ -815,9 +819,9 @@ impl Item for Editor { ) -> Task> { // Add meta data tracking # of auto saves if options.autosave { - self.report_editor_event("Editor Autosaved", None, cx); + self.report_editor_event(ReportEditorEvent::Saved { auto_saved: true }, None, cx); } else { - self.report_editor_event("Editor Saved", None, cx); + self.report_editor_event(ReportEditorEvent::Saved { auto_saved: false }, None, cx); } let buffers = self.buffer().clone().read(cx).all_buffers(); @@ -896,7 +900,11 @@ impl Item for Editor { .path .extension() .map(|a| a.to_string_lossy().to_string()); - self.report_editor_event("Editor Saved", file_extension, cx); + self.report_editor_event( + ReportEditorEvent::Saved { auto_saved: false }, + file_extension, + cx, + ); project.update(cx, |project, cx| project.save_buffer_as(buffer, path, cx)) } @@ -997,12 +1005,16 @@ impl Item for Editor { ) { self.workspace = Some((workspace.weak_handle(), workspace.database_id())); if let Some(workspace) = &workspace.weak_handle().upgrade() { - cx.subscribe(&workspace, |editor, _, event: &workspace::Event, _cx| { - if matches!(event, workspace::Event::ModalOpened) { - editor.mouse_context_menu.take(); - editor.inline_blame_popover.take(); - } - }) + cx.subscribe( + &workspace, + |editor, _, event: &workspace::Event, _cx| match event { + workspace::Event::ModalOpened => { + editor.mouse_context_menu.take(); + editor.inline_blame_popover.take(); + } + _ => {} + }, + ) .detach(); } } diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index dc38c244f1..67baf4e692 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1118,15 +1118,17 @@ impl ExtensionStore { extensions_to_unload.len() - reload_count ); - for extension_id in &extensions_to_load { - if let Some(extension) = new_index.extensions.get(extension_id) { - telemetry::event!( - "Extension Loaded", - extension_id, - version = extension.manifest.version - ); - } - } + let extension_ids = extensions_to_load + .iter() + .filter_map(|id| { + Some(( + id.clone(), + new_index.extensions.get(id)?.manifest.version.clone(), + )) + }) + .collect::>(); + + telemetry::event!("Extensions Loaded", id_and_versions = extension_ids); let themes_to_remove = old_index .themes diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index c8ebe4550b..bba50e4431 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -293,6 +293,7 @@ pub trait Item: Focusable + EventEmitter + Render + Sized { fn deactivated(&mut self, _window: &mut Window, _: &mut Context) {} fn discarded(&self, _project: Entity, _window: &mut Window, _cx: &mut Context) {} + fn on_removed(&self, _cx: &App) {} fn workspace_deactivated(&mut self, _window: &mut Window, _: &mut Context) {} fn navigate(&mut self, _: Box, _window: &mut Window, _: &mut Context) -> bool { false @@ -532,6 +533,7 @@ pub trait ItemHandle: 'static + Send { ); fn deactivated(&self, window: &mut Window, cx: &mut App); fn discarded(&self, project: Entity, window: &mut Window, cx: &mut App); + fn on_removed(&self, cx: &App); fn workspace_deactivated(&self, window: &mut Window, cx: &mut App); fn navigate(&self, data: Box, window: &mut Window, cx: &mut App) -> bool; fn item_id(&self) -> EntityId; @@ -968,6 +970,10 @@ impl ItemHandle for Entity { self.update(cx, |this, cx| this.deactivated(window, cx)); } + fn on_removed(&self, cx: &App) { + self.read(cx).on_removed(cx); + } + fn workspace_deactivated(&self, window: &mut Window, cx: &mut App) { self.update(cx, |this, cx| this.workspace_deactivated(window, cx)); } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 0c35752165..cffeea0a8d 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1829,6 +1829,7 @@ impl Pane { let mode = self.nav_history.mode(); self.nav_history.set_mode(NavigationMode::ClosingItem); item.deactivated(window, cx); + item.on_removed(cx); self.nav_history.set_mode(mode); if self.is_active_preview_item(item.item_id()) { From 48ae02c1cace50491f7e3d471a87634ddf31563d Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Tue, 12 Aug 2025 17:06:01 -0400 Subject: [PATCH 293/693] Don't retry for PaymentRequiredError or ModelRequestLimitReachedError (#36075) Release Notes: - Don't auto-retry for "payment required" or "model request limit reached" errors (since retrying won't help) --- crates/agent/src/thread.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 20d482f60d..1d417efbba 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -2268,6 +2268,15 @@ impl Thread { max_attempts: 3, }) } + Other(err) + if err.is::() + || err.is::() => + { + // Retrying won't help for Payment Required or Model Request Limit errors (where + // the user must upgrade to usage-based billing to get more requests, or else wait + // for a significant amount of time for the request limit to reset). + None + } // Conservatively assume that any other errors are non-retryable HttpResponseError { .. } | Other(..) => Some(RetryStrategy::Fixed { delay: BASE_RETRY_DELAY, From b564b1d5d0c07aff10ab8f351d70604220a4497f Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Tue, 12 Aug 2025 15:08:19 -0600 Subject: [PATCH 294/693] outline: Fix nesting in multi-name declarations in Go and C++ (#36076) An alternative might be to adjust the logic to not nest items when their ranges are the same, but then clicking them doesn't work properly / moving the cursor does not change which is selected. This could probably be made to work with some extra logic there, but it seems overkill. The downside of fixing it at the query level is that other parts of the declaration are not inside the item range. This seems to be fine for single line declarations - the nearest outline item is highlighted. However, if a part of the declaration is not included in an item range and is on its own line, then no outline item is highlighted. Release Notes: - Outline Panel: Fixed nesting of var and field declarations with multiple identifiers in Go and C++ C++ before: image C++ after: image Go before: image Go after: image --- crates/languages/src/cpp/outline.scm | 6 ++++-- crates/languages/src/go/outline.scm | 13 ++++++++----- crates/languages/src/javascript/outline.scm | 4 ++++ crates/languages/src/tsx/outline.scm | 4 ++++ crates/languages/src/typescript/outline.scm | 4 ++++ 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/crates/languages/src/cpp/outline.scm b/crates/languages/src/cpp/outline.scm index 448fe35220..c897366558 100644 --- a/crates/languages/src/cpp/outline.scm +++ b/crates/languages/src/cpp/outline.scm @@ -149,7 +149,9 @@ parameters: (parameter_list "(" @context ")" @context))) - ] - (type_qualifier)? @context) @item + ; Fields declarations may define multiple fields, and so @item is on the + ; declarator so they each get distinct ranges. + ] @item + (type_qualifier)? @context) (comment) @annotation diff --git a/crates/languages/src/go/outline.scm b/crates/languages/src/go/outline.scm index e37ae7e572..c745f55aff 100644 --- a/crates/languages/src/go/outline.scm +++ b/crates/languages/src/go/outline.scm @@ -1,4 +1,5 @@ (comment) @annotation + (type_declaration "type" @context [ @@ -42,13 +43,13 @@ (var_declaration "var" @context [ + ; The declaration may define multiple variables, and so @item is on + ; the identifier so they get distinct ranges. (var_spec - name: (identifier) @name) @item + name: (identifier) @name @item) (var_spec_list - "(" (var_spec - name: (identifier) @name) @item - ")" + name: (identifier) @name @item) ) ] ) @@ -60,5 +61,7 @@ "(" @context ")" @context)) @item +; Fields declarations may define multiple fields, and so @item is on the +; declarator so they each get distinct ranges. (field_declaration - name: (_) @name) @item + name: (_) @name @item) diff --git a/crates/languages/src/javascript/outline.scm b/crates/languages/src/javascript/outline.scm index 026c71e1f9..ca16c27a27 100644 --- a/crates/languages/src/javascript/outline.scm +++ b/crates/languages/src/javascript/outline.scm @@ -31,12 +31,16 @@ (export_statement (lexical_declaration ["let" "const"] @context + ; Multiple names may be exported - @item is on the declarator to keep + ; ranges distinct. (variable_declarator name: (_) @name) @item))) (program (lexical_declaration ["let" "const"] @context + ; Multiple names may be defined - @item is on the declarator to keep + ; ranges distinct. (variable_declarator name: (_) @name) @item)) diff --git a/crates/languages/src/tsx/outline.scm b/crates/languages/src/tsx/outline.scm index 5dafe791e4..f4261b9697 100644 --- a/crates/languages/src/tsx/outline.scm +++ b/crates/languages/src/tsx/outline.scm @@ -34,12 +34,16 @@ (export_statement (lexical_declaration ["let" "const"] @context + ; Multiple names may be exported - @item is on the declarator to keep + ; ranges distinct. (variable_declarator name: (_) @name) @item)) (program (lexical_declaration ["let" "const"] @context + ; Multiple names may be defined - @item is on the declarator to keep + ; ranges distinct. (variable_declarator name: (_) @name) @item)) diff --git a/crates/languages/src/typescript/outline.scm b/crates/languages/src/typescript/outline.scm index 5dafe791e4..f4261b9697 100644 --- a/crates/languages/src/typescript/outline.scm +++ b/crates/languages/src/typescript/outline.scm @@ -34,12 +34,16 @@ (export_statement (lexical_declaration ["let" "const"] @context + ; Multiple names may be exported - @item is on the declarator to keep + ; ranges distinct. (variable_declarator name: (_) @name) @item)) (program (lexical_declaration ["let" "const"] @context + ; Multiple names may be defined - @item is on the declarator to keep + ; ranges distinct. (variable_declarator name: (_) @name) @item)) From cd234e28ce528b8f9c811aa4c5c5d358b9a9eb5d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 12 Aug 2025 14:36:48 -0700 Subject: [PATCH 295/693] Eliminate host targets from rust toolchain file (#36077) Only cross-compilation targets need to be listed in the rust toolchain. So we only need to list the wasi target for extensions, and the musl target for the linux remote server. Previously, we were causing mac, linux, and windows target to get installed onto all developer workstations, which is unnecessary. Release Notes: - N/A --- rust-toolchain.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 3d87025a27..2c909e0e1e 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -3,11 +3,6 @@ channel = "1.89" profile = "minimal" components = [ "rustfmt", "clippy" ] targets = [ - "x86_64-apple-darwin", - "aarch64-apple-darwin", - "x86_64-unknown-freebsd", - "x86_64-unknown-linux-gnu", - "x86_64-pc-windows-msvc", "wasm32-wasip2", # extensions "x86_64-unknown-linux-musl", # remote server ] From 13a2c53381467cf572d282183e53b04ff1d5c674 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Tue, 12 Aug 2025 18:02:10 -0400 Subject: [PATCH 296/693] onboarding: Fix onboarding font context menu not scrolling to selected entry open (#36080) The fix was changing the picker kind we used from `list` variant to a `uniform` list `Picker::list()` still has a bug where it's unable to scroll to it's selected entry when the list is first openned. This is likely caused by list not knowing the pixel offset of each element it would have to scroll pass to get to the selected element Release Notes: - N/A Co-authored-by: Danilo Leal --- crates/onboarding/src/editing_page.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 13b4f6a5c1..e8fbc36c30 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -573,7 +573,7 @@ fn font_picker( ) -> FontPicker { let delegate = FontPickerDelegate::new(current_font, on_font_changed, cx); - Picker::list(delegate, window, cx) + Picker::uniform_list(delegate, window, cx) .show_scrollbar(true) .width(rems_from_px(210.)) .max_height(Some(rems(20.).into())) From 658d56bd726ff44d8105da75302b6a2c24e726cd Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 13 Aug 2025 00:37:11 +0200 Subject: [PATCH 297/693] cli: Do not rely on Spotlight for --channel support (#36082) I've recently disabled Spotlight on my Mac and found that this code path (which I rely on a lot) ceased working for me. Closes #ISSUE Release Notes: - N/A --- crates/cli/src/main.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 8d6cd2544a..67591167df 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -957,17 +957,14 @@ mod mac_os { ) -> Result<()> { use anyhow::bail; - let app_id_prompt = format!("id of app \"{}\"", channel.display_name()); - let app_id_output = Command::new("osascript") + let app_path_prompt = format!( + "POSIX path of (path to application \"{}\")", + channel.display_name() + ); + let app_path_output = Command::new("osascript") .arg("-e") - .arg(&app_id_prompt) + .arg(&app_path_prompt) .output()?; - if !app_id_output.status.success() { - bail!("Could not determine app id for {}", channel.display_name()); - } - let app_name = String::from_utf8(app_id_output.stdout)?.trim().to_owned(); - let app_path_prompt = format!("kMDItemCFBundleIdentifier == '{app_name}'"); - let app_path_output = Command::new("mdfind").arg(app_path_prompt).output()?; if !app_path_output.status.success() { bail!( "Could not determine app path for {}", From 32975c420807d4ac84b89a914be3e32819ff37f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Wed, 13 Aug 2025 08:04:30 +0800 Subject: [PATCH 298/693] windows: Fix auto update failure when launching from the cli (#34303) Release Notes: - N/A --------- Co-authored-by: Max Brunsfeld --- .../src/activity_indicator.rs | 12 +- crates/auto_update/src/auto_update.rs | 112 +++++++++++------- .../src/auto_update_helper.rs | 86 +++++++++++++- crates/auto_update_helper/src/dialog.rs | 4 +- crates/auto_update_helper/src/updater.rs | 14 ++- crates/editor/src/editor_tests.rs | 2 +- crates/gpui/src/app.rs | 31 ++++- crates/gpui/src/app/context.rs | 35 ++++-- crates/gpui/src/platform/windows/platform.rs | 4 +- crates/title_bar/src/title_bar.rs | 2 +- crates/workspace/src/workspace.rs | 22 ++-- crates/zed/src/main.rs | 51 +++----- crates/zed/src/zed/windows_only_instance.rs | 6 +- 13 files changed, 250 insertions(+), 131 deletions(-) diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index f8ea7173d8..7c562aaba4 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -716,18 +716,10 @@ impl ActivityIndicator { })), tooltip_message: Some(Self::version_tooltip_message(&version)), }), - AutoUpdateStatus::Updated { - binary_path, - version, - } => Some(Content { + AutoUpdateStatus::Updated { version } => Some(Content { icon: None, message: "Click to restart and update Zed".to_string(), - on_click: Some(Arc::new({ - let reload = workspace::Reload { - binary_path: Some(binary_path.clone()), - }; - move |_, _, cx| workspace::reload(&reload, cx) - })), + on_click: Some(Arc::new(move |_, _, cx| workspace::reload(cx))), tooltip_message: Some(Self::version_tooltip_message(&version)), }), AutoUpdateStatus::Errored => Some(Content { diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 074aaa6fea..4d0d2d5984 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -59,16 +59,9 @@ pub enum VersionCheckType { pub enum AutoUpdateStatus { Idle, Checking, - Downloading { - version: VersionCheckType, - }, - Installing { - version: VersionCheckType, - }, - Updated { - binary_path: PathBuf, - version: VersionCheckType, - }, + Downloading { version: VersionCheckType }, + Installing { version: VersionCheckType }, + Updated { version: VersionCheckType }, Errored, } @@ -83,6 +76,7 @@ pub struct AutoUpdater { current_version: SemanticVersion, http_client: Arc, pending_poll: Option>>, + quit_subscription: Option, } #[derive(Deserialize, Clone, Debug)] @@ -164,7 +158,7 @@ pub fn init(http_client: Arc, cx: &mut App) { AutoUpdateSetting::register(cx); cx.observe_new(|workspace: &mut Workspace, _window, _cx| { - workspace.register_action(|_, action: &Check, window, cx| check(action, window, cx)); + workspace.register_action(|_, action, window, cx| check(action, window, cx)); workspace.register_action(|_, action, _, cx| { view_release_notes(action, cx); @@ -174,7 +168,7 @@ pub fn init(http_client: Arc, cx: &mut App) { let version = release_channel::AppVersion::global(cx); let auto_updater = cx.new(|cx| { - let updater = AutoUpdater::new(version, http_client); + let updater = AutoUpdater::new(version, http_client, cx); let poll_for_updates = ReleaseChannel::try_global(cx) .map(|channel| channel.poll_for_updates()) @@ -321,12 +315,34 @@ impl AutoUpdater { cx.default_global::().0.clone() } - fn new(current_version: SemanticVersion, http_client: Arc) -> Self { + fn new( + current_version: SemanticVersion, + http_client: Arc, + cx: &mut Context, + ) -> Self { + // On windows, executable files cannot be overwritten while they are + // running, so we must wait to overwrite the application until quitting + // or restarting. When quitting the app, we spawn the auto update helper + // to finish the auto update process after Zed exits. When restarting + // the app after an update, we use `set_restart_path` to run the auto + // update helper instead of the app, so that it can overwrite the app + // and then spawn the new binary. + let quit_subscription = Some(cx.on_app_quit(|_, _| async move { + #[cfg(target_os = "windows")] + finalize_auto_update_on_quit(); + })); + + cx.on_app_restart(|this, _| { + this.quit_subscription.take(); + }) + .detach(); + Self { status: AutoUpdateStatus::Idle, current_version, http_client, pending_poll: None, + quit_subscription, } } @@ -536,6 +552,8 @@ impl AutoUpdater { ) })?; + Self::check_dependencies()?; + this.update(&mut cx, |this, cx| { this.status = AutoUpdateStatus::Checking; cx.notify(); @@ -582,13 +600,15 @@ impl AutoUpdater { cx.notify(); })?; - let binary_path = Self::binary_path(installer_dir, target_path, &cx).await?; + let new_binary_path = Self::install_release(installer_dir, target_path, &cx).await?; + if let Some(new_binary_path) = new_binary_path { + cx.update(|cx| cx.set_restart_path(new_binary_path))?; + } this.update(&mut cx, |this, cx| { this.set_should_show_update_notification(true, cx) .detach_and_log_err(cx); this.status = AutoUpdateStatus::Updated { - binary_path, version: newer_version, }; cx.notify(); @@ -639,6 +659,15 @@ impl AutoUpdater { } } + fn check_dependencies() -> Result<()> { + #[cfg(not(target_os = "windows"))] + anyhow::ensure!( + which::which("rsync").is_ok(), + "Aborting. Could not find rsync which is required for auto-updates." + ); + Ok(()) + } + async fn target_path(installer_dir: &InstallerDir) -> Result { let filename = match OS { "macos" => anyhow::Ok("Zed.dmg"), @@ -647,20 +676,14 @@ impl AutoUpdater { unsupported_os => anyhow::bail!("not supported: {unsupported_os}"), }?; - #[cfg(not(target_os = "windows"))] - anyhow::ensure!( - which::which("rsync").is_ok(), - "Aborting. Could not find rsync which is required for auto-updates." - ); - Ok(installer_dir.path().join(filename)) } - async fn binary_path( + async fn install_release( installer_dir: InstallerDir, target_path: PathBuf, cx: &AsyncApp, - ) -> Result { + ) -> Result> { match OS { "macos" => install_release_macos(&installer_dir, target_path, cx).await, "linux" => install_release_linux(&installer_dir, target_path, cx).await, @@ -801,7 +824,7 @@ async fn install_release_linux( temp_dir: &InstallerDir, downloaded_tar_gz: PathBuf, cx: &AsyncApp, -) -> Result { +) -> Result> { let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?; let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?); let running_app_path = cx.update(|cx| cx.app_path())??; @@ -861,14 +884,14 @@ async fn install_release_linux( String::from_utf8_lossy(&output.stderr) ); - Ok(to.join(expected_suffix)) + Ok(Some(to.join(expected_suffix))) } async fn install_release_macos( temp_dir: &InstallerDir, downloaded_dmg: PathBuf, cx: &AsyncApp, -) -> Result { +) -> Result> { let running_app_path = cx.update(|cx| cx.app_path())??; let running_app_filename = running_app_path .file_name() @@ -910,10 +933,10 @@ async fn install_release_macos( String::from_utf8_lossy(&output.stderr) ); - Ok(running_app_path) + Ok(None) } -async fn install_release_windows(downloaded_installer: PathBuf) -> Result { +async fn install_release_windows(downloaded_installer: PathBuf) -> Result> { let output = Command::new(downloaded_installer) .arg("/verysilent") .arg("/update=true") @@ -926,29 +949,36 @@ async fn install_release_windows(downloaded_installer: PathBuf) -> Result bool { +pub fn finalize_auto_update_on_quit() { let Some(installer_path) = std::env::current_exe() .ok() .and_then(|p| p.parent().map(|p| p.join("updates"))) else { - return false; + return; }; // The installer will create a flag file after it finishes updating let flag_file = installer_path.join("versions.txt"); - if flag_file.exists() { - if let Some(helper) = installer_path + if flag_file.exists() + && let Some(helper) = installer_path .parent() .map(|p| p.join("tools\\auto_update_helper.exe")) - { - let _ = std::process::Command::new(helper).spawn(); - return true; - } + { + let mut command = std::process::Command::new(helper); + command.arg("--launch"); + command.arg("false"); + let _ = command.spawn(); } - false } #[cfg(test)] @@ -1002,7 +1032,6 @@ mod tests { let app_commit_sha = Ok(Some("a".to_string())); let installed_version = SemanticVersion::new(1, 0, 0); let status = AutoUpdateStatus::Updated { - binary_path: PathBuf::new(), version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)), }; let fetched_version = SemanticVersion::new(1, 0, 1); @@ -1024,7 +1053,6 @@ mod tests { let app_commit_sha = Ok(Some("a".to_string())); let installed_version = SemanticVersion::new(1, 0, 0); let status = AutoUpdateStatus::Updated { - binary_path: PathBuf::new(), version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)), }; let fetched_version = SemanticVersion::new(1, 0, 2); @@ -1090,7 +1118,6 @@ mod tests { let app_commit_sha = Ok(Some("a".to_string())); let installed_version = SemanticVersion::new(1, 0, 0); let status = AutoUpdateStatus::Updated { - binary_path: PathBuf::new(), version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())), }; let fetched_sha = "b".to_string(); @@ -1112,7 +1139,6 @@ mod tests { let app_commit_sha = Ok(Some("a".to_string())); let installed_version = SemanticVersion::new(1, 0, 0); let status = AutoUpdateStatus::Updated { - binary_path: PathBuf::new(), version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())), }; let fetched_sha = "c".to_string(); @@ -1160,7 +1186,6 @@ mod tests { let app_commit_sha = Ok(None); let installed_version = SemanticVersion::new(1, 0, 0); let status = AutoUpdateStatus::Updated { - binary_path: PathBuf::new(), version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())), }; let fetched_sha = "b".to_string(); @@ -1183,7 +1208,6 @@ mod tests { let app_commit_sha = Ok(None); let installed_version = SemanticVersion::new(1, 0, 0); let status = AutoUpdateStatus::Updated { - binary_path: PathBuf::new(), version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())), }; let fetched_sha = "c".to_string(); diff --git a/crates/auto_update_helper/src/auto_update_helper.rs b/crates/auto_update_helper/src/auto_update_helper.rs index 7c810d8724..2781176028 100644 --- a/crates/auto_update_helper/src/auto_update_helper.rs +++ b/crates/auto_update_helper/src/auto_update_helper.rs @@ -37,6 +37,11 @@ mod windows_impl { pub(crate) const WM_JOB_UPDATED: u32 = WM_USER + 1; pub(crate) const WM_TERMINATE: u32 = WM_USER + 2; + #[derive(Debug)] + struct Args { + launch: Option, + } + pub(crate) fn run() -> Result<()> { let helper_dir = std::env::current_exe()? .parent() @@ -51,8 +56,9 @@ mod windows_impl { log::info!("======= Starting Zed update ======="); let (tx, rx) = std::sync::mpsc::channel(); let hwnd = create_dialog_window(rx)?.0 as isize; + let args = parse_args(); std::thread::spawn(move || { - let result = perform_update(app_dir.as_path(), Some(hwnd)); + let result = perform_update(app_dir.as_path(), Some(hwnd), args.launch.unwrap_or(true)); tx.send(result).ok(); unsafe { PostMessageW(Some(HWND(hwnd as _)), WM_TERMINATE, WPARAM(0), LPARAM(0)) }.ok(); }); @@ -77,6 +83,41 @@ mod windows_impl { Ok(()) } + fn parse_args() -> Args { + let mut result = Args { launch: None }; + if let Some(candidate) = std::env::args().nth(1) { + parse_single_arg(&candidate, &mut result); + } + + result + } + + fn parse_single_arg(arg: &str, result: &mut Args) { + let Some((key, value)) = arg.strip_prefix("--").and_then(|arg| arg.split_once('=')) else { + log::error!( + "Invalid argument format: '{}'. Expected format: --key=value", + arg + ); + return; + }; + + match key { + "launch" => parse_launch_arg(value, &mut result.launch), + _ => log::error!("Unknown argument: --{}", key), + } + } + + fn parse_launch_arg(value: &str, arg: &mut Option) { + match value { + "true" => *arg = Some(true), + "false" => *arg = Some(false), + _ => log::error!( + "Invalid value for --launch: '{}'. Expected 'true' or 'false'", + value + ), + } + } + pub(crate) fn show_error(mut content: String) { if content.len() > 600 { content.truncate(600); @@ -91,4 +132,47 @@ mod windows_impl { ) }; } + + #[cfg(test)] + mod tests { + use crate::windows_impl::{Args, parse_launch_arg, parse_single_arg}; + + #[test] + fn test_parse_launch_arg() { + let mut arg = None; + parse_launch_arg("true", &mut arg); + assert_eq!(arg, Some(true)); + + let mut arg = None; + parse_launch_arg("false", &mut arg); + assert_eq!(arg, Some(false)); + + let mut arg = None; + parse_launch_arg("invalid", &mut arg); + assert_eq!(arg, None); + } + + #[test] + fn test_parse_single_arg() { + let mut args = Args { launch: None }; + parse_single_arg("--launch=true", &mut args); + assert_eq!(args.launch, Some(true)); + + let mut args = Args { launch: None }; + parse_single_arg("--launch=false", &mut args); + assert_eq!(args.launch, Some(false)); + + let mut args = Args { launch: None }; + parse_single_arg("--launch=invalid", &mut args); + assert_eq!(args.launch, None); + + let mut args = Args { launch: None }; + parse_single_arg("--launch", &mut args); + assert_eq!(args.launch, None); + + let mut args = Args { launch: None }; + parse_single_arg("--unknown", &mut args); + assert_eq!(args.launch, None); + } + } } diff --git a/crates/auto_update_helper/src/dialog.rs b/crates/auto_update_helper/src/dialog.rs index 010ebb4875..757819df51 100644 --- a/crates/auto_update_helper/src/dialog.rs +++ b/crates/auto_update_helper/src/dialog.rs @@ -72,7 +72,7 @@ pub(crate) fn create_dialog_window(receiver: Receiver>) -> Result) -> Result<()> { +pub(crate) fn perform_update(app_dir: &Path, hwnd: Option, launch: bool) -> Result<()> { let hwnd = hwnd.map(|ptr| HWND(ptr as _)); for job in JOBS.iter() { @@ -145,9 +145,11 @@ pub(crate) fn perform_update(app_dir: &Path, hwnd: Option) -> Result<()> } } } - let _ = std::process::Command::new(app_dir.join("Zed.exe")) - .creation_flags(CREATE_NEW_PROCESS_GROUP.0) - .spawn(); + if launch { + let _ = std::process::Command::new(app_dir.join("Zed.exe")) + .creation_flags(CREATE_NEW_PROCESS_GROUP.0) + .spawn(); + } log::info!("Update completed successfully"); Ok(()) } @@ -159,11 +161,11 @@ mod test { #[test] fn test_perform_update() { let app_dir = std::path::Path::new("C:/"); - assert!(perform_update(app_dir, None).is_ok()); + assert!(perform_update(app_dir, None, false).is_ok()); // Simulate a timeout unsafe { std::env::set_var("ZED_AUTO_UPDATE", "err") }; - let ret = perform_update(app_dir, None); + let ret = perform_update(app_dir, None, false); assert!(ret.is_err_and(|e| e.to_string().as_str() == "Timed out")); } } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index b31963c9c8..0d2ecec8f2 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -22456,7 +22456,7 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) { ); cx.update(|_, cx| { - workspace::reload(&workspace::Reload::default(), cx); + workspace::reload(cx); }); assert_language_servers_count( 1, diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index ded7bae316..5f6d252503 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -277,6 +277,8 @@ pub struct App { pub(crate) release_listeners: SubscriberSet, pub(crate) global_observers: SubscriberSet, pub(crate) quit_observers: SubscriberSet<(), QuitHandler>, + pub(crate) restart_observers: SubscriberSet<(), Handler>, + pub(crate) restart_path: Option, pub(crate) window_closed_observers: SubscriberSet<(), WindowClosedHandler>, pub(crate) layout_id_buffer: Vec, // We recycle this memory across layout requests. pub(crate) propagate_event: bool, @@ -349,6 +351,8 @@ impl App { keyboard_layout_observers: SubscriberSet::new(), global_observers: SubscriberSet::new(), quit_observers: SubscriberSet::new(), + restart_observers: SubscriberSet::new(), + restart_path: None, window_closed_observers: SubscriberSet::new(), layout_id_buffer: Default::default(), propagate_event: true, @@ -832,8 +836,16 @@ impl App { } /// Restarts the application. - pub fn restart(&self, binary_path: Option) { - self.platform.restart(binary_path) + pub fn restart(&mut self) { + self.restart_observers + .clone() + .retain(&(), |observer| observer(self)); + self.platform.restart(self.restart_path.take()) + } + + /// Sets the path to use when restarting the application. + pub fn set_restart_path(&mut self, path: PathBuf) { + self.restart_path = Some(path); } /// Returns the HTTP client for the application. @@ -1466,6 +1478,21 @@ impl App { subscription } + /// Register a callback to be invoked when the application is about to restart. + /// + /// These callbacks are called before any `on_app_quit` callbacks. + pub fn on_app_restart(&self, mut on_restart: impl 'static + FnMut(&mut App)) -> Subscription { + let (subscription, activate) = self.restart_observers.insert( + (), + Box::new(move |cx| { + on_restart(cx); + true + }), + ); + activate(); + subscription + } + /// Register a callback to be invoked when a window is closed /// The window is no longer accessible at the point this callback is invoked. pub fn on_window_closed(&self, mut on_closed: impl FnMut(&mut App) + 'static) -> Subscription { diff --git a/crates/gpui/src/app/context.rs b/crates/gpui/src/app/context.rs index 392be2ffe9..68c41592b3 100644 --- a/crates/gpui/src/app/context.rs +++ b/crates/gpui/src/app/context.rs @@ -164,6 +164,20 @@ impl<'a, T: 'static> Context<'a, T> { subscription } + /// Register a callback to be invoked when the application is about to restart. + pub fn on_app_restart( + &self, + mut on_restart: impl FnMut(&mut T, &mut App) + 'static, + ) -> Subscription + where + T: 'static, + { + let handle = self.weak_entity(); + self.app.on_app_restart(move |cx| { + handle.update(cx, |entity, cx| on_restart(entity, cx)).ok(); + }) + } + /// Arrange for the given function to be invoked whenever the application is quit. /// The future returned from this callback will be polled for up to [crate::SHUTDOWN_TIMEOUT] until the app fully quits. pub fn on_app_quit( @@ -175,20 +189,15 @@ impl<'a, T: 'static> Context<'a, T> { T: 'static, { let handle = self.weak_entity(); - let (subscription, activate) = self.app.quit_observers.insert( - (), - Box::new(move |cx| { - let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok(); - async move { - if let Some(future) = future { - future.await; - } + self.app.on_app_quit(move |cx| { + let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok(); + async move { + if let Some(future) = future { + future.await; } - .boxed_local() - }), - ); - activate(); - subscription + } + .boxed_local() + }) } /// Tell GPUI that this entity has changed and observers of it should be notified. diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 9e5d359e43..bbde655b80 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -370,9 +370,9 @@ impl Platform for WindowsPlatform { .detach(); } - fn restart(&self, _: Option) { + fn restart(&self, binary_path: Option) { let pid = std::process::id(); - let Some(app_path) = self.app_path().log_err() else { + let Some(app_path) = binary_path.or(self.app_path().log_err()) else { return; }; let script = format!( diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index d11d3b7081..eb317a5616 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -595,7 +595,7 @@ impl TitleBar { .on_click(|_, window, cx| { if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) { if auto_updater.read(cx).status().is_updated() { - workspace::reload(&Default::default(), cx); + workspace::reload(cx); return; } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index aab8a36f45..98794e54cd 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -224,6 +224,8 @@ actions!( ResetActiveDockSize, /// Resets all open docks to their default sizes. ResetOpenDocksSize, + /// Reloads the application + Reload, /// Saves the current file with a new name. SaveAs, /// Saves without formatting. @@ -340,14 +342,6 @@ pub struct CloseInactiveTabsAndPanes { #[action(namespace = workspace)] pub struct SendKeystrokes(pub String); -/// Reloads the active item or workspace. -#[derive(Clone, Deserialize, PartialEq, Default, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct Reload { - pub binary_path: Option, -} - actions!( project_symbols, [ @@ -555,8 +549,8 @@ pub fn init(app_state: Arc, cx: &mut App) { toast_layer::init(cx); history_manager::init(cx); - cx.on_action(Workspace::close_global); - cx.on_action(reload); + cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx)); + cx.on_action(|_: &Reload, cx| reload(cx)); cx.on_action({ let app_state = Arc::downgrade(&app_state); @@ -2184,7 +2178,7 @@ impl Workspace { } } - pub fn close_global(_: &CloseWindow, cx: &mut App) { + pub fn close_global(cx: &mut App) { cx.defer(|cx| { cx.windows().iter().find(|window| { window @@ -7642,7 +7636,7 @@ pub fn join_in_room_project( }) } -pub fn reload(reload: &Reload, cx: &mut App) { +pub fn reload(cx: &mut App) { let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit; let mut workspace_windows = cx .windows() @@ -7669,7 +7663,6 @@ pub fn reload(reload: &Reload, cx: &mut App) { .ok(); } - let binary_path = reload.binary_path.clone(); cx.spawn(async move |cx| { if let Some(prompt) = prompt { let answer = prompt.await?; @@ -7688,8 +7681,7 @@ pub fn reload(reload: &Reload, cx: &mut App) { } } } - - cx.update(|cx| cx.restart(binary_path)) + cx.update(|cx| cx.restart()) }) .detach_and_log_err(cx); } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e4a14b5d32..457372b4af 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -201,16 +201,6 @@ pub fn main() { return; } - // Check if there is a pending installer - // If there is, run the installer and exit - // And we don't want to run the installer if we are not the first instance - #[cfg(target_os = "windows")] - let is_first_instance = crate::zed::windows_only_instance::is_first_instance(); - #[cfg(target_os = "windows")] - if is_first_instance && auto_update::check_pending_installation() { - return; - } - if args.dump_all_actions { dump_all_gpui_actions(); return; @@ -283,30 +273,27 @@ pub fn main() { let (open_listener, mut open_rx) = OpenListener::new(); - let failed_single_instance_check = - if *db::ZED_STATELESS || *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev { - false - } else { - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - { - crate::zed::listen_for_cli_connections(open_listener.clone()).is_err() - } + let failed_single_instance_check = if *db::ZED_STATELESS + || *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev + { + false + } else { + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + { + crate::zed::listen_for_cli_connections(open_listener.clone()).is_err() + } - #[cfg(target_os = "windows")] - { - !crate::zed::windows_only_instance::handle_single_instance( - open_listener.clone(), - &args, - is_first_instance, - ) - } + #[cfg(target_os = "windows")] + { + !crate::zed::windows_only_instance::handle_single_instance(open_listener.clone(), &args) + } - #[cfg(target_os = "macos")] - { - use zed::mac_only_instance::*; - ensure_only_instance() != IsOnlyInstance::Yes - } - }; + #[cfg(target_os = "macos")] + { + use zed::mac_only_instance::*; + ensure_only_instance() != IsOnlyInstance::Yes + } + }; if failed_single_instance_check { println!("zed is already running"); return; diff --git a/crates/zed/src/zed/windows_only_instance.rs b/crates/zed/src/zed/windows_only_instance.rs index 277e8ee724..bd62dea75a 100644 --- a/crates/zed/src/zed/windows_only_instance.rs +++ b/crates/zed/src/zed/windows_only_instance.rs @@ -25,7 +25,8 @@ use windows::{ use crate::{Args, OpenListener, RawOpenRequest}; -pub fn is_first_instance() -> bool { +#[inline] +fn is_first_instance() -> bool { unsafe { CreateMutexW( None, @@ -37,7 +38,8 @@ pub fn is_first_instance() -> bool { unsafe { GetLastError() != ERROR_ALREADY_EXISTS } } -pub fn handle_single_instance(opener: OpenListener, args: &Args, is_first_instance: bool) -> bool { +pub fn handle_single_instance(opener: OpenListener, args: &Args) -> bool { + let is_first_instance = is_first_instance(); if is_first_instance { // We are the first instance, listen for messages sent from other instances std::thread::spawn(move || { From d78bd8f1d738b3d9da23b707467237500ca4e961 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Tue, 12 Aug 2025 21:41:00 -0400 Subject: [PATCH 299/693] Fix external agent still being marked as generating after error response (#35992) Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index cadab3d62c..d09c80fe9d 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1072,8 +1072,11 @@ impl AcpThread { cx.spawn(async move |this, cx| match rx.await { Ok(Err(e)) => { - this.update(cx, |_, cx| cx.emit(AcpThreadEvent::Error)) - .log_err(); + this.update(cx, |this, cx| { + this.send_task.take(); + cx.emit(AcpThreadEvent::Error) + }) + .log_err(); Err(e)? } result => { From 1957e1f642456e26efff735436ea955d52f920cd Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Tue, 12 Aug 2025 21:48:28 -0400 Subject: [PATCH 300/693] Add locations to native agent tool calls, and wire them up to UI (#36058) Release Notes: - N/A --------- Co-authored-by: Conrad --- Cargo.lock | 1 + crates/acp_thread/src/acp_thread.rs | 155 ++++++++++++++----- crates/agent2/Cargo.toml | 1 + crates/agent2/src/tools/edit_file_tool.rs | 42 ++++- crates/agent2/src/tools/read_file_tool.rs | 63 ++++---- crates/agent_ui/src/acp/thread_view.rs | 38 +++-- crates/assistant_tools/src/edit_agent.rs | 69 ++++++--- crates/assistant_tools/src/edit_file_tool.rs | 2 +- 8 files changed, 257 insertions(+), 114 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d24a399c1c..9ac0809d25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -231,6 +231,7 @@ dependencies = [ "task", "tempfile", "terminal", + "text", "theme", "tree-sitter-rust", "ui", diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index d09c80fe9d..80e0a31f97 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -13,9 +13,9 @@ use agent_client_protocol::{self as acp}; use anyhow::{Context as _, Result}; use editor::Bias; use futures::{FutureExt, channel::oneshot, future::BoxFuture}; -use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task}; +use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity}; use itertools::Itertools; -use language::{Anchor, Buffer, BufferSnapshot, LanguageRegistry, Point, text_diff}; +use language::{Anchor, Buffer, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff}; use markdown::Markdown; use project::{AgentLocation, Project}; use std::collections::HashMap; @@ -122,9 +122,17 @@ impl AgentThreadEntry { } } - pub fn locations(&self) -> Option<&[acp::ToolCallLocation]> { - if let AgentThreadEntry::ToolCall(ToolCall { locations, .. }) = self { - Some(locations) + pub fn location(&self, ix: usize) -> Option<(acp::ToolCallLocation, AgentLocation)> { + if let AgentThreadEntry::ToolCall(ToolCall { + locations, + resolved_locations, + .. + }) = self + { + Some(( + locations.get(ix)?.clone(), + resolved_locations.get(ix)?.clone()?, + )) } else { None } @@ -139,6 +147,7 @@ pub struct ToolCall { pub content: Vec, pub status: ToolCallStatus, pub locations: Vec, + pub resolved_locations: Vec>, pub raw_input: Option, pub raw_output: Option, } @@ -167,6 +176,7 @@ impl ToolCall { .map(|content| ToolCallContent::from_acp(content, language_registry.clone(), cx)) .collect(), locations: tool_call.locations, + resolved_locations: Vec::default(), status, raw_input: tool_call.raw_input, raw_output: tool_call.raw_output, @@ -260,6 +270,57 @@ impl ToolCall { } markdown } + + async fn resolve_location( + location: acp::ToolCallLocation, + project: WeakEntity, + cx: &mut AsyncApp, + ) -> Option { + let buffer = project + .update(cx, |project, cx| { + if let Some(path) = project.project_path_for_absolute_path(&location.path, cx) { + Some(project.open_buffer(path, cx)) + } else { + None + } + }) + .ok()??; + let buffer = buffer.await.log_err()?; + let position = buffer + .update(cx, |buffer, _| { + if let Some(row) = location.line { + let snapshot = buffer.snapshot(); + let column = snapshot.indent_size_for_line(row).len; + let point = snapshot.clip_point(Point::new(row, column), Bias::Left); + snapshot.anchor_before(point) + } else { + Anchor::MIN + } + }) + .ok()?; + + Some(AgentLocation { + buffer: buffer.downgrade(), + position, + }) + } + + fn resolve_locations( + &self, + project: Entity, + cx: &mut App, + ) -> Task>> { + let locations = self.locations.clone(); + project.update(cx, |_, cx| { + cx.spawn(async move |project, cx| { + let mut new_locations = Vec::new(); + for location in locations { + new_locations.push(Self::resolve_location(location, project.clone(), cx).await); + } + new_locations + }) + }) + } } #[derive(Debug)] @@ -804,7 +865,11 @@ impl AcpThread { .context("Tool call not found")?; match update { ToolCallUpdate::UpdateFields(update) => { + let location_updated = update.fields.locations.is_some(); current_call.update_fields(update.fields, languages, cx); + if location_updated { + self.resolve_locations(update.id.clone(), cx); + } } ToolCallUpdate::UpdateDiff(update) => { current_call.content.clear(); @@ -841,8 +906,7 @@ impl AcpThread { ) { let language_registry = self.project.read(cx).languages().clone(); let call = ToolCall::from_acp(tool_call, status, language_registry, cx); - - let location = call.locations.last().cloned(); + let id = call.id.clone(); if let Some((ix, current_call)) = self.tool_call_mut(&call.id) { *current_call = call; @@ -850,11 +914,9 @@ impl AcpThread { cx.emit(AcpThreadEvent::EntryUpdated(ix)); } else { self.push_entry(AgentThreadEntry::ToolCall(call), cx); - } + }; - if let Some(location) = location { - self.set_project_location(location, cx) - } + self.resolve_locations(id, cx); } fn tool_call_mut(&mut self, id: &acp::ToolCallId) -> Option<(usize, &mut ToolCall)> { @@ -875,35 +937,50 @@ impl AcpThread { }) } - pub fn set_project_location(&self, location: acp::ToolCallLocation, cx: &mut Context) { - self.project.update(cx, |project, cx| { - let Some(path) = project.project_path_for_absolute_path(&location.path, cx) else { - return; - }; - let buffer = project.open_buffer(path, cx); - cx.spawn(async move |project, cx| { - let buffer = buffer.await?; - - project.update(cx, |project, cx| { - let position = if let Some(line) = location.line { - let snapshot = buffer.read(cx).snapshot(); - let point = snapshot.clip_point(Point::new(line, 0), Bias::Left); - snapshot.anchor_before(point) - } else { - Anchor::MIN - }; - - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position, - }), - cx, - ); - }) + pub fn resolve_locations(&mut self, id: acp::ToolCallId, cx: &mut Context) { + let project = self.project.clone(); + let Some((_, tool_call)) = self.tool_call_mut(&id) else { + return; + }; + let task = tool_call.resolve_locations(project, cx); + cx.spawn(async move |this, cx| { + let resolved_locations = task.await; + this.update(cx, |this, cx| { + let project = this.project.clone(); + let Some((ix, tool_call)) = this.tool_call_mut(&id) else { + return; + }; + if let Some(Some(location)) = resolved_locations.last() { + project.update(cx, |project, cx| { + if let Some(agent_location) = project.agent_location() { + let should_ignore = agent_location.buffer == location.buffer + && location + .buffer + .update(cx, |buffer, _| { + let snapshot = buffer.snapshot(); + let old_position = + agent_location.position.to_point(&snapshot); + let new_position = location.position.to_point(&snapshot); + // ignore this so that when we get updates from the edit tool + // the position doesn't reset to the startof line + old_position.row == new_position.row + && old_position.column > new_position.column + }) + .ok() + .unwrap_or_default(); + if !should_ignore { + project.set_agent_location(Some(location.clone()), cx); + } + } + }); + } + if tool_call.resolved_locations != resolved_locations { + tool_call.resolved_locations = resolved_locations; + cx.emit(AcpThreadEvent::EntryUpdated(ix)); + } }) - .detach_and_log_err(cx); - }); + }) + .detach(); } pub fn request_tool_call_authorization( diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 1030380dc0..ac1840e5e5 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -49,6 +49,7 @@ settings.workspace = true smol.workspace = true task.workspace = true terminal.workspace = true +text.workspace = true ui.workspace = true util.workspace = true uuid.workspace = true diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 134bc5e5e4..405afb585f 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -1,12 +1,13 @@ use crate::{AgentTool, Thread, ToolCallEventStream}; use acp_thread::Diff; -use agent_client_protocol as acp; +use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields}; use anyhow::{Context as _, Result, anyhow}; use assistant_tools::edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat}; use cloud_llm_client::CompletionIntent; use collections::HashSet; use gpui::{App, AppContext, AsyncApp, Entity, Task}; use indoc::formatdoc; +use language::ToPoint; use language::language_settings::{self, FormatOnSave}; use language_model::LanguageModelToolResultContent; use paths; @@ -225,6 +226,16 @@ impl AgentTool for EditFileTool { Ok(path) => path, Err(err) => return Task::ready(Err(anyhow!(err))), }; + let abs_path = project.read(cx).absolute_path(&project_path, cx); + if let Some(abs_path) = abs_path.clone() { + event_stream.update_fields(ToolCallUpdateFields { + locations: Some(vec![acp::ToolCallLocation { + path: abs_path, + line: None, + }]), + ..Default::default() + }); + } let request = self.thread.update(cx, |thread, cx| { thread.build_completion_request(CompletionIntent::ToolResults, cx) @@ -283,13 +294,38 @@ impl AgentTool for EditFileTool { let mut hallucinated_old_text = false; let mut ambiguous_ranges = Vec::new(); + let mut emitted_location = false; while let Some(event) = events.next().await { match event { - EditAgentOutputEvent::Edited => {}, + EditAgentOutputEvent::Edited(range) => { + if !emitted_location { + let line = buffer.update(cx, |buffer, _cx| { + range.start.to_point(&buffer.snapshot()).row + }).ok(); + if let Some(abs_path) = abs_path.clone() { + event_stream.update_fields(ToolCallUpdateFields { + locations: Some(vec![ToolCallLocation { path: abs_path, line }]), + ..Default::default() + }); + } + emitted_location = true; + } + }, EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true, EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges, EditAgentOutputEvent::ResolvingEditRange(range) => { - diff.update(cx, |card, cx| card.reveal_range(range, cx))?; + diff.update(cx, |card, cx| card.reveal_range(range.clone(), cx))?; + // if !emitted_location { + // let line = buffer.update(cx, |buffer, _cx| { + // range.start.to_point(&buffer.snapshot()).row + // }).ok(); + // if let Some(abs_path) = abs_path.clone() { + // event_stream.update_fields(ToolCallUpdateFields { + // locations: Some(vec![ToolCallLocation { path: abs_path, line }]), + // ..Default::default() + // }); + // } + // } } } } diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index fac637d838..f21643cbbb 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -1,10 +1,10 @@ use action_log::ActionLog; -use agent_client_protocol::{self as acp}; +use agent_client_protocol::{self as acp, ToolCallUpdateFields}; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::outline; use gpui::{App, Entity, SharedString, Task}; use indoc::formatdoc; -use language::{Anchor, Point}; +use language::Point; use language_model::{LanguageModelImage, LanguageModelToolResultContent}; use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store}; use schemars::JsonSchema; @@ -97,7 +97,7 @@ impl AgentTool for ReadFileTool { fn run( self: Arc, input: Self::Input, - _event_stream: ToolCallEventStream, + event_stream: ToolCallEventStream, cx: &mut App, ) -> Task> { let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) else { @@ -166,7 +166,9 @@ impl AgentTool for ReadFileTool { cx.spawn(async move |cx| { let buffer = cx .update(|cx| { - project.update(cx, |project, cx| project.open_buffer(project_path, cx)) + project.update(cx, |project, cx| { + project.open_buffer(project_path.clone(), cx) + }) })? .await?; if buffer.read_with(cx, |buffer, _| { @@ -178,19 +180,10 @@ impl AgentTool for ReadFileTool { anyhow::bail!("{file_path} not found"); } - project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: Anchor::MIN, - }), - cx, - ); - })?; + let mut anchor = None; // Check if specific line ranges are provided - if input.start_line.is_some() || input.end_line.is_some() { - let mut anchor = None; + let result = if input.start_line.is_some() || input.end_line.is_some() { let result = buffer.read_with(cx, |buffer, _cx| { let text = buffer.text(); // .max(1) because despite instructions to be 1-indexed, sometimes the model passes 0. @@ -214,18 +207,6 @@ impl AgentTool for ReadFileTool { log.buffer_read(buffer.clone(), cx); })?; - if let Some(anchor) = anchor { - project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: anchor, - }), - cx, - ); - })?; - } - Ok(result.into()) } else { // No line ranges specified, so check file size to see if it's too big. @@ -236,7 +217,7 @@ impl AgentTool for ReadFileTool { let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?; action_log.update(cx, |log, cx| { - log.buffer_read(buffer, cx); + log.buffer_read(buffer.clone(), cx); })?; Ok(result.into()) @@ -244,7 +225,8 @@ impl AgentTool for ReadFileTool { // File is too big, so return the outline // and a suggestion to read again with line numbers. let outline = - outline::file_outline(project, file_path, action_log, None, cx).await?; + outline::file_outline(project.clone(), file_path, action_log, None, cx) + .await?; Ok(formatdoc! {" This file was too big to read all at once. @@ -261,7 +243,28 @@ impl AgentTool for ReadFileTool { } .into()) } - } + }; + + project.update(cx, |project, cx| { + if let Some(abs_path) = project.absolute_path(&project_path, cx) { + project.set_agent_location( + Some(AgentLocation { + buffer: buffer.downgrade(), + position: anchor.unwrap_or(text::Anchor::MIN), + }), + cx, + ); + event_stream.update_fields(ToolCallUpdateFields { + locations: Some(vec![acp::ToolCallLocation { + path: abs_path, + line: input.start_line.map(|line| line.saturating_sub(1)), + }]), + ..Default::default() + }); + } + })?; + + result }) } } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index f47c7a0bc5..da7915222e 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -27,6 +27,7 @@ use language::{Buffer, Language}; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; use parking_lot::Mutex; use project::{CompletionIntent, Project}; +use rope::Point; use settings::{Settings as _, SettingsStore}; use std::path::PathBuf; use std::{ @@ -2679,26 +2680,24 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) -> Option<()> { - let location = self + let (tool_call_location, agent_location) = self .thread()? .read(cx) .entries() .get(entry_ix)? - .locations()? - .get(location_ix)?; + .location(location_ix)?; let project_path = self .project .read(cx) - .find_project_path(&location.path, cx)?; + .find_project_path(&tool_call_location.path, cx)?; let open_task = self .workspace - .update(cx, |worskpace, cx| { - worskpace.open_path(project_path, None, true, window, cx) + .update(cx, |workspace, cx| { + workspace.open_path(project_path, None, true, window, cx) }) .log_err()?; - window .spawn(cx, async move |cx| { let item = open_task.await?; @@ -2708,17 +2707,22 @@ impl AcpThreadView { }; active_editor.update_in(cx, |editor, window, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let first_hunk = editor - .diff_hunks_in_ranges( - &[editor::Anchor::min()..editor::Anchor::max()], - &snapshot, - ) - .next(); - if let Some(first_hunk) = first_hunk { - let first_hunk_start = first_hunk.multi_buffer_range().start; + let multibuffer = editor.buffer().read(cx); + let buffer = multibuffer.as_singleton(); + if agent_location.buffer.upgrade() == buffer { + let excerpt_id = multibuffer.excerpt_ids().first().cloned(); + let anchor = editor::Anchor::in_buffer( + excerpt_id.unwrap(), + buffer.unwrap().read(cx).remote_id(), + agent_location.position, + ); editor.change_selections(Default::default(), window, cx, |selections| { - selections.select_anchor_ranges([first_hunk_start..first_hunk_start]); + selections.select_anchor_ranges([anchor..anchor]); + }) + } else { + let row = tool_call_location.line.unwrap_or_default(); + editor.change_selections(Default::default(), window, cx, |selections| { + selections.select_ranges([Point::new(row, 0)..Point::new(row, 0)]); }) } })?; diff --git a/crates/assistant_tools/src/edit_agent.rs b/crates/assistant_tools/src/edit_agent.rs index 9305f584cb..aa321aa8f3 100644 --- a/crates/assistant_tools/src/edit_agent.rs +++ b/crates/assistant_tools/src/edit_agent.rs @@ -65,7 +65,7 @@ pub enum EditAgentOutputEvent { ResolvingEditRange(Range), UnresolvedEditRange, AmbiguousEditRange(Vec>), - Edited, + Edited(Range), } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] @@ -178,7 +178,9 @@ impl EditAgent { ) }); output_events_tx - .unbounded_send(EditAgentOutputEvent::Edited) + .unbounded_send(EditAgentOutputEvent::Edited( + language::Anchor::MIN..language::Anchor::MAX, + )) .ok(); })?; @@ -200,7 +202,9 @@ impl EditAgent { }); })?; output_events_tx - .unbounded_send(EditAgentOutputEvent::Edited) + .unbounded_send(EditAgentOutputEvent::Edited( + language::Anchor::MIN..language::Anchor::MAX, + )) .ok(); } } @@ -336,8 +340,8 @@ impl EditAgent { // Edit the buffer and report edits to the action log as part of the // same effect cycle, otherwise the edit will be reported as if the // user made it. - cx.update(|cx| { - let max_edit_end = buffer.update(cx, |buffer, cx| { + let (min_edit_start, max_edit_end) = cx.update(|cx| { + let (min_edit_start, max_edit_end) = buffer.update(cx, |buffer, cx| { buffer.edit(edits.iter().cloned(), None, cx); let max_edit_end = buffer .summaries_for_anchors::( @@ -345,7 +349,16 @@ impl EditAgent { ) .max() .unwrap(); - buffer.anchor_before(max_edit_end) + let min_edit_start = buffer + .summaries_for_anchors::( + edits.iter().map(|(range, _)| &range.start), + ) + .min() + .unwrap(); + ( + buffer.anchor_after(min_edit_start), + buffer.anchor_before(max_edit_end), + ) }); self.action_log .update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); @@ -358,9 +371,10 @@ impl EditAgent { cx, ); }); + (min_edit_start, max_edit_end) })?; output_events - .unbounded_send(EditAgentOutputEvent::Edited) + .unbounded_send(EditAgentOutputEvent::Edited(min_edit_start..max_edit_end)) .ok(); } @@ -755,6 +769,7 @@ mod tests { use gpui::{AppContext, TestAppContext}; use indoc::indoc; use language_model::fake_provider::FakeLanguageModel; + use pretty_assertions::assert_matches; use project::{AgentLocation, Project}; use rand::prelude::*; use rand::rngs::StdRng; @@ -992,7 +1007,10 @@ mod tests { model.send_last_completion_stream_text_chunk("abX"); cx.run_until_parked(); - assert_eq!(drain_events(&mut events), [EditAgentOutputEvent::Edited]); + assert_matches!( + drain_events(&mut events).as_slice(), + [EditAgentOutputEvent::Edited(_)] + ); assert_eq!( buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), "abXc\ndef\nghi\njkl" @@ -1007,7 +1025,10 @@ mod tests { model.send_last_completion_stream_text_chunk("cY"); cx.run_until_parked(); - assert_eq!(drain_events(&mut events), [EditAgentOutputEvent::Edited]); + assert_matches!( + drain_events(&mut events).as_slice(), + [EditAgentOutputEvent::Edited { .. }] + ); assert_eq!( buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), "abXcY\ndef\nghi\njkl" @@ -1118,9 +1139,9 @@ mod tests { model.send_last_completion_stream_text_chunk("GHI"); cx.run_until_parked(); - assert_eq!( - drain_events(&mut events), - vec![EditAgentOutputEvent::Edited] + assert_matches!( + drain_events(&mut events).as_slice(), + [EditAgentOutputEvent::Edited { .. }] ); assert_eq!( buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), @@ -1165,9 +1186,9 @@ mod tests { ); cx.run_until_parked(); - assert_eq!( - drain_events(&mut events), - vec![EditAgentOutputEvent::Edited] + assert_matches!( + drain_events(&mut events).as_slice(), + [EditAgentOutputEvent::Edited(_)] ); assert_eq!( buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), @@ -1183,9 +1204,9 @@ mod tests { chunks_tx.unbounded_send("```\njkl\n").unwrap(); cx.run_until_parked(); - assert_eq!( - drain_events(&mut events), - vec![EditAgentOutputEvent::Edited] + assert_matches!( + drain_events(&mut events).as_slice(), + [EditAgentOutputEvent::Edited { .. }] ); assert_eq!( buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), @@ -1201,9 +1222,9 @@ mod tests { chunks_tx.unbounded_send("mno\n").unwrap(); cx.run_until_parked(); - assert_eq!( - drain_events(&mut events), - vec![EditAgentOutputEvent::Edited] + assert_matches!( + drain_events(&mut events).as_slice(), + [EditAgentOutputEvent::Edited { .. }] ); assert_eq!( buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), @@ -1219,9 +1240,9 @@ mod tests { chunks_tx.unbounded_send("pqr\n```").unwrap(); cx.run_until_parked(); - assert_eq!( - drain_events(&mut events), - vec![EditAgentOutputEvent::Edited] + assert_matches!( + drain_events(&mut events).as_slice(), + [EditAgentOutputEvent::Edited(_)], ); assert_eq!( buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index b5712415ec..e819c51e1e 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -307,7 +307,7 @@ impl Tool for EditFileTool { let mut ambiguous_ranges = Vec::new(); while let Some(event) = events.next().await { match event { - EditAgentOutputEvent::Edited => { + EditAgentOutputEvent::Edited { .. } => { if let Some(card) = card_clone.as_ref() { card.update(cx, |card, cx| card.update_diff(cx))?; } From dc87f4b32e4c370d623a0cefbb32ea368fbc2c05 Mon Sep 17 00:00:00 2001 From: morgankrey Date: Tue, 12 Aug 2025 21:15:48 -0600 Subject: [PATCH 301/693] Add 4.1 to models page (#36086) Adds opus 4.1 to models page in docs Release Notes: - N/A --- docs/src/ai/models.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/src/ai/models.md b/docs/src/ai/models.md index b40f17b77f..8d46d0b8d1 100644 --- a/docs/src/ai/models.md +++ b/docs/src/ai/models.md @@ -12,8 +12,10 @@ We’re working hard to expand the models supported by Zed’s subscription offe | Claude Sonnet 4 | Anthropic | ✅ | 200k | N/A | $0.05 | | Claude Opus 4 | Anthropic | ❌ | 120k | $0.20 | N/A | | Claude Opus 4 | Anthropic | ✅ | 200k | N/A | $0.25 | +| Claude Opus 4.1 | Anthropic | ❌ | 120k | $0.20 | N/A | +| Claude Opus 4.1 | Anthropic | ✅ | 200k | N/A | $0.25 | -> Note: Because of the 5x token cost for [Opus relative to Sonnet](https://www.anthropic.com/pricing#api), each Opus prompt consumes 5 prompts against your billing meter +> Note: Because of the 5x token cost for [Opus relative to Sonnet](https://www.anthropic.com/pricing#api), each Opus 4 and 4.1 prompt consumes 5 prompts against your billing meter ## Usage {#usage} From 96093aa465f14eeb01fa6e6c457f57a0283c1e69 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 13 Aug 2025 01:18:11 -0400 Subject: [PATCH 302/693] onboarding: Link git clone button with action (#35999) Release Notes: - N/A --- Cargo.lock | 1 + crates/git_ui/src/git_panel.rs | 2 +- crates/git_ui/src/git_ui.rs | 4 ---- crates/onboarding/Cargo.toml | 1 + crates/onboarding/src/welcome.rs | 5 ++--- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9ac0809d25..ffcaf64859 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11157,6 +11157,7 @@ dependencies = [ "feature_flags", "fs", "fuzzy", + "git", "gpui", "itertools 0.14.0", "language", diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 75fac114d2..de308b9dde 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -2105,7 +2105,7 @@ impl GitPanel { Ok(_) => cx.update(|window, cx| { window.prompt( PromptLevel::Info, - "Git Clone", + &format!("Git Clone: {}", repo_name), None, &["Add repo to project", "Open repo in new project"], cx, diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 7d5207dfb6..79aa4a6bd0 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -181,10 +181,6 @@ pub fn init(cx: &mut App) { workspace.toggle_modal(window, cx, |window, cx| { GitCloneModal::show(panel, window, cx) }); - - // panel.update(cx, |panel, cx| { - // panel.git_clone(window, cx); - // }); }); workspace.register_action(|workspace, _: &git::OpenModifiedFiles, window, cx| { open_modified_files(workspace, window, cx); diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index 436c714cf3..cb07bb5dab 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -26,6 +26,7 @@ editor.workspace = true feature_flags.workspace = true fs.workspace = true fuzzy.workspace = true +git.workspace = true gpui.workspace = true itertools.workspace = true language.workspace = true diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index d4d6c3f701..65baad03a0 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -1,6 +1,6 @@ use gpui::{ Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, - NoAction, ParentElement, Render, Styled, Window, actions, + ParentElement, Render, Styled, Window, actions, }; use menu::{SelectNext, SelectPrevious}; use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*}; @@ -38,8 +38,7 @@ const CONTENT: (Section<4>, Section<3>) = ( SectionEntry { icon: IconName::CloudDownload, title: "Clone a Repo", - // TODO: use proper action - action: &NoAction, + action: &git::Clone, }, SectionEntry { icon: IconName::ListCollapse, From 8ff2e3e1956543a0bf1f801aaec05f8993030c91 Mon Sep 17 00:00:00 2001 From: Cretezy Date: Wed, 13 Aug 2025 02:09:16 -0400 Subject: [PATCH 303/693] language_models: Add reasoning_effort for custom models (#35929) Release Notes: - Added `reasoning_effort` support to custom models Tested using the following config: ```json5 "language_models": { "openai": { "available_models": [ { "name": "gpt-5-mini", "display_name": "GPT 5 Mini (custom reasoning)", "max_output_tokens": 128000, "max_tokens": 272000, "reasoning_effort": "high" // Can be minimal, low, medium (default), and high } ], "version": "1" } } ``` Docs: https://platform.openai.com/docs/api-reference/chat/create#chat_create-reasoning_effort This work could be used to split the GPT 5/5-mini/5-nano into each of it's reasoning effort variant. E.g. `gpt-5`, `gpt-5 low`, `gpt-5 minimal`, `gpt-5 high`, and same for mini/nano. Release Notes: * Added a setting to control `reasoning_effort` in OpenAI models --- crates/language_models/src/provider/cloud.rs | 1 + .../language_models/src/provider/open_ai.rs | 7 +++++- .../src/provider/open_ai_compatible.rs | 8 ++++++- crates/language_models/src/provider/vercel.rs | 1 + crates/language_models/src/provider/x_ai.rs | 1 + crates/open_ai/src/open_ai.rs | 22 +++++++++++++++++++ 6 files changed, 38 insertions(+), 2 deletions(-) diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index ba110be9c5..ff8048040e 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -942,6 +942,7 @@ impl LanguageModel for CloudLanguageModel { model.id(), model.supports_parallel_tool_calls(), None, + None, ); let llm_api_token = self.llm_api_token.clone(); let future = self.request_limiter.stream(async move { diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 9eac58c880..725027b2a7 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -14,7 +14,7 @@ use language_model::{ RateLimiter, Role, StopReason, TokenUsage, }; use menu; -use open_ai::{ImageUrl, Model, ResponseStreamEvent, stream_completion}; +use open_ai::{ImageUrl, Model, ReasoningEffort, ResponseStreamEvent, stream_completion}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; @@ -45,6 +45,7 @@ pub struct AvailableModel { pub max_tokens: u64, pub max_output_tokens: Option, pub max_completion_tokens: Option, + pub reasoning_effort: Option, } pub struct OpenAiLanguageModelProvider { @@ -213,6 +214,7 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider { max_tokens: model.max_tokens, max_output_tokens: model.max_output_tokens, max_completion_tokens: model.max_completion_tokens, + reasoning_effort: model.reasoning_effort.clone(), }, ); } @@ -369,6 +371,7 @@ impl LanguageModel for OpenAiLanguageModel { self.model.id(), self.model.supports_parallel_tool_calls(), self.max_output_tokens(), + self.model.reasoning_effort(), ); let completions = self.stream_completion(request, cx); async move { @@ -384,6 +387,7 @@ pub fn into_open_ai( model_id: &str, supports_parallel_tool_calls: bool, max_output_tokens: Option, + reasoning_effort: Option, ) -> open_ai::Request { let stream = !model_id.starts_with("o1-"); @@ -490,6 +494,7 @@ pub fn into_open_ai( LanguageModelToolChoice::Any => open_ai::ToolChoice::Required, LanguageModelToolChoice::None => open_ai::ToolChoice::None, }), + reasoning_effort, } } diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs index 38bd7cee06..6e912765cd 100644 --- a/crates/language_models/src/provider/open_ai_compatible.rs +++ b/crates/language_models/src/provider/open_ai_compatible.rs @@ -355,7 +355,13 @@ impl LanguageModel for OpenAiCompatibleLanguageModel { LanguageModelCompletionError, >, > { - let request = into_open_ai(request, &self.model.name, true, self.max_output_tokens()); + let request = into_open_ai( + request, + &self.model.name, + true, + self.max_output_tokens(), + None, + ); let completions = self.stream_completion(request, cx); async move { let mapper = OpenAiEventMapper::new(); diff --git a/crates/language_models/src/provider/vercel.rs b/crates/language_models/src/provider/vercel.rs index 037ce467d0..57a89ba4aa 100644 --- a/crates/language_models/src/provider/vercel.rs +++ b/crates/language_models/src/provider/vercel.rs @@ -356,6 +356,7 @@ impl LanguageModel for VercelLanguageModel { self.model.id(), self.model.supports_parallel_tool_calls(), self.max_output_tokens(), + None, ); let completions = self.stream_completion(request, cx); async move { diff --git a/crates/language_models/src/provider/x_ai.rs b/crates/language_models/src/provider/x_ai.rs index 5f6034571b..5e7190ea96 100644 --- a/crates/language_models/src/provider/x_ai.rs +++ b/crates/language_models/src/provider/x_ai.rs @@ -360,6 +360,7 @@ impl LanguageModel for XAiLanguageModel { self.model.id(), self.model.supports_parallel_tool_calls(), self.max_output_tokens(), + None, ); let completions = self.stream_completion(request, cx); async move { diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 919b1d9ebf..5801f29623 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -89,11 +89,13 @@ pub enum Model { max_tokens: u64, max_output_tokens: Option, max_completion_tokens: Option, + reasoning_effort: Option, }, } impl Model { pub fn default_fast() -> Self { + // TODO: Replace with FiveMini since all other models are deprecated Self::FourPointOneMini } @@ -206,6 +208,15 @@ impl Model { } } + pub fn reasoning_effort(&self) -> Option { + match self { + Self::Custom { + reasoning_effort, .. + } => reasoning_effort.to_owned(), + _ => None, + } + } + /// Returns whether the given model supports the `parallel_tool_calls` parameter. /// /// If the model does not support the parameter, do not pass it up, or the API will return an error. @@ -246,6 +257,7 @@ pub struct Request { pub tools: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub prompt_cache_key: Option, + pub reasoning_effort: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -257,6 +269,16 @@ pub enum ToolChoice { Other(ToolDefinition), } +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum ReasoningEffort { + Minimal, + Low, + Medium, + High, +} + #[derive(Clone, Deserialize, Serialize, Debug)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ToolDefinition { From db497ac867ce8c9a2bad0aef6261ac2acb2896fa Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Wed, 13 Aug 2025 11:01:02 +0200 Subject: [PATCH 304/693] Agent2 Model Selector (#36028) Release Notes: - N/A --------- Co-authored-by: Bennet Bo Fenner --- Cargo.lock | 3 +- crates/acp_thread/Cargo.toml | 3 +- crates/acp_thread/src/acp_thread.rs | 4 + crates/acp_thread/src/connection.rs | 151 ++++-- crates/agent2/src/agent.rs | 395 ++++++++++++--- crates/agent2/src/native_agent_server.rs | 17 +- crates/agent2/src/tests/mod.rs | 42 +- crates/agent_ui/src/acp.rs | 4 + crates/agent_ui/src/acp/model_selector.rs | 472 ++++++++++++++++++ .../src/acp/model_selector_popover.rs | 85 ++++ crates/agent_ui/src/acp/thread_view.rs | 41 +- crates/agent_ui/src/agent_panel.rs | 5 +- crates/agent_ui/src/agent_ui.rs | 4 +- 13 files changed, 1078 insertions(+), 148 deletions(-) create mode 100644 crates/agent_ui/src/acp/model_selector.rs create mode 100644 crates/agent_ui/src/acp/model_selector_popover.rs diff --git a/Cargo.lock b/Cargo.lock index ffcaf64859..d31189fa06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,6 +10,7 @@ dependencies = [ "agent-client-protocol", "anyhow", "buffer_diff", + "collections", "editor", "env_logger 0.11.8", "futures 0.3.31", @@ -17,7 +18,6 @@ dependencies = [ "indoc", "itertools 0.14.0", "language", - "language_model", "markdown", "parking_lot", "project", @@ -31,6 +31,7 @@ dependencies = [ "ui", "url", "util", + "watch", "workspace-hack", ] diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index 1fef342c01..fd01b31786 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -20,12 +20,12 @@ action_log.workspace = true agent-client-protocol.workspace = true anyhow.workspace = true buffer_diff.workspace = true +collections.workspace = true editor.workspace = true futures.workspace = true gpui.workspace = true itertools.workspace = true language.workspace = true -language_model.workspace = true markdown.workspace = true project.workspace = true serde.workspace = true @@ -36,6 +36,7 @@ terminal.workspace = true ui.workspace = true url.workspace = true util.workspace = true +watch.workspace = true workspace-hack.workspace = true [dev-dependencies] diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 80e0a31f97..d1957e1c2a 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -694,6 +694,10 @@ impl AcpThread { } } + pub fn connection(&self) -> &Rc { + &self.connection + } + pub fn action_log(&self) -> &Entity { &self.action_log } diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index cf06563bee..8e6294b3ce 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -1,61 +1,14 @@ -use std::{error::Error, fmt, path::Path, rc::Rc, sync::Arc}; +use std::{error::Error, fmt, path::Path, rc::Rc}; use agent_client_protocol::{self as acp}; use anyhow::Result; -use gpui::{AsyncApp, Entity, Task}; -use language_model::LanguageModel; +use collections::IndexMap; +use gpui::{AsyncApp, Entity, SharedString, Task}; use project::Project; -use ui::App; +use ui::{App, IconName}; use crate::AcpThread; -/// Trait for agents that support listing, selecting, and querying language models. -/// -/// This is an optional capability; agents indicate support via [AgentConnection::model_selector]. -pub trait ModelSelector: 'static { - /// Lists all available language models for this agent. - /// - /// # Parameters - /// - `cx`: The GPUI app context for async operations and global access. - /// - /// # Returns - /// A task resolving to the list of models or an error (e.g., if no models are configured). - fn list_models(&self, cx: &mut AsyncApp) -> Task>>>; - - /// Selects a model for a specific session (thread). - /// - /// This sets the default model for future interactions in the session. - /// If the session doesn't exist or the model is invalid, it returns an error. - /// - /// # Parameters - /// - `session_id`: The ID of the session (thread) to apply the model to. - /// - `model`: The model to select (should be one from [list_models]). - /// - `cx`: The GPUI app context. - /// - /// # Returns - /// A task resolving to `Ok(())` on success or an error. - fn select_model( - &self, - session_id: acp::SessionId, - model: Arc, - cx: &mut AsyncApp, - ) -> Task>; - - /// Retrieves the currently selected model for a specific session (thread). - /// - /// # Parameters - /// - `session_id`: The ID of the session (thread) to query. - /// - `cx`: The GPUI app context. - /// - /// # Returns - /// A task resolving to the selected model (always set) or an error (e.g., session not found). - fn selected_model( - &self, - session_id: &acp::SessionId, - cx: &mut AsyncApp, - ) -> Task>>; -} - pub trait AgentConnection { fn new_thread( self: Rc, @@ -77,8 +30,8 @@ pub trait AgentConnection { /// /// If the agent does not support model selection, returns [None]. /// This allows sharing the selector in UI components. - fn model_selector(&self) -> Option> { - None // Default impl for agents that don't support it + fn model_selector(&self) -> Option> { + None } } @@ -91,3 +44,95 @@ impl fmt::Display for AuthRequired { write!(f, "AuthRequired") } } + +/// Trait for agents that support listing, selecting, and querying language models. +/// +/// This is an optional capability; agents indicate support via [AgentConnection::model_selector]. +pub trait AgentModelSelector: 'static { + /// Lists all available language models for this agent. + /// + /// # Parameters + /// - `cx`: The GPUI app context for async operations and global access. + /// + /// # Returns + /// A task resolving to the list of models or an error (e.g., if no models are configured). + fn list_models(&self, cx: &mut App) -> Task>; + + /// Selects a model for a specific session (thread). + /// + /// This sets the default model for future interactions in the session. + /// If the session doesn't exist or the model is invalid, it returns an error. + /// + /// # Parameters + /// - `session_id`: The ID of the session (thread) to apply the model to. + /// - `model`: The model to select (should be one from [list_models]). + /// - `cx`: The GPUI app context. + /// + /// # Returns + /// A task resolving to `Ok(())` on success or an error. + fn select_model( + &self, + session_id: acp::SessionId, + model_id: AgentModelId, + cx: &mut App, + ) -> Task>; + + /// Retrieves the currently selected model for a specific session (thread). + /// + /// # Parameters + /// - `session_id`: The ID of the session (thread) to query. + /// - `cx`: The GPUI app context. + /// + /// # Returns + /// A task resolving to the selected model (always set) or an error (e.g., session not found). + fn selected_model( + &self, + session_id: &acp::SessionId, + cx: &mut App, + ) -> Task>; + + /// Whenever the model list is updated the receiver will be notified. + fn watch(&self, cx: &mut App) -> watch::Receiver<()>; +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct AgentModelId(pub SharedString); + +impl std::ops::Deref for AgentModelId { + type Target = SharedString; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl fmt::Display for AgentModelId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AgentModelInfo { + pub id: AgentModelId, + pub name: SharedString, + pub icon: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct AgentModelGroupName(pub SharedString); + +#[derive(Debug, Clone)] +pub enum AgentModelList { + Flat(Vec), + Grouped(IndexMap>), +} + +impl AgentModelList { + pub fn is_empty(&self) -> bool { + match self { + AgentModelList::Flat(models) => models.is_empty(), + AgentModelList::Grouped(groups) => groups.is_empty(), + } + } +} diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 7439b2a088..3ddd7be793 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -4,18 +4,22 @@ use crate::{ FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MessageContent, MovePathTool, NowTool, OpenTool, ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, WebSearchTool, }; -use acp_thread::ModelSelector; +use acp_thread::AgentModelSelector; use agent_client_protocol as acp; +use agent_settings::AgentSettings; use anyhow::{Context as _, Result, anyhow}; +use collections::{HashSet, IndexMap}; +use fs::Fs; use futures::{StreamExt, future}; use gpui::{ App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity, }; -use language_model::{LanguageModel, LanguageModelRegistry}; +use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry}; use project::{Project, ProjectItem, ProjectPath, Worktree}; use prompt_store::{ ProjectContext, PromptId, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext, }; +use settings::update_settings_file; use std::cell::RefCell; use std::collections::HashMap; use std::path::Path; @@ -48,6 +52,104 @@ struct Session { _subscription: Subscription, } +pub struct LanguageModels { + /// Access language model by ID + models: HashMap>, + /// Cached list for returning language model information + model_list: acp_thread::AgentModelList, + refresh_models_rx: watch::Receiver<()>, + refresh_models_tx: watch::Sender<()>, +} + +impl LanguageModels { + fn new(cx: &App) -> Self { + let (refresh_models_tx, refresh_models_rx) = watch::channel(()); + let mut this = Self { + models: HashMap::default(), + model_list: acp_thread::AgentModelList::Grouped(IndexMap::default()), + refresh_models_rx, + refresh_models_tx, + }; + this.refresh_list(cx); + this + } + + fn refresh_list(&mut self, cx: &App) { + let providers = LanguageModelRegistry::global(cx) + .read(cx) + .providers() + .into_iter() + .filter(|provider| provider.is_authenticated(cx)) + .collect::>(); + + let mut language_model_list = IndexMap::default(); + let mut recommended_models = HashSet::default(); + + let mut recommended = Vec::new(); + for provider in &providers { + for model in provider.recommended_models(cx) { + recommended_models.insert(model.id()); + recommended.push(Self::map_language_model_to_info(&model, &provider)); + } + } + if !recommended.is_empty() { + language_model_list.insert( + acp_thread::AgentModelGroupName("Recommended".into()), + recommended, + ); + } + + let mut models = HashMap::default(); + for provider in providers { + let mut provider_models = Vec::new(); + for model in provider.provided_models(cx) { + let model_info = Self::map_language_model_to_info(&model, &provider); + let model_id = model_info.id.clone(); + if !recommended_models.contains(&model.id()) { + provider_models.push(model_info); + } + models.insert(model_id, model); + } + if !provider_models.is_empty() { + language_model_list.insert( + acp_thread::AgentModelGroupName(provider.name().0.clone()), + provider_models, + ); + } + } + + self.models = models; + self.model_list = acp_thread::AgentModelList::Grouped(language_model_list); + self.refresh_models_tx.send(()).ok(); + } + + fn watch(&self) -> watch::Receiver<()> { + self.refresh_models_rx.clone() + } + + pub fn model_from_id( + &self, + model_id: &acp_thread::AgentModelId, + ) -> Option> { + self.models.get(model_id).cloned() + } + + fn map_language_model_to_info( + model: &Arc, + provider: &Arc, + ) -> acp_thread::AgentModelInfo { + acp_thread::AgentModelInfo { + id: Self::model_id(model), + name: model.name().0, + icon: Some(provider.icon()), + } + } + + fn model_id(model: &Arc) -> acp_thread::AgentModelId { + acp_thread::AgentModelId(format!("{}/{}", model.provider_id().0, model.id().0).into()) + } +} + pub struct NativeAgent { /// Session ID -> Session mapping sessions: HashMap, @@ -58,8 +160,11 @@ pub struct NativeAgent { context_server_registry: Entity, /// Shared templates for all threads templates: Arc, + /// Cached model information + models: LanguageModels, project: Entity, prompt_store: Option>, + fs: Arc, _subscriptions: Vec, } @@ -68,6 +173,7 @@ impl NativeAgent { project: Entity, templates: Arc, prompt_store: Option>, + fs: Arc, cx: &mut AsyncApp, ) -> Result> { log::info!("Creating new NativeAgent"); @@ -77,7 +183,13 @@ impl NativeAgent { .await; cx.new(|cx| { - let mut subscriptions = vec![cx.subscribe(&project, Self::handle_project_event)]; + let mut subscriptions = vec![ + cx.subscribe(&project, Self::handle_project_event), + cx.subscribe( + &LanguageModelRegistry::global(cx), + Self::handle_models_updated_event, + ), + ]; if let Some(prompt_store) = prompt_store.as_ref() { subscriptions.push(cx.subscribe(prompt_store, Self::handle_prompts_updated_event)) } @@ -95,13 +207,19 @@ impl NativeAgent { ContextServerRegistry::new(project.read(cx).context_server_store(), cx) }), templates, + models: LanguageModels::new(cx), project, prompt_store, + fs, _subscriptions: subscriptions, } }) } + pub fn models(&self) -> &LanguageModels { + &self.models + } + async fn maintain_project_context( this: WeakEntity, mut needs_refresh: watch::Receiver<()>, @@ -297,75 +415,104 @@ impl NativeAgent { ) { self.project_context_needs_refresh.send(()).ok(); } + + fn handle_models_updated_event( + &mut self, + _registry: Entity, + _event: &language_model::Event, + cx: &mut Context, + ) { + self.models.refresh_list(cx); + for session in self.sessions.values_mut() { + session.thread.update(cx, |thread, _| { + let model_id = LanguageModels::model_id(&thread.selected_model); + if let Some(model) = self.models.model_from_id(&model_id) { + thread.selected_model = model.clone(); + } + }); + } + } } /// Wrapper struct that implements the AgentConnection trait #[derive(Clone)] pub struct NativeAgentConnection(pub Entity); -impl ModelSelector for NativeAgentConnection { - fn list_models(&self, cx: &mut AsyncApp) -> Task>>> { +impl AgentModelSelector for NativeAgentConnection { + fn list_models(&self, cx: &mut App) -> Task> { log::debug!("NativeAgentConnection::list_models called"); - cx.spawn(async move |cx| { - cx.update(|cx| { - let registry = LanguageModelRegistry::read_global(cx); - let models = registry.available_models(cx).collect::>(); - log::info!("Found {} available models", models.len()); - if models.is_empty() { - Err(anyhow::anyhow!("No models available")) - } else { - Ok(models) - } - })? + let list = self.0.read(cx).models.model_list.clone(); + Task::ready(if list.is_empty() { + Err(anyhow::anyhow!("No models available")) + } else { + Ok(list) }) } fn select_model( &self, session_id: acp::SessionId, - model: Arc, - cx: &mut AsyncApp, + model_id: acp_thread::AgentModelId, + cx: &mut App, ) -> Task> { - log::info!( - "Setting model for session {}: {:?}", - session_id, - model.name() - ); - let agent = self.0.clone(); + log::info!("Setting model for session {}: {}", session_id, model_id); + let Some(thread) = self + .0 + .read(cx) + .sessions + .get(&session_id) + .map(|session| session.thread.clone()) + else { + return Task::ready(Err(anyhow!("Session not found"))); + }; - cx.spawn(async move |cx| { - agent.update(cx, |agent, cx| { - if let Some(session) = agent.sessions.get(&session_id) { - session.thread.update(cx, |thread, _cx| { - thread.selected_model = model; - }); - Ok(()) - } else { - Err(anyhow!("Session not found")) - } - })? - }) + let Some(model) = self.0.read(cx).models.model_from_id(&model_id) else { + return Task::ready(Err(anyhow!("Invalid model ID {}", model_id))); + }; + + thread.update(cx, |thread, _cx| { + thread.selected_model = model.clone(); + }); + + update_settings_file::( + self.0.read(cx).fs.clone(), + cx, + move |settings, _cx| { + settings.set_model(model); + }, + ); + + Task::ready(Ok(())) } fn selected_model( &self, session_id: &acp::SessionId, - cx: &mut AsyncApp, - ) -> Task>> { - let agent = self.0.clone(); + cx: &mut App, + ) -> Task> { let session_id = session_id.clone(); - cx.spawn(async move |cx| { - let thread = agent - .read_with(cx, |agent, _| { - agent - .sessions - .get(&session_id) - .map(|session| session.thread.clone()) - })? - .ok_or_else(|| anyhow::anyhow!("Session not found"))?; - let selected = thread.read_with(cx, |thread, _| thread.selected_model.clone())?; - Ok(selected) - }) + + let Some(thread) = self + .0 + .read(cx) + .sessions + .get(&session_id) + .map(|session| session.thread.clone()) + else { + return Task::ready(Err(anyhow!("Session not found"))); + }; + let model = thread.read(cx).selected_model.clone(); + let Some(provider) = LanguageModelRegistry::read_global(cx).provider(&model.provider_id()) + else { + return Task::ready(Err(anyhow!("Provider not found"))); + }; + Task::ready(Ok(LanguageModels::map_language_model_to_info( + &model, &provider, + ))) + } + + fn watch(&self, cx: &mut App) -> watch::Receiver<()> { + self.0.read(cx).models.watch() } } @@ -413,13 +560,10 @@ impl acp_thread::AgentConnection for NativeAgentConnection { let default_model = registry .default_model() - .map(|configured| { - log::info!( - "Using configured default model: {:?} from provider: {:?}", - configured.model.name(), - configured.provider.name() - ); - configured.model + .and_then(|default_model| { + agent + .models + .model_from_id(&LanguageModels::model_id(&default_model.model)) }) .ok_or_else(|| { log::warn!("No default model configured in settings"); @@ -487,8 +631,8 @@ impl acp_thread::AgentConnection for NativeAgentConnection { Task::ready(Ok(())) } - fn model_selector(&self) -> Option> { - Some(Rc::new(self.clone()) as Rc) + fn model_selector(&self) -> Option> { + Some(Rc::new(self.clone()) as Rc) } fn prompt( @@ -629,6 +773,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { #[cfg(test)] mod tests { use super::*; + use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelId, AgentModelInfo}; use fs::FakeFs; use gpui::TestAppContext; use serde_json::json; @@ -646,9 +791,15 @@ mod tests { ) .await; let project = Project::test(fs.clone(), [], cx).await; - let agent = NativeAgent::new(project.clone(), Templates::new(), None, &mut cx.to_async()) - .await - .unwrap(); + let agent = NativeAgent::new( + project.clone(), + Templates::new(), + None, + fs.clone(), + &mut cx.to_async(), + ) + .await + .unwrap(); agent.read_with(cx, |agent, _| { assert_eq!(agent.project_context.borrow().worktrees, vec![]) }); @@ -689,13 +840,131 @@ mod tests { }); } + #[gpui::test] + async fn test_listing_models(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/", json!({ "a": {} })).await; + let project = Project::test(fs.clone(), [], cx).await; + let connection = NativeAgentConnection( + NativeAgent::new( + project.clone(), + Templates::new(), + None, + fs.clone(), + &mut cx.to_async(), + ) + .await + .unwrap(), + ); + + let models = cx.update(|cx| connection.list_models(cx)).await.unwrap(); + + let acp_thread::AgentModelList::Grouped(models) = models else { + panic!("Unexpected model group"); + }; + assert_eq!( + models, + IndexMap::from_iter([( + AgentModelGroupName("Fake".into()), + vec![AgentModelInfo { + id: AgentModelId("fake/fake".into()), + name: "Fake".into(), + icon: Some(ui::IconName::ZedAssistant), + }] + )]) + ); + } + + #[gpui::test] + async fn test_model_selection_persists_to_settings(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + fs.create_dir(paths::settings_file().parent().unwrap()) + .await + .unwrap(); + fs.insert_file( + paths::settings_file(), + json!({ + "agent": { + "default_model": { + "provider": "foo", + "model": "bar" + } + } + }) + .to_string() + .into_bytes(), + ) + .await; + let project = Project::test(fs.clone(), [], cx).await; + + // Create the agent and connection + let agent = NativeAgent::new( + project.clone(), + Templates::new(), + None, + fs.clone(), + &mut cx.to_async(), + ) + .await + .unwrap(); + let connection = NativeAgentConnection(agent.clone()); + + // Create a thread/session + let acp_thread = cx + .update(|cx| { + Rc::new(connection.clone()).new_thread( + project.clone(), + Path::new("/a"), + &mut cx.to_async(), + ) + }) + .await + .unwrap(); + + let session_id = cx.update(|cx| acp_thread.read(cx).session_id().clone()); + + // Select a model + let model_id = AgentModelId("fake/fake".into()); + cx.update(|cx| connection.select_model(session_id.clone(), model_id.clone(), cx)) + .await + .unwrap(); + + // Verify the thread has the selected model + agent.read_with(cx, |agent, _| { + let session = agent.sessions.get(&session_id).unwrap(); + session.thread.read_with(cx, |thread, _| { + assert_eq!(thread.selected_model.id().0, "fake"); + }); + }); + + cx.run_until_parked(); + + // Verify settings file was updated + let settings_content = fs.load(paths::settings_file()).await.unwrap(); + let settings_json: serde_json::Value = serde_json::from_str(&settings_content).unwrap(); + + // Check that the agent settings contain the selected model + assert_eq!( + settings_json["agent"]["default_model"]["model"], + json!("fake") + ); + assert_eq!( + settings_json["agent"]["default_model"]["provider"], + json!("fake") + ); + } + fn init_test(cx: &mut TestAppContext) { env_logger::try_init().ok(); cx.update(|cx| { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); Project::init_settings(cx); + agent_settings::init(cx); language::init(cx); + LanguageModelRegistry::test(cx); }); } } diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index 58f6d37c54..cadd88a846 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -1,8 +1,8 @@ -use std::path::Path; -use std::rc::Rc; +use std::{path::Path, rc::Rc, sync::Arc}; use agent_servers::AgentServer; use anyhow::Result; +use fs::Fs; use gpui::{App, Entity, Task}; use project::Project; use prompt_store::PromptStore; @@ -10,7 +10,15 @@ use prompt_store::PromptStore; use crate::{NativeAgent, NativeAgentConnection, templates::Templates}; #[derive(Clone)] -pub struct NativeAgentServer; +pub struct NativeAgentServer { + fs: Arc, +} + +impl NativeAgentServer { + pub fn new(fs: Arc) -> Self { + Self { fs } + } +} impl AgentServer for NativeAgentServer { fn name(&self) -> &'static str { @@ -41,6 +49,7 @@ impl AgentServer for NativeAgentServer { _root_dir ); let project = project.clone(); + let fs = self.fs.clone(); let prompt_store = PromptStore::global(cx); cx.spawn(async move |cx| { log::debug!("Creating templates for native agent"); @@ -48,7 +57,7 @@ impl AgentServer for NativeAgentServer { let prompt_store = prompt_store.await?; log::debug!("Creating native agent entity"); - let agent = NativeAgent::new(project, templates, Some(prompt_store), cx).await?; + let agent = NativeAgent::new(project, templates, Some(prompt_store), fs, cx).await?; // Create the connection wrapper let connection = NativeAgentConnection(agent); diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 88cf92836b..b70fa56747 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1,6 +1,6 @@ use super::*; use crate::MessageContent; -use acp_thread::AgentConnection; +use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelList}; use action_log::ActionLog; use agent_client_protocol::{self as acp}; use agent_settings::AgentProfileId; @@ -686,13 +686,19 @@ async fn test_agent_connection(cx: &mut TestAppContext) { // Create a project for new_thread let fake_fs = cx.update(|cx| fs::FakeFs::new(cx.background_executor().clone())); fake_fs.insert_tree(path!("/test"), json!({})).await; - let project = Project::test(fake_fs, [Path::new("/test")], cx).await; + let project = Project::test(fake_fs.clone(), [Path::new("/test")], cx).await; let cwd = Path::new("/test"); // Create agent and connection - let agent = NativeAgent::new(project.clone(), templates.clone(), None, &mut cx.to_async()) - .await - .unwrap(); + let agent = NativeAgent::new( + project.clone(), + templates.clone(), + None, + fake_fs.clone(), + &mut cx.to_async(), + ) + .await + .unwrap(); let connection = NativeAgentConnection(agent.clone()); // Test model_selector returns Some @@ -705,22 +711,22 @@ async fn test_agent_connection(cx: &mut TestAppContext) { // Test list_models let listed_models = cx - .update(|cx| { - let mut async_cx = cx.to_async(); - selector.list_models(&mut async_cx) - }) + .update(|cx| selector.list_models(cx)) .await .expect("list_models should succeed"); + let AgentModelList::Grouped(listed_models) = listed_models else { + panic!("Unexpected model list type"); + }; assert!(!listed_models.is_empty(), "should have at least one model"); - assert_eq!(listed_models[0].id().0, "fake"); + assert_eq!( + listed_models[&AgentModelGroupName("Fake".into())][0].id.0, + "fake/fake" + ); // Create a thread using new_thread let connection_rc = Rc::new(connection.clone()); let acp_thread = cx - .update(|cx| { - let mut async_cx = cx.to_async(); - connection_rc.new_thread(project, cwd, &mut async_cx) - }) + .update(|cx| connection_rc.new_thread(project, cwd, &mut cx.to_async())) .await .expect("new_thread should succeed"); @@ -729,12 +735,12 @@ async fn test_agent_connection(cx: &mut TestAppContext) { // Test selected_model returns the default let model = cx - .update(|cx| { - let mut async_cx = cx.to_async(); - selector.selected_model(&session_id, &mut async_cx) - }) + .update(|cx| selector.selected_model(&session_id, cx)) .await .expect("selected_model should succeed"); + let model = cx + .update(|cx| agent.read(cx).models().model_from_id(&model.id)) + .unwrap(); let model = model.as_fake(); assert_eq!(model.id().0, "fake", "should return default model"); diff --git a/crates/agent_ui/src/acp.rs b/crates/agent_ui/src/acp.rs index cc476b1a86..b9814adb2d 100644 --- a/crates/agent_ui/src/acp.rs +++ b/crates/agent_ui/src/acp.rs @@ -1,6 +1,10 @@ mod completion_provider; mod message_history; +mod model_selector; +mod model_selector_popover; mod thread_view; pub use message_history::MessageHistory; +pub use model_selector::AcpModelSelector; +pub use model_selector_popover::AcpModelSelectorPopover; pub use thread_view::AcpThreadView; diff --git a/crates/agent_ui/src/acp/model_selector.rs b/crates/agent_ui/src/acp/model_selector.rs new file mode 100644 index 0000000000..563afee65f --- /dev/null +++ b/crates/agent_ui/src/acp/model_selector.rs @@ -0,0 +1,472 @@ +use std::{cmp::Reverse, rc::Rc, sync::Arc}; + +use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector}; +use agent_client_protocol as acp; +use anyhow::Result; +use collections::IndexMap; +use futures::FutureExt; +use fuzzy::{StringMatchCandidate, match_strings}; +use gpui::{Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, Task, WeakEntity}; +use ordered_float::OrderedFloat; +use picker::{Picker, PickerDelegate}; +use ui::{ + AnyElement, App, Context, IntoElement, ListItem, ListItemSpacing, SharedString, Window, + prelude::*, rems, +}; +use util::ResultExt; + +pub type AcpModelSelector = Picker; + +pub fn acp_model_selector( + session_id: acp::SessionId, + selector: Rc, + window: &mut Window, + cx: &mut Context, +) -> AcpModelSelector { + let delegate = AcpModelPickerDelegate::new(session_id, selector, window, cx); + Picker::list(delegate, window, cx) + .show_scrollbar(true) + .width(rems(20.)) + .max_height(Some(rems(20.).into())) +} + +enum AcpModelPickerEntry { + Separator(SharedString), + Model(AgentModelInfo), +} + +pub struct AcpModelPickerDelegate { + session_id: acp::SessionId, + selector: Rc, + filtered_entries: Vec, + models: Option, + selected_index: usize, + selected_model: Option, + _refresh_models_task: Task<()>, +} + +impl AcpModelPickerDelegate { + fn new( + session_id: acp::SessionId, + selector: Rc, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let mut rx = selector.watch(cx); + let refresh_models_task = cx.spawn_in(window, { + let session_id = session_id.clone(); + async move |this, cx| { + async fn refresh( + this: &WeakEntity>, + session_id: &acp::SessionId, + cx: &mut AsyncWindowContext, + ) -> Result<()> { + let (models_task, selected_model_task) = this.update(cx, |this, cx| { + ( + this.delegate.selector.list_models(cx), + this.delegate.selector.selected_model(session_id, cx), + ) + })?; + + let (models, selected_model) = futures::join!(models_task, selected_model_task); + + this.update_in(cx, |this, window, cx| { + this.delegate.models = models.ok(); + this.delegate.selected_model = selected_model.ok(); + this.delegate.update_matches(this.query(cx), window, cx) + })? + .await; + + Ok(()) + } + + refresh(&this, &session_id, cx).await.log_err(); + while let Ok(()) = rx.recv().await { + refresh(&this, &session_id, cx).await.log_err(); + } + } + }); + + Self { + session_id, + selector, + filtered_entries: Vec::new(), + models: None, + selected_model: None, + selected_index: 0, + _refresh_models_task: refresh_models_task, + } + } + + pub fn active_model(&self) -> Option<&AgentModelInfo> { + self.selected_model.as_ref() + } +} + +impl PickerDelegate for AcpModelPickerDelegate { + type ListItem = AnyElement; + + fn match_count(&self) -> usize { + self.filtered_entries.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context>) { + self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1)); + cx.notify(); + } + + fn can_select( + &mut self, + ix: usize, + _window: &mut Window, + _cx: &mut Context>, + ) -> bool { + match self.filtered_entries.get(ix) { + Some(AcpModelPickerEntry::Model(_)) => true, + Some(AcpModelPickerEntry::Separator(_)) | None => false, + } + } + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { + "Select a model…".into() + } + + fn update_matches( + &mut self, + query: String, + window: &mut Window, + cx: &mut Context>, + ) -> Task<()> { + cx.spawn_in(window, async move |this, cx| { + let filtered_models = match this + .read_with(cx, |this, cx| { + this.delegate.models.clone().map(move |models| { + fuzzy_search(models, query, cx.background_executor().clone()) + }) + }) + .ok() + .flatten() + { + Some(task) => task.await, + None => AgentModelList::Flat(vec![]), + }; + + this.update_in(cx, |this, window, cx| { + this.delegate.filtered_entries = + info_list_to_picker_entries(filtered_models).collect(); + // Finds the currently selected model in the list + let new_index = this + .delegate + .selected_model + .as_ref() + .and_then(|selected| { + this.delegate.filtered_entries.iter().position(|entry| { + if let AcpModelPickerEntry::Model(model_info) = entry { + model_info.id == selected.id + } else { + false + } + }) + }) + .unwrap_or(0); + this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx); + cx.notify(); + }) + .ok(); + }) + } + + fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { + if let Some(AcpModelPickerEntry::Model(model_info)) = + self.filtered_entries.get(self.selected_index) + { + self.selector + .select_model(self.session_id.clone(), model_info.id.clone(), cx) + .detach_and_log_err(cx); + self.selected_model = Some(model_info.clone()); + let current_index = self.selected_index; + self.set_selected_index(current_index, window, cx); + + cx.emit(DismissEvent); + } + } + + fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { + cx.emit(DismissEvent); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _: &mut Window, + cx: &mut Context>, + ) -> Option { + match self.filtered_entries.get(ix)? { + AcpModelPickerEntry::Separator(title) => Some( + div() + .px_2() + .pb_1() + .when(ix > 1, |this| { + this.mt_1() + .pt_2() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + }) + .child( + Label::new(title) + .size(LabelSize::XSmall) + .color(Color::Muted), + ) + .into_any_element(), + ), + AcpModelPickerEntry::Model(model_info) => { + let is_selected = Some(model_info) == self.selected_model.as_ref(); + + let model_icon_color = if is_selected { + Color::Accent + } else { + Color::Muted + }; + + Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .toggle_state(selected) + .start_slot::(model_info.icon.map(|icon| { + Icon::new(icon) + .color(model_icon_color) + .size(IconSize::Small) + })) + .child( + h_flex() + .w_full() + .pl_0p5() + .gap_1p5() + .w(px(240.)) + .child(Label::new(model_info.name.clone()).truncate()), + ) + .end_slot(div().pr_3().when(is_selected, |this| { + this.child( + Icon::new(IconName::Check) + .color(Color::Accent) + .size(IconSize::Small), + ) + })) + .into_any_element(), + ) + } + } + } + + fn render_footer( + &self, + _: &mut Window, + cx: &mut Context>, + ) -> Option { + Some( + h_flex() + .w_full() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .p_1() + .gap_4() + .justify_between() + .child( + Button::new("configure", "Configure") + .icon(IconName::Settings) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .icon_position(IconPosition::Start) + .on_click(|_, window, cx| { + window.dispatch_action( + zed_actions::agent::OpenSettings.boxed_clone(), + cx, + ); + }), + ) + .into_any(), + ) + } +} + +fn info_list_to_picker_entries( + model_list: AgentModelList, +) -> impl Iterator { + match model_list { + AgentModelList::Flat(list) => { + itertools::Either::Left(list.into_iter().map(AcpModelPickerEntry::Model)) + } + AgentModelList::Grouped(index_map) => { + itertools::Either::Right(index_map.into_iter().flat_map(|(group_name, models)| { + std::iter::once(AcpModelPickerEntry::Separator(group_name.0)) + .chain(models.into_iter().map(AcpModelPickerEntry::Model)) + })) + } + } +} + +async fn fuzzy_search( + model_list: AgentModelList, + query: String, + executor: BackgroundExecutor, +) -> AgentModelList { + async fn fuzzy_search_list( + model_list: Vec, + query: &str, + executor: BackgroundExecutor, + ) -> Vec { + let candidates = model_list + .iter() + .enumerate() + .map(|(ix, model)| { + StringMatchCandidate::new(ix, &format!("{}/{}", model.id, model.name)) + }) + .collect::>(); + let mut matches = match_strings( + &candidates, + &query, + false, + true, + 100, + &Default::default(), + executor, + ) + .await; + + matches.sort_unstable_by_key(|mat| { + let candidate = &candidates[mat.candidate_id]; + (Reverse(OrderedFloat(mat.score)), candidate.id) + }); + + matches + .into_iter() + .map(|mat| model_list[mat.candidate_id].clone()) + .collect() + } + + match model_list { + AgentModelList::Flat(model_list) => { + AgentModelList::Flat(fuzzy_search_list(model_list, &query, executor).await) + } + AgentModelList::Grouped(index_map) => { + let groups = + futures::future::join_all(index_map.into_iter().map(|(group_name, models)| { + fuzzy_search_list(models, &query, executor.clone()) + .map(|results| (group_name, results)) + })) + .await; + AgentModelList::Grouped(IndexMap::from_iter( + groups + .into_iter() + .filter(|(_, results)| !results.is_empty()), + )) + } + } +} + +#[cfg(test)] +mod tests { + use gpui::TestAppContext; + + use super::*; + + fn create_model_list(grouped_models: Vec<(&str, Vec<&str>)>) -> AgentModelList { + AgentModelList::Grouped(IndexMap::from_iter(grouped_models.into_iter().map( + |(group, models)| { + ( + acp_thread::AgentModelGroupName(group.to_string().into()), + models + .into_iter() + .map(|model| acp_thread::AgentModelInfo { + id: acp_thread::AgentModelId(model.to_string().into()), + name: model.to_string().into(), + icon: None, + }) + .collect::>(), + ) + }, + ))) + } + + fn assert_models_eq(result: AgentModelList, expected: Vec<(&str, Vec<&str>)>) { + let AgentModelList::Grouped(groups) = result else { + panic!("Expected LanguageModelInfoList::Grouped, got {:?}", result); + }; + + assert_eq!( + groups.len(), + expected.len(), + "Number of groups doesn't match" + ); + + for (i, (expected_group, expected_models)) in expected.iter().enumerate() { + let (actual_group, actual_models) = groups.get_index(i).unwrap(); + assert_eq!( + actual_group.0.as_ref(), + *expected_group, + "Group at position {} doesn't match expected group", + i + ); + assert_eq!( + actual_models.len(), + expected_models.len(), + "Number of models in group {} doesn't match", + expected_group + ); + + for (j, expected_model_name) in expected_models.iter().enumerate() { + assert_eq!( + actual_models[j].name, *expected_model_name, + "Model at position {} in group {} doesn't match expected model", + j, expected_group + ); + } + } + } + + #[gpui::test] + async fn test_fuzzy_match(cx: &mut TestAppContext) { + let models = create_model_list(vec![ + ( + "zed", + vec![ + "Claude 3.7 Sonnet", + "Claude 3.7 Sonnet Thinking", + "gpt-4.1", + "gpt-4.1-nano", + ], + ), + ("openai", vec!["gpt-3.5-turbo", "gpt-4.1", "gpt-4.1-nano"]), + ("ollama", vec!["mistral", "deepseek"]), + ]); + + // Results should preserve models order whenever possible. + // In the case below, `zed/gpt-4.1` and `openai/gpt-4.1` have identical + // similarity scores, but `zed/gpt-4.1` was higher in the models list, + // so it should appear first in the results. + let results = fuzzy_search(models.clone(), "41".into(), cx.executor()).await; + assert_models_eq( + results, + vec![ + ("zed", vec!["gpt-4.1", "gpt-4.1-nano"]), + ("openai", vec!["gpt-4.1", "gpt-4.1-nano"]), + ], + ); + + // Fuzzy search + let results = fuzzy_search(models.clone(), "4n".into(), cx.executor()).await; + assert_models_eq( + results, + vec![ + ("zed", vec!["gpt-4.1-nano"]), + ("openai", vec!["gpt-4.1-nano"]), + ], + ); + } +} diff --git a/crates/agent_ui/src/acp/model_selector_popover.rs b/crates/agent_ui/src/acp/model_selector_popover.rs new file mode 100644 index 0000000000..e52101113a --- /dev/null +++ b/crates/agent_ui/src/acp/model_selector_popover.rs @@ -0,0 +1,85 @@ +use std::rc::Rc; + +use acp_thread::AgentModelSelector; +use agent_client_protocol as acp; +use gpui::{Entity, FocusHandle}; +use picker::popover_menu::PickerPopoverMenu; +use ui::{ + ButtonLike, Context, IntoElement, PopoverMenuHandle, SharedString, Tooltip, Window, prelude::*, +}; +use zed_actions::agent::ToggleModelSelector; + +use crate::acp::{AcpModelSelector, model_selector::acp_model_selector}; + +pub struct AcpModelSelectorPopover { + selector: Entity, + menu_handle: PopoverMenuHandle, + focus_handle: FocusHandle, +} + +impl AcpModelSelectorPopover { + pub(crate) fn new( + session_id: acp::SessionId, + selector: Rc, + menu_handle: PopoverMenuHandle, + focus_handle: FocusHandle, + window: &mut Window, + cx: &mut Context, + ) -> Self { + Self { + selector: cx.new(move |cx| acp_model_selector(session_id, selector, window, cx)), + menu_handle, + focus_handle, + } + } + + pub fn toggle(&self, window: &mut Window, cx: &mut Context) { + self.menu_handle.toggle(window, cx); + } +} + +impl Render for AcpModelSelectorPopover { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let model = self.selector.read(cx).delegate.active_model(); + let model_name = model + .as_ref() + .map(|model| model.name.clone()) + .unwrap_or_else(|| SharedString::from("Select a Model")); + + let model_icon = model.as_ref().and_then(|model| model.icon); + + let focus_handle = self.focus_handle.clone(); + + PickerPopoverMenu::new( + self.selector.clone(), + ButtonLike::new("active-model") + .when_some(model_icon, |this, icon| { + this.child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall)) + }) + .child( + Label::new(model_name) + .color(Color::Muted) + .size(LabelSize::Small) + .ml_0p5(), + ) + .child( + Icon::new(IconName::ChevronDown) + .color(Color::Muted) + .size(IconSize::XSmall), + ), + move |window, cx| { + Tooltip::for_action_in( + "Change Model", + &ToggleModelSelector, + &focus_handle, + window, + cx, + ) + }, + gpui::Corner::BottomRight, + cx, + ) + .with_handle(self.menu_handle.clone()) + .render(window, cx) + } +} diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index da7915222e..12fc29b08f 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -38,12 +38,14 @@ use terminal_view::TerminalView; use text::{Anchor, BufferSnapshot}; use theme::ThemeSettings; use ui::{ - Disclosure, Divider, DividerColor, KeyBinding, Scrollbar, ScrollbarState, Tooltip, prelude::*, + Disclosure, Divider, DividerColor, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, + Tooltip, prelude::*, }; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, Workspace}; -use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage}; +use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage, ToggleModelSelector}; +use crate::acp::AcpModelSelectorPopover; use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet}; use crate::acp::message_history::MessageHistory; use crate::agent_diff::AgentDiff; @@ -63,6 +65,7 @@ pub struct AcpThreadView { diff_editors: HashMap>, terminal_views: HashMap>, message_editor: Entity, + model_selector: Option>, message_set_from_history: Option, _message_editor_subscription: Subscription, mention_set: Arc>, @@ -187,6 +190,7 @@ impl AcpThreadView { project: project.clone(), thread_state: Self::initial_state(agent, workspace, project, window, cx), message_editor, + model_selector: None, message_set_from_history: None, _message_editor_subscription: message_editor_subscription, mention_set, @@ -270,7 +274,7 @@ impl AcpThreadView { Err(e) } } - Ok(session_id) => Ok(session_id), + Ok(thread) => Ok(thread), }; this.update_in(cx, |this, window, cx| { @@ -288,6 +292,24 @@ impl AcpThreadView { AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx); + this.model_selector = + thread + .read(cx) + .connection() + .model_selector() + .map(|selector| { + cx.new(|cx| { + AcpModelSelectorPopover::new( + thread.read(cx).session_id().clone(), + selector, + PopoverMenuHandle::default(), + this.focus_handle(cx), + window, + cx, + ) + }) + }); + this.thread_state = ThreadState::Ready { thread, _subscription: [thread_subscription, action_log_subscription], @@ -2472,6 +2494,12 @@ impl AcpThreadView { v_flex() .on_action(cx.listener(Self::expand_message_editor)) + .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| { + if let Some(model_selector) = this.model_selector.as_ref() { + model_selector + .update(cx, |model_selector, cx| model_selector.toggle(window, cx)); + } + })) .p_2() .gap_2() .border_t_1() @@ -2548,7 +2576,12 @@ impl AcpThreadView { .flex_none() .justify_between() .child(self.render_follow_toggle(cx)) - .child(self.render_send_button(cx)), + .child( + h_flex() + .gap_1() + .children(self.model_selector.clone()) + .child(self.render_send_button(cx)), + ), ) .into_any() } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 87e4dd822c..d07581da93 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -916,6 +916,7 @@ impl AgentPanel { let workspace = self.workspace.clone(); let project = self.project.clone(); let message_history = self.acp_message_history.clone(); + let fs = self.fs.clone(); const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent"; @@ -939,7 +940,7 @@ impl AgentPanel { }) .detach(); - agent.server() + agent.server(fs) } None => cx .background_spawn(async move { @@ -953,7 +954,7 @@ impl AgentPanel { }) .unwrap_or_default() .agent - .server(), + .server(fs), }; this.update_in(cx, |this, window, cx| { diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index fceb8f4c45..b776c0830b 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -155,11 +155,11 @@ enum ExternalAgent { } impl ExternalAgent { - pub fn server(&self) -> Rc { + pub fn server(&self, fs: Arc) -> Rc { match self { ExternalAgent::Gemini => Rc::new(agent_servers::Gemini), ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode), - ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer), + ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs)), } } } From 81474a3de01b0d5dd2e68bb30e07e50de722a180 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Wed, 13 Aug 2025 13:17:03 +0200 Subject: [PATCH 305/693] Change default pane split directions (#36101) Closes #32538 This PR adjusts the defaults for splitting panes along the horizontal and vertical actions. Based upon user feedback, the adjusted values seem more reasonable as default settings, hence, go with these instead. Release Notes: - Changed the default split directions for the `pane: split horizontal` and `pane: split vertical` actions. You can restore the old behavior by modifying the `pane_split_direction_horizontal` and `pane_split_direction_vertical` values in your settings. --- assets/settings/default.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 28cf591ee7..0f1818ac7f 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -82,10 +82,10 @@ // Layout mode of the bottom dock. Defaults to "contained" // choices: contained, full, left_aligned, right_aligned "bottom_dock_layout": "contained", - // The direction that you want to split panes horizontally. Defaults to "up" - "pane_split_direction_horizontal": "up", - // The direction that you want to split panes vertically. Defaults to "left" - "pane_split_direction_vertical": "left", + // The direction that you want to split panes horizontally. Defaults to "down" + "pane_split_direction_horizontal": "down", + // The direction that you want to split panes vertically. Defaults to "right" + "pane_split_direction_vertical": "right", // Centered layout related settings. "centered_layout": { // The relative width of the left padding of the central pane from the From 8d63312ecafca3c701a0d3f90da64043127ee3d9 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 13 Aug 2025 14:29:53 +0300 Subject: [PATCH 306/693] Small worktree scan style fixes (#36104) Part of https://github.com/zed-industries/zed/issues/35780 Release Notes: - N/A --- crates/fs/src/fake_git_repo.rs | 9 +++++--- crates/git/src/repository.rs | 42 ++++++++++++++++++---------------- crates/zed/src/main.rs | 10 +++++++- 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 73da63fd47..21b9cbca9a 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -10,7 +10,7 @@ use git::{ }, status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus}, }; -use gpui::{AsyncApp, BackgroundExecutor, SharedString}; +use gpui::{AsyncApp, BackgroundExecutor, SharedString, Task}; use ignore::gitignore::GitignoreBuilder; use rope::Rope; use smol::future::FutureExt as _; @@ -183,7 +183,7 @@ impl GitRepository for FakeGitRepository { async move { None }.boxed() } - fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result> { + fn status(&self, path_prefixes: &[RepoPath]) -> Task> { let workdir_path = self.dot_git_path.parent().unwrap(); // Load gitignores @@ -311,7 +311,10 @@ impl GitRepository for FakeGitRepository { entries: entries.into(), }) }); - async move { result? }.boxed() + Task::ready(match result { + Ok(result) => result, + Err(e) => Err(e), + }) } fn branches(&self) -> BoxFuture<'_, Result>> { diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 518b6c4f46..49eee84840 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -6,7 +6,7 @@ use collections::HashMap; use futures::future::BoxFuture; use futures::{AsyncWriteExt, FutureExt as _, select_biased}; use git2::BranchType; -use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, SharedString}; +use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, SharedString, Task}; use parking_lot::Mutex; use rope::Rope; use schemars::JsonSchema; @@ -338,7 +338,7 @@ pub trait GitRepository: Send + Sync { fn merge_message(&self) -> BoxFuture<'_, Option>; - fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result>; + fn status(&self, path_prefixes: &[RepoPath]) -> Task>; fn branches(&self) -> BoxFuture<'_, Result>>; @@ -953,25 +953,27 @@ impl GitRepository for RealGitRepository { .boxed() } - fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result> { + fn status(&self, path_prefixes: &[RepoPath]) -> Task> { let git_binary_path = self.git_binary_path.clone(); - let working_directory = self.working_directory(); - let path_prefixes = path_prefixes.to_owned(); - self.executor - .spawn(async move { - let output = new_std_command(&git_binary_path) - .current_dir(working_directory?) - .args(git_status_args(&path_prefixes)) - .output()?; - if output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - stdout.parse() - } else { - let stderr = String::from_utf8_lossy(&output.stderr); - anyhow::bail!("git status failed: {stderr}"); - } - }) - .boxed() + let working_directory = match self.working_directory() { + Ok(working_directory) => working_directory, + Err(e) => return Task::ready(Err(e)), + }; + let args = git_status_args(&path_prefixes); + log::debug!("Checking for git status in {path_prefixes:?}"); + self.executor.spawn(async move { + let output = new_std_command(&git_binary_path) + .current_dir(working_directory) + .args(args) + .output()?; + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + stdout.parse() + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + anyhow::bail!("git status failed: {stderr}"); + } + }) } fn branches(&self) -> BoxFuture<'_, Result>> { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 457372b4af..3084bfddad 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -251,7 +251,15 @@ pub fn main() { return; } - log::info!("========== starting zed =========="); + log::info!( + "========== starting zed version {}, sha {} ==========", + app_version, + app_commit_sha + .as_ref() + .map(|sha| sha.short()) + .as_deref() + .unwrap_or("unknown"), + ); let app = Application::new().with_assets(Assets); From 6307105976cc7b33abbd32fc5e1e413c252a3eed Mon Sep 17 00:00:00 2001 From: localcc Date: Wed, 13 Aug 2025 13:58:09 +0200 Subject: [PATCH 307/693] Don't show default shell breadcrumbs (#36070) Release Notes: - N/A --- crates/terminal/src/terminal.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 3e7d9c0ad4..c3c6de9e53 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -408,7 +408,13 @@ impl TerminalBuilder { let terminal_title_override = shell_params.as_ref().and_then(|e| e.title_override.clone()); #[cfg(windows)] - let shell_program = shell_params.as_ref().map(|params| params.program.clone()); + let shell_program = shell_params.as_ref().map(|params| { + use util::ResultExt; + + Self::resolve_path(¶ms.program) + .log_err() + .unwrap_or(params.program.clone()) + }); let pty_options = { let alac_shell = shell_params.map(|params| { @@ -589,6 +595,24 @@ impl TerminalBuilder { self.terminal } + + #[cfg(windows)] + fn resolve_path(path: &str) -> Result { + use windows::Win32::Storage::FileSystem::SearchPathW; + use windows::core::HSTRING; + + let path = if path.starts_with(r"\\?\") || !path.contains(&['/', '\\']) { + path.to_string() + } else { + r"\\?\".to_string() + path + }; + + let required_length = unsafe { SearchPathW(None, &HSTRING::from(&path), None, None, None) }; + let mut buf = vec![0u16; required_length as usize]; + let size = unsafe { SearchPathW(None, &HSTRING::from(&path), None, Some(&mut buf), None) }; + + Ok(String::from_utf16(&buf[..size as usize])?) + } } #[derive(Debug, Clone, Deserialize, Serialize)] From 7f1a5c6ad774650554e756e54a81c5179d600e6c Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Wed, 13 Aug 2025 14:02:20 +0200 Subject: [PATCH 308/693] ui: Make toggle button group responsive (#36100) This PR improves the toggle button group to be more responsive across different layouts. This is accomplished by ensuring each button takes up the same amount of space in the parent containers layout. Ideally, this should be done with grids instead of a flexbox container, as this would be much better suited for this purpose. Yet, since we lack support for this, we go with this route for now. | Before | After | | --- | --- | | Bildschirmfoto 2025-08-13 um 11
24 26 | Bildschirmfoto 2025-08-13 um
11 29 36 | Release Notes: - N/A --- crates/editor/src/element.rs | 4 +- crates/onboarding/src/basics_page.rs | 4 +- crates/onboarding/src/editing_page.rs | 2 +- crates/repl/src/notebook/notebook_ui.rs | 2 +- crates/ui/src/components/button/button.rs | 2 +- .../ui/src/components/button/button_like.rs | 4 +- .../ui/src/components/button/icon_button.rs | 4 +- .../ui/src/components/button/toggle_button.rs | 60 ++++++++++++------- crates/ui/src/traits/fixed.rs | 2 +- .../zed/src/zed/quick_action_bar/repl_menu.rs | 2 +- 10 files changed, 50 insertions(+), 36 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a7fd0abf88..8a5c65f994 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -3011,7 +3011,7 @@ impl EditorElement { .icon_color(Color::Custom(cx.theme().colors().editor_line_number)) .selected_icon_color(Color::Custom(cx.theme().colors().editor_foreground)) .icon_size(IconSize::Custom(rems(editor_font_size / window.rem_size()))) - .width(width.into()) + .width(width) .on_click(move |_, window, cx| { editor.update(cx, |editor, cx| { editor.expand_excerpt(excerpt_id, direction, window, cx); @@ -3627,7 +3627,7 @@ impl EditorElement { ButtonLike::new("toggle-buffer-fold") .style(ui::ButtonStyle::Transparent) .height(px(28.).into()) - .width(px(28.).into()) + .width(px(28.)) .children(toggle_chevron_icon) .tooltip({ let focus_handle = focus_handle.clone(); diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index a19a21fddf..86ddc22a86 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -58,7 +58,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement .tab_index(tab_index) .selected_index(theme_mode as usize) .style(ui::ToggleButtonGroupStyle::Outlined) - .button_width(rems_from_px(64.)), + .width(rems_from_px(3. * 64.)), ), ) .child( @@ -305,8 +305,8 @@ fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoE .when_some(base_keymap, |this, base_keymap| { this.selected_index(base_keymap) }) + .full_width() .tab_index(tab_index) - .button_width(rems_from_px(216.)) .size(ui::ToggleButtonGroupSize::Medium) .style(ui::ToggleButtonGroupStyle::Outlined), ); diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index e8fbc36c30..c69bd3852e 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -706,7 +706,7 @@ fn render_popular_settings_section( }) .tab_index(tab_index) .style(ToggleButtonGroupStyle::Outlined) - .button_width(ui::rems_from_px(64.)), + .width(ui::rems_from_px(3. * 64.)), ), ) } diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index b53809dff0..36a0af30d0 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -295,7 +295,7 @@ impl NotebookEditor { _cx: &mut Context, ) -> IconButton { let id: ElementId = ElementId::Name(id.into()); - IconButton::new(id, icon).width(px(CONTROL_SIZE).into()) + IconButton::new(id, icon).width(px(CONTROL_SIZE)) } fn render_notebook_controls( diff --git a/crates/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs index 19f782fb98..cee39ac23b 100644 --- a/crates/ui/src/components/button/button.rs +++ b/crates/ui/src/components/button/button.rs @@ -324,7 +324,7 @@ impl FixedWidth for Button { /// ``` /// /// This sets the button's width to be exactly 100 pixels. - fn width(mut self, width: DefiniteLength) -> Self { + fn width(mut self, width: impl Into) -> Self { self.base = self.base.width(width); self } diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index 35c78fbb5d..0b30007e44 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -499,8 +499,8 @@ impl Clickable for ButtonLike { } impl FixedWidth for ButtonLike { - fn width(mut self, width: DefiniteLength) -> Self { - self.width = Some(width); + fn width(mut self, width: impl Into) -> Self { + self.width = Some(width.into()); self } diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index 8d8718a634..74fc4851fe 100644 --- a/crates/ui/src/components/button/icon_button.rs +++ b/crates/ui/src/components/button/icon_button.rs @@ -133,7 +133,7 @@ impl Clickable for IconButton { } impl FixedWidth for IconButton { - fn width(mut self, width: DefiniteLength) -> Self { + fn width(mut self, width: impl Into) -> Self { self.base = self.base.width(width); self } @@ -194,7 +194,7 @@ impl RenderOnce for IconButton { .map(|this| match self.shape { IconButtonShape::Square => { let size = self.icon_size.square(window, cx); - this.width(size.into()).height(size.into()) + this.width(size).height(size.into()) } IconButtonShape::Wide => this, }) diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs index 91defa730b..2a862f4876 100644 --- a/crates/ui/src/components/button/toggle_button.rs +++ b/crates/ui/src/components/button/toggle_button.rs @@ -1,6 +1,6 @@ use std::rc::Rc; -use gpui::{AnyView, ClickEvent}; +use gpui::{AnyView, ClickEvent, relative}; use crate::{ButtonLike, ButtonLikeRounding, ElevationIndex, TintColor, Tooltip, prelude::*}; @@ -73,8 +73,8 @@ impl SelectableButton for ToggleButton { } impl FixedWidth for ToggleButton { - fn width(mut self, width: DefiniteLength) -> Self { - self.base.width = Some(width); + fn width(mut self, width: impl Into) -> Self { + self.base.width = Some(width.into()); self } @@ -429,7 +429,7 @@ where rows: [[T; COLS]; ROWS], style: ToggleButtonGroupStyle, size: ToggleButtonGroupSize, - button_width: Rems, + group_width: Option, selected_index: usize, tab_index: Option, } @@ -441,7 +441,7 @@ impl ToggleButtonGroup { rows: [buttons], style: ToggleButtonGroupStyle::Transparent, size: ToggleButtonGroupSize::Default, - button_width: rems_from_px(100.), + group_width: None, selected_index: 0, tab_index: None, } @@ -455,7 +455,7 @@ impl ToggleButtonGroup { rows: [first_row, second_row], style: ToggleButtonGroupStyle::Transparent, size: ToggleButtonGroupSize::Default, - button_width: rems_from_px(100.), + group_width: None, selected_index: 0, tab_index: None, } @@ -473,11 +473,6 @@ impl ToggleButtonGroup Self { - self.button_width = button_width; - self - } - pub fn selected_index(mut self, index: usize) -> Self { self.selected_index = index; self @@ -491,6 +486,24 @@ impl ToggleButtonGroup DefiniteLength { + relative(1. / COLS as f32) + } +} + +impl FixedWidth + for ToggleButtonGroup +{ + fn width(mut self, width: impl Into) -> Self { + self.group_width = Some(width.into()); + self + } + + fn full_width(mut self) -> Self { + self.group_width = Some(relative(1.)); + self + } } impl RenderOnce @@ -511,6 +524,7 @@ impl RenderOnce let entry_index = row_index * COLS + col_index; ButtonLike::new((self.group_name, entry_index)) + .full_width() .rounding(None) .when_some(self.tab_index, |this, tab_index| { this.tab_index(tab_index + entry_index as isize) @@ -527,7 +541,7 @@ impl RenderOnce }) .child( h_flex() - .min_w(self.button_width) + .w_full() .gap_1p5() .px_3() .py_1() @@ -561,6 +575,13 @@ impl RenderOnce let is_transparent = self.style == ToggleButtonGroupStyle::Transparent; v_flex() + .map(|this| { + if let Some(width) = self.group_width { + this.w(width) + } else { + this.w_full() + } + }) .rounded_md() .overflow_hidden() .map(|this| { @@ -583,6 +604,8 @@ impl RenderOnce .when(is_outlined_or_filled && !last_item, |this| { this.border_r_1().border_color(border_color) }) + .w(Self::button_width()) + .overflow_hidden() .child(item) })) })) @@ -630,7 +653,6 @@ impl Component ], ) .selected_index(1) - .button_width(rems_from_px(100.)) .into_any_element(), ), single_example( @@ -656,7 +678,6 @@ impl Component ], ) .selected_index(1) - .button_width(rems_from_px(100.)) .into_any_element(), ), single_example( @@ -675,7 +696,6 @@ impl Component ], ) .selected_index(3) - .button_width(rems_from_px(100.)) .into_any_element(), ), single_example( @@ -718,7 +738,6 @@ impl Component ], ) .selected_index(3) - .button_width(rems_from_px(100.)) .into_any_element(), ), ], @@ -763,7 +782,6 @@ impl Component ], ) .selected_index(1) - .button_width(rems_from_px(100.)) .style(ToggleButtonGroupStyle::Outlined) .into_any_element(), ), @@ -783,7 +801,6 @@ impl Component ], ) .selected_index(3) - .button_width(rems_from_px(100.)) .style(ToggleButtonGroupStyle::Outlined) .into_any_element(), ), @@ -827,7 +844,6 @@ impl Component ], ) .selected_index(3) - .button_width(rems_from_px(100.)) .style(ToggleButtonGroupStyle::Outlined) .into_any_element(), ), @@ -873,7 +889,6 @@ impl Component ], ) .selected_index(1) - .button_width(rems_from_px(100.)) .style(ToggleButtonGroupStyle::Filled) .into_any_element(), ), @@ -893,7 +908,7 @@ impl Component ], ) .selected_index(3) - .button_width(rems_from_px(100.)) + .width(rems_from_px(100.)) .style(ToggleButtonGroupStyle::Filled) .into_any_element(), ), @@ -937,7 +952,7 @@ impl Component ], ) .selected_index(3) - .button_width(rems_from_px(100.)) + .width(rems_from_px(100.)) .style(ToggleButtonGroupStyle::Filled) .into_any_element(), ), @@ -957,7 +972,6 @@ impl Component ], ) .selected_index(1) - .button_width(rems_from_px(100.)) .into_any_element(), )]) .into_any_element(), diff --git a/crates/ui/src/traits/fixed.rs b/crates/ui/src/traits/fixed.rs index 9ba64da090..6ca9c8617f 100644 --- a/crates/ui/src/traits/fixed.rs +++ b/crates/ui/src/traits/fixed.rs @@ -3,7 +3,7 @@ use gpui::DefiniteLength; /// A trait for elements that can have a fixed with. Enables the use of the `width` and `full_width` methods. pub trait FixedWidth { /// Sets the width of the element. - fn width(self, width: DefiniteLength) -> Self; + fn width(self, width: impl Into) -> Self; /// Sets the element's width to the full width of its container. fn full_width(self) -> Self; diff --git a/crates/zed/src/zed/quick_action_bar/repl_menu.rs b/crates/zed/src/zed/quick_action_bar/repl_menu.rs index 5d1a6c8887..ca180dccdd 100644 --- a/crates/zed/src/zed/quick_action_bar/repl_menu.rs +++ b/crates/zed/src/zed/quick_action_bar/repl_menu.rs @@ -216,7 +216,7 @@ impl QuickActionBar { .size(IconSize::XSmall) .color(Color::Muted), ) - .width(rems(1.).into()) + .width(rems(1.)) .disabled(menu_state.popover_disabled), Tooltip::text("REPL Menu"), ); From 2b3dbe8815513fcc6519275dc9e4eb35f0a5cd0e Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Wed, 13 Aug 2025 15:22:05 +0200 Subject: [PATCH 309/693] agent2: Allow tools to be provider specific (#36111) Our WebSearch tool requires access to a Zed provider Release Notes: - N/A --- crates/agent2/src/thread.rs | 21 ++++++++++++++++++--- crates/agent2/src/tools/web_search_tool.rs | 9 ++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 678e4cb5d2..1a571e8009 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -15,9 +15,9 @@ use futures::{ use gpui::{App, Context, Entity, SharedString, Task}; use language_model::{ LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage, - LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, - LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, - LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason, + LanguageModelProviderId, LanguageModelRequest, LanguageModelRequestMessage, + LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent, + LanguageModelToolSchemaFormat, LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason, }; use log; use project::Project; @@ -681,10 +681,12 @@ impl Thread { .profiles .get(&self.profile_id) .context("profile not found")?; + let provider_id = self.selected_model.provider_id(); Ok(self .tools .iter() + .filter(move |(_, tool)| tool.supported_provider(&provider_id)) .filter_map(|(tool_name, tool)| { if profile.is_tool_enabled(tool_name) { Some(tool) @@ -782,6 +784,12 @@ where schemars::schema_for!(Self::Input) } + /// Some tools rely on a provider for the underlying billing or other reasons. + /// Allow the tool to check if they are compatible, or should be filtered out. + fn supported_provider(&self, _provider: &LanguageModelProviderId) -> bool { + true + } + /// Runs the tool with the provided input. fn run( self: Arc, @@ -808,6 +816,9 @@ pub trait AnyAgentTool { fn kind(&self) -> acp::ToolKind; fn initial_title(&self, input: serde_json::Value) -> SharedString; fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result; + fn supported_provider(&self, _provider: &LanguageModelProviderId) -> bool { + true + } fn run( self: Arc, input: serde_json::Value, @@ -843,6 +854,10 @@ where Ok(json) } + fn supported_provider(&self, provider: &LanguageModelProviderId) -> bool { + self.0.supported_provider(provider) + } + fn run( self: Arc, input: serde_json::Value, diff --git a/crates/agent2/src/tools/web_search_tool.rs b/crates/agent2/src/tools/web_search_tool.rs index 12587c2f67..c1c0970742 100644 --- a/crates/agent2/src/tools/web_search_tool.rs +++ b/crates/agent2/src/tools/web_search_tool.rs @@ -5,7 +5,9 @@ use agent_client_protocol as acp; use anyhow::{Result, anyhow}; use cloud_llm_client::WebSearchResponse; use gpui::{App, AppContext, Task}; -use language_model::LanguageModelToolResultContent; +use language_model::{ + LanguageModelProviderId, LanguageModelToolResultContent, ZED_CLOUD_PROVIDER_ID, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use ui::prelude::*; @@ -50,6 +52,11 @@ impl AgentTool for WebSearchTool { "Searching the Web".into() } + /// We currently only support Zed Cloud as a provider. + fn supported_provider(&self, provider: &LanguageModelProviderId) -> bool { + provider == &ZED_CLOUD_PROVIDER_ID + } + fn run( self: Arc, input: Self::Input, From abde7306e3a3a767093d95b48e37862b07d274fc Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 13 Aug 2025 10:35:47 -0300 Subject: [PATCH 310/693] onboarding: Adjust page layout (#36112) Fix max-height and make it scrollable as well, if needed. Release Notes: - N/A --- crates/onboarding/src/onboarding.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 7ba7ba60cb..c86871c919 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -551,6 +551,7 @@ impl Render for Onboarding { .child( h_flex() .max_w(rems_from_px(1100.)) + .max_h(rems_from_px(850.)) .size_full() .m_auto() .py_20() @@ -560,12 +561,14 @@ impl Render for Onboarding { .child(self.render_nav(window, cx)) .child( v_flex() + .id("page-content") + .size_full() .max_w_full() .min_w_0() .pl_12() .border_l_1() .border_color(cx.theme().colors().border_variant.opacity(0.5)) - .size_full() + .overflow_y_scroll() .child(self.render_page(window, cx)), ), ) From f4b0332f78bfc688129280c596e615a96f1f4f63 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Aug 2025 09:50:13 -0400 Subject: [PATCH 311/693] Hoist `rodio` to workspace level (#36113) This PR hoists `rodio` up to a workspace dependency. Release Notes: - N/A --- Cargo.toml | 1 + crates/audio/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index dd14078dd2..8cb3c34a8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -566,6 +566,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77 "socks", "stream", ] } +rodio = { version = "0.21.1", default-features = false } rsa = "0.9.6" runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [ "async-dispatcher-runtime", diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index d857a3eb2f..f1f40ad654 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -18,6 +18,6 @@ collections.workspace = true derive_more.workspace = true gpui.workspace = true parking_lot.workspace = true -rodio = { version = "0.21.1", default-features = false, features = ["wav", "playback", "tracing"] } +rodio = { workspace = true, features = ["wav", "playback", "tracing"] } util.workspace = true workspace-hack.workspace = true From 23cd5b59b2682b82a1e96c20fb9f2c6347c97eee Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Wed, 13 Aug 2025 17:46:28 +0200 Subject: [PATCH 312/693] agent2: Initial infra for checkpoints and message editing (#36120) Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra --- Cargo.lock | 2 + crates/acp_thread/Cargo.toml | 1 + crates/acp_thread/src/acp_thread.rs | 470 ++++++++++++++--- crates/acp_thread/src/connection.rs | 34 +- crates/agent2/src/agent.rs | 38 +- crates/agent2/src/tests/mod.rs | 190 +++++-- crates/agent2/src/thread.rs | 705 ++++++++++++++----------- crates/agent_servers/src/acp/v0.rs | 1 + crates/agent_servers/src/acp/v1.rs | 1 + crates/agent_servers/src/claude.rs | 3 +- crates/agent_ui/src/acp/thread_view.rs | 14 +- crates/agent_ui/src/agent_diff.rs | 3 +- crates/fs/Cargo.toml | 1 + crates/fs/src/fake_git_repo.rs | 118 ++++- crates/fs/src/fs.rs | 364 ++++++++----- crates/git/Cargo.toml | 4 +- crates/git/src/git.rs | 7 + 17 files changed, 1374 insertions(+), 582 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d31189fa06..9078b32f7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,6 +31,7 @@ dependencies = [ "ui", "url", "util", + "uuid", "watch", "workspace-hack", ] @@ -6446,6 +6447,7 @@ dependencies = [ "log", "parking_lot", "pretty_assertions", + "rand 0.8.5", "regex", "rope", "schemars", diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index fd01b31786..b3ec217bad 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -36,6 +36,7 @@ terminal.workspace = true ui.workspace = true url.workspace = true util.workspace = true +uuid.workspace = true watch.workspace = true workspace-hack.workspace = true diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index d1957e1c2a..f8a5bf8032 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -9,18 +9,19 @@ pub use mention::*; pub use terminal::*; use action_log::ActionLog; -use agent_client_protocol::{self as acp}; -use anyhow::{Context as _, Result}; +use agent_client_protocol as acp; +use anyhow::{Context as _, Result, anyhow}; use editor::Bias; use futures::{FutureExt, channel::oneshot, future::BoxFuture}; use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity}; use itertools::Itertools; use language::{Anchor, Buffer, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff}; use markdown::Markdown; -use project::{AgentLocation, Project}; +use project::{AgentLocation, Project, git_store::GitStoreCheckpoint}; use std::collections::HashMap; use std::error::Error; -use std::fmt::Formatter; +use std::fmt::{Formatter, Write}; +use std::ops::Range; use std::process::ExitStatus; use std::rc::Rc; use std::{fmt::Display, mem, path::PathBuf, sync::Arc}; @@ -29,24 +30,23 @@ use util::ResultExt; #[derive(Debug)] pub struct UserMessage { + pub id: Option, pub content: ContentBlock, + pub checkpoint: Option, } impl UserMessage { - pub fn from_acp( - message: impl IntoIterator, - language_registry: Arc, - cx: &mut App, - ) -> Self { - let mut content = ContentBlock::Empty; - for chunk in message { - content.append(chunk, &language_registry, cx) - } - Self { content: content } - } - fn to_markdown(&self, cx: &App) -> String { - format!("## User\n\n{}\n\n", self.content.to_markdown(cx)) + let mut markdown = String::new(); + if let Some(_) = self.checkpoint { + writeln!(markdown, "## User (checkpoint)").unwrap(); + } else { + writeln!(markdown, "## User").unwrap(); + } + writeln!(markdown).unwrap(); + writeln!(markdown, "{}", self.content.to_markdown(cx)).unwrap(); + writeln!(markdown).unwrap(); + markdown } } @@ -633,6 +633,7 @@ pub struct AcpThread { pub enum AcpThreadEvent { NewEntry, EntryUpdated(usize), + EntriesRemoved(Range), ToolAuthorizationRequired, Stopped, Error, @@ -772,7 +773,7 @@ impl AcpThread { ) -> Result<()> { match update { acp::SessionUpdate::UserMessageChunk { content } => { - self.push_user_content_block(content, cx); + self.push_user_content_block(None, content, cx); } acp::SessionUpdate::AgentMessageChunk { content } => { self.push_assistant_content_block(content, false, cx); @@ -793,18 +794,32 @@ impl AcpThread { Ok(()) } - pub fn push_user_content_block(&mut self, chunk: acp::ContentBlock, cx: &mut Context) { + pub fn push_user_content_block( + &mut self, + message_id: Option, + chunk: acp::ContentBlock, + cx: &mut Context, + ) { let language_registry = self.project.read(cx).languages().clone(); let entries_len = self.entries.len(); if let Some(last_entry) = self.entries.last_mut() - && let AgentThreadEntry::UserMessage(UserMessage { content }) = last_entry + && let AgentThreadEntry::UserMessage(UserMessage { id, content, .. }) = last_entry { + *id = message_id.or(id.take()); content.append(chunk, &language_registry, cx); - cx.emit(AcpThreadEvent::EntryUpdated(entries_len - 1)); + let idx = entries_len - 1; + cx.emit(AcpThreadEvent::EntryUpdated(idx)); } else { let content = ContentBlock::new(chunk, &language_registry, cx); - self.push_entry(AgentThreadEntry::UserMessage(UserMessage { content }), cx); + self.push_entry( + AgentThreadEntry::UserMessage(UserMessage { + id: message_id, + content, + checkpoint: None, + }), + cx, + ); } } @@ -819,7 +834,8 @@ impl AcpThread { if let Some(last_entry) = self.entries.last_mut() && let AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) = last_entry { - cx.emit(AcpThreadEvent::EntryUpdated(entries_len - 1)); + let idx = entries_len - 1; + cx.emit(AcpThreadEvent::EntryUpdated(idx)); match (chunks.last_mut(), is_thought) { (Some(AssistantMessageChunk::Message { block }), false) | (Some(AssistantMessageChunk::Thought { block }), true) => { @@ -1118,69 +1134,113 @@ impl AcpThread { self.project.read(cx).languages().clone(), cx, ); + let git_store = self.project.read(cx).git_store().clone(); + + let old_checkpoint = git_store.update(cx, |git, cx| git.checkpoint(cx)); + let message_id = if self + .connection + .session_editor(&self.session_id, cx) + .is_some() + { + Some(UserMessageId::new()) + } else { + None + }; self.push_entry( - AgentThreadEntry::UserMessage(UserMessage { content: block }), + AgentThreadEntry::UserMessage(UserMessage { + id: message_id.clone(), + content: block, + checkpoint: None, + }), cx, ); self.clear_completed_plan_entries(cx); + let (old_checkpoint_tx, old_checkpoint_rx) = oneshot::channel(); let (tx, rx) = oneshot::channel(); let cancel_task = self.cancel(cx); + let request = acp::PromptRequest { + prompt: message, + session_id: self.session_id.clone(), + }; - self.send_task = Some(cx.spawn(async move |this, cx| { - async { + self.send_task = Some(cx.spawn({ + let message_id = message_id.clone(); + async move |this, cx| { cancel_task.await; - let result = this - .update(cx, |this, cx| { - this.connection.prompt( - acp::PromptRequest { - prompt: message, - session_id: this.session_id.clone(), - }, - cx, - ) - })? - .await; - - tx.send(result).log_err(); - - anyhow::Ok(()) + old_checkpoint_tx.send(old_checkpoint.await).ok(); + if let Ok(result) = this.update(cx, |this, cx| { + this.connection.prompt(message_id, request, cx) + }) { + tx.send(result.await).log_err(); + } } - .await - .log_err(); })); - cx.spawn(async move |this, cx| match rx.await { - Ok(Err(e)) => { - this.update(cx, |this, cx| { - this.send_task.take(); - cx.emit(AcpThreadEvent::Error) - }) + cx.spawn(async move |this, cx| { + let old_checkpoint = old_checkpoint_rx + .await + .map_err(|_| anyhow!("send canceled")) + .flatten() + .context("failed to get old checkpoint") .log_err(); - Err(e)? - } - result => { - let cancelled = matches!( - result, - Ok(Ok(acp::PromptResponse { - stop_reason: acp::StopReason::Cancelled - })) - ); - // We only take the task if the current prompt wasn't cancelled. - // - // This prompt may have been cancelled because another one was sent - // while it was still generating. In these cases, dropping `send_task` - // would cause the next generation to be cancelled. - if !cancelled { - this.update(cx, |this, _cx| this.send_task.take()).ok(); - } + let response = rx.await; - this.update(cx, |_, cx| cx.emit(AcpThreadEvent::Stopped)) + if let Some((old_checkpoint, message_id)) = old_checkpoint.zip(message_id) { + let new_checkpoint = git_store + .update(cx, |git, cx| git.checkpoint(cx))? + .await + .context("failed to get new checkpoint") .log_err(); - Ok(()) + if let Some(new_checkpoint) = new_checkpoint { + let equal = git_store + .update(cx, |git, cx| { + git.compare_checkpoints(old_checkpoint.clone(), new_checkpoint, cx) + })? + .await + .unwrap_or(true); + if !equal { + this.update(cx, |this, cx| { + if let Some((ix, message)) = this.user_message_mut(&message_id) { + message.checkpoint = Some(old_checkpoint); + cx.emit(AcpThreadEvent::EntryUpdated(ix)); + } + })?; + } + } } + + this.update(cx, |this, cx| { + match response { + Ok(Err(e)) => { + this.send_task.take(); + cx.emit(AcpThreadEvent::Error); + Err(e) + } + result => { + let cancelled = matches!( + result, + Ok(Ok(acp::PromptResponse { + stop_reason: acp::StopReason::Cancelled + })) + ); + + // We only take the task if the current prompt wasn't cancelled. + // + // This prompt may have been cancelled because another one was sent + // while it was still generating. In these cases, dropping `send_task` + // would cause the next generation to be cancelled. + if !cancelled { + this.send_task.take(); + } + + cx.emit(AcpThreadEvent::Stopped); + Ok(()) + } + } + })? }) .boxed() } @@ -1212,6 +1272,66 @@ impl AcpThread { cx.foreground_executor().spawn(send_task) } + /// Rewinds this thread to before the entry at `index`, removing it and all + /// subsequent entries while reverting any changes made from that point. + pub fn rewind(&mut self, id: UserMessageId, cx: &mut Context) -> Task> { + let Some(session_editor) = self.connection.session_editor(&self.session_id, cx) else { + return Task::ready(Err(anyhow!("not supported"))); + }; + let Some(message) = self.user_message(&id) else { + return Task::ready(Err(anyhow!("message not found"))); + }; + + let checkpoint = message.checkpoint.clone(); + + let git_store = self.project.read(cx).git_store().clone(); + cx.spawn(async move |this, cx| { + if let Some(checkpoint) = checkpoint { + git_store + .update(cx, |git, cx| git.restore_checkpoint(checkpoint, cx))? + .await?; + } + + cx.update(|cx| session_editor.truncate(id.clone(), cx))? + .await?; + this.update(cx, |this, cx| { + if let Some((ix, _)) = this.user_message_mut(&id) { + let range = ix..this.entries.len(); + this.entries.truncate(ix); + cx.emit(AcpThreadEvent::EntriesRemoved(range)); + } + }) + }) + } + + fn user_message(&self, id: &UserMessageId) -> Option<&UserMessage> { + self.entries.iter().find_map(|entry| { + if let AgentThreadEntry::UserMessage(message) = entry { + if message.id.as_ref() == Some(&id) { + Some(message) + } else { + None + } + } else { + None + } + }) + } + + fn user_message_mut(&mut self, id: &UserMessageId) -> Option<(usize, &mut UserMessage)> { + self.entries.iter_mut().enumerate().find_map(|(ix, entry)| { + if let AgentThreadEntry::UserMessage(message) = entry { + if message.id.as_ref() == Some(&id) { + Some((ix, message)) + } else { + None + } + } else { + None + } + }) + } + pub fn read_text_file( &self, path: PathBuf, @@ -1414,13 +1534,18 @@ mod tests { use futures::{channel::mpsc, future::LocalBoxFuture, select}; use gpui::{AsyncApp, TestAppContext, WeakEntity}; use indoc::indoc; - use project::FakeFs; + use project::{FakeFs, Fs}; use rand::Rng as _; use serde_json::json; use settings::SettingsStore; use smol::stream::StreamExt as _; - use std::{cell::RefCell, path::Path, rc::Rc, time::Duration}; - + use std::{ + cell::RefCell, + path::Path, + rc::Rc, + sync::atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, + time::Duration, + }; use util::path; fn init_test(cx: &mut TestAppContext) { @@ -1452,6 +1577,7 @@ mod tests { // Test creating a new user message thread.update(cx, |thread, cx| { thread.push_user_content_block( + None, acp::ContentBlock::Text(acp::TextContent { annotations: None, text: "Hello, ".to_string(), @@ -1463,6 +1589,7 @@ mod tests { thread.update(cx, |thread, cx| { assert_eq!(thread.entries.len(), 1); if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[0] { + assert_eq!(user_msg.id, None); assert_eq!(user_msg.content.to_markdown(cx), "Hello, "); } else { panic!("Expected UserMessage"); @@ -1470,8 +1597,10 @@ mod tests { }); // Test appending to existing user message + let message_1_id = UserMessageId::new(); thread.update(cx, |thread, cx| { thread.push_user_content_block( + Some(message_1_id.clone()), acp::ContentBlock::Text(acp::TextContent { annotations: None, text: "world!".to_string(), @@ -1483,6 +1612,7 @@ mod tests { thread.update(cx, |thread, cx| { assert_eq!(thread.entries.len(), 1); if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[0] { + assert_eq!(user_msg.id, Some(message_1_id)); assert_eq!(user_msg.content.to_markdown(cx), "Hello, world!"); } else { panic!("Expected UserMessage"); @@ -1501,8 +1631,10 @@ mod tests { ); }); + let message_2_id = UserMessageId::new(); thread.update(cx, |thread, cx| { thread.push_user_content_block( + Some(message_2_id.clone()), acp::ContentBlock::Text(acp::TextContent { annotations: None, text: "New user message".to_string(), @@ -1514,6 +1646,7 @@ mod tests { thread.update(cx, |thread, cx| { assert_eq!(thread.entries.len(), 3); if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[2] { + assert_eq!(user_msg.id, Some(message_2_id)); assert_eq!(user_msg.content.to_markdown(cx), "New user message"); } else { panic!("Expected UserMessage at index 2"); @@ -1830,6 +1963,180 @@ mod tests { assert!(cx.read(|cx| !thread.read(cx).has_pending_edit_tool_calls())); } + #[gpui::test(iterations = 10)] + async fn test_checkpoints(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + path!("/test"), + json!({ + ".git": {} + }), + ) + .await; + let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; + + let simulate_changes = Arc::new(AtomicBool::new(true)); + let next_filename = Arc::new(AtomicUsize::new(0)); + let connection = Rc::new(FakeAgentConnection::new().on_user_message({ + let simulate_changes = simulate_changes.clone(); + let next_filename = next_filename.clone(); + let fs = fs.clone(); + move |request, thread, mut cx| { + let fs = fs.clone(); + let simulate_changes = simulate_changes.clone(); + let next_filename = next_filename.clone(); + async move { + if simulate_changes.load(SeqCst) { + let filename = format!("/test/file-{}", next_filename.fetch_add(1, SeqCst)); + fs.write(Path::new(&filename), b"").await?; + } + + let acp::ContentBlock::Text(content) = &request.prompt[0] else { + panic!("expected text content block"); + }; + thread.update(&mut cx, |thread, cx| { + thread + .handle_session_update( + acp::SessionUpdate::AgentMessageChunk { + content: content.text.to_uppercase().into(), + }, + cx, + ) + .unwrap(); + })?; + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) + } + .boxed_local() + } + })); + let thread = connection + .new_thread(project, Path::new(path!("/test")), &mut cx.to_async()) + .await + .unwrap(); + + cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["Lorem".into()], cx))) + .await + .unwrap(); + thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc! {" + ## User (checkpoint) + + Lorem + + ## Assistant + + LOREM + + "} + ); + }); + assert_eq!(fs.files(), vec![Path::new("/test/file-0")]); + + cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["ipsum".into()], cx))) + .await + .unwrap(); + thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc! {" + ## User (checkpoint) + + Lorem + + ## Assistant + + LOREM + + ## User (checkpoint) + + ipsum + + ## Assistant + + IPSUM + + "} + ); + }); + assert_eq!( + fs.files(), + vec![Path::new("/test/file-0"), Path::new("/test/file-1")] + ); + + // Checkpoint isn't stored when there are no changes. + simulate_changes.store(false, SeqCst); + cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["dolor".into()], cx))) + .await + .unwrap(); + thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc! {" + ## User (checkpoint) + + Lorem + + ## Assistant + + LOREM + + ## User (checkpoint) + + ipsum + + ## Assistant + + IPSUM + + ## User + + dolor + + ## Assistant + + DOLOR + + "} + ); + }); + assert_eq!( + fs.files(), + vec![Path::new("/test/file-0"), Path::new("/test/file-1")] + ); + + // Rewinding the conversation truncates the history and restores the checkpoint. + thread + .update(cx, |thread, cx| { + let AgentThreadEntry::UserMessage(message) = &thread.entries[2] else { + panic!("unexpected entries {:?}", thread.entries) + }; + thread.rewind(message.id.clone().unwrap(), cx) + }) + .await + .unwrap(); + thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc! {" + ## User (checkpoint) + + Lorem + + ## Assistant + + LOREM + + "} + ); + }); + assert_eq!(fs.files(), vec![Path::new("/test/file-0")]); + } + async fn run_until_first_tool_call( thread: &Entity, cx: &mut TestAppContext, @@ -1938,6 +2245,7 @@ mod tests { fn prompt( &self, + _id: Option, params: acp::PromptRequest, cx: &mut App, ) -> Task> { @@ -1966,5 +2274,25 @@ mod tests { }) .detach(); } + + fn session_editor( + &self, + session_id: &acp::SessionId, + _cx: &mut App, + ) -> Option> { + Some(Rc::new(FakeAgentSessionEditor { + _session_id: session_id.clone(), + })) + } + } + + struct FakeAgentSessionEditor { + _session_id: acp::SessionId, + } + + impl AgentSessionEditor for FakeAgentSessionEditor { + fn truncate(&self, _message_id: UserMessageId, _cx: &mut App) -> Task> { + Task::ready(Ok(())) + } } } diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 8e6294b3ce..c3167eb2d4 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -1,13 +1,21 @@ -use std::{error::Error, fmt, path::Path, rc::Rc}; - +use crate::AcpThread; use agent_client_protocol::{self as acp}; use anyhow::Result; use collections::IndexMap; use gpui::{AsyncApp, Entity, SharedString, Task}; use project::Project; +use std::{error::Error, fmt, path::Path, rc::Rc, sync::Arc}; use ui::{App, IconName}; +use uuid::Uuid; -use crate::AcpThread; +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UserMessageId(Arc); + +impl UserMessageId { + pub fn new() -> Self { + Self(Uuid::new_v4().to_string().into()) + } +} pub trait AgentConnection { fn new_thread( @@ -21,11 +29,23 @@ pub trait AgentConnection { fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task>; - fn prompt(&self, params: acp::PromptRequest, cx: &mut App) - -> Task>; + fn prompt( + &self, + user_message_id: Option, + params: acp::PromptRequest, + cx: &mut App, + ) -> Task>; fn cancel(&self, session_id: &acp::SessionId, cx: &mut App); + fn session_editor( + &self, + _session_id: &acp::SessionId, + _cx: &mut App, + ) -> Option> { + None + } + /// Returns this agent as an [Rc] if the model selection capability is supported. /// /// If the agent does not support model selection, returns [None]. @@ -35,6 +55,10 @@ pub trait AgentConnection { } } +pub trait AgentSessionEditor { + fn truncate(&self, message_id: UserMessageId, cx: &mut App) -> Task>; +} + #[derive(Debug)] pub struct AuthRequired; diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 3ddd7be793..ced8c5e401 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,8 +1,9 @@ use crate::{AgentResponseEvent, Thread, templates::Templates}; use crate::{ ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DiagnosticsTool, EditFileTool, - FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MessageContent, MovePathTool, NowTool, - OpenTool, ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, WebSearchTool, + FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MovePathTool, NowTool, OpenTool, + ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, UserMessageContent, + WebSearchTool, }; use acp_thread::AgentModelSelector; use agent_client_protocol as acp; @@ -637,9 +638,11 @@ impl acp_thread::AgentConnection for NativeAgentConnection { fn prompt( &self, + id: Option, params: acp::PromptRequest, cx: &mut App, ) -> Task> { + let id = id.expect("UserMessageId is required"); let session_id = params.session_id.clone(); let agent = self.0.clone(); log::info!("Received prompt request for session: {}", session_id); @@ -660,13 +663,14 @@ impl acp_thread::AgentConnection for NativeAgentConnection { })?; log::debug!("Found session for: {}", session_id); - let message: Vec = params + let content: Vec = params .prompt .into_iter() .map(Into::into) .collect::>(); - log::info!("Converted prompt to message: {} chars", message.len()); - log::debug!("Message content: {:?}", message); + log::info!("Converted prompt to message: {} chars", content.len()); + log::debug!("Message id: {:?}", id); + log::debug!("Message content: {:?}", content); // Get model using the ModelSelector capability (always available for agent2) // Get the selected model from the thread directly @@ -674,7 +678,8 @@ impl acp_thread::AgentConnection for NativeAgentConnection { // Send to thread log::info!("Sending message to thread with model: {:?}", model.name()); - let mut response_stream = thread.update(cx, |thread, cx| thread.send(message, cx))?; + let mut response_stream = + thread.update(cx, |thread, cx| thread.send(id, content, cx))?; // Handle response stream and forward to session.acp_thread while let Some(result) = response_stream.next().await { @@ -768,6 +773,27 @@ impl acp_thread::AgentConnection for NativeAgentConnection { } }); } + + fn session_editor( + &self, + session_id: &agent_client_protocol::SessionId, + cx: &mut App, + ) -> Option> { + self.0.update(cx, |agent, _cx| { + agent + .sessions + .get(session_id) + .map(|session| Rc::new(NativeAgentSessionEditor(session.thread.clone())) as _) + }) + } +} + +struct NativeAgentSessionEditor(Entity); + +impl acp_thread::AgentSessionEditor for NativeAgentSessionEditor { + fn truncate(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task> { + Task::ready(self.0.update(cx, |thread, _cx| thread.truncate(message_id))) + } } #[cfg(test)] diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index b70fa56747..637af73d1a 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1,6 +1,5 @@ use super::*; -use crate::MessageContent; -use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelList}; +use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelList, UserMessageId}; use action_log::ActionLog; use agent_client_protocol::{self as acp}; use agent_settings::AgentProfileId; @@ -38,15 +37,19 @@ async fn test_echo(cx: &mut TestAppContext) { let events = thread .update(cx, |thread, cx| { - thread.send("Testing: Reply with 'Hello'", cx) + thread.send(UserMessageId::new(), ["Testing: Reply with 'Hello'"], cx) }) .collect() .await; thread.update(cx, |thread, _cx| { assert_eq!( - thread.messages().last().unwrap().content, - vec![MessageContent::Text("Hello".to_string())] - ); + thread.last_message().unwrap().to_markdown(), + indoc! {" + ## Assistant + + Hello + "} + ) }); assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); } @@ -59,12 +62,13 @@ async fn test_thinking(cx: &mut TestAppContext) { let events = thread .update(cx, |thread, cx| { thread.send( - indoc! {" + UserMessageId::new(), + [indoc! {" Testing: Generate a thinking step where you just think the word 'Think', and have your final answer be 'Hello' - "}, + "}], cx, ) }) @@ -72,9 +76,10 @@ async fn test_thinking(cx: &mut TestAppContext) { .await; thread.update(cx, |thread, _cx| { assert_eq!( - thread.messages().last().unwrap().to_markdown(), + thread.last_message().unwrap().to_markdown(), indoc! {" - ## assistant + ## Assistant + Think Hello "} @@ -95,7 +100,9 @@ async fn test_system_prompt(cx: &mut TestAppContext) { project_context.borrow_mut().shell = "test-shell".into(); thread.update(cx, |thread, _| thread.add_tool(EchoTool)); - thread.update(cx, |thread, cx| thread.send("abc", cx)); + thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["abc"], cx) + }); cx.run_until_parked(); let mut pending_completions = fake_model.pending_completions(); assert_eq!( @@ -132,7 +139,8 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) { .update(cx, |thread, cx| { thread.add_tool(EchoTool); thread.send( - "Now test the echo tool with 'Hello'. Does it work? Say 'Yes' or 'No'.", + UserMessageId::new(), + ["Now test the echo tool with 'Hello'. Does it work? Say 'Yes' or 'No'."], cx, ) }) @@ -146,7 +154,11 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) { thread.remove_tool(&AgentTool::name(&EchoTool)); thread.add_tool(DelayTool); thread.send( - "Now call the delay tool with 200ms. When the timer goes off, then you echo the output of the tool.", + UserMessageId::new(), + [ + "Now call the delay tool with 200ms.", + "When the timer goes off, then you echo the output of the tool.", + ], cx, ) }) @@ -156,13 +168,14 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) { thread.update(cx, |thread, _cx| { assert!( thread - .messages() - .last() + .last_message() + .unwrap() + .as_agent_message() .unwrap() .content .iter() .any(|content| { - if let MessageContent::Text(text) = content { + if let AgentMessageContent::Text(text) = content { text.contains("Ding") } else { false @@ -182,7 +195,7 @@ async fn test_streaming_tool_calls(cx: &mut TestAppContext) { // Test a tool call that's likely to complete *before* streaming stops. let mut events = thread.update(cx, |thread, cx| { thread.add_tool(WordListTool); - thread.send("Test the word_list tool.", cx) + thread.send(UserMessageId::new(), ["Test the word_list tool."], cx) }); let mut saw_partial_tool_use = false; @@ -190,8 +203,10 @@ async fn test_streaming_tool_calls(cx: &mut TestAppContext) { if let Ok(AgentResponseEvent::ToolCall(tool_call)) = event { thread.update(cx, |thread, _cx| { // Look for a tool use in the thread's last message - let last_content = thread.messages().last().unwrap().content.last().unwrap(); - if let MessageContent::ToolUse(last_tool_use) = last_content { + let message = thread.last_message().unwrap(); + let agent_message = message.as_agent_message().unwrap(); + let last_content = agent_message.content.last().unwrap(); + if let AgentMessageContent::ToolUse(last_tool_use) = last_content { assert_eq!(last_tool_use.name.as_ref(), "word_list"); if tool_call.status == acp::ToolCallStatus::Pending { if !last_tool_use.is_input_complete @@ -229,7 +244,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { let mut events = thread.update(cx, |thread, cx| { thread.add_tool(ToolRequiringPermission); - thread.send("abc", cx) + thread.send(UserMessageId::new(), ["abc"], cx) }); cx.run_until_parked(); fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( @@ -357,7 +372,9 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); - let mut events = thread.update(cx, |thread, cx| thread.send("abc", cx)); + let mut events = thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["abc"], cx) + }); cx.run_until_parked(); fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { @@ -449,7 +466,12 @@ async fn test_concurrent_tool_calls(cx: &mut TestAppContext) { .update(cx, |thread, cx| { thread.add_tool(DelayTool); thread.send( - "Call the delay tool twice in the same message. Once with 100ms. Once with 300ms. When both timers are complete, describe the outputs.", + UserMessageId::new(), + [ + "Call the delay tool twice in the same message.", + "Once with 100ms. Once with 300ms.", + "When both timers are complete, describe the outputs.", + ], cx, ) }) @@ -460,12 +482,13 @@ async fn test_concurrent_tool_calls(cx: &mut TestAppContext) { assert_eq!(stop_reasons, vec![acp::StopReason::EndTurn]); thread.update(cx, |thread, _cx| { - let last_message = thread.messages().last().unwrap(); - let text = last_message + let last_message = thread.last_message().unwrap(); + let agent_message = last_message.as_agent_message().unwrap(); + let text = agent_message .content .iter() .filter_map(|content| { - if let MessageContent::Text(text) = content { + if let AgentMessageContent::Text(text) = content { Some(text.as_str()) } else { None @@ -521,7 +544,7 @@ async fn test_profiles(cx: &mut TestAppContext) { // Test that test-1 profile (default) has echo and delay tools thread.update(cx, |thread, cx| { thread.set_profile(AgentProfileId("test-1".into())); - thread.send("test", cx); + thread.send(UserMessageId::new(), ["test"], cx); }); cx.run_until_parked(); @@ -539,7 +562,7 @@ async fn test_profiles(cx: &mut TestAppContext) { // Switch to test-2 profile, and verify that it has only the infinite tool. thread.update(cx, |thread, cx| { thread.set_profile(AgentProfileId("test-2".into())); - thread.send("test2", cx) + thread.send(UserMessageId::new(), ["test2"], cx) }); cx.run_until_parked(); let mut pending_completions = fake_model.pending_completions(); @@ -562,7 +585,8 @@ async fn test_cancellation(cx: &mut TestAppContext) { thread.add_tool(InfiniteTool); thread.add_tool(EchoTool); thread.send( - "Call the echo tool and then call the infinite tool, then explain their output", + UserMessageId::new(), + ["Call the echo tool, then call the infinite tool, then explain their output"], cx, ) }); @@ -607,14 +631,20 @@ async fn test_cancellation(cx: &mut TestAppContext) { // Ensure we can still send a new message after cancellation. let events = thread .update(cx, |thread, cx| { - thread.send("Testing: reply with 'Hello' then stop.", cx) + thread.send( + UserMessageId::new(), + ["Testing: reply with 'Hello' then stop."], + cx, + ) }) .collect::>() .await; thread.update(cx, |thread, _cx| { + let message = thread.last_message().unwrap(); + let agent_message = message.as_agent_message().unwrap(); assert_eq!( - thread.messages().last().unwrap().content, - vec![MessageContent::Text("Hello".to_string())] + agent_message.content, + vec![AgentMessageContent::Text("Hello".to_string())] ); }); assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); @@ -625,13 +655,16 @@ async fn test_refusal(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); - let events = thread.update(cx, |thread, cx| thread.send("Hello", cx)); + let events = thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello"], cx) + }); cx.run_until_parked(); thread.read_with(cx, |thread, _| { assert_eq!( thread.to_markdown(), indoc! {" - ## user + ## User + Hello "} ); @@ -643,9 +676,12 @@ async fn test_refusal(cx: &mut TestAppContext) { assert_eq!( thread.to_markdown(), indoc! {" - ## user + ## User + Hello - ## assistant + + ## Assistant + Hey! "} ); @@ -661,6 +697,85 @@ async fn test_refusal(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_truncate(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let message_id = UserMessageId::new(); + thread.update(cx, |thread, cx| { + thread.send(message_id.clone(), ["Hello"], cx) + }); + cx.run_until_parked(); + thread.read_with(cx, |thread, _| { + assert_eq!( + thread.to_markdown(), + indoc! {" + ## User + + Hello + "} + ); + }); + + fake_model.send_last_completion_stream_text_chunk("Hey!"); + cx.run_until_parked(); + thread.read_with(cx, |thread, _| { + assert_eq!( + thread.to_markdown(), + indoc! {" + ## User + + Hello + + ## Assistant + + Hey! + "} + ); + }); + + thread + .update(cx, |thread, _cx| thread.truncate(message_id)) + .unwrap(); + cx.run_until_parked(); + thread.read_with(cx, |thread, _| { + assert_eq!(thread.to_markdown(), ""); + }); + + // Ensure we can still send a new message after truncation. + thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hi"], cx) + }); + thread.update(cx, |thread, _cx| { + assert_eq!( + thread.to_markdown(), + indoc! {" + ## User + + Hi + "} + ); + }); + cx.run_until_parked(); + fake_model.send_last_completion_stream_text_chunk("Ahoy!"); + cx.run_until_parked(); + thread.read_with(cx, |thread, _| { + assert_eq!( + thread.to_markdown(), + indoc! {" + ## User + + Hi + + ## Assistant + + Ahoy! + "} + ); + }); +} + #[gpui::test] async fn test_agent_connection(cx: &mut TestAppContext) { cx.update(settings::init); @@ -774,6 +889,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) { let result = cx .update(|cx| { connection.prompt( + Some(acp_thread::UserMessageId::new()), acp::PromptRequest { session_id: session_id.clone(), prompt: vec!["ghi".into()], @@ -796,7 +912,9 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) { thread.update(cx, |thread, _cx| thread.add_tool(ThinkingTool)); let fake_model = model.as_fake(); - let mut events = thread.update(cx, |thread, cx| thread.send("Think", cx)); + let mut events = thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Think"], cx) + }); cx.run_until_parked(); // Simulate streaming partial input. diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 1a571e8009..204b489124 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1,12 +1,12 @@ use crate::{ContextServerRegistry, SystemPromptTemplate, Template, Templates}; -use acp_thread::MentionUri; +use acp_thread::{MentionUri, UserMessageId}; use action_log::ActionLog; use agent_client_protocol as acp; use agent_settings::{AgentProfileId, AgentSettings}; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::adapt_schema_to_format; use cloud_llm_client::{CompletionIntent, CompletionMode}; -use collections::HashMap; +use collections::IndexMap; use fs::Fs; use futures::{ channel::{mpsc, oneshot}, @@ -19,7 +19,6 @@ use language_model::{ LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason, }; -use log; use project::Project; use prompt_store::ProjectContext; use schemars::{JsonSchema, Schema}; @@ -30,49 +29,199 @@ use std::fmt::Write; use std::{cell::RefCell, collections::BTreeMap, path::Path, rc::Rc, sync::Arc}; use util::{ResultExt, markdown::MarkdownCodeBlock}; -#[derive(Debug, Clone)] -pub struct AgentMessage { - pub role: Role, - pub content: Vec, +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Message { + User(UserMessage), + Agent(AgentMessage), +} + +impl Message { + pub fn as_agent_message(&self) -> Option<&AgentMessage> { + match self { + Message::Agent(agent_message) => Some(agent_message), + _ => None, + } + } + + pub fn to_markdown(&self) -> String { + match self { + Message::User(message) => message.to_markdown(), + Message::Agent(message) => message.to_markdown(), + } + } } #[derive(Debug, Clone, PartialEq, Eq)] -pub enum MessageContent { +pub struct UserMessage { + pub id: UserMessageId, + pub content: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum UserMessageContent { Text(String), - Thinking { - text: String, - signature: Option, - }, - Mention { - uri: MentionUri, - content: String, - }, - RedactedThinking(String), + Mention { uri: MentionUri, content: String }, Image(LanguageModelImage), - ToolUse(LanguageModelToolUse), - ToolResult(LanguageModelToolResult), +} + +impl UserMessage { + pub fn to_markdown(&self) -> String { + let mut markdown = String::from("## User\n\n"); + + for content in &self.content { + match content { + UserMessageContent::Text(text) => { + markdown.push_str(text); + markdown.push('\n'); + } + UserMessageContent::Image(_) => { + markdown.push_str("\n"); + } + UserMessageContent::Mention { uri, content } => { + if !content.is_empty() { + markdown.push_str(&format!("{}\n\n{}\n", uri.to_link(), content)); + } else { + markdown.push_str(&format!("{}\n", uri.to_link())); + } + } + } + } + + markdown + } + + fn to_request(&self) -> LanguageModelRequestMessage { + let mut message = LanguageModelRequestMessage { + role: Role::User, + content: Vec::with_capacity(self.content.len()), + cache: false, + }; + + const OPEN_CONTEXT: &str = "\n\ + The following items were attached by the user. \ + They are up-to-date and don't need to be re-read.\n\n"; + + const OPEN_FILES_TAG: &str = ""; + const OPEN_SYMBOLS_TAG: &str = ""; + const OPEN_THREADS_TAG: &str = ""; + const OPEN_RULES_TAG: &str = + "\nThe user has specified the following rules that should be applied:\n"; + + let mut file_context = OPEN_FILES_TAG.to_string(); + let mut symbol_context = OPEN_SYMBOLS_TAG.to_string(); + let mut thread_context = OPEN_THREADS_TAG.to_string(); + let mut rules_context = OPEN_RULES_TAG.to_string(); + + for chunk in &self.content { + let chunk = match chunk { + UserMessageContent::Text(text) => { + language_model::MessageContent::Text(text.clone()) + } + UserMessageContent::Image(value) => { + language_model::MessageContent::Image(value.clone()) + } + UserMessageContent::Mention { uri, content } => { + match uri { + MentionUri::File(path) | MentionUri::Symbol(path, _) => { + write!( + &mut symbol_context, + "\n{}", + MarkdownCodeBlock { + tag: &codeblock_tag(&path), + text: &content.to_string(), + } + ) + .ok(); + } + MentionUri::Thread(_session_id) => { + write!(&mut thread_context, "\n{}\n", content).ok(); + } + MentionUri::Rule(_user_prompt_id) => { + write!( + &mut rules_context, + "\n{}", + MarkdownCodeBlock { + tag: "", + text: &content + } + ) + .ok(); + } + } + + language_model::MessageContent::Text(uri.to_link()) + } + }; + + message.content.push(chunk); + } + + let len_before_context = message.content.len(); + + if file_context.len() > OPEN_FILES_TAG.len() { + file_context.push_str("\n"); + message + .content + .push(language_model::MessageContent::Text(file_context)); + } + + if symbol_context.len() > OPEN_SYMBOLS_TAG.len() { + symbol_context.push_str("\n"); + message + .content + .push(language_model::MessageContent::Text(symbol_context)); + } + + if thread_context.len() > OPEN_THREADS_TAG.len() { + thread_context.push_str("\n"); + message + .content + .push(language_model::MessageContent::Text(thread_context)); + } + + if rules_context.len() > OPEN_RULES_TAG.len() { + rules_context.push_str("\n"); + message + .content + .push(language_model::MessageContent::Text(rules_context)); + } + + if message.content.len() > len_before_context { + message.content.insert( + len_before_context, + language_model::MessageContent::Text(OPEN_CONTEXT.into()), + ); + message + .content + .push(language_model::MessageContent::Text("".into())); + } + + message + } } impl AgentMessage { pub fn to_markdown(&self) -> String { - let mut markdown = format!("## {}\n", self.role); + let mut markdown = String::from("## Assistant\n\n"); for content in &self.content { match content { - MessageContent::Text(text) => { + AgentMessageContent::Text(text) => { markdown.push_str(text); markdown.push('\n'); } - MessageContent::Thinking { text, .. } => { + AgentMessageContent::Thinking { text, .. } => { markdown.push_str(""); markdown.push_str(text); markdown.push_str("\n"); } - MessageContent::RedactedThinking(_) => markdown.push_str("\n"), - MessageContent::Image(_) => { + AgentMessageContent::RedactedThinking(_) => { + markdown.push_str("\n") + } + AgentMessageContent::Image(_) => { markdown.push_str("\n"); } - MessageContent::ToolUse(tool_use) => { + AgentMessageContent::ToolUse(tool_use) => { markdown.push_str(&format!( "**Tool Use**: {} (ID: {})\n", tool_use.name, tool_use.id @@ -85,41 +234,106 @@ impl AgentMessage { } )); } - MessageContent::ToolResult(tool_result) => { - markdown.push_str(&format!( - "**Tool Result**: {} (ID: {})\n\n", - tool_result.tool_name, tool_result.tool_use_id - )); - if tool_result.is_error { - markdown.push_str("**ERROR:**\n"); - } + } + } - match &tool_result.content { - LanguageModelToolResultContent::Text(text) => { - writeln!(markdown, "{text}\n").ok(); - } - LanguageModelToolResultContent::Image(_) => { - writeln!(markdown, "\n").ok(); - } - } + for tool_result in self.tool_results.values() { + markdown.push_str(&format!( + "**Tool Result**: {} (ID: {})\n\n", + tool_result.tool_name, tool_result.tool_use_id + )); + if tool_result.is_error { + markdown.push_str("**ERROR:**\n"); + } - if let Some(output) = tool_result.output.as_ref() { - writeln!( - markdown, - "**Debug Output**:\n\n```json\n{}\n```\n", - serde_json::to_string_pretty(output).unwrap() - ) - .unwrap(); - } + match &tool_result.content { + LanguageModelToolResultContent::Text(text) => { + writeln!(markdown, "{text}\n").ok(); } - MessageContent::Mention { uri, .. } => { - write!(markdown, "{}", uri.to_link()).ok(); + LanguageModelToolResultContent::Image(_) => { + writeln!(markdown, "\n").ok(); } } + + if let Some(output) = tool_result.output.as_ref() { + writeln!( + markdown, + "**Debug Output**:\n\n```json\n{}\n```\n", + serde_json::to_string_pretty(output).unwrap() + ) + .unwrap(); + } } markdown } + + pub fn to_request(&self) -> Vec { + let mut content = Vec::with_capacity(self.content.len()); + for chunk in &self.content { + let chunk = match chunk { + AgentMessageContent::Text(text) => { + language_model::MessageContent::Text(text.clone()) + } + AgentMessageContent::Thinking { text, signature } => { + language_model::MessageContent::Thinking { + text: text.clone(), + signature: signature.clone(), + } + } + AgentMessageContent::RedactedThinking(value) => { + language_model::MessageContent::RedactedThinking(value.clone()) + } + AgentMessageContent::ToolUse(value) => { + language_model::MessageContent::ToolUse(value.clone()) + } + AgentMessageContent::Image(value) => { + language_model::MessageContent::Image(value.clone()) + } + }; + content.push(chunk); + } + + let mut messages = vec![LanguageModelRequestMessage { + role: Role::Assistant, + content, + cache: false, + }]; + + if !self.tool_results.is_empty() { + let mut tool_results = Vec::with_capacity(self.tool_results.len()); + for tool_result in self.tool_results.values() { + tool_results.push(language_model::MessageContent::ToolResult( + tool_result.clone(), + )); + } + messages.push(LanguageModelRequestMessage { + role: Role::User, + content: tool_results, + cache: false, + }); + } + + messages + } +} + +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct AgentMessage { + pub content: Vec, + pub tool_results: IndexMap, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AgentMessageContent { + Text(String), + Thinking { + text: String, + signature: Option, + }, + RedactedThinking(String), + Image(LanguageModelImage), + ToolUse(LanguageModelToolUse), } #[derive(Debug)] @@ -140,13 +354,13 @@ pub struct ToolCallAuthorization { } pub struct Thread { - messages: Vec, + messages: Vec, completion_mode: CompletionMode, /// Holds the task that handles agent interaction until the end of the turn. /// Survives across multiple requests as the model performs tool calls and /// we run tools, report their results. running_turn: Option>, - pending_tool_uses: HashMap, + pending_agent_message: Option, tools: BTreeMap>, context_server_registry: Entity, profile_id: AgentProfileId, @@ -172,7 +386,7 @@ impl Thread { messages: Vec::new(), completion_mode: CompletionMode::Normal, running_turn: None, - pending_tool_uses: HashMap::default(), + pending_agent_message: None, tools: BTreeMap::default(), context_server_registry, profile_id, @@ -196,8 +410,13 @@ impl Thread { self.completion_mode = mode; } - pub fn messages(&self) -> &[AgentMessage] { - &self.messages + #[cfg(any(test, feature = "test-support"))] + pub fn last_message(&self) -> Option { + if let Some(message) = self.pending_agent_message.clone() { + Some(Message::Agent(message)) + } else { + self.messages.last().cloned() + } } pub fn add_tool(&mut self, tool: impl AgentTool) { @@ -213,35 +432,36 @@ impl Thread { } pub fn cancel(&mut self) { + // TODO: do we need to emit a stop::cancel for ACP? self.running_turn.take(); + self.flush_pending_agent_message(); + } - let tool_results = self - .pending_tool_uses - .drain() - .map(|(tool_use_id, tool_use)| { - MessageContent::ToolResult(LanguageModelToolResult { - tool_use_id, - tool_name: tool_use.name.clone(), - is_error: true, - content: LanguageModelToolResultContent::Text("Tool canceled by user".into()), - output: None, - }) - }) - .collect::>(); - self.last_user_message().content.extend(tool_results); + pub fn truncate(&mut self, message_id: UserMessageId) -> Result<()> { + self.cancel(); + let Some(position) = self.messages.iter().position( + |msg| matches!(msg, Message::User(UserMessage { id, .. }) if id == &message_id), + ) else { + return Err(anyhow!("Message not found")); + }; + self.messages.truncate(position); + Ok(()) } /// Sending a message results in the model streaming a response, which could include tool calls. /// After calling tools, the model will stops and waits for any outstanding tool calls to be completed and their results sent. /// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn. - pub fn send( + pub fn send( &mut self, - content: impl Into, + message_id: UserMessageId, + content: impl IntoIterator, cx: &mut Context, - ) -> mpsc::UnboundedReceiver> { - let content = content.into().0; - + ) -> mpsc::UnboundedReceiver> + where + T: Into, + { let model = self.selected_model.clone(); + let content = content.into_iter().map(Into::into).collect::>(); log::info!("Thread::send called with model: {:?}", model.name()); log::debug!("Thread::send content: {:?}", content); @@ -251,10 +471,10 @@ impl Thread { let event_stream = AgentResponseEventStream(events_tx); let user_message_ix = self.messages.len(); - self.messages.push(AgentMessage { - role: Role::User, + self.messages.push(Message::User(UserMessage { + id: message_id, content, - }); + })); log::info!("Total messages in thread: {}", self.messages.len()); self.running_turn = Some(cx.spawn(async move |thread, cx| { log::info!("Starting agent turn execution"); @@ -270,15 +490,11 @@ impl Thread { thread.build_completion_request(completion_intent, cx) })?; - // println!( - // "request: {}", - // serde_json::to_string_pretty(&request).unwrap() - // ); - // Stream events, appending to messages and collecting up tool uses. log::info!("Calling model.stream_completion"); let mut events = model.stream_completion(request, cx).await?; log::debug!("Stream completion started successfully"); + let mut tool_uses = FuturesUnordered::new(); while let Some(event) = events.next().await { match event { @@ -286,6 +502,7 @@ impl Thread { event_stream.send_stop(reason); if reason == StopReason::Refusal { thread.update(cx, |thread, _cx| { + thread.pending_agent_message = None; thread.messages.truncate(user_message_ix); })?; break 'outer; @@ -338,15 +555,16 @@ impl Thread { ); thread .update(cx, |thread, _cx| { - thread.pending_tool_uses.remove(&tool_result.tool_use_id); thread - .last_user_message() - .content - .push(MessageContent::ToolResult(tool_result)); + .pending_agent_message() + .tool_results + .insert(tool_result.tool_use_id.clone(), tool_result); }) .ok(); } + thread.update(cx, |thread, _cx| thread.flush_pending_agent_message())?; + completion_intent = CompletionIntent::ToolResults; } @@ -354,6 +572,10 @@ impl Thread { } .await; + thread + .update(cx, |thread, _cx| thread.flush_pending_agent_message()) + .ok(); + if let Err(error) = turn_result { log::error!("Turn execution failed: {:?}", error); event_stream.send_error(error); @@ -364,7 +586,7 @@ impl Thread { events_rx } - pub fn build_system_message(&self) -> AgentMessage { + pub fn build_system_message(&self) -> LanguageModelRequestMessage { log::debug!("Building system message"); let prompt = SystemPromptTemplate { project: &self.project_context.borrow(), @@ -374,9 +596,10 @@ impl Thread { .context("failed to build system prompt") .expect("Invalid template"); log::debug!("System message built"); - AgentMessage { + LanguageModelRequestMessage { role: Role::System, - content: vec![prompt.as_str().into()], + content: vec![prompt.into()], + cache: true, } } @@ -394,10 +617,7 @@ impl Thread { match event { StartMessage { .. } => { - self.messages.push(AgentMessage { - role: Role::Assistant, - content: Vec::new(), - }); + self.messages.push(Message::Agent(AgentMessage::default())); } Text(new_text) => self.handle_text_event(new_text, event_stream, cx), Thinking { text, signature } => { @@ -435,11 +655,13 @@ impl Thread { ) { events_stream.send_text(&new_text); - let last_message = self.last_assistant_message(); - if let Some(MessageContent::Text(text)) = last_message.content.last_mut() { + let last_message = self.pending_agent_message(); + if let Some(AgentMessageContent::Text(text)) = last_message.content.last_mut() { text.push_str(&new_text); } else { - last_message.content.push(MessageContent::Text(new_text)); + last_message + .content + .push(AgentMessageContent::Text(new_text)); } cx.notify(); @@ -454,13 +676,14 @@ impl Thread { ) { event_stream.send_thinking(&new_text); - let last_message = self.last_assistant_message(); - if let Some(MessageContent::Thinking { text, signature }) = last_message.content.last_mut() + let last_message = self.pending_agent_message(); + if let Some(AgentMessageContent::Thinking { text, signature }) = + last_message.content.last_mut() { text.push_str(&new_text); *signature = new_signature.or(signature.take()); } else { - last_message.content.push(MessageContent::Thinking { + last_message.content.push(AgentMessageContent::Thinking { text: new_text, signature: new_signature, }); @@ -470,10 +693,10 @@ impl Thread { } fn handle_redacted_thinking_event(&mut self, data: String, cx: &mut Context) { - let last_message = self.last_assistant_message(); + let last_message = self.pending_agent_message(); last_message .content - .push(MessageContent::RedactedThinking(data)); + .push(AgentMessageContent::RedactedThinking(data)); cx.notify(); } @@ -486,14 +709,17 @@ impl Thread { cx.notify(); let tool = self.tools.get(tool_use.name.as_ref()).cloned(); - - self.pending_tool_uses - .insert(tool_use.id.clone(), tool_use.clone()); - let last_message = self.last_assistant_message(); + let mut title = SharedString::from(&tool_use.name); + let mut kind = acp::ToolKind::Other; + if let Some(tool) = tool.as_ref() { + title = tool.initial_title(tool_use.input.clone()); + kind = tool.kind(); + } // Ensure the last message ends in the current tool use + let last_message = self.pending_agent_message(); let push_new_tool_use = last_message.content.last_mut().map_or(true, |content| { - if let MessageContent::ToolUse(last_tool_use) = content { + if let AgentMessageContent::ToolUse(last_tool_use) = content { if last_tool_use.id == tool_use.id { *last_tool_use = tool_use.clone(); false @@ -505,18 +731,11 @@ impl Thread { } }); - let mut title = SharedString::from(&tool_use.name); - let mut kind = acp::ToolKind::Other; - if let Some(tool) = tool.as_ref() { - title = tool.initial_title(tool_use.input.clone()); - kind = tool.kind(); - } - if push_new_tool_use { event_stream.send_tool_call(&tool_use.id, title, kind, tool_use.input.clone()); last_message .content - .push(MessageContent::ToolUse(tool_use.clone())); + .push(AgentMessageContent::ToolUse(tool_use.clone())); } else { event_stream.update_tool_call_fields( &tool_use.id, @@ -601,30 +820,37 @@ impl Thread { } } - /// Guarantees the last message is from the assistant and returns a mutable reference. - fn last_assistant_message(&mut self) -> &mut AgentMessage { - if self - .messages - .last() - .map_or(true, |m| m.role != Role::Assistant) - { - self.messages.push(AgentMessage { - role: Role::Assistant, - content: Vec::new(), - }); - } - self.messages.last_mut().unwrap() + fn pending_agent_message(&mut self) -> &mut AgentMessage { + self.pending_agent_message.get_or_insert_default() } - /// Guarantees the last message is from the user and returns a mutable reference. - fn last_user_message(&mut self) -> &mut AgentMessage { - if self.messages.last().map_or(true, |m| m.role != Role::User) { - self.messages.push(AgentMessage { - role: Role::User, - content: Vec::new(), - }); + fn flush_pending_agent_message(&mut self) { + let Some(mut message) = self.pending_agent_message.take() else { + return; + }; + + for content in &message.content { + let AgentMessageContent::ToolUse(tool_use) = content else { + continue; + }; + + if !message.tool_results.contains_key(&tool_use.id) { + message.tool_results.insert( + tool_use.id.clone(), + LanguageModelToolResult { + tool_use_id: tool_use.id.clone(), + tool_name: tool_use.name.clone(), + is_error: true, + content: LanguageModelToolResultContent::Text( + "Tool canceled by user".into(), + ), + output: None, + }, + ); + } } - self.messages.last_mut().unwrap() + + self.messages.push(Message::Agent(message)); } pub(crate) fn build_completion_request( @@ -712,49 +938,39 @@ impl Thread { "Building request messages from {} thread messages", self.messages.len() ); + let mut messages = vec![self.build_system_message()]; + for message in &self.messages { + match message { + Message::User(message) => messages.push(message.to_request()), + Message::Agent(message) => messages.extend(message.to_request()), + } + } + + if let Some(message) = self.pending_agent_message.as_ref() { + messages.extend(message.to_request()); + } - let messages = Some(self.build_system_message()) - .iter() - .chain(self.messages.iter()) - .map(|message| { - log::trace!( - " - {} message with {} content items", - match message.role { - Role::System => "System", - Role::User => "User", - Role::Assistant => "Assistant", - }, - message.content.len() - ); - message.to_request() - }) - .collect(); messages } pub fn to_markdown(&self) -> String { let mut markdown = String::new(); - for message in &self.messages { + for (ix, message) in self.messages.iter().enumerate() { + if ix > 0 { + markdown.push('\n'); + } markdown.push_str(&message.to_markdown()); } + + if let Some(message) = self.pending_agent_message.as_ref() { + markdown.push('\n'); + markdown.push_str(&message.to_markdown()); + } + markdown } } -pub struct UserMessage(Vec); - -impl From> for UserMessage { - fn from(content: Vec) -> Self { - UserMessage(content) - } -} - -impl> From for UserMessage { - fn from(content: T) -> Self { - UserMessage(vec![content.into()]) - } -} - pub trait AgentTool where Self: 'static + Sized, @@ -1151,130 +1367,6 @@ impl std::ops::DerefMut for ToolCallEventStreamReceiver { } } -impl AgentMessage { - fn to_request(&self) -> language_model::LanguageModelRequestMessage { - let mut message = LanguageModelRequestMessage { - role: self.role, - content: Vec::with_capacity(self.content.len()), - cache: false, - }; - - const OPEN_CONTEXT: &str = "\n\ - The following items were attached by the user. \ - They are up-to-date and don't need to be re-read.\n\n"; - - const OPEN_FILES_TAG: &str = ""; - const OPEN_SYMBOLS_TAG: &str = ""; - const OPEN_THREADS_TAG: &str = ""; - const OPEN_RULES_TAG: &str = - "\nThe user has specified the following rules that should be applied:\n"; - - let mut file_context = OPEN_FILES_TAG.to_string(); - let mut symbol_context = OPEN_SYMBOLS_TAG.to_string(); - let mut thread_context = OPEN_THREADS_TAG.to_string(); - let mut rules_context = OPEN_RULES_TAG.to_string(); - - for chunk in &self.content { - let chunk = match chunk { - MessageContent::Text(text) => language_model::MessageContent::Text(text.clone()), - MessageContent::Thinking { text, signature } => { - language_model::MessageContent::Thinking { - text: text.clone(), - signature: signature.clone(), - } - } - MessageContent::RedactedThinking(value) => { - language_model::MessageContent::RedactedThinking(value.clone()) - } - MessageContent::ToolUse(value) => { - language_model::MessageContent::ToolUse(value.clone()) - } - MessageContent::ToolResult(value) => { - language_model::MessageContent::ToolResult(value.clone()) - } - MessageContent::Image(value) => { - language_model::MessageContent::Image(value.clone()) - } - MessageContent::Mention { uri, content } => { - match uri { - MentionUri::File(path) | MentionUri::Symbol(path, _) => { - write!( - &mut symbol_context, - "\n{}", - MarkdownCodeBlock { - tag: &codeblock_tag(&path), - text: &content.to_string(), - } - ) - .ok(); - } - MentionUri::Thread(_session_id) => { - write!(&mut thread_context, "\n{}\n", content).ok(); - } - MentionUri::Rule(_user_prompt_id) => { - write!( - &mut rules_context, - "\n{}", - MarkdownCodeBlock { - tag: "", - text: &content - } - ) - .ok(); - } - } - - language_model::MessageContent::Text(uri.to_link()) - } - }; - - message.content.push(chunk); - } - - let len_before_context = message.content.len(); - - if file_context.len() > OPEN_FILES_TAG.len() { - file_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(file_context)); - } - - if symbol_context.len() > OPEN_SYMBOLS_TAG.len() { - symbol_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(symbol_context)); - } - - if thread_context.len() > OPEN_THREADS_TAG.len() { - thread_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(thread_context)); - } - - if rules_context.len() > OPEN_RULES_TAG.len() { - rules_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(rules_context)); - } - - if message.content.len() > len_before_context { - message.content.insert( - len_before_context, - language_model::MessageContent::Text(OPEN_CONTEXT.into()), - ); - message - .content - .push(language_model::MessageContent::Text("".into())); - } - - message - } -} - fn codeblock_tag(full_path: &Path) -> String { let mut result = String::new(); @@ -1287,16 +1379,20 @@ fn codeblock_tag(full_path: &Path) -> String { result } -impl From for MessageContent { +impl From<&str> for UserMessageContent { + fn from(text: &str) -> Self { + Self::Text(text.into()) + } +} + +impl From for UserMessageContent { fn from(value: acp::ContentBlock) -> Self { match value { - acp::ContentBlock::Text(text_content) => MessageContent::Text(text_content.text), - acp::ContentBlock::Image(image_content) => { - MessageContent::Image(convert_image(image_content)) - } + acp::ContentBlock::Text(text_content) => Self::Text(text_content.text), + acp::ContentBlock::Image(image_content) => Self::Image(convert_image(image_content)), acp::ContentBlock::Audio(_) => { // TODO - MessageContent::Text("[audio]".to_string()) + Self::Text("[audio]".to_string()) } acp::ContentBlock::ResourceLink(resource_link) => { match MentionUri::parse(&resource_link.uri) { @@ -1306,10 +1402,7 @@ impl From for MessageContent { }, Err(err) => { log::error!("Failed to parse mention link: {}", err); - MessageContent::Text(format!( - "[{}]({})", - resource_link.name, resource_link.uri - )) + Self::Text(format!("[{}]({})", resource_link.name, resource_link.uri)) } } } @@ -1322,7 +1415,7 @@ impl From for MessageContent { }, Err(err) => { log::error!("Failed to parse mention link: {}", err); - MessageContent::Text( + Self::Text( MarkdownCodeBlock { tag: &resource.uri, text: &resource.text, @@ -1334,7 +1427,7 @@ impl From for MessageContent { } acp::EmbeddedResourceResource::BlobResourceContents(_) => { // TODO - MessageContent::Text("[blob]".to_string()) + Self::Text("[blob]".to_string()) } }, } @@ -1348,9 +1441,3 @@ fn convert_image(image_content: acp::ImageContent) -> LanguageModelImage { size: gpui::Size::new(0.into(), 0.into()), } } - -impl From<&str> for MessageContent { - fn from(text: &str) -> Self { - MessageContent::Text(text.into()) - } -} diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index 8d85435f92..327613de67 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -467,6 +467,7 @@ impl AgentConnection for AcpConnection { fn prompt( &self, + _id: Option, params: acp::PromptRequest, cx: &mut App, ) -> Task> { diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index ff71783b48..de397fddf0 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -171,6 +171,7 @@ impl AgentConnection for AcpConnection { fn prompt( &self, + _id: Option, params: acp::PromptRequest, cx: &mut App, ) -> Task> { diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index c65508f152..c394ec4a9c 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -210,6 +210,7 @@ impl AgentConnection for ClaudeAgentConnection { fn prompt( &self, + _id: Option, params: acp::PromptRequest, cx: &mut App, ) -> Task> { @@ -423,7 +424,7 @@ impl ClaudeAgentSession { if !turn_state.borrow().is_cancelled() { thread .update(cx, |thread, cx| { - thread.push_user_content_block(text.into(), cx) + thread.push_user_content_block(None, text.into(), cx) }) .log_err(); } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 12fc29b08f..0b3ace1baf 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -679,17 +679,19 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) { - let count = self.list_state.item_count(); match event { AcpThreadEvent::NewEntry => { let index = thread.read(cx).entries().len() - 1; self.sync_thread_entry_view(index, window, cx); - self.list_state.splice(count..count, 1); + self.list_state.splice(index..index, 1); } AcpThreadEvent::EntryUpdated(index) => { - let index = *index; - self.sync_thread_entry_view(index, window, cx); - self.list_state.splice(index..index + 1, 1); + self.sync_thread_entry_view(*index, window, cx); + self.list_state.splice(*index..index + 1, 1); + } + AcpThreadEvent::EntriesRemoved(range) => { + // TODO: Clean up unused diff editors and terminal views + self.list_state.splice(range.clone(), 0); } AcpThreadEvent::ToolAuthorizationRequired => { self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx); @@ -3789,6 +3791,7 @@ mod tests { fn prompt( &self, + _id: Option, params: acp::PromptRequest, cx: &mut App, ) -> Task> { @@ -3873,6 +3876,7 @@ mod tests { fn prompt( &self, + _id: Option, _params: acp::PromptRequest, _cx: &mut App, ) -> Task> { diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 0abc5280f4..b9e1ea5d0a 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1521,7 +1521,8 @@ impl AgentDiff { self.update_reviewing_editors(workspace, window, cx); } } - AcpThreadEvent::Stopped + AcpThreadEvent::EntriesRemoved(_) + | AcpThreadEvent::Stopped | AcpThreadEvent::ToolAuthorizationRequired | AcpThreadEvent::Error | AcpThreadEvent::ServerExited(_) => {} diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 633fc1fc99..1d4161134e 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -51,6 +51,7 @@ ashpd.workspace = true [dev-dependencies] gpui = { workspace = true, features = ["test-support"] } +git = { workspace = true, features = ["test-support"] } [features] test-support = ["gpui/test-support", "git/test-support"] diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 21b9cbca9a..f0936d400a 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -1,8 +1,9 @@ -use crate::{FakeFs, Fs}; +use crate::{FakeFs, FakeFsEntry, Fs}; use anyhow::{Context as _, Result}; use collections::{HashMap, HashSet}; use futures::future::{self, BoxFuture, join_all}; use git::{ + Oid, blame::Blame, repository::{ AskPassDelegate, Branch, CommitDetails, CommitOptions, FetchOptions, GitRepository, @@ -12,6 +13,7 @@ use git::{ }; use gpui::{AsyncApp, BackgroundExecutor, SharedString, Task}; use ignore::gitignore::GitignoreBuilder; +use parking_lot::Mutex; use rope::Rope; use smol::future::FutureExt as _; use std::{path::PathBuf, sync::Arc}; @@ -19,6 +21,7 @@ use std::{path::PathBuf, sync::Arc}; #[derive(Clone)] pub struct FakeGitRepository { pub(crate) fs: Arc, + pub(crate) checkpoints: Arc>>, pub(crate) executor: BackgroundExecutor, pub(crate) dot_git_path: PathBuf, pub(crate) repository_dir_path: PathBuf, @@ -469,22 +472,57 @@ impl GitRepository for FakeGitRepository { } fn checkpoint(&self) -> BoxFuture<'static, Result> { - unimplemented!() + let executor = self.executor.clone(); + let fs = self.fs.clone(); + let checkpoints = self.checkpoints.clone(); + let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf(); + async move { + executor.simulate_random_delay().await; + let oid = Oid::random(&mut executor.rng()); + let entry = fs.entry(&repository_dir_path)?; + checkpoints.lock().insert(oid, entry); + Ok(GitRepositoryCheckpoint { commit_sha: oid }) + } + .boxed() } - fn restore_checkpoint( - &self, - _checkpoint: GitRepositoryCheckpoint, - ) -> BoxFuture<'_, Result<()>> { - unimplemented!() + fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> { + let executor = self.executor.clone(); + let fs = self.fs.clone(); + let checkpoints = self.checkpoints.clone(); + let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf(); + async move { + executor.simulate_random_delay().await; + let checkpoints = checkpoints.lock(); + let entry = checkpoints + .get(&checkpoint.commit_sha) + .context(format!("invalid checkpoint: {}", checkpoint.commit_sha))?; + fs.insert_entry(&repository_dir_path, entry.clone())?; + Ok(()) + } + .boxed() } fn compare_checkpoints( &self, - _left: GitRepositoryCheckpoint, - _right: GitRepositoryCheckpoint, + left: GitRepositoryCheckpoint, + right: GitRepositoryCheckpoint, ) -> BoxFuture<'_, Result> { - unimplemented!() + let executor = self.executor.clone(); + let checkpoints = self.checkpoints.clone(); + async move { + executor.simulate_random_delay().await; + let checkpoints = checkpoints.lock(); + let left = checkpoints + .get(&left.commit_sha) + .context(format!("invalid left checkpoint: {}", left.commit_sha))?; + let right = checkpoints + .get(&right.commit_sha) + .context(format!("invalid right checkpoint: {}", right.commit_sha))?; + + Ok(left == right) + } + .boxed() } fn diff_checkpoints( @@ -499,3 +537,63 @@ impl GitRepository for FakeGitRepository { unimplemented!() } } + +#[cfg(test)] +mod tests { + use crate::{FakeFs, Fs}; + use gpui::BackgroundExecutor; + use serde_json::json; + use std::path::Path; + use util::path; + + #[gpui::test] + async fn test_checkpoints(executor: BackgroundExecutor) { + let fs = FakeFs::new(executor); + fs.insert_tree( + path!("/"), + json!({ + "bar": { + "baz": "qux" + }, + "foo": { + ".git": {}, + "a": "lorem", + "b": "ipsum", + }, + }), + ) + .await; + fs.with_git_state(Path::new("/foo/.git"), true, |_git| {}) + .unwrap(); + let repository = fs.open_repo(Path::new("/foo/.git")).unwrap(); + + let checkpoint_1 = repository.checkpoint().await.unwrap(); + fs.write(Path::new("/foo/b"), b"IPSUM").await.unwrap(); + fs.write(Path::new("/foo/c"), b"dolor").await.unwrap(); + let checkpoint_2 = repository.checkpoint().await.unwrap(); + let checkpoint_3 = repository.checkpoint().await.unwrap(); + + assert!( + repository + .compare_checkpoints(checkpoint_2.clone(), checkpoint_3.clone()) + .await + .unwrap() + ); + assert!( + !repository + .compare_checkpoints(checkpoint_1.clone(), checkpoint_2.clone()) + .await + .unwrap() + ); + + repository.restore_checkpoint(checkpoint_1).await.unwrap(); + assert_eq!( + fs.files_with_contents(Path::new("")), + [ + (Path::new("/bar/baz").into(), b"qux".into()), + (Path::new("/foo/a").into(), b"lorem".into()), + (Path::new("/foo/b").into(), b"ipsum".into()) + ] + ); + } +} diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index a2b75ac6a7..22bfdbcd66 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -924,7 +924,7 @@ pub struct FakeFs { #[cfg(any(test, feature = "test-support"))] struct FakeFsState { - root: Arc>, + root: FakeFsEntry, next_inode: u64, next_mtime: SystemTime, git_event_tx: smol::channel::Sender, @@ -939,7 +939,7 @@ struct FakeFsState { } #[cfg(any(test, feature = "test-support"))] -#[derive(Debug)] +#[derive(Clone, Debug)] enum FakeFsEntry { File { inode: u64, @@ -953,7 +953,7 @@ enum FakeFsEntry { inode: u64, mtime: MTime, len: u64, - entries: BTreeMap>>, + entries: BTreeMap, git_repo_state: Option>>, }, Symlink { @@ -961,6 +961,67 @@ enum FakeFsEntry { }, } +#[cfg(any(test, feature = "test-support"))] +impl PartialEq for FakeFsEntry { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + ( + Self::File { + inode: l_inode, + mtime: l_mtime, + len: l_len, + content: l_content, + git_dir_path: l_git_dir_path, + }, + Self::File { + inode: r_inode, + mtime: r_mtime, + len: r_len, + content: r_content, + git_dir_path: r_git_dir_path, + }, + ) => { + l_inode == r_inode + && l_mtime == r_mtime + && l_len == r_len + && l_content == r_content + && l_git_dir_path == r_git_dir_path + } + ( + Self::Dir { + inode: l_inode, + mtime: l_mtime, + len: l_len, + entries: l_entries, + git_repo_state: l_git_repo_state, + }, + Self::Dir { + inode: r_inode, + mtime: r_mtime, + len: r_len, + entries: r_entries, + git_repo_state: r_git_repo_state, + }, + ) => { + let same_repo_state = match (l_git_repo_state.as_ref(), r_git_repo_state.as_ref()) { + (Some(l), Some(r)) => Arc::ptr_eq(l, r), + (None, None) => true, + _ => false, + }; + l_inode == r_inode + && l_mtime == r_mtime + && l_len == r_len + && l_entries == r_entries + && same_repo_state + } + (Self::Symlink { target: l_target }, Self::Symlink { target: r_target }) => { + l_target == r_target + } + _ => false, + } + } +} + #[cfg(any(test, feature = "test-support"))] impl FakeFsState { fn get_and_increment_mtime(&mut self) -> MTime { @@ -975,25 +1036,9 @@ impl FakeFsState { inode } - fn read_path(&self, target: &Path) -> Result>> { - Ok(self - .try_read_path(target, true) - .ok_or_else(|| { - anyhow!(io::Error::new( - io::ErrorKind::NotFound, - format!("not found: {target:?}") - )) - })? - .0) - } - - fn try_read_path( - &self, - target: &Path, - follow_symlink: bool, - ) -> Option<(Arc>, PathBuf)> { - let mut path = target.to_path_buf(); + fn canonicalize(&self, target: &Path, follow_symlink: bool) -> Option { let mut canonical_path = PathBuf::new(); + let mut path = target.to_path_buf(); let mut entry_stack = Vec::new(); 'outer: loop { let mut path_components = path.components().peekable(); @@ -1003,7 +1048,7 @@ impl FakeFsState { Component::Prefix(prefix_component) => prefix = Some(prefix_component), Component::RootDir => { entry_stack.clear(); - entry_stack.push(self.root.clone()); + entry_stack.push(&self.root); canonical_path.clear(); match prefix { Some(prefix_component) => { @@ -1020,20 +1065,18 @@ impl FakeFsState { canonical_path.pop(); } Component::Normal(name) => { - let current_entry = entry_stack.last().cloned()?; - let current_entry = current_entry.lock(); - if let FakeFsEntry::Dir { entries, .. } = &*current_entry { - let entry = entries.get(name.to_str().unwrap()).cloned()?; + let current_entry = *entry_stack.last()?; + if let FakeFsEntry::Dir { entries, .. } = current_entry { + let entry = entries.get(name.to_str().unwrap())?; if path_components.peek().is_some() || follow_symlink { - let entry = entry.lock(); - if let FakeFsEntry::Symlink { target, .. } = &*entry { + if let FakeFsEntry::Symlink { target, .. } = entry { let mut target = target.clone(); target.extend(path_components); path = target; continue 'outer; } } - entry_stack.push(entry.clone()); + entry_stack.push(entry); canonical_path = canonical_path.join(name); } else { return None; @@ -1043,19 +1086,72 @@ impl FakeFsState { } break; } - Some((entry_stack.pop()?, canonical_path)) + + if entry_stack.is_empty() { + None + } else { + Some(canonical_path) + } } - fn write_path(&self, path: &Path, callback: Fn) -> Result + fn try_entry( + &mut self, + target: &Path, + follow_symlink: bool, + ) -> Option<(&mut FakeFsEntry, PathBuf)> { + let canonical_path = self.canonicalize(target, follow_symlink)?; + + let mut components = canonical_path.components(); + let Some(Component::RootDir) = components.next() else { + panic!( + "the path {:?} was not canonicalized properly {:?}", + target, canonical_path + ) + }; + + let mut entry = &mut self.root; + for component in components { + match component { + Component::Normal(name) => { + if let FakeFsEntry::Dir { entries, .. } = entry { + entry = entries.get_mut(name.to_str().unwrap())?; + } else { + return None; + } + } + _ => { + panic!( + "the path {:?} was not canonicalized properly {:?}", + target, canonical_path + ) + } + } + } + + Some((entry, canonical_path)) + } + + fn entry(&mut self, target: &Path) -> Result<&mut FakeFsEntry> { + Ok(self + .try_entry(target, true) + .ok_or_else(|| { + anyhow!(io::Error::new( + io::ErrorKind::NotFound, + format!("not found: {target:?}") + )) + })? + .0) + } + + fn write_path(&mut self, path: &Path, callback: Fn) -> Result where - Fn: FnOnce(btree_map::Entry>>) -> Result, + Fn: FnOnce(btree_map::Entry) -> Result, { let path = normalize_path(path); let filename = path.file_name().context("cannot overwrite the root")?; let parent_path = path.parent().unwrap(); - let parent = self.read_path(parent_path)?; - let mut parent = parent.lock(); + let parent = self.entry(parent_path)?; let new_entry = parent .dir_entries(parent_path)? .entry(filename.to_str().unwrap().into()); @@ -1105,13 +1201,13 @@ impl FakeFs { this: this.clone(), executor: executor.clone(), state: Arc::new(Mutex::new(FakeFsState { - root: Arc::new(Mutex::new(FakeFsEntry::Dir { + root: FakeFsEntry::Dir { inode: 0, mtime: MTime(UNIX_EPOCH), len: 0, entries: Default::default(), git_repo_state: None, - })), + }, git_event_tx: tx, next_mtime: UNIX_EPOCH + Self::SYSTEMTIME_INTERVAL, next_inode: 1, @@ -1161,15 +1257,15 @@ impl FakeFs { .write_path(path, move |entry| { match entry { btree_map::Entry::Vacant(e) => { - e.insert(Arc::new(Mutex::new(FakeFsEntry::File { + e.insert(FakeFsEntry::File { inode: new_inode, mtime: new_mtime, content: Vec::new(), len: 0, git_dir_path: None, - }))); + }); } - btree_map::Entry::Occupied(mut e) => match &mut *e.get_mut().lock() { + btree_map::Entry::Occupied(mut e) => match &mut *e.get_mut() { FakeFsEntry::File { mtime, .. } => *mtime = new_mtime, FakeFsEntry::Dir { mtime, .. } => *mtime = new_mtime, FakeFsEntry::Symlink { .. } => {} @@ -1188,7 +1284,7 @@ impl FakeFs { pub async fn insert_symlink(&self, path: impl AsRef, target: PathBuf) { let mut state = self.state.lock(); let path = path.as_ref(); - let file = Arc::new(Mutex::new(FakeFsEntry::Symlink { target })); + let file = FakeFsEntry::Symlink { target }; state .write_path(path.as_ref(), move |e| match e { btree_map::Entry::Vacant(e) => { @@ -1221,13 +1317,13 @@ impl FakeFs { match entry { btree_map::Entry::Vacant(e) => { kind = Some(PathEventKind::Created); - e.insert(Arc::new(Mutex::new(FakeFsEntry::File { + e.insert(FakeFsEntry::File { inode: new_inode, mtime: new_mtime, len: new_len, content: new_content, git_dir_path: None, - }))); + }); } btree_map::Entry::Occupied(mut e) => { kind = Some(PathEventKind::Changed); @@ -1237,7 +1333,7 @@ impl FakeFs { len, content, .. - } = &mut *e.get_mut().lock() + } = e.get_mut() { *mtime = new_mtime; *content = new_content; @@ -1259,9 +1355,8 @@ impl FakeFs { pub fn read_file_sync(&self, path: impl AsRef) -> Result> { let path = path.as_ref(); let path = normalize_path(path); - let state = self.state.lock(); - let entry = state.read_path(&path)?; - let entry = entry.lock(); + let mut state = self.state.lock(); + let entry = state.entry(&path)?; entry.file_content(&path).cloned() } @@ -1269,9 +1364,8 @@ impl FakeFs { let path = path.as_ref(); let path = normalize_path(path); self.simulate_random_delay().await; - let state = self.state.lock(); - let entry = state.read_path(&path)?; - let entry = entry.lock(); + let mut state = self.state.lock(); + let entry = state.entry(&path)?; entry.file_content(&path).cloned() } @@ -1292,6 +1386,25 @@ impl FakeFs { self.state.lock().flush_events(count); } + pub(crate) fn entry(&self, target: &Path) -> Result { + self.state.lock().entry(target).cloned() + } + + pub(crate) fn insert_entry(&self, target: &Path, new_entry: FakeFsEntry) -> Result<()> { + let mut state = self.state.lock(); + state.write_path(target, |entry| { + match entry { + btree_map::Entry::Vacant(vacant_entry) => { + vacant_entry.insert(new_entry); + } + btree_map::Entry::Occupied(mut occupied_entry) => { + occupied_entry.insert(new_entry); + } + } + Ok(()) + }) + } + #[must_use] pub fn insert_tree<'a>( &'a self, @@ -1361,20 +1474,19 @@ impl FakeFs { F: FnOnce(&mut FakeGitRepositoryState, &Path, &Path) -> T, { let mut state = self.state.lock(); - let entry = state.read_path(dot_git).context("open .git")?; - let mut entry = entry.lock(); + let git_event_tx = state.git_event_tx.clone(); + let entry = state.entry(dot_git).context("open .git")?; - if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry { + if let FakeFsEntry::Dir { git_repo_state, .. } = entry { let repo_state = git_repo_state.get_or_insert_with(|| { log::debug!("insert git state for {dot_git:?}"); - Arc::new(Mutex::new(FakeGitRepositoryState::new( - state.git_event_tx.clone(), - ))) + Arc::new(Mutex::new(FakeGitRepositoryState::new(git_event_tx))) }); let mut repo_state = repo_state.lock(); let result = f(&mut repo_state, dot_git, dot_git); + drop(repo_state); if emit_git_event { state.emit_event([(dot_git, None)]); } @@ -1398,21 +1510,20 @@ impl FakeFs { } } .clone(); - drop(entry); - let Some((git_dir_entry, canonical_path)) = state.try_read_path(&path, true) else { + let Some((git_dir_entry, canonical_path)) = state.try_entry(&path, true) else { anyhow::bail!("pointed-to git dir {path:?} not found") }; let FakeFsEntry::Dir { git_repo_state, entries, .. - } = &mut *git_dir_entry.lock() + } = git_dir_entry else { anyhow::bail!("gitfile points to a non-directory") }; let common_dir = if let Some(child) = entries.get("commondir") { Path::new( - std::str::from_utf8(child.lock().file_content("commondir".as_ref())?) + std::str::from_utf8(child.file_content("commondir".as_ref())?) .context("commondir content")?, ) .to_owned() @@ -1420,15 +1531,14 @@ impl FakeFs { canonical_path.clone() }; let repo_state = git_repo_state.get_or_insert_with(|| { - Arc::new(Mutex::new(FakeGitRepositoryState::new( - state.git_event_tx.clone(), - ))) + Arc::new(Mutex::new(FakeGitRepositoryState::new(git_event_tx))) }); let mut repo_state = repo_state.lock(); let result = f(&mut repo_state, &canonical_path, &common_dir); if emit_git_event { + drop(repo_state); state.emit_event([(canonical_path, None)]); } @@ -1655,14 +1765,12 @@ impl FakeFs { pub fn paths(&self, include_dot_git: bool) -> Vec { let mut result = Vec::new(); let mut queue = collections::VecDeque::new(); - queue.push_back(( - PathBuf::from(util::path!("/")), - self.state.lock().root.clone(), - )); + let state = &*self.state.lock(); + queue.push_back((PathBuf::from(util::path!("/")), &state.root)); while let Some((path, entry)) = queue.pop_front() { - if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() { + if let FakeFsEntry::Dir { entries, .. } = entry { for (name, entry) in entries { - queue.push_back((path.join(name), entry.clone())); + queue.push_back((path.join(name), entry)); } } if include_dot_git @@ -1679,14 +1787,12 @@ impl FakeFs { pub fn directories(&self, include_dot_git: bool) -> Vec { let mut result = Vec::new(); let mut queue = collections::VecDeque::new(); - queue.push_back(( - PathBuf::from(util::path!("/")), - self.state.lock().root.clone(), - )); + let state = &*self.state.lock(); + queue.push_back((PathBuf::from(util::path!("/")), &state.root)); while let Some((path, entry)) = queue.pop_front() { - if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() { + if let FakeFsEntry::Dir { entries, .. } = entry { for (name, entry) in entries { - queue.push_back((path.join(name), entry.clone())); + queue.push_back((path.join(name), entry)); } if include_dot_git || !path @@ -1703,17 +1809,14 @@ impl FakeFs { pub fn files(&self) -> Vec { let mut result = Vec::new(); let mut queue = collections::VecDeque::new(); - queue.push_back(( - PathBuf::from(util::path!("/")), - self.state.lock().root.clone(), - )); + let state = &*self.state.lock(); + queue.push_back((PathBuf::from(util::path!("/")), &state.root)); while let Some((path, entry)) = queue.pop_front() { - let e = entry.lock(); - match &*e { + match entry { FakeFsEntry::File { .. } => result.push(path), FakeFsEntry::Dir { entries, .. } => { for (name, entry) in entries { - queue.push_back((path.join(name), entry.clone())); + queue.push_back((path.join(name), entry)); } } FakeFsEntry::Symlink { .. } => {} @@ -1725,13 +1828,10 @@ impl FakeFs { pub fn files_with_contents(&self, prefix: &Path) -> Vec<(PathBuf, Vec)> { let mut result = Vec::new(); let mut queue = collections::VecDeque::new(); - queue.push_back(( - PathBuf::from(util::path!("/")), - self.state.lock().root.clone(), - )); + let state = &*self.state.lock(); + queue.push_back((PathBuf::from(util::path!("/")), &state.root)); while let Some((path, entry)) = queue.pop_front() { - let e = entry.lock(); - match &*e { + match entry { FakeFsEntry::File { content, .. } => { if path.starts_with(prefix) { result.push((path, content.clone())); @@ -1739,7 +1839,7 @@ impl FakeFs { } FakeFsEntry::Dir { entries, .. } => { for (name, entry) in entries { - queue.push_back((path.join(name), entry.clone())); + queue.push_back((path.join(name), entry)); } } FakeFsEntry::Symlink { .. } => {} @@ -1805,10 +1905,7 @@ impl FakeFsEntry { } } - fn dir_entries( - &mut self, - path: &Path, - ) -> Result<&mut BTreeMap>>> { + fn dir_entries(&mut self, path: &Path) -> Result<&mut BTreeMap> { if let Self::Dir { entries, .. } = self { Ok(entries) } else { @@ -1855,12 +1952,12 @@ struct FakeHandle { impl FileHandle for FakeHandle { fn current_path(&self, fs: &Arc) -> Result { let fs = fs.as_fake(); - let state = fs.state.lock(); - let Some(target) = state.moves.get(&self.inode) else { + let mut state = fs.state.lock(); + let Some(target) = state.moves.get(&self.inode).cloned() else { anyhow::bail!("fake fd not moved") }; - if state.try_read_path(&target, false).is_some() { + if state.try_entry(&target, false).is_some() { return Ok(target.clone()); } anyhow::bail!("fake fd target not found") @@ -1888,13 +1985,13 @@ impl Fs for FakeFs { state.write_path(&cur_path, |entry| { entry.or_insert_with(|| { created_dirs.push((cur_path.clone(), Some(PathEventKind::Created))); - Arc::new(Mutex::new(FakeFsEntry::Dir { + FakeFsEntry::Dir { inode, mtime, len: 0, entries: Default::default(), git_repo_state: None, - })) + } }); Ok(()) })? @@ -1909,13 +2006,13 @@ impl Fs for FakeFs { let mut state = self.state.lock(); let inode = state.get_and_increment_inode(); let mtime = state.get_and_increment_mtime(); - let file = Arc::new(Mutex::new(FakeFsEntry::File { + let file = FakeFsEntry::File { inode, mtime, len: 0, content: Vec::new(), git_dir_path: None, - })); + }; let mut kind = Some(PathEventKind::Created); state.write_path(path, |entry| { match entry { @@ -1939,7 +2036,7 @@ impl Fs for FakeFs { async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()> { let mut state = self.state.lock(); - let file = Arc::new(Mutex::new(FakeFsEntry::Symlink { target })); + let file = FakeFsEntry::Symlink { target }; state .write_path(path.as_ref(), move |e| match e { btree_map::Entry::Vacant(e) => { @@ -2002,7 +2099,7 @@ impl Fs for FakeFs { } })?; - let inode = match *moved_entry.lock() { + let inode = match moved_entry { FakeFsEntry::File { inode, .. } => inode, FakeFsEntry::Dir { inode, .. } => inode, _ => 0, @@ -2051,8 +2148,8 @@ impl Fs for FakeFs { let mut state = self.state.lock(); let mtime = state.get_and_increment_mtime(); let inode = state.get_and_increment_inode(); - let source_entry = state.read_path(&source)?; - let content = source_entry.lock().file_content(&source)?.clone(); + let source_entry = state.entry(&source)?; + let content = source_entry.file_content(&source)?.clone(); let mut kind = Some(PathEventKind::Created); state.write_path(&target, |e| match e { btree_map::Entry::Occupied(e) => { @@ -2066,13 +2163,13 @@ impl Fs for FakeFs { } } btree_map::Entry::Vacant(e) => Ok(Some( - e.insert(Arc::new(Mutex::new(FakeFsEntry::File { + e.insert(FakeFsEntry::File { inode, mtime, len: content.len() as u64, content, git_dir_path: None, - }))) + }) .clone(), )), })?; @@ -2088,8 +2185,7 @@ impl Fs for FakeFs { let base_name = path.file_name().context("cannot remove the root")?; let mut state = self.state.lock(); - let parent_entry = state.read_path(parent_path)?; - let mut parent_entry = parent_entry.lock(); + let parent_entry = state.entry(parent_path)?; let entry = parent_entry .dir_entries(parent_path)? .entry(base_name.to_str().unwrap().into()); @@ -2100,15 +2196,14 @@ impl Fs for FakeFs { anyhow::bail!("{path:?} does not exist"); } } - btree_map::Entry::Occupied(e) => { + btree_map::Entry::Occupied(mut entry) => { { - let mut entry = e.get().lock(); - let children = entry.dir_entries(&path)?; + let children = entry.get_mut().dir_entries(&path)?; if !options.recursive && !children.is_empty() { anyhow::bail!("{path:?} is not empty"); } } - e.remove(); + entry.remove(); } } state.emit_event([(path, Some(PathEventKind::Removed))]); @@ -2122,8 +2217,7 @@ impl Fs for FakeFs { let parent_path = path.parent().context("cannot remove the root")?; let base_name = path.file_name().unwrap(); let mut state = self.state.lock(); - let parent_entry = state.read_path(parent_path)?; - let mut parent_entry = parent_entry.lock(); + let parent_entry = state.entry(parent_path)?; let entry = parent_entry .dir_entries(parent_path)? .entry(base_name.to_str().unwrap().into()); @@ -2133,9 +2227,9 @@ impl Fs for FakeFs { anyhow::bail!("{path:?} does not exist"); } } - btree_map::Entry::Occupied(e) => { - e.get().lock().file_content(&path)?; - e.remove(); + btree_map::Entry::Occupied(mut entry) => { + entry.get_mut().file_content(&path)?; + entry.remove(); } } state.emit_event([(path, Some(PathEventKind::Removed))]); @@ -2149,12 +2243,10 @@ impl Fs for FakeFs { async fn open_handle(&self, path: &Path) -> Result> { self.simulate_random_delay().await; - let state = self.state.lock(); - let entry = state.read_path(&path)?; - let entry = entry.lock(); - let inode = match *entry { - FakeFsEntry::File { inode, .. } => inode, - FakeFsEntry::Dir { inode, .. } => inode, + let mut state = self.state.lock(); + let inode = match state.entry(&path)? { + FakeFsEntry::File { inode, .. } => *inode, + FakeFsEntry::Dir { inode, .. } => *inode, _ => unreachable!(), }; Ok(Arc::new(FakeHandle { inode })) @@ -2204,8 +2296,8 @@ impl Fs for FakeFs { let path = normalize_path(path); self.simulate_random_delay().await; let state = self.state.lock(); - let (_, canonical_path) = state - .try_read_path(&path, true) + let canonical_path = state + .canonicalize(&path, true) .with_context(|| format!("path does not exist: {path:?}"))?; Ok(canonical_path) } @@ -2213,9 +2305,9 @@ impl Fs for FakeFs { async fn is_file(&self, path: &Path) -> bool { let path = normalize_path(path); self.simulate_random_delay().await; - let state = self.state.lock(); - if let Some((entry, _)) = state.try_read_path(&path, true) { - entry.lock().is_file() + let mut state = self.state.lock(); + if let Some((entry, _)) = state.try_entry(&path, true) { + entry.is_file() } else { false } @@ -2232,17 +2324,16 @@ impl Fs for FakeFs { let path = normalize_path(path); let mut state = self.state.lock(); state.metadata_call_count += 1; - if let Some((mut entry, _)) = state.try_read_path(&path, false) { - let is_symlink = entry.lock().is_symlink(); + if let Some((mut entry, _)) = state.try_entry(&path, false) { + let is_symlink = entry.is_symlink(); if is_symlink { - if let Some(e) = state.try_read_path(&path, true).map(|e| e.0) { + if let Some(e) = state.try_entry(&path, true).map(|e| e.0) { entry = e; } else { return Ok(None); } } - let entry = entry.lock(); Ok(Some(match &*entry { FakeFsEntry::File { inode, mtime, len, .. @@ -2274,12 +2365,11 @@ impl Fs for FakeFs { async fn read_link(&self, path: &Path) -> Result { self.simulate_random_delay().await; let path = normalize_path(path); - let state = self.state.lock(); + let mut state = self.state.lock(); let (entry, _) = state - .try_read_path(&path, false) + .try_entry(&path, false) .with_context(|| format!("path does not exist: {path:?}"))?; - let entry = entry.lock(); - if let FakeFsEntry::Symlink { target } = &*entry { + if let FakeFsEntry::Symlink { target } = entry { Ok(target.clone()) } else { anyhow::bail!("not a symlink: {path:?}") @@ -2294,8 +2384,7 @@ impl Fs for FakeFs { let path = normalize_path(path); let mut state = self.state.lock(); state.read_dir_call_count += 1; - let entry = state.read_path(&path)?; - let mut entry = entry.lock(); + let entry = state.entry(&path)?; let children = entry.dir_entries(&path)?; let paths = children .keys() @@ -2359,6 +2448,7 @@ impl Fs for FakeFs { dot_git_path: abs_dot_git.to_path_buf(), repository_dir_path: repository_dir_path.to_owned(), common_dir_path: common_dir_path.to_owned(), + checkpoints: Arc::default(), }) as _ }, ) diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml index ab2210094d..74656f1d4c 100644 --- a/crates/git/Cargo.toml +++ b/crates/git/Cargo.toml @@ -12,7 +12,7 @@ workspace = true path = "src/git.rs" [features] -test-support = [] +test-support = ["rand"] [dependencies] anyhow.workspace = true @@ -26,6 +26,7 @@ http_client.workspace = true log.workspace = true parking_lot.workspace = true regex.workspace = true +rand = { workspace = true, optional = true } rope.workspace = true schemars.workspace = true serde.workspace = true @@ -47,3 +48,4 @@ text = { workspace = true, features = ["test-support"] } unindent.workspace = true gpui = { workspace = true, features = ["test-support"] } tempfile.workspace = true +rand.workspace = true diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index e6336eb656..e84014129c 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -119,6 +119,13 @@ impl Oid { Ok(Self(oid)) } + #[cfg(any(test, feature = "test-support"))] + pub fn random(rng: &mut impl rand::Rng) -> Self { + let mut bytes = [0; 20]; + rng.fill(&mut bytes); + Self::from_bytes(&bytes).unwrap() + } + pub fn as_bytes(&self) -> &[u8] { self.0.as_bytes() } From 6c1f19571a5647bff02ef98b7de159754a884504 Mon Sep 17 00:00:00 2001 From: Gilmar Sales Date: Wed, 13 Aug 2025 12:59:59 -0300 Subject: [PATCH 313/693] Enhance icon detection for files with custom suffixes (#34170) Fixes custom file suffixes (module.ts) of some icon themes like: - **Symbols Icon Theme** image - **Bearded Icon Theme** image Release Notes: - Fixed icon detection for files with custom suffixes like `module.ts` that are overwritten by the language's icon `.ts` --- crates/file_icons/src/file_icons.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/crates/file_icons/src/file_icons.rs b/crates/file_icons/src/file_icons.rs index 2f159771b1..82a8e05d85 100644 --- a/crates/file_icons/src/file_icons.rs +++ b/crates/file_icons/src/file_icons.rs @@ -33,13 +33,23 @@ impl FileIcons { // TODO: Associate a type with the languages and have the file's language // override these associations - // check if file name is in suffixes - // e.g. catch file named `eslint.config.js` instead of `.eslint.config.js` - if let Some(typ) = path.file_name().and_then(|typ| typ.to_str()) { + if let Some(mut typ) = path.file_name().and_then(|typ| typ.to_str()) { + // check if file name is in suffixes + // e.g. catch file named `eslint.config.js` instead of `.eslint.config.js` let maybe_path = get_icon_from_suffix(typ); if maybe_path.is_some() { return maybe_path; } + + // check if suffix based on first dot is in suffixes + // e.g. consider `module.js` as suffix to angular's module file named `auth.module.js` + while let Some((_, suffix)) = typ.split_once('.') { + let maybe_path = get_icon_from_suffix(suffix); + if maybe_path.is_some() { + return maybe_path; + } + typ = suffix; + } } // primary case: check if the files extension or the hidden file name From a7442d8880df3019fd47479849cd4b9653bae364 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 13 Aug 2025 12:02:14 -0400 Subject: [PATCH 314/693] onboarding: Add more telemetry (#36121) 1. Welcome Page Open 2. Welcome Nav clicked 3. Skip clicked 4. Font changed 5. Import settings clicked 6. Inlay Hints 7. Git Blame 8. Format on Save 9. Font Ligature 10. Ai Enabled 11. Ai Provider Modal open Release Notes: - N/A --------- Co-authored-by: Marshall Bowers --- Cargo.lock | 1 + crates/onboarding/Cargo.toml | 1 + crates/onboarding/src/ai_setup_page.rs | 24 ++++++++--- crates/onboarding/src/editing_page.rs | 59 +++++++++++++++++++++++--- crates/onboarding/src/onboarding.rs | 45 +++++++++++++++++--- 5 files changed, 111 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9078b32f7a..b67188c53b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11172,6 +11172,7 @@ dependencies = [ "schemars", "serde", "settings", + "telemetry", "theme", "ui", "util", diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index cb07bb5dab..8aed1e3287 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -38,6 +38,7 @@ project.workspace = true schemars.workspace = true serde.workspace = true settings.workspace = true +telemetry.workspace = true theme.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index 8203f96479..bb1932bdf2 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -188,6 +188,11 @@ fn render_llm_provider_card( workspace .update(cx, |workspace, cx| { workspace.toggle_modal(window, cx, |window, cx| { + telemetry::event!( + "Welcome AI Modal Opened", + provider = provider.name().0, + ); + let modal = AiConfigurationModal::new( provider.clone(), window, @@ -245,16 +250,25 @@ pub(crate) fn render_ai_setup_page( ToggleState::Selected }, |&toggle_state, _, cx| { + let enabled = match toggle_state { + ToggleState::Indeterminate => { + return; + } + ToggleState::Unselected => true, + ToggleState::Selected => false, + }; + + telemetry::event!( + "Welcome AI Enabled", + toggle = if enabled { "on" } else { "off" }, + ); + let fs = ::global(cx); update_settings_file::( fs, cx, move |ai_settings: &mut Option, _| { - *ai_settings = match toggle_state { - ToggleState::Indeterminate => None, - ToggleState::Unselected => Some(true), - ToggleState::Selected => Some(false), - }; + *ai_settings = Some(enabled); }, ); }, diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index c69bd3852e..aa7f4eee74 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -35,6 +35,11 @@ fn write_show_mini_map(show: ShowMinimap, cx: &mut App) { EditorSettings::override_global(curr_settings, cx); update_settings_file::(fs, cx, move |editor_settings, _| { + telemetry::event!( + "Welcome Minimap Clicked", + from = editor_settings.minimap.unwrap_or_default(), + to = show + ); editor_settings.minimap.get_or_insert_default().show = Some(show); }); } @@ -71,7 +76,7 @@ fn read_git_blame(cx: &App) -> bool { ProjectSettings::get_global(cx).git.inline_blame_enabled() } -fn set_git_blame(enabled: bool, cx: &mut App) { +fn write_git_blame(enabled: bool, cx: &mut App) { let fs = ::global(cx); let mut curr_settings = ProjectSettings::get_global(cx).clone(); @@ -95,6 +100,12 @@ fn write_ui_font_family(font: SharedString, cx: &mut App) { let fs = ::global(cx); update_settings_file::(fs, cx, move |theme_settings, _| { + telemetry::event!( + "Welcome Font Changed", + type = "ui font", + old = theme_settings.ui_font_family, + new = font.clone() + ); theme_settings.ui_font_family = Some(FontFamilyName(font.into())); }); } @@ -119,6 +130,13 @@ fn write_buffer_font_family(font_family: SharedString, cx: &mut App) { let fs = ::global(cx); update_settings_file::(fs, cx, move |theme_settings, _| { + telemetry::event!( + "Welcome Font Changed", + type = "editor font", + old = theme_settings.buffer_font_family, + new = font_family.clone() + ); + theme_settings.buffer_font_family = Some(FontFamilyName(font_family.into())); }); } @@ -197,7 +215,7 @@ fn render_setting_import_button( .color(Color::Muted) .size(IconSize::XSmall), ) - .child(Label::new(label)), + .child(Label::new(label.clone())), ) .when(imported, |this| { this.child( @@ -212,7 +230,10 @@ fn render_setting_import_button( ) }), ) - .on_click(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)), + .on_click(move |_, window, cx| { + telemetry::event!("Welcome Import Settings", import_source = label,); + window.dispatch_action(action.boxed_clone(), cx); + }), ) } @@ -605,7 +626,13 @@ fn render_popular_settings_section( ui::ToggleState::Unselected }, |toggle_state, _, cx| { - write_font_ligatures(toggle_state == &ToggleState::Selected, cx); + let enabled = toggle_state == &ToggleState::Selected; + telemetry::event!( + "Welcome Font Ligature", + options = if enabled { "on" } else { "off" }, + ); + + write_font_ligatures(enabled, cx); }, ) .tab_index({ @@ -625,7 +652,13 @@ fn render_popular_settings_section( ui::ToggleState::Unselected }, |toggle_state, _, cx| { - write_format_on_save(toggle_state == &ToggleState::Selected, cx); + let enabled = toggle_state == &ToggleState::Selected; + telemetry::event!( + "Welcome Format On Save Changed", + options = if enabled { "on" } else { "off" }, + ); + + write_format_on_save(enabled, cx); }, ) .tab_index({ @@ -644,7 +677,13 @@ fn render_popular_settings_section( ui::ToggleState::Unselected }, |toggle_state, _, cx| { - write_inlay_hints(toggle_state == &ToggleState::Selected, cx); + let enabled = toggle_state == &ToggleState::Selected; + telemetry::event!( + "Welcome Inlay Hints Changed", + options = if enabled { "on" } else { "off" }, + ); + + write_inlay_hints(enabled, cx); }, ) .tab_index({ @@ -663,7 +702,13 @@ fn render_popular_settings_section( ui::ToggleState::Unselected }, |toggle_state, _, cx| { - set_git_blame(toggle_state == &ToggleState::Selected, cx); + let enabled = toggle_state == &ToggleState::Selected; + telemetry::event!( + "Welcome Git Blame Changed", + options = if enabled { "on" } else { "off" }, + ); + + write_git_blame(enabled, cx); }, ) .tab_index({ diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index c86871c919..3fb6f9b520 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -214,6 +214,7 @@ pub fn init(cx: &mut App) { } pub fn show_onboarding_view(app_state: Arc, cx: &mut App) -> Task> { + telemetry::event!("Onboarding Page Opened"); open_new( Default::default(), app_state, @@ -242,6 +243,16 @@ enum SelectedPage { AiSetup, } +impl SelectedPage { + fn name(&self) -> &'static str { + match self { + SelectedPage::Basics => "Basics", + SelectedPage::Editing => "Editing", + SelectedPage::AiSetup => "AI Setup", + } + } +} + struct Onboarding { workspace: WeakEntity, focus_handle: FocusHandle, @@ -261,7 +272,21 @@ impl Onboarding { }) } - fn set_page(&mut self, page: SelectedPage, cx: &mut Context) { + fn set_page( + &mut self, + page: SelectedPage, + clicked: Option<&'static str>, + cx: &mut Context, + ) { + if let Some(click) = clicked { + telemetry::event!( + "Welcome Tab Clicked", + from = self.selected_page.name(), + to = page.name(), + clicked = click, + ); + } + self.selected_page = page; cx.notify(); cx.emit(ItemEvent::UpdateTab); @@ -325,8 +350,13 @@ impl Onboarding { gpui::Empty.into_any_element(), IntoElement::into_any_element, )) - .on_click(cx.listener(move |this, _, _, cx| { - this.set_page(page, cx); + .on_click(cx.listener(move |this, click_event, _, cx| { + let click = match click_event { + gpui::ClickEvent::Mouse(_) => "mouse", + gpui::ClickEvent::Keyboard(_) => "keyboard", + }; + + this.set_page(page, Some(click), cx); })) }) } @@ -475,6 +505,7 @@ impl Onboarding { } fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) { + telemetry::event!("Welcome Skip Clicked"); go_to_welcome_page(cx); } @@ -532,13 +563,13 @@ impl Render for Onboarding { .on_action(Self::handle_sign_in) .on_action(Self::handle_open_account) .on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| { - this.set_page(SelectedPage::Basics, cx); + this.set_page(SelectedPage::Basics, Some("action"), cx); })) .on_action(cx.listener(|this, _: &ActivateEditingPage, _, cx| { - this.set_page(SelectedPage::Editing, cx); + this.set_page(SelectedPage::Editing, Some("action"), cx); })) .on_action(cx.listener(|this, _: &ActivateAISetupPage, _, cx| { - this.set_page(SelectedPage::AiSetup, cx); + this.set_page(SelectedPage::AiSetup, Some("action"), cx); })) .on_action(cx.listener(|_, _: &menu::SelectNext, window, cx| { window.focus_next(); @@ -806,7 +837,7 @@ impl workspace::SerializableItem for Onboarding { if let Some(page) = page { zlog::info!("Onboarding page {page:?} loaded"); onboarding_page.update(cx, |onboarding_page, cx| { - onboarding_page.set_page(page, cx); + onboarding_page.set_page(page, None, cx); }) } onboarding_page From d9a94a54966cf4e1ebd3aa292ed24b55d7927afb Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 13 Aug 2025 13:18:24 -0300 Subject: [PATCH 315/693] onboarding: Remove feature flag and old welcome crate (#36110) Release Notes: - N/A --------- Co-authored-by: MrSubidubi Co-authored-by: Anthony --- Cargo.lock | 30 -- Cargo.toml | 2 - crates/onboarding/Cargo.toml | 2 - .../src/base_keymap_picker.rs | 2 +- .../src/multibuffer_hint.rs | 0 crates/onboarding/src/onboarding.rs | 52 +- crates/welcome/Cargo.toml | 40 -- crates/welcome/LICENSE-GPL | 1 - crates/welcome/src/welcome.rs | 446 ------------------ crates/workspace/src/workspace.rs | 2 - crates/zed/Cargo.toml | 1 - crates/zed/src/main.rs | 5 +- crates/zed/src/zed.rs | 5 +- crates/zed/src/zed/app_menus.rs | 2 +- crates/zed/src/zed/open_listener.rs | 5 +- docs/src/telemetry.md | 5 +- 16 files changed, 26 insertions(+), 574 deletions(-) rename crates/{welcome => onboarding}/src/base_keymap_picker.rs (99%) rename crates/{welcome => onboarding}/src/multibuffer_hint.rs (100%) delete mode 100644 crates/welcome/Cargo.toml delete mode 120000 crates/welcome/LICENSE-GPL delete mode 100644 crates/welcome/src/welcome.rs diff --git a/Cargo.lock b/Cargo.lock index b67188c53b..8458c4af4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11152,12 +11152,10 @@ dependencies = [ "ai_onboarding", "anyhow", "client", - "command_palette_hooks", "component", "db", "documented", "editor", - "feature_flags", "fs", "fuzzy", "git", @@ -18888,33 +18886,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" -[[package]] -name = "welcome" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "component", - "db", - "documented", - "editor", - "fuzzy", - "gpui", - "install_cli", - "language", - "picker", - "project", - "serde", - "settings", - "telemetry", - "ui", - "util", - "vim_mode_setting", - "workspace", - "workspace-hack", - "zed_actions", -] - [[package]] name = "which" version = "4.4.2" @@ -20669,7 +20640,6 @@ dependencies = [ "watch", "web_search", "web_search_providers", - "welcome", "windows 0.61.1", "winresource", "workspace", diff --git a/Cargo.toml b/Cargo.toml index 8cb3c34a8a..1baa6d3d74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -185,7 +185,6 @@ members = [ "crates/watch", "crates/web_search", "crates/web_search_providers", - "crates/welcome", "crates/workspace", "crates/worktree", "crates/x_ai", @@ -412,7 +411,6 @@ vim_mode_setting = { path = "crates/vim_mode_setting" } watch = { path = "crates/watch" } web_search = { path = "crates/web_search" } web_search_providers = { path = "crates/web_search_providers" } -welcome = { path = "crates/welcome" } workspace = { path = "crates/workspace" } worktree = { path = "crates/worktree" } x_ai = { path = "crates/x_ai" } diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index 8aed1e3287..4157be3172 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -18,12 +18,10 @@ default = [] ai_onboarding.workspace = true anyhow.workspace = true client.workspace = true -command_palette_hooks.workspace = true component.workspace = true db.workspace = true documented.workspace = true editor.workspace = true -feature_flags.workspace = true fs.workspace = true fuzzy.workspace = true git.workspace = true diff --git a/crates/welcome/src/base_keymap_picker.rs b/crates/onboarding/src/base_keymap_picker.rs similarity index 99% rename from crates/welcome/src/base_keymap_picker.rs rename to crates/onboarding/src/base_keymap_picker.rs index 92317ca711..0ac07d9a9d 100644 --- a/crates/welcome/src/base_keymap_picker.rs +++ b/crates/onboarding/src/base_keymap_picker.rs @@ -12,7 +12,7 @@ use util::ResultExt; use workspace::{ModalView, Workspace, ui::HighlightedLabel}; actions!( - welcome, + zed, [ /// Toggles the base keymap selector modal. ToggleBaseKeymapSelector diff --git a/crates/welcome/src/multibuffer_hint.rs b/crates/onboarding/src/multibuffer_hint.rs similarity index 100% rename from crates/welcome/src/multibuffer_hint.rs rename to crates/onboarding/src/multibuffer_hint.rs diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 3fb6f9b520..2444e5d44a 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -1,8 +1,7 @@ -use crate::welcome::{ShowWelcome, WelcomePage}; +pub use crate::welcome::ShowWelcome; +use crate::{multibuffer_hint::MultibufferHint, welcome::WelcomePage}; use client::{Client, UserStore, zed_urls}; -use command_palette_hooks::CommandPaletteFilter; use db::kvp::KEY_VALUE_STORE; -use feature_flags::{FeatureFlag, FeatureFlagViewExt as _}; use fs::Fs; use gpui::{ Action, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, EventEmitter, @@ -27,17 +26,13 @@ use workspace::{ }; mod ai_setup_page; +mod base_keymap_picker; mod basics_page; mod editing_page; +pub mod multibuffer_hint; mod theme_preview; mod welcome; -pub struct OnBoardingFeatureFlag {} - -impl FeatureFlag for OnBoardingFeatureFlag { - const NAME: &'static str = "onboarding"; -} - /// Imports settings from Visual Studio Code. #[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)] #[action(namespace = zed)] @@ -57,6 +52,7 @@ pub struct ImportCursorSettings { } pub const FIRST_OPEN: &str = "first_open"; +pub const DOCS_URL: &str = "https://zed.dev/docs/"; actions!( zed, @@ -80,11 +76,19 @@ actions!( /// Sign in while in the onboarding flow. SignIn, /// Open the user account in zed.dev while in the onboarding flow. - OpenAccount + OpenAccount, + /// Resets the welcome screen hints to their initial state. + ResetHints ] ); pub fn init(cx: &mut App) { + cx.observe_new(|workspace: &mut Workspace, _, _cx| { + workspace + .register_action(|_workspace, _: &ResetHints, _, cx| MultibufferHint::set_count(0, cx)); + }) + .detach(); + cx.on_action(|_: &OpenOnboarding, cx| { with_active_or_new_workspace(cx, |workspace, window, cx| { workspace @@ -182,34 +186,8 @@ pub fn init(cx: &mut App) { }) .detach(); - cx.observe_new::(|_, window, cx| { - let Some(window) = window else { - return; - }; + base_keymap_picker::init(cx); - let onboarding_actions = [ - std::any::TypeId::of::(), - std::any::TypeId::of::(), - ]; - - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.hide_action_types(&onboarding_actions); - }); - - cx.observe_flag::(window, move |is_enabled, _, _, cx| { - if is_enabled { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.show_action_types(onboarding_actions.iter()); - }); - } else { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.hide_action_types(&onboarding_actions); - }); - } - }) - .detach(); - }) - .detach(); register_serializable_item::(cx); } diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml deleted file mode 100644 index acb3fe0f84..0000000000 --- a/crates/welcome/Cargo.toml +++ /dev/null @@ -1,40 +0,0 @@ -[package] -name = "welcome" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/welcome.rs" - -[features] -test-support = [] - -[dependencies] -anyhow.workspace = true -client.workspace = true -component.workspace = true -db.workspace = true -documented.workspace = true -fuzzy.workspace = true -gpui.workspace = true -install_cli.workspace = true -language.workspace = true -picker.workspace = true -project.workspace = true -serde.workspace = true -settings.workspace = true -telemetry.workspace = true -ui.workspace = true -util.workspace = true -vim_mode_setting.workspace = true -workspace-hack.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -editor = { workspace = true, features = ["test-support"] } diff --git a/crates/welcome/LICENSE-GPL b/crates/welcome/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/welcome/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs deleted file mode 100644 index b0a1c316f4..0000000000 --- a/crates/welcome/src/welcome.rs +++ /dev/null @@ -1,446 +0,0 @@ -use client::{TelemetrySettings, telemetry::Telemetry}; -use db::kvp::KEY_VALUE_STORE; -use gpui::{ - Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, - ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, actions, svg, -}; -use language::language_settings::{EditPredictionProvider, all_language_settings}; -use project::DisableAiSettings; -use settings::{Settings, SettingsStore}; -use std::sync::Arc; -use ui::{CheckboxWithLabel, ElevationIndex, Tooltip, prelude::*}; -use util::ResultExt; -use vim_mode_setting::VimModeSetting; -use workspace::{ - AppState, Welcome, Workspace, WorkspaceId, - dock::DockPosition, - item::{Item, ItemEvent}, - open_new, -}; - -pub use multibuffer_hint::*; - -mod base_keymap_picker; -mod multibuffer_hint; - -actions!( - welcome, - [ - /// Resets the welcome screen hints to their initial state. - ResetHints - ] -); - -pub const FIRST_OPEN: &str = "first_open"; -pub const DOCS_URL: &str = "https://zed.dev/docs/"; - -pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, _, _cx| { - workspace.register_action(|workspace, _: &Welcome, window, cx| { - let welcome_page = WelcomePage::new(workspace, cx); - workspace.add_item_to_active_pane(Box::new(welcome_page), None, true, window, cx) - }); - workspace - .register_action(|_workspace, _: &ResetHints, _, cx| MultibufferHint::set_count(0, cx)); - }) - .detach(); - - base_keymap_picker::init(cx); -} - -pub fn show_welcome_view(app_state: Arc, cx: &mut App) -> Task> { - open_new( - Default::default(), - app_state, - cx, - |workspace, window, cx| { - workspace.toggle_dock(DockPosition::Left, window, cx); - let welcome_page = WelcomePage::new(workspace, cx); - workspace.add_item_to_center(Box::new(welcome_page.clone()), window, cx); - - window.focus(&welcome_page.focus_handle(cx)); - - cx.notify(); - - db::write_and_log(cx, || { - KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string()) - }); - }, - ) -} - -pub struct WelcomePage { - workspace: WeakEntity, - focus_handle: FocusHandle, - telemetry: Arc, - _settings_subscription: Subscription, -} - -impl Render for WelcomePage { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let edit_prediction_provider_is_zed = - all_language_settings(None, cx).edit_predictions.provider - == EditPredictionProvider::Zed; - - let edit_prediction_label = if edit_prediction_provider_is_zed { - "Edit Prediction Enabled" - } else { - "Try Edit Prediction" - }; - - h_flex() - .size_full() - .bg(cx.theme().colors().editor_background) - .key_context("Welcome") - .track_focus(&self.focus_handle(cx)) - .child( - v_flex() - .gap_8() - .mx_auto() - .child( - v_flex() - .w_full() - .child( - svg() - .path("icons/logo_96.svg") - .text_color(cx.theme().colors().icon_disabled) - .w(px(40.)) - .h(px(40.)) - .mx_auto() - .mb_4(), - ) - .child( - h_flex() - .w_full() - .justify_center() - .child(Headline::new("Welcome to Zed")), - ) - .child( - h_flex().w_full().justify_center().child( - Label::new("The editor for what's next") - .color(Color::Muted) - .italic(), - ), - ), - ) - .child( - h_flex() - .items_start() - .gap_8() - .child( - v_flex() - .gap_2() - .pr_8() - .border_r_1() - .border_color(cx.theme().colors().border_variant) - .child( - self.section_label( cx).child( - Label::new("Get Started") - .size(LabelSize::XSmall) - .color(Color::Muted), - ), - ) - .child( - Button::new("choose-theme", "Choose a Theme") - .icon(IconName::SwatchBook) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .icon_position(IconPosition::Start) - .on_click(cx.listener(|this, _, window, cx| { - telemetry::event!("Welcome Theme Changed"); - this.workspace - .update(cx, |_workspace, cx| { - window.dispatch_action(zed_actions::theme_selector::Toggle::default().boxed_clone(), cx); - }) - .ok(); - })), - ) - .child( - Button::new("choose-keymap", "Choose a Keymap") - .icon(IconName::Keyboard) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .icon_position(IconPosition::Start) - .on_click(cx.listener(|this, _, window, cx| { - telemetry::event!("Welcome Keymap Changed"); - this.workspace - .update(cx, |workspace, cx| { - base_keymap_picker::toggle( - workspace, - &Default::default(), - window, cx, - ) - }) - .ok(); - })), - ) - .when(!DisableAiSettings::get_global(cx).disable_ai, |parent| { - parent.child( - Button::new( - "edit_prediction_onboarding", - edit_prediction_label, - ) - .disabled(edit_prediction_provider_is_zed) - .icon(IconName::ZedPredict) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .icon_position(IconPosition::Start) - .on_click( - cx.listener(|_, _, window, cx| { - telemetry::event!("Welcome Screen Try Edit Prediction clicked"); - window.dispatch_action(zed_actions::OpenZedPredictOnboarding.boxed_clone(), cx); - }), - ), - ) - }) - .child( - Button::new("edit settings", "Edit Settings") - .icon(IconName::Settings) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .icon_position(IconPosition::Start) - .on_click(cx.listener(|_, _, window, cx| { - telemetry::event!("Welcome Settings Edited"); - window.dispatch_action(Box::new( - zed_actions::OpenSettings, - ), cx); - })), - ) - - ) - .child( - v_flex() - .gap_2() - .child( - self.section_label(cx).child( - Label::new("Resources") - .size(LabelSize::XSmall) - .color(Color::Muted), - ), - ) - .when(cfg!(target_os = "macos"), |el| { - el.child( - Button::new("install-cli", "Install the CLI") - .icon(IconName::Terminal) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .icon_position(IconPosition::Start) - .on_click(cx.listener(|this, _, window, cx| { - telemetry::event!("Welcome CLI Installed"); - this.workspace.update(cx, |_, cx|{ - install_cli::install_cli(window, cx); - }).log_err(); - })), - ) - }) - .child( - Button::new("view-docs", "View Documentation") - .icon(IconName::FileCode) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .icon_position(IconPosition::Start) - .on_click(cx.listener(|_, _, _, cx| { - telemetry::event!("Welcome Documentation Viewed"); - cx.open_url(DOCS_URL); - })), - ) - .child( - Button::new("explore-extensions", "Explore Extensions") - .icon(IconName::Blocks) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .icon_position(IconPosition::Start) - .on_click(cx.listener(|_, _, window, cx| { - telemetry::event!("Welcome Extensions Page Opened"); - window.dispatch_action(Box::new( - zed_actions::Extensions::default(), - ), cx); - })), - ) - ), - ) - .child( - v_container() - .px_2() - .gap_2() - .child( - h_flex() - .justify_between() - .child( - CheckboxWithLabel::new( - "enable-vim", - Label::new("Enable Vim Mode"), - if VimModeSetting::get_global(cx).0 { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - cx.listener(move |this, selection, _window, cx| { - telemetry::event!("Welcome Vim Mode Toggled"); - this.update_settings::( - selection, - cx, - |setting, value| *setting = Some(value), - ); - }), - ) - .fill() - .elevation(ElevationIndex::ElevatedSurface), - ) - .child( - IconButton::new("vim-mode", IconName::Info) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .tooltip( - Tooltip::text( - "You can also toggle Vim Mode via the command palette or Editor Controls menu.") - ), - ), - ) - .child( - CheckboxWithLabel::new( - "enable-crash", - Label::new("Send Crash Reports"), - if TelemetrySettings::get_global(cx).diagnostics { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - cx.listener(move |this, selection, _window, cx| { - telemetry::event!("Welcome Diagnostic Telemetry Toggled"); - this.update_settings::(selection, cx, { - move |settings, value| { - settings.diagnostics = Some(value); - telemetry::event!( - "Settings Changed", - setting = "diagnostic telemetry", - value - ); - } - }); - }), - ) - .fill() - .elevation(ElevationIndex::ElevatedSurface), - ) - .child( - CheckboxWithLabel::new( - "enable-telemetry", - Label::new("Send Telemetry"), - if TelemetrySettings::get_global(cx).metrics { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - cx.listener(move |this, selection, _window, cx| { - telemetry::event!("Welcome Metric Telemetry Toggled"); - this.update_settings::(selection, cx, { - move |settings, value| { - settings.metrics = Some(value); - telemetry::event!( - "Settings Changed", - setting = "metric telemetry", - value - ); - } - }); - }), - ) - .fill() - .elevation(ElevationIndex::ElevatedSurface), - ), - ), - ) - } -} - -impl WelcomePage { - pub fn new(workspace: &Workspace, cx: &mut Context) -> Entity { - let this = cx.new(|cx| { - cx.on_release(|_: &mut Self, _| { - telemetry::event!("Welcome Page Closed"); - }) - .detach(); - - WelcomePage { - focus_handle: cx.focus_handle(), - workspace: workspace.weak_handle(), - telemetry: workspace.client().telemetry().clone(), - _settings_subscription: cx - .observe_global::(move |_, cx| cx.notify()), - } - }); - - this - } - - fn section_label(&self, cx: &mut App) -> Div { - div() - .pl_1() - .font_buffer(cx) - .text_color(Color::Muted.color(cx)) - } - - fn update_settings( - &mut self, - selection: &ToggleState, - cx: &mut Context, - callback: impl 'static + Send + Fn(&mut T::FileContent, bool), - ) { - if let Some(workspace) = self.workspace.upgrade() { - let fs = workspace.read(cx).app_state().fs.clone(); - let selection = *selection; - settings::update_settings_file::(fs, cx, move |settings, _| { - let value = match selection { - ToggleState::Unselected => false, - ToggleState::Selected => true, - _ => return, - }; - - callback(settings, value) - }); - } - } -} - -impl EventEmitter for WelcomePage {} - -impl Focusable for WelcomePage { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl Item for WelcomePage { - type Event = ItemEvent; - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "Welcome".into() - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - Some("Welcome Page Opened") - } - - fn show_toolbar(&self) -> bool { - false - } - - fn clone_on_split( - &self, - _workspace_id: Option, - _: &mut Window, - cx: &mut Context, - ) -> Option> { - Some(cx.new(|cx| WelcomePage { - focus_handle: cx.focus_handle(), - workspace: self.workspace.clone(), - telemetry: self.telemetry.clone(), - _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), - })) - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { - f(*event) - } -} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 98794e54cd..fb78c62f9e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -248,8 +248,6 @@ actions!( ToggleZoom, /// Stops following a collaborator. Unfollow, - /// Shows the welcome screen. - Welcome, /// Restores the banner. RestoreBanner, /// Toggles expansion of the selected item. diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 5997e43864..bdbb39698c 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -157,7 +157,6 @@ vim_mode_setting.workspace = true watch.workspace = true web_search.workspace = true web_search_providers.workspace = true -welcome.workspace = true workspace-hack.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 3084bfddad..fd987ef6c5 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -20,6 +20,7 @@ use gpui::{App, AppContext as _, Application, AsyncApp, Focusable as _, UpdateGl use gpui_tokio::Tokio; use http_client::{Url, read_proxy_from_env}; use language::LanguageRegistry; +use onboarding::{FIRST_OPEN, show_onboarding_view}; use prompt_store::PromptBuilder; use reqwest_client::ReqwestClient; @@ -44,7 +45,6 @@ use theme::{ }; use util::{ResultExt, TryFutureExt, maybe}; use uuid::Uuid; -use welcome::{FIRST_OPEN, show_welcome_view}; use workspace::{ AppState, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, WorkspaceStore, notifications::NotificationId, @@ -623,7 +623,6 @@ pub fn main() { feedback::init(cx); markdown_preview::init(cx); svg_preview::init(cx); - welcome::init(cx); onboarding::init(cx); settings_ui::init(cx); extensions_ui::init(cx); @@ -1044,7 +1043,7 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp } } } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { - cx.update(|cx| show_welcome_view(app_state, cx))?.await?; + cx.update(|cx| show_onboarding_view(app_state, cx))?.await?; } else { cx.update(|cx| { workspace::open_new( diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 8c89a7d85a..23020d3a9b 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -34,6 +34,8 @@ use image_viewer::ImageInfo; use language_tools::lsp_tool::{self, LspTool}; use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType}; use migrator::{migrate_keymap, migrate_settings}; +use onboarding::DOCS_URL; +use onboarding::multibuffer_hint::MultibufferHint; pub use open_listener::*; use outline_panel::OutlinePanel; use paths::{ @@ -67,7 +69,6 @@ use util::markdown::MarkdownString; use util::{ResultExt, asset_str}; use uuid::Uuid; use vim_mode_setting::VimModeSetting; -use welcome::{DOCS_URL, MultibufferHint}; use workspace::notifications::{NotificationId, dismiss_app_notification, show_app_notification}; use workspace::{ AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings, @@ -3975,7 +3976,6 @@ mod tests { client::init(&app_state.client, cx); language::init(cx); workspace::init(app_state.clone(), cx); - welcome::init(cx); onboarding::init(cx); Project::init_settings(cx); app_state @@ -4380,7 +4380,6 @@ mod tests { "toolchain", "variable_list", "vim", - "welcome", "workspace", "zed", "zed_predict_onboarding", diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index 53eec42ba0..9df55a2fb1 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -249,7 +249,7 @@ pub fn app_menus() -> Vec { ), MenuItem::action("View Telemetry", zed_actions::OpenTelemetryLog), MenuItem::action("View Dependency Licenses", zed_actions::OpenLicenses), - MenuItem::action("Show Welcome", workspace::Welcome), + MenuItem::action("Show Welcome", onboarding::ShowWelcome), MenuItem::action("Give Feedback...", zed_actions::feedback::GiveFeedback), MenuItem::separator(), MenuItem::action( diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 2fd9b0a68c..82d3795e94 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -15,6 +15,8 @@ use futures::{FutureExt, SinkExt, StreamExt}; use git_ui::file_diff_view::FileDiffView; use gpui::{App, AsyncApp, Global, WindowHandle}; use language::Point; +use onboarding::FIRST_OPEN; +use onboarding::show_onboarding_view; use recent_projects::{SshSettings, open_ssh_project}; use remote::SshConnectionOptions; use settings::Settings; @@ -24,7 +26,6 @@ use std::thread; use std::time::Duration; use util::ResultExt; use util::paths::PathWithPosition; -use welcome::{FIRST_OPEN, show_welcome_view}; use workspace::item::ItemHandle; use workspace::{AppState, OpenOptions, SerializedWorkspaceLocation, Workspace}; @@ -378,7 +379,7 @@ async fn open_workspaces( if grouped_locations.is_empty() { // If we have no paths to open, show the welcome screen if this is the first launch if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { - cx.update(|cx| show_welcome_view(app_state, cx).detach()) + cx.update(|cx| show_onboarding_view(app_state, cx).detach()) .log_err(); } // If not the first launch, show an empty window with empty editor diff --git a/docs/src/telemetry.md b/docs/src/telemetry.md index 107aef5a96..46c39a88ae 100644 --- a/docs/src/telemetry.md +++ b/docs/src/telemetry.md @@ -4,7 +4,8 @@ Zed collects anonymous telemetry data to help the team understand how people are ## Configuring Telemetry Settings -You have full control over what data is sent out by Zed. To enable or disable some or all telemetry types, open your `settings.json` file via {#action zed::OpenSettings}({#kb zed::OpenSettings}) from the command palette. +You have full control over what data is sent out by Zed. +To enable or disable some or all telemetry types, open your `settings.json` file via {#action zed::OpenSettings}({#kb zed::OpenSettings}) from the command palette. Insert and tweak the following: @@ -15,8 +16,6 @@ Insert and tweak the following: }, ``` -The telemetry settings can also be configured via the welcome screen, which can be invoked via the {#action workspace::Welcome} action in the command palette. - ## Dataflow Telemetry is sent from the application to our servers. Data is proxied through our servers to enable us to easily switch analytics services. We currently use: From 2da80e46418c9e59c83207e0e1b7d44c6ebc0462 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Aug 2025 12:34:18 -0400 Subject: [PATCH 316/693] emmet: Use `index.js` directly to launch language server (#36126) This PR updates the Emmet extension to use the `index.js` file directly to launch the language server. This provides better cross-platform support, as we're not relying on platform-specific `.bin` wrappers. Release Notes: - N/A --- extensions/emmet/src/emmet.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/emmet/src/emmet.rs b/extensions/emmet/src/emmet.rs index e4fb3cf814..1434e16e88 100644 --- a/extensions/emmet/src/emmet.rs +++ b/extensions/emmet/src/emmet.rs @@ -5,7 +5,7 @@ struct EmmetExtension { did_find_server: bool, } -const SERVER_PATH: &str = "node_modules/.bin/emmet-language-server"; +const SERVER_PATH: &str = "node_modules/@olrtg/emmet-language-server/dist/index.js"; const PACKAGE_NAME: &str = "@olrtg/emmet-language-server"; impl EmmetExtension { From 0b9c9f5f2da008336c3e1a7648489764f4e87cca Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 13 Aug 2025 12:42:09 -0400 Subject: [PATCH 317/693] onboarding: Make Welcome page persistent (#36127) Release Notes: - N/A --- crates/onboarding/src/onboarding.rs | 1 + crates/onboarding/src/welcome.rs | 108 +++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 2444e5d44a..e07a8dc9fb 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -189,6 +189,7 @@ pub fn init(cx: &mut App) { base_keymap_picker::init(cx); register_serializable_item::(cx); + register_serializable_item::(cx); } pub fn show_onboarding_view(app_state: Arc, cx: &mut App) -> Task> { diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index 65baad03a0..ba0053a3b6 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -1,6 +1,6 @@ use gpui::{ Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, - ParentElement, Render, Styled, Window, actions, + ParentElement, Render, Styled, Task, Window, actions, }; use menu::{SelectNext, SelectPrevious}; use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*}; @@ -352,3 +352,109 @@ impl Item for WelcomePage { f(*event) } } + +impl workspace::SerializableItem for WelcomePage { + fn serialized_item_kind() -> &'static str { + "WelcomePage" + } + + fn cleanup( + workspace_id: workspace::WorkspaceId, + alive_items: Vec, + _window: &mut Window, + cx: &mut App, + ) -> Task> { + workspace::delete_unloaded_items( + alive_items, + workspace_id, + "welcome_pages", + &persistence::WELCOME_PAGES, + cx, + ) + } + + fn deserialize( + _project: Entity, + _workspace: gpui::WeakEntity, + workspace_id: workspace::WorkspaceId, + item_id: workspace::ItemId, + window: &mut Window, + cx: &mut App, + ) -> Task>> { + if persistence::WELCOME_PAGES + .get_welcome_page(item_id, workspace_id) + .ok() + .is_some_and(|is_open| is_open) + { + window.spawn(cx, async move |cx| cx.update(WelcomePage::new)) + } else { + Task::ready(Err(anyhow::anyhow!("No welcome page to deserialize"))) + } + } + + fn serialize( + &mut self, + workspace: &mut workspace::Workspace, + item_id: workspace::ItemId, + _closing: bool, + _window: &mut Window, + cx: &mut Context, + ) -> Option>> { + let workspace_id = workspace.database_id()?; + Some(cx.background_spawn(async move { + persistence::WELCOME_PAGES + .save_welcome_page(item_id, workspace_id, true) + .await + })) + } + + fn should_serialize(&self, event: &Self::Event) -> bool { + event == &ItemEvent::UpdateTab + } +} + +mod persistence { + use db::{define_connection, query, sqlez_macros::sql}; + use workspace::WorkspaceDb; + + define_connection! { + pub static ref WELCOME_PAGES: WelcomePagesDb = + &[ + sql!( + CREATE TABLE welcome_pages ( + workspace_id INTEGER, + item_id INTEGER UNIQUE, + is_open INTEGER DEFAULT FALSE, + + PRIMARY KEY(workspace_id, item_id), + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ) STRICT; + ), + ]; + } + + impl WelcomePagesDb { + query! { + pub async fn save_welcome_page( + item_id: workspace::ItemId, + workspace_id: workspace::WorkspaceId, + is_open: bool + ) -> Result<()> { + INSERT OR REPLACE INTO welcome_pages(item_id, workspace_id, is_open) + VALUES (?, ?, ?) + } + } + + query! { + pub fn get_welcome_page( + item_id: workspace::ItemId, + workspace_id: workspace::WorkspaceId + ) -> Result { + SELECT is_open + FROM welcome_pages + WHERE item_id = ? AND workspace_id = ? + } + } + } +} From 4238e640fa9a99acfa7edd08f1f7b27b5f5a649f Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Aug 2025 12:55:02 -0400 Subject: [PATCH 318/693] emmet: Bump to v0.0.6 (#36129) This PR bumps the Emmet extension to v0.0.6. Changes: - https://github.com/zed-industries/zed/pull/36126 Release Notes: - N/A --- Cargo.lock | 2 +- extensions/emmet/Cargo.toml | 2 +- extensions/emmet/extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8458c4af4b..ac1a56d53f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20663,7 +20663,7 @@ dependencies = [ [[package]] name = "zed_emmet" -version = "0.0.5" +version = "0.0.6" dependencies = [ "zed_extension_api 0.1.0", ] diff --git a/extensions/emmet/Cargo.toml b/extensions/emmet/Cargo.toml index ff9debdea9..2fbdf2a7e5 100644 --- a/extensions/emmet/Cargo.toml +++ b/extensions/emmet/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_emmet" -version = "0.0.5" +version = "0.0.6" edition.workspace = true publish.workspace = true license = "Apache-2.0" diff --git a/extensions/emmet/extension.toml b/extensions/emmet/extension.toml index 0ebb801f9d..a1848400b8 100644 --- a/extensions/emmet/extension.toml +++ b/extensions/emmet/extension.toml @@ -1,7 +1,7 @@ id = "emmet" name = "Emmet" description = "Emmet support" -version = "0.0.5" +version = "0.0.6" schema_version = 1 authors = ["Piotr Osiewicz "] repository = "https://github.com/zed-industries/zed" From 9a375f14192c9bc9bec54777ec74d1444e11e593 Mon Sep 17 00:00:00 2001 From: ponychicken Date: Wed, 13 Aug 2025 19:36:18 +0200 Subject: [PATCH 319/693] Add some documentation for Helix mode (#35641) Because there is literally no mention of it in the docs Release Notes: - N/A --------- Co-authored-by: ponychicken <183302+ponychicken@users.noreply.github.com> Co-authored-by: Ben Kunkle --- docs/src/SUMMARY.md | 1 + docs/src/configuring-zed.md | 8 +++++++- docs/src/helix.md | 11 +++++++++++ docs/src/key-bindings.md | 4 ++-- 4 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 docs/src/helix.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index fc936d6bd0..c7af36f431 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -21,6 +21,7 @@ - [Icon Themes](./icon-themes.md) - [Visual Customization](./visual-customization.md) - [Vim Mode](./vim.md) +- [Helix Mode](./helix.md) diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 1996e1c4ee..5d11dfe833 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -3195,10 +3195,16 @@ Run the `theme selector: toggle` action in the command palette to see a current ## Vim -- Description: Whether or not to enable vim mode (work in progress). +- Description: Whether or not to enable vim mode. See the [Vim documentation](./vim.md) for more details on configuration. - Setting: `vim_mode` - Default: `false` +## Helix Mode + +- Description: Whether or not to enable Helix mode. Enabling `helix_mode` also enables `vim_mode`. See the [Helix documentation](./helix.md) for more details. +- Setting: `helix_mode` +- Default: `false` + ## Project Panel - Description: Customize project panel diff --git a/docs/src/helix.md b/docs/src/helix.md new file mode 100644 index 0000000000..ddf997d3f0 --- /dev/null +++ b/docs/src/helix.md @@ -0,0 +1,11 @@ +# Helix Mode + +_Work in progress! Not all Helix keybindings are implemented yet._ + +Zed's Helix mode is an emulation layer that brings Helix-style keybindings and modal editing to Zed. It builds upon Zed's [Vim mode](./vim.md), so much of the core functionality is shared. Enabling `helix_mode` will also enable `vim_mode`. + +For a guide on Vim-related features that are also available in Helix mode, please refer to our [Vim mode documentation](./vim.md). + +To check the current status of Helix mode, or to request a missing Helix feature, checkout out the ["Are we Helix yet?" discussion](https://github.com/zed-industries/zed/discussions/33580). + +For a detailed list of Helix's default keybindings, please visit the [official Helix documentation](https://docs.helix-editor.com/keymap.html). diff --git a/docs/src/key-bindings.md b/docs/src/key-bindings.md index feed912787..9fc94840b7 100644 --- a/docs/src/key-bindings.md +++ b/docs/src/key-bindings.md @@ -14,7 +14,7 @@ If you're used to a specific editor's defaults you can set a `base_keymap` in yo - TextMate - None (disables _all_ key bindings) -You can also enable `vim_mode`, which adds vim bindings too. +You can also enable `vim_mode` or `helix_mode`, which add modal bindings. For more information, see the documentation for [Vim mode](./vim.md) and [Helix mode](./helix.md). ## User keymaps @@ -119,7 +119,7 @@ It's worth noting that attributes are only available on the node they are define Note: Before Zed v0.197.x, the ! operator only looked at one node at a time, and `>` meant "parent" not "ancestor". This meant that `!Editor` would match the context `Workspace > Pane > Editor`, because (confusingly) the Pane matches `!Editor`, and that `os=macos > Editor` did not match the context `Workspace > Pane > Editor` because of the intermediate `Pane` node. -If you're using Vim mode, we have information on how [vim modes influence the context](./vim.md#contexts) +If you're using Vim mode, we have information on how [vim modes influence the context](./vim.md#contexts). Helix mode is built on top of Vim mode and uses the same contexts. ### Actions From cb0bc463f103bd5a00d01e0229b9059ea6036d8b Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 13 Aug 2025 14:45:37 -0300 Subject: [PATCH 320/693] agent2: Add new "new thread" selector in the toolbar (#36133) Release Notes: - N/A --- assets/keymaps/default-linux.json | 1 + assets/keymaps/default-macos.json | 1 + crates/agent_ui/src/agent_panel.rs | 838 ++++++++++++-------- crates/agent_ui/src/agent_ui.rs | 2 + crates/agent_ui/src/ui.rs | 4 +- crates/agent_ui/src/ui/new_thread_button.rs | 6 +- crates/zed/src/zed/component_preview.rs | 2 +- 7 files changed, 526 insertions(+), 328 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 708432393c..dda26f406b 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -239,6 +239,7 @@ "ctrl-shift-a": "agent::ToggleContextPicker", "ctrl-shift-j": "agent::ToggleNavigationMenu", "ctrl-shift-i": "agent::ToggleOptionsMenu", + "ctrl-alt-shift-n": "agent::ToggleNewThreadMenu", "shift-alt-escape": "agent::ExpandMessageEditor", "ctrl->": "assistant::QuoteSelection", "ctrl-alt-e": "agent::RemoveAllContext", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index abb741af29..3966efd8df 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -279,6 +279,7 @@ "cmd-shift-a": "agent::ToggleContextPicker", "cmd-shift-j": "agent::ToggleNavigationMenu", "cmd-shift-i": "agent::ToggleOptionsMenu", + "cmd-alt-shift-n": "agent::ToggleNewThreadMenu", "shift-alt-escape": "agent::ExpandMessageEditor", "cmd->": "assistant::QuoteSelection", "cmd-alt-e": "agent::RemoveAllContext", diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index d07581da93..a641d62296 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -12,12 +12,12 @@ use serde::{Deserialize, Serialize}; use crate::NewExternalAgentThread; use crate::agent_diff::AgentDiffThread; use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES}; -use crate::ui::NewThreadButton; use crate::{ AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, - ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu, + ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu, + ToggleNewThreadMenu, ToggleOptionsMenu, acp::AcpThreadView, active_thread::{self, ActiveThread, ActiveThreadEvent}, agent_configuration::{AgentConfiguration, AssistantConfigurationEvent}, @@ -67,8 +67,8 @@ use theme::ThemeSettings; use time::UtcOffset; use ui::utils::WithRemSize; use ui::{ - Banner, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding, PopoverMenu, - PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*, + Banner, ButtonLike, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding, + PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*, }; use util::ResultExt as _; use workspace::{ @@ -86,6 +86,7 @@ const AGENT_PANEL_KEY: &str = "agent_panel"; #[derive(Serialize, Deserialize)] struct SerializedAgentPanel { width: Option, + selected_agent: Option, } pub fn init(cx: &mut App) { @@ -179,6 +180,14 @@ pub fn init(cx: &mut App) { }); } }) + .register_action(|workspace, _: &ToggleNewThreadMenu, window, cx| { + if let Some(panel) = workspace.panel::(cx) { + workspace.focus_panel::(window, cx); + panel.update(cx, |panel, cx| { + panel.toggle_new_thread_menu(&ToggleNewThreadMenu, window, cx); + }); + } + }) .register_action(|workspace, _: &OpenOnboardingModal, window, cx| { AgentOnboardingModal::toggle(workspace, window, cx) }) @@ -223,6 +232,36 @@ enum WhichFontSize { None, } +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum AgentType { + #[default] + Zed, + TextThread, + Gemini, + ClaudeCode, + NativeAgent, +} + +impl AgentType { + fn label(self) -> impl Into { + match self { + Self::Zed | Self::TextThread => "Zed", + Self::NativeAgent => "Agent 2", + Self::Gemini => "Gemini", + Self::ClaudeCode => "Claude Code", + } + } + + fn icon(self) -> IconName { + match self { + Self::Zed | Self::TextThread => IconName::AiZed, + Self::NativeAgent => IconName::ZedAssistant, + Self::Gemini => IconName::AiGemini, + Self::ClaudeCode => IconName::AiClaude, + } + } +} + impl ActiveView { pub fn which_font_size_used(&self) -> WhichFontSize { match self { @@ -453,16 +492,21 @@ pub struct AgentPanel { zoomed: bool, pending_serialization: Option>>, onboarding: Entity, + selected_agent: AgentType, } impl AgentPanel { fn serialize(&mut self, cx: &mut Context) { let width = self.width; + let selected_agent = self.selected_agent; self.pending_serialization = Some(cx.background_spawn(async move { KEY_VALUE_STORE .write_kvp( AGENT_PANEL_KEY.into(), - serde_json::to_string(&SerializedAgentPanel { width })?, + serde_json::to_string(&SerializedAgentPanel { + width, + selected_agent: Some(selected_agent), + })?, ) .await?; anyhow::Ok(()) @@ -531,6 +575,9 @@ impl AgentPanel { if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width.map(|w| w.round()); + if let Some(selected_agent) = serialized_panel.selected_agent { + panel.selected_agent = selected_agent; + } cx.notify(); }); } @@ -732,6 +779,7 @@ impl AgentPanel { zoomed: false, pending_serialization: None, onboarding, + selected_agent: AgentType::default(), } } @@ -1174,6 +1222,15 @@ impl AgentPanel { self.agent_panel_menu_handle.toggle(window, cx); } + pub fn toggle_new_thread_menu( + &mut self, + _: &ToggleNewThreadMenu, + window: &mut Window, + cx: &mut Context, + ) { + self.new_thread_menu_handle.toggle(window, cx); + } + pub fn increase_font_size( &mut self, action: &IncreaseBufferFontSize, @@ -1581,6 +1638,17 @@ impl AgentPanel { menu } + + pub fn set_selected_agent(&mut self, agent: AgentType, cx: &mut Context) { + if self.selected_agent != agent { + self.selected_agent = agent; + self.serialize(cx); + } + } + + pub fn selected_agent(&self) -> AgentType { + self.selected_agent + } } impl Focusable for AgentPanel { @@ -1811,200 +1879,24 @@ impl AgentPanel { .into_any() } - fn render_toolbar(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render_panel_options_menu( + &self, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { let user_store = self.user_store.read(cx); let usage = user_store.model_request_usage(); - let account_url = zed_urls::account_url(cx); let focus_handle = self.focus_handle(cx); - let go_back_button = div().child( - IconButton::new("go-back", IconName::ArrowLeft) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - this.go_back(&workspace::GoBack, window, cx); - })) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Go Back", - &workspace::GoBack, - &focus_handle, - window, - cx, - ) - } - }), - ); - - let recent_entries_menu = div().child( - PopoverMenu::new("agent-nav-menu") - .trigger_with_tooltip( - IconButton::new("agent-nav-menu", IconName::MenuAlt) - .icon_size(IconSize::Small) - .style(ui::ButtonStyle::Subtle), - { - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Toggle Panel Menu", - &ToggleNavigationMenu, - &focus_handle, - window, - cx, - ) - } - }, - ) - .anchor(Corner::TopLeft) - .with_handle(self.assistant_navigation_menu_handle.clone()) - .menu({ - let menu = self.assistant_navigation_menu.clone(); - move |window, cx| { - if let Some(menu) = menu.as_ref() { - menu.update(cx, |_, cx| { - cx.defer_in(window, |menu, window, cx| { - menu.rebuild(window, cx); - }); - }) - } - menu.clone() - } - }), - ); - let full_screen_label = if self.is_zoomed(window, cx) { "Disable Full Screen" } else { "Enable Full Screen" }; - let active_thread = match &self.active_view { - ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()), - ActiveView::ExternalAgentThread { .. } - | ActiveView::TextThread { .. } - | ActiveView::History - | ActiveView::Configuration => None, - }; - - let new_thread_menu = PopoverMenu::new("new_thread_menu") - .trigger_with_tooltip( - IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small), - Tooltip::text("New Thread…"), - ) - .anchor(Corner::TopRight) - .with_handle(self.new_thread_menu_handle.clone()) - .menu({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - let active_thread = active_thread.clone(); - Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { - menu = menu - .context(focus_handle.clone()) - .when(cx.has_flag::(), |this| { - this.header("Zed Agent") - }) - .when_some(active_thread, |this, active_thread| { - let thread = active_thread.read(cx); - - if !thread.is_empty() { - let thread_id = thread.id().clone(); - this.item( - ContextMenuEntry::new("New From Summary") - .icon(IconName::ThreadFromSummary) - .icon_color(Color::Muted) - .handler(move |window, cx| { - window.dispatch_action( - Box::new(NewThread { - from_thread_id: Some(thread_id.clone()), - }), - cx, - ); - }), - ) - } else { - this - } - }) - .item( - ContextMenuEntry::new("New Thread") - .icon(IconName::Thread) - .icon_color(Color::Muted) - .action(NewThread::default().boxed_clone()) - .handler(move |window, cx| { - window.dispatch_action( - NewThread::default().boxed_clone(), - cx, - ); - }), - ) - .item( - ContextMenuEntry::new("New Text Thread") - .icon(IconName::TextThread) - .icon_color(Color::Muted) - .action(NewTextThread.boxed_clone()) - .handler(move |window, cx| { - window.dispatch_action(NewTextThread.boxed_clone(), cx); - }), - ) - .when(cx.has_flag::(), |this| { - this.separator() - .header("External Agents") - .item( - ContextMenuEntry::new("New Gemini Thread") - .icon(IconName::AiGemini) - .icon_color(Color::Muted) - .handler(move |window, cx| { - window.dispatch_action( - NewExternalAgentThread { - agent: Some(crate::ExternalAgent::Gemini), - } - .boxed_clone(), - cx, - ); - }), - ) - .item( - ContextMenuEntry::new("New Claude Code Thread") - .icon(IconName::AiClaude) - .icon_color(Color::Muted) - .handler(move |window, cx| { - window.dispatch_action( - NewExternalAgentThread { - agent: Some( - crate::ExternalAgent::ClaudeCode, - ), - } - .boxed_clone(), - cx, - ); - }), - ) - .item( - ContextMenuEntry::new("New Native Agent Thread") - .icon(IconName::ZedAssistant) - .icon_color(Color::Muted) - .handler(move |window, cx| { - window.dispatch_action( - NewExternalAgentThread { - agent: Some( - crate::ExternalAgent::NativeAgent, - ), - } - .boxed_clone(), - cx, - ); - }), - ) - }); - menu - })) - } - }); - - let agent_panel_menu = PopoverMenu::new("agent-options-menu") + PopoverMenu::new("agent-options-menu") .trigger_with_tooltip( IconButton::new("agent-options-menu", IconName::Ellipsis) .icon_size(IconSize::Small), @@ -2087,6 +1979,139 @@ impl AgentPanel { menu })) } + }) + } + + fn render_recent_entries_menu( + &self, + icon: IconName, + cx: &mut Context, + ) -> impl IntoElement { + let focus_handle = self.focus_handle(cx); + + PopoverMenu::new("agent-nav-menu") + .trigger_with_tooltip( + IconButton::new("agent-nav-menu", icon) + .icon_size(IconSize::Small) + .style(ui::ButtonStyle::Subtle), + { + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Toggle Panel Menu", + &ToggleNavigationMenu, + &focus_handle, + window, + cx, + ) + } + }, + ) + .anchor(Corner::TopLeft) + .with_handle(self.assistant_navigation_menu_handle.clone()) + .menu({ + let menu = self.assistant_navigation_menu.clone(); + move |window, cx| { + if let Some(menu) = menu.as_ref() { + menu.update(cx, |_, cx| { + cx.defer_in(window, |menu, window, cx| { + menu.rebuild(window, cx); + }); + }) + } + menu.clone() + } + }) + } + + fn render_toolbar_back_button(&self, cx: &mut Context) -> impl IntoElement { + let focus_handle = self.focus_handle(cx); + + IconButton::new("go-back", IconName::ArrowLeft) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, window, cx| { + this.go_back(&workspace::GoBack, window, cx); + })) + .tooltip({ + let focus_handle = focus_handle.clone(); + + move |window, cx| { + Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, window, cx) + } + }) + } + + fn render_toolbar_old(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let focus_handle = self.focus_handle(cx); + + let active_thread = match &self.active_view { + ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()), + ActiveView::ExternalAgentThread { .. } + | ActiveView::TextThread { .. } + | ActiveView::History + | ActiveView::Configuration => None, + }; + + let new_thread_menu = PopoverMenu::new("new_thread_menu") + .trigger_with_tooltip( + IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small), + Tooltip::text("New Thread…"), + ) + .anchor(Corner::TopRight) + .with_handle(self.new_thread_menu_handle.clone()) + .menu({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + let active_thread = active_thread.clone(); + Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { + menu = menu + .context(focus_handle.clone()) + .when_some(active_thread, |this, active_thread| { + let thread = active_thread.read(cx); + + if !thread.is_empty() { + let thread_id = thread.id().clone(); + this.item( + ContextMenuEntry::new("New From Summary") + .icon(IconName::ThreadFromSummary) + .icon_color(Color::Muted) + .handler(move |window, cx| { + window.dispatch_action( + Box::new(NewThread { + from_thread_id: Some(thread_id.clone()), + }), + cx, + ); + }), + ) + } else { + this + } + }) + .item( + ContextMenuEntry::new("New Thread") + .icon(IconName::Thread) + .icon_color(Color::Muted) + .action(NewThread::default().boxed_clone()) + .handler(move |window, cx| { + window.dispatch_action( + NewThread::default().boxed_clone(), + cx, + ); + }), + ) + .item( + ContextMenuEntry::new("New Text Thread") + .icon(IconName::TextThread) + .icon_color(Color::Muted) + .action(NewTextThread.boxed_clone()) + .handler(move |window, cx| { + window.dispatch_action(NewTextThread.boxed_clone(), cx); + }), + ); + menu + })) + } }); h_flex() @@ -2105,8 +2130,12 @@ impl AgentPanel { .pl_1() .gap_1() .child(match &self.active_view { - ActiveView::History | ActiveView::Configuration => go_back_button, - _ => recent_entries_menu, + ActiveView::History | ActiveView::Configuration => { + self.render_toolbar_back_button(cx).into_any_element() + } + _ => self + .render_recent_entries_menu(IconName::MenuAlt, cx) + .into_any_element(), }) .child(self.render_title_view(window, cx)), ) @@ -2123,11 +2152,308 @@ impl AgentPanel { .border_l_1() .border_color(cx.theme().colors().border) .child(new_thread_menu) - .child(agent_panel_menu), + .child(self.render_panel_options_menu(window, cx)), ), ) } + fn render_toolbar_new(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let focus_handle = self.focus_handle(cx); + + let active_thread = match &self.active_view { + ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()), + ActiveView::ExternalAgentThread { .. } + | ActiveView::TextThread { .. } + | ActiveView::History + | ActiveView::Configuration => None, + }; + + let new_thread_menu = PopoverMenu::new("new_thread_menu") + .trigger_with_tooltip( + ButtonLike::new("new_thread_menu_btn").child( + h_flex() + .group("agent-selector") + .gap_1p5() + .child( + h_flex() + .relative() + .size_4() + .justify_center() + .child( + h_flex() + .group_hover("agent-selector", |s| s.invisible()) + .child( + Icon::new(self.selected_agent.icon()) + .color(Color::Muted), + ), + ) + .child( + h_flex() + .absolute() + .invisible() + .group_hover("agent-selector", |s| s.visible()) + .child(Icon::new(IconName::Plus)), + ), + ) + .child(Label::new(self.selected_agent.label())), + ), + { + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "New…", + &ToggleNewThreadMenu, + &focus_handle, + window, + cx, + ) + } + }, + ) + .anchor(Corner::TopLeft) + .with_handle(self.new_thread_menu_handle.clone()) + .menu({ + let focus_handle = focus_handle.clone(); + let workspace = self.workspace.clone(); + + move |window, cx| { + let active_thread = active_thread.clone(); + Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { + menu = menu + .context(focus_handle.clone()) + .header("Zed Agent") + .when_some(active_thread, |this, active_thread| { + let thread = active_thread.read(cx); + + if !thread.is_empty() { + let thread_id = thread.id().clone(); + this.item( + ContextMenuEntry::new("New From Summary") + .icon(IconName::ThreadFromSummary) + .icon_color(Color::Muted) + .handler(move |window, cx| { + window.dispatch_action( + Box::new(NewThread { + from_thread_id: Some(thread_id.clone()), + }), + cx, + ); + }), + ) + } else { + this + } + }) + .item( + ContextMenuEntry::new("New Thread") + .icon(IconName::Thread) + .icon_color(Color::Muted) + .action(NewThread::default().boxed_clone()) + .handler({ + let workspace = workspace.clone(); + move |window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = + workspace.panel::(cx) + { + panel.update(cx, |panel, cx| { + panel.set_selected_agent( + AgentType::Zed, + cx, + ); + }); + } + }); + } + window.dispatch_action( + NewThread::default().boxed_clone(), + cx, + ); + } + }), + ) + .item( + ContextMenuEntry::new("New Text Thread") + .icon(IconName::TextThread) + .icon_color(Color::Muted) + .action(NewTextThread.boxed_clone()) + .handler({ + let workspace = workspace.clone(); + move |window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = + workspace.panel::(cx) + { + panel.update(cx, |panel, cx| { + panel.set_selected_agent( + AgentType::TextThread, + cx, + ); + }); + } + }); + } + window.dispatch_action(NewTextThread.boxed_clone(), cx); + } + }), + ) + .item( + ContextMenuEntry::new("New Native Agent Thread") + .icon(IconName::ZedAssistant) + .icon_color(Color::Muted) + .handler({ + let workspace = workspace.clone(); + move |window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = + workspace.panel::(cx) + { + panel.update(cx, |panel, cx| { + panel.set_selected_agent( + AgentType::NativeAgent, + cx, + ); + }); + } + }); + } + window.dispatch_action( + NewExternalAgentThread { + agent: Some(crate::ExternalAgent::NativeAgent), + } + .boxed_clone(), + cx, + ); + } + }), + ) + .separator() + .header("External Agents") + .item( + ContextMenuEntry::new("New Gemini Thread") + .icon(IconName::AiGemini) + .icon_color(Color::Muted) + .handler({ + let workspace = workspace.clone(); + move |window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = + workspace.panel::(cx) + { + panel.update(cx, |panel, cx| { + panel.set_selected_agent( + AgentType::Gemini, + cx, + ); + }); + } + }); + } + window.dispatch_action( + NewExternalAgentThread { + agent: Some(crate::ExternalAgent::Gemini), + } + .boxed_clone(), + cx, + ); + } + }), + ) + .item( + ContextMenuEntry::new("New Claude Code Thread") + .icon(IconName::AiClaude) + .icon_color(Color::Muted) + .handler({ + let workspace = workspace.clone(); + move |window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = + workspace.panel::(cx) + { + panel.update(cx, |panel, cx| { + panel.set_selected_agent( + AgentType::ClaudeCode, + cx, + ); + }); + } + }); + } + window.dispatch_action( + NewExternalAgentThread { + agent: Some(crate::ExternalAgent::ClaudeCode), + } + .boxed_clone(), + cx, + ); + } + }), + ); + menu + })) + } + }); + + h_flex() + .id("agent-panel-toolbar") + .h(Tab::container_height(cx)) + .max_w_full() + .flex_none() + .justify_between() + .gap_2() + .bg(cx.theme().colors().tab_bar_background) + .border_b_1() + .border_color(cx.theme().colors().border) + .child( + h_flex() + .size_full() + .gap(DynamicSpacing::Base08.rems(cx)) + .child(match &self.active_view { + ActiveView::History | ActiveView::Configuration => { + self.render_toolbar_back_button(cx).into_any_element() + } + _ => h_flex() + .h_full() + .px(DynamicSpacing::Base04.rems(cx)) + .border_r_1() + .border_color(cx.theme().colors().border) + .child(new_thread_menu) + .into_any_element(), + }) + .child(self.render_title_view(window, cx)), + ) + .child( + h_flex() + .h_full() + .gap_2() + .children(self.render_token_count(cx)) + .child( + h_flex() + .h_full() + .gap(DynamicSpacing::Base02.rems(cx)) + .pl(DynamicSpacing::Base04.rems(cx)) + .pr(DynamicSpacing::Base06.rems(cx)) + .border_l_1() + .border_color(cx.theme().colors().border) + .child(self.render_recent_entries_menu(IconName::HistoryRerun, cx)) + .child(self.render_panel_options_menu(window, cx)), + ), + ) + } + + fn render_toolbar(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + if cx.has_flag::() { + self.render_toolbar_new(window, cx).into_any_element() + } else { + self.render_toolbar_old(window, cx).into_any_element() + } + } + fn render_token_count(&self, cx: &App) -> Option { match &self.active_view { ActiveView::Thread { @@ -2576,138 +2902,6 @@ impl AgentPanel { }, )), ) - .child(self.render_empty_state_section_header("Start", None, cx)) - .child( - v_flex() - .p_1() - .gap_2() - .child( - h_flex() - .w_full() - .gap_2() - .child( - NewThreadButton::new( - "new-thread-btn", - "New Thread", - IconName::Thread, - ) - .keybinding(KeyBinding::for_action_in( - &NewThread::default(), - &self.focus_handle(cx), - window, - cx, - )) - .on_click( - |window, cx| { - window.dispatch_action( - NewThread::default().boxed_clone(), - cx, - ) - }, - ), - ) - .child( - NewThreadButton::new( - "new-text-thread-btn", - "New Text Thread", - IconName::TextThread, - ) - .keybinding(KeyBinding::for_action_in( - &NewTextThread, - &self.focus_handle(cx), - window, - cx, - )) - .on_click( - |window, cx| { - window.dispatch_action(Box::new(NewTextThread), cx) - }, - ), - ), - ) - .when(cx.has_flag::(), |this| { - this.child( - h_flex() - .w_full() - .gap_2() - .child( - NewThreadButton::new( - "new-gemini-thread-btn", - "New Gemini Thread", - IconName::AiGemini, - ) - // .keybinding(KeyBinding::for_action_in( - // &OpenHistory, - // &self.focus_handle(cx), - // window, - // cx, - // )) - .on_click( - |window, cx| { - window.dispatch_action( - Box::new(NewExternalAgentThread { - agent: Some( - crate::ExternalAgent::Gemini, - ), - }), - cx, - ) - }, - ), - ) - .child( - NewThreadButton::new( - "new-claude-thread-btn", - "New Claude Code Thread", - IconName::AiClaude, - ) - // .keybinding(KeyBinding::for_action_in( - // &OpenHistory, - // &self.focus_handle(cx), - // window, - // cx, - // )) - .on_click( - |window, cx| { - window.dispatch_action( - Box::new(NewExternalAgentThread { - agent: Some( - crate::ExternalAgent::ClaudeCode, - ), - }), - cx, - ) - }, - ), - ) - .child( - NewThreadButton::new( - "new-native-agent-thread-btn", - "New Native Agent Thread", - IconName::ZedAssistant, - ) - // .keybinding(KeyBinding::for_action_in( - // &OpenHistory, - // &self.focus_handle(cx), - // window, - // cx, - // )) - .on_click( - |window, cx| { - window.dispatch_action( - Box::new(NewExternalAgentThread { - agent: Some( - crate::ExternalAgent::NativeAgent, - ), - }), - cx, - ) - }, - ), - ), - ) - }), - ) .when_some(configuration_error.as_ref(), |this, err| { this.child(self.render_configuration_error(err, &focus_handle, window, cx)) }) diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index b776c0830b..231b9cfb38 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -64,6 +64,8 @@ actions!( NewTextThread, /// Toggles the context picker interface for adding files, symbols, or other context. ToggleContextPicker, + /// Toggles the menu to create new agent threads. + ToggleNewThreadMenu, /// Toggles the navigation menu for switching between threads and views. ToggleNavigationMenu, /// Toggles the options menu for agent settings and preferences. diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index b477a8c385..beeaf0c43b 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -2,7 +2,7 @@ mod agent_notification; mod burn_mode_tooltip; mod context_pill; mod end_trial_upsell; -mod new_thread_button; +// mod new_thread_button; mod onboarding_modal; pub mod preview; @@ -10,5 +10,5 @@ pub use agent_notification::*; pub use burn_mode_tooltip::*; pub use context_pill::*; pub use end_trial_upsell::*; -pub use new_thread_button::*; +// pub use new_thread_button::*; pub use onboarding_modal::*; diff --git a/crates/agent_ui/src/ui/new_thread_button.rs b/crates/agent_ui/src/ui/new_thread_button.rs index 7764144150..347d6adcaf 100644 --- a/crates/agent_ui/src/ui/new_thread_button.rs +++ b/crates/agent_ui/src/ui/new_thread_button.rs @@ -11,7 +11,7 @@ pub struct NewThreadButton { } impl NewThreadButton { - pub fn new(id: impl Into, label: impl Into, icon: IconName) -> Self { + fn new(id: impl Into, label: impl Into, icon: IconName) -> Self { Self { id: id.into(), label: label.into(), @@ -21,12 +21,12 @@ impl NewThreadButton { } } - pub fn keybinding(mut self, keybinding: Option) -> Self { + fn keybinding(mut self, keybinding: Option) -> Self { self.keybinding = keybinding; self } - pub fn on_click(mut self, handler: F) -> Self + fn on_click(mut self, handler: F) -> Self where F: Fn(&mut Window, &mut App) + 'static, { diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index ac889a7ad9..4609ecce9b 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/crates/zed/src/zed/component_preview.rs @@ -761,7 +761,7 @@ impl Render for ComponentPreview { ) .track_scroll(self.nav_scroll_handle.clone()) .p_2p5() - .w(px(229.)) + .w(px(231.)) // Matches perfectly with the size of the "Component Preview" tab, if that's the first one in the pane .h_full() .flex_1(), ) From e52f1483049aa6c0b155c04fb4808aeb9a4bcd1a Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 13 Aug 2025 13:56:51 -0400 Subject: [PATCH 321/693] Bump Zed to v0.201 (#36132) Release Notes: -N/A --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ac1a56d53f..3b1337eece 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20500,7 +20500,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.200.0" +version = "0.201.0" dependencies = [ "activity_indicator", "agent", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index bdbb39698c..4335f2d5a1 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition.workspace = true name = "zed" -version = "0.200.0" +version = "0.201.0" publish.workspace = true license = "GPL-3.0-or-later" authors = ["Zed Team "] From 4a3549882905e07b2d4ef259bff7da30d70aa922 Mon Sep 17 00:00:00 2001 From: smit Date: Thu, 14 Aug 2025 00:19:37 +0530 Subject: [PATCH 322/693] copilot: Fix Copilot fails to sign in (#36138) Closes #36093 Pin copilot version to 1.354 for now until further investigation. Release Notes: - Fixes issue where Copilot failed to sign in. Co-authored-by: MrSubidubi --- crates/copilot/src/copilot.rs | 12 ++++++------ crates/languages/src/css.rs | 8 +++++++- crates/languages/src/json.rs | 8 +++++++- crates/languages/src/python.rs | 1 + crates/languages/src/tailwind.rs | 8 +++++++- crates/languages/src/typescript.rs | 1 + crates/languages/src/vtsls.rs | 2 ++ crates/languages/src/yaml.rs | 8 +++++++- crates/node_runtime/src/node_runtime.rs | 15 ++++++++++++++- 9 files changed, 52 insertions(+), 11 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 49ae2b9d9c..166a582c70 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -21,7 +21,7 @@ use language::{ point_from_lsp, point_to_lsp, }; use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName}; -use node_runtime::NodeRuntime; +use node_runtime::{NodeRuntime, VersionCheck}; use parking_lot::Mutex; use project::DisableAiSettings; use request::StatusNotification; @@ -1169,9 +1169,8 @@ async fn get_copilot_lsp(fs: Arc, node_runtime: NodeRuntime) -> anyhow:: const SERVER_PATH: &str = "node_modules/@github/copilot-language-server/dist/language-server.js"; - let latest_version = node_runtime - .npm_package_latest_version(PACKAGE_NAME) - .await?; + // pinning it: https://github.com/zed-industries/zed/issues/36093 + const PINNED_VERSION: &str = "1.354"; let server_path = paths::copilot_dir().join(SERVER_PATH); fs.create_dir(paths::copilot_dir()).await?; @@ -1181,12 +1180,13 @@ async fn get_copilot_lsp(fs: Arc, node_runtime: NodeRuntime) -> anyhow:: PACKAGE_NAME, &server_path, paths::copilot_dir(), - &latest_version, + &PINNED_VERSION, + VersionCheck::VersionMismatch, ) .await; if should_install { node_runtime - .npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &latest_version)]) + .npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &PINNED_VERSION)]) .await?; } diff --git a/crates/languages/src/css.rs b/crates/languages/src/css.rs index 7725e079be..19329fcc6e 100644 --- a/crates/languages/src/css.rs +++ b/crates/languages/src/css.rs @@ -103,7 +103,13 @@ impl LspAdapter for CssLspAdapter { let should_install_language_server = self .node - .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version) + .should_install_npm_package( + Self::PACKAGE_NAME, + &server_path, + &container_dir, + &version, + Default::default(), + ) .await; if should_install_language_server { diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index ca82bb2431..019b45d396 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -340,7 +340,13 @@ impl LspAdapter for JsonLspAdapter { let should_install_language_server = self .node - .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version) + .should_install_npm_package( + Self::PACKAGE_NAME, + &server_path, + &container_dir, + &version, + Default::default(), + ) .await; if should_install_language_server { diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 0524c02fd5..5513324487 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -206,6 +206,7 @@ impl LspAdapter for PythonLspAdapter { &server_path, &container_dir, &version, + Default::default(), ) .await; diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index a7edbb148c..6f03eeda8d 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -108,7 +108,13 @@ impl LspAdapter for TailwindLspAdapter { let should_install_language_server = self .node - .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version) + .should_install_npm_package( + Self::PACKAGE_NAME, + &server_path, + &container_dir, + &version, + Default::default(), + ) .await; if should_install_language_server { diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index f976b62614..a8ba880889 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -589,6 +589,7 @@ impl LspAdapter for TypeScriptLspAdapter { &server_path, &container_dir, version.typescript_version.as_str(), + Default::default(), ) .await; diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index 33751f733e..73498fc579 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -116,6 +116,7 @@ impl LspAdapter for VtslsLspAdapter { &server_path, &container_dir, &latest_version.server_version, + Default::default(), ) .await { @@ -129,6 +130,7 @@ impl LspAdapter for VtslsLspAdapter { &container_dir.join(Self::TYPESCRIPT_TSDK_PATH), &container_dir, &latest_version.typescript_version, + Default::default(), ) .await { diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 815605d524..28be2cc1a4 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -104,7 +104,13 @@ impl LspAdapter for YamlLspAdapter { let should_install_language_server = self .node - .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version) + .should_install_npm_package( + Self::PACKAGE_NAME, + &server_path, + &container_dir, + &version, + Default::default(), + ) .await; if should_install_language_server { diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 08698a1d6c..6fcc3a728a 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -29,6 +29,15 @@ pub struct NodeBinaryOptions { pub use_paths: Option<(PathBuf, PathBuf)>, } +#[derive(Default)] +pub enum VersionCheck { + /// Check whether the installed and requested version have a mismatch + VersionMismatch, + /// Only check whether the currently installed version is older than the newest one + #[default] + OlderVersion, +} + #[derive(Clone)] pub struct NodeRuntime(Arc>); @@ -287,6 +296,7 @@ impl NodeRuntime { local_executable_path: &Path, local_package_directory: &Path, latest_version: &str, + version_check: VersionCheck, ) -> bool { // In the case of the local system not having the package installed, // or in the instances where we fail to parse package.json data, @@ -311,7 +321,10 @@ impl NodeRuntime { return true; }; - installed_version < latest_version + match version_check { + VersionCheck::VersionMismatch => installed_version != latest_version, + VersionCheck::OlderVersion => installed_version < latest_version, + } } } From bd61eb08898e379ae1f093895738d805af726430 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 13 Aug 2025 13:25:52 -0600 Subject: [PATCH 323/693] Use IBM Plex Sans / Lilex (#36084) The Zed Plex fonts were found to violate the OFL by using the word Plex in the name. Lilex has better ligatures and box-drawing characters than Zed Plex Mono, but Zed Plex Sans should be identical to IBM Plex Sans. Closes #15542 Closes zed-industries/zed-fonts#31 Release Notes: - The "Zed Plex Sans" and "Zed Plex Mono" fonts have been replaced with "IBM Plex Sans" and "Lilex". The old names still work for backward compatibility. Other than fixing line-drawing characters, and improving the ligatures, there should be little visual change as the fonts are all of the same family. - Introduced ".ZedSans" and ".ZedMono" as aliases to allow us to easily change the default fonts in the future. These currently default to "IBM Plex Sans" and "Lilex" respectively. --- .../fonts/ibm-plex-sans/IBMPlexSans-Bold.ttf | Bin 0 -> 200872 bytes .../ibm-plex-sans/IBMPlexSans-BoldItalic.ttf | Bin 0 -> 208588 bytes .../ibm-plex-sans/IBMPlexSans-Italic.ttf | Bin 0 -> 207920 bytes .../ibm-plex-sans/IBMPlexSans-Regular.ttf | Bin 0 -> 200500 bytes .../{plex-mono => ibm-plex-sans}/license.txt | 0 assets/fonts/lilex/Lilex-Bold.ttf | Bin 0 -> 192992 bytes assets/fonts/lilex/Lilex-BoldItalic.ttf | Bin 0 -> 197532 bytes assets/fonts/lilex/Lilex-Italic.ttf | Bin 0 -> 198216 bytes assets/fonts/lilex/Lilex-Regular.ttf | Bin 0 -> 194308 bytes .../{plex-sans/license.txt => lilex/OFL.txt} | 7 ++- assets/fonts/plex-mono/ZedPlexMono-Bold.ttf | Bin 163568 -> 0 bytes .../plex-mono/ZedPlexMono-BoldItalic.ttf | Bin 170088 -> 0 bytes assets/fonts/plex-mono/ZedPlexMono-Italic.ttf | Bin 169868 -> 0 bytes .../fonts/plex-mono/ZedPlexMono-Regular.ttf | Bin 161844 -> 0 bytes assets/fonts/plex-sans/ZedPlexSans-Bold.ttf | Bin 206164 -> 0 bytes .../plex-sans/ZedPlexSans-BoldItalic.ttf | Bin 213704 -> 0 bytes assets/fonts/plex-sans/ZedPlexSans-Italic.ttf | Bin 213092 -> 0 bytes .../fonts/plex-sans/ZedPlexSans-Regular.ttf | Bin 205848 -> 0 bytes assets/settings/default.json | 10 +++- crates/assets/src/assets.rs | 4 +- crates/editor/src/display_map/block_map.rs | 2 - crates/editor/src/display_map/wrap_map.rs | 2 +- crates/editor/src/test.rs | 2 +- crates/gpui/src/platform/linux/text_system.rs | 6 +- crates/gpui/src/platform/mac/text_system.rs | 6 +- .../gpui/src/platform/windows/direct_write.rs | 4 +- crates/gpui/src/text_system.rs | 17 +++++- crates/gpui/src/text_system/line_wrapper.rs | 2 +- crates/markdown/examples/markdown.rs | 6 +- crates/storybook/src/storybook.rs | 2 +- crates/vim/src/vim.rs | 8 +-- crates/zed/src/zed.rs | 4 +- docs/src/configuring-zed.md | 10 ++-- docs/src/fonts.md | 56 ------------------ docs/src/visual-customization.md | 12 ++-- nix/build.nix | 4 +- nix/shell.nix | 4 +- 37 files changed, 58 insertions(+), 110 deletions(-) create mode 100644 assets/fonts/ibm-plex-sans/IBMPlexSans-Bold.ttf create mode 100644 assets/fonts/ibm-plex-sans/IBMPlexSans-BoldItalic.ttf create mode 100644 assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf create mode 100644 assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf rename assets/fonts/{plex-mono => ibm-plex-sans}/license.txt (100%) create mode 100644 assets/fonts/lilex/Lilex-Bold.ttf create mode 100644 assets/fonts/lilex/Lilex-BoldItalic.ttf create mode 100644 assets/fonts/lilex/Lilex-Italic.ttf create mode 100644 assets/fonts/lilex/Lilex-Regular.ttf rename assets/fonts/{plex-sans/license.txt => lilex/OFL.txt} (96%) delete mode 100644 assets/fonts/plex-mono/ZedPlexMono-Bold.ttf delete mode 100644 assets/fonts/plex-mono/ZedPlexMono-BoldItalic.ttf delete mode 100644 assets/fonts/plex-mono/ZedPlexMono-Italic.ttf delete mode 100644 assets/fonts/plex-mono/ZedPlexMono-Regular.ttf delete mode 100644 assets/fonts/plex-sans/ZedPlexSans-Bold.ttf delete mode 100644 assets/fonts/plex-sans/ZedPlexSans-BoldItalic.ttf delete mode 100644 assets/fonts/plex-sans/ZedPlexSans-Italic.ttf delete mode 100644 assets/fonts/plex-sans/ZedPlexSans-Regular.ttf delete mode 100644 docs/src/fonts.md diff --git a/assets/fonts/ibm-plex-sans/IBMPlexSans-Bold.ttf b/assets/fonts/ibm-plex-sans/IBMPlexSans-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1d66b1a2e96d5e33d6f5fa8e81e76b4e5621d240 GIT binary patch literal 200872 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 Yi*j@nd=g9YbQIi+QnS(d&{B~B08|`uMgRZ+ literal 0 HcmV?d00001 diff --git a/assets/fonts/ibm-plex-sans/IBMPlexSans-BoldItalic.ttf b/assets/fonts/ibm-plex-sans/IBMPlexSans-BoldItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e07bc1f527dd77ba8fd967ef00a54371da7bfaf0 GIT binary patch literal 208588 zcmZQzWME(rW@KPsVK8tB_H<`pU|?im);q$$z`)_|;_9~c-@0rDCZz%f2F6S70sg_( zMR)%&FsZmOF#K$D4-R!o6VVB0V3s|@z#y>BKUm+Wx2$S00|Vm@1_p+NL zFfa%{VPN29N-ir=;Ge_7!N8!n$IQXNz&L|}fk7cR zwIosRY@H?pvtA9zJ$Z?_sjr?1$}up0UBSS>`Kchkxa5#jQ#u3V|2Yf{N(T#yQVZ_x z+@Qq3AQ!{Hz+?apJ7z|Aj)d&ocz&C&44lj_7#J8B9vV#F1fi?v{JZn-Ez3Wa8U|(t zP6j5BC^&=}SQ!7kWnf@6{deczVwQi*FBnch<-ip4XOP*oj zU;tqze+GFbLk3AEcLqMjcMS4iJ~M+nW8(jnOyd6^F}eN!!zA-CGDI>hWAI=qXGmsV%TU79`2Pk|0YfyC z?fjGP?i24aV#Y35+io5*Xze5*g(foR~Nm5}7y{ zoEYW)UuNR?f0@yOL5b0Vft$&QL5azTft%6l|93_!25H7u|KBmbVsKzgWUymQ{NKRn z{Qozj^Z#>QU)7F zcLsZ=2nH{hKE~buk1;bah%lXEkYf^I5MW_p5M#1tkYg(N|A9$_L6Iqw!GiI||C3Dq z4023{|9>+TF_SiK*E6H^?62~#WsC)14oznP{mNHJCZU&&PUe%ndFNKxG09gUSeG>;owmz~u#~ z%s^m%sF(i-8YZ9)ZdvLRkL)Jthz)qz+A<*#lb6fy%nB{};jK9jMGh#%BLlGR{* zOa~ZbnI!&SX4>)p2U7>MP2%$ZDU$+&1~_l?GDtISV~}SGVUS=dWsqewXHW-~5lj;Q zuQ7qlP-oC!T*V**R@=&8#H9BB8sjqtc_s@6dB&# zgTuLiA)48gK_Ao>Vb)|wLGcGKgEYw9AU8v?1lXM*H^DH>J+l}LnMD~4nS>Y&!S+94 zFk}MN$DnZh$Y2PruRwKy9D^d0CIcUnA%h;{1_nOH#|(T-nhf&bwi`2pJhSWnugu>6 zzcK#&|BdnL|2^P#8K}׮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 M)NFJKVBm0fadlh!Z(TM6ladJo1LGz40RP}D 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Ξ2g7vn7A-F-$91vOw^gh7`MUq;4ll6nq? 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įRm7$u^lA(*ygJA-rKWi*&468e< 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 literal 0 HcmV?d00001 diff --git a/assets/fonts/lilex/Lilex-Italic.ttf b/assets/fonts/lilex/Lilex-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e7aef10f7e7c6803199551463d0ab456d9c9e03a GIT binary patch literal 198216 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�M^D(4(?eo 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�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%0DePy{N6%jxF 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

%kl9Bqb#n85HCsR3%k~1VB|MCp&{U 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 literal 0 HcmV?d00001 diff --git a/assets/fonts/plex-sans/license.txt b/assets/fonts/lilex/OFL.txt similarity index 96% rename from assets/fonts/plex-sans/license.txt rename to assets/fonts/lilex/OFL.txt index f72f76504c..156240bc90 100644 --- a/assets/fonts/plex-sans/license.txt +++ b/assets/fonts/lilex/OFL.txt @@ -1,8 +1,9 @@ -Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" +Copyright 2019 The Lilex Project Authors (https://github.com/mishamyrt/Lilex) This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL +https://scripts.sil.org/OFL + ----------------------------------------------------------- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 @@ -89,4 +90,4 @@ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/plex-mono/ZedPlexMono-Bold.ttf b/assets/fonts/plex-mono/ZedPlexMono-Bold.ttf deleted file mode 100644 index d5f4b5e2855fe9e581155d864835570d2e5a06bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 163568 zcmZQzWME(rVq{=oVNh^$3-NW;lFLdI_atg@JKiO-5>B%07+`7X}7K9R>ylvy6A|8Iqe=QNVVe*^hyNv4equK_M?O zH?>Lk#&!nALkk!fcnk{ii%UWdEnURGcrrdpOd21W)B1}2aw0|Q7O3**1H z3=B*W-~RnO%Tx;zfy#j?=1`Cb%SwjR3`z_c3~V4C0|NsW0|OHSlOzK(10w@7Qv}l< z237_JrY@!k1_lOi2+bJBkiuxnz`(@K!O6Lp8qP0IxLKg!U_zG(?Rk~EKIvVA;G}L6ai8Lav>a_Vv1m>Vv1nkWQt&rV~Sv4 zVv1nkV~SvS#T3D?jwyn{4JvNO6v42HDT1MgDT3iJQv`!2Qv`!FQv`!DQv`z=Qv`zq zQv`z~Qv`z+lyA%w!Jxtv!5|M+57HaT6u|&82SnRK)v+=~F!(Y>FlaJGFgP$pFa$s` z$Xt*bZ>TyKrU-@@rU(XgrU(X*ILK}vs2e~u$R3az%%OhahSDrd5ex=Q5e(a*ZjOcO zv0;i}n8p;rV8Rr^V9gZ40OEtpHiDY3&lJJn$P~d4#}vU31!aTW016|JILN&qeIPwv zOc4x*P`|_6#?BPMAkGxQAj}lO-~@I1TPO_*7Y3#X22j|8?9~IsHB$rwNFHQ%9+Y1T zbr&eiK;d4>6u~f=DT1K@8jc|MgVMFbs+} zP?%%E|NlVa0Tu@^8Wab}m?G$_x5@*O%Y%@pzf9JCAo`3YtRG%bMh07yT`98fxfxgF+4 zP&k9){4P@jLnl)NLlzRg%oM>egDHX`j46Tvl!qG`82)E7F#O-i!0_Lmf#JV11H=Ce zD0>Bz-@(A}KbL{we;8DJ6Qm~j-^{@9e?0@k{{;*T|0gpr{MTlRU;yQBP+9|FkbASC zq2Qm*-?t{`1C_F$Iqz@Fg zpmYvmgW?`UgW?zzZlLf0nF*pnX%i9Fmbaid0oe(v zcR*}umP0^AUDIrL1u#70!n)zJ76?3 zv>pNZ6Xb4?9LT*O`#|{?6z8D01BD;ReIT1A^KYu=WQobs+zP(kdu_g3<{GQv?IZ4WKpx$PFO(gUSz3 z+6I*ourShLieLbx6;Rm&DlM1#T!M1$%g zkR2clVuQp%801D!oe85sX&98>K>h+@ka-{s@-L`u2MT*DXd4Yw4uSFk$S#l^sC@!z z|AH_`3}im231eT84n5%5C)YUps)gA5T8o;{|9LM8k>2b z`W;p_g7PP*+(1@0hbe+#F;fHsD7`IUieOj><%8168Yp`nQv?IC>MEEb7;->qj)CF7 z85(wAVE7Nht_%$S&O+IsumaWhpnA6i+D-=5#~^+qQv?I3jSNb!$n`bId{BKog@NHe zs2)bQ2V^G59vF?qzQ6w%82&LZF#OYEVEDV2f#IJm1H(UF28O@485sVaWMKIFoPpsV zC@vWp82(LRVE8wQf#F{#1H-?~P+FRS;oouwhJX4D4FAqCF#M}$VEE_F!0<1Nf#IJ! z1H<3<3=IERq4YXJGi}#K7?HG{k<8fB$}EVECuOzyPBE-eh3-?G5UQ{yqyL{}_PD z_h1sjwg9ssWG9%-2_k>529wjkiSK~#%CeeeGs|w4Lo7F0?z6mP`O5N>&t9%o zZl2sv1vLc&1uF#?g#v|2g-(TK3hNa1D~c(qDe5T(D#j_!R9vOFTk)6@o05={oRYSZ zhf=EQDRp6WA8oENpVPGj@nZUAuWfjXhmTfEtSkANDVR^*zj^ziRyY;fVzk8_49Ane>497&%pTq z>;K395B)#L!0><3|Ed3d{+s-l{P*ME^nZQ->i)(2i)LW>=g+|Kcm3ajzj1$^{yO}% z`D^vp;;+eHjlXJtRsJgcmHT}2@yy4Kj~f^m9)&&fd*u2^>XG;(K?a70-@v|Qc-ZnV z`C-yS!-q-`K8Sp<`N4DshP#rtFW#Pc{f0OwF2Q3?AUQB@0h5?S2?GN|1*R|-kvJ4F zke&vHISh-CL>Dk@W7x%ThyjxN&LGL0VK~RYz~}%HVRU570MnU_*KGW9>X{mt8W|XvnwXlIpsr_XWtt7r!!(D1foU$&Ql@1L3{1jIF@bYF(;5Z_rnOA#z$(`>fpZxH1Jg#PvrOlh&NE#Ai(h2A1g0-DU17S)z`%5k z={ktdz`%5a=_UgM(=DdkOn0CncbOhQSr3^WF}-Gb!}ONv9RnK^7f6&5HJl+ruu(r4 z4HCtMK_joAT*tz|%D~3J&cMOI$-u?H&A`LJ%fQFL&mh1c$RNZZ%pk%b${@xd&LF`c z$solb%^<@d%OJ-f&!E7d$e_fa%%H-c%Am%e&Y;1d$)Lrc&7i}e%b>@g&tSk{$Y8`^ z%wWP`%3#J|&S1e{$za7`&0xb|%V5V~&)~q|$l%1_%;3V{%HYP}&fvk|$>7D{&EUh} z%izc0&k(>6$PmO3%n-s5$`Hm7&Je*6$q>a5%@D&7%Miy9&yc{7$iTqR%FxEp!O+Dp ziD5d!T!uvq3mFzOEM-{2u#8~^!zzZA467N|Fsx%(%dnnd1H&eUEex9(wlZvI*v7D% zVHd+5hP_Np4229S45rA(pZZhUG-DAjR0Hv7a3_BSb7?K%sneH$;GTmpo!H~mH!_>-<#L&pl!Z3|dm{A0j zw!o>WhN+fe2h(GQ1B{kTolJ|E+L<~)G0D`-P{CBk)X3Dv(9F=y(8SQg(8L@6vSK z2<1qHE`=`7lr9A~g^a|M4J@i)Iw(RpwJTCzVFN>OM5e+9hL8xw4GxjgaAs;`q?PUt z1{RPxEMRlGA{E>evY;kpC8h*LD0fBb|JB{Wz^db|uz^`EAR;hgLxYR-1_zf&X+=c^ z*9`{(A~qawkyeb1)ZM|r26w4~f@^K21k}y!Iy)HHv~+hca6n86j@aNJy@4UJs|#di zaD=krhK8;#=`Q6+7=H)D0W<+d26O=jsK5>eX0_mm4a};pI~W@RB9s+(Fg7SFN`nNH z6_phur4=Jxbayat>Ual7xVlIyDn{xu>|kKfX4u8R$iU0Mq|LaCfe$oFuFbfgfsa9z zL5@L!L68BIJ2|xXGw`E{FzjbwWN>A$WYA=gW)NgxU;rs%Vz6W|U{Gg}X5fd4Ff-^g zs58hjh{Hu#7%~__8Jro^7(mr0NEa(Z2ty!)8G{ysB2*U}g9(E+gCc_%xS|B9VP{Ze zkOrFqYP5hvI2b}0Y#3x2#2JL4x;Vjh%Q1+-?c!pPVGw5!V&G-qfU4nUXlBR(>r{b? z@Gy8Y*fJO}C^85^MR*ys8PpjN<}mDH5MbbBVAtNkzyOKQT?`HkA`FZrAPER<59Jp? z=>$gh8AkfFXvVn!%L8gCUz?C4&<~3BySSEe0=!N``3+GZ+{cc>Y^3@GyEZ zurX#curc~GurU@guraYP@G$LS;9+{gz{9}6U<8FF3=Irl7^g7>Fmo`gFo!U=Ft1|1 z#QchdiA9aYf#n1%4{I9hCpJ5_E$kZXaU2R9Q#clItl>Dt@r_f2vx-ZID~{_Fw+wd@ z_Y&?0JRv+gcs}u(@HX+T;bYRTXBvwiMlPr;ZC*>t|Kw3aLOL~#? zKba_*B{ENBBV-F?pUIiYnaLH%J&_NRUnhS}p+n(?Vw)0&l8BOll7%vdvXXLv@){L3 zl_@GaRFzbtRO?hvsPU*JsO6~DsP(ALQTw6JqAs8=qpqQDqVAx+Lc>DCMI%5XMk7O` zM595YM`MP@0*y5qJ2Z}HT+q0q@j_EdbCu>c%|n{!G;e7>)3Vdrq_t1$l-4z^M_TW+ zerX@k3DSwv$6__0>HFzN>8I)6F=#UAGni$t%wUtjK7&&R*9;yRyfZ8^{9!cBXpzx6qg_VFj4m17 zGY&F-XZ*{A%|ysV&P2<^%*4sW&m_tu&7{br&g7SAjcJGJ6w?K!YfN{T9x>B1+huml z?4P-x`4#gA=5Ne@Sg=?GSj1QqSkzc_SWK~4V6nzxhozKdo8=_Sd6ugzw^<&tJZEKP zHN)zkHJ7!RwUV`-wUxD-^&*=vnQ>xs~q$kHaYBbIOTB7;gQ2ThhL6tjzW%d zj#`dpjx!wJI7K;~bJlU5;k?9ogYzEe6V6whA2`2p{^7#nBH-fUvdQI=%Li9I*A&-f zu8-VY+#1}Txl6b!x$C%_xjVRfxreyNxo5b~abM=X!F`we5%+WMA3P*HG(3zvY&_aL zCV9;B6!4VstnqB|?D3r8xxh=oYl*jjw~V)jw~2R$_XO`}-tT;*eA;~ae06+J_zC&R z`OWZq=J(F;o8LcwGk-gOH-A6>SpjMRdI3!V&jNJB% zb(NMiQ$)Sqi;YIxB&r%9=4OLJ87 zhnA?8FRf**kJ@tDJ36E~?sO(}F6iRxn%6DYy{t!}XGgC}Z&&Z1KAt|AKAk?BKA*mr zzMQ_AzMj50eQWym^quLu)6dp#)1THqr~ldnnF($a3MR~&aAYFGM4O2P6PHZ9F^OlA z*QAa~A0|gl&X`;_`Nb5KDKS%)P1!eX(A_z*>~oo&9$1lZ0@?byXGF7dui^yxv%Dao5wVdZ{C)92j-obcVpg@ zd0*x;%;%XeF<)iA$$W?TKJz2yr_3*yUo*dB{=50V7R*_&X2FgH2Ns-JaATpPH#_EdI2iC}}$yoDXt;O1&wQJU1Tc@!uW?j>|73*HCS6Cmne$M)T8}c?R+wftd z)y8R?D20gnS22Qm&c9B4Q&@4&nRj}AOK$aawJpvpm&gJ}oT4z4)3 z;tI1GX(4ujx}!!HiM zIKl*mdPhJo;0OpVIs$?(j=VVX;;0fB+8hPJgrguhYYEsC66!1CE1W)o~DS_niX452rqy`fyqT z4BbwHV8v+=Ty%QT=|yM6z%b?v2zHzS!7XP%@ZOnwXYQS40YkmBAn11%1QX7J;IgwI zc3B3A8qZ$%2`Ofti7w!D<%+3#hXyC}g>Vfkj{k z1H%PSliW382Lpq?^iI&q9lOXK42*YnFfa)0U|_kkgMm?C2Lt;Z14Cm$ML|_XK}7{7 ziGMzf0sl5I^Zfae!sNmLTIh1>{};wSrdCWQftP{n%T5MX297TVhN|kO#-jX;N@})@CT7M)VzP`PY>Xn7i!3F~ zbaYKkb#*@fIeq%ie?0{Sc|ARO1qHqT{~4+n=P}-AdLpF8z%_wEfQ^9*JP^mkz{$YC z|o%#010@(3p*H?1$Hn9U4R6&=mi5q zb7fOwFg9jZW;A9u7F7mg<)mX(&|!1+fpSc=TKf$-l4y4OuN7g24)7i|NTrB z%-Rep4B8Av49j;hu!9B=z)oTp*ukK2VFv?ALM*~Bo88z0LZnxUv@C?3+!aj zWZ?a>i-ChdlYs$j7{9;{2EjWZIf&~u1$HnsOx%=@QP@OHUDS^8uXmwxxPqsqctT7@Vv&8MT$q-FctVV{vA02yjiRcm;%g;U z6=g0qbKQ6)HMSirg4w>IS-hDU90tk>dVI@SgwlO9JuK8YbC_}z6uG9esr_58q{O+D zLzjV#f$9GjrZA>s4BQNo46+Q047v=~3`ch{sDcJwz+O@XrUP%jY#iOpdgn*3UWn|fFdXfLDQ5HNI(fB02*snWMBhZ%b>rLfd`y^*aUVm2s40E zmj;6rI1G0%Xz1@?kb!1dxjQ=;lmvD%=z&#h2<&7qW#If`U?{AlW(v+qq9SbSMrO8* zCThw`Y_g0ZVvNSj%IwC>%B(n5EcFfw@&;j9adBB$adF1j!s&%u3#S({ZUTvdFia*W zIXpZWgu}Izb(FQh1f$@;D@p&Z@PLFt7)c6LcJTfG!W7PQjKPe-kzv~|1|`tI7TD8D z0y`M2;9)5M3K#*TphKiqXwVsfR2YGR4wR*KGMIydNr^##!5nO!fWS@$NwAnTgCsaH zgOarpgEcrwszBB4V$fv}0L$)R(AD3`pbgex4A)`6pu%7b)?o!@85oKy=`n#4FDMDy zF&P`lF^RM5F+##uj!B-$*pAuMSX7yhkzHI_U73$j-N?*bosSU|`sejM3?*%>6clAm z-L+I53>7)jxb@`ZR5?WCB`gitm|2(^H@UHj2sF(LU}yJ}6xZNuloBxJQ8f3|w2xEL zbyHJy)Rz`j(v>h*P*ss-<&%`xl=JWs)YR4#%u{eyo|kBBRVXOFMABH6$-of23gs2! zJjOhxC!i7ol*CvVSOtYFVYL7wC=3}H6zn1m421=a1z)u=&SP}@2O6Mh3V4WsIv}=7S1r35fZE`a2ld^bHId zL4|>`9kVT?i5`=>u^cnwDzF85p&$!F^$-@s$s<{y5DT_|kwK2ZhRKS_f`N}g5H#+@ z&maIXA7r+|kIL*ulUBD%(w!O^r>BMU6$3 zMc=Gj*RXD#-Hx&yJIZ!|<7CzUOvZ%_>lm0pVavpTh!g!C49xllhUUi1%8&PT=B@j; z8?3&E;T7W<#tH^z262%6Q1!dP<7x~HEZPQ!=BA3GJ&X>o`uafQ+=u^vVVugamw^|e zo{xba;$C*}Xd6g9D{5V%aY7%uRnl^J>ft(*M6O*+R=ybx;k=02=NF=SUuC@L4iSv!GJ-EfhpFJm0<#d6vF~AXD5Rq z1N#?8W`^^ib;_}h%nSk`Mxi5U)#61|Io2{dH@iVJ8NhS3T^_CF&7 z$p0JyI~iEObut^MF$eZPC`?$vB|fOR#(BZO(Ad;iP?=rb+*r_9G;R-PU&8WdQz{v+ z?DD?zcYh@~4XH6OFljR#V~}SE-o+pcnvy_rhcKud5JoC(p#=lDr6vcqotZ(HK@M!Y zu)t0RF0hy^0~Z4$xL(&}G&Q$lgjO@`>UPYaQV~?2^D`m+ryy(Ws@}>N(7D~vLxW34*Gj`k#nC`k!NONp$J4<^E*%th692z|-E7Lx02&E3 z1DEWuvJ0bzPzHHI8B!sD3V(L68%-Dl7?i>8HUXuY^9&{ojG)924`Hw}=rfow*fW?g zFvWr@Pe?*DWiSD2Q5M+Apu@oX#lVmeUWp?6j$IVeYEf3wV*<5nj3KJD8BG}ltxS#e zwY{a{MGUMw^(r z8M9A{S*MiC;CQ#+XWXrURfs28UAs*E5gp_WO zrY0Xecd>#3gcT`P5vdDQ!m~oFJw`-p;{v1uzhKi<&ri`Fur7Wsy zEU3&F{O=f}*p{fMf9nr&wly$r`R7p5_KOiTbIr)0_5TZ#Inyo%Er#4(;D)IVQX10) zxkeMoH9{Z(AxL~_3hV@xW?w+sgcvlzfv*CIFBMRHu`>8Gs4&DcsDPu11Dy7S1a>mW zLLx_z-58PNppn50jSOWaHgVdj&YTWz>umw0*6WR9w>>jRS0T1#)?$m9=Fo zEUk>S%=xON7&kJR-<8mm6_B$GHZhHIQUwRUh>A47tucq8g`$8UV>bh+9i#UD3zH?& zE(Uo9U54ac3<{u$Jg~DBQ2IbzAWw23`5HM6)IsGjSd+T`P6jb>(?gI!ok0vN2C4}e z1a>kgfLnB|;6?|e&Se)z_!g~oXJDV~qN3*+t$W@v-Pe$tlT%8~SV_mnTtUq>$=1xm z($v_>oJZX=&EB&)KSEA7I5@(XSwu}%NZvBQ$TZ4PrPR`d&&1r^gwMnhl-3v-j2Rf1 zG?_q6CwCCR$-spar+88rCVG!E&^8?nxJ&Y z%n;82W`N2GO$K|AdQdgvz@W(x0OEkuf;2ONL=zY^845t6g*zDxz_H1}U;s|dyTEl6 zC|!eU9Z2)h6w!VZ6=8!FC8+T!3@K0`ZA&pm9s48~Wj|XJCk4AGYpZBG1t$|*KV_FB zdlPdrQ&Tf@6C)E-Lqk&&9(glw9hYE14q-oA4HrXcX+swcTR&kA!C)61Z!`H~15FVv zeSIwvO#?;~J#~I{U0rp4bv;llF~~D8Fd2dC2`uSa8eXU_+goNHiyO|i6!7VUVQC3k_(NM;9 z|E@D?zUg|_HJ4*<7dTAq7@C=+81I9}WQ;)NH3tJHI4GcFGN6_mXxfoQdp83s11AFu z1B3%HpgY^o^AYN{w|$9U(T3gaEd%(Aj)Wo77Y0&QagnF@9j3&c&TrmCi_ zqO75en*XjduKO1>0pz4-UC$Vp7;OK4Vd7^x#vsF>#SplQK@hYY0qiD0fgKF8@OZ|^ zWwM}%mxYwtpj^fQZl%Zy>|{^@kLbvNM|8|hv9<=-McGjrRu{Y@3{^BuxT-jnE&NPO z!|jx04INcnOm%flP4#qN&SciJ9EHCABr_^PL%pr8vL2?B?& z+W&qgO{QZE#te?2mI61tPsuH?gTWr;7iUn56*L3|9n}H%(VYcAV_zV@b2B(Y{I0)) zLHxo_1`}|S6=pDD5C`}DK(oM*p&eQM9Sq#i#0#n{cQP1(Dh zD;sdLi>MmQt6FJ_it2foWLnC|%F0N~$`+br%*`yC9j~e89A)ya$5vG(y`UgnMO9kM zN<~1zD#g3eD#l)Mn}T$Yw1S3(lZ6I2yg+-?f|)>RmzyC4RH{RIG&>kLKp_GOFYtgD zG)sfS3M{|^ihCAF+=B&>D>E(zRt6SuD01mT3Vu)(02*^MFjQ1mW>*$A7G_jtHfG$c zvtN70-Ya|m-Q;kYuwn(%uD@@M8AYLGur{Np@7MHPYja2U{FHxR?wJ`GnfX|n8W@;bGVMwUvvC(-69{mR z$jDSNkTp;_%BZQVC#R>hAJmT3{Qrf?nCTdU0%)`pv~3bJ7zS&{;^_nJU;wrB1;IHC zRF6r3TRX5`3wYRp8Co?M8=0Al^D#5p^t8A4m<8LYbN8?bNUBQN8XMc1=yUgQtJ?(g z>|L{FudIoeo|2@NoM3!NRY<&pk&>R5sSE=Xg98HtlNq?47h>=RB@JN)5pb}vgU7&N zuEhv(XctBRJjg2mD(ykFE~wWc1a>VnT(ueD0V?S5u(zY5_u+MCJ3D7*rd?ZBuG~@< z7E~V?_Rk2?<_KV5VB!P!nZkE5aH92@pj{J0oI{-ib^#;2x5UA~0&U-N=!3?YL9POY zAt%^X#)68XFc`qN@81nZxqpusHJEl)FQ{Hw4T>|68|0Zl*8748F7R+KEELFg19)H^ zVzXcXXi;dIjlOZ#KTz|Tfsw(Mfq_XH9LKXT`^C_L1`)^5RE7{>289GOQc(mg z*um`&Mg|rJW^m$V1oa_6D~T9EBL(vr7#Y?xFoH%3Kn*)aNDI%Nfsp|$t`83jf7n3R>&pq*D!L1k0M|BAL@rrD(eeH^;F zYTD13cKzej_cfEv4wu!nFf?Y&1+C{}WN=|%V2Wni#lQ>d{cwPmK_aD7jQHaOMHeSh z!o`R`UXTi2P=tcK#0iZ*(1a4>1p`Ayc2H0V8rw4(GYcy-O4v=C#^F=F;^OU zn0EcUQ?lXT&40O&@*{?UfeDm%Ss6@sF))LcY$CZ0Pu|_hzzmLdP>d^z#!P3_VcPZg zAjH4w3=B+$OrT+iLXeq)3_?g|Vt5bg8$^1ACSY*B1ZPy}@C8_aS6~MN(*;QN%Y0`C z1E;_a23F{d5jV*9=E~fR%Hp83C~j;H>V>c8;B0E*u4G&8r81Vyj9>TpC@q@@;kYNUf3!sM34hHrM28P1mY$6N_ zHsurC4O`}OH*8_N@Xvs0*FOWs3n2F}p^OVUgGwYR25E2><$2;|(t1Fi)@ zMVc@;j$pMQsM1F5GcoFbM(RM=!O+mb!O##sN>>>lS{V`#uK2+1ARh(>CIco=F)P6k z3W{1u1}U(cK&=dfyYQqKP+Nr&TF>z7!}1rX7J}5(#)67Qa!jCsFl|OrL1S2(!iSO1 zIKW&+#yr5dJkV5A(=?E2m%LSwv2l=<{6EHW4LvzM4F*t{IfBPhg&7nX5_d6(fEE#h z-6tZjgMk|!ZqTSfgd4Qj0jDj{jH8e~qO{q;0Gf*gl{zBehAXHsD-8)zWl-B3)F}nc z!GhaD#;767#Mc+-lAQ_~3Qf&k_y`)j`~93WMYm4}4TMhLF6ws>IgptcKw)eLZPOKi zf*Ka@d;&Wd1mJ$am@9?`I@k~330Ug9hP2 z4L#7Pov|`B{Mp!rm6?^{O%TxsQy?+mVBNW-)HMtg1!0gV@X8YW_mYvh1RMvaAmxet z|1V5BOve~BKxG{}Xz?~u-WLHCTp|pJ&__;7TU(1aUm_` zffcHuGCl^Wy2hTNF-k7UJXS^tS{eqrs%jiO0@=Q)`9|9D^2)&~DxAFhNx_D}w&1M^ zj0_SC3``E-{(vp04ukYgVPyq0F(Ld1jVf@=fYK3YbQ-j@z`&4EP+1T(vcp(D?Ie>1 zll|X=O!n2F;*)`q!5rMK0nHb?fhsOYDuCG`2?|3=q%g#ojfd81;D$YtB>dj)#hh zM~(;MGVr)KCv?34CxZ*9XHKA<#}0BRJCZ{Yk-dX~1vC)PY%DBpY%Xlfd}!zLAWaUf z;Q2e5cKw@s@+4z2xV^y5z`*3o1nP8_gGwAB24SSM1Wojau*S&R$o7G=1++;BjtvIT zB9057`V&?rvFSrfYf#$)RF?|CohWQ940fh6vof=pcB2Rzr%k&JC!0u(_C8}SW2RmI z;#wH{{uMPaF8k}o*!Zs=RHwkgN|2!(R0pEE9it9}_QepMfd(GLsi4j{)Vts;&BDMB z&eEXCMow^!*u}uXzys}4f;y&xpwVb$eMV(|MrC1UV|hk!Yk8bCTIo* zO@uN+2g*THe#WM7H;F#HyWGo+!^~^p-S3Qt7j44HU}w+-`2?QNA@g~V;S3gVtpMsNfo7Nt48;W@t;8Z>4REdpms33z)l7>aCd}JP!ZJQFlAi$Z_nAYOh5kAB{S_whLp+D4DZ2n zAnXispverlS>Q~82ulM)Rc2*obJ)z$Eh@ zWVblT94vFI;--qCHjEDrR91r2Ph+xTEMZn>U}n$(#UWHZXx)yQ|NaVF6?2a?QSe9YwWIV z=ON5K<%^M-nUT7sCDW6ChxBX|oYLIf(wr2m_2bfQ%>vA9)ih^ls)5ps&Hpb<@=O8@ za-i`aLC}UUq%t2mLxk`Ua)fe$M^r$$0+b^`qgSBz0cidJG)gW8X%m3g^*{!I7#SUH z6P;C5oD*$A)1`~}bhV7l%}uop1q)=dc~sp}?Cn$BR9!9YWfT|m1 z1{JWMIRtiqP9lSP5n3T2yoiV)$m%Rm&uu4z6gW-sGYCUF5&WRBhX7EuRtQ-LD0X27 zgP1;ODcw#6IR?frpr}-X&3DK$DvFATL1U2_v{uX5)I^QZs;N!cG1kf|#zFbtd^;OG zCy4;LoOlDv3+6l%YV1?pRaIRQZFD$|Y?WohRm~VTSsQV17+XQdS9F-3Fo=N0kpw{- z6Oqy!w5~?vDrlr3ya;X8g4JCsf4eFzs z8k?G!Su=_%gBHOE8jC8r&sem`H*o#VojVu(Gb(13EB1B@F3v6f_ox`Ow^IMV4wE}m zI)g2P7efTYX;9f3$q)sO4bZ|#cc?Ai@RSIhfkHSLI{%Llfc9a9KxGGLCSM60LAngg z(4MC*D1t69=t7F@0uZARw3KHjg9igAXywQS1`h_t*j)^k47v;+V5eAuW)AZiEE(z< zEE$*zcQQCK@PbqxU~pt$jNQSYE3lKn8yrZ|0y`Okz=bNS2peQdz|0&xb*9N*a0S|J%f9mBJ}3Bm57%U7v{|j)8$mkLf*VofK#t1p{c~E;yR`;i(L|NCA37CjoGpK<@f8F|aa#mfwN8`Jh1%NLsLGG-el8R%aJgcAUcQnzQP}-3`+W zL6IL5SbXN+i6@4eL1`54&<24sads3!$#CxQw-P)~~u zw9XF_TE?P^F7xIw8qND>P|WzR_-{Wb!a!>f8JMh?K$BR@L30R@77nb>3(Xq{Z$n#l z2mxpZ3LyXuIz+(8fT~s*NYx4sDMU9JxiQGhpaAY8)PkC+5|9cA(gNaS5C<;~tlh!D zsV@PVoiG*!H-yB&3u;6`nM+Msi4BP%SI*9(tLZJs#wH}CB9T)rDIzRgAub{!!^GmF zViF;wAi+2PUq54#imas6zX>oJ+}8%B3u$nDn~gaJ01a-0Z!rQ0!~4ihKTxFvSt-rT zzzObDLwpPK6MA8RSynKy#9=Kdm>59wclt~M44`p1@YtmibV->Ayqth$F@)<8j@`)s z8EFHx<)k4s0(318XpRLmHi*`)(XmMYHQ^F$V6>5ig^`hk1-tSx8WE-?NtjVStbEO?Nw-J0O40?{elpH=6gh@f>udj&x1-w zNpQ1)0ZV&Tgh7D;S9=xIaYe1ONoudg{WW3K2m2N5Ye>231zwxO!XO4}(}6lKph;|4 zc>?tZ!eh`eG=u;&RW^I1`#Y!4V<`UgHO90q`-%LI++zJt5HU8qg2|AGloG#h}E%4qY{;qz_ta zB@15Z1#V!&rnbOism!L}Dj3plf{djyi;A!@s)f}n1j_}Q=ol%<1uNU=NYxbhWGi@y zdl-i)$ptCf=t|Y{nCSW{s!Gd<39|CCimI5X2F7Trg-9xyh{FZq7#JC}7#Ns@!E*)=%EOzqm@m;GYjD5 z=7Pqej3EJjJ<*CX9Vbto3~~HZETvqz4bsd8^$Y%gVG?IzVGw7?1Pw<^fJX^nae+~a zLAwEn$bqIZaO8kX2ITe-D7S#xKcMLyK4|ZbLEpd--kSrB)WU{rl^L7a1U>CwZ8b@O zDO2Kf>_F`_M!$c@!0j}~;J<%B^|mwv1CuQi3j@9}T7(lZ+L*{;$_g0(g$^EG**VAA zki*b<+D;~xyKC0m1zE?y$iU9Pz!b>D!obf^1sX{aU=T!#L#Sy8`=CLB5WsK%v?50Y z8Z;e%1wb3DK$Cw=pp6gE?P8#Xx1a%92xbP2(JHf7t8&>k*>I_q+HNrBGTy+%@-L2Y z5eWbN!bR-RuyP@Npc$^9K8Q4|exP_J=;PF{j&>7j_ z5l24IxGb2($iNCN|3D*$JHY!MjD^*q1GDVHkfB=fgIiXbYjJ2>%-(Wvm7^|)juRu> zzhGzQ;BWsvCAzvMg5p>BzXg*kIJ_e;!y6+_LY;~52s9Ico41gHftP^`I@kl6S_LhT zXEbK!W7K9eH8uy2(wZ}_oBmnc$0@Bw;lg}xeGXkFmcJ`4ZHgn97;jFS26B@V0|S#7 zIL!rv=0vcz6%kH=HeSF*D`a&tC@+Az%b{tgC3@QOQ7$;i*3 z0xcOqGmxMmy`9jN%;$zTa>b%6F)+A*2iF`9yUmY_nz)I?2P546Vy(o+`^ z18>8FE=FZz2W?t17G?BuvEmjG;S}UGGmDBcGvgKH6cOOIa@n*~l3!5H%Erb@PLN-6 zXP2$MuB4irIJbzbfRs#%jFf<^2$zJMnxwA2ZL)!yhKQ(vf|e0G2fLA$f`O=rhMGY# z10%!R|7DEXustSN#*xJZjRoJ}0S4*VsY~R^VCy zv|kBSxq%8&&|W!Zu&f>fKX?isw6q_z;gpYw9n|#}gH&#!ppXFVZDIqZ8E_~;_A7#C z6z*ff?4f-VGWZXW#|z zI|5Y<40k}C3pQ{XfVATwu?}esi!tzmTf;S=u_p-wLsca;b?|02=&-K193ynliE*Za zrJR(Su!Jm6mVmzvGUt*QKd+#olA22hoBC4lRQ%yXg4H7@h%2A(6L(JKD!)vg&@pMjLsCaHvul(z)RZhfO<%v zQVT@yWRM5<+d*^VpxJv+`xdg_71Xtsgs#kyx?^C-3LC?L>|E7jR2Q^kG#BS%WETZ( zTovSF6lG+J%gBf;vhx$q=28%sQsv1M4{=zfsHmV+lq1-co7*Lrlf~s=U?r-;_3tdF zs<^d*16LNKm%1e%qXDlaXo)5RBZD3T1Cu_}F$OKroU;;ibuP?z7^`xjWg^&jpjEk` z)d!GNpaGs7XJ?RL&;SP$yFO?(36vM)pu2b#K~HZ=w%aL~{cY-|~n6F|Gz zK-=*|6-8Y&bQ!bVd4yFQV=R2~)RM)`we-ydx!3pbiOcXZdUgofX>qXR$=Iss`IyO> z8OO@0n;OU%O60LiX~>DOb2fm}HK?-^0d6Cefks^+sRrgZXlag^kAqeK2mwSV7t%2Y zSlWugoCd8H*}ftX-S2 zU`{Vr-}L$EYuEmhD9kBj%=tG3gbP78WiT)yl~wEvNHbWVfdSA2IKvJG(CinBKB)b} z3SNN?+EmF5F2U;R7$-8O|C{;uDToBM=b0EB{|7M1F&$&DWe5bdG=xBhxPc={NMHv8 zc%&0F$q25BRX{y`70ARhXc`c*Jy=BmwqsR@K^wf}eiwrwg9^C)zKg*XE+)fZ$lwaD zvO(KuK&|^-3|0*O&;n2ubqyH2p$r+WL`w|vjB=nBGibYlI#?y6I=I{ftx6ELV`TiV zXcc5=lBwe<<*24(qOIqrZLK0;YN@4)OFTx=$d^-qLr4ThGiy5OODbqYNXe?oOUv_! z@+hk7h-tfv3rKlr;g&itrNt-8#KtV5DGq0W%8NWEf5ul#&lq?aq#1HRE1_fggHkAH=57L*v6F!vw2P3PA%TINp@4y%p@D&&fjQQZoq>UY zok4(s9W=DUz#I$8Y>-iYP~igF-Y<+&t{J27_US4s>*}hgtk+dh(bZK}X7X2+l9EzU zk&=>9MX;bJeF`x}Fs)z^X3%8t0 zuww%6#|4eI%7Qljn!{DV$_vowG^9ddX4J9I4b;~U%(aKn#^LrVD)!;VP})Yz#Xw5h z&{@;YODL2}UP?|$NnS>gJCsvO-9$()tjgD?I!sS5tlG!7DooE@%{j{0#yHAZ4bBLV zG4{~W^)!~T)YTT3Rh9LWRg)9b7EzS}ofpTz#8AuV!}t=smj%lls{$z07|fWs7;_j{A$x?`z#VCZ-3*}7GbZq!VpUT`Q$<0>iT~Cz zo-sR6_U{zfJm_9zAvVyqc?K4SQpPC8$4tk->C=qCj^Pui@Uv%d04FI}3N?kMRC7?6 zgUd5Sp$@8gxxegU&}NVauQ;#S!Jw@#;mFCbfI*vK1A{ih0S0Y`3k=!}ETCK|z@W{* z2-(RXBJFklb?AFan!2&y_j z*6Kj5-NB#(DhKv6=rAxA?qslK07=?0*n-zz?_jXg-^pMG7Bd0`9oTo;pezL*+DA-O zLWc7&vI(e%E+#4hJ`lj%$d=JWn^BZeN6pztM#cz~pbceZ4V~3w4OK)%RX}Vo&2AB+ zrlh29D<>u)B_+WaC1c>MuI_9g1EV#Cl?>!W>~Q$c%ICZ z&UlpR9Axh}(jIQexg~6%vmz8l6(=(q{X55WE*(-Y6)^fSz5vImJm?G=2Iy|#9So51 zPFS8n#388k0~I}>lnyF-q!@U>DP4~VHoXZ67cqM*H>59 z*Jt#THFDL^a5a*J(eY;L>SiDeYBn&~F)%PmF;y{eGw_3E(-}aAuYzNP=K@md-Oa!Y z-Z;Rb4N8to&=~^;(EfVR0XH~y>Vq~}L3Zo^yTo{B$Fj0z%gUDRC@%-~2d6Q5G9F?& zz#t2nc@c)@eOQ|X+9E+TQJ}3&5m1v?h5-~TOcy|fhcJT-*s-7$*gOoN0z;aC2fXgd zz>rbV)Es=?ff{(rDrh-^7%15I8I2ha{kvZ#B*x1sAuKM<8p-Et@8lySuPI@@kQcFdSR}tjqE=Fgto3$C{gT{Jv7<7@w1E2$;2sc9~R7F5L z_%s>#z>}mkI~X+eB^;T-naVcXM-^l2+;9gMjL?=mc=Q=803E>w z4=sR}WPz3(i-OyFH9Hvi^(A&ONHDO1M;SqjwTuNt-~()?;N2W*>dNAxj#EMf6(sqm zOkraakyDcpQ4wU5J;8M5--SHTd?;h^-#>98YH~t?lC6mhj0__Gvzau&YZOgDW0>%D ze&8L+pxP3V^I;uphC855w~#I4@J4|mXg#Q)@>l&pJ7s0NKz&Bza@X>2N;biUhQT&U ze>O7R{;LW~r-vENFivHh3Yv!nWmi7%&;(tKisomX)Nd{hOP0GYGz zzYgO*1~~>f1{2UYvpjgu3j?VCzk@;ijw3q*KZ7{pad4Xr)F)uS1F8x@H8ZOc zV&SbGlRl%lF}pZC7cyFzSo7*}DT)hAvI)wHi;6i5vG7Rn$Z3nI8eEa(x8M=v;br6D z=44{Z2xVbrQBajq660iGWLU!J$#@>TS08--12?#B4l6gHy;MXVgvLI&++YNk8=%#2 z%;0>(0y_I0TzY`UmRP~Epx!A5Xa&Bhu_^TUhdr_WY6V$=jGq3-R6H#H-2%qk4i!0i%T@=7T32ZYp_B{B6gFfn-k zw_^g$O@j8r+JM3bGD`v53kwS$Xg>@*mLUrs%iv+)_yP$K5y&!nXpN>?jgi(SEOCAR=Uzm;z;8m|Nk?9_i{2{6jB3?0t&D( z+k>ibEaG-3;(Org!Kb~$)%U~2!S^`8#V5eUZBWd43l|5S49CF0^#2b71CtGSy}vL+ z52&ny^zK2E&Y%GmSjvY^`Xf?4MnMQ2uLci33&IDQ1@%E&890s2qj-kmr1^{sXHT3s``>Lbn;_73 z)cj5PyYrcX{vBnsW?*E{{I3JvyJ-WOb5jBx$c|LL8-aS|Mvz`PxUfd{g^>WLV1YRX zvJ(Kb8blaU5(zWNGYB*2GYErAB2X7i=?-X{0I1UjQly#3L>!imK{t1rG3^p!W4H(|N5K20m_hl#5NsbTA4vcI!ek3} zhY?sDme0_|VeWv4gWLg84|4}Z9ArL39Myb~`yk?A_c1UsxG{J$2{DN?a5I31l~C8N zLvs;*5nv~+2GaQF{Q{aYVF0g#1T~5vEJz^QEDBy= zV8f`+EXJtgZ*CgrWYyXOrT;Q*lhSoCEOanr+9t^(A8M8zy?FIL7|p1|EyTrWBM~3+ z?{a)d6}KSwzatWmItjdY1swMlC~=Q24vTw;I4JHR>S1vY5eLOFL>wH)|Nnz_Dl>(E z&9?!Y4>pGZvUf@wyysAgAsMu~P8!;M0F4hK)_Xx)nTWC!5hJ@m%g@1K30nFC3T*~( zZv)hF1Wh4>N+)Ik(Arh-b}2z)QP`RkZAMYh1QDdo?K8c|B*08Y#w@_3$QQI(%9m+Z zrGizEkx`J9LhC>9UMU7f2AlsDOm<997!(+KK|OCp1|_7j5;{$aSYL%PO$r^=Ml^jD zK*g&9WO);)%>$X-lV#ulH|%yW$m;K8kO9x;a5Kn4g9bGE4OwpkT0ICF)d#Hrg04hW z6lDkZDL|7tkRFFNqp7*Fpp9?fyfusg-GMQE6;77+)^<)3)d!gTor3cVvySp7hGysH z>lla|>g2Egm;L`g19@i_r0-qSl13fg0Lw^^87=tjlp$pk& z;J^S<3tAhi0#>WQ-~e4{t)LHCJ8cObi!@{~VXy={TR~tag9~_xH1x1V&}IZgw^-a5 zT-C`kD#O=IGlObG$YG1N(6boD7{RMlOik1}WeinB;vzJxrKCAy1Ou$}>;ze~eAVo9 zB;Knk%gAsfa!9J1Dw&1}v2Z#FC`ihxYDh40DJnE6@Cd6}sRagWN2n+$I@+lzdn(yQ znA`GOxogWx%bKW(ncLY~M41>GswfM~C@^MNYbhvbS%W%{3^xCDz-#`kKx-_h~PE`g|0CqbYbgTb}$$Vz}Ax*3+!MJy#Nx~!5{@`KS(jiGe|M$Gf08k4~BPk zFlgS{$)Lo*4H|hd!W@oeB-nhgV`6kb+chAa9)!F3keH+5oX8}AvYo)(l}Q_W)4^Gw zD<8D`;QxOH@Va|&{_y~ncMQ&qkobX3}^@TP6iF|>^>tqXkG@inIAUjCMIsK#|Ww_^%&KSTjG&XG!E@}ONki^Cpi>w@ zYrH`{ZlOD%qh&#>r9tOzq8@34+RL+Jyz|aLL&E?Ge=94ysUa&Ns{tk$+rSi<0Jp`& z{~u*?WU^-foq(eYYKw`&PnHt}?@HOh0HRgkWjRJQiWo40tUUoOKm(o51X?S?&ASmFoL(Xz=?HYu-2eaJv#pTDOHsr@=?JWzF$7s0oX^4H z44F`I@VX?h`7vPgOF?}S1||l`I$MH!*Px|5BL71NR}jTMbnPRe#UlW!2LvGX0Gj}4 z(g_r6*w)#CmVbhdw1upH2Mtq^xy}}C?;6@dTX3HU91h^{frLXQC>-Ep0~PQw04q>F zgSC&C&cWI#3hEAM6xhk2 z1z!Ck0X;t)bkI5I{6x@9vAh6iG7P-b6l)mhG2+`Xs*0G*WU|2)L|TR{*f)_riPlus zFcZfTX3Tn87&}XgK)XuSRG{?^s2(u}_oviA`IgxZblKGZ{|w-LnBe^E1`=l|1znQ~ zt&fbELH#v|dIuEspz;E&o-qVP{Y)V>28eoVWc5t%h13`z>cdggABC%TL{`r%4Obru zRqw#Sz+}b@Qm+njC$r;gP)32Atz66Wgh7ZwfkA~~7U=u{$et3&xoDvMg#rRQ7!Y*> zw84QWfuIM_BLpytBxq(rBp>7Zb7oa^(M7e-i z&$@#F)SnauuZ?105QX+tz;i#KHXb8rtos0{9S>r}!x-m54AArzXl=*>&~zk7`U6B1 zR3U|=W!OT+dHfuaV~jsUGB11+_e0uL1ngAUY!oI8hfJ{oAhq^PU4^&QYD zX?HGxPD!)N%VRVzViZIim9`+`7${AE_Cb7M3IX@I^*~_(Iyx0{9fP944hBRS3GLt^ zA|4v8cnT6|@PQ|u6v4f01_nj&s%}SSh6Dya(9{4s0|Nseg8%~`XeR`?BLW(D1+_>) z0{|M}VOdajl#c;&2A??eoLfkTR2sA)R$vE%+y$gH;wGR4N1z?Qpe0(6Bh&?z(RRpS z+4{vp54l7Y-Kkj%^LmJ0q?suXK(<`nu>z1HwCxCK`mPw zc>aYpWD!XeTAU$#3=I>ok3k0+fwqx@+Q&N?Y`_UthCzYB25hp7{tgCZ@L1+f21D@F znT)_r1_$sE1LSCBGjnz5K3zxxMBb3i%#Ius%#c+KvW$$`61vJF_U252idJ#XD(D-v zIfcdLMWvO+IAQGqHgf|5%PXA13KE(o;;gL7t|^Z8DJWaGxdeqJ1Vm*-g}9&%1twMw zV=HST4sbgRlKw#T0wg_wkFtlQS8zQ7Nl&4m^z{Ef1NbaKaJ{Alu16f8;^4j3%tAtH zOdmnwj3J;)xZ&zS=^LWn2}M00TzwdddQkd?sJBK|4^H1+VDrOa>i>NPooy(j2HGVf zz|NS%s0H>H#640%Y77wdK~VLOv3O9utPL`U89I*o{}gD?G4lx_HPGpM0_=<-{}ULt zF)%T(F)%O%f!CFaGpq*mC!_yVC$Arj9(0TzO0Npu`NNCUjGjPvcoPh(p zo__~}IB2Z^1A{mNV=QD=3e=;7NwR<>9l%HUf)h)5VXdIL4iRKwD1vB^b3M_#d9#Qfj7qN zU;v%4$q8;jf)2#t0G~nxS`Gl3&H^rw4r}HSl_WZ(CQFqhXaw~pvy%N z2@IOzz~wf$KZeL(khQDaVDEzNqXO+G1C4V*&aDGAHl@MEH%Lwiyl-6-yl)*|h%172 zUYaTj8lzNSsKYXhS0Ft!#(xvQo9>arV+Uw^yOFy36<8k)w4?D~elc>WFiRn}yu;HH zs2qf(4bTWMXgvxzZGiU>;ofhK2qok_MCj2CYCEFtH;)559JJpYc0d5A5BUXr7Aa_K zzz;Op11tYPd(04RLFlGFMCyZvK0*LmM1Y%qpwsn0N4tR+iNmkCFcnlZ7F1^9Rj>&& zGYhs+;h4mst*fT}@5HoeumeM7joq~rbgc}H8DIPZ9RkM!9ygI?de6Yj0BX5wFiZwb z^=UF_fzyx#^bkxC&3$191BgbuSbtaa-e?ple(T_$zq!@6W2f9Ck8C*B(Lh3fqvbg{M8KVAMFzJEnX2t-JIAaK? z()<6P!R6m)rf8;JLTaF$A>g{x6>N^ZQ$*sBi{3*-z2 z9Ottl0uP$@5CRxAAaoNCxCUfnKtG>V6?{IcDCk5sc41{kHp3~fV;G(oZf0V+5DPkm z0qkDz*{tBcxjx9f%yytvJ|O=wFffTQ-xX40@MmBUWM=@4{cdC6W&quC0y)hdGUgZa z?=$FZS0Ob9uztoIMq9ACpzsj^hl4-JT*e$m9k4jqKCn1MeK5mzu(%5Y15*T89HKrL zMLj5-A?m{!CjMLZ|38BnLj5NO0d~f4hRgqEF))Iz6bHG7ftO(-=;%7=+BN8;88p=* zG8S~I795J8gK0sx=Rjs=xfs}>GqYU!pr#8mcrF}yLknnb9aPJLX4F~07YKkh&%jvR z3@{y_{YyOH{-q|PDZ8n;ps}F2D7&a4ll(tVclYT#b}%Y@5ib;H4E=Y*w$OGN1LQ1U z(770b46{K)#_;ui;G!AYT0%4zpbZLe;Q%S8Kr^k7xmnPylaPA@ne{;(b9U(5GiYHB zXv+%3XP^xz2%kax13F$G64wwGFQ}9N7Yv}Xj}N>79qb!rQ$?_Uj6r8^G&as-G-_-F zox9Pim8-?bQv7e5R<72rVsIQo+K-^MQING9py44ane1H>?T_MBzpu=cD zfiDQ&fQmRq2eh9bGy);0EC@baNK{an*_d59zlV)OLRnAFIjBcXO~I%qv(d>FdOk)> z+`mtNUn*Y`^J-H03=WPF5FiA6k7AVw!Y8}YQ@vycIG|40K0Cdp`LICPtgyW$N z2!sH%;|DI=L3J4m`0!!aDO8{-8PKjSV?kr^8N{HH+tk=pnORxT7IYS|pZ~$D+Vi=g zrx6P~hU7(oPA3LM7G(V{Xx*P2Lm{a34qr10PW#Y01rhMjsuLjqbs55`(EBmKqpghK zRt)I$S5VypnsZ?RZ*i#I!N3P<2a6jE!j3Kit-OG>ghB4Vx(RZA5z>i9z5y#k8T-K- zsJDfRL)0q^sWCv*M}x$n<8)ro^(*P<=^b$wEk-Gc zoFLf1!;7GGDx4QUo5O{nB?{+{mKpoP${7ZxoQKk&`(tmI_7B1DB9lA z(#~i)8@$?&&Bfl{W$TI+Tfyu7@)_?$MV3W^W)PVfc%bVkv=}TH92qw5Vi1MjC;}Rx z(7ON{W7xsq2+y(5A`y`+pcxb)zz!O>XGfZnK=vbOjjuL%WdsX@Hh3crr1xY57879* zWH17kgrM`KY{6?OK&==hu&f+|EqIyD4hA`W&~aqYW91-&@`$NmEDI*UhdF{XJ!sFC zDtu65xw?}$i-?7$ijB6oxVDX&j+HQrwvVcjTN<>eXe zWfc@;dE}K9l;j-JJw4ML<BmNn}CVC3R>3IS_e-kbQOpO8yMxrTF@{3T2cPo{c8?jzUS>PcEm8mfL(VS&yC)1J&QJ;! zho}dQPe9Z=K*ho5oq*bK3}E$)As}%EMh5x+I^c7p-9V#W5)AI(L*-!Qq$((ds3NT; zg(fO+(P9r-^I^{*&tT7>&tMN8YUKxQVZH-eWC~g{DkuOs^%^pH1G=>ibUB$0xQh;& zPf~?mc>=0ytk8~`2CsJ417FQ+Yy{e*gXh?33?;4@`B7&d@<5s>vx zu&`7Ig{3;uESU{Rzy^~293f%p$RN+)$e_>Qh$Sp-K$jk0*ukJK0J&Nh6q0)2kwiz( zO?2`M@eJ||OtCu{Y`}pI8siXUkOv3wE(T);bui13mBE3*m?40{n1Lx4G<~iIzDX9e zfDvt<0b0o8IHU$8;EmA&-XmKE|M@k!X}E&kfz?QZz{xh=G3-cz56)YVv=7Q#khBlJ zQ4O9~KzR$2_CrBw|NnmmiGQC#XN(J}F_@#Y&$a%22A!89q{a{q70(2l4?dF|JO>6+ zk7!$g&ezdr=7FmZW@Kj2U_hK<&JQ{%p9wT#g|yxZ+Py(EIS_qA&?t(Ta5cLM4ngH!+;f7Xzppkqw@J3$Hs5ck5 znE{&n0v-0x1zjoz+J_99b^`GYRhx z@Qa9m9v?J)`pFPS`^*r?5keuEiI8#_vW^ur)&^O}3L9$!ug3t7wSm`BFh)VwQ9#s# z+9eS6uyzSVJ!q^Ate&9&MLnop0#OfZmw?rS+a+N2jA1D1L3cM*v{=?RRfYwVw z)JGtxS7ipZV<75b?HG{xD$JmE3`Bi2NIm%MdL1T3rd?|pLPwJs1ashW6%SqL(nNDpiwT+rOc>@+@s7x zz=!of7mT4Ee$Ohdr>UYN!6Ctiv?9UIl8Hlz-z(M><4k-5A15nWDFrcp`1%A91#t~S zNmeFCRnKg9@KO2Tv!isF%$Zmi^g*|*X+TH+U}Y|1XAHFS3-va(vKL}9|S|<)s4_hY=K0hAZKCuOv&kXC&g3l@fw@+e0;^46pkogex zpm9lvdhod9|NjhN_2Bs#uzJRDP*L^&KjeIRaG48H59`l@)r0%95cQy41E8}N!2JiN zV+`U93XtrV8XL+wGp?5lYx|* zxnW$|zSXPul_iEUGlzo8G6u%~=l)wTWio-9`0=0~iWvCDKUkW^s5hYfKX3wt^zK0C zPC`aDK%<(l@f^@j_Wt>r5>Eu~FvG`wulP4^!1DSQfm%tS5V&H_Idjq;GNE9C4 z&>VyaZ)htLAprFgxLFArM3(_Cf?#IgX8MG~Ov*x4XU=OJgq z8ykrmn;Ii7MF#aCm;#ke)MP=I!?gAosRz{jyA8b{hUu8T5wliv1n7pC)%$pg|2o8@ z-opdxKe{uOGe|LLfL2TMKyO$94cqQuK-@VB9ppe1DA0K-gaEYY1^Z1*0JIt!)HDMf zm{w0%O}F{bqdA$%({+tl6++BFYZhV0bMh4beGNXG6HI{XkuOXU%(@Kn3^AY+{~>En zVQEbsR2a(Rt4E;o`{02$(9SoIA9gVC-2t7Y$^%_sA*_#DX_y*=Jcs3el)xHs1?*R( z6sM~jaQ<6|c25eZZVY28XOLy62QBS@oPq&!JG32xhzKc=XQYrC;Lt%IaLj<~K*XF7 zs0Ahk8|~DG%>RIPaDWb!;sTGCfKH;^!5}5DgF)sFBq<7FE$K~-nZl%0r=XVX;WbPG zS_T~df-p<-V%VBQmOA*FMDTI=|6zMPuE5qLGV6fWB>w*o+205*gWEx6Ewde{FbD17 z0gV}e%i!f8aR%6$Q}Es@u=<%Gad?>xUgrx|zY`=5FSF(UeFpUrh15VJ&fsx=6>xjl z@ZV=pe@93S+@}QfaaF`bx0d$}Te5q9|Y)*71gCclU2k4MH6)!_LzWh8?o+ zs5H^sz`*hXqZ;f){RxOe_M@Q-F&G){GC4E$FiU|J)_`^jquv3c5884CIWhor8Hj-) zXssKgNrcc`W~u88;JKMjCSFF++5F55N}$9K*UzXA-lNXQAkV-ETJ+Dr91FT!&D{7^ zBHLUh-s_;T)+{DV#&^s@3~US`AieD1@mvN_(dGy$!$CI&J2ErqgV!B^ZzKVUfKFor zEqU9)z;*|uiw(An%NpibFEGQvkWri+v}J%XtHryr(!GUQXuef(vDJKtyR(>_7~e5V zGq8cqBm@ohgRYwlo4roUu>|kI5TLrl;$B~%P&6L72fB6vmKU#5nA7WM&RrKXk;U;rJ$mzsiL4IZDXVe@$Y3O zH^x@*`S30vH}FBuCD_3LUMKh-W+Q0XBiLi0Py#cc>jXj30NOwgTK)mrEP#5RI-?F~ z-q1*0oeAl@caXhI3^SNi84oZ!gU_3e1KEsv-ZUt*^I^6_{0mMu3=HxN44`Q-$U2Sn zFeUw9#tsJQJ3GOPogqV}B9MbOpu5$Lq1V)*Ews~-H`5Ro*D#ZZ(UMY9l9EzVOsb-4 zrt_LvFT%?agBfV0^{<GfA7?jeX_SS+T4RnqP z_yB`KY{7*VqL8e@30hMG?vrtXj%O7B-SEc--S;NU!1e{S(+zUI;tmEeeFH;AC7h=# zR){MLi>ZhRN(q<IC*(^U8Ggv4AAOPhDIhUCRJu-@V(&{plLG5Ek}@h zm6^e9K2TbKCIMJ-1D6A!bO+C3@}N^oE(Io+xCbVeFj=V|vfQA4$P(O_`Jc^X#o)=T zF2K&<{O>9#`ThU@eRG9h9NjQ=~B z3>YezB^h`bOc{1CaDn`r&%gy*d& zFQX>Lr64Kety#q^$s;1gFY7F)q#X1A|NmSjRfeg|&I0U=5fHoo|NkGxWX14{Sy_Oc zF&Zvr&*a2#m|0qYoiP+Hwu#A^A(2^1fSoZ2CdR4^Y4WAOh9P`ENM{%>Yn z%}~o!4%)v8%L&kS5xB+$UHu6<;hFu8fuS&H63Eop)VNl|W?EehlYp^NWHAE+hmr=$Pve$3Oq_!EGbR zdP&gyFJ!$W>e&RK{xw7#eAWO1BLnMyMx+%NJ*I z@`{^=+5h_unPTZ;(3Q9d ze?z;O;I0U0N%1ZQWd;`TKCvAP%KD%w3DB6SDEQDj(9UvUaFK4u1S;KOJxKWdI7LIZT7U&W z%~wVSP>_I{w2-(49pJ1DO)lCDpnVfN7{JK|mN>M)Yd9E{L1%!m^MMbq1|1Kkh_po? zx~T$mm>iQCr-+=WxQ#NG8m|Z+JENAghO>dRw1Kn6>~0w~H5qOu&ydRb={BO`BIUe> zk-b^F|HxSdo0tY!$rY-od8nz}0hI?#3^EK1VE+p-$S`aM$yTF`A_prZ^xM?)a*toRz)&=A=m7m<+>F|SWqQ&X9bQS;w* zrd|IYFe>~h&B`i;1qJA2a|Uo(AO+gt%gz8>W`KCZk(uEDSY#&yXy66X2L|ongcW_TGC-D588Tzb zE~wmzTmnpo6beY0|Mq=Ic>pdGL2F_h!1=!mlK(+#Q~v)4&B`z-g2g+);;{2g1VHzd zGVc&lW1I{Ycjf@46GjHx|5@NQAKVOepcX0*_=r86;e#>74_y@kFF+ti$a6vuTIRmv z$js2szzr!j|HBya5C$tlJOejFJ_9!cQ=x&OG5m&hQRSyArf-vYQFi5VQnY2tBhCI+%{(VrbI>oCIMmHZTOa zNl-bPap}KL)0uWv|2@dS#K8SOi^&}vCX%4kF3bRFHG{g{pcCC-u?k(ofCv-l9wKlg zLW(y~1cDAP5&%Wue+B_i1cIVVfFYkjfB}3&XFo)gnL!@R03AREIe{B=BRwP;NPt&+ zfeSl9@C8ev%FtranA=EDPLzkS+_~%DjV{KFWKIEbzJ=Aowz2gg`~M#N_kP+mrkinI z#o-QOu>C#Y@B98j(RZCYdp53=uI5CFA~z{vo#j}DwLKn#9x!T_H?Cji<|2RiB#bWy4RgFb@* zgFS-)gFk}+11snh*m#gqke>G-2B-$92Qxs&5`yN`R2T%nop$6&cgT@8(8FzzM~Fbz z9x#JyTSl>vQuT1TKrK@fZG(gaO|^JkC)rTl0{_x_edX`TvVa zjS1ww5YYNaaq!X&n4bx^@}P4PU_WEF@(c{2A8AEMHBzQb4M9jFwE3alKpHgkW#Q&qB;>X{E z%KEaPjo8c#694ZpIWXN~5M&T%kY(5j+OP_l-+*jW1zidOI!6gStH}mB2~yvYl>s#D z2O4i<0+l)mpw-);ox%DHYz+1cY@n)+m0%LcwHlnrzeF{qyG zXJCg5fGSYXf@jdlq##-lq@aNTwBrhL)+VHuWDtO~F+fwTpr$eCm`Vdf$RU8BgC{|= zS#0d0%EF-Y1I3NaY#B||A0=gECQbnpTmbcI@q3@|AB!0x^Rt+D~nJeY$W-oh$wt708yE;U`xOwLer`t)y{ywcVQ zuJ<2%m~nD22K;qnUNGEu`DBR^x5#~58QL?0DWvO=;9c+E5;gyCm6edDhEaeng3Z#{@`;xC+}k5KwDph(I3KS?_-2IW(NS27(lxr zK`X65iGm-L^!veq32qDULn;h;hzO`v$)8wXp-7kYVk-6;fBBA_)5E({DzMoge)kqAQ+XmPhFG_Qd+WpTsD8ZdkV zZK{J?m!Ly;K^1`rHn$2xx?v!LPoX)fhHPF$O`=MKxb}Ooz%P|{+P9X%}x&zw#%B~LD0stDc zV3dc9f3XNitIIl=Y~RjkVlS&MC4d+pF;C!`5ZF2Bn|d2sI{gB zjsmcMp(8UOKSENDx)J(C35=b(PP*dE<_!(zOyb(k`UbALqGEckdc4BC?CiY4yil4) z&b>SmGH?@G>?S84*pr*x6QoecC2gqoL(Nc{3(f$I=`koUFffTSftHXdfod*A_*^vT zj0|CTSU|G?!s~cPb)=xj`|e^;WZ;92=qT#%WB{c(P{0U-`)HsGph1UJf~o@W;vZ07 z4SXpAxLL){$E?lBs0=zK$C^>W#)Mx{A~*z87%&P&D2vT|!ZqgUQ9q&FnaL_U##!l_nds}8 zn(|EREn8J!!NT6g$|PyzYi4AsU>j`%T3M54XY6Ne0y<{S4pc6J#`opH_XbKZC^0Mp z^^BFl$D8e7kObcb2B8JuF%0z}Vjco{99shHA0-9}Xi1_3YUTAaC^5`uP-0+$H13ob z>_Hq*#ebeb2~;70`YTXTP%GdANVE{tIAi<*TCuAH?dpO85p;+OXaL3tl*2(6D2a-& zK}Utuv>8PagSi2W>K;DER(8%2e*dmB^6N;5)aQzbiHYPgv3S_%SaGwmS{b_qWok)? ziRK;^7Znv1&jq)yt3ZV+6AJ??1Mcx5gx{fkO-RND9~S~T4Mf<0Tuwt)kFZ~WEn0#cI|4ey8?-eI zRBC`uxHK>{H@0K6WmFV27F7mKc`ynpgBCDx*+|JTRtB{FJKx3_mntSwtX>&B?_d7^ zo*st(jB@&ZRqCMij^Ohn!Fj*~93FfO3`|~3EDXvFH*k*kAY2O_??DJa$9oV0&@Kuj z#6X>K;>UYH$%K&sGNSDW>h&^$5)U)O2he~wC^>=Jt>BAo62KymgFitnEk5YBP$BTh zk1(`S0X2|7N0XT=vm=cFffnV78k-}H0I4gBDt|OIuGivHR*_ZW)UGnoEz>ov)aF!@ zRZ-^BYA`Y^yL4%lhBhZVr<(H8)2C0LUaG9d$*vFg$#keiJEe_@hf5@65)o#`vb zpbK824C_-u$9)hX1kGUJdJwWBl%IheI)MOgK%a-T-t-yx8SKHWH%Cy|KslgX25Rzw zTGgQbKOcCJJ7nw#RNAO9@Pp430+%-G@M}R3=lR0Nb3io=Xg~+FxQ|gWutqUd&cj^K zz@jooL0VT&*HJb|GdIL6#9GeUz}Tsd$3(|pQ9(hPO)p7WoK0LoR?bJqP*lxW&cK?J zmBYcEF?jhX1}Y!*8DN7o;8YD7)B*J^Kyk&%zyxj1f!YC}a~wdUHYmvxHfkfP z>^{XOV{VF|x}>bCu)4XcmbIvh&lDz>W5+fr$g;Ar$*cMnD`_^Jh4e+l{{LbE#e)cF z4HP#6q}2#ZlNiAc&BlmaiyRN2&6$veyr7~8RN*0tAZ1Y51HKnjn^DY7OoFj%N+Fk! z1fQ&Q93y>TggdkVLO2w8 zga&l58}}W=LN3r*GN1thUhvqTxH0(X4|8R1MrCnPWus~79O)eutQ__A9IO>gEH`id z{qsHg-&aQN= zoDB91oX}A{a7|+@Y79QBLs?YWWy*>vQ&vo2VmW&B@1HYgpl9;wgUj+Da9Iw^d+ZFj z$F>kj7~vwQ6H$t8@YojE3E)PUpt1|&1+eX)2nAJn|Nn!}sxSoG9}I3^fzJ*E^FbYA zMg~g;1}0G3gPp+^RL`T1wLyE1*xkbfU)}(^j9M66%?T<$Ud8xq)g$pu2U&m7%*@z~{k14iZFaQ-V+X z7E~4n^`GENv`>hMh=@r_Njb9f>&VIK%JK8d>B`FK@UvH72rK|CWD^q+Ns?3FrY@&r zQDLE@sH-63knZiB?jWO}%gFriFEan%cSbF+DzN(h|6zA+3$ZbPb~`gNm_yr0+zdgW z2|CoV8jSP+9Unm?5Y*ui(ApPJuO3v(bKfyAL=10;Dl;DZS2ktFlz=uSmVfz-6aVFd zHYqVOSTHa!aWSzlh%lrhdeX2m5nAFQtb-Qu2mwSP3DV60U3Se2%EAE*yx@*MXeBOa z$uKW?@hI%XJ~jBjmMx>PpfY0_tAwjjWZ)E49bKI%Q?4th#>8*mpsp+@eemxeP#G!5 zz`$h51S)%?LA@l@a3WmxVE6=47(wbC%rPyw^62vD^1n}+SpH=&&H!PMOF(Xs`d`8n z3hqB-gX(kCd)^7h8FWkw5s26$5p_%pIWj>5nWD;!um9le4seEV1jkwKw~_hL1a)rrKy;aJzveq5Oj5+#{b8%%F43-j1!9|OX$1k>bmMn z zB*(*|ih+^A`u`Uu872V+CC~~0ap(vMto(pBR1wjD7(RrZQO&?02VMfQ1F||Ax&#_j z?=vuf){-!RJOEmW3|b`(?M*{vVj!MsiG=~ONCUb)|2$+_J}6XJ80?|h4Rodt#NUuO1zi=Z1kQq>3+a?amBD9&AdRMA z4w{7qvpdw(IBbYo(is?A(@Cxy;vhoP=LTR4N(o0M%r;AeM9MiyOG-Ps0YAfX^ zX-jg#8IbZumw|zan~8-%oIw#ZRwe{JO>hSTD5rw&%msTABU}(E0@_qSE`UG>SAbgC zpfkLAp(7#+`k)g>K}8WOg95nG07(Xr(#PD`+{_q!)w&&%u_9=@4|oY2>}C&ZMqL|8 zNfXB)DLqFuMujO3b`lo4{)}FE(z3GBdBr+fY-}!$>h?MkfB)>v(^g=dtt28YA`fa; zfyW5I`)fmeCVfg~;N5lvkv@}Ab z4n*|?4Hh*}8%qt;#sW=Za()4yuqeYIzyLa{R0gtgKY>98JRt~bSAlW`Xr@8{+M43i z2dzN|H4s4SY(UFGK!fF?7eJGiEMPU@ojLGxWkB1!q5Hd`1Fzu4qgSEl%b=aLhICvP=RD3ZODXkO8tA6c$z(wIy=K0iA6H8wUcN z<^oCbP|_HF zAqu;(s51CAQFC!VMn*^5L}wKh=R}*(bm?3^106FfrbUaGtjzUH__JlRcwps*tEH8q zrnO0uiM6JJt+_KO9f8mI0QU#N!2JWrS%2?^*cg0}_@Fx|z~@3j?hb*R@5XqP38c;! zJT}en{})pT6AO5~=?qYB9X3u2I%x*6-V~$Qfi~|E#SV0hE?5AxwiMDd7lq#t&j~+T zh7(jJGB9v52!Q)zj;ss~44e!Tz#Pa@EJ#5F8Uh9#3~yj)Y7AbCg|zY%scQ^9M@E}b z)PReRlZ8=B0aoKocH$I}5)iUb7h+rlIxa>zhfmci)P6d=@?@N$AC~W_Eg&S4p`s?K zrm_}N27%fQpfC|*Pyn52Ed)(T=w%Qz0UCG#fU*1-=?upFxDd zojc-3xgW03gFi74hA+*AT}_tF-(9=@Pm%I0Nshr2CkhU z_f83ed$>rUW~wZzj0i5d@{DqASXj-=C}(T|1(F^U%ZU^45V~+72^up0|ATHM0iE+I z#K!OfoW{W8Jm7q|3X%^&!3Q4~QiqKTF*t+9h5r9%0QV`u=O=*on=@;J79%h)GPwLN z0gtC}GlYTGr;^$xN0f1}COGIU3eZq5h~@%i;DrxL4el0=raj0fEGi{ZnA*X>LibzNcSl!6XTv4(b>QG|CWh2rc5k1sEv0neKps z541@F+(rkT>#7J^xhmcVUb;H%-}}D@L1~AP0bCz3O~7jpqLd(PkEpVsaup*3cnYNF zKWIP*bQco?lL8Z{b36$&$i)X;R=+;BXa!wvl+{S~h z{*HBIXPD2x!?2!#hhaYh52OK550L@w<>7%1pYbr*Gw^^57s$P)pnWks0y`MM*Z&HF z4sACExm!@#v>LRycip<_p^QC@dexhYz(>-6`n#Yp0x|ITbsA{W9n}vQg(tL$2lfMK z*Dqv*hlzm~dO{hvZ*lg60(rVOj$+| zP!Ew&phya`Y6le1u!THQ2CmwfE($^Epanet{xR*UJ_%XUlWrGjAq5`8^!jhXB*VnQ zAjgmg8gD>7YY^H2M5I&X@CUcKp|dUEU6YX79n}2;HJd{I9%K0*MR!302kDs~1=XmbcJZ1*aGEDHUUU>$v2x#GvIzv8# zI(XrcB6y9pAZW=g?seFplaiSnEi{@mke6a>G-<$>VoTZPF>3z14#GGWV|V=f$-v0K z@V|s9g^7hho#7$Qut%gJXxJkJpka>?ph4ItfWjU$r}+WQK#l}Zvcw+=pkf4cq>1_+ z14Bh+(DbD#{wYplQDw$oc1DI}IA=NeowF`o!ZFKf_k)3vLG6DQ6KHL_K4`}iAKJM_ z&|HT|t|oHpV_+x_KR%Ao%68C-(DN3-a`=|Er$_g(aT_Y(UE3a-Coln2pBVoy0nZKVGtAw^ zAcsBN5Gf5c+(0Qo06sS?0BTKxHUP{A4RC>GfniJi!Q)174rqzRdFTp%UW~b60ca5j zYCXV0NDn=P2+S3;iz?r<3YWq+TO1S7#?Gm)jCaD=CXl-YT;`kp&teK?0xeCs2+FTg z4AMw*#?aAhMB0MZZ3qFB#tNu9M1&$}ngMeB!wv>kNPB`6Qe@3%05d?tOsov{46KkM z@jO@*bOZosX9Va27%#&ouXOIM~YX*XiI|SO7C*F)TTAQMzuRbQ@j**=L2K9lmoV8f zu`o!2=Bz=3(^$d@k(Qw0gb*MQP8`r&$qGtm`V3e@1(YHM8Mqi&!Mz61f*mpNC>+F> zkYPBar4^uZXS=MXfQ!!*!G_&M0x}x~hWo4ClO)PBnZG~Lqvl!L? z-DKMJ?@?bLqudX54GuOoRkfMbnr0_@!TAH!*LP-OVNhWBk272l(F_e2gaGm30?G{- zD{w%;0jhC0?l`hCTwvf}c)-8`O5>~y2N*cO0^nX%00Rd@0$7G{sDX@wgqkBWLji*V zsQbpua00{tm0lk}3{d6B0AhglFg*YSlFgs2}lVe!C=oI0re!PVBuz9fwnb4yMzS51q;ZNIF`dy z6f?5hB+N||RFjfdI97Fr22m!pUZDs~m=r{^9q>cd{Y6Om1Fas2!%%Jm#A$81r@Yz1FGgCo5 zU(jd_sDZtMft2<9ilE{Kwy_s9t-LdA?)2$%r~CGSu0Uei_3tU8G-$ggB#p!7-uXdm z)VR_1CO|7pM3h1cTZ8~&5hyf`BL=iU{e1>-SAPcsFC=Dp8SEK&A-+8iof(IW5`wn& zgAS>H+*Sh__5!bP0WD=z1`Yk0gXitzW`NhZfL1S_IRh>OLF@ZL=cMy8^kI%IK%I+- zE~tkP0vH`dM4KM842c&ym&gr?Kk%k%P_%I~*n_rEg9@ed;EmOw(K}F?59#cKRxE?J zhJY7@f{vh&>8fK?o6}X-1uD)N@6U*`A`Tw%dode|=&Hq_U#!SZ;6hW(_ zLCc4gk=kV#c?RkUaIOc9A_{`nw}aXUkaH?QElzo8n_Lmnf>MOEpz=Y-krjel>82*& zTkFNy^_ZcHT#Sv(K+_QHqRL-0liPdDf^F2fd)NddRi$i=jcrZzxqAddz4}CDkR>)NXa`@a1tc_+x#y9-!(4}I=h3PK>@t-99G6b+n9*pM)nKn z<_>=F7&pi-p!N=^JqX%O2BGC4t!Go{A{=(`j4bG|M}*7&#il|hP>T3;wT#WpO|=aL z3&eaZ+gODhEAcI0O{DpiRo4gv$fk z?hbdBu_$<$-1OnyLHZ@Ia`p$TGQD^6(f3`)ftwkV1Vc{hRI_HX=0d+4W zMizsX`H1ksXg5Ju1c3t(bpADDfC;pu3^b4rqCxwzK%vNg!N5>l*%T3wU>84XYJ!Jm zQ`3~pOeU72;1JCOj~OvA|IY&5U&J5^x=(=@x`G@QrqBWl5rohp86f}-WN-!q4-rA5 z0UXFIphd-kLYAOGO-^u2A2hti1D&Y>-A4!7!U?L4AiF0)%`;I@E(OgVg2#6L!$tz- zAq-H5N0cESG_hC+8i&wlROV+?7KTTXIH*oKtM6gRY2-Ec$oAEi#vCR#bGJ8!_I2mw zbu(r%2IdzOL>W3kPK2zUNAo)(YM_2c2oUf)JM=Cf9tJ{w z2PHO$-;w;JZY<9TzPCvn)T7y{Arp6xVfcm6Wwk<_AnP@3`Dd;jG$`(#ThC=Q}&SC*Ko!&bf6Rw&&XlI z2U<4^4ii35m;`{Y+XeX-)R+X_90e&h`tl?w5%IC#tqblbAB63iE%Lg5rvSK@eObK?;A+2n!piuZglX+yS&TybyHf%MJ#iI}lm~ zdM7fwsIsY&9uwHl5HCa4S=O?0h;S#Ed+G{L(Mb>$pEBhvwoOp_U15HOR+NZngpL^_idtxBfQwo-1_tP&em2lyr689F zFt9Oz_P;ZOT9N`Fuf#fXGr*?CVIDoez{bED3z><9tX6}~sUXfVvYif_OqmXuUt!vX zvMu`G2Jo4M;JfXlnVtx-fo}1{a@H3U0|QeS_m%FI{yu=OA%{)piK?%_%EcE1hqUtXX3(+poL7zfmh^%TA|>Z z%H}a%mNx88L&- zFoUQEwVXh0DX@C*+9el|ISiQ)^^miQ!ELN|@Ol9j&>6+xH7m=(e9-jB{R3G)f5F@V=XF>6Eb^@X1|!RP?i4_-?JGLJD3%!h>g1R*s>NVvn?2M%|T`#={7 z2(U3^g2Mg(e+KZGG9dpjLe5gQ`2vYw$eyETLTZf5A^w4?HwT@i0=l!65pr$`>>ddh z(A|Nc`>i1RU>FLZ>m4BKLGFR5hq(u$9%Mdv-wZ=0R6W>#Aonmp?ze)ehs^PV*7AVw zw}QJLtR8gU1>*&<{~_uGgxDA_g8AU{F2L@EoSkI_Iy(!t4g=&~(As_hHiiOdxI@%~ z+zU}}gQ6Z}KX}~+Lney)v+%RIY?0N2@38=@XN-ob2cL@o@*m@LP`EPNe1(KJXkM5L z96#GZ;tZMJAmX5YsSQ~DY>+rJEIdGCc9vlEyFucR@L*tNaQNTPWWlV>Aj%-gpva)g zpv$m#7lRUm9)mu(CIj6P%XDD}1BeFQOah{%pnNs>z6EH0LzG9zO;yG_uw#rNG-#1L zsI{sJ-hVI2zyjWjx`RPdA2gu{Dt~!FC(wee2en~EE`aob7D0e&9ndCGkOM$hjVfLM zU7WOoLG8j01{DF&42yvwtFo!Gps}g3DCjs1=!Q*gMrKoGQFF+AsyZ^8F|BCJmZIq7 zVh8uUWX24OX)-c0GI18u)YR0}3JaNb6&A6k`WGv(@dvp@WH3%Vc#wr%MNv^vh5et@ zv15!Z%9<*g$}He?!wo)j4RmfLFNm4n#eu9D$e*i&wCEP`pCT2j^1;(77L9m?FUEW)* z)SE!mGl1R05(zgy5Tc#|Tpuv6hnsJTqTU~_J{U#)Qn-3^n0iq7fbT5<)i-LOdrkyE zp`imUkHO&tzP|(%PHK!HKS4L6W`lHt(=o(-2>-jI*oW|sE=UKgenaxV2M+amDC!aZ z_e4>T@V`EadRMsry-?K8hWpTTkv)0$Qwb^E94AVQ22t*fMtj0 z-#7*aCOvTYg4XJS@-Ji!ulIjb(0$rMY7AiY@bV$Y0ro>H7cA|6iDF!SM%C4=YbV;`&JHO(5za_lJVx527Aco`B4^0>>Z3 zd{Y$lp!kERhm|KF_4?rWgQz!$t2bq`1;-yqJ!W|V4kvK@f%K_??oUD~Z$SPJ1fNj~ z%86|7yEH)dxr6FOMrUw)1Xj3IUhjAoU<~Am$7IH)V1Khd-!&4HAC@4u6RGpzsH)hnFu9 z^Fi$ci24+0IR{Y>D!)PM*_dJX=z-OP{0ngpV-%=dXJBNoVfex%!#j@$+g_a^yD8%B?BYFe#Uu>+hBT`7@+RKqF30M zS^3Alu3WHIMut`YGZ_~$tYcti5Cv^^L+I1r!N9B!+JMij{CHny-nxIg!QraJI1jw; zU5%-xfkA+ssfM8mEN;&@kI91RiI5soEm*u3Dz5uKlSz$Xosb$+T{B319YZU4o(*zO zHK?B-1@ZxEKOZ^^is5NIJJ3j+sq zkc>qibQl?Eco#IG!wR0z0S$X|U)aF_x={eMI~=qP6?Cc~$eltUcbY1Lci)5hH=vcn zpi%s{8#aJlykWzM2RV!)AXoo;lk))N?nw-D7>_X?VGsqy83Szm9yHL$z`(3+V5kZn zqyoNs9I}5={X|G1~N)A2FfCE2B>0YW{~~=lgXN4G6OGzG=nBX z3TW(9i$NRQHvnyN0{H?&%ftJK(3n6BQDO8spc}CuD!>OVf{vE~tp(k|pbYXo%H;^g zcz9`Q%F1dgDk_UqRa8_}Ra6$LsHmu@s;ICli0O$b;3OD8H;4QO^_z|{a5G3U$b$Fn z2Qxg|#bC`4!Vn7fowdLY2GEI*AQ}`mAX@gq4h9e{2c-kyzQ!1Vg)U8mcoMWI2(dkX zH-i)d3xg~Ji}r2?X$BSs83q<@P?~3h9>&6;54t^_=L={(3+T)$o-ex?R2X2gD*8J? ztIfacVo+zWhn}be>J)2VfGl>jU;wE%WsqaA0QO<$YN zwCmrMq<>dH`&xP!of*$C9RR0Yb;y1k$U1MtejF*>DHG}>5xYptWF`g58Q^3ir4PES ziv@g76WV?pc%+-7WKBk$K!5*05SA4dmIYx!9v(qK9v(*Ll<4T>5G7?FWkp40 zA7v#!J}yo!J}?2^YuC%@$#|Fvw8e>!Armwc#}C~o0NO{v0QU{F;75cf!Z)DmiUB&W z$OPVr3Cak}43IMsKxYqtRz*QFHK@P^9p1obs%&Zwt`9_+mD!CMjh9U=*3(O34+v-U zbPK(9-^Y!Sxe%PbCNerR9%lj#nU(BfkU`sn4c+jEa22%769H96pnVXG;7lpQz{miZ z8%_v4Qd<{Cp)f&|V02 zb4A8=+u2#Ul{6*w6h(sk?A(OBc$8(Oq@=_|;>L--Uq{CT03UM*-rjs#?= zNDS;H0R|4})P#UO=r#y=y0T+3H|Aqx7Y9u^fQlM*&^i2&6&Q>L-hq-D#!~&h(OhgC zs_F(#!JdW&;?j)HLC$KSdJK#VOBp>Gk1>Jfm~BA> zCwRjatPY2^Be1z1lyO0eY(NDi%N+wlX-3dgzoIDf)0(s}M#WM_Pew)O$zlJVGBAU~ z>Il;T21y2W22;qsGBXBqNVb74G|_;kSm+E5HakJr=YrCx6az1K>v9d~{#?*aSPK}W zAWIWDAPjbf2@FyUAkhsBQVh&Rj%Wg)*_{s{rLj9f*UN!Vx03>o?1Ju}gs>nxxD~-S zdNG2_c*t>wX2#%(06K(f`9O1&K;}M{xETih~>guj4qpTI+AtoUq2FBuI;^JcB zjLtR&in^RETp9}6mX_KI8eA-#x{3xik3uSZ9v1)Z z`GeZG(-;^S4>5x7Hw_1+XUO^Au#^E^^oei-VyYf=xF^UKrVG0mm>Gn@g(~Rc3`o5P znoH*aAK(W%_r=s4w2c~EwXv&1OCvreenw-)L;voV35oHtN(hTfvqtjy+PioO$!khj zZ;XiI6BZQYWE2#X6j9W&a4?kAQjz0@-cQ)|{}n);wJL4iS&0elGI2N>f4hymK8IDtWvVF80C!v+RT2Ig4M zygjT!VFzb-=(c8XMFI61cwOCl#bd-&)b>ywY<%I>ALL`H-JA{RjFbI6K+)cPDE#&!WwpMyG5p!!@9XMK*S%^4Xc{{O{zoUxEW ziJ=yh2q5Q@!qSrh=nx16284^C1vt1R4>~270i2qp85p2DTBJdBIR}F@q{MyzV#I=W z#)9GiwDVB}tX_~o1iTOdv>8VlJh@_EC`_nM7yXX6N@tY9QKf?~4QFDQ^8Xj(X~seZ zQ3hh_bcBzQ;{acs4%xy8x+?@$r-RERGU{|T+*LZLPG?|XJjPhazyUhXje`MeosP}* z=ykfWFj}1stE}WHF@kkEHamAQXn;n1AhnSOIOl#~&;YGBWo2++ z&|nB)&;acd2UXx23=0@E7&b6yKx*^@3>pj<7&I6jfCNC-U`}8_QUTg`RluOZ&;Zh3 z2x*&xHsxx77nFjEMFXHK2AE4By4=w?~@nS}}!bd&l4NHX&spuhP&(R<-JMB^ zLztUSNnEmthhJ7+l7~5?T6t~rw5{SfsZ30n)xxf3W{Tqf!X>TDT)g>mn2!GwVq#)^ z!}jl#ri&!wa`A9W21W+ZdN6&aV+{HXO`uT^1Ms1Qu<{zCWr(Q%cQa@(u!3$s)CS+X z3d)6`Go*Hc>dh~plh#2zNXJlFUK%WV`@7e;gD@putNnfgKE>vyMPJgGDcZ)b3yaEg=D=H_+-pPzN3~ zV8$k}g8{S-Us_-Xg96wT$Z(%3s89xt%Yv{m`O#^@_{y-Mi;;264}!6rOhB^fDW4htwNLskC}qT@gx|a7mW$*WUvCKC}TlLBc2_67q+R1nm!{qBf<|x z5)~1XX9TTt0rmVryE>sqYicu!s!ofD)zr4)QB~Fx5aL%*3b25m6AW1P@h=5dw@vh)814=?KL6WV;zyL0e+M zX%4c33zW)0$N#c{Qy=JLT~Im#r8`ji0<9ef9c%$gf1thuDE)!NLFo=egVG&{Rss!s zDw`UEvaC922Z}N1tSJy?Rt7wv}9%~Xe3H?n5@{Ex@T%SeK0mxPg* z(Z9`%KGsH{IT2Lgb22e%^(fV-TL3br0=1KHH znMfa$i9nlIcQ8oY*$G<6|78aQA84Kb0qF8+4e-o}z5pn1fjVt$cR*#E2Dmy0HFrRH zON4{04i)@2d^}6XsIss!v-0g} zMn=;#mM&aBv3=``bsCHs6^v4#BYOTltN3?=QGPb)zN8LvN?J6St z1>M9dU>CWYfdgE&fhK)~8CV&_z-1h$WMctGnV0}*r$6Xo3ea({;@}-K%+TGppaYs% z!Q-c(7C#5{LSoQ#A}3fcXxpj~14u6y{QMPA+aI#O6LN7i z7XWe}uOtJgO`^$o5LC~>W(h%aQxedHo1o$t#7DGH@J2i+H^_ti#lavC9;w;EzyZnw z9~d|wJt_w9bywhR;p_~so!|uw9H1T*D3x%)hDd4`n%t{yaEssJ*j2(%f15p>ie z=&HB*4D1Z+8Q2+^V`(41; zNC0Ul+6g){;tRxT(CS){)k@GjqI_Wo1ITI(c;0}Nnnq%v#w@6a0Ns=*XspMm%_wfF zEGiB>Fno=jXy$ zB`bB*(y~`t7QC2(nL+t~Ka(P}HiI;SGJ__=e9*oYSgMf{fV5{pv!W1Ms;>$(1oO+y=Q!kvW%cM8ffLQ^3h;_dB-GY zz7m00l|ptAL7DvuHsKb0C3%*H!lE%|dcLfR>pMeRV+ss0i94P3a?9y zzztR|22t=;Q;?BzM^NJA0_6zsRTNd^V+3DZ z5AK~ZnwSM!$S||IlurHkjWLi-TTR(g!do%b-pJ2PhK0qaa4I9)zg4Wv2D(blsy@m^ zJaVRP8oZL+K_P5Gyu#{gvQm+1x-$CqN_?_lF)mSVORo;Tokjp^;j@6+=cbCH?4Z5uri!AvjQ_Sw+seei6quR0 z@m~Vtg3L_NT6YEpCKIMz46>jxb#?~Sc>rh=8DT4OUf>siT-vjPffLl`ybs#J7YnnT z86*?{Dr{pxGk~CW11O<_iegZo7SzNAl_QcDKpBu3d@{R%p(3a&0V)Aa!384dJ|t0i zw_lhsVMkS^ku%GuPpr;{l}v1xMAf)^npv%kFMZ3E*Njn9$jxPCk(Z0H^cI7ju>-nK z)`w|0o?$G zNCk)%7-$D0EDv%pD1hU!2GpRCa0H!80TSY1Fks+daA4p7w0lHd|m4WpO zs8tC{tqd1HH7F zA^SYt#>U2mktIDZFFp6%Id@Y{O;h)xqM}@rTt=(n#Ka<#Tt+?(J$XG124)7||NTrl z%-RgH4C)MqpuNhP3`Pvb;9@~jUBlUv=0(3Oh6uBhSZJB)EPE_DhJrgAJ8R4pdDNw8nj^wL?c>G80%)B%NY^L13D*( z5P%js;Pisl1>4P_%D}>)0`9_qM!G=*5ujU6K}iXe{6)Yck)U&`K$Bpg^_a2@;Hh^d zQxjt&Q4vN#V`)avxx=74kpvmRbMI=9RUcyPrpBU-4=XB4{_a^XCCC#2A{Z;gd3nVp zcz7c2FuLyFw{QQy-AudwZT$C>DeSKsqdz12-&Ks0)l|Jz)YVnIRn#~7LeB5zWt_>R z0lsrvf}sbraT#{sgBZ#^o6s>(gwHX;2Dx7cY9cd$Y7Ec?(~O{YHuw;QLQv)aRUh1- zb$ej{gSMK264VX`0ccwXRF#0bp2DD(HE4`e&=`I@6uY{zxM+~;1J|f%3pR^T4-OW8 ze-;kLnNMtOpD-$BXaBpO7&~cFY$5|>FF5EtW_5-*P?ZEp0XXXl=v6|9c!X{a2Md5M z0D^3kg&gn%DbPR@=b$rY%pu48!kJpBDVQp63kRJcJ0iZd}t{r|#b z$8?MV)Tqh_RVonQfbQl54G+V71>NM0h$Con1+SK1X5jt;npc%zUK>_fAo^eRGhth0Wn1A;K?xBRBRN?`8yzY0gcd))mIQMUZE{mkR8;Wh zf6xhY;B&r&)S1QqXEQJes4;bd_NFkaGfV@om4>b*#E{p7$^ZWaIwuT6UKvIHfRH+~ z1d92n^1Cqfs}j(UDKCj)KB_#(d?^C*sQy<&u^*&g8bv>*JgR;T6#XFoqq+}O9@BhG zd07*x3Og2nD41A!q{TvLCm5B_X5gpJ8T%fi6!s7gl;^vB?it6n0jO^;) z18j5{0~mS!eFtL(@B{9x}SPg3R$uQ(G1;E@7 zY9fM~fegDr;~WfZ;Jpc|>h_H4?24v};^z8{=HiTr5L3P|@<2?w`MnsP2bW9SHX$YFz_ER&{fEMssyRV?lOtdq#0ao%cwF^SlS!%sB5~64++2T?`C8 zOc6|a43ePm2OZ`PK7VpIgD?YX_!~o9&kpvCG9)T42FU3!25c|CLMBy;$1<0wf zu&}VGL1f@;u$`cNEui?3WY7oAc@Q{51u+f*S}_dro+%`D#KDmz3ic_xE!d-wL;&$L zg#8%o=O(a+-$Mcb%7KglXfXsbxijTK(jVyBDQNtG)-5oBLXs3DhCEa)EL8PE?bX!m zL-jl$amOSHj$w5j9rYqfV{aoPZ(~VF6odW3fNU=-!d^Det|72rAo&n7bz#S3EULsN zDk5gh2uatBnc#Hdp&tUWG(;cd0I0>*V0&R!7J(dqWUmAR1CufnxUmR2G!OOeOK4sN z4^gu+Ff(w09l{7M(2POdafqiG*};}SuF8aZbr;ymzdsoJpf)iwm_g^J1sF6yW8=8( z;bZ^}&VmX~sD}lW1)-UjF$io=X4PYeJ$Jy?{3~MOfLa3`gU9I~Xi+H#8gj?C9RG@-wt&iKRt8lj zFQ&N+3ZV2T4ei-7>}CL^aWQao2!o4ENY?|jg$H^ri!39gU||KDDrxLxr0Z=eSEa3{ zq6IOYQ3Mp4fo1ysR*LeLetNR9x@Hy@d4b?s37jVR7*v^pVRnPAx&bXZWZ2Ch!obYH ziDEaTfv${j1jO!Au-#fJYT8wDrrx@S@FWklJI}(xOjlM`&(Bg`(aK-H49RZs|6jms z8MHy8N&IMQB%xIXq7H!$@PM1GpxH4-2Ieo26*KY-%wKjfXfZH?`)fNGwDb)OAtfU0 zbOP{Hpt(4ksE8bsF*sL)>QGT-K_6G*1mHyO1DBElUO0P*rVy z7PbPWT?O1Myb`94fu`PNfm+%Dg>J#EmTrEvdLD*SX{omH+WbtmA&lU8`%d9VzR==YoG@jAz~RZO0kQ9 zpFs?I;xlM9moj)PLWn^bJa$#PgF#4N0V1SlRnCmgB8_O}H_AZ(9 zRbApOK{rdun0V<~_(*Wd&a1tp7wM+;?=Pt1Vq{^#DCCvnrljhcY-bVUsA8rC!Y^*Zx%DlfH7`ZtzZqzeb|2UEpdL z`EG5DaVDsD!P_9Y8MqjjwLv3qkedcT1OK4oL_p(5oZxdhLF2)oc}fFARz*``Q_yt0 zqOd5lB9p{FrN@sM@BKS|{5YfKi6%x{7Dn5q>VI!i{=ET}nRX1{n4}nUKs6_*{R!Qt zx*K#W9MXPUJH|WT%F00M`5|XoGH`?LfMr8F!x9=Ii0z~pBLL9S2pndhLv0|(%7P9+ zW4i-7xKh~^bX1?Yurc%1vVirLCL9*IHRl*RYK~mL&RF#qbS*J>)Vz+#m+=XMG=m0% zDMKM>M8XWZZv-?VA%9^91Bf=hz_6P^iGdT`HV56r!U^6Y2%0C@!Jv5o)R$EPU+Yo3 zgMky22SCY<7dqqs8i@oQ-eUkhsZ9pFJgH^}gP6XAfuT8U#{l})K4#FxLh$oL72)GC z>~f3?J^X^bjXlL#I9#=0Ocrqkbq)m$E?!jy5ou{g0TF3w5iqwdE;KaG#?{#lLMs`{ z^QWmxDVwP%@hPc*2@nrl@5ubOU{Ydw!obU55AqNnI8(!V`)r`Hl8pf|Y6l&FLAqWA zl!8FF-au~DGqYwCRTc!@mm;&%H*ozOj`>K*xYU zs|`TA96&P*5E`_M5ELVzsa?=u5opZ;WQ`K2Z=h^yY_4poXbxL|q{tp^5;(>FG9#}^ z9hmX&vx!Q4SwUGmm;mpM3uRznVrB{kryw0rIfH%P1(8M}JB|$ujZGCr1zAN|MO95z zLmAioyU(cjFNkqnNeSciB=&^5xt()S%x7odX3#;LvIZ%fxZ&j(bZi|w9 zF>eGNsz;Pu2oFJm9JC!5l;uFz7a21P8w)e5o2x6ci+{UyE9cfN`|L=+$Bg?-(;5;G6;K>xo6vrR{Y6r3~K-9DAhomP7&4dwnQ9^Ca$_?WGNvG^ zXsQV9w=xAn46bKnhZ@`mHuz0d5zJuFW?4@rE0}wc%mwvlP}+{58?Yd)MRu^UjE(<( zLXCyE7s*%#khvL5RxtM>g#*G|n0rNG?qz%mF_`^dJ=9=`dyxV{iNWu`3KJJI=qy%Y z25|;y1|%iXg3^&tItEI|L+KPKoergQp>#f!E{D=J zP`U+5cS7lIC_Mp6PleJmp!6ary#h*afzo>~FzjMD!Z3q@SsOH`ID_E`c-ByW0mhPt zvlQSgH8@Kh&T@sb+~6!V$G`l%y;N4gpG&KU6WMc&_Wi&MwRX1i= z2JzKRjYUCnAR2^0Y;$9FWqn3taYJ)senw+?H2sHDdN zagn|tVOenn1#wy7pgs}t7}b2$d=WuG5k_VZ$tA?cCxndu^$H6M3#)<%FAy;WM1Z+X z5OFWCtg5guSQ<>k2nz`_t3g~ODgbeifwHcprLL}}Wx9&2jGC^lnvARpXsz^rMC2a>Xde0!lpa5D=!wDVn0BtM) zZ5@EM0-+;Qh*ls*GaY#*3p_;$9XN;7-2$MUkgy3Y$W7j$_86#r25Los3TaSNUYLR7 z3+P^B_Ads8!k|3}Cg9ejD6~I>>UD1&9p8Y&EJlkdOQtY38L6uq8L6u?PAv9z3@Oh2 z`-gGjzkDV)O<7qv4GlS2Sxrbk5#(QSrY8&n3?h(QBt*fBIU)WPfw!-q9W{i1p$#HL z(*(K*7NJ4_)KC@x%?m(VGtk4l!2tkS1Of^Gb^*}UQXm$nAqolr&|-N$P=Z79FzBQi zWkzE`Ww0lcvU75#Fj`CjU2FpOUGcwsrYC=87?>E;|9@cu-6bu-paxor1KI|!0j>u@ zI{{eW{(rPoDZGm13MoyZ7d1y^D{EAK-cGi4oQHliWX+zV*st9 z0x$MvMD+XRm_R#71eH-&c{6shi@4iqyBo{O8oO)D8>mWiu}_(jpl)fYZe(T_r*Ey` zl;-A^=AF)a;ik?{Dm5V~HgwzCc4lzPLeK zA6B>!sFH0wYiVrm9D!IYUn|Gy5CJCgu|8G{Q$Aj4@;A3TU5 z7@P{spuL zFNg#!wzdPaK>H0m7p1I6u(GlV z@$vAnGl^-KDF*p5UeZyLP|^Vt{|<|Y3rb4J%P^qc-7L#c4vHtpx;exRLynRMEm?!Hq#1a?YiU4B6Bxj&%RphOW(sS4fLF?h zh?}cHLQS1r3~{Ton+4-D8DB67zt)*4F(JH)jpet)KLIEMbdDeBJV0B}`OXYR;Ql?h zOah&Y$Mha@F5VBw_$5S~PY6`sFbJ?QhM|ZH!No(6#X;xnLe0s9i<^SV0H}I)#%xAq ztoAy?)qi1I$s)UegQ~5w2ZN35@0Z3uw$6Bi@}h=p1}c}n+#Fbsz7(A zBQg#~PJ)&~h%yFc+#EFihn&|y%L5I;MGI(d%@AxODF0c2#XwgKLQataMY$GqD>0~? zF*8?(U&;WR5df7sf_%*2MN!~YxsV7}gD&i0JS?fJBw}yDXo95xQq`5<5a$%sMb(!_eWxIT*bY`C}ql6V-B{YG$cko~Z5b+>*ID+cW z_i*uWkT?Si1L)4>C}wr=j5DZhR1F$|gSCx7Tbx1b*+4X?#R8%Q;UyI`4I}aawCF+z zKpRhpd;r~t2(D{cz=c1k@DsSRg8_7(gRrrnvN&i-8fXxa*<73*G`iw7Wy+K}Q~UxZ zO-inAyOCVo#>DdXPbgF5-;<6Z%({ncH)tQSh2DD*_1}Vt2~_?t27tnfq0|A?_5=&|D2`;X;R*5$!psqY=J9EcAso@a#a13_D1Z3DLj?OGKdFm0zw)NvW&_~d`u|&O+oWa z;FY`ZeW~V)e zo-PK~mU?y@i%N3yNV=E>#Ve`$iHorEiV2EH@$yKz7}|UB za$4HiTK)YBPH&(yv%zPZGJIxW5Kw2f`yUTpKV=79|D*Q*3*&$A*`d0iuxGXtMJoG2 zcOZb$H7x&QE!#o)5?)6zYlG7#Gs7IlRK`7&p0g$fIcE*D5E4{_3Nx^L0c~_)1J{wD z1D`;f8zGA$5p^VL>lOVxA1^mIFA$a$7ncQL*g0#d(ZNB{(Luq{4(iHoptX>0%IbmO zBi6tKc#Lr|V>aU^@HuPIppjnG^L(J02vPq-w`YOQ=yy^HJUv>Z6br=lz3m!y$Dm(|_MwA^BnvV>aVPrUMMp4B?QiILP|S=TyLcI7p)t-2&X=IDlm+kk1EmLL2FU%$28N7E zYLFZUJ&y;vY+6|fwD=g}2k7!@MlDMRL3?gxX#*uGX?Yn5aS=TQ6A3E=dk-yHWi4OE zR1GODVR0o6VPSg_J`P?%O(SVJBTWSj78X`DMLjbHCWeKK*^E2DX@QR+6SOh}vWg3I zQW$8u4(2;(Cj}83h*SZJ17>I{VAKcgu>kpoiGdHg5|i};WD)^#7X;|c3eZt@rqCnz zlx-Q6*%>XSH)rVRgs_G%uJSr|+19?Mu8x6`VKHMW<8JVsycM8LkEnO@LRWPoT!b0k z(hS1TvlXR5m*MPZkY+f~AkA=}L7D+{L=qo3QdmHHmB2%lf>0LZcqlRGYGr6tn1SK~ zoQyyNagalMK;vkPnih5f_S~uxQWBCP!ZNHryf4;puyU!X>p6NcrfN!SiAczZu!=|s zvT+=A&6HF$H8ybtjejg*Ol8~!-b1PiO8pEBYT!kxI~XM3^&52T1G^7FIRMo5=76St zA^n}8t%+YiQ7F$K1YWZaKH?325)U->qZTWSdQKinOdP@j+!h8V&UR8FTq06n0yL+(2AqdM2Q1iw;t2Iz9%$bl z;RC3b#6Sn0f%^803@l$j8zn)vLNbDO5h#i>D>FtUdor@-#4}Ymw|f5j1u83;!D(SD z{JgzVP#*?1ZwlJP2r6_CEB>KVg$R3*(}o5(+ELHjBYb2YqI3o=42E}Pp!pCrP|=Uf zbMVxXmr`}KvvX9HlGpNd5S0)Y6BCyZ6^8)ERC6swH4YXIRe5z26Loo24i*kIMJ@Bw zqM+0AK!@ju_JWvVVgek3q6|z7^BA)kw}8*fQw8mN;D@IjQ2PjxexQwhY@q;KGXu)0 zoDA~NQi&6KJ|1ZNUV?!WoQ8za4#@*`e%VpIyB(T#WQDb@y&WOxi3gU1vXRpcG&yYq zryei?8Z%{>^Zys)9>!3T&dielB_Z(m2{dlOQ<6O3dI7nPWq|IKCg;pNQ2h$Rp!yYr zp>-@9!73J1b}=w8ZesL@oSDZ6nifRbvk9$Z5q^M{reHsiQpcKO*0HXzDwdI95d#C` zM#fOQ^((>+(E1hZ22kk&8l47}?fB|fdqz7(c)>_a{pyXqdSzr-@c$R%HpWoAbt}TH z$Z<|u-O5U>x|NMU)e5Rx85kIMfYSipx)oYVB7BEP4WRI6!CJSnfZO-Tbt{X&4hEh( zps`RKb*nwRY6Ydw|GyY_Gx{?qFyO9R5iY_EZ)pa0XwJf0x3Yr|y9KrG_@GrWXu}nx zZiPn$Idv-=uBw%RiDAkAUyQpL{Taj=2-U6FeF#eZpt@Cp0krEIw8#vzZUvvFMnT=m z1}zJbi$|2Im4T6A4LJY$L(aToM?3QlBh^6bR&Zw*R=4h8V7>!tZHa=8bU@UsjJ(jA zm6>7j|6hz-8AG9G-Vs-?Vh=S~y{Z8|6A^NbEphcKI4lvPte|EUHR@G1lB!iuy~@DA zxP>tkoNnSl;}Ebm8P<9gTPUE`s|>JyHkNwTn6i2mIoUu{5^}A|$WXx)z<7e`DT6rZ z{$YLw=ouxopo7psCyCaBI)b1>EBF~0K=nTZ=#C!n4UzdE8PH6nKbWzD0dxot2mDNM z4t>y8Z_q(kY@qo|0nk|qY+pcUitvD2ojVvrKr2<1)YL(1$)Ft;@S#?q9g>VmB65Pl z@`92Q#)5__NNlD69u7`kQ9Btm7|p=Qki%rf_=cH}L6E@+)ZRnfp$wV`fH=>Q8SHZK zCBY!CfTmVC8Q364mg_T`gKahgjpOh!GU_;mh>8{~uu1XqO0g*vi;9LYSy`K#YiBdF zurOz9o10rh+L}|Cv>A^uUjeNvWKd>E12uF)$QV(qmFL7nfr;HUb4GA2TR;^_&glxx7XB zB_*T; zL*|A+d$g3XI&VXVjG7pis-(1&b`@4<*6@f(@yR*KD=H*|*6BgxaW;b}gFZtb$?j#l z1KEfw20C%#&Q1npNbG`Fo@4caIW!})A?IZ9z&=**$cifqi>ZhRN(mSVS!f`0uzBt} zD6jMII!LR+8KC~ae+MQfh6Bv|8Q2&^7?i+kDA+-%RiA+!GWEv~W`MF0cn!rJ$eK&g zLMzaj4Kf!D44Ku<#m(_ASkg>9%mHrO zIWjX?gT3U)%-{uP7#Iqxn~SrGv#YbKo38~e`1mu?t)|9lB6y)k1H_5o^x(>5!myip zGXoog6oV6NwFz1=w1WXOhz=^UKxH}TL@CfY;$X|5OIPoJF8&220Aa9|_KeWH&Irrv z2HiFdbvE4wTJ}1!oUS5L+Oo3RQX;OLvO4xmuSy+*gB?r7CDrxCb)*y}B^9M~#P!uB zLGJr^mdTLe02$#0I-LL%PN?C<1#XdH3ok}uIcPb+l2B00H?CR!fZ8ECjTxy`~U(N}2@p^EMca~FB zN_2xc9JYq10J^To5*E|&dInMjflloJz^1|R2C1?RV zS@6U?XjPpYDAj^m8=w_!pjHUD@PdZ8H*%qbQbH~6bFQgz>|Er_$i=IPkOk8(h+|z~WvPp_D#$SmB}~5!fsV`!SltV{q!?5_fXX(Adm(il$h|^f_lhtGf#>uIwDsbU+IJ{zJ+NER+Io|j z1sRVrS1>R$sO(|@6;Z5^@e=UqehCbq^&ZR&OyGm@ z=9vt<3<{vOB4RHAC|)47H|Uf%&;l>i)m)4^&=p)vAJErq7bP=I~YJ40@wvWw^|q&3ggL! z`=AT^)ciJpw;b|cMyTxI3E;9Ajg0Y zssJ4b2Rga}6fR7$I~c%6FM+mXtDB2o3QR6>4@@p$dZK>Fas#-<21@r&n4T~mV!p`0 z%wPZtU+7*NXgb1Yx`83+_9RA|h~#RfC)$T>85kL+F+E`{ftiIG*O1fkz%8wK@M%nr zpgppnMm{L^4Ge|N#o4D-Cr2<(u|1>>a#t49W5##Pdl=XlB0%W|e&-XoT|OV&=5}Oe zh=(vhWP8w4)eL6suBb~G?#gj6exSuNg`mF_Lfd*)je7h8e+%)rQy z#q@^p9rIBJHU=*g`x!thj}T$(2=W-HOaMCu#Mr^W1Uf;)72-=~1`{v?9v9-~;_UAr z7BanoTL`l63FABF9Sp(@W}tY0?o}Zo9zd)AKsg7aYG>5Zb<~yP3>Fnvla*By7Y*i= z({*HeqG)6zt)-x%si~r%C2eD*2=0$PVSK~9gFz5tH)?+j6durc19=Y|Zw%nh87SA1 z(kFX@*(YOSn89?O@c{E%24Mz8(1|Au3`)=?UWm1m&_OB%yGRtvF@xTb8Pv}MuO5@W z0}2;#Z%kk(1L&Ft(DWO6FAO$71zJVNs3UKtAug_ACJ&<}rKBV!rKFh7i>jH*%bTc) z!Dw$8aU}_9Fac`UGE8B*%6NpO4%)9QBGj)$POXlhQ%zYwl`1O(s7nb-&`h9g4C+>b zk~tG7bN>e?bK3MVFA(ZuGXC#iy2Vh*e3*fk!4}jr0j)EF4rPMk1e61~Kt2FvM=%5A zK`sV;1}+9rc7(N<3=El(yG<4SpdONfq>Q&_74u;p5h;FIXE`P17;xY1F2gC7ba0;* zY$K?oI}a)8SQ+{m7#Ki_5*#vEiappE1{rD=_YyNSLHQFjW&v`yD}yA1K0`GFXq^Pu-BJwm8KhwDmV%`y zFazXnDF%J8^iBpf2DUFd85F>-*8(j_1)X?y2ej3YfdSIO;e?K6;ct-%@&14@iKMsvN_7202)Ml4Af3RX_tBG>QQmvjBJTctMM`K!^DTluG*wn&1MNsug)tbp8QGnK zrj|77xd%-vZk$n87G)o!5h|fBqpWVC$*84W9%UD+5hkuKtE_JFFU{K0#zfzUNx<6D z+F0L^ad-N^FHtp#;YMI;zAB4+wP%v5j<=+!j;f}qh8Gg&-?7V{8Fn%t0+devF)&PJ zyvGc>uat*@fmu;hk(pgt*jU(9QIwPM`QIaRGw1B<4`s4pd{xJ&eU{PfZ)I@8w;0=|a!6!Z@WhD>|9nO(sGO}Yb0-r#`SQnujVz23GBO#)tXsG~V z$Y_d2s0Q09+NtTOYbjVd*{SIX%V;t#*7OfHHdatmFg8}#)bt5cP*;E;b&z}i?)|@j zDT3)HgDXP-0|RtB3TW&Lydww_(BguipamagrNqw0&d1CS-Yo>a`~iBdwy}|!xgC?a znXsuEG?I)M<-=>-<(;F=t=XA{+>Jwhv$7-og~X)!LyKb!RiyZp1mu`hLhLlXtRF~n)6jfAW18qT5R$}{?7O4yg6-iAa6~;6F z{FPJnL2)W9qkbP8m2z@*mI~?$py*>@`g@zfk4cp&fq@USK9o^hon4$=++5sT-CUjB zToIM~Z=u*){oM_fv2m4+d-T_eF-{O$tG}nQGA_2VVYmKTF{Xrrfs@rHM6Os7IYDi5 z;6YR#1JmD={~s|vVp<5gEtP>mm{F9`RF}yZG;$6Z!daIt3qU zyqum}TbSGPO8pVj!hg3*{@t2$WMBL5s7C9pL8lG{t+Q>60jI&||9crO)H1s-Hz{O1Q+JH>Py zH0Nxr%xKKXXq87}qepV&DdsrHZ1U9jL~Fii~Ui7BEftTQGSt(*(wx zsM>%3nZy2r{I>c33dT65o8ZgG8JHPG8I=Wv8C5Sa8T`FaUCp@sUnJAbe~U~0EnqhM zo5k?|JBl6b3=E7=3qa;ip3K-#_iqw&*uQDu@ck$Kzlkx2*%y4yFazj%6eTu(&=JUT z;HwTm$E|=zFx8YzK{LX3Odz*b?c1j-$H>I!6AGFV_CUEFgcn4~Of)irz^8ZbYdCWcxptDFASOr0WpeV{Hs4Vzz zIpczV-x#?V)hE|6pQ`+;Ucr3oj~VkR2IhbI|C<ZT|4z@J&uIEps6{wIs9DJ7 zpPAHi#toA3QjDigXM;Aq|9e--bffa$%Sn?aP5O80^y$;5LFWwpJNN$`qal+f13Lph zgCqlJ2abuEk(daZk_wb&1<|4+Y)WdXaMr(bb`dr<5sbmM5w^Aw|BS#qJI0A%t}Wxl z2nUA7lM2w@Tg+sO?c|6pJMnJmJlq$;Xt$_QdZ>=RWq z{a0@nZfhIP9P)SbzujP_9rL8Wo1s?BsH^*T9cpkLs3K$j=l%aD<1eNT1~$-XlMLWg zWva`_0y0#DO^FFagA8Y8{QHlYG3MWre}48Luh@f0rjCDKO8$MEvURHo*zy1FMu1(< z!2B=k|4+ulP;(_fX^$6F{-}Y%#l*}=7(|@35Qx zU3PnLdj9*KVJed#<10ux#VD$1$||a8$|U&v9*ffNdyKE@>TZC-o$(**|7T1ZOfwh+ z85o2)89^1DqN$>}x}vF~nm*%CM*V*m(^W;J{wgrLYij=4$@D->LX~O8zvcgyM{7z8 z)avTi@ycp2Ffjgo|G$z+keLB={}Q7jJE;AlZVoa@(Ns}boL!Mg@L#A2y)#zb(*^2GXKZB!kjnB`qiI{Wzl9V0)}aj@G38KfB)xEYn16`7Tp z89^yrSxL>rOcBmX6mAe@5)!Q!`S(iA)KpE?)ReJb&BR1a)y$0P_&3<$$0MkJv#lQx-#g&m&lvQ1q2~0Bn(_%dI&)Nc%=$Z5xkNmST2Q#zYm3%7S zy0sWWfy-E~|1TNeGR=D z_?X$nI(9DUZ_80S}^3BW+VinL1icNza@+@|CpKAgUe!AiHuru#zi=SsuEEAF#qHJAI10_sq929G8s)7 z*?AdJ%S~}nHFa3oX~$@4qW&|hrLG{rGtm5>G2?Ri{|t<-1&KLv1yKPGu?~x^0%R2= zN)@c47&jsnnV>M?UpH$%YRRd!Fo9(zpcf;N9o8V0#XoDNtm3U(%RiO8gHQ}0_da5>1gAA21_oGK69ga00lubA1d`C0<^TL? z{rww}zPSGVs{Qwik*$^ybYn2%-#7pL7;iAUfl53^aYl7>MpIRBUPi{as(*Ghn*THz zJKg^6Vr*x2v-;y?70Rex{4@XX8&EmS_P>Df3Ue&@mPacF21X?|c}6idb~#3VMm8~H zQ6)8dMnzC_3WUK&*uk1pit1`+#*F65?B?u>pw^%{;}s(V76FdH;BY@q0Tu(J?tCt; zf4hX6LG3C*YcEM#AK_+^1mPwjMj>vVf}X!|QVEiOBpLN(J^MaNY*oo|J@8~^|AcZV_;xB z$8?-Q8rI@uMs9GTS}O`Vh>6wMT$$CFU71tT&PTXeI9|9}(ArB1#0Ce1&B-IuNB{B0 zOC>O#J0g7)*5ZVEJG=f^%jxyU8Cd?FWnf^uPprM5wjkJEQAN{IK6^jWX3;pY7BL$y zemftj7U_8DW@%d{&VOeAS`zr{R`k&ck2A034 z7#Nt$nT|7PGw3rIGng|lFru{ZL2(Vjr~waZ>4PvRX@Xk&Ak1X$Eotv31dajWuVA`G z*v3oB-cPUv6e}X%1Y1NBM4Clx|Lm8Fmu9>!{Z*3js&oRp6M&Q)Kq(T^4>*0A0aQlE zF&<{lXHa5L1C3QMf?JrN>$8O@hn^VN;bm5Z5X{kf!r?i3p@Njsmb z@lpx)4UEB4ra*1WKAoL?nt_4q@16gTn3S3Bf!i;jcC9LdK7%ELGlMTfI0FN-IHNkK zichlzMzJL zAmcNA0S$4%f0r=j1Q?&|3u=lBGTl@8tD(Yl1592H_wo!6_jHef(p>6Vf$Ca1su1em zb*M0QX;4@*|1*a7O+bf#fW`w%nLv$tBQX(B>A@uUchk|zprFbirh|Vsf%+={PKVS4 zhtzO^x-1M#|4bORGQMTJ0qLu&Ky`|nDvJL557HJ~#iY0utbq;GB4qk^=l?rK6^6^; zzA^)>e+;Al-EoX@c8+0GcaCv%j4_IHc8+s#0h8b~-TVI?V@CN`!ZP(O_sdQq6Ms3PcKKT*(L*qyB> zPP8%>ww^rM%E)y2a_!~IU;-3xEdK=mzhk@y4hv8&(Pm&^#u^%|qKf9A-X=RdEC0`} zRyQ|C3X5pwqQ5^g8Rz}ow*?#~#>Ut(tzAIgdvHfJ3lmVlCpuJlD4XcS#oqWG!_{5Kp8mP0FDkw&(X}!jDh)|)BksjtC?;x z7&2HhxMEEcpwVuWa!=jN*hm~}IysV833As2B|`%x9Tlxu6@D8tEqJO)jhWVBAM)=U zsB5c6Amx}j$eGBSDj^CzyMR1zZ#36~`q!ubo5IJAWEnIW7??oerKDz}0;R!a3m+pt zqq4c0DM(aQ1j_yb8g#K^JO>&ivHiCu%9@8q$TiJSkMV4jji!65p&pYadaZRh?0uNga6z`rm|F#%0w-Vr%qwAqJkY z4&9=T-3l`*b22J3rZW0&`1kMMzYUCjOvnFPGHLw1!E_vypZ|XS-^+LpY%gS_4^-wt z(yRHu!Wzb<)r=?qp0#28{x{PKY~8s4n_{kb#+jfz?z|^zTg;zx@pjpghR<+x!1Zrf8;F3_;L&342DQX$kP` zgc6tp&BZ|PnF7!J*nt-~feuuGa3OORI-p*6hMS!wqp+%slMZ;!LRH>H-`G_~QC&sO zCC)`&Rar&eRnM|StcXuXTu?;;EGEb=Ev;m&ZsaDdC?co|nb;5#kdrV`k~2Z-x=S#F zR-%C?HWbtplr)tvCOJgqn$xTnqm^Ff3#R8Xlnj0K#cg!VCtO1E35JEnyxN5J^rzPj?h^Lu_v z3riBpsLpvNrSp)19kxH6|KBmng2$El8N?YtXAPP#z{i$h^)!seHm(hp0}a>x=>!jD zGcE=X(lIUuk4XGs01MhOJ^+i_F+KzfL&u^a!`%=PHi!os^k#$(BQvo5;bf3tyvG#8 zz`-C09{W~e(1yAZWG~zSp!N%>?gI~8qsaW>1l!IS3?6i6Vf*XNwDqs|ABdZvqKvyE z?Co`Q?LkGTj;?(^xaiphc7(kHgkoS|`@NbWgz*nJyg=t0nfaWwzhb{=Z`qgpLP; zd@Kx_vjmL^vqSv~a)qL(qA8-qaZ|WKH2iNKTlL?(e=8yM8c-doYHA7@6}}7d=#ARi z+FDTK0yHSh^n3OHXN=##W8vZq49pm#;i@1J^&U`>nXV!%^T(S-MnmKG6XvZF;wp^0 zz`~Co!?KE`5NL3mPevmKJXrqkF9Y-M-~X30{$jeuz{&tRw48xa(OjHakx`sooSl(X zQJ7s_@%L}$HRm%v{}EuFp7Ho`#=ncdXE1;GoN@k72;-|aG5>css07tI){`|m^a zJ8=H{J?;M?CPAhXp!OVO+?^dh?rshqcmJzm!YuG-MRjT9-x8*)CV##&2R9bSGff7K zyI;1MUK>?+&uR(GEXWwXG-w_W)CU4%gn6Lxe2AIp#!P4ayf$I>{F9ea?D}^t(>#;E zdd$)$e>O7DOe%C>@@KpmQ6E{i$-2BQvOc1Ig=Ibi1M8o_|L+(>q45am|1dBz;~v>Z zwB7zPh%|^WofmEp{ih3#MKd!dA27qzl*tFydczp)M{Pwj{$2}>U!)Qc6k6u$it6U# zp!p6+fu$lW^ZVD#^xZ6*G&O(w?M|P`!X_cEf{57l(HC`fFT||llL5~OGyh)ue>vkC zq<9roWHuLvq!va|MRs*z#x;NVKV_W%C&Fm(??U>MC#?Vfgq+X#1X|3&7xQKvG===T zR{JiRQ2-Pd%)d`Ch%v5*j_ZjqFd)weK<5SiFr&>2z@`We|Ff~gGDQHJDPa1&5js!6 z%D@L{MX0liLZ=J-~J z3`y{HG5Y=TTl$kH>EBo+4Lp1y28D!rEB$)~o`-lT?+?0S;`c6S9l`^e$4BHlHGM`% zARYv*tjKV4HTt8*d|TVsjqwA>{Pf3^dRZ`JA(k@dZrTae3vQ6&ybO8$ds2nBRB-WhXUeS$@I6u6*M&M{;!h7 zkg@$=l{=&Bzuozf%FxwK+6q>X)lKSRk%-kzR*=P#j1hxZr%DE|2a)KcFUs|i{S zgk!A}^Y8EfA2F_H0GqETJd12%LJJXL7SJ6V?6uM803<_k3p^j&CqlRKGcNH z(7Z%m(1L5KhWStR{}l||!Sghnpm{Rz>Jd{AsVd0&Z#8JThWTK1^}l@hlnrC-pYyXp zvo_3sqW@1}^ai(0xj=KepcN|OjNlbF5cfWr{Dbk-KQl(BfBXI|{&$ek{+~H$a*jF7 z_uu`JfA{7b*tKBx{@n{1m5Raf^hfl66ytj4JO)h$1JFHtAivu)qR(?Pnlkb;vZ7Af zz$R?Klii?T2{8WpZ}(!W%;s9qWVhKrL&g;f{~6s15_01Tqd=41a&qFhC%q*VvCrUu z=e}hW{vCGLgUx;auKWLtF^_2m1Ej4BUWI|kG>WW>ri$#Zo11@MlMz+{XUKFFaS0Z! z-}x*Pf9Gp5wf!xhAtb38%^1lT8KWV?cOy!dfe~!xZ*bdyfl-lN96CJ%$%l%JzkkJR zYO*f;bqv`^Rx8FFUENw_vp`|W1TqUe=PnAG+lI}$Be@XPAZJIk$nPK<%%UPDq5As- z%O_3E-+Y>|ib+}k*(PWsU6+A@@%M)R&lrC(%>cFh85PCZ)xk4MP^W_$3G9D*ST^s@ znECs+l!$8jzYB~8>8cW9EUYs#cK`O(U=+F-ytZf|8vh`3Uoz$~&0+wx z{n0}i6u|Srb5SWZAfYixu_7=Z$QH-04 zQg|a93+YyX{SBE92lcU#=hXf@08Of~ru>=Kj zKhF62`-Q2KZz5>6H__M4m__O>qFBh+y8zWCL#YF-HEoqN`hr92cPZ;@=yfbulZb z{sgxtp^*!nL;UjqRFVI?z_cUX)kl~4&L1@+S69enBD4aE@ePn`QS$bSW{hB9{{5Xn zjPX8F+XFJ42%66W&4faDe|KZdF5;eNgiSYs%tk6pC1LA)U}Y)NcqXXCKq*L>W&iza z{__X2BnGq~5MF+wPDL^>{{H^ok8wS-8z}ymVdFHg2}yAm#?O$-BIe(`Y7NlLB&Z%~ zXH2R_4BQC7rzn{|Sb_2^+wVf?T#z+`J%clYJA*faKd$*8(4-3pqs|?e8?(cvF2OWt zvI8_Xgp9Gy6MYeG5>60q2G1Ayg64}vnnCkLsPjg@Ql&xjMp9p-7_Z@+K|-3j0nH+T zFl;6XG`$HpuLG;?sH zw)O#dFdsgo&%pfW#Q%4U-r%+eAA=aY?E&jRgZuI#Y)Xu5poHP-{;%?{H^f+Vu(7bV z!@upQ1~dP;^8X#fRp>l}I0ME^1I*w*SHR{l20$o(u;IuP4+yt1{=Uqh&v=C?2vk#m z#_JVP#_SQ}_HUa1^+Sg4n;DB*PMvB2joyQY?%^Z&tiR{{?`0B%jzdEF(csZYMo~r3 z72>AsQ2LJqXx%L1iN9xTSX%xb3;LU3&GPc^G4LoQXgCx+Dghf8IQPF7Jns+M3l18M zf{t#2MmYa?)i5rr3anvVRviEu7zG;u8RmqL3=E8aME^U2$I{p#vu>dEl24dQf3+}` zOtx=mVAcfH5%U-rn0T2KK=)oU8Z#?%Gom17V`)YdB+OVd(QmD>eVc=Qn}b7}okN?w z$r`_j$`-BGZmpK?^_~{3ZZ@qJObUM=Fy{T6gpQdM{<$)i|La7@pmKoa*NOit7=D7s z?${YX8!{lHcc68^uo1l9&ecqwHB6>V`hPE0S2HgA7sYfFG>8YnCv6W!Eg5}32aJ>Usrw2MkU5)|Vn`Z~rw8o;!pgNNg zGc%DCAn(;c}k{e6ujb4I-OL69J|DgS8RtC_e&X86I zqatYZ%FGkY?70vRN2} zm8%40;R-42}#r3^@!6NVG1qL^UOok$cM5t{k45sjYakRMw41%?oY46rV2CIx`)$zn)lNCx}Uk)f2Kgdu|=pP`7Mm_ZY#d(*&f2dM=44dm-o zhBSsuhCHbIlNc%)6d0nQJWqx^h7^WUhGK>iaQJ}ytOE|mJceYj9cf@26&O+(@)$B0 z5*a|^pzwm23<;eihD?SWhD?SMu-PCrDGUk>$qe}nDPWt^89@3g7z)7Qoe4H8nIV-y zkAcBCzo4=xGd-h3VWonRfsu)Vr<1RObAC~Qo`PdejsjS!SfMDjIJKxOHAPRsCo?^< zq_iltSRpYbv7jV1MIo&yKUcvmvnWx)IX@*;K?Bu9LsMNN10!RFkc?C$MFBnM2UCF?1q z1r1A`MoK0^*e3Il^veohKLjS6t3*3e{S z0v=W|Vz59nQ~_j^H7JA)%#1*i&;)?#7=)Rq&H|gjzz_zG!(xU^u-id-PmjR>3>m^w zi;6Sz^AwEq3=9ll&W2e4^Q$i0$sprFo(B0@7vww!c=m;AQdDA4V8~DP+)+m3}VPrz$umC1vJi>nH>ymgXpwCFUulq!uao zBqrwRC^!3Zj%k`4-b5e5i^HLHk^^)^JQVb;wn5JbiBr)VNVAG$Olnc=aDosHV z2`ZUE`3h7!1cMVu2}3zJ!GKC=hzd|?1uB8^z;ys9VU&VPLP(hjDLoY!f*Cv+d>9lM z{J|wx9=ObhnGC8c5GH_v7(HhgGUzdYFr=KvW-lUfgCZo8A%`K6AqiYMC@_GM9LOD@ z8VKYgM}|NK5W9rIia`M_$cq`0!KF8-mMUh@V*usO90omb@=XWdB3b_mg@VX09 z=cj`UqGWKEQUJ#isO11^S%AVDRGx!$fNooc)Q{;5l?=HIc?^l*+O-(84%8#RT%jnj zD!(WxF*#c|t2DO&R7<24<);@bBq!!6RM8eQR57H2a}h&gPDyG}USdhALQZ~pYEg1x zajHUUaaC$b2?Ij`IK4p%#5{&lhFpduaM`W^)(&drFcjn@X67m6mF6a;7AaJv7UeTA z6oYjYgIg}3e1k)0aZX}!MrsPW-b@BixPl5B?2b=oU?|SaEyzK3FG!4mkj6wT8WTZx zlz@8KAe;gs&@szO(A*v)6XdWy1~vwE1`f~><)Et$7Yw3}y`G3>FNQ3|0)*3^ok540fPvv=|&2oEV%LTo_y#+!)*$JQzF~ycoP0d>DKg z{22Th0vG}rf*67sLKs3B!WhCCA{ZhWq8OqXVi;l>;uzu?5*QL0nHd%{v@oYh5c?_!Gk zwG1^3I~W!+)HBpEG%z$WykU69u#1tEk&Tg^k%N(wk&EFk!#_rDMjl39Mm~oBj0}wY zi~@{;jEsyzjKYi}jG~NUjN*(EjFOB}jM9uUjIswm~Z}Dl#fDDl@7usxqoE zsxxXZYBFjuYBTCE>N5OdxXuXL5pBR|$Y{i9%xJ=B%4o)D&S=4C$!NuB&1l2$h~Y7# zEu$TyJ);AoBcl_eGouTmE2A5uJEI4qC!-gmH^T;ojf_5wzKnj1{)_>Pfs8?n!Hgk{ zp$xwn{xCdYc*+>Y7!DexXN+QuW{hEsWsGBtXG~yBWK3dAW=vsBWlUpCXUt&CWXxjB zX4uV`!feAO2#V2YQ`GITE;rYdd3FE zM#d(F8w?K_ZZq6rxXbW>;U>d<#%9JA##Y8Q#&*UI#!kjA#%{(Q#$Lug#(u^Lj1w6r zF-~Tj!Z?*-HsdtL>5MZNXEM%WoXt3gaW3OL#`%m37#A`wVqDC)gmEe3GREbMD;QTY zu3}uxxQ1~p<2uImj2jp?GHznr%(#VdE8{lC?TkAZcQWo`+|9U$aWCUO#{G;37!NWY zVm!=vgz+fjF~;MJCm2sMo?<-Bc!u#T<2lCjj29R$GG1c5%y@x?%TZ!+Fu zyv=xr@h;;%#`}y97#}h|VtmZ_gz+ikGsfqPFBo4kzG8gM_=fQ<<2%Opj2{?3GJazG z%=m@zE8{oD?~Fefe=`1J{LT1>@h{^)#{WzVOpHuSOw3FyOsq_7Ozcb?Oq@(yOx#R7 zOuS5dO#Dm&OoB{8Ou|eeOrlI;OyW!uOp;7eOwvp;OtMUJO!7<$Oo~iOOv+3uOsY(3 zOzKP;OqxtuOxjF3Ou9^ZO!`a)OomKGOvX$mOr}g`Oy*1$OqNVmOx8>`OtwsRO!iC; zOpZ)WOwLR$Os-6BOzun`OrA_$Ox{dBOukHhO#Vy(Oo2>6OucMOvy|sOsPz1OzBJ+OqonsOxa91Ou0;XO!-U&OodEEOvOwk zOr=a^Oyx`!OqEPkOw~*^OtnmPO!Z6+OpQ!UOwCL!Os!09Ozlh^Or12&}(>$j644W7>Gi+nn&Txcb3&U21 zqYO<9hZx!!Rx=!9*vGJ+VGq*+riDz4m=-fFVOq+xjA=R33Z|7ztC&_ZtzlZrw2o;# z(*~xEOq-ZCGi_no%CwDXJJSxPolLu!b~Ei^+RL<$X+P5erh`m}m<}@?VLHmt!!V!e z7}Ig46HF(WPBEQkI>U69=^WE}rVC6LnJzJ1X1c<3mFXJOb*39kH<@lR-DbMObeHKK z(|x7~Ob?kJF+FB_!t|8s8Pjv77fdgiUNOvISivxhVJ5?ShNTRX80Ii6XXs~`!Z4L# zC(~=DH%xDt-Z8yr`oQ#&=@Zjuh64--nZ7W6W%|bSo#_YDPo`f?znT6p{bl;c^q-l5 znUR@^nVFe|nU$H1nVp$~nUk4|nVXr1nU|T5nV(sJS&&(XS(sUbS(I6fS)5sdS&~_b zS(;gfS(aIjS)N&eS&>f*^$|a*_qje*_GLi*`3*g*^}9e*_+vi*_YXm z*`GOpIgmMsIhZ+wIg~k!Ih;9yIg&YwIhr|!IhHw&Ii5LzIgvSuIhi?yIh8q$Ih{F! zIg>eyIh#3$IhQ$)IiI(i6=0(hlnU^pxWnRX-oOuQFO6FC}tC`m@uVr4xyqM{GRy(^GD`S%%7RRFn?wK#{8Z62lG$nU(COm|1ke$ z{>S{Eg@J{Ug^7ikg@uKcg^h)sg@c8Yg^Puog@=Wgg^z`wMSw++MTkY1MTA9^MT|w9 zMS?|=MT$k5MTSL|MUF+DMS(?;MTte3MTJF`MU6$BMT13?MT#frt6#fHU}#g4_E#ev0<#fin4#f8O{#f`C@#f!z8 z#fQb0#gE0GC4eQ6C5R=MC4?oEC5$DUC4wcAC5k1QC59!IC5|PYC4nW8C5a`OC50uG zC53}y3Z}SR z;kvm};Y@Z{i1qBL5Sq;uVqz+o;&w+kggYI<=5mJ{z?BYX^0+7G=jJ9t9LtlA#NqZp z=;h8puvt8k5{p;<+=Fvac(aVvW!gl6*e zV#>_o_ClD;orPfYcq2K4CmV&6UX+@emy?*6l9|lrlbBpulFF6?rg(gi)#V^@*nGet z!X^O1RML14?-iog_45R$b# zMMxaBAh6|ZMGz_&temYFOmPPzf{wcw!R84;GJvN9iNh8GF{T7ev4nu4v;-8Tq2Rb= zD+N>Rp%CA&mqKW^P_TE{O2HIsXi;WfI%_G2oFjCO*6eDrK_8J*FLTM)mZEOyqO)Vj`6I9&E8O%2} zfRs1J28K|+5tKHD(k4*a3`#@HGd8e*@*(CL8$j$eHh`FKYydUi5Ne*GDMY;?RNe?` zz7f=1BdEDX5OYnS?lOV8%ftX;j)?(8pNRp)T_y$)b4?5&=9m~j%rSww&%^-YJ` zOrY*Ffx6EGTJD+{xPskhV&Dd$4I$xTVh9Nr6R1B-459WLLhXn8%ft|Bzai9qL#RIt zq4q=L#RTeq6GKS&m>5FJeG@~d{f1EcjiB}$LG3q!`X3r@CPq;EjiCOAhNB5I98F;1 zXau$22ogUgMv(9~F@oA}1hwA?YQGWGeq*Tp#!&l>q4pa?{cjAl-xzAYG1ULi^k!lV z^}jLHeq*Tp#!&l>q4pa??Kg(nZw$5H1Zuwt)P57F{U%WRO`!IhKQbipze2py59wAzYElU7pVO%Q2Sk=;o|}g9~Y?qT%h*3Ld|!Dn(qoV-xX@UE7W{f zsQIo?^If6lyF$%#g_`FEb-x?b{cce6-Js^XLCtrAn(qcR&kbUptFaqfczRJ{St?sO zgbD{&C2ZwjiZeVVGqotSIJ20u9L55bdxox-V0Re0LgL@h6%zl3u8{aQbcMvfp(`Z* z4P7DeZ|Ld>_Mf4vBiMh2u8v^$7`i%w-DBtqNgsx;kn~~b>Ie=`Lsv&|cpADw(u<)h zB)u5ALeh(&D;K0-_RA3z6@O<>C4a+lD-UGox$O6=;{o$-x+GZGt~di zQ2#qa{qGF*zcV=f8oD}z^M|3UGt_=(sQu1R`yuJq&=rz?4P7DW*U%M`ehpnA>DSN| zl70EF;5lKu@{-3-|xAqlt=OhHm9B=sA*LQ=n>s~a@*-Jqcl$qk0C zklbKoU=H?;k%2il){G3y!Lep!U=G%2WMB@~XJlXwjx{3#b8xH~8CXE|S%5>q$iM=e z3ylmcz^TBq>&S(32Ed6 zX+pX>xtjCB%35$C4Cg@-cA{QlPKkjbdpZhj#08p0f(Rj)Mm&jNBOu~DkRbsGM=%l5 zU4e-3C&H|S@Oew~GC>79gvXNzwjRQPN*P0W(2)li57K9X@VLQU7YJJb%tmn#xX%WW zg_`Mx%yTkF;TfXvV77yu>12v5=ZMO4L6Ji-)6o(|&I!WfNe6cqAsj)Z5ebNh5b~e{ zL`VQJJ^>M6&&&e*lnp`&q^B0;Cg!CiyRlz^F>d6`fHA$FwX<>!L!05d>#fEZ9aKum}o5EjS|5DRPvhyk?&%!JqhVnXZy zvAB`qo(mG($(2PpobawLL=YTE5GFVQa)Om+fFF^v!Mc(7V9iK; zu#tQS^&oTkVA%jNf`FtNY!?@p)B{cV3P70P{0J7{hG+q^`QSZ$Fcv9?%$~v4JI+1JQ-B z7Q#o^%?CCW6e384MTtgVw2+TEtagD)TV;I*2%r${=O~G7K z7}pHUHG^@@!CZ3~*8?$LetBk;| zGJ?6v2<$2&n5&Gyt}=qT$_VT#Bbcj?&iJtBk>} zGKRUz80;!zn5&Gzt}=$X${6e_W0?&iJt4zSI zGJ(0u1nepkn5#^{t}=nS$^`5x6PT+^z^*cZxyl6WDifHiOu()(fw{^A>?#wOt4zSI zGJ(0u1nepkn5#^|t}=zW$`tG>Q<$qv!LBlexylsmDpQ!NOu?=)g}KTU>?%{3t4zVJ zGKIOy6znQfn5#^|t}=zW$`tG>Q<$sFz^*cbxylUeDl?d?%)qWPgSpBK>?$*utIWWz zGK0Cw4D2d1n5)ddt}=tU$_(r(GnlK)z^*cbxylUeDl?d?%)zcQhq=le>?(7ZtIWZ! zGKaa!9PBD{n5)det}=(Y${g$}b5lKtk*0cv;9PDFcAdEyRM-qA40fftIaJsjCJc71 zxdl|%0wxT0wYeo!*b*vi0k+D*03vK*022oL!om`M!%FD+ocv;h0k z0_sZ(m@h5CzO;n;(h}xNORz63p}w?)`O*^XOG~IPEn&X21pCqw>Pt(QFD=2ow1oQ7 z66Q-wurDp4zO;n-(h}@TOQUi zDNM!`E@K9fF@wpN!DY-LGUhNDbGVEJM8*OpV*!`3gveOJWZ+?72=TfhEDQ_{V0sK8 zVPFUg14DQi7(&9p5Ecf8@Gvlhgn=O}3=H96Uw#7#Kss094jrKBbX8+J(vj8XTa&kmK&?qri~k8SJaTkmC&|eZZuzKS%=M1kI-ZOk=_|EW~;Xfl2BP$~Z zBR3--qadRQqd21!qb#EWqcWo!qb8#cqdub%qbZ{Wqcx))qa&jWqdTJ)qc39sV=!YF zVZ6vYQ}2E>c<+!8poQ(TE<$(+Q!<) zI*)Z3>pIqLtjAc-v0h`n$NG--8|yzdHa0OfIW{#mJvKWwH#R@EFt#+dJhn2nI<`Kx zX>9Y@ma%PP+sAf{?Ht=Zwr6bb*uH^o0%I3rmt)ssH)FSBcViD@k7G|`&ttD+Z)5Lc zpT@q7eI5HY_I>Q<*srnQV}Hi}jr|`78wVeU9ETc*9)}r+8;2i97)KmO9!D8R9Y-6- zG>&;3%Q)6??Bh7bagO5}$1{$19N#$paq@ABamsP3ahh@3ak_E(amI0`aprNBakg>x zaZcl$$GMJk8|OaGW1QDG?{Plke8>5Zi;atqON>j6OOMNp%Z|&BD~v0SD~+p+tB$LU ztB-3Q*D|hkT-&&gah>D3#&wVD9oILmf81=`V%&1vYTSC#wF zn)tf-X7DZGTfw)DZy(<=zAJop_@3~6;`_zV#4o@v!LPut$8W}O$M3@*!XLw*#b3l< z#oxg{fqw@7GX8b^+xU<0pW(m4|A_w;|0e+k0S*BH0XYFR0X+d50T%%ufhd6_fh>Uv zfd+vNfoTHs1eOVG5!fSeMBtLZErCY@9|V2~G6?bsiV4aI>Ij+$+6a0H1_?$9W(XDt zRtUBU_6bfCTq3weaEss}!Bc{l1Rn^#5d0wcPl!#3Pe?{cMMy`;O2|pbODIApK`2A0 zOsGz%O=ybH9HAvbn}l`=9TK`AbVKNY&^w`TLjQz$ghhm9gtdf?gsp@lgwurc zgv*3mgnNXi2rm*|CA>-afba?73&Qt=p9#Mc{v*O7!XqLjq9mdvVj!ISoqB zf~b?6C%I4Zo)m+W5=2}I8sbt=_eeoPoK?y}DnY72YLV0tsJd%V8nOePQR*JVJgG<0 zOwvlyF4E8tkcNhUG&BUH8zAbWTcnprACZ10!vQfz2ATq7dh&mZH88?|UnGP9f zn9J;e${&EzN1*CX$h?r{kkyj)fr!h-LFptYod!`Sn^q1!=)eb1O(e$x z<#RyP$??eP$oa_S$xVQYFM`t0G$6MMs&1X!Ik|W80`f)>d3hHo?E$5IAnN1;%>>UB`M2}-v?)Twu=uTnpy{ziijBCeqTrB$G`21K2Pj)sp$ zjz*ux2BTq5THpE^TPJrTqn}?uU+qj)hK?4z!%m>4T`#ftFi3 z(;(_}X6fwGxux?*7g|o}LTd_LXt||p0a2%GqZ^}JqdQL*T2AO*f|#cZEw^-ULDk*U z{ii3V=b#5IC-h1n>hz%HmR=1+onC|90=)xzkMyDCguWO=oj$bO(wBp%(^t}W(ofQF z(TA23`a7WNpyihS0jRno`cDj44Acyu<%B^LM4bV&+%ials53}2Xfs%5aKZpuP8j@w zs568%oDEqZ>I^vyH4Hrrvkal-gyB4>I%v6NxD2XpmEkGFS4KQW&~m~Ea(W1(5wzSg z@_?u_@-fOW>M>ep1T80w9zo1Af|grGub}GQ8S@$I83!0c%L(Hah&p3vxn7InqRs?bZkgyo)R`EW1ep|>Ofi9$6DB91>Y(M8$pxsoD<)q|MNG|1 zq2+{W7DSyXwA?Z+f~YesGo5C-&Gd#Tw45;Gfv7WsmRn{b5Orn}W)@}sd;a>5K+ zx0^xBEwgPGk!pB0*eC{4=kB1q2+|78N^;oXt`x+2T^C~ zWSL~yWVyr=T25F(TQ`=_a?29Zx?#0^V#Q*mV&!H9Ehnt1Am&*?%Pp%WhiYTMYacQ@7Y3YEjwuJ+d*QV#ZJP`%r3+Zn)mIXb%$LSM6X?+-5R@7cF-JX z2hD-@pz#3)1{Ql3dnJ1Z`vi!6_RxG_52-I%?5pf&*l)9kmOAz?psLB=560q&c)WEOLN`paZn!;BW(?*Wr%CFGmSSXb3t&%U?%7h^%9fV}WCz zBQyjZp{1ZBq_xE2c*60S6N?ix1f8I1$;k?$*U8Q)#;M8)8iG#H7eE&=S#00AjM2h?kL9fEP3by`bfi7o`2d;??D~!t0n9Gz7hV zK*HJU4@9pwgSVWwjW;v|y`g2XH>50P@hKf!0bskk%`U&kdh%z9PQR5cG9}xXRZHqSx2YH^;Zj7aD@T z(3;p6QWLZI9`SwT$KVGIK|g3a-p>r8*U!o?!mrE^8iIb%){x&CsO$#6bAE69p&{rG zZIk#z`YtT~YW^<%N&e6f^oN%0{xcwY{pa}a^1tB^4Z#3t`5FLehp_|*1sDYQ1wcbE z0NRTSfb<(#0y+Yg1sn;0hF}1+P6>e2DJ%j10%ZcN0-+%om;(uozygT9fhB>H0yhLg zLog6puLnMZ%DxKX2vQ4zhF}o1b`65G!dZe6g6e|i1VKYE=p4l4pi2$Hp$VbT5DbO3Cqp6aNtV!Ap*upa zg+fCx4B8tFLM+41?5-EMaY7OTrF?K|?SM+OiIV^uSrd{)9_~ zTZBVHFdW)G3D1L=99|SYA$(mpGz7z;t%Pt$Z-OQKMFd-fN(3|nBf=oz91#W48xa># z6EQ0S8iEnfmQe(xWyBJ3CE`=0KqNE-Bcc7ONH>VyNUz9@$hJsm2u4C%r;&T0vIipX zMgEC`hF}!5jTi;#OR_|nMTJBaML|O_Y6-+uQ7a&Nqt-;7ih2_5_>wB z4RQxe927omENx(M5F23wD4aodf@o$2HZTN*6}k$L7^t6xTLm-AVwOcL3*li0I)@S6 z6eg$&Mh1|54B!|*SHlQ4g9+>skY71b!VRPkG*1VbA%(aFqz05q*jPZVHHgV$EHN7k zsP79gnT@3%>{pO2u&@D{&c*`Ddyu??ZYD&OnLz=3z7EI_=qf;BEKqsuxrmu%Hp?uQ znecQVh~hs`8v~R(ASz%v2-KfJ*aT6-2sVQW>=KawK(pKkHOOHAaVU@qN@Ojv4CYURm6i!5@wd^EYnz~!ovVG zQi3oAqJ{~of{_8lXJ8OV2`7*kC>?;zU;^6(@*ilV6JZX>^{{jRatTNe1A`>EY%m4; z8zjQU0_q<@Y+++DM-m5>#Sq^j%mnEGr4kU$47L|NmO(NQTOnb9S>l319+Vd*vrJ-{ z2oD2kEdFDHs(_^fP_2vM7O)whFaVj$z#xmo98iqFTnh>(P#cPkB_HfpkS;bBP`elw z2F74JGT zQiu(p(oF%YOF_0k%3*YQQ27P&NgMRUWRNRBbA|{TASxKa@{sb5fdLf02o)d~LE;_a z0+1P?mM$9$XzU!Khm8d^e+6R~!+nS_1r%1tXJTn!sfXtiP=3U)0W1$$y~V%)%Dw0+K=BT7 z0mvtyF%>qJTyWTf?0}VN5H_f13^5Dc6o@Fu9C@rR1=+&PAjlxkfG!WIQP7?l)6AP6N3{2H(H#4#9=-G$)d}HPC5tW_;MCdc>;0` z0|RKz0$~HFtOS+9AbCaxka`9NM--nR?0~och0BRG%SifRwR} zV0l>k22`^lR3PktxB%o6P`{at1vG;Q(Zj|9s$n6u1sh8sIHiH~p_>Bp2_K&RFQ^p8 z?UOukIRo(tXfz&S1E|~tr7lP~g4$D{gOQQ@ydW1rQZT3#hNuAffsMr*>|&4z8w+TD z08;Y8dvI)2B&F|ItB*N+!TflV0lQ0F))C_6kP>K zFT@3)I021eu(5z*2BL?J1r$CoHpGV@H=~;ZQ2{asy|)Nb4GAxhPcZwNpgtQY$7isl z!^>7p*V0lP?kAWc+C7&SdfVcqU6VMnC8w-ep=wV|4rAUZ7VeK)HJ`8)nBP}3v z&`V*EYJ^X4m%^#wmIWwW85lriCx#7Rc}R)Mzz~VYC!lg2bjmqs1jHGfxcM!SWE>85m-5`UKh0Bls6G3 zzl8S)=)beFx0kvu%dSE#oWEvX_ zs1*STeS|&eK8Z)G<00}epMZMnAT~ljsFVlg_(*We0wf1IrW-lLL3V?D0?Iv*9xDR_ zsO5n$17Qa!Re(%nU;x$bY|Nk;7Kk2Ljt7|r^C3tdx+xI==san3c~Dva#Yr$YM?!o8s_hXrzDz2bm1=NdP!ULVN=12O(^Ls9*%k zL&B7S0o1ESr~tVL;S-R01_n@jlZ^$`bA;%D`3fWpV}taen}YBOzI>tp9)AM47NH-M zszE;S1=qnKIR*yMIwgb(klmov1*xM!>KPb7JphCXkc$vLfv5nD4YDzVW+7nt1k_`O z`3k}Y=|eXK<`Z_j?JkfkuGHlPu7e>y0rj~MHh@OZK|X=h(U7!Li?i$k*#XKYpwV46 z7IkoG3(~{J0;wfIrm?Yr#w#FhM%V-L7orr7$LkZ2EH0n8gX>_3Pe5~x2pd3Q4Dt!2 zjt04kfuRwnPe68nd;%I3U}FJ|H$n3WGic=lEPfz91i2ky4~U2C6MU%)R0gBVL)^*4 z;sOpch)+N(2oN?vR4{_&AueHH0Nq@LPyulRNH1u_4Wyod0W|Z%#{3u@Cm<0v=BEfY z)Q2E_=%zqoiiv?2Z=8T+am9%v3#g|Eat&xd4|071ns)%j2}m9iz6=bVIO7Cl2PjUu zz@y8c@l}W(*vKl#G&UAc%tG9Z%O{C=eFBojeBs>O+t|gejofACyl(G^kaNUdMyPVLk!X@aXa&(?CA40M92te1dgm7Ay}* z5ey8VxeRnSfb>FK0P+cFjR_kIXeI=rhm8d^n*n1(ay-aw=%zs0sGwF4dQS_a8s-yF zI~BV;6N@Q$h5+Om1_sc2BMckB@(_C%7(laI=qf;ZAua&<1T+`P#-a>PQ6M+4v4BR! zA^Kru8b}=76o?9tIq3NSq#EH9+-+ea7SISO$TbWMpqWn$8^H3AG|#{QnkPk90n!U` z0mvtyRkUm@pxH%;9yS)xJOsp@upAF^8@eeF6_ELFkcsH>ATeg}>^QnlL8S;Nb?Jj! z79e#D44^gi2ooSG7{T%oS1~YvW}Xo$KrVuW7Q_W0pMcf@v$25YWFdOkSU@8iFg7H| zgY+Rxf!YI}n}LJ~$TjHlATj*?6CD<9c>e^nW&~jZ$Uacn1(Jujgng5@DDVPIH^QVK&1?Ug{NfSAAtmWQ-u85lO=@d?O81_sc2Z#EXt+7gH!HWtvT9Ej^-;vkbT?1B0O z)RzLe23;N`2I@27juQnIP|X5T$H1@!#hoCRf$|AR9^w)PhOHKy1_sbtW`qjZSUpG{;t~digD5^h*a2|?$S0sx>1@nj!Kn)*56kf&)7Y3n>nR~- zA?yLELihyKvI4mVT^=L`8W+Zt2c>>c%_zbGS`7nI$G`yEjfgM-qJj}D4{0|sFdW6{ z6OaucpMZ8surYrI`vhbH%vTULL>%OHggqc0!Y7zH8X^yJA*k%aln3QTkWU0zK&3uN z9RmYsp9{hUhzdrqJS3MgFr38c6OaucpMdtLu`#~``vhbH8#8E^6+}Ob4RRa89uN=V z6VTcwkZaK8L1LgBk2`hov4HwOAax83XHeV;G96TQf#e}6f`Q>IiVBDuKzczr9>il{ z0IiFKjYxn*V7VV;8XNOlun$4v=q`oW0Lt;`H6uthET4eNXmoi{c!A=C8{C2bxrTuO zw4M{g2CzINOc@w1;`9l~4p5wcR=cx7`|cn+*qC3#UC+i0+EW5?Glo6jRWl%;;LGu# z7{M;j#KHk?K|p*0+WCNC16UqnI|IX2JU#)19RmYs4+|SJXyrdd56o8}SvKb9a9?5A z1NDh4Mja2*35yeuEH0n0vVh_p;uBC$4`BmnJrpQ)f#e~sVqmz5(kWWA>mf4s=Yo8%{U}G!DJ^``OO<{!lL>8@|4pNPX6IllA@=VPC z!7T`oYZw?n_G8!pmWSBG!0-?yP7rp0d;;PzFo4!v!+Zh~VPgh~K=iXQgY-k#2zx+d zNuYQK(U9H&$TjHlAThlC6Xrk6zv2B8P~0O-0F}-x42)oTSdIscT_IF}?1qFE#08)@ z0p$=jW>6}G=z*1EAk)~GLA_GQ*crMh5K&N$M=zTYwt!kIpwb@XD};Vfxx&K0#QYOH z*8!4aU;vF4B2<7kGh0F}`;1#|gHbOrrj6gp525wn^qRWHA803@B z%+MVP3=9mQ+8DzIuskGu85lm|j1!O@AfJFtV`B!@Ob|V6%%HI+h&$PsCxc62kgqW8 z0k5j23yFo1Fdx(Y_HJR^8@Is?N`lsG}y0dWDyC!jVw8#AaZhUkHnV<5F` z%=f`@0%9ZV0gY~gd;+3DW0~lw2P6*j324j~T^^RYUNM7mF~ldJU6mL%faM`6g@FOo z7erS9(hG3`$S0s)Ash2?u!})TI;CmPgQx(FIDt$+mj#J|*0^F{+04j%9TY|&%m^B5!loN!GRRG! z(I7Tv(3k^6gpCG6|brWd(5fn}!8We*dvk|6(L}B5CJB@?#Hb@@>BWQjfn{JTFAUA>L&DfYhBlHjv zHfGQWA|!30>w}1bNRZj+vLG>hX&hAUfz&WCf@T`A=?0k$aua9{gpGMOIBY=vg_R2+ zHo88D9#A_SWH!1iNDS0(LhmJjNty|2AK?U6KDpBjTtmf3=x5` z@wo}TMgysZ#T~9zFe5W)6b|Ah(7G7xZi3Wzpp`*v%%FK9SiS>|g+oFFp%26Yr6&*# zYWsl92C+e@4#WqI2ZO|z7&sU>&`MO0I#4))+G`*+42+<4RUlDx-4K%)7(uh6urvfx z$Hu%L9FHJ22t)L-f%ox2R-l90Rv@#{WkF*2+H{~Y5Tu5I5wyMwn{JTFptu9=6=7or zt#^Tlz}y6K1-d?%o6zePkXl&W;m&uU{0wmuX!RVr*&rE6E@NN>?G$8V2F<%dL}2wD z$R2ckFgKy^TmY$sxrrNN-vel$0x0f4Z8VV042+-^m*{4LWFY$!7#Kn8CSdgmNH;7G zg6u)p2Xhk}T8aUwg}Di|e*k1YhF)-*0Ld^gf>wK?%Y$Sf<})yYR-LdhL((EqArG<@q6cI$C_RDJ^uXK% z(hX}DfK;OEgSiR4EC8v6n2N6s29<#jH-UO8=w^dtAUU6b(HXRIh#9ml0wMy7N02?} z`e1HCA9(|*g}DjWxEv!m{6RJ|FoM>BqnizqfsCUuFoM>ju`$mE=YPnE8>C$TvIkus z%uRN9(vuwnx-4k75Gd|I?J|(f42-C|L_jt{e8k8AQp>;yTJ6Wi3|i9&5n*En&DcV0 z$K|FtJZ_3(K$iur>IS(9)GmX#3ABd+-E5EyBLhe+10!fHB^xtj-vCGi)#Ho7cG476_r`~E~maQK7NFfiiUD*`eZgWLpKWzEJ6IST+3Hf+q0aU~EN!(8z0 z2vGY0WCFS@NDNh%z#;qm=~+pE5D9 zW5|L=NkC=6a!?q7FeB<2WH!1i zNDMSSi+yDb#l{S(^&lc_%%GLT5Zlr9K}12LA0YeD zWkF*2##KNo-#}^@7;)_f0htVP6J$3CWP}_tt^#RuffR58ZpbJq10!hX2OBe}6$KGtV+O4qhuDsA6R4B`)h!?zBnL7ZT^1yU zugnCkbp)wlU<92EflW8aWKcMPc6qQdgVx$ZMA(=?>xn_-BBU1q=_7#Lf^ZY|9vsL% zd}Su6?EzB5zzEtEg56CZdl(o&``KanA0)!Y3^{WF#75T#aVch*2~rD-J6v;UjNn-< zh?`J%fFN{(Oa_G$c>f18(s?V;v2u_~Ty8?oSs=AAH-XmVgKS3Vh0Uvg=Bhw442+|alo@ohK{AjO z&AIFhX*qA|k1|YU0^nq#}PSzC&<5S%%BtqiCJ`g5K#~bG8+=P$>(s9ib1D zmS7mvGX$A{E(;RF*XIP4F(5SzjG&eWHr*hTLE!{a39ENOw!-oqh>flfq6ZY7AhXeB zL1Osg4m2MJQp3Oq3NdWDK_-LT1Zq{XF+0~<4F4=O~2jTzKZfVdc4A4C*Hg3Lyj1&QGcCy-x3 zY8V(nr2;nHAd^9E0?nVZF@suB5D}RBKp}$A2Wsts!U;r!+zv7uT^1yUFPxe|Z6^?B z1g$v0rW<53$W5RUpN$#Ro`8t3F@s7`NGlYf50v^rZUWJux*B9Zx-3WxU%LQQDuL86 zFoM=|VABmU8RRC==mpG8pq30Q%s{rI>x1M~klR6KqsxNC@P!kobOxzmU<9p}!KNEz zGRRG!Rt+07s3!ptVPgjE!-m+7t`8y#ier%3=&~R&eCY|)9s;RhU<9@9u;~Vw4001_ z)R2uCv^yRm!p02hRX}V<*9Q>=ks!0tWkF*2%1ltr3sS?t2s7(Vh8(}I)6jrz3Za;zAXdrzIjG)nYY`Q@vgWLq_ zow6}Q_F#cpUoba;>_O-Qg&oLEAR43-WH!1iNDNs;sbOFQ_0qBF2AK?U6KM8_ zjTtl>3K4<14`e$+AIP;JH-TtSD+gpYx-3WxU;7Ev$_A-nUwz@{5yGRRG!Su$8& zgS4MOrw>BhhprDI3d)}#v(aTiV))7e(2jDD8U{wtNFX-dAd^9E0`J^l291nCMA(=? zBV!QT(e*(@LHQG8Ho7cG3}0CQYNLVFFff8-whoVFaJO!N3R_NoQjQ ztq_5TurY&HZa{2D*9Q>=ksuS$WkF*2@*QZD0HlV25j3NKO*hD7P&k2RTG^OEt6Cr; zF!zD{j?f31jR5645Dglg1(}U53lhUO?h9%Ifz&WCf>y_1(+x5il=mU*zfZPP4LFzzeqsxNC@VN=pMgyr~UZKc z_&q2@(DlLGgnKn1zVRne8x7(n(9AHp*&rE+`3#JZ-5J5)`4*5nVDSiIBlLmH1H~PP z29>rT6VPQrV))8&PznX9VPHhvivcndl50UGgTe_k>kliRKqV6!6KDk>q@Rv%D?}6& z!XUHJWkF*2;tteC1F2zP1g(C+rW<53$W5TNt!&KJ;BW%R9TR9}BgA%eeGpMb24%GJ z3E@*F24xIc(9UE~`2;!_2jV8s>OFL`K{6ndL2d%A=!1=`fONAlf!2INY)97z5e1PT z6VPQrVxaYw=%X$W7c(-0PJaTaVPFKU@WG}VWHQK2pmo`7Oy9xb1k%mM1X{}qkwxeO zG82}bK)odfM$r0jHfGT5I>;{28b8n~ zEk?BS_&{YU$W0)ck%1E}?m(gtQ<)ezamX?fX3&fth|R=M!eGv9&-9+zhS`eQj#+@|C({?EZ%qH0zBB!0`pxu}=?^nEGZ!;2 zGY`{0W(Q_RW@}~_W?N=AW?^PgW)Y?zOuv{}nc0~+m^qo*m>HOvn3HS9nSGgE zncbN^m_3=jn0=W2nEjapm;;%Em_fPUoWYsFg&~unfT5IO0mDLuMGT7>mM|=3SjKRO z;RwSqhItJW+P@}W)o&pW;5_uU>O51g93vVg8_pXgAIcdg9n2j zLkL3@Ljpq@LmmSQ(3{1?D%*qT*%u>uM3{1?@%&H7b z%reYs3{1?j%<2qG%yP^c3{1@O%$f{LOdps&GB7hcF>5nBGcYr2FG`7GscMHUO*B2dmQqtJ6hOVaCA3n80kvz{Hr& z^aiAY=`~mdBLg#I9AhG5G6QH;FK9I%Wc@D#BWQ;JBO|CLW?|50aA)vm2*;rYGTP(? zxf=j{axTams0j={pc|YSV;Hj-7(lVYzz@DjS`de7U!1BL!6)=0T*t(~%$UNM0yPzS zKQ#jr12=;TgDnFCLpwtUgA~I|hM5d93`ZG`GRQLAWVp#7$MBxvJ%c=>FrzSo0;4gb zF@qwb1ET|j5~CBN6N56N3!@8z3Ih`Z7XuSRHUp?X$;9Bo5X8X92)eUHh+zxEJw_Qu zBgPm80R|-oEe0b7D+VV9FNPq7D2614EQTV6DuyP8E`~`AvzYcWfOeotgJ`Bs25F|< zV7?@XX6j^+WZDkqOMqymP6i34tzfl=|hM?ib08K2Utb|M1yVG2Ih+~ZDU{t*~Gxcz{eoQ zAjhD_pvPdwv=1yR4WhyB-U;SQFzp1ZmSCs{^Tk0l*d9>tmyc-|SX2T;gX}=EeLq-6 znrS~+zckYxFkccxgTjFUqW=I`RGR4kL=|X#2!jZND1#V-ID-TO==KaYW@ZM^-g8JU z0+pq_NPH#+DF!a4E`}!nZXW9gz*`_csGJas(#=yw(;tD1~r7o&DV3kn4)?hU%U=l12wF?(p z8>|wd-VDsv1(V>Fj*LcNwkDX=1Cu^r(gaK*n+cJHh=5llGOB{rWP(XdA3;|;&t4zBX7#Y>Uqy?B11C#n-(i}|cf=L}D(hufTgq85q7XFfjZ9B~1nfMh*rBMm`1x zMiB-EMkxjcMg;~2Ml}WoP-z1yH5e@z7#Qss7#Lj`7#O`67#IT>7#PDC7#L$17#Ncn z7(lLL%wu3+EMZ_^tYTnbY+ztuY-3rmpY7$4opuN7?}bJs9vtd4Pq@*Zfl0`MfuSzQKUm+WS3PGg0|Vm<1_p+NlFLdI_}!SdGcZU^VPIfzNh?UtWs96+&cGnNJC`v6@#_)xQfk9#m0|Vm&a9A-j zFqA&}!x7JK^OZrK`2_<51H-kXx6?p$-6G)ARYq)0~Z4W69bbZ12Y3712au)z!r44zKD z3Ji)248NI}dH$;~>aZ{}3M()$P6x>|u`uldg#-f|Qv`zoSR?}EN-$o*6u~f^DdPWs zrU(WhrilMHnIagtm?9WKYF9BuFi0^)FlaDEFxWChFr+a>Fmy9TFf3+@U|?j5U{GO- zV9;lZVBmpbkRBnZxEE6dg9K9qgAG(YNN*rh1cMe+1cNS91Va{61cNeD#Q)n6n!$)E zf_;!OTXY|Nnr7hXqpv z12P810Wt=;0~!Ym+Ds8B*p?~c|2t?rfZ_s%*`V?;( zA~qUiCnzpKeuudm74$acfr0P;VK4T@t>IHS|nOc4y6(EJO^1E72eif0fF;)BWsP`BPJO>K{TI|skbR&s z9u!_6w||0)gTfRf2THp>Optm@4^kh4>k*Kf?VxcAqCtKGxle&9;{Pp3Trm_t;};a4 zurdysp26iC$PAEQjga(!@(9SCpg0Hl4@SfM38O(_0}2Cl+MOwa!37e(;CdP4CRlz% zqyK-!;y#!=sYJv535pYte@Uf5c?jf37!C73t!QZZ18%o~!k<_^D4l@f0~ZZSBcOb+ zktu@V22%t>1yckANGuXs4@N-SBA~V(s7(dRhoE-dLP%SI;Sf^i_pp zGu4?Q7(n^_EY!asb3t_}2*cWeAU}f284zY?ieLa?KL&>X;UElYuY%gF462ZJ9=NRs zN{=86DvxP^L3SWxa61E3_wh4DFo4<}pf)+Oy~m+#&%I0$3SnLF-l! zJBTTQp#xeMgWA3?nIafKX7@nrW>8+JW{O|{*$=W0)Sd>3tzlsJznFpHe=r(Gwl@Ik z1_lO(|1fp{Q^enY3=IDm7#RL(F);ky%fRr@mVx0PF9XBh+YAhUPckt4ea^t}4-}V- z3=IFKFfja^#K7>clY!yiW+*Mq!0>N51H(Uk28Ms<7#RN5Gcf$~W?=Z2#lY~-oq^%+ zdj^JotWf$R)ci|OJ{JSSKVb%je<4uX210|~A;-Y*ZxI8-KQ;!2f7}cV|28l%`~!)B z^nhqN28O?Hpz*5b`Ei z9Kr^ThA{m84i*8Ef55%#KhYqOKcXP=cO3?~0W5+^Ld--K2b+m({_jGtUa+hF|bD&3=F?QKs*M9pQ;f0$0rE=QyfBn10_xflYxPm z0fJ>9BM;0*;4udV1{nqxW-$f^hCYS{1~vvh1_lORhB}7DjCG8Y7#A{bVEoS{&1B9L z$+U;*IMZEb7G@r1QD$jo4dw$Z@hrtGb6J+MtY+EFvYX`)%T1R1EH7EUvi#(;mur=q zC-+l9O~F9HO2I{;K%r8hQ(>9HI)(j;Vv1^tdWwOHaf&k)S1ImRJf_5^B%~y#q^;zk zl&X44U0B^mTk!SsZ#RB3{r?YkJTHR+!vcn-jQxxY7}qoYVv+#6XE)O^rW?!*%xug; z%+k!t%m-K)Sc+ICuqVKdACjTY>{rETiU*EsFe=+}}85sWgGcf#J|F__8++U}^4u5U_ zTK%>7Yw}m)ui9UgzY2flKHq#i^Ks+j1_p*lVUPSCxjvG5B>qT{f#Kmdux}Y2wmeLJ znDo%_p%R1-A|GsiFr9(nuH@~Dw`X3zAr6X5@R$=w4vbsCBqmY9z`#&}DU3xV4n+*4 zr-5M(!y+Wn1q|C5b}<}cfTX@NNHS*_&M`1BI)FqN9T_vgbS7gqn9gC$Wz1u$VglzU z1_q`Yrdp;t1_q{jrUs@)1_q`kre-Fn>zP`aW`p!F&0%0*n#;75X&D0p({d)zNFxK& zN~Tpz;GEC2hJk@;Ez>%%%Jod(T*kn_w2|p7(>bQ|Oc%i77nv@B>B~%4n65G~FkNH1 z4&pN~Fx_Cf$-uyLi|IDg9jM4%rUy{gL#9VeubJL3y=8jGz{bP{5@kdUXNVAN)DK33 zM6qGe$SWwK(a4~Q*@G$T)@GfJXI{7?c@Q7*rY57}Oaw7&IBQ7_=F57<3u*81xwo7z`PV7>pTA z7)%+=7|a7(yAs7{VDM7$O;>7@`?s7-AXX7~&Zc7!ny67+M+H7&;ib7$z}H zXPC>dh+!eaVuqy*OBj|htYBEhu##al!y1Nl3~L$IGi+ek#IS{7Gs9Mf?F`!(b~Ef^ z*u$`wsfnSGA%!88A%m%fp`2j>Lq0oF$yz^fYKH? zHPtZHGVEY_%y59wlBttv5mP%;2Ph_)ni(pX>X;gt+8CM{x*3`ndKfwx`WX5dCNT6e zOktSHz`!t>VK&1YhFJ`s7(*DN7z!A}7{eJO7%CZ~86z1(cQ7#Mc<*4~42ams$k-RT zgYkds4h9$5oeZ1|%<*x$I~W*s6ck)DT{bW#=3+LYHSsmx7x@Mqh54*)$vx?z^oP!5g4(d!9{w5gG;2eqN0N9 zh64c+8xFWgD@I1@?qFbpyHr8JwKh`%>SlJG9Sm$*x;q#+Af^OIY;cg?z!2Hh1u`=@ zLRoP`Lsyq{mvSVGzk}fbngAmMx_|>zUwc6j12)1%8EM}8eFfeE{>|$VK;ALRaX57WV2O1^UX57!f$Dqa_ z&mh4d#K6tKz`&uspMjr2l|ha{f&&7%-?a zNHg$5MVJ}%8Ppl%8N}ftEDRY8p$yIpY7C(26J#_iLkL44gBgPsXmA~D85@HsgEoUA zgBZA?1gT+XP-Kt>n*wUIfJ8VLLKtiqWEsR6grRnEg6)=L5QE#r#UR5V&LG6V%fJCu z!_CmlkOS7K0u|w5@Mf@OFknz*5Q2*EGH5fXGa$@i*u@~gz{$X_y@P=P5}zOe5eCMR zT@3aN5V`GM$99!lpkNPyLU0MVtp z7!nwy7#NFpG4O-v(p?PsP1b zm`#|2nA@0lF+X78Vo_qTVaZ}?VU=KwW8KH*z}Ca|jXi^X5l0-yHI7Fd?>PB5EjS}M zk8qW69pJX$p2U5RM};Se=MJwEZv*cEJ^?-(zG-|9_^tRS35W^Q2)q+i5-bzkAjBt> zBeY5Ai?EEanQ)wNi|{<*Ln1CBr$m)RSBR;JO%vA=pC-W}Q6P3zaaHf2vigZK_LD z*QiZUTc!3#-A;X$hKz=T#s-a7nnIcpn)9?6w0N{+v~;v=v=(Wt)7qtVOzV=?J*`*T zA=*#0KWP8a;m{G$QP9!RvCwhR@zIIUNzp0LsnO}snWA${H%T{7w@SB7carWr-G6#D zdL4RG^cLu?(c7VSM1PBcn?aC4oI#dBnL(35pTR7HWrk{oMuv8VUWQ?YNrrib7mPBD zN{kwedW>cmEiu+IHZyiI_A`z$PBXq?(q=NrWS+??lWiu4OwO6yGI?h5$+W}tgV`Lj z6=qw^4w#)WyJ7akJj8;*g2zI_LdC+s!p6eGBE%xWBFCb_qQzo@rI6(k%MF%$EKgWo zv3y|p#wx_>oz*XE59?_*4mLhE5jH6{1vXo3PTAbEd1dp>mdTdSR?1e*w#$ykPQp&b z&cM#b&ciOm?t;CG{Tcfk_D}3T*#B|ha1e1Qb98eIa*T7#ax8Ofa_n=Q<+#joljA<8 z1ZO^HDQ7ijBWF8jFXu4lWiBc%1}-)(9xfp+2`)J<6)r6<6I|xFs<>`*J>+`M^_J^1 z*H5nh+_>Dt+?3q(+^pQTxhuHO^N{gq@nrE7@Rae?@HFvs@bvMF@J#V6@T~DX;pOI) z<~7OdnAaC?Gw%}bTRuWQ6+Wwcw)pJxIpK53=Z?=apASC2e06-yd>wqfd_#QWd|P~1 z`R?*P;(N|d&(F%w&F_rg4Szd-H-A6>DE}n?M*&#@Hv*mndhg1>}hgzN}85ON~qLMT_LP^eU>QfOZ2uP~;ttgs{DJmDSTFCs!BRz>nf zE{c+gQi)m@?Ge2uMkmH3=2dK3Y+h_t?2|Z?xURSxaS!5N#C?eS5zi3M5&t8BAz@7- zSK^YSx@3*yO(`lVeW^mJeW|ljm!*ZIC8V85yO4Gx?Md33^aU9~8BrN&8ATa&nGBf) znIE!LvLdn$WQ%3b$x+F9l)E6$ChtJLU;e%Vr-CDe4uzYF42pV+J`}qZuPWgw5h;l& z*-*+++EHd!wy9jMd{PBlMM*_XMN7qmia8aRD(+Rhs`ysPRLNB-R4G@fRcTadRq0k4 zR2f&9RasWqRM}TKt8!W8rpjHFhbqri-l}|7`Kj_>6<3v5l~R>nl~vV?sxQ?H)jZV_ z)hg8n)yt|kRqv}lRei1cQT4m(Uo~tsLN#(VS~X@hZZ%;wNi}&jRW)rjlWOMGtg6{o zbFAiC&7+!kHNR@vYK3a$YPD+3YMpBRYNKk?YKv;?YWr&E)vl`DR(q)STD5`)xzz>L#nt82)zx*?O{-f}x2|qi-Lbk$b@%FC)qSgHs^_bhs#mKw zs<*56st>DAs?V#hs&A{GR6nnNRsFX5Lk&R+1W}KVyQ#gszDalZ+;vnml2O*p#R#XQn1iy)mt9+PmqCW(dt_nelBV z+f1>UYBSAdy3GumnKrX*X4}kZGndWWHuKobYqJ<;naxU=HEq^~*#-TwOVbp(Q3QZUaP}aC#}A-`o)?FYZk0o zvu4Yh18dH#^;?Ynga>kh3qSU+$5r42b7?rex~YWs^FHan_zoZ6|jbJosJyWDnV?V7dg*KWPtA-k(~Z`gfr zkIbH^JqPw0>`mLdY45*%5&LfJx7a^t|E~i!2c{gjc2MJB*TG+hQVyjYYB9;IfpMDzI6EC;eSU|j;I{5IAU?c=ZMddjw2mM79Cl1WY>{hN3I;X za^%I47e`f&svNaDYIn5eXwA`WN4Fh)a`eeDfnx&4Y>wF+>p9kQY{9Vw$BrC3a_rTy zSI1?J%N+MQ?sYunc*^m%<88<19G`Ri*zse>ZydjI{M+$wC%8^>bCvKh8I;nNiChSbunOkRWo%wa<*IA*nLT3%m8k{XTTXJ^B z*%@cIo!xf!$Jrm}tj<}Tn{{s1xjX0XoM$@EbY9}T#CfyxX6J*>2c6G4pLM?Ce8u@U zAb7zF45Kc9VATZ>oN@sK4_pAjM;9Jlcy!SO4681J;FgOZcJ4@ z1RE}a;Ic~~ciAUAlPvg1UFpWaCO5qE-*B?27*!7KycDE5ZreS1Rq>`aP7f$J}}g} z4uS#KL9ppM2(G#gf;X<;xPIdX7Z{q{0KpYEK=9p-cQ@YMlmbJyn;=+l69lK-1i=G0 zLGZy%5d3uW)6Gw}EWj|~76i84g1~*ZAn@BQFudIbg%@r^Ap;X=jR=zkGY8}%ZD8j4^CgAJg#onSWySw5jE9+aF|aa-GdS;JU;=e~ z!FDhS>|o%B+rb2~gNZ>7WCsJtQZ|qP8-svdoW+26hI{FFP3c zEtdXl^KoMjoFpKSUEROs8K#!sLr|geKF(4 zVrMw_a869l;lFN7yTA+vW(J}E{Y?JM+6*cT+6;yai+3?7gN7Ht&QccG!Ju(r2Lp&U zfcu&c3>bL7>|kKOu!Dh5U?+nn1Me45FlsU|fDPso z*ufxh2PC(H0pu%9fgKDI7j`j7G6*slfK@07>|jv2vx`B4fscV*P{?u@gFb@|oFl*vVi6PH`pzI~m* z8X5x!hN{302E7YA7(lcwJSGG{!76|h6Uap%I72IdR49M~9+W9TNgWi(91NNa0$_6m z1a>l5fdgKGfq}saET#aD3~2@_u)19gG7Jh}Sx`cUsN2Qh2sOpPP#hA#=6Z~#CZO1m zV*-_S@{H!U_-Xe2k(ZV!}#nq9S7QjCPFXc8sPbYBTKv zMD;w3x%s3`d<-=l^`z3`JOo{W3}me|L{t=|x!5>dI5~xd1lcV`B>31EnHc4EDarEl zF>9)`F*7Oh#6;@`+Nv=txG1PP8OTWLIjRTe3!Y?&w+=K^6;QU+bO^0y<`xxR$tEHx z$1fqpsi4C6T324dUY(O)S4BvgPg<3Mk&%sYKI1;7Ck)ICa-e}876w*9Axl{8!UzgU zMg|4DNCQJgVPj@SHnS!(#`%n{|8|4MV^{qzX9{8Z!63;Xx{HAaG-L|V%c{SFK~&$s z(A>n1$=sNanV%8leK{s^Wj*FqdcNtF{bupr#+(AoOj0@)a?NsP>f-N%XSe66sYNE| zSi}p-2pA^E1jxw+B!XsS7#XDgmopxL*&~XuM?`-I1Dn2qA)^Q%6T7kqG&j0_qK8ca@1DhzxKf}pW3esDTqfY@QKZmw=FZf?wOF3v8_u0A)R zbDsG;xjMN`qFWYxgihk+LqHoF;^ zK;xAREZUHI15_G=DqF4#28Q;Gpo#-hVVsp;FS1U)PJV;PdU?B@B|CSP>||hMNc*43 z*vGJzff*FuObm#)(%-?r3<_^!X64Jfw->Jcw+FlywwvKK<9)_*24)6vq`2J;9?xT7 zV9^HIsmR_f&S)$Cx~B(J&oVMB{{Mxsf&nyeqynlQ1Q-NC?uJ&Ch^)4gfrEkTi-94y zn1NOH?0k$2|BbZRSk2ABB#*ibzlwqZgk)f3VEkXk6wdSm;(lg?`x*5?4H*MNaYkcC zM#c+m{|vgAoBn|2`Is40{(oT#U^>Pi$e_t!$xye8K?^jr4$iY$;Fi!11`sU-FCQ=p zW@JYTf$Km}$twr0|Fjt7p!J`Yz9TaOKZ6znW9&`_BL?;_j?4__8H^YhV;z~nLtKop zP~XBbJE-inV}hhPHDx7G9S2J0V&EU+v#Z-NLn}jm zMnxuue>`G;74;Q_m>C(hH6_*M#W@;;44s0Eb1UU7gN&y!x^(Q8&{9(qVddqJ*AS3V z5jIkFG?31Y(DiV1P|X45U7r76!0tC^NZiH10GiH0a=#GB{X$6YR|W|vgQ}Zd45kc1 z49Z}an}XYqpca>q0BDW^)Y_6_U|=u>%PI@(WYA&Y{bFFq2&%t9l>w*%2m1tEk?=D@ z+D+h!#9WTi7^X~{(VS7u){>Qp(MU_nTQNx7#LipKB-mOZ+?Ub9Mb#tA)il6bohM7! z(c098osrR8-&>S%nU*vsH@l*ms&0&=rmTRtj;)$&v>+>&m%U-It&*6kfwZrNk*+2y zAG?ZX4JbV^F|aW(FxfEeV&G!nV~7TY0;DwD!N3F0X{;cRvm%8ABBz0h8&+t}V$|OW znrZv8gMsY=q+`N)VFv@Nzzzo93kHUY=8EQwpyZ^?u4*i-%%~;y@0b{4{ATY+;YhK6 zclYRXv^6nq`e$F<_LEVPfssMz{}-k}CQw5z9Mmq>V$ep48x4?)G>}{bjVu9B*F{4B zREC1$L772=K?o8L`k;ag7E98Q7*S+52Bj!aJn%6gM}V2Bi5fVXgcX^r|Lry5;8##L zQq*)cmR0r2_p}VQR_7@da5A&7=U_I{)v^*Qi(y>PWO-joQ-)v4$V1OK$xmNg)lk}7 z&BRJqNK8XVQ9yvPn}G>j##u1!VvuFfWr*L!paz;$1UpX+T*AR>V?4PSq=^F>-fH?g z8AQN&hMz%=K?H0NgTPJ(P>BaG!a&tByEwv|pk^e@d!R-NqfkkeSyZvzW%uGpM_~?L z1x-^$4JTbmHLqN2V|xx}V*@bB(~#rSTa}`$6BH6@%q*fNDQ4ex&q+oTAl1(?$@@`i`s&2@L8C1q|v8OofiD;7Kfd5C>FS#51Uastr~K z1_pHo0gz~sBQwJVh$u6|0}!JS)E@&iOW7FI!TqaU3}R3gq|pkBtsM;f0y`Njz^M{k z(VE#a>Oq^EpuRP%kN`y&XVi$BZQT4P^iSf3Nlwjxgw$u!CP%kpn#l1K*;NRrlc1SRPlcdJMn`G0T0Ng9%{8E=8pwRSLix>mFcUj|9k5E3+I0cta? zXW|!915I=aurmlUm@|g{{||1jLB#nP7+~TIj0}Mc>`XRHF%0aWb{uSEhhY~3I|CB~ zGq_cz%FfEp${r}rctzshUPeZKvA11M`{#1ZYX*m}7DEe@1LHmL*pMcusO4bb1P2wU znr6Si0BYfZra@V>cQddua5Auf+j_$0itMcBitGU*jCVx-{sWPWStTXUN=iU=o-P9$ zlO)VdjL^|0u$x%GZZcOjS2bs4XLV(CVf^<&jImSvpZ4^*9CMqWc0C7;C1w5p!eqvD zj6sG$kHHsIZRmrO^$rFufgKEL@HiI+#knw2oXdg)WI?s(P6jpbSP}yR+m{^-!U8)P zxWKUsYGIk1nZU|kP?;qL9#X&@X*wnu8pX+^ucT%sP{gZk<7bpwtZ3=0&65oobAk>% zy_&(IXT~YWq@<*$W~yMJC7BYY=WU_F=Kc*j4h5yaVXgeXpUIc$7=txK07Jtr1}o4~ z4RBan3G86-2l*=q)OrPV_>qU2f&@V0al05q8F(0i!0JUoN%jJ$&f;N^28$@`?*w=0 zAxRn3&ELf!!QjSV1D53x*vX&=?)o!A`>O_qtjeIaC8)!1Yy_&F*w~qs&6PpjO*KfG z7ZW!|G>jnKE-`CHNcmvTXa*`E_!;?_L_|dxr^8K3#i4W}F~+#C0~K@j0I(oOwMfS2FGT>&B${57a|Z zW?*1)U;<6#%Q9#&OZf~XO$kmB zHf6cvj9T)#60E}PO40`y7#WQIe_@JYI>w+18VQyKEptMupYgOjAmdoV;64P%2Oyuq zTN$8KD$cIQtgOTaO^n7yc1-5te2k1r)unnNjs<`NnT zQe5?#^*p_`X)7w6Kq*UBQcF%SDI`}&MZ(lZSV3D-Eh(LWi9wQqfyoElMipc52IVzz z1_^Lbih^<r|k34cB&ZAN&=Ycn$Uoh_@@39So! z{LI;hPn4NC)6th{S4(aBk|LW@-@rN{6~2Ohpp~?Y3~CGvOp)NeSU6^14BFj6q!Vbu z0k=^>Ssl^~<6vNcw&ytXLA@|g8x<73oM0Ck3oEk2pjs*8K9PS{q#5Nz{ymgo)D~gd zRkf&URTZcm4{-~q-Ngwim03W`-jL#zY_}K~!d<|uR?2uz$eyiikp^6DCQIddOf88Iz+;e^06Awv;Udna9k%zr(g zdQ^mgfhh$X_cIZt8LR~lEwm7EkCCyMK}nMtnlwR`3SznfRME3AFoP2~BWScD9yFu^ zYH)&6NeOk7$q_y9fSH= zpiu)>ft?I2;3^C>&<<*xz{e2G71@=UjoB51rUy(HVmt(<@)+;s<^5BFjQPp_|H9-B zE`RMotqDPJhYc2v0-#V5Kx#!Gf(_DY*uelAmYGdiNewhT0qR5xE1NU92dmu8 z%x3ClQZdjl=CFGu!nEt3z_r{6DIIGABgUdCP&`UAFfh$#+Qq;NYQeICmWd+y9U~q& zK@Q@?>L6%h<^`$X1x4&G1~$+bAUGb`zy+0oAtPvh#F*LGp3#_5Sea2VNMTCJ6eEs; zmUaLB%wEDM6}_Kn*T1Wsi~gPZ2kxIUG8i*3FmZzC?o2^5cWmGx8(8?@$=5p>n8Bd} zihMRbT;LfgKD? z7eF<}4hH5sI~X{@-2uo*DE9>eLq>CDZboHxP~sFfHU}lnT^gMG-8dyp8cJ6haMrCT zZD2G%`fny<`q6(UnRdN>`_~OT&WXr#iB!&WkXb&43p*INK@+MM3=D~yaI9*q)!hTw-kods0`2=bq9k8yk3GM=puc%X(KXzbC#aI~BBmlMEwHcX(jggu%s-=JQ+zlj^9b+uZwe9_kjW}2t8C7-tn084U zxoFtLJE{Cy{3x~wC7P46u1p7J;1%3g8^Xs-&h5?~X$v zg&i{e!^{lw7;5B9Q_~iVjSW-wDF;W7v}L57nLwzIfta$q>;y;*K_lv{vP6$yTNW&; z{3ca~3kI=-xSC4H3$Za;LZb^xLHZ~R3`{{xps<%_C;&yD478aEn$(qohXckuJ~Sf1 zc@rEk(CjQ=7YT}eCI&`uXiDpYrujhwXWXEbyI?UU@O&Alo;6m6#wi;+C~tsUOdzjo zGqP`;1c}s0zxX-64#{Q)>@3V$s=-Q3yQB?Wv|_8o{=H>nDN)gp5aL&sKMjggMh4K> zYY@{h1}#vz3tDLlniz$ZYhs{6REz<8j$sFmTQuWB-u`rI)P&YSFR^$>8jCC{#u~9J5i&RiH(vnl)5mfCli*!&1msh?F3{0Ki zK94OZ)k1pjFh63{tI%iz#}cGQ1-Au3RoeV`fkT z3sfqZnF})td$KaC>M3eT^09eMcIQ#FP4u=4H5OxIbz$1|&r4c`TTMe$%}j}L_rEZ= zEDsfFLsxAnO9jy3D2xoD3=B+DnRYR7GPr=sZ!QMVatl~^Kocp#S?nNZv13gHI~Z6% zlN8Lx!s5o}!p4jh&`d7!m!1(_@_-vY83}Cl& zgSM_9xgDd9#u(Iu1|Y<#pf)DdyWnik!oUyC_Miz}PH>}Q7Xt?a541ZCT5KZ-8nsu} zXH@2ARAyv0mS+UDT9}#CmKrK?IwdY4s#bH`wq>?^9R#!R8ELxe=7PSitK$Kus|Y zaHts=YBHKLDuTwR6&Zj3;}v6U{&zuy@xJK4T1Mw4#(h;ypzWHB3}yezn0%OiFt9Tq zjpQ-xV1Uetg62zLZD>&Mj}^QEL|hosdM=yB=pb;q^t50Xb5qh^rzDV{7XB||JP4a( zMVcdqm}g+f%4{sm=(VzZC2We7fssM}e;MOfup3lCaRM)o*!6cXFzJI@jG)#$y8x*F z09t{<$gBvO2Q+8=I_=*9umhNW{HaS~+Lr_=!<88RFgbzCH#tx&!Oa8LF^F(AFl1y_ zW>#l37G^x1#eFP~;s2agMg~Tsf2T|syZ)7d&HDf4za!)0|2rUa*zh^y-QfL^jNoxi zc5_8`9udadBKynB!Fm`rGg&j%Fsm^zGw47%P0%$B%%EPAKWM`xsMn+qWAK9+28P1s z;_RDie8ZVuTOUyejcKa=|H99cW^-i8p;fy>37g98MrqKDr`aBSilCBRxS6dzH@m>3u=mnhLOgP;eC+B5W>#EWOiYZj z)pCX^A~L34y1uoMhL*+&fB&RPiSrpN1Z(QpTibAo^Qsy#FfoY!|H5R%^aLCRjiAtk zg#oBLCkYP&Xy#CWhXFL_BEkT%%nlj`$a8a`8F`TJcQAlvOF!hEj=4-lp&|CXW!MJg-+ zU;zS2SC;?3Fv&9sFern@$Rt7A0g=jG3=bnmI>>+g;CKe*Vvzs9Z4XF;9@g4G9U^09 z6wJy~^T>9Mj1w&9R@2e8=Hy~x(lio~QPh-1gSIQen?C~ZxDy2>S5eR^A85=#`)r7~lK`oZ0JQ|5D%d~* z&=sH{-$44?pcn)t5lH$4m0`OWgc+2<%V-P?ApHT zh8Z3H%3vm9a2wg;{}(1TCIJQ|23>|S&^WsugFZMcBm{OaD8s{oU@U_C2$`Xi01rnA zF(^ZO3qqh-L45`x273k}2Bz3u46+PDV5yy;2|w6c8Bq3xb{5z~L`B5FZ7Fd+W?|3} zsU5Sa38Q3jo{oQktGcU+oRoKG9;2SAsIsD>NVJH)jSs(p3m>zqzd9FBX}U*Qh@OzL zsfxUvu`&xAJByYMhp3{IwXCta7~@t;XFe7o2^k&}P=#*qoeYLxpYk!NGZ=yw z!74Bqf_qyapFu~NK&yhVl-GQW%-~fVh=~}`qQD0pDeB=8w#qU($`Ty>YMyj-2>34dGR6Du2^xS*7pAh$fL?j%s@Z0{kVX(lJ8r}FPD zqbi@QFqfDRw}d#qslva{;Joef{|l2k(|ZO%24#l)T?|6dk{MK)fEJy=d=WIaB-I>86 zpy6djW^wRpP{v?gRVHRu9bE_&W@pF3rZGpD(ZE-*C&1P?lF36^id9fZK?zFzUE^pj zs33o>=wBLZzq^~I6$29k%YPjvPNw$^pb{(}G%}~mpaM?gp!rqAm=e*MmmAu22GvKf z%*z9A!|q@JWpY7qSV%Fje*qbaL|c>SC@femWM$Oyt)iDhQbDA2{6ckSI>Yd3y3Tt zErV8HKnHsml|kb`pfs%pPtuboS+YrmIXSpRg=_H#xtS=qXc(9Y8b!q_D+sHJ^GmWp z(zTVZyRk=MHj11xbzc6t#Jz)@J2nTh?Q2VIR zejXyfLIVh#8#x6)eH2jP1&UuDu)ld3I2k~NGpGuHtQCT`LCio!F+8pE+1aqL1`xa_J3tuHb*+Bptr?op6xb!6;tr1Xc?_dD+T6Dm@CS_q` zc4P3i6v+B6&~j~NWpHVv29G%~7slfWE8}Mqw9wShW@Y7+kW-b2U}3f{vt(l8=i?RU z=HzBI0dZux#d)~7nar42qI4~djm1=@1rh``h5pT94CNH#5)$PV=2w>fcLK!YR^t&7 zRO0&wTAs@QYUhL2YzjciV_62&@|bWKVgwnqDhHB49RrvIP+TQC_x`+AL_Z7;BXjwE<{56sQbFh`_Y3{ODY%HY-#I3Xj~GN66{ zqy>qspCiVA=;uJvC@6|RYY^4cm6??pp`J$W=VVX9(%&xLv={F3F44VJHFiA5BFzABXb*j+Sqp%bU zjRWk?k`UO*AP=q`_!#8Dr39oo58CAh8VZ8#^ipMD2X|#an_}d_%elch5@qb18MQYH z-svI--u%VL*%H86w9Nn87MC= zD=L z<(SIm;7Kg-t_?_QkC{=-$(ET#wM95sL6KFYL++IDDFcx?9ySUl`g%NWR(!JJcZ#Hx zsAy}`&J4&H=P1L~MdfcxX%eM+F+nxMgaMo|$tCeReQ9ius@YtER3yB3%fHVIJ; zFbe;>!zYJml{51G{R5uo0PWq-WD;PIVF&=NAeRL%`3pAYwUg-f|)4?d`$YbE7DI_G&BqeE;Q0?eo&&;eG zDa^$3FNQ$ywGwA==!xCDFpkQczO(SrKSQ40x=Mhrt}Q%1;b!%|Ax-1!Krh z9poEzB;Oz>FmT~$08(K9ZJi2$X4F6_PVUYw1~Uc*@Q`uM4hAz&IRM^SW(JA_=xht9 zsg1<7V=^^?dK}c>0Ihg6S7ysG(q&~aE3;){;uGYT z2x;?)$}sXXg4dApF@cLFaL){sY?S#Jvy8178B6#@1*JszI9M1Nd75qA)m&7~OxVSo z3=ORrO9Vxv6h$}~8F|`ljr9Ul4XoHjU3olQg#y_B`9QBvx9*bnx-IyHDqi8SH`zz zG-l>wya^wYP?F%_LzDvAlk|`~=USkV3CK7EuadQX7`$D`$n@_ma)Iy%G$aAty8#-f z(FCO*X3!C8Na+W|ztG+r!oSd52(C}TSqhP$U_EpWXrFBd1E_BY>G$w6h(J>jzdoqG z-~sb?F(^XC!8MsaXa$bBD!6GSs%&avW^5!T&j{Mc4BFIe4&BAfI8Vh_GnIv>R)o<( z)<{80EhoZTKiPzdji*-R-<~7fa-sZ!yh4hGJp7_2%$^nh#Dx^KIm2UBOqf_bEB^gc z<&994memuN&{Gx#jmISaFJlT~`oZA9;0~HBbYyUXga;@M=<7qG6EuhhIYtUHTxqWl zSzQHMA;X1@MsVwy9W+L6%nnNY zCZNT{;3dP_jKz_bwjzAu%*@P;9L~-g1-G-Y@bYkrIJz>W@d%0Y2+Ol^Y!}#IZN$vZ zDkv((EhUf~$(W(2ASoxRqr@sIF2ZVHounemAt@^&q$Q z$|kEUsU|1REhWcq2+0F$|CcfDhs{;N=XOBV6LdeIurc%6X*JUzQ&o%%693DX*umii zUXKP3FFpMo3(V;KmKTUp2Sk~klakh6iDiJF+G0Ei`Tq6R*om5HJMzZc^wCIRrd5=NjIKnd_f z4rC2B8)!ThoPD7a(=wp)iw(T=pBa3Ngt$GUu{b;CAXC3mf+>#@H_BT6ZKwbKJGf6ogEC2&H;F_33x98Xax|cn*dtHyMuvGe-{G_==?oFAxl_m zMiqRx1n7i{T@1Pm{NVMz(BhR{8C1&HFsdt?8$*gGVosT-;oI(i%L%f>uGj zB911k9IUB=>LMl)cFIn+?nxaIcP;fepNt1T-=Qnz{#tA86SJ=-d__=y4(Z z;Qqx92GBAP$YCL%#kj(dZIH(7ifStbRtVSEO^f6>F#lJB@JfM|!vB;CQVJN${+(qs z{?}EIQUFThi1L%2K@~I#2bpOH4SMZhU<9Rc(5yd;KB)1{3f{%TC~V9P-pJ2xQ)6?@ zrpAVG0%OL%8GoNKrvIA>9!r=1AHZY`PCwqDxfFNMA*JAa<1Vm+K_6r$Xt4!2H9_Z6 zz@Y z+|bY4J4)W%Ti4l{KZ;dWN>-hfn~_mQT1u5KkVjtASV-M7!zrXE#R5i~DcOgcYN~SU z1ly}2F$1H$bp!2Ht#viUWmRPz*|^xm<>bV)MO0-38JHMmGx{=qV>-s5z@W#F13K{l zwwDfc(g6!74=R8{5wvs*v76qJnW3IR0krXync+W}v4a7$-;(9R4hGORdLag$FQ9r* z0i45jFevJSmdb;I9<-!Yi-8BU8QokNv{v5K#LOCcLpyE3vUdr>)KG z85#4qI21e$Y+@rVnHd=sWE3=o3`D(bodTHDnibTTnWV*~HCWjhL+#xSbnNYgSUFk6 zB_u`6rPa*M+4(u2%n}pj;9wFIl# zWiV%wWy}V(CqP9Rbglbt24)6M(9D>&fuX9oqPZe7W0AtY)gp{%Ow13I`~w|~{r^7$ z^b8CkHt;=gEDVzvqnMbOjxm7NWg9ctGK%bCFa{mc3{G0c(9{Z|MK0`M0MUq(XAp%y zXo)HJ7tnrUS@0B5%?<{2eF;ZSh6N1j3>z5K84fV0gE#!LfO0TM5WGDDBnoDH0Idj! zbwt`B4BEW`IvE$7_d$Dx8H+%}TIvjTV26o-PZI!HslfoTatDKkKDap0U?^bFU}#{_ zU|=qEWM^<-&|nB)&|pXa34m-@V9)@cX<@?v(r(LO!=Mf}(pG;bgE3f4pTQQoZW^>3 z+7Ps`6eB}{h9JPbXV8d)sE8QofGbE_SX^0M+1v=cK~tNNozYC$Mn_WK%v%RaYuf~J zF)>@%K`0#y`4|N`NfB9FQ89TbW=1gy4#p^PEo)UxFLOl*ZK0syWX&hUZsG!^Om@nO zODS&!Ey2w3(g@ES%>#)>9Uy7GbDzUPf&!1m7PUUP)<-@!_vxB-$6+O zTu!WJbY^_TbbvvZp&HcwgskuZ)vTbB5;_Eoa5r?oN(8jnU7JAwybIHjm7$(No1vdU zn}G?GKhHC0Gu#Jrb}(oQfKnEyzyu8#bApRg&=J+311KP)R?3j%0bb+*%`9+_fsV%# z0Tr&0VHik$0v+bzt{~0C$S5fyqAF}A?PO^0rm5!~>|^7^=fbHdE2_ZE$Y`j+%cLXB z$;!seCm%t!t zOrSyml!?Ga4WzwcV92O$F3#v2R$RnnrMJ(9f${(M{}xP6Oe_pCpz%<4wDEOllp}^_ zpabp*0cghpF$;z~HwjKQ&>>Xt=q_mV3bby69lZ2-2ZNA4XbfBoJSu8n$S!Qm%qA)V zs;8kt)8>#p&+5wJ?3@*ojF}xowFLQuSqvtHO%h=fb1@cC;$dSGIKg!0-vtptFL4n; zElEa&zkf=sT?HkZlR<&N;QBv@shVjwgCK(mXoeC#w+4f0_O-&bqr~NL#hA)uq!~bw^EQit^^(x z0uKj4_hNzCn`(%S&WsMefwE@424GT6QJ0O2kx@wjN;CQfxf=&rD?mseJrx^PZdN%3 z7!5j8LY_gJNtN-dkUCSx|2hT+0XC+NLWckU|6gNZU|PhqOGuqr{C_qBgMb=SC+KVm zW)+5M4EW@gP~;B?sWVGr>SthLkoj-N_yRmKUmGaQ&C zpo2e93KSOLvjvzh2&plsfI^Vj9<*KX|9{At14!a_NaD`SyW#2`ki=b?`{CjiNa7wy z;xRmwM3{3z3GB7Z00PoWh293@zFhE8dK>amXPC=}WhYrXB^N%yYjZ=?a5;b z{&$4Y22=(a{nr7X8(_zf1zLt<&)@(lgTQ5|iN5qs1{0*MI>=kPK_^;*hD;!*T!PN} z0I@)hg3#i396`53h%+$8?qJZm13G*Z((X0@4cWpwZ;l#FZ>gp@@Ur0a5ROA`Wr~MBD<0xD800fsw(K!H3C&NsNJ; z0X)owdhQ)GHzArO(27U~bfBdwJLE`5(0PuGS}T3ll*gB^^j=*ae`i9(r2fDO6T&A= z0NrKA{Qv3yy-fW~e;Gha{`DE=gK885@ao?k42sZ^JXKJy9GvE%bM+W?9XJQ`3V=qB zK?A8wpvz3agJY~v7Np=*1rH+fGw?Ei4$T8KieM}`xR^Xt2dI&&2p;3##h?e31y$?n z?CS7iNkJ=6L8D`QOw4?YvW&u@DhhUnh%F!}6Wmx{z>v=S8MWMnjvjSu^GF+MB@#QS$f7MxbV`wzis!UB>e z+#zWKUEB^u9F!&?>K#zTL1_XaZUYqu?6pkr--D(cQ%V@A@6Lif^BVX`-il#nUO*8zXg*W(-Q_2 z&^##%=+Y^qG8MY81F`NC+KfRYa)dXabqI7O72FsHwW&bOZP1JcY~__AgEBZ3K?a2s zz=OBk42ld2;K~CulL}f016rU2nNeg{1P!%<+KHgaGFW#<8#F`4ESSI@=HDYcS(wpF zXqB_KmS3)uo3N$569)^UrKN=M#I4Ru!S+D~1(~P$g59j6Y~>5Ijl_9b)sza>{mc3P zp8>q*idjuajll&Jrwq;tkTioXUWy_v2vg4(f-KJT6fPcxU7R5kS)ADku08@J&cOWt z$Nw)(kxT*%mJDtTK@8t^F^Dq+GlYNxMqFSAgYAVK3?SMYp8uhNjff76W#<_2V+cx! zhLCj-pwQ-JV1hQItQeTSfTo6d!3Pn8PNH>WWl&&XV=!P~1Me{rVBlqd9Fr!X4|1Cb zbQr);U?+nX*cfR6&=4xf7-a@6=n$&1KB(wWX3zrrLl<<6uE0(PJMciOIfE5+&UG$shz*o_zx6o)RY0H5y-9&!iu(ZoULQNfNswukP?0u7A7cDShJnK!~J zu`@ABGP-~!ywpq-_#}B8Ttkz!gm`7;RF(A{O_X(%b-b;mtqc_zx%nhH)pfNL)SVo7 zWpos*ts>J5l~nlzn54uQv+SHW7zM=T1SLS{Nm~Bb0q^Ou0i97J2)eKhDX&3e3K5s4 zpqMg+#1w-7Y=elY0O*7|&M!L{Oa*o@$XtNX3XmR{0{8+seFg<^56t+^4hG#jprg*X zLF29PDKOOL3^SqT3}ZNSSD20!@~$v(12cPdZ0#Az$XGRxY$gHBy<)0X+LA1oZ5ap7 zf_SIW2xD+L4&FNj&XXRHJP94IMHeqc64zn|x5#t;HcSbn#LYagh07^^76N;$ZW^;tZJ}anM}~|GzLd1(5wgOU~vOO_?fM+&Y`$D=m>mJ zWvp%tIvrmeyo48;iJ+%;T+ubx3odreDOPieHnnsWads;8`l-3U6SIE>#t+1%c2*dX>Ma6Ya-4m#BFTzca5F3nGmnGHk1Oj zK_UBoKxJVdD0~@VWg&Q-HaOgaK;jI}(D*u3i9?;X&T zDrkQWXhcBl4rui%XoreDqar(GNhWOY1T;>K*~zp84R-$fsHMWp#HynUp@bn6l=r=) z;Z_D zfTu2uY8qNmfZK(jkp;+pLPiD=@Jibq;1x_t;1tHizyX~g1zl7Jx^V$?S|~)_E(Uo9 zCGftTT?`5g65#0w(8NECr3IBWFjO~(CQDG05WEgZSxKFp5mYMkF^Zb2fe$UQV>D$K z6AG6CEu;@H3v_|A7=;XMIe8hG4~PXD+CUi0kwz|{MfEB{ws02XT`My`Rw1V20{`~G zSl}|m0JOdpoYo>BX-y4MucM2XqKJdi8dyDJ2(maht%1cEGNIz&^>kqKW5DK@g4Y2RHPgiz&dZDp33px2_*!|1$P<{aE)e<5<^^v40us!v6pN z8Ngu#4m(H~WrD&8Iv%3MTmc`iumX)&z}jL==U{Cjh5}HV2&z7Uc?Vp*6S8_{KA3vO zFpzrC8IS*Um|Vc;kSKyq+hBn1blJfm56>OY;70VCpoK9wcYuQ%I@5^|fQ~4Gmp^iW zN8>@(LC#lH&<8CP1`UUR&f<^Am6lgW`c ztI9DlzQqh9H`E;{CidVH2mb9w?7T3ygq%2l8GdLxQWE|pNW{$HJ0hvKVwQrd4~41+)xSQ>Aoc1XcQQMIrj5Y& z{Cr_L&h&&qj6sV*k6{*Q$2e@XRaO9U4G)OcgQp(o*aD(Zg02Zh2w;?B&}lDlVgaWf zXiftQfQl{9Vm8oNp(3~v0adfG4Ff`;$_>2R0lXp|vUCD;U=8TZS!Q!^zYjTih=?&7 zvnx+?x0N;Z(ls&SW;5%q=@ww;k&shW7Zl>+m1Y%Xms3(x=j3K$mSpT`jP&8jjfNaP z0ou49FRmdYP{_r>qpwh-rfp~D#3w5xr^!?gxzSb7i=BKeg{^ck&IXmv6UfDsU z4D25y$5gHLP#btfgktQ`#E`k=E>K|LH$XIJ{d4hE1j<-o(o ze9X%5331TDB%p2mko_>q#GdXT-6Pb)If>Iu$SjeFQyv!P7#rvO-JJ?6DiE8tKy?=A z>{W1G1{o){g62(dUjwwRKRRr}fK^I$qu7`k)Ktb~&BDG>{?2u5Z4u1E{a+vWK-X1O zm}NJp9n8dF@m~jgu7)Lp3&Yf13`(FY&cW#z)cUo8r(-}$TBh5gcNuN0kWbS);wSawTkUPr*XKMx!4&qvWjt_oywta0y&j~TS7)f z!d^#25YlXrfF00rMO#bFN|1$-5$&K3Rt{E8$Uz+fva&M#8Wv*0{NM%!Q!V1m4rn@# z0@oRkbOx(qKyF3 zc|*YI9-`g}MLnnu2T>n}q}~^t?jh=}k=29iNQnAynEHR8nS7a!38^uF)Uz|@FlvGO z0TB0q>PU$CAgFrKJVOfD{n{XRGQ;L2Ky{}&^Kl_H25-tuSs>vqK% z*6(8Agx?1UTK7oGeSpw?oQQ;oSkenB0Xd*2?uvufPuyn!%|?MQ1OhQ2Q*NMT&@fdj z@EZa_TL{?&P;UwZEddq-wM9W^h(e1=b7)a1Y_81C2s(>5)Ku7ncM9JYzA3z>!X}|p zTm`oXfX?M*{9WK57Uo~@kCky=N(uvL?hPC^Oivgz8J2=p*`wZt0<8%VH3zgNL}Wot zP|cwU9n1pN9MB$(fL$cG&fsGZVBlj=VBmw*8GH>S2913{=D|R72atI!*nAauYzf>K0*@6jMuEnP{(}}yg67je zeIbZ?E9m?dL_Mf41Xj;bfTTVKoR1*tosiXo>kqJc#xRh2(3#5rb(q4Kb}=Y2q=D8+ zpzia8#u=hMKq>w~QG}=h6G6nS|~oXHI9Uw_kncy40!EiB&CsFC zIJkMr4L(R0bgC%w3Ehgu%*ss7LBg$ButU0yI0OG3o8mZyY1cpD!VK_1-9~1N&;EhW zIb~sx`>(?k%k-XsnL&U-g~0+e8ppx_ONSu$fkqTTv=a2%RtHHwZeVA9SV)Xel*lPbwz^7kJ?lq#Fe}Lq~=IG+rqvu!8}#Jw`@g2ZI4VZI}z#^BGuAjr;8${@wCje(m1G~B}p-d_U=3*&#EL1(iIsWIq7{LCl{ z4vX-ApF!vOK=gydhz~3da)U8Aj3DZR8QH<&p!0t!!Q%d4bAlOt!QvqGs!Y4!=7%%1 z{#yriUnr9LaE7h_UNJC&&-E4ppJm^R8P?G5A|iuAr^N+8=^M0}333QDvp%Si#Ri=P z=LBya0Ht^)24-ks3_9WhblN|tDZ&E2kO*WMjKvLC2RaQLbN~kUgj9BW&~_wqekMi6 zfB(EReT021rWhU;J}kqi^i{l2oH69zP1_>d<)FR>BZD#YJS0hmm7tYrQs8@ZV0|fQ z0zkBP5RnI3!VRh_L8~tyWg`cJ2sEE^=fbwcU+ejrKb9tb44hQ)3O~_0+CwM@O z3$)n=TnX{RSwajDb)a)9MHoQWe1f|33}EwNy$^dvc6%meb46xzekRcHJ0Bw>_?#u7 zTA_Mv&{<0slZB>gBA>UEpq8!ny6E3|oowB^@MD+$|A(%>0j+6+tiOS+>jAd~!DC_I zIUWYs{J6-!&!GD#gwz;7>OuLE37m!`|9xgsXA%%nV*sfKiL-*mA?*upF@moaU{_w&Bg87@ zW~pQ-&&R^1-($$kEGjD|FTgF!YS6>m7~{#rl4Ic@u4XJFZlLn-IisqOys(gpfH04d z!oNpMPe9{Qj0__GbwJ_DAi*#J)Q*z`@3x1fMQ9d6poA(4&1|4{5vYX(Is{r2x}yga;^L5lrNQ?TfI7U8gWK#uH-0dSK<+B2 z;dix`R8v$Gm5&VN7cdUw@`K)6AQ9!mBcURx>K@CI0J_cqTuw`Z+jjyCIt;C#0W`>| zNHBk5ENy@`PZ5!bvYplyabZ2U_5>Xv2|A}vOjRB%Q8r1J+U}CUlU|`zI1ge2# z8PY+=8$eEZ2W?0Ib<1Ee1sxa0o&Z6^z@P=YI~cexfYv%8&PxEbB|$?#c$5^h zWmOT>q_$%+Mywiz-0;LEDq_vZD4L(G8B`a_Xd*Nlx_I;;yNfTs7+Vf|TTSA^Qb))N zQXk)_W)-P2&^?Wu|GzNBGYK&0GdMB$FdW{+z|7#w;0JEug9c^Ipu1+>;iU<5>Iad` zpgmWF0JOn?$WF)^4%AvU0I#fJVK4yKR6D>&XIO&8L>L6YlO~WhoF;e?4QMO0GgwxM zL6gB5>@+3)T?{%5O5h&u4h9{4$ktcTZbfjZuMVD3Q3GuP2kmr*r%m`h!O)o~@ae_y zB>~`7G~hW|@cJ>veqB30R#ss-O(_R|88bg4EemcIF;`uA6BQ9*^OOK4CMH=~DGi=z zsnCB{WmLGtnI*+R6lk4{sjN0Pqldi=kE$k*w75WIvVT*Iv8s}ynuxN#w2Y0BA`>^C zBKQ{o>KQ|n40pO*_h>&Ms#lt%O!K`TnoHbp}wX4pX|IDqbfk%t`efmlNH8)ap) zcZp0ao_j15?PIN>D~MowJ-}<5+0AtEUT$F=YOe-cM+6$1()_OjK9kIaVcISR6X>Wg ztTa*wg(323NyuSn4+%qi26+a1@Wrj*ohv9|s181l0Mz`HWzYkM94uTxV!{lv;4lQ) zs}5ypGw3mxfU_+qT&)R*E3VVbKt~incFC}PMG04@c<~@HUwvH@8A&fWV^!oM&NTES zrF`vu%2KceEUT6Z=+rY=VG&jVb`eE=MIHF5XR^XVtb%;Dc7buQ)#i|N3rf!+;QRtf z&yGmx*$mu9gQVwBWr%v;f1g43jR>hRn1lNfIgF4tp#8tkpfl8j)EL5{;*#L}2)>^L zY(7Xm8#8R|8Fc1aDKigTeK4aZXmcq8SUnS{=12rp;;47VLrY>r&4avw0lb$H94*YC zT{+-_ekbTGlP{p#p+M~dQ0)UcHh~R#Uo<0VzZLk*1W4_}SfxKjzgM_d4t)Q~DY;%@ zMj6lrW(8ZSsI%w4rXyp%RPrvvDkQa6^NPrIw*}(vsG-D6|Eg1%NML-v6$lWn8gp5;x z24sxETWZZfeO6=WZVYHoLDI#JnT0DxFop~HOgXLzg8gFr(;V&0f+45M*``=S&Xu!H zY37Uy)mBpn^=~0-twH0TkhRkm(6!SBp!GQ5@lWtN8pbH-IvR+2(D)}ry%majQ2Pd~ zo}mCmJ*a&HQSXGT9z6aDR?irQq8_w22BO{tMLlT!Eku0;lKLWWdkCW57D;_DxIF|> z9}QB^z{p_uUk7~WjTvZ`Sd+n=!2+r5HUpJAW=Lf>G_Dbm4_%)DF3iEraOmys;AXfk zc*Q5Ez!qW91#eCST?GNU$&&}Pm=pbS9CmEut)Kx(&}jvjcjj=2$Q!CDX^C?njlde4 zF>&&6ATHAJmR1E{q+_ncxa(i2L4c!?u%L>9G-6STqLm066XJ3m(7A$;bH>2u+6keZ zYX_~%5J?>xKnMYh)hIHcAe2Gsl|hSc@G>08N)*u4n?AU^EWw};-o~(lK|&wT$`f3t zk%6x8d4V}6>4x=qT2&$NdEuDD)c6i6gp`lqyMVxBT~?rW1AOj@7jz~dxP1@>5@!ri zfvAVvOAo~tr?FsPuDzJL+`YOf{$oeYCoj2e;GZ6JQ&Jc6J_s@XaF}5J{ne7}P z;-EY3UBT^`SdchFDd?!0|Nj{v>Otei5cSr`>cMkxVD*gQDC$9dd5C&PWcA>B0-`sQ+paoi|rNE%g0J?n(a*hN8y!8!jY$GxaqJRVy{}Rx>1fbhvK<#GG zEsUVqp`8qny)7aP{NO|Z8ZVK6<{}0Gq+8WYP0Z{W6+y=ZfsXg(V}kDvhwijghBT4| zOG=nnIT%}*n3?oVrL`0#xocH>Qc6lx{HlW--1r#Zu(0UxbT{%DOB?YB$#Ppc3MuNz z7$zolPYIY)?Nb^T#wMgAXlTX2_zqAPw@hBw`;exB!6mH86Y&E)hTn?umdW zDnLE}jlhGJ(uhEp(s1j8*6u@2S_k!{K#dc22G}XtByF6HII>!U@2t_UjV%33?Murbans&euunpE_!Ef{nu0_bkMFH9?$bs3}?bU`Z* zc%Y}kf(~~FEm4Qne$ZXm2;X5uE_6T~Tq1%lSA#?%3xgOq5_d4L=tCBRNZkRM4Jr&l zOJ*2B2dP2!9U)eSfG%~!Tmj8AgG0}ZNkj;=0SR_&VSY7pemF%c00Ew=@q zTkWW;<}dOQwA2>q@M@;=bbWnBDQhjzYTI(~LDoFQe_z2)uZGd!@fuM5y`EW@L75>2 zlo=tTaj@8iR%VFU#;E@g<29ho4WPyss7=Fv2Q=Qw3m)Uz!62d!y15CQ3Bk9Q2!ked zL4#|sl|blqo2N*%AiIpbCEBfU$dy@fx~e|szh$ndSHnTi#qDD%XHaKo1TApZ0C%@x z{=t}4#5i*lc~%jeouMOf(9vxb=x`dS^$8kMlLc2>pyLie*TL27U|%8K7;yu zLTcc?G)NpWhe1-`gMpEO<-ZQ&Jth_g0nphaTnwo9{zCI3B7;I}9&lOABe0VJbb>6X z%*A~*4QMkHYy=TBAkW8$d3Oc+Z55#NyFhD76%h9a3Bb!!(0S2_`-3n>M4>JO7oVW5 zSOW0c0#cZ8F$jRKVF6uS0N&fh#bCg|1zy%9${+wvp0N9a#G%XVz-R5KfsUJj9@q%l zi3dIrAJpLdfbHTSZzJ&XB{SISBp&pOgOnt}7Y9KWE2Se=B7w`47fjBKJDH^zm>HBo zW56unv)mX!J0rn6w3%TCCx93RhM+~DjENDZbC{*BgAxrRLmCq=qYl#(24)63(AXYa zKlm1FeaM~F%nbQ3hChq}y2ToNN(moW1hRnSc%tT9Cf@6yF@{!D$7(*Y-0PO&UT*kHcbT2Q_k_iRLI6@@K~IYVyI?)o8e|v1-INcL>W48P;Q~5~6BIU}lSM!+ zGZE;i9iUPhG+YK+c?51vf?JuO9gU#sjL}ruKv_gu*H#%q+et~uvM@7igGnYeVMRSD zWos=780{q^E+ovM0U<$g8Twy`(St#lL5x8KG+8PR-pB)rOVo*voeZGkL_h@*WMLxc zJPi1nlyfGoj@~TR;H8*qe4woeR#(7#4FuVQC6uIu*dgmmK;C8OVhUhlWO~XV2|D4F znE^T3Dd~B>4=!jFsR&hl^5oHlU86gcZI}KzGQvfdq zCod1Lv$QIl0Zs>fOx8@g%u3*!T`WLT$gs04LFZmEf!l1L9074KX4cpNzWG&H-CQ1Y zlF=Q%oE%%foE#==wSyMx)eb`Iv<@a~hCpUD0d@xGe^)_8)Bpef4>4IWEC-8~!o`A^ ztQf8^3kt9^hQP(*nF1KDF+CMvXN-c2RWfBT>}EPAz|K$r7fWTbVtC2SC&11a1{XWP z}SgFYbvKudk= zz-=|qn)gU>n-8+K*dDs(9U|T@q{aXd2cI#*z{p_x-c%h4!<+{$;-dnO5TfA9EqkAP(NJxPw6+l(`)k(K zmG?%4g2T> zT6u85h%?B88#9od7ZTuQa^Rt2NaY1O$p++MV|K_`2&ne~s`TZnOTow1gjguB_p|Vd zD@)os@vReC&%~lY177(mu9g3<|O{UYqF7|@zAcd$BLP?`S!>i=I% zf#7|AS`2ldlmi=20_};@0+;Eanh{zyAW{&<-aqIV3^)aW?qGx*8w47B(EWcXg4DDb*(KEYq-5oV zfc-DTAkVN0G;W{(z8DeG`oy#M5xV>h5g^bx3b6md zSqi$A8{CT(U|@hQ01*IXqVwRbJ&w!_@el?pgFb@*gFS-)15@ly1}1Pq1Dyf|NoZ^g z^5FI_=x7P}b|+)dY1Hh9pkh`=38b_JgK&ceEw0Gu#=Mz^;f4*2rJ6b%%uGxY8r-T( zyZ$|3RQMC?-_pmZ`R{t6rm?V~2#<`y9Z)&KzyK~sWI?wnOG8g(f#nfsxry*JMk+uK z1xUFG$~3GOK&y_K7$6%zxj;d1fPo8AjyS*=4PeGj21rg42Hmry3|S@wy2+295qvng z2`Ec3D<4ED8`xN7rYORS4Mc%pqqyP-te61j326ofrh0I>&;=1{vsd6Q&8Eb>oba!Q#%K{S*wKbwyd=bw%8u9K*$cdY%zR7(o|JAp#A$@<;%5 zupSFIt{Fi!?FG<9;-IF2BQrxk12-ge{=*pZ5C$tlJOejFJ_9!cQ=x&Ou`=j9a?nLU z?8^IBmrP06k}#!Yjbtm+uD=JE9R9jZ1N#p=KA{25^D3Y;4_+r`1wMO6njs#vItg+^ z0ce564hDXBo`+Tv2!|st?gEvA%wWe0fG_+3&A6~INJGb!L3=Euz?m9!!Ye!IWG_$$ zSXmLANX<-5v>DlZ8P|&ZdoEcAJ9y{cBSubRV-9BKkVFNhT~*Vh4IEW1V;q&sRP_b< zg)H`g{Q_R+-~jfEDyY4{050p9K#fsLP`?j)?l5%07KRTHBTJAf4&(#K7A$7vZH!Aq z|9yy`!nCXE?*ULa{XhLbi^(4xPLd4GpwTr*>wO0UXgrY}9!?l#BO>R68V;ZgAPJgi z1n;}p!5{!SGff_n@j)>S+U4g6idO-Kd{7Bc2tMf$B)WqEG^+|abrDol8W=(gNM>VZ zaOQv(kn9Gc8mdbC3XH`GF1oFf|1L_lGfweI7UYwaQxvEYvWaet-23;yzYo)Esh?>VgEGTj#Odd-ypK^oLI-*PEw3BOs1vHfz6=ayW>bbQI>kM@>T7MfEv9WRL>A(DUk5LJPKj~|*v2hw1{9@Gl zcMXI=jcG;(&|HuW(=i5PhRLAZgnG{=G!PJ>0!@_Q?9UF)L7+o?WWklB2m?E`kQ4!h z0q9&%@L|;b3?iUzA~OS6qzGJdKt(|3hk^%S>OpE^K`UcHc~P4|5Zr77PhUfi5VdE7 z)}fHouRx1cl$DTXZeS-yF|vi!sKiPK85@|gF)=c#8bpXh80c{@F&P+yS%>OY`WKW* z>Dj9?y4xwcrtnx8$0_S*sq?e)u*u1ntLbxdamg!rg_)X11ghHU$+EEVxLFv4*n^55 z21ZC(3top13(B;p>kyzpg9s~(vJ<*e0goy(~F*Nu^R@1lQ?Ky4~WJ+MPajS*50ID^J<85lvO z8dE(J=#JTb(CS&#b)*<>#%LKpr)$7Y1h)(XLG!bMp!r!)f1Lq51hIob46??_}VEl&;`r05dFggYOH{2PN|UDf&^fr}c{H z$}&pHaw=JfI2oGo>O>lXwr4zMl>PTdRgsmALrr~Jm9F`rf49Kt`2XGiznIdPKyC^F zZB0S#Ghvi+7=0$>aeUA$6eBosgNiT6QgT@FrHp9U@wy2s%FFPnGIp0tNyr!BS5#3K zQ4cmztP->gD+{>w*Nw@sEUYQsQAAzt|NsC07#NtU!DYVzxIPE3g9h`p!FrW$2wjIG#0FaD!@vk>V=#e=jR=q%5#xHGRyWiIi2MLe z{s;j?QUZ0lK^Y&k*dKIeG3aJ*(9PNghUUiR%*OJJpsf~+pssvGkzDUIYY}GS`q}?Z z3#(}v$tPD?GO_=tsrm8upt8O!4`{uo(*Ju*p-i_x#|toMgU-0)V9;UE1sBhtbuq9N zGfJRZkOQL*avtcCUi6;0W1Pqs}CAjfY6}(dO#cEKr^i%S{!_j z52O_csiPIa$q2M92z0fnHmH1uoEHjOh9k!eYFB{vl7R96Xn+uO;2ET#Q$OMrq3oPw z$JfoLYNI6%VWz8C^6)b;+tfj6L*9g#$i7r-w%}lPIYU=H2%E`~)6F%KUxwFH0?q)p zKmOlmU|{M3$FDUwjdA{WVsd8M#URa)2dXn6Eh$(UgT@Xbx}Yn_5dz3fD$YBgizFGk*ha9UHw5539gpCQf3}A3)U|>pQ z+QlHn5DPkHKpK4HI?O-N>mL!$#~4^Z%uRxHb3KvSm zfq&gV_gsVeaBWPWS^a2G;{dfij*-tX%4dw04b+?98X8pgLb4Goe1(mf71J6L0}{cw zAw?G0(k#uJ7{|{>i$vad?=nUgf@y0Dv&!>ppisYaB=`8 z641aaY`BIWEDG9|FAB~vpf)FHQv#?(1X_`gJ+***1v+SnNsfUTdmVtrE8{A%&1@$XfpuI;(Us?uKQh=*J zj2;qlCO~u=LCGD`Dq>-f1rM@;W{qHr7r^~PM8CoudhfCcXoO%>sR$#dRxGSz@t@(f zu@R{6tE%g)%;Zq@N5;@a-8RuhrA$j#kV}M1NddH7gn{`#EFJMMh%-oo?k8Y}t}_9R zSAZIu5Lz1U6X+-*BBr2KJwgB)mxxLQnq$Ce3sfzFR%AjF><$LdxuuZpmMGp~Hiq}} z8I75h7s5L7GOBV7TGF5%{wgMshLVN?En{IpQ63qEdrV&7&ONAR0?mucF)%RcGl7aH zY0yqSKDe*=z;~&@ayN!=5RM0}i~&_Hpfhhldnu9K3hvTD8ULZoA$ps!A;PO=R{}(1treh4MpfNf{1~mrIO(-xILK6xic%k794oy%! z0h*jrgr0{2Izt|mh9GThP%#Qx7Gq!tx@;7F^8+(@1O;@D6=-q}bg7=1xiTZ;TIA6< z1IUP-buehej$O;x&WUd=<2*iD5%_={(r}%DNm3XC1JnOc|GzLLfY)i6gH}bDLRa-c zj-3>OryyvX3E?A*8UkA7BT^8wKobD%{bOg~g!Y6$`}{%a2~uY&fJ*?-tsXkyMhkdc z(##xmrhzf&bTim~YVgHN>Y%Hbz*}`76V{M5l8h-{KFlnJhK(f+21XoWQgVV))@oip zLW%}*Qq~H5qJn&EIxH-Lf^Zg3Y_7PO+`sFf(Is^+CsRQw$=F=Oj?Pj!DakA@X+t$e zHc3@JDKQZ9-!Cu|dfqyytdw9-1>I$)3XSm{45|V<7=+>B1np8I!U+-c(7ryjt_6n^ zXh|Dr{Bak9DuV>{I3ZPi&<)a{M9c=dSP0yb0gbUqg9FIi7}{wB?*#!j@xb>?GMXc< z3osTmvlCHOV03g>af&f#6eyXZV_Ko;YT&{+TQLNj(izpYs$ykjIoSAuT+O2#RsOm$ za&0wGWvoA{r76hG&n+hhZUchP$z>7{Vgt4Q7?>FJ{(oVLV*0?K%wW!t3#vCQpqpES zK^JbJhXKa^Jg}ERUV+pApp>oxULzyLzz01EQ%Zj)gC1B+oIwv-DU0ia`-UJ7n}Y^k zLF4eCRZ&R0V89C&z*!p+3XGhYMPi!fisB{?{zjeB%@z)#;+p*I(w-(N_IgrUKG~)Q z*4$i7%!V4qR-Bwn%z8ZK=_Ym6*^1)I61+?-Ei6ouqT<2UMuyVH9(tg~Qn^-UCY<7& zdb*aTW?&k8Um55O70`X@k__4mRt)W+28lI;4LH_7CxS9u*uemz&ETN}t+WwyI*5F- zlR*mXcPj=d25ztsR-o3ZK7$p5J%bgfuf@vH&tSzcAI#awpawo#6wHeP`0?q)7I3AlaxAZTtIbY6@YlK_JnL+~yJb_R9uX>zc# z6WZ>;9zKxsq(K=2a-KBks0PR`Tq)=hde~Grq-hNrg;hrc0=)Sq&d2yCJtHi^$5_Qt z*GJ3k;gB`m?mOi@KwD@fW~tCGjt-62kZms46)Q$gHU&CuJ`4#bre6%&<}@>Meg z)mIwed*eSa$TJu*WP@5z#?XBspo6V6;V}-Kv%;1iKr2;2D>*@Xh#@}W(BH`bx$=;k zK@&W0Q@ewK8?_;!x1lx8vbvX4c^;O!O%a)G{{Khz{alt7BuI*gEsH zsisPD(%29);Iz~zGns80NUmw|z)oQZ|On&HSU215oL23w@{ z^3WOt5wFnI-v|MWn70OvKUjmtAE0F?#wjyKAQeUs71rQ8l|bDR(CRzL9%~Z@YiPY= z0&3dpGngMNBgY4a$GNLcF13ORyh zT!d6XGJ48o%G$hO8O0o_Fau>9Ns%1K06}@~GiNM=^{XgO@_!{U@LuWisI6NSi?j zym=j*@SsC2@PC2pf{sAng?J^_ij~Y=6CTw8EsE4NQz!83kl>1%lPpSQwS` z_3dT7MfELRwNuh;>>2fq+jw+!!sM0Ylv!Dr`9%_CMOj&yL}e7@d_|Nc`Ms=-U3pnK z^tAlqL1SMA;Jqd+LTn5X;5rJ-N9+w@)`srg0JSAunOGPkK>O~P8L*CpA(9@nuZ0l6 zQ*DFBosh=DK=+EkhQB~rnG-ZB1}=_4bBds<&cG0KroTC;!wVX@lm{(pQ&Q7sRAyH$ znWX62G_zPtLxE93flu08*jm?Iz`$K;5);eGlbaOf*x1<=)I5q*wd&6?F#dn|{}+=X zc&$LlE(TEJUjiwAL5GD9ULZK`#Q;wCkZ~{2h2L1ly|_IEq(lYSWf>d7Cb?wraEnRG z3CVez=@$uWn>%Z-|NDn=s+(PuqppygJOd*GXzznRc)evVXdgT3dP`^$M7R$l%%QDj za5Dki0fTx8EC5;>$$ba0;0|;q7HAxX7d(b4ZftJMtZdJy%+09G&!`+f$%&(@E3{l) zf@4h(2akF=6U)t;fB(G9{P&5GFY_g+4iaZzU~*t$Vc-UhhkzE+gAQAO|V0$yIk2`+V*z*RFOEifyaE3+%-PEuU2I4NwB(t1#gpE&XN&)Ku^ z{okN=R}eUjgU@3Cm$QK&{{PSaEtrhJ{9q8D0dlq|NWB|~|NjdE15*H4J^;jrp4AJ= zGoS?q%nbbhb(lPvo-mj*crnB>Tm`jR;uzw=X%^I0v4FN&qCqv7rv45FQP8*osWGFXFknlTuIFR3;#WQ1NX z0d6INPpD=GU+1ID4j#_|ot0zH2)X(V+_D4>Pl+3|n}NF)peClM2za1J+z52%6ryd( zCdR1F6XFvhR3OhPt|zNwz$c|DCapTGp17M7FqH)P!b616Ri z_Y{{`)Ja#DQDWC>0U zR*=n$C<8z0px6S}s-XA+t+g}*&60pxGjdFzH7ej0^33Aq;-JW4+@+i-zZb~Iz%erZt79>96j^I;2r5Hd7#NsLm{=I3K&K@LGhiKqL}U%G>WF|>6Gbt+Qn{i4_3Y+xDLf_R%NS=Si zR3T9wDOm&4S%3d9Ffwrb|H72Q#KK_6Fnbq+Hd^}uI-P}ZDzvwc5P;4WA_SlpxFHe= zvF zO(DjVf2%pgxP(M`h541G87)9OZZ#eeK_xy$m%o3abS;gIbwCGaFfuUxcVY@>Vqx%S zI0+gK3t$LD%16-a$Pgi72`VftkqS#UkboPK05t1CLIhO4BA><$u2rDn4pzZ)2Q;+< z3N!Q(RmeU7(4pN9;8IYA!GS>zT%3a%amHXVB?ew3OV);mi>%DeMk%rJR^#0>YFKXbL6sdm}&`1OU>t2W)YN-Rc2(> zbXQklWfd{kQPpH&wk$PgVv-c&=ip&ADnT$yUAW~8ylDz7zk<$F^2uy#wpIFD#NEL$EbtCPG^=g=j#XMP*5KYG)`y0B*37` zV8*ZsRMSCr@j`m#YM>ep9MBk<6I#Y0q7#vxK^<>Uwv=N~V-N=ivmB@i&A=eXAiy98 z>83AWkYm^Y=79E$2|=R+biOobA_-LV@qiaZ^WE9Oz%8(Y0n`@(MIdDGLz+Pk+@%K% ze1M8RP!$A@BycTd&uGlgC=QM<(4-RRU}R`-j~$dR#2BsgJdLEdM3p3!V!7FJID`}B zY;@Sz%ek_+WG$7g-Bq2U)a3LzSd?Ye^w^jfRd}TI>{Vn<4D}_I1XtK^*+fjWjf|BnJk`ZjIGLE$q}17%Si~6JtHY{! z0(~XSR2UiVXldKKI3+nLq^O&5N%I-%s_;tl$xAb?`TGZy&OHBrVUhs1%{3S@Kw|-r zm93B)oe=w7p`)Y-k0bk29Gp`@r-4C|6+gtE{0ssN{NSD$WcY)JK>>VD1uS)dDn@oh zI|q~=Kpq7R@<0=WxRH;kGz+`-BsDf^Pcs)!b=NozWg`wICM5-9eKsaWRUTGh&X52aw?W-Mhmm0r)f?5D6*h z%9txFTAG8#zrXzd!sNmvz#z_`2|9CBg8`P3HBjapFh(z+ofSkxLSq(ORD+8YM9hK$ z3o-r;?h&wqdjz1NE66}N=)#sA44`QM&~dk*m<6X`Wp-usL<}F;0=0WYMHo}uy%`yu zm8!$4mF&ec0^PmkEko4Ap!l1ZdTnvs2@clgujIjPKgEB)3s5XV{ z>Vkzi2dGuWfz+ykhA22if=(j?4Z%P%nIyP;6=IMCkE?>N26qH4+Y@52XAlCHLZaXs z>cEpwkl|IxK}evT9pLUD+gTx2ju?0Tw8FKg7 z|No%>XU_;YicXAeq<;4VCt zs;$M_o0Uz{k%LDBktG0s3R~6O9FKS7xer%cF0&YdL04jCW$b}Kuc2*kg^Nl)s&8)a25d{ zKFSme-uf%TV89^4z*OkS$}oXJgkb@U17d6dabkBesDfiakUC-qv7&NX68RL2)#K!OfoCm<;x8Sm16{IYX z1FvghfQP98Z(GIf{|&EixqIDf%e|OB{x_AI@ATpRsx_|Tu?7To&H zNfMyE1RBx;wNXJWdr;dAJdce$r~&TJf(o6zYNkp8tZaId46z+L>DnmW$RnX?CMTw+ z!YKUj4$4Rc@83Vw_8xNcK<#ztdP4?baP0x!84GLWfOd+3hEWlxt3p#TVg?)GanRYf zpowV}1`+72iVFO0EYO?_Xk`N^AA>vwDnCJ$2WV$3sQUuhTqLZ-#%>JitT2khj^}{V zA&Le{5*&N+zoHXWR4qZ>at3DTd6}R_ zvLu5NXl=6q^jt1biwbmdC4^Rhmxy@k4A6#JP_!vA2taEIB~Ui|z@P*v&NeV8F&tn} zg5)d#1|Jcv+3~E5YM?puGjf%mEDS z4340+nWU^A6#(rq2PI0-*aGOV6=6klV`fFrE(3YVRz1i*gK7Uh{5`|LsNGm!`*qMIb9wcQAmKc!Ac^f`SLMv<}j{p3JRq#>7-S6Ik|DQ3c)9)* z{b0r(M!l*n1)!7TAZr!X7#NtW!RM2ggLYz~`Uhhg1iHKl>>nluUhv2tXpt6q0jjV;*EfK=Pavo7U;tg+0NSv_4O)o`YSw{wGqNkQD>uam z#;7qi3BMPaC^$i3(!P{O`Nb)qeJc!%3}XL{ne>=;F{m^2gK9d|J|1)?8>4JMl!3_I z0obZgHt?1K83qyPhHn{AhT>D1~~>X=p+Yt^hlpUj=>&0VB`pjS~-RSFb7f`flfu* z$pC4-g7&&Wu80LK@_@`^z`FaO^~K;=hK30EXdp%#@Y%)uu`ax_&~ahDNnxPqSAmTf zi$TvV7IXDwT?-v14q{@-zX}~YZUr^a8UJ7TpT*P)P8;7rlc1>K0c}(w(grku5CYID z6(InvbRi`KX#5jp+Y?9tu>}p(ycGghq&pbE?W6Myp!N|sQ?N7GGq5u-#qMAL?W_Pz zXo5~41|?s1aLWdCa3bh#Q)cK62GFVTD&Uo8p!}f@+UL~Ipbkoo%nbiw40#9xwBbpe zAs;kMQD|VOsLTvno2JYR+T>@?h<)#lF|#t`S$%Fz!#YFljMfOm1|Cg=Is;9_1|C)& zom9}~91zC2g{SM^FHo8I|H}UoraUGV1{;Pmpfbc3e6KdF4+L#_Akv~fs8ghm)G5Lk zaDygJMA$>)4z-Es(Or|5q{*Q6XuW=7DOz&U_7j#$jFGighGhW zvWsBNOb%X0YeD!@3gq<_7>h1I*Gn*f_Dy9mf%Z+AG0XyOfPjo3!@?dq6M{(9h>+dM z09n(C*f%8%-!}zbY7S}}LibGxL-tL@gC>4J{U?4915`P|mZJ-SOHt5T4A5~>kUGf( zdc~mG9RovgJx1u-3}^`nS)5_d2x@L&-9E+7sC>{mLWZafR8q!1h7z4@oF*ECH&JPM z*=TZ1U|?jB`Cq~W8ZR^fof>D1HdX>{J|ogBYPf;gU;^+pDgvO+Ci326Veoh%XnF&b z;y^Q4!m#DE0y`LtKxgv8!vnHD1ri((HfStK-3~IU2wSVdi5*k6H&;JwDrW)>UF-*~`-kk#gry&7TNIIgF!n1V0tPx}glJhontSZv zNeEU*7_lmSOFu74ju zW6C1`OTcX-3DD>(7uq@P6QGhpixN>YX^hG9Rou}$m$%>&@1Y)9mY;$anSOdFw}KB!CH+%w)N*w7VZ24 zxBdSAVqjpJ$^<%YZZ@b2QUqTv2#b4Y+Ye9NL)TLv{3i)2*(DjktwTlz7U+#~j3ED= z2cK*Ot`Qj->=_urLt3E5*A51*3!s7vcH;`@LN76R{Rg^v6SUJ**q9l-z70IvioQ_k zj+SA2NxOlTM7o61l+Jb`(0)A`c3BG%Q$uqeWd(_JMvZ?rn0Ebp*x%16|L>PFc-NlF z%qo3{oqgc?3$!LBj){dqfngq~?SnNO5YYP;h{@Yk@Z8f>-&0*K8fvQEmxqQPL5O7nQ5)&K1>EV3bni z7Bd&t)3xN&mKTb@aDj>COij(1O-izC?5v7vK6xtIjk%z4Oa=y~iQqj;GeL; z+9DDb05vqh{Xft_cA$%S(LK(ryrx)$kwf3De_E4}9%#EJn1_mZj+)IH*V3^U?NicyDG!s%&1?q1^+#`Dv>~G|N zfcP6!TSzb<`5Uw<59U>nBsYTuv?B%aD!2;@+MSL)jwcogF|sI{*38Tk)sbbCkmr>! z7q&Jq=hrY-n#4FY|K!O{%CfBNZ1U;vt+Mcpq9^%5c~ zF$P)iwtPW1C$WOdcxF&Ok7ocKjt9=?%nbGn%&@2hiGs#%K^yr&OU*|1Xa)=QP@c;knTG;6N0K& z(AiAjBLLF63~CG*m4s&`b{WAI(Y6~rMZBZ^zZee0Mbt>y$+Jy@Y zj0{HqvzTI-jxlI3_=D~!hs-j<>O+jS5VYF@F5y61DM0f%kgcU^0-)pXK}lUrU z!Fz;lK zuDm^ahMXvePvVq+FF|WykFHp8^zR?gwdM?rkh9Lgd(JaJ^(E?_b7;MR$e$RA2XW*A zs8nJCubJ4vz^K220dzJRs9XdsA_kR|kOfqrrNrh4w}Ea>P&QXishvE}P@dB-W$C}0 zwZgSxQ^ab2Fdkac*}3GOT|q|&sQlA|hMOSho&W)~yEZYh8%FsD-4TWeH$-CxG!zNS zXv_kT&Nhe!owWi=C;XsJjxxG~msJQ>AcC|)utIQBN(vJTBwQhP{rwtzUc%YRp zXiL5zxUmJglM9r4LD3)zsh&h()f1E<&j4nCswYu~d0J1?)BF_SSczn~!h-`cnTK7i8s|3CjrK@>ftIR$jQg8tuCu?ZD%dc zBg)Mh9cU`6dgL0ibcybq( zwxBf)A}K+8GYA3Xq$CO&jR!Y-L_y7-dT??8HR~biW+ww^OC;nlDmh3Bl7ponC_|nB z%mAezIfi`DfGW6wgILoDI+_8z7!0(S6yDy^WD~KFjZJ{=>sQy5&=^}fX zRE!LE&^C}9LnTgsBg#&wzYzj>{2c)IH>e@Y%pl6Z1`ZTZ!6gk&H=sNOx&THIJaGtF zt%>Y2(E4CVTS$vl*4x0^2f9XBK}ySnQwrQH!dxW`Y9c|;S<43ZUu!Y@uh5bJ5q%hA zzEF>Xvj#r{JA)+H>HMJhbO5(LK_wh0K0#M03ETl)y|jZt2wGQwx@Dk}4%{sR4-~?E zC2nra4p|Be_A@`D^7DuwXoQJKNu^5*%JG5DV$fvckroh5f5Z{#4?B@TLWyJ6JSGlS z#=yUB-`F^qYe8iKgB=3{lNA%_D1c_rpbBcb#hA#4?l?!pA7bS$s95ELuJ;6|TUh*o zW@bQ11Gb70R3n0Jb!WS?g8?)>AO@u+Ez5O7!EM7F|fvhW=SE9UHIZfHjtlTlNs#H${kZQA(J-Hn1vp(!L$n_BmP?t zy;oG7>4^{^W@pG*rI3A#%zQ#>jLX2{+S-t^ ztbl=m@iNmB26hH#Q2PUQUKQH@KrHQoW(IJz0PB^4mg4LL4K#ty)du&h*%cW%W(YHC z7tav>ca^EA=x<*U$gSYB?*c$>V^9IR0eq&@|Nji&J=h?(f%aevurX^x&*s0)z`*1I zz6S=P-Uf7}2TVQqE+!X{ISiRl^?w-{m^OgJq8)4>_zZvW8l>eQKIDuwu>FwpTA^XX z09prU4z?e>4vrZdHvb{_j#}5qyRFH5wb@Kd`26{eT2A}`>|34(G zLGFWuH3N8E-T(iPv&q2fL3a(XG22)|)_chO|H7mJRu8#rAQN;x*Z=>J^SMBGXF|`w zvV!^_ygr2K9DIEU>~1Mu@Y#v|@H4Yu;RR6-T5AQq`-veFx}F5QW&-3M=-rt%ptT0j zy;+cTf#AC{;qC{k2ZzT6aCm_34bcUM$3-w7v{z&T*u9W58m&NQG{V-cfZYpTx57{W zTDS85KPa}DCV=dRp6O_Vq8{X4i26(v^=E|Cpz3Xr)W?DEwgRhXjE1Ur2c4e-Hh(%O zoiN*gjtPR=YXT1M?I3Y@`T?Ig0#-j8B+d+tPX@?Z@TWp*jJrYNknmt&Wsv;e&*aCf z%^<-b170O=!mtr^=mliGS zcvJCa;R0d5zyKc`M@73xb5<1v3x&t}@(PNqtju>6Efjw$C@ZS#v9U5S6&5n>DlFy* zb@GVjXXSCSHVm+oXPk84K(jvgE>;BvIiY`X$B(zk%QGKiR#2AXgxq}z+GjZxbPgy3 zUq}sVKI}|huzFD1V1Sqp&Wj9; z|G)nK!juj^)45<5g9-y`pB|%5K~y2oVG|8di(3OUqzzdO3|R&batmY`yo&w~22f?G zA^=%P4(cw0_F#Z34@G_^$k|$;tAQXBV0KK9Wi{H2;Pb3A8UKmSVU#e|l#;cHbrjXr zR_6=nR~HwR7v*DNVUkx15n1viX)@EU#<^^af~r=UI&r>+?EK>VF(Ul@l8QnCk{nX< zrOW?r1Bdbd+yBj&KxdSyfy-%j#%xAbaQ;^NZ^q;dz5^Rn?y@sxGyDUIgUfXd=0ngs zSJ;`|7{TQ|v|N{f-nqign8N4-SAUEp4X)k~N&O<`JuvmmMo8)%kkkhtsdq+FZ-S(L zD@!EY{6Hl26Pee;%{N6-pUC0|S09X|-kEs`T)jC=Jt!Q&_n(34B{gOTHwFP{D1rRP z01hYcy=I_rQezBJ0Lz0y3nUK?uSk{*xP9&@_U(h)rwg->fq^L!$^RZG>Jj0khoT9a2KiHWKVCw&yF-3suQHcMYu=<}5>JK%>FjW78!zYqy z7xdh8P{>Jx!iB-&zZnzgtOYd&P(2J1hn&*~T3ZwePG6vU8QflQ!Qv40pu7!I&&F&4X#;`lc^z=x2B~Lbi~_ZRK<%8^^Doj`%zu~n=z?_@741G-KE5s&8PuZ zukzoF33L`D>g0N z1mqq_e1p{Q0>=kLy&sZ#Nc@4-!{ZO6J^)F50FruLaQs2k!{QI5ekC~mAm#@msfWZL zL_IA2K)E^R31Dg*@ABgy90L2F*$b8iJ zSjpT6RnNu@Uc(A)S2i#`5mE!IXJgian{NiXdk?A}lnxN-qk*X$svaZ`Ngsy)%|L4> zh13{8cbtO6xxwkf;J+D@9TN+*U5pS1-SL(MPETO<%x;X}yTYLBtU=`ms9g+lKjbU| zkoxuD@C226Y|M5@?e0Esc)CKuQxjZYGN7x6ommcQcO$EJMo|w6PmuYb@PyQt5cLv5 zYK$QBLGFRvp#U)-6rLdUY|NIRq3r+v!S3$^hbKrq8?zSNd^4u?%(_Bqj3D)hZ~%o* zA2>Wg>Otb7;BW(vk%Rop0J(3|-WlRA$o-t4widWu&EO1eSNr`pW10ib{~+}sbC^JT zHDG5jFz7JYfKHsSV1TUP+QDD}I^qD{>V75-6f_HW@D1n#s^D-zwr+|6&K`YInw|+y{VS`#q;CnGaCz5~%7QttVfi_vIg9aQR zql=;8l?bOYb1z1_tCuuP=Nhq4A@JO*KPYMw2Et8XB zVUbmomu6v+y&|U7N3dQrGE-_RZZd|Y~rGl z5E4|*Lc$r;P6CG;vm0YRIQ${u3`&Qfa0bPDELa?(9#oHk)Uz>LAjb>19s{XoV~hgD z3j-sA9m7{9UB(;+W(IN4APn^0u-y#clb#t^v<(bFS31~4Zh?&UFo4G@L01GYf`?f_6L+jo)(!^HK5o#lrhFGbE(hI502zuF0=XA-|2C4F zK~qVKRW}H2(1W-g#yasJn^6qpgMY8GA3*#tonbEHCC0-Hf{=KG&Ut~(lwn|C);2Iy z1?5nbyNVc<{p=W7x%>kl)La`qb_r=~cPPcc!2JK;|DQ}U3=9gfE?ZrqSfK05Hy{FhqAykJQ#~Zp_gMqR0u$hNe10Y&M5%uVuQ;Z(7I08I%IY} zMq>|)SkfXV9V?_}@= ziy1QbGU$Qbswc36!Suoo2GAt93s}yP!3AoXt-wx(Fb0rmz5+WK;z7F=Kp_v>j|1C$ z0_h%u7D0mNKG;>oS%u+hK<83}CY3}$+pX2qmH8Mkcin+zOW?z_U^7-YIEd>yXo5+3 z33*l~Hc>GM70LLN^PhCRjiZE)y_&KvABT{+0y0M!q716~9tw|fqKl=5v!M)x^iz`N zl;;qZgi;;-&i(%$xLK<^8AwZribx_@XbPd~p%eon!(s+)#%+vW8Q4I1jFka?2O~4+ zoFjb$LsfNqMsap=M&DIp*VJcZX|HHn5YyWQN=J+g3mLQ-x4`r=qUvSS2h}3#pd)D( z&d9P}C3da7w<~-_(*g#j|JN88m=-aCrd!!TryjC0a6lKcgUS$Qc)Eh-JVd%ej4Ffl zGJt1PK_xnD|Vgjj7*o~Q$nJlvIXD!LPpT)H6-<70)S3vu}4lz12eq}lUI!=_q zoFN&s7!h`IrviAFJLFbw#CZ+SDQbi(pw1EjH9A1|AMRk_0-hxnJO92}+Lo z3@o6rZYA_RU}ms|$ZY(K;GIvP1czFVd1{%;a*Js@XhUgDS#fr579nv6rOe08$jHRc z51|;HWp#|j48t5$K(tY)gNl!)x|6tos5q1gYM>OkIkgNlYtkU@tEN|bl5IH_T6woD+F%PEx+cOc5H5p`qltiajw^w$egnmZECVA0A2>E-L6d9`7-Yc<{@55;zCcFTK+y-fKT8CB zfd?op$bvUX7#K1lt}%k-eRJrEJK(uxHgPwkwNoy+W>dNx5aD0Y`ntHw8QClLX*e0HakH}ViOP!V7>a4ysOWPriHL&;MrUtZ zO?MkZAqh=QJ$WW}5g}d+U0G8NNe?|vE*1e12nn7)yvXRu#KitG8?D3IeGVDsM4 zb`T=&5I)+$z$yS*P6aBkS?(AZN`sCCG*@J2e!eA_J4k|2k#`NFC!>l}zyH5`puD^T znwMo5bQtUy_UvNNVz6g$07tkMG%tf_OL%BPlL}$$tMACpFo8jq zVF80I!v+Rf24>J5F5kh4$dMVec>%Odft}$1gDk@Z23ZDB@?r*E3yPvA7IX|PC`v$= zUz>p!gNQK5GJsf+G^h%_LKSqzg*_u;Yb10nJ~*zR^$X+#6Lup|Q2|Yd;0lM)+Qr^b zgV9h>)l$>mR$GZtMovu2j!B=5kx@(-Omay}FflTU3qvSIXJZ|8BW`9fb0IZ(VG~s) zJsuWLWd$*9CDBK!tn6%j{19@s1RpC0E1w{g0+$=x7(E%kFdbl!X3z$m7q1CVx1i(c z1mPhKo$E$~G#e<(vN3>jC}_>K3OKYucMPk5BTNu{UIQqgco<|DK$l?{7&40RF|jM# zF`KLNF@tCF!4)^V5n9^)f;Hu;ySUE*rCV^~WwJm@x*Q@xJQlh#W*U;-X^>P4AsIkv zs{8*h#(j+Ci26YvehRq00BGejXb;a023fdIq4k3lsOSN=525K9e7-rTfWc8e=wr#%+w{46>lJl6TsX7-DZez2V&_6==2YeO-vU+)rSmp zdm-q&FFvrVq@i_#fuSa&J)<2nq8*5IMzS$u-@oU4T^ypq{7j6D5`vO4Y{5eAHdcO) zmM)7!Vp#bEMR-`*Sp)^;1m!g>txWYDlr$I^8A|^DV%*GF&Y;UszKa2NqdCmY(8F>N zZblp_2->Y4K{ZxAgEnZ@04u|J25pA>4B8B!1D+Vb)feb^98R!L#26UBXT|Mc z0L|;{=F`~_A&M2S&s<$LXL{x>%q@4`y-8A)_gMDnA z_*^&@WknU385s>VcpnRMva&Jr35bZ>$jBRO%Nueq@jDo~XvrIjNC^qCvN6l~GB7cK zD(`LJbi>Dx37TGnwJZ1pb}%r)a}Bf>K!gG!*MO=F2I%e_)V2j=-8XXE0@PpxwJj9U z+7_ayZ40E91t?9`{r|;yn6Z#SnW1DCgA4;|+d>gkP$@DXTn1gJ1@7J{g3nb3wJl^A zU~8JeTX-LUxA1~Cj)U43pu1*;!I2}#APkOB(0U;m@Sa}-Lq_--4_r+Pc1A}0O$$Co zIb3avU{Kqfi2+oOon$Nom($UpnMl}8Kq>;D?a;8)2yI^=e2a(&NDcyBp@*-10X-mw zQ2RoRVEclN$mRtDBf~7vS`@}Y1`Y;WP$L6t`vRMf(AyWr!l1pDitNlA5bXjG34 zG(zhF83sKDJJ3y|`tbGzs4f7Fc7SNa$$8NB1va}uE3`rB26UbyDoJ(*eQ+Na6g?0Yq%~m<&i>#?F~iZC zfMk0$c$e24bcP|gIRR;3FtT84O~^x<4$u~a0k~BGXEf!|GvjMhv>(n%lq-ba55CvN>qoA!y*-nB7QB+}xO5Oq^X!of&*L zJ!rc0VyOAuduXCb5r|Emc{AOO%a=i7B&QwmLFmW|=QXjiK^H?KXDsVS5Mw- zrsMyFn3x&gvHoilQRHJ>DHdj3qr$+*09qfF#B_|omSH(4tnC==k=FA;QxW#C1vlIf z$!a%)HUlez8Uu?qBng48W&sT{D1d`w2LtG!RvqxJThOX`ZGjyOycfWirl~P7#)5{T zb}$G)hon@&%7np7dO%yhL1)GoGDtEQ2?|+)!&RS=9khWSG)k(+sIF{o3~39(Hap0I zPDqzy6bD~P&Ce)qu5K<4S$o5HNyblm22ZV+mN`cuUlJ2Lvp1iKrhB}(Pqsplps~7v z;}*VIEbL58eAPVaVk~dD1(UhBxp-x?c!UM5f_epw)cpBn1^qcpBJ7l%Y~7Vqt*jaS z1!M*MMI_nSy?7&~2K?9bcv@CgH2Lq454hGQ5S&%qrH516Gpfyk+^~&J2rJ#EX z%#A^zZ*B}aIh`FeJ^{kY%<9VQ;>PC6jFK7ZIcmE6dK`NE`f8hH(lwBHs&ZUfr?@l& z-@ktzYZUt&N-^60{JHZdsK508_x~?U#Z0>x%o$u57J-IaTp8TJS(Hm)2ZKF4w4e=F zMBpIjd~o0xg4!j9pfS{43@!|e(3I$+59(in7DIC}xIlZAhR_3rL2<;$-~zTyS|2ta zCd8n`U(rBi+U_+@ouDtZxOSPtfQE8#up%iUm-b12qvqX%V!~gGm6ARzZCq zP+A3vgVH7_O@h*<9C-3`2ZOx84hB%#6b8-nfG}t~8F;=2ls1{c+u+QFm6;WVm6_eF z`7OB3`Ax*@-|9>(tFg1hs{>d|xf(DOZ_o;&xq9fYu(AEti#MnVek{xLn4!Nle zPPEW7AHi*B(BYP{;Lrh`*$iqTGl6+xcR)v+GK0p^!P~9186k%eurn(QgU)NvW@J`= z9`6;Z<;oSD7A%$47TEfDZHo4KngD$b++@n7|GOP}u~!C`#r6 zXahLxs(3ljcsp#P3uvrEjtNu%nS+*H8iUSPg4|}K#{_S?F`Fy0GuE()yIDB;$(wuW z$ZE<6va*Xe2-S0TaT*I5xplc}7_)FPu?fkEY3Xq=Gb!H{VmvMMN8QTD&pOgtRzONc zxHH4pIOAVTtcJK5w}ddSzA~Qxhph6ye_2_ezLM$xFHD6@#~4&WXCN^^Z~8*(Js?s9 za;XKHz!3oNtOvDFAh!aFG6;Z&T6Zvjw*$n3wsU|^_yFx_Vgw!5r_UhDU=P~f6bm|F z4HUqjF1rK+>la6627U&}HfBlGqbcQ>#M!~kSx~WU%&rV7xIk?ZW=50x2BYYDUwuz= zHNG;12s3w)`}|LMp77sevbNN6l#LP5we;Yb+?=ww)Img9PtrZW&LYYq%Fa|E=B2X4E9_A-O^hk@oW z9GMyJgZA@+LIb*~8MG%BvX7YsGSdnjOb2ax11;9&zp#S=L<@rkQj|f(1!&g}2(yC= zi49W>jYW+0r|7F+TX1iZNLTCpCT&K|GDhitPeHis-*raNLJ&p-zJ zzzk=xfpmZ?B6bE2aMu`eN*pIxFKBBz=<*#G6ixkUX%M=EXtCbiSm`cIt6euvLFlaHZ1sx!(&7cF$%Aou1 z#4kYZy#p2DAU>WN2D+dHoESj4LmnJ93Jmhl1u6=lF`o|@hlPTAf%Xgv4E_uX4Dk#K z49uW30y;_*d`=r!eIcm5s{l1d3)HbZ&!EL{pFxY^J%biV11Cd0gBC+SgBHVl1}%p5 z3|b5tD+N@Rl75#R;ZV2h->kk=qj&c zN?~+2mtnlXB4_BNQ6a5d&17w_1!{jVGDv{W&_Bjt!tikygFf2XNYG4!D1VUa7%BMC zqM$X9pu?bQb}(@2OE@w!2rz(d8ggW1aA4qM2w>m@r9fsd2XyNh2e^;{RZRNet}`!# z6obB?kY()-241lCe$a-?SVw*a(1D2Z47?1m^Ar6Uco{%vBjz*kGO&a45a6M>RkzElqQEY5vF3!iusA}eKD#h)>m%IAkcRt22<{)KjSzEOfYeQdC8CDi| zzMNHzjGTP`mT~K8d3oAu=JLoIJF6>628OT&^6&-AO2w&a$mrQA2}tvRghXVtG@X+) zKx-_)XTBL7cNj*vKi^gjc@ELA2k}kx!JJ zSGGDTW@)WwYME$=sk)RF-#=w05d}?gRb3Aw=~PBfGa1J79PvT5Y79)E)#Xej;B^Yz zpgVY38F;{_0Dy)Ok;k&3bvz=4LYoEP+zaYjF)%QG0Ufo(`UP}Y2dGiQ0$PecXaDuRY6Kn13`F?hHFbi^WTc!FJ+(ROCJP@|a*-xvP(Y`SV)LQJeeml#Fl*?alx zxP9%Hf6viWh*4H#%E@76m6MCH4i)_e>LxQVGJx7y?%@90Wl)umdM7(ZYD09*c7t0W zOyC7YJPe@vhD95+_5iee4iwU$5D)<8Oi*hMbXX*)4FWj;0W_q;3ZB>kMJZ@EHAn}K zz)sMSeqTV%JvQ(Tb&!K#EM5kP4$#yF%oITeM(E-RX@Q*#ki+T37=)l_=8Az<9738R z#^7cMKlsijW6)v#X+A>U+&)6ys$Ej+1lMt|6HD+_cJ*#Yct3&Xfl|B`fhUY^?INVGpM5tq7i*JXy!xs8amYk$&D94F};I<8F~;F z2b2aG1M)AZRRL6%sZH0q{V@jiigN5ycf<;WtCNi2^r$h+{ zvss#(u(L8TX=^wH`xx4=voJA-27!0U6&BVC`k75qVdZwRh$!GIVPaO)l;h;*P?G+~ ztD-5vDaB>Uz{H>k-Y@!rL5;x>G*=J18FL2%sOyPX;RroK3enp|EC7~&A=@ZY?B(J_hImBF8L6(8#{|g2NrdFmy45Hw*ot6w6 zK%HyYJpiCo0%}ZxXyXez7(g_}O~i;)1D%yc2tb=;2m$E+esH=1ckK{W;BE#p1{MZ$ zaP~3>twgy48P);q^aPDYftm)OKvZD>%@%?(lrD6s7-$uUIdqv08#^B}qp&eIBj^Y+ zWobrbW=7CJ8)!@fd}lGJMZj)u%+C15*ocLdO;k}=9>(~4wPc-~Bu{cUgksz!Cd9_W z%Eu3(YI*MPFuFNe2^vW%+vrHb8UJ=M?fSRz-%qBnziy2FjO>3`F-}rb^OfP_6NXaj zeS8?07jZwbmy44@iG9#rRn zx;XJ*2B?n)#e(2|JZQNf1GlNaUm zLDvR@#5fq38Pvgjdrk(}0T7@I-jx}+zZe*rgGOi|_lH3CUqHsXL>0|V^%z0TU(uaH zcFL;O25S5!Li)Bo`gW-v>gpb;b|Cs+^)q2xWo298P(cY16(c!WMH?L{^Mn>Zzm^1Z z5N&c(P*ha#=Kufyq3cYA)S1QqXEQJes4;bd_lc=9Oargu$0e@`lmGto}2^8~D<#%D|S0$hyQ(h9qd{lXm`BDVrQT?xmVn0Z~G>U#qc~t!xDEdMEM|B^n zJf``W^0FxQgXKZ@GBV^b*)aJq@PYQJa4wu^zGusCQ+Oi@%(on4-h zUHyB2jSgb~BhSAt0gO5}0q>c70&LV+SgKf9)NBIYgNhRdTLuOuX{KWgd<^=aR1Y;1 znscFv8!ACI|yNc%Kbub+tOXqN$>|Iq39q#zcrIUl@5HrsN^p0dfETFHF)* zAg5z-KStXJ>LCHpCL4Q3c}8<}L1RI7aeGE_c3WhlAA{{>ocAvYVlSj$&cM*a6v3p& zAPEY8VFu_~y}KEN8JHPZ!S08IzcIx1?CSQ6>g>vpsJIv)r^6V)5x}S;7jO}U!=x7= zr^dpHiQlF zbrZzP@4-O;7g0ICz-=5;BhUy4=#U9Wl7hsLhpL5zs$QtQnwov6 zo(ClEm?Xims;;A>ULR~};L1^Y>3aA-CPx4T^^DHdPbY*4r{4C`at^D=Nkn9%!{{@_0 zv>6;gv%QcJK3KZc232|5ph?}`4Dt-1NjDa4$N(}E1M?TqJOdMW{AmY+mOiWiM4UQe zYGP(C&L%1XzQPG|LLaDLWO59!V&a$9Qxw+lOtZC5^VG1i3kj0cvQ%&lRn_KaVJl$T zRlv={D`DywXzE=SsHGiH=oZ{+>E>6f=V2(7mTDWX&Cg^T!srf)Hx&0vLsy`~+^+?4 zzZS&(poo+P_rgF^0-9j=Gcss`2X#QFUxM~|fC^jiDtb^i)zk!ZYznB~3M&i1Rhp?H zV_X3n3%|B%sH=jdmSj+fot1`Xn!RnBr-rbio-{v`RRE(qV{wQr6Tf!6ZEBj7p@&|r zpPOZCuv=k(wpL)7w`riGsRS^RGG6gWp38^z3W?*2rB*4yekb&X9KB!HK zBzF)hcNikazyv-g#R7bGzBXvj4hPidpg|ktHangXLC|tZerPYALmxDm0~(3tW>5x? zRn>wH=mlTPwUa>#d<`XdAt9)BE+%fS$Ea=$iD<|)G$boPb}%t=7nZ0xMVVSS3v%$c z6`oM_FjvwG$Tu}`;NoO9(1lPu#c2+C0lLPzEP-9lk}eTBj!|t9I=Ob%=0d#MdQb{< z4=L!JcsVA}(Q}*(o}eAdT(B4eAN>i7X^e4Kl#w2=Qc#x^G~5YV+G5UVEX=ORsLX84 z&+z|(gUH_xj2Rr8@6fwX#OfoPORs@anGAnm7-YuP&usdNQ)2_d6Opbr|gWDPZ-~YE@0=qfH0(wvb&UXp=D)1G6^hk`c&#FQ7$5pyPo+V^W;pxp&a`GwASZ14C9t zb4GK}f&oQgc4kG!v;UOD7$q6ki!$C5`L}PO;5SCA6Ag^MER2Q?mH(c^|9cH~8-p#w zcP3rNY*0-JiV5gm>fNAIzCrV&(7oceB8+!LzL%6h^r$m1FwJEGxh@DaNB|i%hxrE@ zLWr%#7^4EvPzHw~=pZX5aIYEE-)FmHU}(&&Y;G*iXlyQQ%(%uhKxD0i5J&Wynt%Hk zyQ&V~yvbPk*A3J@<7cR2ieh}iAkSdH;K)!28p3vBa0Uf?w z4F)y_P=8`Kg8~C+T_vc%1I;lSUI3lkssUauRl9?MM_&Sx)j)HOAm@N4@<3O@T7gH% z6&S1pg)D1!Fvx&z6*Y(Ky8 zd0u%#6?>;tH8}x!NdYwlJ|R{KNf-qdV_;zXfA+r)Qx$lxcsOYK5Os|Mbc_aLbP#+* zFK9*{bYSoao zGbNp;h&wEBlkl!0zK|=R>xivgJ6M_4IR+N}jSPXDT`0wnz!bp5ETqPeC|oSS#*p}~ z51d!E7*v@Ym~0t@A+s}3ckgCkXW#^{9o!ABOF=Dl(2T$i2GF`AcIXCe5DnV44qK%K z8h!%BD`b@xXzwa$>6Nm%BIxQz(2x?lBD;l4kjUhq%OZ?CE_G1mzt7I9@g+qi@nC|1 zi9wrzfyss`n1Pvrn?VOufP{(CRNI9dFk1uNq^;eSDlf=TR&6>~f1$`yg~F%yFh0|S!} zxE$tYa7XtG8{8h~v;v}3M0gGo?x05ea)De(O8&K-5j*I^1>2Eft)4(J}7TY zW!sp_7|R&@Z}Gpyl`kH=0386p2zEpS(=7&026K?@kXnENbi^jKw1pOh7-b2h=(u2D zC}<8YDa09-+iguatmicT`@Dqlnd+_$8yP!v{(|~rT>oD(`7vDtZEylj^FU(Dz|dTn z*<6}YoLQKio003%rTi_M^1-AYqamY?+rK}IEUy2~|GVPG$nx(GI4#OCcrpbt#W4tg z+LA1=Hu!D^&`20Na(hnER1w-OEe5yX{{3WZgf<#2z>UVDsy9e&H93Y1ra-1zh`HR@ z%w>dhbrnq&p?zPbK#0NhjOSJ`qa$Q&AM=UUsmt zjE(<(LXCyE7s*%#khvL5RxtM>g#*G|n0rNG?qz%mF_`^dJ=9=`dyxV{iNWu`3KJJI z4+AHIFoQUQG=ma&KFE>5mBEw2mm!cLlp&HKmLZWLl_8TMm!XiMl%bNLmZ6cMm7$ZN zmti8qRE8N0OBgmV>|r>?B)N;BkKr`K8E|dTC$NK|545uC!VU%y8#Ja2qCrz%AR08~ z1)@PyJs=uXbAV`FsCs)S?FyycptLWP_Jh(vP&yJy$3W?LD4hbO)1h=Ol+K6JUy&kPEYT8Q8x-7OjEKS_Mr!f!6rQgVuAXn~O7oQVnQ>6(nI`J?cfa$CRQ~q2(YunYqQ{pxPv?PK64B=A1xlDf5`#GYB!LG4z6(hw2O(;4&E0 z7!!lHC)hzfcyNAMuVxj`)sNRtY3-ZU4u^8jkqfm(Z@1`?=%2d&u` zX5jd;lR=Jw9kxyo(llj<_SHa9X~Ss82s;6cjh&A%RaaP-k2}mSRX9tS(IRY;^D^g2 zj4yRn8JU<^H4SuC8Ch6087CD5+xr#e{r$r@>0cg`hq4qi3kM&!tg5mUE3cTWDgzS( zC>+w6o`A0??*oN{D0tB-$iX`pL`Vn+=wf0-NC|*~P5?At0c{~6&qabf%?J(-(B^E+ zfZ+orVo<9W8W7+m=Zwb8$}n%I31!I+^1G(}+oT`B=uR)n}49y*hbh)(48B6ubpx(64$NScj796DgZrVm=a2)Yax zG@lCU;%dWJ@j;fGfflW)K{{bdpiY=LIB8ph;t*0?vV(6r1Pv>*u``2D%Qpv2se^iA zY@l;6j(dkU)yA_hGU^!X+ev%z>sYvG7>C#>urM3+@Ux2fGiuqhafRxdvM{OexY))< zi?Q;uDQYOmdJ8E_3y5kvXbC8Y{JR%zWSXF= zGhEok;KmTa5D8BGZUQ?Pv@Yym0MWMa*oV%%B0@<46t@Zt2mxrYfjeRf0Mme}8CFb17!0a|))4BpEI z8pN<+-~`ECV6XuTfyUC@!08AyrRxtKC=v%Ba$^R%1{Qpr9&FQ)xH&X2ffmDnwmYkX zd%V_+MxaVoRYc615j@zh&B!K<+zSS)y5|&WCT45FC?jua%%q~O<`QpVq|VA}s2VL) z!=#|3<{EFQuf#8`DVvuVJN@4mHQfYbeL2ldGCQ;k*qE7=wII|LerYvvV-r3$PDL3V zZ4P!32VG--a~U2PIgM;?5nC-|e=}JwHcH3~mg;pg1&T2w?~XhqWnqb>t2P5FLm+4xy_`u*ac{0BDmiXh2R49*1g> zI0Ee}bYx~IU{GUVEZoK51v;W0Y_AtY-U}Ru`mi|EhsB{jIKn_@5rI}O?O*^!m>oFk zK=B#~i`PH~##l%v9UghmsDzCDfj3n^MnzHLj~OG%giX}6!DkwQ;uTB$iE9cT7IbH2 zMUA@BAq6pM&=9uD|1Y3(Wf=q+ zq#0B}XEK16rGl0xKsIE7x~4MlID^j1VDHU>R<=UcQG>!y2D~mx0($-q=;|xTPWfF7 zN(@5aOa|It3t@p)NU6cb@IgVzti;E}�=$$ZQ1KQp+mG$S77@s_au9VjpcIqwG`8 zqhhEe$}Yml&0$aiVgCEdVZhU08@8n0SJgGy9z-*xNb0-jR6bDC{>KGlfya@deimW_ zo!S5YKcs91ok_~{9&}`qXbZ3DW~03yy9h9s^II!hKR9)cteJ{uV#&X5Tg zHv^UZLTX@fcE)T*MzA|T=1U>j>kL=_g-Hr@CM;Ba3S$UDJ?IVqus9n-0Z2W#ZqWgm z&tT2q3A(9Bn!$^~8=M0`+vgnNu?Ibi50Sy3!&l%D0dU=dTxx;^pb-vM0UFN&m3E*t z$tvKns~rr&`a2mM!7E6)7;G3E!IpxKUx%EL4@w87NSp4>)xp&Yq)ULS=HX)oA2kZv zq{PUKQUNixnYlQ6Gdh4O8&lksk-DA)yPycaG`E?s^%g{(bVXZB)k%aU+Q<}lW}0u!o8(Fkgm=GwUFUYl6-fg~SzO6q2|t^FFA0 zc4l38+?p{V(wZ75t%1)&2aohX?Xd-&s|+@eommgA?+cSH==^J_xIT*bOt?J;NaCxI z%rSzCn=v8Mv>IcG2ITx?WcyW+^qIi*ePNo3WWOnrxGU(KZK%7#kR^#nLieFWt- z#xNytUW1AY3aK$c#6wWTrG(U&K7iE+A&Y~~$%cxDgTxtF807xzFwJIGV*qV(5MhvD zm<;N;OG0=5fVw51jnW_*w3Hh}Bh~~%Q$HeqK$lb?1fZwBfm;>e`~e-X2RFtz7+9d| zr9rnufSQn?(>S@HVxWRl~pBpzXn+L0@BLK1OD9adyzUlafhclNbez zyrgB5f_c*>#qfo^EiUT6UR>1A#Pavg6>~LBk9bz5l)u-*BbaUW*=*3;XS3n|e+HKS zI!xA}a)dDe6h;iCnV@zRBLn~cFH9j!9~c}Mrh`t9aAa@-&7(qFztE`#MEe+{Cjo6# zfipkYw+^6ImII^(jc71~npcojuqF(g;FWDV7)w(=rRa`FZtTR zz$vhk!5Z8&0`EbBE$QS3t#3o!K@FO5GzK??WZ$3dy0| zI+C_hQsSa~{9=~c>TV|TN~->1l5&dTQhfa4W_tDk0_^O%iqZVUzkpU z&v^a}S~16LcacGWo!O27yiEW9e+JqAUzps%^17gSV77|^r)^060=nY`loz0R8GAkP zg~|9n^&LlZZ2Au8CvkTap!1V9UALH=cCU}E3`*HoYh zG#2nOV+QbyC}^)W?8q(UYhen(V)=18-rPSKHylv6G5z<=m4T6AFJm_2Go}NOGqa4bkN$bwyEU?^;E#|S&cjSaM9 z96VkEY7iN-E1s;-lw)F)5D-(~b`f;4wy~2?(-E~_#{KUXFLm3?3j(5oqT(D0L|dfKEsPg|;SxFnIrmfgvO6*;;DajOND3 zZDhzm9e6_ks6)iY=xk;yV8^Z`D<#Rq$S5Zv%`Go0#l*-cCL*pWs3U8qYvru3#;7bG z$e5}ouOcKW$j8db%r9s!$j8dY%)>7xY$PqGucn~O%EGKJt#8A?#ITDooAE9<4e^1_ z-{OLTMg`uED*q~W6UeMW| z%CMt7mF*do*%^ap@+`?x*5T6O3K0=y&0t*Rb?maOeN9~*IGsb{3A8OT9W*Zs83lsH z6LfR{;WFfS;snQ&JbXw%ULUmI2IM|A2H0+3Q3fFfPH@^67XYnefSth#EoaSPnH+TP z9B5(>JYote0vKK7brgizoY`cgq~(}F$0Z6%aJlO~=UF4k&a1AVVcskqMYk#hR9an5e-I9dj}akYx$a>BtbS- zK2cc_RZ|glb2$kK7A7HaFv*x|ZK>j9Wgw{Ur_014EWmA~C1ap0Vk;@m$-yir1f>`l z86GfZGyY}*ts$`o9nyw+?iRG&fQUC}0WAhP7!lN3U}RwVvJ*5T3px`Qv{OcromrW2 zS`xRX7$Ymsv;?LK=T^^uzZjSq4l|}Qz5t&wsmovwxjWGT_6%K|9AGEYPisJ3)t3gIoz7K7x#CKr0G1 z=!PE5bD*&t%jsn6WT?g{CM&39tZZYgqsSZ8? zJJ9|J#A(JDr8gqoK}u?6aA<*c(?WK0bLvC#5U7bM1wau2(?5qjD? z2~Bfy!r(Ng3`vQ?;6w*adfBLH4w?`*Lem_S0_6lLDV;w}Syh z>%x12(BvlpN`By$9n`1b>2LvXjmQBlA3zO92Ji}~9Skb^pp#%hWd>sH6Uci+whws0 zEdyfO2W*gr0fZ!?dBDKP(9giYIGNEOawaD$Xv!L?tqW}*Ap8q0(ZT*DrG0=nl2dsW zqJ7{BYaTE#GBh$UFs@<@g|rVOv9}Kp?tr!r!0rH*Y{aw=KuusoK?bWeq3wfSob3Z| zT+IVUhL-=o7(wYrpP?93=U{CcAl!_cR$y%dW$=j34hHnL0W*Uog8%~~-gW`#{50^c z7tq|0DQJw0K)ZmQ$aVo6QOyDdCWbl&2F8Wpv;%GzfYxtbyeAY6tVM~HR- zq~@0gwcj2v$TNIkkY`{54SzE*$TKh&64zP)oybeLwZK8RwZMkAu>d+BjG^!UFUEC@ z{@`*u8dSAHR^vhznqizJg5CF^Tt%q804>c3w->lgr{<7djWfBlHXnchcWWNIijIYRC|Gq#P$Lk zxy=OzCeRHxjB6P~!Ram@wCWF*?gYWdkHKOI+FrmGQmE|(Sm}tZy{rK;?ozgu%|h&j2bS z7#Q?H$H~R+U|_og%8hK$LZ3r_2Lq^k&BMS3n*IlG^Jn`4+8xRRZjgfx-v>9!)%6&a zm7qOZ@DY086FbsGWcdYU1tq05MJ-g2*i3;u9GtwOw$f@a8szpVOjb-h%zO-j3`V;c z7#N`U-hiC$$jktBFf-Vp;1hsAE(cBFaWb$$8m*v}mY^+`X7-HY?0k%j)>b}3{KYKX zN<4fDoJ#p}axqL+mUc$k*~}~~%-PzOR%W37Gc&_FCLP8f%$GrHeHoM)(m(}1>};YUj8TStjz?t-RYat9ZIvOkounx{vz{qCGqajS zssFzfeA4QY@ptS;$pKh(#HMLH=U;AIfCR@RE5SgD8U}gDEIwgWQgu01Z@(qXB6jS28FSxu9Kb&yO)rprV_uDl&qknmym{$J)eJiE|amO zmZ_}1jG8Q)fTW0wzJi&C_(F|CR>B9x%5Oaw6zzWYDq?(6#b1cR<@7 zK)F~XF*_|}4mK~{=4R*Q z=HYdeQid}?^UI($NWYl(Gq5p8Fld3-AhCl|sy+id=oENRlMTcGjc$V1Al-p17y&I% z6ab$cX<*2#ZZ1ybk{+H4*X&-$6&vxa>S1~_#Xd9Be#%Pd8Xl|^1JnN?CL_i$X3#n$ zE(TAA9Sj^GSA(j3aB9hC;D9(^AI9JZGa&1+IRqfv`av|;9nfMJ4saXOz))D-T%28; zU7cOs{IY*a-v3Ik+z$KYOxHB_nKwb-!fLVXy6AI#XnAan<`JxmBZwh5{Xg}@tAYCsVzv4es8&Q1mfa0v-Y zec;7INI4xEbJ?9*>N28c?j{8+P`9mDl9V@;a#BighdL0pj;#Q?j?EGli}1Px5@(=W zhCu0_8E!AAmw~(jk6Dpj5whe5dCVU3YPDEKL&1ON`SGND=7O~2SW`Px5K@i@F&Qy@ zWM0A`$)Lbc$*_Y#4wCNWVBrd7=rceW{9p#CwA{hKduIoOFx)4gd1p2TP=f-ra{+SM z9;f~;2GD6@yx^26rVlx?1axAkEQ1_$mPuCMz!0<`6d6lUc$q{#lyqvB&7mptZlPTNU(1MwT>7%n0_(- zXYOZ^1l_^GjI<{k*9P*Z7y~c#%tz4qNT6|Z zxU)ffX5b!TY=%2nL(C50dJQoWjJu&H1fAs+0d0GPRu32ry&R;*9ZEy*H>`mNqZt?(7BLGkK4Gq4U}n$- zwez5RtDvWW=7SFADs*IJNMHah{9%UP{Q)kU6xkIwgVQi`MH&Mm!%L=5jPsahg61AU z=AgD0A<+UlehjqGkr6zkiJUzct#wtIm|1mnAr#XmWhqucAq6EU1sYpyVfw|y#M}?Q zGcgFXB?{Im1Rbv}3Th29fKmdoO^)mg3=Ev$L--UJI6+5BGBYHAPviq_5Q40`0gX&b zgVr2kxCt}Mp3_uhVrEs*fl%up6qLvGOJ1CnUqDI`E5*RX(8KhUNt^i$1L)Ew8_-S? z$gV!fo*X8)`$0Enf>Sd1;2F?qw4kGCK)zy%1sy;Mu9D4G~2&r-yC+7HMwYkk= z-aXHb4Euk zTP-P$KoKD&1!)awsbB$hBYUPNat5~YTB=HFs;bIb3bqEId*miDJz?Tv-oYRUu^Tmy z;kO%SzswS|U-ksEU&h3+k?8{CZRR%&!VJow6Nwn0H`eSxS}P143{$X+bVT;JBP?_g z;R3!49DJH9s3pwB0Pc|qfDUEk0rwt3$5A5p#2_PW~vAA+`?VnJ=nV5Yka z&sj3S{YbEFpt9jSs0suXUi}OV;H-t5*^tu)Bvb5Qz}>$U#@)ZYi?4qRaaS6J?!w*A z#qKU}KNpL;5dB<;yCNC5859}Z89*C)9a$Or8F(1xGw?u0iO)0efXWU~`3Giz!i$GN zA1u9-K@8l60i79$J)SVTksf)0dlhxgFaXqbRQ(BtE+Gavcw!T^aASfz}jb^HZ@2dvt4FjC=6}m zfSZyiZ8CO5rwFukRh*AG0lCME(*4EmZ{{8FjxU^X8%qE%{|{w)%W#<`9ojc=fb|W~ zDk^X`f)x!QPk}lLpyMn-WeRc^0o19)Yy)AoH#7-t(qXP8W|I!Y{XbYTp?!Lg`@wzs z`LI5HJcI#?G{067d6nXvMS@_~$aa3>JoKn9Bx z!GR2LI5WIw@dJ-z6hp=_K;bOMFrPsVmOr3F81djC3~+HP#{e4G0EIKCcELTK!2rFV z4cB-E5hEB}@G$@!BN!}x@PP|B12%%e{P*AgcT7S|HyA7#oEW?r7#I~rl?Bg~a)H?6PNuoeYQo%^Uw?V3^8yk6D0$gMo*Efmu;hk(pgt z*jU(9QIwPM`QIaRGw1B<4`s4pd{xJ&eU{PfZ)gN=<9)D(=3)ipJJ!W7gMAV?kL-oJbQFJOvby2;?m5Wv8|2tL^j6a}Eu zP{1)^W^5!b2nt&8(QiuZZ0vl@?4ZrfDDDC;rercV6E;*IeOGxEWfcV%eam9eB0g;~Ar*O-IFOj4l#RBWksH&jf6M=^ zi1u?+mt^9XloC?XR8mk=2y--3)KgGXP*78lQjlhpH1}m-{Kw01lJPC$Dh5dg237Fd z8PI5&9iy?LsG<@Z$W6*hZ2!_Cl_8-bscEFbc;=tKa;iQkPK9OE?}MXKPOi>UL0th9 zeGE*0Z!`EYsWK%n@G;0TFffX%vx~Efn}cqnQ)f3++ft-n@`DdAw?WVH#AD^^5KP@5ch5S7Qk^!McdM~sh{7Ba9gh%zt; zGm0{r>M|LF>I=xQgSxr62;;ebMvQL%cKut^Dfm$1<@DUz!rY!$>W`Qf{<~fB@7A0n z``Uj;HCk^CI&~;$ooyp%kvh}g=l^>dUopEeFoSkyiK{b;v!XiOwT5wN&7VV}YxH(C zRK!7CFIsHHTyHhyVBjQmh>O)H1s-Hz{O8BOz_^L&IOyCPV`WBTPDbO@lm9ktU}R!s z+Q4-D?+qsKx^|}jjQ`Fs{AHdE9(!V77E~5g6jT<>p2GNN67%fehAihnn1O-m-<1DN zj8B<;LFeW&FfbYmDl;0%G0HQ_F&c}4j(-zj(`Ga^HZrqiG%+*RV+8FK&}KCKH)Zl< z#%IxqN{md5t}d>wQSJNo=_z=I1o$vAG0N%gWA^=H%6wWo(7VDyw?)^YDJP&@TCE|} zo0r!oq+VUBl7Z>pr2kEfk<32e^J>8MYBHKC3M!f^{+l#;GSl&Y35;C-zA-Lf_W5I0 z!F00{T!wW1pTf9?=@kPvxGYr^1$oI>P?2%X-vXuye+wp0W}3j56IJ`~KXcfBkl!}{ zU%?p1bd!OVfrEj8nNgHcSx}f!^%9f8-wV~%jLZK;GTr>Qxa8jgX2ZW(4FA8Q*ul=g zzzDSfWd7vIj16`FCNYQon+6(lWBMokzlkx2*_S~XbUHb+u_*XtY(7SIIYwDV(C$)E zWhFJxiQ~$qpnX<$Odz*b?c1j-$H>I!6AGFV_CUEFgcn4~Of)irz^8ZbYdCWfGGk6$S1wnzJD9R|PEckCZJpv7n-1@ye5C7g(GTo^B_j1ytNt6DaI(_=|Y0x=c|IYn?$7sl;$-vIQ&mhU5%)r24 zVrC>J!ltAGrCC9=s0f>qnkt<2@0?wPjZFk&ux*5`ZNxt#Fwc&0BA9E-I5EP(A;R7s zOn%p~v(wRaaL|R)3=Ay)>KQ_q1i^N4gU15kcB+ahnli#|0rCFT+lAZOhBJr!-TZGi zm}$p6>F;K!6*KDU{#}O}TnDPinE!eI|H=4^se^$HbUG^}RhjBCvVaT~VN+rP(IC$- zGyeU@%oy`;$v;1PkXP)%BvZ$~FD3szPT9KE1nl^KcO$^AXJG!9_5Ua1VW_zhptQ#e zDu2{K;bLNDBn+ZWL54FP{Y{dxu8imZ1sHw%w{x*&W=maupl6_YVM0z^VPv3v>|0rR@$-=imXOl#-L6Ud5?D5? zH2Ehk=L+y-l~P#f$jb0vHPI`qjkC6fRq59xRy9d!#eaw0^zX9UgVXch_Y6~+1Q}mJ z$|*)sMN?K$MN=lh-}hLQe&1t!RabWd6z+`wSpPp`(qNjwAjrTV%*hC<=oC#A&D9l6 z71i_^e=_R-yO^#jBK22+*(-?TA)@}w~kjFZ1ip*tfXdQrU+*x3O9%{ z35nK={ClNlYO1DcYRcHJW@4hIYG%fC{GUDJK@k4?(A>w|+{fI^&&=G%oPmM)pY8vj zjMEV5f)kV~5b2E_lq5juP3@m8IK@T(Tguc1NfZzgJ;}lH3#fd&^goX=faxg%8v_>u zgFd4!qdBW6yEv=*Z)3(a|3ZwJw1kZRUNx>PJ$Rt(PwDjHKcx&zf8R5RF+O6-Vqjz7 zWnf@r6=hY|Wdf6o|Fjqn{j;_JC3+@(#v}i%%)!j8cO{?7w{9(lP~b9_>;Fr}w@kA@ zr81+aBD1==y16jp+rPCx(;q)h{|PE+pGW`w8}siO1LHqoh8v8Z8Fzx(6pV;=BcrmS zsiKjYF|@Hb3%O14&xCO!qD=v9A0jm>82^R*pTbzq^ooIZa7#RQV{r{5jFw-p1>MKUrm8g77N}xL#`IyI(9DUZ_80S}^3BW+VinL1icNza@+@|CpKA zgUe!AiHuru#zi=SsuEBA)ol);!2;&;#zt@C3=D#dqMVGTjJk}hjF0}w{gY#S#2EE& z*}vsXYyX`u`FD=dsQBOCQ|Hcs`Wvvk3+nxW(wm~FA}H}O9#41kFkt5X^UcEEE&bm` zrdct*{_-vAZlL4?a;qGJ7?U8Z++Y@mI2J<5S%Mt0;-58BR`J%Y<)2F4 zK_~{0dmk}bg43E10|P9r2^xzkgO02=H5L?sBs6CEKYv<(|3;)Qu7AI3|NUZQt7Qb8 zu*vxM&3`|}8_aH?5|2@wQQe%;R8^dpkuk37pIwdSKTXC?w|~1B+nL?0{y15MGHMt9 z%s>1FR1UNKFJQdF9Lu1`V8md>z`&@)CeJ9w#xBRm&&VcbEUKht&!`BxmI#DRjX`I3 zfevsqH)dB84B53UNAKU*X#*55I?LI+cP?HwqL=Z-CCa4{4uFP)DXsXP3 zasK>&r@vrv(SAnLzll-_l8>LAWxD)Vy@F8)$$Ccdznej=K9+xa3=E9tn2s|@!&;oo z$PG?ZYehkajE3+E2D|1TP`3N@)#|t+LT6;->*x-P$IeA3-=s(_gsRYJzN2HI! zTAWaCXV)KVIlcZk1IypD3=E9-iM1Eh`UBf5s%TouXYVK4EE*@)B4*>oZ|5V`A{{T? zEN#og`OoZMO9Fp9v-Cd;#-@_2B}DJi$p5)Y=DOCUb8|dp{v?3C+6LGCGd&FmpbG5`!9O ztb!5T!W3s$Q&$E#4TMphXJ!t{GuDi1rY7dfjEB=Ag0rJnSl-8yC|8)T`FjeKD7Ue* z&0&@*K3rSBry^|fVkEaLp8j{j>C>jhx}XKeAh*0@5@5Q?V8UR*-~<{K5)&2SV+NnH zZpREhunDw3Qxu2G>=;2VV}&>D7&Y@M)hx`_CMp{kD(k2g+RTHb;sha3Dn?{yTg4gw z;-wNKnQTIbtB<*~z#!DsGH!ucI znF6&f`*e2pX$A(azjyvWVp3+h2X4QB+O?_-`V5u~&J4Z`;S3DS;*9E`l9K^Eat&HS zqk>?81~9>A3aG$Y#Hy zze|{M0*ue~1vSM5neM6l)lgx&0VXepdwGV3d%8zKX)blGKy@u0RS5O(I#d|DG$^c@ z{~5#kCZLNbK;r?XOrS=+k(dam^k5SFyXk0UP*7zM)4{)+Kz)^er$cIjLu$A{T^0tW zepgP4tZNdK_ZNXJcid(@N*g!2prhj+-zhhKkxD4(qGr;=CF#6ve z#~5el7)EvH7)Qq#qc~^hI2RW%2`=k<|G#6bWV#9NW9u?72!rATElsPNVe6Mn&aG6l zFjJeTWN4_QqvBzf67%nss;Q}}x`_$nc04K7E+Fskc}Q;sDSiGu4j)4kg7h{`bs5!> z>uJ~s+F3|N4IejaMXsnB|NdgoXJTUt0`=3FMU_pJO+klNDuNEN6jc^gHr?5J;zTQB zVe840t&B{UFV|ka3?@MF#_~__|2xKe;IIJY5^V+sW~`yXDynD>>TR;av-1DkYISpS zq_Bu)F8cd3lX2ePeOtg`Vr+~p)7k~(y$5$xvtS{?{ICE2JH{}u8jDru{Fm?cM7Lt}w)50rt!4dCd2^c>Cn%ov#eIsJdfxSHuEgCT=8 z=&o%nX#zCbjZ*HZgT}+LrjsLil^}ObP%<=7(oxZhRpGZW(}Jg()R<{4_96eyfx5P8 z1X7NfgPe)HsS={lvkS=c_C|9(s7^io-xNM}B+H=5z`z6wFC{e-6(|iZTlg6H8I{e| zOhKZeB2e}h(4dPQ<2leEiS55NQPw;>Lau3sdW>hIY&6|d4fU8b;X_FF5&s_A>Ivz| zYA?DhXQgXvtEZL(W`WwR!vA}jm|{ggjnUhhOF_qDG!@qz3{%v6NV>V0%HQZ8NZf%3Mf# zHUC#w!??7X@#NpLHjLl@W?F%*`*#nV0>S-jjsLw&B9Jz&Fe9jk&Il^96&Sz#tMX)Y z{}=slPc`GxDyAL(j+Fe!JM!nRKWM0#feB3IFXo^*3ms6eJHyS+l2KSy#z_Y}XQ3+ZqHpXfqo}ST=Mv{4 zud1vf@2Y26B38twBQB_-02UMEmzGwtRyT5!RumCbg-mP+3CKy9D9M>1b=@VHrDen+ z6B`O@3QC$v5H`{zhp3#KC}g@rK}|tiPKHIo64cpe{(Y9gkMSncFYx??4#o@x+CV@0 zghhwwTD{$fA^*Rn;0cV1_}I#ZUHWUqm_x+Y>Os@hZoRc)Our5VPEwl)n!uQ-HYxDX zEYukcR2~D7$-7;#@6^57+3|31sOqyYJ*0ijRh6|7))k4_h%w={-24HCo|{g zFo*qF#drfgdd|SW{OA4u6^uGeH$i77fW|>Uqs)r1QS`srHB7w7Bk6z6GaI6erX$p^l45rRGys{equIcK6)8iit=y;9*V*C(pDtt`m_xzR? zmL!x>o%2je=OF_-Y=1iczhjgIk1O*th%+cMXfv2#j%UN_Yi#4%U^&om-JeeIP&VUY z@E{%IV(^H>9|o|XE#m{Qs2$@&urPEi8Zz7sAz_1fut9G|=rA$^+aFE_3C4R&K@1!W zg5a@l(9xuj@o$j5a0h_eFQB>)JaCO7^M?~`J7X|-(4B?tuQ$`yzutc!Zi0$3?vAjx z*U_~H6`?x1_W9tVXBXHJ_6`t=fr0J!YK9QTKj82JwVyzz@F*g}OI;LnB0chWJWS+Y z7T5)w{%&Op{JZ7fE-=ro1j2=e#thIX8uHjYxXH!9{CgUMHj^Mz1i0-AnkRyeD1-dU zB>1zHtr~)vBI@g}*Vo^ut7BmNy#|`*@Q*G3I?lR4Q}b5>t0nfaWwzhb{=Z`qgpLP; zdi`RYF{a zaTi$l@ncw4krVidw*BC%YJqUuv$<4)?6&XRBq8M2f zh1t~=fB$A)b3Wtq9|6|s8IK=l{JZ#j2J?r{8R!3mFur;d^Y0U*PR!qb(TtJ0|2{;& z1Lwcr)BYb~5@b37YR`dgl^0cHhmX6PgU8+fs+cef{8>?58u_<`>8i<}@65rC#qms& zLF4Y1ZKl^o)!nmN0y7IThA$0r8>kNi#t8F3 zfAyH9P5x|To|#nWz~s+(Gon7SZj*I+U1WVk{R+!`1_stYf&bqzhC<^J(*I##WX3(R zk7&F7We{l)VLC6|Ao@=i9E)aVOg>G^v91OCPw>tE49c3`GRuVis-+8L2Wf8Gm+-CkmnhG z&1FqNsgYpw49vf8GBPoqgU&O6$~x#wgR!Zi=Bjz7sKob!WjFAkCzkU9{WQt*$#SjW= z>+msxR&^k?rFt-GZ#4( zbwwFhV|^ESRY87f8D%RqT}N3JVMa3t_Y9DmGThCiWppG(CHMqYn-1Rk<^|{@a}&sSI7+q^)2DS>2>A7KvEhWCdB>q|Q{5e|UfKkK&&{KrQusv6`UO zKseSqG5`Ml{}JPQCeZp7UIrQ1+%ywpHUw>6Mvn39KVy(f{yqk|4m3m4A^1=eIz#gk zc|i-VsT$@#)&Ey8YzNQNaDv;1pw%O$pfx0_f~@~mgQjbk4^~(I%ZE?dFvk8lKN~b_ z!~7@u{}e`VaNCp%G^Yz%p(4%*UU36)?~}HD6%S=Dzd+BZvK5uMpy-$A=6dFC0Mk6 z=d(=wov+E%_P2b7kfdfbVR#av`ii&W>o2-$6E*MMX?P_4f&u zPnw#)`7~h_le7S`P0&WVE&~JO?+yQ-G5%tj0c!a(DvGnKgJ+hYP6ss-*#GpfY~GzQ z^Y?El5!Lj67Z?rFRVBn&SZ8MJ{_U&5D0DG;y?`_#asY2{z2xxWXxll#Q>s7&jHA z@J2Qk(yajd8!{ix11ewG*+8usklBoX9)Kp*SW|w@g_wvssm2JdJAN_E0_7jb{4q!u zG`xfve?5Mj@%8r$Qzzd<&}?s_ubVN8)K^HME~erc8UmWv4GHyAU=(Ly0*Cz{rWp*Z zpfe6Zrg1W1B%ia^PN9xMy{@q$wX)c6yqBp*P`U@7tI*K z04kfs81EysJs{JGp!qz|Oelo+cQ?lDBJO!c*mNVvY^1VO61^-%8qWlk7$^lPv+Tcr z&42ztmc)P-1j5Tt)Tu}Y#^2xn`!TL(b_2y9Gi;m&HX$kQ!uT0dS;YLCSFHh>nFQ4% z?Tks)h=Cgc_!K462k;pmY`+Vkb3xV&_6*Jp?hM|bgIloA2Z1JCKp1uIz}%P}yb26P zgC;vbb3@1&>pamH;U?h(;b!oBkuPYzNTeAwUxYeu^ea^wG;bvJMT+qn&KV@6nH$h7 z5(vX)l0eg&;Iy>%zXRhV=6nWC(C8|oh?qFLD(;yOcJPvF&^~om(7HC(wDPdu!nz5< zO@h{*()K>WEh6AGZQc@gzEaE=7qrwzr$zj}B9$P;nBIRo&z})>lH~I4*9*QEh87=w z3R&a!C-46|#x>A2OrVhg@Js=#sG=#jca5~xjd9ICOUC2>EdO+Z2k{vj9b=rFVn8zo zH)?AifCuy8L;4KNe@^^=$LI}id+;%cfyxGO+XH<89U~hkVYs^gtNiN?F;*RHEUfMD zZ#$~N%zv)@f5&hYI?o`^fHBhm9wZQ9Q~GlSYz|`pgz^U)jy&;za4X~Q%MALASD1o8 zH3ewAUJ+%?9x-nJrukn#Waz$`v8d(LsTR=aJ$UFIK7!Bsd(Qt}CPC;pB%~h=9*txa zRWt|po}u&~3DCM(#uIS`v=X6JO&0PUM2+w70^mrW@To0 zXtOt2<2O;+qSe~1)zZD*)1uYQrqzN;;qL>+ynmC>F_XeSSH|*xo#+@;ceDID@qY!w zPw?0sI|DDM<^zx3fz|=TM(}<+S2KCmFqtyx|GijU&A9Ae6w^)6ARY*#4CH~g&Ra~WV(A8T)t#!=briRBC{mxqn`=jo;+;1o-Wb?7pImM&?GHr zbcTU}Z3zk+Ej<@FC9sdy>I>r&u({&;D|>x4^gETuPk2@WpBnZNym_NQ4HK$oC{*T8Wy zDuPC@%*>J7e((?|DGt!>p&&O1Fz|A8LdAs{tT=fgY(@qKQLX|gn~_0?%Mi+DVi4y1 z4rMbl2y*U)vRN3UIO{+MJFsvvFfd*M%|0rA{HGen}b1&RSC-GWJqHL?TG-X=VmZseFPQf zVNhW+fUHr7z!Bl7!(*B8FCnM7!;6n6@z)H48;to3`GoO45S>R5BCI!O$f3^@#$3?*Q*L26PM6c~~j@)=UVHm5Uy^j9zxfWtcz zY*sQuDuW&agL8gCWl?5&Mv1~o1tS9^69rEvUj^sfCx<&>@#tI=BsYr?f zit@8klS>pFOG`5Hi;6Xo%}pyUD#=JKQYcDI%gjqnQAny(h)PXS@XSjoEiNg_OfA+? z@XSlrQ%K7%Qb^6qNX$!4O;IRHO-xBl%FM|usZ>bJOHoM9Psz+nS4gcWNG-}t%}Y+z zV}OTw07DK#DnkW>0)sC@K0_WuJ_7^Df`FXV3I*T%ynF@*Cx(2690pJrdNPzSB!a^> znSsG6KPN@Ovm`MmGZ~+03UK|%x~-w<&ICN}V#Hv9W~&0obco^BpolOqGXg1rrV~sz zW48s>CrD;8Foc04znCEt>}gP@)nhOKLx!-_qTCa5cg=hp7%%F$_74o1g3aV&=!3m^I9T9O2LIHr1*st#0m_-44w==3vSR^cX-G zQe|MX7m>I@5t7M}!;r|31TJV47(hu5diU3sM z6*K5Dfbwe&gC02frZea>_%paM_%JYpWMmdAxaH@SClJ6_=;ars>-)R;pr;%ny@Ag_SQQB^K=2gT9t`;m z<=`eoB1088q>>mC8Il>Y8FU#yg+eYv0lXT7G#}Ez1yM3MODTY33DgJyHBvy~4XPSI zIzX3{L#oda|ALGJu|SRhc>gUY~=0&3o)W0sYm`9ek}2GFUYtPE@n z>lhe#8TddK?l1^~4sB)-VGv~yV-N=&J`FlFkwJz5baJCSg93vhgA#)> z=ol{sH3oGC4F*jHEe37Sl^qOv4Emspix`X;j2TQAOhK1iFjz2HGFUNKGuSZLGT1TL zGdM6fGB`0fGq^CgGPp6gGk7p~g04ek@L}*}@MG|22w(_g2x15ZT~5jn#t;s=XqF+0 zA(|nEA(kPIA)XM&ajRli6NO`0z(SJX@*pW6AY&q z&NG~0ILmO3;Uz;F!)1mG3>O*JGo&+oV3^2ojo}KzRfY_P*9>nNIvKLSbyO}x4nrQp z0)~8sLWTlxyBE|3E@LQXc*Rh`P{mNmP|Z-wP{Xi;VIf03Lmfi{LnFf*hIb6R7+D$F z7}*&)7&#fa82&Q+W8`M!VdQ1xWBAX=z{t-iz$nPb$SA}p%qYSr$|%Mt&M3hs$tcAr z%_zet%kYEYCnIQUwF09eqY|SsqY9%cqZ*?+qXwfUqZXq!qYk4k!!L&GjG$fC28@P` zMvTUcCXA+xW{l>H7L1mRR*cq+HVls#9y8i9+A-QQIxspiIx#vkx-hyjx-q&ldN6u2 zdNF!4Y+%^P=)>sC=*Q^K7{D0F7{nON7{VCJ@SEWe!xM(5jA4x7ppk#ZD8^{U7{*w} zIL3I!1jaG{$ts48}~xEXHhx-HbVmxr}*?`3%n(o--COEMqKWEMhEX zEMY8VEMqKZtYEBUtYWNYtYNHWtYfTaY+!6;Y+|^<@Q~p)!ySgZ3=bG?GTdiuW^7?> zWo%X6#|?W$a_@XPm$|k#Q2^WX36sQyFG6PGg+TID>H}<1EJ6jB^<0 zGR|Y1&$xhbA>$&(#f(cBmohG6T+Xv7|$}E zV?57zf$<{aCC1B)R~WA{USqt@c!TjK<1NP9jCUCCGTvjn&-j4xA>$*)$Ba)HpE5pU ze9riS@g?Ic#@CE*7~e9!V|>r}f$<~bC&tf=Ul_kKeq;R3_=E8$<1fbFjDHyaGX7)y z&&0sQ$i&3N%*4XP%EZRR&cwmQ$;8FP&BVjR%f!dT&m_Pk$Rxxh%p}4j$|S}l&LqJk z$t1-j%_PGl%OuAn&!oVl$fU%i%%sAk%B04m&ZNPl$)v@k&7{Mm%cRGo&t$-4$YjK1 z%w)o3%4Ei5&Sb%4$z;W3&1A!5%Vfu7&*Z@5$mGQ2%;du4%H+o6&g8-5$>hc4&E&)6 z%jC!8&lJEE$P~mB%oM^D$`r;F&J@8E$rQyD%@o5F%M`~H&y>KF$dtsC%#^~E%9O^G z&XmEF$&|&E&6LBG%aq5I&s4xv$W+8s%v8cu%2dWw&Q!rv$yCKu%~Zow%T&iy&(y%w z$kfEt%+$iv%GAcx&eXxw$<)Qv&D6ux%hboz&oqH)BGV+M$xKt2rZP=qn$9$XX(rPw zrrAt$nC3FgW17#fiD5ItHiqpCM;Nv+Y-Kpg(8O?vp`Bqh!!d?^4Eq`OFfCwO$h3%Q zG1C&JrA*71mNTtjTFJDEX*JUtrnOA#nAS6GVA{yEiD@&_7N)IC+nBa9?O@u;w2Nss z(;lY1O#7JjGaX<$$aIM5Fw+sHqYOO^^O=q@9cMbhbdu>5(`lwNOlO(SF`Z|+z;u!6 z64Pa-D@<3Jt}$I_y1{gl=@!#%raMe`neH*&XL`W&km(WAW2PrePnn)EJ!g8s^pfcn z!wiNM46_(!GQ4M4$}ovx4#RSWeugOwQyF$Ly=HpD^p@!z(|e{5Odpv(F@0t@z;KZ1 z3)5GoZ%p5relY!H`o;8{=?~LirhiQTnHiWFnVFcGnOT@wnc0}xnK_s_nYoy`nR%Fb znfaLcnFW{ynT42znMIgInZ=mJnI)JdnWdPenPr$|ndO+}nH887nU$E8nN^ronbnxp znKhU-nYEa;nRS?Tne~|UnGKi?nT?o@nN65Yna!BZnJt(tnXQvnM0UEnZuaFnIo7ZnWLDa znPZq^nd6w_nG={3nUk24nNyfknbVllnKPI(nX{O)nRA$Pne&+QnG2W;nTwc`1=6B5RnLjXp zWd6kbnfVLzSLScb-P*!SQuHDSeRK@SXf!uSlC%OSU6d@ zSh!huSa@0ZSom24SOi&wScF+bSVURGSj1T*SR`4bSfp8GSY%n`Smaq0SQJ^5Sd>{* zSX5cmSkzfGSTtF*ShQJmSaezRSoB#8SPWT=Sd3XrSWH>WSj<^0SS(qrSgcuWSZrDB zSnOFGSR7fLSe#j0SX^1$Sln4WSUg$0SiD($SbSOhSo~Q6SOQssSb|wXSVCFCSi)H% zSRz@XSfW{CSYlb?SmId{SQ1&1Sdv*%SW;QiSkhTCSTb3%Sh87iSaMnNSn^p4SPEH+ zSc+LnSV~#SSjt%{SSnenSgKiSSZZ17Sn63CSQ=THSejW{SXx=ySlU@SSUOp{Sh`ty zSbACdSo&EeuuNo`#4?#>3d>ZMX)M!OX0Xg;nZ+`jWe&?+mU%4mSr)J?WLdb%UM>itYlfmvYKTL%Ubr*yi5ZF0~ZMGXaJ>M*d22dlZ*26*b^Z%n`2T@YFR2< zBA8-#%umnHOU-6agwWj1$(cpTrMYQ2sTJJG2sW2x%rSd^c~ zmI9$%l8f>aOW0i@7O|&7Xf{`{O>C)Pipv$Qn=2L0WOs#F&z=gQ*<2werh+MMcZ5T@ z(-CYgcenvu>2M~GdvbnmZX(37Jn2XrZV!ZB?hFK*#Um-Ph$SN_v53vHBr_)^l`RuY zv3o*%z@7=A**w8sV9Nwk?4A&}vS&hQCQmP>%q(s%gt^>V2sV#5l0$g1Q8?*Esi}E6 ziFqlR$!tD}$)zQ!Y&l?x#|K$m4ibmW2OKhNIbe##CndjxB_}1ngv}4Eku48Q@%SNW z;mJedu=#;CvgLs(?!3f;{Nj?L{DO>BX4kxQ=G44&c7KSc+4CVZn?KmsZ24e{*FU2) zFFmoSG&d))w1hVwna36cww$d9Oz{LES<6#|#9<2pTh3Mlp@PB6*^0pwcQ7L8xQh{N zo)9DhcuJ5sY#|V1O28CL2q;QRKv5bBj!U*uFvT7U@eO+^gk}o`dxxzQOtFR*W#*-` zmV!v$P-H*xmLl`mLm^SlUJ9YP!x1jzE=OWVBC#tGY_=${C)ujN6ju~H?Q&JYnc)0u zXyL}{pOIgb$C?i&L&0Pzh~#nvxe{VPBAm(Y=n58MPfP{V-2ULMA%vZeVDkhc1szW@ z5(jLrk)b1$c7o8x<`CM{5<)vc#hskNd}9Mhd1Gu~2<00=X=5mD0;SEMG{ih(0}Ci0 zVy>|P#9m_qi223_Q1cC;<{6qo)Eh$OjiBZmLCrOSnrj3x*97V=6R5jP3?Sy17(n!y z7(m=*VgNDM!~kNBi2=kM6R7)43?S|^fx6EG>OK>w`%Iwau8Dyw*nK7jZV=iK5-ui& zkZ>`9`oqK!YQG`WeyG1p459WLLhUz%`oj=vKQvxUp#C>8goKZYA*9?lF@)N02({k` zYQGWGej}*=q2Xp?1hwA?>VIfBnn1(R1Qw1)Q2UJ_@nd2H34aqKsQpGz`;DOX8$sq4pa?{SQrVCdN?z8$<0khT3lowci+OzcJK)W2pVcQ2R}w z_M1TMH-Xx30=3@+YQG88eiNwuCeZLVf!c2ZwciA4zX{ZS6R7^33ZPp)IF9^_gF&RV+jopOQ?G+q3*GSy2ldg9%wtn#1U$r zBh)@esC|x5`y8S6IYP~Igqr6F^^YUeKTc5hJ3-BJf|}<9HO~p^UMHw~ouKY@g1XlU zYCg35GI55Q=L|K^8ET$0)IJxeybDy`1uE|Xwa*3Weix|wU7+^6K<#&d+V29j-vt^z zF3|9Cf%?w{YM(3Ad{?OXu2Az`q2{|n&3A>G?+P{F6>7dK)I3+Hd2UemyFuOW1~uOe zYQ7uPd^f21Zcy{wAm+ImyRn6*7bTXZvXw)qaBx+^Rt}~(!&5R-i&Bd-i#f|-EKs>; z=xPaehoLJZ{taCr@o(q~iGM>^Ncfn=Leh_+DDSN|l70VH?L{~_t$&=r#Y4P7DW-_X^~kS!9D zfGfciB$Yx^zo9E6^&7gnK||jS8v2mjVCV|T4Mqm$VBZ)Sn1f@@$iN&NYeoj%3?WsYks+k&Gctr!eMW|ms?W#}QuP@bLaII^LrB$UWC*GHj0_=FpOGP? z>N7HgRDDKMuyOEGlYhlA*4t#GK3T%Muw0g#K;gDUWSk&!^jYl<&6v> zS>4DS8gAy0ri76bqzP%{1ZhGVIYF9`Moy3>q^pywIWMfN1sB3_9wcEW>Luos7#Omr zqtHfNp!p<-5Q1sMlL$5fBF+OD5`b_76A|4NhzNfo%vuPaw=^#kRIo#MJc(fIAsncb zF_Z@#d4TaCeI^Kx8{Bn)um!+u6bFI(Y!F$fnQq8DCvy~@Aqo#>JJ^{{rpR)Rs5}=G zITSM;Em7p0AUvLQaCZ^H5kwl1fQSeo4>~}E1Q6pB5CQhgEU-`6Ae2CQYEf=tUP@9< zF;w2u3nCApK)yFHF*blmLU=AnJ~S|Kf%={k9MO5DxkViLd8v9}MoD=-n88(&QIrZ1 z<4nshEdmR2re&5v#5jsGE5Jfr#i?bfc@So5W_m^mn8}%!2{jO6M@n9PF4zt*17rt? z0ks3fgxCRLf$RXWz;=KbP&>d(h#ep%#10UP8!7I&Ai-5h)w28;K9rjKl{U$%jx6GM5jQ4Im>3NUFhhae+xa(A=*8 zgbB`%U;%E37BHI+-qQzj;Z6ke1d&G@z#?2=r$U%uSAm&GmV^0RU=Ko=V84Kw{7`0P zQBFL_UEnYT%kaTN6U^lSjUgHvSb{kaT?lI-e1zS6U{gUMf}|YcV~{06uwX)%02e~I z5-x-=R|FB<2(3thU~9ovA_*e23nR3{-6jlk3sU$aX+}7jH77G&&ydYINY7Bu0L(E0 zag3lGV-Uv}$}s_POrRW75XThCF#~bTpd51$#~jMB0C6m!97_<#QqO=5Y^eb|$W{Xw z*AUD#gmI0)Tq79Q7|b<>aZSKn6BySN%r%8^&A?nU7}p%kHHUF6z+4L$*AmQyxylIa zDkGSyjKHokg1O2F>?$LetBk;|GJ?6v2<$2&n5&Gyt}=qT$_VT#Bbcj?&iJtBk>}GKRUz80;!zn5&Gzt}=$X${6e_W0?&iJtBk>}GKRUz1nepkn5#^{t}=nS$^`5x6PT+^z^*cZxyl6W zDifHiOu()(fw{^A>?#wOt4zSIGJ(0u1nepkn5#^{t}=nS$`tG>Q<$qv!LBlexylsm zDpQ!NOu?=)g}KTU>?%{3t4zVJGKIOy6znQfn5#^|t}=zW$`tG>Q<$qv!LBlexylUe zDl?d?%)qWPgSpBK>?$*utIWWzGK0Cw4D2d1n5)ddt}=tU$_(r(GnlK)z^*cbxylUe zDl?d?%)qWPgSpBa>?(7ZtIWZ!GKaa!9PBD{n5)det}=(Y${g$}bC|2l!LBkl)q@ym zs%Hq!<>p}5nVUg{&0xY{SDKqch0S5YVAq;kK!q(}!eCdMTSA2`p~4nmt1Jv4!WITF zVX!YO457k?Fk!H7ER3MSMlfNpuPltA!p1ORuPrilFD<~nw1E230_IB#urDp3zO;b((h}@TOQ`P0iFD+rdv;_Oo66#A!OFct&NNr%i0j>@V^xzDbj3GqE5GG>?*JA{c zF@nh$!DWmgGR80&W4Me7M8*UrV*-~kg~*t~WK7{QW)K-On2Z@*#vCGJ4wEs5%UD2U zEMPJga2ZR8j3rD49tMUGuN%U`z|a7u#}E<*hOjU&golA4Bn%8;VPFUk14Bp{7{bE9 z5FQ4GkT5WWg@GYF3=AP*U z!@v*{28OUOFocJJ5hM&iH841sjNoBl1PKFBWekylhk+3!3_$fVL21bxD09EG@8F&~NLBar3vqNOy zVPFIa15hOok%5PSF(eE?^*va|7#;@3kT3vM{SX;LJ-CPwBqJF^0t3_>fG9D7DKXN6 zDS?CvsEGhkVhmGa3|9gP9#FFZqQnHI#00Jc5>B9|1Vo7`Oo=I62_(Qk%?pSUGnf)H zJ*YFy^o&7^B^eo*88{fY7z7wt|Nm#;2k&m;VPIhB1ML@P0PVyQWME_vVi00rVGw2z zW?*CxVUS^9VUT60VPIsaWvFLhVrXEDW?*EDVa#M;X3S#DVqj#I)~VBlgbVvb_qa&hwsVX#XqN=#zNNG!_DV<-W2*BBboQ}c@$ zcBB`jW-}bgNz7MZxR8@rlE-i-H?bsxF`y(PF_STdfd%C0|NlXII~f=m@U07lsbqw@ zgW>;w1`wNzfiXD9NrAzD0VD!C&}2d=>}Q(#&e7p8Lu#2XS@XtoBNCpKq;T`37C8YCLe>zhhXw8n0&^d#lXn;fVJ}lEG5IQo+)|GKpmt%OaLlESp%4u$*DJ!g7b@3ClN@ zf2?e*e5_)uYOH#!W~_Fseym}vaja>qWvq3qZLEE)^H`U$u4CQCdW`iP>owMUtnXOA zvHoLYV-sVOV^d?(W3yv(WAkGRV@qSpV=H5;W9ws^#x{>_8QV6teQd|r&avHNd&c&T z?HlNJF?KO_Id(mEGj=<6H})|0IQBI5JoY;FHugUDY3$3`*RgM7-^YHA{Tll{_Gj$h z*#B{`aqw}-aj0?VahP$qarkkBal~=tag=e?akOzv>jAI?gK8|A?=Qyr$JmYxB z@r~mjCm*L6ryQpmrx~XmryHjqXB=l5XC7x6XB%f9=QPfFoa;EZaqibTmt`ncwCE#q3pwT?s`2Xan(?~v`tgSG#_{Iymhsl{w((BmoyWV3cOCCO-ebJyc(3t3 z;eEsVg^!7ki%*D8flq_afX|N4jn9uShA)LLhp&pSiLZ-q2Hyg{6@1(H_VFF#yTW&e z?+M>0zF+)I`~v(E{0jVf{AT=i{6734{4xAl{6+j#{2lxg_-F7h<6pa6&LqwyE(E&sjDcO)N;poqNqB|uIpHrNQs7#iQA7ty8$f9j zC~X0yZNPfjL>xqtL|Q~vh@27m0Fe=8g3@eInhQ$vL3D@;iJFLpiB^fu6Fmf#f$R-u z6uklE-+|H(pgNw2v59Gj`G^&WO@YXWErZglpfqI1H>21lhz_xBVmHKoiOY#QiKjtS zh*v;qP|E~#@J9ob-vZGg-XXq9{EYYq2@wgX4<)=HDkS`%G-Q`JqeK{32dhMsM3uxW zi9Hhcpz=`vOMHRye?Zm!k(80Nk&Kgs+9){*A}=`&O3#9*lbk2HPx77=gOn0PTnZZE zQc(9uK|-8W%0VhYszGXz)DfthGUN778vO42UU&=8P@hJZ9Q1f&}v z>ZDtwmq;IxekQ{KF-HcP0%YW%d?koF88sOQ2bKkmZoo zlJ$Xz%f>`xIzb%?t#)1AnFvRC~Q%o0wS)c1*P?%v=Ky|qM2ftVwvI$#a&SG zGf*0GiUFhI6{xx!ia(Sjl&q9uAo5CiP`U_8mqFAiRVmF<+NE?y33AE;qp|=*UReZ6 zOF+~q%P89@$0*k+FMx_~g3{Zd^e(8neaiQg|EVabI6=f!LZEa6l#YR@Q%O*1P+6dI zNaYDs{2P@11*QK%)TuJ5DycfDrl__-#8qcN={ZmubjuL~1FPy1)g!7;)Y#NCAmVC} zlN=b;?4W!nhRsxq)K96u(cpuKYbZcz6)3F%QKzA!;iHkG(WkKiDh@5RG>$>}r=aT2X}r_q(=^b8 zmJ^yO5P8iED4hdQr&*vmL34xVB~55Kp#?1`wV>ss7NneH)e_P&(hAZl(Snu}TA)4; z0|TSh8i;vX8zAOsZPB`-^+j7u8(L0iL(45~Xt|{w1W~6Qrd_5zO?!tnw4Bg>12IP% zT5f57fvWqVBcWrV6Qu(!Cv^HC>U5yxmd-SYI-OZMyL4{p{LzJ$6S~lvLKj+Y=~_V4 z>DuVV=+@}Y(}k83x|bm4=|amb-CIy~_jLd1$>}-hLCXof5{NoIXt||V15u~fptnHp zfZij0XgQ$|IpKp*A6jnd%R$uXE9pDwC+WB7L(2*M9T4;Mq2-qT0jRno`cDj44Acyu z<%B^LM4bV&+%ials53}2Xfs%5aKZpuP8j@ws568%oDEqZ>I^vyH4Hrrvkal-gyB4> zI%v6NxD2XpmEkGFS4KQW&~n1a0iw#gpz5IImeDJy zx_8EW#(Ks9#?W%YxCNrl7+P)__dwJcPcYtKe8KpW3ACIrQG=*6ftFh)dJuIcMkYZf zMJ7{Bpyh<%Bup^cY5S zXt`w`0a0fjV_suE$9$hTw45;i1hLl~T5g&Df~xyxA!lJ{kzfHWCoG_C4GU!PfSVPMRYZ-_-YXvB+0#RqJVeMg^Vclg7 zEhnt^LFJ+4mh~~PIu`3w)-P<>Y@o4k11&vlAgv4*8wZ;>n;IKv9N0kPzy{L3VzJp~ zbH(P9tq{ayTWCnwLP|^)TOC_3+YDQ1y0e`HRW%P{uk9k+1Ge{Up|zGBH1_QvvCm>B zVP|F+Vh7FpcF?-Rt_z~quFr0b-6=b04zz>jKzjy=tUZgplD&g{0>nOhXg;uq)R!#w zRrWLNx7kBW9s3thRd1krKREC?=r};zEDq4T?*PgBEDmW7Ee?wupdsi0Z8ou{fS^eCEXB1PwtaXj*czg6MU! zbBb}Qa)O4S6EsGhASFMG(-x;oP9L11A?OTkH#)09^g3%fdpM^#LqpIRnu?tvshGui zf%87+JI>G$bb;1^E|4Api;I|xiA#_RGz49sC5a2@j#dT+7MC8ERW2u7pdsi2&Cf27 zdY{FW$yLGC&J`Mhu8?z+7+pc<{W37HxK_AMbKT+!4MA6E8RiNp!&qG3x$(GZxj{qF z4cY>7OMv*uEyb@Bw+?qcrH5Ojx@=I)SIAB%f{d!BobJ2V8{ zp?Sjn5Ja#0G504POdik>^nli29*`Q0#lyxU%A>*q8iF3s_OHh}h+dCP9v3{`c|t?b z6Pg=6A-R#oQ^V8EGsP1cf}YU2+!NB~XYripxySRCCo}}Tpe3T00K{Z35icXJ0550= zdO^!2FG%}^#jDF}h1W4JXb5`!U;v%9#pv}1qSu?jTh80Y8ybS%(6ZPYQWmp#mw8X| z-sBApL2qbF&ie&a?;9U39}OR92>L+lL!UT^tWT0pgU>u4XbAd1Yb75@>y^dlhR-)& z5npHs`no|}+9#6ES5cG$(N&F#w7Z!gte;5BGe`pB$LrZr584$hxbNqMt z-|&ZqU;wmy4S=-6SOSCs3yr7znM`1D`=ckEI|oD zbwP82pdlD^4q|f9C5YajYe8Rvg@U0W7z}Mu1VdUBEWtj(S-~B_&=3rU_D6#ELG%V6 z3Vsm$F9aHbA<$M`hzUeC#3Cdtq$C6yf+5hhRS2XH#uBnFBcdRBBjO@zB4$NELofo`GKzq-j94PBM0|=Ah=hh6!ANN9G;$AA_CVyl$Ujlg5R8Jh5u+e|NtP(HsF0|lC};>qLC(r! zj9LNF8?`3tRMd-TXb46_+r`n4b}>t|QnW*KTr@NUqoKX(=qV7r(KDjAMPG@AhF}b| zZxX`=k&WSt(TVYjfrelVw1*G_8A)J?X^B}Bb07v9f-%r`R?Ii3-e0j2v1YN*5R8Sk zc48sDB9_>K*uK~`vCt5Vg^sDjLSr=c8R$kN2FAD};5iz`cu3vK8h?*nRRYws zgkuo-1ZbX0g4QHS&^}obG$$rO`}9fBP)LH73`x*FQ8E)GKP5x`pA1dW$puPet15AY?XuJbSjUw0_7M2w(%UPDOEQPoM ztOnU6h$?2d8b$_?2xu-0Srw>Nz{0=;HHQi8B2W#%jKx)qV7r*1?gps>^{ueEodxc8 zkSbONMg}&PX7J1x$PH{P?MUqDU^d7dFmX^gv9Yv)#X)R@4WMua*$JYV8Q8!O6jtae zKw_YN7H$>HEQ?tdu`Gm#9XpDDLFR)(1)_qH0mNqj#{jxbj9@dEz%Bv#l@p6OAlJZL z3sM70C2TC9)*8fQHkP?yzk+OGV*$0LVdAsl;s|>|Ap!~m5Dhw!MgTewiLL@b) z14te+!-!A;3T+k!W|sLZ^T2k4)G{!DYDIK4Oi&e!3?MxW44|_v5o$p0U}0bco52LO z3FJReYDcI6xf3)$37RE_xCW#KG)v6J0_yuhOlD*02m2Ld3oL9vrn9kt@*X75pqmL1 zWoA$SpRWV*1G);37ztL8hRa2@8XG1`q`K z7hMHNj0G%qm?bVKd;MK1j{orfb3xa*CpsGKz2Y}0P+>6m&C>baXm;6 z8w(_sA#8{bL2QI6pq3BFCm#L~mk4Ub1qj~QVD zC=@~I6eQ2c08-Dupo+x|h)?37DnMf&Y%HLD4nz+di!M@}c!Jp=vk>-xN(hioKr|D0 zn-NDPVOe$QDRBj4lr;Z=r$Q<;1015*{oPgQ@AYUQ$LvjHVOBG8c zJWgz|xRVhq4@u_?47ONQfXs!s0OS+Us5cu6s80*g!^V;ccRd>ms6Pbp6~ZN;RwyWS zfoLWMCkAe`I01>nd;*e1m*)nzDVSKwSwQ6p$TbWMpg9YK4WP0TR0@OS85uz885kT< ze1fn8;sTIQK)TpiKz&Mx9$0+>5eJQaL0ZiSdmyGjL?JD7kZaK8L1M7hIf#ue&%{y! z&Yd7R1_n?MA6*3_SRP^z1A{A0pMZKJAfJH7YG5e}t~L%6OCbv=oIvUr7(n$I!Ujkg%LtZ-wQoQ*8$t!b4u}gtJ^}Tc z*;qg`h!8z&ET9?|Qd_XG1cFlFrV<@>HmUCVcb5+1D7)ppMXZ=5jKFzO;GBB zgd?au1v()axz7u75hMkJN@0i!kRRAsyumI8iLkMN<_91pFU*G^eF%F%JWxIX(M&kY zCXfr6!Lqo@u556c2B~9U0L@Kd*Z`J?gct(@C`{2+fb>FK0E!dP7zP^)C}tpf*jPZ} z17kyc2y!#JDG(JPbI^N>Ak~oY0{H~gp9I;6&=2aff$I1SmUMX81)5z$sDPNj2$qNR z_ZS#LQSu2y1;|{83qU>rjRCQ-fJlfQHWpBdgt!yd9s}vaum?QS0x}1^6b7kA_yl(; zoCm1hNIX6PmFu7b&_N?0&fwGq5@BNjl>soHfJzJq8^a!` zPXut(d>|Ks#$#~Tj7cns@Hhe0X9yc0vBn6NhuF@*5R20%AQyn*1TA$r(Y zKJ66O$5$QS|0)#&mdF_2Gir>=Mwkli443=E*Wi7)}?6OcT_C7@G* zapicB4IrO@)(^0;_=4jEWC9zDKN8y>%m%p~VGoFh$nl`jJCJM8_PWQ zJX#$Ok%##N)LRF!5&A);JSfLUf?F0KIna6C$RQ508{`vE?t%1J85lq<4}=*AJ3y%d zWFi9tsCH*#2Fg26cw;uBD9kFWve6G)B)sb^p)#F@H4c7S{Wn%!k%0rhlXsSDDl z2bl)z-{bcQzET)e2BXV^Oa}QR0GuNsJ^}TEFnt2au^{yf44_^uLIuc0h&X|$0JS&S zSU^2TSUf;{1(JpN5Tp;?6hxfh%O?up@h6aL5&A)?8srmSa2*ViV_*QSQ$nZ!*$qlv zkUAQqo`C_>13;(%xd`DChziiyAR9Ai76O(}Ks{!duOMuYK6F!HK4Hh(?gGi;N?l&y zIvC;;P@fB7184*tjqb9ssDn#ekRCP`NG%C6jg18~UIB44 z!XA*n5T$TDUY~$uarwj@Tn9sZ0-9??*Z>M+kWV0WG{{v942?K_0 z37SusK`S3%@dNQ8$n6MwKs;oh;7eVgG8kPR;!Y+O7jT$Cd;(fQfUp6gf)OkaaR~zh zXdDKi0^$acUeJgeNIe4sXy%2D`7t<7Kq73+PZ4aW4?+6SO@YJ|69X^aI04DxiW5f` zP)`x$8qj_oaq!;1>P@HsuN0&k4s}Mb~kyVgsY%HLd zg}52r6o@DjLn2F14A#))CIBwBX3@W>pjlvq2@n;GV0lIc5RZWYG=h#$fv^JeBs>O+t| zgejofACyl(G^kaNUdMyPVLk!X@aXa&(?CA40M92te1dgm7Ay~mdjPQ6M+4v4BR!A^Kru8b}=76o?9t zIq3NSq#EH9+-+ea7SISO$TbWMpqWn$8^H3AG|#{QnkPk90n!U`0mvtyRkUm@pxH%; z9yS)xJOsp@upAF^8@eeF6_ELFkcsH>ATeg}>^QnlL8S;Nb?Jj!79e#D44^gi2ooSG z7{T%oS1~YvW}Xo$KrVuW7Q_W0pMcf@v$25YWFdOkSU@8iFg7H|gY+Rxf!YI}n}LJ~ z$TjHlATj*?6CD<9c>e^nW&~jZ$Uacn1(Jujgng5@DDVPIH^QVK&1?Ug{NfSAAt zmWQ-u85lO=@d?O81_sc2Z#EXt?r4Y}HWtvT9Ej^-;vkbT?1B0O)XxFA23;N`2I@27 zjuQnIP|X5T$H1@!#hoCRf$|AR9^w)PhOHQ zE{rJ;O8uakQG^Aw8V00}fdRA|5n%#E1tVA<(r#j4IEvFJAR9nF0qvAvWBv^G3CIMP zuOMuQILPe?dq6ydPcU;dL>}ZqP}zkk56X=op9r#mN_~(z1_sbR7laKE6^vkcNG@ey zIEm9IAR9nF0qs#^V}1wr3CILCX3#Dxh<+FwKGW#ptuucI;iXd$wN{E1H)Mq6%aRo^n!9ch{wPHS{Dl&kpPLnazDs4Hs-fr zAA-cuT?(-Ql;6;6Mv!V)J^_`{=<=ZO0>ue8xCH@n4FdycJtu|@V0lQGGB8}k=@XD0 zpf~}oc4ve3-9dJ+F~5epo{br_rv&0=412(JwRvIv%7G7AGKCTs~oC z0mVDSC!n4l!UoWKC{XGG$wOSlz;F|%Pe68n;sn$~gpEjm+yL_l$TT))(5iZfSs34&fp_JGEcK=BTuA-w~TYtZFEVtD%} z%zv1F!}}+oxJQ@(DxFyv7{T(e91j}1LZ|@Q4GArX3qWxK${}pbpi~Ia11rZsrm-=D zdZm!DGjvlRqM#g)UN#|Y0ku{@r9H@32>qaPg@u8M`6qa;10=`502(bsr~svMkWV0U zAt3b(3@=gQ1Yrln1t6b*N+&jEP`-!x1mY`@X>81(aR7*$5%xe#frx@e0MUC|5P6Ub znHl22D||t0gnm#Mfqe1}+_C`4F))C}r4cG1CNP5KVLdHSeSoe4q!;1>kWWB$A}nWv z>|kSttP=;B#>Na9d4aeY-4uu@$Q*oe0vfYLmj{J0$S0qfp*s>77#KjcF@_Cbc}Vy& zFnq)rCm=gOJ^`7=#tf>NAbQxCL1Rx4cd{{02A9GhUt!n-UTF*R33}NCQVmJNpf~~5 zD4?)I=m*6%$R{6|-^23>Xr~531;hkKusoz3VqgHZ4AE79%!RlBu)Dpix;~w6Y5%4)X~w0|8rS(E}^TKx)~T?}Ot6#75Wy8oLDf1Vn?z zGSO2HNF3%9(3mZ{JS=s+Vg}`6h)+PfDlu#T%R^EM0|Tfph^_*p7vchtPe8pwHs<4C z7lZ6zV+M_iLq}7YL90h0Y=kMGIeL&!Ks0DQE66-_S&$fLLHNX=&~R&P;UnNT3JT$ z90f=X10!fu1DkG;$sjj@Y+_>uweuh%Y|NLz;RmuET^~dRC_X@DqsxNCaF6ORg6B{` zY8V(%*K>hvf`t<())*K;V@GVvpk5rvE=C46W>8BNVmrF65K&Mb1euL43lam>2;y8ku8b2K7Bbc7fb z1=#>{6R0f*l3`#3jV2@H5k3N$3`$Rs`Uw&?Al+=tr@`(6sYK`ljo5-x9*AaSkV6Y6 zkSHWYFfqvCkY!}P4a#sJ%!tiwkZVCEgWLq_akDY+0lNcaHXAc&tOFv8a1&^39ONbt z&B&mH<|dFR%uPx-WEsKjCXmexjG&Vq(ai?QfJ_Ft2{c~8#ta(ef{3s&ZwI>%WDmMN zhzih%6UYQ~S&$fLjVtz*&5X>~L16^KjG(b5Y`Q@vgWLof4Ps*kjX6L>*qA}%bP(Io z^+7~Ic>!cUx-3WxU%mtN*Fb6*7(ugm*mQ$T2Du3|I?l!n8c~FZurY7O5>BA;Wl-FK zXwW)Ekl6@RL87p@!?o6(5nS$p^f54k(g-%)Ad^9Ef{d+zMsFY@urLGJgRT#v0#uHH z%tn_5iQy|VLFFDu4Fe;nWW=T$WHQK2pmf8=ydE4jAhm4Fhrw)AH-W|N$^gY+>lg68+J=?0k$auaCYjExyILJtvPV+M^NLedtxK8PrY1euL4 z3lhVZ#zEyCNDTucXr>XHZji|!H-Y9r*qC>N!v^GESh)aVqw9m{0kzXXW~0l3#6bNf z^j-p}1O%0tpmGnShJg{Zas!)gkjWr7fo71{m_g&j5D^#~pPSHYG>}?Y+~H~kGctol z;UI1Tt&73#CP;k;S{cN~44Nl`0Kzz`6Fi4Dv zfrEhqtwaT>1BDZ)y#`XlzzAA_1QJEp4Kay<5i~mrOG6-aY|Q(?@d#prFhn04c+EXz z1v;p01u`3579@tRO$RCiL24KnLF>D)=?0k$iaXF=5jJMfdKZWY%uOIypzDLV3B7Is zsfEQI?tBNz&k#3(R?nfE4U&Q6G6qJ_PC+(i(7Y=|1XkaH>_OKDa})Z`1&~^po47Id zJ%IKpfZ`6+Mg!T*zzAA#iEcJX2C_eaff2NB0#=`Zbi?u>$R2ckFgLNGr5KP}n43WR z2SDay=mn<%{)ddXLD~f%d(id4++>F*J=rm!%Yt?bf#MF- zE(6)jz=*m_1Y{G$M~n<0wG51))qZTupf!yU5jJMfj4i}=TyBcP>2wW2^~qsxNCK>Jp(?@wd| zhd)RS10$}zA|R7NZUU_XW@84eGl7V(F@x4-L2O626(R~EL1v@Ng2W)}qChNkSw?Uh z4J5vj9M0!^R95R|2sy%mwd`0JR@LCZNlL#PGFMKx-sG zY8V*9QO1ivHo?+3s9nRr2wKAq8wUo7urY&H<3U`EZYx9-)ItTBjV=ok!`Jp*4eGOi zFeB>DXk<5mTDlCN)AAW$V?7`dHfGQ&Lx}C@wn9WfL=-go0kR)m79@smTm`i94Wx#F5!ZeY zkjWr7L3V>cM#v%KDv&lOC}hxWg}DiR6)Q+BEbcJZz=Bc-DDFVxNf0-o?)^aMhK!;z zFoJe|urY&LQ4kR}X3*Mki0ueBfl3Kb-2$RPav-zOWkF*2%1qE&N01r@M$pL+*mQ$T z289!7mj@d&Xsta&gpC=ro)}avLV6L9J_5)s2sdHx!GY|zzAv?A>EJT5ws5kn{JTFptu8_Q~>L9g8a+I3`&8J zm_^qI5e1PTv(aTiV)*J{(3}=X4Fehbug$s1X9Dm2r8Aa=?0k$auX<~*qA}P&mbZ&Hpunp`e1HCukS!= zAwI=d-+{^)h?_t)8M@gZ8Cbss)FOa|6QsTam9h}q5&A%B35G#ELy!sRvLG>heNIpr z15(4l2x@s?(+x5i6iy(OuzD9{D=goE*y#EodO+a`G8OwnK}10$$ZT|3kQlyj0{IoBhJg`ODqzzMG8yD1(EK?Y zGpH2>5rMf66e0+Hpw=EJoIo_l?I5$!WkF*2!l@b5b^>8W(24_Wx*Y`Q@vgWLpa)vz&xdJ+&3HfGR1 zY>4gX`XHj9I0l)GE(;RFm!3fFA&?pdMo{Yxn{JTFAUA*lTM$mo^Y`Q@vgWLp~C4=QPNc#zN`XI!8==va{p!^9k z8(kJ8hOaCD?I;JSVPFJ}1Y*+-G8yD1@Xifp(8w4>gpC=rGJpw=&~R&CI$xv z2ec6mP+kS~IYB*1kQxR?&}b_*-5`@eZUT+gvw`k7WPpgUF++NEpp=8I59X!>G+RMx zVfikB0bLeU-htc%I@t>1CeX|ry4fHZM)27i42+2bc0L=g%fC|m5mv+ss$nfb05g>2z{X02vEKQ(V)>; zklE<6ATfO7zMwV`NDTucXmt!W-5`@eZUW5~u`z>2Js~1&%%Is=$Z8gZKG5g}$W0&` zqz+^@x-3WxpPN8!G>{qwM$oJoHr*hTL2d%g;Mt>n9Y|Nk$PROVWx;}^~D1U;? zMwbPN;VTP3BViyl42+;X8`yM%Oa{3LG$YE!3^`W@)c=5u--AK~T_4O%xK|V68-D_| z(I9RD%?zWP4U&PF&%g-Toe>P4ZvnXj7LOn{LLbOHP~3rNP-zP?0bLd(hOZn4rBIL> z21eAq7$7qtxfWzHD4amE{;={1R5GzKfmQ%Q`swJlLPS9!3^E&C79@r*?m%rckQxR? z(CPKxU)Mg2eFE zcc2*!kQxR?)ZG^#Ghyio)LUX;1g#HeV+PHxgX{vW@dK^WVnjQS4^*at+ytT-89346 z4kQXOm5G59hb$vAXl4iECeSK2gpUxqK_-L33AE3GjTtn{2N7Xo2F>V!*h~y14Cc)C zOz)X(n5~%Ym<5=AGJRqC#`K@*JJVmL-%MYb{xEYhb20NW^DzBmc3^g7wq|x=wq z7G@S@7Ge6q^oyC5nVp$~nUk4~nSq&!nVFe|nUUF>*_YXs*`3*g*^}9e*@xMW*`GOp zIgmMs8I=3Y8Jrnh7%~|O7)luyFf3$P#ITrQ3Byu`WekTHjxZc!xX18-;Ss|VMt8;# z#!$v^#t6n}#uUZ{j6WHFGbu7DGpRDEGifquGwCwvGZ``&Gnq1(gKjQn)?wCV)??OZ zHefbnHexnrHeohpHUp0ZmND=$C@^R-7%-SI*f2OTcrf@egfK)gBrv2g3CxBJOpNJFZ$K)TUV~LIGB7j7F(xu5 zGk{k0f>!fE*8egvf_4ZnGJ*=EzBpAgf=}p0xQ>Z|nK6Yi1!^kverg6L25trw23rOOhIWPy z1}TP_3^N&I7>+U=WsqgK$#9cFj^RDSdj@$%VMbvF1x8~=V+KV=2Sx`5B}OMkCkACk z7e*Hb6$T~-E(RusYz9z&l8M2EA&7yI5p-vZ5W^ORdyF!SMvO5G0t`wFS`0=ERt!!I zUJOADQ4C28Sqw!CRSZoGT?~^LW-;w$0PR4P2GLBN4AM-y!F)*&&D6;t$+R8JmjKaB zoeUC8TfuxW5Y5!dAjY%>%$EVtOq~oeOq;=cDG<%n$som02j+`{Xr@jEQHEMDUmQd; zbux&9(}xg)6oV4e4zP>_hz8rV4a^r~+Qz^PvWbC>fsa9qL5@Ls5H|7h$_(f5C#zjQ3f#vaRvzn(Cry)%*+g+z2}fz1S(5;k@!puQVd*7T?|zW z#SA4(-Ap}9y-a;f{Y(>()G#uzG1Z~*L2IKK8H5>_7_1ninBHTScufEBmu?J<3{ng( znB^9uJN`0?0n}n)W%>XsQ5hH+SQ)%vr5(usOfa{B+zr}O2688e4H*Ytz`(@d!Jx?W zndv>#2Lfd^-tw6N)Y1@UaAEKwQ2ydAa~YTzv>1dKJ~0L`yD?@mW`Ik5TxGrxgAao* zV+vzBV-BcPz)@0z>oW!}hD{8|Q0|oX0^cbgl3ZDo!w?6)Q$7oPr+f+cPWif=#F9LQ z4$z(Q3{yaN$}`LZ-$w$u<(&b}#>N8e`2f|mAhp=!5n{MhGUg^0WrJ5Hg3N*4JI}=c z&WT{VK`jBW9FrD=&$J3cv-N;(2?vceFn(g-VK8CHVklrJW2j+hV(4J#W0=A)i(vu7 zGKMt_n;3R5>|;2>aEjpq!!?FG438LIFuY^Dl9g;9&qfYFT6 zhS7=9gVB#MgfWUSfiaCShp~vUg0YUVg|Ulq0^>B$m?9%9sLjC$b`_&4h|Ta0L^4W) zNQMFs$*2V;8NehHh-CN(A{m9jq$rpKg$g6+=5hu`Mo2p*4=fH*1MX2kWx=ZhLHC?9 zfC)GWG5Ie@7sEdg$p~IW#V7(|GaLhvjA~#K;#v(5o8bqDWCX9-0UO5%^${~joZ%*j zWCX8;V3Y*2!L~#E$N;{no#6vW9Ar5IBO~~PBSsaF2*ekRU~?G3x)>2Y1+~Xi7>pS} zr+3>j2r#%XcrZvZcr*Ag$T9da1TiQugfhf3s4{$F_{5;c=)~yCpwIN4={*Bz?vI%< zn=ywm7hDg5+zs*{sFVYTFNgq*i-TOiPzGLiRl~r;z{McOAj6=jp9yAP0+TPmWG9#et7PC}P+`zu&|$CuyWWQ(g5eVr zXkQy6BWRWjbf+IEPC?hv+c4f^U}TD5;9;<$N2+98#JGZS9pe_pU5p19k1?JB)xk`= z7#JCI!DK0zj0BSqb`zML2PVsq$PO^O0Yoyq1(A%%vT0zEY%mF7CxO}dU=m_-F_?|4 z3nC6tlLMCZVPaumWDEwA;b77mL^50j%esNdWRM6WL?uMcWUz=7m`nka{$Mf)OhQZx z0J9UpWGt9;0F%eSW~P8iU$BS{n6v|v5WhfdDFUkr1&dDwlObTz4^A>M@I%{8E(|UV zQVebkZVb{49t<7~G7Me}-VCw~{tRIZ@(d9Su?*@A?F{Va2mU|>Js9vtd4Pq@*Zfk`lifuSzQKUm+W`*&t20|Vm<1_p+NlFLdI_{^B*GB8MGFfcGUq!py+vPI4@XJ8O_U|?V@PERZ@V31%?WMC55 z!oa{Fke*YSwm<8_H3kOdKMai9RWec&Q}$JN=rb@d>M$@cm}O+7CjMh&KE=Smn8Luo zppubWQt{Dlfg%F~69)qW|DT-v|(00}}%W0~1J;fdQnCh4J58 z1_q`Gh&)3Ig8)ghGsZF4Fq$$jFmbbTvNACEx>bGVeDN zGtYk&MjaMLMqvd8#_1q=CKje$ppal-V~Su<0Es zV2WViXNq7jV2WU{Wr|?Pfa(p0st4(fV2WTcW{O}iWQt&DV2WU{WQzFzohgDrk12w| zk12w|oGF4qABvrrA{f-6W>_&rFla;7NI}Ixc7xQyXpnnAc6l;IFyuna{QrR|f+312 zf`OGOf?)wu1j9C_2nH^u2nI){2nHsm2!^Lj5ey)|nKDH%cthEqOc7vx@=Os7)=Uu$ zK}-=0Ap1e=Y-l)v*bYn)48l-7IZ%K5Ges~2Get0f+zxUtJ5vOMG}O=eOc4wxp#A`v z$G{Z9upMfz5K{z$B2*p}&g+;W7`T}t7}hXFFt|a(9u)4Am?9XqFhwwc@hgW>^{HbCJ93Uh226egf}K-LFhgW>=g+d$(6WEMIGg&zkr9zbyc z!=QKo#g{ZR%~@d5;4}!bACwlc(V+CH!4$y&@;}J^pfCctj}Q$?4iXT2QtSU8jqm#3i1m!S`8Y8Fgwv{6KFmInFoqvP&lL0pu8^v z&99(501Ag>Bt9rlg5n$(4a%>eJcCYy!W0y5#L}Sh1LSv58Uw{Mhz8|(P+5ac+c8D_ z2jwf6U!Zn?@&l+W_z%hhpmYdI!=Q8ob34q9pm26!ieO}9ieOm86u~f$DS}}mBu_Fj zGet05VTxb?<#kYbbA*B6|1JiG|IZj0{ueVa{4Zr-_}|IE@V}XX;r|UNeTISI|4Igi z|1%jF{&a{pFrFJETZx6$SolM z!Dx^=P?*DLP+Wt;0G)=p2jmZsUqNQU@*_Geip71P`X5VK3{IzHh=bb?Ab*0~K@JT{ zPoQ)FqhbE16%BGDDBMYoieR|L6u~foDS}}lQv^dDQv^dbQv^dB zRBQ&ceb~zs!7v|M2Z7o)AbUXRLyjqe0hBjEX&02Ld^bnG3=oF!&J@A0 zmMMY()W%)P6v42HDS}}IDBUqI{I5mBpl~UG)~U7(4F5rFkXRA4oe$Exg((7DAA{P; zN1$!wXQ=fx11Pgy{E4F5s(FgANY=7KQD4p1HLz`*do5EL&UnknKRKLf)*B?g9n z_6!VvFEKFu3x)9ieqdnudzpdZ?{5Z%e<1y;3=IEnGcf$S!@%%wEd#^9Gf-NGf#Kg_ z28Mq=3=IFCF);kAXJGi3$-wZhnStS-83V&VK?a6@AiaVN4F5S882))MF#NmE!0`7U z#GZf33=IED85qEHIF#0AVEAXq!0>Mw1H(TF28MsK3=IEHL&X#s82%YR*nj^sF#HpT zVh09>zjqlJ{()$a8)4$&3=Ds-GBEs$W?=Xi!@%(GGXulFr3?&zUobHIlVV`_XUxFx zZvz9vzf=Z>f7clp{(<~c#lY~74-zII`tMB!hTp3|{m$QyK;)kg5c!u2L_*kqg28ME zxd6;o1d+ctfywP)@(h?nV*imw5&?_<0gX5?{JsMgzXv9-gUOR%5+ZU6%!ZKvk=UTo z1%^KmS3$^nknEpaFsT6|e^0|8Pk==*$>Zqae>g!R$maiU28sOM2O|HpfyoD8@)d~u z{TWPd1(VM}Th1_oY+I)=rJb&Qi37cy>O{LduKWX=@Hw1??9 z(_Lm3W*%lyW@%;(<^wG8EX6EyS(dV_X4%ZLo8=J8O_uvCFIm2_{N%HjYn7WP_ftVl z!9c-E!9}4!p;Dn!VVS}@h5d?RifW2_ih+u8iZc~gDehK0ro^Tsq$H=Lt>mGUs(MOY zSlvfk@b&X=H-0ny2geWtFM|TZ0*0lG{frA3*E9ZNk^sABH`6hu8_W#MY|KK;(#*=t z2Ur+bidZJFEMQs1vW{gN%K?`2EO%HQvAkpX!DlbmAU98Lfr7Atfr2I2JtYba3JVoh zC~Q;|R#Z~dQw&fHR-B-?QgMUg5hW%iekCcedlI1T;d}l3+x6d!|Nk>E{{Q;_@&7~r z4>B$DBZNVB7*GF^Lie28IeuVJsqXC}JQz4GeP_79oi) zVA#g6i{TIhIK4A~b0k#u48u7F21W-EkI|74oDUf@8MDFs9L8M6Jfj=`S6fx(f%iNTq{g~647r zhCK{>nVJ|18B!Qh88Vn!7|IzIFyu32F>PQdWyoZDz|hVxlj$x)5h&yt(iqB^t~1?Y zy2+T&bdMpM0hD5vGwft&U`S@jWxB)Y$aJ6S215=*4O1&a5*I#UxWRLj_YEQzKIwLo-7+LlZ*}LnlKYLqEd=hF*p# z3{x2x7$!5!W|+e;i{TSv2xAmO0b>|rIAa7uC1W&WBxC3f1_mAP9SocS5gQp9`yzKR z{%_sE;3B(|fs=tbK2CQB1EY?Df@`MB2F3*49Slr5I~W+X6m@qnFzYC|ZD3Y)3yM&V zROnLZ@=WPca8t-gOxeJq3Z{c1lvBGR^%XWS1V>~lY+wk9P~6}UDGg_)Mn+oc?qFa6 znZp7$rz=vyO(6?vLRMl*P=s<PKBD=akW(G$n zD{g4$>XPnKj)d`dFdRS=U}QiSaDWQzU|?1Yj@ZDg>birmAs|9oaR+0AvZ6FdKv_{) zF;ZGF(nWU%1E-F6aD=Oiw4!39F2fE625p9242%ri3{2XLyBK&tqvYC*`x$r`Tp27F zbQt6rgc%qZIJEaO@G>|tm@#NE$T0|kMHu!oFfzn2cr#ctXfc2)HjpAFhCl{q1``G~ z1_`jD{S3?u84RHe&I~#Xa!?T#20I2L1}z2!1`((TD?D%>1427Lx~1~~?C z1|Fyyb_PQRH3nq{QSfj-$Sw{BKL!T|BL)??E=~q51|BK0Euujcr(~B7{GNg>|)?!;ACLe-od~CiK|@<0t`Y7j3v7m?4h&+lrDhM3!pSu zT`5#u=`IGaIxt@ZtlxnFLKi^k`A~WRls*ro^BKgz>Ju0w7#NFpG30~jl3fh^Ai5OF zFWtp3AIxVEXRu~)VK8AxU~pn6Whh~AVd!8`W$<9|X8^VNc>Y^3@GyEZurX#curc~G zurU@guraYP@G$LS;9+_K4p1Wo2&`l1Vq{=k##F$p!|cUe!@Pm{8Vdu91d9<%3QHX; z3#%9F8a5-g2DVr1aqKfVY&bS??Bh7a@rqM~(}Z&oR~*+WZWZn(?h8C(JRv+ccrAF# zcz5t|@R{)S@!jAz<8Kq-6DSgRAt)nQB)CS1ODIEVgU}~oDPa@g7~v-2Il_BHtVDK+ zN{G%76A^0@7ZR@${~(bf@k!E2@{yF6)GBE$=@l{zG7T~>Wc_59$yv$0l8=*rp^&A} zq{yKdp%|e!M@dF$gEF78neq#jDpdv5C8`@#Kh+o$$JJwSbu`X`M7jR~4;nhu&d znp-q~X(edoXw_)-XwA`P(H78_(bmv5(RR@G(cY#Lp_8Ihpi`sMp)*BifzBG89Xbbe z&gk6Gd7|?{=Z`Lju9fa1-FLdb^w{)-^yKs^^#15`=!@tp=}QA<44BtjDMN1 znN*l6nd+HZnYx(n7_y>w7jjHrH$(*}Sv) zWy@w8Vw+%FV%uQbV>`okiR}j4J$4FqOYAn-?Xf#ycg603-5dK1`+E*?4p|Om4owby z4znDVIr2HaaQx!L;Kbu3;iTeZ;AG?E;S}PO;4I{P!ug8x1Lrr+KU`Q`1Y8na9=W`8 z`Q^&yDXs^x0t>g4L@8s+-Tt;DUtt;cPK+Y+}8ZhPELxLt93;P%GthkK3tGmi|9 zSDqoBXFP9sKJonE`NxaHOTe8YSb ze6xH@eCvEWd?)$N@m=Qo&i98OlOK z2wf1mB6LIOzpz{ z#OcIMh`SeW7T*xx5kDb*M*M>K74aJqRwQglOh`PHl#=X{{2`?yl_51P^+j4zT3%XJ zx>CAc`lR$(>5J0WrEklK$@r7Wk|~fWlc|xpEK4e@D{EiYr)-bxc{vI>OLAlKWb&5f zE9I{$5Gj~aC{Wl^_^BwNXh$(maa{4S60H)WlB$w(rD~P zU&X14Yn42e5|t{I29-9IE|or&5tS*GIh7@q4V68WGb)!^R4G*HR9RHHR0UMURAp3^RI61RRohj2Rfkn4Rp(XTseV!YrG}w~r$(YirN*Gf zrpBWtq$Z&zr>3T+r)EaYl9~-QdumS9T&a0b^QPucEl;gPtxByytxc^*ZAfiGZBA`P zZA6{#*D^wO#)3;O?#RTH9u(n(_+=Kqm`$% zp!H6hK-;o*k@iXL*E&o(Hgz&}rgdKEV(FUI&Cz|Vr>s|~_gG(2-=+SB2@Dg`CcK*X zYtoX*ag#qyF`Kess>jql(-Nj#ncg)0)r^E0S7tn!@nt5XyD*<`zQ_D&^Eb@@vY=!;+iIcJa;vpgo2_acam)+gIkwv}vqv)yHT)%IiC z|LkztF>5E=&a|D!cK+C9vTN3^W4oU1X4`GHJ7f2%-FNob?OC?x%3guJK6|I_6WCX@ z@5X+s{RR8??EiGY;=r7PYzGSt798w3*mLmM!DELk4p|&(I@EM%)uB~~9vpgbnC~#( zVUxoqhdmB^9F930b9mO_S%<-Es8H(KAOs9Q|-i?3mawuVY@vYL3+$n{jN$v0cY@9lLbw z(y?F1ejVpK&UakrxXy8h;||9ojz=6{a(v103&$@Ue{uZ938@oOCvr~Yoaj2ybz<3x zWhV}uICSFCiAN_{PO_X-IH_>5>15N%11Arh{B!cpDXUXfrz%cWoH}#r%xRI+BBv`( zSDapTdes@dGkRzI&iI{4JCk;%<4ngH5cqKB!&$zwd}o8s2A!RDcG}rrXMde@Ip=aN z>s;2khI0+)=AD~&ZqK=q8Ff_UVf*}_` zaL0um7j|4^14E~aAQ*EI1RE}b;I@k(_~PP=i!Uw-fT7tX5KOxSf-5e8;FU`t`0vuc zOaCtGfMLL85UjWif{QML;DO5^c;hk%{<-|;@}DbZV0h}vsVk?hvVft=RS@*K3WCe7 zg5agAAo%0zkE=hf8GvETH4v=327+s@f#8X2Ao$_hhif0MD}kZUbr7t&4uWg0gW$F6 z*REf?;RA*Jj^tadT5fI6XqLY6xiSOj)3FkApNwOu22 zFfiy#?_^+L;IoU|!N7QD2Lpq^4hEJxI~W)Rb}+EtF)%b1R1{QI6jW4TlK7{>xZqy` zGtZwdHcV3(Knqw7{Qu6lkZBhKD}y+LGpN}v!5|5?1EiZDZU+;{4kiW}kR1#lOW8mI zY#>W_GVn8Se%Z;u%E11`z))4#+?bu2T}jQ<#LV1|(bz~#RD_M4kC9PAP$^8w!%SI@ ziHTK3146xIWIKKO->*-S!mNCJ(h5+Df#LsuhRci>7=JQ75mIB|TE!s1#=r$0Fk=F> zHJP%Rb}?`>h%?OH#lQjTc!S-`A+Uo%4DMbIkn=c@+{*(J;6W1L1_^M3Jg|cS>?3iI zfH+8C7Xvc`2Lm_QSY~}t*m8V<1V2QKMSmw~MBvK~2EGfB$Pm1+gMnFK2ZPWBNbrkZ zFfcS%Ha7-iV`gPWV|HV9WiVFmYvS(JYvk_n@_gsXxXjZF&b{f-;c)Y>8`CZ@gMpbr z{C_`_C$lz#DuWJ#F~j0r42GcL0o_G#LaK*ad|wcQF_+NHQo33RxN$it90|o69kZ3#+S{n%FVf zGa9qYF^aRRsk4jAF^Y=tG4V4hGmD6coAWU;ny9I>+cDnvNR_sebvM!xSg33tX(nhR zlT9Sd!63j|nJJYiS5Kes9joiV8QP}2|JZyP7});5`2U4z0@E=Deg;JbRR&E4 zO9pR-ox2#sKm#gZZ;1))VBom0g8@XVLTPm|c z(f06A7Xk&L5K^ck7k%JNtN>D>019MKM%>Au2@X&$22BPbu(?74I~gp%VxZ9k3$U1i zz)l7!u$VN16jIMN)NQpy1+_da#YJQcT{Jynb0Rwh z{mhNTwPm%<#aY=DvRO6Pawp2LvzYKm8#-$!dD>~S>e(x4 zI_XOWhsI~}|NGJ?6RWM_>7`?-$m`;ux1Ci&K~+dVp4-uk$=B3c(b+&$!cRn<&lraCk)ICa=REni%VGrg)CvU2_q;385!j4A`J`~g^igRHTAso7%wo2{Cfi$ zPd)d)jLC`V2ZIEID5wyYWRQaBW!2xoAfj(zXl`Q1WNysI%+JUsDk8@uuB^v=&M+X| zJk%)G$Bc`QO+wQ`(O2G7UHnG)lD=XUrHG7D`%o1%(}YlW87Z%*ERY?V|H~NXVzEP5 ze+L7bzJVd52pvK_NMqnW9R9+SGU95drwm?fs%NR}{83t!S-1h>T`5pD~}9t#F@ zCL<;l20jKs(2xy3g8;-YAUn*}&DG7t&5ha3#o5K#)hC)x+a?Od$>Vs83V2cGS+ z`~QW>is=}GFoQaSB}4Kq1`W{gI5@v*fLlL17(lcLylmhCrCTnnWy4Me5pbQyC9so0 z4qV@9fQAkshUtT9L^hb$>{!4a<6}`bGP7s2V>C5UQ&wW*X9VYBb5MDT>Uoy*cy1Xz z*El;UZ4sCvB+SBW?F6N><)oOHSmc!<6i-#LPF7B;zdnrisL3f3*An#bgVE1L1z7nw z#buxr133J&7#Ns1!EK)CT@37?DF~!E!l-zVBaIR4Z&1y`0`@l>0}BHq*xzgdpcWgb z-T^iAI4>9&8k-vnE3>Pc8#5cTYw?_j=Qyo-wK;Pqj|Y><64SMR`#ivIk!4_DQfE5G zpu!Nmi-8$5xq;*s2~ZqJAjJW+FyXU{+{vH}b^$YkGJ^!z1rh=~8F;~BN(_wP>|$W3 z$!M->$82r}4t72!c6B>uV6?gcY?VWt6OSq@?tn)U~`^ot4*t{UGuG3zHhtF$Qaf zL{Rx>!(fZ#2aGyK733*ZP?fTa!ID9UK^5$JOK`go)RF>KDV$(`$S^Q4Sb}9$1$HtR zFmQh{Fl2<*&1#^^8O1x029dIo9+SGc9J4V@oi?L6qpC*;7Za1cv8A7sxsbM*vzE5I zv5c6Ood%<+yQ*uly@8vt5|0a)q>7QUM>rP~lcQ@e59368GY$a`Eqzl}eQ`|%ArTEL zRYhw<6;2ioUnkulJ5^C-9dRuYB`H1!doE!PV@o%1`=6hIfk}&L7XudqA44=KG$5tm z4h9~0&SC`x04q|gB2pNrm|=ys92xa@GVp-Y8`}j)=Y#XY4hB|%9Spn|3=9>`70nqz zNlTesRZ&=(F;M*9a$d&H{NPOPO!0pw7|?)HuOYM?^_WTu)RsWIvi<*sNt|gHgCv7C zs8mx1O$dUW2WnO$`gC}5^)3cw1`cSLD}zcoWKeaAs)P z#h?wEJ_Q?~4XrakvYj21R3?wJ;T(OgW-p3V@e#x;_B@<{30t9j*!aR|B@ zs(Dx{2fNEz7^?Ab2zWZ{_}i$sxmfcFaT}XMDaLR+BO!iWLkI~Y=zh7tu+o%qg;D811kdu0}H&}#%iv}9>UFdo%>}(<@Z>1|0?PKg`smz}80NUt;QlQdy#r1bE z$XwXT;0W$1sxZian=CsRRP=W;_=3eG85|jW!DWIvgD-<4SQoFrP6mAjNHc3EgFU$O z&I+n%Oij#TZ5uXrW@U3_>17Um=JAJ#+jv#QRb3D z0tU{})+T<&lFUrDZhS0!7JB+tVpJEn0?&3;( zd_t^(Oe})3V%mD#Tue;z1yX(gOc_LNzSCLJX6940Mzg_+)u) zJjAV%yk>CA%S*BAxEV<9784fHmQLlB5$CtFx8W1#)KmwR|4jeCGcYg-FzsSsXW(W? z0SzxgdPJb&eFp<4JWXNL1T3IPU;(8mj6Mjck;2Nr0uEO$eMlt%Gta}1deR|dl1z6BdUqZ~6R6DTX1!#f$;jO@Z74yfg;&B(s^VNjTt6{EO@tbl@LpuxYd zzid1?S(%tDP3CAHwY2A8Wj59AS7X{0?&TULz``!3q$jTvV5{n3VkXBa#HFV5pV8S+ zS5AmeL-{!aBZJ@nFHCMs#~3t0BfRnqS`6AqbupfP(+&nuk4Y3^!s6f|r>CwW;wYkL z?a9+rrIDJM?58iIYauHkrXeR(pOYZ2BkB<@sAQt7sOh99!N9~|&cMKA3R-8tAi>}b z8n1(lI6%hf7;(oEbi5ZlC=PPFIM^3F;4wB(eIpEVKPVQo8O@ELJw0tk<~6I$gDqrq zf=fLQo>-|Oz|7*~6&=g8OUWkKplVa8g~!bVA$?IF|G(Ej<*q&h15*IFUltB3Y#}Ke zZ5#!WZlDPX>>@^ioeYp38K{JZjJ$H_gL-739tFs~pdN*Rp|P+cI}GZVGOpqIw@ic4 zpXc93O~x1=rd=LyJidXtWekiAAh&?pTb!U0oCUO04JnSH-7wPKg5&~b{ZhtlJpbls zFuL>n+iA+Q%j2yFXazI_BZCvP-DSj3wu?cJ!5G{!f`tRdm^5PM0Gh*%KpDyil%YVw z&N~^T!I?^rK^oeC&;t$o2<&7~2WKn>20aFKaK_RDm#v`4R8{6-814QYV%qiZ45P_kP+yRdL6d=j$q$^Ky+LC^ zsOcG_q(-D?NM8$76hl%qWFQ^Xpn;Dom@Bd?GaIvOa<7kH&&{|2OgS-`IXV43U>oCl4^V#AW?*1yWdd2B4C)E< zG4La~0cs^8{y0I=#R+LHfTIf|{-6yCKD)?W3~Zn^hv4{Q12ru!7#K40GlF8q*q+gt zQCOK#zE(A;G{}~v?Ow*eYulq3BWvz5?fN&h@9w`F|MEa}DkFm*0|VoC@LZiKsKLnw zo_>L)FFbjCCj&D$WQ;)xf!(h(FoV$>6awHO9R@}QDFz0nSSHXSwZdHt%%Fv>NdCs~ zCe&AmfPf}saKQ!+2xyiE3-Ai;U|_lcn%&sJzT zdJGqKFmQt=P%ju53M(U=cb`4gr?k8kOfp6NUBvkd{tRn((M2 z0~5IZ@Bohki7|wNdhFuRVLH&fDq<`MPbvcqdox0N%-s5*0b@{I2CC`AV5tv08LZ97 zENqNu>*$yMj0i5z+hAn3#?qdHjoDalCDX2G&w|>2*BJ}VbX9~zwKN$(Ve17R4_0I_ zWheyA6qzxYgTodyL!<<62S5jb5Q!6-ZTajXcQZ(V7jc5d;z1MW5*KzcNHg$3M@&Ft zLOS4zM}|QM>I)WuoeZE3JvbKhAzgUb_%ORXBWShPWR6$GQk;)!1P!TJJRKz0JIqcxr0_}Pc1ACkg zJkkhi7=qg1pmw<`13LpmPE}wBgW3gfJc8z@jg3Igg~lios3j=QuE(ehouq?|%`kFB zm&@BJMA!!<7}|OV`=~m_Sm=6MXz@VT zB4`%hz>twySr|MLHaRmW^A!)18V^&^-#(@y4`^G^^ZyrcyHuXR4U|(97!;Ah6xwM= zq+R2y?mzx$r8O zg}Y`)axZclYU|_0d0yQ^W zKm-?f*##_2potaXR(4Q=V8@yub}+DjCNY?er5Qnp@nnpjjxuZI+^~Q9n0EbJ{O=1R z4`VE-oMr^~0c)9dF$gl0gDPer24SS|!LSpWK@g5WBtmEd7Fy_lV+y?Z<^srASXs!X z52;Z=iwi(?zW~UY!p6+(#-Iuh6r9S;%FKGUo~oi8i{nH&jN`pmSMqbrX4>_y{66ER ze||R@C;W9|TngHt#Q6UM0|Qet%sa38h{X|f(AXH-UYW&m>KxN z*&kG#aDp2bpo)M8+U^8Z1cIQEd}Vz`Wqw9wMrLDqMsVweNi@R2g{3S%fl+y3gq;IR zZdSs-H@#D*_A+KL2DNu}wf|cVj_W7}2Brz%_GJ{PvOsNLLX$fpn4!%Iga9JkK{Y-T zw2%f(JTpQk8bH&k#=>xSG2hTz6X9;nQrw*M@1#DX0OP9d3m0zx7yIueq&)>XuO|_l z_JTnsqNY8J{D=stouJi0Ul8qG&?pBBcs&QGS;helIq+N-qatX$T9NV3KTaOTXaBl* z7_adBlVY6d#iZfk_4fv-&AH%z8Iu>&4+eGyq|rbI*xV>+9tG5<1`QyAdWEdu0VHu@ zNNak*G)57wi;Wk!x|o}6|NO88nYZnK8RKr)Tq_giTq`THu`pxJG5ce%sa6I?2Cn~Q zj9b8NP(>-1*!6cXFzJI@k0ATm1wefY(CQOLW<}6kp*iE0Y5(4EU2MF_&Gh3>oh>tm zEhJnW8C98Vz~!7AC|q%sbKruES)I{XnDKW9%NHv~-iT?8s*HjE)+;j>{R3~TWMp9a zAIbRg|3b)|Hhk_FG)fBEkO7+07B^R9XX0VJ&9fX<{yk;#WK3q}W?*Je2Kfg*-^>i^ zIq8EoPl9?*{9p!XPF$S*sh5c{(+BBwpzR|}3>N>tFex($Fo-Z{Fw}y&1DXtwFm)bGfpB%1)Jjs~SFP`e8h5}=#|@inMihJ=KHp|CN^ z{G1wSj1S!0fDVs>Ru=LzvWac+26fO_nJj4A9#Y)^ZMh>ni5$V8u`*$BoPu(yEI6mKLKg}lH8G%T3BXk;Xt0dW zz|&At%OlZT(^f~C-I?Foz$uuIil2TO+(PUl`=f-LMs-8cadWev{phH z9*dA=VM6eERU!Rd4Dt*@;64LnKnmQZg!mD%PJx|G1hnu88o!Lppw%OG%%&!KOpKyQ zSz6wic4`j#Qj-2ts~Nduj5K9*HPyw;b@Dm=lDHY;ifu)>CDeIx3*1UW^@Ws8l%%bU zl$beqnWT->#d)Q*<&`|V8E?l$a50N3s0jTn=1} zz}irRV;kyy0Z?8B4Ulq!$Jy8z7{F~Y$h;;8SPZlcmIoZ7pczgPa4><=mMFBdEO7y{ zHcp?>TpTp=0-DCKW@HC1d}ZciJ|ZBaq%CJ_I!h>N2loyk4kxR%+!v%Sa$k&P<`Y-C z!YiYz=xnfrg~{7F+t|r@uJhk3qH3~&pgM(#LH@rExJ;I3FlML%jRTu7n1Wp*Ca{A+ z3lz6}uz4J5Q0kRtK*TS!8-Wl&_!v^9?O*^ELkeL3h=CWyf|g-&Gbn(|eb8jR76WL2 zALKh||G}OSEqR+8GxISrgI95&md#gcEX_n54BSKbL|J6RT>^rntYVy@Wwc0(y@H(# zp9;GGr>m7`I1eWiv-uTw6Db*wuo^p$)Oe%fL`QI8U6jNqCnYant(9bE9pPcdC(LaJ zDz6zCtQi=X*qNR%$TMVvvLb3*AL>s;PRA&vp}he_dl0>p295rM%4SF&uLU&-Bv8s_ z(Cn2kqBv$|XI9?sXT>5LVDIS7%xvOk%*>>vqhu<^A}A!lV&DfVgzY^1-1(IS|D9ko z6q4o8GnJEK6_r;P`}dfEi6Q9!7ba<@Hw=Odstoy{xkxqWXfJ3kk_{Bk;9e_6978K( zut%f?Kx?+yzktRd*uLyw0P#UN88S*J2TscJ4AKm8;M!IJy6DrukP$S5jIzFq86pDe zZ83|3`}>R`3ZNBT3UUxCNll%Z$z>NeBUiZiQT=FTV+}>7atQ%8K2C8-DD}5iU5-nX zlQF^hpB?8aoe)JWDe(4mCI+kjI!ydbZy1CaR6u^^gs!H743Z(%vSSp_&}hZTu-wol zF{u85Wmq0?yA)Dh3p0SiM1}#f+D(rMmQ59z`Ix~gic$Q|D9kUTsH5O$I!`F+DmSBY z1~Z?y;sS`bp}xPuEv={QZmv?T$}&)~QQwIr0lG0F^GH4SRCC_(%Epv(+fT@7keuz{C%f<~AjvY-kA zB8HNl`Iw+XJD`QU`i!8W8&p}b^D#nGwoHhlou`0app!dqtG2tkiL8{eK%jlFv8cAD znwhAW94jP|+jzNJiJKbh3e0Az(sfr?mQmioud1OVuB@RbC2ghf?=h%cV`Rww|AmQ< z=?Q}{LpZ1#Ap&mr!OCuEe-05g(7*!woKpbQ4*|8IK=I834g+2WP6ki`4GIIu8X-`T z1>TE+l!kw5Dlsw@Y~yBRZ<0K#o1tQ&F3+PO&o0EyUXb}PfBY3}s z1(N`SA;WUe!XqOFW2Cr)wgM3aE3|uw5P%jZ2mxdtfOE16sI*o=YHJ{3cNch-1ZZ}w z7PR110#v#2F{lU%S=R1g;L?|X6x%Z3VjI*u(E_)*jD?NaA&a`86lenqvog4h05tZlV6JoLOuwoMx6;t&ygEp)~!i8C10yJv}3j-w2tAYK?%+72b<|l%yfM#N;!d*NwFflm% z|H34}BmiEQi!sK{1uviRw3I<(+@MqoYA=JzVo(bTv`7lHodME>!nXF7k<-ZANJ`5y z$f!yn$H<7$!vXaXEWqt3Xy2AV9|7Ti zj7kW@|HSkW@bwSOjhU6PwVxQ#`Um`e6@Hm;KV$9NnlLiLI(4uh0F900FfcIrGO;j- zgI1)7F+g^@!OB|b>>a|#&~ZP605na2BbZqLwEhH?jJXANGVpCRX^ScT^n&$K_Pw?UB-jXUGl~%B7Tz6Y@%{% zV*frfFfxGFzlt&mFc^c{cI?o-UoiJVM`^J;545&g5nM9xF(@Ln3fRG7EDU_$F)Gj= zE_U!V4`{=SB6zY2lpEEcZCq0mHGM`sW=QWw3^J<@-qj%wUBSZ0Zh&X zEn}`Nt!xvZ=aL|6&MvNOsOaprs1Fq+ML74;Q#*%lMoZgZ6%-uJF?&g8_fUE?1mT~ zgw8r*4-n8+H%R9kv;_mS$&CxVRS(jg1@$Kcz%?&u#g{NRz>q6W@N^Y;rv|hY_+D9^ zk;!H%cczpitGJi^Rqm?}A`jIIm5ntOc+^zbB{&#koJFPDw>kf7;+bX?AgL(A$-u}E z{QnD+JQE9p978&&5ReD=DPaD=C^0dbxN@MHQVyx6M9y(S;Fx7(5CSi>tpRn$C6M~y zpkZe4`bqFQb7l}%nXw;I39w-_(!>0~m4JdWqs71T$W3%c*}s1v^_tWFA55}LTnsV{ z0id~Z)VPrWxl{(J2ZoWC5Tiq|d?X_PTKNGPUK9tnx3aulE0M zOy*3S3}&EJfTExSmyqgVjAjdTsUspSseyc?hU6pU!T?w#40L1PD8CV)x@&>9uc zP|z+069xuwrmNY(U;-)~#GzdaV`k7gUvRWAf}#Z)OLk1ACg6Mss=(PmD+WPBfvvi_ z%*@tq7Hr~TB656u{4AC(7R*ed;xY!xB0StqJp3AJ<{=`A;@nKEVwPOy`U3JIjH&-t zDoe{riwTLCh%kEnTPZBdD=%#kRbZ~>At55=rx(4@)Q4MAL)Op&R7SG@|H2f=B*5U! z(7TJl1G+mBmPTbkew0ND1;WAN4ASC^2o_K~bq9m2z)l8naEeuCkOgPA9Smv$I~k0? zZ8>LwoeZ{MSx=}eC^MRxnAt=2;TRi%LI>1;VOIz5F_vQz1+Q%v6=CCtZQ9{u0u94J zW@#Bg;b*GO$C#DO$l}SwBqE_?qAbYB!p6!!Pu@w*MP62cpP5}G$Oa~@Yz&fSXXBru zXk_jzE~f~V<|*ftmu3@`3^Nlu=eNwwGs&4=(3akV43FD}dxP z^tAQmwwDuEl?dK3px-Ck`Cdc7epsL0nnxzHgJG}MlT?#jEg}Q zT#nU%8_1ww1LY!c`2`!VFhfdZX6AO_vVf2A0%VlJBbZNwSvJt#(TADY$j^|ONgLjG z*Yyh%X#o#Tc!pMi+K7B|B8)o!ZVO5yy6}vAfBzIEfzpxj|8GqCOacs=pmfBHw&oC{ ze1Nw35%~?8Bf)hoIC~-T8)(%fXn2N$K>?|k2TE6gM9031BwHm7)d;n_0R1xf#>!yk%sS zh1nbey|u!vL_lKyW-TAV22t?lRLFoFXc;mndeuO+Gbk28EkeL%6@z$+T@>s$ zc0NXaMq~EBDoTt@EG#U7ViMd^0$GV%i4M*pJQ9pdtgNiY8tb|Cv9U2T@^f*E*xBpq zsY#2-vGeZb>Qa?vW*3x|l-5v?DUB`P5&3JN(;p0Z+i%cRFx${@+$xr>1rbljkz zkR`)z1~vu`243(UQ_%b}!yV8>IvY6U*Y0581oe{{1a>ljnm?e14lj7RyJiQ27^oUj zRZ>$21jTpOwc44I;z;@~R^b1e!sWX;IraM6`gQ)T^^&&VUAud1kOr6JCg&Zuo>%f;Bo5#nhhB*MVR zAoTwWlO5AB20MllP&%<^Z~&zf=-`};(iE7eh?uxByE3RK;$z}R zE_2UIIx0Wr3g(f~Qxs-S<@RG`Rkn{Z(|0$LVRh%Rv-S38VcEhxmx+;ETvdvR(T!Wd zPJo+Vi${RN)X|?;UdKv9T*yU4Q`|hpSxrjQSW-sICM8-_Rmg=ylv6@qU5c5-p1Vm{ zP1;IA&5(f+)H7o02JZzZ11)qw9eaa@0-}EbZJ;9rklU~f;E-WvU}FHSB>|0Afu`+2 z3*bP@K0wROc%X|O`N92;9SoplAdmw>n86D{7>&X6B#Jsa6?Sp=HHTNSe2@S4zmI#D z!fx(=b}oBd7|Z^3fw0RS7X~H46*1Ciiy5$FG@;C;Qe41u8e6(`W)qTs^R3A`(UVFv?f zp#``qhK|^Q0|K-#2~_p&U{C>Xa{yJ$LJZpAo#(q4T;VK31{LVQq%?yo1Lz=;T?{^O zF)IciaMfmDs44;(2Qdd1L+}nLv=9aj5rO)lh^1Q0pysit9HTk7tIDVjZavF0$}x)D zF)|*MH}}&sh?g}MwO6xo)6=!lFqY+Z_LP&AH}}=YCeEyDW5dqFrfUqNh1DGNq-5ny zM8(aul{NT;c;rNZ0Y{uJ6ml*^Zlo_^w z7TKskHxonV&=__za58}QJhEszvNC{<(*_-o%T(yd%Fw{Tz%T(6PoRYhph6D3w5W(27-0NvaLQU_WQ{-1#pvLbvwhyfCL4`ZB%FjyJn88{jA88{i3K+@|$q97aU z!3O2|Q|IS>lUW6BoeW94NR5ywh_ z_i%DE2r*?atzZyl&}Hxit@LyU9d`>(iSE!noFE#!@1G0YZUT)bGJk>WRZ(OBu|Ugh z_3rFoP!!m~V0H)8mjH=^x?=7O%wKjfSTWc#xPy(f(%;441?PF`8yKo0&DeuFpX#7Z zEugj)XwwzAc>wBGfKLKIPL1$ceKT`V)yZ#};HqX4UGHrf@2YMRUGHWRKZW>~VY7^s&@ zv-7e_OUa2TOX`^DvI}$Fn=T^G#mlU!EC)JF{r`UkHwH^41IEWfYE1157zEgv+8J8G z^HRQA9b%qBF>I@$k)EU@f9a$L| z7}Oa+3s%5+7^DJRCV-TH86dr2h5>^*Bw9g=8H+#*)YTd6zz!4vpJo8ES_5kJ4h9W< zM|Oq;1`UP+1`UP=1`P)0LPvH62L=s>00s@P1T!c_C@^S%_eI-)U2e-@1D&t6)!)fr z3>MR8um#T=fwtxEU@!zN`p3v*pqWSLE>Un1Zmw(&*(}E{uB@(XZUjy<+KlXs=E}A@ zlJe$W+E7~8-jADw$=nt~sjAC|NJvSED(XnfYDls&imGrjMu}@#t7>|gD?(@s1q}yt zAwez^7bs=AQ&LP^f!)VVR)U3%N!T1*yfSQIs$;yubdZ6WAqyW)OEQSN`QnGQOG>||nC$5_C` z22MlDpw&kVpu@u<(F0B^u)K;$FOJL%380i53o5FS%Rw1Pro#wk(71`3vJz-)M2yi} z$x2IH#?Vz0N~=q1%M0pjOKHgq>N6IIs#~e3yBf>FXnSEr9Vu;NVMSd@ZDWXgMHv{F z445(*xEc6Cy)zcjq3B@ug4#R$puNnXbJRdNX*UBe0|x^S1B*5&Fqs%YhvR_yd-zYq z0quDHs(76Hn4*{BF|K2Z|E_T}-sGNs!s)~brxVj5d!M&5dNST)I>4X+nm=V`P-IX- zYU@JhD8MZhaMJ}kVJQUaddP!|D5eXbCbB#@yX{~Q0iQ_%sw3nWU}usrD%vxGb}>Vf zr#h(igUnO$Ga57Q`uCfwSwv2ZmzhafQc;!DM8MucLRV2lLQO|FZa<@^bppS*n1lcq zmzboQn7FDbkEEKUte(0&A2`k#88$IGGu~!8z@W!a1ll{N&tQP$e&~=k!u`+zI3duE zA{_<+23c@$>wt3V1O^?31q?b28yIvLn2SKE3{>fX#=SYg<+C^g19;vYGWezfNg&|m zD##uKpVkK%BmkXb1!;zvgCN`EV%t@yzy+`y1g-CUf}K|jZZ$x~*TI;cM^{@(&TmZrqe0P6B8 zgQtdJW0laTMhq!ISBM}4pdBB?3?6jo4czeoCl}~A6?nNOCwP!S4Ae3L2fCR4E(Qe# z8Sw6X14D7ps(8p|L)e%aXiqn2SE9NyKcjdVXdMhZ|Ng0x&{vmGQcRx1z{n8zKby$`yzbisv{4JbW?c}oQ~({R02+H{xU+*n z5WFb@(X6os?;T)PX5`a!Gm_EqiZ_1V=hht6?DkFC*h|kk&Pn;tMrOf3??GYC@PgqD zV;y4&gCK)ADBD8q1hrB@ccOsS-l}3W{}?qY#mC z4A2#*I~c$%UIxtAc4TG%ZKVNk{RDRiK$Q=u^T3EvE671IJw~lyX3q#~lJPTwmM@Ai zI>;HTh)5f{X+vpcU1N4GCJl86#TYB9W+HD8V66b7%}lgxIJwvqv|u#o3{O17zEguIuaTF|Ns92vJXH=omu>UHUooz8dE3ej16WLhH0R)KXJ+{p~xQ) zQfHRL)X%`gp!eU7@g@^5_&f^-P{{yW2MX%PK*oPy;iv%`4bcFN0fCmYg2tja8Q8x- zs#D0eHSjzQWH5)18N9et3^G85Wy#b#=!q4+7-w0SqvVQ1uoEj#&$3Xk)Rkg#hh`Nh z1qv_lSqjWsh13{SK%vNN@5KNPCv5Na7~Ujc{=bBym;dTDZ6klDGi# zIk>nBNSuM`|4Rl2reY@0T)r?v2dHF#jGTb_im+S*UFeR;HPGo=MA-`6@4*ME$^;o$ zp@%dH>LYI^1|8S5i-8+-xe+*1fwmQLFn~r?Kri%a{cJ~%ay6_-vma`ZQ7tS5)8p- zB)EclN?b_e%%I+pH7J!>BaN9Ef&>g9vs%{R)rKHP847?pJ`mOp2GF8dLxCL(Qgg2y2@0tA9Am)SiyZ!&qfG%!_A`UViqTT^T9ArL3+y*Kx4>@Z>h>hVQD1Slr^Mmt? zA=o|(cZh$$`+ULfFanF)J7O0HFVX$~AF?kPoF5_T9Z=MR@*_mt0!19;K8UytNSuL@ z!HprFNtKC}ftvw5w1>KX3Yy~(%^PUNB?UgnlO1xLCFm?mMuP)d2iuK1z$7EgJ${Fwo{)&@x?6R$*c0V-%HP6lYWeEqenWu42b%YQw0`D8|U5=OSrl=_$$$-ZqWI ziC@FQ%*1FX=NyGAYYB1*axgB{s>%7c z46;X;OOWGVv=%rGf%YUo(!K>Gt$^D7|Nk?fi`$`ygVHQSy#tCkC`~}bZJ^@dJ$2x; zXahFi4t$jt6N3S`Zw4ANcbRx-BO+Xd>bf@hIH!`Yx>m;t;y za0dgZ7={$Spk+`J;4PxyRhQ8H-Rz)QNbupTYTAqjr9l$QKs&tGOR24f?(km0w5uPq z#e3Ex*amNKd(+{+1(OBS69x^?oG}}8_X;eXLC50}Yhj_?U_^pPPI%zvJ9JhV+`wlO z0IdcGHH$&(4k1U0z|s|HCUGZ&3b=vK%b*JGMnLuvgVrd3R#VzBgQf}D75Nzb ziB>x%$SFnlFC}*o!o1r7{hqaMI@+u<+-`>IBL3@DzGv$1;+?*pZoXI ziz#1OSDMRtuHRGk03VAeM`dRLMPp?NaSk&r=coVd!D$A(|BRViNR7b-6sHW%plxaY z|3mhrA&HkFi!=R&sb>s95x))>k3tc@3=?O_L>6aOfr&FlfW$#9+W%jeLYM>?tQkBR z!WmwIl0pPSBsev33+!NUfbRYXfR_o-em5d&p#4^a07hJyf@)M#NR0~Vg)>5%VKxkm zUqJh5c^PcLRR)!90C^(0QIV(4IdieK>-3f@a|Z7#P1mXV=Z_nLxw8ph5)J^@pr328|A*4EsVl zdwigCy6hQ2>&3yfIHMA1j?0eO)I_b)#I-Qij*E%OOU*=FPdvy*K}Uv{McmE6#6{9J z&6kCVNkdItU&M@CPSZr*+=GdY-&I}RhJ(+{+L?oiQH?QIT3b<+mD$`=M_7)>(cV9V zmDz$%oFhi@SsZLWSUqD1ia5x8usA~|NSuL*!3Mm~<|BhTgEd1cXebzR3NB~}YzKo8 zJSQuHB3co28u$(daG44%b-_a-khLlz42ldI;2Z@SMFy?l0xhC60*ir8*iZ!LFatyA zno;mlEECAwiMbx5I=ecEG?rr&Hy1})`V378j2lJO_@dOrU$OMTqFoUkXV^U?(V_;?wh3rY?g^vh; zb|Zs!zJq8P&}b_I0|x^mcpV4>XxQftXbKv%{|Ph#A$AAU!3FL75jIz32cO{v8gmC{ zEzm?B=CTWC&?%GuJ}XEvF|x=hLMR0Y1?4dcI62)B7v$h$6_ z6u_j$AkCn`U zgE)f>*cj0HqMG3Pn}>lDyp9>vJB6{Npz3xps4-}Q2kbyoEHVt@;P$971B_(?l?9!J z4enThE?5AyCn1$SsPDzj2=AVWGpaGem+GrCig5(<@@aV{Tk5$HmLnUpMLB)x*q?ZyBoADiF!vc>e7o(igzZ^L4-yVI>vUh^u-f(D3 z4H0|L>I2bq!x#*JHW=V7HRAS8pzjUGzIOs+Z#ee76DXU*v2LCKx6Qy|!vqR5NZ4e8 z!sh>f@OhVV%-Qg<5-ZSH39QY=bP(32VkiK$si5kum{-EpJE5rm4^z(=22#(!^#A*R zL-4uLatunKQ%e{?SIdHvz5+bgK!X|4mPc971RBSX0+r@c&}thb0G(g}FC_&{+3o_Z zi2)DCLZ)$Lz=JKI(nJ9?ZD%YDUQ1|f1e)ZA6y#>+rplmQ`HY}rHy!g5H4G#7gzw=AO))c zjU{n_hiYZP%|}ot0n`XI0~aV#3_1*E;DSPf!3;cuVqmCpx6*xVR$d@<^F zV8*kULBx-?yNBQJ-$jfaE?7biX}eFcyB@;p#)7>cQuKg4L^o+{x@H4=Rh888rTXVY;lbU;8X+ZMj{Rh0Cf~WCxs|OD-{t2 zaquzM&`t#S05`}H2%uB~KL!D`s04Pq1ZV-+K`(!9aTOC+9djRkW)r^}KM`ixAQfqA zSwVR=VOBl`T`9Lf(5V(CjK|XgtawsVlze?$P34V^L1!XV*?PK4D2dpq6v}AH@z_6Rz*#Q<8#0p3>)nJI-VUSx%b5_C%mq6~qS5a3~2&=ei$iWJbyE$CDj z&{@x*c@EHlojbtyUxC{EpwdDfdbY?nhB{y=0JNS8F+v5M)5Djy)~t&!A=FoDVki287ldeAwx{|&)smnnf(A<9B)IoKEwH2)z| zC(3nCpb8CI@~(Ou#-C2H!gXs=q+H5ft z5&*RznC77#g#u5{p!x-p-W-wAn<}_|fuy%kPOturqTb0FQlCQ9gVH}leHe;*Q2K|cw?e(4{7!|Ia3pO&9gYedeGb`Y>hr>ZWM9m1axgYBBw#iOoRY*zbqmlLX#@ErVwY~fSwvB z4mxwooD6 zMOZ-z>di8O&ovHr<#H1a5@8e+4ia(WatjY~6#Mr<9DK+zn|*0%X{r6+UyN-HptZ6L zh;viaK`l*IXrCHZ&OnQQM2!J$KO(Xr^!6Eq@1bMOe0GuGx`K~EfPs%efq{>KITm!T z0}FJ16IA|yj^+Rz7S4d!H~%|$nt1VEujuR3dR6_6b$ILR&{Wp58i02 z2;Ju_DgrI#F^7j3IY5QIl`bQ{tSr(II6Kw!r8uD-KuwgxV!-9Qnz;n4mL$?~IExhZ z6~s6p9YSU|=&3PK3N+3NncD@GpO7?T4N5Z%jL>s}SRm)7@GwADAi(l3#`qC51i{%A zG=2olg5Y~fKutJo=cYi%$dJxWVHChT?S}{J;XX{@dc_rdrnU%!AE;*_3XYK-44~0E zA$VLvPv=6UVrY9CAplM1;8YBnbOW852yQpR4mejdW>#i$jS#4a3eD#9XXcgGQ?3yU z`L`*^ISB2P^Y8yMKsU=U*#FmI@?mT2urdPYzi;58AT=;)BoE z0;fD^Kq3l4jFJ(fUVwT)0JPA9ivdy)g6@X_g*!I`542!p5CH9S1f5SL#{e3$6cyOP zpnd_;Juv{So&w)z3fl8)4&Eom#|%B23A7v&ylRn;Q4wP`+El+VKSluo8CgY9Wz!6y zT)zy^Nn?{y(j}u93YUrH@1HBY63Sv~26-$@eb|pRf+kQX1xlx&^Xm=3^{_6a9u9%D z3qWT)C4>6Si~*2(n4uk9pKJeT2c0!9qy{>TRsdAbbAiRJ|AXp((7nfgVD&i+zrk~( z?hFh}{!A3iPZ>aBa=V0KZ#60JMP!&|&1GWw_bYMf#eZi(e)j{NOAj7n&Ap2~=;t=)0jDlcskox~jyWr-BGt~TB`Tsxo>?J8A_2CTD z|2<$}1dU{X+{3^Nnulgb3v1};CL)7Er_jM63EBV#xnGf4AJn*FgU*<9>VsAWfl@pZ z12Y5YDl|}k8PpX4iLo-UKu4uPw}F7Jg91&l!qhQ>ODWJb7n+R1?Dn8-T;}{tij1%S zCF_N8hg%1l-{!uf%IM7~B;_i_nEY?EtE=k+aGC*~yT!-!gh85NC1{C-47hI!DWIFp0i%uczghaO2Hz2fSpEMGZu3IcU_jRK*g@Cv zK-$Qld3Nwz4?`(*u1EbpJE#vTq{aYJ&(4^`@DH4hKzWm$NkB-A0i+%z{ue9`X@i6M z93b^<%#KLri-7waAoXmFp&;`am_SECK+o(`2HjF83SZ|BD)bcLbpy0>ixESJYz|F} z2mvCF^8+O~CeZp?$VoL!piRK=6SF|O>7Yktfd(Exd!#{EAFwM=fSwrT18rF%FJ)GV z4l;or8^tFl^6x043Gzbbe~+1->;R3~F*2zC*8znqgA~IA&^lXb@Z=3FJwme)B40rl zG$8~K2^2a?gFGt)uAQKr6mVZyTwn(S^94}47Y8S;9Soom8&JXqjn0UJCnZ70D}$~# z2QBdcZ^8thY!0bp>={AFa;uku?|NtlRU2aPdPG92l|2=5^TVIppkwb~^+gxxN(gA) z(_vy~5@66_C&yiiYS41-3wH#4&v$oue0k#RTjkud)r zL*pLQtB3^!1~^YZ*2sbS29Pyu;Nyk=|A(F*2C5Sv^PB~sc~1BmIZ)pKqTUI#<_)S| z8a%E9R?irQq&|pwi;x-vM7=GNdK+dgAvFew`e=}P1}0E%nyCbQzlb}(2%cN_b_ zOA}~I6C;Ts1fUBB!CnPtCuF~a*0>vi*VC{t7=g=g(B=ZrHJ6~(IbsYp;KOYo-3(3e zIvUX3j&5KbpsQQmz)n-r-^HNApah@ zu%rrFLIYk+16_Kp3ZHmI8XiR~v|*fX;Ka(x=WeQOAj>Od>}w>gAdPOFltnCN+5|&6te5&c@u_z)sfGUCXW}$yP{KMaaMueT|KzuCl1LS7^GBBA1yi zjK)}F109?40JoD&A?+mS7!>$iGVokXC`g>4)D_Zx(*MuSg_@f`Osqkjq}Pf=z;HS z0@4QTS67G-#4TTtFz(=a= zVlZb=gR-<3^r83ifWqAYavB73xSE3xwE~|~gZFSbHsqCGjEpFu++JV^Qv1*QN0{~5ynvom#q^RGFi|H8-w z&ZqwW*+J*p38{f@gaP$;7+!+K!S};}%?GJxV|H|b3kFkAS8~K;3^f&`KiE z*ei7C6?E?ms3v1fwhXdd!@XJ!eE-ZVwbk5=LI2h<2H8FG@OWegY43sD4LZM1g<%S4 zNth~w8dBO6f=sR=+z0KDAnGGT^#f`_!IpS}u1VwsUo#8qoMmeW7rblgIabl zNQ2MW0PUy-wRq(iWT9OR(EKszh+J?106tSp8FCB zA?hQL)Q5oESrGNMNa`)Y?JS7;XpnjaM)15A=uS*a(23T13|0)*NO5inigQb(eKgQU z2_g|7-vtS7gFuTgaFgChU?&6ULL$(xCO7nGe^3#n0N&RojB!a3@-!7}wTe2M5^R_g z^X?)}F$Du<6@6)L=l~{o1&gIVBO5mx?i-CPB3(^|MN}1}xgn#NplK{sOEFf)82B|u z;IkhM!FyFr85%(S56JChu)L}a3KwMtMAA0_378-WKnJ(Ml_NM@kQcCk1)u|Q;H5l> z)hycJt~uzkd1Y{Rm(<6)dIfYnChUAT&?!vtwift!IL4D229oT^Vd7%RC?LWj>u>Mq zgL!%+mceiEd2!f|lLVK0pgVbM!DDAu3=9Hn%ntUDJOVkh5i|!E1rlcr0UdAh|33p{ z&jY9(0@?H60NV5L|3CC>J&=0v+APKp$l5HZdeGhphym#XN ze+KZ`b>Q|-EJ&Q86jWyX|IYwX4;qVxsJDii16B{7%Lc1w3`bE98W(`5cSKeXt}h_! zLqY1nciepe&HXVbGZ=u@c&Whe;Z_0fyn&?_Xx|W#X^;y_PS9hp)GzqK2csRBfeZdRB_RzI%NpkBpw6l7*%isPFuL&3_#x(4An?pitmtKpnTj zC^DeF2bU0n0-#e6K$9U1(9_XDBWWVgGw`|fLHh_Gr{u#%pxGHw&g2;J?Iz zv0G41UQIH{WR6hhBJP#yd>&S_xsRA0;6C2Mqk4r)LQOhSZy5*UZ|8m!N9Tji|HO1b zi57H!;TNV{W=#fJ21C#adpQQkN$QXRZa#P~4Vuppaf=bz(0mRq2|zPf8sL^M3xfoB z!SxOX7X2LzpgR#|?m*@h<-pO(2s+jdyo{9vT>OC+B#A>-s9{`R$P~jWr^+WQ4%!_R zZwI+lGR|H{IatW`-(1M1DEQryOh=?t=3b!k=PLfe2}UT)Cu3gAbbb32}Y0sbbZtg22dG~RvDTbgU>Yt4{t$k z13<6*;)GlUS>;rPt?}O9m!B%7@^3Es9e$v;Mmtk6gDOKc$Q_UyJ79i-dI1r6&}mtO z0LGFQ=&&YuM4bh611GpV10`(GvKC2jWV16!f}6Xb5riEKDt8PF!AYDMbm{`=<_1#} z%zDh+n5m3aPF2DeHnN3Lcf||2GVw{N{4>B=VdX>D?uxLa!PoA(BCp+L-UC~^%d7)h zyZiq?WWO}Ho@ocwC(L%Bs}KMGXJ7#Bs{+?E%R%A{rACl?54;BxtbQg)oEf%{6ugce ztbQj*oB_7iK=MC3sGlvQ#sKP|AqL0^Dxj-gb}%6Be8L!Ug}M@4gn;gc5dxp<2yz=} zQ75P?Ed(7P8>YZ z4cJ{zLEzO<=AgAt@kO>GdOSF8dV;Kja)hmMQqW~!WMpLWWn99{2D#e~>HJh?eejtn z%#d|9u)FO*Yf2f@ZOwC-*+8dfFfcMKV$x*PVS2*A%wPu^GlZXO1in>VA9ja$K8)cH zV}NfJ2OTZJ2Nr>>Ou1vFlE$RD8+3-oJSKm}J4}BW*chBZl`K2B%4GmmM~P z8MIc;o)L7zo3)0q0zb0>KL@{{xS)WzxP*YT5x1O{HB+#huBo`FlCX?0C#ch)Br0yI zE62dd$jp?&*vxbj%egC{T*RV{?WQ6|TLmd*MrJuh2*s2lA;2okDkKM`z-NnGVRB;J z#SGe4t;pa4I=2Bf1}*@udqKX^hXowmdkP?t!W|5fcRpK%~B?lrk$5lcXesVsbLqk=9cZfsp#L(wsu9A`(!FfsrBq zzagVBgAjuRg9@k#CkdY50EInhWe+s-g8MqcV3$BLq#5Y+W^l%XtWCTKTB71=#caxh zv|0ze4n@maN{~$msiy);TML*{7+*16XOLuQ1f?y=jmiwQpykdIj?AD*Xh^ z*npB5JhTEpBA~;*K&?K;!W|5tgw4r-dKxciuPHBh2m=(9!VIimKr^7I=khY*K9{#$ zQiWeokxxKQP)5*50hz;;BFxFn!_TKFq5x+wFfklq@?sKX<^tcQW3h{Yi2-(2C=>V; zKTxP*k9&x3AUEp@tDDP%vdRaYY)3_%Y)2+9sb#9OrIvx_H2?qqzl+I}!Gf7vfStkl z-&N4?&;S4bpD}qdOaqIR!o`Z2{24Yf{S{zm41tT4GNmx=XSy!H&KLz3>tw29Sjluy zfSsWLF4oKx%y5+HzW_U97+mZIQwl=_(@g<(#&Eb;GgCIh2Bymb>Y4sBa5E$_>|o#k`S${7yBsL?^dSsZhWQK}4C@&<7??n!^&nADRPuuuAQ6xhxG({4 zv@3FCWdIrR0HmRC2Lm_wfFe*C0O>8U@iVHMD{G}GD@aRfXuB6P{go9L*Ok(eP63_i zvXaS(p`TerfSoY{;>Q2~|IcLdVtB^PCBV)Y4Hs);3THUN%qqan7z!7=#^lRT$;>9e z&KLw2d&H#4ki+yufSob;Kd74tx)*Rc;{}FDreX%rN$JojQ|P2BxVC1#1G>T%bh46x zp)ja_U~X(~?7=Fh8p#<1X$P6)gOc2y#)Hnz{~9>#wYVkZgxEEu`IHqHEuee-L31{cwF!{* z%J!h?J?LCf0QgKEh&cER6b42H_y1l@a!jE8i*>sgkk_q(#&JPqBXl7uq7Q*sm%D?3 z^$w)34>?#zjzJ81T(KM|KR7VRLCUucFh&EIu@ij14dg%s&~<>28FN-8HAssMKEDhe zAqLNN+A)I13)tA1`4|~nr1TV(O#%chSedo-6%3`Vf->jvF!u4xQ)Ur2m-X{yV=>^+ z-KMClW)#Z8=qjbir=rfq{ckzwI`i|KT)qw>CgT5I>RCg_eL_I(V#a1r`xg)So7*S`K>HHlxi+R_LTsQmJ#4Kl z(=H)41|4u49%Kx;{Lz`@4Jpaae+paKH4vKh3T4>XAY znoa;6@?v19ZVWm?UlCF|@-b;M3M+#an1D(i#C)zcBeRU5sIs!Ms3aqYzED7byj75K zW@awGsEV?(sH~MTH{%>*Cr%b7CPNJoeGg%)%%u&}|GkLxvX6IAWGW1=S)S)0Dl6`4 zZYIGe%de~R?*%B0FfnjL!vVBgy&H7u9IWjFIw23S<_4NL5a9q_y@(JH0R;^5-MG*d zo8aE65Ca3abF_m&NFQ{u4JZIWM=&aagNThm5!}Gt!N8^uy2S#NvOqT+gD~jm6iApb zE2D%)gTKj46MucV;9v!t5R+2VnWp}XDQ2#q5HQhJ&}Z89?+l{}EC4{O-I2Y{$N*Y5 z2r4h+KsQ-~&U*&kV+zX~&~g&tIgA~n&{-OAW`UHGATP0A0CkX&FR%!FdpWioPQ29<7<^kv%hw~wjluiG?G{{Ig?`wg7W zL6eP443-QGOiE0kO>hzn@u2m_kQOs&{A&jTH$0zXq;F{V9UQ@+GLRYUcu>C`bgmC* zkA?)SucQyUvj?1J713K${HtKoKj?zz2!geh32;pL`7Y41ADN)&GMMQxLLeiIUQS4lCDOWuY zhf}l9{_VRrZQ4C1tytB z&k$Y}0F`V4NHryNS_%@vpjrZ>Qji8|kp|TgyBHK1l);^T(9Usia#sYO3(W;C`dAng zp_MA%1<=F;j0aj>E(==T4GIlCMs>(t2q2OfbV-IBqqs0=8J;qTj2BdwU=$Hzk@r?{ zHSsrbZ3`&nQxsuFms(6#wetw%*x8Cq{zq#E*C*%<0nN$R#py0m49zR z#UltaFfz#g|H7oobd15AAr@2{STI;3%~L{m_aK4>n)txkn;q;S5e9bfrq3M=BKn{e zu%MKoF0hjUG~5n4Wk?WQ4}d3`q01kjC(ZU88xTU0u1E7#VG?OuZD%#0<^0pJd0fp5jg6daMcH^*wM^Y? zExB2_*>w!n)ik6ur1^r}wOkD3SXeon&CMh2l|lVn@OoA7x`9|wWg+fFjkHncdi&(U@6T*j$;NQF%^~r7>FtBcp#Qh*9|Ohd9aukN{O@{R|S|)XM~pY)%Fy1_|)sC8)h40M5XmN*jFWFF&Ik zvpi^^4b&i3W>#JrWKz06a)XGx2qPyS4-dPY1CN=429H;%X%Musej+Eq%*wpv)6uf+{L%BFeU^QXXRVNfR><{&iz2tk0RAYA2#B z`u{&@R*@+dT-F#cfXW_l9D(`TU_N-gC%C-|8pLJHj*Y<>>?Xhe zUqEMi3bBD!^n=#pFff4E_Tjm?FP8TpvC8H-a4d;A0BnOv8J{oBAN zrJycl>k-W){_k{GJEO(l8IlS@pq=Inpz{qLGx;)IWsqc0V=!iz4H|-m-FOe0*^;`j zg8@W?cez9E!Ej_|*bk}(KqD0f;I@||GsAosBLT)>fH5E~HqgBpps@=G%?n;M1DcEj z^%FsZejxFk3{v3a1Zp3sfmxuPIZ^^U7>q$)gq*_#x;{;g8L~W1S(uL*y2@1?+~^1Q z5!D|Xx#@{&cx5>9`|}&RgP55P{QjwGwtDhx98AnM_TH*?Fs3cDl7+X9cT=JjQ&bcZ zn<Y6hoEYfxY?sQ-6jGG+o*hk2kSyQp_=Lfi6) zD8-n5MHCS`75aPy~beyP$30;5!7B8P~JOyQpV`va-kqDKN7L z%4nFNHe`Zv=|5puSb`eU;55Sh-wE9IS7K-fT?&u7 z&y;W)frcd_aLG+HpdpqW408Ar56DZP1hk)9Qq5Eil7Qs+q{NF@mH)|?`eznoLa;YD z5%EcifD;iL2T~#e6_}8O#02ginjrcOpaC_|)&kI8*&PhL@O}eEPZ8RB1ee%|jv;El zfeBn?g7$WcLyB)@b7M&FpAFP+fVP3O8JV9i;Q6GV8y!|xc>H{&5DOENnPI*jQ<2BN z7twx|4IWt%psu{;zjqAG;C?(P?eH*2GRT7Fo0%Epp!43Kb?*Y;rHnfmWZ^D{t`$JU z4zwA75WrYU1HgeC{|!ycu<39HLQp3 zqA%^Q?*!?*&twwzFZD<2pEH%hI_{vOkwE4^vFcYq?{5hBEyiXd?S z&L?{RzcA@A9b?b{jkB?%ozDtgmx6E=v@8XOB4lI@v~3Zx3=MRSJ?PjaP+J;Ogo2i_ z7#Ip0g98-OEQSms^09!{YCtx-n%OfdzkiGvOAEk0;>Plru}xeLHlBtu-X^XgCs>~Y zx(fqz(EoqPxS=A0C1}RS1iD&r2ZIUh&NFan3T;~ukpfy?0 zz~N(pR<4X8u6Atf{-*w={w99xB9d}~QdVlNcI><&(#oPzR#KwMGUD7atZXXsqRKK7 zTp$)tu)l1C@xMc$rl?l1rPW&v)@GMaJ@ z2X0S5&fH}Z5Ml$J#mm6N0BTd(Fuh?=WH4pO-Nm2;Kgm!@UYav{wdPhKlP$c6325du6m|1SJ5_5wnO5GT=oA z&;$ZMQcBRk!$?v@%}iN2-f24TbZti|DQyu}Wj|}b1Wk_wBbP8{7CP`V@1TRyQv<%SNCYMNG8*XuKTN`gTEfF0)CtL8@j-c>h zV-jEhEittP?dy|buw$?XI~vqLWVo<{0Ysa_Lk3z|Bj#ohc?HzY1TEIK1+_Q9M%aQH zB=;F?LE|f|4EzkX4Dt-NpkWnO27Lxw273@Eb|-@xIJg8EY@zGFK+A;8p(omampj=( zs~uPm8*;posR<&{fUeMmUV;J2UW{^y`7Ep)x~|dY-VqL_M#c)3{yIi(?2L?YB4&nK zjEv5_CLa3Y%#1oZ>UO#^tV}#PS%T^cM&S;su4ejnd@P(+x~h%_GLD8?tlVtgEFA2t zyd3Tx@7M&zWCd9{SY?!@bZu35AnhTy|6iB{m;@Nq7=l5Y`B2w{LE9hL!%G3|aW)1K z=x^vdV>GpmOL<%!>|^*iIE|E*ROICiHPu{f?IU+bu;XVuMx;*zyMK6mBliygxVvaOv-4FaZ0Km%#vhGI#9` z242vewxGq2pkvoSK7_2sF*XttH#Ri^t;b~|V)!_jnE4frlx)0sIhmQQdF0J~bv?^Mbj^%Z zoFmP|nV8&}m|CaJyJ(~)1R;%h1-!R=rRJ>y*jTy}sCtzi8Bgn@yn zfQf~{nPE3*&x{L$E7F(~M!tX!wjm-OTFoN_Q0`U-jn6@kv;!A1;Ccx8oMvb6qz$P1 z0?Hm-;4rmfaE7)Mtn^_kXF(e%KodNQ;1Wt#U?&4;5%~@VE6{C<7r;XajOL)k1iIVR z2y{La8$0Mo9%DO@FzB3gKF~@_Q4!D`ui$l3X6DN5pmlkWaR)(vK{;+VHa-V#2}w<9 zenuvL6?YXsK1C@JJqdMgcZj41w}O<0G(R(wznZ(6FNc7TD6fc~guat_u!E|btbv>p zpOS^PsG`vA+aBRkB7CUgvIg9*U+X&PDe{W&fyTS^|9@fPU=m=^2lbWs84SSpkHT70 z7+Y_V%Ol7nupk3FgCaOJ3c@eERTO}gR_Y9b;HnE$Lc=yUfx`zf(q|4D8%J)?u)zu; zMs|-pB`Z093kOGbMyB)x?pS>nHYQV3<4|>Noh*;g6l+TzJq7D99wU7{HFbS8R(=jA zXAMOzZaFnIO+y_McN<*?J{B$mT~$Xz8PJ#vXdR|J6N?ZVLj-vI1I%Y;5K;s21=yIi zLBqrhj0~;}3{2WgEDYidm7sBU#JVO>8iINsk;VA`0Z>KG2QHXFTh7gm+1WrR>LW(a*p+AdsbnpQn4+M~C@8^gYsGCU zt;pw*rS8YXa`Wa9EiM*j5$Va!G74*6FfjiA&A`AU$;84S!Vm%~Z6GJCz~U7;l8-(|8`_Tq3xEnv25^E04bg)#2WadD)YJeCIl<>C*_;(kM5Gvf!~DFr@$yK?%JEB? zC@VUvbXLsy`-kyhkXx3YF~5{310#bH0|S#86X;63Tu>Mbg73zJxs7lbBZ3q<9EChW zCLjPAv0=IZ>862Z78s!Ww1gr1*g+$%+>FZX%*qvMUQCNK!@R6{*-k{WaawsXvHbhN z$np0NW3=19y^PLo|CWOC2x$IDk%@(Y8*~~tBLk$nL%e&Tr589X!6gsEy`bqd$eMf5 z)G%mPl;aDir4K4JK+AR6mHYivzp46#`Kf(V1IP2IGC7JN8l2J11$Ho4T-d#4u$5PP-qz~Ca{yi6D%gd;0fK{3OYDj=?>&lKhVlm z&{5hu8Qj3CKxeONg1uuRu#>?GEM~@F0zP!gz>v|75mZ>i&lLk-ZmX=MuFMWTUI?_f z!yZ2R1HS9R*ht)%-3)09J7|@i5mLv5pOKBxmBYy@n7c-qSyWe6+n8HiSyD(-)Lw{L zfkRzcN>@e6I8aClMb1u$nVZK+P*y^OgO`;_TH1lH8kwWP!<29AWcDW?NL!x${}-k}aC(Ra_h;SyJ2BZY zu`oC>%mj7uof%w^>SO3yd_>fmgJRMgX-}2{NI(Hc)Djvj1RX!A2aXZYre)B^$eJAt zvY>ViD3(ESwS&R>4yaB+A4XIMM-S-0F4)i<=1O6ke9Vj+_~bQAk%k$oS#_ED zBn73FnORw6{cP<$Axq#jv=ofRSOkRxSPcBU`MlI6WLX8oWcHzqLAEgQajDAj%ZY)G zkW)}*wD@-(`5XvF*?+d|{;INzWYxt$*8?&zGQ=}5FbOfSFvx-Kp%-O99eah2%OJuR zI+utLKrBYs#URSS3tep>s_zKu34n|fg)G$b2M<_+#!5uNv-Xfw>{1M} zD%Y4dB%@SV!$c8s7@0&DL> zc%U{JXs-n5NaAvBT^42=4=ZLC9Z`8+J|Pwh4=XlNQBfrxFgwhHmqo}}TStwVS;U6N z*nm$>h%x=&G9g873vodaeQ`#=f6G+F6{Mk@zkdSN%`CKZO~Lt0^uH5R6cY*WvCeVqzX{`*%xoV1cj!uk{Xl(LDAZ{)W&a0ON6!l$1J|m|293G&EH}L9eBu4UHysA$T+i3kfMgqREHHRaID)RX|i`A1j{} zc<5J+m0gdelZlT@L7G<-5-9|u@1F&`w~{zCCo?ga{r|!w4(^|tGi(A)l|p9AA^Xh0 zGf?0vAKI@&L>zR23?YC>+mMYEpk-<5;F-=H4Dz6S&A=efAiy9G%Gaz63mD`XHh?*x zVJFz`CeUaCXk86xs~;D55jD>p(Due13}P2{Fn}Tx(p?1YE>jm2vIG^Opu54C>>1(H zF>*}e_Ke2JCk(&_1k^w+bxwkplF(ET%Vp&ZU}M)*;FXlukY(lc=c;8^c2jo= zQL+ivHTL0RWHd0g_hw@@=85u^F*CPTmat|qQPEUm6Oa*Ov2dTyEGz5or0;K~;Am^b z%FU{&Wuz`7qGJLcJJEoaC1MO(40;UnLB53TUIF#)(w5^nQRKyvzJ}Vn~d+P>RD?8d)a)@$i zXxr#=3Ulb1GBASf4P^Yo1iGy~95gPZ0p7L?t81V=1cWaUBdt3@D{;PnDnw;)&lI$Q zVFv@K?*;15LPnoKH=u!9;h>H@IMJ(v#>gO%3LS7~1TAb;K7X5u(bZDZNzF_`-^fD2 zz|&At%OlYoap_VB-)%-U9zhN#do5i}Ic*tU*d|C3MO`T=1EsJ~(AbdR|1V5>Oactz z4BDWvb~OeaXtAn>vbPx{?l8&_Bhb7xe3S?j9?(G|#)w8Hrf7AKFb~y8P6Z=pokkh606jfZer{G) zIRz-qGf!Mw>=hHsZNpGI<$1w9X_ihNQFhV>f<{^}8a#%s$iM*Jt0u*u!B7TjJ!mp$ zfvXl#fgKDQ@bn8EJ4VDT@{l>$%g9j;>a~Iz0aDO|G&S^hGCm09u(GHx1gy5+`9r5rtHua@Sqc5!6!9qG6*neGAJ-;g3rIv6oAa;gU(Ht2j@3X zs~pnQ0(G)>FeqMt>{nK~0J@<>0K90Z7IcH41SnbIICKqk#0d0k5ooaiS{sk&@U=9| z6P*bh!iIXhGvpw)|Nj}lW6PlMXIu;pfAF4B@O^Jz!1ukWg68t%8Bq7^LT9`X;S3E$ za03gpW?mSapFlYal&nCjmgJ#xTw?m56~CY!K4?!TXuiT2ak(34s|UD_0F^A_e2k2I zn8isj-)-*OOiYj>#~_ghQJUyUN*O4H20HT#ae~Vl8?*oa8Ng>%f%|k};66U&tm-#H zYz#g~{L4aW4B)e+A@`sC{}0;J%EZqEQs)cGoBuCD>tfJ-X!Ag&E$lut&>7v9@Vp64 zUx<)FULXz5o5(>0n%@E?G|(#3oeYRTgIt`Zz@Pz6Jv$f_AZ;H7@a-H-vEW)+fuVpw zfq^M@CxaolAqqM;Km*a90c|;ft)4drudy+P)XSh5L#);EWEXd4W~~f9UTJw{5ixyP zL2fevIVo{&$!}^bLe|P=HqwSJYD(rjOic0$Vk*)?%*;%xJOK{;G9ru&A-?&(#)889 z&O*kX3HDQ-DX4qenTFdcT3doDYaJC4c?}UEac)h}&NwCp9%xvCnqccelhLqyIrs#2 zFd$kp7=tI!b%cn(g{DJrvO^18(2+pg&@{)PzmtI%o+2R!se_ip@Iwk$NZ^Vx2!Mtm zA%QE(Pypt@rjCRd)WCe`CN5N42LP>1*v(r$$K4>VK^@~1q!M1%ShTjYZL3F-qV z!TqTO_9v(h4Du(a><8r-P@5RE3mDRR1UI9=XT*pJE3vUFg62OMIbla=82K1WYI!D` zdWQ2d{=31%bg>a z9Sls++8?%@6m*jWXt#icq^~MuzreJ6pv9z&pb=sw6Q&7x?LmYYv||QM?#T9lYC>T} zh&`JamBl@j|2_8dV`AyK@XrA>?(V|Cz?8-W>Xj9P{DXR~H^!nHYGX zJD-?9rLjH(6N5bi6QsYD&%ne`59WY&Sb)+N=uRm}PX)9=8FWPhXfq8rXss!zAq90Y zyYl2Dt|WEFEbf1gWfpTSR`tDa`@&_3?R~IYwf}oDsWR#V}<5*Pzi@~pCojw z4jhM=`y@rdbqRQ%BtLjpq$4ZCdD?>sZRIz~a3~0FqGh>acj)I~|0Iv=+i;%pwgszLCaZo8ZKD1cmT{KKQ zbVOKKRap4M6}BlWs~Cl_{1DO*Qq>k`=i~l&k!hF5Pi`IwU1x1kBZ<@CQ+OE}T>k4Y zu`sbP=z>=42r%d|=nD#2g2DhaFn}>y11;sj{s))g&;k!!sDsuTBR8EvwTA?>3k@p# z1i;m=gaBy6B&f)cVGsbzLW&2_Tmt+cV$kvmtO1Y$KZ#feJ{&9P7Y2@C4bW(76L27a(!&3%|Ff9NnLvAHUx79>LCQl|I6xb4h;kk} zc8Cyw)@29*XeSjBQqZ|ZgaBeY7$~C%K~LFY2j$a#26hI}K@^~k5}@_@paX?LZ6tQ^ zhLIf%5*I+DV$9G{G0@@fD&U1;pe&#c+HTa(pbpCD%nbiw40#9xwC_lrA)i4VypK!~ zzHdetG>wUEZw+|fm5IgPz}Ymw%z0{917d@Xvx&cnGh%~{T%f8qXyXhR|_AdT00m(^J|*mM5}uNwBTtJd^%n}I3o673w>lnC+S_7fv!op$iA&az{0d<`Nmh}(kKy`}j|19v{CM(bi zH9-bz@Wtt{^iIUyCSmyACdB1+$a|Y08zn$t1(_rTg_ISj83#W?7gE}(K~_LOIm$|G z?BGErP2sf1FALfza6m_rSs^7GD|q7oWXGQ%0~Z4;xY7g- z6M#08LK2KLqzsdWm0?hZJOh{kD#N51@)@KdWf-XLgf55$Polv0S}`6`RtImXDh)#0 zZ565N3*J)&THwlP@(*dR6{u`tu>D`mq|5|5F0T<(R6usg!tw?*JP>6QG&~Rj1i}My z91-XMI@o<50t~FsJ_0BhAf*r}zewCMFjUlI)JN)QBG$q%E)upzOga zEwknY@>-Y+42%pi&^DwJLmy}z4C?*`XuAwg97ESEAc|M$Of0zNhIBU!B#s%uaSS>@ z0<@)d7lR-J?2;4EF4>sXY@5 zgFM4jkpC6Hi?v{J3iUrC3Nhjo!~f8x5r+R^CnR&+*}(wXTLj8#l zfVSTd=^NSOpuQ0!cz{iU0pU-`5(1F-B%u8ki1)y~Ib~=(LN+KPyvMG5bTaotm6%x( z-2%$ujKX4ECZP3es(e;4>Yy`VZr(g1F2TyeA}TY%SwVdv=wJ%aohVu0{b~H5`9F3B z)V;V+uOOlf+F3^k;B9$=ZX<#e!OZ$Q86cOPf*O>NJqVyw$`2`w`C)}Ilp)UmW`GJ~ zeujL|Tv(xjp(1z_HYhuTFuOA2i5Sz*;OzU^Bq%dn)fbji8BPB7fy-L;|0PWPOe_ri zpmSUqvHJ}XO;Eof1W585sNV?j8-%rkfge=DDkJh9XsbQro+!1qYJOpUs&7^OyhD^d zz*+0&KZgsTbOzd&)(h^7=Yz@-)c!5B;zI;Iv|vF9AUeFzxd+r%DFb+M3Z&%;*`)^Q z=Ysa;fI?Ilbf^$Gj2nVXHk&YV@NCNrGTjW@HSF=X4-~*4cY@ZnWrF+RxZQ~eMyNXx z0>ru#e1ZumG$HN;r+s5~V{@=OLE)O@r@C2{k&S1kr=Qwp$kIKRe-1BRfZXW!KMSlv64 z%lJX-d(6c_IaB#ls6V%io@;`=fs3vLn-{B~jFx;zvXB4^i-U)pp1O*NqiT}BxDO|9 zeJ`dp?NEvA+KNYX<|P{tgCKXoUp?-fLYZejrC0sadH zhT`Dr6&jde2Vd;r>Oll)4_A+bpRFw{Ty48S@%;bQ|173jCeWnwM$i}tAcuCJk^vgAVuv-FgQ)xPTMfBLL0L@IY?><`CGy0J_B&6zD?W+zhID zL?JZ~XsfX!GsA!I5YxUiJv zCon26jIeWH$<0dm_ogLM)wg%*)LzC6#-R4DuJ(V+|2+rSMeP4eKx;o4{6TF*PKE%6 zK&0|W3$$uR3u%QJ#+U>~nF)ZLjc%c*dyaJHf&m9a}09{WD2Ii8+M?RA$CllEiL+t?CRhxEpkkg@-wrG1lc4rvUoBviAX4!C<`*Ou(9&blXp^c zk(W^rK#^8921&EC@%Jifx)?Jm%gP%$C@{tcmvhQXvkA&_t0~(=Bx`UfDl2lcC+Zu@ zD9ahz%CmtLC`cg`a4UcmBgCB`2~s$N=RW24N*iAgZ4TQ0?0{B z95lKO?oEO(l->ZnM;g)%0i`U^_CUyCOA3&br2uQwKpFB3UJ7D}H4qYDCUq9V!%WWS3r@IbGT6ajU?{6TlWfTl6|8AKT58AKSEVnMBc$d!z+ zE($0}K&PHVf&|nF0X5&CD~IhFLC1lE@Ad^P-ZqA|jC5J0yv$t!*}%(T^|VyXH9*T@ zL9Havf><|mQMI{qd8J{iVWDj%*f|6Wp!3D#8BpUGT52Go6k}`^vDF&10-K#d5*)|; zAb)NEH$p+BA!tAybe)jEogEAyS_oRN$U$iZQ2qgr(!qTuZf?wO2VU~342oe;;lUK) z20f@i!%)UiM!`S^bW(vHi-fVPxZ_{;s6bI8&~XKtd={oGt5!1ca)J&lU=-%yVd_R& z%d7(0XiA2kD7%Phel&maMrMeqloI|I7S zM*=*c1Q~t;oxln@Qx9~UEM(QC;ji1YLI8w2k}UPo{;=f8(7& zZUvwHW(RT`g9_LUP`5$$Dudhx+N%uO?*%=79&}cmD)=l>h*rE*-G_;5A*$+R(LL z(7AUZHbw`qe(0I)LTrqIAU-s#YlYMpAz=-5A0(_n?t_FiLnbJ!|Nn=a-39UwBjh}N z8&Fdgbp8)`pWkI6HAcvJ`kB!65a6?ln6rh{pl1PEfzFeHt)F1Ryi*CH9^{@5P`EJL zps2qr1l>=^05c!#KahK%cTw6P-9;$@azErQN`_3(U6lX-gVuF2Suq_GVq?4j@;~%W zOaUP_#*1J+_*^2edm(2OT0!FrvVRHWUhujOh62#Kj{pB5=NN(Phn{H&4L|68I3V{z z)Muio-vU1a(H6;kJ7&<`nPByd(NOi^G9GOHbVzuBwtoNr58dAZ3h(V8afVD;$UYlT zKROqzel|#)*~S|p4q5LEQokD{4hatiRtEk5{YR!nU5E``l4m1}D>I{NtefT;CXoCz<{vcY^I~W-6fZAM;;uF;30?jFd_F8FyM}A}& zSfK4U8GYCSznu)4;P#o`1yDCc5qy0s=+0))1RcmMSWiyj0;sLGgFzp3YKyYDvaqqa zF+1oa5a_@@_)Jf8Wp-5)HDz|tM4TD3urf1<#|*k~%#M+f+eUbE=_X+t;n1)Edp#=! zn=ms07PSO5Ms;(26$>X0R^~ri3EGTimS$RZo}8>qOofF^y9x`#J=~%Mn0X!Tb^L8q z7`tA+oU5VolF2|(q4r{Wmp};}im?DbQJqwai@LM z1Uh|DfQ`W!ba46q|DapRnUK}n;ZR?Sq8@Y}EyR2qWcA=Y0KUVCArqvYfssM<{}-kt zrd@<|fy+y9_XAqNf(1Z_&Va6O0GY!8-e(C~vk9p)Wua3v zpe{FP>JL2FqR7uE3cd@6Nm)rv-CPW`8WG%()@HP4RAg4}V!Xk3HaJ1nD%{F0TFODy z&%%q7mD$KRj`za9i=Z>dg08YK2Z!s1IcwQ&+Nun1`>ys&!GFP zAnH>fI0F~2Qt^g)te%z4`9)Ns}Dv}ugqKrS8one|38#T4ennxW(O|@0ca>e{S68yn7`E+ zLqx#xpwI%zgZ*!Z4S92L1n`pmPv~)EGeZFDT^1 zLE*yS`9G8ibl!j(1E~H5iA#dTA@wyVeSy_8yD?US#X9Msi24-9B(OL{eIwL- zHf9T?`dtBhXCX*E8)Fo-eg~;v4~`Fr`F=>|PXnh%ka|#hL^2A#rU4z6K!Qzni2q?ZmcVn|NyD{d1#UX10LGFR5PhkuPi-XiJ1jiply&sbLZgBj8 z)iWC*skZ{hA4Gisl6nbn{6W;4AgQkh#~(y}Ad>nFaQs2knY3db^TFu`e7*$8{owN@K<*C#i-Xi}28So8{92>OtWEbAKpPJ2*T+>Ota=@BxomgZ#??xnI&AbO8r+9kx8kzo6Bm zptcpXJstHwlxYGu{6XqL<}icyAVAyeEDSmfb_{vD7%Um=863cUWJ>|$6YZc26%m6) z(EYFo0ciV4z%CLt9;AC`2ZI^tM!-9;8*#Z9WWjef?_l842d%({pHd7T5W}({n~zx? zsIB8*I|qS6rRio1!F zjAtnLqBTA_5k{SVw*{p+^i1WXSViU4#Tfbi{;>=+7LOO^W#wZP6oXRW^aTlTP<{u8 zAF~@{4p7(u5GGJx)15;j+4x94HJ&BMs!0Y2M@frXKWDT$yS79OS`gq|0S z7r1PmNW2&ixTNK<7gcZh_7-VRH*;HiZM} zh$m5l*(&ZRTcF$Qi1MFv#{dxlVkY=#HB7@Qe$7;?eov9rJq2F?pR7(lcL zlvah(wop0~J~Dx^iUV3+BSHvzFE)5&l!1W_e2QHSXxFX;=vq0@WFVUWXebo4vRVf! zU%P`rM_&RmjkJqFiGhc~8|*gF(FLGMCeR`D-q89(2YggB=btVEX@pfdSM$0N>XZ3|VH$ z365JVQcNHC+xa7A1KIrO(IB$jHPe0HGLNylf33oi!n(v4y^kxRjzIl*;4g zVB%oq;)7BQObmM&JsDpxL6-hxgSO=GgXdr%+hq{v3qUI-Y(556;|$OhV@%*(i6CDy z!*{>1T>!1DgA^X1RW;n;%L^IJl|d6%ppu_incbK%jb|(8Id3gv4r3k;=9#sOo}Q+k zf9ZKL>TL&=PmB!58J!v5GVNke0qxyoM4O9(u4P8J4m#~3WEZ)E0kmS7kpXmxBj^lL zQE;5FF(`v8KggbWIf#ptkv5Zpvbeb%_%s0UC>f|sP&YSbe7ahNna@C0$C6*kLetEK zPmfzgSzD2nkx@ZTT$$I*#7 z=b&^1F5?&>2@SF>kI~1*QQpX2A;K?FfQ3&_R^Q6cQAU@Ikx@kfLNPkqnQHmFnhI(p znlMSqSnAq%>&vRKa?+7Tb9$|E5e8F^pL6$+6!5*~6 zPJ_XL!4aJ4H9)7+U)aF_q7gf8p{rxC*$p~{7!+!94E)fsZaIC>o(PZ_JA)j9xuB3G zX!Qt$1)7Hi?VW|^Uo(3~&;%@K7a%BbL8tmbj#L2OkgKMw#Lfn)L`03uU;=Cq5k?nV zX9F!paXDcXV+B)tZAC^|1u=CqW=Bp&C20_$t<1`(rT``xolUhgO*vRORAj`|Wd#j2 zly$jSxs>Ilbrghdo3XPpi%CGpH7e5VtgNEaPzrSZ#5G1w#-~gN7(^M=LH8rUZq5S* z`3?p|T7xbPMT8Kv@&@M~VF5(iVq<_@w#};#I`khDN?Z(LU@-$jMiD+Hc4a$8bMRUY z(0*cLBQbU(Gw`&f8YugKmv%5dMN4fiHjaI=-~=ZRp_qKN{9Vli5XsH1K~{~Gi(OnE zN`cZFGsA@czZrKhmLtk+ZFrfjEwF=u;{xcoDA28aurvrQvn4=90=UfvO@rW5V(^sN z`k=EwL2(Y*+sZ^rnau+(tqGUetdPPQLJ}yl85lu53&y#Og$!Jv5(IQSJm^Acm_MNv zB6fd5%0bW;EM{;S!UC!@!I=kCGP8n9Nl>SP#p##0z$n90Mb`n`B_9$!O1L$BcTMC}{T}Xg{4XWAne?T+JeKV!X^u%94tzoF)SH z781IOA`)sk!g2ep6Zpl&Bm}s)#3a?k#8pjsB-JEk_0;A0oI&&Y)&GAnE@3QZ&|@e9 zbsivR5W&I_y0!t~cIbJ9;Kr&Bg8+EnJg6<|$j&f7v`V0UOkC(qcBsvc0AK%3E=QA$&enUPUOT1rn) zP1Ii3(#l9#R>jA~%#lxrTT4wzosCIVL5t&toCqr?i;#qbgp!Q1nYx+*I}@v#vZ0fP zth~64lsp%kxQrpV4r*gyVB82!KYR@7px!X7&gX;IKhP2z5dw(Z0;%sIYphZ0dx#iv zeGfWV6jI+SqSg2MsP#QkZO_Os;r}njZH$GGx?UEou7_^BN4N~Ss0!S(0JX3fz^NFt zDoq3&0jvy4(7kc2`k;XzPy{d%uIkwtnF&<&9E=V)>w3`k42Bv1e=#m+ECiRo(V!iU zu)1DeU>}X4Z5KWR9hJ^@H5DP%NhfH&|z#KF?I$6@PYfFdLF`p)bkeLHX%4}A3=E9x7|X%w zEFL|bAke z`7moUXfPNsSTXG1#b5&6+X>phx`RO-ypa|8gcWEyK(uC%(;&Y9WQ_x)41=5#3tD5$ z3_T+Oaylswygdk7c_$1SLIz*p2WcgO)`Dq*dzPTdcRlcO>s_GRwxCBRfz~c-T-e2+ z!vK>rVSuPNFceo)XIC}{_x(Vd3B!b_*dc4g4T2Fm8fdW@jG%4p{?$%3w}lVb!O)D8+; zR&#Z8anQmDHpVLob}Em#f_XqEAhHy5XEL#~`f*z++eez|yBo=}y7SoCdIy~5-oe7o zG>>~KyN)IkqZ_w^od7q#4vzqbiGx44l!~6bgNT->1HVO_vznx)v6PIKO>#81gP5j> zgPJ8Pi#>Ofu$r`$l&S%!j8kA>V2Wn~4L8a&C^H-e)x?ndD|ax+K$m$c!)s*dau7ti zhBi_V0?3Phz!3nAB0jsw-3*)zEDUVm2w($WEe9H4W4r)5L=v)Va|Z)xpq2|7(x72V z(4C)b0+1snL4z=$h&* zWDp^ak!TS~5<2CBIDum~11kdy0~0unBG!k3T9A;_!9kl?KbPC@Arw7&v$Rtjik zG^o!4O0OVs&n3&KD*5A0wNy8}Aok{L7-Eo{uprp*Yu2ZmW$7(AAx z&B(0$-6t^E%!w<^Cq#bFJduUI{!Gl4KDJCux(1<{K1}NWB&;l%IR3HhW7_5I$t5TB zZ#iQThX|LN*56hJW(Lrj=t8C^3~~%c3}y`5L0e$Vq4@+WP6p6Y#T^Wwyazg%cn1T^9Y=PC^$aWw`x#gm&NHwu zFvo%lO)c=~BdAvi%8Wt`Jm4{j9SmXupaB3-I|fvs?gCw!23EO)K~5i3RzbH0fCg~n zn8b~t3Bwq4FpxU zu&A1>fTNkV2Di5H0cX(sJOd;6t|d37V+?u>m7u{+)P5;O{z5L%KqJn=;Jro+;C(ot zDprC)7{2TtGMNHeLJ#T%v3zj^ZDEpy>=?Pv0AhgJ$)FvN;8rg*RM_=EI}JhSkwH!+ z69H`%v1095{&RAVuS5-;GNle$=ou{QD zZdIeNjIM>OgqVh$P>i38lA*ARqOF09fP$`^qKb`{1gKvQ$~zHEyBLH)C#LcK1KneW5P+8X;0yxJG0;tC;5II3Z!l=j7HB2|vg#Z%Py|{X3)*c4YPo?r79d&} zGy6#Mh|C3{eKrh*!kZM zM$q&)10zE?1NiP$2652ZUPcDgJ4Y~bKXk!7xVGkC-~g3|+PgsqC^CqF%S2E~$O5jG z*ce#Bqr9MVb~qS>!PN~Y_Bp|&0cgYyvNV*7ffKqk6jW9~c0dbU03BZd5eIG3f#?VI zejsLoat$P<7#OOuE1R?1GqKwn zE+~_Nifj-K+8zp`)!-!uMv9jNm64L5DjPD#0xAJPT^>2;HX87e+V>gc8Qz0W(RSoy z*v}x(aGpV)0Vc>&=*Z5{z#z{sfkB=Dblx^IXk$?VgFHh4NJ}B;_#SzvY1*KT8u1L; z42&R6pe?=n4B8C#4B8CLpq(4l14xFs(2<#;0VD%rd;l{* zRS~S!pv9mK&74~Lj+_kq3|b8G3|b8O3|b8K3|b5Xff0?Xo0T(VrS@Q&|;X+ zpvADBK?{6vD+72P_D%*>aJ+*KS5gsxECH5>4uykg&_p~)98_c5x#!b4B)4b8a(!b8a(<7ZU|txh)yF zOt~Et@;#;~1Tk)5HqxEJ`EBoWDnoa^e_weR10A9q`uiR9wH;hN4>M`1sd~>+ z&=ZJjQ&Ryg!e?f1`rprF!>rAq#9+i=4LTFRngMnasWr3#3Zk`erwYXQ93)j}F@Q=x z1_oK^8h6lw1kjP`prz5qU^zYp4rr52ltBwTp0|s^nt=n%+QDEA9^nSX6Iv8)?!p=R~8ll_1%<|l|X}(e2n5^M#i9~obqZD zQ#IcbKQ3W*WjjrMBMw(~Y3cYq0-Wj&F%E1zY|6HLQd0Zbc{w!g4dgimSeVmUIaxg- z`8aA>CCj4X)-?JuGNzY{`DlsD8uI;nonR5~rp}ng7;Nk#%B0VzXd7mdB(AlGF*eiy zJckRqvmt>Abl3a&T?~c{7ASYULxT%ZY$EEB-3)vTEDT%>EZX42%LPjLtPBneTnqsW zTntQwpo`6RGJr}NM`nh620oBHGsAii1C$XOKnzfFzW`#yf|@)#8Ti5J6BPDh;POk7 zK@8k$2930U+GQYBI~de1fXoIR3JvO&>|oFacY$C>!J05|g0HZG9Ij?+Vg^dA_Kb?= zir_X4Xo3MTkZR1X%+ILE4mpq!)DQxlyDO{ZW+cPU9mQS0j`80^ZpI=GPFpiocUf)q zMB|7QE{UTin4iac7)rt|cF%Fhe!S{iH z>Tl3MGc>uv661*?r(tZw;l?2?bBJG5K~GIt#12%A8}Lc+sMv~1NbKd* zb}?4u1dTDsl!St+cE+?~kpLYD1v9?CCs_m)^dWUR6H_Lmudb^Ila7*IxG88{fr%lE zfq|)z2{fd@%@7Eh>xZn00u2%FU|@vja%f$SNTtx~9~@4g?iJ{yM^IX1`2suXg9Wsx z!(5Rcd`YIcB6|Th<2UZN{z3j1xf#E5GnKix%=@Roc*Dg7oJSnMchiAJaVCLgVj*|x z!p1iUjw3al=Ms|U4W*ubZHf>t5GSiB4n9iX%hGewXA zVlreZ1+=do)Q}JYr}iBTVxU!gkmiapxT(U=1PVW6&=vG^v$(VQvw5;qgX9kL9^yaD zeMH&GKSwZt+rTr?X7)EdH@#?mH@$n?L)}87Lft~IRu;Gv2IzqLa!6YpRA+%MQU!(14hGQTDp0Zz zzW_Nm1$2+FfuT9*HbHZ69|ba;z@`m4C0HF?w}6X4ZN@`lL1Mw&!2-eD!4|h8LMrkm z2|VQ9-EZo`!N$aBtl!<<-tH3P!ss9ElUEnx^2EeQl}CtG7qr2UnL+P=Ka&o#HiIIA zE`t@rT+m7?Yw#I*knuRsAP;C<4$;?#W=4dMp;JbX+zDO$2O3ue_1O`pM?w4wYHfqs zvyhTbiGdAT(kbb~?(7m_P-3tGmusNbIA{wTXxvH{JfNZo9%TaWIs=U`fk*H7!GkBD zDpwiQ^@Xgb0r!2)>>(!xXfrYjx(eG0Czd7(+X}l1+lrW&d08=vYsd=7nt134+uHV9 z+HI2cBqb5PL0X(m;gPQQj!yM45VnkBHD0`vndBAB4+^vI-D1Zwl0|sUWLvZ#o1g+M( z0~t~Pm9C)Sdnsr$LIyl72%7p<15fQRDuEgiW}s^@*x1<^g^jrxjfIVwm8BV#nHfP1 zHgKjwoU+4iZp_a3A(Vw#GB=ckS?cfd(n=Oqwlrr5#kf&cf}NL%SwI9zXK|k6WQ;5j zP!`B_y9O3GXdUjgO;r4p^qI@hF zCM>+7jEi6D>AhqWZ)^MavOajuoZxzJJemSR1@1!I#Jr6_1DclzY^@4Z>IBguA0tJLvnOOy7#RX5haB-`c`I>l5C~|A7 zw=Pch73Fft;EWH+G*@&F4vWr|(h!t0)mGxl0LPsabk0PMK^=5T4j=r^a8QkpC_SNm zLiiey!gn$-GjM$YjaErOPm=+yN>T@laWF7LXDc}w_@Hx{pnyg@82?COsV!i*Rb7z6%oNMJMyGdRzrV-Tjr%;v(ztQBT( z9-Pl(85kIUGaX~#W6%dR+aRk-VEG)Hz!51PS`F}lH+!(_GqQ^dDhryc^E0ZOGyX<4 zoAJA0xDjLGKVinViHt_!hUY=!gt82MOs+8ZGcrK!+Rebmz{J1~-md~$rL4}bXsRe~ z4myjQu?*~ie;XJBARg#Lwgck+|6dq?Gl86r#r+uV9jJ%E?zd-@XEawAG!|qR2Q7_` zMK<~{*!7HW{|Uof4{|>PLmyKFlMaI!d;BeN5nyKUnx1LtM|UZVwt; zfJDW4u!+kc#-VVakphX6^I%s%*hv06#vlobA4!;<(1{p~au_kv23oZZ_8%m6#37M| z>{W;*$QD9FIy<8}qq#b}vZ*4IESe#Vjv!0Grh+U%H>fO`Oel57Q$jZP39^QgwFhxOSL1jn~`~a5_jI1sPAteOUF0iG4KQo?$ z+Y`jVz@!hehmirdJ)CflGeYvNpt2yWC`tfZbkK#76=D&i4A`Q7=1ja$i@@mwub-gB zrwpWPj_N04L1RH^u?_LlL6?8Op;pZUTg7c{i{re3IJg_xPy#LIh)-ZrhRn}zkX6j>50i{vUHY3m#zzn+?K3W;VxrnM5$*P(v@IWuovBVQ9~WH&5Qu+f(lRX0`S<~Hy%msfKL)mw~k1Eh_(i@|`w0dxWw zYI_*k`~}CW0s|8RKe%TDYWFfRFns~_%b6Gyz#U_KeOMt0S~tMQ1j$;Uo*yIVWIWI@ znxNnV)vTh*f=njGwu~CmrkYaf?x}V*DPEe!}J1D8SM4Rb)SQ(0|Fj|%{DljlIp!h|B0o5=1 zpr*V&XhM58=s;Zt1+ZU0t9|sqeqn?zkpi7o2|54@R5=+Mo+sQxEZ)9q2w& zP`SZIlst?89wW0&f#E~RNIt-)wp%&5RPtHhE~McmNJ zL)R?YMNP>e+A_FW%TRZAqI+SWwq9_tt96XKnV2FotIPlY3?l!(GPy7v6H;e7{Qp10 zECF_=gA5G+(?RW7u$(Jc?jQpLSne=Hj)4h$W{Eg>orFH9&(FnR03ND_3}++evIIe) zAqWZ$XqAiT^Mgjd1fe~BE`8801JJl3sLu~N&<->>sm1_0It;XF8FurKn7Fwfqq;F9 z?!gxjq4ik*=y)2-s5-}*8wUw9i`1voYI_*VXn19sJ16jRGBdeaL1`X2b3c8Df*=D! zEvD#6dJ-Nn*){>qvBs`GeilMvT-Mew8dNv4GB7Z4Fo8OyoD80z9j#p8p*zT#d+hMO zDaP0>%IFYub_z5C1UjMFz|fr0SeRXrQJGm$kWn`;hWj4}V>;(=Mqh8H{J#^unUeoL z1?6281_ma7aKG3Kax`c_6ZL##j5Zb_@j5r7fD`CIFGdDC z1_q{9CXnlbKm!brk#|@d9U4N2Ex#Be3eZpnhaqGn0g_NbqX`Cv#-K54abaU=Mq|cn z#;IKM1!Y(pX9xT{zNl%U%?c^S08^LVwOg{u;fPzJ3G0u5+L zgXc$gFxXrG8LtE$&a43~{*W**G*?38Bk;yfNNWf*Cj#Bt30e36Ui+blyk1|9xkFT2 zNlZf5Uc=IZlU3YT56*SxViot5QL~a{<`$L}Q#ImXVpU|+fzS$2;W_~o3k^*lYh`yE zCr3Ej*2&4v(p*MYLD0yEQ=Lyi14>z#%jzl!GBEys`d^2sjOhu3C}^*RAOq@Iw$O1N zjL}5!QM;hIf5^x@=m0`cHxIIQ5_Cx;C<$;t_gaW6AujT?W@J}p=40L|Afu!$XKOl3 zsGh^gYAyGLCdfTRGP;V+21{6&ei}PD|M>#Bbm;$o1}%mjCVR#kLTU_&g2@7G42d7+ zgYpoAF@qPA8Iv@FFle?1zNTY0XmcpIZMd6(nSleo@@oeJXqbu}wB7g)=zKuXk}1fF zL(mL1Xc!B$Mgy|)3)C|KEj3g&M=a-3WY2Q1<6a!|lADpo-51LI_r={S#i`XP1xzq7 zF@Og5Et&inK-*_@K&1&JCqTy7F!q~*Hwl4uxr4UFD6$K)va_0IU;>{fCe7r>z|O$UpaYr&f!P6CaKZ&IE1`1-U^_sO zVqj>lYOZPyvO|$wF_5u}@!t+!#xA~p<{<0-g`9xeVCKxg1UlJ_$qHN^b2GT3`-2T` z2Xs0DQ92@LWKdXwwswQ^4Cux&V`fHUVMcXfW@TpK8}k^20_OdDCv(4s{eBH&EMx4y z#s3z&GfFcG{(JiGJve?r=d1-WonYW(&;srKgVYENph_LIzY<#dLW>ee$pN0GlV>zG z7Zzt!*7Nc5W({8A{_jN?;}fxyH*PRam;4*Zz{p_4z|W+{w3>mNfd^DULPF2L&|H|= zT$)jwS(u%h(df(>`#rnt!K68(2BV6`zmJUk8vkzmyQ9I#|L-H%eNqgmOx8?cpzFDI zF)+c}?7P8f4l-6CY!2$*nkt$qLVNIY!EL;M7Z^(*%{oRCaNF{V%fA;$?Kp;JCTpe= zh`GGj%w>dhl@(1DO(A_}CTobnJ&XoWgO`F0{`bP=3e;dm23dwwCOepWk<0~+ZE%9y zs1WxGDvBzKLfWLVU}G6e{#}3?3vn-_1l(8#kh#rFb};uMg#*G|hG^f8=fI0LQ; z`UG|`^nuQgxUho(#0HH}gJ{rFPY?}Sk_@6jtEEA-GE^OC&H==?hw@#av>TN6h0=ad zItWTfLg^SN9S@~bpmaKv&V|zXP`VsS*FfnODBTIAyP@<1C_NQQ&w$d4p!5nTy#-3| zy}+=G;RwSF24-zg@iBwp2zZgP00WFA4`(UBS!!^WI-KPSXSu;yzHpWwoD~FTMM7CS z7-k6UU`V|)4rrLcE{T@vhy*TE1MdNDw`U!E2*g)^D#0TiHU>upn~Mhz>BUytEo**ploJS z@I;2FDwJk72Wu8*H#HVDHZ>Myh0^NA?8+d%x~Z`!NDf4UFoXDNeAqJD*<$X+u(ikL z#s0eyn-{x3jLj>?0tD`X2nz{2b^}Q^19o-;HfI$!2{s-!RZ#%}Q4rQMP*yeo;pajD zMn(ZbV6sWeLsHaJSJzWi(qnaATAOWfH=rNS=Z81SJ%=q zT}4(#O;=Y0|Uuz=R(@iVY6FvfzeL1y~`nNSDS ztDxB{UV$A9@}N3K-CUd*oEVUgQouZEMNucm@r$fD6BDb19E4f|p`bjbBT~y$|9uzZ zW94TR5ywg~Fu>f|f#02=WqRoDWHuM)XC%U%_Uwb*ZuWpIyOMyn8K5;bqE!iPL?F5c(1}}coerL=gHCIM1(*b2 zYeP5%U`M)uS{l6YgMc9GM?@LezkpUAvwblz1g}E0V+J)p*%g_=%knY9Ws(QChY*X1 zyslgU&jcw((=b2p=iYvdCl#ca8JT62Ar#|5=WIzW1tI6TfB!HZ{AbTpE+N1s#402Q zr9gdIP&l|UJz)@H5Ch#J#>pTKUQ`7M2Qd=D0lKpe5mFfE0YMLeCn{hBKq(j0s)Ysw zctJYoI4p$6tGH)N&EcNo?Z>#muNrjE64bBG|LmEb`~{8q+W!B-_>W0|L50DDp#`)k z%#^_lT&IH0#X+ud3yuC&|oY=EKh-9;~Hcp)JGATjJr(&ctZK<8B)m zry|5}V5q60Bdj7LD6U~H&nM0IZ%?$Ym9wCXs)(PKCLcG4ydtQt@n452k;#+6g29a; zg5lUM26u)?hA438xC=lQAcAPbP9ls@k_Qdt$Rk~z0Sz{Ahf7`nl$=3Z5j5c8r2#6i z1Q;|J7>mF+-D@xuFlc~pF>nSQXbQI58FWo>KZ7&Fd~E7tFjzZBPKy%YXe4Q4JF%PLj@yICRSyoSe_zwbtRh+Lpcdv zIcXtHrIP5Nj(_(|Yyv$)bu70lC||U7;b3MqHib}E>@2k`c(^&`CA4(7*+iW6v^>q^ zdAWJy)Lk{S>_8kgCT;<7esNwWYv0f=1uX+z83k^2Mp08EAsJRR9Vi9bGsp2?hslLW zfWe5tg&~OH&@KjJhG2#eaQGQRH~WC-0C?`jh&$-K7Dkc>$DO3WP6o&_8zp$$DM8{+ z2^4qC3<(TM42-e67(5u1pn1Uq5^o+1^BFu~@#X=JH_&pW9SopN>BjKbg2W&wpWux* z(EdbhF~*D$N5Up*pvV)2L?uRyX)7-03XnvPt3XJ^K;!2yIL^Sahc&8rY*qBpV~KGO zG*+P0zlWeGlU2|Htv_alo%asDlU)Nb1RhTqTREUv3S4A@ z?#B`VkC}l24s<^jXxAhcc%Khw(Ikwe#vlTY6wtlL5Egiq6y)A4P;jycD}ipFW)x-A zV`PS|@?n%?1m8$6qvslDmmUwgKr2osFhxk1nT0Wu(`qJ!`ENO=6~>M9OkH(ZMM83X zj28NafA>My|4!(GmIH&&u>Ar)!Yl zgnG~&0T6o`3P9=^m>6vT8-mPdux9XPmVGP@Xf7(~G%csm$WQ4Tma1260n z71+t(3@!*+!6SQS=IY=|MHyTy;;44OMWL|~=<+mXBb3UAF+*5ITENYckr!0g*y5~; zGz=s;MEIm-)%XnOqEtxdIfP_Iw5+9Aqjjxt)=g6CvLYOUd{Wwc;*4x4l@uZ#m{p+h z!^Ri^iU)W)+5;13)I4i6H;UP z09GG_EY5TjE*=gNXJBEl|F6T;#>~yY%pk%b!63r`-roedDF)O^0(4b|nptX>a z@KO(&6A+aVbX5sL0DAHoBCkLP>A`u0i-Cm!vfUVTeh&`=sL{dAzylQnjps?+*})(K zYET+8D}!}wTpAf!a$FjjSpNRG z!YiRHre=`G!qoS7B51gm*=ni!Y}uviv%%@a>c0+?2dG?Q3;?YGV<=^42amg1|Np|I z#q@!}iD4dS4~jE`3uuiCbUY9`uZ(CjWArY}K;?)TQaK_A5|Be$8R!HOaDoURx*KK! zuq`KY0+7v7pluRn;MORpB@8e+FeB_kGoekwp+%${=WGsX%mCei~l({S@ogzaYi74x6D@d8@NULyj za0<(4$*WpxiZOEY%W#>PgnIw|01k7f|6iCkfX`q43|f81Y`2&}fSuWn0W>1Z@c%!9 z)&DO{nqYZdP#R{o3xU@)pfX&D4U|VF@ zF{Uy;q{dl^BG}JLWPq;b2Q`|s8Ng@tfJ$(1Qx+5(;E7I9On_H&g0{_LIV;i0)LKa0 zMhi@;D66q^F-yxsC>_WliLkR0Q(dfe18kHbq>+)ftpLBQqLH?(AfK#aJU0guJLIfH z5XHdAaDg$K@fi55#AwjWCpUOY1FY6#1Eobaq}g3)od)(N67qfKGs5{sOvwMjCu?w}GLsxg8_o<6=mjS5)qS?7E?AdRnxJT zw}ZGhmGLIi0R|n=d3^#5y5PnBuyBL+FEM5&!0u%ShZY|L19Zs)pS~mLz#cvZ2L?Wd z00uq=<|0RCFoUshCxal^cM9MpJjg$441x@x(h+Vp?sx2cYCacfJ!J?@z zF385gA|RrrEG4g{tftMz!m1=|WXr(Bu%9uT@j5uY@G)eAW-$1{8)G1ol!&vopanW2 z1Q00)6i>|HI04P3>;!rE3up?154tiKcEKlT2{$MufzD3@Eu;V)E(1Q;0JLPAF?AW& z`Y=^J4n1xgVHxHw#?{tW?i)B1tX|E)$Z(M{mGJ@iT+NtW3{tT5Fwim@dg?mDWzgyh zDQ;vLM8PfY9SpLdZc;pGBYELY2FQVWphg)e$st7m(t&{Lps`e>gSZ$Sjg7hVIb>wS zbvc-rWW>Z(xr}tqan6-vzWNysI3_n{Fe2S|%v|wTfl}n5+=2mk0meQfh7P^XDtbCdZIwqoOCepI% ztjzLKV3IM_%1FcAT3_abBw8s_rc|gHiHc)uc|ZHGT4C&33cd_4G^sZ4@u~}Gs14<^q~U|H5mq8=<#LjcBa^(0vLY)ZlPqJZzM7gICkuy?l!m3Y zl9CoT3x}edn7W+cF)2|Nc4kcoIo*hbk5yPiR+O2aRY;V9iQyq*Hsb@P0}SGjb2XLW zYy3fb!o=aB1MT);3mr(mMgbZcu+oiPegtSZQzw3cs6S?j=J44T0%vfl?%OU2f6qE zFUGlyp^&;=3VYp-a4&NDfYt2^(E5Yix}6ntpBd<2C38@PPN;5YB3QSxL8^8ZE*6|s zI|CC#GXn$TY;bx(J42Hjo;#o=Gs5?XR0E157OeF;3%IyNuGc|jI1dA;WK>l~t=BbC z>vacswa&oEFco~hsXv1X=q`Ez?DaarWti~-s@EZBsmtnPR_E|Ahm^-^paL1AI%g$R zowMPo%|Y`s)BpcsoW|(SAjtr(&xIJY7$Ehz5O|~))`G-a3K3JEL(jV*RG)J|%2hn| zIUBTO6+^Gih1fyWIfw+UV_eI?z_^*wA998!I|J7G91%BA?}10pKxqL~pYLE`z5{CK zu`8Q{Yx5jfZO$kIt<6E@`K1597#D-f5@>y{04Mz) z92~mj*5^#5)aPvERObv#3`-dp7?&`H!s>Gcczvz_K8X;P&Y<-zf%=?qZ4OFjtC%tv?=f9rkYuRX#lQ^P?^6r9#6iN5nE||ME7p-2bhj<&_yW** zPz(@80BHQC(2p=2)aZRG*8S1E~;yHFmUQifR^Ne=H6MqKxU?d8Cbu7_5kyO z4%blpph<6wO|I?k;TWzXl-UG zAmSy)rO3snz-{HGqTtCCY@ll?p2fn%#F8a$X`~IxGgp`_7*8?pVPIwuW>97b0X4;7 zXWoHk?LkK-f@l#?o&m2y@CUC#06PG*9044hAjS>`nLD6qZ_sJbpaqSftP47j*uanx zwkiSMo>NC_&M_vaMww33wGdU&lLC?YqT(v-ERvF7l37otNd4bpa~)|tB@qazFDWm= zBO(DILF;Ro{?{^jGMr#;0k6o?0i|G2?+jOrg9ZRV@hAd1I!;NC34A0b_+$ZbK4wrj za%&nZ@H6WQ2&pK@8YoB^@+j!pi0I@vGI>gC8j6W4s3^#)D~L-NYDiT}EmLJ+Ak%(v zZ1#i3DY4mK;%;JqX+NW))H)%!{Z$OY4Dt+S#QI<6&JG5-J3ARf!D&bsJtUYRONu}{ z6qT{LC@NA}K}66(%RL>7yA0(-g;YfBrBe{`bB;NmL6||CA%j@=v4M}G-N7JoX9t7i zot+E{;5;Y8pa4GLx^@SH3@BfM4pLVI%V7uEF@L=G{psE1Z4ybl#$q&em%g?F2cvFETRCTp=)EVFt;7Z)q`Hbd9L zV5LCmw}8o&(FDBah>O9TVFv>T$hG#ZYi?g0gZcsiacSkP4zF$k zm>KYe0qBemkl#_mfD1g1YhVb$M1s*GEm~-1COPG zmpOoQEHoxEQ=yr(m>uetdRWf%hdT(emTM=oC3G#90W4M^kpZgrKR%GB`c^1Pc%Kh)PD4rC{Z0WoJYbu2bGBEuwVsd4;&0NPI$)LcH%>dfq0Ir4P zU||Ph>|jv1vx9;6&JG4)xT8Tk1K7X?1t$ZfYOeuT?VxI07`!W>7F><*U;u5!khuUl zHAoITE&&?tGcW`#w*=R)pdtgb_z_;ygEow_tDEyks_+Xc@(IWZ$_N@MAahzH^po1v zTbS#FIk|cG`7}io;Ebu=lKJ_{jSP$o3z+#BUokf_NHS!Al00;eG_<~l)@7^=pnW=^ zeFjXR%mPZdG-|K3BfB5PGt7K&$HN(*_R1<|A;x>)cp=mM4B#W#@wc}i5k*mZi&+TX z%z`sO?%%?!!FYu^n}Hd$vjB9EBj|z$@I7FTtPB?zm>C{0Ff%ZLjwu8k!QuepfKIvs z$udD2kjxCAv3Ukam8!_DxF4M4nX{ciZA?aHW$fVN#Rg1iedLIC6nkPjhkWr%Y?P2n93prdC% z2dPMdN+8he6{wdCX*;m4T;R zAUoqg%PV&N*>f8{h*WWsHl_e$n-^Ond)p%BaV?#km)ny zLFTOt%nZt)xIwxz6I{N4qXZT=kY+8TgMN++(`T7w>Y()VjOjCDGE6^c`!6E>fLsGQ z4-Fi*AO>j8Nu2$er-?E1H2HNRAQv*sWBSQ>hj}>z8-pci?IHXgI&ixiUO!#{H^9;B zM>g%pl8Q(LnXJ7-}Wr1{89RsND0mVGH{?!M$1yr^` zf(@Pyb}%sAfrKF_{LIDK89AVKFe2Hpj_EJs2j-Ow;tWQhFhraI3$g>3(V%_9l*07||P8`VJVeQ>D`?KfFs_M85q_nVj)t}wk}+{Jtj zJa!xf@~IN^SVG7>C!o9}&%glgYJv-DP>Br=-gsE>g3>Z$A*f*s?o3I7iwzF&lpccs zWC#k}p9D?4fHMSc( z()WABc#0(r-1k#uhyeBd)WFB>KyF;c?EB3JCp|}226+Y+@D&GOuf>B!cQDA3)c-^C z2WAgYl?~(84eFkg6&IdPK9hn*8VGKx{ z7SVVzFhuU=Xr(DDNK0yHyB9MrmlYS+mC^!tU_kx5&kQG6^uYZ~Wmx|Ol9s@E8dNxt z->v+Ny;}+H*AZ_o>D@^Tdm-IPxV|dY9Kd(T7Md#qtNbn$R2ny~njc{4vd7ZcB`d`5Gp3wxHrm*#MFMvx*Tz!35KNpmS zpgl~azCJibp!M}XFxi_4S#n@b>i`nBFt2V=>0j*N24x(S7{` z?j{CjAk85bN%X#cA=7(CAr?(=U*8?v*9V;e!wNpL0G#|moqcdE0%Cy9!-VwwIT#>4 ze~1X*1;khY=)@$9o_-d3PoG5-R-D3m`po~Um>C#$GCzi38BE3?QQW&x^Ci4DJ52Xu$i+a0aaV&%peT|Nj>z zUZyh)<_wMu-V6-FilWMbkPV&YFov1ADd;$M&^i%ub~R-sHW4vnMbJhgVLA2T-t2LlfS1GA#2A~U-bvtIzUMeFyCL_@O9E>ft zvfkDju2zzwddijxAcm=)EuWOSiGq@~ijKOrvbBqivW~C`V~>KRr?Ihunu4)$n4Gk_ zwwQvt0t7KI{k`;mF_SmbSq3+TUDeo&XDo&;W5~`A1OlOUJOA6f1 zZBEutsmpC{Q~P(p*HSApDAXY_&PLTsHZ+K{3CUX#T;cHwiGSxaa`7i-tP)ZJ4RSL6 z3uWMByvkI`V93BA%*m*3Zf0)E$E>WR$E*aRpeujvn2qGXeK2!1ea0h-wQ!-I7P7(4DGL;fml2tNs1PKcZtH~J1u!>G#s{D82-wCsY`aDw1 zva%9l3W{Q)a`K{qTFNT=Acm;CoT#{r3@fX|LIwuLe@YC`8SgUAV31&7Pz6t$f#TH6 zSW#3_2^73c%1Zy@ZDo9{6s%PBL}dfi84v&S)d<%Fhm!Gkc}q_PH3btdQw4PeaGLx3 zfgzPifGLoHk3p7!fl*wYU7TIqT-;pUT%Fxq5taMzAp1V4!!^!D#m+T{rS`EgUSb3D zi;KW~cBa7du7TD|j8?BUT4EjOdLEU>!1VVr0}JB?rgjEC(7HfIW6#G}!X@_y6gPmzeDsm>Gl^7{t{X#aU4uujazI*7?tGkc(?wii%xo4odE4 z&y-{CRN8dGHPC8_@tQTpORNH2FEB9vt72eaY-2hE8uv6-W;EtxG`1-Cw<(HIhfzD4 z>CoR^rebi|{a5{WhT#_TZ1C6$12gChH$i2=ejmoI-psRq8?u}SVFm`Kf4Bb^GhShi zW?%+w=VLS$R0eIZ6Bm(VG8P4$!Ysn3tZ1reWM<1~Vg|ZE!rVknS<&?0ZEtVJ)iF_w zobt9tah5UB|6VhSTIy?>8f!BeSpIv>9Q{Xv`GsTT8ChjHURNXQqfu_oDtcF4Y`D1W z9k1&uJA>W$@6P{DMq6eV@L47dAiFggO%(+dO%?y$@$zCy{AbEo{jZhrG_%VeS1)FD z(48BM|JMGW!Pvv}jDeehff00!1n3w%V?jm6p1(OvQ~u_7doxX8tgi6>_kh{&-+czA ze`o$LVT@wB#lXtI!N9=GD9WfTD9p+Dg30OcJ|7>(U;o&cZvC4Q`LCbZ?5{eb1L&qU zh#lQb&luPl7#Kw%7BF`I&Gq(XO!N9z!|eC30pfn0|HX`|%+U-g3>uI!QHf1dT!f8X zj!~8oRGy0}Dye}Eu~s$(9bRZ|Vg@p+jZyU9YfA%0ZDUhSeM=B0I>s{2$X1?{F^Y-Z zn>pH9S@*i5Jr|da%T+xUXSb-M)<&+pa>}x2A{{}Y%E0vR@c&Lm6=oOk`78{qpwl!2 z6-9*wl?DGDW<33`m9cu7S2^=;uRk}vn0NniW!}xe!2HkWe<$NhW)}v~jeVf9See~e z6tt)kgcSvqO^v}Ad>W0pF}pD%=m1r7Wp>7wk@A6&Vjf}>#XQ9RGyGYz<`1KRn1{q< zad(NJe;IQBZZbZSn_6DjIRMk{ngbmyuG&T|w1mDPK3JOxj9si0L`~E3!(|6I=1Dmg`r|+WAlooRF zV#v>k4sf99>;KD!h8)Ozv;WCVOw4Hvsth`yU4X)jqO9h+jB4tl;M$a3RN0hGP2E(E z(OeMJYB4u878PSQ1rKEjo2aP^nnf_K`qwMQDEe>SYDw?u_qVN45CFXm7lo*eCc^v}92ID_f z26iSvrg8>B1_n+hP&KJ&s%WmRXsW2D&&bFCsuJxTq?P|XWA-sK`?G~vN?zW9sT^GE zxhsfx+t_#ssknjaHo^Z(7~eBJW)KFIJM5tLkGeU?C`D66VR3dv#`phXTp3^feZ9cB z_g^pLJhy)`OajXtCo@)pOQSbRpS&x)808fIGBB|G=lK7H;XBhIu-m~Y1a!ruA~Wdx zb}?|QnV2cUGJ>gzSqFy?7vpgbAFhAL<D&D%)rLL1*-LR8O>Qm*~MAa|NWI?eDhC4 zj!BhU?(Z$R9=E&q+3Va5CO!ssy#1p=~ut z`a1_oes+Btl7Egc+sG;R!J1pz0zM+@3n6t(IHL^XN=DGwE~qv#RTMQgRTTXf&R7@5 zSjQ;i<>iHB$319RFblIYgVH7AJy1COEoSQdz~ubz-@kumjKW@w!r-vbX5eI84QW3z zg1Q`_#xEbU66o+aJ|=cCc}8|lMsapFc2RH(o!Q8anUTpDXG78M-+M+uENx51IRRQK z7;Qx}Mhiv@P^(gsSq9Ong!tExfrUvI)(#U!ZHF<6@`74o=OekkN&Z;oIi=NS=70IG z{~7+xV@&!NhS>;F@aGg` zVdTHbO!fZ`g7S_8gAC(zXx@=!U|x>D($)G~kxv}WqDJ@gY?=Dt#ObQ9SsO2l8VC3htT@Q@p zm=UF{=P9&OmVtrk-|7GJ7!_gpn~|51UDZ^T(VUf?QSqM$;~TlZx0zJ`o{(cwlVj?= zch~J#(C@!ToxVZx_&>w{Zy7HzO=e(a;ACJBWEACOG-cFfWM#bY&)}aS<9Wu8e--~K znI`|c9r^DDqekSv+dsa21C_Ok(0mQ*X@b(AqNpM$K{B4R>r<9vw)t~JQlroA-+QJ? zvxVv+J_6bc%@{2}ZZ%-gWnzMrDa_&!$3iGkB~TTp^v{)f1B}HK;&kz%^NWb*5Q>56 zpCJPalLS*W0~@HF4vuj~Q&mA@QDsqOaO+4Fn$X_=_~G~E3sW^TeSQ7U;PIc~zXu}& z1LNQS|1%g5GTVVlLq>7Xk?^Lf;;f8}wJ!hcT(tg4F*a)co5#4H*-q(?k9;JfY2;_8 zd;gv>F#i+(U&?rdITCy`sRIK8qY|4uqZp_u$2S|b1 zY39c4it1{1Ova4n%Iu1IjLN3qo9-Bo*toHXvKyG1>#>WnxY_7>+Q_jA{Hqi75Su9O zE*==65EvouAu(CZL!2>KkX6pcQ}=JH{A5|CFh-})02h{j3+0$a)pRb|J92T^IbG6G zbKzHZ)SU@x;yFKd2DR=M={hR&|DEypv9_kGirQn)Jk&q&|DB9mklUD`uoJ}LYC*7* zjTKE58Mmxi^XK1rY;I%x^LMKJWZ9dH>i@no{rYp$i%}KH4UD>f*Mggv3@raZVYY_p z5Q8+Vh0Bc8z*R;GFk?~B1};`(b7fXzc4bYuz-Unq@k!$D;sN0bptdfkaVsA1vqik+ z@A--HlbM*C#hV$S4P2=2o#U3weSCBY1IypL3=E9Vh_x5g#01+bs%Sc0C@@;wLt>J+ zr({5+U|^Jjr}AV)Pvrn6wSNZx(kBT`Vz&Kf%-G%GbnxK8gH9dJU%q_#;%wpMbfBXS zd?YH=&)LM<&8jSH%xG+`%&2T`jLXlA3^6t_e@j80md+P0K=1@Q;uSj1u*fm?G*a!DuPw0cHgJc_}|dmPttN-*FH*8Q$$cN)w>83F&!&`W~Qs zRlvB0IhH}0K^=6irI3Ia=;CU1?1>Vz$XT2Xl$ER*)l5yym9M52>BbaE|NDp~ao&+C zj@K>zCjd&HUpYChF){o*cPzueXL>Y}bK++D>u3JcdHmSG%|RDbuZaKu!uXNtEQ1w; z9fJpGbP9aB2jsMFX7J7^J|@uJHSo?ETH3W^204$_2-Z+!)QQZI($LqC3RN*NRdF^E zHLx_`-VILE7+JlJ&)UQFpB*Tl$H}Uz%gU;$F`bQdP_t21RWMgp*3fg)Q8QN1Hsn+G zF@^N!pvm3XOI_%n%Hzi+kTxa*1Jgh8|87j`Ootd`859{9KAxh zJcsG&NaX)0dKn<$X+*DEYJ6Kl_6Vq{zEI0QoK5%1_>EGx7Ul@59Rx)scPNh)+ zcNaj79S95DUHJFeNL9_)SWT7D+*no3$Vg39)5pZb$JErv#6(Tqgn{|r(*Iu=qnXY! zfUaKGV_*;l^=J5)k<&T5x*2G!31qSwG**;BZkra7t7>Yh8meMys^V-MWMdop?*h2H zqo&5V1(a-MAjwwOMN`9CU0FlVT}RzWUfa-HH}&rZaQDYb4W3T_z5tKwLF;s3Q1elj zQ5{q-LF;ri*cjfgZ{PgCeg$_(!6SFhh*}-gCXr$YV0^}80P6oSiz=Hcn}QBnR0JKo z2x=afUhzMC*q?Ek-?3wUjMAr1dz?Lc_N>S0)1Y=9%Rl4)Ul{j+!vd6BbQlRX;AAK*T`yU z$dV8x49x#h|9@fZV>-)V%HY7@1xp(&C~3o#50$SDo+&ZKo?=c!=1OboX-YwpOl~rl zj*hl8nCD~?WMdOr>yai9`tKUd$!o!>NL?KibQp!-EG8*E1r-w|l!WDA$b*r%SpHrA z?+zc=QU+}~0f#7L1O~YS4^HO%jLPPqp%SnJjB^q^uBFGg88WP;_ivK2o=BjNfg)H) zB+y4+L63>YKpEQOhSLA$87OIK>^~!}t7K@Pq@lI{jF_Gh=Ld z=@Z-+wf{ezi3!qXW@Z#+RafN%741w+EdN^c7?b`f|6A_LxYmVf%fDTbpB?Tq>iyLK zjg5oy7SlhFy`P}=f^8L6XB3Cn=mD}8Y}ub`s0PNrH$nB*|A`FD3=FKMilTpSviR)+ z7n2N(zgIDEGC4CiNFcb37|cY=nO zjKKZ-ehmo;ZCMLdb$dk}F=6nGi<+IXwyut%9c&gxUeQ$EI91RCHVdPmV4@@kot;sG zXp>boR#dlB)Df2rQD+hFM+_B+u}I5^DQPN!=3``)WaU-lu};%~q!rW^pp+LUixg;p zf%*3%hE&EQOs~OnEIJsoENBxIim3Az|5mc?mpEACQdI0*4Vtp}yN!Ln)S(*Z640cJ z^gedxsh}y18s{Q#I@{0A^cr>AWfm$ImB+yJC*=PO#)(YN7{D96SOtv*kw^Uhgm|-j z`jg8%`A@F5H}m8SX1_n17$3t&{uvmU|8o6b!l=P?i-DPe6Eyb12pXb+%>euj^I^OL zng#Inh0Fr{ImK-D4|OJh0mTj63=Cj5KxYJ?_IrCnY(C@lua4R8UmZwKFm%jO05q2b z9d#5{7Bm(G4?2Q|9}y#uzmxp@Smw#gJN$mYa>2~(w*g}GaTb%?d?6LLe}`;rKxv)v zPcU>GNdP|Q2bx)i7z-au`t9WB$FdJ)H0dPc{Zo(uCAL2c{(oU)W#VDrW)KJ8^=ZN2 z%zzk=2Mv3J>w9o5j~JXsADf5OP^zFt2t@6l1(3lHC4I(6uz_;MhtQ$KKeqbdF>H_= zn4!n`5IVewF>a4Key?l@AHP?Ij^{HlFtGhmWYA>X$>ab(yIT!36ABy4S5|_wRX}YE zaGwh_K8$cGva1zAF4R+2VoZWEm~Z?oV4Co^;Li`311G{5r``1R-3$!Cq!zgNYJ)ln z$!QF1zc(@DGJb;2F$h3=q=he@&o}GjO^5_bQtMc*X$|SW6*% z#GC`_tOKO{V*b6IA%gKelLolG44NMU`5KBD-~T+pW&y!W8eU$9!Omx3{Cx(RR`HLI z|LSF3YG(E;nzb1F_&D3|?f<_pzK4!!gIcM=pqURwW@Tn}sCPjv8<4lzL9KLfqvAgU zr#Dy1-%2)%zt#V)KMLE!sqM%vur%z$oOkNZ;$Sa#!6|DQ9SWSY(ZI#NN9fq_xcT%27|6?Aw%Bdek~ySm~ZY38fj?N0w;WG%FP z`qcK{yWb0$|DU$o{zsef=q0mff6O2Qt`;`W%q}u8{@(uo72|uR`JnbA=!SPuMRxf7 z0mx*=_kZU)7P!aPt|fafIsX1X!m{qw_(+3!zVx=ZNa8BCMi|Ee*wx&7I~T%PL~$&|x*uhOf+ zdyCQ}?+ULfaXb+7`AqE)+H3kMyOhRWUAlV(c#g1qv{<+EN&BfHp z>CN?z6VzsxQCDYjhDE6htfh!NX#tw9fVI~Vam@Jp4g)(ljztk=9Vo=i)fLsv#X;pL zq`UL&%r=2%aTpkwf8YE6oUsQf zUPVFXK~fAOI8qpU{xF`g-}WyRlzwcVJYik;M{Aq?8OEbt&&)1uf+v!HhrBPEJp+}? z%)hTQ=rYcOjysB=&SpU8G=Bd?Ok^O37Q8JhUOT^ zgb64t&x8DGK3zlN_nZ&5PoCI*U`duyo(?fge}%Tdzjx4ily|}!D?ol``h5yo7x93{ zN}(o#XVoBqnFMm1eZP+A@7XM=a?1UT`5-&&o<4<`YPL|94>ZYvl_BrH$nz(#oXu##!1z0bfs^qsq|FGO zQ-ZV)QCo|8OzNVbA#BK;3TT%Cd}$6+!x3}=IOA3i!Bk^;Q$;ytRV7%XPgh&nP7SF| zsA?&zEg`MeZ~yNDqkw%skAQlpEYk8FF&#yFHKgXIf{Bu>lCYF1V~es@AZX2wn4*H1 zyo$W6k}QlTCL_%vCZq`(Ne4~M@CtHzftrdSX~?oF7*C&p`41O^65~9kMDX05DX7f| z8EJ=1-^nwAt`7iDCxLEu!?McjZ#QTRJW2OoGm8*o)4yh2&=Q&y8=Nb@Y?O7>b(Cye zY?O3_O_>s%?me*o9QpY(s4sAT3hs4aAa}Ab&V$Y+f#%4u&L+VY_biX%`YQDUvbg8J z+&9>)67wtA00GYVB}QKOY9Ho5_5YVJ%mvRgLE4;aFWmQ4Oe+PVh|L$kLA|GMUfAxawbz7^B#~d6N<~-JKy5PG0}Q7#sNiU1v-RP6AIjH67J5(Ev|3;hNQl z%{t+q+y6a{ft@jgshk1aeg>@zG6m27Kr)XatD>nQJ0qK)-|r-4X?t+)w6m9&XPNg~ zpXK6jeKV$%zq@9MD7Zsr8QoNb4pl(f&tP+JgWC-ZjEd~y&}k*8v5dEWMVXngF8kGs zY$|IpW3`QqH?nb{@`edy7I?iHIWTwxejXcm$2HH znf*FqhN!TRZGtxbZ5S9Bf1hArXFSbR4(d@bg4-X6;so3@VgHlPvUR=P?B6StWbAA~ zMV76dygW<9Y`gWpz0DYzkC|;mHhPoUF;Jj@%;jVZVX9;hU;wYcLJ4VbAUjHG^g+z) zQXe?3JqaKA<+CWIQ&6rhm}Db z)EWX+6Ra3%=&u;4CHS6Ekd^6or>QAR`R_d_aqhz82yGQITKqvzR!qN7!0UP;kZ-|t z5%T=!pPS&8=6faw`+gM}mek*~h4esm08=HRN-|rhA?(AW4=TvP?O09*UB>fBZ4=0J zs45Ee_Xv1SRQaD9^9IF##f-g*pm|a-OX;5*+B_+2x)fXwfXY|U*eWlBBxqb3JUtIB zr;rC(|89ZIoFWR--`~Icfks=QQ>cg{6lK_zf$lwxoZ8BhR&`smOCQ`i~jsX%(F6!D={#z{ceWNU0E~OGdMH2Gk7!jp;A zG6~_H^g^0x0!@5@Fl_P*y!ejk_rCvbj8~as8FWD7ldSCOqKddDYM2$(nAPkULA&DF z*~Gw;g;!OXe)H%~6!#Dhh>#DA6nB@H3}S<(?37tNuQ4(@W<}^0B(bt~{$`!1Fu7lw zk&zK)CgbWEO z@QgkGf*IHT3;welHtjIM7&`v|S?P5PKKtNf3M!wO|2+8rh0zM!MuC_PN$`*X0&ouo z$#g+jd*p9`ks4?=Lygf4YCf#F@ozU|W&Gv6 zzXi%HKmP6qkD!8vYr#VXu(5_`|EGi3Ie@kwgGR>G8O1>(uOR$K+m&&xvnOcu)eAIm z3pM~U1PdV<7#RPE{&xe9?Xfd3u!0<+C}{le2h+@7Ele}K^S!;9Lm3!|W*UR^#kdoDk+5lHwPb<`znQt zpW??P^Y;m8y(KzklKCgexaeOTItHaDmS2zlFJbrw9!q3r;04!~pwUDShK(rx*7ISK z@MdBK4JyKx7J~*AK^T2tk>yv!|0N9B&~_rkJa|hH)Z$SUWc)J|+*LpVb;Lp(z=Lpp(S zo4E2DZ#hn6dH(MhXc38+hZtn#f)}JF6^-s_~?Z`xZnrX z{oh5QeRNg^Y0xY^s2#+~sK^Q4ErQlqg>>3~q&$5JZcwvVz`E~0ritqG*?}5Kpv710 zStwn3=%PXkMhoz`$Us5==M z7(}^jA>xdT3__exe=#yK2y-5RiZe3^a&|%4EDTbd-VDqPj4YfC42+jRu>)o^GDtD) zgNidT@G!lAvY8pInEpc9EDRdV;ZQa!gB0^LD4UHzg&A~Y3CK)#hDFRjq2e42Vl3~W zY)*zWR$(Zco56^6J(SJEpu!3>pO;aA%?v8e$6&(#63P}}km9g{vV|E+I8q>NMg|5g zE<5^Z}cj$WX#i%231rl2>3zWJqC1WGG-L0jpAANMk5s$Y;o9P+)Ll$YdyD zNQBy!!jQ@UIy@1d8x0vu8FZnpHD*v?2w}(o>%wMI0N9=^hE#@Rusvq`!ip036<#V6&1LQbFqnobwARi!#$QN)%Qq7#SFuD0n*gDmdpC73e8A=Hw`V zrHU1bQj1fI%2HGG6nrw%6H7{qQi~N5QxXeGQd1Ptit=+6+%k(26`b=^QWZ2%O*Ays zH8L{Fr9xs}ib8UJN@iZV zLTW`pYEfotUUI4)13b(F7;+d=87deQ78S)tN85lqo1mvVvDEQ{*rxh6N|DH^3&jHT1TNMH9fPqB(*3tMIkd!AvZCv zQX#9fD6=>vGr1%)Kd)G!JijO>r93kQlwh0~@)JWqppdd!iABGHi3?K|C|FPMNNZg1VCoP5(6^cnma+!%Zq7(y~Kixu4R^GXzg^V3So z6N^$oX*ee{IW@01HASH`4;0TO8L0}vo<0ix1*v%u#Xb-PIts8{Zm4IdrvNq#suyHT zVp(ElPGV9{szP~YNrpnAf}3NYLSl)P0yL_NlZ!G7N{aQ0GjsIvi_-P|-F(nf4w2r# zXCS2HQDA@;Ab84N4~Be(a&Y?~k)aA4Qb`Pn49N`H47v=ULLrx-0A8m-+5+j|f+!iB zr4+!i1Zq)$S{$J829@(59fb^~&|)^7p^_n&A&(&uTNZMIpU1H!rccq*##wTv0&sFUUv` z3*-opCqRi7Ks(qeFn29$4b z=q%1jEY3(xLDvhq8@&J=cGw-C&cINdnOl&9?A~vU2s4NrR;3ILn^}whEojZ8O|`AWjM$1k|B-ZGQ$Oiiwx@-(iuK5Ok}vm zaE0M2Lk7cZhPMoz3|Zi6DwiRLA&+4JLq0L%)JBD41tc+}o?2H_YoQzxye;NKUax?NU@-p%<{AXlfJQH@cZ zQG-#FQHxQVQHN2N;TOYoMmWX2T6RK_&MbjA$EOvWt6 zY=+&8IgGiCd5rlC&lsLF7BDPhEMzQVEM_cWEM+WXEN84>tYoZWtY)lXtYxfYtY>Us zY-DU=xWVv{;Won^hPw<87;ZA$XKZF{VQgh=V{B*aVC-bW9(;~z&Md{ z660jXDU4GYW;0G>oX$9daVFy|#@UQ>80Rw1W1P>pfN>$?BF4pxOBk0jE@NEIxPoyd z<0{70jB6OzGOlA>&$xkcBjYB<&5TWCFV!NkeL#l+3T z!^F$P$HdPhz$C~d#3all!X(Nh#w5-p!6eBf#U#xn!z9Zj$0X0Bz@*5e#H7rm!lcTi z#-z@q!KBHg#iY%o!=%fk$E43>z+}i|#AM85!eq*1#$?W9!DPu~#bnK7!(_{3$7Ijs zz~so}#N^E6!sN>2#^lcA!Q{#0#pKQ8!{p24$K=lxz!b<7#1zaF!W7CB#uUyJ!4%09 z#T3mH!xYOD#}v<$z?8_8#FWgG!j#IC#+1&K!Ia6A#gxsI!<5UE$CS@hz*NXo#8k{w z!c@vs##GK!!Bojq#Z=8y!&J*u$5hYMz|_dp#MI2x!qm#t#?;Q#!PLpr#njEz!_>>v z$JEa>foUSsB&NwsQ<$bQO=FtQG=pg-(=4XhOmmp#GRg4wlHjE zILgq(aEPIuVKu`shJ6hC8TK$OU|Ptuh-op?5~ih0%b1oktzcTow2Emp(;B9=OzW7| zGi_kn$h3)RGt(BPtxVgPwlnQu+R3zwX*bgzroBx2nD#RrU^>Wji0LrX5vHRIJq+`i zjxil)I>B_3=@ipxrZY@una(ktXS%?2k?9iCWu_}kSDCIcU1z$%bd%{8(`}|ZOm~^? zG2Lf+!1R#m5z}L)CrnS7o-sXVdcpLP=@r8ah7}C67-llOXIRQGiD3@Ia)y3}DGXB? zb~3$Ydc*XV=^fL1rVmUXnLaUnW;nobkm(E4SEg@F-HRwn3*m^qobn7NsGn0cA`nE9Cnm<5@In1z`|m_?bzn8leTm?fE| zn5CIzm}QyenB|!jm=&3on3b7Tm{pn8nAMpzm^GQTn6;U8n01-;nDvrp&n4Otjm|dCOnBAE@m_3=jn7x^On0=Z3 znEjapm;;%En1h)^m_wPvn8TSPm?N2^n4_6vm}8manB$ofm=l?kn3I`Pm{Xb4nA4dv zm@}EPn6sI4m~)x)nDdznmZd! zn46hfm|L0KnA@2A*8`4977 z=6}rpSr}LtS(sRuSy)(DS=dA^Sp--FS%g@GSwvVwS;Sbx zStM8_S)^E`S!7sbS>#ycSrk|lS(I3mSyWh5S=3n6Su|KQS+rQRS#(%*S@c-+SqxYV zS&UeWSxi_=SSu9vAS*%#BS!`HrS?pNsSsYj#S)5p$SzK6LS=?CMSv*)gS-e=h zS$tT0S^QZ1SprxBS%O%CSwdJsS;APtSt3{>S)y2?Sz=gXS>jmYSrS+hS&~?iSyEV1 zS<+b2Su$8MS+ZENS#nr%S@Kx&SqfMRS&CSSSxQ(+S;|<-St?j6S*lp7S!!5nS?XBo zSsGXxS(;dySz1_HS=w0ISvpucS-M!dS$bG{S^8M|SthVdWSPV=nPm#gRF-Kh(^+P) z%w(CxGMi-%%UqUuEc00wuqP9rCrz^a}$$`^7GgeAvBv~Qc-GIDqA9$Vt33>&(BNEW>19B+|J3FMaiYPX*sDC z+{p+wmvc&fNn&zxYF-IfGMvfglAKtSpU0L0pv?3ZdCtAtt7RDQDCXah^er|3e#IZc-NE~ht zgkJ6p1e?VpDY1wpBPp?n&9fvkCnc3F6HKvtLVUoU38C3M!Cqj?1XJvu5Vx{tLTDyW zFQ&{aZZCwn+*t@Vk2jJ-c(PGA=|!ojc{z!BDVfP^K8eYtC8=yVV2Z~FSzQhihs_5Z zGHf|uip3`-zl0?xCBKBt53G?b4@~j+A!*^sL*lUcfi<$_fhq31#De_dlA`>Aj8taV zymaQ&ymWSdh^N`}AvBvm*w<|NV2am2qckr)v8XgRC$Y4IHy@eD76i7Otq4r<1R+_= zQ-s7}3j$ltRs^Af!OGc+!4!8eBIvk_5p13iBm;O#kT`515MxTf6iWywN=rad8VZg} zwo)*~9t!addntrw3k7?JtrSeLh8AV!rL&fTNZwFnKk=3#^VmZnQO;fpp}E5mF61sp zVn-seD-mqAD6l8ls=yRi6g=&6Rl%9y{A+08#_FGuUzEq14<r(hV*`l!#s*OH4WZ^4nnKhYLgkI1 z<{LrHHG-OJ1TohH>Mj$gyG#rq=9m~j^qCky++|__G1tTZVvdOc#2gc-`%DZV?lXb9 z&jji|6R7)4pyjTKfh*X3CI)U0+7J>hCWer3F@gHS#1Lw~A=G}Tzf26F_8UU&H-!4b z5Nba(UQD3=H!*~SkBK3q+&3|V+HVN8-w0~I5!8MosQ;niW?}@j-w5h|XgHcc!_fp5 zjz&=XjUe%3Vgw0)6CZW~ zQ2!f4?Kg(nZw$5H7;3*U)P7^A{l-xHO`!IhKuv4pzE66zjkJH*5hYM&$2K1Zm1j!^p?q4qgK&2xmB=Lq$WBh)`m zQ1?4Q&2xg9=L9v+3F=-asC%8D?sbB?*9mGqwEQx0hMMOLHP0Dpo-@=w7pS}oRNe(D z?*g^Y1?qkmsQX=@_PapscY)gP0=3@-8a^)2@Nt3q&jo6qE7W{fsQIo?^If6lyF$%( zg_`dQHQyC#zAMx`SEzYzQ1`n*-R}l9-wkTM8`OL^sQGSC^V}fjxf;8%g{K!KmZh?l zL#S|YRl-&drZ~e>GE<9Ei!+Nk%V8`~xo7BV33i8}Dkn~~b3P~S^u8!dFG<0fn=IzjDsg4*u{^|uo^{0&_p>C4a+lD-UGA?eG|)fpWAhOW*~`<DSN|l70EF;5lKu@{A?e@H)yDm6d^{2kRrs$5E@>FkRrp#5R&DM3?W(F$Q&AO=8&d@krSi|Y2*ZHLK-PuU=pKzeFXZem_aQcf{c-qQ;r51~N5H!v|afJj1kE=WE!FmZwU zo)a9=d8N5U9Qk>vdSFILc|MrIRgzJZ3K8Q>%P%bg3v#AqmO;cgiZd&~LR`hEWvO`( zW@=`7MhTe7nU@JQ5MoD4UVbjv4ln~`2Z#Z+1H^>b0bzmc0I|S!fEZ9az)Xl8AST2P z5Q`fr?zterom^Rz!wK)|LIlBq1Yv>`ASYO9Mt*)aL=bEWJd1$G2KW&v8>}0N57vyt z2OG(UP!BSf50(ueBM3;U!FF+hNj=c?uKUnpd4cm#~8{n0dY*A98(a-6v{CJam=6` za}dWI%CP`(ET9}q5XVx_fDLS^0XxW60~ps3%r%5@jlf(Z7}prgHHL9bz+4j;*A&b( zg>lWmTr(Kg9LzO`aV@}H3mDfD%!Rqi2<$2&n5&Gyt}=qT$_VT#Bbcj?$LetBk;|GJ?6v2<$2&n5&Gyt}=qT${6e_W0?&iJtBk>}GKRUz80;!zn5&Gzt}=$X${6e_W0?#wOt4zSIGJ(0u1nepkn5#^{t}=nS$^`5x6PT+^z^*cZxyl6W zDifHiOu()(fw{^Q>?%{3t4zVJGKIOy6znQfn5#^|t}=zW$`tG>Q<$qv!LBlexylsm zDpQ!NOu?=)g}KTU>?%{3t4zVJGKIOy4D2d1n5)ddt}=tU$_(r(GnlK)z^*cbxylUe zDl?d?%)qWPgSpBK>?$*utIWWzGK0Cw4D2d1n5)ddt}=tU${g$}bC|2l!LBlgxyl^u zDsz~t%)zcQhq=le>?(7ZtIWZ!GB?$O7-_0!2+rl^VAq+OL50m=!eCdLn?r@oVZvb7 znp;4HEnvc6SDRZxg)O1N7GSF^3?RZ51~6f;FDwk9!iF$mux~7kpu$ElVX&_(jG@BD zFk!IoEKH!nCNN>JFD;`P0iFD+rdv;_Oo66#A!m@h5CzO;n;(h}xNORz63p}w?)`O*^XOG~IP zEn&X21pCqw>Pt(QFD=2ow1oQ766Q-wurDp4zO;n-(h}@TOQ1SVqwmobIN zn8IXC;WB0r88euS8C=F3B4ZAdF^9`oKx8anG8S+dONfjmOa>kXh7hkC!otAN0H((f z5(b8_FffFNfgvOe3}Inl2oD28NEjHx!oUz728NI@FocDHAv_EWAz@$$3j;%V7#Kpr zzz`M&hVU>jgoJ@1EDQ|cVPFUe14CFC7{bHA5E2H4urM%$hk+3!3_vw7IGBv!VPFIa z15jlQk%5PS5hM&i^)f^T9tK8`FaTB25E*zF7(v1SR9i!2;9+0{2?J2Y4UvI|fe|DO zKy^4o1|9}RkT3vM=MWiq7#Kmq093O>WZ+?71PKFBB@dB-hk-F93_$fgSjHG02F8#u z09E}E8ACm|h!G?s8AAdC)Es~)F@h;E(t|01gbJvM08wHLQ(_EP0tp^avjL*S1g69U zt^^WJpr!;wi78BpDO?F8z(CClh!QiH5;HxhGtKmjK?@}r8JHP37`PY&7+C-RXW$3# zR^nk`VCVzwd1nx1U}O+vU}O+t5Mp3q5M~f&U}O+skYQk9kY%W0U}UIesApheXkd(H zU}TJ8%w%9@%wo)9U}Vf@%wu3?%xCIgU}WlK>SSPH>SEf&z{s?j=>P*0(?OI)~VBlgbVvb_q za&hwsVX#XqN=#zNNG!_DV<-W2*BBboQ}c@$cBB`jW-}bgNz7MZxR8@rlE-i-H?bsx zF`y(PF_STdfd%C0|NlYzHyIci@U07lsbqw@gW>;w1`wNzfiXD9NddH*nkaK%`f!wn_9H79ECAz>wn^?5n_#;|(T#z@)D~NZb$14)O=d273o9FysV- zMHoOff-ZT+?>~?#kbjWvM`jBoCgzkdBqf8$tmNd}0>(N}Z<(u?N)MXPlH@ zl#<6dqc}0AgmGSRW?mZOlHy_mL&jCb#Rf)<8;Xk!j2X8T7aN!`?kO%dFl9VcTv|}f zcmgzVz<3Un!a(Z<85kHkKqtB}aD&hLkb<7y4LesFd5=EwzRzs%HivSCYKD4-W`=f# zZiaq_$qdsOW;4uZSj@1TVKu{ghRqDy8Fn-5XE@AooZ&RXd4|gj*BNdz+-G>q@SNc_ z!+VC$4Br`kGyG>{Vq|6HVB}`xV-#c*VH9VSVw7c6U{q#QW7K5SVbo_dVl-v6V6gpgTE+&(X2v$gPR1U_ ze#S|RQyFJ4&Ssp)xR7xP<8sDTjB6P;Fm7ht#<-Jl595BuLySimPcWWlJjZyE@e1R0 z##`X9xzG3jl=2y$fXPQ-60~`qk?|pz{T56V0|k0pU6gQb9_f~A3F63Z->MJ%gWHnALGIm2>=J#u`Xj>$GVO680$IKYpnNJ-?4sU{l~_}CdMYmrpBhn zX2<5n=EoMsmd2LHR>oGx*2gxDZ64b)wry>jAI?gK8|A?=Qyr$JmYxB@r~mjCm*L6ryQpm zrx~XmryHjqXB=l5XC7x6XB%f9=QPfFoa;EZaqibTmt`ncwCE#q3pwT? zs`2Xan(?~v`tgSG#_{Iymhsl{w((BmoyWV3cOCCO-ebJyc(3t3;eEsVg^!7ki%*D8 zflq_afX|N4jn9uShA)LLhp&pSiLZ-q2Hyg{6@1(H_VFF#yTW&e?+M>0zF+)I`~v(E z{0jVf{AT=i{6734{4xAl{6+j#{2lxg_-F7h<6pa6&LqwyE(E$Kj)7g+N;poqNqB|uIpHrNQs7#iQA7ty8$f9jC~X0yZNPfjL>xqt zL|Q~vh@27m0Fe=8g3@eInhQ$vL3D@;iJFLpiB^fu6Fmf#f$a5W6uklE-+|H(pgNw2 zv59Gj`G^&WO@YXWErZglp!7N@y$Pa2Y@65(v0vhH;!fge5E=0bC=F_vfR4{;fbv@) zI>bA~SBaky{~#eE0rjDT7es}GACwM)(qUj7tP)WYRT8r#_DI}=ibMS`@de8N0af=$ zQby87GENd|qvRxryyP?}Jqw~va-QTq$$L@^Qc4hUDQJjGLER$-32{~_2dM<92B}3- zke%d=QrDpBA-m5RrS3t@lX@i0B&{UvA`J}zX=n&YLqkBi0isU2MS6+!5$R_#91wG4 zpeaB`4$4=8sFP8Xag#}t>5zejxy&A@`~fI^1gh?Y%nMl#SuI%~h`4MVlum-upqtMa z7+7VqWV>Wn$)1sY2N9QpRzPwrP(BC5JUJdY9XTJlJh=%_@kLM?ng--nLDj93J16%} zUO?UmA}{X(r9Ggu4@8}OfP8`c1o=(!&{QW6IU#^i{uNZ+JE*!(3PK7-3Ly$*5OIYL zDBT04CqUFGOi|dPa7E#pq69=-Q431zL1`n1Iz=h>w$Q~sx-pyC7(R|$dAkdqG>Rbn9OR1#DgR2HZlQh5TEhn&d3 zsPYTS{|8a0%A~5K>ZF>Y+6ECLfrzWuLFpza-3C#o-le`u{gnC}4L*pt zh60pUfzlcfbs9PvJ{mb1eHt5};?Pn{;~12G3aaj$#yd?uO#@A6IiZ;Xk=M+C(m4=y zngyB@G&g8o(u9^1TF`P*3tCQULCQ&1Eg>x zdkJEmF0|azy#-ZwPxqgmoSuUow4Bf@fvD4imRou?5OsPDdJFUp=snVhmJ|A75Ow;{ za!X$hqE25)-$_47zeOKfPU!D|s)LqW`Ujxuj_5xzU@=fLfR+;mQ4n7-|@L7-ktl%L&7IP<7C9%WxS~-73RVhOdlx zjG*O&kpo1X5wzSg@_?u_@-fOW>M>ep1T80w9zoSX%PpfGoE0)!T5slClhEnVWI|6X96v^O!Oe?OpHu|Oo~jVm_W-3lM_&N&~nS< z0#w}zla>6tVqRtdrZkZNA)R~r0;0}5#=ORSj`==w zXgOj2391fSZkhjrs{3alXJKcNU;!;BETC--3uw7zF$bc~Vu8g0iwBlWme6v-(hOpr zCA8eKw1cR#bh1pcY_eQp2`wiqp{*NBXt`wxY2C0|KCxo4Qn7Ngf|e6jRS@&6pyifT z6U02LHmhY;$E;phL(2(k8HhS-1t_foQD?1T?O~l^-DM3eC#?5D<)P)4^)awI7VA^i zFKpOsps{ZQEj?@?tqc|$2b(yX8XIUF*g)gJ2GYJ_vDs#G#paW(5X59#Xh_&XN=z18 z9a}Hk3|nZrvz-N1H4kF1?IPO)w)bqIwU!+;_U$0C&tfNGXJ!{-2hIC-(7MB}3!>Mq z&u)#~DLZHmw1ehAdj^QCJ&V1Py@P!M#6EjyKCp+>mn`;G_A~6a*+WYm`xj7EZ=iZV zIPf{>I6&Jh4$!>s0Ll9-4rvZ84vQS1A?N^YIXK*a=ykZ`@XJxc5gLMy(DK*O4`IG%8P=EULz4M8VpT5__2=ykGligBuPf`*_IG)A2uB|nSP z7N<*2ADp2f=nQQ)I;%nSI%_$5IHx&7L(mzTik%^;n8kU4^FHT0&d?Baf!2X8kRAbx ziWS1V7RW2u7pdsi2&Cf27dY{FW$yLGC&J`Mht_2LBGo2V+ zOCWk(D_p0!ZgGW%pewWtbA^;)EUxd|c-*wypdsi6Z2`F@K=it$xHY*gaD#@R8?+vA zy9SlL<@Uo}%pDqn?$FZQ9n$J!aSw3MbMJA7hM+q%Pq>3`%4T43Kj!|#gUJILf*#Ns z%mY$`v3S^cM0r$rKts?2+Wz%e2eH>*wmOT^2_E5HjHf?m*a$qUkcVe#tnTH$rf3mSr6 zKOo`k^#`KYo55So+r}Fjg5J=w*c(z7vv`+zPx0R54GlqWXiLuf1yt`FA1)saA7}{r zKHy>|0I8C2>L@ycK;a=z5a9jclqD&hlXGPw0sSKw8K~egaQl#`~sjM z7y#|X1wi_ZECC$>%L0xBKtnJ9TBig+>J*lMe}OWAR)NqE49tOqMqmNN-oTQ;Nr4*z zp&=Lut=9veL1kYBaRjLaK|?SITDt~8TH!1~2|;y1bAq5D7<3L|a?mA+-k@tiUxI~# zp&=LyZBYb6S`;k7KEYYR9l_8L42Jecg7-o61|JH35d1F$8iFCvR$Yh*L^i}CBrK#P z1R8=N(6&_wqz}dtvM%II$g5Cj2!=vys89un-cXfLr_h8@Xb6Tv+moS?_9RQ_tk4~y z*FvEo7zXW)hVel3h6#k}h53X*Lof_lH-W@Ob#y!pAfz-92$b*&{je?q&LA5{vv`cLL~wkf)S9@-WVgIAbKO> zB5ERLML1hF~PLUlr*F(HrR%nGx9*2@SzWXzMg`4^;L* zKg0>N(Abm-eD6^=LsG=xn2u3Y|#D3HYh~B6*QKzC_L_3Q&=8D)wzFcsLG}KMm54Qqg@#}(w6zlp=@qfW7R2_&u8D<)U@UY@B^DZ^ zv7lRi7#JAij)3QA7~>&zD{K6z_;(3B304VE(-MwB)4S~4U-`$Wl1kPuCV`ac<(qLZPed@?BggTfcYXJi1K2MFqoKxlae0R~0T*)I%? z4Crb=eFatqm5(D+KaI0WuSrzj{zJ5=xP|jW-x(W0`e;-O1Odaf#$Q}KN(Ue_n80>{{0AE8M3@6|JuDr7TmsU=z#s`O8%)7r z01{zi0rig{wy?37BZ-5`Vu)`MW`cBpQVED=2HT4s%ODwut&lLlEO9|056TOZSthYe zgolAN7XL9pRlw2#sMf`B3)l=$7=X-WV35UP4k$)ot_6h?s13!&k`MMPNEaIms9g*T z17ol_$RBJhpq3g$9APi0t^$Pth-PAlVu%9Q1nBY*J3+G8<(XLeS)e0$3=H5Dh;9NS zSe}sqWDf(lEp^M8oOg-DFwS2Bo8ZnAZ$nu z1F_Lv3b6rHx+$Q=2}m_0yddQ;x;&^X1No$lr4=40pgBW?4Gi5FrR>A`Otg{ zk!NCQW`VW>85kHqZFUSB!19a?AoUCkhB)kixB%o6P@ZOEfw&l?2NpjNHfV$kViv+4 zP#p#G35aH55Cq+7fnp0t9Oe^{tRR|CAo5Hs4J`HWI05BH3>(1mkkwlZ44~YLt^$&} zKz4w90vc0cW61@FJ;)ANnFe8ldd3j5&`p7ef_x&6)ukX?m>C2a80VyR-OgvW^uieDinFoNYF>70Q9bb2_#42TMl zxeynCd;%KvW@7>MX(4*pSTfIZVW@2z+;6{rRkT}dIAX#*I zZg87|iKUzcRGxrb!@vNVvq0DYDl0*yFi4(}0i>RR!4bu;2s46ZnR z0_p*Pd;%J)fu$&r8`)SOc>}_R|A3n*qFde~S%;R9nsd(1mkp3P6Lnul1bV<3GP_JBuPK<1#A!XVWMpWrTqQ^73@P`ENMfXYq`8^H3A5|x1= z5|2+n$C%8-D7?x;woPb)e2opf=0F_-Jc}R)Mz>tb3 zPCz9U0|Th#%fKUa)^WM2KfY(dmuel1_n^e17QZj4p6E9naIEZs@>U`K{G56J+K@PG7aWK zkUn%%ApQgO4zQQ9AQyuAC%E%T7z?O80;ywQ0JZfHHh}Ug$R{9qh&>Dppt=R20^}k{ zsRD5UD0P8qbXW@oB+teIY8^vL3>X_Ej&2IX29P=EH6KVd#1@cGaHpN!I6 zzy0rj~MHh@OZK|X=h(IE8<47E5@7sw7!>H>}KvazUxOIwg0HWo-N2{Mh11vFj( zaWldmkiQVAD;}>;K(e@e;tsBZAwB`kH6m;Pg)zt{kUARVDh3A7y-^4i5I2DIf<}oT zDnO$GY%HMhCTKok2CaO6#Sg@XAbsejK>P_17QQGJqGd#NFL%U28LdoJ^|SQ z@(E~cmW>58Dh<)Y#(WnXCm_>cJ_NZPVGoFh>=V#k2go%vL>}ZqX7F_|=<*9mq7SK!xL=PJaXf^}JhU9pV+t5vcv{6B=9`v3TNHxqSpmr*Dc_tQ9@C*US zH4F@(^+p&rfaM|fFff2-x6oC9^g>(!@(E}zl8r?foT5N(U}FJ|ibM3nay&>J-4uul zkU8l20Hhk>6Wpy8BNosIDabVp44|1$3>(1mkTlQ00J@nET?I%l#04OqfL776v4Cb5 zA$r(YK=Tj~cfxW!$ZhDRKvY2HzdlRuGO=i~Kz9o;Fff4D zF(FhyOkf1dLtMhZuo9&dhNu9U3vmI+C!iH+upAGvgN+4bBE+3+ETA?Egbfl!#1^RR z0+|Lf2Yc!P*#hzj?tG#K&XFK>3=E*vi5ND3SY;_(Tn z?aaUcTJO!q0@{TP(Zj|9T9pHFJxm0+NTggn?lziVBd6AfW|n)q{8p44@VCY|MYbr7%c@jRj;j)K|=)mCz8g z5H1D96AUvkuwcvtLF7QDfL4lt=4n7|gnr130TYWXI7foy7#Kh+%n&LdCNP5KAu|OG z3_DSL0#O0d3vmG`pMX}n!E!dp4p_|yQp?5yS{DGGy+oSN1j)cKC`8ax7eo$Z3n)%- zr!FaQ9Sl;(zyMnHi0}z4P9SwOh{wRN7pG4^Hh_ErTE7Y#kpSsoV+O6lg~kaB=xhUY zpMX?>TnnN>BjxBm0f|Fm9yC@DvIpc^bayhbh_is!VSwZq7(i>85h`F~^&okOOBfgq zqWA=12gC&+pMX}SvoU`Kr!J5@EXRXPV`B!br-Yb=um_|H;S&Jc}Tm7f#E1lpMYEd@(F0C1RL{buuniHz!q>h0Bw9f@$14IQQSRRr~85mCD^a;oYkWWB+ z)YzEcfqeopfsGlo%L<|&#s;|!VGoFh@Cj&b6Ua5_@*pu#j>nz4_*g)FAdor+hBGMc z1ep$s6OcS4MKCa&MNt8914u6@$Afqb44`$fun`H62rT!5Ok-ny3-%#M9Nnc58$dZ8 zy=DZdhUF7b8I3Lv3NKKcaD!VAAlEQ3fYx(j*Z`J?gee2VMVvkX*#U|Z&}w%!Xx|-V z2OINixa--NL3>IdZpN?&ylMvI6MQ)y6eHN>nOHc$EeMEDKsz5WYyitcY-eD&ipM9Q zuw!5V?O|a9-)s%h1M?L~mW}y2+*cU(Kz$;MQOARH!r}xZi_0ghETDLY_yp9`L)ZXX z4+TnHAbE(Z7#MEi^a;ogP@I5zh_Dd}kQ-n=0hz|e3|dtWF$=>U@cJtz29P-**PzRT z#F)XdxO~C{ZdriTF))C3WTBhD2$qM~&cFcblcTEu>4mreTVLkzgurY%~Ao|&uLHZ$Vggv0~BT&49Xh`n>fSAAtmWTDUK=lE-3Xooi3qU>r)rqj24YGrc8M00sWEvYY zXygUrW^_{^q9Akd#R;BqVdl@w&>aa33=E*!7{dmzJS2P>7(U{R6ObJspMXqbV+Pes z5It2JK2~ggG*tMuQ2QZue1gE1ifqmsfMIsP@I5j6j0b9^n+p>6ek~;-^23> zXr~531;hkKusoz3VqgHZ4AE79%!RlBu)DObomXyl7cKdJR^8@Is?N`lsG}y0dWDy zC!jVw8#AaZhUkHnV<5F`%=f`@0%9ZV0gY~gd;+3DW0~lw3nUKn324j~T^^K!L2>em z8I+45J^}5j#IOM@4@oHu44}Rsx(bk9hzmeI0rd*mn2&>946=ib88j*m9Zh8hjiE!> z2vb0F^dO&rXwZ6Aka_5`ATiL&E9@(58NqD_kQz|(LLO5FnF$F6kjbDkqCov}HfGQ$ z5=4ZJ88lu5VIyn>^+7;x0@0w=Lm;!!WkF(~-VFA&vW(z43XmEGM$o7RHr*hTL2d%s z#KsJ2=Rri+m@k9F4`e&KK8OlXe1ObGmj#L89@Sw4&!K?SFfgL7=K|RT3nx&lF))J0 zj@Xz%y*Q9vj0|kdpq47cc63`IqM$qoG8PgQx(FIDt$+mj#J|*0^F{+04j% z9TY|&%m^B5!loN!GRRG!(I7Tv(3k^6gpC14IA@%aM*yt-lxCyi_2D_U;bt3~KXk`!^ zGiaU&mhV7g;gAqP=mW7p=?O%G+CCt&L2OW}1MxxQ!5}dv1`Y-ev=SAh4irwH_8LeH z10!f%6-X3aH^d|cM$qgiEDeFwu`%xl$0LXh!VrCI;5GM<73iS0707IKS&$gMHXW!8 z1gT+Q1g-DFrW<53DDFUeMc9}@>s=rsFgJl*fvykcCiJ=mq!t!;xbqz-KSSIET0Mtu zHb@4N%NQ6zI|bR8LG!K<5m89QJuo+cbi>*OAeHF)U~WP$3qWcirsAuEL1iGsO`x6%y4fHZ zNX}_OKDbCVsO^km0?E(_W%1d2OQy9{JA10(7#5s*y~A2Bk3 z)G{!FR{OCrgVr=cMA(=?Gqw=hak(iDkDKBc&}BiZx9R46R42-z;ihxW8xe2rm zn2i~<&IBUD#td4U1+g97R){Ew1euL43lf8@ivqFGWf{S3G>{AfBes1ZAQ_O!AUAQ#N927<%%!s-Z1fd&bGRRG!aXHv{0Z2C+ zbW9AS5|^9M%W;reNQ!{Wet<#)p%+vVgWLof*8<5fFec+nPaw4njG#RqY|M}|P(UJV z%%BywkPtzb3$Ya<3K~}hnT;+B5(ACTVqY1|$h;U7Mj*@x+9QHZH^^jAIDvLc zJ%|V!GiW6-#CCLj5K++R2grVOS&$gMaTUV)42+m?38_ zfY|8zATGr$GeK%$affRTjS)Pn1#uJV4iJQHkjbEM0`LD|Mmlc=I#v!+iOWssISZr~ z<|fd3d63Nry|8%|&|DQrhJg{(GD65BbVG6(10!g!2OBe}_W}`tl?x!1==xycgr15) zYGH1|oyI}ynIUchoic-NHb@4Nq8S)L`#RW|LA^kT2pcnK&j7@Bgg#KM14>UI8YBlY z0bLd(hA%yV@-|2f10!f32sYgylR{e?meTWHQK2pwftq8I(IAB5cf{oCsl~+X@i{)s!H!(Pcqm`08L# zdkCb4fe}X8RRBVOtCS8cAr5+U~G`<(e=UHgkIl))IxlUuf7A7F%UO_YBF@Q zK{Bv@3#df^3nxf@2P$PDwj=a`(h>}VdWIkq&}Bhl`1+inG6tlEff3a5z@{5yGANut zDq;05$W~as1F_NdLG*ya6J$2JEJzGr+=1rfKx!BmK_P}sH^^j=n?S8fHfG3~43PE{ zC^jLsBlLk}K&}MQpfCiPjV=ok!X8RRBVZeU{u?LmczurY&L z3J@2g>w}1bNRZj+vLG>h;RNz4NDTucs8qnF8)P!bO`!R6HfB&O3L*k?A1Fi+`arEc zP&k2TklR6KqsxNC@P$(|sO<#8jGz?<*mQ$T2Du4T;+7l2FHfB&M3TcHR^np@8 z$W0&`R9A!SN0$YO;cFLwN+pmQ21d|&4s5zXCWG7r8ohwI3DlB-g&D|pbbXM#3UWKh zY;;+W7`|`N<6klx2pxzZk7NHL`at2CIAR0900x}z279@uI^bdKWa7!p0050cAux-32r*1acFI2DNEGW+O}miNfj@ z-0de&8x5q7fe|zsk4-noWRROcy;C-3$Q~?E>kH;4kUa=}ps)kE2}FZ*g3Lyj1&QJ7 zOMzOKATT`m6k{~q< zjG)n0Y`Q@vgWLoft!D$>amWA>VPl5$=s+n4T_4O%323&0)WY&z0t31%sJsKY33Rd* z#7&@?Idro@GK}D}Hy9W}Bk63+pcNtz5jJMf$_gw2{en(#ta($g@~{*gGM+Zqb}(BAfllB2{Id979@tREC7v!fz&WC zg7$1+(+x5it~6Tp3XR12%pS3K4XDFgIaeEeNs?-}n=#jRsP~zzCWdMmHNI z12Lb05wbfY7(CwsatACPL2QIRka?iE1JR(;7GwgtEJzGrISxvpATLv*%};9;J9M~t!#wYj;;?P%E+LMRz4wo%EX|IAq(1>3@V>M=i)%z1X{g^ZZ=2; zWHQK2pcQ?vaTSnmHYU)TPl)a4`XHhp5@Z6pEJzHr-V%M(1>#~xX3*(RAT3mfR0n~~MwbPN;j8aJGa4W@42-C| zFFXa zGb=MYGY2y#GaEAlGZQm2GYc~#vp2IZvn#VZvj?*$vlp`uvmdiRa{zN7a}YBq_nR{~ zGq^BhG88bBGAv+N$gqfEF~bsur3}j$4lx{IIL2^~;Q_-Vh9`{fj3JDnjNyzCjM0oK zj0+flGX7>#WKw2QWm0F-WYT8RWzuIdWHM$lWikidT+FP)tjny&tj}z~Y{+cHY|L!J zY|3l~9t$jE;AK!?&|)xPFk`S`aANRa@M8#Jh+;@!NMp!jU}5^i^qE7y~zB6k`3h74H=ji)0y6YR4}~;t6*ecW{hJ@WK3oNt?C7>=7X&N zWncvD5MX2k)x<0e`V8(2{tV$b)Idg?ycn3l=@!(+1epUhfx!oKgEM0cV-^DgC{`Hw z!8b_@;!y32Q#B*_gkFT}m>8HDQy5dArb6$hW?*9AW>8_UWnf@vXXs#%VwlM=lR<{z zD8o?(S%#YoHyPv@-ZQ*skY^NT6lPFhG-fnrP-JvqbYM_obYgU3P-b*tbYW0oU}E56 zU}DH-0QD!C7+e^F7#JBrceV&IY+<;^D8p#P7{egIpv0iXV8md>;Kbm?5X2D0ki?M1 zP{dHh(8SQiFo|In(_RM94peCn&D6;t&9ocLmjuyFoeYvp+rfMZ5Y5!dAi=a1%ohXE zOq~p3Ok2Qw84%6X$sohD8O)ag(M+8TQVexqz9@)h>SPdQs0H)IK{Qh*gE%;S2r)=8 zC^78-%SeD|uua>*d@-hN49p;#7}yy27{nOl7}OZ_7|fXVfn}vZG}zrc!F&m(onX}x z4E12XIEV(@1M2#9U33BHky$^n1PAGib0C$J!Xl=^bddO#=yuR#o&TjZZW#!FQXVh zEf!X$53mxIfsuig!3$Q}f&9+|a~sIrpgmlj-YyBH@hP6Lf8GJ;o9GID`Ra7M^l!cSnfIEZ8j2a$}BHDUk3 zY*rA-@CQUfbxDKS3?P!>ABbdxtUZVTv%zf|MsSaUkqImUQ3){(A_6v(5nPLa?muT> zWcUJB32w_WLS|GUcdJW+#lc|+aXG_pkO;#M5DC@$7l{qJtDO;Q!b`9?*e-@|AU4By zFbVbx14I_QdjMiG#1{}VK~c`Y$OzF3@*}7{rov#%06M+fmO+5Qg~5YClEIt7he3|P zk0FRbfgzM3mO+)_6T>G4Jw_)+R|b8i_e}2@Ky!c0jMf znJfdZyQ*Q}Vc=pAV~}A`V$fjFV=!T`VsKz^WAI^MWCDi`<6V$H7;l2f958tnOlE*d zCop*uOhVX)L2L#t1{DSk1|0?quL4K%vDjSV_=WA_FKbFi?xMVgRkxcVOTFx8elAr4A!wG1C(U zMy7TU$#@A&9s~EV5&Z*321f9zBWS6?1d1zg>2d$ssYp2{+m^Fn;{Qz)%%9d}d6G7#O577#J8F(hAaZ*&^qdGcbrdFfcF{rzaK{Fi0>cGBAD& zVPIeoNYANEdu6b#nt?(24+G=$BN?fQDf=`#bQl;Ibr={J%rY`k6aOw`p3cC)2y&lF zMs7*PhrhdR7#Ns17#R5fMjO_V+@SD4=^zBgcRf#mxL58tzuw2A;Q3*X8HO8JHOu8JL+O7$-2W zGB7Z8F-0&iFnB{~#yAEWMpFg`CT>)nVI5Nhg8)+mgAr2% zgAh{$gCtV~BS`I8rU-^CrU(XYrU(W*rU-^JOc4yTnIafKdZL&j7%Z3~7)+TW7#x`* z7z~&q7?hYI7(AIG7#yHt=1?|BJxFgT)Ep4)3uVhQMKDA#MKHKAMKHuOMKFNO1!0ic z0Zb7LAxserUQ7`TAUzdK5ezO&5ey)4kUUHsh}MO;8!V^76v3d(6u}_H6u}?}b=yOz z*&sVWYHgSz7L?=Q0AIlWMkO{Tdj46TvBo5OD(gX4fNWD5! z1Vaqe&vHx=47Skl0l7VsDT0v`YK{pgZlU1_vKPb`gUSaoMKDZ*%7fAX$nT)A2Zehn zQv|~TrU-^cXxJ@eieQ+|6v6Nx>L!rCKw*8ADS}}ZQv^c~Qv?IZU7$FHVNh6s!WTuBkOcDQIF-0(d{I3WN1CW0~Zox-`(p3;Ne}K{`h!0AqAU$Q!H~{4tP#mDs8c=zV z94IW&X$Gh~NDky~Pk$aYrl- z%Il!~21;X~_yy6R@(2_j=(Gh>#D7r!fcXVx2P`ds@&G6vKxqhsVQz=H5fsiKKYeD3 zVA#hL!O)F_-!ero>|lyuC})abXkm(A*v!E2e*y!;|0@g({}UJ({zoz}{4a&F4>2(O z&t_owzlMR~e=h^W{|>152}n)yAEfs<1H=Cf3=IEQF);l1VTxb?xeXM?APjON$PSQO zKyC!36%h7fiunHzTIPY&fcyu-I!qA^!AubhAb*0w1(Z)g`eE_M#1z4RoF+heSkcN` zkbaQep!@*}50IIlG8TkE>Opw|6y6{VvKvH$;?WxF9#C9^(vd4u1Vc7c1VcMh1Oq59 zf&2i9KTvrI%3~l5k_VaV0WF_F`VygRkUv3g0L3-P4Iq6WF_0TTTvSy&(HQc@v}uWF{#5K;oeI1%(xi2I&XI1t{J?{sg%nM8ovJ+zHYP@+XW2 zsRNkr$ONc!sz0p@d0EvOhUl0bRLy&qR z(0UUTR*}$p7o-=|P6pM-ptkg8Xd4;S-bb#l!RABl0M*0j_JGU;*#o1o*!Pd0f#IJL z1H(Ui28O?v7#RMALim3_FfjbR%)s#XHv_}JDh7uCstgSOZZk0ayTicnZ!H7EzcWx; zhk@bWVFreOJ`4>1o-r`|t7l;Nm&w5JubF}2pBV$gKS2hDe;~br3=ID{7#RL}FfjbP z&%p5aAH<%2$_xzuN*NfybU2jOW?=Yd$iVP#83V&V2?mCLvJ4FWPD8~M85sT7sJ5t?=u6#zoiTee_t>#{F7o} z_-D+(@NWYH!@pDphJV)?82*9$Q^mmWj}Hg0&k3i%fP`{tyFBgLS zCm76zkPED;(2dhL9|HBCqK{o$)Gf3q3J`nk* z4NN`&ldnMJ@6TXzE0}x+BL9ej$ln`4V7zWI3Ose!4Hfq|)sshJ7tdZt#U*&sbka~K$y<}xj1TE@V@w44bv z(#XKHl4%vwYOw4Y1_q|JOzXhx^-SO#!N9<@k?AbcIi~YW7r^2dnJ$6p%S>08t}-w% zU1I{51q=*KH<)fRFfiR>y3GVGDHs@-?lOUM9s>i@L#9VeubJL3y=8jGz{UhF2N+Sq z8DuCUeAEv%`UeulhCw5*pj^koz{|V9sE{V98*`V9j8|V9Q{~V9(&d;K<;_;LPB{;L6~};LhN| z;K|^{;LYH};LG60;Li}i5Xcb35X=z55Xun75Y7<65Xlh55X}(75X%t95YLdnkjTKm z(8|!p(818fFo|J0!(4_%3=0_+Gc09T!mx~C1;Z+al?z$DGaF$8B8q<45 zVH%?_qX;N%fm2frQ!T>|rpF8i7%iDPnHDj%Gj)JslBt=Yf~k(Fk*STLnW3AZiJ^y~ zlcA5HpJ4(+FT)gusSFGZlNn|+%wd?t@QE>mF^ZvpF^n;sF@m9zF`6-wF?0t5gO2wO z2F`$pjf{+akvkawx9(tYk=@C_$-o>Rr@MoJQAa_+HPdASV}kAu1}2>y42)Wex;q${ zbrjq-Fsr%+MJPuqbSZRsrgSN|DP$z3Y+z9Z(?Jo+sa=u!3L6-LBQg~>FoZ-XZg7Z{ zhBH$mBdv6IFtC8kVF8=d6{+B+kOeg%D={S~Lb)qa|F7;2238$!g$>MV0TF=_8yZ}s zH#oRNN-HWVxNbNQ5V7Hai?m{7r0xy|Hn>X_6kKaFC7^C**V)0qrlq@sfdgVnaKr`& z=?x5#U0onEgCmp`H#Bs0Np~qn!uUHF4xkAzGN21MKm~R%FslVeY+zP(-ND!p5TUHN zgRwzbQ5qzmtf;IQDXkdkqPv5EQ^z|v!qr7uQ87}NVFv?)Hp4CkMh0#MCT+%D3_PGw za&5-_3_J`j4CV~l4Dt-Zpz(L@{S3Se4h*IYnhbIbLSPYw{S1r@F$~@e)(lzL7hR4L7agHs)n7xkU@<>nL(6604l=4;K$&=V8oyT*Tu=8#h}C>#URW8 zs-Qu(b1_V0C})UeaAE*8EkGjN4BiZO3QnAB3LI0HGH^>GM#!07~aGh=J87 zFi0>k7Vl!n2hkX4 z#=yY9^WTDjhtZRPjWL^njnSWhjj@n{jfsVUhiMlB57QHHfEqDCU>QRb!#BoxOhL>% z%v#J*%w5bIm~SwDVBugfV)0=)#mdK;!TN&Dgl!(X5W5%q9gYBw7>*2%HjZ^1XE=>G zPjQKHwQ$|yR^U$Jp2s7?lf$!&=M}F7ZxZh=J|?~dzBT*`{B`^v1e65w1Xc*L2_^}y z5PTuTC!`_dCsZWVCA3P|NO+m>ACUr)C!#iD0%AF0$Hb+?)5PaVs7W+QT$A*Y+$SX_ zHBIW7w3Bp|^fehHnL3$$vPQCAvIpb@%|mU8+6J{fYA4jLs69}7qaLFE zO#PGkKMgJoF%2aRJq;@jHw{0HD2+6YB8@tYE{$m#Cp0rOOEeoado*WgF46p+5TgX63&sV;HO3vrQ;ZiFuQA?Xe8l*I@g3tA#$QYrOd3r7m~xnkm@1g+m|B>+ zn65H2GP5%)GdpJPV;*6iVqRcgV}8Q?iunukFBS|IJQflbDi#J7Q!E)Qc`PL?RV)oG zZ7e-3w^@l;RamuHO|Y6{wZdwP)d6c6>vPt(te;tbvi@hoWg}*zWTR(eW#eY^#&((Q zCfj|sr);m;KC*phXJxm>k*?vHM}qVlQAXV?W11#zDiu#KFPA$05QY z#i78V#-YPuio*g&KF1`-M@|t=+niOL4V-P9J)A?F6P$CLE1X-LCpgb>zU1QKlHxMK z<%r8KS2Ncr*DbDZ+#=k%+@`qAb6erI$!(9@F}DkDx7?N7b==L|9o)U#L)^>Ur?@Y1 zU+2EVgUN%>L&~GaV}{2+Pc~0JPbp6&&lz4KUUgnwUeml5dHwKa@DA~g@$T^c;QhmA znlFp5pKp}!DL)B61wRcx1HTNv0>28s2EQ}@0sayGYXX=8JOb7QG6iM?ZVD0!nib3y zEEL=p{3S#oq$H#!lr6L!2eicE;y z6vY!&6?G=sAbMAfR*YGUQ_QiLOR;9LcCl`;L9tP>*Wv==BH~iw3gT+w_Qe~-Pm4bh z|0}^OVM(GyVo#D+(u3rPO0P-3mZ6o=lkq0wOQuz3U*@x{ zl&o{vUfG*+lyWBIe9QTl%a$vYE0-IUo0eOYTbJ9FJ1KWo?y}rXxw~=?<(|vEmHRCB zQ|`Y!t~{|kr98bnt30PXue`9lq`bVms=T(mNqO_~R^@HWJCvW6UzA^$-<3Zte^LIr z0x#RIrxhPEF*^}OnLH9j>PY97@l)t;(rs^_Yo z*WlOiqA{ZJM^jnTjpn45l2)Fmq^$%Y@*Xz++)H|p5SRY%TTVGY*hQ25L z68%yAb0+XisF~0;Va7y`iEb0?CZ3x3Xp+jL6_XxJdNbK%a?0c>ldnx-nxZx(XiD9b zRa34_XPfRbebNlB8AUUm%`BK@H|x->Q?stkdNk|ZtY5R)W(&=ho2@nb*z8NQ@6CQS z``aA0IYM*f=4j0^o8vYoXinUmtT|Huv1zTXUbyOPaTA z-mm#3^N%c0SkSUy+d`X#ix%-L@>{fEvDD(M#mAOtEs0t3XsO%MbxR*C^IO)p?A&s> z<+GMQSs}F|ZN;1wPgd%zoU!u9s)$t`tM;rGT5Y#FZgt)2b*mq&QCkzU=D=F5wFztI zti7>LY2BQ4AJ%)UpR@kb2DuF(8)j{IvoUPrfsF??-r0C(lh`J)O&yy$HeK3uY16Mw zzcw3eHrO1uIc{^y=9bMfHqY3+X7ifOuQtEh!m@>Bi_8|8EoNKHws>vv+S0bAZOei! z3%1(W9yD>9NRdy$!wF^=CI9STiUj?ZL_w`+ID8!nQbq& zz1YsRool<&cBSoJ+r75OZI9btv%O~fgzXcyuh_m~`dRT>`rVcKPh`+10hHYuAZgCw9y1 zmf4-MJ7xEY-6wYc+Wl)!-JZHV^Y+Z!vu)3|Jva8;*ekPFX0O9uhrMxoQRJAh_)i z2>v+qF}o`0$`|d1O$DKfZ(ws$BrC3Dg}li zM?tXUC> zI^_U{8K)qy?-T?cI|YWP8K7|1X$X9D8VVVhKx;mjESPy1m>Jj^tadT5fI6XqLY6xi zSOj)3FkApNwOu22Ffiy#?_^+L;IoU|!N7QD2Lpq^4hEJxI~W)Rb}+EtF)%b1R1{QI z6jW4TlK7{>xZqy`GtZwdHcV3(KnqwN{r}Eb#lYy0i{fmL2DzmAvC^Nf~nk}P=nX!?WETae;ql`?1 zjDV)1qNb*z;!8%h)2ILa`lKW!B&7r<7#RNlXIRI0o^dtP6CpJQt`!UdYz$nHpk6r> zsA#i2W;Yg924m$ZEfd6>`_(+FJsFKX)u5akPHj#%{<<;k0y7wx8I1n- zGifkuGpIA@F_noG~DoA3^EKN48~xUpk$;9^`H8k zT@2a`f(-0}LYBK2j2NUDR0V}B4GdZJ7}d??7{vwE^_Wdf%*;)VMdcX9+4UIJMaAWq zMMc=yMU|OF#KcYc7#U5})J5$W8O4M1RBTlOOvO#yw1jfw?OapU?4@0euvE zsx6&u92l)_-RxbsS)GlHbd6XUSy?z$jqQ~}6WP-sb%^=Aj%b>`h%V5p0a~FduXn+OmJyme} z*}(v!`7c0dStuPBX^j3#QzN^G)>B4Uij%*yP>%*w1dRjhOh4t4@z6G8@T}9!N ztfw}=nF9kOBOBv+#vrCA49pC2prpmZz$z$Y39AhlLE*^AAZHh8V8|$FEXc^_!+4%i z^xtbxzF=fH`@f9w9n%j6F$U3H3_PIn6rz_^e+Pq*zJZ}IC?Jgam}MD7j^_b5p zTKZ@O>iAkKDOvmK1Zw$MDl(dTwwJ^zD2At(JA^rur-v)b$Cb20%+LB?#@G!rA5>^d zK+G4^-@(A9Z(ztM!pFp}Y{zWNXrjlYZY;;l*bTNo+Ye-cpEkmRIC&%s6k@>^FfwQ} zcrfWQSupT12!e)L_!$Hs=7Y>OS2tHT7dJH)H5X?WXIEcuS#w@K=oyy$WfG}e6Pe=%bdLk9yhC~TP+5b>hF zgMnG!z)%%bGcYz5J9mI$gOOo2BOBvh#wZ46262%6Q1!dPV`mHuEZPQ!!lsI%vl*2b z*+N1X7#YBIHA5oz;-B-2fgvkuC9DIjfH}dHE|_3oWQh7- z#`urv2gJ?H2sbn8gBlhFhT@FIj8Tj+9{-kfF*p6G1J6WR{QttFz;uj3m_d`loFN|M z77J)D1yw1aibD%ta&m#vBNx`3zmq|P0aQ0~^>I zkn9P{m!cwkOzK8u2hzyv3q#4j^UQKX& zBzhME6KFaBDGo4NA;^)!2=*(7z)l7huwU62SQr?=1r?|!U}XT+37{q$=LG{pV^d>6 zWp;IQV?krl&=c$%40kr~@MJPxV!HNkzbAO5nIi)OlO)qI26=|yT@1{i=>{Zs2!q00 z7%9x5g#w>l|&5*-~zkIz)+LX)ZC5{TFtPl+cCoe zpPx~YN%EiGOJgS|M+a9yZ!TpsUt^O%b6IgsOXb6i)nS+P63uw*T^-G2DA%xnrjfwT2DRt;;5mjB0(igIW@x{KIx(2e`4% zd0__wtH2Hh-U|kXil&ODjG*MBEUKy~sLWXPZv~@XRYu0Y#f-cxonDO3|B1MEo?~1M ziWkrSUzkLhb}?u$gzsXI22E5U#fv(~HR?#NfkqU#yP+%0p`c(FVWHz{sv%&{ zC#z||g9=N}eO>I4T4i@t%OF{7fWvZ+b~6e5m>VFHDL| z#~5T8v>7ZJ8bD<~Bp>f!kb`y>bl@qOACz|ZkkCPo+LbUBtfAgi4;1} z=mO^~Nr9aVs^Hj>V~_+l@OCiB>4TbVu(Zdczk@+eU?&4VB!@xURiK6`D1X6Pvg|0{ zW|GkNiiD**sX#|PUvqgyyKqxbihKi2e2lx$Ql7sNDCMcTrmP01J1_z6vxD0sOuRyB z41x>{0_-4?vFHDP25>tFEY6P*XJBLqWe{LuW(s0p2epq_p`#fLpsp?x12ed_qbkZO z$|@SlxbELv#$-m$fSSi=qyyybnuqwzBcy^X5`9_ck!_HIq{^b5&M>HuA2| zWpQ@lv#@Y7mQl3Uk&-ra)7E!4l4nbO0B-n!2~e(Qu=_uOiI?dZgCT=GLmOzA#eu;Q z>=$l<9SpW0zc_-*KG2X4bOZ(52X+(yjWvP%&J9%$DhkCf>|`(k_qb#k#GwN!vfyeO z)Xf!UFaoy{L6wCigAvr0oeb*GrVps<26c8>k<+iJpt7klAEPp~dn?BzZfpwc{K_#| zgZg67W(ue~#(1pSC*59J-X_x8Ce%#Qos~;Z$JWEcR!mVsfR)ufAktSr+uNqvUqxG6 zMN3<|z&L$YM#D7%7?|XlKoKI%pvsU78pu(DR>(4-xVo@|0YnSIT?n-w;X-I~1-B7FwKk~! zV_=X5J4;o6Cxa9?V%Zr~8Kl5spi)}}T=obX8;OC(dLZSlvZ6Vp(9vcTg_gS7jG_^5 zqta{*tbJsRT{ZrF{bc3jWHoPssf~^41g2d{LG~W}%zVC{T3%-Io+f6Rrlx-xo%9XV z4Gf+!FfzFP|H352bc{g>G(yP$T2zBn$6<`jKs^R7McD*)G6*t&+DIVRN`PGp8z}+z z@|mIafw7Stlest_Goy1uZEb^rn~@T)2d9Lhu0n8NV6cL&q6DW0uac1)&#v|BcS-4( z%P5L#$_v$H7G>56$!m%$%9`m&LCXGk1_mYtaD6Pq;0;P9!VDtd-~x@_A;v4Ajzok$ zwCezNHOQ?HSA%*BLSR=z!&sXU9=3wId2@MU&X4K@%2|*bgqT3q`+^8AaH)VcRzbQOz=PTln+2m7cl?{f z==N_H*jCV@T#&t>J}MW}F$PVBvRw=cpruzxX$E6(7@CN{eh24HO;C2#gk)D9ft?KE z;Hp@aK^!`a2Wk+45|;v4oPj}=K>-}gpvIsYIA~Or`IwPPEp|Ot|qO zU@UZGW&kr{9ht#vc0g@3P?O)lkWo=kSr9bgU@WN2xaHr6kPt?JkPt@Oe}|cN{X4^G z^4E=lks*YE0lc@B8d=!N39U`9S;3ri!A(()D zShvoZ$;8?D?-^$XMh36{Ul<=S?P3sRum`Pz5n~WXN)Mu-5D-Obi6PPhqwEhwrgE#Tz^dXu(AhCyZ%in`SI`8zZ^(9s$gJX5(m!(nS#a? z*uca6MC8?-49wspZw!ig(TWg8Z>C+aaS9g(1||_EP|kKJ0I7PJ@9bdU1Xsb3Q9Evs?@g7t8I?srDN@*27?dK9b4;Gh;hR&< zR-NI?7X~exS}nxECXS5P1nQbHs3A2Lq^S z#eTuSP*@q_G(lrQ<;Cni2VtiLYf}hjFODt zV%0=Vn=$HNVp>|_suiZz)}||%c4fuKXZ^duSZHc!VrU9>m&N}W@c5-9gFa{^N(!_X z7wj%6fgKF|@NmWGy+Vs7aAK6Wu!BKDA5kLhU=Rn7zJS_EAUA?~x1j!;fg!6hXm}Dd zKL!hRHg-{CEaCpT)Y{zC*GNJJI&_(oHGMxc$YDXh#K}aJ-8{P94>oK$cQ;@7T+Crh zP=00t*Z->EvPPVt02KZb43c1fg2Ep$-h?r?3yl!4KQSr~z6b8!BxM3GoW>!YDTGoa9OH7Lh*%Y@b9}z>LX&nD9F>1Mkj-uRQ22`p>|~H-0F4;(F~~ww5FdDK zY!`zvgBZAI+rgkLu!BM60w`C2igQpBGBr`tX9PtSX!0654J0ZeCeH{OIAmkgimOn! z)r_|H39$0;Gj`Pys>=>3R<@Q6a&-5x^zsHxV%GDxni&}zx!D_A^6+x17~3i)Bv}|6 zs~OnX=v(vha;g~FDJABC{3yx5z+?~ZOW1)R+qY_A?cFf~p%(dG`Mc69>~S21y3DT@1pYCF)4!FUAN2M#Bg? zk__$?Kx%klK@m_B#*WEU&{z;$6Y4RU3NB%jGB-3dmtqUy*AI7h57*~s+VwBiIKWa) z&N9H5aq+)GzY;%nb-xln#(wblwF(0RlL@$8=mP4^6KEH*gCd+AYlQD$U;z!7GaE}Y zf)MlP^O4Ektlr6C=b3i>Tm0__BL`zFs3>J%WKd>cU@`^wk;*|8k`RM1QdmF}JtDlJ zB{xC{jLe0NnU$H9 znWN0TW%)T`;$k@XWj)O+Lpeg3cKs{3%((Vn@MXpcf87`t{)-2t6)9+15o9O_g%_&Z zG3rr_q6->;;KT#Y(opY$+jyXXXwXaqsEFVM=L|^47SbgJb!-JeBg)G9jLQ6s%EHXX z@{HgX1(SVza12X)M9h=x@qytiVIeU$x@XMjW=vrWYwzuC|F@cf5i};m!Yoa*Bd*HwiK_eN0FRWeHD-`^YH7xMJJ< z`P=?Q{CfuqLjwi|CJk_!1J9YDra6rKh6tgZpw%H?5baJ-w~mE@13ZlknkxjYiU7}< zFe-w^O%<6m{&6sN|7&Bs_OFC-t~ZmKr}y7m42%p9|CcfTWctCt&Y%hM4LrX?=Gs7G z_@H(zXdVdE0|dyD{U%cGQve)iX z)FnoF#;|{@@Bwl?G*PX$C3ydMAtwgh-j7l{q^Zl)%FyybSEn zAroHxoeY{_F%||T22DXB%bFbwEcy~V7^DSuG6;dkyg<<`4_@G*sH6s3SpexUS~Eh1 zxF9oEVxoe^qN?C#8g#IN@x8tVE32TlePBXnX`X|%iIa#Kmy(f#dUUqOzuWT0YNB}_ zPL>8Ly2c@%9)8A_mYT{MB5JaNF+u;HxS8`yDvN|_sEKN5g4&ny|GzNFFg;-qU{GRc z1Pvc4GpK;u2%xb{Zg^V=ni=Ha;Q@_eL;yjHOGF6+ErY-Up!!Mz>}787hzzLFz|0`Q zAPCMwybOX2pk)FEhQh{z#-NH@Rk^YwL; zHc$~2RWV?C^6yMSUqeG*LWp&!TZCIEpR~HvE-7_s(6&fWpY#70CKe`;FP#y-gvBHHo1EV*B+8Mryi3mLRP}ij#mAXoN_+`TB-HVuaD;uZ%T3KESa9ZihRA=N$zYWEr#= zvO#ln+6+42v z1WJ35UJxwpK_d+ji_lIcA{IqJWswM|nE_dv2x?Zq3TsXQ&?GP@2taelywGKdAa8?K zVSuV_b7Mi!niUf>YerFJ(83i#V^PK6bvt<dzHT{``)AUM`F^E?Fi{E^}P|wYY%h z_?-XiFc~s=F<3IVF@!K21C_I(3}N7y0WErTf!g8@k0EF~65(X%VmO2VG$jgv$`3_> zoeT=#$kJwDhW1FbL4!*I4B8BgMUJct2@Ki{1q|8@OtHHdY#ErLJziUVM^=XU47LpG z8EhGtK!Z>h7;G6pV^8MbCO;@aJA*T-w!lsXcQ6Yyjphf5bWo83T15w)D}+#>E}6I~ zG;pB9*Gg*Ye9WREV%CgCV(g#}B^!ie#ta&`gH1k&F&Q>B)#mDGh)I}g7?{eKdg)r} z$SB2^$jHf>c&&X@QU+$JIkwz8Y&rTGX`7e$?BP#>&fa_ zfa<~|1_s7IOivgjK)FYdK@vPL4@*}VB{sBeg@_Zx0$6B!7TWp;m)M|@XHcmJ>X--&H0~%ywY6f(dZHDwBbM$&Tp>WPFhYv{f0Yj{~jN5M>QUEe zz)1kyl!K1Qf%^$e46N{O{tgDv2pg!rwr4bE7glCw7gf#-V2N6jbnSie$w*M_>xRxd z_3s!XPavo@2N_p;!oUeyr@#nWaD?P8Xs99Fg>VKamLbI(EYv^+8yjf7H9Q^zwrpW! z+49fBg(=46ZvrU77#YmKX-t4Y1++9~wa5@Bmdppaw0dq7VXi(ICEs`H2}c zNGGT)2rDcw$_gfy5Nss{sH_0B*Tt9w7=#%VA+vu<49eh|2xI&cn`1%apP)l-oZBqsYSlg5bd)pM$S7iqGlT7rOU|l6bZPSpyCXCE*FM>S`E=PSC7?|{#Sg^O* zpyd_9chFKEA%L&V21<;);Gs!223}ZpVFUFHK}8#)%?2tBK#exgLVsgXMc?VupR)=? zx_TtKhMxEr2x_%8*}4Vq$}0c8dg@G5s${)TQ=KsXPY48egf zC9so08QdHb0F97=GZ(0*!wwc>VGv*djcymr@W4l(sifmbdWHbxT(@;gnW4QL=U9mr@iAl(92W zF6S}V(^uEh))SEy;AG}u;gdJfQnyqW)=@X)78mBx(m)dT4FSiOIs*d}9}{SZGX>Pf z7GV%YD(|6L2r({THOc=dfM5WrdLE6-e3^D(|Fn(iVVUS?R1kFF9w$(6_ z4|GBj;XbH?!4U?|EfS#WM*>p)fE>&Zjx7cTe((Z7$l7h#6f|g{Rt&Vl0W@T*%(x7_ zr4|4!eHp#~9R{`2RHYcx{{8{Adl;c>*;p7j8E}u&BAkiQ#zanNtdIdv=-}bcoq=8f zECF8rJDFH+ynJ~BY#}4)NCze>CeVmv6=;%F0Ne?MrDLdR2>UQHB8CHyMNBvbz|KUp#dk5Vf=;ag_saPg zI2d@qEJg-a20n0J1+5te?M*NiR)-GEvI|3oYHbhfPVlp1vGI-Bdnh5?n#D4Nk?mh@ zSXl1ge?KdOf+|6M2-E)-Ov+$)M}X2L>bNr05eRoforw^DdPD%!yyXNZL4MHoCa}Xm zp$lpxG8!}TF>5oL8k>VhY0Vig2mEK?6qD1E^{KGB5ESmq;>*PHw_8k6l-s3`@z;$b zN5E?Z8JL)vKpv-B=&ddE89zJC`fXP$O@<_+bOFF$ck`F%44c zU54EZYz!O>yx{#opm`mJJD`P4Y~VCdyMuufQjx><_K7j@f~T2kb})#6TKTF&FxhYcFekhhSMGL@87;kD&+vq0v&73SrYP?wW5u!l21ZR1D(y%@`wc)@#j zjX=p8QtLtLEhhNr45BRt8rTPI)nx`RG-WjvHWn6TM(JrXGRP>%z;+64JI%=U@7HN2 zOYm}0Fad4@hWzhiVqq!+?OJ3ghBOBtW5%FyJ4U#@7@aC;Hv?SAv4gf>-U0QLKvOs% z8ng!pltw^%Ye6&opz%D&4qH$OCkb8YBX!5XkQK7p8)>(xpdBM<+?8DvwAoaUk5QCS zJT5gY*3L0V#DUjDUE5mFQZ&qEwS}3PMNuYiUqL}1Z)Q5Hqk)pLHtW9vRy{R!11HvW zMi1*?ZpJR|U~5R-XTre1q{eiNK?gKSAqhIT3#m+pj!q!j6wtB}>^smHgd})S3e=L) z0yp+J7$h0Azy%{{B^{`++`$0aXbPHNQvy|urr?4>UD?!F54sT%)Qke96;Tl}abrRM=v1K~etBLyLV z=7c20W(GEJVh8ngK~q_viFnZTCunSv2Rdp9+G`7H=Ypm~&5c3(>6pP2qRgN@ zb;hEK-aB{p9*W(;vLpIX@6Mh7>|A%bGM4@80%6x(uAuaXRA#X=AkAcf#zjFB;tV?& zK(k;h`k>`9tl-t$ppB5skdn;Jjd3Dl`oEcfpMpq`c?^O7Q<=EI`;xpFs&+95feu*% zN0gAj4hA<+dIn86f~#fdre1I$fI4H4t-PRO4Kmjw#GnaYtiOvvpFs&)RXf1Nq!{!W z9KhALCtTKy!4p~#s)A?JQ5Pa0N0oZ`5bR}Trml)zRNKnVUQke81xhiiSZRq#$ytbq>uaj%@(GCt8XI_t zvP!sU;F7qbZ6YYh!Y*pA3}b-u4->;QrYy!oOjj6q8KfC>K}&gL7-YfW3R(aqcmcff zpMilHbWWB4Xb=rl3hZD2?WGq+DU^&+c<1G%q~ztLq-H~DrYtEDJ`pK20yK`zz#zmF z!L$OrPtgUmCYk|sm>$>#DEkz-z;!xktbq9oY^@ZCB_gncLF>*A26=%U3xnVWfMwUBDR@v4GJ^!3N`X}cI`+AN z`uc&n_AuHw++Ib+KHL~eTX+Trc^KFVnsO`2sB3F!%Bk=f2|5btg;n|bREO#5g;o3b zR)y)At2sv*+ZabVtHBvbiGe{0Ix4afGRB&gTE=pc^2&Okx}=LSmhn8(F$M(&ZH8H( zmbVUrF4$|JMRtmyJfHvyCD1+qXm3)+F4B>iVF7~z10!h4^#Y6`0AnbC7==3+6u{d% zK>@}LIv5e0{?(zZnjH-4`Vyc)T#!63gA{{2#8&+s44{swAUMc*1$HthGBAU-^`Xy` zLTXab2$L+Mv8XzzuPg#y3C6fqQHF_y+g#tw(%-|=S6QE1i(OJlN5;WZZG)DMt%eO_ zs*I$#nwhSdk&K_LxDv0V2#=HPDvPNS;#`Kxa+?4DGk7srFo`if5mIAnU%(*1&eYD( z3T_wJG59fYF$OcRLiX)K*GcXMpCZ8o-glyE3L3~|Jo>Ma@r>U=mw&&(`(>c}eTCRS zr&=(uFqASzF+PUwqcvl&V|WWHuz4<84fULGhATMW?%uu5OjKl9YvNIG>>8cns@;z`2b81F2^!+{GZmpv_s4h9|loeZ`NATc`zTL#D? zMLYeS3}#?4BL+JLGq54r0y`K?z@zez-Ez2BB7&+y(9QzzwtI6UTSgOYMo~r`HD@Ck z86!}dHk6e$bXJo!1kC`0*kGDbBUDCSRZmx4Rz*rmg)vITz*$}0*+2$HYYHnF$ce}q zC<((EhCAh@)Omf}WF^=o%t2)rBf|owTE-nr2N{?_{a&Pf(afMAC;%-l00rE65Car& z1~A3}5CbG70A_%Ki4}B!gQBS70!E2{FPIL3`WH+Ljf@41*THcw4;j}~U{C}H4`f^u zme&w*?a0iK07`wakUoV7IAef@lBA#+13ffBoeM})no&o^PG3q&-%bTagC>#m%*`1K zr1U`Lik=jVcCgdcwF6;LnPA7jz$C?##lX$L4{;-;H@<^`=K=#FweM!&1@%f9ShPXO zmkByUzyR9w4myAa$L@L1t}N&d`hRa2@60&va{Rc<@fj|lv8;uRo{Za=4lqcAW?F?_M+I+C zj4(`RbY@(~bbvvNArEvW0b~{%){cS>lq1|L4H~(TW)K3cuGe7T19vKRFlc}#Hys!> z7y=kH7?_Gc=>}BU@PRizgB&l)z`&pZP9f3)pw%Ye(hxNK1R7OmV^;_FCg8hpKvVXh zlUcw;p@}x5DWjB{sQoKN0i4m zC7WPF!(bbwKO33(|9k|6;Uk9EjAe|apt(~}c7yI8*v-Js06M1}v|dUTrM1Ke>&(3d z_us$-$egSH4H=g+h%iVqn1E_~NXv|22ZMnA4hE4sj_eHl3?iVSok5>Lgn>C0G7idq z2a+yP2Y%J`8O@E^#Ubl<_?Q?iH1)aFIOW8IC8e}vgpCB4g+*l5h2%BPOY>=R3-Sx_ ziL$XI1~RiLDTpeFa4|44%wqIp+y~wl4?a_Y8{BS&r3+{;5|MwPu@27fjNnobv|^1J z+#+KEos$mEB%rxfRFMg~=`qG8goY#}goGxTnrmBYn}Z1ka67`8 z!GejO@rjT+Q^)@r1_l8(rjA6=8CpUN3{0L(yM)x4#s6nBFbJqIb%OTaGOI951M36H zyJ3-6LX!7nIv}LZEQzU~fr%mNza0}d6E6d3->VHMd>|#=4hG1cS6H2;0tz1$(AqiB z5^T_T0}liH7g)6gI-CKvdYnMWubdq4mT z-lKsmZigZcN`nyf4k+TFv<4Bkfr^KK_SAvR*MXQ1N^AfBL-yJsiNn%3L_NrSh>+yM~> zxdWme<_?HB$b5)6s`()ILBzrCV_;+e-3Y|M#LB?U03P;1U1tu>NrNaMq{;M*L6Sj@!I)t_s04(}v+iI}g!aNCUB+Jjh%K$o61vDrHW63h8 zgWK4kfmcQFsPirc0|pq&5Go6*qSe{e!HFI;bOiDrXvBz*2{c413>t#wV-gjHOqZ*v zGs`hD>O`CC+t^4+g7zOpS-_aL**Ja(n<|4gBFzR7U@nhAfJ&NPLm+4y5|kO%7Gub0 zpkyt{xKK<2bbivm<=}luO4gG9;>9FDku>Z$bCQ^Du}pBtsV7fR-DBhI&B5POz2+v;~SS#z6fE z*uD;Ceb7)A19TGTsp3bZk871OSX z*)cKMv!9q4m>8IV+RO}T|1Fqon4T~wG4z5Cu7Rw?hPe(p=Y?1!1)X|BBx-2S2+16nbDVL;9yV!R|WF=pu!Zi7Mq(v9=d84)LRk*&!BOD2VFojSH^umpr>W4_S-P%q^tG-~x&-2Imh9;Iaj@=0^=lycAiS=`Tz@V+e}) z4Y+s|iue_nI723~II|i|oG}6<&cMvT1wJ!UfWef(nZbwQ>@Efg&}AFoppp>S!C(#A z@+Yu^!2_Php{qO)Q2_1RAq0>!J-9od52_CJA@lYk0y`PF85p5WBQplZFQ7>rZs<4( zn?9(!0^{j}PYD631$F;iz-r|gR2W>q&Xd>Q#bC@}2Hso`s?I?pF(9?_0y`Nz85qBS z4mkw1JkfR*Ko^>U*Fb|82!M`209BjdLk>X$mat_spm8`>KPp{;_>nF z^EY)i)lpUyQIiwWP}5c}FEx{NkK&h95R?|zRS~l>vo^OAlvNcC(a_`9)MH%Y>Y=3M z0UGuPui-aj0OOL>|oHj13HEW($|L0#-J|sC(z!oV`8+$wg(`_n+t1ugD)mk!6uAJ z0Apu>u`?6awgz8cK4@7QINvyf*3*OYjRzz@fhw2(|G{g6M3KZxk;TFJ39Ozm1VtQ_ zZ@}UVnNV@?x?Qk2pCIOgE~Wnep8;LG6j>Z>K3F|t2#Pq!e6Tn}CPQv=UQ zi!tbe7lMLz#)CI>B95976F1jmgw&Yo#&V3}=BTT^8Bd4HTkA^KWLreoDkxa`>DvZK zut+*8m0y9NaAk}FT@nQ?AH~4o9Rw0*aE7+&K; z3QslAM1m+E5X;{|YeYC1KnFsAGCQa~iD$_z4K z6F`IUFqRHf*1!;ywjd{Ri6I(dpn{fN7*w)~hzqMhlAAiSn7*ro6lh*o$3xa$7m0nr zIf(zYssos03sH_X1Lb=uabG168?+=mT?kvj6`X!0XSz zX(0;^{XK@kU~8L&7*CR7}}jtFdi4A^{dnq**NfUJupxW5cq zdL!~QMw1A^$8#4Jb zfsT7pV5kPQ8Wo|9J6XuNXwXhKG<*?lTa4xF$O{U=;fn}x(D|UCA!vC92Jn#W4hB%m zQ3|@gVF!a8Xmrh37`$o5*a)-`1F~VvTwNJ-com~Ev+>vbYwEhkwuIg`$U&askEkrrVW6_C;tl+e&nv6W^%ejI$X6}Vn7WU^+0w96LmVlV?; ziU!Wv$eo|CN)sLWww)n zlnWuC{gUAP>;@8NDCLESL)3%%WDxZZ$m+r61z0^}2#Wd^AvFewdTSK*w}jLfAnLQw?Zm zE9e*&5UmDJJ0j1%?#+cFQNv4W-4$A1kN(hK0jChH0TRjxDKiqA*+T# z%fuj?DnYYE5EiUq4%=I4s?4Si>60Utup6^0zl}~!jZV{N2?$`(_mR|778O<2lXP-& zVvI>QwB-SBdj)N>3`uZ@FJ%w*i0}wzTDg~ji2*ux!@$EJ#gGr`yh3JPAP3R#!b1Ss z!$g!3(B+@t;YHAN4`}5nXdE2269?3%6a|+8plyL-&>;{|n;LXv7N~qMFcelcMI6!s zKLrFdxG0zw5U>Yyg4v!OpcBkedV3iI92m6_$Cy2I`1cb$E^qVy3zHRipPLrLCeTq9 zuyJQe@UjtD4;gxZ9U=ztRQb^SkJy9&YN>F1ab#y;VBlmBVBlm>09_Io3#sfO3q2Va zB*A;5LB}MhfcGszng^gw%%JI|ozM+vpix}m3!tq&I~c^kqpG0UIPh8zaqxmC$N}Pl zcsJ6{oEhNgh;1|N20J%5yT1!C_S3@05+~;$|(wnD~fPJt8W8QRZ|7fCg}4T8ZM^HEUaqo$#%9WZt5u8pataR zWq5^U#YMOwwL0TZ@P26U&S*$o0!e?MvKx|~9Ffx#xU7eyr%+IO`v0E+G$zc&461vy zz-7Gy8^j#Qe&xSHYD^zN;_$XAL_H{dL)1GVs|TfTsQNHy8y2D-l)fSAt&!D()3+Dc z{BXE>Rwgc{V?t^SAoc8wIgC2sIu-05W)2}W28j9~sCvklImrFmAaj_Zc779+0O+_WSWe;qRaAKIzCjdo z&~gu&B*7)SIOw(maK$JNT7#g^AkJXVAkM%9n#;e>AP(MO4LZ9Xa%2ePEI`DKI9$-X zWB8!kCqS!rLCZZs1t0h@K+xuMV^Kv|nI{N3Aq8|Wpqm@$Xh1i&kbfV*#{;rCl$4Z| zIQ;#^*wz3(!_41x^Iv7iHQ*uZ;Db}%58=Q3V^94^Ol0djl*5F?L2LIXVW+oQbyOhA9D5if_QS=@C9>(tle`gYpFfSUqDHNIi7iO%hy= zXJMA(7_A1pCTQvcm)YQA9-1J*DnMiB zkQLsbnI6a@VNj(2S^NxYYk-Pwkeobt|F;@=lmv1zt+_hn)E&?kbtIpo4wx|B0(YVQ zb%QsqBZa~i=mzogkWLh6m*77K7o@OYb_H)12NUqL11j?%X#u?Soq-WDrU%}C;|>}g zAz}Xwdh~*#7InXN7TD4MJfMyPxA8*%e_{LwE$jS1?J8JV2Wm};!P}_N5qCtZ0a}tF z1fazLcnuKrXmrRiWuVDm(1j1Cf{LJ{Apa>?1{eh8352i+%IYZEx%}G@62i3WAFIB% ziENO!sEDepkee6dn}6Ud(ODRR{#!7yF}-162DhR$7$$?-(V7g91^uA)%%FW_AR2Tq zAc&TOyBX>tL|TF-9fSb1p^ZpOh>86j44}3&3%EhRAOJdW4|HCD2m@%04YV{DbaxkM zoib<#0qBTJ@DVMb69iDxiz3RRpv-`PfM)^b)<-m;sfAG(%3xyo`=^De1N~?LaB2Y) zpz%cTS*qZASQk={LHlB{|1Fp_L482R0FXFiNDKqGJ`DZO3fgBNq{g7fz#sssC!N6J zng3ZqXB7ykG5A5nox$^9pmNBGi3M_o0SDR{2GH^b5qQwn6hZ)_wu5G7aBT;=bp=v~ zf%fh&f|}=uGg8@wl^LZI0|Hp$S0!D0pZqssDHF@T-?1w${W}NpcNyq>RB&HeALL$U zyZ_~2e?aPWP&w<*z#z!Z02=$+#=y-0DqbM_47RoN1Sk8Ut8AV-BM`ME`$Q zCJrXh-FN;VbK&g>$a$7A93Nv&MDriO&l5#=oR6*q-XqKA=JU$PqaA7RaXf%V3_Y9egfST2nq=n z@EjZ{Oqs#U*g)NX&?K{gp}Das$VbYiieNt(iz)_enK^R{Bg@R0ptCFP7&#j;+PeIk zX5?)2*##V*pm>9<4KW1eCuZ<`Hf+8CJT?QKFJLHz&KCrL(-A2CLFz&I4ALG=`p?Sv zhe<$4jRB+{B(9HSKB)Z-QqRWh2ntTP`Jk~Dka{-8P>}hcJ*c2|D!3mm!_Wd+?I;VK zGzN{NNWsfHjN%O%1&ACf32L!QA`PuTyL#Zd6C7)ZEDLIZ!NyORpa&cvj-tWR;8JEb zX2)_IO<1gnDb{l)n4W-6aSixy0X`>Fh@l2l`#?@@hqbVwNgt6Hprg|W0jQr59>BHl z7+l7K>NHTB5_A|EC|Q9LK4^zI_`nQd&>>r(65LeLRGC>>FaUHeu(|b5MtSXb>`c2r zM+5))V&de|0y-)fTwa3K)&5}uougFI{ct{u+iW|d^^-u;adw{loLGIu53wja|{AeI^>l-bM2SHoR zK}P{0Vk6na2~@s<`dL7Oj_ zFYI6d(TG+R^o}P)lF$QXP(7rfbi`yCw0c0?WdbTiL2C^_cR{g(S2O5=2VX#9pi`O5 z!D|(a8H5yN2OSX%TI(RqzyMv{q{YAv-Wq0L2puAX`dJln5TY!jFlgTo8}e)| zQTtzzYQ9?x*8g~x{NjYIy(BXT2A838o?S?>cYb6RvJ2vLd;?=THfA2 z4E2O$METgP*d1lY7%=Wj8_OVe6VhWmifK)4tVIgbRzGPB+xt1~D-v-72hlie? z55a?xj0_O>f`)lIKx1z3eIejh8FWe*;V$SLJR-3osy@h4Hpr#ZZ14-TK`kCGuv0;O zWYG25pm7&b=pZ6!3JK(B&=eA=T7(?gr3gMiA2g1r3|UMGK5z##g(Pas#BS65=hv1gxH+07X5h9Rg7gYlncYcxRX5+Uj% zP|OFlTOjIT?G}jnpmqyHeKbft_{?`ha9h?8G~36`*r2;SL1&>zgZH0Q$0l9SopL+|`UZyB76X7TGy`7^gnTX&<2R&1JjnV8UbI2u0Hjeo$mkxHbDKczXz)4r z;PDwNP@NB(XMmoq1X@2E1rmp^4-;fyU~&hmhpd|h7X|P&Eg<#aH7#iBLF=y}>S61z z!RM=k+aR_e^Wptf(49BT;5JAsNE|#q0&*urJ!tF^q8>c<`2RnsCB(!Go^JuGXAB25 zYybaefT#zR;Slw({wlzyzKz0^OYgI$s!kOEGNT7%?^nof1GKenjMh z3NSHfYaMi!zZiJ*4K&>dDw;sI=t5T7LT~v{(qpn^1fLuXnp1=9#{?ZQ2%XOdpE3-& zT7|J+P~AaKR7X>V&rQ56+Bx1vy0k&dT2<)Z2_aQ$@a-yMiY8%pid?d0x{Be^MzO6S zYxaRJC+6Y;T}sTr!1({_e+#B0CeVFG@eqfCmaoC`I7a;e9X3Vk(Sd3TP}dVwpYT9O zZb7ONmMr%fPWp-nBWoAA`ZASUa!8^HkNpRX&Z|Aw9 zu#3E|X1cUH7~EGdBSpi3N@WRc0*)X$C{ks$CvvX#^V9+`%9NA2Yy+ z2k3|ZB4VJ$6u6fPiVDaE2^I!*aO-6U1B?C+1~Gvh3{rPMW`h>!fp(fPf;ys*HS37Z zgSfG&G15VS&;`9rs*-9ldK#eJ53q}GbX1bue}XnWAl-h$bWBQBXKE&By94b0Xr6+< z!r;9QC|5^=5+P(ws|-^K_*&f}PzMlpe-CKz7U6$r{|ZqmL5D~X0?>Xu*#Dqzl`;dU z9mmQb4m}SObVLbg<)ieS9Sop}1<=|P14Bkp9034XZm0(BXEDi0s)?y-f|eb^4!+b@ ziCPI;&{~orsiK_a0$OAU>lpDA{5=Xj{Sr)o{L07xs@EczwHOo_VnA&N)cJnsv=Byw zA_SyB1+f&U-hu2|1I6PG2EIF>69{;qo4G~w4GfVh8B=4h2O%T;C>N2$x=X4F^6O)} zjHDnACkd4_t>{j!kV_^|v09Wq2j26Ti3Apji%0LKov&O}5P zXx>x?ykiHFNI{VU+F=2@MM4moc-i$K*E7o9F)#!tS@5lw7?p&nF_X8XnizaM1$xwS zoI4Yrl zEAZ$R=pYYJ9)h323_E%m6o{ZTAfUUmL1S-PpfMtG_+7A|yr>K>-=N6>kzk-19-Lr6 zDFV`dM`UaU1~zEM2A!k;y4f34>wpd!GB9KWb;BSBpP<~SZObSQKFS`naYK>uAmYS& zU-Kx?QT7ogprh==V5>L&-Cz_$oLR4ie3ZRA=;V6nN)1Ma&rIHoHOw54J2yZJ!B`>f zDezes`k-?zKx+kHcWy8mvnw;&+sfoJbL<6$5hKGsCUr&~rYE44<+~V=&e8ziD5wv* zQ4pj*AI9*9G2nL$@_|J_i^G2d(~RjLX+8E!ECv`r9vG zS}NZUu_v7IEHf(u8-qJY2izX;xd))ZNbm-5$hilO%;3Xaz;`4b0QL4`cQ7!4ErMNM z1G=tA7_#B;SH50psa8HyII>;AjAxk{7{oz$#4s>GW(=8bd9NwhPkR3yRj&bkg&L*fP}CRkEk)bn5sEbu$-=`xR{c#j1VWM zkc_aBn7FB~90Mc6Tc#AoY^GaS&J_SHf&rZ?fO;f3qYiAwFa2qqYq7Y zCbV1CbtJUqg@oj_C1A9)yu7ruygZYGpuCo(q?WuOj5d^&5|fez6Oi;-{@;+%kwKV2 zj6nsoMpGPo83`!NL8l!-8-(EE0dyJ>Y{e#M&nVLL{1ZKUD@!&H#1e|?bKvG@jbftG=xu)4WC=;)Bg z2BEHI2BEG@USg{h=ZmdU1lK|TCop+3=rMB(uroOSy9$cF|NsAAVe(`s0*jTx#S)nO z85S}96<}u!fr~{lr7)~yx*@>M7zGzAWvXT9WjZLp&QJgsOJE9SSi}sPsSbmS9b!sh zaA3M6z|I&B7t3bKVwlc!MSz_l6C%dI_O^0W#tNNJAlHj|~Gj--AxQ0-su= zZmt}XtEM0&E}>^t#`IS~T+BpPPCSF*|NsBJOb!gC%xVJcj1dqw{{R2KgUO5G5Hptm zJ7Y9lEP*MUVG%Q{06SwST?O)pc7=^`VMq8CoAYEpgRVJ!l0_u)Y#P6S5hrH7~F0#1)U-A|M>q9 z2Fw5Z7D5HSWuaJ$EpiG@J`)PCh*K)tIJ zIs^yy5x9j1?PP&#aIgS$bPO!OBmkOJ1a+lA3*%TXK-Lk1DtS;X2EC;gG$H~z_N5?( zNiinI%G&&70OJ7{(23DbCX8PH96)V1&^!QS4j8gV(jK}-5+V-jgG0o@=KwG;GI0Hm z0pD>Z3)(|0!~mZQl!cwn0d0vOn%&U$4`@{!Xr5P=0d#l?Xgq@fyaip9ffqWRFAB=M z8yG|(iP-_hXaF;IGO&VYvp{Vb&mAXIj;>DJod1@CZZkZ`!JV7@?~RGm|Njgv3=B-% zp!OzXGpJq3YzLZ1VqgUCbChK|#-POz46+*1+lQq{XoCykZ)l?)yif=`Q>=JQMAJSNKj)_E6=INKmY9QUm@tEzjG$9S;rCy|Z^VXmT|wuW$bd5t7wB#+ z@OV9!{!RvIXmEgT{uKnx^n>yMs8EDXd}=caGRn>l3Ys0H0`hxV8OZM{jPMJz(L4>R zN*TDI_Zmttw1Ya8sAtPV%PoZGpd%}Ym_?qfgOppKK`zz{pyez~;IW`NE*2IU$y1}<>K!-_a)7MEpIhD_tK3o5TdWem`b6QIEq(BvN|o>)LTg1{*fw2m9JgU7%SyiEd< z7L^qdX)%))w0pp^KZ;U6d|u43Bdc^*06{r}Gp0Xbhohz+!2f`JiIK7;QOw*(D- zuz@d-!0B3OQvs45p{|9vM^L$ivHjoK5T;$8fBPAj82J8YF*z`SHcyFyQo0}m*;WE_SwJv z_oq!`X78GR!W=xV0j|Tr@vQ;Q$4t<&TAg9%F3@f@a7hjG6?D!4;VWpFgb=_OL&Ye@ zpzAy!K@IBMK=-(T*Yq(mFvD&X09Ewx&i8(B=X)mu$lstMK>#{h3_3y=lnGQXfMyFt z!26{Q3>lS~m4)q?K_|7VgAR5Em2twx%zTXOqH@gQpsSz-l^K~`)51bH1O$b-MHP6w zf}&i5T%!ZMdF4d8MTGb{g2Pi?gBb1p9b)8^P!Q%~Vr5m*W@P?%hS3C!|LG{PvNCar z$V>eD4pIebgfcQ{{Qttl$#jfCmmwCEQ}h`0A!8KCbr&M2pjif-twD7^Xvr5Fcm)b* zS<_AiK5!g^sx;8D1kjDq{NS(wZ?aK02OU>t%V?qos?0#glB&T6V?d`!f_w@(shd$U ztW?d`E6X*=%_}LU-Py^?OTu0y(=RXI$5P%*LqgFmlE=-|DAmbU)Y#FpNYubeOGVSv zA|=GrpOckEOv74T&(|E(HwUi+1h3Ns?^8fsrwR26!fzPKA9~mkBG#czK(GL4F%YP6 z4!Wp8*jSL=n9*2JSTUdf87`pPW`dE^i^_ zurnwcKy!}_3{2`wprcc|L6Z}xbrFVJF{)yW$_dd*1NB`cpz|?ekQ6Eg%D2o6`x!ut zoeWIi`j(SH3_3q92)+{zGz}nZENBc%g^coy#-hqfYWj@Ig319Q)l(u{1?7aqY3FMX#(7ZjQM26*2!mTK1 z9t8)zFu0EZDvn^SC{XqRr5tw942!8Tq@AS9xb+CHgo?PRiHf|lxS}W*V^#=LVO`Fw zEEjQ6VNWspf8RntTf0Fwl7rf`pz=;0 z>^@_#d%$~%!R~>M^Fr>p7GeXfxnN)f?S5f00-voJ0dgPex?ZU5h;$F_k0AsQNe8q< z4OH1ui^En5G6;YU#0DiDCD84r z0y`KqK`Yro_tx7nflgHf9d5$M#4f5V3^`vBbkYx~#8PKuvJ1DAlCli93$cv=F(Pb3 znEYK_`~yIQEsMTys+Co$uRcpu6cU>$kR#qB$RnNuNbbmtzuFh%ySKn+w`P76cVZ8yEx`Ko^pNnu?&z3|Z@7%V-Sg zJDPzOv@0{NVU>2*vI{d258+cZ*U)uT2nk^n=H`*Ji?u&?%)nVwn1=^`XFYh%8yu$5 zyTF@Ic#-Ngj68}_o?>)u5dAtxdAWlDv^NJ-J_>>ZO;FJws4=K9=pP5PZQ2iNpF+w$ zga27f!QeGZlR?Y(QP(UHj$e$B#cW)IA{?}P5VU#;6!H9^_E|pzKP2b;hcV4|;baWPdpubf38d+{GAa0#EvdE@y#847fEU0`4wRZ1nr-K+=L7{hkz5e z4=8Y7Ovq}G+y4Irxosu7yBHW5tU+gHF&$%21C1FmfLh&1`3t&M2$A)nfeQ{PZg5Bm zGH^qe`+$yJ1+|eOcSC_v1E{sA$E0o!nwT-OWdyBY1C4?4F~Kf&7hz)*g^dp>=_!T= zJ%1h)s;H+V!Rf)P1Q|c#km&m@II3^9bx@kN^+xfEUbwIziyOr9oq!;5HrT@)lV}(ACoF4nh115;0M! zabH8~@gL7*}SJjbWTmKN7i!`d9O&rd8OWpNR~}TfAj622%b%NB$}+~)$Xvxa+1@(A zP0cmX&psr?(=ydBkb#*Y8#Lw%9-Eh9PyyZLA`QLMcL#&Czzzn`PHhk^2=_JAlZf$p z#PB?*6$M&6qXON?y@NqTAG9YIB*w-7Iobn~ww1vr`Uoq6%00-XY#K|@(wS$)lGC3!J< z<(ZIrwVi>1@h1}t11kgW@e+hTp#3{YCIyd|fX?Uyjo%0=w=)*~yBEO3;_}adfr&xv ze+iR06X+6XQPB925PZA@G#-kmKvPU=uSPE#U@tOPBM|edzcw2CYi)iy%1Uf``y?lAvoQ;LkXz$IN zH*fBhl3`_GGe7dbxP@{`V0x;Nu8CPOkBNbSnx>u#pSq^ACy%10hKic8g_om&1vd+ay`eW~RVO1u zJT&d|g8YTP*B2uXVHq%_ zH-Xk|Gp+!SC2RkMfh`q-fD`k4}0nmz9 zPzx7y=_`2niWdHkz(K+VdPDb~be|s4a>6aUH z_X`&TYM+;|TcLF`*l7%)q6y(v&{7m`=y(?=q$J`5mqbjEO37H%*c5d1h_a}1e88`O zfM4K9{`=>}3(!~#sGT4NE>nZRWojY=1LJol76yD{J&3qQxB?o4C`BlEtOuM16+tZ< zLFGioX#w8?KxYRq9)O>gWCsfs%G1nqTU zcMlVI*bLNS0bMA}2p)D9RAyw^$9QHRWd7U%zIL5Ohz->1U|?p@`ELPkBMUNEGx#!G z2F(uoG5CY?6sR@?4TFMcZ+QCwV^15j(m*&0x=#WjU=1pUtr@`kB*2l0(Wo>9sW1i2 zBZ4}bpkXPHKR}0~K=z*tGcbUgm!J`61@N>9XshN<26ymcUkwIr26wQNO$Bx`*nq`A zSJRq;H*G=NBB0ghvWy~fOybIr(Fagl&Dcl`QXAPZf-XP+jWL2wA5|7rhMoWqKW*-i zqKb;5kc8XaEBP<129a zSb|wTLTes`bH?-|w;00x6e+FLAkOnIQ=zxED23`iHSkPH!pt7D3 zbhRuvLRG;Ls0JQAg3LI8&Mle6D&uY*5*}b?YvUad@K#>lKVaK>13h_#bASIZFoISK zFex#yFmN;A9xp;T5u@zH@D-wff>cJB<3(m(?OyF(|3sKr{@F1e0^z^VyTi@?moPiXav2ST_7(MI0E04tF|jRGF%+=D3SEG-5GQ3e49QF!daCSk#4r!wfs zC{V`&c|b*U;k0Ly{9>Z~A79rsHr8!tJm9iB!q+d%MMXnPL-`=69l{KcW5}3L4`@OZ zGKL8m69Uan^9k%=z!(!kxE?wtgb;v+HKHCuZkq^$Rv3WG9bt&Cgc$@Fgu%W7)kUBI zYtSN1u&)p|A3)0^(3O40g34wA;UI6Vm;>^b52H^&022$c&lnGYQVkPBJ_Bfe1+;cs zgdqu3`$JkKppNPe24;Baj8U^9JOJveazX1&^4yyrumI%Nk555vor>yCP+J^Ymocb; z?x&ML8#BV_jvzu3bX_ATi!(6DgJ%;#Yt=we25F;%`cj}V6i6^)UE&RGnuA7Cl+Qg7 zRj|(&pw^2~A2{ZRg61Ns{GE+8J6?wp@#-U`TC9bWhXyT&omSPLqz2Rb{ zsbwIpWu?Rm8Ui=_|Ak4533R%F0Yld=2GnsQ9(Y(|6l~CeBt+_g#s(twK#LJ@e+qP4 zHYYgsuz`xc4Ge7HRtz_I7bs}Z0kq2#G(H3KJY)b#R2g=h1Z;2t^t+w})5g-#^D& z6%`o|sA$M*s6gsia9!IQYze21W+O{}$jg_*5AdfXZ++ z@PQ4mJ_0mlAV!v;i^LGYf?Q03n?%rNl>q3rM$p&-_#{4A2G9ly1?asCplNs5VI#2q zHNQS+QXW)?gU$dzoURI*Xoeg}0$*at$0Q1Bm4X(TLiPw_JCX$DP(HL{Nl?z_1078Q zYEOg5Xh7k{xERzYWGEE@r58{e517ze) zoIwG)*iu{{GD>A&2+9tyHXJ+T-X73Zx5}_7C2>ARMmvje8+lLxfOOZy18@Oh;v!-x zp2!0#06^!(!|#>gaOMS-4hCB4CN3cVf#M&u_l6C0?gQ*j2c|bdYz#i&_B-fIE+ugK z0G|yAxwGN_fAC(L<6w2Z;PF?`8Wt}m76#BQMeU&ZG1z)T&>2C9^@h+61|npjDF;!6 zK+_gj0F)v?i3M~ggdnt8tOluy)EER9)WB5{=zJ;A4bqVL3P^#X4(=+MgJ;qaH4~^m z3SEq-3cJNdl*y1!oS);Lm48Txe@hOxh>U=+o)kahOnpN`eLW{(Q_mEa&fQ^te&LLV zJiUs7Ed+%5ozyfmHPnuR{cXm;zy$I)Xzk%bP>l>*dkCtD5o-^j2>{`5j20Ml^a|{6 zaE^giDB!YGfI$davk5@TQUP#T3aW)ci*Wd$3lpUwVI$2Tz#t6{8_>dINKGRH4iRB+ z=MjEMk*Tt%G9o0*ycT;wf?=^2V=CDHOe{BVf<6E8CCt}@eyzy zSp~@>pmCG`u<;abAvMT&3WGCL9DHUzXq*MqM-yOU)&^xM21bU&|0UqQDmOzIXu%Qr zeQH>f6?E1BXb2NTbAnc(L#8^FMU_pJ69c{lEc09rY5D*C)6)ZT1T%QwDroPpFoObv zE@iU4rv5u6sAg5jXa?J(3f@2rx|qu_ract2O%=RzSQ5PIn9)-J zwnvqL8GQee7!#-|C&Qo&@uLcO85iRAX~@lvpn(af9}y`R)sLWxOabmk1?YG>#E-Jz zG>7R&Gh0SQQ4uj=B{oq-6E#L1*w#}R?Hu&)T9C7I5TjNQ4{YD5nmeQ=;~Egc#ugI5 zI4K|`BmlPNkU4;Xoxu?_RfRg90Cg%NexSt$5{lB2)aNs(CX#!q55aC0d9qo*op5P(-o(umR7#JCh85o#k!E3$pK?6XjYrUWy zAcQ}lkplJyqX4K230mgD16of4ZgKH|3WE6zJPhj@cwoCtc)%+ZAa|C5HYR}9L4xl` z6;u>e1l5?}+H)Fc<>j$sA<2vr7_~efI)Kj!fv(8`ovpwNIsuM{(AkX0qp_gV;vu6P zObooxLYxWYzV{4F4F4IJK<;B@$Y)?;sE2X%8JHOC!5q-SO;GI*I{6shi@#uCXs&E( zYzlR^sPfF*T+jr_!Gi(!?Vh_VwgWBq0NoAvKZZ$$X%~YMLo=v6f#0j91ltn<9Wg|N zE28n{2-*{&1l|*&1i!3OKmbz7fLb5?;3dyH7$iUiGCzX^s9(>_P!D0SGUzi%FxWFl zfJb6 zFaWO*`FD|Nm*)@2s*#9_98kX`4}69l=(3&;&`coeya}`#L8MRQxB*u*&;c$1Sknwr zVSt(_pnkC+cr_1bO3)D$OJWT5pmlb!jvz0JG1!ASI~h2@gHoWabR6I@3|8>+SkSCD zn=GTUAo%6$lL->WjLm7;2-ykC!VjAX3dM!c@x`{5P@(R^; z%ZDbF8@lR%?!abX{Qu*B7E=uq=vcGQp!LN%47x~j_RvumL|j7qUkCx{WDi0B+MfaE zB5+7Rrx_3e(2NJJgF!bfi-2<%J1ENcGq6LVydTT}by6X%5YP&MT@0Xk7csDEsSBWm zV$9Hu?4aBVD*GWv|7km1~aw9?kb+Z(ik7K{n3wLJjMOQ1Q$P$m`z6^28guvcYJLkfFnK0>51 zXk;J+ppk(PAU*5_z@Z5a`wgHVFGK{n5IFVmGO#lUffb2e0Bw9>1`nx%0vfbv4pfSO zj$&5116q?0nvyfcGiPTks?5Z%=MdqAeb!FZ%(bT~zstdx|2SMao%mzFZtm`uJ8UhvspE8cmE0w*O1ObCkLu{|Z9eOR&BW z5p$G+@HtA*S{gp^00HtGr66q7L|_MlHq^g5F#m$je3%E8$!V~324U!3pjs+PeK4SF~KJZz;;}K zN`V~=9CtwH3_;difF>^#E`Yk#T;SC>pv5ncB}bs*Z3lxAsB;5ddj%R^hQ%i{xJCd+ zw5ak4i;xftC;h(2dVU2VZXtdF&QS2m_cVPcP)z`e+F7$0P5!-Alw)RP;uMk*I{{h& zKMOo35)K_NS74Y23J=sa1~fbnQ3?$YgaAf(K+n0s79JZw8&3*B8|px1HhM6~L4!dK z9DtJ0lMNxkpl}D&GK9oEC>T)V9~umI%>n|<>@-?~D+EDv<$V5DR#NIB(HeFyK*^x2 z?8aef_#w7 zfXpF)^0Wj46SR#0Zc_F`T9Tk$4xqLrtc}mjzye)(4BAEjT6YNEb!UuYpTnXBUs5|F zD+Ogm#bgD89PMTGh2m2~mpL_}0TJ%JFw=-43In4k$IUg!uKH>6&LZr-3)aio^U3`%^N^Yg@-f5so=Z; z8q`x1RR*0MC8}%|u$7T@Yrs};=(_xK00%B3gWLZsCMl+4462az&Zy)080jABaBvC5 zCa{wMwBQv|$%2--f>^NYsCF=@-Z3y_)nhb8-WJaWTI_0U1Ua3VT~wJ-CO5OT!NAQ3 zvL#+uAviEFSV0%GVV*};%RFXM?eSK^&CJAN7y~}OuK~G7(gc{GBP;)w_uV0 z&)LU<`c$anozSi)BJDt%NeBUGmP9N&LH04|n0e4(B`4@?ab{zAMq_(MV`fohMwTOi zUI8otUj9b{m{?9dcyQ|PpA!s>4B)dM!F#SUKyE?Za|Lw^BCA2&f)Id?GJvadP`2O$ z*Uq3LtPlj77mivZjno9}`kSS;AA|K2Dt)GL=z?amBXz^SAwr zaJX~{d^jZ{%mf*VLFt z7=S{OA9MhJ*-{Lf;FWdf}b*a#{(Av>30X%<>Q zA%YQF>>>o90nKL@2`z`AQ2`EU7SMJW@L|rNdyCn@EDi=92FN`p90EHSK(}&%>Kh?& z4*^u)h(dY@(8E0bgU|4A1Z{Q(i-39vq73<0>+yl%EMXxrH#P7}6jRtN`FfcNBL)TeQ#{gh6X4Q zM8O3(C?X}mV>qT@&!Mi90>$OW7*-iK3)oVrP*3EwQqc83jGz*ji2;0nayw`S4K*Gy z>O<({93o<&3u(aFkeh*xK@?oxftwcKYC0CQ#t+n@05yVm?tq#_pxaN_v8>ZyW<|S5 zn`6hq0@+urVB+Qi?Hptj=HOxKhO~2`YuF?hCV&PN2*elEJBav#j@N&OQh zw&!Qy1Fd5L4cmjxEaL+wTF3xD0|OtlvkoftK?4$OcR-`bI~e$(v>ng0`PC&SDkPleAkG5ND@l5)kl4RE8}kZ2q?O5v&s?u*yk;7P^7`20C{FG_Ehf zPz%a~sD6W1f{3U>cm-O7BHCP_86yU0cZv<SpgSP3 zoc9BoR}Ta4w{HW@tui}p{fJn% z0c}En#~vXyAE;T$49;kv`VW*wL32b%m+~nFY}vxNV+-i&BBmG@m%mFOcNT%qFaf!f zK?Up{m^(r1k<~!%1g)?E&Gkah8ilB@gP%t+2A@d^URS&v%m?i;2cJs-y4^~EjoBG`UKVJ5gDBX3@cI^JZRole_?~!12e5vy zI*@scfnYu)-0OtY7$M;fb01`n$`v6sNVqe=?t9k&okPJ4Qr`}8H?z%0Nc=+fZ-CT8 z&Q8tz1Q7?v6LYqZ8uUCH*je-O;C&GX;dAB%&^dF6dXV{$a~ELlfv5+W58kuDkO@@} z_8-VS&^vEn>OtWv1`c2F9XW9KgVlrfSO_k!p0 z!FLRU&kAP%olpsiM~Hfmdm-v!_CwTP5mJN9{lo2tsNVuV#|L)SJy<<6=&XCN`H(w$ z{{M%R-yr`nPKSgy;+!DHS0Mi}ZU>1oz``56pBk)wHb|Tq<{nW04ZOaEaW_aD;vNQ8 zhKT?DOj69+4AKnp4C)Np48{ywK`Z4<7(k1zcQAlm1cz;M+ z73369&Xoh_Ty^M0KRS>WtFj>YmQwJNQ`i|L+KkMm%A&@QRWQoTpi`-!Y(_V`4IAt@ zqS71;rSz19WsF@lBy8hnTiV!I+QiSccXYHbEM(eMSeO*#>@L8}A*F7jtmS1U&)D_y zrM`itf#KgPj12!7Ow4plO+o1Zawh#&AvFfbne?!8o51-9v}S|>d?q~uth_*154-ye zT|MZoJ%st7as*;NEMCFtLFoW$J~*E;Ff!Qx|H1^?|1HN*02;uT2M?#h+A$b)2ee^> zXnsRyGr$6{g|BOK}jpz6i{e_>KXQg4EyUIT7^AXGi5 z{80ntSBU$WO;OZ??#h6;KNv-Q4ctBEF!iAD0pD=|Qm+QO6F~qJ8amK=6cSFLyADA9 zR$~nL06PCO8>AZ%UM%`>_qn6k*9x~!7o_9=e+F>)BB}Slp_4#o!Sy4=|6yqUkNO`1x>H0*4P38+ zLe3DZ&-s4{=nixtHPEgtaEL0LF;XhdQ)(EgqZJ#WWEtNeSy@2(j&xtT?Pgw4kYyfQ1x1% z{r^bnO&B5SLFP-cfYKwx{6MIBQ2Rg(oE{b zPjGq!saIomV1=YNLuh)0gexqZ)EGmUAo8H=zCh(&2m=EX3s^ts-d<4nLh?IgohC>h zXq_e~d?D*ML;i;_v4G15uzGm;ko7-=Nr~x=kQ!*6Cpi8f>Mj08@=@c~xPYy?f$ zpmvo4l6qKq0umQPQf~rP4>Df?9Dfk=k;@ZE{6Wk&MNtomKZttd@{7E`2ILXP)=lnpP3IT?_@yrA)_<69RjQ0(AC50PjvOp z(DVaQzg$QSY(64=fYpQI18hEOe5iubHCR0xvnA9Vh9^`H1iYs|UG<5oA6nJdxc4YWITFBf=A+9u%G+^=!;qaPvc$BEaDZQjZcopnEBV z)S%{PL;VXLrv>>J)B+Y@V}`jO(oW`vx2s|4LGXVFlRG&4L3j2b%m=Fnhd-!Y4H7>C zE{`GRgUWBPdU*Il)Pu?wi24+0et@V4mER!sh&$^c>Tkf@!x#lhKMafvHjLa%GK`@N z%nah7wQ4Nj`Fn=l4B(kZ2GDu)!lsI%HjED#xxKs?7#SECxtSus>UmLA37RU3GDUzD zfyV?6GoELx1m86vhh#3a#}94S$=O937&00&D>Iron>I2%`S%*+C`N|+jOQ7r!t^pR zK;48(uSjFDGeYl~|HX_=3>^&245FYxJA__+&|Sg?hN>Xz7#oY7JN~@}mES>(=NZ2< zJrPo4s_ADCU}LJOV*rPH9^-i?VX$~DSiBY{{=b+>n4v>Rjj3)Cg8(~I9m7Phf8G9D zFbOe%`uR~H|MGxW>A~8u&{;}EKOfo=L5%M}Lj*kR4eB_6cBw$RA)szQXpD{Pf`K8c zAZVE-_}Vo=M$W^B-3}i|$KTxQu}p6lb6@D}K=2C}=u?fq_}u zz)%(C>~TgNXLECBXLIviHaapoHedpjZa{mB|1jw=)HCpcZ}~|9t^S9d2M1dH&ka4J zNglpl92)B2-UE0c31Zp=n%bai6d@|WXQzXX*#Ipp1WmYs?;x;c1kJR87E-bqqc{|m zmkqtAVICUIA}b~|c(SC4WV~nQCfp$a7L3TrfgFzo8pbruNrDZnoffY5N zU2+nT^EW^XVL-!gpw$5?3=sKR(CIA_uKps!*Z3>mH8M^6~emH zO5o|wZbvUqClFRrRaH__Rn=iUB=XO0xxJ^Sy|X&2stbtWqRJ*@Di2lg1xhnc@b!a0 zBV82(6lhfAKzmb|+?YVa_3WUTSyl$fDtt(O#n`)v7!5&8@IdrJE{$Z? z2Negf8`xR&Aw@Pjs2u(0vA|=E#{v(gUH`5m{ky`zz|1h4(V1~C(*bZ=Rfp_lfbBej zP23@FD91>N&4O%KGJgTxMWP0t=LQ}3iW>Lk zC|QwF$J@=#8-%r$m9;@wkcUT5kcWrSIXNaeIXOBe+1Sd^(a;J^Eei67{TET+VUt2b`@wFq%b(^g3cF{ zg1AH(a=sKOoIwW`KyLX34VHjU+%-35oWGfyg-=Dv#Le5q)K*BBUrkv}O-)Top4)`) z2~$U_n6#~#g`0+yl9-H&0;__G5I6fdaM&(nbY|Sm1X{+W3fjfXzyMhZfxK%PT2WyO zSJ0X}P)G_fa6qRGg!DnXdqHBL$twx);&(<+QDVnrZp_EbE)HJxE-J!?np_z5oV}#9 zj3q;q%`~LhS$MV8j9q*klr=S#l{GaPoqe4(+^qHa#kF({ zFoDsN(cY!Z=Gz(IxA?&(u2kTqJakqCo1HruRKPJU z#lXv;0*+}=j?xB;u`@`4$2ma>3Bm#$a}EhpSbl?^rv@o_p+jiup!1iNm1G%3#GwP1 zkW!aX$IVMuSx(i(-rhx3PDRJd&DGV_RZU%8O-)^$(b-l{MVFI>OItzP#6(*`n~Q~0 zS4GeEjtQrU38%>#IcXkgIWPfED{~n=8MiSVU=U|e1(htK@U$X|vdbPid4}*gw3q>> zcu@g}UqBX--A|lOE7@a3=e|@SPXRg8y^$9vK_OzI%vigTFS7adTpDOrm1Xsekx`#3sz^+FR3lL1na5mT@;wQ%(q8xO#g{e7s_mCR6{DbUl7GdLOG-llP??aQUqN0MbGKU$PsG^>n zos+3n;#tc~2?HrMDS0Ic5g`Rhep>?_M+I*NMuzGCe=)9OEN9SSNCCB1QSVBDHb)U| zMP9Y6!N3Pz=)QwN12k3Yz@Whpz@WjvR0Nvp26=-|U?&4RxNw&i09~j8sxYCuOChdi zM9IM@<+?VbDWjB{slQS_)W?*1E0!|Nj>rQA1iSQ{Rbwlb-&{cOk7*Oj@ zh!}F+3Az^mQg&=VJv5mWKhRdcOvEqp>-!VJ0WGnE(Q$-UTFEC0d0+f z`l}iYir`gW!VC}=r0Du1!poyg~Pn!{^S%(@d=Z>qVvy0T%fIt{?pC74(PsYN0A za4xh=B(Cm6_#9eyf;+&-b*DVEe!*6Es)|rpcRnXjbuutBxIyQGH5iN-EE%?gQoj|0 zHMly5ZNV~ww}qff*$^!PL@wCD0J;{9UtlK#=uS^i_lyC2-3MfGq&~Rg%FVzJzQ$q) z1L)8T-a9)OgfD|!uwfXSIb)f*VH zs;RRpn}ZkY+A|ungVws58?(zXinHr6sx#X$g08p%E%?)7WM>u=7uI8B77-WbV-#i7 zP>|%YwOkdBWQXk6vO4hB_$9Sop8qN)I>feM;f2OYi-I`dZ+y4-Fjg8_J(z9yro z9V2MVu^ywkvZ*mB{E!w3vx$m`fffi z742obEuDN>S?--;=V98qjYD68u_2SsOvO=5L&Q-?T~yD{Lf+n3S5w!?S42h7QA9(` zQO{0*6;z%`GcYjOGJ%$=$ulT390sL7$ogl{ZQ7v30~#NKg%8F$MCb-hM1UbLL;$z$ z5HB`XvY#{9Rp|+rZRXL7Rt2w14Rdqa6bigW>-#OukIJ7%Ul_ z85Zqguw`&za0Ta5TY()6h#U${Fo*y_&Y|D{F$VQxjUi)F&Y*#4uqn>^I~gp%nUsUU znZW`qW-I_1r2<6`BZD(oOj;k*0s@U!i7+UE7i#Qc09}U81C|A?W^@9lEMw?(Xzb$P zeT<;fpY$0)=?m0w2IWd+B{tB3FY=6_n;k$42SC@C2t)b>+Ki%xjWGdwW)XtAYAPD+ zd?LKU3R?Eok_O@mymEq)Qf#~;{QLs4qCCEVR>2anI$Fkhdd6BzyAq>ZA_Om}t0*e+ zi3$tz3M<%Z89MO`nsM<+OK=Mdi-9yLs7l&+>RCViw~w*IKu=H4;1vTX9fIzD0NwQf zS{DOOhmz2B_&XRR1$HpVUVzZxJ21fEh|!zINSuhI2OR{3uA1eui`>n?%D}?F1Wt>P zol>A|3TkzM7KlRj#(~loC_RD(he7L|LE{>rbO~y^i$c>QC>?^1i3X)ZIe{GvpxZO# z1wdH_v|-HD7?i8kjl~tsjoFPwmBCnh%D0WQ#b=L4XQQ0z_y zHL&+|1$Hv%fmtkf9N8JxGq5o1XJBDC&%naK91AKK)xaZPpgZ?K84$F56|_Nj2LtG! zPf&^g)v6K~KqGw8V3nW|J_AGOv5S!LJ#lEU1S*aMmDP>lXOMx1cG*o8MHy$ah`Aeu zCM1L!xr?#Poay80>QgGAr64SWMeSs)@;yLVNhcew4(^z2sZDu1G^1iHsthWmAe(v^of~SQJ9^wWB^31)JOa3%Jb(U#JHSH$bP=W-uac1)59qD} z(54;*2~BySxIot!*T6U-c})ogSu-6e@I00b=>9$?(CAMi=)ybHIV23fV`LF%IS$SM zXgOjR12bsdF1WyD)`x5^+`#}^Wd|8Xg{+(dO<#aoOu`oo42?lssK9&cgpHYvMU_Ff zmhA}f@CY%x8h_))#fNEkEEwHg8TI~M0%6yGI~m=(0b3vsW3pja$B1#-w*?=xz7YDcWLD#55SZoZe z-~$do{QwT=UIEaQ8RR@HP-y@;7J-X_6WT{azoQi*zKcPG0iqw&#)Oz@V5lmpY|3tG z4#wc4c16ua*+tDo8N)++dU|?ZFsZ)i>FK$7bEBiHW3;2I<84pR%@8IiO|vk7;uTa+ z$T6reXfWt8&fmqL2|Wp62ZJWGrU22(7a)8@ZwPNBgBIPXfWw4?K?Qmv9S10#-)G=p zcn`WNIo6SrVLt;0!+8b{2ACjAp(8s(0|N)c1O^U<1q>Vv%%BYi2@D(z1t2Yjpn(ex zP==5Poh$*~jl~W%oJAjO9;AG^&%na)o`HpdrO=TReA7H=nWjDi3xhpKFxHWs0jAhTPE42&R`>}SwpIM1NRaGwDp%L2Mqx&b5`yORO3CKR+I z22}fj>;bJ01=*u<0g?xF;b|Yz76Tm`EUGLfA}YfOstQ2|cnd1(F={iinktHl8=09a ziY^T@H#ax`=ocaAY{|%I=`0xGH=mK4(@cLZBmcjTbM?(Q|9xc)bqsKv?C0vpCCtq& z%;o6n$C#|9^)Mu)S4%Ab)RwUL-_OLutj(arpvPd&Fdx*Nv0$(SdsbFp2ZQPb$ZfUy z@OlHIJb)gi0nYxQvQQOV0x~ejLXWcowGFuLfQ;G6pbwVgVPFIItw3pA70NPUfZYLT zq7OPo0JLudhLCQPu@$2@sLR9*YMiMv8^exNW>*$ggC4;wCeH|J zpDDA;F<$cZt*vM0WfQU&kR$I1dUxNS_qe4}b;_qU=G`9lII07(h2ffcr_{3tkHySs6eVyaq6EF)$TE zPVfSiC63JC%Q3+hldp$ej@bZWfKu575F-|}$pcnnBTg!jWDo=QP9gnRP;`Q(#X+eK z)OXN=_8oM=dti1l7=z=-jDZu}T!x*0Xa?$bnVTw_D%vrcg3l9TR|ie0fX+)}1Eo%O zQAJTjK1NXy&{4_ephFQ2^u0}FxH;k`PWX49F`t9o&O*&y-c&o?OxxW^hJ!6}$^=H0 ze-pWRoNP3GwTv|jcw`KnHKf%A9K)F%d8CER%;l5~^)w`O?bOBOc|n5wD&kfS%G$;T z;6-{&@UZ|@27S=UQX&in42IxTDc-DAgZXZ1WK7oMs5buBEt4gc;x<#Ptc6Ne#pkH<}aDcnvETGAGQ$7)sU-<|CmZ$ zUFZMPV!Y<+%D~8A!N9WY za579_;AB|9z{$W|2yzXm@4*aGlK?7|L8<=$h!G2#PXo#EA@ztsJz-FpD|rFr9A@z8 z&<2Kz%*v+5MrNSo3@T5-_o2gw4TKpd->Y3??)vWrqbjSj;TpzI`$beZ>nd0S+#WM3 zyP8?JYU{hYvT|yuxH`r0{ks9q|1f{cgU+^>V^9Dm5?KDn=zBpYXTfa`@IohW+k=Mz zw38In_CWNpK%pW4u4h4`)}ZNUPYqTF@`oj*hN7J+te6* zh}i7p>zDZ+oa~XmqG+=v76VFjI&8V14kM z9xR=L&iSndo!$ePy@qrSB;e^3I`EE2r^u}~P@j|o+$Tk})fg^->T*!Z1*HT~%!3ZA z1@%cm$3}w2JV3QNXyjf2-0?R@41Pd|0JIrJjYZX=Ye=*iuY?2#2M0e%OiNGffBblY zskOD~gtoRe*JxKp|D1$`jA+-VCI)r}CJfBry(;p|+6?jxnheIE8_*RQVEa@+_fdkJ z2BNj$zJq2ugzuo!A&^`M?K6YMDM9ro@&PO$e}RU~K>pSOmtG1CY~b$74hGQKkH+8> zBf@~ZHwDz{0M*T!V1FxuhRmTmQcR6S!~HJ^XEEBl+NGq}x!O5+ zSR2`TNoZTE*iD`Y3XqBOtsEV#=0i556qfTvyV|)6GV}R)sJoadu@*D_(Kk>xF#LPX z)J)S9ynq^1c7I{~$Mk_gnL!UUrz;3=lY-8vMa*VHJJyIEDx&6u^sOOnP;t<1H*nF< zAOP!cYA^_b=gB}dn3^pkA0w#3G8Z%fj~Ie3M}k*X%*>!UYskDFqhml2pESRsn4YMp zy1AmOM^2iqmX5iM6_296jgh>%ldB3(ypII8Ag39hj3A?inYX@ZSYYznC|wtG3u9$j zV^uMxou+Poom-p*jlqj`Ss1h#7?^yS4l%GZh%zWMsDo}*5@3L>jsabW0=f(ZM1zh& z0MQ8lW2`NKF6Toe8R%RgLI7IWfYT7T)P;s2xUf-WU}jJOXBp6_D`bFT2LothEGTP; z+yRYXf|_2Sc|_2H6j|^T2BVUxiLsHW2&15}G$W{E1|Ap$x30iV0a->7F?Lg9QO1Wk zIXQpt)f=m^xqt}9A{ike8CgL=?+c8PTeoc4`foYYu78XF{bfq{>&6(%$ozK!W51rZ zx3(Ub=(hx|dS+q}U|h^3#{??BBtUn_almg}1SNY`cOBi;Jd(JqT-X zbYyn4Ghk%#_h(^bT>Mf`?1jwGYV+`61aiB#ysOKbN^sb=S znIPf~I=T)P&<3A<2`bV+ol#+Mw-nR`gBD`Orh1H!GpZr&G0<>{Dd_eeSoFv-iHj~r8kb83r;eSM zT6TP9W_-4qmz@r02Iyp@|6iEwn2s^XF{m@-gDMeNItEp`%J8^>ZuUjQ4K$g8S1m9z zaDCauAjcp9Jt093e9Ik3jDvw0e84&62p7oFQW63?8I&2gz8DySS06!!J(QL7nBX2( zR5b;i%gx3vI#I&kM9t1nLqJnV&&Eg3F4aR_-6PcwME?tBG?Vf(HujUsmD3ebF_IHk zwA7I_PiXP;Ye_H%(HdvvH8thWLe>EbF))DE(g~?Ei~rANU=UDa>ICiBVOD3D23{wO zOI{Nu|Nj?O{mLlv2ZYp_B~Z*qmEVP7zbXO!nDUY+=ELOq;qH^dDlY_=NA zNu%h;ltMx!u;^GrGhVOq>=E^N$NVFu^H z`8k$>f$=xfF$O*ceNf7Wnh6~rgeGuAif0FnrLu#@QVk3l+4ULO#X&b)sq-_cn=}4K zHk!XRA*N-RTMYZ zXEYaQECYK0bWSkD1AWMLK-~ZT3*&DlkkhfaAEPY-^$;IuvA#W{Jfpd~ps^skxILpd zdn~fihrzCAeEUxr=6aC(85sJQBA9d-BthXX4m*nwG`_}!68^>z*Rz8?qYR0P^I#K~ zL5xG;KqCbbC+ESgfUuGLcZ@+26hD$MJE8G|G2MYU`3|(A7VJNBaO{XfA`9885KE9P zg!)n?(i!-)J+PVPrpDst>Wt=$;^yk6#)3ak zOh^S;0CqIUmH%#o;{nC*ya+S78JHOOz-9^?i?WNGGm0~^i<=vZDq5qN!q^3}1Z*tW znc)2F%jCk8$DjmCPYMjsb0BwvPFO)rPoU`)(BaOeMxd$?bjC3xQ9TAHs!k~b6)`at z11T*pOGQOXFRe~Ulrg>rCni1_U3qz389o?cTm zgjYy*K^YscY!$sZoei20}~q)=saT$ zPzFZ5lMm7415MO1frgY21sv$)G)NHq0GAMqtS$#3B?Qwhu%&-LGoFOo1DZnuovA9o zpaI%Tg4-TWctOMn$-AJVw4q*209$m>g^?9v5u*&)qJQR0yikk4=>)Hzpv9*QWbzT! zPoO3!wAhCD>7dKM-%zXOfvsXZ`S&x_Dn{_U7*0PS+k^0vBG^xgphJ34{PgcPEbzeA zF!BB~hg!qH$iTv&$>hz{$DjgAqlygBvzd1@fYQALIE_HcQCPzTep(4=R0vYsuz-z~ zHt{vm^){1p5mhmgRW()QhuHB0oPrkXg*vFooBJ7Xb1Ryvi^}TTC@g{`VoJw}GFzyqZI(-eQCsq`+;&T?_`G5h#A>93*Tk1loZ^)RWLbA#gJn)O%xOVEO_Y zsRZ50!t@0+zQzbX9AF27K4=dDxYPtq!t*gf@|qcFuo0Bgz#|eSdQ70IR#aJ#$)wnp zQA65PQ%c=E)y^ixOEcO_&B;~H+(%E}$5d8GK}TAfnbn19mkTSiqL`U`jJ0cVu%32c zp?l(NT|=$vV9RI+B{i35GhGiWLva;G%MwNf&=?yd1B(9?(EJD8Lxb=i^jLSW|3KSa z^uYcD-8sn50P-IrgC2M^X9oi#$baAxoR1mdA@HSBkl}4en-^5=E1N1Zy11}1YfI}W z2+5lI=e^FwQEmWK$sLBsF))G8j1dEm{b_^x^l}W49uH{A>Rq&=H$U1FE{Db@R;^unH>c(=+%%Bl}Sf@vfNld{e z)YKx^&Sk2wj$Ka;4OsQ3|tJ%+K`jBK-YSL<``HmfC33LDhHa=0gW$%PF^)I zWK}d}G-U)`AR#QutjHw%Plu8F-&e*f|Jr{4W~@5l%~-<1SmN#ZZ?PHpoF)cF20KQ6 zCMm{X2F!h{91INL@(r|a)sFEFBfpCa10#b30|S!{6X@L0AW*oW?$3mV3Sv7Z#s~tm zv;v14WaI$U2M3K7u-!2*GzJYLiwlDWMVSBAr!Cg?WerUBJIi>)Vd=joj53VTf89Xy zYupTVOumdy7$h0g7>pSTL5GjQ#s$ToGq51q@B+hb&^=V(HD9|K7#KJhq`*TpQUW^| z)GvVg#0ucvaqSKUPJIbbh=T6T1r6DNRwnLXP!QO`pmzaeycC0;ppaz^=(Y$614DDz z4ghiRFHKVAqpa`2bqw*DV>rEis!t}|+$@MS4i4(Z2GGXXu zQenI)q{fiQkRrgwkjO9(><&8y4<;!l83tiU{RCa>up6||4jT5%;C>>DHt2529Soph zBhY*;=N-rfP|$`_(1HUIq{!ZB-n88P9iyTI1atmfYJH&iO_?!@EX%;uU+=7m^gNJB9 z_lOu6nyZ?snu5$%6jcmmO#U~QaUIC~e?cdq#+$l;`ZHP#4B&J4SsAz)+(B6llFJ}P z0K)yyp?gHli0}|3oI$(bKv|0$d{(Nku`si`FtakVFeA&YTY!SMq0MYA z%_z<+%+Aedbmolxo?Z4}(wtF)QAOk5M@D{)e>eW!(E!cDgVUT8Ln@OsQy7B)sBOr^ z0Bu)8#v|Av<(4_94+@%+g7&iKg4<#LE-;ornqG`1;P&4Ymwzvi+FA_FOx8>#5OaC4 znF}3zP&8FEh4gQktRV*XFd9G&UJ5q&-wT&3P=gs6WEoPK>|pLiG8fd};Y4yT_|hUs zyHOTwEMv*P3s7Sr?q!sK8_NJPx0%Tf=3b<5K$r`0FK9Md5aM3O-4KHf{`Ei&hPd~N z3!?BhWbkDOWC&%5WQb)*WJqPmWXNSG zWGH2*WT<6mWN2mRWawp>$S{>*2E!7D4Geo2PBBUDV(4Qy&2R=hq0lF=gQ4%j4hB#a z24aIoi9s}I0UL-0EwKgBpwq`dG-w?Shz3okgJ}B;I~YK;E0lJF(!NmI4@w6?=}0IY z1Eu4kbPAMChtj!FIv+}xL+KhQ-2$aMp>#Kto&cq%Lg^V$dJ&Xf0j0M<>Ae>ib}<}b zn8CoT4VrJ9!Egk;{z-rV#*&A#6yPj1I7=PQa)q9g0mu_tQ`z91a>ea zU4YPO7j`jZL1lI^WW!l)a8^5<)eUF$!dZQA)&w|f5}Y*|&N={Roq)2yClcs0ii0Xp zMNpk;&&bXPUXL!SY--G|q^54n$H;6XCa!G92(NQRML?^CO~Gdch>EZ=gO*sE8;hz! zX?AnadF;mGpy?CP@o=o5`w~n+6Oio6Ailb(u_#CmM1wGhZEno2tj}mHZfI`I&uGl9 zEN;xs3^O0Jn+v=I1F}d%9K4GWyi!9Fh zYOJz%rc9YK#Y;q)vw>4t1VY*I2@CV_2@5Ad>539|BlePiH!|3K*fQGLV(!JTwa4bg z{<{&I7rQ@<%`3(N1nz+d3kf@R14%Xmc6I|cXB9RHHXb%rQ2_x_5Y{tLRyF|P=RyHS zMgc-#vPsKBQq)sd*Hcu|V}ZCxUy!h@xPpSXtZ-1DhlS659&7Ibkd^Zyj4P=-0ooeazjf(-HuS`0fFSU`v7@-wi2&t2taVEeLz0W`x4 zTF?br_6$03RuWXlsGEy3qijk?Js*O{Fx2g)yp)u@yp+^(D9v<2e3jC_cTysJB2s7s z0|Sw60X4nR-NI}x&d-R)EoE+@hT%{*z}&)|uCz+L7|j)6F1T%>#=yYD%k+jph(Up& z7u2eQ&EtdS6UE?d3+M#Cs1?TtN@<{jSD+p?23KCSdjtuonisu)PS`TB+=^fff}C4~I_W6dY6 zF11TaU79b{Ey67n)IJ3HQwQu%c?Ls_g~=L7n0P_LgqgwlzYdcjlK}W+8Gq1yatsUs41wTQk12F++!r1)(Aj82 zKtShD5CYKVJoqdl(0xFl>m7D6C^ImCm(J~APzDv40u0Iwj75&D3<(U%3Gx`7tg&S!9ASkK_bz*OkS%y5Ch4P3&4mW}RUP!`z906JA>2Losx+yuHxp~1x>8f32Sq%XzeAYv*DVlXi=aq^4sit~Hh`*}0&v(S^( zGdI_h)wB3_U0zkxP{|N{fHeC3x80%%l#Gb3o3fCvL< z+yxX+is02kvJ70{1!|yKco<8IK?J&+5#$8W4U3?aGq4m1nM`4ZEnsBSV}zzpHFZWg zMl3hu+NcUKF)=C%I-6*Lng8w!I%8ao%hai86J}B*sL0P~qigv0fTB&PDTw#)h%RW2 z=l}n(dw_-5K<6ie&Km}mSIpc(YE17T=gED6%-4ga8U6O6!9x? zb28!LA)s;sDh@i2O&&a!05Kn7uQOadsQmm3SD(U&q#kr0GsIqo0+4!ed27hT$0Wd@ z%3#hg2ee%jw$fTnUsAq3w$;gXTMyu*h(`a3T+7$X%Ti&0Vz#E z2}UkZVTD|P!{c2I8qREt5uosdi|>JnGwUFWgU&65gg;{xvN&@qR6RShEWDgF`q^x*owF#cq&f}5w0B3=j=H$aFpDTB_-1iK4#J}NA|fYKc-9jP&f ze1V)dDi5l@{{Lr?2B-T@xOpZ>=COm$jf9$KiV$b~$y@_hZw?pNf%@|UDF1-`3H2YS zegxSM5l7?`ka`XwH71C75Ih_~>Tkit!$INYS55Atc?WP z@C91y2BJYN7Z5E7FU6qg6_Lc*K#eFi27~~#5rs(N&@>OOdm(#uz^%bMph6P7+fG>= zG*<{3BxE)hXIC{BXU_}>2zVA?Zha&))H92bDbzEIiRJH~7N(BBt4y4jHCHLmmsq7d z9~>sJ|1B7Qg321k08m&ll*WJ}gONez{}(13rVk934D&$SyHVHrLmNhjb{*8w2wxb2 zyk&^wEsTwqmLL_Dprs?AficKv-wp;t_yTEZ@VOqKJS@Us$e;vnR%pVH1pzg-Ky@Un z2@I+N#bg=5J6Of!89_B6s7nfIcbJ))sOdB6F`Js0nS;BGqJqk4US86sJ_ZSC`Zf-l zDiZF_(Shdv3dZ6#s=E5pnvyc68j?|dR`xaij0ap;LHC*0W!kFgs>tzhh&$T*daG#Y zDM+bl$r$i*aEizqsJeMEaRmCz{rdrQ?qcZwFHElBb2C3PFbJqK+x?FOudA|SSOE@m zum4{dAAsd`L1E5p$BR_HgYE?YrE^%m2c0Vp;tQ}bAj)r0eq?~Adp2fmaJpn>SizXe zxB-4XSre!})qrmJ2Q8!pB^$!$lZg;IpUe?-6NeONGY~7odj=_n{|r)~EkdB(M^X&( zAWk9aN0_~E%3LkVuC(Iqt zt{%c2(8>+$4pE3r7eLPE10QV(+2sN{;s&&5jvaggn1P|NIqWnI&_Z9x5jXsd#-hrb z3(cgZq~y6wxou4?ElpK@w|rw<8l|nzDl9E4C8lI-siEZ{YYlbha>i7~#Y_hnv=}Nt zjU#Oa9i%;=(B1*UozNl+>`sV#_&_&;fSa;>pysYWXj=}b85;m%6ge_87=Rf&83e)J z0v#p;If6=s0dl=A_^>T#-n3;jQPXAw?Nk7+cGM%GrBMC3)0uOWiDb|ZGGjwfkVNnRp7Dn zO^m6GYryB+1%uYUKt>o~u7d7iMz{(yPNW%xq1WMnjspO>4Ya!%vdkGhxIvq2)j?x& zh|_x*wTvwIba@qJ<&{-r#AP^4c`q#BWaU;+(6e)4Ox2QB5|dMsW|oo@;^f$CTq-7I zW@PLH4%fAeso=Z^I(r5GIX&224@nr5pK+&a<07u2BNnb;tR}_A#;vKWYwc<+tD+(+ ztD?e~>S3eeV5-F@rlFxD$ta>=VPNcJATP}=Ee|F@Yf3gTW-~4apK)goT9l1?#vR5A zb?8u!2yiWmaa)YBc{^=fKqFP-6D)IRi5TD6Ce4&$v@( zFom3PXU1R-&PYGQ zIqD9y(1ab;!`JQocwngpl9HHVi6>jz*BF{+pvh<%IMsj&@HrJL{{Ld!z!*x>S$ATf zqyz5LK=U$qN)p>ycaZZc$vNu|RH1?}s6qu{Xr;zYM02pmql@IIF{!kufT%se>1bLAxQ4 zqq0EbfQ;GVihRNnd;-#ZihQ~X$Q-5&es(S{K3)wGMK}Z0rfg*jX1okK=#@bM)E7tG z9}Ma>LEH{nF31Ww>>ISfMUK(f$PBdmgpZL?$Jjwa#zTW!frm$dTf;*}!htDR*T7Od zi-n1aB}?4WKo^t`b}*SSE@0jRS}Vw)%8&+Hz5qKZ6Erglo2>=!Hb$Dw1s}8kn$~q> zW&oWj0uD}p@I)^-q(M?U807Cjx;c>LVW7+jDq;)_L1(iwBkxWV;bTH=PcfQndfQ}5 zXv+%;$!kl%Xi$4wT3()6PpU-q-+V!NElEi&c|jO$C@UojJLf$acjmcA5(@;!YK}AkZML}G`NJFw(e3cRd1DW=V-!U*0R?=fau^%)r$Ea?h zF3EwgpIuztVwPK|A(N-1hLHr!elbH$X+|lfRpM~_s~ChCgLMWTvQjQrXVFQp=Xtj#a)I9;$kMUa^e|~_=#llWLVBz$RNz1%aB2= z`@pxfUf96^+J`H3XD5Rq11KNLGAJ^DrekV%Fvx9Z@(N1v%k$|eBXcnQyF-YBmz$4QMMM$K0JZo3CoqLFEMRVDU}FFs zxCdG%!4689{NVF^Ksg4yP6E1+610X2v~N`Af`K8ky16)krV88|@aOMnJDKnzGW25m3~ohuBY zx$c1aP8{Htn1P|NI_Rnfc6D}j^I3+WZkO_ON=vo!zza1>CdieS%T0j9dm@tu!+ho< z1~vvM220o~5qK#ASuV1J0W=5>DuO^oG3X>IHgN6Aeg`xx1WuabAe-zN(esL4fl_U) zLV=!|nW{LuzKDpntgN<(h(5cxsu|P8bfxfcrF01~MNJ`bSvesgIazTbO+_)VyZ-%Q za%EURFbqIv4S@WP8U|e8lOPNX8O_Dn&A~1+|M$l&)bI=>5Wp)$wBdok^gn^gmEi?* z9RoLm5Q8Vf4h8{8cniS78^i#eEeDz!5CWe(0a~&t1l|Qv1BxDr9Sq!eKr22#g%)T- zJa~BnQto7jxWY6Snjy=Wp-!oT)= zSy5C`9M7tm9!80OFEr5-C9|c=0<@$E=7Q2=B$F${a^@NaNd^UmY=#{Sa**(mgM|-> zv4cV3&JG6fnX_<*fyOG>z!{&D0aAU}fLDTps%l|y3#}Hs;v2HzLgoVKAR;;NS_4qY zVPFVang*^#L3K80p%}b22W^05S2y=S&hsb*z+7nf*1&T+oH4mgE z3Ud>KBtr)1kP;~d&~blgWgja8=)%nTpm7dR;{=pA84GFBCMkt`MiIF*Q{>ZQ=7Tt1 z16Fuxh$w>EB$Jqh7>_a6k?(%U;rsa8RFH_Gs7=Kz1aC^g86fvBU{+_`!JN&&%-{?P zfA}3(%%DXT7Z{is9)J%!0}nzogAyAn1L)u!kPLXKg#qXkokB>yGcXiZ6jfXRO4`iX zpnX9Mj0|s?`53dATNrp5%s_26#QJ?u`h~Oj|AG4mm>vSMPG#w@~kh`A1YpHnz!c@X3zf5>`r$b2ElM+oyBnHd&<8n=iP<;ctc zY4?Iolmk~e0-$@}A?x==K{FP3GW2<9UY-r5nMJ@^7mWaUn2BLC(-+3K%v%_k8Q4L) zjzJp-IKhQEXn7iB&l4zg;LY+2V0VI+6NBp%c~Eolu|cS-nL(&4(-*N-iu1)*DKaoJ z{Ac>iIG1@V12cm%D6HXe$$-T)NTZX{*d)}G>9fQtWl&n!&GeZu5~d$i#efdY2j4ve zZqbX{tIdaBisrGMuu*tzl>*@S22h)ScAe4aULYd zjvXLxf)Whe4tNrR#1ZH)VNhKRtAv?B`wPJ>W=0(ib5${RV^JO+a~lx;QWz+SOrt1?%0wXp~+KGs6z12aF3?;-USwM8f^IcyMxrchVeL!6!a~QY+Xq z^TE;-b=|D=J>BwP{(#X?|1jScE|mN?3+5RZjqEAL|7)0DGsH75XW(YA1=Wn8lS!e2 zJaTrCj?4`6Va?Kb2m|B=4hB$T6?94^tW{)S$c)q)3PCi3mc!aVpms(S(`SZdEc)O+ zqB6)fP|Sm(7M!VUlr_5c1ScWfQcl485DP8L48MzZY*;% z-fk>--9KgFO06vcEP{Bp1TcX5fA1Mt!10f*zkLB*u;J=&!}^+__=fiKkow!;7)9%E ze`1)=B7>v94M}p?+b261DC%!BR}<)Ozh_uLFbv4*Z-av$wZHwI;RTBZxWDZV?r($6 zoB&@p8?e22Di*Xj2#T19<|UNNS_&h((^l`Av2sQFo zH#T-QGWRfMj4{m&H3V^;Ow2w0t*`O$@o*1kQmFOtarcN|T<84nMSN{|jyag}PZeP1 z4>Wa8RM0lCwv2Pv@Pjk|9r~SQXB%P$CPA%DrhiHdI~eydb2D%-@GvldMq!xQm4%H# z`};*1FaQ0swIh0;cRv#g<54e0=P!(of7g0@gPN=V>=|A$9%P)&pw7U+2;Kk(8s`8l z{s+Y!WO|6+jGAJzG90brS_8YZV=JZDng0 z8)Y3~6UH6|OHX5C1vLd@<1jgCb!{;Pbp;4wVETLM|6(R@rn3xg48aTxj7FdXO2EO7 zw0K$&v@g$&$%P~`-s z1vLc|FH;3|@Yya*e?Ks!G6^sRGVn3Tf>Mtml&;HZM4KX(DghjkAdm$Wd;_;3ry_{d<>!t z42;I0i4aJSS>0TmlTnn>RG0DJtw^q~(m$7ZO>Xs>`A_y6qXXlre_@PC|K>5Z|N9md z?ta?p-VM`J?%^?v4F5hy{(B#H|2}AJ?eFjZ(-|)@+c7YMZUq)sXB1~eb-bDj<67rG zzdLKv7q=RpZ73-oJ;|o2V%( zn*O`(?ajD4CW?_$-qtA2GA8=pYerE^eN9tiZAJsjf3KON|41;uaEv@7t1QRsYGi#h z%FS6t@2ZOp7ni-`bzNm=up9r~`QOQC%j^O^Yk>i@K~9s=R8dgTRPo;(FE6IVf2NGp z|5_POGrRn8^ zoBdU1bYQe+U|{?QvZI^n83Q{510#65qOl-j_upJ^Z^ksQe>Kd0|3E!<2Bv>H|BD$_ znWMqCIe^PVB{orU5jJ)?M$i!+;B>DBUMy+~Itb0�+%AOBF%PkcVjg1u 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%0AfNbx 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

*vdm|Owc$na^nWvh=ne=0S7qfG6JRx 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ⓈBgC3 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 AaR2}S diff --git a/assets/fonts/plex-sans/ZedPlexSans-Italic.ttf b/assets/fonts/plex-sans/ZedPlexSans-Italic.ttf deleted file mode 100644 index 8769c232ee69236158ed03a1306ff7f2be4ebdd8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 213092 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ν}7g@a9&0Z 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

5`Kmm;vfae7u?g6;~ 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 YFfcHJTABB+or(m}4-KYofzZ`+0Lrf4dH?_b diff --git a/assets/fonts/plex-sans/ZedPlexSans-Regular.ttf b/assets/fonts/plex-sans/ZedPlexSans-Regular.ttf deleted file mode 100644 index 3ea293d59a31d2d80f0a1b35ef2c4507e7e4eec2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 205848 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Ι(Gs--R6vpivFbXo-QLxhbrb0j)^IK=miP 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

_7lwpbSo-DX=LazEbIUMua+~G$&k3|ESNp#X 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

{ ], }), MenuItem::separator(), + #[cfg(target_os = "macos")] MenuItem::os_submenu("Services", gpui::SystemMenuType::Services), MenuItem::separator(), MenuItem::action("Extensions", zed_actions::Extensions::default()), From a6e2e0d24a60ea852b9b5262cd16e7efd41ba524 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Wed, 13 Aug 2025 22:31:28 +0200 Subject: [PATCH 327/693] onboarding: Fix minimap typo on editing page (#36143) This PR fixes a small typo on the onboarding editing page where it should be "Minimap" instead of "Mini Map" Release Notes: - N/A --- crates/onboarding/src/editing_page.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index aa7f4eee74..d941a0315a 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -721,7 +721,7 @@ fn render_popular_settings_section( .items_start() .justify_between() .child( - v_flex().child(Label::new("Mini Map")).child( + v_flex().child(Label::new("Minimap")).child( Label::new("See a high-level overview of your source code.") .color(Color::Muted), ), From 1d2eaf210a15bc6c67de60696caaf08f4be9c1c6 Mon Sep 17 00:00:00 2001 From: smit Date: Thu, 14 Aug 2025 02:48:20 +0530 Subject: [PATCH 328/693] editor: Fix first `cmd-left` target for cursor in leading whitespace (#36145) Closes #35805 If the cursor is between column 0 and the indent size, pressing `cmd-left` jumps to the indent. Pressing it again moves to the true column 0. Further presses toggle between indent and column 0. This PR changes the first `cmd-left` to go to column 0 instead of indent. Toggling between is unaffected. Release Notes: - Fixed issue where pressing `cmd-left` with the cursor in the leading spaces moved to the start of the text first. It now goes to the beginning of the line first, then the start of the text. --- crates/editor/src/editor_tests.rs | 45 +++++++++++++++++++++++++++++++ crates/editor/src/movement.rs | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 0d2ecec8f2..4421869703 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -1901,6 +1901,51 @@ fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) { }); } +#[gpui::test] +fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let move_to_beg = MoveToBeginningOfLine { + stop_at_soft_wraps: true, + stop_at_indent: true, + }; + + let editor = cx.add_window(|window, cx| { + let buffer = MultiBuffer::build_simple(" hello\nworld", cx); + build_editor(buffer, window, cx) + }); + + _ = editor.update(cx, |editor, window, cx| { + // test cursor between line_start and indent_start + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3) + ]); + }); + + // cursor should move to line_start + editor.move_to_beginning_of_line(&move_to_beg, window, cx); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)] + ); + + // cursor should move to indent_start + editor.move_to_beginning_of_line(&move_to_beg, window, cx); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)] + ); + + // cursor should move to back to line_start + editor.move_to_beginning_of_line(&move_to_beg, window, cx); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)] + ); + }); +} + #[gpui::test] fn test_prev_next_word_boundary(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index a8850984a1..fdda0e82bc 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -230,7 +230,7 @@ pub fn indented_line_beginning( if stop_at_soft_boundaries && soft_line_start > indent_start && display_point != soft_line_start { soft_line_start - } else if stop_at_indent && display_point != indent_start { + } else if stop_at_indent && (display_point > indent_start || display_point == line_start) { indent_start } else { line_start From 8452532c8f0f0dc93afc8c75ebd1683c93e6118f Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 13 Aug 2025 18:34:53 -0300 Subject: [PATCH 329/693] agent2: Iterate on "new thread" selector in the toolbar (#36144) Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 61 +++++++++++------------------- 1 file changed, 22 insertions(+), 39 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 9aeb7867ac..e47cbe3714 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -67,8 +67,8 @@ use theme::ThemeSettings; use time::UtcOffset; use ui::utils::WithRemSize; use ui::{ - Banner, ButtonLike, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding, - PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*, + Banner, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding, PopoverMenu, + PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*, }; use util::ResultExt as _; use workspace::{ @@ -1996,9 +1996,7 @@ impl AgentPanel { PopoverMenu::new("agent-nav-menu") .trigger_with_tooltip( - IconButton::new("agent-nav-menu", icon) - .icon_size(IconSize::Small) - .style(ui::ButtonStyle::Subtle), + IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small), { let focus_handle = focus_handle.clone(); move |window, cx| { @@ -2135,9 +2133,10 @@ impl AgentPanel { .pl_1() .gap_1() .child(match &self.active_view { - ActiveView::History | ActiveView::Configuration => { - self.render_toolbar_back_button(cx).into_any_element() - } + ActiveView::History | ActiveView::Configuration => div() + .pl(DynamicSpacing::Base04.rems(cx)) + .child(self.render_toolbar_back_button(cx)) + .into_any_element(), _ => self .render_recent_entries_menu(IconName::MenuAlt, cx) .into_any_element(), @@ -2175,33 +2174,7 @@ impl AgentPanel { let new_thread_menu = PopoverMenu::new("new_thread_menu") .trigger_with_tooltip( - ButtonLike::new("new_thread_menu_btn").child( - h_flex() - .group("agent-selector") - .gap_1p5() - .child( - h_flex() - .relative() - .size_4() - .justify_center() - .child( - h_flex() - .group_hover("agent-selector", |s| s.invisible()) - .child( - Icon::new(self.selected_agent.icon()) - .color(Color::Muted), - ), - ) - .child( - h_flex() - .absolute() - .invisible() - .group_hover("agent-selector", |s| s.visible()) - .child(Icon::new(IconName::Plus)), - ), - ) - .child(Label::new(self.selected_agent.label())), - ), + IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small), { let focus_handle = focus_handle.clone(); move |window, cx| { @@ -2419,15 +2392,24 @@ impl AgentPanel { .size_full() .gap(DynamicSpacing::Base08.rems(cx)) .child(match &self.active_view { - ActiveView::History | ActiveView::Configuration => { - self.render_toolbar_back_button(cx).into_any_element() - } + ActiveView::History | ActiveView::Configuration => div() + .pl(DynamicSpacing::Base04.rems(cx)) + .child(self.render_toolbar_back_button(cx)) + .into_any_element(), _ => h_flex() .h_full() .px(DynamicSpacing::Base04.rems(cx)) .border_r_1() .border_color(cx.theme().colors().border) - .child(new_thread_menu) + .child( + h_flex() + .px_0p5() + .gap_1p5() + .child( + Icon::new(self.selected_agent.icon()).color(Color::Muted), + ) + .child(Label::new(self.selected_agent.label())), + ) .into_any_element(), }) .child(self.render_title_view(window, cx)), @@ -2445,6 +2427,7 @@ impl AgentPanel { .pr(DynamicSpacing::Base06.rems(cx)) .border_l_1() .border_color(cx.theme().colors().border) + .child(new_thread_menu) .child(self.render_recent_entries_menu(IconName::HistoryRerun, cx)) .child(self.render_panel_options_menu(window, cx)), ), From 09e90fb023cc136ad2a2fdefc692f6270345544a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 13 Aug 2025 14:45:34 -0700 Subject: [PATCH 330/693] Use trace log level for potentially high-volume vsync duration log (#36147) This is an attempt to fix https://github.com/zed-industries/zed/issues/36125 Release Notes: - N/A --- crates/gpui/src/platform/windows/vsync.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gpui/src/platform/windows/vsync.rs b/crates/gpui/src/platform/windows/vsync.rs index 09dbfd0231..6d09b0960f 100644 --- a/crates/gpui/src/platform/windows/vsync.rs +++ b/crates/gpui/src/platform/windows/vsync.rs @@ -99,7 +99,7 @@ impl VSyncProvider { // operation for the first call after the vsync thread becomes non-idle, // but it shouldn't happen often. if !wait_succeeded || elapsed < VSYNC_INTERVAL_THRESHOLD { - log::warn!("VSyncProvider::wait_for_vsync() took shorter than expected"); + log::trace!("VSyncProvider::wait_for_vsync() took less time than expected"); std::thread::sleep(self.interval); } } From 665006c4144c35f8673bd342c56e8d39df8b9e17 Mon Sep 17 00:00:00 2001 From: Aleksei Gusev Date: Thu, 14 Aug 2025 00:45:50 +0300 Subject: [PATCH 331/693] Move the cursor on search in Terminal if ViMode is active (#33305) Currently, the terminal search function doesn't work well with ViMode. It matches the search terms, scrolls the active match in the view, but it doesn't move the cursor to the match, which makes it useless for navigating the scrollback in vimode. With this improvement, if a user activates ViMode before the search Zed moves the cursor to the active search terms. So, when the search dialog is dismissed the cursor is places on the latest active search term and it's possible to navigate the scrollback via ViMode using this place as the starting point. https://github.com/user-attachments/assets/63325405-ed93-4bf8-a00f-28ded5511f31 Release Notes: - Improved the search function in the terminal when ViMode is activated --- crates/terminal/src/terminal.rs | 20 +++++++++++++++++--- crates/terminal_view/src/terminal_view.rs | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index c3c6de9e53..86728cc11c 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -167,6 +167,7 @@ enum InternalEvent { // Vi mode events ToggleViMode, ViMotion(ViMotion), + MoveViCursorToAlacPoint(AlacPoint), } ///A translation struct for Alacritty to communicate with us from their event loop @@ -972,6 +973,10 @@ impl Terminal { term.scroll_to_point(*point); self.refresh_hovered_word(window); } + InternalEvent::MoveViCursorToAlacPoint(point) => { + term.vi_goto_point(*point); + self.refresh_hovered_word(window); + } InternalEvent::ToggleViMode => { self.vi_mode_enabled = !self.vi_mode_enabled; term.toggle_vi_mode(); @@ -1100,12 +1105,21 @@ impl Terminal { pub fn activate_match(&mut self, index: usize) { if let Some(search_match) = self.matches.get(index).cloned() { self.set_selection(Some((make_selection(&search_match), *search_match.end()))); - - self.events - .push_back(InternalEvent::ScrollToAlacPoint(*search_match.start())); + if self.vi_mode_enabled { + self.events + .push_back(InternalEvent::MoveViCursorToAlacPoint(*search_match.end())); + } else { + self.events + .push_back(InternalEvent::ScrollToAlacPoint(*search_match.start())); + } } } + pub fn clear_matches(&mut self) { + self.matches.clear(); + self.set_selection(None); + } + pub fn select_matches(&mut self, matches: &[RangeInclusive]) { let matches_to_select = self .matches diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 0ec5f816d5..219238496c 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1869,7 +1869,7 @@ impl SearchableItem for TerminalView { /// Clear stored matches fn clear_matches(&mut self, _window: &mut Window, cx: &mut Context) { - self.terminal().update(cx, |term, _| term.matches.clear()) + self.terminal().update(cx, |term, _| term.clear_matches()) } /// Store matches returned from find_matches somewhere for rendering From 293992f5b1f4456a6493dec9e315943aad3f7054 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 13 Aug 2025 15:01:00 -0700 Subject: [PATCH 332/693] In auto-update-helper, fix parsing of `--launch false` (#36148) This fixes an issue introduced in https://github.com/zed-industries/zed/pull/34303 where, after an auto-update was downloaded, quitting Zed would always restart Zed. Release Notes: - N/A --- .../src/auto_update_helper.rs | 110 +++++++----------- 1 file changed, 41 insertions(+), 69 deletions(-) diff --git a/crates/auto_update_helper/src/auto_update_helper.rs b/crates/auto_update_helper/src/auto_update_helper.rs index 2781176028..3aa57094d3 100644 --- a/crates/auto_update_helper/src/auto_update_helper.rs +++ b/crates/auto_update_helper/src/auto_update_helper.rs @@ -18,7 +18,7 @@ fn main() {} #[cfg(target_os = "windows")] mod windows_impl { - use std::path::Path; + use std::{borrow::Cow, path::Path}; use super::dialog::create_dialog_window; use super::updater::perform_update; @@ -37,9 +37,9 @@ mod windows_impl { pub(crate) const WM_JOB_UPDATED: u32 = WM_USER + 1; pub(crate) const WM_TERMINATE: u32 = WM_USER + 2; - #[derive(Debug)] + #[derive(Debug, Default)] struct Args { - launch: Option, + launch: bool, } pub(crate) fn run() -> Result<()> { @@ -56,9 +56,9 @@ mod windows_impl { log::info!("======= Starting Zed update ======="); let (tx, rx) = std::sync::mpsc::channel(); let hwnd = create_dialog_window(rx)?.0 as isize; - let args = parse_args(); + let args = parse_args(std::env::args().skip(1)); std::thread::spawn(move || { - let result = perform_update(app_dir.as_path(), Some(hwnd), args.launch.unwrap_or(true)); + let result = perform_update(app_dir.as_path(), Some(hwnd), args.launch); tx.send(result).ok(); unsafe { PostMessageW(Some(HWND(hwnd as _)), WM_TERMINATE, WPARAM(0), LPARAM(0)) }.ok(); }); @@ -83,39 +83,27 @@ mod windows_impl { Ok(()) } - fn parse_args() -> Args { - let mut result = Args { launch: None }; - if let Some(candidate) = std::env::args().nth(1) { - parse_single_arg(&candidate, &mut result); + fn parse_args(input: impl IntoIterator) -> Args { + let mut args: Args = Args { launch: true }; + + let mut input = input.into_iter(); + if let Some(arg) = input.next() { + let launch_arg; + + if arg == "--launch" { + launch_arg = input.next().map(Cow::Owned); + } else if let Some(rest) = arg.strip_prefix("--launch=") { + launch_arg = Some(Cow::Borrowed(rest)); + } else { + launch_arg = None; + } + + if launch_arg.as_deref() == Some("false") { + args.launch = false; + } } - result - } - - fn parse_single_arg(arg: &str, result: &mut Args) { - let Some((key, value)) = arg.strip_prefix("--").and_then(|arg| arg.split_once('=')) else { - log::error!( - "Invalid argument format: '{}'. Expected format: --key=value", - arg - ); - return; - }; - - match key { - "launch" => parse_launch_arg(value, &mut result.launch), - _ => log::error!("Unknown argument: --{}", key), - } - } - - fn parse_launch_arg(value: &str, arg: &mut Option) { - match value { - "true" => *arg = Some(true), - "false" => *arg = Some(false), - _ => log::error!( - "Invalid value for --launch: '{}'. Expected 'true' or 'false'", - value - ), - } + args } pub(crate) fn show_error(mut content: String) { @@ -135,44 +123,28 @@ mod windows_impl { #[cfg(test)] mod tests { - use crate::windows_impl::{Args, parse_launch_arg, parse_single_arg}; + use crate::windows_impl::parse_args; #[test] - fn test_parse_launch_arg() { - let mut arg = None; - parse_launch_arg("true", &mut arg); - assert_eq!(arg, Some(true)); + fn test_parse_args() { + // launch can be specified via two separate arguments + assert_eq!(parse_args(["--launch".into(), "true".into()]).launch, true); + assert_eq!( + parse_args(["--launch".into(), "false".into()]).launch, + false + ); - let mut arg = None; - parse_launch_arg("false", &mut arg); - assert_eq!(arg, Some(false)); + // launch can be specified via one single argument + assert_eq!(parse_args(["--launch=true".into()]).launch, true); + assert_eq!(parse_args(["--launch=false".into()]).launch, false); - let mut arg = None; - parse_launch_arg("invalid", &mut arg); - assert_eq!(arg, None); - } + // launch defaults to true on no arguments + assert_eq!(parse_args([]).launch, true); - #[test] - fn test_parse_single_arg() { - let mut args = Args { launch: None }; - parse_single_arg("--launch=true", &mut args); - assert_eq!(args.launch, Some(true)); - - let mut args = Args { launch: None }; - parse_single_arg("--launch=false", &mut args); - assert_eq!(args.launch, Some(false)); - - let mut args = Args { launch: None }; - parse_single_arg("--launch=invalid", &mut args); - assert_eq!(args.launch, None); - - let mut args = Args { launch: None }; - parse_single_arg("--launch", &mut args); - assert_eq!(args.launch, None); - - let mut args = Args { launch: None }; - parse_single_arg("--unknown", &mut args); - assert_eq!(args.launch, None); + // launch defaults to true on invalid arguments + assert_eq!(parse_args(["--launch".into()]).launch, true); + assert_eq!(parse_args(["--launch=".into()]).launch, true); + assert_eq!(parse_args(["--launch=invalid".into()]).launch, true); } } } From e67b2da20c387fb2a82fbf91c767279a2c6bad79 Mon Sep 17 00:00:00 2001 From: Tom Planche Date: Thu, 14 Aug 2025 00:07:49 +0200 Subject: [PATCH 333/693] Make alphabetical sorting the default (#32315) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow up of this pr: #25148 Release Notes: - Improved file sorting. As described in #20126, I was fed up with lexicographical file sorting in the project panel. The current sorting behavior doesn't handle numeric segments properly, leading to unintuitive ordering like `file_1.rs`, `file_10.rs`, `file_2.rs`. ## Example Sorting Results Using `lexicographical` (default): ``` . ├── file_01.rs ├── file_1.rs ├── file_10.rs ├── file_1025.rs ├── file_2.rs ``` Using alphabetical (natural) sorting: ``` . ├── file_1.rs ├── file_01.rs ├── file_2.rs ├── file_10.rs ├── file_1025.rs ``` --- .../project_panel/src/project_panel_tests.rs | 32 +- crates/util/src/paths.rs | 523 +++++++++++++++++- 2 files changed, 520 insertions(+), 35 deletions(-) diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index 6c62c8db93..de3316e357 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -740,9 +740,9 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) { " > .git", " > a", " v b", + " > [EDITOR: ''] <== selected", " > 3", " > 4", - " > [EDITOR: ''] <== selected", " a-different-filename.tar.gz", " > C", " .dockerignore", @@ -765,10 +765,10 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) { " > .git", " > a", " v b", - " > 3", - " > 4", " > [PROCESSING: 'new-dir']", - " a-different-filename.tar.gz <== selected", + " > 3 <== selected", + " > 4", + " a-different-filename.tar.gz", " > C", " .dockerignore", ] @@ -782,10 +782,10 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) { " > .git", " > a", " v b", - " > 3", + " > 3 <== selected", " > 4", " > new-dir", - " a-different-filename.tar.gz <== selected", + " a-different-filename.tar.gz", " > C", " .dockerignore", ] @@ -801,10 +801,10 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) { " > .git", " > a", " v b", - " > 3", + " > [EDITOR: '3'] <== selected", " > 4", " > new-dir", - " [EDITOR: 'a-different-filename.tar.gz'] <== selected", + " a-different-filename.tar.gz", " > C", " .dockerignore", ] @@ -819,10 +819,10 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) { " > .git", " > a", " v b", - " > 3", + " > 3 <== selected", " > 4", " > new-dir", - " a-different-filename.tar.gz <== selected", + " a-different-filename.tar.gz", " > C", " .dockerignore", ] @@ -837,12 +837,12 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) { " > .git", " > a", " v b", - " > 3", + " v 3", + " [EDITOR: ''] <== selected", + " Q", " > 4", " > new-dir", - " [EDITOR: ''] <== selected", " a-different-filename.tar.gz", - " > C", ] ); panel.update_in(cx, |panel, window, cx| { @@ -863,12 +863,12 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) { " > .git", " > a", " v b", - " > 3", + " v 3 <== selected", + " Q", " > 4", " > new-dir", - " a-different-filename.tar.gz <== selected", + " a-different-filename.tar.gz", " > C", - " .dockerignore", ] ); } diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 585f2b08aa..211831125d 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -1,4 +1,7 @@ -use std::cmp; +use globset::{Glob, GlobSet, GlobSetBuilder}; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; use std::path::StripPrefixError; use std::sync::{Arc, OnceLock}; use std::{ @@ -7,12 +10,6 @@ use std::{ sync::LazyLock, }; -use globset::{Glob, GlobSet, GlobSetBuilder}; -use regex::Regex; -use serde::{Deserialize, Serialize}; - -use crate::NumericPrefixWithSuffix; - /// Returns the path to the user's home directory. pub fn home_dir() -> &'static PathBuf { static HOME_DIR: OnceLock = OnceLock::new(); @@ -545,17 +542,172 @@ impl PathMatcher { } } +/// Custom character comparison that prioritizes lowercase for same letters +fn compare_chars(a: char, b: char) -> Ordering { + // First compare case-insensitive + match a.to_ascii_lowercase().cmp(&b.to_ascii_lowercase()) { + Ordering::Equal => { + // If same letter, prioritize lowercase (lowercase < uppercase) + match (a.is_ascii_lowercase(), b.is_ascii_lowercase()) { + (true, false) => Ordering::Less, // lowercase comes first + (false, true) => Ordering::Greater, // uppercase comes after + _ => Ordering::Equal, // both same case or both non-ascii + } + } + other => other, + } +} + +/// Compares two sequences of consecutive digits for natural sorting. +/// +/// This function is a core component of natural sorting that handles numeric comparison +/// in a way that feels natural to humans. It extracts and compares consecutive digit +/// sequences from two iterators, handling various cases like leading zeros and very large numbers. +/// +/// # Behavior +/// +/// The function implements the following comparison rules: +/// 1. Different numeric values: Compares by actual numeric value (e.g., "2" < "10") +/// 2. Leading zeros: When values are equal, longer sequence wins (e.g., "002" > "2") +/// 3. Large numbers: Falls back to string comparison for numbers that would overflow u128 +/// +/// # Examples +/// +/// ```text +/// "1" vs "2" -> Less (different values) +/// "2" vs "10" -> Less (numeric comparison) +/// "002" vs "2" -> Greater (leading zeros) +/// "10" vs "010" -> Less (leading zeros) +/// "999..." vs "1000..." -> Less (large number comparison) +/// ``` +/// +/// # Implementation Details +/// +/// 1. Extracts consecutive digits into strings +/// 2. Compares sequence lengths for leading zero handling +/// 3. For equal lengths, compares digit by digit +/// 4. For different lengths: +/// - Attempts numeric comparison first (for numbers up to 2^128 - 1) +/// - Falls back to string comparison if numbers would overflow +/// +/// The function advances both iterators past their respective numeric sequences, +/// regardless of the comparison result. +fn compare_numeric_segments( + a_iter: &mut std::iter::Peekable, + b_iter: &mut std::iter::Peekable, +) -> Ordering +where + I: Iterator, +{ + // Collect all consecutive digits into strings + let mut a_num_str = String::new(); + let mut b_num_str = String::new(); + + while let Some(&c) = a_iter.peek() { + if !c.is_ascii_digit() { + break; + } + + a_num_str.push(c); + a_iter.next(); + } + + while let Some(&c) = b_iter.peek() { + if !c.is_ascii_digit() { + break; + } + + b_num_str.push(c); + b_iter.next(); + } + + // First compare lengths (handle leading zeros) + match a_num_str.len().cmp(&b_num_str.len()) { + Ordering::Equal => { + // Same length, compare digit by digit + match a_num_str.cmp(&b_num_str) { + Ordering::Equal => Ordering::Equal, + ordering => ordering, + } + } + + // Different lengths but same value means leading zeros + ordering => { + // Try parsing as numbers first + if let (Ok(a_val), Ok(b_val)) = (a_num_str.parse::(), b_num_str.parse::()) { + match a_val.cmp(&b_val) { + Ordering::Equal => ordering, // Same value, longer one is greater (leading zeros) + ord => ord, + } + } else { + // If parsing fails (overflow), compare as strings + a_num_str.cmp(&b_num_str) + } + } + } +} + +/// Performs natural sorting comparison between two strings. +/// +/// Natural sorting is an ordering that handles numeric sequences in a way that matches human expectations. +/// For example, "file2" comes before "file10" (unlike standard lexicographic sorting). +/// +/// # Characteristics +/// +/// * Case-sensitive with lowercase priority: When comparing same letters, lowercase comes before uppercase +/// * Numbers are compared by numeric value, not character by character +/// * Leading zeros affect ordering when numeric values are equal +/// * Can handle numbers larger than u128::MAX (falls back to string comparison) +/// +/// # Algorithm +/// +/// The function works by: +/// 1. Processing strings character by character +/// 2. When encountering digits, treating consecutive digits as a single number +/// 3. Comparing numbers by their numeric value rather than lexicographically +/// 4. For non-numeric characters, using case-sensitive comparison with lowercase priority +fn natural_sort(a: &str, b: &str) -> Ordering { + let mut a_iter = a.chars().peekable(); + let mut b_iter = b.chars().peekable(); + + loop { + match (a_iter.peek(), b_iter.peek()) { + (None, None) => return Ordering::Equal, + (None, _) => return Ordering::Less, + (_, None) => return Ordering::Greater, + (Some(&a_char), Some(&b_char)) => { + if a_char.is_ascii_digit() && b_char.is_ascii_digit() { + match compare_numeric_segments(&mut a_iter, &mut b_iter) { + Ordering::Equal => continue, + ordering => return ordering, + } + } else { + match compare_chars(a_char, b_char) { + Ordering::Equal => { + a_iter.next(); + b_iter.next(); + } + ordering => return ordering, + } + } + } + } + } +} + pub fn compare_paths( (path_a, a_is_file): (&Path, bool), (path_b, b_is_file): (&Path, bool), -) -> cmp::Ordering { +) -> Ordering { let mut components_a = path_a.components().peekable(); let mut components_b = path_b.components().peekable(); + loop { match (components_a.next(), components_b.next()) { (Some(component_a), Some(component_b)) => { let a_is_file = components_a.peek().is_none() && a_is_file; let b_is_file = components_b.peek().is_none() && b_is_file; + let ordering = a_is_file.cmp(&b_is_file).then_with(|| { let path_a = Path::new(component_a.as_os_str()); let path_string_a = if a_is_file { @@ -564,9 +716,6 @@ pub fn compare_paths( path_a.file_name() } .map(|s| s.to_string_lossy()); - let num_and_remainder_a = path_string_a - .as_deref() - .map(NumericPrefixWithSuffix::from_numeric_prefixed_str); let path_b = Path::new(component_b.as_os_str()); let path_string_b = if b_is_file { @@ -575,27 +724,32 @@ pub fn compare_paths( path_b.file_name() } .map(|s| s.to_string_lossy()); - let num_and_remainder_b = path_string_b - .as_deref() - .map(NumericPrefixWithSuffix::from_numeric_prefixed_str); - num_and_remainder_a.cmp(&num_and_remainder_b).then_with(|| { + let compare_components = match (path_string_a, path_string_b) { + (Some(a), Some(b)) => natural_sort(&a, &b), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + }; + + compare_components.then_with(|| { if a_is_file && b_is_file { let ext_a = path_a.extension().unwrap_or_default(); let ext_b = path_b.extension().unwrap_or_default(); ext_a.cmp(ext_b) } else { - cmp::Ordering::Equal + Ordering::Equal } }) }); + if !ordering.is_eq() { return ordering; } } - (Some(_), None) => break cmp::Ordering::Greater, - (None, Some(_)) => break cmp::Ordering::Less, - (None, None) => break cmp::Ordering::Equal, + (Some(_), None) => break Ordering::Greater, + (None, Some(_)) => break Ordering::Less, + (None, None) => break Ordering::Equal, } } } @@ -1049,4 +1203,335 @@ mod tests { "C:\\Users\\someone\\test_file.rs" ); } + + #[test] + fn test_compare_numeric_segments() { + // Helper function to create peekable iterators and test + fn compare(a: &str, b: &str) -> Ordering { + let mut a_iter = a.chars().peekable(); + let mut b_iter = b.chars().peekable(); + + let result = compare_numeric_segments(&mut a_iter, &mut b_iter); + + // Verify iterators advanced correctly + assert!( + !a_iter.next().map_or(false, |c| c.is_ascii_digit()), + "Iterator a should have consumed all digits" + ); + assert!( + !b_iter.next().map_or(false, |c| c.is_ascii_digit()), + "Iterator b should have consumed all digits" + ); + + result + } + + // Basic numeric comparisons + assert_eq!(compare("0", "0"), Ordering::Equal); + assert_eq!(compare("1", "2"), Ordering::Less); + assert_eq!(compare("9", "10"), Ordering::Less); + assert_eq!(compare("10", "9"), Ordering::Greater); + assert_eq!(compare("99", "100"), Ordering::Less); + + // Leading zeros + assert_eq!(compare("0", "00"), Ordering::Less); + assert_eq!(compare("00", "0"), Ordering::Greater); + assert_eq!(compare("01", "1"), Ordering::Greater); + assert_eq!(compare("001", "1"), Ordering::Greater); + assert_eq!(compare("001", "01"), Ordering::Greater); + + // Same value different representation + assert_eq!(compare("000100", "100"), Ordering::Greater); + assert_eq!(compare("100", "0100"), Ordering::Less); + assert_eq!(compare("0100", "00100"), Ordering::Less); + + // Large numbers + assert_eq!(compare("9999999999", "10000000000"), Ordering::Less); + assert_eq!( + compare( + "340282366920938463463374607431768211455", // u128::MAX + "340282366920938463463374607431768211456" + ), + Ordering::Less + ); + assert_eq!( + compare( + "340282366920938463463374607431768211456", // > u128::MAX + "340282366920938463463374607431768211455" + ), + Ordering::Greater + ); + + // Iterator advancement verification + let mut a_iter = "123abc".chars().peekable(); + let mut b_iter = "456def".chars().peekable(); + + compare_numeric_segments(&mut a_iter, &mut b_iter); + + assert_eq!(a_iter.collect::(), "abc"); + assert_eq!(b_iter.collect::(), "def"); + } + + #[test] + fn test_natural_sort() { + // Basic alphanumeric + assert_eq!(natural_sort("a", "b"), Ordering::Less); + assert_eq!(natural_sort("b", "a"), Ordering::Greater); + assert_eq!(natural_sort("a", "a"), Ordering::Equal); + + // Case sensitivity + assert_eq!(natural_sort("a", "A"), Ordering::Less); + assert_eq!(natural_sort("A", "a"), Ordering::Greater); + assert_eq!(natural_sort("aA", "aa"), Ordering::Greater); + assert_eq!(natural_sort("aa", "aA"), Ordering::Less); + + // Numbers + assert_eq!(natural_sort("1", "2"), Ordering::Less); + assert_eq!(natural_sort("2", "10"), Ordering::Less); + assert_eq!(natural_sort("02", "10"), Ordering::Less); + assert_eq!(natural_sort("02", "2"), Ordering::Greater); + + // Mixed alphanumeric + assert_eq!(natural_sort("a1", "a2"), Ordering::Less); + assert_eq!(natural_sort("a2", "a10"), Ordering::Less); + assert_eq!(natural_sort("a02", "a2"), Ordering::Greater); + assert_eq!(natural_sort("a1b", "a1c"), Ordering::Less); + + // Multiple numeric segments + assert_eq!(natural_sort("1a2", "1a10"), Ordering::Less); + assert_eq!(natural_sort("1a10", "1a2"), Ordering::Greater); + assert_eq!(natural_sort("2a1", "10a1"), Ordering::Less); + + // Special characters + assert_eq!(natural_sort("a-1", "a-2"), Ordering::Less); + assert_eq!(natural_sort("a_1", "a_2"), Ordering::Less); + assert_eq!(natural_sort("a.1", "a.2"), Ordering::Less); + + // Unicode + assert_eq!(natural_sort("文1", "文2"), Ordering::Less); + assert_eq!(natural_sort("文2", "文10"), Ordering::Less); + assert_eq!(natural_sort("🔤1", "🔤2"), Ordering::Less); + + // Empty and special cases + assert_eq!(natural_sort("", ""), Ordering::Equal); + assert_eq!(natural_sort("", "a"), Ordering::Less); + assert_eq!(natural_sort("a", ""), Ordering::Greater); + assert_eq!(natural_sort(" ", " "), Ordering::Less); + + // Mixed everything + assert_eq!(natural_sort("File-1.txt", "File-2.txt"), Ordering::Less); + assert_eq!(natural_sort("File-02.txt", "File-2.txt"), Ordering::Greater); + assert_eq!(natural_sort("File-2.txt", "File-10.txt"), Ordering::Less); + assert_eq!(natural_sort("File_A1", "File_A2"), Ordering::Less); + assert_eq!(natural_sort("File_a1", "File_A1"), Ordering::Less); + } + + #[test] + fn test_compare_paths() { + // Helper function for cleaner tests + fn compare(a: &str, is_a_file: bool, b: &str, is_b_file: bool) -> Ordering { + compare_paths((Path::new(a), is_a_file), (Path::new(b), is_b_file)) + } + + // Basic path comparison + assert_eq!(compare("a", true, "b", true), Ordering::Less); + assert_eq!(compare("b", true, "a", true), Ordering::Greater); + assert_eq!(compare("a", true, "a", true), Ordering::Equal); + + // Files vs Directories + assert_eq!(compare("a", true, "a", false), Ordering::Greater); + assert_eq!(compare("a", false, "a", true), Ordering::Less); + assert_eq!(compare("b", false, "a", true), Ordering::Less); + + // Extensions + assert_eq!(compare("a.txt", true, "a.md", true), Ordering::Greater); + assert_eq!(compare("a.md", true, "a.txt", true), Ordering::Less); + assert_eq!(compare("a", true, "a.txt", true), Ordering::Less); + + // Nested paths + assert_eq!(compare("dir/a", true, "dir/b", true), Ordering::Less); + assert_eq!(compare("dir1/a", true, "dir2/a", true), Ordering::Less); + assert_eq!(compare("dir/sub/a", true, "dir/a", true), Ordering::Less); + + // Case sensitivity in paths + assert_eq!( + compare("Dir/file", true, "dir/file", true), + Ordering::Greater + ); + assert_eq!( + compare("dir/File", true, "dir/file", true), + Ordering::Greater + ); + assert_eq!(compare("dir/file", true, "Dir/File", true), Ordering::Less); + + // Hidden files and special names + assert_eq!(compare(".hidden", true, "visible", true), Ordering::Less); + assert_eq!(compare("_special", true, "normal", true), Ordering::Less); + assert_eq!(compare(".config", false, ".data", false), Ordering::Less); + + // Mixed numeric paths + assert_eq!( + compare("dir1/file", true, "dir2/file", true), + Ordering::Less + ); + assert_eq!( + compare("dir2/file", true, "dir10/file", true), + Ordering::Less + ); + assert_eq!( + compare("dir02/file", true, "dir2/file", true), + Ordering::Greater + ); + + // Root paths + assert_eq!(compare("/a", true, "/b", true), Ordering::Less); + assert_eq!(compare("/", false, "/a", true), Ordering::Less); + + // Complex real-world examples + assert_eq!( + compare("project/src/main.rs", true, "project/src/lib.rs", true), + Ordering::Greater + ); + assert_eq!( + compare( + "project/tests/test_1.rs", + true, + "project/tests/test_2.rs", + true + ), + Ordering::Less + ); + assert_eq!( + compare( + "project/v1.0.0/README.md", + true, + "project/v1.10.0/README.md", + true + ), + Ordering::Less + ); + } + + #[test] + fn test_natural_sort_case_sensitivity() { + // Same letter different case - lowercase should come first + assert_eq!(natural_sort("a", "A"), Ordering::Less); + assert_eq!(natural_sort("A", "a"), Ordering::Greater); + assert_eq!(natural_sort("a", "a"), Ordering::Equal); + assert_eq!(natural_sort("A", "A"), Ordering::Equal); + + // Mixed case strings + assert_eq!(natural_sort("aaa", "AAA"), Ordering::Less); + assert_eq!(natural_sort("AAA", "aaa"), Ordering::Greater); + assert_eq!(natural_sort("aAa", "AaA"), Ordering::Less); + + // Different letters + assert_eq!(natural_sort("a", "b"), Ordering::Less); + assert_eq!(natural_sort("A", "b"), Ordering::Less); + assert_eq!(natural_sort("a", "B"), Ordering::Less); + } + + #[test] + fn test_natural_sort_with_numbers() { + // Basic number ordering + assert_eq!(natural_sort("file1", "file2"), Ordering::Less); + assert_eq!(natural_sort("file2", "file10"), Ordering::Less); + assert_eq!(natural_sort("file10", "file2"), Ordering::Greater); + + // Numbers in different positions + assert_eq!(natural_sort("1file", "2file"), Ordering::Less); + assert_eq!(natural_sort("file1text", "file2text"), Ordering::Less); + assert_eq!(natural_sort("text1file", "text2file"), Ordering::Less); + + // Multiple numbers in string + assert_eq!(natural_sort("file1-2", "file1-10"), Ordering::Less); + assert_eq!(natural_sort("2-1file", "10-1file"), Ordering::Less); + + // Leading zeros + assert_eq!(natural_sort("file002", "file2"), Ordering::Greater); + assert_eq!(natural_sort("file002", "file10"), Ordering::Less); + + // Very large numbers + assert_eq!( + natural_sort("file999999999999999999999", "file999999999999999999998"), + Ordering::Greater + ); + + // u128 edge cases + + // Numbers near u128::MAX (340,282,366,920,938,463,463,374,607,431,768,211,455) + assert_eq!( + natural_sort( + "file340282366920938463463374607431768211454", + "file340282366920938463463374607431768211455" + ), + Ordering::Less + ); + + // Equal length numbers that overflow u128 + assert_eq!( + natural_sort( + "file340282366920938463463374607431768211456", + "file340282366920938463463374607431768211455" + ), + Ordering::Greater + ); + + // Different length numbers that overflow u128 + assert_eq!( + natural_sort( + "file3402823669209384634633746074317682114560", + "file340282366920938463463374607431768211455" + ), + Ordering::Greater + ); + + // Leading zeros with numbers near u128::MAX + assert_eq!( + natural_sort( + "file0340282366920938463463374607431768211455", + "file340282366920938463463374607431768211455" + ), + Ordering::Greater + ); + + // Very large numbers with different lengths (both overflow u128) + assert_eq!( + natural_sort( + "file999999999999999999999999999999999999999999999999", + "file9999999999999999999999999999999999999999999999999" + ), + Ordering::Less + ); + + // Mixed case with numbers + assert_eq!(natural_sort("File1", "file2"), Ordering::Greater); + assert_eq!(natural_sort("file1", "File2"), Ordering::Less); + } + + #[test] + fn test_natural_sort_edge_cases() { + // Empty strings + assert_eq!(natural_sort("", ""), Ordering::Equal); + assert_eq!(natural_sort("", "a"), Ordering::Less); + assert_eq!(natural_sort("a", ""), Ordering::Greater); + + // Special characters + assert_eq!(natural_sort("file-1", "file_1"), Ordering::Less); + assert_eq!(natural_sort("file.1", "file_1"), Ordering::Less); + assert_eq!(natural_sort("file 1", "file_1"), Ordering::Less); + + // Unicode characters + // 9312 vs 9313 + assert_eq!(natural_sort("file①", "file②"), Ordering::Less); + // 9321 vs 9313 + assert_eq!(natural_sort("file⑩", "file②"), Ordering::Greater); + // 28450 vs 23383 + assert_eq!(natural_sort("file漢", "file字"), Ordering::Greater); + + // Mixed alphanumeric with special chars + assert_eq!(natural_sort("file-1a", "file-1b"), Ordering::Less); + assert_eq!(natural_sort("file-1.2", "file-1.10"), Ordering::Less); + assert_eq!(natural_sort("file-1.10", "file-1.2"), Ordering::Greater); + } } From 32f9de612449d4aee68e28ffe1373e26a84516ca Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 13 Aug 2025 17:01:17 -0700 Subject: [PATCH 334/693] Add grid support to GPUI (#36153) Release Notes: - N/A --------- Co-authored-by: Anthony --- crates/gpui/Cargo.toml | 4 + crates/gpui/examples/grid_layout.rs | 80 ++++++++++++++++++++ crates/gpui/src/geometry.rs | 33 +++++++++ crates/gpui/src/style.rs | 23 +++++- crates/gpui/src/styled.rs | 109 +++++++++++++++++++++++++++- crates/gpui/src/taffy.rs | 35 ++++++++- 6 files changed, 278 insertions(+), 6 deletions(-) create mode 100644 crates/gpui/examples/grid_layout.rs diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 6e5a76d441..d720dfb2a1 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -305,3 +305,7 @@ path = "examples/uniform_list.rs" [[example]] name = "window_shadow" path = "examples/window_shadow.rs" + +[[example]] +name = "grid_layout" +path = "examples/grid_layout.rs" diff --git a/crates/gpui/examples/grid_layout.rs b/crates/gpui/examples/grid_layout.rs new file mode 100644 index 0000000000..f285497578 --- /dev/null +++ b/crates/gpui/examples/grid_layout.rs @@ -0,0 +1,80 @@ +use gpui::{ + App, Application, Bounds, Context, Hsla, Window, WindowBounds, WindowOptions, div, prelude::*, + px, rgb, size, +}; + +// https://en.wikipedia.org/wiki/Holy_grail_(web_design) +struct HolyGrailExample {} + +impl Render for HolyGrailExample { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let block = |color: Hsla| { + div() + .size_full() + .bg(color) + .border_1() + .border_dashed() + .rounded_md() + .border_color(gpui::white()) + .items_center() + }; + + div() + .gap_1() + .grid() + .bg(rgb(0x505050)) + .size(px(500.0)) + .shadow_lg() + .border_1() + .size_full() + .grid_cols(5) + .grid_rows(5) + .child( + block(gpui::white()) + .row_span(1) + .col_span_full() + .child("Header"), + ) + .child( + block(gpui::red()) + .col_span(1) + .h_56() + .child("Table of contents"), + ) + .child( + block(gpui::green()) + .col_span(3) + .row_span(3) + .child("Content"), + ) + .child( + block(gpui::blue()) + .col_span(1) + .row_span(3) + .child("AD :(") + .text_color(gpui::white()), + ) + .child( + block(gpui::black()) + .row_span(1) + .col_span_full() + .text_color(gpui::white()) + .child("Footer"), + ) + } +} + +fn main() { + Application::new().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| HolyGrailExample {}), + ) + .unwrap(); + cx.activate(true); + }); +} diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index 3d2d9cd9db..2de3e23ff7 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -9,12 +9,14 @@ use refineable::Refineable; use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use std::borrow::Cow; +use std::ops::Range; use std::{ cmp::{self, PartialOrd}, fmt::{self, Display}, hash::Hash, ops::{Add, Div, Mul, MulAssign, Neg, Sub}, }; +use taffy::prelude::{TaffyGridLine, TaffyGridSpan}; use crate::{App, DisplayId}; @@ -3608,6 +3610,37 @@ impl From<()> for Length { } } +/// A location in a grid layout. +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, JsonSchema, Default)] +pub struct GridLocation { + /// The rows this item uses within the grid. + pub row: Range, + /// The columns this item uses within the grid. + pub column: Range, +} + +/// The placement of an item within a grid layout's column or row. +#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema, Default)] +pub enum GridPlacement { + /// The grid line index to place this item. + Line(i16), + /// The number of grid lines to span. + Span(u16), + /// Automatically determine the placement, equivalent to Span(1) + #[default] + Auto, +} + +impl From for taffy::GridPlacement { + fn from(placement: GridPlacement) -> Self { + match placement { + GridPlacement::Line(index) => taffy::GridPlacement::from_line_index(index), + GridPlacement::Span(span) => taffy::GridPlacement::from_span(span), + GridPlacement::Auto => taffy::GridPlacement::Auto, + } + } +} + /// Provides a trait for types that can calculate half of their value. /// /// The `Half` trait is used for types that can be evenly divided, returning a new instance of the same type diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 560de7b924..09985722ef 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -7,7 +7,7 @@ use std::{ use crate::{ AbsoluteLength, App, Background, BackgroundTag, BorderStyle, Bounds, ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges, EdgesRefinement, Font, - FontFallbacks, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, + FontFallbacks, FontFeatures, FontStyle, FontWeight, GridLocation, Hsla, Length, Pixels, Point, PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, Window, black, phi, point, quad, rems, size, }; @@ -260,6 +260,17 @@ pub struct Style { /// The opacity of this element pub opacity: Option, + /// The grid columns of this element + /// Equivalent to the Tailwind `grid-cols-` + pub grid_cols: Option, + + /// The row span of this element + /// Equivalent to the Tailwind `grid-rows-` + pub grid_rows: Option, + + /// The grid location of this element + pub grid_location: Option, + /// Whether to draw a red debugging outline around this element #[cfg(debug_assertions)] pub debug: bool, @@ -275,6 +286,13 @@ impl Styled for StyleRefinement { } } +impl StyleRefinement { + /// The grid location of this element + pub fn grid_location_mut(&mut self) -> &mut GridLocation { + self.grid_location.get_or_insert_default() + } +} + /// The value of the visibility property, similar to the CSS property `visibility` #[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] pub enum Visibility { @@ -757,6 +775,9 @@ impl Default for Style { text: TextStyleRefinement::default(), mouse_cursor: None, opacity: None, + grid_rows: None, + grid_cols: None, + grid_location: None, #[cfg(debug_assertions)] debug: false, diff --git a/crates/gpui/src/styled.rs b/crates/gpui/src/styled.rs index b689f32687..c714cac14f 100644 --- a/crates/gpui/src/styled.rs +++ b/crates/gpui/src/styled.rs @@ -1,8 +1,8 @@ use crate::{ self as gpui, AbsoluteLength, AlignContent, AlignItems, BorderStyle, CursorStyle, - DefiniteLength, Display, Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla, - JustifyContent, Length, SharedString, StrikethroughStyle, StyleRefinement, TextAlign, - TextOverflow, TextStyleRefinement, UnderlineStyle, WhiteSpace, px, relative, rems, + DefiniteLength, Display, Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, + GridPlacement, Hsla, JustifyContent, Length, SharedString, StrikethroughStyle, StyleRefinement, + TextAlign, TextOverflow, TextStyleRefinement, UnderlineStyle, WhiteSpace, px, relative, rems, }; pub use gpui_macros::{ border_style_methods, box_shadow_style_methods, cursor_style_methods, margin_style_methods, @@ -46,6 +46,13 @@ pub trait Styled: Sized { self } + /// Sets the display type of the element to `grid`. + /// [Docs](https://tailwindcss.com/docs/display) + fn grid(mut self) -> Self { + self.style().display = Some(Display::Grid); + self + } + /// Sets the whitespace of the element to `normal`. /// [Docs](https://tailwindcss.com/docs/whitespace#normal) fn whitespace_normal(mut self) -> Self { @@ -640,6 +647,102 @@ pub trait Styled: Sized { self } + /// Sets the grid columns of this element. + fn grid_cols(mut self, cols: u16) -> Self { + self.style().grid_cols = Some(cols); + self + } + + /// Sets the grid rows of this element. + fn grid_rows(mut self, rows: u16) -> Self { + self.style().grid_rows = Some(rows); + self + } + + /// Sets the column start of this element. + fn col_start(mut self, start: i16) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.column.start = GridPlacement::Line(start); + self + } + + /// Sets the column start of this element to auto. + fn col_start_auto(mut self) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.column.start = GridPlacement::Auto; + self + } + + /// Sets the column end of this element. + fn col_end(mut self, end: i16) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.column.end = GridPlacement::Line(end); + self + } + + /// Sets the column end of this element to auto. + fn col_end_auto(mut self) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.column.end = GridPlacement::Auto; + self + } + + /// Sets the column span of this element. + fn col_span(mut self, span: u16) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.column = GridPlacement::Span(span)..GridPlacement::Span(span); + self + } + + /// Sets the row span of this element. + fn col_span_full(mut self) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.column = GridPlacement::Line(1)..GridPlacement::Line(-1); + self + } + + /// Sets the row start of this element. + fn row_start(mut self, start: i16) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.row.start = GridPlacement::Line(start); + self + } + + /// Sets the row start of this element to "auto" + fn row_start_auto(mut self) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.row.start = GridPlacement::Auto; + self + } + + /// Sets the row end of this element. + fn row_end(mut self, end: i16) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.row.end = GridPlacement::Line(end); + self + } + + /// Sets the row end of this element to "auto" + fn row_end_auto(mut self) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.row.end = GridPlacement::Auto; + self + } + + /// Sets the row span of this element. + fn row_span(mut self, span: u16) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.row = GridPlacement::Span(span)..GridPlacement::Span(span); + self + } + + /// Sets the row span of this element. + fn row_span_full(mut self) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.row = GridPlacement::Line(1)..GridPlacement::Line(-1); + self + } + /// Draws a debug border around this element. #[cfg(debug_assertions)] fn debug(mut self) -> Self { diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index f7fa54256d..ee21ecd8c4 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -3,7 +3,7 @@ use crate::{ }; use collections::{FxHashMap, FxHashSet}; use smallvec::SmallVec; -use std::fmt::Debug; +use std::{fmt::Debug, ops::Range}; use taffy::{ TaffyTree, TraversePartialTree as _, geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize}, @@ -251,6 +251,25 @@ trait ToTaffy { impl ToTaffy for Style { fn to_taffy(&self, rem_size: Pixels) -> taffy::style::Style { + use taffy::style_helpers::{fr, length, minmax, repeat}; + + fn to_grid_line( + placement: &Range, + ) -> taffy::Line { + taffy::Line { + start: placement.start.into(), + end: placement.end.into(), + } + } + + fn to_grid_repeat( + unit: &Option, + ) -> Vec> { + // grid-template-columns: repeat(, minmax(0, 1fr)); + unit.map(|count| vec![repeat(count, vec![minmax(length(0.0), fr(1.0))])]) + .unwrap_or_default() + } + taffy::style::Style { display: self.display.into(), overflow: self.overflow.into(), @@ -274,7 +293,19 @@ impl ToTaffy for Style { flex_basis: self.flex_basis.to_taffy(rem_size), flex_grow: self.flex_grow, flex_shrink: self.flex_shrink, - ..Default::default() // Ignore grid properties for now + grid_template_rows: to_grid_repeat(&self.grid_rows), + grid_template_columns: to_grid_repeat(&self.grid_cols), + grid_row: self + .grid_location + .as_ref() + .map(|location| to_grid_line(&location.row)) + .unwrap_or_default(), + grid_column: self + .grid_location + .as_ref() + .map(|location| to_grid_line(&location.column)) + .unwrap_or_default(), + ..Default::default() } } } From 5a6df38ccf61ac6196d56644e53e1909c11ff16a Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Thu, 14 Aug 2025 07:11:53 +0300 Subject: [PATCH 335/693] docs: Add example of controlling reasoning effort (#36135) Release Notes: - N/A --- docs/src/ai/llm-providers.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/src/ai/llm-providers.md b/docs/src/ai/llm-providers.md index 21ff2a8a51..58c9230760 100644 --- a/docs/src/ai/llm-providers.md +++ b/docs/src/ai/llm-providers.md @@ -392,26 +392,26 @@ Zed will also use the `OPENAI_API_KEY` environment variable if it's defined. #### Custom Models {#openai-custom-models} The Zed agent comes pre-configured to use the latest version for common models (GPT-5, GPT-5 mini, o4-mini, GPT-4.1, and others). -To use alternate models, perhaps a preview release or a dated model release, or if you wish to control the request parameters, you can do so by adding the following to your Zed `settings.json`: +To use alternate models, perhaps a preview release, or if you wish to control the request parameters, you can do so by adding the following to your Zed `settings.json`: ```json { "language_models": { "openai": { "available_models": [ + { + "name": "gpt-5", + "display_name": "gpt-5 high", + "reasoning_effort": "high", + "max_tokens": 272000, + "max_completion_tokens": 20000 + }, { "name": "gpt-4o-2024-08-06", "display_name": "GPT 4o Summer 2024", "max_tokens": 128000 - }, - { - "name": "o1-mini", - "display_name": "o1-mini", - "max_tokens": 128000, - "max_completion_tokens": 20000 } - ], - "version": "1" + ] } } } From ab9fa03d55fc52cc0afdde7fe6b5f063ce12ce92 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 13 Aug 2025 22:24:47 -0600 Subject: [PATCH 336/693] UI for checkpointing (#36124) Co-authored-by: Antonio Scandurra Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra --- crates/agent_ui/src/acp/thread_view.rs | 27 +++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 3aefae7265..5f67dc15b8 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1,6 +1,6 @@ use acp_thread::{ AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, - LoadError, MentionUri, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, + LoadError, MentionUri, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, UserMessageId, }; use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; @@ -921,6 +921,16 @@ impl AcpThreadView { cx.notify(); } + fn rewind(&mut self, message_id: &UserMessageId, cx: &mut Context) { + let Some(thread) = self.thread() else { + return; + }; + thread + .update(cx, |thread, cx| thread.rewind(message_id.clone(), cx)) + .detach_and_log_err(cx); + cx.notify(); + } + fn render_entry( &self, index: usize, @@ -931,8 +941,23 @@ impl AcpThreadView { ) -> AnyElement { let primary = match &entry { AgentThreadEntry::UserMessage(message) => div() + .id(("user_message", index)) .py_4() .px_2() + .children(message.id.clone().and_then(|message_id| { + message.checkpoint.as_ref()?; + + Some( + Button::new("restore-checkpoint", "Restore Checkpoint") + .icon(IconName::Undo) + .icon_size(IconSize::XSmall) + .icon_position(IconPosition::Start) + .label_size(LabelSize::XSmall) + .on_click(cx.listener(move |this, _, _window, cx| { + this.rewind(&message_id, cx); + })), + ) + })) .child( v_flex() .p_3() From 5bbdd1a262732ceb195de65988fd7126d632b271 Mon Sep 17 00:00:00 2001 From: Maksim Bondarenkov <119937608+ognevny@users.noreply.github.com> Date: Thu, 14 Aug 2025 13:02:52 +0700 Subject: [PATCH 337/693] docs: Update information in MSYS2 section (#36158) - we are about to drop Zed for MINGW64 because `crash-handler` uses a symbol which is not presented in `msvcrt.dll` - mention MSYS2 docs page and CLANGARM64 environment Release Notes: - N/A --- docs/src/development/windows.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/development/windows.md b/docs/src/development/windows.md index ac38e4d7d6..551d5f9f21 100644 --- a/docs/src/development/windows.md +++ b/docs/src/development/windows.md @@ -114,19 +114,19 @@ cargo test --workspace ## Installing from msys2 -[MSYS2](https://msys2.org/) distribution provides Zed as a package [mingw-w64-zed](https://packages.msys2.org/base/mingw-w64-zed). The package is available for UCRT64, MINGW64 and CLANG64 repositories. To download it, run +[MSYS2](https://msys2.org/) distribution provides Zed as a package [mingw-w64-zed](https://packages.msys2.org/base/mingw-w64-zed). The package is available for UCRT64, CLANG64 and CLANGARM64 repositories. To download it, run ```sh pacman -Syu pacman -S $MINGW_PACKAGE_PREFIX-zed ``` -then you can run `zeditor` CLI. Editor executable is installed under `$MINGW_PREFIX/lib/zed` directory - You can see the [build script](https://github.com/msys2/MINGW-packages/blob/master/mingw-w64-zed/PKGBUILD) for more details on build process. > Please, report any issue in [msys2/MINGW-packages/issues](https://github.com/msys2/MINGW-packages/issues?q=is%3Aissue+is%3Aopen+zed) first. +See also MSYS2 [documentation page](https://www.msys2.org/docs/ides-editors). + Note that `collab` is not supported for MSYS2. ## Troubleshooting From 0291db0d78d751bff518a49974b14ab6cf146a1a Mon Sep 17 00:00:00 2001 From: Alvaro Parker <64918109+AlvaroParker@users.noreply.github.com> Date: Thu, 14 Aug 2025 02:10:38 -0400 Subject: [PATCH 338/693] git: Add handler to get default branch on remote (#36157) Closes #36150 Release Notes: - Fixed `git: branch` action not worked with ssh workflow --- crates/project/src/git_store.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 5d48c833ab..32deb0dbc4 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -414,6 +414,7 @@ impl GitStore { pub fn init(client: &AnyProtoClient) { client.add_entity_request_handler(Self::handle_get_remotes); client.add_entity_request_handler(Self::handle_get_branches); + client.add_entity_request_handler(Self::handle_get_default_branch); client.add_entity_request_handler(Self::handle_change_branch); client.add_entity_request_handler(Self::handle_create_branch); client.add_entity_request_handler(Self::handle_git_init); @@ -1894,6 +1895,23 @@ impl GitStore { .collect::>(), }) } + async fn handle_get_default_branch( + this: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); + let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; + + let branch = repository_handle + .update(&mut cx, |repository_handle, _| { + repository_handle.default_branch() + })? + .await?? + .map(Into::into); + + Ok(proto::GetDefaultBranchResponse { branch }) + } async fn handle_create_branch( this: Entity, envelope: TypedEnvelope, From 8e4f30abcb83ab5ca920f8d545adbe15aaf4053a Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 14 Aug 2025 10:16:25 +0200 Subject: [PATCH 339/693] project: Print error causes when failing to spawn lsp command (#36163) cc https://github.com/zed-industries/zed/issues/34666 Display printing anyhow errors only renders the error itself, but not any of its causes so we've been dropping the important context when showing the issue to the users. Release Notes: - N/A --- crates/lsp/src/lsp.rs | 4 ++-- crates/project/src/lsp_store.rs | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 22a227c231..ce9e2fe229 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -318,6 +318,8 @@ impl LanguageServer { } else { root_path.parent().unwrap_or_else(|| Path::new("/")) }; + let root_uri = Url::from_file_path(&working_dir) + .map_err(|()| anyhow!("{working_dir:?} is not a valid URI"))?; log::info!( "starting language server process. binary path: {:?}, working directory: {:?}, args: {:?}", @@ -345,8 +347,6 @@ impl LanguageServer { let stdin = server.stdin.take().unwrap(); let stdout = server.stdout.take().unwrap(); let stderr = server.stderr.take().unwrap(); - let root_uri = Url::from_file_path(&working_dir) - .map_err(|()| anyhow!("{working_dir:?} is not a valid URI"))?; let server = Self::new_internal( server_id, server_name, diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 827341d60d..60d847023f 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -390,13 +390,17 @@ impl LocalLspStore { delegate.update_status( adapter.name(), BinaryStatus::Failed { - error: format!("{err}\n-- stderr--\n{log}"), + error: if log.is_empty() { + format!("{err:#}") + } else { + format!("{err:#}\n-- stderr --\n{log}") + }, }, ); - let message = - format!("Failed to start language server {server_name:?}: {err:#?}"); - log::error!("{message}"); - log::error!("server stderr: {log}"); + log::error!("Failed to start language server {server_name:?}: {err:?}"); + if !log.is_empty() { + log::error!("server stderr: {log}"); + } None } } From b3d048d6dcc60060f194d3402317397ccd84d561 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 14 Aug 2025 11:11:27 +0200 Subject: [PATCH 340/693] Add back `DeletePathTool` to agent2 (#36168) This was probably removed accidentally as a result of a merge conflict. Release Notes: - N/A --- crates/agent2/src/agent.rs | 28 ++++----- crates/agent2/src/thread.rs | 109 ++++++++++++++---------------------- 2 files changed, 57 insertions(+), 80 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index ced8c5e401..6ebcece2b5 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,8 +1,8 @@ use crate::{AgentResponseEvent, Thread, templates::Templates}; use crate::{ - ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DiagnosticsTool, EditFileTool, - FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MovePathTool, NowTool, OpenTool, - ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, UserMessageContent, + ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DeletePathTool, DiagnosticsTool, + EditFileTool, FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MovePathTool, NowTool, + OpenTool, ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, UserMessageContent, WebSearchTool, }; use acp_thread::AgentModelSelector; @@ -583,22 +583,22 @@ impl acp_thread::AgentConnection for NativeAgentConnection { default_model, cx, ); - thread.add_tool(CreateDirectoryTool::new(project.clone())); thread.add_tool(CopyPathTool::new(project.clone())); + thread.add_tool(CreateDirectoryTool::new(project.clone())); + thread.add_tool(DeletePathTool::new(project.clone(), action_log.clone())); thread.add_tool(DiagnosticsTool::new(project.clone())); - thread.add_tool(MovePathTool::new(project.clone())); - thread.add_tool(ListDirectoryTool::new(project.clone())); - thread.add_tool(OpenTool::new(project.clone())); - thread.add_tool(ThinkingTool); - thread.add_tool(FindPathTool::new(project.clone())); - thread.add_tool(FetchTool::new(project.read(cx).client().http_client())); - thread.add_tool(GrepTool::new(project.clone())); - thread.add_tool(ReadFileTool::new(project.clone(), action_log)); thread.add_tool(EditFileTool::new(cx.entity())); + thread.add_tool(FetchTool::new(project.read(cx).client().http_client())); + thread.add_tool(FindPathTool::new(project.clone())); + thread.add_tool(GrepTool::new(project.clone())); + thread.add_tool(ListDirectoryTool::new(project.clone())); + thread.add_tool(MovePathTool::new(project.clone())); thread.add_tool(NowTool); + thread.add_tool(OpenTool::new(project.clone())); + thread.add_tool(ReadFileTool::new(project.clone(), action_log)); thread.add_tool(TerminalTool::new(project.clone(), cx)); - // TODO: Needs to be conditional based on zed model or not - thread.add_tool(WebSearchTool); + thread.add_tool(ThinkingTool); + thread.add_tool(WebSearchTool); // TODO: Enable this only if it's a zed model. thread }); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index b48f9001ac..4156ec44d2 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -411,7 +411,7 @@ pub struct Thread { /// Survives across multiple requests as the model performs tool calls and /// we run tools, report their results. running_turn: Option>, - pending_agent_message: Option, + pending_message: Option, tools: BTreeMap>, context_server_registry: Entity, profile_id: AgentProfileId, @@ -437,7 +437,7 @@ impl Thread { messages: Vec::new(), completion_mode: CompletionMode::Normal, running_turn: None, - pending_agent_message: None, + pending_message: None, tools: BTreeMap::default(), context_server_registry, profile_id, @@ -463,7 +463,7 @@ impl Thread { #[cfg(any(test, feature = "test-support"))] pub fn last_message(&self) -> Option { - if let Some(message) = self.pending_agent_message.clone() { + if let Some(message) = self.pending_message.clone() { Some(Message::Agent(message)) } else { self.messages.last().cloned() @@ -485,7 +485,7 @@ impl Thread { pub fn cancel(&mut self) { // TODO: do we need to emit a stop::cancel for ACP? self.running_turn.take(); - self.flush_pending_agent_message(); + self.flush_pending_message(); } pub fn truncate(&mut self, message_id: UserMessageId) -> Result<()> { @@ -521,74 +521,58 @@ impl Thread { mpsc::unbounded::>(); let event_stream = AgentResponseEventStream(events_tx); - let user_message_ix = self.messages.len(); self.messages.push(Message::User(UserMessage { - id: message_id, + id: message_id.clone(), content, })); log::info!("Total messages in thread: {}", self.messages.len()); - self.running_turn = Some(cx.spawn(async move |thread, cx| { + self.running_turn = Some(cx.spawn(async move |this, cx| { log::info!("Starting agent turn execution"); let turn_result = async { - // Perform one request, then keep looping if the model makes tool calls. let mut completion_intent = CompletionIntent::UserPrompt; - 'outer: loop { + loop { log::debug!( "Building completion request with intent: {:?}", completion_intent ); - let request = thread.update(cx, |thread, cx| { - thread.build_completion_request(completion_intent, cx) + let request = this.update(cx, |this, cx| { + this.build_completion_request(completion_intent, cx) })?; - // Stream events, appending to messages and collecting up tool uses. log::info!("Calling model.stream_completion"); let mut events = model.stream_completion(request, cx).await?; log::debug!("Stream completion started successfully"); let mut tool_uses = FuturesUnordered::new(); while let Some(event) = events.next().await { - match event { - Ok(LanguageModelCompletionEvent::Stop(reason)) => { + match event? { + LanguageModelCompletionEvent::Stop(reason) => { event_stream.send_stop(reason); if reason == StopReason::Refusal { - thread.update(cx, |thread, _cx| { - thread.pending_agent_message = None; - thread.messages.truncate(user_message_ix); - })?; - break 'outer; + this.update(cx, |this, _cx| this.truncate(message_id))??; + return Ok(()); } } - Ok(event) => { + event => { log::trace!("Received completion event: {:?}", event); - thread - .update(cx, |thread, cx| { - tool_uses.extend(thread.handle_streamed_completion_event( - event, - &event_stream, - cx, - )); - }) - .ok(); - } - Err(error) => { - log::error!("Error in completion stream: {:?}", error); - event_stream.send_error(error); - break; + this.update(cx, |this, cx| { + tool_uses.extend(this.handle_streamed_completion_event( + event, + &event_stream, + cx, + )); + }) + .ok(); } } } - // If there are no tool uses, the turn is done. if tool_uses.is_empty() { log::info!("No tool uses found, completing turn"); - break; + return Ok(()); } log::info!("Found {} tool uses to execute", tool_uses.len()); - // As tool results trickle in, insert them in the last user - // message so that they can be sent on the next tick of the - // agentic loop. while let Some(tool_result) = tool_uses.next().await { log::info!("Tool finished {:?}", tool_result); @@ -604,29 +588,21 @@ impl Thread { ..Default::default() }, ); - thread - .update(cx, |thread, _cx| { - thread - .pending_agent_message() - .tool_results - .insert(tool_result.tool_use_id.clone(), tool_result); - }) - .ok(); + this.update(cx, |this, _cx| { + this.pending_message() + .tool_results + .insert(tool_result.tool_use_id.clone(), tool_result); + }) + .ok(); } - thread.update(cx, |thread, _cx| thread.flush_pending_agent_message())?; - + this.update(cx, |this, _| this.flush_pending_message())?; completion_intent = CompletionIntent::ToolResults; } - - Ok(()) } .await; - thread - .update(cx, |thread, _cx| thread.flush_pending_agent_message()) - .ok(); - + this.update(cx, |this, _| this.flush_pending_message()).ok(); if let Err(error) = turn_result { log::error!("Turn execution failed: {:?}", error); event_stream.send_error(error); @@ -668,7 +644,8 @@ impl Thread { match event { StartMessage { .. } => { - self.messages.push(Message::Agent(AgentMessage::default())); + self.flush_pending_message(); + self.pending_message = Some(AgentMessage::default()); } Text(new_text) => self.handle_text_event(new_text, event_stream, cx), Thinking { text, signature } => { @@ -706,7 +683,7 @@ impl Thread { ) { events_stream.send_text(&new_text); - let last_message = self.pending_agent_message(); + let last_message = self.pending_message(); if let Some(AgentMessageContent::Text(text)) = last_message.content.last_mut() { text.push_str(&new_text); } else { @@ -727,7 +704,7 @@ impl Thread { ) { event_stream.send_thinking(&new_text); - let last_message = self.pending_agent_message(); + let last_message = self.pending_message(); if let Some(AgentMessageContent::Thinking { text, signature }) = last_message.content.last_mut() { @@ -744,7 +721,7 @@ impl Thread { } fn handle_redacted_thinking_event(&mut self, data: String, cx: &mut Context) { - let last_message = self.pending_agent_message(); + let last_message = self.pending_message(); last_message .content .push(AgentMessageContent::RedactedThinking(data)); @@ -768,7 +745,7 @@ impl Thread { } // Ensure the last message ends in the current tool use - let last_message = self.pending_agent_message(); + let last_message = self.pending_message(); let push_new_tool_use = last_message.content.last_mut().map_or(true, |content| { if let AgentMessageContent::ToolUse(last_tool_use) = content { if last_tool_use.id == tool_use.id { @@ -871,12 +848,12 @@ impl Thread { } } - fn pending_agent_message(&mut self) -> &mut AgentMessage { - self.pending_agent_message.get_or_insert_default() + fn pending_message(&mut self) -> &mut AgentMessage { + self.pending_message.get_or_insert_default() } - fn flush_pending_agent_message(&mut self) { - let Some(mut message) = self.pending_agent_message.take() else { + fn flush_pending_message(&mut self) { + let Some(mut message) = self.pending_message.take() else { return; }; @@ -997,7 +974,7 @@ impl Thread { } } - if let Some(message) = self.pending_agent_message.as_ref() { + if let Some(message) = self.pending_message.as_ref() { messages.extend(message.to_request()); } @@ -1013,7 +990,7 @@ impl Thread { markdown.push_str(&message.to_markdown()); } - if let Some(message) = self.pending_agent_message.as_ref() { + if let Some(message) = self.pending_message.as_ref() { markdown.push('\n'); markdown.push_str(&message.to_markdown()); } From ffac8c512858789158d3c52d2cd50bca3a904f54 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 14 Aug 2025 11:11:46 +0200 Subject: [PATCH 341/693] editor: Render all targets in go to def multbuffer title (#36167) Release Notes: - N/A --- crates/editor/src/editor.rs | 44 +++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c77262143d..cbee9021ed 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -15842,19 +15842,23 @@ impl Editor { let tab_kind = match kind { Some(GotoDefinitionKind::Implementation) => "Implementations", - _ => "Definitions", + Some(GotoDefinitionKind::Symbol) | None => "Definitions", + Some(GotoDefinitionKind::Declaration) => "Declarations", + Some(GotoDefinitionKind::Type) => "Types", }; let title = editor .update_in(acx, |_, _, cx| { - let origin = locations.first().unwrap(); - let buffer = origin.buffer.read(cx); - format!( - "{} for {}", - tab_kind, - buffer - .text_for_range(origin.range.clone()) - .collect::() - ) + let target = locations + .iter() + .map(|location| { + location + .buffer + .read(cx) + .text_for_range(location.range.clone()) + .collect::() + }) + .join(", "); + format!("{tab_kind} for {target}") }) .context("buffer title")?; @@ -16050,19 +16054,17 @@ impl Editor { } workspace.update_in(cx, |workspace, window, cx| { - let title = locations - .first() - .as_ref() + let target = locations + .iter() .map(|location| { - let buffer = location.buffer.read(cx); - format!( - "References to `{}`", - buffer - .text_for_range(location.range.clone()) - .collect::() - ) + location + .buffer + .read(cx) + .text_for_range(location.range.clone()) + .collect::() }) - .unwrap(); + .join(", "); + let title = format!("References to {target}"); Self::open_locations_in_multibuffer( workspace, locations, From e5402d546414a19112a2a0c4c3046bb414a3ec68 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 14 Aug 2025 07:39:33 -0600 Subject: [PATCH 342/693] Allow editing Agent2 messages (#36155) Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra Co-authored-by: Agus Zubiaga --- Cargo.lock | 1 + assets/keymaps/default-linux.json | 2 - assets/keymaps/default-macos.json | 2 - crates/acp_thread/Cargo.toml | 1 + crates/acp_thread/src/acp_thread.rs | 15 +- crates/acp_thread/src/mention.rs | 94 ++- crates/agent2/src/thread.rs | 4 +- crates/agent_ui/src/acp.rs | 3 +- .../agent_ui/src/acp/completion_provider.rs | 113 +-- crates/agent_ui/src/acp/message_editor.rs | 469 +++++++++++ crates/agent_ui/src/acp/message_history.rs | 88 -- crates/agent_ui/src/acp/thread_view.rs | 766 +++++++----------- crates/agent_ui/src/agent_panel.rs | 15 +- crates/zed_actions/src/lib.rs | 4 - 14 files changed, 956 insertions(+), 621 deletions(-) create mode 100644 crates/agent_ui/src/acp/message_editor.rs delete mode 100644 crates/agent_ui/src/acp/message_history.rs diff --git a/Cargo.lock b/Cargo.lock index f0fd3049c0..cb087f43b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,7 @@ dependencies = [ "collections", "editor", "env_logger 0.11.8", + "file_icons", "futures 0.3.31", "gpui", "indoc", diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index dda26f406b..01c0b4e969 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -331,8 +331,6 @@ "use_key_equivalents": true, "bindings": { "enter": "agent::Chat", - "up": "agent::PreviousHistoryMessage", - "down": "agent::NextHistoryMessage", "shift-ctrl-r": "agent::OpenAgentDiff", "ctrl-shift-y": "agent::KeepAll", "ctrl-shift-n": "agent::RejectAll" diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 3966efd8df..e5b7fff9e1 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -383,8 +383,6 @@ "use_key_equivalents": true, "bindings": { "enter": "agent::Chat", - "up": "agent::PreviousHistoryMessage", - "down": "agent::NextHistoryMessage", "shift-ctrl-r": "agent::OpenAgentDiff", "cmd-shift-y": "agent::KeepAll", "cmd-shift-n": "agent::RejectAll" diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index 2ac15de08f..2d0fe2d264 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -23,6 +23,7 @@ anyhow.workspace = true buffer_diff.workspace = true collections.workspace = true editor.workspace = true +file_icons.workspace = true futures.workspace = true gpui.workspace = true itertools.workspace = true diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index a5b512f31a..da4d82712a 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -32,6 +32,7 @@ use util::ResultExt; pub struct UserMessage { pub id: Option, pub content: ContentBlock, + pub chunks: Vec, pub checkpoint: Option, } @@ -804,18 +805,25 @@ impl AcpThread { let entries_len = self.entries.len(); if let Some(last_entry) = self.entries.last_mut() - && let AgentThreadEntry::UserMessage(UserMessage { id, content, .. }) = last_entry + && let AgentThreadEntry::UserMessage(UserMessage { + id, + content, + chunks, + .. + }) = last_entry { *id = message_id.or(id.take()); - content.append(chunk, &language_registry, cx); + content.append(chunk.clone(), &language_registry, cx); + chunks.push(chunk); let idx = entries_len - 1; cx.emit(AcpThreadEvent::EntryUpdated(idx)); } else { - let content = ContentBlock::new(chunk, &language_registry, cx); + let content = ContentBlock::new(chunk.clone(), &language_registry, cx); self.push_entry( AgentThreadEntry::UserMessage(UserMessage { id: message_id, content, + chunks: vec![chunk], checkpoint: None, }), cx, @@ -1150,6 +1158,7 @@ impl AcpThread { AgentThreadEntry::UserMessage(UserMessage { id: message_id.clone(), content: block, + chunks: message.clone(), checkpoint: None, }), cx, diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 03174608fb..b18cbfe18e 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -1,16 +1,21 @@ use agent::ThreadId; use anyhow::{Context as _, Result, bail}; +use file_icons::FileIcons; use prompt_store::{PromptId, UserPromptId}; use std::{ fmt, ops::Range, path::{Path, PathBuf}, }; +use ui::{App, IconName, SharedString}; use url::Url; #[derive(Clone, Debug, PartialEq, Eq)] pub enum MentionUri { - File(PathBuf), + File { + abs_path: PathBuf, + is_directory: bool, + }, Symbol { path: PathBuf, name: String, @@ -75,8 +80,12 @@ impl MentionUri { } else { let file_path = PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path)); + let is_directory = input.ends_with("/"); - Ok(Self::File(file_path)) + Ok(Self::File { + abs_path: file_path, + is_directory, + }) } } "zed" => { @@ -108,9 +117,9 @@ impl MentionUri { } } - fn name(&self) -> String { + pub fn name(&self) -> String { match self { - MentionUri::File(path) => path + MentionUri::File { abs_path, .. } => abs_path .file_name() .unwrap_or_default() .to_string_lossy() @@ -126,15 +135,45 @@ impl MentionUri { } } + pub fn icon_path(&self, cx: &mut App) -> SharedString { + match self { + MentionUri::File { + abs_path, + is_directory, + } => { + if *is_directory { + FileIcons::get_folder_icon(false, cx) + .unwrap_or_else(|| IconName::Folder.path().into()) + } else { + FileIcons::get_icon(&abs_path, cx) + .unwrap_or_else(|| IconName::File.path().into()) + } + } + MentionUri::Symbol { .. } => IconName::Code.path().into(), + MentionUri::Thread { .. } => IconName::Thread.path().into(), + MentionUri::TextThread { .. } => IconName::Thread.path().into(), + MentionUri::Rule { .. } => IconName::Reader.path().into(), + MentionUri::Selection { .. } => IconName::Reader.path().into(), + MentionUri::Fetch { .. } => IconName::ToolWeb.path().into(), + } + } + pub fn as_link<'a>(&'a self) -> MentionLink<'a> { MentionLink(self) } pub fn to_uri(&self) -> Url { match self { - MentionUri::File(path) => { + MentionUri::File { + abs_path, + is_directory, + } => { let mut url = Url::parse("file:///").unwrap(); - url.set_path(&path.to_string_lossy()); + let mut path = abs_path.to_string_lossy().to_string(); + if *is_directory && !path.ends_with("/") { + path.push_str("/"); + } + url.set_path(&path); url } MentionUri::Symbol { @@ -226,12 +265,53 @@ mod tests { let file_uri = "file:///path/to/file.rs"; let parsed = MentionUri::parse(file_uri).unwrap(); match &parsed { - MentionUri::File(path) => assert_eq!(path.to_str().unwrap(), "/path/to/file.rs"), + MentionUri::File { + abs_path, + is_directory, + } => { + assert_eq!(abs_path.to_str().unwrap(), "/path/to/file.rs"); + assert!(!is_directory); + } _ => panic!("Expected File variant"), } assert_eq!(parsed.to_uri().to_string(), file_uri); } + #[test] + fn test_parse_directory_uri() { + let file_uri = "file:///path/to/dir/"; + let parsed = MentionUri::parse(file_uri).unwrap(); + match &parsed { + MentionUri::File { + abs_path, + is_directory, + } => { + assert_eq!(abs_path.to_str().unwrap(), "/path/to/dir/"); + assert!(is_directory); + } + _ => panic!("Expected File variant"), + } + assert_eq!(parsed.to_uri().to_string(), file_uri); + } + + #[test] + fn test_to_directory_uri_with_slash() { + let uri = MentionUri::File { + abs_path: PathBuf::from("/path/to/dir/"), + is_directory: true, + }; + assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/"); + } + + #[test] + fn test_to_directory_uri_without_slash() { + let uri = MentionUri::File { + abs_path: PathBuf::from("/path/to/dir"), + is_directory: true, + }; + assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/"); + } + #[test] fn test_parse_symbol_uri() { let symbol_uri = "file:///path/to/file.rs?symbol=MySymbol#L10:20"; diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 4156ec44d2..260aaaf550 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -124,12 +124,12 @@ impl UserMessage { } UserMessageContent::Mention { uri, content } => { match uri { - MentionUri::File(path) => { + MentionUri::File { abs_path, .. } => { write!( &mut symbol_context, "\n{}", MarkdownCodeBlock { - tag: &codeblock_tag(&path, None), + tag: &codeblock_tag(&abs_path, None), text: &content.to_string(), } ) diff --git a/crates/agent_ui/src/acp.rs b/crates/agent_ui/src/acp.rs index b9814adb2d..630aa730a6 100644 --- a/crates/agent_ui/src/acp.rs +++ b/crates/agent_ui/src/acp.rs @@ -1,10 +1,9 @@ mod completion_provider; -mod message_history; +mod message_editor; mod model_selector; mod model_selector_popover; mod thread_view; -pub use message_history::MessageHistory; pub use model_selector::AcpModelSelector; pub use model_selector_popover::AcpModelSelectorPopover; pub use thread_view::AcpThreadView; diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 46c8aa92f1..720ee23b00 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -1,5 +1,5 @@ use std::ops::Range; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; @@ -8,7 +8,7 @@ use anyhow::{Context as _, Result, anyhow}; use collections::{HashMap, HashSet}; use editor::display_map::CreaseId; use editor::{CompletionProvider, Editor, ExcerptId, ToOffset as _}; -use file_icons::FileIcons; + use futures::future::try_join_all; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{App, Entity, Task, WeakEntity}; @@ -28,10 +28,7 @@ use url::Url; use workspace::Workspace; use workspace::notifications::NotifyResultExt; -use agent::{ - context::RULES_ICON, - thread_store::{TextThreadStore, ThreadStore}, -}; +use agent::thread_store::{TextThreadStore, ThreadStore}; use crate::context_picker::fetch_context_picker::fetch_url_content; use crate::context_picker::file_context_picker::{FileMatch, search_files}; @@ -66,6 +63,11 @@ impl MentionSet { self.uri_by_crease_id.drain().map(|(id, _)| id) } + pub fn clear(&mut self) { + self.fetch_results.clear(); + self.uri_by_crease_id.clear(); + } + pub fn contents( &self, project: Entity, @@ -79,12 +81,13 @@ impl MentionSet { .iter() .map(|(&crease_id, uri)| { match uri { - MentionUri::File(path) => { + MentionUri::File { abs_path, .. } => { + // TODO directories let uri = uri.clone(); - let path = path.to_path_buf(); + let abs_path = abs_path.to_path_buf(); let buffer_task = project.update(cx, |project, cx| { let path = project - .find_project_path(path, cx) + .find_project_path(abs_path, cx) .context("Failed to find project path")?; anyhow::Ok(project.open_buffer(path, cx)) }); @@ -508,9 +511,14 @@ impl ContextPickerCompletionProvider { }) .unwrap_or_default(); let line_range = point_range.start.row..point_range.end.row; + + let uri = MentionUri::Selection { + path: path.clone(), + line_range: line_range.clone(), + }; let crease = crate::context_picker::crease_for_mention( selection_name(&path, &line_range).into(), - IconName::Reader.path().into(), + uri.icon_path(cx), range, editor.downgrade(), ); @@ -528,10 +536,7 @@ impl ContextPickerCompletionProvider { crease_ids.try_into().unwrap() }); - mention_set.lock().insert( - crease_id, - MentionUri::Selection { path, line_range }, - ); + mention_set.lock().insert(crease_id, uri); current_offset += text_len + 1; } @@ -569,13 +574,8 @@ impl ContextPickerCompletionProvider { recent: bool, editor: Entity, mention_set: Arc>, + cx: &mut App, ) -> Completion { - let icon_for_completion = if recent { - IconName::HistoryRerun - } else { - IconName::Thread - }; - let uri = match &thread_entry { ThreadContextEntry::Thread { id, title } => MentionUri::Thread { id: id.clone(), @@ -586,6 +586,13 @@ impl ContextPickerCompletionProvider { name: title.to_string(), }, }; + + let icon_for_completion = if recent { + IconName::HistoryRerun.path().into() + } else { + uri.icon_path(cx) + }; + let new_text = format!("{} ", uri.as_link()); let new_text_len = new_text.len(); @@ -596,9 +603,9 @@ impl ContextPickerCompletionProvider { documentation: None, insert_text_mode: None, source: project::CompletionSource::Custom, - icon_path: Some(icon_for_completion.path().into()), + icon_path: Some(icon_for_completion.clone()), confirm: Some(confirm_completion_callback( - IconName::Thread.path().into(), + uri.icon_path(cx), thread_entry.title().clone(), excerpt_id, source_range.start, @@ -616,6 +623,7 @@ impl ContextPickerCompletionProvider { source_range: Range, editor: Entity, mention_set: Arc>, + cx: &mut App, ) -> Completion { let uri = MentionUri::Rule { id: rule.prompt_id.into(), @@ -623,6 +631,7 @@ impl ContextPickerCompletionProvider { }; let new_text = format!("{} ", uri.as_link()); let new_text_len = new_text.len(); + let icon_path = uri.icon_path(cx); Completion { replace_range: source_range.clone(), new_text, @@ -630,9 +639,9 @@ impl ContextPickerCompletionProvider { documentation: None, insert_text_mode: None, source: project::CompletionSource::Custom, - icon_path: Some(RULES_ICON.path().into()), + icon_path: Some(icon_path.clone()), confirm: Some(confirm_completion_callback( - RULES_ICON.path().into(), + icon_path, rule.title.clone(), excerpt_id, source_range.start, @@ -654,7 +663,7 @@ impl ContextPickerCompletionProvider { editor: Entity, mention_set: Arc>, project: Entity, - cx: &App, + cx: &mut App, ) -> Option { let (file_name, directory) = crate::context_picker::file_context_picker::extract_file_name_and_directory( @@ -664,27 +673,21 @@ impl ContextPickerCompletionProvider { let label = build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx); - let full_path = if let Some(directory) = directory { - format!("{}{}", directory, file_name) - } else { - file_name.to_string() + + let abs_path = project.read(cx).absolute_path(&project_path, cx)?; + + let file_uri = MentionUri::File { + abs_path, + is_directory, }; - let crease_icon_path = if is_directory { - FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into()) - } else { - FileIcons::get_icon(Path::new(&full_path), cx) - .unwrap_or_else(|| IconName::File.path().into()) - }; + let crease_icon_path = file_uri.icon_path(cx); let completion_icon_path = if is_recent { IconName::HistoryRerun.path().into() } else { crease_icon_path.clone() }; - let abs_path = project.read(cx).absolute_path(&project_path, cx)?; - - let file_uri = MentionUri::File(abs_path); let new_text = format!("{} ", file_uri.as_link()); let new_text_len = new_text.len(); Some(Completion { @@ -729,16 +732,17 @@ impl ContextPickerCompletionProvider { }; let new_text = format!("{} ", uri.as_link()); let new_text_len = new_text.len(); + let icon_path = uri.icon_path(cx); Some(Completion { replace_range: source_range.clone(), new_text, label, documentation: None, source: project::CompletionSource::Custom, - icon_path: Some(IconName::Code.path().into()), + icon_path: Some(icon_path.clone()), insert_text_mode: None, confirm: Some(confirm_completion_callback( - IconName::Code.path().into(), + icon_path, symbol.name.clone().into(), excerpt_id, source_range.start, @@ -757,16 +761,23 @@ impl ContextPickerCompletionProvider { editor: Entity, mention_set: Arc>, http_client: Arc, + cx: &mut App, ) -> Option { let new_text = format!("@fetch {} ", url_to_fetch.clone()); let new_text_len = new_text.len(); + let mention_uri = MentionUri::Fetch { + url: url::Url::parse(url_to_fetch.as_ref()) + .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}"))) + .ok()?, + }; + let icon_path = mention_uri.icon_path(cx); Some(Completion { replace_range: source_range.clone(), new_text, label: CodeLabel::plain(url_to_fetch.to_string(), None), documentation: None, source: project::CompletionSource::Custom, - icon_path: Some(IconName::ToolWeb.path().into()), + icon_path: Some(icon_path.clone()), insert_text_mode: None, confirm: Some({ let start = source_range.start; @@ -774,6 +785,7 @@ impl ContextPickerCompletionProvider { let editor = editor.clone(); let url_to_fetch = url_to_fetch.clone(); let source_range = source_range.clone(); + let icon_path = icon_path.clone(); Arc::new(move |_, window, cx| { let Some(url) = url::Url::parse(url_to_fetch.as_ref()) .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}"))) @@ -781,12 +793,12 @@ impl ContextPickerCompletionProvider { else { return false; }; - let mention_uri = MentionUri::Fetch { url: url.clone() }; let editor = editor.clone(); let mention_set = mention_set.clone(); let http_client = http_client.clone(); let source_range = source_range.clone(); + let icon_path = icon_path.clone(); window.defer(cx, move |window, cx| { let url = url.clone(); @@ -795,7 +807,7 @@ impl ContextPickerCompletionProvider { start, content_len, url.to_string().into(), - IconName::ToolWeb.path().into(), + icon_path, editor.clone(), window, cx, @@ -814,8 +826,10 @@ impl ContextPickerCompletionProvider { .await .notify_async_err(cx) { - mention_set.lock().add_fetch_result(url, content); - mention_set.lock().insert(crease_id, mention_uri.clone()); + mention_set.lock().add_fetch_result(url.clone(), content); + mention_set + .lock() + .insert(crease_id, MentionUri::Fetch { url }); } else { // Remove crease if we failed to fetch editor @@ -911,8 +925,8 @@ impl CompletionProvider for ContextPickerCompletionProvider { for uri in mention_set.uri_by_crease_id.values() { match uri { - MentionUri::File(path) => { - excluded_paths.insert(path.clone()); + MentionUri::File { abs_path, .. } => { + excluded_paths.insert(abs_path.clone()); } MentionUri::Thread { id, .. } => { excluded_threads.insert(id.clone()); @@ -1001,6 +1015,7 @@ impl CompletionProvider for ContextPickerCompletionProvider { is_recent, editor.clone(), mention_set.clone(), + cx, )), Match::Rules(user_rules) => Some(Self::completion_for_rules( @@ -1009,6 +1024,7 @@ impl CompletionProvider for ContextPickerCompletionProvider { source_range.clone(), editor.clone(), mention_set.clone(), + cx, )), Match::Fetch(url) => Self::completion_for_fetch( @@ -1018,6 +1034,7 @@ impl CompletionProvider for ContextPickerCompletionProvider { editor.clone(), mention_set.clone(), http_client.clone(), + cx, ), Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry( @@ -1179,7 +1196,7 @@ mod tests { use serde_json::json; use settings::SettingsStore; use smol::stream::StreamExt as _; - use std::{ops::Deref, rc::Rc}; + use std::{ops::Deref, path::Path, rc::Rc}; use util::path; use workspace::{AppState, Item}; diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs new file mode 100644 index 0000000000..fc34420d4e --- /dev/null +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -0,0 +1,469 @@ +use crate::acp::completion_provider::ContextPickerCompletionProvider; +use crate::acp::completion_provider::MentionSet; +use acp_thread::MentionUri; +use agent::TextThreadStore; +use agent::ThreadStore; +use agent_client_protocol as acp; +use anyhow::Result; +use collections::HashSet; +use editor::{ + AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode, + EditorStyle, MultiBuffer, +}; +use gpui::{ + AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, Task, TextStyle, WeakEntity, +}; +use language::Buffer; +use language::Language; +use parking_lot::Mutex; +use project::{CompletionIntent, Project}; +use settings::Settings; +use std::fmt::Write; +use std::rc::Rc; +use std::sync::Arc; +use theme::ThemeSettings; +use ui::{ + ActiveTheme, App, InteractiveElement, IntoElement, ParentElement, Render, Styled, TextSize, + Window, div, +}; +use util::ResultExt; +use workspace::Workspace; +use zed_actions::agent::Chat; + +pub struct MessageEditor { + editor: Entity, + project: Entity, + thread_store: Entity, + text_thread_store: Entity, + mention_set: Arc>, +} + +pub enum MessageEditorEvent { + Send, + Cancel, +} + +impl EventEmitter for MessageEditor {} + +impl MessageEditor { + pub fn new( + workspace: WeakEntity, + project: Entity, + thread_store: Entity, + text_thread_store: Entity, + mode: EditorMode, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let language = Language::new( + language::LanguageConfig { + completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']), + ..Default::default() + }, + None, + ); + + let mention_set = Arc::new(Mutex::new(MentionSet::default())); + let editor = cx.new(|cx| { + let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + + let mut editor = Editor::new(mode, buffer, None, window, cx); + editor.set_placeholder_text("Message the agent - @ to include files", cx); + editor.set_show_indent_guides(false, cx); + editor.set_soft_wrap(); + editor.set_use_modal_editing(true); + editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new( + mention_set.clone(), + workspace, + thread_store.downgrade(), + text_thread_store.downgrade(), + cx.weak_entity(), + )))); + editor.set_context_menu_options(ContextMenuOptions { + min_entries_visible: 12, + max_entries_visible: 12, + placement: Some(ContextMenuPlacement::Above), + }); + editor + }); + + Self { + editor, + project, + mention_set, + thread_store, + text_thread_store, + } + } + + pub fn is_empty(&self, cx: &App) -> bool { + self.editor.read(cx).is_empty(cx) + } + + pub fn contents( + &self, + window: &mut Window, + cx: &mut Context, + ) -> Task>> { + let contents = self.mention_set.lock().contents( + self.project.clone(), + self.thread_store.clone(), + self.text_thread_store.clone(), + window, + cx, + ); + let editor = self.editor.clone(); + + cx.spawn(async move |_, cx| { + let contents = contents.await?; + + editor.update(cx, |editor, cx| { + let mut ix = 0; + let mut chunks: Vec = Vec::new(); + let text = editor.text(cx); + editor.display_map.update(cx, |map, cx| { + let snapshot = map.snapshot(cx); + for (crease_id, crease) in snapshot.crease_snapshot.creases() { + // Skip creases that have been edited out of the message buffer. + if !crease.range().start.is_valid(&snapshot.buffer_snapshot) { + continue; + } + + if let Some(mention) = contents.get(&crease_id) { + let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot); + if crease_range.start > ix { + chunks.push(text[ix..crease_range.start].into()); + } + chunks.push(acp::ContentBlock::Resource(acp::EmbeddedResource { + annotations: None, + resource: acp::EmbeddedResourceResource::TextResourceContents( + acp::TextResourceContents { + mime_type: None, + text: mention.content.clone(), + uri: mention.uri.to_uri().to_string(), + }, + ), + })); + ix = crease_range.end; + } + } + + if ix < text.len() { + let last_chunk = text[ix..].trim_end(); + if !last_chunk.is_empty() { + chunks.push(last_chunk.into()); + } + } + }); + + chunks + }) + }) + } + + pub fn clear(&mut self, window: &mut Window, cx: &mut Context) { + self.editor.update(cx, |editor, cx| { + editor.clear(window, cx); + editor.remove_creases(self.mention_set.lock().drain(), cx) + }); + } + + fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context) { + cx.emit(MessageEditorEvent::Send) + } + + fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { + cx.emit(MessageEditorEvent::Cancel) + } + + pub fn insert_dragged_files( + &self, + paths: Vec, + window: &mut Window, + cx: &mut Context, + ) { + let buffer = self.editor.read(cx).buffer().clone(); + let Some((&excerpt_id, _, _)) = buffer.read(cx).snapshot(cx).as_singleton() else { + return; + }; + let Some(buffer) = buffer.read(cx).as_singleton() else { + return; + }; + for path in paths { + let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else { + continue; + }; + let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else { + continue; + }; + + let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len())); + let path_prefix = abs_path + .file_name() + .unwrap_or(path.path.as_os_str()) + .display() + .to_string(); + let Some(completion) = ContextPickerCompletionProvider::completion_for_path( + path, + &path_prefix, + false, + entry.is_dir(), + excerpt_id, + anchor..anchor, + self.editor.clone(), + self.mention_set.clone(), + self.project.clone(), + cx, + ) else { + continue; + }; + + self.editor.update(cx, |message_editor, cx| { + message_editor.edit( + [( + multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), + completion.new_text, + )], + cx, + ); + }); + if let Some(confirm) = completion.confirm.clone() { + confirm(CompletionIntent::Complete, window, cx); + } + } + } + + pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context) { + self.editor.update(cx, |editor, cx| { + editor.set_mode(mode); + cx.notify() + }); + } + + pub fn set_message( + &mut self, + message: &[acp::ContentBlock], + window: &mut Window, + cx: &mut Context, + ) { + let mut text = String::new(); + let mut mentions = Vec::new(); + + for chunk in message { + match chunk { + acp::ContentBlock::Text(text_content) => { + text.push_str(&text_content.text); + } + acp::ContentBlock::Resource(acp::EmbeddedResource { + resource: acp::EmbeddedResourceResource::TextResourceContents(resource), + .. + }) => { + if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() { + let start = text.len(); + write!(&mut text, "{}", mention_uri.as_link()).ok(); + let end = text.len(); + mentions.push((start..end, mention_uri)); + } + } + acp::ContentBlock::Image(_) + | acp::ContentBlock::Audio(_) + | acp::ContentBlock::Resource(_) + | acp::ContentBlock::ResourceLink(_) => {} + } + } + + let snapshot = self.editor.update(cx, |editor, cx| { + editor.set_text(text, window, cx); + editor.buffer().read(cx).snapshot(cx) + }); + + self.mention_set.lock().clear(); + for (range, mention_uri) in mentions { + let anchor = snapshot.anchor_before(range.start); + let crease_id = crate::context_picker::insert_crease_for_mention( + anchor.excerpt_id, + anchor.text_anchor, + range.end - range.start, + mention_uri.name().into(), + mention_uri.icon_path(cx), + self.editor.clone(), + window, + cx, + ); + + if let Some(crease_id) = crease_id { + self.mention_set.lock().insert(crease_id, mention_uri); + } + } + cx.notify(); + } + + #[cfg(test)] + pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context) { + self.editor.update(cx, |editor, cx| { + editor.set_text(text, window, cx); + }); + } +} + +impl Focusable for MessageEditor { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.editor.focus_handle(cx) + } +} + +impl Render for MessageEditor { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .key_context("MessageEditor") + .on_action(cx.listener(Self::chat)) + .on_action(cx.listener(Self::cancel)) + .flex_1() + .child({ + let settings = ThemeSettings::get_global(cx); + let font_size = TextSize::Small + .rems(cx) + .to_pixels(settings.agent_font_size(cx)); + let line_height = settings.buffer_line_height.value() * font_size; + + let text_style = TextStyle { + color: cx.theme().colors().text, + font_family: settings.buffer_font.family.clone(), + font_fallbacks: settings.buffer_font.fallbacks.clone(), + font_features: settings.buffer_font.features.clone(), + font_size: font_size.into(), + line_height: line_height.into(), + ..Default::default() + }; + + EditorElement::new( + &self.editor, + EditorStyle { + background: cx.theme().colors().editor_background, + local_player: cx.theme().players().local(), + text: text_style, + syntax: cx.theme().syntax().clone(), + ..Default::default() + }, + ) + }) + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use agent::{TextThreadStore, ThreadStore}; + use agent_client_protocol as acp; + use editor::EditorMode; + use fs::FakeFs; + use gpui::{AppContext, TestAppContext}; + use lsp::{CompletionContext, CompletionTriggerKind}; + use project::{CompletionIntent, Project}; + use serde_json::json; + use util::path; + use workspace::Workspace; + + use crate::acp::{message_editor::MessageEditor, thread_view::tests::init_test}; + + #[gpui::test] + async fn test_at_mention_removal(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/project", json!({"file": ""})).await; + let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; + + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx)); + let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + + let message_editor = cx.update(|window, cx| { + cx.new(|cx| { + MessageEditor::new( + workspace.downgrade(), + project.clone(), + thread_store.clone(), + text_thread_store.clone(), + EditorMode::AutoHeight { + min_lines: 1, + max_lines: None, + }, + window, + cx, + ) + }) + }); + let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone()); + + cx.run_until_parked(); + + let excerpt_id = editor.update(cx, |editor, cx| { + editor + .buffer() + .read(cx) + .excerpt_ids() + .into_iter() + .next() + .unwrap() + }); + let completions = editor.update_in(cx, |editor, window, cx| { + editor.set_text("Hello @file ", window, cx); + let buffer = editor.buffer().read(cx).as_singleton().unwrap(); + let completion_provider = editor.completion_provider().unwrap(); + completion_provider.completions( + excerpt_id, + &buffer, + text::Anchor::MAX, + CompletionContext { + trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER, + trigger_character: Some("@".into()), + }, + window, + cx, + ) + }); + let [_, completion]: [_; 2] = completions + .await + .unwrap() + .into_iter() + .flat_map(|response| response.completions) + .collect::>() + .try_into() + .unwrap(); + + editor.update_in(cx, |editor, window, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let start = snapshot + .anchor_in_excerpt(excerpt_id, completion.replace_range.start) + .unwrap(); + let end = snapshot + .anchor_in_excerpt(excerpt_id, completion.replace_range.end) + .unwrap(); + editor.edit([(start..end, completion.new_text)], cx); + (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx); + }); + + cx.run_until_parked(); + + // Backspace over the inserted crease (and the following space). + editor.update_in(cx, |editor, window, cx| { + editor.backspace(&Default::default(), window, cx); + editor.backspace(&Default::default(), window, cx); + }); + + let content = message_editor + .update_in(cx, |message_editor, window, cx| { + message_editor.contents(window, cx) + }) + .await + .unwrap(); + + // We don't send a resource link for the deleted crease. + pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]); + } +} diff --git a/crates/agent_ui/src/acp/message_history.rs b/crates/agent_ui/src/acp/message_history.rs deleted file mode 100644 index c8280573a0..0000000000 --- a/crates/agent_ui/src/acp/message_history.rs +++ /dev/null @@ -1,88 +0,0 @@ -pub struct MessageHistory { - items: Vec, - current: Option, -} - -impl Default for MessageHistory { - fn default() -> Self { - MessageHistory { - items: Vec::new(), - current: None, - } - } -} - -impl MessageHistory { - pub fn push(&mut self, message: T) { - self.current.take(); - self.items.push(message); - } - - pub fn reset_position(&mut self) { - self.current.take(); - } - - pub fn prev(&mut self) -> Option<&T> { - if self.items.is_empty() { - return None; - } - - let new_ix = self - .current - .get_or_insert(self.items.len()) - .saturating_sub(1); - - self.current = Some(new_ix); - self.items.get(new_ix) - } - - pub fn next(&mut self) -> Option<&T> { - let current = self.current.as_mut()?; - *current += 1; - - self.items.get(*current).or_else(|| { - self.current.take(); - None - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_prev_next() { - let mut history = MessageHistory::default(); - - // Test empty history - assert_eq!(history.prev(), None); - assert_eq!(history.next(), None); - - // Add some messages - history.push("first"); - history.push("second"); - history.push("third"); - - // Test prev navigation - assert_eq!(history.prev(), Some(&"third")); - assert_eq!(history.prev(), Some(&"second")); - assert_eq!(history.prev(), Some(&"first")); - assert_eq!(history.prev(), Some(&"first")); - - assert_eq!(history.next(), Some(&"second")); - - // Test mixed navigation - history.push("fourth"); - assert_eq!(history.prev(), Some(&"fourth")); - assert_eq!(history.prev(), Some(&"third")); - assert_eq!(history.next(), Some(&"fourth")); - assert_eq!(history.next(), None); - - // Test that push resets navigation - history.prev(); - history.prev(); - history.push("fifth"); - assert_eq!(history.prev(), Some(&"fifth")); - } -} diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 5f67dc15b8..2a72cc6f48 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -12,34 +12,25 @@ use audio::{Audio, Sound}; use buffer_diff::BufferDiff; use collections::{HashMap, HashSet}; use editor::scroll::Autoscroll; -use editor::{ - AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode, - EditorStyle, MinimapVisibility, MultiBuffer, PathKey, SelectionEffects, -}; +use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, SelectionEffects}; use file_icons::FileIcons; use gpui::{ - Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId, - FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, PlatformDisplay, - SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, - Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop, - linear_gradient, list, percentage, point, prelude::*, pulsating_between, + Action, Animation, AnimationExt, App, BorderStyle, ClickEvent, EdgesRefinement, Empty, Entity, + EntityId, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, + PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, + TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, + linear_color_stop, linear_gradient, list, percentage, point, prelude::*, pulsating_between, }; +use language::Buffer; use language::language_settings::SoftWrap; -use language::{Buffer, Language}; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; -use parking_lot::Mutex; -use project::{CompletionIntent, Project}; +use project::Project; use prompt_store::PromptId; use rope::Point; use settings::{Settings as _, SettingsStore}; -use std::fmt::Write as _; -use std::path::PathBuf; -use std::{ - cell::RefCell, collections::BTreeMap, path::Path, process::ExitStatus, rc::Rc, sync::Arc, - time::Duration, -}; +use std::{collections::BTreeMap, process::ExitStatus, rc::Rc, time::Duration}; use terminal_view::TerminalView; -use text::{Anchor, BufferSnapshot}; +use text::Anchor; use theme::ThemeSettings; use ui::{ Disclosure, Divider, DividerColor, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, @@ -47,14 +38,12 @@ use ui::{ }; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, Workspace}; -use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage, ToggleModelSelector}; +use zed_actions::agent::{Chat, ToggleModelSelector}; use zed_actions::assistant::OpenRulesLibrary; use crate::acp::AcpModelSelectorPopover; -use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet}; -use crate::acp::message_history::MessageHistory; +use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; use crate::agent_diff::AgentDiff; -use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES}; use crate::ui::{AgentNotification, AgentNotificationEvent}; use crate::{ AgentDiffPane, AgentPanel, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll, @@ -62,6 +51,9 @@ use crate::{ const RESPONSE_PADDING_X: Pixels = px(19.); +pub const MIN_EDITOR_LINES: usize = 4; +pub const MAX_EDITOR_LINES: usize = 8; + pub struct AcpThreadView { agent: Rc, workspace: WeakEntity, @@ -71,11 +63,8 @@ pub struct AcpThreadView { thread_state: ThreadState, diff_editors: HashMap>, terminal_views: HashMap>, - message_editor: Entity, + message_editor: Entity, model_selector: Option>, - message_set_from_history: Option, - _message_editor_subscription: Subscription, - mention_set: Arc>, notifications: Vec>, notification_subscriptions: HashMap, Vec>, last_error: Option>, @@ -88,9 +77,16 @@ pub struct AcpThreadView { plan_expanded: bool, editor_expanded: bool, terminal_expanded: bool, - message_history: Rc>>>, + editing_message: Option, _cancel_task: Option>, - _subscriptions: [Subscription; 1], + _subscriptions: [Subscription; 2], +} + +struct EditingMessage { + index: usize, + message_id: UserMessageId, + editor: Entity, + _subscription: Subscription, } enum ThreadState { @@ -117,83 +113,30 @@ impl AcpThreadView { project: Entity, thread_store: Entity, text_thread_store: Entity, - message_history: Rc>>>, - min_lines: usize, - max_lines: Option, window: &mut Window, cx: &mut Context, ) -> Self { - let language = Language::new( - language::LanguageConfig { - completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']), - ..Default::default() - }, - None, - ); - - let mention_set = Arc::new(Mutex::new(MentionSet::default())); - let message_editor = cx.new(|cx| { - let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx)); - let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - - let mut editor = Editor::new( + MessageEditor::new( + workspace.clone(), + project.clone(), + thread_store.clone(), + text_thread_store.clone(), editor::EditorMode::AutoHeight { - min_lines, - max_lines: max_lines, + min_lines: MIN_EDITOR_LINES, + max_lines: Some(MAX_EDITOR_LINES), }, - buffer, - None, window, cx, - ); - editor.set_placeholder_text("Message the agent - @ to include files", cx); - editor.set_show_indent_guides(false, cx); - editor.set_soft_wrap(); - editor.set_use_modal_editing(true); - editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new( - mention_set.clone(), - workspace.clone(), - thread_store.downgrade(), - text_thread_store.downgrade(), - cx.weak_entity(), - )))); - editor.set_context_menu_options(ContextMenuOptions { - min_entries_visible: 12, - max_entries_visible: 12, - placement: Some(ContextMenuPlacement::Above), - }); - editor + ) }); - let message_editor_subscription = - cx.subscribe(&message_editor, |this, editor, event, cx| { - if let editor::EditorEvent::BufferEdited = &event { - let buffer = editor - .read(cx) - .buffer() - .read(cx) - .as_singleton() - .unwrap() - .read(cx) - .snapshot(); - if let Some(message) = this.message_set_from_history.clone() - && message.version() != buffer.version() - { - this.message_set_from_history = None; - } - - if this.message_set_from_history.is_none() { - this.message_history.borrow_mut().reset_position(); - } - } - }); - - let mention_set = mention_set.clone(); - let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0)); - let subscription = cx.observe_global_in::(window, Self::settings_changed); + let subscriptions = [ + cx.observe_global_in::(window, Self::settings_changed), + cx.subscribe_in(&message_editor, window, Self::on_message_editor_event), + ]; Self { agent: agent.clone(), @@ -204,9 +147,6 @@ impl AcpThreadView { thread_state: Self::initial_state(agent, workspace, project, window, cx), message_editor, model_selector: None, - message_set_from_history: None, - _message_editor_subscription: message_editor_subscription, - mention_set, notifications: Vec::new(), notification_subscriptions: HashMap::default(), diff_editors: Default::default(), @@ -217,12 +157,12 @@ impl AcpThreadView { auth_task: None, expanded_tool_calls: HashSet::default(), expanded_thinking_blocks: HashSet::default(), + editing_message: None, edits_expanded: false, plan_expanded: false, editor_expanded: false, terminal_expanded: true, - message_history, - _subscriptions: [subscription], + _subscriptions: subscriptions, _cancel_task: None, } } @@ -370,7 +310,7 @@ impl AcpThreadView { } } - pub fn cancel(&mut self, cx: &mut Context) { + pub fn cancel_generation(&mut self, cx: &mut Context) { self.last_error.take(); if let Some(thread) = self.thread() { @@ -390,193 +330,118 @@ impl AcpThreadView { fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context) { self.editor_expanded = is_expanded; - self.message_editor.update(cx, |editor, _| { - if self.editor_expanded { - editor.set_mode(EditorMode::Full { - scale_ui_elements_with_buffer_font_size: false, - show_active_line_background: false, - sized_by_content: false, - }) + self.message_editor.update(cx, |editor, cx| { + if is_expanded { + editor.set_mode( + EditorMode::Full { + scale_ui_elements_with_buffer_font_size: false, + show_active_line_background: false, + sized_by_content: false, + }, + cx, + ) } else { - editor.set_mode(EditorMode::AutoHeight { - min_lines: MIN_EDITOR_LINES, - max_lines: Some(MAX_EDITOR_LINES), - }) + editor.set_mode( + EditorMode::AutoHeight { + min_lines: MIN_EDITOR_LINES, + max_lines: Some(MAX_EDITOR_LINES), + }, + cx, + ) } }); cx.notify(); } - fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context) { + pub fn on_message_editor_event( + &mut self, + _: &Entity, + event: &MessageEditorEvent, + window: &mut Window, + cx: &mut Context, + ) { + match event { + MessageEditorEvent::Send => self.send(window, cx), + MessageEditorEvent::Cancel => self.cancel_generation(cx), + } + } + + fn send(&mut self, window: &mut Window, cx: &mut Context) { + let contents = self + .message_editor + .update(cx, |message_editor, cx| message_editor.contents(window, cx)); + self.send_impl(contents, window, cx) + } + + fn send_impl( + &mut self, + contents: Task>>, + window: &mut Window, + cx: &mut Context, + ) { self.last_error.take(); + self.editing_message.take(); - let mut ix = 0; - let mut chunks: Vec = Vec::new(); - let project = self.project.clone(); + let Some(thread) = self.thread().cloned() else { + return; + }; + let task = cx.spawn_in(window, async move |this, cx| { + let contents = contents.await?; - let thread_store = self.thread_store.clone(); - let text_thread_store = self.text_thread_store.clone(); - - let contents = - self.mention_set - .lock() - .contents(project, thread_store, text_thread_store, window, cx); - - cx.spawn_in(window, async move |this, cx| { - let contents = match contents.await { - Ok(contents) => contents, - Err(e) => { - this.update(cx, |this, cx| { - this.last_error = - Some(cx.new(|cx| Markdown::new(e.to_string().into(), None, None, cx))); - }) - .ok(); - return; - } - }; + if contents.is_empty() { + return Ok(()); + } this.update_in(cx, |this, window, cx| { - this.message_editor.update(cx, |editor, cx| { - let text = editor.text(cx); - editor.display_map.update(cx, |map, cx| { - let snapshot = map.snapshot(cx); - for (crease_id, crease) in snapshot.crease_snapshot.creases() { - // Skip creases that have been edited out of the message buffer. - if !crease.range().start.is_valid(&snapshot.buffer_snapshot) { - continue; - } - - if let Some(mention) = contents.get(&crease_id) { - let crease_range = - crease.range().to_offset(&snapshot.buffer_snapshot); - if crease_range.start > ix { - chunks.push(text[ix..crease_range.start].into()); - } - chunks.push(acp::ContentBlock::Resource(acp::EmbeddedResource { - annotations: None, - resource: acp::EmbeddedResourceResource::TextResourceContents( - acp::TextResourceContents { - mime_type: None, - text: mention.content.clone(), - uri: mention.uri.to_uri().to_string(), - }, - ), - })); - ix = crease_range.end; - } - } - - if ix < text.len() { - let last_chunk = text[ix..].trim_end(); - if !last_chunk.is_empty() { - chunks.push(last_chunk.into()); - } - } - }) - }); - - if chunks.is_empty() { - return; - } - - let Some(thread) = this.thread() else { - return; - }; - let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx)); - - cx.spawn(async move |this, cx| { - let result = task.await; - - this.update(cx, |this, cx| { - if let Err(err) = result { - this.last_error = - Some(cx.new(|cx| { - Markdown::new(err.to_string().into(), None, None, cx) - })) - } - }) - }) - .detach(); - - let mention_set = this.mention_set.clone(); - this.set_editor_is_expanded(false, cx); - - this.message_editor.update(cx, |editor, cx| { - editor.clear(window, cx); - editor.remove_creases(mention_set.lock().drain(), cx) - }); - this.scroll_to_bottom(cx); + this.message_editor.update(cx, |message_editor, cx| { + message_editor.clear(window, cx); + }); + })?; + let send = thread.update(cx, |thread, cx| thread.send(contents, cx))?; + send.await + }); - this.message_history.borrow_mut().push(chunks); - }) - .ok(); + cx.spawn(async move |this, cx| { + if let Err(e) = task.await { + this.update(cx, |this, cx| { + this.last_error = + Some(cx.new(|cx| Markdown::new(e.to_string().into(), None, None, cx))); + cx.notify() + }) + .ok(); + } }) .detach(); } - fn previous_history_message( - &mut self, - _: &PreviousHistoryMessage, - window: &mut Window, - cx: &mut Context, - ) { - if self.message_set_from_history.is_none() && !self.message_editor.read(cx).is_empty(cx) { - self.message_editor.update(cx, |editor, cx| { - editor.move_up(&Default::default(), window, cx); - }); - return; - } - - self.message_set_from_history = Self::set_draft_message( - self.message_editor.clone(), - self.mention_set.clone(), - self.project.clone(), - self.message_history - .borrow_mut() - .prev() - .map(|blocks| blocks.as_slice()), - window, - cx, - ); + fn cancel_editing(&mut self, _: &ClickEvent, _window: &mut Window, cx: &mut Context) { + self.editing_message.take(); + cx.notify(); } - fn next_history_message( - &mut self, - _: &NextHistoryMessage, - window: &mut Window, - cx: &mut Context, - ) { - if self.message_set_from_history.is_none() { - self.message_editor.update(cx, |editor, cx| { - editor.move_down(&Default::default(), window, cx); - }); + fn regenerate(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { + let Some(editing_message) = self.editing_message.take() else { return; - } - - let mut message_history = self.message_history.borrow_mut(); - let next_history = message_history.next(); - - let set_draft_message = Self::set_draft_message( - self.message_editor.clone(), - self.mention_set.clone(), - self.project.clone(), - Some( - next_history - .map(|blocks| blocks.as_slice()) - .unwrap_or_else(|| &[]), - ), - window, - cx, - ); - // If we reset the text to an empty string because we ran out of history, - // we don't want to mark it as coming from the history - self.message_set_from_history = if next_history.is_some() { - set_draft_message - } else { - None }; + + let Some(thread) = self.thread().cloned() else { + return; + }; + + let rewind = thread.update(cx, |thread, cx| { + thread.rewind(editing_message.message_id, cx) + }); + + let contents = editing_message + .editor + .update(cx, |message_editor, cx| message_editor.contents(window, cx)); + let task = cx.foreground_executor().spawn(async move { + rewind.await?; + contents.await + }); + self.send_impl(task, window, cx); } fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context) { @@ -606,92 +471,6 @@ impl AcpThreadView { }) } - fn set_draft_message( - message_editor: Entity, - mention_set: Arc>, - project: Entity, - message: Option<&[acp::ContentBlock]>, - window: &mut Window, - cx: &mut Context, - ) -> Option { - cx.notify(); - - let message = message?; - - let mut text = String::new(); - let mut mentions = Vec::new(); - - for chunk in message { - match chunk { - acp::ContentBlock::Text(text_content) => { - text.push_str(&text_content.text); - } - acp::ContentBlock::Resource(acp::EmbeddedResource { - resource: acp::EmbeddedResourceResource::TextResourceContents(resource), - .. - }) => { - let path = PathBuf::from(&resource.uri); - let project_path = project.read(cx).project_path_for_absolute_path(&path, cx); - let start = text.len(); - let _ = write!(&mut text, "{}", MentionUri::File(path).to_uri()); - let end = text.len(); - if let Some(project_path) = project_path { - let filename: SharedString = project_path - .path - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string() - .into(); - mentions.push((start..end, project_path, filename)); - } - } - acp::ContentBlock::Image(_) - | acp::ContentBlock::Audio(_) - | acp::ContentBlock::Resource(_) - | acp::ContentBlock::ResourceLink(_) => {} - } - } - - let snapshot = message_editor.update(cx, |editor, cx| { - editor.set_text(text, window, cx); - editor.buffer().read(cx).snapshot(cx) - }); - - for (range, project_path, filename) in mentions { - let crease_icon_path = if project_path.path.is_dir() { - FileIcons::get_folder_icon(false, cx) - .unwrap_or_else(|| IconName::Folder.path().into()) - } else { - FileIcons::get_icon(Path::new(project_path.path.as_ref()), cx) - .unwrap_or_else(|| IconName::File.path().into()) - }; - - let anchor = snapshot.anchor_before(range.start); - if let Some(project_path) = project.read(cx).absolute_path(&project_path, cx) { - let crease_id = crate::context_picker::insert_crease_for_mention( - anchor.excerpt_id, - anchor.text_anchor, - range.end - range.start, - filename, - crease_icon_path, - message_editor.clone(), - window, - cx, - ); - - if let Some(crease_id) = crease_id { - mention_set - .lock() - .insert(crease_id, MentionUri::File(project_path)); - } - } - } - - let snapshot = snapshot.as_singleton().unwrap().2.clone(); - Some(snapshot.text) - } - fn handle_thread_event( &mut self, thread: &Entity, @@ -968,12 +747,28 @@ impl AcpThreadView { .border_1() .border_color(cx.theme().colors().border) .text_xs() - .children(message.content.markdown().map(|md| { - self.render_markdown( - md.clone(), - user_message_markdown_style(window, cx), - ) - })), + .id("message") + .on_click(cx.listener({ + move |this, _, window, cx| this.start_editing_message(index, window, cx) + })) + .children( + if let Some(editing) = self.editing_message.as_ref() + && Some(&editing.message_id) == message.id.as_ref() + { + Some( + self.render_edit_message_editor(editing, cx) + .into_any_element(), + ) + } else { + message.content.markdown().map(|md| { + self.render_markdown( + md.clone(), + user_message_markdown_style(window, cx), + ) + .into_any_element() + }) + }, + ), ) .into_any(), AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => { @@ -1035,7 +830,7 @@ impl AcpThreadView { }; let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); - if index == total_entries - 1 && !is_generating { + let primary = if index == total_entries - 1 && !is_generating { v_flex() .w_full() .child(primary) @@ -1043,6 +838,28 @@ impl AcpThreadView { .into_any_element() } else { primary + }; + + if let Some(editing) = self.editing_message.as_ref() + && editing.index < index + { + let backdrop = div() + .id(("backdrop", index)) + .size_full() + .absolute() + .inset_0() + .bg(cx.theme().colors().panel_background) + .opacity(0.8) + .block_mouse_except_scroll() + .on_click(cx.listener(Self::cancel_editing)); + + div() + .relative() + .child(backdrop) + .child(primary) + .into_any_element() + } else { + primary } } @@ -2561,34 +2378,7 @@ impl AcpThreadView { .size_full() .pt_1() .pr_2p5() - .child(div().flex_1().child({ - let settings = ThemeSettings::get_global(cx); - let font_size = TextSize::Small - .rems(cx) - .to_pixels(settings.agent_font_size(cx)); - let line_height = settings.buffer_line_height.value() * font_size; - - let text_style = TextStyle { - color: cx.theme().colors().text, - font_family: settings.buffer_font.family.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_features: settings.buffer_font.features.clone(), - font_size: font_size.into(), - line_height: line_height.into(), - ..Default::default() - }; - - EditorElement::new( - &self.message_editor, - EditorStyle { - background: editor_bg_color, - local_player: cx.theme().players().local(), - text: text_style, - syntax: cx.theme().syntax().clone(), - ..Default::default() - }, - ) - })) + .child(self.message_editor.clone()) .child( h_flex() .absolute() @@ -2633,6 +2423,129 @@ impl AcpThreadView { .into_any() } + fn start_editing_message(&mut self, index: usize, window: &mut Window, cx: &mut Context) { + let Some(thread) = self.thread() else { + return; + }; + let Some(AgentThreadEntry::UserMessage(message)) = thread.read(cx).entries().get(index) + else { + return; + }; + let Some(message_id) = message.id.clone() else { + return; + }; + + self.list_state.scroll_to_reveal_item(index); + + let chunks = message.chunks.clone(); + let editor = cx.new(|cx| { + let mut editor = MessageEditor::new( + self.workspace.clone(), + self.project.clone(), + self.thread_store.clone(), + self.text_thread_store.clone(), + editor::EditorMode::AutoHeight { + min_lines: 1, + max_lines: None, + }, + window, + cx, + ); + editor.set_message(&chunks, window, cx); + editor + }); + let subscription = + cx.subscribe_in(&editor, window, |this, _, event, window, cx| match event { + MessageEditorEvent::Send => { + this.regenerate(&Default::default(), window, cx); + } + MessageEditorEvent::Cancel => { + this.cancel_editing(&Default::default(), window, cx); + } + }); + editor.focus_handle(cx).focus(window); + + self.editing_message.replace(EditingMessage { + index: index, + message_id: message_id.clone(), + editor, + _subscription: subscription, + }); + cx.notify(); + } + + fn render_edit_message_editor(&self, editing: &EditingMessage, cx: &Context) -> Div { + v_flex() + .w_full() + .gap_2() + .child(editing.editor.clone()) + .child( + h_flex() + .gap_1() + .child( + Icon::new(IconName::Warning) + .color(Color::Warning) + .size(IconSize::XSmall), + ) + .child( + Label::new("Editing will restart the thread from this point.") + .color(Color::Muted) + .size(LabelSize::XSmall), + ) + .child(self.render_editing_message_editor_buttons(editing, cx)), + ) + } + + fn render_editing_message_editor_buttons( + &self, + editing: &EditingMessage, + cx: &Context, + ) -> Div { + h_flex() + .gap_0p5() + .flex_1() + .justify_end() + .child( + IconButton::new("cancel-edit-message", IconName::Close) + .shape(ui::IconButtonShape::Square) + .icon_color(Color::Error) + .icon_size(IconSize::Small) + .tooltip({ + let focus_handle = editing.editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Cancel Edit", + &menu::Cancel, + &focus_handle, + window, + cx, + ) + } + }) + .on_click(cx.listener(Self::cancel_editing)), + ) + .child( + IconButton::new("confirm-edit-message", IconName::Return) + .disabled(editing.editor.read(cx).is_empty(cx)) + .shape(ui::IconButtonShape::Square) + .icon_color(Color::Muted) + .icon_size(IconSize::Small) + .tooltip({ + let focus_handle = editing.editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Regenerate", + &menu::Confirm, + &focus_handle, + window, + cx, + ) + } + }) + .on_click(cx.listener(Self::regenerate)), + ) + } + fn render_send_button(&self, cx: &mut Context) -> AnyElement { if self.thread().map_or(true, |thread| { thread.read(cx).status() == ThreadStatus::Idle @@ -2649,7 +2562,7 @@ impl AcpThreadView { button.tooltip(Tooltip::text("Type a message to submit")) }) .on_click(cx.listener(|this, _, window, cx| { - this.chat(&Chat, window, cx); + this.send(window, cx); })) .into_any_element() } else { @@ -2659,7 +2572,7 @@ impl AcpThreadView { .tooltip(move |window, cx| { Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx) }) - .on_click(cx.listener(|this, _event, _, cx| this.cancel(cx))) + .on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx))) .into_any_element() } } @@ -2723,10 +2636,10 @@ impl AcpThreadView { if let Some(mention) = MentionUri::parse(&url).log_err() { workspace.update(cx, |workspace, cx| match mention { - MentionUri::File(path) => { + MentionUri::File { abs_path, .. } => { let project = workspace.project(); let Some((path, entry)) = project.update(cx, |project, cx| { - let path = project.find_project_path(path, cx)?; + let path = project.find_project_path(abs_path, cx)?; let entry = project.entry_for_path(&path, cx)?; Some((path, entry)) }) else { @@ -3175,57 +3088,11 @@ impl AcpThreadView { paths: Vec, _added_worktrees: Vec>, window: &mut Window, - cx: &mut Context<'_, Self>, + cx: &mut Context, ) { - let buffer = self.message_editor.read(cx).buffer().clone(); - let Some((&excerpt_id, _, _)) = buffer.read(cx).snapshot(cx).as_singleton() else { - return; - }; - let Some(buffer) = buffer.read(cx).as_singleton() else { - return; - }; - for path in paths { - let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else { - continue; - }; - let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else { - continue; - }; - - let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len())); - let path_prefix = abs_path - .file_name() - .unwrap_or(path.path.as_os_str()) - .display() - .to_string(); - let Some(completion) = ContextPickerCompletionProvider::completion_for_path( - path, - &path_prefix, - false, - entry.is_dir(), - excerpt_id, - anchor..anchor, - self.message_editor.clone(), - self.mention_set.clone(), - self.project.clone(), - cx, - ) else { - continue; - }; - - self.message_editor.update(cx, |message_editor, cx| { - message_editor.edit( - [( - multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), - completion.new_text, - )], - cx, - ); - }); - if let Some(confirm) = completion.confirm.clone() { - confirm(CompletionIntent::Complete, window, cx); - } - } + self.message_editor.update(cx, |message_editor, cx| { + message_editor.insert_dragged_files(paths, window, cx); + }) } } @@ -3242,9 +3109,6 @@ impl Render for AcpThreadView { v_flex() .size_full() .key_context("AcpThread") - .on_action(cx.listener(Self::chat)) - .on_action(cx.listener(Self::previous_history_message)) - .on_action(cx.listener(Self::next_history_message)) .on_action(cx.listener(Self::open_agent_diff)) .bg(cx.theme().colors().panel_background) .child(match &self.thread_state { @@ -3540,13 +3404,16 @@ fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { } #[cfg(test)] -mod tests { +pub(crate) mod tests { + use std::{path::Path, sync::Arc}; + use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::SessionId; use editor::EditorSettings; use fs::FakeFs; use futures::future::try_join_all; use gpui::{SemanticVersion, TestAppContext, VisualTestContext}; + use parking_lot::Mutex; use rand::Rng; use settings::SettingsStore; @@ -3576,7 +3443,7 @@ mod tests { cx.deactivate_window(); thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.chat(&Chat, window, cx); + thread_view.send(window, cx); }); cx.run_until_parked(); @@ -3603,7 +3470,7 @@ mod tests { cx.deactivate_window(); thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.chat(&Chat, window, cx); + thread_view.send(window, cx); }); cx.run_until_parked(); @@ -3649,7 +3516,7 @@ mod tests { cx.deactivate_window(); thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.chat(&Chat, window, cx); + thread_view.send(window, cx); }); cx.run_until_parked(); @@ -3683,9 +3550,6 @@ mod tests { project, thread_store.clone(), text_thread_store.clone(), - Rc::new(RefCell::new(MessageHistory::default())), - 1, - None, window, cx, ) @@ -3899,7 +3763,7 @@ mod tests { } } - fn init_test(cx: &mut TestAppContext) { + pub(crate) fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index e47cbe3714..73915195f5 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1,4 +1,3 @@ -use std::cell::RefCell; use std::ops::{Not, Range}; use std::path::Path; use std::rc::Rc; @@ -11,7 +10,6 @@ use serde::{Deserialize, Serialize}; use crate::NewExternalAgentThread; use crate::agent_diff::AgentDiffThread; -use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES}; use crate::{ AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, @@ -477,8 +475,6 @@ pub struct AgentPanel { configuration_subscription: Option, local_timezone: UtcOffset, active_view: ActiveView, - acp_message_history: - Rc>>>, previous_view: Option, history_store: Entity, history: Entity, @@ -766,7 +762,6 @@ impl AgentPanel { .unwrap(), inline_assist_context_store, previous_view: None, - acp_message_history: Default::default(), history_store: history_store.clone(), history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)), hovered_recent_history_item: None, @@ -824,7 +819,9 @@ impl AgentPanel { thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx)); } ActiveView::ExternalAgentThread { thread_view, .. } => { - thread_view.update(cx, |thread_element, cx| thread_element.cancel(cx)); + thread_view.update(cx, |thread_element, cx| { + thread_element.cancel_generation(cx) + }); } ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} } @@ -963,7 +960,6 @@ impl AgentPanel { ) { let workspace = self.workspace.clone(); let project = self.project.clone(); - let message_history = self.acp_message_history.clone(); let fs = self.fs.clone(); const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent"; @@ -1016,9 +1012,6 @@ impl AgentPanel { project, thread_store.clone(), text_thread_store.clone(), - message_history, - MIN_EDITOR_LINES, - Some(MAX_EDITOR_LINES), window, cx, ) @@ -1575,8 +1568,6 @@ impl AgentPanel { self.active_view = new_view; } - self.acp_message_history.borrow_mut().reset_position(); - self.focus_handle(cx).focus(window); } diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 64891b6973..9455369e9a 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -285,10 +285,6 @@ pub mod agent { ResetOnboarding, /// Starts a chat conversation with the agent. Chat, - /// Displays the previous message in the history. - PreviousHistoryMessage, - /// Displays the next message in the history. - NextHistoryMessage, /// Toggles the language model selector dropdown. #[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])] ToggleModelSelector From ba2c45bc53194d3e2b94d909966a06f213017de5 Mon Sep 17 00:00:00 2001 From: David Kleingeld Date: Thu, 14 Aug 2025 17:02:51 +0200 Subject: [PATCH 343/693] Add FutureExt::with_timeout and use it for for Room::maintain_connection (#36175) Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra --- crates/call/src/call_impl/room.rs | 86 ++++++++++++++--------------- crates/gpui/src/app/test_context.rs | 4 +- crates/gpui/src/gpui.rs | 2 +- crates/gpui/src/util.rs | 73 ++++++++++++++++++++---- 4 files changed, 105 insertions(+), 60 deletions(-) diff --git a/crates/call/src/call_impl/room.rs b/crates/call/src/call_impl/room.rs index afeee4c924..73cb8518a6 100644 --- a/crates/call/src/call_impl/room.rs +++ b/crates/call/src/call_impl/room.rs @@ -10,10 +10,10 @@ use client::{ }; use collections::{BTreeMap, HashMap, HashSet}; use fs::Fs; -use futures::{FutureExt, StreamExt}; +use futures::StreamExt; use gpui::{ - App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, ScreenCaptureSource, - ScreenCaptureStream, Task, WeakEntity, + App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FutureExt as _, + ScreenCaptureSource, ScreenCaptureStream, Task, Timeout, WeakEntity, }; use gpui_tokio::Tokio; use language::LanguageRegistry; @@ -370,57 +370,53 @@ impl Room { })?; // Wait for client to re-establish a connection to the server. - { - let mut reconnection_timeout = - cx.background_executor().timer(RECONNECT_TIMEOUT).fuse(); - let client_reconnection = async { - let mut remaining_attempts = 3; - while remaining_attempts > 0 { - if client_status.borrow().is_connected() { - log::info!("client reconnected, attempting to rejoin room"); + let executor = cx.background_executor().clone(); + let client_reconnection = async { + let mut remaining_attempts = 3; + while remaining_attempts > 0 { + if client_status.borrow().is_connected() { + log::info!("client reconnected, attempting to rejoin room"); - let Some(this) = this.upgrade() else { break }; - match this.update(cx, |this, cx| this.rejoin(cx)) { - Ok(task) => { - if task.await.log_err().is_some() { - return true; - } else { - remaining_attempts -= 1; - } + let Some(this) = this.upgrade() else { break }; + match this.update(cx, |this, cx| this.rejoin(cx)) { + Ok(task) => { + if task.await.log_err().is_some() { + return true; + } else { + remaining_attempts -= 1; } - Err(_app_dropped) => return false, } - } else if client_status.borrow().is_signed_out() { - return false; + Err(_app_dropped) => return false, } - - log::info!( - "waiting for client status change, remaining attempts {}", - remaining_attempts - ); - client_status.next().await; + } else if client_status.borrow().is_signed_out() { + return false; } - false + + log::info!( + "waiting for client status change, remaining attempts {}", + remaining_attempts + ); + client_status.next().await; } - .fuse(); - futures::pin_mut!(client_reconnection); + false + }; - futures::select_biased! { - reconnected = client_reconnection => { - if reconnected { - log::info!("successfully reconnected to room"); - // If we successfully joined the room, go back around the loop - // waiting for future connection status changes. - continue; - } - } - _ = reconnection_timeout => { - log::info!("room reconnection timeout expired"); - } + match client_reconnection + .with_timeout(RECONNECT_TIMEOUT, &executor) + .await + { + Ok(true) => { + log::info!("successfully reconnected to room"); + // If we successfully joined the room, go back around the loop + // waiting for future connection status changes. + continue; + } + Ok(false) => break, + Err(Timeout) => { + log::info!("room reconnection timeout expired"); + break; } } - - break; } } diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 35e6032671..a96c24432a 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -585,7 +585,7 @@ impl Entity { cx.executor().advance_clock(advance_clock_by); async move { - let notification = crate::util::timeout(duration, rx.recv()) + let notification = crate::util::smol_timeout(duration, rx.recv()) .await .expect("next notification timed out"); drop(subscription); @@ -629,7 +629,7 @@ impl Entity { let handle = self.downgrade(); async move { - crate::util::timeout(Duration::from_secs(1), async move { + crate::util::smol_timeout(Duration::from_secs(1), async move { loop { { let cx = cx.borrow(); diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 09799eb910..f0ce04a915 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -157,7 +157,7 @@ pub use taffy::{AvailableSpace, LayoutId}; #[cfg(any(test, feature = "test-support"))] pub use test::*; pub use text_system::*; -pub use util::arc_cow::ArcCow; +pub use util::{FutureExt, Timeout, arc_cow::ArcCow}; pub use view::*; pub use window::*; diff --git a/crates/gpui/src/util.rs b/crates/gpui/src/util.rs index 5e92335fdc..f357034fbf 100644 --- a/crates/gpui/src/util.rs +++ b/crates/gpui/src/util.rs @@ -1,13 +1,11 @@ -use std::sync::atomic::AtomicUsize; -use std::sync::atomic::Ordering::SeqCst; -#[cfg(any(test, feature = "test-support"))] -use std::time::Duration; - -#[cfg(any(test, feature = "test-support"))] -use futures::Future; - -#[cfg(any(test, feature = "test-support"))] -use smol::future::FutureExt; +use crate::{BackgroundExecutor, Task}; +use std::{ + future::Future, + pin::Pin, + sync::atomic::{AtomicUsize, Ordering::SeqCst}, + task, + time::Duration, +}; pub use util::*; @@ -70,8 +68,59 @@ pub trait FluentBuilder { } } +/// Extensions for Future types that provide additional combinators and utilities. +pub trait FutureExt { + /// Requires a Future to complete before the specified duration has elapsed. + /// Similar to tokio::timeout. + fn with_timeout(self, timeout: Duration, executor: &BackgroundExecutor) -> WithTimeout + where + Self: Sized; +} + +impl FutureExt for T { + fn with_timeout(self, timeout: Duration, executor: &BackgroundExecutor) -> WithTimeout + where + Self: Sized, + { + WithTimeout { + future: self, + timer: executor.timer(timeout), + } + } +} + +pub struct WithTimeout { + future: T, + timer: Task<()>, +} + +#[derive(Debug, thiserror::Error)] +#[error("Timed out before future resolved")] +/// Error returned by with_timeout when the timeout duration elapsed before the future resolved +pub struct Timeout; + +impl Future for WithTimeout { + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut task::Context) -> task::Poll { + // SAFETY: the fields of Timeout are private and we never move the future ourselves + // And its already pinned since we are being polled (all futures need to be pinned to be polled) + let this = unsafe { self.get_unchecked_mut() }; + let future = unsafe { Pin::new_unchecked(&mut this.future) }; + let timer = unsafe { Pin::new_unchecked(&mut this.timer) }; + + if let task::Poll::Ready(output) = future.poll(cx) { + task::Poll::Ready(Ok(output)) + } else if timer.poll(cx).is_ready() { + task::Poll::Ready(Err(Timeout)) + } else { + task::Poll::Pending + } + } +} + #[cfg(any(test, feature = "test-support"))] -pub async fn timeout(timeout: Duration, f: F) -> Result +pub async fn smol_timeout(timeout: Duration, f: F) -> Result where F: Future, { @@ -80,7 +129,7 @@ where Err(()) }; let future = async move { Ok(f.await) }; - timer.race(future).await + smol::future::FutureExt::race(timer, future).await } /// Increment the given atomic counter if it is not zero. From f514c7cc187eeb814415d0e78546ac780c857900 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 14 Aug 2025 11:22:38 -0400 Subject: [PATCH 344/693] Emit a `BreadcrumbsChanged` event when associated settings changed (#36177) Closes https://github.com/zed-industries/zed/issues/36149 Release Notes: - Fixed a bug where changing the `toolbar.breadcrumbs` setting didn't immediately update the UI when saving the `settings.json` file. --- crates/editor/src/editor.rs | 6 ++++++ crates/editor/src/items.rs | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cbee9021ed..689f397341 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -20209,6 +20209,7 @@ impl Editor { ); let old_cursor_shape = self.cursor_shape; + let old_show_breadcrumbs = self.show_breadcrumbs; { let editor_settings = EditorSettings::get_global(cx); @@ -20222,6 +20223,10 @@ impl Editor { cx.emit(EditorEvent::CursorShapeChanged); } + if old_show_breadcrumbs != self.show_breadcrumbs { + cx.emit(EditorEvent::BreadcrumbsChanged); + } + let project_settings = ProjectSettings::get_global(cx); self.serialize_dirty_buffers = !self.mode.is_minimap() && project_settings.session.restore_unsaved_buffers; @@ -22843,6 +22848,7 @@ pub enum EditorEvent { }, Reloaded, CursorShapeChanged, + BreadcrumbsChanged, PushedToNavHistory { anchor: Anchor, is_deactivate: bool, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 1da82c605d..480757a491 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1036,6 +1036,10 @@ impl Item for Editor { f(ItemEvent::UpdateBreadcrumbs); } + EditorEvent::BreadcrumbsChanged => { + f(ItemEvent::UpdateBreadcrumbs); + } + EditorEvent::DirtyChanged => { f(ItemEvent::UpdateTab); } From 528d56e8072048b9b588fd60786c937be018f94d Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Thu, 14 Aug 2025 10:29:58 -0500 Subject: [PATCH 345/693] keymap_ui: Add open keymap JSON button (#36182) Closes #ISSUE Release Notes: - Keymap Editor: Added a button in the top left to allow opening the keymap JSON file. Right clicking the button provides shortcuts to opening the default Zed and Vim keymaps as well. --- Cargo.lock | 2 ++ assets/icons/json.svg | 4 ++++ crates/icons/src/icons.rs | 1 + crates/settings_ui/Cargo.toml | 2 ++ crates/settings_ui/src/keybindings.rs | 29 ++++++++++++++++++++++++++- 5 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 assets/icons/json.svg diff --git a/Cargo.lock b/Cargo.lock index cb087f43b7..96cc1581a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15054,8 +15054,10 @@ dependencies = [ "ui", "ui_input", "util", + "vim", "workspace", "workspace-hack", + "zed_actions", ] [[package]] diff --git a/assets/icons/json.svg b/assets/icons/json.svg new file mode 100644 index 0000000000..5f012f8838 --- /dev/null +++ b/assets/icons/json.svg @@ -0,0 +1,4 @@ + + + + diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index f5c2a83fec..8bd76cbecf 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -140,6 +140,7 @@ pub enum IconName { Image, Indicator, Info, + Json, Keyboard, Library, LineHeight, diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index a4c47081c6..8a151359ec 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -42,8 +42,10 @@ tree-sitter-rust.workspace = true ui.workspace = true ui_input.workspace = true util.workspace = true +vim.workspace = true workspace-hack.workspace = true workspace.workspace = true +zed_actions.workspace = true [dev-dependencies] db = {"workspace"= true, "features" = ["test-support"]} diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index a62c669488..1aaab211aa 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -23,7 +23,7 @@ use settings::{BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAss use ui::{ ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Indicator, Modal, ModalFooter, ModalHeader, ParentElement as _, Render, Section, SharedString, - Styled as _, Tooltip, Window, prelude::*, + Styled as _, Tooltip, Window, prelude::*, right_click_menu, }; use ui_input::SingleLineInput; use util::ResultExt; @@ -1536,6 +1536,33 @@ impl Render for KeymapEditor { .child( h_flex() .gap_2() + .child( + right_click_menu("open-keymap-menu") + .menu(|window, cx| { + ContextMenu::build(window, cx, |menu, _, _| { + menu.header("Open Keymap JSON") + .action("User", zed_actions::OpenKeymap.boxed_clone()) + .action("Zed Default", zed_actions::OpenDefaultKeymap.boxed_clone()) + .action("Vim Default", vim::OpenDefaultKeymap.boxed_clone()) + }) + }) + .anchor(gpui::Corner::TopLeft) + .trigger(|open, _, _| + IconButton::new( + "OpenKeymapJsonButton", + IconName::Json + ) + .shape(ui::IconButtonShape::Square) + .when(!open, |this| + this.tooltip(move |window, cx| { + Tooltip::with_meta("Open Keymap JSON", Some(&zed_actions::OpenKeymap),"Right click to view more options", window, cx) + }) + ) + .on_click(|_, window, cx| { + window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx); + }) + ) + ) .child( div() .key_context({ From 20be133713690fd92148a448bd57146fff73cbce Mon Sep 17 00:00:00 2001 From: Romans Malinovskis Date: Thu, 14 Aug 2025 18:04:01 +0100 Subject: [PATCH 346/693] helix: Allow yank without a selection (#35612) Related https://github.com/zed-industries/zed/issues/4642 Release Notes: - Helix: without active selection, pressing `y` in helix mode will yank a single character under cursor. --------- Co-authored-by: Conrad Irwin --- assets/keymaps/vim.json | 2 +- crates/vim/src/helix.rs | 69 +++++++++++++++++++++++++ crates/vim/src/test/vim_test_context.rs | 30 +++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 98f9cafc40..a3f68a7730 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -390,7 +390,7 @@ "right": "vim::WrappingRight", "h": "vim::WrappingLeft", "l": "vim::WrappingRight", - "y": "editor::Copy", + "y": "vim::HelixYank", "alt-;": "vim::OtherEnd", "ctrl-r": "vim::Redo", "f": ["vim::PushFindForward", { "before": false, "multiline": true }], diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 686c74f65e..29633ddef9 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -15,6 +15,8 @@ actions!( [ /// Switches to normal mode after the cursor (Helix-style). HelixNormalAfter, + /// Yanks the current selection or character if no selection. + HelixYank, /// Inserts at the beginning of the selection. HelixInsert, /// Appends at the end of the selection. @@ -26,6 +28,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, Vim::helix_normal_after); Vim::action(editor, cx, Vim::helix_insert); Vim::action(editor, cx, Vim::helix_append); + Vim::action(editor, cx, Vim::helix_yank); } impl Vim { @@ -310,6 +313,47 @@ impl Vim { } } + pub fn helix_yank(&mut self, _: &HelixYank, window: &mut Window, cx: &mut Context) { + self.update_editor(cx, |vim, editor, cx| { + let has_selection = editor + .selections + .all_adjusted(cx) + .iter() + .any(|selection| !selection.is_empty()); + + if !has_selection { + // If no selection, expand to current character (like 'v' does) + editor.change_selections(Default::default(), window, cx, |s| { + s.move_with(|map, selection| { + let head = selection.head(); + let new_head = movement::saturating_right(map, head); + selection.set_tail(head, SelectionGoal::None); + selection.set_head(new_head, SelectionGoal::None); + }); + }); + vim.yank_selections_content( + editor, + crate::motion::MotionKind::Exclusive, + window, + cx, + ); + editor.change_selections(Default::default(), window, cx, |s| { + s.move_with(|_map, selection| { + selection.collapse_to(selection.start, SelectionGoal::None); + }); + }); + } else { + // Yank the selection(s) + vim.yank_selections_content( + editor, + crate::motion::MotionKind::Exclusive, + window, + cx, + ); + } + }); + } + fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context) { self.start_recording(cx); self.update_editor(cx, |_, editor, cx| { @@ -703,4 +747,29 @@ mod test { cx.assert_state("«xxˇ»", Mode::HelixNormal); } + + #[gpui::test] + async fn test_helix_yank(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); + + // Test yanking current character with no selection + cx.set_state("hello ˇworld", Mode::HelixNormal); + cx.simulate_keystrokes("y"); + + // Test cursor remains at the same position after yanking single character + cx.assert_state("hello ˇworld", Mode::HelixNormal); + cx.shared_clipboard().assert_eq("w"); + + // Move cursor and yank another character + cx.simulate_keystrokes("l"); + cx.simulate_keystrokes("y"); + cx.shared_clipboard().assert_eq("o"); + + // Test yanking with existing selection + cx.set_state("hello «worlˇ»d", Mode::HelixNormal); + cx.simulate_keystrokes("y"); + cx.shared_clipboard().assert_eq("worl"); + cx.assert_state("hello «worlˇ»d", Mode::HelixNormal); + } } diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 904e48e5a3..5b6cb55e8c 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -143,6 +143,16 @@ impl VimTestContext { }) } + pub fn enable_helix(&mut self) { + self.cx.update(|_, cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings::(cx, |s| { + *s = Some(true) + }); + }); + }) + } + pub fn mode(&mut self) -> Mode { self.update_editor(|editor, _, cx| editor.addon::().unwrap().entity.read(cx).mode) } @@ -210,6 +220,26 @@ impl VimTestContext { assert_eq!(self.mode(), Mode::Normal, "{}", self.assertion_context()); assert_eq!(self.active_operator(), None, "{}", self.assertion_context()); } + + pub fn shared_clipboard(&mut self) -> VimClipboard { + VimClipboard { + editor: self + .read_from_clipboard() + .map(|item| item.text().unwrap().to_string()) + .unwrap_or_default(), + } + } +} + +pub struct VimClipboard { + editor: String, +} + +impl VimClipboard { + #[track_caller] + pub fn assert_eq(&self, expected: &str) { + assert_eq!(self.editor, expected); + } } impl Deref for VimTestContext { From 9a2b7ef372021e5bcad759a2dc871e0743b602c4 Mon Sep 17 00:00:00 2001 From: fantacell Date: Thu, 14 Aug 2025 19:04:07 +0200 Subject: [PATCH 347/693] helix: Change f and t motions (#35216) In vim and zed (vim and helix modes) typing "tx" will jump before the next `x`, but typing it again won't do anything. But in helix the cursor just jumps before the `x` after that. I added that in helix mode. This also solves another small issue where the selection doesn't include the first `x` after typing "fx" twice. And similarly after typing "Fx" or "Tx" the selection should include the character that the motion startet on. Release Notes: - helix: Fixed inconsistencies in the "f" and "t" motions --- crates/text/src/selection.rs | 13 ++ crates/vim/src/helix.rs | 290 ++++++++++++++++++----------------- crates/vim/src/motion.rs | 3 +- 3 files changed, 162 insertions(+), 144 deletions(-) diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index 18b82dbb6a..d3c280bde8 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -104,6 +104,19 @@ impl Selection { self.goal = new_goal; } + pub fn set_head_tail(&mut self, head: T, tail: T, new_goal: SelectionGoal) { + if head < tail { + self.reversed = true; + self.start = head; + self.end = tail; + } else { + self.reversed = false; + self.start = tail; + self.end = head; + } + self.goal = new_goal; + } + pub fn swap_head_tail(&mut self) { if self.reversed { self.reversed = false; diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 29633ddef9..0c8c06d8ab 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -1,9 +1,11 @@ +use editor::display_map::DisplaySnapshot; use editor::{DisplayPoint, Editor, SelectionEffects, ToOffset, ToPoint, movement}; use gpui::{Action, actions}; use gpui::{Context, Window}; use language::{CharClassifier, CharKind}; use text::{Bias, SelectionGoal}; +use crate::motion; use crate::{ Vim, motion::{Motion, right}, @@ -58,6 +60,35 @@ impl Vim { self.helix_move_cursor(motion, times, window, cx); } + /// Updates all selections based on where the cursors are. + fn helix_new_selections( + &mut self, + window: &mut Window, + cx: &mut Context, + mut change: impl FnMut( + // the start of the cursor + DisplayPoint, + &DisplaySnapshot, + ) -> Option<(DisplayPoint, DisplayPoint)>, + ) { + self.update_editor(cx, |_, editor, cx| { + editor.change_selections(Default::default(), window, cx, |s| { + s.move_with(|map, selection| { + let cursor_start = if selection.reversed || selection.is_empty() { + selection.head() + } else { + movement::left(map, selection.head()) + }; + let Some((head, tail)) = change(cursor_start, map) else { + return; + }; + + selection.set_head_tail(head, tail, SelectionGoal::None); + }); + }); + }); + } + fn helix_find_range_forward( &mut self, times: Option, @@ -65,49 +96,30 @@ impl Vim { cx: &mut Context, mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool, ) { - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let times = times.unwrap_or(1); - let new_goal = SelectionGoal::None; - let mut head = selection.head(); - let mut tail = selection.tail(); + let times = times.unwrap_or(1); + self.helix_new_selections(window, cx, |cursor, map| { + let mut head = movement::right(map, cursor); + let mut tail = cursor; + let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map)); + if head == map.max_point() { + return None; + } + for _ in 0..times { + let (maybe_next_tail, next_head) = + movement::find_boundary_trail(map, head, |left, right| { + is_boundary(left, right, &classifier) + }); - if head == map.max_point() { - return; - } + if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail { + break; + } - // collapse to block cursor - if tail < head { - tail = movement::left(map, head); - } else { - tail = head; - head = movement::right(map, head); - } - - // create a classifier - let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map)); - - for _ in 0..times { - let (maybe_next_tail, next_head) = - movement::find_boundary_trail(map, head, |left, right| { - is_boundary(left, right, &classifier) - }); - - if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail { - break; - } - - head = next_head; - if let Some(next_tail) = maybe_next_tail { - tail = next_tail; - } - } - - selection.set_tail(tail, new_goal); - selection.set_head(head, new_goal); - }); - }); + head = next_head; + if let Some(next_tail) = maybe_next_tail { + tail = next_tail; + } + } + Some((head, tail)) }); } @@ -118,56 +130,33 @@ impl Vim { cx: &mut Context, mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool, ) { - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let times = times.unwrap_or(1); - let new_goal = SelectionGoal::None; - let mut head = selection.head(); - let mut tail = selection.tail(); + let times = times.unwrap_or(1); + self.helix_new_selections(window, cx, |cursor, map| { + let mut head = cursor; + // The original cursor was one character wide, + // but the search starts from the left side of it, + // so to include that space the selection must end one character to the right. + let mut tail = movement::right(map, cursor); + let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map)); + if head == DisplayPoint::zero() { + return None; + } + for _ in 0..times { + let (maybe_next_tail, next_head) = + movement::find_preceding_boundary_trail(map, head, |left, right| { + is_boundary(left, right, &classifier) + }); - if head == DisplayPoint::zero() { - return; - } + if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail { + break; + } - // collapse to block cursor - if tail < head { - tail = movement::left(map, head); - } else { - tail = head; - head = movement::right(map, head); - } - - selection.set_head(head, new_goal); - selection.set_tail(tail, new_goal); - // flip the selection - selection.swap_head_tail(); - head = selection.head(); - tail = selection.tail(); - - // create a classifier - let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map)); - - for _ in 0..times { - let (maybe_next_tail, next_head) = - movement::find_preceding_boundary_trail(map, head, |left, right| { - is_boundary(left, right, &classifier) - }); - - if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail { - break; - } - - head = next_head; - if let Some(next_tail) = maybe_next_tail { - tail = next_tail; - } - } - - selection.set_tail(tail, new_goal); - selection.set_head(head, new_goal); - }); - }) + head = next_head; + if let Some(next_tail) = maybe_next_tail { + tail = next_tail; + } + } + Some((head, tail)) }); } @@ -255,58 +244,53 @@ impl Vim { found }) } - Motion::FindForward { .. } => { - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let goal = selection.goal; - let cursor = if selection.is_empty() || selection.reversed { - selection.head() - } else { - movement::left(map, selection.head()) - }; - - let (point, goal) = motion - .move_point( - map, - cursor, - selection.goal, - times, - &text_layout_details, - ) - .unwrap_or((cursor, goal)); - selection.set_tail(selection.head(), goal); - selection.set_head(movement::right(map, point), goal); - }) - }); + Motion::FindForward { + before, + char, + mode, + smartcase, + } => { + self.helix_new_selections(window, cx, |cursor, map| { + let start = cursor; + let mut last_boundary = start; + for _ in 0..times.unwrap_or(1) { + last_boundary = movement::find_boundary( + map, + movement::right(map, last_boundary), + mode, + |left, right| { + let current_char = if before { right } else { left }; + motion::is_character_match(char, current_char, smartcase) + }, + ); + } + Some((last_boundary, start)) }); } - Motion::FindBackward { .. } => { - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let goal = selection.goal; - let cursor = if selection.is_empty() || selection.reversed { - selection.head() - } else { - movement::left(map, selection.head()) - }; - - let (point, goal) = motion - .move_point( - map, - cursor, - selection.goal, - times, - &text_layout_details, - ) - .unwrap_or((cursor, goal)); - selection.set_tail(selection.head(), goal); - selection.set_head(point, goal); - }) - }); + Motion::FindBackward { + after, + char, + mode, + smartcase, + } => { + self.helix_new_selections(window, cx, |cursor, map| { + let start = cursor; + let mut last_boundary = start; + for _ in 0..times.unwrap_or(1) { + last_boundary = movement::find_preceding_boundary_display_point( + map, + last_boundary, + mode, + |left, right| { + let current_char = if after { left } else { right }; + motion::is_character_match(char, current_char, smartcase) + }, + ); + } + // The original cursor was one character wide, + // but the search started from the left side of it, + // so to include that space the selection must end one character to the right. + Some((last_boundary, movement::right(map, start))) }); } _ => self.helix_move_and_collapse(motion, times, window, cx), @@ -630,13 +614,33 @@ mod test { Mode::HelixNormal, ); - cx.simulate_keystrokes("2 T r"); + cx.simulate_keystrokes("F e F e"); cx.assert_state( indoc! {" - The quick br«ˇown - fox jumps over - the laz»y dog."}, + The quick brown + fox jumps ov«ˇer + the» lazy dog."}, + Mode::HelixNormal, + ); + + cx.simulate_keystrokes("e 2 F e"); + + cx.assert_state( + indoc! {" + Th«ˇe quick brown + fox jumps over» + the lazy dog."}, + Mode::HelixNormal, + ); + + cx.simulate_keystrokes("t r t r"); + + cx.assert_state( + indoc! {" + The quick «brown + fox jumps oveˇ»r + the lazy dog."}, Mode::HelixNormal, ); } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 7ef883f406..a6a07e7b2f 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -2639,7 +2639,8 @@ fn find_backward( } } -fn is_character_match(target: char, other: char, smartcase: bool) -> bool { +/// Returns true if one char is equal to the other or its uppercase variant (if smartcase is true). +pub fn is_character_match(target: char, other: char, smartcase: bool) -> bool { if smartcase { if target.is_uppercase() { target == other From 5a9546ff4badfb2c153663d51c41297f60ed25bc Mon Sep 17 00:00:00 2001 From: Mostafa Khaled <112074172+m04f@users.noreply.github.com> Date: Thu, 14 Aug 2025 20:04:38 +0300 Subject: [PATCH 348/693] Add alt-s to helix mode (#33918) Closes #31562 Release Notes: - Helix: bind alt-s to SplitSelectionIntoLines --------- Co-authored-by: Ben Kunkle --- assets/keymaps/vim.json | 1 + crates/editor/src/actions.rs | 12 ++++++++++-- crates/editor/src/editor.rs | 30 ++++++++++++++++++++++++++---- crates/editor/src/editor_tests.rs | 6 +++--- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index a3f68a7730..560ca3bdd8 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -407,6 +407,7 @@ "g w": "vim::PushRewrap", "insert": "vim::InsertBefore", "alt-.": "vim::RepeatFind", + "alt-s": ["editor::SplitSelectionIntoLines", { "keep_selections": true }], // tree-sitter related commands "[ x": "editor::SelectLargerSyntaxNode", "] x": "editor::SelectSmallerSyntaxNode", diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 39433b3c27..ce02c4d2bf 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -273,6 +273,16 @@ pub enum UuidVersion { V7, } +/// Splits selection into individual lines. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] +#[serde(deny_unknown_fields)] +pub struct SplitSelectionIntoLines { + /// Keep the text selected after splitting instead of collapsing to cursors. + #[serde(default)] + pub keep_selections: bool, +} + /// Goes to the next diagnostic in the file. #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = editor)] @@ -672,8 +682,6 @@ actions!( SortLinesCaseInsensitive, /// Sorts selected lines case-sensitively. SortLinesCaseSensitive, - /// Splits selection into individual lines. - SplitSelectionIntoLines, /// Stops the language server for the current file. StopLanguageServer, /// Switches between source and header files. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 689f397341..1f350cf0d0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -13612,7 +13612,7 @@ impl Editor { pub fn split_selection_into_lines( &mut self, - _: &SplitSelectionIntoLines, + action: &SplitSelectionIntoLines, window: &mut Window, cx: &mut Context, ) { @@ -13629,8 +13629,21 @@ impl Editor { let buffer = self.buffer.read(cx).read(cx); for selection in selections { for row in selection.start.row..selection.end.row { - let cursor = Point::new(row, buffer.line_len(MultiBufferRow(row))); - new_selection_ranges.push(cursor..cursor); + let line_start = Point::new(row, 0); + let line_end = Point::new(row, buffer.line_len(MultiBufferRow(row))); + + if action.keep_selections { + // Keep the selection range for each line + let selection_start = if row == selection.start.row { + selection.start + } else { + line_start + }; + new_selection_ranges.push(selection_start..line_end); + } else { + // Collapse to cursor at end of line + new_selection_ranges.push(line_end..line_end); + } } let is_multiline_selection = selection.start.row != selection.end.row; @@ -13638,7 +13651,16 @@ impl Editor { // so this action feels more ergonomic when paired with other selection operations let should_skip_last = is_multiline_selection && selection.end.column == 0; if !should_skip_last { - new_selection_ranges.push(selection.end..selection.end); + if action.keep_selections { + if is_multiline_selection { + let line_start = Point::new(selection.end.row, 0); + new_selection_ranges.push(line_start..selection.end); + } else { + new_selection_ranges.push(selection.start..selection.end); + } + } else { + new_selection_ranges.push(selection.end..selection.end); + } } } } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 4421869703..a5966b3301 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -6401,7 +6401,7 @@ async fn test_split_selection_into_lines(cx: &mut TestAppContext) { fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) { cx.set_state(initial_state); cx.update_editor(|e, window, cx| { - e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx) + e.split_selection_into_lines(&Default::default(), window, cx) }); cx.assert_editor_state(expected_state); } @@ -6489,7 +6489,7 @@ async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestA DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4), ]) }); - editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx); + editor.split_selection_into_lines(&Default::default(), window, cx); assert_eq!( editor.display_text(cx), "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i" @@ -6505,7 +6505,7 @@ async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestA DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1) ]) }); - editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx); + editor.split_selection_into_lines(&Default::default(), window, cx); assert_eq!( editor.display_text(cx), "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii" From 1a169e0b16801b278140bb9a59fa45ab56644f4d Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 14 Aug 2025 13:54:19 -0400 Subject: [PATCH 349/693] git: Clear set of dirty paths when doing a full status scan (#36181) Related to #35780 Release Notes: - N/A --------- Co-authored-by: Kirill Bulatov --- crates/project/src/git_store.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 32deb0dbc4..3163a10239 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -2349,7 +2349,7 @@ impl GitStore { return None; }; - let mut paths = vec![]; + let mut paths = Vec::new(); // All paths prefixed by a given repo will constitute a continuous range. while let Some(path) = entries.get(ix) && let Some(repo_path) = @@ -2358,7 +2358,11 @@ impl GitStore { paths.push((repo_path, ix)); ix += 1; } - Some((repo, paths)) + if paths.is_empty() { + None + } else { + Some((repo, paths)) + } }); tasks.push_back(task); } @@ -4338,7 +4342,8 @@ impl Repository { bail!("not a local repository") }; let (snapshot, events) = this - .read_with(&mut cx, |this, _| { + .update(&mut cx, |this, _| { + this.paths_needing_status_update.clear(); compute_snapshot( this.id, this.work_directory_abs_path.clone(), @@ -4568,6 +4573,9 @@ impl Repository { }; let paths = changed_paths.iter().cloned().collect::>(); + if paths.is_empty() { + return Ok(()); + } let statuses = backend.status(&paths).await?; let changed_path_statuses = cx From 2acfa5e948764cbe9ae5cbf9f95d6bf66ea904c2 Mon Sep 17 00:00:00 2001 From: smit Date: Thu, 14 Aug 2025 23:28:15 +0530 Subject: [PATCH 350/693] copilot: Fix Copilot fails to sign in on newer versions (#36195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up for #36093 and https://github.com/zed-industries/zed/pull/36138 Since v1.355.0, `@github/copilot-language-server` has stopped responding to `CheckStatus` requests if a `DidChangeConfiguration` notification hasn’t been sent beforehand. This causes `CheckStatus` to remain in an await state until it times out, leaving the connection stuck for a long period before finally throwing a timeout error. ```rs let status = server .request::(request::CheckStatusParams { local_checks_only: false, }) .await .into_response() // bails here with ConnectionResult::Timeout .context("copilot: check status")?; ```` This PR fixes the issue by sending the `DidChangeConfiguration` notification before making the `CheckStatus` request. It’s just an ordering change i.e. no other LSP actions occur between these two calls. Previously, we only updated our internal connection status and UI in between. Release Notes: - Fixed an issue where GitHub Copilot could get stuck and fail to sign in. --- crates/copilot/src/copilot.rs | 95 +++++++++++++------------ crates/languages/src/css.rs | 5 +- crates/languages/src/json.rs | 5 +- crates/languages/src/python.rs | 5 +- crates/languages/src/tailwind.rs | 5 +- crates/languages/src/typescript.rs | 5 +- crates/languages/src/vtsls.rs | 8 +-- crates/languages/src/yaml.rs | 5 +- crates/node_runtime/src/node_runtime.rs | 34 +++++---- 9 files changed, 83 insertions(+), 84 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 166a582c70..dcebeae721 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -21,7 +21,7 @@ use language::{ point_from_lsp, point_to_lsp, }; use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName}; -use node_runtime::{NodeRuntime, VersionCheck}; +use node_runtime::{NodeRuntime, VersionStrategy}; use parking_lot::Mutex; use project::DisableAiSettings; use request::StatusNotification; @@ -349,7 +349,11 @@ impl Copilot { this.start_copilot(true, false, cx); cx.observe_global::(move |this, cx| { this.start_copilot(true, false, cx); - this.send_configuration_update(cx); + if let Ok(server) = this.server.as_running() { + notify_did_change_config_to_server(&server.lsp, cx) + .context("copilot setting change: did change configuration") + .log_err(); + } }) .detach(); this @@ -438,43 +442,6 @@ impl Copilot { if env.is_empty() { None } else { Some(env) } } - fn send_configuration_update(&mut self, cx: &mut Context) { - let copilot_settings = all_language_settings(None, cx) - .edit_predictions - .copilot - .clone(); - - let settings = json!({ - "http": { - "proxy": copilot_settings.proxy, - "proxyStrictSSL": !copilot_settings.proxy_no_verify.unwrap_or(false) - }, - "github-enterprise": { - "uri": copilot_settings.enterprise_uri - } - }); - - if let Some(copilot_chat) = copilot_chat::CopilotChat::global(cx) { - copilot_chat.update(cx, |chat, cx| { - chat.set_configuration( - copilot_chat::CopilotChatConfiguration { - enterprise_uri: copilot_settings.enterprise_uri.clone(), - }, - cx, - ); - }); - } - - if let Ok(server) = self.server.as_running() { - server - .lsp - .notify::( - &lsp::DidChangeConfigurationParams { settings }, - ) - .log_err(); - } - } - #[cfg(any(test, feature = "test-support"))] pub fn fake(cx: &mut gpui::TestAppContext) -> (Entity, lsp::FakeLanguageServer) { use fs::FakeFs; @@ -573,6 +540,9 @@ impl Copilot { })? .await?; + this.update(cx, |_, cx| notify_did_change_config_to_server(&server, cx))? + .context("copilot: did change configuration")?; + let status = server .request::(request::CheckStatusParams { local_checks_only: false, @@ -598,8 +568,6 @@ impl Copilot { }); cx.emit(Event::CopilotLanguageServerStarted); this.update_sign_in_status(status, cx); - // Send configuration now that the LSP is fully started - this.send_configuration_update(cx); } Err(error) => { this.server = CopilotServer::Error(error.to_string().into()); @@ -1156,6 +1124,41 @@ fn uri_for_buffer(buffer: &Entity, cx: &App) -> Result { } } +fn notify_did_change_config_to_server( + server: &Arc, + cx: &mut Context, +) -> std::result::Result<(), anyhow::Error> { + let copilot_settings = all_language_settings(None, cx) + .edit_predictions + .copilot + .clone(); + + if let Some(copilot_chat) = copilot_chat::CopilotChat::global(cx) { + copilot_chat.update(cx, |chat, cx| { + chat.set_configuration( + copilot_chat::CopilotChatConfiguration { + enterprise_uri: copilot_settings.enterprise_uri.clone(), + }, + cx, + ); + }); + } + + let settings = json!({ + "http": { + "proxy": copilot_settings.proxy, + "proxyStrictSSL": !copilot_settings.proxy_no_verify.unwrap_or(false) + }, + "github-enterprise": { + "uri": copilot_settings.enterprise_uri + } + }); + + server.notify::(&lsp::DidChangeConfigurationParams { + settings, + }) +} + async fn clear_copilot_dir() { remove_matching(paths::copilot_dir(), |_| true).await } @@ -1169,8 +1172,9 @@ async fn get_copilot_lsp(fs: Arc, node_runtime: NodeRuntime) -> anyhow:: const SERVER_PATH: &str = "node_modules/@github/copilot-language-server/dist/language-server.js"; - // pinning it: https://github.com/zed-industries/zed/issues/36093 - const PINNED_VERSION: &str = "1.354"; + let latest_version = node_runtime + .npm_package_latest_version(PACKAGE_NAME) + .await?; let server_path = paths::copilot_dir().join(SERVER_PATH); fs.create_dir(paths::copilot_dir()).await?; @@ -1180,13 +1184,12 @@ async fn get_copilot_lsp(fs: Arc, node_runtime: NodeRuntime) -> anyhow:: PACKAGE_NAME, &server_path, paths::copilot_dir(), - &PINNED_VERSION, - VersionCheck::VersionMismatch, + VersionStrategy::Latest(&latest_version), ) .await; if should_install { node_runtime - .npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &PINNED_VERSION)]) + .npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &latest_version)]) .await?; } diff --git a/crates/languages/src/css.rs b/crates/languages/src/css.rs index 19329fcc6e..ffd9006c76 100644 --- a/crates/languages/src/css.rs +++ b/crates/languages/src/css.rs @@ -4,7 +4,7 @@ use futures::StreamExt; use gpui::AsyncApp; use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; use lsp::{LanguageServerBinary, LanguageServerName}; -use node_runtime::NodeRuntime; +use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; use serde_json::json; use smol::fs; @@ -107,8 +107,7 @@ impl LspAdapter for CssLspAdapter { Self::PACKAGE_NAME, &server_path, &container_dir, - &version, - Default::default(), + VersionStrategy::Latest(version), ) .await; diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 019b45d396..484631d01f 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -12,7 +12,7 @@ use language::{ LspAdapter, LspAdapterDelegate, }; use lsp::{LanguageServerBinary, LanguageServerName}; -use node_runtime::NodeRuntime; +use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; use serde_json::{Value, json}; use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore}; @@ -344,8 +344,7 @@ impl LspAdapter for JsonLspAdapter { Self::PACKAGE_NAME, &server_path, &container_dir, - &version, - Default::default(), + VersionStrategy::Latest(version), ) .await; diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 5513324487..40131089d1 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -13,7 +13,7 @@ use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery}; use language::{Toolchain, WorkspaceFoldersContent}; use lsp::LanguageServerBinary; use lsp::LanguageServerName; -use node_runtime::NodeRuntime; +use node_runtime::{NodeRuntime, VersionStrategy}; use pet_core::Configuration; use pet_core::os_environment::Environment; use pet_core::python_environment::PythonEnvironmentKind; @@ -205,8 +205,7 @@ impl LspAdapter for PythonLspAdapter { Self::SERVER_NAME.as_ref(), &server_path, &container_dir, - &version, - Default::default(), + VersionStrategy::Latest(version), ) .await; diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index 6f03eeda8d..0d647f07cf 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -5,7 +5,7 @@ use futures::StreamExt; use gpui::AsyncApp; use language::{LanguageName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; use lsp::{LanguageServerBinary, LanguageServerName}; -use node_runtime::NodeRuntime; +use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; use serde_json::{Value, json}; use smol::fs; @@ -112,8 +112,7 @@ impl LspAdapter for TailwindLspAdapter { Self::PACKAGE_NAME, &server_path, &container_dir, - &version, - Default::default(), + VersionStrategy::Latest(version), ) .await; diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index a8ba880889..1877c86dc5 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -10,7 +10,7 @@ use language::{ LspAdapterDelegate, }; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName}; -use node_runtime::NodeRuntime; +use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; use serde_json::{Value, json}; use smol::{fs, lock::RwLock, stream::StreamExt}; @@ -588,8 +588,7 @@ impl LspAdapter for TypeScriptLspAdapter { Self::PACKAGE_NAME, &server_path, &container_dir, - version.typescript_version.as_str(), - Default::default(), + VersionStrategy::Latest(version.typescript_version.as_str()), ) .await; diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index 73498fc579..90faf883ba 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -4,7 +4,7 @@ use collections::HashMap; use gpui::AsyncApp; use language::{LanguageName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName}; -use node_runtime::NodeRuntime; +use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; use serde_json::Value; use std::{ @@ -115,8 +115,7 @@ impl LspAdapter for VtslsLspAdapter { Self::PACKAGE_NAME, &server_path, &container_dir, - &latest_version.server_version, - Default::default(), + VersionStrategy::Latest(&latest_version.server_version), ) .await { @@ -129,8 +128,7 @@ impl LspAdapter for VtslsLspAdapter { Self::TYPESCRIPT_PACKAGE_NAME, &container_dir.join(Self::TYPESCRIPT_TSDK_PATH), &container_dir, - &latest_version.typescript_version, - Default::default(), + VersionStrategy::Latest(&latest_version.typescript_version), ) .await { diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 28be2cc1a4..15a4d590bc 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -6,7 +6,7 @@ use language::{ LanguageToolchainStore, LspAdapter, LspAdapterDelegate, language_settings::AllLanguageSettings, }; use lsp::{LanguageServerBinary, LanguageServerName}; -use node_runtime::NodeRuntime; +use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; use serde_json::Value; use settings::{Settings, SettingsLocation}; @@ -108,8 +108,7 @@ impl LspAdapter for YamlLspAdapter { Self::PACKAGE_NAME, &server_path, &container_dir, - &version, - Default::default(), + VersionStrategy::Latest(version), ) .await; diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 6fcc3a728a..f92c122e71 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -29,13 +29,11 @@ pub struct NodeBinaryOptions { pub use_paths: Option<(PathBuf, PathBuf)>, } -#[derive(Default)] -pub enum VersionCheck { - /// Check whether the installed and requested version have a mismatch - VersionMismatch, - /// Only check whether the currently installed version is older than the newest one - #[default] - OlderVersion, +pub enum VersionStrategy<'a> { + /// Install if current version doesn't match pinned version + Pin(&'a str), + /// Install if current version is older than latest version + Latest(&'a str), } #[derive(Clone)] @@ -295,8 +293,7 @@ impl NodeRuntime { package_name: &str, local_executable_path: &Path, local_package_directory: &Path, - latest_version: &str, - version_check: VersionCheck, + version_strategy: VersionStrategy<'_>, ) -> bool { // In the case of the local system not having the package installed, // or in the instances where we fail to parse package.json data, @@ -317,13 +314,20 @@ impl NodeRuntime { let Some(installed_version) = Version::parse(&installed_version).log_err() else { return true; }; - let Some(latest_version) = Version::parse(latest_version).log_err() else { - return true; - }; - match version_check { - VersionCheck::VersionMismatch => installed_version != latest_version, - VersionCheck::OlderVersion => installed_version < latest_version, + match version_strategy { + VersionStrategy::Pin(pinned_version) => { + let Some(pinned_version) = Version::parse(pinned_version).log_err() else { + return true; + }; + installed_version != pinned_version + } + VersionStrategy::Latest(latest_version) => { + let Some(latest_version) = Version::parse(latest_version).log_err() else { + return true; + }; + installed_version < latest_version + } } } } From 43ee604179ccda222eed29a173ac19e0514e8679 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Thu, 14 Aug 2025 15:30:18 -0300 Subject: [PATCH 351/693] acp: Clean up entry views on rewind (#36197) We were leaking diffs and terminals on rewind, we'll now clean them up. This PR also introduces a refactor of how we mantain the entry view state to use a `Vec` that's kept in sync with the thread entries. Release Notes: - N/A --- crates/acp_thread/Cargo.toml | 3 +- crates/acp_thread/src/acp_thread.rs | 36 +- crates/acp_thread/src/connection.rs | 156 +++++- crates/agent2/src/agent.rs | 8 +- crates/agent2/src/tests/mod.rs | 2 +- crates/agent_servers/src/acp/v0.rs | 2 +- crates/agent_servers/src/acp/v1.rs | 2 +- crates/agent_servers/src/claude.rs | 2 +- crates/agent_servers/src/e2e_tests.rs | 4 +- crates/agent_ui/Cargo.toml | 1 + crates/agent_ui/src/acp.rs | 1 + crates/agent_ui/src/acp/entry_view_state.rs | 351 +++++++++++++ crates/agent_ui/src/acp/thread_view.rs | 536 +++++++++----------- 13 files changed, 758 insertions(+), 346 deletions(-) create mode 100644 crates/agent_ui/src/acp/entry_view_state.rs diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index 2d0fe2d264..2b9a6513c8 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -13,7 +13,7 @@ path = "src/acp_thread.rs" doctest = false [features] -test-support = ["gpui/test-support", "project/test-support"] +test-support = ["gpui/test-support", "project/test-support", "dep:parking_lot"] [dependencies] action_log.workspace = true @@ -29,6 +29,7 @@ gpui.workspace = true itertools.workspace = true language.workspace = true markdown.workspace = true +parking_lot = { workspace = true, optional = true } project.workspace = true prompt_store.workspace = true serde.workspace = true diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index da4d82712a..4bdc42ea2e 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1575,11 +1575,7 @@ mod tests { let project = Project::test(fs, [], cx).await; let connection = Rc::new(FakeAgentConnection::new()); let thread = cx - .spawn(async move |mut cx| { - connection - .new_thread(project, Path::new(path!("/test")), &mut cx) - .await - }) + .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) .await .unwrap(); @@ -1699,11 +1695,7 @@ mod tests { )); let thread = cx - .spawn(async move |mut cx| { - connection - .new_thread(project, Path::new(path!("/test")), &mut cx) - .await - }) + .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) .await .unwrap(); @@ -1786,7 +1778,7 @@ mod tests { .unwrap(); let thread = cx - .spawn(|mut cx| connection.new_thread(project, Path::new(path!("/tmp")), &mut cx)) + .update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx)) .await .unwrap(); @@ -1849,11 +1841,7 @@ mod tests { })); let thread = cx - .spawn(async move |mut cx| { - connection - .new_thread(project, Path::new(path!("/test")), &mut cx) - .await - }) + .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) .await .unwrap(); @@ -1961,10 +1949,11 @@ mod tests { } })); - let thread = connection - .new_thread(project, Path::new(path!("/test")), &mut cx.to_async()) + let thread = cx + .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) .await .unwrap(); + cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["Hi".into()], cx))) .await .unwrap(); @@ -2021,8 +2010,8 @@ mod tests { .boxed_local() } })); - let thread = connection - .new_thread(project, Path::new(path!("/test")), &mut cx.to_async()) + let thread = cx + .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) .await .unwrap(); @@ -2227,7 +2216,7 @@ mod tests { self: Rc, project: Entity, _cwd: &Path, - cx: &mut gpui::AsyncApp, + cx: &mut gpui::App, ) -> Task>> { let session_id = acp::SessionId( rand::thread_rng() @@ -2237,9 +2226,8 @@ mod tests { .collect::() .into(), ); - let thread = cx - .new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx)) - .unwrap(); + let thread = + cx.new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx)); self.sessions.lock().insert(session_id, thread.downgrade()); Task::ready(Ok(thread)) } diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index c3167eb2d4..0f531acbde 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -2,7 +2,7 @@ use crate::AcpThread; use agent_client_protocol::{self as acp}; use anyhow::Result; use collections::IndexMap; -use gpui::{AsyncApp, Entity, SharedString, Task}; +use gpui::{Entity, SharedString, Task}; use project::Project; use std::{error::Error, fmt, path::Path, rc::Rc, sync::Arc}; use ui::{App, IconName}; @@ -22,7 +22,7 @@ pub trait AgentConnection { self: Rc, project: Entity, cwd: &Path, - cx: &mut AsyncApp, + cx: &mut App, ) -> Task>>; fn auth_methods(&self) -> &[acp::AuthMethod]; @@ -160,3 +160,155 @@ impl AgentModelList { } } } + +#[cfg(feature = "test-support")] +mod test_support { + use std::sync::Arc; + + use collections::HashMap; + use futures::future::try_join_all; + use gpui::{AppContext as _, WeakEntity}; + use parking_lot::Mutex; + + use super::*; + + #[derive(Clone, Default)] + pub struct StubAgentConnection { + sessions: Arc>>>, + permission_requests: HashMap>, + next_prompt_updates: Arc>>, + } + + impl StubAgentConnection { + pub fn new() -> Self { + Self { + next_prompt_updates: Default::default(), + permission_requests: HashMap::default(), + sessions: Arc::default(), + } + } + + pub fn set_next_prompt_updates(&self, updates: Vec) { + *self.next_prompt_updates.lock() = updates; + } + + pub fn with_permission_requests( + mut self, + permission_requests: HashMap>, + ) -> Self { + self.permission_requests = permission_requests; + self + } + + pub fn send_update( + &self, + session_id: acp::SessionId, + update: acp::SessionUpdate, + cx: &mut App, + ) { + self.sessions + .lock() + .get(&session_id) + .unwrap() + .update(cx, |thread, cx| { + thread.handle_session_update(update.clone(), cx).unwrap(); + }) + .unwrap(); + } + } + + impl AgentConnection for StubAgentConnection { + fn auth_methods(&self) -> &[acp::AuthMethod] { + &[] + } + + fn new_thread( + self: Rc, + project: Entity, + _cwd: &Path, + cx: &mut gpui::App, + ) -> Task>> { + let session_id = acp::SessionId(self.sessions.lock().len().to_string().into()); + let thread = + cx.new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx)); + self.sessions.lock().insert(session_id, thread.downgrade()); + Task::ready(Ok(thread)) + } + + fn authenticate( + &self, + _method_id: acp::AuthMethodId, + _cx: &mut App, + ) -> Task> { + unimplemented!() + } + + fn prompt( + &self, + _id: Option, + params: acp::PromptRequest, + cx: &mut App, + ) -> Task> { + let sessions = self.sessions.lock(); + let thread = sessions.get(¶ms.session_id).unwrap(); + let mut tasks = vec![]; + for update in self.next_prompt_updates.lock().drain(..) { + let thread = thread.clone(); + let update = update.clone(); + let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) = &update + && let Some(options) = self.permission_requests.get(&tool_call.id) + { + Some((tool_call.clone(), options.clone())) + } else { + None + }; + let task = cx.spawn(async move |cx| { + if let Some((tool_call, options)) = permission_request { + let permission = thread.update(cx, |thread, cx| { + thread.request_tool_call_authorization( + tool_call.clone(), + options.clone(), + cx, + ) + })?; + permission.await?; + } + thread.update(cx, |thread, cx| { + thread.handle_session_update(update.clone(), cx).unwrap(); + })?; + anyhow::Ok(()) + }); + tasks.push(task); + } + cx.spawn(async move |_| { + try_join_all(tasks).await?; + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) + }) + } + + fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) { + unimplemented!() + } + + fn session_editor( + &self, + _session_id: &agent_client_protocol::SessionId, + _cx: &mut App, + ) -> Option> { + Some(Rc::new(StubAgentSessionEditor)) + } + } + + struct StubAgentSessionEditor; + + impl AgentSessionEditor for StubAgentSessionEditor { + fn truncate(&self, _: UserMessageId, _: &mut App) -> Task> { + Task::ready(Ok(())) + } + } +} + +#[cfg(feature = "test-support")] +pub use test_support::*; diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 6ebcece2b5..9ac3c2d0e5 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -522,7 +522,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { self: Rc, project: Entity, cwd: &Path, - cx: &mut AsyncApp, + cx: &mut App, ) -> Task>> { let agent = self.0.clone(); log::info!("Creating new thread for project at: {:?}", cwd); @@ -940,11 +940,7 @@ mod tests { // Create a thread/session let acp_thread = cx .update(|cx| { - Rc::new(connection.clone()).new_thread( - project.clone(), - Path::new("/a"), - &mut cx.to_async(), - ) + Rc::new(connection.clone()).new_thread(project.clone(), Path::new("/a"), cx) }) .await .unwrap(); diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 637af73d1a..1df664c029 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -841,7 +841,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) { // Create a thread using new_thread let connection_rc = Rc::new(connection.clone()); let acp_thread = cx - .update(|cx| connection_rc.new_thread(project, cwd, &mut cx.to_async())) + .update(|cx| connection_rc.new_thread(project, cwd, cx)) .await .expect("new_thread should succeed"); diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index 327613de67..15f8635cde 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -423,7 +423,7 @@ impl AgentConnection for AcpConnection { self: Rc, project: Entity, _cwd: &Path, - cx: &mut AsyncApp, + cx: &mut App, ) -> Task>> { let task = self.connection.request_any( acp_old::InitializeParams { diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index de397fddf0..d93e3d023e 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -111,7 +111,7 @@ impl AgentConnection for AcpConnection { self: Rc, project: Entity, cwd: &Path, - cx: &mut AsyncApp, + cx: &mut App, ) -> Task>> { let conn = self.connection.clone(); let sessions = self.sessions.clone(); diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index c394ec4a9c..dbcda00e48 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -74,7 +74,7 @@ impl AgentConnection for ClaudeAgentConnection { self: Rc, project: Entity, cwd: &Path, - cx: &mut AsyncApp, + cx: &mut App, ) -> Task>> { let cwd = cwd.to_owned(); cx.spawn(async move |cx| { diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index ec6ca29b9d..5af7010f26 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -422,8 +422,8 @@ pub async fn new_test_thread( .await .unwrap(); - let thread = connection - .new_thread(project.clone(), current_dir.as_ref(), &mut cx.to_async()) + let thread = cx + .update(|cx| connection.new_thread(project.clone(), current_dir.as_ref(), cx)) .await .unwrap(); diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index b6a5710aa4..13fd9d13c5 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -103,6 +103,7 @@ workspace.workspace = true zed_actions.workspace = true [dev-dependencies] +acp_thread = { workspace = true, features = ["test-support"] } agent = { workspace = true, features = ["test-support"] } assistant_context = { workspace = true, features = ["test-support"] } assistant_tools.workspace = true diff --git a/crates/agent_ui/src/acp.rs b/crates/agent_ui/src/acp.rs index 630aa730a6..831d296eeb 100644 --- a/crates/agent_ui/src/acp.rs +++ b/crates/agent_ui/src/acp.rs @@ -1,4 +1,5 @@ mod completion_provider; +mod entry_view_state; mod message_editor; mod model_selector; mod model_selector_popover; diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs new file mode 100644 index 0000000000..2f5f855e90 --- /dev/null +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -0,0 +1,351 @@ +use std::{collections::HashMap, ops::Range}; + +use acp_thread::AcpThread; +use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer}; +use gpui::{ + AnyEntity, App, AppContext as _, Entity, EntityId, TextStyleRefinement, WeakEntity, Window, +}; +use language::language_settings::SoftWrap; +use settings::Settings as _; +use terminal_view::TerminalView; +use theme::ThemeSettings; +use ui::TextSize; +use workspace::Workspace; + +#[derive(Default)] +pub struct EntryViewState { + entries: Vec, +} + +impl EntryViewState { + pub fn entry(&self, index: usize) -> Option<&Entry> { + self.entries.get(index) + } + + pub fn sync_entry( + &mut self, + workspace: WeakEntity, + thread: Entity, + index: usize, + window: &mut Window, + cx: &mut App, + ) { + debug_assert!(index <= self.entries.len()); + let entry = if let Some(entry) = self.entries.get_mut(index) { + entry + } else { + self.entries.push(Entry::default()); + self.entries.last_mut().unwrap() + }; + + entry.sync_diff_multibuffers(&thread, index, window, cx); + entry.sync_terminals(&workspace, &thread, index, window, cx); + } + + pub fn remove(&mut self, range: Range) { + self.entries.drain(range); + } + + pub fn settings_changed(&mut self, cx: &mut App) { + for entry in self.entries.iter() { + for view in entry.views.values() { + if let Ok(diff_editor) = view.clone().downcast::() { + diff_editor.update(cx, |diff_editor, cx| { + diff_editor + .set_text_style_refinement(diff_editor_text_style_refinement(cx)); + cx.notify(); + }) + } + } + } + } +} + +pub struct Entry { + views: HashMap, +} + +impl Entry { + pub fn editor_for_diff(&self, diff: &Entity) -> Option> { + self.views + .get(&diff.entity_id()) + .cloned() + .map(|entity| entity.downcast::().unwrap()) + } + + pub fn terminal( + &self, + terminal: &Entity, + ) -> Option> { + self.views + .get(&terminal.entity_id()) + .cloned() + .map(|entity| entity.downcast::().unwrap()) + } + + fn sync_diff_multibuffers( + &mut self, + thread: &Entity, + index: usize, + window: &mut Window, + cx: &mut App, + ) { + let Some(entry) = thread.read(cx).entries().get(index) else { + return; + }; + + let multibuffers = entry + .diffs() + .map(|diff| diff.read(cx).multibuffer().clone()); + + let multibuffers = multibuffers.collect::>(); + + for multibuffer in multibuffers { + if self.views.contains_key(&multibuffer.entity_id()) { + return; + } + + let editor = cx.new(|cx| { + let mut editor = Editor::new( + EditorMode::Full { + scale_ui_elements_with_buffer_font_size: false, + show_active_line_background: false, + sized_by_content: true, + }, + multibuffer.clone(), + None, + window, + cx, + ); + editor.set_show_gutter(false, cx); + editor.disable_inline_diagnostics(); + editor.disable_expand_excerpt_buttons(cx); + editor.set_show_vertical_scrollbar(false, cx); + editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx); + editor.set_soft_wrap_mode(SoftWrap::None, cx); + editor.scroll_manager.set_forbid_vertical_scroll(true); + editor.set_show_indent_guides(false, cx); + editor.set_read_only(true); + editor.set_show_breakpoints(false, cx); + editor.set_show_code_actions(false, cx); + editor.set_show_git_diff_gutter(false, cx); + editor.set_expand_all_diff_hunks(cx); + editor.set_text_style_refinement(diff_editor_text_style_refinement(cx)); + editor + }); + + let entity_id = multibuffer.entity_id(); + self.views.insert(entity_id, editor.into_any()); + } + } + + fn sync_terminals( + &mut self, + workspace: &WeakEntity, + thread: &Entity, + index: usize, + window: &mut Window, + cx: &mut App, + ) { + let Some(entry) = thread.read(cx).entries().get(index) else { + return; + }; + + let terminals = entry + .terminals() + .map(|terminal| terminal.clone()) + .collect::>(); + + for terminal in terminals { + if self.views.contains_key(&terminal.entity_id()) { + return; + } + + let Some(strong_workspace) = workspace.upgrade() else { + return; + }; + + let terminal_view = cx.new(|cx| { + let mut view = TerminalView::new( + terminal.read(cx).inner().clone(), + workspace.clone(), + None, + strong_workspace.read(cx).project().downgrade(), + window, + cx, + ); + view.set_embedded_mode(Some(1000), cx); + view + }); + + let entity_id = terminal.entity_id(); + self.views.insert(entity_id, terminal_view.into_any()); + } + } + + #[cfg(test)] + pub fn len(&self) -> usize { + self.views.len() + } +} + +fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement { + TextStyleRefinement { + font_size: Some( + TextSize::Small + .rems(cx) + .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx)) + .into(), + ), + ..Default::default() + } +} + +impl Default for Entry { + fn default() -> Self { + Self { + // Avoid allocating in the heap by default + views: HashMap::with_capacity(0), + } + } +} + +#[cfg(test)] +mod tests { + use std::{path::Path, rc::Rc}; + + use acp_thread::{AgentConnection, StubAgentConnection}; + use agent_client_protocol as acp; + use agent_settings::AgentSettings; + use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; + use editor::{EditorSettings, RowInfo}; + use fs::FakeFs; + use gpui::{SemanticVersion, TestAppContext}; + use multi_buffer::MultiBufferRow; + use pretty_assertions::assert_matches; + use project::Project; + use serde_json::json; + use settings::{Settings as _, SettingsStore}; + use theme::ThemeSettings; + use util::path; + use workspace::Workspace; + + use crate::acp::entry_view_state::EntryViewState; + + #[gpui::test] + async fn test_diff_sync(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/project", + json!({ + "hello.txt": "hi world" + }), + ) + .await; + let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; + + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + let tool_call = acp::ToolCall { + id: acp::ToolCallId("tool".into()), + title: "Tool call".into(), + kind: acp::ToolKind::Other, + status: acp::ToolCallStatus::InProgress, + content: vec![acp::ToolCallContent::Diff { + diff: acp::Diff { + path: "/project/hello.txt".into(), + old_text: Some("hi world".into()), + new_text: "hello world".into(), + }, + }], + locations: vec![], + raw_input: None, + raw_output: None, + }; + let connection = Rc::new(StubAgentConnection::new()); + let thread = cx + .update(|_, cx| { + connection + .clone() + .new_thread(project, Path::new(path!("/project")), cx) + }) + .await + .unwrap(); + let session_id = thread.update(cx, |thread, _| thread.session_id().clone()); + + cx.update(|_, cx| { + connection.send_update(session_id, acp::SessionUpdate::ToolCall(tool_call), cx) + }); + + let mut view_state = EntryViewState::default(); + cx.update(|window, cx| { + view_state.sync_entry(workspace.downgrade(), thread.clone(), 0, window, cx); + }); + + let multibuffer = thread.read_with(cx, |thread, cx| { + thread + .entries() + .get(0) + .unwrap() + .diffs() + .next() + .unwrap() + .read(cx) + .multibuffer() + .clone() + }); + + cx.run_until_parked(); + + let entry = view_state.entry(0).unwrap(); + let diff_editor = entry.editor_for_diff(&multibuffer).unwrap(); + assert_eq!( + diff_editor.read_with(cx, |editor, cx| editor.text(cx)), + "hi world\nhello world" + ); + let row_infos = diff_editor.read_with(cx, |editor, cx| { + let multibuffer = editor.buffer().read(cx); + multibuffer + .snapshot(cx) + .row_infos(MultiBufferRow(0)) + .collect::>() + }); + assert_matches!( + row_infos.as_slice(), + [ + RowInfo { + multibuffer_row: Some(MultiBufferRow(0)), + diff_status: Some(DiffHunkStatus { + kind: DiffHunkStatusKind::Deleted, + .. + }), + .. + }, + RowInfo { + multibuffer_row: Some(MultiBufferRow(1)), + diff_status: Some(DiffHunkStatus { + kind: DiffHunkStatusKind::Added, + .. + }), + .. + } + ] + ); + } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + Project::init_settings(cx); + AgentSettings::register(cx); + workspace::init_settings(cx); + ThemeSettings::register(cx); + release_channel::init(SemanticVersion::default(), cx); + EditorSettings::register(cx); + }); + } +} diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 2a72cc6f48..0e90b93f4d 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -12,24 +12,22 @@ use audio::{Audio, Sound}; use buffer_diff::BufferDiff; use collections::{HashMap, HashSet}; use editor::scroll::Autoscroll; -use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, SelectionEffects}; +use editor::{Editor, EditorMode, MultiBuffer, PathKey, SelectionEffects}; use file_icons::FileIcons; use gpui::{ Action, Animation, AnimationExt, App, BorderStyle, ClickEvent, EdgesRefinement, Empty, Entity, - EntityId, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, - PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, - TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, - linear_color_stop, linear_gradient, list, percentage, point, prelude::*, pulsating_between, + FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, PlatformDisplay, + SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, + Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop, + linear_gradient, list, percentage, point, prelude::*, pulsating_between, }; use language::Buffer; -use language::language_settings::SoftWrap; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; use project::Project; use prompt_store::PromptId; use rope::Point; use settings::{Settings as _, SettingsStore}; use std::{collections::BTreeMap, process::ExitStatus, rc::Rc, time::Duration}; -use terminal_view::TerminalView; use text::Anchor; use theme::ThemeSettings; use ui::{ @@ -41,6 +39,7 @@ use workspace::{CollaboratorId, Workspace}; use zed_actions::agent::{Chat, ToggleModelSelector}; use zed_actions::assistant::OpenRulesLibrary; +use super::entry_view_state::EntryViewState; use crate::acp::AcpModelSelectorPopover; use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; use crate::agent_diff::AgentDiff; @@ -61,8 +60,7 @@ pub struct AcpThreadView { thread_store: Entity, text_thread_store: Entity, thread_state: ThreadState, - diff_editors: HashMap>, - terminal_views: HashMap>, + entry_view_state: EntryViewState, message_editor: Entity, model_selector: Option>, notifications: Vec>, @@ -149,8 +147,7 @@ impl AcpThreadView { model_selector: None, notifications: Vec::new(), notification_subscriptions: HashMap::default(), - diff_editors: Default::default(), - terminal_views: Default::default(), + entry_view_state: EntryViewState::default(), list_state: list_state.clone(), scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), last_error: None, @@ -209,11 +206,18 @@ impl AcpThreadView { // }) // .ok(); - let result = match connection - .clone() - .new_thread(project.clone(), &root_dir, cx) - .await - { + let Some(result) = cx + .update(|_, cx| { + connection + .clone() + .new_thread(project.clone(), &root_dir, cx) + }) + .log_err() + else { + return; + }; + + let result = match result.await { Err(e) => { let mut cx = cx.clone(); if e.is::() { @@ -480,16 +484,29 @@ impl AcpThreadView { ) { match event { AcpThreadEvent::NewEntry => { - let index = thread.read(cx).entries().len() - 1; - self.sync_thread_entry_view(index, window, cx); + let len = thread.read(cx).entries().len(); + let index = len - 1; + self.entry_view_state.sync_entry( + self.workspace.clone(), + thread.clone(), + index, + window, + cx, + ); self.list_state.splice(index..index, 1); } AcpThreadEvent::EntryUpdated(index) => { - self.sync_thread_entry_view(*index, window, cx); + self.entry_view_state.sync_entry( + self.workspace.clone(), + thread.clone(), + *index, + window, + cx, + ); self.list_state.splice(*index..index + 1, 1); } AcpThreadEvent::EntriesRemoved(range) => { - // TODO: Clean up unused diff editors and terminal views + self.entry_view_state.remove(range.clone()); self.list_state.splice(range.clone(), 0); } AcpThreadEvent::ToolAuthorizationRequired => { @@ -523,128 +540,6 @@ impl AcpThreadView { cx.notify(); } - fn sync_thread_entry_view( - &mut self, - entry_ix: usize, - window: &mut Window, - cx: &mut Context, - ) { - self.sync_diff_multibuffers(entry_ix, window, cx); - self.sync_terminals(entry_ix, window, cx); - } - - fn sync_diff_multibuffers( - &mut self, - entry_ix: usize, - window: &mut Window, - cx: &mut Context, - ) { - let Some(multibuffers) = self.entry_diff_multibuffers(entry_ix, cx) else { - return; - }; - - let multibuffers = multibuffers.collect::>(); - - for multibuffer in multibuffers { - if self.diff_editors.contains_key(&multibuffer.entity_id()) { - return; - } - - let editor = cx.new(|cx| { - let mut editor = Editor::new( - EditorMode::Full { - scale_ui_elements_with_buffer_font_size: false, - show_active_line_background: false, - sized_by_content: true, - }, - multibuffer.clone(), - None, - window, - cx, - ); - editor.set_show_gutter(false, cx); - editor.disable_inline_diagnostics(); - editor.disable_expand_excerpt_buttons(cx); - editor.set_show_vertical_scrollbar(false, cx); - editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx); - editor.set_soft_wrap_mode(SoftWrap::None, cx); - editor.scroll_manager.set_forbid_vertical_scroll(true); - editor.set_show_indent_guides(false, cx); - editor.set_read_only(true); - editor.set_show_breakpoints(false, cx); - editor.set_show_code_actions(false, cx); - editor.set_show_git_diff_gutter(false, cx); - editor.set_expand_all_diff_hunks(cx); - editor.set_text_style_refinement(diff_editor_text_style_refinement(cx)); - editor - }); - let entity_id = multibuffer.entity_id(); - cx.observe_release(&multibuffer, move |this, _, _| { - this.diff_editors.remove(&entity_id); - }) - .detach(); - - self.diff_editors.insert(entity_id, editor); - } - } - - fn entry_diff_multibuffers( - &self, - entry_ix: usize, - cx: &App, - ) -> Option>> { - let entry = self.thread()?.read(cx).entries().get(entry_ix)?; - Some( - entry - .diffs() - .map(|diff| diff.read(cx).multibuffer().clone()), - ) - } - - fn sync_terminals(&mut self, entry_ix: usize, window: &mut Window, cx: &mut Context) { - let Some(terminals) = self.entry_terminals(entry_ix, cx) else { - return; - }; - - let terminals = terminals.collect::>(); - - for terminal in terminals { - if self.terminal_views.contains_key(&terminal.entity_id()) { - return; - } - - let terminal_view = cx.new(|cx| { - let mut view = TerminalView::new( - terminal.read(cx).inner().clone(), - self.workspace.clone(), - None, - self.project.downgrade(), - window, - cx, - ); - view.set_embedded_mode(Some(1000), cx); - view - }); - - let entity_id = terminal.entity_id(); - cx.observe_release(&terminal, move |this, _, _| { - this.terminal_views.remove(&entity_id); - }) - .detach(); - - self.terminal_views.insert(entity_id, terminal_view); - } - } - - fn entry_terminals( - &self, - entry_ix: usize, - cx: &App, - ) -> Option>> { - let entry = self.thread()?.read(cx).entries().get(entry_ix)?; - Some(entry.terminals().map(|terminal| terminal.clone())) - } - fn authenticate( &mut self, method: acp::AuthMethodId, @@ -712,7 +607,7 @@ impl AcpThreadView { fn render_entry( &self, - index: usize, + entry_ix: usize, total_entries: usize, entry: &AgentThreadEntry, window: &mut Window, @@ -720,7 +615,7 @@ impl AcpThreadView { ) -> AnyElement { let primary = match &entry { AgentThreadEntry::UserMessage(message) => div() - .id(("user_message", index)) + .id(("user_message", entry_ix)) .py_4() .px_2() .children(message.id.clone().and_then(|message_id| { @@ -749,7 +644,9 @@ impl AcpThreadView { .text_xs() .id("message") .on_click(cx.listener({ - move |this, _, window, cx| this.start_editing_message(index, window, cx) + move |this, _, window, cx| { + this.start_editing_message(entry_ix, window, cx) + } })) .children( if let Some(editing) = self.editing_message.as_ref() @@ -787,7 +684,7 @@ impl AcpThreadView { AssistantMessageChunk::Thought { block } => { block.markdown().map(|md| { self.render_thinking_block( - index, + entry_ix, chunk_ix, md.clone(), window, @@ -803,7 +700,7 @@ impl AcpThreadView { v_flex() .px_5() .py_1() - .when(index + 1 == total_entries, |this| this.pb_4()) + .when(entry_ix + 1 == total_entries, |this| this.pb_4()) .w_full() .text_ui(cx) .child(message_body) @@ -815,10 +712,12 @@ impl AcpThreadView { div().w_full().py_1p5().px_5().map(|this| { if has_terminals { this.children(tool_call.terminals().map(|terminal| { - self.render_terminal_tool_call(terminal, tool_call, window, cx) + self.render_terminal_tool_call( + entry_ix, terminal, tool_call, window, cx, + ) })) } else { - this.child(self.render_tool_call(index, tool_call, window, cx)) + this.child(self.render_tool_call(entry_ix, tool_call, window, cx)) } }) } @@ -830,7 +729,7 @@ impl AcpThreadView { }; let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); - let primary = if index == total_entries - 1 && !is_generating { + let primary = if entry_ix == total_entries - 1 && !is_generating { v_flex() .w_full() .child(primary) @@ -841,10 +740,10 @@ impl AcpThreadView { }; if let Some(editing) = self.editing_message.as_ref() - && editing.index < index + && editing.index < entry_ix { let backdrop = div() - .id(("backdrop", index)) + .id(("backdrop", entry_ix)) .size_full() .absolute() .inset_0() @@ -1125,7 +1024,9 @@ impl AcpThreadView { .w_full() .children(tool_call.content.iter().map(|content| { div() - .child(self.render_tool_call_content(content, tool_call, window, cx)) + .child( + self.render_tool_call_content(entry_ix, content, tool_call, window, cx), + ) .into_any_element() })) .child(self.render_permission_buttons( @@ -1139,7 +1040,9 @@ impl AcpThreadView { .w_full() .children(tool_call.content.iter().map(|content| { div() - .child(self.render_tool_call_content(content, tool_call, window, cx)) + .child( + self.render_tool_call_content(entry_ix, content, tool_call, window, cx), + ) .into_any_element() })), ToolCallStatus::Rejected => v_flex().size_0(), @@ -1257,6 +1160,7 @@ impl AcpThreadView { fn render_tool_call_content( &self, + entry_ix: usize, content: &ToolCallContent, tool_call: &ToolCall, window: &Window, @@ -1273,10 +1177,10 @@ impl AcpThreadView { } } ToolCallContent::Diff(diff) => { - self.render_diff_editor(&diff.read(cx).multibuffer(), cx) + self.render_diff_editor(entry_ix, &diff.read(cx).multibuffer(), cx) } ToolCallContent::Terminal(terminal) => { - self.render_terminal_tool_call(terminal, tool_call, window, cx) + self.render_terminal_tool_call(entry_ix, terminal, tool_call, window, cx) } } } @@ -1420,6 +1324,7 @@ impl AcpThreadView { fn render_diff_editor( &self, + entry_ix: usize, multibuffer: &Entity, cx: &Context, ) -> AnyElement { @@ -1428,7 +1333,9 @@ impl AcpThreadView { .border_t_1() .border_color(self.tool_card_border_color(cx)) .child( - if let Some(editor) = self.diff_editors.get(&multibuffer.entity_id()) { + if let Some(entry) = self.entry_view_state.entry(entry_ix) + && let Some(editor) = entry.editor_for_diff(&multibuffer) + { editor.clone().into_any_element() } else { Empty.into_any() @@ -1439,6 +1346,7 @@ impl AcpThreadView { fn render_terminal_tool_call( &self, + entry_ix: usize, terminal: &Entity, tool_call: &ToolCall, window: &Window, @@ -1627,8 +1535,11 @@ impl AcpThreadView { })), ); - let show_output = - self.terminal_expanded && self.terminal_views.contains_key(&terminal.entity_id()); + let terminal_view = self + .entry_view_state + .entry(entry_ix) + .and_then(|entry| entry.terminal(&terminal)); + let show_output = self.terminal_expanded && terminal_view.is_some(); v_flex() .mb_2() @@ -1661,8 +1572,6 @@ impl AcpThreadView { ), ) .when(show_output, |this| { - let terminal_view = self.terminal_views.get(&terminal.entity_id()).unwrap(); - this.child( div() .pt_2() @@ -1672,7 +1581,7 @@ impl AcpThreadView { .bg(cx.theme().colors().editor_background) .rounded_b_md() .text_ui_sm(cx) - .child(terminal_view.clone()), + .children(terminal_view.clone()), ) }) .into_any() @@ -3075,12 +2984,7 @@ impl AcpThreadView { } fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context) { - for diff_editor in self.diff_editors.values() { - diff_editor.update(cx, |diff_editor, cx| { - diff_editor.set_text_style_refinement(diff_editor_text_style_refinement(cx)); - cx.notify(); - }) - } + self.entry_view_state.settings_changed(cx); } pub(crate) fn insert_dragged_files( @@ -3379,18 +3283,6 @@ fn plan_label_markdown_style( } } -fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement { - TextStyleRefinement { - font_size: Some( - TextSize::Small - .rems(cx) - .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx)) - .into(), - ), - ..Default::default() - } -} - fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { let default_md_style = default_markdown_style(true, window, cx); @@ -3405,16 +3297,16 @@ fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { #[cfg(test)] pub(crate) mod tests { - use std::{path::Path, sync::Arc}; + use std::path::Path; + use acp_thread::StubAgentConnection; use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::SessionId; use editor::EditorSettings; use fs::FakeFs; - use futures::future::try_join_all; use gpui::{SemanticVersion, TestAppContext, VisualTestContext}; - use parking_lot::Mutex; - use rand::Rng; + use project::Project; + use serde_json::json; use settings::SettingsStore; use super::*; @@ -3497,8 +3389,8 @@ pub(crate) mod tests { raw_input: None, raw_output: None, }; - let connection = StubAgentConnection::new(vec![acp::SessionUpdate::ToolCall(tool_call)]) - .with_permission_requests(HashMap::from_iter([( + let connection = + StubAgentConnection::new().with_permission_requests(HashMap::from_iter([( tool_call_id, vec![acp::PermissionOption { id: acp::PermissionOptionId("1".into()), @@ -3506,6 +3398,9 @@ pub(crate) mod tests { kind: acp::PermissionOptionKind::AllowOnce, }], )])); + + connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]); + let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); @@ -3605,115 +3500,6 @@ pub(crate) mod tests { } } - #[derive(Clone, Default)] - struct StubAgentConnection { - sessions: Arc>>>, - permission_requests: HashMap>, - updates: Vec, - } - - impl StubAgentConnection { - fn new(updates: Vec) -> Self { - Self { - updates, - permission_requests: HashMap::default(), - sessions: Arc::default(), - } - } - - fn with_permission_requests( - mut self, - permission_requests: HashMap>, - ) -> Self { - self.permission_requests = permission_requests; - self - } - } - - impl AgentConnection for StubAgentConnection { - fn auth_methods(&self) -> &[acp::AuthMethod] { - &[] - } - - fn new_thread( - self: Rc, - project: Entity, - _cwd: &Path, - cx: &mut gpui::AsyncApp, - ) -> Task>> { - let session_id = SessionId( - rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(7) - .map(char::from) - .collect::() - .into(), - ); - let thread = cx - .new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx)) - .unwrap(); - self.sessions.lock().insert(session_id, thread.downgrade()); - Task::ready(Ok(thread)) - } - - fn authenticate( - &self, - _method_id: acp::AuthMethodId, - _cx: &mut App, - ) -> Task> { - unimplemented!() - } - - fn prompt( - &self, - _id: Option, - params: acp::PromptRequest, - cx: &mut App, - ) -> Task> { - let sessions = self.sessions.lock(); - let thread = sessions.get(¶ms.session_id).unwrap(); - let mut tasks = vec![]; - for update in &self.updates { - let thread = thread.clone(); - let update = update.clone(); - let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) = &update - && let Some(options) = self.permission_requests.get(&tool_call.id) - { - Some((tool_call.clone(), options.clone())) - } else { - None - }; - let task = cx.spawn(async move |cx| { - if let Some((tool_call, options)) = permission_request { - let permission = thread.update(cx, |thread, cx| { - thread.request_tool_call_authorization( - tool_call.clone(), - options.clone(), - cx, - ) - })?; - permission.await?; - } - thread.update(cx, |thread, cx| { - thread.handle_session_update(update.clone(), cx).unwrap(); - })?; - anyhow::Ok(()) - }); - tasks.push(task); - } - cx.spawn(async move |_| { - try_join_all(tasks).await?; - Ok(acp::PromptResponse { - stop_reason: acp::StopReason::EndTurn, - }) - }) - } - - fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) { - unimplemented!() - } - } - #[derive(Clone)] struct SaboteurAgentConnection; @@ -3722,19 +3508,17 @@ pub(crate) mod tests { self: Rc, project: Entity, _cwd: &Path, - cx: &mut gpui::AsyncApp, + cx: &mut gpui::App, ) -> Task>> { - Task::ready(Ok(cx - .new(|cx| { - AcpThread::new( - "SaboteurAgentConnection", - self, - project, - SessionId("test".into()), - cx, - ) - }) - .unwrap())) + Task::ready(Ok(cx.new(|cx| { + AcpThread::new( + "SaboteurAgentConnection", + self, + project, + SessionId("test".into()), + cx, + ) + }))) } fn auth_methods(&self) -> &[acp::AuthMethod] { @@ -3776,4 +3560,142 @@ pub(crate) mod tests { EditorSettings::register(cx); }); } + + #[gpui::test] + async fn test_rewind_views(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/project", + json!({ + "test1.txt": "old content 1", + "test2.txt": "old content 2" + }), + ) + .await; + let project = Project::test(fs, [Path::new("/project")], cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + let thread_store = + cx.update(|_window, cx| cx.new(|cx| ThreadStore::fake(project.clone(), cx))); + let text_thread_store = + cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx))); + + let connection = Rc::new(StubAgentConnection::new()); + let thread_view = cx.update(|window, cx| { + cx.new(|cx| { + AcpThreadView::new( + Rc::new(StubAgentServer::new(connection.as_ref().clone())), + workspace.downgrade(), + project.clone(), + thread_store.clone(), + text_thread_store.clone(), + window, + cx, + ) + }) + }); + + cx.run_until_parked(); + + let thread = thread_view + .read_with(cx, |view, _| view.thread().cloned()) + .unwrap(); + + // First user message + connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(acp::ToolCall { + id: acp::ToolCallId("tool1".into()), + title: "Edit file 1".into(), + kind: acp::ToolKind::Edit, + status: acp::ToolCallStatus::Completed, + content: vec![acp::ToolCallContent::Diff { + diff: acp::Diff { + path: "/project/test1.txt".into(), + old_text: Some("old content 1".into()), + new_text: "new content 1".into(), + }, + }], + locations: vec![], + raw_input: None, + raw_output: None, + })]); + + thread + .update(cx, |thread, cx| thread.send_raw("Give me a diff", cx)) + .await + .unwrap(); + cx.run_until_parked(); + + thread.read_with(cx, |thread, _| { + assert_eq!(thread.entries().len(), 2); + }); + + thread_view.read_with(cx, |view, _| { + assert_eq!(view.entry_view_state.entry(0).unwrap().len(), 0); + assert_eq!(view.entry_view_state.entry(1).unwrap().len(), 1); + }); + + // Second user message + connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(acp::ToolCall { + id: acp::ToolCallId("tool2".into()), + title: "Edit file 2".into(), + kind: acp::ToolKind::Edit, + status: acp::ToolCallStatus::Completed, + content: vec![acp::ToolCallContent::Diff { + diff: acp::Diff { + path: "/project/test2.txt".into(), + old_text: Some("old content 2".into()), + new_text: "new content 2".into(), + }, + }], + locations: vec![], + raw_input: None, + raw_output: None, + })]); + + thread + .update(cx, |thread, cx| thread.send_raw("Another one", cx)) + .await + .unwrap(); + cx.run_until_parked(); + + let second_user_message_id = thread.read_with(cx, |thread, _| { + assert_eq!(thread.entries().len(), 4); + let AgentThreadEntry::UserMessage(user_message) = thread.entries().get(2).unwrap() + else { + panic!(); + }; + user_message.id.clone().unwrap() + }); + + thread_view.read_with(cx, |view, _| { + assert_eq!(view.entry_view_state.entry(0).unwrap().len(), 0); + assert_eq!(view.entry_view_state.entry(1).unwrap().len(), 1); + assert_eq!(view.entry_view_state.entry(2).unwrap().len(), 0); + assert_eq!(view.entry_view_state.entry(3).unwrap().len(), 1); + }); + + // Rewind to first message + thread + .update(cx, |thread, cx| thread.rewind(second_user_message_id, cx)) + .await + .unwrap(); + + cx.run_until_parked(); + + thread.read_with(cx, |thread, _| { + assert_eq!(thread.entries().len(), 2); + }); + + thread_view.read_with(cx, |view, _| { + assert_eq!(view.entry_view_state.entry(0).unwrap().len(), 0); + assert_eq!(view.entry_view_state.entry(1).unwrap().len(), 1); + + // Old views should be dropped + assert!(view.entry_view_state.entry(2).is_none()); + assert!(view.entry_view_state.entry(3).is_none()); + }); + } } From eb9bbaacb1ccd0f4d92325e24a158739faa3872c Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 14 Aug 2025 15:07:28 -0400 Subject: [PATCH 352/693] Add onboarding reset restore script (#36202) Release Notes: - N/A --- script/onboarding | 176 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100755 script/onboarding diff --git a/script/onboarding b/script/onboarding new file mode 100755 index 0000000000..6cc878ec96 --- /dev/null +++ b/script/onboarding @@ -0,0 +1,176 @@ +#!/usr/bin/env bash + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +CHANNEL="$1" +COMMAND="$2" + +if [[ "$CHANNEL" != "stable" && "$CHANNEL" != "preview" && "$CHANNEL" != "nightly" && "$CHANNEL" != "dev" ]]; then + echo -e "${RED}Error: Invalid channel '$CHANNEL'. Must be one of: stable, preview, nightly, dev${NC}" + exit 1 +fi + +if [[ "$OSTYPE" == "darwin"* ]]; then + DB_BASE_DIR="$HOME/Library/Application Support/Zed/db" + DB_DIR="$DB_BASE_DIR/0-$CHANNEL" + DB_BACKUP_DIR="$DB_BASE_DIR/0-$CHANNEL.onboarding_backup" + case "$CHANNEL" in + stable) APP_NAME="Zed" ;; + preview) APP_NAME="Zed Preview" ;; + nightly) APP_NAME="Zed Nightly" ;; + dev) APP_NAME="Zed Dev" ;; + esac +elif [[ "$OSTYPE" == "linux-gnu"* ]]; then + DB_BASE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/zed/db" + DB_DIR="$DB_BASE_DIR/0-$CHANNEL" + DB_BACKUP_DIR="$DB_BASE_DIR/0-$CHANNEL.onboarding_backup" + case "$CHANNEL" in + stable) APP_NAME="zed" ;; + preview) APP_NAME="zed-preview" ;; + nightly) APP_NAME="zed-nightly" ;; + dev) APP_NAME="zed-dev" ;; + esac +elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "win32" ]]; then + LOCALAPPDATA_PATH="${LOCALAPPDATA:-$USERPROFILE/AppData/Local}" + DB_BASE_DIR="$LOCALAPPDATA_PATH/Zed/db" + DB_DIR="$DB_BASE_DIR/0-$CHANNEL" + DB_BACKUP_DIR="$DB_BASE_DIR/0-$CHANNEL.onboarding_backup" + + case "$CHANNEL" in + stable) APP_NAME="Zed" ;; + preview) APP_NAME="Zed Preview" ;; + nightly) APP_NAME="Zed Nightly" ;; + dev) APP_NAME="Zed Dev" ;; + esac +else + echo -e "${RED}Error: Unsupported OS type: $OSTYPE${NC}" + exit 1 +fi + +reset_onboarding() { + echo -e "${BLUE}=== Resetting $APP_NAME to First-Time User State ===${NC}" + echo "" + + if [ ! -d "$DB_DIR" ]; then + echo -e "${YELLOW}No database directory found at: $DB_DIR${NC}" + echo "Zed will create a fresh database on next launch and show onboarding." + exit 0 + fi + + if [ -d "$DB_BACKUP_DIR" ]; then + echo -e "${RED}ERROR: Backup already exists at: $DB_BACKUP_DIR${NC}" + echo "" + echo "This suggests you've already run 'onboarding reset'." + echo "To avoid losing your original database, this script won't overwrite the backup." + echo "" + echo "Options:" + echo " 1. Run './script/onboarding $CHANNEL restore' to restore your original database" + echo " 2. Manually remove the backup if you're sure: rm -rf $DB_BACKUP_DIR" + exit 1 + fi + + echo -e "${YELLOW}Moving $DB_DIR to $DB_BACKUP_DIR${NC}" + mv "$DB_DIR" "$DB_BACKUP_DIR" + + echo -e "${GREEN}✓ Backed up: $DB_BACKUP_DIR${NC}" + echo "" + echo -e "${GREEN}Success! Zed has been reset to first-time user state.${NC}" + echo "" + echo "Next steps:" + echo " 1. Start Zed - you should see the onboarding flow" + echo " 2. When done testing, run: ./script/onboarding $CHANNEL restore" + echo "" + echo -e "${YELLOW}Note: All your workspace data is safely preserved in the backup.${NC}" +} + +restore_onboarding() { + echo -e "${BLUE}=== Restoring Original $APP_NAME Database ===${NC}" + echo "" + + if [ ! -d "$DB_BACKUP_DIR" ]; then + echo -e "${RED}ERROR: No backup found at: $DB_BACKUP_DIR${NC}" + echo "" + echo "Run './script/onboarding $CHANNEL reset' first to create a backup." + exit 1 + fi + + if [ -d "$DB_DIR" ]; then + echo -e "${YELLOW}Removing current database directory: $DB_DIR${NC}" + rm -rf "$DB_DIR" + fi + + echo -e "${YELLOW}Restoring $DB_BACKUP_DIR to $DB_DIR${NC}" + mv "$DB_BACKUP_DIR" "$DB_DIR" + + echo -e "${GREEN}✓ Restored: $DB_DIR${NC}" + echo "" + echo -e "${GREEN}Success! Your original database has been restored.${NC}" +} + +show_status() { + echo -e "${BLUE}=== Zed Onboarding Test Status ===${NC}" + echo "" + + if [ -d "$DB_BACKUP_DIR" ]; then + echo -e "${YELLOW}Status: TESTING MODE${NC}" + echo " • Original database: $DB_BACKUP_DIR" + echo " • Zed is using: $DB_DIR" + echo " • Run './script/onboarding $CHANNEL restore' to return to normal" + elif [ -d "$DB_DIR" ]; then + echo -e "${GREEN}Status: NORMAL${NC}" + echo " • Zed is using: $DB_DIR" + echo " • Run './script/onboarding $CHANNEL reset' to test onboarding" + else + echo -e "${BLUE}Status: NO DATABASE${NC}" + echo " • No Zed database directory exists yet" + echo " • Zed will show onboarding on next launch" + fi +} + +case "${COMMAND:-}" in + reset) + reset_onboarding + ;; + restore) + restore_onboarding + ;; + status) + show_status + ;; + *) + echo -e "${BLUE}Zed Onboarding Test Script${NC}" + echo "" + echo "Usage: $(basename $0) [channel] " + echo "" + echo "Commands:" + echo " reset - Back up current database and reset to show onboarding" + echo " restore - Restore the original database after testing" + echo " status - Show current testing status" + echo "" + echo "Channels:" + echo " stable, preview, nightly, dev" + echo "" + echo "Working with channel: $CHANNEL" + echo "Database directory: $DB_DIR" + echo "" + echo "Examples:" + echo " ./script/onboarding nightly reset # Reset nightly" + echo " ./script/onboarding stable reset # Reset stable" + echo " ./script/onboarding preview restore # Restore preview" + echo "" + echo "Workflow:" + echo " 1. Close Zed" + echo " 2. ./script/onboarding nightly reset" + echo " 3. Open Zed" + echo " 4. Test onboarding" + echo " 5. Close Zed" + echo " 6. ./script/onboarding nightly restore" + exit 1 + ;; +esac From b65e9af3e97a5198dd0b3665f7c712690cf19561 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 14 Aug 2025 13:08:35 -0600 Subject: [PATCH 353/693] Add [f/]f to follow the next collaborator (#36191) Release Notes: - vim: Add `[f`/`]f` to go to the next collaborator --- assets/keymaps/vim.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 560ca3bdd8..be6d34a134 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -58,6 +58,8 @@ "[ space": "vim::InsertEmptyLineAbove", "[ e": "editor::MoveLineUp", "] e": "editor::MoveLineDown", + "[ f": "workspace::FollowNextCollaborator", + "] f": "workspace::FollowNextCollaborator", // Word motions "w": "vim::NextWordStart", From 3a711d08149dbaf1ac40ee5578d276dbc69e35c1 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 14 Aug 2025 15:19:37 -0400 Subject: [PATCH 354/693] Remove onboarding script (#36203) Just use `ZED_STATELESS=1 zed` instead! Release Notes: - N/A *or* Added/Fixed/Improved ... --- script/onboarding | 176 ---------------------------------------------- 1 file changed, 176 deletions(-) delete mode 100755 script/onboarding diff --git a/script/onboarding b/script/onboarding deleted file mode 100755 index 6cc878ec96..0000000000 --- a/script/onboarding +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/env bash - -set -e - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -CHANNEL="$1" -COMMAND="$2" - -if [[ "$CHANNEL" != "stable" && "$CHANNEL" != "preview" && "$CHANNEL" != "nightly" && "$CHANNEL" != "dev" ]]; then - echo -e "${RED}Error: Invalid channel '$CHANNEL'. Must be one of: stable, preview, nightly, dev${NC}" - exit 1 -fi - -if [[ "$OSTYPE" == "darwin"* ]]; then - DB_BASE_DIR="$HOME/Library/Application Support/Zed/db" - DB_DIR="$DB_BASE_DIR/0-$CHANNEL" - DB_BACKUP_DIR="$DB_BASE_DIR/0-$CHANNEL.onboarding_backup" - case "$CHANNEL" in - stable) APP_NAME="Zed" ;; - preview) APP_NAME="Zed Preview" ;; - nightly) APP_NAME="Zed Nightly" ;; - dev) APP_NAME="Zed Dev" ;; - esac -elif [[ "$OSTYPE" == "linux-gnu"* ]]; then - DB_BASE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/zed/db" - DB_DIR="$DB_BASE_DIR/0-$CHANNEL" - DB_BACKUP_DIR="$DB_BASE_DIR/0-$CHANNEL.onboarding_backup" - case "$CHANNEL" in - stable) APP_NAME="zed" ;; - preview) APP_NAME="zed-preview" ;; - nightly) APP_NAME="zed-nightly" ;; - dev) APP_NAME="zed-dev" ;; - esac -elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "win32" ]]; then - LOCALAPPDATA_PATH="${LOCALAPPDATA:-$USERPROFILE/AppData/Local}" - DB_BASE_DIR="$LOCALAPPDATA_PATH/Zed/db" - DB_DIR="$DB_BASE_DIR/0-$CHANNEL" - DB_BACKUP_DIR="$DB_BASE_DIR/0-$CHANNEL.onboarding_backup" - - case "$CHANNEL" in - stable) APP_NAME="Zed" ;; - preview) APP_NAME="Zed Preview" ;; - nightly) APP_NAME="Zed Nightly" ;; - dev) APP_NAME="Zed Dev" ;; - esac -else - echo -e "${RED}Error: Unsupported OS type: $OSTYPE${NC}" - exit 1 -fi - -reset_onboarding() { - echo -e "${BLUE}=== Resetting $APP_NAME to First-Time User State ===${NC}" - echo "" - - if [ ! -d "$DB_DIR" ]; then - echo -e "${YELLOW}No database directory found at: $DB_DIR${NC}" - echo "Zed will create a fresh database on next launch and show onboarding." - exit 0 - fi - - if [ -d "$DB_BACKUP_DIR" ]; then - echo -e "${RED}ERROR: Backup already exists at: $DB_BACKUP_DIR${NC}" - echo "" - echo "This suggests you've already run 'onboarding reset'." - echo "To avoid losing your original database, this script won't overwrite the backup." - echo "" - echo "Options:" - echo " 1. Run './script/onboarding $CHANNEL restore' to restore your original database" - echo " 2. Manually remove the backup if you're sure: rm -rf $DB_BACKUP_DIR" - exit 1 - fi - - echo -e "${YELLOW}Moving $DB_DIR to $DB_BACKUP_DIR${NC}" - mv "$DB_DIR" "$DB_BACKUP_DIR" - - echo -e "${GREEN}✓ Backed up: $DB_BACKUP_DIR${NC}" - echo "" - echo -e "${GREEN}Success! Zed has been reset to first-time user state.${NC}" - echo "" - echo "Next steps:" - echo " 1. Start Zed - you should see the onboarding flow" - echo " 2. When done testing, run: ./script/onboarding $CHANNEL restore" - echo "" - echo -e "${YELLOW}Note: All your workspace data is safely preserved in the backup.${NC}" -} - -restore_onboarding() { - echo -e "${BLUE}=== Restoring Original $APP_NAME Database ===${NC}" - echo "" - - if [ ! -d "$DB_BACKUP_DIR" ]; then - echo -e "${RED}ERROR: No backup found at: $DB_BACKUP_DIR${NC}" - echo "" - echo "Run './script/onboarding $CHANNEL reset' first to create a backup." - exit 1 - fi - - if [ -d "$DB_DIR" ]; then - echo -e "${YELLOW}Removing current database directory: $DB_DIR${NC}" - rm -rf "$DB_DIR" - fi - - echo -e "${YELLOW}Restoring $DB_BACKUP_DIR to $DB_DIR${NC}" - mv "$DB_BACKUP_DIR" "$DB_DIR" - - echo -e "${GREEN}✓ Restored: $DB_DIR${NC}" - echo "" - echo -e "${GREEN}Success! Your original database has been restored.${NC}" -} - -show_status() { - echo -e "${BLUE}=== Zed Onboarding Test Status ===${NC}" - echo "" - - if [ -d "$DB_BACKUP_DIR" ]; then - echo -e "${YELLOW}Status: TESTING MODE${NC}" - echo " • Original database: $DB_BACKUP_DIR" - echo " • Zed is using: $DB_DIR" - echo " • Run './script/onboarding $CHANNEL restore' to return to normal" - elif [ -d "$DB_DIR" ]; then - echo -e "${GREEN}Status: NORMAL${NC}" - echo " • Zed is using: $DB_DIR" - echo " • Run './script/onboarding $CHANNEL reset' to test onboarding" - else - echo -e "${BLUE}Status: NO DATABASE${NC}" - echo " • No Zed database directory exists yet" - echo " • Zed will show onboarding on next launch" - fi -} - -case "${COMMAND:-}" in - reset) - reset_onboarding - ;; - restore) - restore_onboarding - ;; - status) - show_status - ;; - *) - echo -e "${BLUE}Zed Onboarding Test Script${NC}" - echo "" - echo "Usage: $(basename $0) [channel] " - echo "" - echo "Commands:" - echo " reset - Back up current database and reset to show onboarding" - echo " restore - Restore the original database after testing" - echo " status - Show current testing status" - echo "" - echo "Channels:" - echo " stable, preview, nightly, dev" - echo "" - echo "Working with channel: $CHANNEL" - echo "Database directory: $DB_DIR" - echo "" - echo "Examples:" - echo " ./script/onboarding nightly reset # Reset nightly" - echo " ./script/onboarding stable reset # Reset stable" - echo " ./script/onboarding preview restore # Restore preview" - echo "" - echo "Workflow:" - echo " 1. Close Zed" - echo " 2. ./script/onboarding nightly reset" - echo " 3. Open Zed" - echo " 4. Test onboarding" - echo " 5. Close Zed" - echo " 6. ./script/onboarding nightly restore" - exit 1 - ;; -esac From b7c562f359b65f2d529916503d579c027c49614d Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 14 Aug 2025 21:28:59 +0200 Subject: [PATCH 355/693] Bump `async-trait` (#36201) The latest release has span changes in it which prevents rust-analyzer from constantly showing `Box` and `Box::pin` on hover as well as those items polluting the go to definition feature on every identifier. See https://github.com/dtolnay/async-trait/pull/293 Release Notes: - N/A --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96cc1581a3..b4e4d9f876 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1304,9 +1304,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", From e2ce787c051032bd6d3ad61c6ffd26b062d0f246 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 14 Aug 2025 23:18:07 +0200 Subject: [PATCH 356/693] editor: Limit target names in hover links multibuffer titles (#36207) Release Notes: - N/A --- crates/editor/src/editor.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1f350cf0d0..a9780ed6c2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -15879,6 +15879,8 @@ impl Editor { .text_for_range(location.range.clone()) .collect::() }) + .unique() + .take(3) .join(", "); format!("{tab_kind} for {target}") }) @@ -16085,6 +16087,8 @@ impl Editor { .text_for_range(location.range.clone()) .collect::() }) + .unique() + .take(3) .join(", "); let title = format!("References to {target}"); Self::open_locations_in_multibuffer( From b1e806442aefd7cd5df740234b2d7c3539dc905a Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 14 Aug 2025 17:31:14 -0400 Subject: [PATCH 357/693] Support images in agent2 threads (#36152) - Support adding ImageContent to messages through copy/paste and through path completions - Ensure images are fully converted to LanguageModelImageContent before sending them to the model - Update ACP crate to v0.0.24 to enable passing image paths through the protocol Release Notes: - N/A --------- Co-authored-by: Conrad Irwin --- Cargo.lock | 4 +- Cargo.toml | 2 +- crates/acp_thread/src/acp_thread.rs | 9 +- crates/acp_thread/src/mention.rs | 9 + .../agent_ui/src/acp/completion_provider.rs | 218 ++++++++++----- crates/agent_ui/src/acp/message_editor.rs | 255 ++++++++++++++++-- crates/agent_ui/src/acp/thread_view.rs | 10 +- 7 files changed, 415 insertions(+), 92 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b4e4d9f876..d0809bd880 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,9 +172,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.23" +version = "0.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fad72b7b8ee4331b3a4c8d43c107e982a4725564b4ee658ae5c4e79d2b486e8" +checksum = "8fd68bbbef8e424fb8a605c5f0b00c360f682c4528b0a5feb5ec928aaf5ce28e" dependencies = [ "anyhow", "futures 0.3.31", diff --git a/Cargo.toml b/Cargo.toml index 1baa6d3d74..a872cadd39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -425,7 +425,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.23" +agent-client-protocol = "0.0.24" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 4bdc42ea2e..4005f27a0c 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -443,9 +443,8 @@ impl ContentBlock { }), .. }) => Self::resource_link_md(&uri), - acp::ContentBlock::Image(_) - | acp::ContentBlock::Audio(_) - | acp::ContentBlock::Resource(_) => String::new(), + acp::ContentBlock::Image(image) => Self::image_md(&image), + acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => String::new(), } } @@ -457,6 +456,10 @@ impl ContentBlock { } } + fn image_md(_image: &acp::ImageContent) -> String { + "`Image`".into() + } + fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str { match self { ContentBlock::Empty => "", diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index b18cbfe18e..b9b021c4ca 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -6,6 +6,7 @@ use std::{ fmt, ops::Range, path::{Path, PathBuf}, + str::FromStr, }; use ui::{App, IconName, SharedString}; use url::Url; @@ -224,6 +225,14 @@ impl MentionUri { } } +impl FromStr for MentionUri { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + Self::parse(s) + } +} + pub struct MentionLink<'a>(&'a MentionUri); impl fmt::Display for MentionLink<'_> { diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 720ee23b00..adcfab85b1 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -1,5 +1,6 @@ +use std::ffi::OsStr; use std::ops::Range; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::AtomicBool; @@ -8,13 +9,14 @@ use anyhow::{Context as _, Result, anyhow}; use collections::{HashMap, HashSet}; use editor::display_map::CreaseId; use editor::{CompletionProvider, Editor, ExcerptId, ToOffset as _}; - -use futures::future::try_join_all; +use futures::future::{Shared, try_join_all}; +use futures::{FutureExt, TryFutureExt}; use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{App, Entity, Task, WeakEntity}; +use gpui::{App, Entity, ImageFormat, Img, Task, WeakEntity}; use http_client::HttpClientWithUrl; use itertools::Itertools as _; use language::{Buffer, CodeLabel, HighlightId}; +use language_model::LanguageModelImage; use lsp::CompletionContext; use parking_lot::Mutex; use project::{ @@ -43,24 +45,43 @@ use crate::context_picker::{ available_context_picker_entries, recent_context_picker_entries, selection_ranges, }; +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MentionImage { + pub abs_path: Option>, + pub data: SharedString, + pub format: ImageFormat, +} + #[derive(Default)] pub struct MentionSet { uri_by_crease_id: HashMap, - fetch_results: HashMap, + fetch_results: HashMap>>>, + images: HashMap>>>, } impl MentionSet { - pub fn insert(&mut self, crease_id: CreaseId, uri: MentionUri) { + pub fn insert_uri(&mut self, crease_id: CreaseId, uri: MentionUri) { self.uri_by_crease_id.insert(crease_id, uri); } - pub fn add_fetch_result(&mut self, url: Url, content: String) { + pub fn add_fetch_result(&mut self, url: Url, content: Shared>>) { self.fetch_results.insert(url, content); } + pub fn insert_image( + &mut self, + crease_id: CreaseId, + task: Shared>>, + ) { + self.images.insert(crease_id, task); + } + pub fn drain(&mut self) -> impl Iterator { self.fetch_results.clear(); - self.uri_by_crease_id.drain().map(|(id, _)| id) + self.uri_by_crease_id + .drain() + .map(|(id, _)| id) + .chain(self.images.drain().map(|(id, _)| id)) } pub fn clear(&mut self) { @@ -76,7 +97,7 @@ impl MentionSet { window: &mut Window, cx: &mut App, ) -> Task>> { - let contents = self + let mut contents = self .uri_by_crease_id .iter() .map(|(&crease_id, uri)| { @@ -85,19 +106,59 @@ impl MentionSet { // TODO directories let uri = uri.clone(); let abs_path = abs_path.to_path_buf(); - let buffer_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(abs_path, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_buffer(path, cx)) - }); + let extension = abs_path.extension().and_then(OsStr::to_str).unwrap_or(""); - cx.spawn(async move |cx| { - let buffer = buffer_task?.await?; - let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; + if Img::extensions().contains(&extension) && !extension.contains("svg") { + let open_image_task = project.update(cx, |project, cx| { + let path = project + .find_project_path(&abs_path, cx) + .context("Failed to find project path")?; + anyhow::Ok(project.open_image(path, cx)) + }); - anyhow::Ok((crease_id, Mention { uri, content })) - }) + cx.spawn(async move |cx| { + let image_item = open_image_task?.await?; + let (data, format) = image_item.update(cx, |image_item, cx| { + let format = image_item.image.format; + ( + LanguageModelImage::from_image( + image_item.image.clone(), + cx, + ), + format, + ) + })?; + let data = cx.spawn(async move |_| { + if let Some(data) = data.await { + Ok(data.source) + } else { + anyhow::bail!("Failed to convert image") + } + }); + + anyhow::Ok(( + crease_id, + Mention::Image(MentionImage { + abs_path: Some(abs_path.as_path().into()), + data: data.await?, + format, + }), + )) + }) + } else { + let buffer_task = project.update(cx, |project, cx| { + let path = project + .find_project_path(abs_path, cx) + .context("Failed to find project path")?; + anyhow::Ok(project.open_buffer(path, cx)) + }); + cx.spawn(async move |cx| { + let buffer = buffer_task?.await?; + let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; + + anyhow::Ok((crease_id, Mention::Text { uri, content })) + }) + } } MentionUri::Symbol { path, line_range, .. @@ -130,7 +191,7 @@ impl MentionSet { .collect() })?; - anyhow::Ok((crease_id, Mention { uri, content })) + anyhow::Ok((crease_id, Mention::Text { uri, content })) }) } MentionUri::Thread { id: thread_id, .. } => { @@ -145,7 +206,7 @@ impl MentionSet { thread.latest_detailed_summary_or_text().to_string() })?; - anyhow::Ok((crease_id, Mention { uri, content })) + anyhow::Ok((crease_id, Mention::Text { uri, content })) }) } MentionUri::TextThread { path, .. } => { @@ -156,7 +217,7 @@ impl MentionSet { cx.spawn(async move |cx| { let context = context.await?; let xml = context.update(cx, |context, cx| context.to_xml(cx))?; - anyhow::Ok((crease_id, Mention { uri, content: xml })) + anyhow::Ok((crease_id, Mention::Text { uri, content: xml })) }) } MentionUri::Rule { id: prompt_id, .. } => { @@ -169,25 +230,39 @@ impl MentionSet { cx.spawn(async move |_| { // TODO: report load errors instead of just logging let text = text_task.await?; - anyhow::Ok((crease_id, Mention { uri, content: text })) + anyhow::Ok((crease_id, Mention::Text { uri, content: text })) }) } MentionUri::Fetch { url } => { - let Some(content) = self.fetch_results.get(&url) else { + let Some(content) = self.fetch_results.get(&url).cloned() else { return Task::ready(Err(anyhow!("missing fetch result"))); }; - Task::ready(Ok(( - crease_id, - Mention { - uri: uri.clone(), - content: content.clone(), - }, - ))) + let uri = uri.clone(); + cx.spawn(async move |_| { + Ok(( + crease_id, + Mention::Text { + uri, + content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, + }, + )) + }) } } }) .collect::>(); + contents.extend(self.images.iter().map(|(crease_id, image)| { + let crease_id = *crease_id; + let image = image.clone(); + cx.spawn(async move |_| { + Ok(( + crease_id, + Mention::Image(image.await.map_err(|e| anyhow::anyhow!("{e}"))?), + )) + }) + })); + cx.spawn(async move |_cx| { let contents = try_join_all(contents).await?.into_iter().collect(); anyhow::Ok(contents) @@ -195,10 +270,10 @@ impl MentionSet { } } -#[derive(Debug)] -pub struct Mention { - pub uri: MentionUri, - pub content: String, +#[derive(Debug, Eq, PartialEq)] +pub enum Mention { + Text { uri: MentionUri, content: String }, + Image(MentionImage), } pub(crate) enum Match { @@ -536,7 +611,10 @@ impl ContextPickerCompletionProvider { crease_ids.try_into().unwrap() }); - mention_set.lock().insert(crease_id, uri); + mention_set.lock().insert_uri( + crease_id, + MentionUri::Selection { path, line_range }, + ); current_offset += text_len + 1; } @@ -786,6 +864,7 @@ impl ContextPickerCompletionProvider { let url_to_fetch = url_to_fetch.clone(); let source_range = source_range.clone(); let icon_path = icon_path.clone(); + let mention_uri = mention_uri.clone(); Arc::new(move |_, window, cx| { let Some(url) = url::Url::parse(url_to_fetch.as_ref()) .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}"))) @@ -799,6 +878,7 @@ impl ContextPickerCompletionProvider { let http_client = http_client.clone(); let source_range = source_range.clone(); let icon_path = icon_path.clone(); + let mention_uri = mention_uri.clone(); window.defer(cx, move |window, cx| { let url = url.clone(); @@ -819,17 +899,24 @@ impl ContextPickerCompletionProvider { let mention_set = mention_set.clone(); let http_client = http_client.clone(); let source_range = source_range.clone(); + + let url_string = url.to_string(); + let fetch = cx + .background_executor() + .spawn(async move { + fetch_url_content(http_client, url_string) + .map_err(|e| e.to_string()) + .await + }) + .shared(); + mention_set.lock().add_fetch_result(url, fetch.clone()); + window .spawn(cx, async move |cx| { - if let Some(content) = - fetch_url_content(http_client, url.to_string()) - .await - .notify_async_err(cx) - { - mention_set.lock().add_fetch_result(url.clone(), content); + if fetch.await.notify_async_err(cx).is_some() { mention_set .lock() - .insert(crease_id, MentionUri::Fetch { url }); + .insert_uri(crease_id, mention_uri.clone()); } else { // Remove crease if we failed to fetch editor @@ -1121,7 +1208,9 @@ fn confirm_completion_callback( window, cx, ) { - mention_set.lock().insert(crease_id, mention_uri.clone()); + mention_set + .lock() + .insert_uri(crease_id, mention_uri.clone()); } }); false @@ -1499,11 +1588,12 @@ mod tests { .into_values() .collect::>(); - assert_eq!(contents.len(), 1); - assert_eq!(contents[0].content, "1"); - assert_eq!( - contents[0].uri.to_uri().to_string(), - "file:///dir/a/one.txt" + pretty_assertions::assert_eq!( + contents, + [Mention::Text { + content: "1".into(), + uri: "file:///dir/a/one.txt".parse().unwrap() + }] ); cx.simulate_input(" "); @@ -1567,11 +1657,13 @@ mod tests { .collect::>(); assert_eq!(contents.len(), 2); - let new_mention = contents - .iter() - .find(|mention| mention.uri.to_uri().to_string() == "file:///dir/b/eight.txt") - .unwrap(); - assert_eq!(new_mention.content, "8"); + pretty_assertions::assert_eq!( + contents[1], + Mention::Text { + content: "8".to_string(), + uri: "file:///dir/b/eight.txt".parse().unwrap(), + } + ); editor.update(&mut cx, |editor, cx| { assert_eq!( @@ -1689,13 +1781,15 @@ mod tests { .collect::>(); assert_eq!(contents.len(), 3); - let new_mention = contents - .iter() - .find(|mention| { - mention.uri.to_uri().to_string() == "file:///dir/a/one.txt?symbol=MySymbol#L1:1" - }) - .unwrap(); - assert_eq!(new_mention.content, "1"); + pretty_assertions::assert_eq!( + contents[2], + Mention::Text { + content: "1".into(), + uri: "file:///dir/a/one.txt?symbol=MySymbol#L1:1" + .parse() + .unwrap(), + } + ); cx.run_until_parked(); diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index fc34420d4e..8d512948dd 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1,4 +1,5 @@ use crate::acp::completion_provider::ContextPickerCompletionProvider; +use crate::acp::completion_provider::MentionImage; use crate::acp::completion_provider::MentionSet; use acp_thread::MentionUri; use agent::TextThreadStore; @@ -6,30 +7,44 @@ use agent::ThreadStore; use agent_client_protocol as acp; use anyhow::Result; use collections::HashSet; +use editor::ExcerptId; +use editor::actions::Paste; +use editor::display_map::CreaseId; use editor::{ AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode, EditorStyle, MultiBuffer, }; +use futures::FutureExt as _; +use gpui::ClipboardEntry; +use gpui::Image; +use gpui::ImageFormat; use gpui::{ AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, Task, TextStyle, WeakEntity, }; use language::Buffer; use language::Language; +use language_model::LanguageModelImage; use parking_lot::Mutex; use project::{CompletionIntent, Project}; use settings::Settings; use std::fmt::Write; +use std::path::Path; use std::rc::Rc; use std::sync::Arc; use theme::ThemeSettings; +use ui::IconName; +use ui::SharedString; use ui::{ ActiveTheme, App, InteractiveElement, IntoElement, ParentElement, Render, Styled, TextSize, Window, div, }; use util::ResultExt; use workspace::Workspace; +use workspace::notifications::NotifyResultExt as _; use zed_actions::agent::Chat; +use super::completion_provider::Mention; + pub struct MessageEditor { editor: Entity, project: Entity, @@ -130,23 +145,41 @@ impl MessageEditor { continue; } - if let Some(mention) = contents.get(&crease_id) { - let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot); - if crease_range.start > ix { - chunks.push(text[ix..crease_range.start].into()); - } - chunks.push(acp::ContentBlock::Resource(acp::EmbeddedResource { - annotations: None, - resource: acp::EmbeddedResourceResource::TextResourceContents( - acp::TextResourceContents { - mime_type: None, - text: mention.content.clone(), - uri: mention.uri.to_uri().to_string(), - }, - ), - })); - ix = crease_range.end; + let Some(mention) = contents.get(&crease_id) else { + continue; + }; + + let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot); + if crease_range.start > ix { + chunks.push(text[ix..crease_range.start].into()); } + let chunk = match mention { + Mention::Text { uri, content } => { + acp::ContentBlock::Resource(acp::EmbeddedResource { + annotations: None, + resource: acp::EmbeddedResourceResource::TextResourceContents( + acp::TextResourceContents { + mime_type: None, + text: content.clone(), + uri: uri.to_uri().to_string(), + }, + ), + }) + } + Mention::Image(mention_image) => { + acp::ContentBlock::Image(acp::ImageContent { + annotations: None, + data: mention_image.data.to_string(), + mime_type: mention_image.format.mime_type().into(), + uri: mention_image + .abs_path + .as_ref() + .map(|path| format!("file://{}", path.display())), + }) + } + }; + chunks.push(chunk); + ix = crease_range.end; } if ix < text.len() { @@ -177,6 +210,56 @@ impl MessageEditor { cx.emit(MessageEditorEvent::Cancel) } + fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context) { + let images = cx + .read_from_clipboard() + .map(|item| { + item.into_entries() + .filter_map(|entry| { + if let ClipboardEntry::Image(image) = entry { + Some(image) + } else { + None + } + }) + .collect::>() + }) + .unwrap_or_default(); + + if images.is_empty() { + return; + } + cx.stop_propagation(); + + let replacement_text = "image"; + for image in images { + let (excerpt_id, anchor) = self.editor.update(cx, |message_editor, cx| { + let snapshot = message_editor.snapshot(window, cx); + let (excerpt_id, _, snapshot) = snapshot.buffer_snapshot.as_singleton().unwrap(); + + let anchor = snapshot.anchor_before(snapshot.len()); + message_editor.edit( + [( + multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), + format!("{replacement_text} "), + )], + cx, + ); + (*excerpt_id, anchor) + }); + + self.insert_image( + excerpt_id, + anchor, + replacement_text.len(), + Arc::new(image), + None, + window, + cx, + ); + } + } + pub fn insert_dragged_files( &self, paths: Vec, @@ -234,6 +317,68 @@ impl MessageEditor { } } + fn insert_image( + &mut self, + excerpt_id: ExcerptId, + crease_start: text::Anchor, + content_len: usize, + image: Arc, + abs_path: Option>, + window: &mut Window, + cx: &mut Context, + ) { + let Some(crease_id) = insert_crease_for_image( + excerpt_id, + crease_start, + content_len, + self.editor.clone(), + window, + cx, + ) else { + return; + }; + self.editor.update(cx, |_editor, cx| { + let format = image.format; + let convert = LanguageModelImage::from_image(image, cx); + + let task = cx + .spawn_in(window, async move |editor, cx| { + if let Some(image) = convert.await { + Ok(MentionImage { + abs_path, + data: image.source, + format, + }) + } else { + editor + .update(cx, |editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let Some(anchor) = + snapshot.anchor_in_excerpt(excerpt_id, crease_start) + else { + return; + }; + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + Err("Failed to convert image".to_string()) + } + }) + .shared(); + + cx.spawn_in(window, { + let task = task.clone(); + async move |_, cx| task.clone().await.notify_async_err(cx) + }) + .detach(); + + self.mention_set.lock().insert_image(crease_id, task); + }); + } + pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context) { self.editor.update(cx, |editor, cx| { editor.set_mode(mode); @@ -243,12 +388,13 @@ impl MessageEditor { pub fn set_message( &mut self, - message: &[acp::ContentBlock], + message: Vec, window: &mut Window, cx: &mut Context, ) { let mut text = String::new(); let mut mentions = Vec::new(); + let mut images = Vec::new(); for chunk in message { match chunk { @@ -266,8 +412,13 @@ impl MessageEditor { mentions.push((start..end, mention_uri)); } } - acp::ContentBlock::Image(_) - | acp::ContentBlock::Audio(_) + acp::ContentBlock::Image(content) => { + let start = text.len(); + text.push_str("image"); + let end = text.len(); + images.push((start..end, content)); + } + acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) | acp::ContentBlock::ResourceLink(_) => {} } @@ -293,7 +444,50 @@ impl MessageEditor { ); if let Some(crease_id) = crease_id { - self.mention_set.lock().insert(crease_id, mention_uri); + self.mention_set.lock().insert_uri(crease_id, mention_uri); + } + } + for (range, content) in images { + let Some(format) = ImageFormat::from_mime_type(&content.mime_type) else { + continue; + }; + let anchor = snapshot.anchor_before(range.start); + let abs_path = content + .uri + .as_ref() + .and_then(|uri| uri.strip_prefix("file://").map(|s| Path::new(s).into())); + + let name = content + .uri + .as_ref() + .and_then(|uri| { + uri.strip_prefix("file://") + .and_then(|path| Path::new(path).file_name()) + }) + .map(|name| name.to_string_lossy().to_string()) + .unwrap_or("Image".to_owned()); + let crease_id = crate::context_picker::insert_crease_for_mention( + anchor.excerpt_id, + anchor.text_anchor, + range.end - range.start, + name.into(), + IconName::Image.path().into(), + self.editor.clone(), + window, + cx, + ); + let data: SharedString = content.data.to_string().into(); + + if let Some(crease_id) = crease_id { + self.mention_set.lock().insert_image( + crease_id, + Task::ready(Ok(MentionImage { + abs_path, + data, + format, + })) + .shared(), + ); } } cx.notify(); @@ -319,6 +513,7 @@ impl Render for MessageEditor { .key_context("MessageEditor") .on_action(cx.listener(Self::chat)) .on_action(cx.listener(Self::cancel)) + .capture_action(cx.listener(Self::paste)) .flex_1() .child({ let settings = ThemeSettings::get_global(cx); @@ -351,6 +546,26 @@ impl Render for MessageEditor { } } +pub(crate) fn insert_crease_for_image( + excerpt_id: ExcerptId, + anchor: text::Anchor, + content_len: usize, + editor: Entity, + window: &mut Window, + cx: &mut App, +) -> Option { + crate::context_picker::insert_crease_for_mention( + excerpt_id, + anchor, + content_len, + "Image".into(), + IconName::Image.path().into(), + editor, + window, + cx, + ) +} + #[cfg(test)] mod tests { use std::path::Path; diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 0e90b93f4d..ee016b7503 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -5,9 +5,10 @@ use acp_thread::{ use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; use agent::{TextThreadStore, ThreadStore}; -use agent_client_protocol as acp; +use agent_client_protocol::{self as acp}; use agent_servers::AgentServer; use agent_settings::{AgentSettings, NotifyWhenAgentWaiting}; +use anyhow::bail; use audio::{Audio, Sound}; use buffer_diff::BufferDiff; use collections::{HashMap, HashSet}; @@ -2360,7 +2361,7 @@ impl AcpThreadView { window, cx, ); - editor.set_message(&chunks, window, cx); + editor.set_message(chunks, window, cx); editor }); let subscription = @@ -2725,7 +2726,7 @@ impl AcpThreadView { let project = workspace.project().clone(); if !project.read(cx).is_local() { - anyhow::bail!("failed to open active thread as markdown in remote project"); + bail!("failed to open active thread as markdown in remote project"); } let buffer = project.update(cx, |project, cx| { @@ -2990,12 +2991,13 @@ impl AcpThreadView { pub(crate) fn insert_dragged_files( &self, paths: Vec, - _added_worktrees: Vec>, + added_worktrees: Vec>, window: &mut Window, cx: &mut Context, ) { self.message_editor.update(cx, |message_editor, cx| { message_editor.insert_dragged_files(paths, window, cx); + drop(added_worktrees); }) } } From 8366b6ce549d7695a5a544d98a035208531e2e5d Mon Sep 17 00:00:00 2001 From: Cretezy Date: Thu, 14 Aug 2025 17:46:38 -0400 Subject: [PATCH 358/693] workspace: Disable padding on zoomed panels (#36012) Continuation of https://github.com/zed-industries/zed/pull/31913 | Before | After | | -------|------| | ![image](https://github.com/user-attachments/assets/629e7da2-6070-4abb-b469-3b0824524ca4) | ![image](https://github.com/user-attachments/assets/99e54412-2e0b-4df9-9c40-a89b0411f6d8) | Release Notes: - Disable padding on zoomed panels --- crates/workspace/src/workspace.rs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index fb78c62f9e..ba9e3bbb8a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -6664,25 +6664,15 @@ impl Render for Workspace { } }) .children(self.zoomed.as_ref().and_then(|view| { - let zoomed_view = view.upgrade()?; - let div = div() + Some(div() .occlude() .absolute() .overflow_hidden() .border_color(colors.border) .bg(colors.background) - .child(zoomed_view) + .child(view.upgrade()?) .inset_0() - .shadow_lg(); - - Some(match self.zoomed_position { - Some(DockPosition::Left) => div.right_2().border_r_1(), - Some(DockPosition::Right) => div.left_2().border_l_1(), - Some(DockPosition::Bottom) => div.top_2().border_t_1(), - None => { - div.top_2().bottom_2().left_2().right_2().border_1() - } - }) + .shadow_lg()) })) .children(self.render_notifications(window, cx)), ) From 4d27b228f776725b6f0f090b4856a7028b3dfe95 Mon Sep 17 00:00:00 2001 From: Alvaro Parker <64918109+AlvaroParker@users.noreply.github.com> Date: Thu, 14 Aug 2025 22:31:01 -0400 Subject: [PATCH 359/693] remote server: Use env flag to opt out of musl remote server build (#36069) Closes #ISSUE This will allow devs to opt out of the musl build when developing zed by running `ZED_BUILD_REMOTE_SERVER=nomusl cargo r` which also fixes remote builds on NixOS. Release Notes: - Add a env flag (`ZED_BUILD_REMOTE_SERVER=nomusl`) to opt out of musl builds when building the remote server --- crates/remote/src/ssh_session.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 4306251e44..df7212d44c 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -2069,11 +2069,17 @@ impl SshRemoteConnection { Ok(()) } + let use_musl = !build_remote_server.contains("nomusl"); let triple = format!( "{}-{}", self.ssh_platform.arch, match self.ssh_platform.os { - "linux" => "unknown-linux-musl", + "linux" => + if use_musl { + "unknown-linux-musl" + } else { + "unknown-linux-gnu" + }, "macos" => "apple-darwin", _ => anyhow::bail!("can't cross compile for: {:?}", self.ssh_platform), } @@ -2086,7 +2092,7 @@ impl SshRemoteConnection { String::new() } }; - if self.ssh_platform.os == "linux" { + if self.ssh_platform.os == "linux" && use_musl { rust_flags.push_str(" -C target-feature=+crt-static"); } if build_remote_server.contains("mold") { From 23d04331584edc7656a480715cab9532ccfc5861 Mon Sep 17 00:00:00 2001 From: smit Date: Fri, 15 Aug 2025 12:51:32 +0530 Subject: [PATCH 360/693] linux: Fix keyboard events not working on first start in X11 (#36224) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #29083 On X11, `ibus-x11` crashes on some distros after Zed interacts with it. This is not unique to Zed, `xim-rs` shows the same behavior, and there are similar upstream `ibus` reports with apps like Blender: - https://github.com/ibus/ibus/issues/2697 I opened an upstream issue to track this: - https://github.com/ibus/ibus/issues/2789 When this crash happens, we don’t get a disconnect event, so Zed keeps sending events to the IM server and waits for a response. It works on subsequent starts because IM server doesn't exist now and we default to non-XIM path. This PR detects the crash via X11 events and falls back to the non-XIM path so typing keeps working. We still need to investigate whether the root cause is in `xim-rs` or `ibus-x11`. Release Notes: - Fixed an issue on X11 where keyboard input sometimes didn’t work on first start. --- Cargo.lock | 6 ++-- crates/gpui/Cargo.toml | 2 +- crates/gpui/src/platform/linux/x11/client.rs | 38 ++++++++++++-------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0809bd880..0bafc3c386 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20277,7 +20277,7 @@ dependencies = [ [[package]] name = "xim" version = "0.4.0" -source = "git+https://github.com/XDeme1/xim-rs?rev=d50d461764c2213655cd9cf65a0ea94c70d3c4fd#d50d461764c2213655cd9cf65a0ea94c70d3c4fd" +source = "git+https://github.com/zed-industries/xim-rs?rev=c0a70c1bd2ce197364216e5e818a2cb3adb99a8d#c0a70c1bd2ce197364216e5e818a2cb3adb99a8d" dependencies = [ "ahash 0.8.11", "hashbrown 0.14.5", @@ -20290,7 +20290,7 @@ dependencies = [ [[package]] name = "xim-ctext" version = "0.3.0" -source = "git+https://github.com/XDeme1/xim-rs?rev=d50d461764c2213655cd9cf65a0ea94c70d3c4fd#d50d461764c2213655cd9cf65a0ea94c70d3c4fd" +source = "git+https://github.com/zed-industries/xim-rs?rev=c0a70c1bd2ce197364216e5e818a2cb3adb99a8d#c0a70c1bd2ce197364216e5e818a2cb3adb99a8d" dependencies = [ "encoding_rs", ] @@ -20298,7 +20298,7 @@ dependencies = [ [[package]] name = "xim-parser" version = "0.2.1" -source = "git+https://github.com/XDeme1/xim-rs?rev=d50d461764c2213655cd9cf65a0ea94c70d3c4fd#d50d461764c2213655cd9cf65a0ea94c70d3c4fd" +source = "git+https://github.com/zed-industries/xim-rs?rev=c0a70c1bd2ce197364216e5e818a2cb3adb99a8d#c0a70c1bd2ce197364216e5e818a2cb3adb99a8d" dependencies = [ "bitflags 2.9.0", ] diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index d720dfb2a1..6be8c5fd1f 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -209,7 +209,7 @@ xkbcommon = { version = "0.8.0", features = [ "wayland", "x11", ], optional = true } -xim = { git = "https://github.com/XDeme1/xim-rs", rev = "d50d461764c2213655cd9cf65a0ea94c70d3c4fd", features = [ +xim = { git = "https://github.com/zed-industries/xim-rs", rev = "c0a70c1bd2ce197364216e5e818a2cb3adb99a8d" , features = [ "x11rb-xcb", "x11rb-client", ], optional = true } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 573e4addf7..053cd0387b 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -642,13 +642,7 @@ impl X11Client { let xim_connected = xim_handler.connected; drop(state); - let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) { - Ok(handled) => handled, - Err(err) => { - log::error!("XIMClientError: {}", err); - false - } - }; + let xim_filtered = ximc.filter_event(&event, &mut xim_handler); let xim_callback_event = xim_handler.last_callback_event.take(); let mut state = self.0.borrow_mut(); @@ -659,14 +653,28 @@ impl X11Client { self.handle_xim_callback_event(event); } - if xim_filtered { - continue; - } - - if xim_connected { - self.xim_handle_event(event); - } else { - self.handle_event(event); + match xim_filtered { + Ok(handled) => { + if handled { + continue; + } + if xim_connected { + self.xim_handle_event(event); + } else { + self.handle_event(event); + } + } + Err(err) => { + // this might happen when xim server crashes on one of the events + // we do lose 1-2 keys when crash happens since there is no reliable way to get that info + // luckily, x11 sends us window not found error when xim server crashes upon further key press + // hence we fall back to handle_event + log::error!("XIMClientError: {}", err); + let mut state = self.0.borrow_mut(); + state.take_xim(); + drop(state); + self.handle_event(event); + } } } } From 8d6982e78f2493bb3ef2a23010f38dab141dc76a Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 15 Aug 2025 09:56:47 +0200 Subject: [PATCH 361/693] search: Fix some inconsistencies between project and buffer search bars (#36103) - project search query string now turns red when no results are found matching buffer search behavior - General code deduplication as well as more consistent layout between the two bars, as some minor details have drifted apart - Tab cycling in buffer search now ends up in editor focus when cycling backwards, matching forward cycling - Report parse errors in filter include and exclude editors Release Notes: - N/A --- crates/search/src/buffer_search.rs | 617 ++++++++++++---------------- crates/search/src/project_search.rs | 526 ++++++++++-------------- crates/search/src/search_bar.rs | 83 +++- crates/workspace/src/workspace.rs | 8 +- 4 files changed, 545 insertions(+), 689 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 14703be7a2..ccef198f04 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -3,20 +3,23 @@ mod registrar; use crate::{ FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleRegex, - ToggleReplace, ToggleSelection, ToggleWholeWord, search_bar::render_nav_button, + ToggleReplace, ToggleSelection, ToggleWholeWord, + search_bar::{ + input_base_styles, render_action_button, render_text_input, toggle_replace_button, + }, }; use any_vec::AnyVec; use anyhow::Context as _; use collections::HashMap; use editor::{ - DisplayPoint, Editor, EditorElement, EditorSettings, EditorStyle, + DisplayPoint, Editor, EditorSettings, actions::{Backtab, Tab}, }; use futures::channel::oneshot; use gpui::{ - Action, App, ClickEvent, Context, Entity, EventEmitter, FocusHandle, Focusable, - InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, - Styled, Subscription, Task, TextStyle, Window, actions, div, + Action, App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _, + IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task, + Window, actions, div, }; use language::{Language, LanguageRegistry}; use project::{ @@ -27,7 +30,6 @@ use schemars::JsonSchema; use serde::Deserialize; use settings::Settings; use std::sync::Arc; -use theme::ThemeSettings; use zed_actions::outline::ToggleOutline; use ui::{ @@ -125,46 +127,6 @@ pub struct BufferSearchBar { } impl BufferSearchBar { - fn render_text_input( - &self, - editor: &Entity, - color_override: Option, - cx: &mut Context, - ) -> impl IntoElement { - let (color, use_syntax) = if editor.read(cx).read_only(cx) { - (cx.theme().colors().text_disabled, false) - } else { - match color_override { - Some(color_override) => (color_override.color(cx), false), - None => (cx.theme().colors().text, true), - } - }; - - let settings = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: rems(0.875).into(), - font_weight: settings.buffer_font.weight, - line_height: relative(1.3), - ..TextStyle::default() - }; - - let mut editor_style = EditorStyle { - background: cx.theme().colors().toolbar_background, - local_player: cx.theme().players().local(), - text: text_style, - ..EditorStyle::default() - }; - if use_syntax { - editor_style.syntax = cx.theme().syntax().clone(); - } - - EditorElement::new(editor, editor_style) - } - pub fn query_editor_focused(&self) -> bool { self.query_editor_focused } @@ -185,7 +147,14 @@ impl Render for BufferSearchBar { let hide_inline_icons = self.editor_needed_width > self.editor_scroll_handle.bounds().size.width - window.rem_size() * 6.; - let supported_options = self.supported_options(cx); + let workspace::searchable::SearchOptions { + case, + word, + regex, + replacement, + selection, + find_in_results, + } = self.supported_options(cx); if self.query_editor.update(cx, |query_editor, _cx| { query_editor.placeholder_text().is_none() @@ -220,268 +189,205 @@ impl Render for BufferSearchBar { } }) .unwrap_or_else(|| "0/0".to_string()); - let should_show_replace_input = self.replace_enabled && supported_options.replacement; + let should_show_replace_input = self.replace_enabled && replacement; let in_replace = self.replacement_editor.focus_handle(cx).is_focused(window); + let theme_colors = cx.theme().colors(); + let query_border = if self.query_error.is_some() { + Color::Error.color(cx) + } else { + theme_colors.border + }; + let replacement_border = theme_colors.border; + + let container_width = window.viewport_size().width; + let input_width = SearchInputWidth::calc_width(container_width); + + let input_base_styles = + |border_color| input_base_styles(border_color, |div| div.w(input_width)); + + let query_column = input_base_styles(query_border) + .id("editor-scroll") + .track_scroll(&self.editor_scroll_handle) + .child(render_text_input(&self.query_editor, color_override, cx)) + .when(!hide_inline_icons, |div| { + div.child( + h_flex() + .gap_1() + .when(case, |div| { + div.child(SearchOptions::CASE_SENSITIVE.as_button( + self.search_options.contains(SearchOptions::CASE_SENSITIVE), + focus_handle.clone(), + cx.listener(|this, _, window, cx| { + this.toggle_case_sensitive(&ToggleCaseSensitive, window, cx) + }), + )) + }) + .when(word, |div| { + div.child(SearchOptions::WHOLE_WORD.as_button( + self.search_options.contains(SearchOptions::WHOLE_WORD), + focus_handle.clone(), + cx.listener(|this, _, window, cx| { + this.toggle_whole_word(&ToggleWholeWord, window, cx) + }), + )) + }) + .when(regex, |div| { + div.child(SearchOptions::REGEX.as_button( + self.search_options.contains(SearchOptions::REGEX), + focus_handle.clone(), + cx.listener(|this, _, window, cx| { + this.toggle_regex(&ToggleRegex, window, cx) + }), + )) + }), + ) + }); + + let mode_column = h_flex() + .gap_1() + .min_w_64() + .when(replacement, |this| { + this.child(toggle_replace_button( + "buffer-search-bar-toggle-replace-button", + focus_handle.clone(), + self.replace_enabled, + cx.listener(|this, _: &ClickEvent, window, cx| { + this.toggle_replace(&ToggleReplace, window, cx); + }), + )) + }) + .when(selection, |this| { + this.child( + IconButton::new( + "buffer-search-bar-toggle-search-selection-button", + IconName::Quote, + ) + .style(ButtonStyle::Subtle) + .shape(IconButtonShape::Square) + .when(self.selection_search_enabled, |button| { + button.style(ButtonStyle::Filled) + }) + .on_click(cx.listener(|this, _: &ClickEvent, window, cx| { + this.toggle_selection(&ToggleSelection, window, cx); + })) + .toggle_state(self.selection_search_enabled) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Toggle Search Selection", + &ToggleSelection, + &focus_handle, + window, + cx, + ) + } + }), + ) + }) + .when(!find_in_results, |el| { + let query_focus = self.query_editor.focus_handle(cx); + let matches_column = h_flex() + .pl_2() + .ml_2() + .border_l_1() + .border_color(theme_colors.border_variant) + .child(render_action_button( + "buffer-search-nav-button", + ui::IconName::ChevronLeft, + self.active_match_index.is_some(), + "Select Previous Match", + &SelectPreviousMatch, + query_focus.clone(), + )) + .child(render_action_button( + "buffer-search-nav-button", + ui::IconName::ChevronRight, + self.active_match_index.is_some(), + "Select Next Match", + &SelectNextMatch, + query_focus.clone(), + )) + .when(!narrow_mode, |this| { + this.child(div().ml_2().min_w(rems_from_px(40.)).child( + Label::new(match_text).size(LabelSize::Small).color( + if self.active_match_index.is_some() { + Color::Default + } else { + Color::Disabled + }, + ), + )) + }); + + el.child(render_action_button( + "buffer-search-nav-button", + IconName::SelectAll, + true, + "Select All Matches", + &SelectAllMatches, + query_focus, + )) + .child(matches_column) + }) + .when(find_in_results, |el| { + el.child(render_action_button( + "buffer-search", + IconName::Close, + true, + "Close Search Bar", + &Dismiss, + focus_handle.clone(), + )) + }); + + let search_line = h_flex() + .w_full() + .gap_2() + .when(find_in_results, |el| { + el.child(Label::new("Find in results").color(Color::Hint)) + }) + .child(query_column) + .child(mode_column); + + let replace_line = + should_show_replace_input.then(|| { + let replace_column = input_base_styles(replacement_border) + .child(render_text_input(&self.replacement_editor, None, cx)); + let focus_handle = self.replacement_editor.read(cx).focus_handle(cx); + + let replace_actions = h_flex() + .min_w_64() + .gap_1() + .child(render_action_button( + "buffer-search-replace-button", + IconName::ReplaceNext, + true, + "Replace Next Match", + &ReplaceNext, + focus_handle.clone(), + )) + .child(render_action_button( + "buffer-search-replace-button", + IconName::ReplaceAll, + true, + "Replace All Matches", + &ReplaceAll, + focus_handle, + )); + h_flex() + .w_full() + .gap_2() + .child(replace_column) + .child(replace_actions) + }); + let mut key_context = KeyContext::new_with_defaults(); key_context.add("BufferSearchBar"); if in_replace { key_context.add("in_replace"); } - let query_border = if self.query_error.is_some() { - Color::Error.color(cx) - } else { - cx.theme().colors().border - }; - let replacement_border = cx.theme().colors().border; - - let container_width = window.viewport_size().width; - let input_width = SearchInputWidth::calc_width(container_width); - - let input_base_styles = |border_color| { - h_flex() - .min_w_32() - .w(input_width) - .h_8() - .pl_2() - .pr_1() - .py_1() - .border_1() - .border_color(border_color) - .rounded_lg() - }; - - let search_line = h_flex() - .gap_2() - .when(supported_options.find_in_results, |el| { - el.child(Label::new("Find in results").color(Color::Hint)) - }) - .child( - input_base_styles(query_border) - .id("editor-scroll") - .track_scroll(&self.editor_scroll_handle) - .child(self.render_text_input(&self.query_editor, color_override, cx)) - .when(!hide_inline_icons, |div| { - div.child( - h_flex() - .gap_1() - .children(supported_options.case.then(|| { - self.render_search_option_button( - SearchOptions::CASE_SENSITIVE, - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_case_sensitive( - &ToggleCaseSensitive, - window, - cx, - ) - }), - ) - })) - .children(supported_options.word.then(|| { - self.render_search_option_button( - SearchOptions::WHOLE_WORD, - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_whole_word(&ToggleWholeWord, window, cx) - }), - ) - })) - .children(supported_options.regex.then(|| { - self.render_search_option_button( - SearchOptions::REGEX, - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_regex(&ToggleRegex, window, cx) - }), - ) - })), - ) - }), - ) - .child( - h_flex() - .gap_1() - .min_w_64() - .when(supported_options.replacement, |this| { - this.child( - IconButton::new( - "buffer-search-bar-toggle-replace-button", - IconName::Replace, - ) - .style(ButtonStyle::Subtle) - .shape(IconButtonShape::Square) - .when(self.replace_enabled, |button| { - button.style(ButtonStyle::Filled) - }) - .on_click(cx.listener(|this, _: &ClickEvent, window, cx| { - this.toggle_replace(&ToggleReplace, window, cx); - })) - .toggle_state(self.replace_enabled) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Toggle Replace", - &ToggleReplace, - &focus_handle, - window, - cx, - ) - } - }), - ) - }) - .when(supported_options.selection, |this| { - this.child( - IconButton::new( - "buffer-search-bar-toggle-search-selection-button", - IconName::Quote, - ) - .style(ButtonStyle::Subtle) - .shape(IconButtonShape::Square) - .when(self.selection_search_enabled, |button| { - button.style(ButtonStyle::Filled) - }) - .on_click(cx.listener(|this, _: &ClickEvent, window, cx| { - this.toggle_selection(&ToggleSelection, window, cx); - })) - .toggle_state(self.selection_search_enabled) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Toggle Search Selection", - &ToggleSelection, - &focus_handle, - window, - cx, - ) - } - }), - ) - }) - .when(!supported_options.find_in_results, |el| { - el.child( - IconButton::new("select-all", ui::IconName::SelectAll) - .on_click(|_, window, cx| { - window.dispatch_action(SelectAllMatches.boxed_clone(), cx) - }) - .shape(IconButtonShape::Square) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Select All Matches", - &SelectAllMatches, - &focus_handle, - window, - cx, - ) - } - }), - ) - .child( - h_flex() - .pl_2() - .ml_1() - .border_l_1() - .border_color(cx.theme().colors().border_variant) - .child(render_nav_button( - ui::IconName::ChevronLeft, - self.active_match_index.is_some(), - "Select Previous Match", - &SelectPreviousMatch, - focus_handle.clone(), - )) - .child(render_nav_button( - ui::IconName::ChevronRight, - self.active_match_index.is_some(), - "Select Next Match", - &SelectNextMatch, - focus_handle.clone(), - )), - ) - .when(!narrow_mode, |this| { - this.child(h_flex().ml_2().min_w(rems_from_px(40.)).child( - Label::new(match_text).size(LabelSize::Small).color( - if self.active_match_index.is_some() { - Color::Default - } else { - Color::Disabled - }, - ), - )) - }) - }) - .when(supported_options.find_in_results, |el| { - el.child( - IconButton::new(SharedString::from("Close"), IconName::Close) - .shape(IconButtonShape::Square) - .tooltip(move |window, cx| { - Tooltip::for_action("Close Search Bar", &Dismiss, window, cx) - }) - .on_click(cx.listener(|this, _: &ClickEvent, window, cx| { - this.dismiss(&Dismiss, window, cx) - })), - ) - }), - ); - - let replace_line = should_show_replace_input.then(|| { - h_flex() - .gap_2() - .child( - input_base_styles(replacement_border).child(self.render_text_input( - &self.replacement_editor, - None, - cx, - )), - ) - .child( - h_flex() - .min_w_64() - .gap_1() - .child( - IconButton::new("search-replace-next", ui::IconName::ReplaceNext) - .shape(IconButtonShape::Square) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Replace Next Match", - &ReplaceNext, - &focus_handle, - window, - cx, - ) - } - }) - .on_click(cx.listener(|this, _, window, cx| { - this.replace_next(&ReplaceNext, window, cx) - })), - ) - .child( - IconButton::new("search-replace-all", ui::IconName::ReplaceAll) - .shape(IconButtonShape::Square) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Replace All Matches", - &ReplaceAll, - &focus_handle, - window, - cx, - ) - } - }) - .on_click(cx.listener(|this, _, window, cx| { - this.replace_all(&ReplaceAll, window, cx) - })), - ), - ) - }); let query_error_line = self.query_error.as_ref().map(|error| { Label::new(error) @@ -491,10 +397,26 @@ impl Render for BufferSearchBar { .ml_2() }); + let search_line = + h_flex() + .relative() + .child(search_line) + .when(!narrow_mode && !find_in_results, |div| { + div.child(h_flex().absolute().right_0().child(render_action_button( + "buffer-search", + IconName::Close, + true, + "Close Search Bar", + &Dismiss, + focus_handle.clone(), + ))) + .w_full() + }); v_flex() .id("buffer_search") .gap_2() .py(px(1.0)) + .w_full() .track_scroll(&self.scroll_handle) .key_context(key_context) .capture_action(cx.listener(Self::tab)) @@ -509,43 +431,26 @@ impl Render for BufferSearchBar { active_searchable_item.relay_action(Box::new(ToggleOutline), window, cx); } })) - .when(self.supported_options(cx).replacement, |this| { + .when(replacement, |this| { this.on_action(cx.listener(Self::toggle_replace)) .when(in_replace, |this| { this.on_action(cx.listener(Self::replace_next)) .on_action(cx.listener(Self::replace_all)) }) }) - .when(self.supported_options(cx).case, |this| { + .when(case, |this| { this.on_action(cx.listener(Self::toggle_case_sensitive)) }) - .when(self.supported_options(cx).word, |this| { + .when(word, |this| { this.on_action(cx.listener(Self::toggle_whole_word)) }) - .when(self.supported_options(cx).regex, |this| { + .when(regex, |this| { this.on_action(cx.listener(Self::toggle_regex)) }) - .when(self.supported_options(cx).selection, |this| { + .when(selection, |this| { this.on_action(cx.listener(Self::toggle_selection)) }) - .child(h_flex().relative().child(search_line.w_full()).when( - !narrow_mode && !supported_options.find_in_results, - |div| { - div.child( - h_flex().absolute().right_0().child( - IconButton::new(SharedString::from("Close"), IconName::Close) - .shape(IconButtonShape::Square) - .tooltip(move |window, cx| { - Tooltip::for_action("Close Search Bar", &Dismiss, window, cx) - }) - .on_click(cx.listener(|this, _: &ClickEvent, window, cx| { - this.dismiss(&Dismiss, window, cx) - })), - ), - ) - .w_full() - }, - )) + .child(search_line) .children(query_error_line) .children(replace_line) } @@ -792,7 +697,7 @@ impl BufferSearchBar { active_editor.search_bar_visibility_changed(false, window, cx); active_editor.toggle_filtered_search_ranges(false, window, cx); let handle = active_editor.item_focus_handle(cx); - self.focus(&handle, window, cx); + self.focus(&handle, window); } cx.emit(Event::UpdateLocation); cx.emit(ToolbarItemEvent::ChangeLocation( @@ -948,7 +853,7 @@ impl BufferSearchBar { } pub fn focus_replace(&mut self, window: &mut Window, cx: &mut Context) { - self.focus(&self.replacement_editor.focus_handle(cx), window, cx); + self.focus(&self.replacement_editor.focus_handle(cx), window); cx.notify(); } @@ -975,16 +880,6 @@ impl BufferSearchBar { self.update_matches(!updated, window, cx) } - fn render_search_option_button( - &self, - option: SearchOptions, - focus_handle: FocusHandle, - action: Action, - ) -> impl IntoElement + use { - let is_active = self.search_options.contains(option); - option.as_button(is_active, focus_handle, action) - } - pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context) { if let Some(active_editor) = self.active_searchable_item.as_ref() { let handle = active_editor.item_focus_handle(cx); @@ -1400,28 +1295,32 @@ impl BufferSearchBar { } fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { - // Search -> Replace -> Editor - let focus_handle = if self.replace_enabled && self.query_editor_focused { - self.replacement_editor.focus_handle(cx) - } else if let Some(item) = self.active_searchable_item.as_ref() { - item.item_focus_handle(cx) - } else { - return; - }; - self.focus(&focus_handle, window, cx); - cx.stop_propagation(); + self.cycle_field(Direction::Next, window, cx); } fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context) { - // Search -> Replace -> Search - let focus_handle = if self.replace_enabled && self.query_editor_focused { - self.replacement_editor.focus_handle(cx) - } else if self.replacement_editor_focused { - self.query_editor.focus_handle(cx) - } else { - return; + self.cycle_field(Direction::Prev, window, cx); + } + fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context) { + let mut handles = vec![self.query_editor.focus_handle(cx)]; + if self.replace_enabled { + handles.push(self.replacement_editor.focus_handle(cx)); + } + if let Some(item) = self.active_searchable_item.as_ref() { + handles.push(item.item_focus_handle(cx)); + } + let current_index = match handles.iter().position(|focus| focus.is_focused(window)) { + Some(index) => index, + None => return, }; - self.focus(&focus_handle, window, cx); + + let new_index = match direction { + Direction::Next => (current_index + 1) % handles.len(), + Direction::Prev if current_index == 0 => handles.len() - 1, + Direction::Prev => (current_index - 1) % handles.len(), + }; + let next_focus_handle = &handles[new_index]; + self.focus(next_focus_handle, window); cx.stop_propagation(); } @@ -1469,10 +1368,8 @@ impl BufferSearchBar { } } - fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window, cx: &mut Context) { - cx.on_next_frame(window, |_, window, _| { - window.invalidate_character_coordinates(); - }); + fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window) { + window.invalidate_character_coordinates(); window.focus(handle); } @@ -1484,7 +1381,7 @@ impl BufferSearchBar { } else { self.query_editor.focus_handle(cx) }; - self.focus(&handle, window, cx); + self.focus(&handle, window); cx.notify(); } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 96194cdad2..9e8afa4392 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,20 +1,25 @@ use crate::{ BufferSearchBar, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleIncludeIgnored, - ToggleRegex, ToggleReplace, ToggleWholeWord, buffer_search::Deploy, + ToggleRegex, ToggleReplace, ToggleWholeWord, + buffer_search::Deploy, + search_bar::{ + input_base_styles, render_action_button, render_text_input, toggle_replace_button, + }, }; use anyhow::Context as _; -use collections::{HashMap, HashSet}; +use collections::HashMap; use editor::{ - Anchor, Editor, EditorElement, EditorEvent, EditorSettings, EditorStyle, MAX_TAB_TITLE_LEN, - MultiBuffer, SelectionEffects, actions::SelectAll, items::active_match_index, + Anchor, Editor, EditorEvent, EditorSettings, MAX_TAB_TITLE_LEN, MultiBuffer, SelectionEffects, + actions::{Backtab, SelectAll, Tab}, + items::active_match_index, }; use futures::{StreamExt, stream::FuturesOrdered}; use gpui::{ Action, AnyElement, AnyView, App, Axis, Context, Entity, EntityId, EventEmitter, FocusHandle, Focusable, Global, Hsla, InteractiveElement, IntoElement, KeyContext, ParentElement, Point, - Render, SharedString, Styled, Subscription, Task, TextStyle, UpdateGlobal, WeakEntity, Window, - actions, div, + Render, SharedString, Styled, Subscription, Task, UpdateGlobal, WeakEntity, Window, actions, + div, }; use language::{Buffer, Language}; use menu::Confirm; @@ -32,7 +37,6 @@ use std::{ pin::pin, sync::Arc, }; -use theme::ThemeSettings; use ui::{ Icon, IconButton, IconButtonShape, IconName, KeyBinding, Label, LabelCommon, LabelSize, Toggleable, Tooltip, h_flex, prelude::*, utils::SearchInputWidth, v_flex, @@ -208,7 +212,7 @@ pub struct ProjectSearchView { replacement_editor: Entity, results_editor: Entity, search_options: SearchOptions, - panels_with_errors: HashSet, + panels_with_errors: HashMap, active_match_index: Option, search_id: usize, included_files_editor: Entity, @@ -218,7 +222,6 @@ pub struct ProjectSearchView { included_opened_only: bool, regex_language: Option>, _subscriptions: Vec, - query_error: Option, } #[derive(Debug, Clone)] @@ -879,7 +882,7 @@ impl ProjectSearchView { query_editor, results_editor, search_options: options, - panels_with_errors: HashSet::default(), + panels_with_errors: HashMap::default(), active_match_index: None, included_files_editor, excluded_files_editor, @@ -888,7 +891,6 @@ impl ProjectSearchView { included_opened_only: false, regex_language: None, _subscriptions: subscriptions, - query_error: None, }; this.entity_changed(window, cx); this @@ -1152,14 +1154,16 @@ impl ProjectSearchView { Ok(included_files) => { let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Include); - if should_unmark_error { + if should_unmark_error.is_some() { cx.notify(); } included_files } - Err(_e) => { - let should_mark_error = self.panels_with_errors.insert(InputPanel::Include); - if should_mark_error { + Err(e) => { + let should_mark_error = self + .panels_with_errors + .insert(InputPanel::Include, e.to_string()); + if should_mark_error.is_none() { cx.notify(); } PathMatcher::default() @@ -1174,15 +1178,17 @@ impl ProjectSearchView { Ok(excluded_files) => { let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Exclude); - if should_unmark_error { + if should_unmark_error.is_some() { cx.notify(); } excluded_files } - Err(_e) => { - let should_mark_error = self.panels_with_errors.insert(InputPanel::Exclude); - if should_mark_error { + Err(e) => { + let should_mark_error = self + .panels_with_errors + .insert(InputPanel::Exclude, e.to_string()); + if should_mark_error.is_none() { cx.notify(); } PathMatcher::default() @@ -1219,19 +1225,19 @@ impl ProjectSearchView { ) { Ok(query) => { let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Query); - if should_unmark_error { + if should_unmark_error.is_some() { cx.notify(); } - self.query_error = None; Some(query) } Err(e) => { - let should_mark_error = self.panels_with_errors.insert(InputPanel::Query); - if should_mark_error { + let should_mark_error = self + .panels_with_errors + .insert(InputPanel::Query, e.to_string()); + if should_mark_error.is_none() { cx.notify(); } - self.query_error = Some(e.to_string()); None } @@ -1249,15 +1255,17 @@ impl ProjectSearchView { ) { Ok(query) => { let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Query); - if should_unmark_error { + if should_unmark_error.is_some() { cx.notify(); } Some(query) } - Err(_e) => { - let should_mark_error = self.panels_with_errors.insert(InputPanel::Query); - if should_mark_error { + Err(e) => { + let should_mark_error = self + .panels_with_errors + .insert(InputPanel::Query, e.to_string()); + if should_mark_error.is_none() { cx.notify(); } @@ -1512,7 +1520,7 @@ impl ProjectSearchView { } fn border_color_for(&self, panel: InputPanel, cx: &App) -> Hsla { - if self.panels_with_errors.contains(&panel) { + if self.panels_with_errors.contains_key(&panel) { Color::Error.color(cx) } else { cx.theme().colors().border @@ -1610,16 +1618,11 @@ impl ProjectSearchBar { } } - fn tab(&mut self, _: &editor::actions::Tab, window: &mut Window, cx: &mut Context) { + fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { self.cycle_field(Direction::Next, window, cx); } - fn backtab( - &mut self, - _: &editor::actions::Backtab, - window: &mut Window, - cx: &mut Context, - ) { + fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context) { self.cycle_field(Direction::Prev, window, cx); } @@ -1634,29 +1637,22 @@ impl ProjectSearchBar { fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context) { let active_project_search = match &self.active_project_search { Some(active_project_search) => active_project_search, - - None => { - return; - } + None => return, }; active_project_search.update(cx, |project_view, cx| { - let mut views = vec![&project_view.query_editor]; + let mut views = vec![project_view.query_editor.focus_handle(cx)]; if project_view.replace_enabled { - views.push(&project_view.replacement_editor); + views.push(project_view.replacement_editor.focus_handle(cx)); } if project_view.filters_enabled { views.extend([ - &project_view.included_files_editor, - &project_view.excluded_files_editor, + project_view.included_files_editor.focus_handle(cx), + project_view.excluded_files_editor.focus_handle(cx), ]); } - let current_index = match views - .iter() - .enumerate() - .find(|(_, editor)| editor.focus_handle(cx).is_focused(window)) - { - Some((index, _)) => index, + let current_index = match views.iter().position(|focus| focus.is_focused(window)) { + Some(index) => index, None => return, }; @@ -1665,8 +1661,8 @@ impl ProjectSearchBar { Direction::Prev if current_index == 0 => views.len() - 1, Direction::Prev => (current_index - 1) % views.len(), }; - let next_focus_handle = views[new_index].focus_handle(cx); - window.focus(&next_focus_handle); + let next_focus_handle = &views[new_index]; + window.focus(next_focus_handle); cx.stop_propagation(); }); } @@ -1915,37 +1911,6 @@ impl ProjectSearchBar { }) } } - - fn render_text_input(&self, editor: &Entity, cx: &Context) -> impl IntoElement { - let (color, use_syntax) = if editor.read(cx).read_only(cx) { - (cx.theme().colors().text_disabled, false) - } else { - (cx.theme().colors().text, true) - }; - let settings = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: rems(0.875).into(), - font_weight: settings.buffer_font.weight, - line_height: relative(1.3), - ..TextStyle::default() - }; - - let mut editor_style = EditorStyle { - background: cx.theme().colors().toolbar_background, - local_player: cx.theme().players().local(), - text: text_style, - ..EditorStyle::default() - }; - if use_syntax { - editor_style.syntax = cx.theme().syntax().clone(); - } - - EditorElement::new(editor, editor_style) - } } impl Render for ProjectSearchBar { @@ -1959,28 +1924,43 @@ impl Render for ProjectSearchBar { let container_width = window.viewport_size().width; let input_width = SearchInputWidth::calc_width(container_width); - enum BaseStyle { - SingleInput, - MultipleInputs, - } - - let input_base_styles = |base_style: BaseStyle, panel: InputPanel| { - h_flex() - .min_w_32() - .map(|div| match base_style { - BaseStyle::SingleInput => div.w(input_width), - BaseStyle::MultipleInputs => div.flex_grow(), - }) - .h_8() - .pl_2() - .pr_1() - .py_1() - .border_1() - .border_color(search.border_color_for(panel, cx)) - .rounded_lg() + let input_base_styles = |panel: InputPanel| { + input_base_styles(search.border_color_for(panel, cx), |div| match panel { + InputPanel::Query | InputPanel::Replacement => div.w(input_width), + InputPanel::Include | InputPanel::Exclude => div.flex_grow(), + }) }; + let theme_colors = cx.theme().colors(); + let project_search = search.entity.read(cx); + let limit_reached = project_search.limit_reached; - let query_column = input_base_styles(BaseStyle::SingleInput, InputPanel::Query) + let color_override = match ( + project_search.no_results, + &project_search.active_query, + &project_search.last_search_query_text, + ) { + (Some(true), Some(q), Some(p)) if q.as_str() == p => Some(Color::Error), + _ => None, + }; + let match_text = search + .active_match_index + .and_then(|index| { + let index = index + 1; + let match_quantity = project_search.match_ranges.len(); + if match_quantity > 0 { + debug_assert!(match_quantity >= index); + if limit_reached { + Some(format!("{index}/{match_quantity}+")) + } else { + Some(format!("{index}/{match_quantity}")) + } + } else { + None + } + }) + .unwrap_or_else(|| "0/0".to_string()); + + let query_column = input_base_styles(InputPanel::Query) .on_action(cx.listener(|this, action, window, cx| this.confirm(action, window, cx))) .on_action(cx.listener(|this, action, window, cx| { this.previous_history_query(action, window, cx) @@ -1988,7 +1968,7 @@ impl Render for ProjectSearchBar { .on_action( cx.listener(|this, action, window, cx| this.next_history_query(action, window, cx)), ) - .child(self.render_text_input(&search.query_editor, cx)) + .child(render_text_input(&search.query_editor, color_override, cx)) .child( h_flex() .gap_1() @@ -2017,6 +1997,7 @@ impl Render for ProjectSearchBar { let mode_column = h_flex() .gap_1() + .min_w_64() .child( IconButton::new("project-search-filter-button", IconName::Filter) .shape(IconButtonShape::Square) @@ -2045,109 +2026,46 @@ impl Render for ProjectSearchBar { } }), ) - .child( - IconButton::new("project-search-toggle-replace", IconName::Replace) - .shape(IconButtonShape::Square) - .on_click(cx.listener(|this, _, window, cx| { - this.toggle_replace(&ToggleReplace, window, cx); - })) - .toggle_state( - self.active_project_search - .as_ref() - .map(|search| search.read(cx).replace_enabled) - .unwrap_or_default(), - ) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Toggle Replace", - &ToggleReplace, - &focus_handle, - window, - cx, - ) - } - }), - ); + .child(toggle_replace_button( + "project-search-toggle-replace", + focus_handle.clone(), + self.active_project_search + .as_ref() + .map(|search| search.read(cx).replace_enabled) + .unwrap_or_default(), + cx.listener(|this, _, window, cx| { + this.toggle_replace(&ToggleReplace, window, cx); + }), + )); - let limit_reached = search.entity.read(cx).limit_reached; - - let match_text = search - .active_match_index - .and_then(|index| { - let index = index + 1; - let match_quantity = search.entity.read(cx).match_ranges.len(); - if match_quantity > 0 { - debug_assert!(match_quantity >= index); - if limit_reached { - Some(format!("{index}/{match_quantity}+")) - } else { - Some(format!("{index}/{match_quantity}")) - } - } else { - None - } - }) - .unwrap_or_else(|| "0/0".to_string()); + let query_focus = search.query_editor.focus_handle(cx); let matches_column = h_flex() .pl_2() .ml_2() .border_l_1() - .border_color(cx.theme().colors().border_variant) - .child( - IconButton::new("project-search-prev-match", IconName::ChevronLeft) - .shape(IconButtonShape::Square) - .disabled(search.active_match_index.is_none()) - .on_click(cx.listener(|this, _, window, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.select_match(Direction::Prev, window, cx); - }) - } - })) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Go To Previous Match", - &SelectPreviousMatch, - &focus_handle, - window, - cx, - ) - } - }), - ) - .child( - IconButton::new("project-search-next-match", IconName::ChevronRight) - .shape(IconButtonShape::Square) - .disabled(search.active_match_index.is_none()) - .on_click(cx.listener(|this, _, window, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.select_match(Direction::Next, window, cx); - }) - } - })) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Go To Next Match", - &SelectNextMatch, - &focus_handle, - window, - cx, - ) - } - }), - ) + .border_color(theme_colors.border_variant) + .child(render_action_button( + "project-search-nav-button", + IconName::ChevronLeft, + search.active_match_index.is_some(), + "Select Previous Match", + &SelectPreviousMatch, + query_focus.clone(), + )) + .child(render_action_button( + "project-search-nav-button", + IconName::ChevronRight, + search.active_match_index.is_some(), + "Select Next Match", + &SelectNextMatch, + query_focus, + )) .child( div() .id("matches") - .ml_1() + .ml_2() + .min_w(rems_from_px(40.)) .child(Label::new(match_text).size(LabelSize::Small).color( if search.active_match_index.is_some() { Color::Default @@ -2169,63 +2087,30 @@ impl Render for ProjectSearchBar { .child(h_flex().min_w_64().child(mode_column).child(matches_column)); let replace_line = search.replace_enabled.then(|| { - let replace_column = input_base_styles(BaseStyle::SingleInput, InputPanel::Replacement) - .child(self.render_text_input(&search.replacement_editor, cx)); + let replace_column = input_base_styles(InputPanel::Replacement) + .child(render_text_input(&search.replacement_editor, None, cx)); let focus_handle = search.replacement_editor.read(cx).focus_handle(cx); - let replace_actions = - h_flex() - .min_w_64() - .gap_1() - .when(search.replace_enabled, |this| { - this.child( - IconButton::new("project-search-replace-next", IconName::ReplaceNext) - .shape(IconButtonShape::Square) - .on_click(cx.listener(|this, _, window, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.replace_next(&ReplaceNext, window, cx); - }) - } - })) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Replace Next Match", - &ReplaceNext, - &focus_handle, - window, - cx, - ) - } - }), - ) - .child( - IconButton::new("project-search-replace-all", IconName::ReplaceAll) - .shape(IconButtonShape::Square) - .on_click(cx.listener(|this, _, window, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.replace_all(&ReplaceAll, window, cx); - }) - } - })) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Replace All Matches", - &ReplaceAll, - &focus_handle, - window, - cx, - ) - } - }), - ) - }); + let replace_actions = h_flex() + .min_w_64() + .gap_1() + .child(render_action_button( + "project-search-replace-button", + IconName::ReplaceNext, + true, + "Replace Next Match", + &ReplaceNext, + focus_handle.clone(), + )) + .child(render_action_button( + "project-search-replace-button", + IconName::ReplaceAll, + true, + "Replace All Matches", + &ReplaceAll, + focus_handle, + )); h_flex() .w_full() @@ -2235,6 +2120,45 @@ impl Render for ProjectSearchBar { }); let filter_line = search.filters_enabled.then(|| { + let include = input_base_styles(InputPanel::Include) + .on_action(cx.listener(|this, action, window, cx| { + this.previous_history_query(action, window, cx) + })) + .on_action(cx.listener(|this, action, window, cx| { + this.next_history_query(action, window, cx) + })) + .child(render_text_input(&search.included_files_editor, None, cx)); + let exclude = input_base_styles(InputPanel::Exclude) + .on_action(cx.listener(|this, action, window, cx| { + this.previous_history_query(action, window, cx) + })) + .on_action(cx.listener(|this, action, window, cx| { + this.next_history_query(action, window, cx) + })) + .child(render_text_input(&search.excluded_files_editor, None, cx)); + let mode_column = h_flex() + .gap_1() + .min_w_64() + .child( + IconButton::new("project-search-opened-only", IconName::FolderSearch) + .shape(IconButtonShape::Square) + .toggle_state(self.is_opened_only_enabled(cx)) + .tooltip(Tooltip::text("Only Search Open Files")) + .on_click(cx.listener(|this, _, window, cx| { + this.toggle_opened_only(window, cx); + })), + ) + .child( + SearchOptions::INCLUDE_IGNORED.as_button( + search + .search_options + .contains(SearchOptions::INCLUDE_IGNORED), + focus_handle.clone(), + cx.listener(|this, _, window, cx| { + this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, window, cx); + }), + ), + ); h_flex() .w_full() .gap_2() @@ -2242,62 +2166,14 @@ impl Render for ProjectSearchBar { h_flex() .gap_2() .w(input_width) - .child( - input_base_styles(BaseStyle::MultipleInputs, InputPanel::Include) - .on_action(cx.listener(|this, action, window, cx| { - this.previous_history_query(action, window, cx) - })) - .on_action(cx.listener(|this, action, window, cx| { - this.next_history_query(action, window, cx) - })) - .child(self.render_text_input(&search.included_files_editor, cx)), - ) - .child( - input_base_styles(BaseStyle::MultipleInputs, InputPanel::Exclude) - .on_action(cx.listener(|this, action, window, cx| { - this.previous_history_query(action, window, cx) - })) - .on_action(cx.listener(|this, action, window, cx| { - this.next_history_query(action, window, cx) - })) - .child(self.render_text_input(&search.excluded_files_editor, cx)), - ), - ) - .child( - h_flex() - .min_w_64() - .gap_1() - .child( - IconButton::new("project-search-opened-only", IconName::FolderSearch) - .shape(IconButtonShape::Square) - .toggle_state(self.is_opened_only_enabled(cx)) - .tooltip(Tooltip::text("Only Search Open Files")) - .on_click(cx.listener(|this, _, window, cx| { - this.toggle_opened_only(window, cx); - })), - ) - .child( - SearchOptions::INCLUDE_IGNORED.as_button( - search - .search_options - .contains(SearchOptions::INCLUDE_IGNORED), - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_search_option( - SearchOptions::INCLUDE_IGNORED, - window, - cx, - ); - }), - ), - ), + .child(include) + .child(exclude), ) + .child(mode_column) }); let mut key_context = KeyContext::default(); - key_context.add("ProjectSearchBar"); - if search .replacement_editor .focus_handle(cx) @@ -2306,16 +2182,33 @@ impl Render for ProjectSearchBar { key_context.add("in_replace"); } - let query_error_line = search.query_error.as_ref().map(|error| { - Label::new(error) - .size(LabelSize::Small) - .color(Color::Error) - .mt_neg_1() - .ml_2() - }); + let query_error_line = search + .panels_with_errors + .get(&InputPanel::Query) + .map(|error| { + Label::new(error) + .size(LabelSize::Small) + .color(Color::Error) + .mt_neg_1() + .ml_2() + }); + + let filter_error_line = search + .panels_with_errors + .get(&InputPanel::Include) + .or_else(|| search.panels_with_errors.get(&InputPanel::Exclude)) + .map(|error| { + Label::new(error) + .size(LabelSize::Small) + .color(Color::Error) + .mt_neg_1() + .ml_2() + }); v_flex() + .gap_2() .py(px(1.0)) + .w_full() .key_context(key_context) .on_action(cx.listener(|this, _: &ToggleFocus, window, cx| { this.move_focus_to_results(window, cx) @@ -2323,14 +2216,8 @@ impl Render for ProjectSearchBar { .on_action(cx.listener(|this, _: &ToggleFilters, window, cx| { this.toggle_filters(window, cx); })) - .capture_action(cx.listener(|this, action, window, cx| { - this.tab(action, window, cx); - cx.stop_propagation(); - })) - .capture_action(cx.listener(|this, action, window, cx| { - this.backtab(action, window, cx); - cx.stop_propagation(); - })) + .capture_action(cx.listener(Self::tab)) + .capture_action(cx.listener(Self::backtab)) .on_action(cx.listener(|this, action, window, cx| this.confirm(action, window, cx))) .on_action(cx.listener(|this, action, window, cx| { this.toggle_replace(action, window, cx); @@ -2362,12 +2249,11 @@ impl Render for ProjectSearchBar { }) .on_action(cx.listener(Self::select_next_match)) .on_action(cx.listener(Self::select_prev_match)) - .gap_2() - .w_full() .child(search_line) .children(query_error_line) .children(replace_line) .children(filter_line) + .children(filter_error_line) } } diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 805664c794..2805b0c62d 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -1,8 +1,14 @@ -use gpui::{Action, FocusHandle, IntoElement}; +use editor::{Editor, EditorElement, EditorStyle}; +use gpui::{Action, Entity, FocusHandle, Hsla, IntoElement, TextStyle}; +use settings::Settings; +use theme::ThemeSettings; use ui::{IconButton, IconButtonShape}; use ui::{Tooltip, prelude::*}; -pub(super) fn render_nav_button( +use crate::ToggleReplace; + +pub(super) fn render_action_button( + id_prefix: &'static str, icon: ui::IconName, active: bool, tooltip: &'static str, @@ -10,7 +16,7 @@ pub(super) fn render_nav_button( focus_handle: FocusHandle, ) -> impl IntoElement { IconButton::new( - SharedString::from(format!("search-nav-button-{}", action.name())), + SharedString::from(format!("{id_prefix}-{}", action.name())), icon, ) .shape(IconButtonShape::Square) @@ -26,3 +32,74 @@ pub(super) fn render_nav_button( .tooltip(move |window, cx| Tooltip::for_action_in(tooltip, action, &focus_handle, window, cx)) .disabled(!active) } + +pub(crate) fn input_base_styles(border_color: Hsla, map: impl FnOnce(Div) -> Div) -> Div { + h_flex() + .min_w_32() + .map(map) + .h_8() + .pl_2() + .pr_1() + .py_1() + .border_1() + .border_color(border_color) + .rounded_lg() +} + +pub(crate) fn toggle_replace_button( + id: &'static str, + focus_handle: FocusHandle, + replace_enabled: bool, + on_click: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static, +) -> IconButton { + IconButton::new(id, IconName::Replace) + .shape(IconButtonShape::Square) + .style(ButtonStyle::Subtle) + .when(replace_enabled, |button| button.style(ButtonStyle::Filled)) + .on_click(on_click) + .toggle_state(replace_enabled) + .tooltip({ + move |window, cx| { + Tooltip::for_action_in("Toggle Replace", &ToggleReplace, &focus_handle, window, cx) + } + }) +} + +pub(crate) fn render_text_input( + editor: &Entity, + color_override: Option, + app: &App, +) -> impl IntoElement { + let (color, use_syntax) = if editor.read(app).read_only(app) { + (app.theme().colors().text_disabled, false) + } else { + match color_override { + Some(color_override) => (color_override.color(app), false), + None => (app.theme().colors().text, true), + } + }; + + let settings = ThemeSettings::get_global(app); + let text_style = TextStyle { + color, + font_family: settings.buffer_font.family.clone(), + font_features: settings.buffer_font.features.clone(), + font_fallbacks: settings.buffer_font.fallbacks.clone(), + font_size: rems(0.875).into(), + font_weight: settings.buffer_font.weight, + line_height: relative(1.3), + ..TextStyle::default() + }; + + let mut editor_style = EditorStyle { + background: app.theme().colors().toolbar_background, + local_player: app.theme().players().local(), + text: text_style, + ..EditorStyle::default() + }; + if use_syntax { + editor_style.syntax = app.theme().syntax().clone(); + } + + EditorElement::new(editor, editor_style) +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ba9e3bbb8a..ca98404194 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3878,9 +3878,7 @@ impl Workspace { local, focus_changed, } => { - cx.on_next_frame(window, |_, window, _| { - window.invalidate_character_coordinates(); - }); + window.invalidate_character_coordinates(); pane.update(cx, |pane, _| { pane.track_alternate_file_items(); @@ -3921,9 +3919,7 @@ impl Workspace { } } pane::Event::Focus => { - cx.on_next_frame(window, |_, window, _| { - window.invalidate_character_coordinates(); - }); + window.invalidate_character_coordinates(); self.handle_pane_focused(pane.clone(), window, cx); } pane::Event::ZoomIn => { From a3dcc7668756f4ab6aae6d3d5b2ba9a309303723 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Fri, 15 Aug 2025 12:12:18 +0300 Subject: [PATCH 362/693] openai: Don't send reasoning_effort if it's not set (#36228) Release Notes: - N/A --- crates/open_ai/src/open_ai.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 5801f29623..8bbe858995 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -257,6 +257,7 @@ pub struct Request { pub tools: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub prompt_cache_key: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, } From 4f0b00b0d9cd25798a3e20a789cf93835251d8c3 Mon Sep 17 00:00:00 2001 From: David Kleingeld Date: Fri, 15 Aug 2025 12:10:52 +0200 Subject: [PATCH 363/693] Add component NotificationFrame & CaptureAudio parts for testing (#36081) Adds component NotificationFrame. It implements a subset of MessageNotification as a Component and refactors MessageNotification to use NotificationFrame. Having some notification UI Component is nice as it allows us to easily build new types of notifications. Uses the new NotificationFrame component for CaptureAudioNotification. Adds a CaptureAudio action in the dev namespace (not meant for end-users). It records 10 seconds of audio and saves that to a wav file. Release Notes: - N/A --------- Co-authored-by: Mikayla --- .config/hakari.toml | 2 + Cargo.lock | 9 + Cargo.toml | 2 +- crates/audio/Cargo.toml | 2 +- crates/livekit_client/Cargo.toml | 2 + crates/livekit_client/src/lib.rs | 64 +++++ crates/livekit_client/src/livekit_client.rs | 2 + .../src/livekit_client/playback.rs | 57 +---- crates/livekit_client/src/record.rs | 91 ++++++++ crates/workspace/src/notifications.rs | 218 ++++++++++++------ crates/workspace/src/workspace.rs | 3 +- crates/zed/Cargo.toml | 1 + crates/zed/src/zed.rs | 122 +++++++++- 13 files changed, 448 insertions(+), 127 deletions(-) create mode 100644 crates/livekit_client/src/record.rs diff --git a/.config/hakari.toml b/.config/hakari.toml index 2050065cc2..f71e97b45c 100644 --- a/.config/hakari.toml +++ b/.config/hakari.toml @@ -25,6 +25,8 @@ third-party = [ { name = "reqwest", version = "0.11.27" }, # build of remote_server should not include scap / its x11 dependency { name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" }, + # build of remote_server should not need to include on libalsa through rodio + { name = "rodio" }, ] [final-excludes] diff --git a/Cargo.lock b/Cargo.lock index 0bafc3c386..2353733dc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7883,6 +7883,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + [[package]] name = "html5ever" version = "0.27.0" @@ -9711,6 +9717,7 @@ dependencies = [ "objc", "parking_lot", "postage", + "rodio", "scap", "serde", "serde_json", @@ -13972,6 +13979,7 @@ checksum = "e40ecf59e742e03336be6a3d53755e789fd05a059fa22dfa0ed624722319e183" dependencies = [ "cpal", "dasp_sample", + "hound", "num-rational", "symphonia", "tracing", @@ -20576,6 +20584,7 @@ dependencies = [ "language_tools", "languages", "libc", + "livekit_client", "log", "markdown", "markdown_preview", diff --git a/Cargo.toml b/Cargo.toml index a872cadd39..baa4ee7f4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -363,6 +363,7 @@ remote_server = { path = "crates/remote_server" } repl = { path = "crates/repl" } reqwest_client = { path = "crates/reqwest_client" } rich_text = { path = "crates/rich_text" } +rodio = { version = "0.21.1", default-features = false } rope = { path = "crates/rope" } rpc = { path = "crates/rpc" } rules_library = { path = "crates/rules_library" } @@ -564,7 +565,6 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77 "socks", "stream", ] } -rodio = { version = "0.21.1", default-features = false } rsa = "0.9.6" runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [ "async-dispatcher-runtime", diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index f1f40ad654..5146396b92 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -18,6 +18,6 @@ collections.workspace = true derive_more.workspace = true gpui.workspace = true parking_lot.workspace = true -rodio = { workspace = true, features = ["wav", "playback", "tracing"] } +rodio = { workspace = true, features = [ "wav", "playback", "tracing" ] } util.workspace = true workspace-hack.workspace = true diff --git a/crates/livekit_client/Cargo.toml b/crates/livekit_client/Cargo.toml index 821fd5d390..58059967b7 100644 --- a/crates/livekit_client/Cargo.toml +++ b/crates/livekit_client/Cargo.toml @@ -39,6 +39,8 @@ tokio-tungstenite.workspace = true util.workspace = true workspace-hack.workspace = true +rodio = { workspace = true, features = ["wav_output"] } + [target.'cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))'.dependencies] libwebrtc = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks" } livekit = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks", features = [ diff --git a/crates/livekit_client/src/lib.rs b/crates/livekit_client/src/lib.rs index 149859fdc8..e3934410e1 100644 --- a/crates/livekit_client/src/lib.rs +++ b/crates/livekit_client/src/lib.rs @@ -1,7 +1,13 @@ +use anyhow::Context as _; use collections::HashMap; mod remote_video_track_view; +use cpal::traits::HostTrait as _; pub use remote_video_track_view::{RemoteVideoTrackView, RemoteVideoTrackViewEvent}; +use rodio::DeviceTrait as _; + +mod record; +pub use record::CaptureInput; #[cfg(not(any( test, @@ -18,6 +24,8 @@ mod livekit_client; )))] pub use livekit_client::*; +// If you need proper LSP in livekit_client you've got to comment out +// the mocks and test #[cfg(any( test, feature = "test-support", @@ -168,3 +176,59 @@ pub enum RoomEvent { Reconnecting, Reconnected, } + +pub(crate) fn default_device( + input: bool, +) -> anyhow::Result<(cpal::Device, cpal::SupportedStreamConfig)> { + let device; + let config; + if input { + device = cpal::default_host() + .default_input_device() + .context("no audio input device available")?; + config = device + .default_input_config() + .context("failed to get default input config")?; + } else { + device = cpal::default_host() + .default_output_device() + .context("no audio output device available")?; + config = device + .default_output_config() + .context("failed to get default output config")?; + } + Ok((device, config)) +} + +pub(crate) fn get_sample_data( + sample_format: cpal::SampleFormat, + data: &cpal::Data, +) -> anyhow::Result> { + match sample_format { + cpal::SampleFormat::I8 => Ok(convert_sample_data::(data)), + cpal::SampleFormat::I16 => Ok(data.as_slice::().unwrap().to_vec()), + cpal::SampleFormat::I24 => Ok(convert_sample_data::(data)), + cpal::SampleFormat::I32 => Ok(convert_sample_data::(data)), + cpal::SampleFormat::I64 => Ok(convert_sample_data::(data)), + cpal::SampleFormat::U8 => Ok(convert_sample_data::(data)), + cpal::SampleFormat::U16 => Ok(convert_sample_data::(data)), + cpal::SampleFormat::U32 => Ok(convert_sample_data::(data)), + cpal::SampleFormat::U64 => Ok(convert_sample_data::(data)), + cpal::SampleFormat::F32 => Ok(convert_sample_data::(data)), + cpal::SampleFormat::F64 => Ok(convert_sample_data::(data)), + _ => anyhow::bail!("Unsupported sample format"), + } +} + +pub(crate) fn convert_sample_data< + TSource: cpal::SizedSample, + TDest: cpal::SizedSample + cpal::FromSample, +>( + data: &cpal::Data, +) -> Vec { + data.as_slice::() + .unwrap() + .iter() + .map(|e| e.to_sample::()) + .collect() +} diff --git a/crates/livekit_client/src/livekit_client.rs b/crates/livekit_client/src/livekit_client.rs index 8f0ac1a456..adeea4f512 100644 --- a/crates/livekit_client/src/livekit_client.rs +++ b/crates/livekit_client/src/livekit_client.rs @@ -8,6 +8,8 @@ use gpui_tokio::Tokio; use playback::capture_local_video_track; mod playback; +#[cfg(feature = "record-microphone")] +mod record; use crate::{LocalTrack, Participant, RemoteTrack, RoomEvent, TrackPublication}; pub use playback::AudioStream; diff --git a/crates/livekit_client/src/livekit_client/playback.rs b/crates/livekit_client/src/livekit_client/playback.rs index f14e156125..d1eec42f8f 100644 --- a/crates/livekit_client/src/livekit_client/playback.rs +++ b/crates/livekit_client/src/livekit_client/playback.rs @@ -1,7 +1,6 @@ use anyhow::{Context as _, Result}; -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait as _}; -use cpal::{Data, FromSample, I24, SampleFormat, SizedSample}; +use cpal::traits::{DeviceTrait, StreamTrait as _}; use futures::channel::mpsc::UnboundedSender; use futures::{Stream, StreamExt as _}; use gpui::{ @@ -166,7 +165,7 @@ impl AudioStack { ) -> Result<()> { loop { let mut device_change_listener = DeviceChangeListener::new(false)?; - let (output_device, output_config) = default_device(false)?; + let (output_device, output_config) = crate::default_device(false)?; let (end_on_drop_tx, end_on_drop_rx) = std::sync::mpsc::channel::<()>(); let mixer = mixer.clone(); let apm = apm.clone(); @@ -238,7 +237,7 @@ impl AudioStack { ) -> Result<()> { loop { let mut device_change_listener = DeviceChangeListener::new(true)?; - let (device, config) = default_device(true)?; + let (device, config) = crate::default_device(true)?; let (end_on_drop_tx, end_on_drop_rx) = std::sync::mpsc::channel::<()>(); let apm = apm.clone(); let frame_tx = frame_tx.clone(); @@ -262,7 +261,7 @@ impl AudioStack { config.sample_format(), move |data, _: &_| { let data = - Self::get_sample_data(config.sample_format(), data).log_err(); + crate::get_sample_data(config.sample_format(), data).log_err(); let Some(data) = data else { return; }; @@ -320,33 +319,6 @@ impl AudioStack { drop(end_on_drop_tx) } } - - fn get_sample_data(sample_format: SampleFormat, data: &Data) -> Result> { - match sample_format { - SampleFormat::I8 => Ok(Self::convert_sample_data::(data)), - SampleFormat::I16 => Ok(data.as_slice::().unwrap().to_vec()), - SampleFormat::I24 => Ok(Self::convert_sample_data::(data)), - SampleFormat::I32 => Ok(Self::convert_sample_data::(data)), - SampleFormat::I64 => Ok(Self::convert_sample_data::(data)), - SampleFormat::U8 => Ok(Self::convert_sample_data::(data)), - SampleFormat::U16 => Ok(Self::convert_sample_data::(data)), - SampleFormat::U32 => Ok(Self::convert_sample_data::(data)), - SampleFormat::U64 => Ok(Self::convert_sample_data::(data)), - SampleFormat::F32 => Ok(Self::convert_sample_data::(data)), - SampleFormat::F64 => Ok(Self::convert_sample_data::(data)), - _ => anyhow::bail!("Unsupported sample format"), - } - } - - fn convert_sample_data>( - data: &Data, - ) -> Vec { - data.as_slice::() - .unwrap() - .iter() - .map(|e| e.to_sample::()) - .collect() - } } use super::LocalVideoTrack; @@ -393,27 +365,6 @@ pub(crate) async fn capture_local_video_track( )) } -fn default_device(input: bool) -> Result<(cpal::Device, cpal::SupportedStreamConfig)> { - let device; - let config; - if input { - device = cpal::default_host() - .default_input_device() - .context("no audio input device available")?; - config = device - .default_input_config() - .context("failed to get default input config")?; - } else { - device = cpal::default_host() - .default_output_device() - .context("no audio output device available")?; - config = device - .default_output_config() - .context("failed to get default output config")?; - } - Ok((device, config)) -} - #[derive(Clone)] struct AudioMixerSource { ssrc: i32, diff --git a/crates/livekit_client/src/record.rs b/crates/livekit_client/src/record.rs new file mode 100644 index 0000000000..925c0d4c67 --- /dev/null +++ b/crates/livekit_client/src/record.rs @@ -0,0 +1,91 @@ +use std::{ + env, + path::{Path, PathBuf}, + sync::{Arc, Mutex}, + time::Duration, +}; + +use anyhow::{Context, Result}; +use cpal::traits::{DeviceTrait, StreamTrait}; +use rodio::{buffer::SamplesBuffer, conversions::SampleTypeConverter}; +use util::ResultExt; + +pub struct CaptureInput { + pub name: String, + config: cpal::SupportedStreamConfig, + samples: Arc>>, + _stream: cpal::Stream, +} + +impl CaptureInput { + pub fn start() -> anyhow::Result { + let (device, config) = crate::default_device(true)?; + let name = device.name().unwrap_or("".to_string()); + log::info!("Using microphone: {}", name); + + let samples = Arc::new(Mutex::new(Vec::new())); + let stream = start_capture(device, config.clone(), samples.clone())?; + + Ok(Self { + name, + _stream: stream, + config, + samples, + }) + } + + pub fn finish(self) -> Result { + let name = self.name; + let mut path = env::current_dir().context("Could not get current dir")?; + path.push(&format!("test_recording_{name}.wav")); + log::info!("Test recording written to: {}", path.display()); + write_out(self.samples, self.config, &path)?; + Ok(path) + } +} + +fn start_capture( + device: cpal::Device, + config: cpal::SupportedStreamConfig, + samples: Arc>>, +) -> Result { + let stream = device + .build_input_stream_raw( + &config.config(), + config.sample_format(), + move |data, _: &_| { + let data = crate::get_sample_data(config.sample_format(), data).log_err(); + let Some(data) = data else { + return; + }; + samples + .try_lock() + .expect("Only locked after stream ends") + .extend_from_slice(&data); + }, + |err| log::error!("error capturing audio track: {:?}", err), + Some(Duration::from_millis(100)), + ) + .context("failed to build input stream")?; + + stream.play()?; + Ok(stream) +} + +fn write_out( + samples: Arc>>, + config: cpal::SupportedStreamConfig, + path: &Path, +) -> Result<()> { + let samples = std::mem::take( + &mut *samples + .try_lock() + .expect("Stream has ended, callback cant hold the lock"), + ); + let samples: Vec = SampleTypeConverter::<_, f32>::new(samples.into_iter()).collect(); + let mut samples = SamplesBuffer::new(config.channels(), config.sample_rate().0, samples); + match rodio::output_to_wav(&mut samples, path) { + Ok(_) => Ok(()), + Err(e) => Err(anyhow::anyhow!("Failed to write wav file: {}", e)), + } +} diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 96966435e1..7d8a28b0f1 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -6,6 +6,7 @@ use gpui::{ Task, svg, }; use parking_lot::Mutex; + use std::ops::Deref; use std::sync::{Arc, LazyLock}; use std::{any::TypeId, time::Duration}; @@ -189,6 +190,7 @@ impl Workspace { cx.notify(); } + /// Hide all notifications matching the given ID pub fn suppress_notification(&mut self, id: &NotificationId, cx: &mut Context) { self.dismiss_notification(id, cx); self.suppressed_notifications.insert(id.clone()); @@ -462,16 +464,144 @@ impl EventEmitter for ErrorMessagePrompt {} impl Notification for ErrorMessagePrompt {} +#[derive(IntoElement, RegisterComponent)] +pub struct NotificationFrame { + title: Option, + show_suppress_button: bool, + show_close_button: bool, + close: Option>, + contents: Option, + suffix: Option, +} + +impl NotificationFrame { + pub fn new() -> Self { + Self { + title: None, + contents: None, + suffix: None, + show_suppress_button: true, + show_close_button: true, + close: None, + } + } + + pub fn with_title(mut self, title: Option>) -> Self { + self.title = title.map(Into::into); + self + } + + pub fn with_content(self, content: impl IntoElement) -> Self { + Self { + contents: Some(content.into_any_element()), + ..self + } + } + + /// Determines whether the given notification ID should be suppressible + /// Suppressed motifications will not be shown anymore + pub fn show_suppress_button(mut self, show: bool) -> Self { + self.show_suppress_button = show; + self + } + + pub fn show_close_button(mut self, show: bool) -> Self { + self.show_close_button = show; + self + } + + pub fn on_close(self, on_close: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { + Self { + close: Some(Box::new(on_close)), + ..self + } + } + + pub fn with_suffix(mut self, suffix: impl IntoElement) -> Self { + self.suffix = Some(suffix.into_any_element()); + self + } +} + +impl RenderOnce for NotificationFrame { + fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let entity = window.current_view(); + let show_suppress_button = self.show_suppress_button; + let suppress = show_suppress_button && window.modifiers().shift; + let (close_id, close_icon) = if suppress { + ("suppress", IconName::Minimize) + } else { + ("close", IconName::Close) + }; + + v_flex() + .occlude() + .p_3() + .gap_2() + .elevation_3(cx) + .child( + h_flex() + .gap_4() + .justify_between() + .items_start() + .child( + v_flex() + .gap_0p5() + .when_some(self.title.clone(), |div, title| { + div.child(Label::new(title)) + }) + .child(div().max_w_96().children(self.contents)), + ) + .when(self.show_close_button, |this| { + this.on_modifiers_changed(move |_, _, cx| cx.notify(entity)) + .child( + IconButton::new(close_id, close_icon) + .tooltip(move |window, cx| { + if suppress { + Tooltip::for_action( + "Suppress.\nClose with click.", + &SuppressNotification, + window, + cx, + ) + } else if show_suppress_button { + Tooltip::for_action( + "Close.\nSuppress with shift-click.", + &menu::Cancel, + window, + cx, + ) + } else { + Tooltip::for_action("Close", &menu::Cancel, window, cx) + } + }) + .on_click({ + let close = self.close.take(); + move |_, window, cx| { + if let Some(close) = &close { + close(&suppress, window, cx) + } + } + }), + ) + }), + ) + .children(self.suffix) + } +} + +impl Component for NotificationFrame {} + pub mod simple_message_notification { use std::sync::Arc; use gpui::{ - AnyElement, ClickEvent, DismissEvent, EventEmitter, FocusHandle, Focusable, ParentElement, - Render, SharedString, Styled, div, + AnyElement, DismissEvent, EventEmitter, FocusHandle, Focusable, ParentElement, Render, + SharedString, Styled, }; - use ui::{Tooltip, prelude::*}; + use ui::prelude::*; - use crate::SuppressNotification; + use crate::notifications::NotificationFrame; use super::{Notification, SuppressEvent}; @@ -631,6 +761,8 @@ pub mod simple_message_notification { self } + /// Determines whether the given notification ID should be supressable + /// Suppressed motifications will not be shown anymor pub fn show_suppress_button(mut self, show: bool) -> Self { self.show_suppress_button = show; self @@ -647,71 +779,19 @@ pub mod simple_message_notification { impl Render for MessageNotification { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let show_suppress_button = self.show_suppress_button; - let suppress = show_suppress_button && window.modifiers().shift; - let (close_id, close_icon) = if suppress { - ("suppress", IconName::Minimize) - } else { - ("close", IconName::Close) - }; - - v_flex() - .occlude() - .p_3() - .gap_2() - .elevation_3(cx) - .child( - h_flex() - .gap_4() - .justify_between() - .items_start() - .child( - v_flex() - .gap_0p5() - .when_some(self.title.clone(), |element, title| { - element.child(Label::new(title)) - }) - .child(div().max_w_96().child((self.build_content)(window, cx))), - ) - .when(self.show_close_button, |this| { - this.on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify())) - .child( - IconButton::new(close_id, close_icon) - .tooltip(move |window, cx| { - if suppress { - Tooltip::for_action( - "Suppress.\nClose with click.", - &SuppressNotification, - window, - cx, - ) - } else if show_suppress_button { - Tooltip::for_action( - "Close.\nSuppress with shift-click.", - &menu::Cancel, - window, - cx, - ) - } else { - Tooltip::for_action( - "Close", - &menu::Cancel, - window, - cx, - ) - } - }) - .on_click(cx.listener(move |_, _: &ClickEvent, _, cx| { - if suppress { - cx.emit(SuppressEvent); - } else { - cx.emit(DismissEvent); - } - })), - ) - }), - ) - .child( + NotificationFrame::new() + .with_title(self.title.clone()) + .with_content((self.build_content)(window, cx)) + .show_close_button(self.show_close_button) + .show_suppress_button(self.show_suppress_button) + .on_close(cx.listener(|_, suppress, _, cx| { + if *suppress { + cx.emit(SuppressEvent); + } else { + cx.emit(DismissEvent); + } + })) + .with_suffix( h_flex() .gap_1() .children(self.primary_message.iter().map(|message| { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ca98404194..3129c12dbf 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -15,6 +15,8 @@ mod toast_layer; mod toolbar; mod workspace_settings; +pub use crate::notifications::NotificationFrame; +pub use dock::Panel; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; use anyhow::{Context as _, Result, anyhow}; @@ -24,7 +26,6 @@ use client::{ proto::{self, ErrorCode, PanelId, PeerId}, }; use collections::{HashMap, HashSet, hash_map}; -pub use dock::Panel; use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE}; use futures::{ Future, FutureExt, StreamExt, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 4335f2d5a1..d69efaf6c0 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -82,6 +82,7 @@ inspector_ui.workspace = true install_cli.workspace = true jj_ui.workspace = true journal.workspace = true +livekit_client.workspace = true language.workspace = true language_extension.workspace = true language_model.workspace = true diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index ceda403fdd..84145a1be4 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -56,6 +56,7 @@ use settings::{ initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content, update_settings_file, }; +use std::time::{Duration, Instant}; use std::{ borrow::Cow, path::{Path, PathBuf}, @@ -69,13 +70,17 @@ use util::markdown::MarkdownString; use util::{ResultExt, asset_str}; use uuid::Uuid; use vim_mode_setting::VimModeSetting; -use workspace::notifications::{NotificationId, dismiss_app_notification, show_app_notification}; +use workspace::notifications::{ + NotificationId, SuppressEvent, dismiss_app_notification, show_app_notification, +}; use workspace::{ AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings, create_and_open_local_file, notifications::simple_message_notification::MessageNotification, open_new, }; -use workspace::{CloseIntent, CloseWindow, RestoreBanner, with_active_or_new_workspace}; +use workspace::{ + CloseIntent, CloseWindow, NotificationFrame, RestoreBanner, with_active_or_new_workspace, +}; use workspace::{Pane, notifications::DetachAndPromptErr}; use zed_actions::{ OpenAccountSettings, OpenBrowser, OpenDocs, OpenServerSettings, OpenSettings, OpenZedUrl, Quit, @@ -117,6 +122,14 @@ actions!( ] ); +actions!( + dev, + [ + /// Record 10s of audio from your current microphone + CaptureAudio + ] +); + pub fn init(cx: &mut App) { #[cfg(target_os = "macos")] cx.on_action(|_: &Hide, cx| cx.hide()); @@ -897,7 +910,11 @@ fn register_actions( .detach(); } } + }) + .register_action(|workspace, _: &CaptureAudio, window, cx| { + capture_audio(workspace, window, cx); }); + if workspace.project().read(cx).is_via_ssh() { workspace.register_action({ move |workspace, _: &OpenServerSettings, window, cx| { @@ -1806,6 +1823,107 @@ fn open_settings_file( .detach_and_log_err(cx); } +fn capture_audio(workspace: &mut Workspace, _: &mut Window, cx: &mut Context) { + #[derive(Default)] + enum State { + Recording(livekit_client::CaptureInput), + Failed(String), + Finished(PathBuf), + // Used during state switch. Should never occur naturally. + #[default] + Invalid, + } + + struct CaptureAudioNotification { + focus_handle: gpui::FocusHandle, + start_time: Instant, + state: State, + } + + impl gpui::EventEmitter for CaptureAudioNotification {} + impl gpui::EventEmitter for CaptureAudioNotification {} + impl gpui::Focusable for CaptureAudioNotification { + fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle { + self.focus_handle.clone() + } + } + impl workspace::notifications::Notification for CaptureAudioNotification {} + + const AUDIO_RECORDING_TIME_SECS: u64 = 10; + + impl Render for CaptureAudioNotification { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let elapsed = self.start_time.elapsed().as_secs(); + let message = match &self.state { + State::Recording(capture) => format!( + "Recording {} seconds of audio from input: '{}'", + AUDIO_RECORDING_TIME_SECS - elapsed, + capture.name, + ), + State::Failed(e) => format!("Error capturing audio: {e}"), + State::Finished(path) => format!("Audio recorded to {}", path.display()), + State::Invalid => "Error invalid state".to_string(), + }; + + NotificationFrame::new() + .with_title(Some("Recording Audio")) + .show_suppress_button(false) + .on_close(cx.listener(|_, _, _, cx| { + cx.emit(DismissEvent); + })) + .with_content(message) + } + } + + impl CaptureAudioNotification { + fn finish(&mut self) { + let state = std::mem::take(&mut self.state); + self.state = if let State::Recording(capture) = state { + match capture.finish() { + Ok(path) => State::Finished(path), + Err(e) => State::Failed(e.to_string()), + } + } else { + state + }; + } + + fn new(cx: &mut Context) -> Self { + cx.spawn(async move |this, cx| { + for _ in 0..10 { + cx.background_executor().timer(Duration::from_secs(1)).await; + this.update(cx, |_, cx| { + cx.notify(); + })?; + } + + this.update(cx, |this, cx| { + this.finish(); + cx.notify(); + })?; + + anyhow::Ok(()) + }) + .detach(); + + let state = match livekit_client::CaptureInput::start() { + Ok(capture_input) => State::Recording(capture_input), + Err(err) => State::Failed(format!("Error starting audio capture: {}", err)), + }; + + Self { + focus_handle: cx.focus_handle(), + start_time: Instant::now(), + state, + } + } + } + + workspace.show_notification(NotificationId::unique::(), cx, |cx| { + cx.new(CaptureAudioNotification::new) + }); +} + #[cfg(test)] mod tests { use super::*; From d891348442f2196b248f992ef9067b3eee534f7c Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 15 Aug 2025 12:34:54 +0200 Subject: [PATCH 364/693] search: Simplify search options handling (#36233) Release Notes: - N/A --- crates/search/src/buffer_search.rs | 55 +++++------ crates/search/src/mode.rs | 36 ------- crates/search/src/project_search.rs | 71 +++++--------- crates/search/src/search.rs | 111 +++++++++++++--------- crates/search/src/search_bar.rs | 21 ---- crates/search/src/search_status_button.rs | 4 +- crates/zed/src/zed/quick_action_bar.rs | 2 +- 7 files changed, 115 insertions(+), 185 deletions(-) delete mode 100644 crates/search/src/mode.rs diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index ccef198f04..da2d35d74c 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,12 +1,10 @@ mod registrar; use crate::{ - FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, - SelectAllMatches, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleRegex, - ToggleReplace, ToggleSelection, ToggleWholeWord, - search_bar::{ - input_base_styles, render_action_button, render_text_input, toggle_replace_button, - }, + FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOption, + SearchOptions, SelectAllMatches, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, + ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord, + search_bar::{input_base_styles, render_action_button, render_text_input}, }; use any_vec::AnyVec; use anyhow::Context as _; @@ -215,31 +213,22 @@ impl Render for BufferSearchBar { h_flex() .gap_1() .when(case, |div| { - div.child(SearchOptions::CASE_SENSITIVE.as_button( - self.search_options.contains(SearchOptions::CASE_SENSITIVE), - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_case_sensitive(&ToggleCaseSensitive, window, cx) - }), - )) + div.child( + SearchOption::CaseSensitive + .as_button(self.search_options, focus_handle.clone()), + ) }) .when(word, |div| { - div.child(SearchOptions::WHOLE_WORD.as_button( - self.search_options.contains(SearchOptions::WHOLE_WORD), - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_whole_word(&ToggleWholeWord, window, cx) - }), - )) + div.child( + SearchOption::WholeWord + .as_button(self.search_options, focus_handle.clone()), + ) }) .when(regex, |div| { - div.child(SearchOptions::REGEX.as_button( - self.search_options.contains(SearchOptions::REGEX), - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_regex(&ToggleRegex, window, cx) - }), - )) + div.child( + SearchOption::Regex + .as_button(self.search_options, focus_handle.clone()), + ) }), ) }); @@ -248,13 +237,13 @@ impl Render for BufferSearchBar { .gap_1() .min_w_64() .when(replacement, |this| { - this.child(toggle_replace_button( - "buffer-search-bar-toggle-replace-button", - focus_handle.clone(), + this.child(render_action_button( + "buffer-search-bar-toggle", + IconName::Replace, self.replace_enabled, - cx.listener(|this, _: &ClickEvent, window, cx| { - this.toggle_replace(&ToggleReplace, window, cx); - }), + "Toggle Replace", + &ToggleReplace, + focus_handle.clone(), )) }) .when(selection, |this| { diff --git a/crates/search/src/mode.rs b/crates/search/src/mode.rs deleted file mode 100644 index 957eb707a5..0000000000 --- a/crates/search/src/mode.rs +++ /dev/null @@ -1,36 +0,0 @@ -use gpui::{Action, SharedString}; - -use crate::{ActivateRegexMode, ActivateTextMode}; - -// TODO: Update the default search mode to get from config -#[derive(Copy, Clone, Debug, Default, PartialEq)] -pub enum SearchMode { - #[default] - Text, - Regex, -} - -impl SearchMode { - pub(crate) fn label(&self) -> &'static str { - match self { - SearchMode::Text => "Text", - SearchMode::Regex => "Regex", - } - } - pub(crate) fn tooltip(&self) -> SharedString { - format!("Activate {} Mode", self.label()).into() - } - pub(crate) fn action(&self) -> Box { - match self { - SearchMode::Text => ActivateTextMode.boxed_clone(), - SearchMode::Regex => ActivateRegexMode.boxed_clone(), - } - } -} - -pub(crate) fn next_mode(mode: &SearchMode) -> SearchMode { - match mode { - SearchMode::Text => SearchMode::Regex, - SearchMode::Regex => SearchMode::Text, - } -} diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 9e8afa4392..6b9777906a 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,11 +1,9 @@ use crate::{ BufferSearchBar, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, - SearchOptions, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, ToggleIncludeIgnored, - ToggleRegex, ToggleReplace, ToggleWholeWord, + SearchOption, SearchOptions, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, + ToggleIncludeIgnored, ToggleRegex, ToggleReplace, ToggleWholeWord, buffer_search::Deploy, - search_bar::{ - input_base_styles, render_action_button, render_text_input, toggle_replace_button, - }, + search_bar::{input_base_styles, render_action_button, render_text_input}, }; use anyhow::Context as _; use collections::HashMap; @@ -1784,14 +1782,6 @@ impl ProjectSearchBar { } } - fn is_option_enabled(&self, option: SearchOptions, cx: &App) -> bool { - if let Some(search) = self.active_project_search.as_ref() { - search.read(cx).search_options.contains(option) - } else { - false - } - } - fn next_history_query( &mut self, _: &NextHistoryQuery, @@ -1972,27 +1962,17 @@ impl Render for ProjectSearchBar { .child( h_flex() .gap_1() - .child(SearchOptions::CASE_SENSITIVE.as_button( - self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx), - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx); - }), - )) - .child(SearchOptions::WHOLE_WORD.as_button( - self.is_option_enabled(SearchOptions::WHOLE_WORD, cx), - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx); - }), - )) - .child(SearchOptions::REGEX.as_button( - self.is_option_enabled(SearchOptions::REGEX, cx), - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_search_option(SearchOptions::REGEX, window, cx); - }), - )), + .child( + SearchOption::CaseSensitive + .as_button(search.search_options, focus_handle.clone()), + ) + .child( + SearchOption::WholeWord + .as_button(search.search_options, focus_handle.clone()), + ) + .child( + SearchOption::Regex.as_button(search.search_options, focus_handle.clone()), + ), ); let mode_column = h_flex() @@ -2026,16 +2006,16 @@ impl Render for ProjectSearchBar { } }), ) - .child(toggle_replace_button( - "project-search-toggle-replace", - focus_handle.clone(), + .child(render_action_button( + "project-search", + IconName::Replace, self.active_project_search .as_ref() .map(|search| search.read(cx).replace_enabled) .unwrap_or_default(), - cx.listener(|this, _, window, cx| { - this.toggle_replace(&ToggleReplace, window, cx); - }), + "Toggle Replace", + &ToggleReplace, + focus_handle.clone(), )); let query_focus = search.query_editor.focus_handle(cx); @@ -2149,15 +2129,8 @@ impl Render for ProjectSearchBar { })), ) .child( - SearchOptions::INCLUDE_IGNORED.as_button( - search - .search_options - .contains(SearchOptions::INCLUDE_IGNORED), - focus_handle.clone(), - cx.listener(|this, _, window, cx| { - this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, window, cx); - }), - ), + SearchOption::IncludeIgnored + .as_button(search.search_options, focus_handle.clone()), ); h_flex() .w_full() diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 5f57bfb4b1..89064e0a27 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -9,6 +9,8 @@ use ui::{Tooltip, prelude::*}; use workspace::notifications::NotificationId; use workspace::{Toast, Workspace}; +pub use search_status_button::SEARCH_ICON; + pub mod buffer_search; pub mod project_search; pub(crate) mod search_bar; @@ -59,48 +61,87 @@ actions!( bitflags! { #[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] pub struct SearchOptions: u8 { - const NONE = 0b000; - const WHOLE_WORD = 0b001; - const CASE_SENSITIVE = 0b010; - const INCLUDE_IGNORED = 0b100; - const REGEX = 0b1000; - const ONE_MATCH_PER_LINE = 0b100000; + const NONE = 0; + const WHOLE_WORD = 1 << SearchOption::WholeWord as u8; + const CASE_SENSITIVE = 1 << SearchOption::CaseSensitive as u8; + const INCLUDE_IGNORED = 1 << SearchOption::IncludeIgnored as u8; + const REGEX = 1 << SearchOption::Regex as u8; + const ONE_MATCH_PER_LINE = 1 << SearchOption::OneMatchPerLine as u8; /// If set, reverse direction when finding the active match - const BACKWARDS = 0b10000; + const BACKWARDS = 1 << SearchOption::Backwards as u8; } } -impl SearchOptions { +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum SearchOption { + WholeWord = 0, + CaseSensitive, + IncludeIgnored, + Regex, + OneMatchPerLine, + Backwards, +} + +impl SearchOption { + pub fn as_options(self) -> SearchOptions { + SearchOptions::from_bits(1 << self as u8).unwrap() + } + pub fn label(&self) -> &'static str { - match *self { - SearchOptions::WHOLE_WORD => "Match Whole Words", - SearchOptions::CASE_SENSITIVE => "Match Case Sensitively", - SearchOptions::INCLUDE_IGNORED => "Also search files ignored by configuration", - SearchOptions::REGEX => "Use Regular Expressions", - _ => panic!("{:?} is not a named SearchOption", self), + match self { + SearchOption::WholeWord => "Match Whole Words", + SearchOption::CaseSensitive => "Match Case Sensitively", + SearchOption::IncludeIgnored => "Also search files ignored by configuration", + SearchOption::Regex => "Use Regular Expressions", + SearchOption::OneMatchPerLine => "One Match Per Line", + SearchOption::Backwards => "Search Backwards", } } pub fn icon(&self) -> ui::IconName { - match *self { - SearchOptions::WHOLE_WORD => ui::IconName::WholeWord, - SearchOptions::CASE_SENSITIVE => ui::IconName::CaseSensitive, - SearchOptions::INCLUDE_IGNORED => ui::IconName::Sliders, - SearchOptions::REGEX => ui::IconName::Regex, - _ => panic!("{:?} is not a named SearchOption", self), + match self { + SearchOption::WholeWord => ui::IconName::WholeWord, + SearchOption::CaseSensitive => ui::IconName::CaseSensitive, + SearchOption::IncludeIgnored => ui::IconName::Sliders, + SearchOption::Regex => ui::IconName::Regex, + _ => panic!("{self:?} is not a named SearchOption"), } } - pub fn to_toggle_action(&self) -> Box { + pub fn to_toggle_action(&self) -> &'static dyn Action { match *self { - SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord), - SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive), - SearchOptions::INCLUDE_IGNORED => Box::new(ToggleIncludeIgnored), - SearchOptions::REGEX => Box::new(ToggleRegex), - _ => panic!("{:?} is not a named SearchOption", self), + SearchOption::WholeWord => &ToggleWholeWord, + SearchOption::CaseSensitive => &ToggleCaseSensitive, + SearchOption::IncludeIgnored => &ToggleIncludeIgnored, + SearchOption::Regex => &ToggleRegex, + _ => panic!("{self:?} is not a toggle action"), } } + pub fn as_button(&self, active: SearchOptions, focus_handle: FocusHandle) -> impl IntoElement { + let action = self.to_toggle_action(); + let label = self.label(); + IconButton::new(label, self.icon()) + .on_click({ + let focus_handle = focus_handle.clone(); + move |_, window, cx| { + if !focus_handle.is_focused(&window) { + window.focus(&focus_handle); + } + window.dispatch_action(action.boxed_clone(), cx) + } + }) + .style(ButtonStyle::Subtle) + .shape(IconButtonShape::Square) + .toggle_state(active.contains(self.as_options())) + .tooltip({ + move |window, cx| Tooltip::for_action_in(label, action, &focus_handle, window, cx) + }) + } +} + +impl SearchOptions { pub fn none() -> SearchOptions { SearchOptions::NONE } @@ -122,24 +163,6 @@ impl SearchOptions { options.set(SearchOptions::REGEX, settings.regex); options } - - pub fn as_button( - &self, - active: bool, - focus_handle: FocusHandle, - action: Action, - ) -> impl IntoElement + use { - IconButton::new(self.label(), self.icon()) - .on_click(action) - .style(ButtonStyle::Subtle) - .shape(IconButtonShape::Square) - .toggle_state(active) - .tooltip({ - let action = self.to_toggle_action(); - let label = self.label(); - move |window, cx| Tooltip::for_action_in(label, &*action, &focus_handle, window, cx) - }) - } } pub(crate) fn show_no_more_matches(window: &mut Window, cx: &mut App) { diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 2805b0c62d..094ce3638e 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -5,8 +5,6 @@ use theme::ThemeSettings; use ui::{IconButton, IconButtonShape}; use ui::{Tooltip, prelude::*}; -use crate::ToggleReplace; - pub(super) fn render_action_button( id_prefix: &'static str, icon: ui::IconName, @@ -46,25 +44,6 @@ pub(crate) fn input_base_styles(border_color: Hsla, map: impl FnOnce(Div) -> Div .rounded_lg() } -pub(crate) fn toggle_replace_button( - id: &'static str, - focus_handle: FocusHandle, - replace_enabled: bool, - on_click: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static, -) -> IconButton { - IconButton::new(id, IconName::Replace) - .shape(IconButtonShape::Square) - .style(ButtonStyle::Subtle) - .when(replace_enabled, |button| button.style(ButtonStyle::Filled)) - .on_click(on_click) - .toggle_state(replace_enabled) - .tooltip({ - move |window, cx| { - Tooltip::for_action_in("Toggle Replace", &ToggleReplace, &focus_handle, window, cx) - } - }) -} - pub(crate) fn render_text_input( editor: &Entity, color_override: Option, diff --git a/crates/search/src/search_status_button.rs b/crates/search/src/search_status_button.rs index ff2ee1641d..fcf36e86fa 100644 --- a/crates/search/src/search_status_button.rs +++ b/crates/search/src/search_status_button.rs @@ -3,6 +3,8 @@ use settings::Settings as _; use ui::{ButtonCommon, Clickable, Context, Render, Tooltip, Window, prelude::*}; use workspace::{ItemHandle, StatusItemView}; +pub const SEARCH_ICON: IconName = IconName::MagnifyingGlass; + pub struct SearchButton; impl SearchButton { @@ -20,7 +22,7 @@ impl Render for SearchButton { } button.child( - IconButton::new("project-search-indicator", IconName::MagnifyingGlass) + IconButton::new("project-search-indicator", SEARCH_ICON) .icon_size(IconSize::Small) .tooltip(|window, cx| { Tooltip::for_action( diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index e76bef59a3..2b7c38f997 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -140,7 +140,7 @@ impl Render for QuickActionBar { let search_button = editor.is_singleton(cx).then(|| { QuickActionBarButton::new( "toggle buffer search", - IconName::MagnifyingGlass, + search::SEARCH_ICON, !self.buffer_search_bar.read(cx).is_dismissed(), Box::new(buffer_search::Deploy::find()), focus_handle.clone(), From 2a57b160b03c8e8543fdae12a0c191ed1a985e54 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Fri, 15 Aug 2025 13:54:24 +0300 Subject: [PATCH 365/693] openai: Don't send prompt_cache_key for OpenAI-compatible models (#36231) Some APIs fail when they get this parameter Closes #36215 Release Notes: - Fixed OpenAI-compatible providers that don't support prompt caching and/or reasoning --- crates/language_models/src/provider/cloud.rs | 1 + crates/language_models/src/provider/open_ai.rs | 8 +++++++- crates/language_models/src/provider/open_ai_compatible.rs | 5 ++++- crates/language_models/src/provider/vercel.rs | 1 + crates/language_models/src/provider/x_ai.rs | 1 + crates/open_ai/src/open_ai.rs | 7 +++++++ crates/vercel/src/vercel.rs | 4 ++++ crates/x_ai/src/x_ai.rs | 4 ++++ 8 files changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index ff8048040e..c1337399f9 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -941,6 +941,7 @@ impl LanguageModel for CloudLanguageModel { request, model.id(), model.supports_parallel_tool_calls(), + model.supports_prompt_cache_key(), None, None, ); diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 725027b2a7..eaf8d885b3 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -370,6 +370,7 @@ impl LanguageModel for OpenAiLanguageModel { request, self.model.id(), self.model.supports_parallel_tool_calls(), + self.model.supports_prompt_cache_key(), self.max_output_tokens(), self.model.reasoning_effort(), ); @@ -386,6 +387,7 @@ pub fn into_open_ai( request: LanguageModelRequest, model_id: &str, supports_parallel_tool_calls: bool, + supports_prompt_cache_key: bool, max_output_tokens: Option, reasoning_effort: Option, ) -> open_ai::Request { @@ -477,7 +479,11 @@ pub fn into_open_ai( } else { None }, - prompt_cache_key: request.thread_id, + prompt_cache_key: if supports_prompt_cache_key { + request.thread_id + } else { + None + }, tools: request .tools .into_iter() diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs index 6e912765cd..5f546f5219 100644 --- a/crates/language_models/src/provider/open_ai_compatible.rs +++ b/crates/language_models/src/provider/open_ai_compatible.rs @@ -355,10 +355,13 @@ impl LanguageModel for OpenAiCompatibleLanguageModel { LanguageModelCompletionError, >, > { + let supports_parallel_tool_call = true; + let supports_prompt_cache_key = false; let request = into_open_ai( request, &self.model.name, - true, + supports_parallel_tool_call, + supports_prompt_cache_key, self.max_output_tokens(), None, ); diff --git a/crates/language_models/src/provider/vercel.rs b/crates/language_models/src/provider/vercel.rs index 57a89ba4aa..9f447cb68b 100644 --- a/crates/language_models/src/provider/vercel.rs +++ b/crates/language_models/src/provider/vercel.rs @@ -355,6 +355,7 @@ impl LanguageModel for VercelLanguageModel { request, self.model.id(), self.model.supports_parallel_tool_calls(), + self.model.supports_prompt_cache_key(), self.max_output_tokens(), None, ); diff --git a/crates/language_models/src/provider/x_ai.rs b/crates/language_models/src/provider/x_ai.rs index 5e7190ea96..fed6fe92bf 100644 --- a/crates/language_models/src/provider/x_ai.rs +++ b/crates/language_models/src/provider/x_ai.rs @@ -359,6 +359,7 @@ impl LanguageModel for XAiLanguageModel { request, self.model.id(), self.model.supports_parallel_tool_calls(), + self.model.supports_prompt_cache_key(), self.max_output_tokens(), None, ); diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 8bbe858995..604e8fe622 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -236,6 +236,13 @@ impl Model { Self::O1 | Self::O3 | Self::O3Mini | Self::O4Mini | Model::Custom { .. } => false, } } + + /// Returns whether the given model supports the `prompt_cache_key` parameter. + /// + /// If the model does not support the parameter, do not pass it up. + pub fn supports_prompt_cache_key(&self) -> bool { + return true; + } } #[derive(Debug, Serialize, Deserialize)] diff --git a/crates/vercel/src/vercel.rs b/crates/vercel/src/vercel.rs index 1ae22c5fef..8686fda53f 100644 --- a/crates/vercel/src/vercel.rs +++ b/crates/vercel/src/vercel.rs @@ -71,4 +71,8 @@ impl Model { Model::Custom { .. } => false, } } + + pub fn supports_prompt_cache_key(&self) -> bool { + false + } } diff --git a/crates/x_ai/src/x_ai.rs b/crates/x_ai/src/x_ai.rs index ac116b2f8f..23cd5b9320 100644 --- a/crates/x_ai/src/x_ai.rs +++ b/crates/x_ai/src/x_ai.rs @@ -105,6 +105,10 @@ impl Model { } } + pub fn supports_prompt_cache_key(&self) -> bool { + false + } + pub fn supports_tool(&self) -> bool { match self { Self::Grok2Vision From f8b01052583d3e27fbbbf5f46eb4f5bd5ec279aa Mon Sep 17 00:00:00 2001 From: smit Date: Fri, 15 Aug 2025 16:24:54 +0530 Subject: [PATCH 366/693] project: Fix LSP TextDocumentSyncCapability dynamic registration (#36234) Closes #36213 Use `textDocument/didChange` ([docs](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_synchronization)) instead of `textDocument/synchronization`. Release Notes: - Fixed an issue where Dart projects were being formatted incorrectly by the language server. --- crates/project/src/lsp_store.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 60d847023f..196f55171a 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -11817,14 +11817,16 @@ impl LspStore { notify_server_capabilities_updated(&server, cx); } } - "textDocument/synchronization" => { - if let Some(caps) = reg + "textDocument/didChange" => { + if let Some(sync_kind) = reg .register_options - .map(serde_json::from_value) + .and_then(|opts| opts.get("syncKind").cloned()) + .map(serde_json::from_value::) .transpose()? { server.update_capabilities(|capabilities| { - capabilities.text_document_sync = Some(caps); + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)); }); notify_server_capabilities_updated(&server, cx); } @@ -11974,7 +11976,7 @@ impl LspStore { }); notify_server_capabilities_updated(&server, cx); } - "textDocument/synchronization" => { + "textDocument/didChange" => { server.update_capabilities(|capabilities| { capabilities.text_document_sync = None; }); From 6f3cd42411c64879848d0bc96d838d3bef8c374c Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Fri, 15 Aug 2025 13:17:17 +0200 Subject: [PATCH 367/693] agent2: Port Zed AI features (#36172) Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra --- crates/acp_thread/src/acp_thread.rs | 173 +++++--- crates/acp_thread/src/connection.rs | 26 +- crates/agent2/src/agent.rs | 282 +++++++------ crates/agent2/src/tests/mod.rs | 202 +++++++++- crates/agent2/src/tests/test_tools.rs | 2 +- crates/agent2/src/thread.rs | 186 ++++++--- crates/agent2/src/tools/edit_file_tool.rs | 2 +- crates/agent_servers/src/acp/v0.rs | 6 +- crates/agent_servers/src/acp/v1.rs | 6 +- crates/agent_servers/src/claude.rs | 5 + crates/agent_ui/src/acp/thread_view.rs | 376 ++++++++++++++++-- crates/agent_ui/src/agent_ui.rs | 1 - crates/agent_ui/src/burn_mode_tooltip.rs | 61 --- crates/agent_ui/src/message_editor.rs | 4 +- crates/agent_ui/src/text_thread_editor.rs | 2 +- crates/agent_ui/src/ui/burn_mode_tooltip.rs | 6 +- .../language_model/src/model/cloud_model.rs | 12 + 17 files changed, 994 insertions(+), 358 deletions(-) delete mode 100644 crates/agent_ui/src/burn_mode_tooltip.rs diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 4005f27a0c..4995ddb9df 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -33,13 +33,23 @@ pub struct UserMessage { pub id: Option, pub content: ContentBlock, pub chunks: Vec, - pub checkpoint: Option, + pub checkpoint: Option, +} + +#[derive(Debug)] +pub struct Checkpoint { + git_checkpoint: GitStoreCheckpoint, + pub show: bool, } impl UserMessage { fn to_markdown(&self, cx: &App) -> String { let mut markdown = String::new(); - if let Some(_) = self.checkpoint { + if self + .checkpoint + .as_ref() + .map_or(false, |checkpoint| checkpoint.show) + { writeln!(markdown, "## User (checkpoint)").unwrap(); } else { writeln!(markdown, "## User").unwrap(); @@ -1145,9 +1155,12 @@ impl AcpThread { self.project.read(cx).languages().clone(), cx, ); + let request = acp::PromptRequest { + prompt: message.clone(), + session_id: self.session_id.clone(), + }; let git_store = self.project.read(cx).git_store().clone(); - let old_checkpoint = git_store.update(cx, |git, cx| git.checkpoint(cx)); let message_id = if self .connection .session_editor(&self.session_id, cx) @@ -1161,68 +1174,63 @@ impl AcpThread { AgentThreadEntry::UserMessage(UserMessage { id: message_id.clone(), content: block, - chunks: message.clone(), + chunks: message, checkpoint: None, }), cx, ); + + self.run_turn(cx, async move |this, cx| { + let old_checkpoint = git_store + .update(cx, |git, cx| git.checkpoint(cx))? + .await + .context("failed to get old checkpoint") + .log_err(); + this.update(cx, |this, cx| { + if let Some((_ix, message)) = this.last_user_message() { + message.checkpoint = old_checkpoint.map(|git_checkpoint| Checkpoint { + git_checkpoint, + show: false, + }); + } + this.connection.prompt(message_id, request, cx) + })? + .await + }) + } + + pub fn resume(&mut self, cx: &mut Context) -> BoxFuture<'static, Result<()>> { + self.run_turn(cx, async move |this, cx| { + this.update(cx, |this, cx| { + this.connection + .resume(&this.session_id, cx) + .map(|resume| resume.run(cx)) + })? + .context("resuming a session is not supported")? + .await + }) + } + + fn run_turn( + &mut self, + cx: &mut Context, + f: impl 'static + AsyncFnOnce(WeakEntity, &mut AsyncApp) -> Result, + ) -> BoxFuture<'static, Result<()>> { self.clear_completed_plan_entries(cx); - let (old_checkpoint_tx, old_checkpoint_rx) = oneshot::channel(); let (tx, rx) = oneshot::channel(); let cancel_task = self.cancel(cx); - let request = acp::PromptRequest { - prompt: message, - session_id: self.session_id.clone(), - }; - self.send_task = Some(cx.spawn({ - let message_id = message_id.clone(); - async move |this, cx| { - cancel_task.await; - - old_checkpoint_tx.send(old_checkpoint.await).ok(); - if let Ok(result) = this.update(cx, |this, cx| { - this.connection.prompt(message_id, request, cx) - }) { - tx.send(result.await).log_err(); - } - } + self.send_task = Some(cx.spawn(async move |this, cx| { + cancel_task.await; + tx.send(f(this, cx).await).ok(); })); cx.spawn(async move |this, cx| { - let old_checkpoint = old_checkpoint_rx - .await - .map_err(|_| anyhow!("send canceled")) - .flatten() - .context("failed to get old checkpoint") - .log_err(); - let response = rx.await; - if let Some((old_checkpoint, message_id)) = old_checkpoint.zip(message_id) { - let new_checkpoint = git_store - .update(cx, |git, cx| git.checkpoint(cx))? - .await - .context("failed to get new checkpoint") - .log_err(); - if let Some(new_checkpoint) = new_checkpoint { - let equal = git_store - .update(cx, |git, cx| { - git.compare_checkpoints(old_checkpoint.clone(), new_checkpoint, cx) - })? - .await - .unwrap_or(true); - if !equal { - this.update(cx, |this, cx| { - if let Some((ix, message)) = this.user_message_mut(&message_id) { - message.checkpoint = Some(old_checkpoint); - cx.emit(AcpThreadEvent::EntryUpdated(ix)); - } - })?; - } - } - } + this.update(cx, |this, cx| this.update_last_checkpoint(cx))? + .await?; this.update(cx, |this, cx| { match response { @@ -1294,7 +1302,10 @@ impl AcpThread { return Task::ready(Err(anyhow!("message not found"))); }; - let checkpoint = message.checkpoint.clone(); + let checkpoint = message + .checkpoint + .as_ref() + .map(|c| c.git_checkpoint.clone()); let git_store = self.project.read(cx).git_store().clone(); cx.spawn(async move |this, cx| { @@ -1316,6 +1327,59 @@ impl AcpThread { }) } + fn update_last_checkpoint(&mut self, cx: &mut Context) -> Task> { + let git_store = self.project.read(cx).git_store().clone(); + + let old_checkpoint = if let Some((_, message)) = self.last_user_message() { + if let Some(checkpoint) = message.checkpoint.as_ref() { + checkpoint.git_checkpoint.clone() + } else { + return Task::ready(Ok(())); + } + } else { + return Task::ready(Ok(())); + }; + + let new_checkpoint = git_store.update(cx, |git, cx| git.checkpoint(cx)); + cx.spawn(async move |this, cx| { + let new_checkpoint = new_checkpoint + .await + .context("failed to get new checkpoint") + .log_err(); + if let Some(new_checkpoint) = new_checkpoint { + let equal = git_store + .update(cx, |git, cx| { + git.compare_checkpoints(old_checkpoint.clone(), new_checkpoint, cx) + })? + .await + .unwrap_or(true); + this.update(cx, |this, cx| { + let (ix, message) = this.last_user_message().context("no user message")?; + let checkpoint = message.checkpoint.as_mut().context("no checkpoint")?; + checkpoint.show = !equal; + cx.emit(AcpThreadEvent::EntryUpdated(ix)); + anyhow::Ok(()) + })??; + } + + Ok(()) + }) + } + + fn last_user_message(&mut self) -> Option<(usize, &mut UserMessage)> { + self.entries + .iter_mut() + .enumerate() + .rev() + .find_map(|(ix, entry)| { + if let AgentThreadEntry::UserMessage(message) = entry { + Some((ix, message)) + } else { + None + } + }) + } + fn user_message(&self, id: &UserMessageId) -> Option<&UserMessage> { self.entries.iter().find_map(|entry| { if let AgentThreadEntry::UserMessage(message) = entry { @@ -1552,6 +1616,7 @@ mod tests { use settings::SettingsStore; use smol::stream::StreamExt as _; use std::{ + any::Any, cell::RefCell, path::Path, rc::Rc, @@ -2284,6 +2349,10 @@ mod tests { _session_id: session_id.clone(), })) } + + fn into_any(self: Rc) -> Rc { + self + } } struct FakeAgentSessionEditor { diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 0f531acbde..b2116020fb 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -4,7 +4,7 @@ use anyhow::Result; use collections::IndexMap; use gpui::{Entity, SharedString, Task}; use project::Project; -use std::{error::Error, fmt, path::Path, rc::Rc, sync::Arc}; +use std::{any::Any, error::Error, fmt, path::Path, rc::Rc, sync::Arc}; use ui::{App, IconName}; use uuid::Uuid; @@ -36,6 +36,14 @@ pub trait AgentConnection { cx: &mut App, ) -> Task>; + fn resume( + &self, + _session_id: &acp::SessionId, + _cx: &mut App, + ) -> Option> { + None + } + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App); fn session_editor( @@ -53,12 +61,24 @@ pub trait AgentConnection { fn model_selector(&self) -> Option> { None } + + fn into_any(self: Rc) -> Rc; +} + +impl dyn AgentConnection { + pub fn downcast(self: Rc) -> Option> { + self.into_any().downcast().ok() + } } pub trait AgentSessionEditor { fn truncate(&self, message_id: UserMessageId, cx: &mut App) -> Task>; } +pub trait AgentSessionResume { + fn run(&self, cx: &mut App) -> Task>; +} + #[derive(Debug)] pub struct AuthRequired; @@ -299,6 +319,10 @@ mod test_support { ) -> Option> { Some(Rc::new(StubAgentSessionEditor)) } + + fn into_any(self: Rc) -> Rc { + self + } } struct StubAgentSessionEditor; diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 9ac3c2d0e5..358365d11f 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,9 +1,8 @@ -use crate::{AgentResponseEvent, Thread, templates::Templates}; use crate::{ - ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DeletePathTool, DiagnosticsTool, - EditFileTool, FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MovePathTool, NowTool, - OpenTool, ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, UserMessageContent, - WebSearchTool, + AgentResponseEvent, ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DeletePathTool, + DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool, ListDirectoryTool, + MovePathTool, NowTool, OpenTool, ReadFileTool, TerminalTool, ThinkingTool, Thread, + ToolCallAuthorization, UserMessageContent, WebSearchTool, templates::Templates, }; use acp_thread::AgentModelSelector; use agent_client_protocol as acp; @@ -11,6 +10,7 @@ use agent_settings::AgentSettings; use anyhow::{Context as _, Result, anyhow}; use collections::{HashSet, IndexMap}; use fs::Fs; +use futures::channel::mpsc; use futures::{StreamExt, future}; use gpui::{ App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity, @@ -21,6 +21,7 @@ use prompt_store::{ ProjectContext, PromptId, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext, }; use settings::update_settings_file; +use std::any::Any; use std::cell::RefCell; use std::collections::HashMap; use std::path::Path; @@ -426,9 +427,9 @@ impl NativeAgent { self.models.refresh_list(cx); for session in self.sessions.values_mut() { session.thread.update(cx, |thread, _| { - let model_id = LanguageModels::model_id(&thread.selected_model); + let model_id = LanguageModels::model_id(&thread.model()); if let Some(model) = self.models.model_from_id(&model_id) { - thread.selected_model = model.clone(); + thread.set_model(model.clone()); } }); } @@ -439,6 +440,124 @@ impl NativeAgent { #[derive(Clone)] pub struct NativeAgentConnection(pub Entity); +impl NativeAgentConnection { + pub fn thread(&self, session_id: &acp::SessionId, cx: &App) -> Option> { + self.0 + .read(cx) + .sessions + .get(session_id) + .map(|session| session.thread.clone()) + } + + fn run_turn( + &self, + session_id: acp::SessionId, + cx: &mut App, + f: impl 'static + + FnOnce( + Entity, + &mut App, + ) -> Result>>, + ) -> Task> { + let Some((thread, acp_thread)) = self.0.update(cx, |agent, _cx| { + agent + .sessions + .get_mut(&session_id) + .map(|s| (s.thread.clone(), s.acp_thread.clone())) + }) else { + return Task::ready(Err(anyhow!("Session not found"))); + }; + log::debug!("Found session for: {}", session_id); + + let mut response_stream = match f(thread, cx) { + Ok(stream) => stream, + Err(err) => return Task::ready(Err(err)), + }; + cx.spawn(async move |cx| { + // Handle response stream and forward to session.acp_thread + while let Some(result) = response_stream.next().await { + match result { + Ok(event) => { + log::trace!("Received completion event: {:?}", event); + + match event { + AgentResponseEvent::Text(text) => { + acp_thread.update(cx, |thread, cx| { + thread.push_assistant_content_block( + acp::ContentBlock::Text(acp::TextContent { + text, + annotations: None, + }), + false, + cx, + ) + })?; + } + AgentResponseEvent::Thinking(text) => { + acp_thread.update(cx, |thread, cx| { + thread.push_assistant_content_block( + acp::ContentBlock::Text(acp::TextContent { + text, + annotations: None, + }), + true, + cx, + ) + })?; + } + AgentResponseEvent::ToolCallAuthorization(ToolCallAuthorization { + tool_call, + options, + response, + }) => { + let recv = acp_thread.update(cx, |thread, cx| { + thread.request_tool_call_authorization(tool_call, options, cx) + })?; + cx.background_spawn(async move { + if let Some(option) = recv + .await + .context("authorization sender was dropped") + .log_err() + { + response + .send(option) + .map(|_| anyhow!("authorization receiver was dropped")) + .log_err(); + } + }) + .detach(); + } + AgentResponseEvent::ToolCall(tool_call) => { + acp_thread.update(cx, |thread, cx| { + thread.upsert_tool_call(tool_call, cx) + })?; + } + AgentResponseEvent::ToolCallUpdate(update) => { + acp_thread.update(cx, |thread, cx| { + thread.update_tool_call(update, cx) + })??; + } + AgentResponseEvent::Stop(stop_reason) => { + log::debug!("Assistant message complete: {:?}", stop_reason); + return Ok(acp::PromptResponse { stop_reason }); + } + } + } + Err(e) => { + log::error!("Error in model response stream: {:?}", e); + return Err(e); + } + } + } + + log::info!("Response stream completed"); + anyhow::Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) + }) + } +} + impl AgentModelSelector for NativeAgentConnection { fn list_models(&self, cx: &mut App) -> Task> { log::debug!("NativeAgentConnection::list_models called"); @@ -472,7 +591,7 @@ impl AgentModelSelector for NativeAgentConnection { }; thread.update(cx, |thread, _cx| { - thread.selected_model = model.clone(); + thread.set_model(model.clone()); }); update_settings_file::( @@ -502,7 +621,7 @@ impl AgentModelSelector for NativeAgentConnection { else { return Task::ready(Err(anyhow!("Session not found"))); }; - let model = thread.read(cx).selected_model.clone(); + let model = thread.read(cx).model().clone(); let Some(provider) = LanguageModelRegistry::read_global(cx).provider(&model.provider_id()) else { return Task::ready(Err(anyhow!("Provider not found"))); @@ -644,25 +763,10 @@ impl acp_thread::AgentConnection for NativeAgentConnection { ) -> Task> { let id = id.expect("UserMessageId is required"); let session_id = params.session_id.clone(); - let agent = self.0.clone(); log::info!("Received prompt request for session: {}", session_id); log::debug!("Prompt blocks count: {}", params.prompt.len()); - cx.spawn(async move |cx| { - // Get session - let (thread, acp_thread) = agent - .update(cx, |agent, _| { - agent - .sessions - .get_mut(&session_id) - .map(|s| (s.thread.clone(), s.acp_thread.clone())) - })? - .ok_or_else(|| { - log::error!("Session not found: {}", session_id); - anyhow::anyhow!("Session not found") - })?; - log::debug!("Found session for: {}", session_id); - + self.run_turn(session_id, cx, |thread, cx| { let content: Vec = params .prompt .into_iter() @@ -672,99 +776,27 @@ impl acp_thread::AgentConnection for NativeAgentConnection { log::debug!("Message id: {:?}", id); log::debug!("Message content: {:?}", content); - // Get model using the ModelSelector capability (always available for agent2) - // Get the selected model from the thread directly - let model = thread.read_with(cx, |thread, _| thread.selected_model.clone())?; - - // Send to thread - log::info!("Sending message to thread with model: {:?}", model.name()); - let mut response_stream = - thread.update(cx, |thread, cx| thread.send(id, content, cx))?; - - // Handle response stream and forward to session.acp_thread - while let Some(result) = response_stream.next().await { - match result { - Ok(event) => { - log::trace!("Received completion event: {:?}", event); - - match event { - AgentResponseEvent::Text(text) => { - acp_thread.update(cx, |thread, cx| { - thread.push_assistant_content_block( - acp::ContentBlock::Text(acp::TextContent { - text, - annotations: None, - }), - false, - cx, - ) - })?; - } - AgentResponseEvent::Thinking(text) => { - acp_thread.update(cx, |thread, cx| { - thread.push_assistant_content_block( - acp::ContentBlock::Text(acp::TextContent { - text, - annotations: None, - }), - true, - cx, - ) - })?; - } - AgentResponseEvent::ToolCallAuthorization(ToolCallAuthorization { - tool_call, - options, - response, - }) => { - let recv = acp_thread.update(cx, |thread, cx| { - thread.request_tool_call_authorization(tool_call, options, cx) - })?; - cx.background_spawn(async move { - if let Some(option) = recv - .await - .context("authorization sender was dropped") - .log_err() - { - response - .send(option) - .map(|_| anyhow!("authorization receiver was dropped")) - .log_err(); - } - }) - .detach(); - } - AgentResponseEvent::ToolCall(tool_call) => { - acp_thread.update(cx, |thread, cx| { - thread.upsert_tool_call(tool_call, cx) - })?; - } - AgentResponseEvent::ToolCallUpdate(update) => { - acp_thread.update(cx, |thread, cx| { - thread.update_tool_call(update, cx) - })??; - } - AgentResponseEvent::Stop(stop_reason) => { - log::debug!("Assistant message complete: {:?}", stop_reason); - return Ok(acp::PromptResponse { stop_reason }); - } - } - } - Err(e) => { - log::error!("Error in model response stream: {:?}", e); - // TODO: Consider sending an error message to the UI - break; - } - } - } - - log::info!("Response stream completed"); - anyhow::Ok(acp::PromptResponse { - stop_reason: acp::StopReason::EndTurn, - }) + Ok(thread.update(cx, |thread, cx| { + log::info!( + "Sending message to thread with model: {:?}", + thread.model().name() + ); + thread.send(id, content, cx) + })) }) } + fn resume( + &self, + session_id: &acp::SessionId, + _cx: &mut App, + ) -> Option> { + Some(Rc::new(NativeAgentSessionResume { + connection: self.clone(), + session_id: session_id.clone(), + }) as _) + } + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { log::info!("Cancelling on session: {}", session_id); self.0.update(cx, |agent, cx| { @@ -786,6 +818,10 @@ impl acp_thread::AgentConnection for NativeAgentConnection { .map(|session| Rc::new(NativeAgentSessionEditor(session.thread.clone())) as _) }) } + + fn into_any(self: Rc) -> Rc { + self + } } struct NativeAgentSessionEditor(Entity); @@ -796,6 +832,20 @@ impl acp_thread::AgentSessionEditor for NativeAgentSessionEditor { } } +struct NativeAgentSessionResume { + connection: NativeAgentConnection, + session_id: acp::SessionId, +} + +impl acp_thread::AgentSessionResume for NativeAgentSessionResume { + fn run(&self, cx: &mut App) -> Task> { + self.connection + .run_turn(self.session_id.clone(), cx, |thread, cx| { + thread.update(cx, |thread, cx| thread.resume(cx)) + }) + } +} + #[cfg(test)] mod tests { use super::*; @@ -957,7 +1007,7 @@ mod tests { agent.read_with(cx, |agent, _| { let session = agent.sessions.get(&session_id).unwrap(); session.thread.read_with(cx, |thread, _| { - assert_eq!(thread.selected_model.id().0, "fake"); + assert_eq!(thread.model().id().0, "fake"); }); }); diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 1df664c029..cf90c8f650 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -12,9 +12,9 @@ use gpui::{ }; use indoc::indoc; use language_model::{ - LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, - LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolUse, Role, StopReason, - fake_provider::FakeLanguageModel, + LanguageModel, LanguageModelCompletionEvent, LanguageModelId, LanguageModelRegistry, + LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse, MessageContent, + Role, StopReason, fake_provider::FakeLanguageModel, }; use project::Project; use prompt_store::ProjectContext; @@ -394,8 +394,194 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) { assert_eq!(update.fields.status, Some(acp::ToolCallStatus::Failed)); } +#[gpui::test] +async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let events = thread.update(cx, |thread, cx| { + thread.add_tool(EchoTool); + thread.send(UserMessageId::new(), ["abc"], cx) + }); + cx.run_until_parked(); + let tool_use = LanguageModelToolUse { + id: "tool_id_1".into(), + name: EchoTool.name().into(), + raw_input: "{}".into(), + input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(), + is_input_complete: true, + }; + fake_model + .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone())); + fake_model.end_last_completion_stream(); + + cx.run_until_parked(); + let completion = fake_model.pending_completions().pop().unwrap(); + let tool_result = LanguageModelToolResult { + tool_use_id: "tool_id_1".into(), + tool_name: EchoTool.name().into(), + is_error: false, + content: "def".into(), + output: Some("def".into()), + }; + assert_eq!( + completion.messages[1..], + vec![ + LanguageModelRequestMessage { + role: Role::User, + content: vec!["abc".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::Assistant, + content: vec![MessageContent::ToolUse(tool_use.clone())], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec![MessageContent::ToolResult(tool_result.clone())], + cache: false + }, + ] + ); + + // Simulate reaching tool use limit. + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::StatusUpdate( + cloud_llm_client::CompletionRequestStatus::ToolUseLimitReached, + )); + fake_model.end_last_completion_stream(); + let last_event = events.collect::>().await.pop().unwrap(); + assert!( + last_event + .unwrap_err() + .is::() + ); + + let events = thread.update(cx, |thread, cx| thread.resume(cx)).unwrap(); + cx.run_until_parked(); + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!( + completion.messages[1..], + vec![ + LanguageModelRequestMessage { + role: Role::User, + content: vec!["abc".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::Assistant, + content: vec![MessageContent::ToolUse(tool_use)], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec![MessageContent::ToolResult(tool_result)], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec!["Continue where you left off".into()], + cache: false + } + ] + ); + + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text("Done".into())); + fake_model.end_last_completion_stream(); + events.collect::>().await; + thread.read_with(cx, |thread, _cx| { + assert_eq!( + thread.last_message().unwrap().to_markdown(), + indoc! {" + ## Assistant + + Done + "} + ) + }); + + // Ensure we error if calling resume when tool use limit was *not* reached. + let error = thread + .update(cx, |thread, cx| thread.resume(cx)) + .unwrap_err(); + assert_eq!( + error.to_string(), + "can only resume after tool use limit is reached" + ) +} + +#[gpui::test] +async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let events = thread.update(cx, |thread, cx| { + thread.add_tool(EchoTool); + thread.send(UserMessageId::new(), ["abc"], cx) + }); + cx.run_until_parked(); + + let tool_use = LanguageModelToolUse { + id: "tool_id_1".into(), + name: EchoTool.name().into(), + raw_input: "{}".into(), + input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(), + is_input_complete: true, + }; + let tool_result = LanguageModelToolResult { + tool_use_id: "tool_id_1".into(), + tool_name: EchoTool.name().into(), + is_error: false, + content: "def".into(), + output: Some("def".into()), + }; + fake_model + .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone())); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::StatusUpdate( + cloud_llm_client::CompletionRequestStatus::ToolUseLimitReached, + )); + fake_model.end_last_completion_stream(); + let last_event = events.collect::>().await.pop().unwrap(); + assert!( + last_event + .unwrap_err() + .is::() + ); + + thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), vec!["ghi"], cx) + }); + cx.run_until_parked(); + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!( + completion.messages[1..], + vec![ + LanguageModelRequestMessage { + role: Role::User, + content: vec!["abc".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::Assistant, + content: vec![MessageContent::ToolUse(tool_use)], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec![MessageContent::ToolResult(tool_result)], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec!["ghi".into()], + cache: false + } + ] + ); +} + async fn expect_tool_call( - events: &mut UnboundedReceiver>, + events: &mut UnboundedReceiver>, ) -> acp::ToolCall { let event = events .next() @@ -411,7 +597,7 @@ async fn expect_tool_call( } async fn expect_tool_call_update_fields( - events: &mut UnboundedReceiver>, + events: &mut UnboundedReceiver>, ) -> acp::ToolCallUpdate { let event = events .next() @@ -429,7 +615,7 @@ async fn expect_tool_call_update_fields( } async fn next_tool_call_authorization( - events: &mut UnboundedReceiver>, + events: &mut UnboundedReceiver>, ) -> ToolCallAuthorization { loop { let event = events @@ -1007,9 +1193,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) { } /// Filters out the stop events for asserting against in tests -fn stop_events( - result_events: Vec>, -) -> Vec { +fn stop_events(result_events: Vec>) -> Vec { result_events .into_iter() .filter_map(|event| match event.unwrap() { diff --git a/crates/agent2/src/tests/test_tools.rs b/crates/agent2/src/tests/test_tools.rs index 7c7b81f52f..cbff44cedf 100644 --- a/crates/agent2/src/tests/test_tools.rs +++ b/crates/agent2/src/tests/test_tools.rs @@ -7,7 +7,7 @@ use std::future; #[derive(JsonSchema, Serialize, Deserialize)] pub struct EchoToolInput { /// The text to echo. - text: String, + pub text: String, } pub struct EchoTool; diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 260aaaf550..231ee92dda 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -2,10 +2,10 @@ use crate::{ContextServerRegistry, SystemPromptTemplate, Template, Templates}; use acp_thread::{MentionUri, UserMessageId}; use action_log::ActionLog; use agent_client_protocol as acp; -use agent_settings::{AgentProfileId, AgentSettings}; +use agent_settings::{AgentProfileId, AgentSettings, CompletionMode}; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::adapt_schema_to_format; -use cloud_llm_client::{CompletionIntent, CompletionMode}; +use cloud_llm_client::{CompletionIntent, CompletionRequestStatus}; use collections::IndexMap; use fs::Fs; use futures::{ @@ -14,10 +14,10 @@ use futures::{ }; use gpui::{App, Context, Entity, SharedString, Task}; use language_model::{ - LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage, - LanguageModelProviderId, LanguageModelRequest, LanguageModelRequestMessage, - LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent, - LanguageModelToolSchemaFormat, LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason, + LanguageModel, LanguageModelCompletionEvent, LanguageModelImage, LanguageModelProviderId, + LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, + LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, + LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason, }; use project::Project; use prompt_store::ProjectContext; @@ -33,6 +33,7 @@ use util::{ResultExt, markdown::MarkdownCodeBlock}; pub enum Message { User(UserMessage), Agent(AgentMessage), + Resume, } impl Message { @@ -47,6 +48,7 @@ impl Message { match self { Message::User(message) => message.to_markdown(), Message::Agent(message) => message.to_markdown(), + Message::Resume => "[resumed after tool use limit was reached]".into(), } } } @@ -320,7 +322,11 @@ impl AgentMessage { } pub fn to_request(&self) -> Vec { - let mut content = Vec::with_capacity(self.content.len()); + let mut assistant_message = LanguageModelRequestMessage { + role: Role::Assistant, + content: Vec::with_capacity(self.content.len()), + cache: false, + }; for chunk in &self.content { let chunk = match chunk { AgentMessageContent::Text(text) => { @@ -342,29 +348,30 @@ impl AgentMessage { language_model::MessageContent::Image(value.clone()) } }; - content.push(chunk); + assistant_message.content.push(chunk); } - let mut messages = vec![LanguageModelRequestMessage { - role: Role::Assistant, - content, + let mut user_message = LanguageModelRequestMessage { + role: Role::User, + content: Vec::new(), cache: false, - }]; + }; - if !self.tool_results.is_empty() { - let mut tool_results = Vec::with_capacity(self.tool_results.len()); - for tool_result in self.tool_results.values() { - tool_results.push(language_model::MessageContent::ToolResult( + for tool_result in self.tool_results.values() { + user_message + .content + .push(language_model::MessageContent::ToolResult( tool_result.clone(), )); - } - messages.push(LanguageModelRequestMessage { - role: Role::User, - content: tool_results, - cache: false, - }); } + let mut messages = Vec::new(); + if !assistant_message.content.is_empty() { + messages.push(assistant_message); + } + if !user_message.content.is_empty() { + messages.push(user_message); + } messages } } @@ -413,11 +420,12 @@ pub struct Thread { running_turn: Option>, pending_message: Option, tools: BTreeMap>, + tool_use_limit_reached: bool, context_server_registry: Entity, profile_id: AgentProfileId, project_context: Rc>, templates: Arc, - pub selected_model: Arc, + model: Arc, project: Entity, action_log: Entity, } @@ -429,7 +437,7 @@ impl Thread { context_server_registry: Entity, action_log: Entity, templates: Arc, - default_model: Arc, + model: Arc, cx: &mut Context, ) -> Self { let profile_id = AgentSettings::get_global(cx).default_profile.clone(); @@ -439,11 +447,12 @@ impl Thread { running_turn: None, pending_message: None, tools: BTreeMap::default(), + tool_use_limit_reached: false, context_server_registry, profile_id, project_context, templates, - selected_model: default_model, + model, project, action_log, } @@ -457,7 +466,19 @@ impl Thread { &self.action_log } - pub fn set_mode(&mut self, mode: CompletionMode) { + pub fn model(&self) -> &Arc { + &self.model + } + + pub fn set_model(&mut self, model: Arc) { + self.model = model; + } + + pub fn completion_mode(&self) -> CompletionMode { + self.completion_mode + } + + pub fn set_completion_mode(&mut self, mode: CompletionMode) { self.completion_mode = mode; } @@ -499,36 +520,59 @@ impl Thread { Ok(()) } + pub fn resume( + &mut self, + cx: &mut Context, + ) -> Result>> { + anyhow::ensure!( + self.tool_use_limit_reached, + "can only resume after tool use limit is reached" + ); + + self.messages.push(Message::Resume); + cx.notify(); + + log::info!("Total messages in thread: {}", self.messages.len()); + Ok(self.run_turn(cx)) + } + /// Sending a message results in the model streaming a response, which could include tool calls. /// After calling tools, the model will stops and waits for any outstanding tool calls to be completed and their results sent. /// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn. pub fn send( &mut self, - message_id: UserMessageId, + id: UserMessageId, content: impl IntoIterator, cx: &mut Context, - ) -> mpsc::UnboundedReceiver> + ) -> mpsc::UnboundedReceiver> where T: Into, { - let model = self.selected_model.clone(); + log::info!("Thread::send called with model: {:?}", self.model.name()); + let content = content.into_iter().map(Into::into).collect::>(); - log::info!("Thread::send called with model: {:?}", model.name()); log::debug!("Thread::send content: {:?}", content); + self.messages + .push(Message::User(UserMessage { id, content })); cx.notify(); - let (events_tx, events_rx) = - mpsc::unbounded::>(); - let event_stream = AgentResponseEventStream(events_tx); - self.messages.push(Message::User(UserMessage { - id: message_id.clone(), - content, - })); log::info!("Total messages in thread: {}", self.messages.len()); + self.run_turn(cx) + } + + fn run_turn( + &mut self, + cx: &mut Context, + ) -> mpsc::UnboundedReceiver> { + let model = self.model.clone(); + let (events_tx, events_rx) = mpsc::unbounded::>(); + let event_stream = AgentResponseEventStream(events_tx); + let message_ix = self.messages.len().saturating_sub(1); + self.tool_use_limit_reached = false; self.running_turn = Some(cx.spawn(async move |this, cx| { log::info!("Starting agent turn execution"); - let turn_result = async { + let turn_result: Result<()> = async { let mut completion_intent = CompletionIntent::UserPrompt; loop { log::debug!( @@ -543,13 +587,22 @@ impl Thread { let mut events = model.stream_completion(request, cx).await?; log::debug!("Stream completion started successfully"); + let mut tool_use_limit_reached = false; let mut tool_uses = FuturesUnordered::new(); while let Some(event) = events.next().await { match event? { + LanguageModelCompletionEvent::StatusUpdate( + CompletionRequestStatus::ToolUseLimitReached, + ) => { + tool_use_limit_reached = true; + } LanguageModelCompletionEvent::Stop(reason) => { event_stream.send_stop(reason); if reason == StopReason::Refusal { - this.update(cx, |this, _cx| this.truncate(message_id))??; + this.update(cx, |this, _cx| { + this.flush_pending_message(); + this.messages.truncate(message_ix); + })?; return Ok(()); } } @@ -567,12 +620,7 @@ impl Thread { } } - if tool_uses.is_empty() { - log::info!("No tool uses found, completing turn"); - return Ok(()); - } - log::info!("Found {} tool uses to execute", tool_uses.len()); - + let used_tools = tool_uses.is_empty(); while let Some(tool_result) = tool_uses.next().await { log::info!("Tool finished {:?}", tool_result); @@ -596,8 +644,17 @@ impl Thread { .ok(); } - this.update(cx, |this, _| this.flush_pending_message())?; - completion_intent = CompletionIntent::ToolResults; + if tool_use_limit_reached { + log::info!("Tool use limit reached, completing turn"); + this.update(cx, |this, _cx| this.tool_use_limit_reached = true)?; + return Err(language_model::ToolUseLimitReachedError.into()); + } else if used_tools { + log::info!("No tool uses found, completing turn"); + return Ok(()); + } else { + this.update(cx, |this, _| this.flush_pending_message())?; + completion_intent = CompletionIntent::ToolResults; + } } } .await; @@ -678,10 +735,10 @@ impl Thread { fn handle_text_event( &mut self, new_text: String, - events_stream: &AgentResponseEventStream, + event_stream: &AgentResponseEventStream, cx: &mut Context, ) { - events_stream.send_text(&new_text); + event_stream.send_text(&new_text); let last_message = self.pending_message(); if let Some(AgentMessageContent::Text(text)) = last_message.content.last_mut() { @@ -798,8 +855,9 @@ impl Thread { status: Some(acp::ToolCallStatus::InProgress), ..Default::default() }); - let supports_images = self.selected_model.supports_images(); + let supports_images = self.model.supports_images(); let tool_result = tool.run(tool_use.input, tool_event_stream, cx); + log::info!("Running tool {}", tool_use.name); Some(cx.foreground_executor().spawn(async move { let tool_result = tool_result.await.and_then(|output| { if let LanguageModelToolResultContent::Image(_) = &output.llm_output { @@ -902,7 +960,7 @@ impl Thread { name: tool_name, description: tool.description().to_string(), input_schema: tool - .input_schema(self.selected_model.tool_input_format()) + .input_schema(self.model.tool_input_format()) .log_err()?, }) }) @@ -917,7 +975,7 @@ impl Thread { thread_id: None, prompt_id: None, intent: Some(completion_intent), - mode: Some(self.completion_mode), + mode: Some(self.completion_mode.into()), messages, tools, tool_choice: None, @@ -935,7 +993,7 @@ impl Thread { .profiles .get(&self.profile_id) .context("profile not found")?; - let provider_id = self.selected_model.provider_id(); + let provider_id = self.model.provider_id(); Ok(self .tools @@ -971,6 +1029,11 @@ impl Thread { match message { Message::User(message) => messages.push(message.to_request()), Message::Agent(message) => messages.extend(message.to_request()), + Message::Resume => messages.push(LanguageModelRequestMessage { + role: Role::User, + content: vec!["Continue where you left off".into()], + cache: false, + }), } } @@ -1123,9 +1186,7 @@ where } #[derive(Clone)] -struct AgentResponseEventStream( - mpsc::UnboundedSender>, -); +struct AgentResponseEventStream(mpsc::UnboundedSender>); impl AgentResponseEventStream { fn send_text(&self, text: &str) { @@ -1212,8 +1273,8 @@ impl AgentResponseEventStream { } } - fn send_error(&self, error: LanguageModelCompletionError) { - self.0.unbounded_send(Err(error)).ok(); + fn send_error(&self, error: impl Into) { + self.0.unbounded_send(Err(error.into())).ok(); } } @@ -1229,8 +1290,7 @@ pub struct ToolCallEventStream { impl ToolCallEventStream { #[cfg(test)] pub fn test() -> (Self, ToolCallEventStreamReceiver) { - let (events_tx, events_rx) = - mpsc::unbounded::>(); + let (events_tx, events_rx) = mpsc::unbounded::>(); let stream = ToolCallEventStream::new( &LanguageModelToolUse { @@ -1351,9 +1411,7 @@ impl ToolCallEventStream { } #[cfg(test)] -pub struct ToolCallEventStreamReceiver( - mpsc::UnboundedReceiver>, -); +pub struct ToolCallEventStreamReceiver(mpsc::UnboundedReceiver>); #[cfg(test)] impl ToolCallEventStreamReceiver { @@ -1381,7 +1439,7 @@ impl ToolCallEventStreamReceiver { #[cfg(test)] impl std::ops::Deref for ToolCallEventStreamReceiver { - type Target = mpsc::UnboundedReceiver>; + type Target = mpsc::UnboundedReceiver>; fn deref(&self) -> &Self::Target { &self.0 diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 405afb585f..c77b9f6a69 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -241,7 +241,7 @@ impl AgentTool for EditFileTool { thread.build_completion_request(CompletionIntent::ToolResults, cx) }); let thread = self.thread.read(cx); - let model = thread.selected_model.clone(); + let model = thread.model().clone(); let action_log = thread.action_log().clone(); let authorize = self.authorize(&input, &event_stream, cx); diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index 15f8635cde..e936c87643 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -5,7 +5,7 @@ use anyhow::{Context as _, Result, anyhow}; use futures::channel::oneshot; use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity}; use project::Project; -use std::{cell::RefCell, path::Path, rc::Rc}; +use std::{any::Any, cell::RefCell, path::Path, rc::Rc}; use ui::App; use util::ResultExt as _; @@ -507,4 +507,8 @@ impl AgentConnection for AcpConnection { }) .detach_and_log_err(cx) } + + fn into_any(self: Rc) -> Rc { + self + } } diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index d93e3d023e..36511e4644 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -3,9 +3,9 @@ use anyhow::anyhow; use collections::HashMap; use futures::channel::oneshot; use project::Project; -use std::cell::RefCell; use std::path::Path; use std::rc::Rc; +use std::{any::Any, cell::RefCell}; use anyhow::{Context as _, Result}; use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity}; @@ -191,6 +191,10 @@ impl AgentConnection for AcpConnection { .spawn(async move { conn.cancel(params).await }) .detach(); } + + fn into_any(self: Rc) -> Rc { + self + } } struct ClientDelegate { diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index dbcda00e48..e1cc709289 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -6,6 +6,7 @@ use context_server::listener::McpServerTool; use project::Project; use settings::SettingsStore; use smol::process::Child; +use std::any::Any; use std::cell::RefCell; use std::fmt::Display; use std::path::Path; @@ -289,6 +290,10 @@ impl AgentConnection for ClaudeAgentConnection { }) .log_err(); } + + fn into_any(self: Rc) -> Rc { + self + } } #[derive(Clone, Copy)] diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index ee016b7503..87af75f046 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -7,20 +7,21 @@ use action_log::ActionLog; use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::{self as acp}; use agent_servers::AgentServer; -use agent_settings::{AgentSettings, NotifyWhenAgentWaiting}; +use agent_settings::{AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; use anyhow::bail; use audio::{Audio, Sound}; use buffer_diff::BufferDiff; +use client::zed_urls; use collections::{HashMap, HashSet}; use editor::scroll::Autoscroll; use editor::{Editor, EditorMode, MultiBuffer, PathKey, SelectionEffects}; use file_icons::FileIcons; use gpui::{ - Action, Animation, AnimationExt, App, BorderStyle, ClickEvent, EdgesRefinement, Empty, Entity, - FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, PlatformDisplay, - SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, - Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop, - linear_gradient, list, percentage, point, prelude::*, pulsating_between, + Action, Animation, AnimationExt, App, BorderStyle, ClickEvent, ClipboardItem, EdgesRefinement, + Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, + PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, + TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, + linear_color_stop, linear_gradient, list, percentage, point, prelude::*, pulsating_between, }; use language::Buffer; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; @@ -32,8 +33,8 @@ use std::{collections::BTreeMap, process::ExitStatus, rc::Rc, time::Duration}; use text::Anchor; use theme::ThemeSettings; use ui::{ - Disclosure, Divider, DividerColor, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, - Tooltip, prelude::*, + Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle, + Scrollbar, ScrollbarState, Tooltip, prelude::*, }; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, Workspace}; @@ -44,16 +45,39 @@ use super::entry_view_state::EntryViewState; use crate::acp::AcpModelSelectorPopover; use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; use crate::agent_diff::AgentDiff; -use crate::ui::{AgentNotification, AgentNotificationEvent}; +use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip}; use crate::{ - AgentDiffPane, AgentPanel, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll, + AgentDiffPane, AgentPanel, ContinueThread, ContinueWithBurnMode, ExpandMessageEditor, Follow, + KeepAll, OpenAgentDiff, RejectAll, ToggleBurnMode, }; const RESPONSE_PADDING_X: Pixels = px(19.); - pub const MIN_EDITOR_LINES: usize = 4; pub const MAX_EDITOR_LINES: usize = 8; +enum ThreadError { + PaymentRequired, + ModelRequestLimitReached(cloud_llm_client::Plan), + ToolUseLimitReached, + Other(SharedString), +} + +impl ThreadError { + fn from_err(error: anyhow::Error) -> Self { + if error.is::() { + Self::PaymentRequired + } else if error.is::() { + Self::ToolUseLimitReached + } else if let Some(error) = + error.downcast_ref::() + { + Self::ModelRequestLimitReached(error.plan) + } else { + Self::Other(error.to_string().into()) + } + } +} + pub struct AcpThreadView { agent: Rc, workspace: WeakEntity, @@ -66,7 +90,7 @@ pub struct AcpThreadView { model_selector: Option>, notifications: Vec>, notification_subscriptions: HashMap, Vec>, - last_error: Option>, + thread_error: Option, list_state: ListState, scrollbar_state: ScrollbarState, auth_task: Option>, @@ -151,7 +175,7 @@ impl AcpThreadView { entry_view_state: EntryViewState::default(), list_state: list_state.clone(), scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), - last_error: None, + thread_error: None, auth_task: None, expanded_tool_calls: HashSet::default(), expanded_thinking_blocks: HashSet::default(), @@ -316,7 +340,7 @@ impl AcpThreadView { } pub fn cancel_generation(&mut self, cx: &mut Context) { - self.last_error.take(); + self.thread_error.take(); if let Some(thread) = self.thread() { self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx))); @@ -371,6 +395,25 @@ impl AcpThreadView { } } + fn resume_chat(&mut self, cx: &mut Context) { + self.thread_error.take(); + let Some(thread) = self.thread() else { + return; + }; + + let task = thread.update(cx, |thread, cx| thread.resume(cx)); + cx.spawn(async move |this, cx| { + let result = task.await; + + this.update(cx, |this, cx| { + if let Err(err) = result { + this.handle_thread_error(err, cx); + } + }) + }) + .detach(); + } + fn send(&mut self, window: &mut Window, cx: &mut Context) { let contents = self .message_editor @@ -384,7 +427,7 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) { - self.last_error.take(); + self.thread_error.take(); self.editing_message.take(); let Some(thread) = self.thread().cloned() else { @@ -409,11 +452,9 @@ impl AcpThreadView { }); cx.spawn(async move |this, cx| { - if let Err(e) = task.await { + if let Err(err) = task.await { this.update(cx, |this, cx| { - this.last_error = - Some(cx.new(|cx| Markdown::new(e.to_string().into(), None, None, cx))); - cx.notify() + this.handle_thread_error(err, cx); }) .ok(); } @@ -476,6 +517,16 @@ impl AcpThreadView { }) } + fn handle_thread_error(&mut self, error: anyhow::Error, cx: &mut Context) { + self.thread_error = Some(ThreadError::from_err(error)); + cx.notify(); + } + + fn clear_thread_error(&mut self, cx: &mut Context) { + self.thread_error = None; + cx.notify(); + } + fn handle_thread_event( &mut self, thread: &Entity, @@ -551,7 +602,7 @@ impl AcpThreadView { return; }; - self.last_error.take(); + self.thread_error.take(); let authenticate = connection.authenticate(method, cx); self.auth_task = Some(cx.spawn_in(window, { let project = self.project.clone(); @@ -561,9 +612,7 @@ impl AcpThreadView { this.update_in(cx, |this, window, cx| { if let Err(err) = result { - this.last_error = Some(cx.new(|cx| { - Markdown::new(format!("Error: {err}").into(), None, None, cx) - })) + this.handle_thread_error(err, cx); } else { this.thread_state = Self::initial_state( agent, @@ -620,9 +669,7 @@ impl AcpThreadView { .py_4() .px_2() .children(message.id.clone().and_then(|message_id| { - message.checkpoint.as_ref()?; - - Some( + message.checkpoint.as_ref()?.show.then(|| { Button::new("restore-checkpoint", "Restore Checkpoint") .icon(IconName::Undo) .icon_size(IconSize::XSmall) @@ -630,8 +677,8 @@ impl AcpThreadView { .label_size(LabelSize::XSmall) .on_click(cx.listener(move |this, _, _window, cx| { this.rewind(&message_id, cx); - })), - ) + })) + }) })) .child( v_flex() @@ -2322,7 +2369,12 @@ impl AcpThreadView { h_flex() .flex_none() .justify_between() - .child(self.render_follow_toggle(cx)) + .child( + h_flex() + .gap_1() + .child(self.render_follow_toggle(cx)) + .children(self.render_burn_mode_toggle(cx)), + ) .child( h_flex() .gap_1() @@ -2333,6 +2385,68 @@ impl AcpThreadView { .into_any() } + fn as_native_connection(&self, cx: &App) -> Option> { + let acp_thread = self.thread()?.read(cx); + acp_thread.connection().clone().downcast() + } + + fn as_native_thread(&self, cx: &App) -> Option> { + let acp_thread = self.thread()?.read(cx); + self.as_native_connection(cx)? + .thread(acp_thread.session_id(), cx) + } + + fn toggle_burn_mode( + &mut self, + _: &ToggleBurnMode, + _window: &mut Window, + cx: &mut Context, + ) { + let Some(thread) = self.as_native_thread(cx) else { + return; + }; + + thread.update(cx, |thread, _cx| { + let current_mode = thread.completion_mode(); + thread.set_completion_mode(match current_mode { + CompletionMode::Burn => CompletionMode::Normal, + CompletionMode::Normal => CompletionMode::Burn, + }); + }); + } + + fn render_burn_mode_toggle(&self, cx: &mut Context) -> Option { + let thread = self.as_native_thread(cx)?.read(cx); + + if !thread.model().supports_burn_mode() { + return None; + } + + let active_completion_mode = thread.completion_mode(); + let burn_mode_enabled = active_completion_mode == CompletionMode::Burn; + let icon = if burn_mode_enabled { + IconName::ZedBurnModeOn + } else { + IconName::ZedBurnMode + }; + + Some( + IconButton::new("burn-mode", icon) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .toggle_state(burn_mode_enabled) + .selected_icon_color(Color::Error) + .on_click(cx.listener(|this, _event, window, cx| { + this.toggle_burn_mode(&ToggleBurnMode, window, cx); + })) + .tooltip(move |_window, cx| { + cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled)) + .into() + }) + .into_any_element(), + ) + } + fn start_editing_message(&mut self, index: usize, window: &mut Window, cx: &mut Context) { let Some(thread) = self.thread() else { return; @@ -3002,6 +3116,187 @@ impl AcpThreadView { } } +impl AcpThreadView { + fn render_thread_error(&self, window: &mut Window, cx: &mut Context<'_, Self>) -> Option
{ + let content = match self.thread_error.as_ref()? { + ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx), + ThreadError::PaymentRequired => self.render_payment_required_error(cx), + ThreadError::ModelRequestLimitReached(plan) => { + self.render_model_request_limit_reached_error(*plan, cx) + } + ThreadError::ToolUseLimitReached => { + self.render_tool_use_limit_reached_error(window, cx)? + } + }; + + Some( + div() + .border_t_1() + .border_color(cx.theme().colors().border) + .child(content), + ) + } + + fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout { + let icon = Icon::new(IconName::XCircle) + .size(IconSize::Small) + .color(Color::Error); + + Callout::new() + .icon(icon) + .title("Error") + .description(error.clone()) + .secondary_action(self.create_copy_button(error.to_string())) + .primary_action(self.dismiss_error_button(cx)) + .bg_color(self.error_callout_bg(cx)) + } + + fn render_payment_required_error(&self, cx: &mut Context) -> Callout { + const ERROR_MESSAGE: &str = + "You reached your free usage limit. Upgrade to Zed Pro for more prompts."; + + let icon = Icon::new(IconName::XCircle) + .size(IconSize::Small) + .color(Color::Error); + + Callout::new() + .icon(icon) + .title("Free Usage Exceeded") + .description(ERROR_MESSAGE) + .tertiary_action(self.upgrade_button(cx)) + .secondary_action(self.create_copy_button(ERROR_MESSAGE)) + .primary_action(self.dismiss_error_button(cx)) + .bg_color(self.error_callout_bg(cx)) + } + + fn render_model_request_limit_reached_error( + &self, + plan: cloud_llm_client::Plan, + cx: &mut Context, + ) -> Callout { + let error_message = match plan { + cloud_llm_client::Plan::ZedPro => "Upgrade to usage-based billing for more prompts.", + cloud_llm_client::Plan::ZedProTrial | cloud_llm_client::Plan::ZedFree => { + "Upgrade to Zed Pro for more prompts." + } + }; + + let icon = Icon::new(IconName::XCircle) + .size(IconSize::Small) + .color(Color::Error); + + Callout::new() + .icon(icon) + .title("Model Prompt Limit Reached") + .description(error_message) + .tertiary_action(self.upgrade_button(cx)) + .secondary_action(self.create_copy_button(error_message)) + .primary_action(self.dismiss_error_button(cx)) + .bg_color(self.error_callout_bg(cx)) + } + + fn render_tool_use_limit_reached_error( + &self, + window: &mut Window, + cx: &mut Context, + ) -> Option { + let thread = self.as_native_thread(cx)?; + let supports_burn_mode = thread.read(cx).model().supports_burn_mode(); + + let focus_handle = self.focus_handle(cx); + + let icon = Icon::new(IconName::Info) + .size(IconSize::Small) + .color(Color::Info); + + Some( + Callout::new() + .icon(icon) + .title("Consecutive tool use limit reached.") + .when(supports_burn_mode, |this| { + this.secondary_action( + Button::new("continue-burn-mode", "Continue with Burn Mode") + .style(ButtonStyle::Filled) + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .layer(ElevationIndex::ModalSurface) + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in( + &ContinueWithBurnMode, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(10.))), + ) + .tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use.")) + .on_click({ + cx.listener(move |this, _, _window, cx| { + thread.update(cx, |thread, _cx| { + thread.set_completion_mode(CompletionMode::Burn); + }); + this.resume_chat(cx); + }) + }), + ) + }) + .primary_action( + Button::new("continue-conversation", "Continue") + .layer(ElevationIndex::ModalSurface) + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in(&ContinueThread, &focus_handle, window, cx) + .map(|kb| kb.size(rems_from_px(10.))), + ) + .on_click(cx.listener(|this, _, _window, cx| { + this.resume_chat(cx); + })), + ), + ) + } + + fn create_copy_button(&self, message: impl Into) -> impl IntoElement { + let message = message.into(); + + IconButton::new("copy", IconName::Copy) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .tooltip(Tooltip::text("Copy Error Message")) + .on_click(move |_, _, cx| { + cx.write_to_clipboard(ClipboardItem::new_string(message.clone())) + }) + } + + fn dismiss_error_button(&self, cx: &mut Context) -> impl IntoElement { + IconButton::new("dismiss", IconName::Close) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .tooltip(Tooltip::text("Dismiss Error")) + .on_click(cx.listener({ + move |this, _, _, cx| { + this.clear_thread_error(cx); + cx.notify(); + } + })) + } + + fn upgrade_button(&self, cx: &mut Context) -> impl IntoElement { + Button::new("upgrade", "Upgrade") + .label_size(LabelSize::Small) + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .on_click(cx.listener({ + move |this, _, _, cx| { + this.clear_thread_error(cx); + cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)); + } + })) + } + + fn error_callout_bg(&self, cx: &Context) -> Hsla { + cx.theme().status().error.opacity(0.08) + } +} + impl Focusable for AcpThreadView { fn focus_handle(&self, cx: &App) -> FocusHandle { self.message_editor.focus_handle(cx) @@ -3016,6 +3311,7 @@ impl Render for AcpThreadView { .size_full() .key_context("AcpThread") .on_action(cx.listener(Self::open_agent_diff)) + .on_action(cx.listener(Self::toggle_burn_mode)) .bg(cx.theme().colors().panel_background) .child(match &self.thread_state { ThreadState::Unauthenticated { connection } => v_flex() @@ -3100,19 +3396,7 @@ impl Render for AcpThreadView { } _ => this, }) - .when_some(self.last_error.clone(), |el, error| { - el.child( - div() - .p_2() - .text_xs() - .border_t_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().status().error_background) - .child( - self.render_markdown(error, default_markdown_style(false, window, cx)), - ), - ) - }) + .children(self.render_thread_error(window, cx)) .child(self.render_message_editor(window, cx)) } } @@ -3299,8 +3583,6 @@ fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { #[cfg(test)] pub(crate) mod tests { - use std::path::Path; - use acp_thread::StubAgentConnection; use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::SessionId; @@ -3310,6 +3592,8 @@ pub(crate) mod tests { use project::Project; use serde_json::json; use settings::SettingsStore; + use std::any::Any; + use std::path::Path; use super::*; @@ -3547,6 +3831,10 @@ pub(crate) mod tests { fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) { unimplemented!() } + + fn into_any(self: Rc) -> Rc { + self + } } pub(crate) fn init_test(cx: &mut TestAppContext) { diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 231b9cfb38..4f5f022593 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -5,7 +5,6 @@ mod agent_diff; mod agent_model_selector; mod agent_panel; mod buffer_codegen; -mod burn_mode_tooltip; mod context_picker; mod context_server_configuration; mod context_strip; diff --git a/crates/agent_ui/src/burn_mode_tooltip.rs b/crates/agent_ui/src/burn_mode_tooltip.rs deleted file mode 100644 index 6354c07760..0000000000 --- a/crates/agent_ui/src/burn_mode_tooltip.rs +++ /dev/null @@ -1,61 +0,0 @@ -use gpui::{Context, FontWeight, IntoElement, Render, Window}; -use ui::{prelude::*, tooltip_container}; - -pub struct BurnModeTooltip { - selected: bool, -} - -impl BurnModeTooltip { - pub fn new() -> Self { - Self { selected: false } - } - - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -impl Render for BurnModeTooltip { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let (icon, color) = if self.selected { - (IconName::ZedBurnModeOn, Color::Error) - } else { - (IconName::ZedBurnMode, Color::Default) - }; - - let turned_on = h_flex() - .h_4() - .px_1() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().text_accent.opacity(0.1)) - .rounded_sm() - .child( - Label::new("ON") - .size(LabelSize::XSmall) - .weight(FontWeight::SEMIBOLD) - .color(Color::Accent), - ); - - let title = h_flex() - .gap_1p5() - .child(Icon::new(icon).size(IconSize::Small).color(color)) - .child(Label::new("Burn Mode")) - .when(self.selected, |title| title.child(turned_on)); - - tooltip_container(window, cx, |this, _, _| { - this - .child(title) - .child( - div() - .max_w_64() - .child( - Label::new("Enables models to use large context windows, unlimited tool calls, and other capabilities for expanded reasoning.") - .size(LabelSize::Small) - .color(Color::Muted) - ) - ) - }) - } -} diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 4b6d51c4c1..5d094811f1 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -6,7 +6,7 @@ use crate::agent_diff::AgentDiffThread; use crate::agent_model_selector::AgentModelSelector; use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip}; use crate::ui::{ - MaxModeTooltip, + BurnModeTooltip, preview::{AgentPreview, UsageCallout}, }; use agent::history_store::HistoryStore; @@ -605,7 +605,7 @@ impl MessageEditor { this.toggle_burn_mode(&ToggleBurnMode, window, cx); })) .tooltip(move |_window, cx| { - cx.new(|_| MaxModeTooltip::new().selected(burn_mode_enabled)) + cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled)) .into() }) .into_any_element(), diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 49a37002f7..2e3b4ed890 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -1,6 +1,6 @@ use crate::{ - burn_mode_tooltip::BurnModeTooltip, language_model_selector::{LanguageModelSelector, language_model_selector}, + ui::BurnModeTooltip, }; use agent_settings::{AgentSettings, CompletionMode}; use anyhow::Result; diff --git a/crates/agent_ui/src/ui/burn_mode_tooltip.rs b/crates/agent_ui/src/ui/burn_mode_tooltip.rs index 97f7853a61..72faaa614d 100644 --- a/crates/agent_ui/src/ui/burn_mode_tooltip.rs +++ b/crates/agent_ui/src/ui/burn_mode_tooltip.rs @@ -2,11 +2,11 @@ use crate::ToggleBurnMode; use gpui::{Context, FontWeight, IntoElement, Render, Window}; use ui::{KeyBinding, prelude::*, tooltip_container}; -pub struct MaxModeTooltip { +pub struct BurnModeTooltip { selected: bool, } -impl MaxModeTooltip { +impl BurnModeTooltip { pub fn new() -> Self { Self { selected: false } } @@ -17,7 +17,7 @@ impl MaxModeTooltip { } } -impl Render for MaxModeTooltip { +impl Render for BurnModeTooltip { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let (icon, color) = if self.selected { (IconName::ZedBurnModeOn, Color::Error) diff --git a/crates/language_model/src/model/cloud_model.rs b/crates/language_model/src/model/cloud_model.rs index 3b4c1fa269..0e10050dae 100644 --- a/crates/language_model/src/model/cloud_model.rs +++ b/crates/language_model/src/model/cloud_model.rs @@ -42,6 +42,18 @@ impl fmt::Display for ModelRequestLimitReachedError { } } +#[derive(Error, Debug)] +pub struct ToolUseLimitReachedError; + +impl fmt::Display for ToolUseLimitReachedError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Consecutive tool use limit reached. Enable Burn Mode for unlimited tool use." + ) + } +} + #[derive(Clone, Default)] pub struct LlmApiToken(Arc>>); From 708c434bd4c21614a282fc31ae35d3e9414ec2c9 Mon Sep 17 00:00:00 2001 From: Daniel Sauble Date: Fri, 15 Aug 2025 04:43:29 -0700 Subject: [PATCH 368/693] workspace: Highlight where dragged tab will be dropped (#34740) Closes #18565 I could use some advice on the color palette / theming. A couple options: 1. The `drop_target_background` color could be used for the border if we didn't use it for the background of the tab. In VSCode, the background color of tabs doesn't change as you're dragging, there's just a border between tabs. My only concern with this option is that the current `drop_target_background` color is a bit subtle when used for a small area like a border. 2. Another option could be to add a `drop_target_border` theme color, but I don't know how much complexity this adds to implementation (presumably all existing themes would need to be updated?). Demo: https://github.com/user-attachments/assets/0b7c04ea-5ec5-4b45-adad-156dfbf552db Release Notes: - Highlight where a dragged tab will be dropped between two other tabs --------- Co-authored-by: Smit Barmase --- crates/theme/src/default_colors.rs | 2 ++ crates/theme/src/fallback_themes.rs | 1 + crates/theme/src/schema.rs | 8 ++++++++ crates/theme/src/styles/colors.rs | 4 ++++ crates/workspace/src/pane.rs | 15 +++++++++++++-- 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/crates/theme/src/default_colors.rs b/crates/theme/src/default_colors.rs index 1c3f48b548..051b7acf10 100644 --- a/crates/theme/src/default_colors.rs +++ b/crates/theme/src/default_colors.rs @@ -54,6 +54,7 @@ impl ThemeColors { element_disabled: neutral().light_alpha().step_3(), element_selection_background: blue().light().step_3().alpha(0.25), drop_target_background: blue().light_alpha().step_2(), + drop_target_border: neutral().light().step_12(), ghost_element_background: system.transparent, ghost_element_hover: neutral().light_alpha().step_3(), ghost_element_active: neutral().light_alpha().step_4(), @@ -179,6 +180,7 @@ impl ThemeColors { element_disabled: neutral().dark_alpha().step_3(), element_selection_background: blue().dark().step_3().alpha(0.25), drop_target_background: blue().dark_alpha().step_2(), + drop_target_border: neutral().dark().step_12(), ghost_element_background: system.transparent, ghost_element_hover: neutral().dark_alpha().step_4(), ghost_element_active: neutral().dark_alpha().step_5(), diff --git a/crates/theme/src/fallback_themes.rs b/crates/theme/src/fallback_themes.rs index 4d77dd5d81..e9e8e2d0db 100644 --- a/crates/theme/src/fallback_themes.rs +++ b/crates/theme/src/fallback_themes.rs @@ -115,6 +115,7 @@ pub(crate) fn zed_default_dark() -> Theme { element_disabled: SystemColors::default().transparent, element_selection_background: player.local().selection.alpha(0.25), drop_target_background: hsla(220.0 / 360., 8.3 / 100., 21.4 / 100., 1.0), + drop_target_border: hsla(221. / 360., 11. / 100., 86. / 100., 1.0), ghost_element_background: SystemColors::default().transparent, ghost_element_hover: hover, ghost_element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0), diff --git a/crates/theme/src/schema.rs b/crates/theme/src/schema.rs index bfa2adcedf..425fedbc71 100644 --- a/crates/theme/src/schema.rs +++ b/crates/theme/src/schema.rs @@ -225,6 +225,10 @@ pub struct ThemeColorsContent { #[serde(rename = "drop_target.background")] pub drop_target_background: Option, + /// Border Color. Used for the border that shows where a dragged element will be dropped. + #[serde(rename = "drop_target.border")] + pub drop_target_border: Option, + /// Used for the background of a ghost element that should have the same background as the surface it's on. /// /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons... @@ -747,6 +751,10 @@ impl ThemeColorsContent { .drop_target_background .as_ref() .and_then(|color| try_parse_color(color).ok()), + drop_target_border: self + .drop_target_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()), ghost_element_background: self .ghost_element_background .as_ref() diff --git a/crates/theme/src/styles/colors.rs b/crates/theme/src/styles/colors.rs index aab11803f4..198ad97adb 100644 --- a/crates/theme/src/styles/colors.rs +++ b/crates/theme/src/styles/colors.rs @@ -59,6 +59,8 @@ pub struct ThemeColors { pub element_disabled: Hsla, /// Background Color. Used for the area that shows where a dragged element will be dropped. pub drop_target_background: Hsla, + /// Border Color. Used for the border that shows where a dragged element will be dropped. + pub drop_target_border: Hsla, /// Used for the background of a ghost element that should have the same background as the surface it's on. /// /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons... @@ -304,6 +306,7 @@ pub enum ThemeColorField { ElementSelected, ElementDisabled, DropTargetBackground, + DropTargetBorder, GhostElementBackground, GhostElementHover, GhostElementActive, @@ -418,6 +421,7 @@ impl ThemeColors { ThemeColorField::ElementSelected => self.element_selected, ThemeColorField::ElementDisabled => self.element_disabled, ThemeColorField::DropTargetBackground => self.drop_target_background, + ThemeColorField::DropTargetBorder => self.drop_target_border, ThemeColorField::GhostElementBackground => self.ghost_element_background, ThemeColorField::GhostElementHover => self.ghost_element_hover, ThemeColorField::GhostElementActive => self.ghost_element_active, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index cffeea0a8d..45bd497705 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2478,8 +2478,19 @@ impl Pane { }, |tab, _, _, cx| cx.new(|_| tab.clone()), ) - .drag_over::(|tab, _, _, cx| { - tab.bg(cx.theme().colors().drop_target_background) + .drag_over::(move |tab, dragged_tab: &DraggedTab, _, cx| { + let mut styled_tab = tab + .bg(cx.theme().colors().drop_target_background) + .border_color(cx.theme().colors().drop_target_border) + .border_0(); + + if ix < dragged_tab.ix { + styled_tab = styled_tab.border_l_2(); + } else if ix > dragged_tab.ix { + styled_tab = styled_tab.border_r_2(); + } + + styled_tab }) .drag_over::(|tab, _, _, cx| { tab.bg(cx.theme().colors().drop_target_background) From 846ed6adf91fc63f585c921da0101802b031c855 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 15 Aug 2025 14:54:05 +0200 Subject: [PATCH 369/693] search: Fix project search not rendering matches count (#36238) Follow up to https://github.com/zed-industries/zed/pull/36103/ Release Notes: - N/A --- crates/search/src/project_search.rs | 89 +++++++++++++++-------------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 6b9777906a..b791f748ad 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1975,49 +1975,6 @@ impl Render for ProjectSearchBar { ), ); - let mode_column = h_flex() - .gap_1() - .min_w_64() - .child( - IconButton::new("project-search-filter-button", IconName::Filter) - .shape(IconButtonShape::Square) - .tooltip(|window, cx| { - Tooltip::for_action("Toggle Filters", &ToggleFilters, window, cx) - }) - .on_click(cx.listener(|this, _, window, cx| { - this.toggle_filters(window, cx); - })) - .toggle_state( - self.active_project_search - .as_ref() - .map(|search| search.read(cx).filters_enabled) - .unwrap_or_default(), - ) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Toggle Filters", - &ToggleFilters, - &focus_handle, - window, - cx, - ) - } - }), - ) - .child(render_action_button( - "project-search", - IconName::Replace, - self.active_project_search - .as_ref() - .map(|search| search.read(cx).replace_enabled) - .unwrap_or_default(), - "Toggle Replace", - &ToggleReplace, - focus_handle.clone(), - )); - let query_focus = search.query_editor.focus_handle(cx); let matches_column = h_flex() @@ -2060,11 +2017,55 @@ impl Render for ProjectSearchBar { }), ); + let mode_column = h_flex() + .gap_1() + .min_w_64() + .child( + IconButton::new("project-search-filter-button", IconName::Filter) + .shape(IconButtonShape::Square) + .tooltip(|window, cx| { + Tooltip::for_action("Toggle Filters", &ToggleFilters, window, cx) + }) + .on_click(cx.listener(|this, _, window, cx| { + this.toggle_filters(window, cx); + })) + .toggle_state( + self.active_project_search + .as_ref() + .map(|search| search.read(cx).filters_enabled) + .unwrap_or_default(), + ) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Toggle Filters", + &ToggleFilters, + &focus_handle, + window, + cx, + ) + } + }), + ) + .child(render_action_button( + "project-search", + IconName::Replace, + self.active_project_search + .as_ref() + .map(|search| search.read(cx).replace_enabled) + .unwrap_or_default(), + "Toggle Replace", + &ToggleReplace, + focus_handle.clone(), + )) + .child(matches_column); + let search_line = h_flex() .w_full() .gap_2() .child(query_column) - .child(h_flex().min_w_64().child(mode_column).child(matches_column)); + .child(mode_column); let replace_line = search.replace_enabled.then(|| { let replace_column = input_base_styles(InputPanel::Replacement) From f63036548c2229a4dfe1cd7576bf6cee5cd3f1ca Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Fri, 15 Aug 2025 15:17:56 +0200 Subject: [PATCH 370/693] agent2: Implement prompt caching (#36236) Release Notes: - N/A --- crates/agent2/src/tests/mod.rs | 135 ++++++++++++++++++++++++++++++++- crates/agent2/src/thread.rs | 8 ++ 2 files changed, 140 insertions(+), 3 deletions(-) diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index cf90c8f650..cc8bd483bb 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -16,6 +16,7 @@ use language_model::{ LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role, StopReason, fake_provider::FakeLanguageModel, }; +use pretty_assertions::assert_eq; use project::Project; use prompt_store::ProjectContext; use reqwest_client::ReqwestClient; @@ -129,6 +130,134 @@ async fn test_system_prompt(cx: &mut TestAppContext) { ); } +#[gpui::test] +async fn test_prompt_caching(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + // Send initial user message and verify it's cached + thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Message 1"], cx) + }); + cx.run_until_parked(); + + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!( + completion.messages[1..], + vec![LanguageModelRequestMessage { + role: Role::User, + content: vec!["Message 1".into()], + cache: true + }] + ); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text( + "Response to Message 1".into(), + )); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + // Send another user message and verify only the latest is cached + thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Message 2"], cx) + }); + cx.run_until_parked(); + + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!( + completion.messages[1..], + vec![ + LanguageModelRequestMessage { + role: Role::User, + content: vec!["Message 1".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::Assistant, + content: vec!["Response to Message 1".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec!["Message 2".into()], + cache: true + } + ] + ); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text( + "Response to Message 2".into(), + )); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + // Simulate a tool call and verify that the latest tool result is cached + thread.update(cx, |thread, _| thread.add_tool(EchoTool)); + thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Use the echo tool"], cx) + }); + cx.run_until_parked(); + + let tool_use = LanguageModelToolUse { + id: "tool_1".into(), + name: EchoTool.name().into(), + raw_input: json!({"text": "test"}).to_string(), + input: json!({"text": "test"}), + is_input_complete: true, + }; + fake_model + .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone())); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + let completion = fake_model.pending_completions().pop().unwrap(); + let tool_result = LanguageModelToolResult { + tool_use_id: "tool_1".into(), + tool_name: EchoTool.name().into(), + is_error: false, + content: "test".into(), + output: Some("test".into()), + }; + assert_eq!( + completion.messages[1..], + vec![ + LanguageModelRequestMessage { + role: Role::User, + content: vec!["Message 1".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::Assistant, + content: vec!["Response to Message 1".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec!["Message 2".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::Assistant, + content: vec!["Response to Message 2".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec!["Use the echo tool".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::Assistant, + content: vec![MessageContent::ToolUse(tool_use)], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec![MessageContent::ToolResult(tool_result)], + cache: true + } + ] + ); +} + #[gpui::test] #[ignore = "can't run on CI yet"] async fn test_basic_tool_calls(cx: &mut TestAppContext) { @@ -440,7 +569,7 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) { LanguageModelRequestMessage { role: Role::User, content: vec![MessageContent::ToolResult(tool_result.clone())], - cache: false + cache: true }, ] ); @@ -481,7 +610,7 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) { LanguageModelRequestMessage { role: Role::User, content: vec!["Continue where you left off".into()], - cache: false + cache: true } ] ); @@ -574,7 +703,7 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) { LanguageModelRequestMessage { role: Role::User, content: vec!["ghi".into()], - cache: false + cache: true } ] ); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 231ee92dda..2fe2dc20bb 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1041,6 +1041,14 @@ impl Thread { messages.extend(message.to_request()); } + if let Some(last_user_message) = messages + .iter_mut() + .rev() + .find(|message| message.role == Role::User) + { + last_user_message.cache = true; + } + messages } From 91e6b382852fde4e880bc4aba7a15f7bb08c11aa Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Fri, 15 Aug 2025 10:58:57 -0300 Subject: [PATCH 371/693] Log agent servers stderr (#36243) Release Notes: - N/A --- crates/agent_servers/src/acp/v1.rs | 21 ++++++++++++++++++--- crates/agent_servers/src/claude.rs | 21 +++++++++++++++++---- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index 36511e4644..6cf9801d06 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -1,7 +1,9 @@ use agent_client_protocol::{self as acp, Agent as _}; use anyhow::anyhow; use collections::HashMap; +use futures::AsyncBufReadExt as _; use futures::channel::oneshot; +use futures::io::BufReader; use project::Project; use std::path::Path; use std::rc::Rc; @@ -40,12 +42,13 @@ impl AcpConnection { .current_dir(root_dir) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::piped()) .kill_on_drop(true) .spawn()?; - let stdout = child.stdout.take().expect("Failed to take stdout"); - let stdin = child.stdin.take().expect("Failed to take stdin"); + let stdout = child.stdout.take().context("Failed to take stdout")?; + let stdin = child.stdin.take().context("Failed to take stdin")?; + let stderr = child.stderr.take().context("Failed to take stderr")?; log::trace!("Spawned (pid: {})", child.id()); let sessions = Rc::new(RefCell::new(HashMap::default())); @@ -63,6 +66,18 @@ impl AcpConnection { let io_task = cx.background_spawn(io_task); + cx.background_spawn(async move { + let mut stderr = BufReader::new(stderr); + let mut line = String::new(); + while let Ok(n) = stderr.read_line(&mut line).await + && n > 0 + { + log::warn!("agent stderr: {}", &line); + line.clear(); + } + }) + .detach(); + cx.spawn({ let sessions = sessions.clone(); async move |cx| { diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index e1cc709289..14a179ba3d 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -14,7 +14,7 @@ use std::rc::Rc; use uuid::Uuid; use agent_client_protocol as acp; -use anyhow::{Result, anyhow}; +use anyhow::{Context as _, Result, anyhow}; use futures::channel::oneshot; use futures::{AsyncBufReadExt, AsyncWriteExt}; use futures::{ @@ -130,12 +130,25 @@ impl AgentConnection for ClaudeAgentConnection { &cwd, )?; - let stdin = child.stdin.take().unwrap(); - let stdout = child.stdout.take().unwrap(); + let stdout = child.stdout.take().context("Failed to take stdout")?; + let stdin = child.stdin.take().context("Failed to take stdin")?; + let stderr = child.stderr.take().context("Failed to take stderr")?; let pid = child.id(); log::trace!("Spawned (pid: {})", pid); + cx.background_spawn(async move { + let mut stderr = BufReader::new(stderr); + let mut line = String::new(); + while let Ok(n) = stderr.read_line(&mut line).await + && n > 0 + { + log::warn!("agent stderr: {}", &line); + line.clear(); + } + }) + .detach(); + cx.background_spawn(async move { let mut outgoing_rx = Some(outgoing_rx); @@ -345,7 +358,7 @@ fn spawn_claude( .current_dir(root_dir) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::piped()) .kill_on_drop(true) .spawn()?; From 10a2426a58e913e2715eb5eab760d40385c839f2 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Fri, 15 Aug 2025 16:06:56 +0200 Subject: [PATCH 372/693] agent2: Port profile selector (#36244) Release Notes: - N/A --- crates/agent2/src/thread.rs | 4 ++ crates/agent_ui/src/acp/thread_view.rs | 42 ++++++++++++++++++++- crates/agent_ui/src/message_editor.rs | 27 +++++++++++-- crates/agent_ui/src/profile_selector.rs | 50 ++++++++++++------------- 4 files changed, 91 insertions(+), 32 deletions(-) diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 2fe2dc20bb..3f152c79cd 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -499,6 +499,10 @@ impl Thread { self.tools.remove(name).is_some() } + pub fn profile(&self) -> &AgentProfileId { + &self.profile_id + } + pub fn set_profile(&mut self, profile_id: AgentProfileId) { self.profile_id = profile_id; } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 87af75f046..cb1a62fd11 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -7,7 +7,7 @@ use action_log::ActionLog; use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::{self as acp}; use agent_servers::AgentServer; -use agent_settings::{AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; +use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; use anyhow::bail; use audio::{Audio, Sound}; use buffer_diff::BufferDiff; @@ -16,6 +16,7 @@ use collections::{HashMap, HashSet}; use editor::scroll::Autoscroll; use editor::{Editor, EditorMode, MultiBuffer, PathKey, SelectionEffects}; use file_icons::FileIcons; +use fs::Fs; use gpui::{ Action, Animation, AnimationExt, App, BorderStyle, ClickEvent, ClipboardItem, EdgesRefinement, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, @@ -29,6 +30,7 @@ use project::Project; use prompt_store::PromptId; use rope::Point; use settings::{Settings as _, SettingsStore}; +use std::sync::Arc; use std::{collections::BTreeMap, process::ExitStatus, rc::Rc, time::Duration}; use text::Anchor; use theme::ThemeSettings; @@ -45,10 +47,11 @@ use super::entry_view_state::EntryViewState; use crate::acp::AcpModelSelectorPopover; use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; use crate::agent_diff::AgentDiff; +use crate::profile_selector::{ProfileProvider, ProfileSelector}; use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip}; use crate::{ AgentDiffPane, AgentPanel, ContinueThread, ContinueWithBurnMode, ExpandMessageEditor, Follow, - KeepAll, OpenAgentDiff, RejectAll, ToggleBurnMode, + KeepAll, OpenAgentDiff, RejectAll, ToggleBurnMode, ToggleProfileSelector, }; const RESPONSE_PADDING_X: Pixels = px(19.); @@ -78,6 +81,22 @@ impl ThreadError { } } +impl ProfileProvider for Entity { + fn profile_id(&self, cx: &App) -> AgentProfileId { + self.read(cx).profile().clone() + } + + fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) { + self.update(cx, |thread, _cx| { + thread.set_profile(profile_id); + }); + } + + fn profiles_supported(&self, cx: &App) -> bool { + self.read(cx).model().supports_tools() + } +} + pub struct AcpThreadView { agent: Rc, workspace: WeakEntity, @@ -88,6 +107,7 @@ pub struct AcpThreadView { entry_view_state: EntryViewState, message_editor: Entity, model_selector: Option>, + profile_selector: Option>, notifications: Vec>, notification_subscriptions: HashMap, Vec>, thread_error: Option, @@ -170,6 +190,7 @@ impl AcpThreadView { thread_state: Self::initial_state(agent, workspace, project, window, cx), message_editor, model_selector: None, + profile_selector: None, notifications: Vec::new(), notification_subscriptions: HashMap::default(), entry_view_state: EntryViewState::default(), @@ -297,6 +318,17 @@ impl AcpThreadView { _subscription: [thread_subscription, action_log_subscription], }; + this.profile_selector = this.as_native_thread(cx).map(|thread| { + cx.new(|cx| { + ProfileSelector::new( + ::global(cx), + Arc::new(thread.clone()), + this.focus_handle(cx), + cx, + ) + }) + }); + cx.notify(); } Err(err) => { @@ -2315,6 +2347,11 @@ impl AcpThreadView { v_flex() .on_action(cx.listener(Self::expand_message_editor)) + .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| { + if let Some(profile_selector) = this.profile_selector.as_ref() { + profile_selector.read(cx).menu_handle().toggle(window, cx); + } + })) .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| { if let Some(model_selector) = this.model_selector.as_ref() { model_selector @@ -2378,6 +2415,7 @@ impl AcpThreadView { .child( h_flex() .gap_1() + .children(self.profile_selector.clone()) .children(self.model_selector.clone()) .child(self.render_send_button(cx)), ), diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 5d094811f1..127e9256be 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -14,7 +14,7 @@ use agent::{ context::{AgentContextKey, ContextLoadResult, load_context}, context_store::ContextStoreEvent, }; -use agent_settings::{AgentSettings, CompletionMode}; +use agent_settings::{AgentProfileId, AgentSettings, CompletionMode}; use ai_onboarding::ApiKeysWithProviders; use buffer_diff::BufferDiff; use cloud_llm_client::CompletionIntent; @@ -55,7 +55,7 @@ use zed_actions::agent::ToggleModelSelector; use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention}; use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; -use crate::profile_selector::ProfileSelector; +use crate::profile_selector::{ProfileProvider, ProfileSelector}; use crate::{ ActiveThread, AgentDiffPane, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll, ModelUsageContext, NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode, @@ -152,6 +152,24 @@ pub(crate) fn create_editor( editor } +impl ProfileProvider for Entity { + fn profiles_supported(&self, cx: &App) -> bool { + self.read(cx) + .configured_model() + .map_or(false, |model| model.model.supports_tools()) + } + + fn profile_id(&self, cx: &App) -> AgentProfileId { + self.read(cx).profile().id().clone() + } + + fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) { + self.update(cx, |this, cx| { + this.set_profile(profile_id, cx); + }); + } +} + impl MessageEditor { pub fn new( fs: Arc, @@ -221,8 +239,9 @@ impl MessageEditor { ) }); - let profile_selector = - cx.new(|cx| ProfileSelector::new(fs, thread.clone(), editor.focus_handle(cx), cx)); + let profile_selector = cx.new(|cx| { + ProfileSelector::new(fs, Arc::new(thread.clone()), editor.focus_handle(cx), cx) + }); Self { editor: editor.clone(), diff --git a/crates/agent_ui/src/profile_selector.rs b/crates/agent_ui/src/profile_selector.rs index ddcb44d46b..27ca69590f 100644 --- a/crates/agent_ui/src/profile_selector.rs +++ b/crates/agent_ui/src/profile_selector.rs @@ -1,12 +1,8 @@ use crate::{ManageProfiles, ToggleProfileSelector}; -use agent::{ - Thread, - agent_profile::{AgentProfile, AvailableProfiles}, -}; +use agent::agent_profile::{AgentProfile, AvailableProfiles}; use agent_settings::{AgentDockPosition, AgentProfileId, AgentSettings, builtin_profiles}; use fs::Fs; -use gpui::{Action, Empty, Entity, FocusHandle, Subscription, prelude::*}; -use language_model::LanguageModelRegistry; +use gpui::{Action, Entity, FocusHandle, Subscription, prelude::*}; use settings::{Settings as _, SettingsStore, update_settings_file}; use std::sync::Arc; use ui::{ @@ -14,10 +10,22 @@ use ui::{ prelude::*, }; +/// Trait for types that can provide and manage agent profiles +pub trait ProfileProvider { + /// Get the current profile ID + fn profile_id(&self, cx: &App) -> AgentProfileId; + + /// Set the profile ID + fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App); + + /// Check if profiles are supported in the current context (e.g. if the model that is selected has tool support) + fn profiles_supported(&self, cx: &App) -> bool; +} + pub struct ProfileSelector { profiles: AvailableProfiles, fs: Arc, - thread: Entity, + provider: Arc, menu_handle: PopoverMenuHandle, focus_handle: FocusHandle, _subscriptions: Vec, @@ -26,7 +34,7 @@ pub struct ProfileSelector { impl ProfileSelector { pub fn new( fs: Arc, - thread: Entity, + provider: Arc, focus_handle: FocusHandle, cx: &mut Context, ) -> Self { @@ -37,7 +45,7 @@ impl ProfileSelector { Self { profiles: AgentProfile::available_profiles(cx), fs, - thread, + provider, menu_handle: PopoverMenuHandle::default(), focus_handle, _subscriptions: vec![settings_subscription], @@ -113,10 +121,10 @@ impl ProfileSelector { builtin_profiles::MINIMAL => Some("Chat about anything with no tools."), _ => None, }; - let thread_profile_id = self.thread.read(cx).profile().id(); + let thread_profile_id = self.provider.profile_id(cx); let entry = ContextMenuEntry::new(profile_name.clone()) - .toggleable(IconPosition::End, &profile_id == thread_profile_id); + .toggleable(IconPosition::End, profile_id == thread_profile_id); let entry = if let Some(doc_text) = documentation { entry.documentation_aside(documentation_side(settings.dock), move |_| { @@ -128,7 +136,7 @@ impl ProfileSelector { entry.handler({ let fs = self.fs.clone(); - let thread = self.thread.clone(); + let provider = self.provider.clone(); let profile_id = profile_id.clone(); move |_window, cx| { update_settings_file::(fs.clone(), cx, { @@ -138,9 +146,7 @@ impl ProfileSelector { } }); - thread.update(cx, |this, cx| { - this.set_profile(profile_id.clone(), cx); - }); + provider.set_profile(profile_id.clone(), cx); } }) } @@ -149,22 +155,14 @@ impl ProfileSelector { impl Render for ProfileSelector { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let settings = AgentSettings::get_global(cx); - let profile_id = self.thread.read(cx).profile().id(); - let profile = settings.profiles.get(profile_id); + let profile_id = self.provider.profile_id(cx); + let profile = settings.profiles.get(&profile_id); let selected_profile = profile .map(|profile| profile.name.clone()) .unwrap_or_else(|| "Unknown".into()); - let configured_model = self.thread.read(cx).configured_model().or_else(|| { - let model_registry = LanguageModelRegistry::read_global(cx); - model_registry.default_model() - }); - let Some(configured_model) = configured_model else { - return Empty.into_any_element(); - }; - - if configured_model.model.supports_tools() { + if self.provider.profiles_supported(cx) { let this = cx.entity().clone(); let focus_handle = self.focus_handle.clone(); let trigger_button = Button::new("profile-selector-model", selected_profile) From 1e41d86b31b2225173c201cc00770bd485e044ce Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Fri, 15 Aug 2025 16:23:55 +0200 Subject: [PATCH 373/693] agent2: Set thread_id, prompt_id, temperature on request (#36246) Release Notes: - N/A --- crates/agent2/src/thread.rs | 57 +++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 3f152c79cd..cfd67f4b05 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -28,6 +28,48 @@ use smol::stream::StreamExt; use std::{cell::RefCell, collections::BTreeMap, path::Path, rc::Rc, sync::Arc}; use std::{fmt::Write, ops::Range}; use util::{ResultExt, markdown::MarkdownCodeBlock}; +use uuid::Uuid; + +#[derive( + Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize, JsonSchema, +)] +pub struct ThreadId(Arc); + +impl ThreadId { + pub fn new() -> Self { + Self(Uuid::new_v4().to_string().into()) + } +} + +impl std::fmt::Display for ThreadId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From<&str> for ThreadId { + fn from(value: &str) -> Self { + Self(value.into()) + } +} + +/// The ID of the user prompt that initiated a request. +/// +/// This equates to the user physically submitting a message to the model (e.g., by pressing the Enter key). +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] +pub struct PromptId(Arc); + +impl PromptId { + pub fn new() -> Self { + Self(Uuid::new_v4().to_string().into()) + } +} + +impl std::fmt::Display for PromptId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} #[derive(Debug, Clone, PartialEq, Eq)] pub enum Message { @@ -412,6 +454,8 @@ pub struct ToolCallAuthorization { } pub struct Thread { + id: ThreadId, + prompt_id: PromptId, messages: Vec, completion_mode: CompletionMode, /// Holds the task that handles agent interaction until the end of the turn. @@ -442,6 +486,8 @@ impl Thread { ) -> Self { let profile_id = AgentSettings::get_global(cx).default_profile.clone(); Self { + id: ThreadId::new(), + prompt_id: PromptId::new(), messages: Vec::new(), completion_mode: CompletionMode::Normal, running_turn: None, @@ -553,6 +599,7 @@ impl Thread { T: Into, { log::info!("Thread::send called with model: {:?}", self.model.name()); + self.advance_prompt_id(); let content = content.into_iter().map(Into::into).collect::>(); log::debug!("Thread::send content: {:?}", content); @@ -976,15 +1023,15 @@ impl Thread { log::info!("Request includes {} tools", tools.len()); let request = LanguageModelRequest { - thread_id: None, - prompt_id: None, + thread_id: Some(self.id.to_string()), + prompt_id: Some(self.prompt_id.to_string()), intent: Some(completion_intent), mode: Some(self.completion_mode.into()), messages, tools, tool_choice: None, stop: Vec::new(), - temperature: None, + temperature: AgentSettings::temperature_for_model(self.model(), cx), thinking_allowed: true, }; @@ -1072,6 +1119,10 @@ impl Thread { markdown } + + fn advance_prompt_id(&mut self) { + self.prompt_id = PromptId::new(); + } } pub trait AgentTool From 485802b9e5226cb00c14bf9d94211cabfd42a51b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 10:46:06 -0400 Subject: [PATCH 374/693] collab: Remove endpoints for issuing notifications from Cloud (#36249) This PR removes the `POST /users/:id/refresh_llm_tokens` and `POST /users/:id/update_plan` endpoints from Collab. These endpoints were added to be called by Cloud in order to push down notifications over the Collab RPC connection. Cloud now sends down notifications to clients directly, so we no longer need these endpoints. All calls to these endpoints have already been removed in production. Release Notes: - N/A --- crates/collab/src/api.rs | 92 ---------------------------------------- crates/collab/src/rpc.rs | 47 -------------------- 2 files changed, 139 deletions(-) diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 6cf3f68f54..078a4469ae 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -11,9 +11,7 @@ use crate::{ db::{User, UserId}, rpc, }; -use ::rpc::proto; use anyhow::Context as _; -use axum::extract; use axum::{ Extension, Json, Router, body::Body, @@ -25,7 +23,6 @@ use axum::{ routing::{get, post}, }; use axum_extra::response::ErasedJson; -use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::sync::{Arc, OnceLock}; use tower::ServiceBuilder; @@ -102,8 +99,6 @@ pub fn routes(rpc_server: Arc) -> Router<(), Body> { Router::new() .route("/users/look_up", get(look_up_user)) .route("/users/:id/access_tokens", post(create_access_token)) - .route("/users/:id/refresh_llm_tokens", post(refresh_llm_tokens)) - .route("/users/:id/update_plan", post(update_plan)) .route("/rpc_server_snapshot", get(get_rpc_server_snapshot)) .merge(contributors::router()) .layer( @@ -295,90 +290,3 @@ async fn create_access_token( encrypted_access_token, })) } - -#[derive(Serialize)] -struct RefreshLlmTokensResponse {} - -async fn refresh_llm_tokens( - Path(user_id): Path, - Extension(rpc_server): Extension>, -) -> Result> { - rpc_server.refresh_llm_tokens_for_user(user_id).await; - - Ok(Json(RefreshLlmTokensResponse {})) -} - -#[derive(Debug, Serialize, Deserialize)] -struct UpdatePlanBody { - pub plan: cloud_llm_client::Plan, - pub subscription_period: SubscriptionPeriod, - pub usage: cloud_llm_client::CurrentUsage, - pub trial_started_at: Option>, - pub is_usage_based_billing_enabled: bool, - pub is_account_too_young: bool, - pub has_overdue_invoices: bool, -} - -#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)] -struct SubscriptionPeriod { - pub started_at: DateTime, - pub ended_at: DateTime, -} - -#[derive(Serialize)] -struct UpdatePlanResponse {} - -async fn update_plan( - Path(user_id): Path, - Extension(rpc_server): Extension>, - extract::Json(body): extract::Json, -) -> Result> { - let plan = match body.plan { - cloud_llm_client::Plan::ZedFree => proto::Plan::Free, - cloud_llm_client::Plan::ZedPro => proto::Plan::ZedPro, - cloud_llm_client::Plan::ZedProTrial => proto::Plan::ZedProTrial, - }; - - let update_user_plan = proto::UpdateUserPlan { - plan: plan.into(), - trial_started_at: body - .trial_started_at - .map(|trial_started_at| trial_started_at.timestamp() as u64), - is_usage_based_billing_enabled: Some(body.is_usage_based_billing_enabled), - usage: Some(proto::SubscriptionUsage { - model_requests_usage_amount: body.usage.model_requests.used, - model_requests_usage_limit: Some(usage_limit_to_proto(body.usage.model_requests.limit)), - edit_predictions_usage_amount: body.usage.edit_predictions.used, - edit_predictions_usage_limit: Some(usage_limit_to_proto( - body.usage.edit_predictions.limit, - )), - }), - subscription_period: Some(proto::SubscriptionPeriod { - started_at: body.subscription_period.started_at.timestamp() as u64, - ended_at: body.subscription_period.ended_at.timestamp() as u64, - }), - account_too_young: Some(body.is_account_too_young), - has_overdue_invoices: Some(body.has_overdue_invoices), - }; - - rpc_server - .update_plan_for_user(user_id, update_user_plan) - .await?; - - Ok(Json(UpdatePlanResponse {})) -} - -fn usage_limit_to_proto(limit: cloud_llm_client::UsageLimit) -> proto::UsageLimit { - proto::UsageLimit { - variant: Some(match limit { - cloud_llm_client::UsageLimit::Limited(limit) => { - proto::usage_limit::Variant::Limited(proto::usage_limit::Limited { - limit: limit as u32, - }) - } - cloud_llm_client::UsageLimit::Unlimited => { - proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {}) - } - }), - } -} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 18eb1457dc..584970a4c6 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1081,53 +1081,6 @@ impl Server { Ok(()) } - pub async fn update_plan_for_user( - self: &Arc, - user_id: UserId, - update_user_plan: proto::UpdateUserPlan, - ) -> Result<()> { - let pool = self.connection_pool.lock(); - for connection_id in pool.user_connection_ids(user_id) { - self.peer - .send(connection_id, update_user_plan.clone()) - .trace_err(); - } - - Ok(()) - } - - /// This is the legacy way of updating the user's plan, where we fetch the data to construct the `UpdateUserPlan` - /// message on the Collab server. - /// - /// The new way is to receive the data from Cloud via the `POST /users/:id/update_plan` endpoint. - pub async fn update_plan_for_user_legacy(self: &Arc, user_id: UserId) -> Result<()> { - let user = self - .app_state - .db - .get_user_by_id(user_id) - .await? - .context("user not found")?; - - let update_user_plan = make_update_user_plan_message( - &user, - user.admin, - &self.app_state.db, - self.app_state.llm_db.clone(), - ) - .await?; - - self.update_plan_for_user(user_id, update_user_plan).await - } - - pub async fn refresh_llm_tokens_for_user(self: &Arc, user_id: UserId) { - let pool = self.connection_pool.lock(); - for connection_id in pool.user_connection_ids(user_id) { - self.peer - .send(connection_id, proto::RefreshLlmToken {}) - .trace_err(); - } - } - pub async fn snapshot(self: &Arc) -> ServerSnapshot<'_> { ServerSnapshot { connection_pool: ConnectionPoolGuard { From 7993ee9c07a56e61ead665ca95343c038ea2765a Mon Sep 17 00:00:00 2001 From: Igal Tabachnik Date: Fri, 15 Aug 2025 18:26:38 +0300 Subject: [PATCH 375/693] Suggest unsaved buffer content text as the default filename (#35707) Closes #24672 This PR complements a feature added earlier by @JosephTLyons (in https://github.com/zed-industries/zed/pull/32353) where the text is considered as the tab title in a new buffer. It piggybacks off that change and sets the title as the suggested filename in the save dialog (completely mirroring the same functionality in VSCode): ![2025-08-05 11 50 28](https://github.com/user-attachments/assets/49ad9e4a-5559-44b0-a4b0-ae19890e478e) Release Notes: - Text entered in a new untitled buffer is considered as the default filename when saving --- crates/editor/src/items.rs | 4 +++ crates/gpui/src/app.rs | 3 +- crates/gpui/src/platform.rs | 6 +++- crates/gpui/src/platform/linux/platform.rs | 29 +++++++++++++------- crates/gpui/src/platform/mac/platform.rs | 12 +++++++- crates/gpui/src/platform/test/platform.rs | 1 + crates/gpui/src/platform/windows/platform.rs | 20 ++++++++++++-- crates/workspace/src/item.rs | 11 ++++++++ crates/workspace/src/pane.rs | 4 ++- crates/workspace/src/workspace.rs | 3 +- 10 files changed, 75 insertions(+), 18 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 480757a491..45a4f7365c 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -654,6 +654,10 @@ impl Item for Editor { } } + fn suggested_filename(&self, cx: &App) -> SharedString { + self.buffer.read(cx).title(cx).to_string().into() + } + fn tab_icon(&self, _: &Window, cx: &App) -> Option { ItemSettings::get_global(cx) .file_icons diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 5f6d252503..e1df6d0be4 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -816,8 +816,9 @@ impl App { pub fn prompt_for_new_path( &self, directory: &Path, + suggested_name: Option<&str>, ) -> oneshot::Receiver>> { - self.platform.prompt_for_new_path(directory) + self.platform.prompt_for_new_path(directory, suggested_name) } /// Reveals the specified path at the platform level, such as in Finder on macOS. diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index b495d70dfd..bf6ce68703 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -220,7 +220,11 @@ pub(crate) trait Platform: 'static { &self, options: PathPromptOptions, ) -> oneshot::Receiver>>>; - fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver>>; + fn prompt_for_new_path( + &self, + directory: &Path, + suggested_name: Option<&str>, + ) -> oneshot::Receiver>>; fn can_select_mixed_files_and_dirs(&self) -> bool; fn reveal_path(&self, path: &Path); fn open_with_system(&self, path: &Path); diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index fe6a36baa8..31d445be52 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -327,26 +327,35 @@ impl Platform for P { done_rx } - fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver>> { + fn prompt_for_new_path( + &self, + directory: &Path, + suggested_name: Option<&str>, + ) -> oneshot::Receiver>> { let (done_tx, done_rx) = oneshot::channel(); #[cfg(not(any(feature = "wayland", feature = "x11")))] - let _ = (done_tx.send(Ok(None)), directory); + let _ = (done_tx.send(Ok(None)), directory, suggested_name); #[cfg(any(feature = "wayland", feature = "x11"))] self.foreground_executor() .spawn({ let directory = directory.to_owned(); + let suggested_name = suggested_name.map(|s| s.to_owned()); async move { - let request = match ashpd::desktop::file_chooser::SaveFileRequest::default() - .modal(true) - .title("Save File") - .current_folder(directory) - .expect("pathbuf should not be nul terminated") - .send() - .await - { + let mut request_builder = + ashpd::desktop::file_chooser::SaveFileRequest::default() + .modal(true) + .title("Save File") + .current_folder(directory) + .expect("pathbuf should not be nul terminated"); + + if let Some(suggested_name) = suggested_name { + request_builder = request_builder.current_name(suggested_name.as_str()); + } + + let request = match request_builder.send().await { Ok(request) => request, Err(err) => { let result = match err { diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index c573131799..533423229c 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -737,8 +737,13 @@ impl Platform for MacPlatform { done_rx } - fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver>> { + fn prompt_for_new_path( + &self, + directory: &Path, + suggested_name: Option<&str>, + ) -> oneshot::Receiver>> { let directory = directory.to_owned(); + let suggested_name = suggested_name.map(|s| s.to_owned()); let (done_tx, done_rx) = oneshot::channel(); self.foreground_executor() .spawn(async move { @@ -748,6 +753,11 @@ impl Platform for MacPlatform { let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc()); panel.setDirectoryURL(url); + if let Some(suggested_name) = suggested_name { + let name_string = ns_string(&suggested_name); + let _: () = msg_send![panel, setNameFieldStringValue: name_string]; + } + let done_tx = Cell::new(Some(done_tx)); let block = ConcreteBlock::new(move |response: NSModalResponse| { let mut result = None; diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index a26b65576c..69371bc8c4 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -336,6 +336,7 @@ impl Platform for TestPlatform { fn prompt_for_new_path( &self, directory: &std::path::Path, + _suggested_name: Option<&str>, ) -> oneshot::Receiver>> { let (tx, rx) = oneshot::channel(); self.background_executor() diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index bbde655b80..c1fb0cabc4 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -490,13 +490,18 @@ impl Platform for WindowsPlatform { rx } - fn prompt_for_new_path(&self, directory: &Path) -> Receiver>> { + fn prompt_for_new_path( + &self, + directory: &Path, + suggested_name: Option<&str>, + ) -> Receiver>> { let directory = directory.to_owned(); + let suggested_name = suggested_name.map(|s| s.to_owned()); let (tx, rx) = oneshot::channel(); let window = self.find_current_active_window(); self.foreground_executor() .spawn(async move { - let _ = tx.send(file_save_dialog(directory, window)); + let _ = tx.send(file_save_dialog(directory, suggested_name, window)); }) .detach(); @@ -804,7 +809,11 @@ fn file_open_dialog( Ok(Some(paths)) } -fn file_save_dialog(directory: PathBuf, window: Option) -> Result> { +fn file_save_dialog( + directory: PathBuf, + suggested_name: Option, + window: Option, +) -> Result> { let dialog: IFileSaveDialog = unsafe { CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)? }; if !directory.to_string_lossy().is_empty() { if let Some(full_path) = directory.canonicalize().log_err() { @@ -815,6 +824,11 @@ fn file_save_dialog(directory: PathBuf, window: Option) -> Result + Render + Sized { /// Returns the textual contents of the tab. fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString; + /// Returns the suggested filename for saving this item. + /// By default, returns the tab content text. + fn suggested_filename(&self, cx: &App) -> SharedString { + self.tab_content_text(0, cx) + } + fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { None } @@ -497,6 +503,7 @@ pub trait ItemHandle: 'static + Send { ) -> gpui::Subscription; fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement; fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString; + fn suggested_filename(&self, cx: &App) -> SharedString; fn tab_icon(&self, window: &Window, cx: &App) -> Option; fn tab_tooltip_text(&self, cx: &App) -> Option; fn tab_tooltip_content(&self, cx: &App) -> Option; @@ -631,6 +638,10 @@ impl ItemHandle for Entity { self.read(cx).tab_content_text(detail, cx) } + fn suggested_filename(&self, cx: &App) -> SharedString { + self.read(cx).suggested_filename(cx) + } + fn tab_icon(&self, window: &Window, cx: &App) -> Option { self.read(cx).tab_icon(window, cx) } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 45bd497705..759e91f758 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2062,6 +2062,8 @@ impl Pane { })? .await?; } else if can_save_as && is_singleton { + let suggested_name = + cx.update(|_window, cx| item.suggested_filename(cx).to_string())?; let new_path = pane.update_in(cx, |pane, window, cx| { pane.activate_item(item_ix, true, true, window, cx); pane.workspace.update(cx, |workspace, cx| { @@ -2073,7 +2075,7 @@ impl Pane { } else { DirectoryLister::Project(workspace.project().clone()) }; - workspace.prompt_for_new_path(lister, window, cx) + workspace.prompt_for_new_path(lister, Some(suggested_name), window, cx) }) })??; let Some(new_path) = new_path.await.ok().flatten().into_iter().flatten().next() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3129c12dbf..ade6838fad 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2067,6 +2067,7 @@ impl Workspace { pub fn prompt_for_new_path( &mut self, lister: DirectoryLister, + suggested_name: Option, window: &mut Window, cx: &mut Context, ) -> oneshot::Receiver>> { @@ -2094,7 +2095,7 @@ impl Workspace { }) .or_else(std::env::home_dir) .unwrap_or_else(|| PathBuf::from("")); - cx.prompt_for_new_path(&relative_to) + cx.prompt_for_new_path(&relative_to, suggested_name.as_deref()) })?; let abs_path = match abs_path.await? { Ok(path) => path, From 7671f34f88aefbaf75a313cf4b1fc0523cb7a43a Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Fri, 15 Aug 2025 18:37:24 +0300 Subject: [PATCH 376/693] agent: Create checkpoint before/after every edit operation (#36253) 1. Previously, checkpoints only appeared when an agent's edit happened immediately after a user message. This is rare (agent usually collects some context first), so they were almost never shown. This is now fixed. 2. After this change, a checkpoint is created after every edit operation. So when the agent edits files five times in a single dialog turn, we will now display five checkpoints. As a bonus, it's now possible to undo only a part of a long agent response. Closes #36092, #32917 Release Notes: - Create agent checkpoints more frequently (before every edit) --- crates/agent/src/thread.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 1d417efbba..f3f1088483 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -844,11 +844,17 @@ impl Thread { .await .unwrap_or(false); - if !equal { - this.update(cx, |this, cx| { - this.insert_checkpoint(pending_checkpoint, cx) - })?; - } + this.update(cx, |this, cx| { + this.pending_checkpoint = if equal { + Some(pending_checkpoint) + } else { + this.insert_checkpoint(pending_checkpoint, cx); + Some(ThreadCheckpoint { + message_id: this.next_message_id, + git_checkpoint: final_checkpoint, + }) + } + })?; Ok(()) } From c39f294bcbae49e649d5cdd7d5bc774fa7a7190a Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Fri, 15 Aug 2025 21:43:18 +0530 Subject: [PATCH 377/693] remote: Add support for additional SSH arguments in SshSocket (#33243) Closes #29438 Release Notes: - Fix SSH agent forwarding doesn't work when using SSH remote development. --- crates/remote/src/ssh_session.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index df7212d44c..2f462a86a5 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -400,6 +400,7 @@ impl SshSocket { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) + .args(self.connection_options.additional_args()) .args(["-o", "ControlMaster=no", "-o"]) .arg(format!("ControlPath={}", self.socket_path.display())) } @@ -410,6 +411,7 @@ impl SshSocket { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) + .args(self.connection_options.additional_args()) .envs(self.envs.clone()) } @@ -417,22 +419,26 @@ impl SshSocket { // On Linux, we use the `ControlPath` option to create a socket file that ssh can use to #[cfg(not(target_os = "windows"))] fn ssh_args(&self) -> SshArgs { + let mut arguments = self.connection_options.additional_args(); + arguments.extend(vec![ + "-o".to_string(), + "ControlMaster=no".to_string(), + "-o".to_string(), + format!("ControlPath={}", self.socket_path.display()), + self.connection_options.ssh_url(), + ]); SshArgs { - arguments: vec![ - "-o".to_string(), - "ControlMaster=no".to_string(), - "-o".to_string(), - format!("ControlPath={}", self.socket_path.display()), - self.connection_options.ssh_url(), - ], + arguments, envs: None, } } #[cfg(target_os = "windows")] fn ssh_args(&self) -> SshArgs { + let mut arguments = self.connection_options.additional_args(); + arguments.push(self.connection_options.ssh_url()); SshArgs { - arguments: vec![self.connection_options.ssh_url()], + arguments, envs: Some(self.envs.clone()), } } From 257e0991d8069face34e734ff9ca4e9baa027817 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 12:13:52 -0400 Subject: [PATCH 378/693] collab: Increase minimum required version to connect (#36255) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR increases the minimum required version to connect to Collab. Previously this was set at v0.157.0. The new minimum required version is v0.198.4, which is the first version where we no longer connect to Collab automatically. Clients on the v0.199.x minor version will also need to be v0.199.2 or greater in order to connect, due to us hotfixing the connection changes to the Preview branch. We're doing this to force clients to upgrade in order to connect to Collab, as we're going to be removing some of the old RPC usages related to authentication that are no longer used. Therefore, we want users to be on a version of Zed that does not rely on those messages. Users will see a message similar to this one, prompting them to upgrade: Screenshot 2025-08-15 at 11 37
55 AM > Note: In this case I'm simulating the error state, which is why I'm signed in via Cloud while still not being able to connect to Collab. Users on older versions will see the "Please update Zed to Collaborate" message without being signed in. Release Notes: - N/A --- crates/collab/src/rpc/connection_pool.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/collab/src/rpc/connection_pool.rs b/crates/collab/src/rpc/connection_pool.rs index 35290fa697..729e7c8533 100644 --- a/crates/collab/src/rpc/connection_pool.rs +++ b/crates/collab/src/rpc/connection_pool.rs @@ -30,7 +30,19 @@ impl fmt::Display for ZedVersion { impl ZedVersion { pub fn can_collaborate(&self) -> bool { - self.0 >= SemanticVersion::new(0, 157, 0) + // v0.198.4 is the first version where we no longer connect to Collab automatically. + // We reject any clients older than that to prevent them from connecting to Collab just for authentication. + if self.0 < SemanticVersion::new(0, 198, 4) { + return false; + } + + // Since we hotfixed the changes to no longer connect to Collab automatically to Preview, we also need to reject + // versions in the range [v0.199.0, v0.199.1]. + if self.0 >= SemanticVersion::new(0, 199, 0) && self.0 < SemanticVersion::new(0, 199, 2) { + return false; + } + + true } } From 75b832029a7ab35442e030fff05df55dbbd2d6de Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 13:26:21 -0400 Subject: [PATCH 379/693] Remove RPC messages pertaining to the LLM token (#36252) This PR removes the RPC messages pertaining to the LLM token. We now retrieve the LLM token from Cloud. Release Notes: - N/A --- Cargo.lock | 2 - crates/collab/Cargo.toml | 2 - crates/collab/src/llm.rs | 3 - crates/collab/src/llm/token.rs | 146 --------------------------------- crates/collab/src/rpc.rs | 96 +--------------------- crates/proto/proto/ai.proto | 8 -- crates/proto/proto/zed.proto | 6 +- crates/proto/src/proto.rs | 4 - 8 files changed, 4 insertions(+), 263 deletions(-) delete mode 100644 crates/collab/src/llm/token.rs diff --git a/Cargo.lock b/Cargo.lock index 2353733dc0..bfc797d6cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3324,7 +3324,6 @@ dependencies = [ "http_client", "hyper 0.14.32", "indoc", - "jsonwebtoken", "language", "language_model", "livekit_api", @@ -3370,7 +3369,6 @@ dependencies = [ "telemetry_events", "text", "theme", - "thiserror 2.0.12", "time", "tokio", "toml 0.8.20", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 9af95317e6..9a867f9e05 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -39,7 +39,6 @@ futures.workspace = true gpui.workspace = true hex.workspace = true http_client.workspace = true -jsonwebtoken.workspace = true livekit_api.workspace = true log.workspace = true nanoid.workspace = true @@ -65,7 +64,6 @@ subtle.workspace = true supermaven_api.workspace = true telemetry_events.workspace = true text.workspace = true -thiserror.workspace = true time.workspace = true tokio = { workspace = true, features = ["full"] } toml.workspace = true diff --git a/crates/collab/src/llm.rs b/crates/collab/src/llm.rs index de74858168..ca8e89bc6d 100644 --- a/crates/collab/src/llm.rs +++ b/crates/collab/src/llm.rs @@ -1,7 +1,4 @@ pub mod db; -mod token; - -pub use token::*; pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial"; diff --git a/crates/collab/src/llm/token.rs b/crates/collab/src/llm/token.rs deleted file mode 100644 index da01c7f3be..0000000000 --- a/crates/collab/src/llm/token.rs +++ /dev/null @@ -1,146 +0,0 @@ -use crate::db::billing_subscription::SubscriptionKind; -use crate::db::{billing_customer, billing_subscription, user}; -use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG}; -use crate::{Config, db::billing_preference}; -use anyhow::{Context as _, Result}; -use chrono::{NaiveDateTime, Utc}; -use cloud_llm_client::Plan; -use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation}; -use serde::{Deserialize, Serialize}; -use std::time::Duration; -use thiserror::Error; -use uuid::Uuid; - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LlmTokenClaims { - pub iat: u64, - pub exp: u64, - pub jti: String, - pub user_id: u64, - pub system_id: Option, - pub metrics_id: Uuid, - pub github_user_login: String, - pub account_created_at: NaiveDateTime, - pub is_staff: bool, - pub has_llm_closed_beta_feature_flag: bool, - pub bypass_account_age_check: bool, - pub use_llm_request_queue: bool, - pub plan: Plan, - pub has_extended_trial: bool, - pub subscription_period: (NaiveDateTime, NaiveDateTime), - pub enable_model_request_overages: bool, - pub model_request_overages_spend_limit_in_cents: u32, - pub can_use_web_search_tool: bool, - #[serde(default)] - pub has_overdue_invoices: bool, -} - -const LLM_TOKEN_LIFETIME: Duration = Duration::from_secs(60 * 60); - -impl LlmTokenClaims { - pub fn create( - user: &user::Model, - is_staff: bool, - billing_customer: billing_customer::Model, - billing_preferences: Option, - feature_flags: &Vec, - subscription: billing_subscription::Model, - system_id: Option, - config: &Config, - ) -> Result { - let secret = config - .llm_api_secret - .as_ref() - .context("no LLM API secret")?; - - let plan = if is_staff { - Plan::ZedPro - } else { - subscription.kind.map_or(Plan::ZedFree, |kind| match kind { - SubscriptionKind::ZedFree => Plan::ZedFree, - SubscriptionKind::ZedPro => Plan::ZedPro, - SubscriptionKind::ZedProTrial => Plan::ZedProTrial, - }) - }; - let subscription_period = - billing_subscription::Model::current_period(Some(subscription), is_staff) - .map(|(start, end)| (start.naive_utc(), end.naive_utc())) - .context("A plan is required to use Zed's hosted models or edit predictions. Visit https://zed.dev/account to get started.")?; - - let now = Utc::now(); - let claims = Self { - iat: now.timestamp() as u64, - exp: (now + LLM_TOKEN_LIFETIME).timestamp() as u64, - jti: uuid::Uuid::new_v4().to_string(), - user_id: user.id.to_proto(), - system_id, - metrics_id: user.metrics_id, - github_user_login: user.github_login.clone(), - account_created_at: user.account_created_at(), - is_staff, - has_llm_closed_beta_feature_flag: feature_flags - .iter() - .any(|flag| flag == "llm-closed-beta"), - bypass_account_age_check: feature_flags - .iter() - .any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG), - can_use_web_search_tool: true, - use_llm_request_queue: feature_flags.iter().any(|flag| flag == "llm-request-queue"), - plan, - has_extended_trial: feature_flags - .iter() - .any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG), - subscription_period, - enable_model_request_overages: billing_preferences - .as_ref() - .map_or(false, |preferences| { - preferences.model_request_overages_enabled - }), - model_request_overages_spend_limit_in_cents: billing_preferences - .as_ref() - .map_or(0, |preferences| { - preferences.model_request_overages_spend_limit_in_cents as u32 - }), - has_overdue_invoices: billing_customer.has_overdue_invoices, - }; - - Ok(jsonwebtoken::encode( - &Header::default(), - &claims, - &EncodingKey::from_secret(secret.as_ref()), - )?) - } - - pub fn validate(token: &str, config: &Config) -> Result { - let secret = config - .llm_api_secret - .as_ref() - .context("no LLM API secret")?; - - match jsonwebtoken::decode::( - token, - &DecodingKey::from_secret(secret.as_ref()), - &Validation::default(), - ) { - Ok(token) => Ok(token.claims), - Err(e) => { - if e.kind() == &jsonwebtoken::errors::ErrorKind::ExpiredSignature { - Err(ValidateLlmTokenError::Expired) - } else { - Err(ValidateLlmTokenError::JwtError(e)) - } - } - } - } -} - -#[derive(Error, Debug)] -pub enum ValidateLlmTokenError { - #[error("access token is expired")] - Expired, - #[error("access token validation error: {0}")] - JwtError(#[from] jsonwebtoken::errors::Error), - #[error("{0}")] - Other(#[from] anyhow::Error), -} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 584970a4c6..715ff4e67d 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1,14 +1,12 @@ mod connection_pool; -use crate::api::billing::find_or_create_billing_customer; use crate::api::{CloudflareIpCountryHeader, SystemIdHeader}; use crate::db::billing_subscription::SubscriptionKind; use crate::llm::db::LlmDatabase; use crate::llm::{ - AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG, LlmTokenClaims, + AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG, MIN_ACCOUNT_AGE_FOR_LLM_USE, }; -use crate::stripe_client::StripeCustomerId; use crate::{ AppState, Error, Result, auth, db::{ @@ -218,6 +216,7 @@ struct Session { /// The GeoIP country code for the user. #[allow(unused)] geoip_country_code: Option, + #[allow(unused)] system_id: Option, _executor: Executor, } @@ -464,7 +463,6 @@ impl Server { .add_message_handler(unfollow) .add_message_handler(update_followers) .add_request_handler(get_private_user_info) - .add_request_handler(get_llm_api_token) .add_request_handler(accept_terms_of_service) .add_message_handler(acknowledge_channel_message) .add_message_handler(acknowledge_buffer_version) @@ -4251,96 +4249,6 @@ async fn accept_terms_of_service( accepted_tos_at: accepted_tos_at.timestamp() as u64, })?; - // When the user accepts the terms of service, we want to refresh their LLM - // token to grant access. - session - .peer - .send(session.connection_id, proto::RefreshLlmToken {})?; - - Ok(()) -} - -async fn get_llm_api_token( - _request: proto::GetLlmToken, - response: Response, - session: MessageContext, -) -> Result<()> { - let db = session.db().await; - - let flags = db.get_user_flags(session.user_id()).await?; - - let user_id = session.user_id(); - let user = db - .get_user_by_id(user_id) - .await? - .with_context(|| format!("user {user_id} not found"))?; - - if user.accepted_tos_at.is_none() { - Err(anyhow!("terms of service not accepted"))? - } - - let stripe_client = session - .app_state - .stripe_client - .as_ref() - .context("failed to retrieve Stripe client")?; - - let stripe_billing = session - .app_state - .stripe_billing - .as_ref() - .context("failed to retrieve Stripe billing object")?; - - let billing_customer = if let Some(billing_customer) = - db.get_billing_customer_by_user_id(user.id).await? - { - billing_customer - } else { - let customer_id = stripe_billing - .find_or_create_customer_by_email(user.email_address.as_deref()) - .await?; - - find_or_create_billing_customer(&session.app_state, stripe_client.as_ref(), &customer_id) - .await? - .context("billing customer not found")? - }; - - let billing_subscription = - if let Some(billing_subscription) = db.get_active_billing_subscription(user.id).await? { - billing_subscription - } else { - let stripe_customer_id = - StripeCustomerId(billing_customer.stripe_customer_id.clone().into()); - - let stripe_subscription = stripe_billing - .subscribe_to_zed_free(stripe_customer_id) - .await?; - - db.create_billing_subscription(&db::CreateBillingSubscriptionParams { - billing_customer_id: billing_customer.id, - kind: Some(SubscriptionKind::ZedFree), - stripe_subscription_id: stripe_subscription.id.to_string(), - stripe_subscription_status: stripe_subscription.status.into(), - stripe_cancellation_reason: None, - stripe_current_period_start: Some(stripe_subscription.current_period_start), - stripe_current_period_end: Some(stripe_subscription.current_period_end), - }) - .await? - }; - - let billing_preferences = db.get_billing_preferences(user.id).await?; - - let token = LlmTokenClaims::create( - &user, - session.is_staff(), - billing_customer, - billing_preferences, - &flags, - billing_subscription, - session.system_id.clone(), - &session.app_state.config, - )?; - response.send(proto::GetLlmTokenResponse { token })?; Ok(()) } diff --git a/crates/proto/proto/ai.proto b/crates/proto/proto/ai.proto index 67c2224387..1064ed2f8d 100644 --- a/crates/proto/proto/ai.proto +++ b/crates/proto/proto/ai.proto @@ -158,14 +158,6 @@ message SynchronizeContextsResponse { repeated ContextVersion contexts = 1; } -message GetLlmToken {} - -message GetLlmTokenResponse { - string token = 1; -} - -message RefreshLlmToken {} - enum LanguageModelRole { LanguageModelUser = 0; LanguageModelAssistant = 1; diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 856a793c2f..b6c7fc3cac 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -250,10 +250,6 @@ message Envelope { AddWorktree add_worktree = 222; AddWorktreeResponse add_worktree_response = 223; - GetLlmToken get_llm_token = 235; - GetLlmTokenResponse get_llm_token_response = 236; - RefreshLlmToken refresh_llm_token = 259; - LspExtSwitchSourceHeader lsp_ext_switch_source_header = 241; LspExtSwitchSourceHeaderResponse lsp_ext_switch_source_header_response = 242; @@ -419,7 +415,9 @@ message Envelope { reserved 221; reserved 224 to 229; reserved 230 to 231; + reserved 235 to 236; reserved 246; + reserved 259; reserved 270; reserved 247 to 254; reserved 255 to 256; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index a5dd97661f..8be9fed172 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -119,8 +119,6 @@ messages!( (GetTypeDefinitionResponse, Background), (GetImplementation, Background), (GetImplementationResponse, Background), - (GetLlmToken, Background), - (GetLlmTokenResponse, Background), (OpenUnstagedDiff, Foreground), (OpenUnstagedDiffResponse, Foreground), (OpenUncommittedDiff, Foreground), @@ -196,7 +194,6 @@ messages!( (PrepareRenameResponse, Background), (ProjectEntryResponse, Foreground), (RefreshInlayHints, Foreground), - (RefreshLlmToken, Background), (RegisterBufferWithLanguageServers, Background), (RejoinChannelBuffers, Foreground), (RejoinChannelBuffersResponse, Foreground), @@ -354,7 +351,6 @@ request_messages!( (GetDocumentHighlights, GetDocumentHighlightsResponse), (GetDocumentSymbols, GetDocumentSymbolsResponse), (GetHover, GetHoverResponse), - (GetLlmToken, GetLlmTokenResponse), (GetNotifications, GetNotificationsResponse), (GetPrivateUserInfo, GetPrivateUserInfoResponse), (GetProjectSymbols, GetProjectSymbolsResponse), From e452aba9da0cd66ec227371a2466f7a97847d5a9 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 13:59:08 -0400 Subject: [PATCH 380/693] proto: Order `reserved` fields (#36261) This PR orders the `reserved` fields in the RPC `Envelope`, as they had gotten unsorted. Release Notes: - N/A --- crates/proto/proto/zed.proto | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index b6c7fc3cac..7e7bd6b42b 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -417,10 +417,10 @@ message Envelope { reserved 230 to 231; reserved 235 to 236; reserved 246; - reserved 259; - reserved 270; reserved 247 to 254; reserved 255 to 256; + reserved 259; + reserved 270; reserved 280 to 281; reserved 332 to 333; } From bd1fda6782933678be7ed8e39494aba32af871d1 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 14:27:31 -0400 Subject: [PATCH 381/693] proto: Remove `GetPrivateUserInfo` message (#36265) This PR removes the `GetPrivateUserInfo` RPC message. We're no longer using the message after https://github.com/zed-industries/zed/pull/36255. Release Notes: - N/A --- crates/client/src/test.rs | 67 +++++++++++------------------------- crates/collab/src/rpc.rs | 25 -------------- crates/proto/proto/app.proto | 9 ----- crates/proto/proto/zed.proto | 3 +- crates/proto/src/proto.rs | 3 -- 5 files changed, 21 insertions(+), 86 deletions(-) diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index 439fb100d2..3c451fcb01 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -1,16 +1,12 @@ use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore}; use anyhow::{Context as _, Result, anyhow}; -use chrono::Duration; use cloud_api_client::{AuthenticatedUser, GetAuthenticatedUserResponse, PlanInfo}; use cloud_llm_client::{CurrentUsage, Plan, UsageData, UsageLimit}; use futures::{StreamExt, stream::BoxStream}; use gpui::{AppContext as _, BackgroundExecutor, Entity, TestAppContext}; use http_client::{AsyncBody, Method, Request, http}; use parking_lot::Mutex; -use rpc::{ - ConnectionId, Peer, Receipt, TypedEnvelope, - proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse}, -}; +use rpc::{ConnectionId, Peer, Receipt, TypedEnvelope, proto}; use std::sync::Arc; pub struct FakeServer { @@ -187,50 +183,27 @@ impl FakeServer { pub async fn receive(&self) -> Result> { self.executor.start_waiting(); - loop { - let message = self - .state - .lock() - .incoming - .as_mut() - .expect("not connected") - .next() - .await - .context("other half hung up")?; - self.executor.finish_waiting(); - let type_name = message.payload_type_name(); - let message = message.into_any(); + let message = self + .state + .lock() + .incoming + .as_mut() + .expect("not connected") + .next() + .await + .context("other half hung up")?; + self.executor.finish_waiting(); + let type_name = message.payload_type_name(); + let message = message.into_any(); - if message.is::>() { - return Ok(*message.downcast().unwrap()); - } - - let accepted_tos_at = chrono::Utc::now() - .checked_sub_signed(Duration::hours(5)) - .expect("failed to build accepted_tos_at") - .timestamp() as u64; - - if message.is::>() { - self.respond( - message - .downcast::>() - .unwrap() - .receipt(), - GetPrivateUserInfoResponse { - metrics_id: "the-metrics-id".into(), - staff: false, - flags: Default::default(), - accepted_tos_at: Some(accepted_tos_at), - }, - ); - continue; - } - - panic!( - "fake server received unexpected message type: {:?}", - type_name - ); + if message.is::>() { + return Ok(*message.downcast().unwrap()); } + + panic!( + "fake server received unexpected message type: {:?}", + type_name + ); } pub fn respond(&self, receipt: Receipt, response: T::Response) { diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 715ff4e67d..8366b2cf13 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -462,7 +462,6 @@ impl Server { .add_request_handler(follow) .add_message_handler(unfollow) .add_message_handler(update_followers) - .add_request_handler(get_private_user_info) .add_request_handler(accept_terms_of_service) .add_message_handler(acknowledge_channel_message) .add_message_handler(acknowledge_buffer_version) @@ -4209,30 +4208,6 @@ async fn mark_notification_as_read( Ok(()) } -/// Get the current users information -async fn get_private_user_info( - _request: proto::GetPrivateUserInfo, - response: Response, - session: MessageContext, -) -> Result<()> { - let db = session.db().await; - - let metrics_id = db.get_user_metrics_id(session.user_id()).await?; - let user = db - .get_user_by_id(session.user_id()) - .await? - .context("user not found")?; - let flags = db.get_user_flags(session.user_id()).await?; - - response.send(proto::GetPrivateUserInfoResponse { - metrics_id, - staff: user.admin, - flags, - accepted_tos_at: user.accepted_tos_at.map(|t| t.and_utc().timestamp() as u64), - })?; - Ok(()) -} - /// Accept the terms of service (tos) on behalf of the current user async fn accept_terms_of_service( _request: proto::AcceptTermsOfService, diff --git a/crates/proto/proto/app.proto b/crates/proto/proto/app.proto index 353f19adb2..66baf968e3 100644 --- a/crates/proto/proto/app.proto +++ b/crates/proto/proto/app.proto @@ -6,15 +6,6 @@ message UpdateInviteInfo { uint32 count = 2; } -message GetPrivateUserInfo {} - -message GetPrivateUserInfoResponse { - string metrics_id = 1; - bool staff = 2; - repeated string flags = 3; - optional uint64 accepted_tos_at = 4; -} - enum Plan { Free = 0; ZedPro = 1; diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 7e7bd6b42b..8984df2944 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -135,8 +135,6 @@ message Envelope { FollowResponse follow_response = 99; UpdateFollowers update_followers = 100; Unfollow unfollow = 101; - GetPrivateUserInfo get_private_user_info = 102; - GetPrivateUserInfoResponse get_private_user_info_response = 103; UpdateUserPlan update_user_plan = 234; UpdateDiffBases update_diff_bases = 104; AcceptTermsOfService accept_terms_of_service = 239; @@ -402,6 +400,7 @@ message Envelope { } reserved 87 to 88; + reserved 102 to 103; reserved 158 to 161; reserved 164; reserved 166 to 169; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 8be9fed172..82bd1af6db 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -105,8 +105,6 @@ messages!( (GetPathMetadataResponse, Background), (GetPermalinkToLine, Foreground), (GetPermalinkToLineResponse, Foreground), - (GetPrivateUserInfo, Foreground), - (GetPrivateUserInfoResponse, Foreground), (GetProjectSymbols, Background), (GetProjectSymbolsResponse, Background), (GetReferences, Background), @@ -352,7 +350,6 @@ request_messages!( (GetDocumentSymbols, GetDocumentSymbolsResponse), (GetHover, GetHoverResponse), (GetNotifications, GetNotificationsResponse), - (GetPrivateUserInfo, GetPrivateUserInfoResponse), (GetProjectSymbols, GetProjectSymbolsResponse), (GetReferences, GetReferencesResponse), (GetSignatureHelp, GetSignatureHelpResponse), From 3c5d5a1d57f8569fa2818a0538d0ba950036c710 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Fri, 15 Aug 2025 20:34:22 +0200 Subject: [PATCH 382/693] editor: Add access method for `project` (#36266) This resolves a `TODO` that I've stumbled upon too many times whilst looking at the editor code. Release Notes: - N/A --- crates/diagnostics/src/diagnostics_tests.rs | 10 +++--- crates/editor/src/editor.rs | 36 ++++++++++--------- crates/editor/src/editor_tests.rs | 2 +- crates/editor/src/hover_popover.rs | 2 +- crates/editor/src/items.rs | 2 +- crates/editor/src/linked_editing_ranges.rs | 2 +- crates/editor/src/signature_help.rs | 2 +- crates/editor/src/test/editor_test_context.rs | 20 +++++------ crates/git_ui/src/conflict_view.rs | 4 +-- crates/vim/src/command.rs | 4 +-- .../zed/src/zed/edit_prediction_registry.rs | 3 +- 11 files changed, 42 insertions(+), 45 deletions(-) diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 8fb223b2cb..5df1b13897 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -971,7 +971,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) let mut cx = EditorTestContext::new(cx).await; let lsp_store = - cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); + cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store()); cx.set_state(indoc! {" ˇfn func(abc def: i32) -> u32 { @@ -1065,7 +1065,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) { let mut cx = EditorTestContext::new(cx).await; let lsp_store = - cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); + cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store()); cx.set_state(indoc! {" ˇfn func(abc def: i32) -> u32 { @@ -1239,7 +1239,7 @@ async fn test_diagnostics_with_links(cx: &mut TestAppContext) { } "}); let lsp_store = - cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); + cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store()); cx.update(|_, cx| { lsp_store.update(cx, |lsp_store, cx| { @@ -1293,7 +1293,7 @@ async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) fn «test»() { println!(); } "}); let lsp_store = - cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); + cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store()); cx.update(|_, cx| { lsp_store.update(cx, |lsp_store, cx| { lsp_store.update_diagnostics( @@ -1450,7 +1450,7 @@ async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) { let mut cx = EditorTestContext::new(cx).await; let lsp_store = - cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); + cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store()); cx.set_state(indoc! {"error warning info hiˇnt"}); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a9780ed6c2..f77e9ae08c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1039,9 +1039,7 @@ pub struct Editor { inline_diagnostics: Vec<(Anchor, InlineDiagnostic)>, soft_wrap_mode_override: Option, hard_wrap: Option, - - // TODO: make this a access method - pub project: Option>, + project: Option>, semantics_provider: Option>, completion_provider: Option>, collaboration_hub: Option>, @@ -2326,7 +2324,7 @@ impl Editor { editor.go_to_active_debug_line(window, cx); if let Some(buffer) = buffer.read(cx).as_singleton() { - if let Some(project) = editor.project.as_ref() { + if let Some(project) = editor.project() { let handle = project.update(cx, |project, cx| { project.register_buffer_with_language_servers(&buffer, cx) }); @@ -2626,6 +2624,10 @@ impl Editor { &self.buffer } + pub fn project(&self) -> Option<&Entity> { + self.project.as_ref() + } + pub fn workspace(&self) -> Option> { self.workspace.as_ref()?.0.upgrade() } @@ -5212,7 +5214,7 @@ impl Editor { restrict_to_languages: Option<&HashSet>>, cx: &mut Context, ) -> HashMap, clock::Global, Range)> { - let Some(project) = self.project.as_ref() else { + let Some(project) = self.project() else { return HashMap::default(); }; let project = project.read(cx); @@ -5294,7 +5296,7 @@ impl Editor { return None; } - let project = self.project.as_ref()?; + let project = self.project()?; let position = self.selections.newest_anchor().head(); let (buffer, buffer_position) = self .buffer @@ -6141,7 +6143,7 @@ impl Editor { cx: &mut App, ) -> Task> { maybe!({ - let project = self.project.as_ref()?; + let project = self.project()?; let dap_store = project.read(cx).dap_store(); let mut scenarios = vec![]; let resolved_tasks = resolved_tasks.as_ref()?; @@ -7907,7 +7909,7 @@ impl Editor { let snapshot = self.snapshot(window, cx); let multi_buffer_snapshot = &snapshot.display_snapshot.buffer_snapshot; - let Some(project) = self.project.as_ref() else { + let Some(project) = self.project() else { return breakpoint_display_points; }; @@ -10501,7 +10503,7 @@ impl Editor { ) { if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| { let project_path = buffer.read(cx).project_path(cx)?; - let project = self.project.as_ref()?.read(cx); + let project = self.project()?.read(cx); let entry = project.entry_for_path(&project_path, cx)?; let parent = match &entry.canonical_path { Some(canonical_path) => canonical_path.to_path_buf(), @@ -14875,7 +14877,7 @@ impl Editor { self.clear_tasks(); return Task::ready(()); } - let project = self.project.as_ref().map(Entity::downgrade); + let project = self.project().map(Entity::downgrade); let task_sources = self.lsp_task_sources(cx); let multi_buffer = self.buffer.downgrade(); cx.spawn_in(window, async move |editor, cx| { @@ -17054,7 +17056,7 @@ impl Editor { if !pull_diagnostics_settings.enabled { return None; } - let project = self.project.as_ref()?.downgrade(); + let project = self.project()?.downgrade(); let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms); let mut buffers = self.buffer.read(cx).all_buffers(); if let Some(buffer_id) = buffer_id { @@ -18018,7 +18020,7 @@ impl Editor { hunks: impl Iterator, cx: &mut App, ) -> Option<()> { - let project = self.project.as_ref()?; + let project = self.project()?; let buffer = project.read(cx).buffer_for_id(buffer_id, cx)?; let diff = self.buffer.read(cx).diff_for(buffer_id)?; let buffer_snapshot = buffer.read(cx).snapshot(); @@ -18678,7 +18680,7 @@ impl Editor { self.active_excerpt(cx).and_then(|(_, buffer, _)| { let buffer = buffer.read(cx); if let Some(project_path) = buffer.project_path(cx) { - let project = self.project.as_ref()?.read(cx); + let project = self.project()?.read(cx); project.absolute_path(&project_path, cx) } else { buffer @@ -18691,7 +18693,7 @@ impl Editor { fn target_file_path(&self, cx: &mut Context) -> Option { self.active_excerpt(cx).and_then(|(_, buffer, _)| { let project_path = buffer.read(cx).project_path(cx)?; - let project = self.project.as_ref()?.read(cx); + let project = self.project()?.read(cx); let entry = project.entry_for_path(&project_path, cx)?; let path = entry.path.to_path_buf(); Some(path) @@ -18912,7 +18914,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if let Some(project) = self.project.as_ref() { + if let Some(project) = self.project() { let Some(buffer) = self.buffer().read(cx).as_singleton() else { return; }; @@ -19028,7 +19030,7 @@ impl Editor { return Task::ready(Err(anyhow!("failed to determine buffer and selection"))); }; - let Some(project) = self.project.as_ref() else { + let Some(project) = self.project() else { return Task::ready(Err(anyhow!("editor does not have project"))); }; @@ -21015,7 +21017,7 @@ impl Editor { cx: &mut Context, ) { let workspace = self.workspace(); - let project = self.project.as_ref(); + let project = self.project(); let save_tasks = self.buffer().update(cx, |multi_buffer, cx| { let mut tasks = Vec::new(); for (buffer_id, changes) in revert_changes { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index a5966b3301..cf9954bc12 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -15082,7 +15082,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu let mut cx = EditorTestContext::new(cx).await; let lsp_store = - cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); + cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store()); cx.set_state(indoc! {" ˇfn func(abc def: i32) -> u32 { diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index bda229e346..3fc673bad9 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -251,7 +251,7 @@ fn show_hover( let (excerpt_id, _, _) = editor.buffer().read(cx).excerpt_containing(anchor, cx)?; - let language_registry = editor.project.as_ref()?.read(cx).languages().clone(); + let language_registry = editor.project()?.read(cx).languages().clone(); let provider = editor.semantics_provider.clone()?; if !ignore_timeout { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 45a4f7365c..34533002ff 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -678,7 +678,7 @@ impl Item for Editor { let buffer = buffer.read(cx); let path = buffer.project_path(cx)?; let buffer_id = buffer.remote_id(); - let project = self.project.as_ref()?.read(cx); + let project = self.project()?.read(cx); let entry = project.entry_for_path(&path, cx)?; let (repo, repo_path) = project .git_store() diff --git a/crates/editor/src/linked_editing_ranges.rs b/crates/editor/src/linked_editing_ranges.rs index a185de33ca..aaf9032b04 100644 --- a/crates/editor/src/linked_editing_ranges.rs +++ b/crates/editor/src/linked_editing_ranges.rs @@ -51,7 +51,7 @@ pub(super) fn refresh_linked_ranges( if editor.pending_rename.is_some() { return None; } - let project = editor.project.as_ref()?.downgrade(); + let project = editor.project()?.downgrade(); editor.linked_editing_range_task = Some(cx.spawn_in(window, async move |editor, cx| { cx.background_executor().timer(UPDATE_DEBOUNCE).await; diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs index e9f8d2dbd3..e0736a6e9f 100644 --- a/crates/editor/src/signature_help.rs +++ b/crates/editor/src/signature_help.rs @@ -169,7 +169,7 @@ impl Editor { else { return; }; - let Some(lsp_store) = self.project.as_ref().map(|p| p.read(cx).lsp_store()) else { + let Some(lsp_store) = self.project().map(|p| p.read(cx).lsp_store()) else { return; }; let task = lsp_store.update(cx, |lsp_store, cx| { diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index bdf73da5fb..dbb519c40e 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -297,9 +297,8 @@ impl EditorTestContext { pub fn set_head_text(&mut self, diff_base: &str) { self.cx.run_until_parked(); - let fs = self.update_editor(|editor, _, cx| { - editor.project.as_ref().unwrap().read(cx).fs().as_fake() - }); + let fs = + self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake()); let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone()); fs.set_head_for_repo( &Self::root_path().join(".git"), @@ -311,18 +310,16 @@ impl EditorTestContext { pub fn clear_index_text(&mut self) { self.cx.run_until_parked(); - let fs = self.update_editor(|editor, _, cx| { - editor.project.as_ref().unwrap().read(cx).fs().as_fake() - }); + let fs = + self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake()); fs.set_index_for_repo(&Self::root_path().join(".git"), &[]); self.cx.run_until_parked(); } pub fn set_index_text(&mut self, diff_base: &str) { self.cx.run_until_parked(); - let fs = self.update_editor(|editor, _, cx| { - editor.project.as_ref().unwrap().read(cx).fs().as_fake() - }); + let fs = + self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake()); let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone()); fs.set_index_for_repo( &Self::root_path().join(".git"), @@ -333,9 +330,8 @@ impl EditorTestContext { #[track_caller] pub fn assert_index_text(&mut self, expected: Option<&str>) { - let fs = self.update_editor(|editor, _, cx| { - editor.project.as_ref().unwrap().read(cx).fs().as_fake() - }); + let fs = + self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake()); let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone()); let mut found = None; fs.with_git_state(&Self::root_path().join(".git"), false, |git_state| { diff --git a/crates/git_ui/src/conflict_view.rs b/crates/git_ui/src/conflict_view.rs index 0bbb9411be..6482ebb9f8 100644 --- a/crates/git_ui/src/conflict_view.rs +++ b/crates/git_ui/src/conflict_view.rs @@ -112,7 +112,7 @@ fn excerpt_for_buffer_updated( } fn buffer_added(editor: &mut Editor, buffer: Entity, cx: &mut Context) { - let Some(project) = &editor.project else { + let Some(project) = editor.project() else { return; }; let git_store = project.read(cx).git_store().clone(); @@ -469,7 +469,7 @@ pub(crate) fn resolve_conflict( let Some((workspace, project, multibuffer, buffer)) = editor .update(cx, |editor, cx| { let workspace = editor.workspace()?; - let project = editor.project.clone()?; + let project = editor.project()?.clone(); let multibuffer = editor.buffer().clone(); let buffer_id = resolved_conflict.ours.end.buffer_id?; let buffer = multibuffer.read(cx).buffer(buffer_id)?; diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 264fa4bf2f..ce5e5a0300 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -299,7 +299,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, action: &VimSave, window, cx| { vim.update_editor(cx, |_, editor, cx| { - let Some(project) = editor.project.clone() else { + let Some(project) = editor.project().cloned() else { return; }; let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else { @@ -436,7 +436,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { let Some(workspace) = vim.workspace(window) else { return; }; - let Some(project) = editor.project.clone() else { + let Some(project) = editor.project().cloned() else { return; }; let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else { diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index da4b6e78c6..5b0826413b 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -229,8 +229,7 @@ fn assign_edit_prediction_provider( if let Some(file) = buffer.read(cx).file() { let id = file.worktree_id(cx); if let Some(inner_worktree) = editor - .project - .as_ref() + .project() .and_then(|project| project.read(cx).worktree_for_id(id, cx)) { worktree = Some(inner_worktree); From 19318897597071a64282d3bf4e1c4846485e7333 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 15 Aug 2025 14:55:34 -0400 Subject: [PATCH 383/693] thread_view: Move handlers for confirmed completions to the MessageEditor (#36214) Release Notes: - N/A --------- Co-authored-by: Conrad Irwin --- .../agent_ui/src/acp/completion_provider.rs | 435 +++++------------- crates/agent_ui/src/acp/message_editor.rs | 360 ++++++++++++--- crates/agent_ui/src/context_picker.rs | 41 +- crates/editor/src/editor.rs | 28 ++ 4 files changed, 455 insertions(+), 409 deletions(-) diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index adcfab85b1..4ee1eb6948 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -1,38 +1,34 @@ use std::ffi::OsStr; use std::ops::Range; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::sync::Arc; use std::sync::atomic::AtomicBool; -use acp_thread::{MentionUri, selection_name}; +use acp_thread::MentionUri; use anyhow::{Context as _, Result, anyhow}; -use collections::{HashMap, HashSet}; +use collections::HashMap; use editor::display_map::CreaseId; -use editor::{CompletionProvider, Editor, ExcerptId, ToOffset as _}; +use editor::{CompletionProvider, Editor, ExcerptId}; use futures::future::{Shared, try_join_all}; -use futures::{FutureExt, TryFutureExt}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{App, Entity, ImageFormat, Img, Task, WeakEntity}; use http_client::HttpClientWithUrl; -use itertools::Itertools as _; use language::{Buffer, CodeLabel, HighlightId}; use language_model::LanguageModelImage; use lsp::CompletionContext; -use parking_lot::Mutex; use project::{ Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId, }; use prompt_store::PromptStore; use rope::Point; -use text::{Anchor, OffsetRangeExt as _, ToPoint as _}; +use text::{Anchor, ToPoint as _}; use ui::prelude::*; use url::Url; use workspace::Workspace; -use workspace::notifications::NotifyResultExt; use agent::thread_store::{TextThreadStore, ThreadStore}; -use crate::context_picker::fetch_context_picker::fetch_url_content; +use crate::acp::message_editor::MessageEditor; use crate::context_picker::file_context_picker::{FileMatch, search_files}; use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules}; use crate::context_picker::symbol_context_picker::SymbolMatch; @@ -54,7 +50,7 @@ pub struct MentionImage { #[derive(Default)] pub struct MentionSet { - uri_by_crease_id: HashMap, + pub(crate) uri_by_crease_id: HashMap, fetch_results: HashMap>>>, images: HashMap>>>, } @@ -488,36 +484,31 @@ fn search( } pub struct ContextPickerCompletionProvider { - mention_set: Arc>, workspace: WeakEntity, thread_store: WeakEntity, text_thread_store: WeakEntity, - editor: WeakEntity, + message_editor: WeakEntity, } impl ContextPickerCompletionProvider { pub fn new( - mention_set: Arc>, workspace: WeakEntity, thread_store: WeakEntity, text_thread_store: WeakEntity, - editor: WeakEntity, + message_editor: WeakEntity, ) -> Self { Self { - mention_set, workspace, thread_store, text_thread_store, - editor, + message_editor, } } fn completion_for_entry( entry: ContextPickerEntry, - excerpt_id: ExcerptId, source_range: Range, - editor: Entity, - mention_set: Arc>, + message_editor: WeakEntity, workspace: &Entity, cx: &mut App, ) -> Option { @@ -538,88 +529,39 @@ impl ContextPickerCompletionProvider { ContextPickerEntry::Action(action) => { let (new_text, on_action) = match action { ContextPickerAction::AddSelections => { - let selections = selection_ranges(workspace, cx); - const PLACEHOLDER: &str = "selection "; + let selections = selection_ranges(workspace, cx) + .into_iter() + .enumerate() + .map(|(ix, (buffer, range))| { + ( + buffer, + range, + (PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1), + ) + }) + .collect::>(); - let new_text = std::iter::repeat(PLACEHOLDER) - .take(selections.len()) - .chain(std::iter::once("")) - .join(" "); + let new_text: String = PLACEHOLDER.repeat(selections.len()); let callback = Arc::new({ - let mention_set = mention_set.clone(); - let selections = selections.clone(); + let source_range = source_range.clone(); move |_, window: &mut Window, cx: &mut App| { - let editor = editor.clone(); - let mention_set = mention_set.clone(); let selections = selections.clone(); + let message_editor = message_editor.clone(); + let source_range = source_range.clone(); window.defer(cx, move |window, cx| { - let mut current_offset = 0; - - for (buffer, selection_range) in selections { - let snapshot = - editor.read(cx).buffer().read(cx).snapshot(cx); - let Some(start) = snapshot - .anchor_in_excerpt(excerpt_id, source_range.start) - else { - return; - }; - - let offset = start.to_offset(&snapshot) + current_offset; - let text_len = PLACEHOLDER.len() - 1; - - let range = snapshot.anchor_after(offset) - ..snapshot.anchor_after(offset + text_len); - - let path = buffer - .read(cx) - .file() - .map_or(PathBuf::from("untitled"), |file| { - file.path().to_path_buf() - }); - - let point_range = snapshot - .as_singleton() - .map(|(_, _, snapshot)| { - selection_range.to_point(&snapshot) - }) - .unwrap_or_default(); - let line_range = point_range.start.row..point_range.end.row; - - let uri = MentionUri::Selection { - path: path.clone(), - line_range: line_range.clone(), - }; - let crease = crate::context_picker::crease_for_mention( - selection_name(&path, &line_range).into(), - uri.icon_path(cx), - range, - editor.downgrade(), - ); - - let [crease_id]: [_; 1] = - editor.update(cx, |editor, cx| { - let crease_ids = - editor.insert_creases(vec![crease.clone()], cx); - editor.fold_creases( - vec![crease], - false, - window, - cx, - ); - crease_ids.try_into().unwrap() - }); - - mention_set.lock().insert_uri( - crease_id, - MentionUri::Selection { path, line_range }, - ); - - current_offset += text_len + 1; - } + message_editor + .update(cx, |message_editor, cx| { + message_editor.confirm_mention_for_selection( + source_range, + selections, + window, + cx, + ) + }) + .ok(); }); - false } }); @@ -647,11 +589,9 @@ impl ContextPickerCompletionProvider { fn completion_for_thread( thread_entry: ThreadContextEntry, - excerpt_id: ExcerptId, source_range: Range, recent: bool, - editor: Entity, - mention_set: Arc>, + editor: WeakEntity, cx: &mut App, ) -> Completion { let uri = match &thread_entry { @@ -683,13 +623,10 @@ impl ContextPickerCompletionProvider { source: project::CompletionSource::Custom, icon_path: Some(icon_for_completion.clone()), confirm: Some(confirm_completion_callback( - uri.icon_path(cx), thread_entry.title().clone(), - excerpt_id, source_range.start, new_text_len - 1, - editor.clone(), - mention_set, + editor, uri, )), } @@ -697,10 +634,8 @@ impl ContextPickerCompletionProvider { fn completion_for_rules( rule: RulesContextEntry, - excerpt_id: ExcerptId, source_range: Range, - editor: Entity, - mention_set: Arc>, + editor: WeakEntity, cx: &mut App, ) -> Completion { let uri = MentionUri::Rule { @@ -719,13 +654,10 @@ impl ContextPickerCompletionProvider { source: project::CompletionSource::Custom, icon_path: Some(icon_path.clone()), confirm: Some(confirm_completion_callback( - icon_path, rule.title.clone(), - excerpt_id, source_range.start, new_text_len - 1, - editor.clone(), - mention_set, + editor, uri, )), } @@ -736,10 +668,8 @@ impl ContextPickerCompletionProvider { path_prefix: &str, is_recent: bool, is_directory: bool, - excerpt_id: ExcerptId, source_range: Range, - editor: Entity, - mention_set: Arc>, + message_editor: WeakEntity, project: Entity, cx: &mut App, ) -> Option { @@ -777,13 +707,10 @@ impl ContextPickerCompletionProvider { icon_path: Some(completion_icon_path), insert_text_mode: None, confirm: Some(confirm_completion_callback( - crease_icon_path, file_name, - excerpt_id, source_range.start, new_text_len - 1, - editor, - mention_set.clone(), + message_editor, file_uri, )), }) @@ -791,10 +718,8 @@ impl ContextPickerCompletionProvider { fn completion_for_symbol( symbol: Symbol, - excerpt_id: ExcerptId, source_range: Range, - editor: Entity, - mention_set: Arc>, + message_editor: WeakEntity, workspace: Entity, cx: &mut App, ) -> Option { @@ -820,13 +745,10 @@ impl ContextPickerCompletionProvider { icon_path: Some(icon_path.clone()), insert_text_mode: None, confirm: Some(confirm_completion_callback( - icon_path, symbol.name.clone().into(), - excerpt_id, source_range.start, new_text_len - 1, - editor.clone(), - mention_set.clone(), + message_editor, uri, )), }) @@ -835,112 +757,46 @@ impl ContextPickerCompletionProvider { fn completion_for_fetch( source_range: Range, url_to_fetch: SharedString, - excerpt_id: ExcerptId, - editor: Entity, - mention_set: Arc>, + message_editor: WeakEntity, http_client: Arc, cx: &mut App, ) -> Option { let new_text = format!("@fetch {} ", url_to_fetch.clone()); - let new_text_len = new_text.len(); + let url_to_fetch = url::Url::parse(url_to_fetch.as_ref()) + .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}"))) + .ok()?; let mention_uri = MentionUri::Fetch { - url: url::Url::parse(url_to_fetch.as_ref()) - .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}"))) - .ok()?, + url: url_to_fetch.clone(), }; let icon_path = mention_uri.icon_path(cx); Some(Completion { replace_range: source_range.clone(), - new_text, + new_text: new_text.clone(), label: CodeLabel::plain(url_to_fetch.to_string(), None), documentation: None, source: project::CompletionSource::Custom, icon_path: Some(icon_path.clone()), insert_text_mode: None, confirm: Some({ - let start = source_range.start; - let content_len = new_text_len - 1; - let editor = editor.clone(); - let url_to_fetch = url_to_fetch.clone(); - let source_range = source_range.clone(); - let icon_path = icon_path.clone(); - let mention_uri = mention_uri.clone(); Arc::new(move |_, window, cx| { - let Some(url) = url::Url::parse(url_to_fetch.as_ref()) - .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}"))) - .notify_app_err(cx) - else { - return false; - }; - - let editor = editor.clone(); - let mention_set = mention_set.clone(); - let http_client = http_client.clone(); + let url_to_fetch = url_to_fetch.clone(); let source_range = source_range.clone(); - let icon_path = icon_path.clone(); - let mention_uri = mention_uri.clone(); + let message_editor = message_editor.clone(); + let new_text = new_text.clone(); + let http_client = http_client.clone(); window.defer(cx, move |window, cx| { - let url = url.clone(); - - let Some(crease_id) = crate::context_picker::insert_crease_for_mention( - excerpt_id, - start, - content_len, - url.to_string().into(), - icon_path, - editor.clone(), - window, - cx, - ) else { - return; - }; - - let editor = editor.clone(); - let mention_set = mention_set.clone(); - let http_client = http_client.clone(); - let source_range = source_range.clone(); - - let url_string = url.to_string(); - let fetch = cx - .background_executor() - .spawn(async move { - fetch_url_content(http_client, url_string) - .map_err(|e| e.to_string()) - .await + message_editor + .update(cx, |message_editor, cx| { + message_editor.confirm_mention_for_fetch( + new_text, + source_range, + url_to_fetch, + http_client, + window, + cx, + ) }) - .shared(); - mention_set.lock().add_fetch_result(url, fetch.clone()); - - window - .spawn(cx, async move |cx| { - if fetch.await.notify_async_err(cx).is_some() { - mention_set - .lock() - .insert_uri(crease_id, mention_uri.clone()); - } else { - // Remove crease if we failed to fetch - editor - .update(cx, |editor, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let Some(anchor) = snapshot - .anchor_in_excerpt(excerpt_id, source_range.start) - else { - return; - }; - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting( - vec![anchor..anchor], - true, - cx, - ); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - } - Some(()) - }) - .detach(); + .ok(); }); false }) @@ -968,7 +824,7 @@ fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: impl CompletionProvider for ContextPickerCompletionProvider { fn completions( &self, - excerpt_id: ExcerptId, + _excerpt_id: ExcerptId, buffer: &Entity, buffer_position: Anchor, _trigger: CompletionContext, @@ -999,32 +855,18 @@ impl CompletionProvider for ContextPickerCompletionProvider { let thread_store = self.thread_store.clone(); let text_thread_store = self.text_thread_store.clone(); - let editor = self.editor.clone(); + let editor = self.message_editor.clone(); + let Ok((exclude_paths, exclude_threads)) = + self.message_editor.update(cx, |message_editor, cx| { + message_editor.mentioned_path_and_threads(cx) + }) + else { + return Task::ready(Ok(Vec::new())); + }; let MentionCompletion { mode, argument, .. } = state; let query = argument.unwrap_or_else(|| "".to_string()); - let (exclude_paths, exclude_threads) = { - let mention_set = self.mention_set.lock(); - - let mut excluded_paths = HashSet::default(); - let mut excluded_threads = HashSet::default(); - - for uri in mention_set.uri_by_crease_id.values() { - match uri { - MentionUri::File { abs_path, .. } => { - excluded_paths.insert(abs_path.clone()); - } - MentionUri::Thread { id, .. } => { - excluded_threads.insert(id.clone()); - } - _ => {} - } - } - - (excluded_paths, excluded_threads) - }; - let recent_entries = recent_context_picker_entries( Some(thread_store.clone()), Some(text_thread_store.clone()), @@ -1051,13 +893,8 @@ impl CompletionProvider for ContextPickerCompletionProvider { cx, ); - let mention_set = self.mention_set.clone(); - cx.spawn(async move |_, cx| { let matches = search_task.await; - let Some(editor) = editor.upgrade() else { - return Ok(Vec::new()); - }; let completions = cx.update(|cx| { matches @@ -1074,10 +911,8 @@ impl CompletionProvider for ContextPickerCompletionProvider { &mat.path_prefix, is_recent, mat.is_dir, - excerpt_id, source_range.clone(), editor.clone(), - mention_set.clone(), project.clone(), cx, ) @@ -1085,10 +920,8 @@ impl CompletionProvider for ContextPickerCompletionProvider { Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol( symbol, - excerpt_id, source_range.clone(), editor.clone(), - mention_set.clone(), workspace.clone(), cx, ), @@ -1097,39 +930,31 @@ impl CompletionProvider for ContextPickerCompletionProvider { thread, is_recent, .. }) => Some(Self::completion_for_thread( thread, - excerpt_id, source_range.clone(), is_recent, editor.clone(), - mention_set.clone(), cx, )), Match::Rules(user_rules) => Some(Self::completion_for_rules( user_rules, - excerpt_id, source_range.clone(), editor.clone(), - mention_set.clone(), cx, )), Match::Fetch(url) => Self::completion_for_fetch( source_range.clone(), url, - excerpt_id, editor.clone(), - mention_set.clone(), http_client.clone(), cx, ), Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry( entry, - excerpt_id, source_range.clone(), editor.clone(), - mention_set.clone(), &workspace, cx, ), @@ -1182,36 +1007,30 @@ impl CompletionProvider for ContextPickerCompletionProvider { } fn confirm_completion_callback( - crease_icon_path: SharedString, crease_text: SharedString, - excerpt_id: ExcerptId, start: Anchor, content_len: usize, - editor: Entity, - mention_set: Arc>, + message_editor: WeakEntity, mention_uri: MentionUri, ) -> Arc bool + Send + Sync> { Arc::new(move |_, window, cx| { + let message_editor = message_editor.clone(); let crease_text = crease_text.clone(); - let crease_icon_path = crease_icon_path.clone(); - let editor = editor.clone(); - let mention_set = mention_set.clone(); let mention_uri = mention_uri.clone(); window.defer(cx, move |window, cx| { - if let Some(crease_id) = crate::context_picker::insert_crease_for_mention( - excerpt_id, - start, - content_len, - crease_text.clone(), - crease_icon_path, - editor.clone(), - window, - cx, - ) { - mention_set - .lock() - .insert_uri(crease_id, mention_uri.clone()); - } + message_editor + .clone() + .update(cx, |message_editor, cx| { + message_editor.confirm_completion( + crease_text, + start, + content_len, + mention_uri, + window, + cx, + ) + }) + .ok(); }); false }) @@ -1279,13 +1098,13 @@ impl MentionCompletion { #[cfg(test)] mod tests { use super::*; - use editor::AnchorRangeExt; + use editor::{AnchorRangeExt, EditorMode}; use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext}; use project::{Project, ProjectPath}; use serde_json::json; use settings::SettingsStore; use smol::stream::StreamExt as _; - use std::{ops::Deref, path::Path, rc::Rc}; + use std::{ops::Deref, path::Path}; use util::path; use workspace::{AppState, Item}; @@ -1359,9 +1178,9 @@ mod tests { assert_eq!(MentionCompletion::try_parse("test@", 0), None); } - struct AtMentionEditor(Entity); + struct MessageEditorItem(Entity); - impl Item for AtMentionEditor { + impl Item for MessageEditorItem { type Event = (); fn include_in_nav_history() -> bool { @@ -1373,15 +1192,15 @@ mod tests { } } - impl EventEmitter<()> for AtMentionEditor {} + impl EventEmitter<()> for MessageEditorItem {} - impl Focusable for AtMentionEditor { + impl Focusable for MessageEditorItem { fn focus_handle(&self, cx: &App) -> FocusHandle { self.0.read(cx).focus_handle(cx).clone() } } - impl Render for AtMentionEditor { + impl Render for MessageEditorItem { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { self.0.clone().into_any_element() } @@ -1467,19 +1286,28 @@ mod tests { opened_editors.push(buffer); } - let editor = workspace.update_in(&mut cx, |workspace, window, cx| { - let editor = cx.new(|cx| { - Editor::new( - editor::EditorMode::full(), - multi_buffer::MultiBuffer::build_simple("", cx), - None, + let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx)); + let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + + let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| { + let workspace_handle = cx.weak_entity(); + let message_editor = cx.new(|cx| { + MessageEditor::new( + workspace_handle, + project.clone(), + thread_store.clone(), + text_thread_store.clone(), + EditorMode::AutoHeight { + max_lines: None, + min_lines: 1, + }, window, cx, ) }); workspace.active_pane().update(cx, |pane, cx| { pane.add_item( - Box::new(cx.new(|_| AtMentionEditor(editor.clone()))), + Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))), true, true, None, @@ -1487,24 +1315,9 @@ mod tests { cx, ); }); - editor - }); - - let mention_set = Arc::new(Mutex::new(MentionSet::default())); - - let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx)); - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - - let editor_entity = editor.downgrade(); - editor.update_in(&mut cx, |editor, window, cx| { - window.focus(&editor.focus_handle(cx)); - editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new( - mention_set.clone(), - workspace.downgrade(), - thread_store.downgrade(), - text_thread_store.downgrade(), - editor_entity, - )))); + message_editor.read(cx).focus_handle(cx).focus(window); + let editor = message_editor.read(cx).editor().clone(); + (message_editor, editor) }); cx.simulate_input("Lorem "); @@ -1573,9 +1386,9 @@ mod tests { ); }); - let contents = cx - .update(|window, cx| { - mention_set.lock().contents( + let contents = message_editor + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( project.clone(), thread_store.clone(), text_thread_store.clone(), @@ -1641,9 +1454,9 @@ mod tests { cx.run_until_parked(); - let contents = cx - .update(|window, cx| { - mention_set.lock().contents( + let contents = message_editor + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( project.clone(), thread_store.clone(), text_thread_store.clone(), @@ -1765,9 +1578,9 @@ mod tests { editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); }); - let contents = cx - .update(|window, cx| { - mention_set.lock().contents( + let contents = message_editor + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( project.clone(), thread_store, text_thread_store, diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 8d512948dd..32c37da519 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1,56 +1,55 @@ -use crate::acp::completion_provider::ContextPickerCompletionProvider; -use crate::acp::completion_provider::MentionImage; -use crate::acp::completion_provider::MentionSet; -use acp_thread::MentionUri; -use agent::TextThreadStore; -use agent::ThreadStore; +use crate::{ + acp::completion_provider::{ContextPickerCompletionProvider, MentionImage, MentionSet}, + context_picker::fetch_context_picker::fetch_url_content, +}; +use acp_thread::{MentionUri, selection_name}; +use agent::{TextThreadStore, ThreadId, ThreadStore}; use agent_client_protocol as acp; use anyhow::Result; use collections::HashSet; -use editor::ExcerptId; -use editor::actions::Paste; -use editor::display_map::CreaseId; use editor::{ - AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode, - EditorStyle, MultiBuffer, + Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, + EditorMode, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, ToOffset, + actions::Paste, + display_map::{Crease, CreaseId, FoldId}, }; -use futures::FutureExt as _; -use gpui::ClipboardEntry; -use gpui::Image; -use gpui::ImageFormat; +use futures::{FutureExt as _, TryFutureExt as _}; use gpui::{ - AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, Task, TextStyle, WeakEntity, + AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, Image, + ImageFormat, Task, TextStyle, WeakEntity, }; -use language::Buffer; -use language::Language; +use http_client::HttpClientWithUrl; +use language::{Buffer, Language}; use language_model::LanguageModelImage; -use parking_lot::Mutex; use project::{CompletionIntent, Project}; use settings::Settings; -use std::fmt::Write; -use std::path::Path; -use std::rc::Rc; -use std::sync::Arc; +use std::{ + fmt::Write, + ops::Range, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, +}; +use text::OffsetRangeExt; use theme::ThemeSettings; -use ui::IconName; -use ui::SharedString; use ui::{ - ActiveTheme, App, InteractiveElement, IntoElement, ParentElement, Render, Styled, TextSize, - Window, div, + ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Icon, IconName, + IconSize, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement, + Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div, + h_flex, }; use util::ResultExt; -use workspace::Workspace; -use workspace::notifications::NotifyResultExt as _; +use workspace::{Workspace, notifications::NotifyResultExt as _}; use zed_actions::agent::Chat; use super::completion_provider::Mention; pub struct MessageEditor { + mention_set: MentionSet, editor: Entity, project: Entity, thread_store: Entity, text_thread_store: Entity, - mention_set: Arc>, } pub enum MessageEditorEvent { @@ -77,8 +76,13 @@ impl MessageEditor { }, None, ); - - let mention_set = Arc::new(Mutex::new(MentionSet::default())); + let completion_provider = ContextPickerCompletionProvider::new( + workspace, + thread_store.downgrade(), + text_thread_store.downgrade(), + cx.weak_entity(), + ); + let mention_set = MentionSet::default(); let editor = cx.new(|cx| { let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx)); let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); @@ -88,13 +92,7 @@ impl MessageEditor { editor.set_show_indent_guides(false, cx); editor.set_soft_wrap(); editor.set_use_modal_editing(true); - editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new( - mention_set.clone(), - workspace, - thread_store.downgrade(), - text_thread_store.downgrade(), - cx.weak_entity(), - )))); + editor.set_completion_provider(Some(Rc::new(completion_provider))); editor.set_context_menu_options(ContextMenuOptions { min_entries_visible: 12, max_entries_visible: 12, @@ -112,16 +110,202 @@ impl MessageEditor { } } + #[cfg(test)] + pub(crate) fn editor(&self) -> &Entity { + &self.editor + } + + #[cfg(test)] + pub(crate) fn mention_set(&mut self) -> &mut MentionSet { + &mut self.mention_set + } + pub fn is_empty(&self, cx: &App) -> bool { self.editor.read(cx).is_empty(cx) } + pub fn mentioned_path_and_threads(&self, _: &App) -> (HashSet, HashSet) { + let mut excluded_paths = HashSet::default(); + let mut excluded_threads = HashSet::default(); + + for uri in self.mention_set.uri_by_crease_id.values() { + match uri { + MentionUri::File { abs_path, .. } => { + excluded_paths.insert(abs_path.clone()); + } + MentionUri::Thread { id, .. } => { + excluded_threads.insert(id.clone()); + } + _ => {} + } + } + + (excluded_paths, excluded_threads) + } + + pub fn confirm_completion( + &mut self, + crease_text: SharedString, + start: text::Anchor, + content_len: usize, + mention_uri: MentionUri, + window: &mut Window, + cx: &mut Context, + ) { + let snapshot = self + .editor + .update(cx, |editor, cx| editor.snapshot(window, cx)); + let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else { + return; + }; + + if let Some(crease_id) = crate::context_picker::insert_crease_for_mention( + *excerpt_id, + start, + content_len, + crease_text.clone(), + mention_uri.icon_path(cx), + self.editor.clone(), + window, + cx, + ) { + self.mention_set.insert_uri(crease_id, mention_uri.clone()); + } + } + + pub fn confirm_mention_for_fetch( + &mut self, + new_text: String, + source_range: Range, + url: url::Url, + http_client: Arc, + window: &mut Window, + cx: &mut Context, + ) { + let mention_uri = MentionUri::Fetch { url: url.clone() }; + let icon_path = mention_uri.icon_path(cx); + + let start = source_range.start; + let content_len = new_text.len() - 1; + + let snapshot = self + .editor + .update(cx, |editor, cx| editor.snapshot(window, cx)); + let Some((&excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else { + return; + }; + + let Some(crease_id) = crate::context_picker::insert_crease_for_mention( + excerpt_id, + start, + content_len, + url.to_string().into(), + icon_path, + self.editor.clone(), + window, + cx, + ) else { + return; + }; + + let http_client = http_client.clone(); + let source_range = source_range.clone(); + + let url_string = url.to_string(); + let fetch = cx + .background_executor() + .spawn(async move { + fetch_url_content(http_client, url_string) + .map_err(|e| e.to_string()) + .await + }) + .shared(); + self.mention_set.add_fetch_result(url, fetch.clone()); + + cx.spawn_in(window, async move |this, cx| { + let fetch = fetch.await.notify_async_err(cx); + this.update(cx, |this, cx| { + if fetch.is_some() { + this.mention_set.insert_uri(crease_id, mention_uri.clone()); + } else { + // Remove crease if we failed to fetch + this.editor.update(cx, |editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let Some(anchor) = + snapshot.anchor_in_excerpt(excerpt_id, source_range.start) + else { + return; + }; + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }); + } + }) + .ok(); + }) + .detach(); + } + + pub fn confirm_mention_for_selection( + &mut self, + source_range: Range, + selections: Vec<(Entity, Range, Range)>, + window: &mut Window, + cx: &mut Context, + ) { + let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx); + let Some((&excerpt_id, _, _)) = snapshot.as_singleton() else { + return; + }; + let Some(start) = snapshot.anchor_in_excerpt(excerpt_id, source_range.start) else { + return; + }; + + let offset = start.to_offset(&snapshot); + + for (buffer, selection_range, range_to_fold) in selections { + let range = snapshot.anchor_after(offset + range_to_fold.start) + ..snapshot.anchor_after(offset + range_to_fold.end); + + let path = buffer + .read(cx) + .file() + .map_or(PathBuf::from("untitled"), |file| file.path().to_path_buf()); + let snapshot = buffer.read(cx).snapshot(); + + let point_range = selection_range.to_point(&snapshot); + let line_range = point_range.start.row..point_range.end.row; + + let uri = MentionUri::Selection { + path: path.clone(), + line_range: line_range.clone(), + }; + let crease = crate::context_picker::crease_for_mention( + selection_name(&path, &line_range).into(), + uri.icon_path(cx), + range, + self.editor.downgrade(), + ); + + let crease_id = self.editor.update(cx, |editor, cx| { + let crease_ids = editor.insert_creases(vec![crease.clone()], cx); + editor.fold_creases(vec![crease], false, window, cx); + crease_ids.first().copied().unwrap() + }); + + self.mention_set + .insert_uri(crease_id, MentionUri::Selection { path, line_range }); + } + } + pub fn contents( &self, window: &mut Window, cx: &mut Context, ) -> Task>> { - let contents = self.mention_set.lock().contents( + let contents = self.mention_set.contents( self.project.clone(), self.thread_store.clone(), self.text_thread_store.clone(), @@ -198,7 +382,7 @@ impl MessageEditor { pub fn clear(&mut self, window: &mut Window, cx: &mut Context) { self.editor.update(cx, |editor, cx| { editor.clear(window, cx); - editor.remove_creases(self.mention_set.lock().drain(), cx) + editor.remove_creases(self.mention_set.drain(), cx) }); } @@ -267,9 +451,6 @@ impl MessageEditor { cx: &mut Context, ) { let buffer = self.editor.read(cx).buffer().clone(); - let Some((&excerpt_id, _, _)) = buffer.read(cx).snapshot(cx).as_singleton() else { - return; - }; let Some(buffer) = buffer.read(cx).as_singleton() else { return; }; @@ -292,10 +473,8 @@ impl MessageEditor { &path_prefix, false, entry.is_dir(), - excerpt_id, anchor..anchor, - self.editor.clone(), - self.mention_set.clone(), + cx.weak_entity(), self.project.clone(), cx, ) else { @@ -331,6 +510,7 @@ impl MessageEditor { excerpt_id, crease_start, content_len, + abs_path.clone(), self.editor.clone(), window, cx, @@ -375,7 +555,7 @@ impl MessageEditor { }) .detach(); - self.mention_set.lock().insert_image(crease_id, task); + self.mention_set.insert_image(crease_id, task); }); } @@ -429,7 +609,7 @@ impl MessageEditor { editor.buffer().read(cx).snapshot(cx) }); - self.mention_set.lock().clear(); + self.mention_set.clear(); for (range, mention_uri) in mentions { let anchor = snapshot.anchor_before(range.start); let crease_id = crate::context_picker::insert_crease_for_mention( @@ -444,7 +624,7 @@ impl MessageEditor { ); if let Some(crease_id) = crease_id { - self.mention_set.lock().insert_uri(crease_id, mention_uri); + self.mention_set.insert_uri(crease_id, mention_uri); } } for (range, content) in images { @@ -479,7 +659,7 @@ impl MessageEditor { let data: SharedString = content.data.to_string().into(); if let Some(crease_id) = crease_id { - self.mention_set.lock().insert_image( + self.mention_set.insert_image( crease_id, Task::ready(Ok(MentionImage { abs_path, @@ -550,20 +730,78 @@ pub(crate) fn insert_crease_for_image( excerpt_id: ExcerptId, anchor: text::Anchor, content_len: usize, + abs_path: Option>, editor: Entity, window: &mut Window, cx: &mut App, ) -> Option { - crate::context_picker::insert_crease_for_mention( - excerpt_id, - anchor, - content_len, - "Image".into(), - IconName::Image.path().into(), - editor, - window, - cx, - ) + let crease_label = abs_path + .as_ref() + .and_then(|path| path.file_name()) + .map(|name| name.to_string_lossy().to_string().into()) + .unwrap_or(SharedString::from("Image")); + + editor.update(cx, |editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + + let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?; + + let start = start.bias_right(&snapshot); + let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len); + + let placeholder = FoldPlaceholder { + render: render_image_fold_icon_button(crease_label, cx.weak_entity()), + merge_adjacent: false, + ..Default::default() + }; + + let crease = Crease::Inline { + range: start..end, + placeholder, + render_toggle: None, + render_trailer: None, + metadata: None, + }; + + let ids = editor.insert_creases(vec![crease.clone()], cx); + editor.fold_creases(vec![crease], false, window, cx); + + Some(ids[0]) + }) +} + +fn render_image_fold_icon_button( + label: SharedString, + editor: WeakEntity, +) -> Arc, &mut App) -> AnyElement> { + Arc::new({ + move |fold_id, fold_range, cx| { + let is_in_text_selection = editor + .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx)) + .unwrap_or_default(); + + ButtonLike::new(fold_id) + .style(ButtonStyle::Filled) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .toggle_state(is_in_text_selection) + .child( + h_flex() + .gap_1() + .child( + Icon::new(IconName::Image) + .size(IconSize::XSmall) + .color(Color::Muted), + ) + .child( + Label::new(label.clone()) + .size(LabelSize::Small) + .buffer_font(cx) + .single_line(), + ), + ) + .into_any_element() + } + }) } #[cfg(test)] diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index 7dc00bfae2..6c5546c6bb 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -13,7 +13,7 @@ use anyhow::{Result, anyhow}; use collections::HashSet; pub use completion_provider::ContextPickerCompletionProvider; use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId}; -use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset}; +use editor::{Anchor, Editor, ExcerptId, FoldPlaceholder, ToOffset}; use fetch_context_picker::FetchContextPicker; use file_context_picker::FileContextPicker; use file_context_picker::render_file_context_entry; @@ -837,42 +837,9 @@ fn render_fold_icon_button( ) -> Arc, &mut App) -> AnyElement> { Arc::new({ move |fold_id, fold_range, cx| { - let is_in_text_selection = editor.upgrade().is_some_and(|editor| { - editor.update(cx, |editor, cx| { - let snapshot = editor - .buffer() - .update(cx, |multi_buffer, cx| multi_buffer.snapshot(cx)); - - let is_in_pending_selection = || { - editor - .selections - .pending - .as_ref() - .is_some_and(|pending_selection| { - pending_selection - .selection - .range() - .includes(&fold_range, &snapshot) - }) - }; - - let mut is_in_complete_selection = || { - editor - .selections - .disjoint_in_range::(fold_range.clone(), cx) - .into_iter() - .any(|selection| { - // This is needed to cover a corner case, if we just check for an existing - // selection in the fold range, having a cursor at the start of the fold - // marks it as selected. Non-empty selections don't cause this. - let length = selection.end - selection.start; - length > 0 - }) - }; - - is_in_pending_selection() || is_in_complete_selection() - }) - }); + let is_in_text_selection = editor + .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx)) + .unwrap_or_default(); ButtonLike::new(fold_id) .style(ButtonStyle::Filled) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f77e9ae08c..85f2e01ed4 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2369,6 +2369,34 @@ impl Editor { .is_some_and(|menu| menu.context_menu.focus_handle(cx).is_focused(window)) } + pub fn is_range_selected(&mut self, range: &Range, cx: &mut Context) -> bool { + if self + .selections + .pending + .as_ref() + .is_some_and(|pending_selection| { + let snapshot = self.buffer().read(cx).snapshot(cx); + pending_selection + .selection + .range() + .includes(&range, &snapshot) + }) + { + return true; + } + + self.selections + .disjoint_in_range::(range.clone(), cx) + .into_iter() + .any(|selection| { + // This is needed to cover a corner case, if we just check for an existing + // selection in the fold range, having a cursor at the start of the fold + // marks it as selected. Non-empty selections don't cause this. + let length = selection.end - selection.start; + length > 0 + }) + } + pub fn key_context(&self, window: &Window, cx: &App) -> KeyContext { self.key_context_internal(self.has_active_edit_prediction(), window, cx) } From b3cad8b527c773c3a541e1a9e3ff23a8fbbae548 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 15:21:04 -0400 Subject: [PATCH 384/693] proto: Remove `UpdateUserPlan` message (#36268) This PR removes the `UpdateUserPlan` RPC message. We're no longer using the message after https://github.com/zed-industries/zed/pull/36255. Release Notes: - N/A --- crates/client/src/user.rs | 21 ---- crates/collab/src/llm.rs | 8 -- crates/collab/src/rpc.rs | 223 ----------------------------------- crates/proto/proto/app.proto | 10 -- crates/proto/proto/zed.proto | 3 +- crates/proto/src/proto.rs | 1 - 6 files changed, 1 insertion(+), 265 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index faf46945d8..33a240eca1 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -177,7 +177,6 @@ impl UserStore { let (mut current_user_tx, current_user_rx) = watch::channel(); let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded(); let rpc_subscriptions = vec![ - client.add_message_handler(cx.weak_entity(), Self::handle_update_plan), client.add_message_handler(cx.weak_entity(), Self::handle_update_contacts), client.add_message_handler(cx.weak_entity(), Self::handle_update_invite_info), client.add_message_handler(cx.weak_entity(), Self::handle_show_contacts), @@ -343,26 +342,6 @@ impl UserStore { Ok(()) } - async fn handle_update_plan( - this: Entity, - _message: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let client = this - .read_with(&cx, |this, _| this.client.upgrade())? - .context("client was dropped")?; - - let response = client - .cloud_client() - .get_authenticated_user() - .await - .context("failed to fetch authenticated user")?; - - this.update(&mut cx, |this, cx| { - this.update_authenticated_user(response, cx); - }) - } - fn update_contacts(&mut self, message: UpdateContacts, cx: &Context) -> Task> { match message { UpdateContacts::Wait(barrier) => { diff --git a/crates/collab/src/llm.rs b/crates/collab/src/llm.rs index ca8e89bc6d..dec10232bd 100644 --- a/crates/collab/src/llm.rs +++ b/crates/collab/src/llm.rs @@ -1,9 +1 @@ pub mod db; - -pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial"; - -/// The name of the feature flag that bypasses the account age check. -pub const BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG: &str = "bypass-account-age-check"; - -/// The minimum account age an account must have in order to use the LLM service. -pub const MIN_ACCOUNT_AGE_FOR_LLM_USE: chrono::Duration = chrono::Duration::days(30); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 8366b2cf13..957cc30fe6 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1,12 +1,6 @@ mod connection_pool; use crate::api::{CloudflareIpCountryHeader, SystemIdHeader}; -use crate::db::billing_subscription::SubscriptionKind; -use crate::llm::db::LlmDatabase; -use crate::llm::{ - AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG, - MIN_ACCOUNT_AGE_FOR_LLM_USE, -}; use crate::{ AppState, Error, Result, auth, db::{ @@ -146,13 +140,6 @@ pub enum Principal { } impl Principal { - fn user(&self) -> &User { - match self { - Principal::User(user) => user, - Principal::Impersonated { user, .. } => user, - } - } - fn update_span(&self, span: &tracing::Span) { match &self { Principal::User(user) => { @@ -997,8 +984,6 @@ impl Server { .await?; } - update_user_plan(session).await?; - let contacts = self.app_state.db.get_contacts(user.id).await?; { @@ -2832,214 +2817,6 @@ fn should_auto_subscribe_to_channels(version: ZedVersion) -> bool { version.0.minor() < 139 } -async fn current_plan(db: &Arc, user_id: UserId, is_staff: bool) -> Result { - if is_staff { - return Ok(proto::Plan::ZedPro); - } - - let subscription = db.get_active_billing_subscription(user_id).await?; - let subscription_kind = subscription.and_then(|subscription| subscription.kind); - - let plan = if let Some(subscription_kind) = subscription_kind { - match subscription_kind { - SubscriptionKind::ZedPro => proto::Plan::ZedPro, - SubscriptionKind::ZedProTrial => proto::Plan::ZedProTrial, - SubscriptionKind::ZedFree => proto::Plan::Free, - } - } else { - proto::Plan::Free - }; - - Ok(plan) -} - -async fn make_update_user_plan_message( - user: &User, - is_staff: bool, - db: &Arc, - llm_db: Option>, -) -> Result { - let feature_flags = db.get_user_flags(user.id).await?; - let plan = current_plan(db, user.id, is_staff).await?; - let billing_customer = db.get_billing_customer_by_user_id(user.id).await?; - let billing_preferences = db.get_billing_preferences(user.id).await?; - - let (subscription_period, usage) = if let Some(llm_db) = llm_db { - let subscription = db.get_active_billing_subscription(user.id).await?; - - let subscription_period = - crate::db::billing_subscription::Model::current_period(subscription, is_staff); - - let usage = if let Some((period_start_at, period_end_at)) = subscription_period { - llm_db - .get_subscription_usage_for_period(user.id, period_start_at, period_end_at) - .await? - } else { - None - }; - - (subscription_period, usage) - } else { - (None, None) - }; - - let bypass_account_age_check = feature_flags - .iter() - .any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG); - let account_too_young = !matches!(plan, proto::Plan::ZedPro) - && !bypass_account_age_check - && user.account_age() < MIN_ACCOUNT_AGE_FOR_LLM_USE; - - Ok(proto::UpdateUserPlan { - plan: plan.into(), - trial_started_at: billing_customer - .as_ref() - .and_then(|billing_customer| billing_customer.trial_started_at) - .map(|trial_started_at| trial_started_at.and_utc().timestamp() as u64), - is_usage_based_billing_enabled: if is_staff { - Some(true) - } else { - billing_preferences.map(|preferences| preferences.model_request_overages_enabled) - }, - subscription_period: subscription_period.map(|(started_at, ended_at)| { - proto::SubscriptionPeriod { - started_at: started_at.timestamp() as u64, - ended_at: ended_at.timestamp() as u64, - } - }), - account_too_young: Some(account_too_young), - has_overdue_invoices: billing_customer - .map(|billing_customer| billing_customer.has_overdue_invoices), - usage: Some( - usage - .map(|usage| subscription_usage_to_proto(plan, usage, &feature_flags)) - .unwrap_or_else(|| make_default_subscription_usage(plan, &feature_flags)), - ), - }) -} - -fn model_requests_limit( - plan: cloud_llm_client::Plan, - feature_flags: &Vec, -) -> cloud_llm_client::UsageLimit { - match plan.model_requests_limit() { - cloud_llm_client::UsageLimit::Limited(limit) => { - let limit = if plan == cloud_llm_client::Plan::ZedProTrial - && feature_flags - .iter() - .any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG) - { - 1_000 - } else { - limit - }; - - cloud_llm_client::UsageLimit::Limited(limit) - } - cloud_llm_client::UsageLimit::Unlimited => cloud_llm_client::UsageLimit::Unlimited, - } -} - -fn subscription_usage_to_proto( - plan: proto::Plan, - usage: crate::llm::db::subscription_usage::Model, - feature_flags: &Vec, -) -> proto::SubscriptionUsage { - let plan = match plan { - proto::Plan::Free => cloud_llm_client::Plan::ZedFree, - proto::Plan::ZedPro => cloud_llm_client::Plan::ZedPro, - proto::Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial, - }; - - proto::SubscriptionUsage { - model_requests_usage_amount: usage.model_requests as u32, - model_requests_usage_limit: Some(proto::UsageLimit { - variant: Some(match model_requests_limit(plan, feature_flags) { - cloud_llm_client::UsageLimit::Limited(limit) => { - proto::usage_limit::Variant::Limited(proto::usage_limit::Limited { - limit: limit as u32, - }) - } - cloud_llm_client::UsageLimit::Unlimited => { - proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {}) - } - }), - }), - edit_predictions_usage_amount: usage.edit_predictions as u32, - edit_predictions_usage_limit: Some(proto::UsageLimit { - variant: Some(match plan.edit_predictions_limit() { - cloud_llm_client::UsageLimit::Limited(limit) => { - proto::usage_limit::Variant::Limited(proto::usage_limit::Limited { - limit: limit as u32, - }) - } - cloud_llm_client::UsageLimit::Unlimited => { - proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {}) - } - }), - }), - } -} - -fn make_default_subscription_usage( - plan: proto::Plan, - feature_flags: &Vec, -) -> proto::SubscriptionUsage { - let plan = match plan { - proto::Plan::Free => cloud_llm_client::Plan::ZedFree, - proto::Plan::ZedPro => cloud_llm_client::Plan::ZedPro, - proto::Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial, - }; - - proto::SubscriptionUsage { - model_requests_usage_amount: 0, - model_requests_usage_limit: Some(proto::UsageLimit { - variant: Some(match model_requests_limit(plan, feature_flags) { - cloud_llm_client::UsageLimit::Limited(limit) => { - proto::usage_limit::Variant::Limited(proto::usage_limit::Limited { - limit: limit as u32, - }) - } - cloud_llm_client::UsageLimit::Unlimited => { - proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {}) - } - }), - }), - edit_predictions_usage_amount: 0, - edit_predictions_usage_limit: Some(proto::UsageLimit { - variant: Some(match plan.edit_predictions_limit() { - cloud_llm_client::UsageLimit::Limited(limit) => { - proto::usage_limit::Variant::Limited(proto::usage_limit::Limited { - limit: limit as u32, - }) - } - cloud_llm_client::UsageLimit::Unlimited => { - proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {}) - } - }), - }), - } -} - -async fn update_user_plan(session: &Session) -> Result<()> { - let db = session.db().await; - - let update_user_plan = make_update_user_plan_message( - session.principal.user(), - session.is_staff(), - &db.0, - session.app_state.llm_db.clone(), - ) - .await?; - - session - .peer - .send(session.connection_id, update_user_plan) - .trace_err(); - - Ok(()) -} - async fn subscribe_to_channels( _: proto::SubscribeToChannels, session: MessageContext, diff --git a/crates/proto/proto/app.proto b/crates/proto/proto/app.proto index 66baf968e3..fe6f7be1b0 100644 --- a/crates/proto/proto/app.proto +++ b/crates/proto/proto/app.proto @@ -12,16 +12,6 @@ enum Plan { ZedProTrial = 2; } -message UpdateUserPlan { - Plan plan = 1; - optional uint64 trial_started_at = 2; - optional bool is_usage_based_billing_enabled = 3; - optional SubscriptionUsage usage = 4; - optional SubscriptionPeriod subscription_period = 5; - optional bool account_too_young = 6; - optional bool has_overdue_invoices = 7; -} - message SubscriptionPeriod { uint64 started_at = 1; uint64 ended_at = 2; diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 8984df2944..4b023a46bc 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -135,7 +135,6 @@ message Envelope { FollowResponse follow_response = 99; UpdateFollowers update_followers = 100; Unfollow unfollow = 101; - UpdateUserPlan update_user_plan = 234; UpdateDiffBases update_diff_bases = 104; AcceptTermsOfService accept_terms_of_service = 239; AcceptTermsOfServiceResponse accept_terms_of_service_response = 240; @@ -414,7 +413,7 @@ message Envelope { reserved 221; reserved 224 to 229; reserved 230 to 231; - reserved 235 to 236; + reserved 234 to 236; reserved 246; reserved 247 to 254; reserved 255 to 256; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 82bd1af6db..18abf31c64 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -275,7 +275,6 @@ messages!( (UpdateProject, Foreground), (UpdateProjectCollaborator, Foreground), (UpdateUserChannels, Foreground), - (UpdateUserPlan, Foreground), (UpdateWorktree, Foreground), (UpdateWorktreeSettings, Foreground), (UpdateRepository, Foreground), From 75f85b3aaa202f07185a39d855143851f609ddf7 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 15 Aug 2025 15:37:52 -0400 Subject: [PATCH 385/693] Remove old telemetry events and transformation layer (#36263) Successor to: https://github.com/zed-industries/zed/pull/25179 Release Notes: - N/A --- crates/collab/src/api/events.rs | 166 +----------------- .../telemetry_events/src/telemetry_events.rs | 108 +----------- 2 files changed, 4 insertions(+), 270 deletions(-) diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index 2f34a843a8..cd1dc42e64 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -564,170 +564,10 @@ fn for_snowflake( country_code: Option, checksum_matched: bool, ) -> impl Iterator { - body.events.into_iter().filter_map(move |event| { + body.events.into_iter().map(move |event| { let timestamp = first_event_at + Duration::milliseconds(event.milliseconds_since_first_event); - // We will need to double check, but I believe all of the events that - // are being transformed here are now migrated over to use the - // telemetry::event! macro, as of this commit so this code can go away - // when we feel enough users have upgraded past this point. let (event_type, mut event_properties) = match &event.event { - Event::Editor(e) => ( - match e.operation.as_str() { - "open" => "Editor Opened".to_string(), - "save" => "Editor Saved".to_string(), - _ => format!("Unknown Editor Event: {}", e.operation), - }, - serde_json::to_value(e).unwrap(), - ), - Event::EditPrediction(e) => ( - format!( - "Edit Prediction {}", - if e.suggestion_accepted { - "Accepted" - } else { - "Discarded" - } - ), - serde_json::to_value(e).unwrap(), - ), - Event::EditPredictionRating(e) => ( - "Edit Prediction Rated".to_string(), - serde_json::to_value(e).unwrap(), - ), - Event::Call(e) => { - let event_type = match e.operation.trim() { - "unshare project" => "Project Unshared".to_string(), - "open channel notes" => "Channel Notes Opened".to_string(), - "share project" => "Project Shared".to_string(), - "join channel" => "Channel Joined".to_string(), - "hang up" => "Call Ended".to_string(), - "accept incoming" => "Incoming Call Accepted".to_string(), - "invite" => "Participant Invited".to_string(), - "disable microphone" => "Microphone Disabled".to_string(), - "enable microphone" => "Microphone Enabled".to_string(), - "enable screen share" => "Screen Share Enabled".to_string(), - "disable screen share" => "Screen Share Disabled".to_string(), - "decline incoming" => "Incoming Call Declined".to_string(), - _ => format!("Unknown Call Event: {}", e.operation), - }; - - (event_type, serde_json::to_value(e).unwrap()) - } - Event::Assistant(e) => ( - match e.phase { - telemetry_events::AssistantPhase::Response => "Assistant Responded".to_string(), - telemetry_events::AssistantPhase::Invoked => "Assistant Invoked".to_string(), - telemetry_events::AssistantPhase::Accepted => { - "Assistant Response Accepted".to_string() - } - telemetry_events::AssistantPhase::Rejected => { - "Assistant Response Rejected".to_string() - } - }, - serde_json::to_value(e).unwrap(), - ), - Event::Cpu(_) | Event::Memory(_) => return None, - Event::App(e) => { - let mut properties = json!({}); - let event_type = match e.operation.trim() { - // App - "open" => "App Opened".to_string(), - "first open" => "App First Opened".to_string(), - "first open for release channel" => { - "App First Opened For Release Channel".to_string() - } - "close" => "App Closed".to_string(), - - // Project - "open project" => "Project Opened".to_string(), - "open node project" => { - properties["project_type"] = json!("node"); - "Project Opened".to_string() - } - "open pnpm project" => { - properties["project_type"] = json!("pnpm"); - "Project Opened".to_string() - } - "open yarn project" => { - properties["project_type"] = json!("yarn"); - "Project Opened".to_string() - } - - // SSH - "create ssh server" => "SSH Server Created".to_string(), - "create ssh project" => "SSH Project Created".to_string(), - "open ssh project" => "SSH Project Opened".to_string(), - - // Welcome Page - "welcome page: change keymap" => "Welcome Keymap Changed".to_string(), - "welcome page: change theme" => "Welcome Theme Changed".to_string(), - "welcome page: close" => "Welcome Page Closed".to_string(), - "welcome page: edit settings" => "Welcome Settings Edited".to_string(), - "welcome page: install cli" => "Welcome CLI Installed".to_string(), - "welcome page: open" => "Welcome Page Opened".to_string(), - "welcome page: open extensions" => "Welcome Extensions Page Opened".to_string(), - "welcome page: sign in to copilot" => "Welcome Copilot Signed In".to_string(), - "welcome page: toggle diagnostic telemetry" => { - "Welcome Diagnostic Telemetry Toggled".to_string() - } - "welcome page: toggle metric telemetry" => { - "Welcome Metric Telemetry Toggled".to_string() - } - "welcome page: toggle vim" => "Welcome Vim Mode Toggled".to_string(), - "welcome page: view docs" => "Welcome Documentation Viewed".to_string(), - - // Extensions - "extensions page: open" => "Extensions Page Opened".to_string(), - "extensions: install extension" => "Extension Installed".to_string(), - "extensions: uninstall extension" => "Extension Uninstalled".to_string(), - - // Misc - "markdown preview: open" => "Markdown Preview Opened".to_string(), - "project diagnostics: open" => "Project Diagnostics Opened".to_string(), - "project search: open" => "Project Search Opened".to_string(), - "repl sessions: open" => "REPL Session Started".to_string(), - - // Feature Upsell - "feature upsell: toggle vim" => { - properties["source"] = json!("Feature Upsell"); - "Vim Mode Toggled".to_string() - } - _ => e - .operation - .strip_prefix("feature upsell: viewed docs (") - .and_then(|s| s.strip_suffix(')')) - .map_or_else( - || format!("Unknown App Event: {}", e.operation), - |docs_url| { - properties["url"] = json!(docs_url); - properties["source"] = json!("Feature Upsell"); - "Documentation Viewed".to_string() - }, - ), - }; - (event_type, properties) - } - Event::Setting(e) => ( - "Settings Changed".to_string(), - serde_json::to_value(e).unwrap(), - ), - Event::Extension(e) => ( - "Extension Loaded".to_string(), - serde_json::to_value(e).unwrap(), - ), - Event::Edit(e) => ( - "Editor Edited".to_string(), - serde_json::to_value(e).unwrap(), - ), - Event::Action(e) => ( - "Action Invoked".to_string(), - serde_json::to_value(e).unwrap(), - ), - Event::Repl(e) => ( - "Kernel Status Changed".to_string(), - serde_json::to_value(e).unwrap(), - ), Event::Flexible(e) => ( e.event_type.clone(), serde_json::to_value(&e.event_properties).unwrap(), @@ -759,7 +599,7 @@ fn for_snowflake( }) }); - Some(SnowflakeRow { + SnowflakeRow { time: timestamp, user_id: body.metrics_id.clone(), device_id: body.system_id.clone(), @@ -767,7 +607,7 @@ fn for_snowflake( event_properties, user_properties, insert_id: Some(Uuid::new_v4().to_string()), - }) + } }) } diff --git a/crates/telemetry_events/src/telemetry_events.rs b/crates/telemetry_events/src/telemetry_events.rs index 735a1310ae..12d8d4c04b 100644 --- a/crates/telemetry_events/src/telemetry_events.rs +++ b/crates/telemetry_events/src/telemetry_events.rs @@ -2,7 +2,7 @@ use semantic_version::SemanticVersion; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fmt::Display, sync::Arc, time::Duration}; +use std::{collections::HashMap, fmt::Display, time::Duration}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct EventRequestBody { @@ -93,19 +93,6 @@ impl Display for AssistantPhase { #[serde(tag = "type")] pub enum Event { Flexible(FlexibleEvent), - Editor(EditorEvent), - EditPrediction(EditPredictionEvent), - EditPredictionRating(EditPredictionRatingEvent), - Call(CallEvent), - Assistant(AssistantEventData), - Cpu(CpuEvent), - Memory(MemoryEvent), - App(AppEvent), - Setting(SettingEvent), - Extension(ExtensionEvent), - Edit(EditEvent), - Action(ActionEvent), - Repl(ReplEvent), } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -114,54 +101,12 @@ pub struct FlexibleEvent { pub event_properties: HashMap, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct EditorEvent { - /// The editor operation performed (open, save) - pub operation: String, - /// The extension of the file that was opened or saved - pub file_extension: Option, - /// Whether the user is in vim mode or not - pub vim_mode: bool, - /// Whether the user has copilot enabled or not - pub copilot_enabled: bool, - /// Whether the user has copilot enabled for the language of the file opened or saved - pub copilot_enabled_for_language: bool, - /// Whether the client is opening/saving a local file or a remote file via SSH - #[serde(default)] - pub is_via_ssh: bool, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct EditPredictionEvent { - /// Provider of the completion suggestion (e.g. copilot, supermaven) - pub provider: String, - pub suggestion_accepted: bool, - pub file_extension: Option, -} - #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum EditPredictionRating { Positive, Negative, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct EditPredictionRatingEvent { - pub rating: EditPredictionRating, - pub input_events: Arc, - pub input_excerpt: Arc, - pub output_excerpt: Arc, - pub feedback: String, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct CallEvent { - /// Operation performed: invite/join call; begin/end screenshare; share/unshare project; etc - pub operation: String, - pub room_id: Option, - pub channel_id: Option, -} - #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct AssistantEventData { /// Unique random identifier for each assistant tab (None for inline assist) @@ -180,57 +125,6 @@ pub struct AssistantEventData { pub language_name: Option, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct CpuEvent { - pub usage_as_percentage: f32, - pub core_count: u32, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct MemoryEvent { - pub memory_in_bytes: u64, - pub virtual_memory_in_bytes: u64, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct ActionEvent { - pub source: String, - pub action: String, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct EditEvent { - pub duration: i64, - pub environment: String, - /// Whether the edits occurred locally or remotely via SSH - #[serde(default)] - pub is_via_ssh: bool, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct SettingEvent { - pub setting: String, - pub value: String, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct ExtensionEvent { - pub extension_id: Arc, - pub version: Arc, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct AppEvent { - pub operation: String, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct ReplEvent { - pub kernel_language: String, - pub kernel_status: String, - pub repl_session_id: String, -} - #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct BacktraceFrame { pub ip: usize, From 2a9d4599cdeb61d5f6cf90f01d7475b14bf5b510 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 15:46:23 -0400 Subject: [PATCH 386/693] proto: Remove unused types (#36269) This PR removes some unused types from the RPC protocol. Release Notes: - N/A --- .../agent_ui/src/language_model_selector.rs | 6 ++-- crates/client/src/user.rs | 13 -------- crates/proto/proto/app.proto | 31 ------------------- 3 files changed, 3 insertions(+), 47 deletions(-) diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index 7121624c87..bb8514a224 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -1,5 +1,6 @@ use std::{cmp::Reverse, sync::Arc}; +use cloud_llm_client::Plan; use collections::{HashSet, IndexMap}; use feature_flags::ZedProFeatureFlag; use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; @@ -10,7 +11,6 @@ use language_model::{ }; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; -use proto::Plan; use ui::{ListItem, ListItemSpacing, prelude::*}; const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro"; @@ -536,7 +536,7 @@ impl PickerDelegate for LanguageModelPickerDelegate { ) -> Option { use feature_flags::FeatureFlagAppExt; - let plan = proto::Plan::ZedPro; + let plan = Plan::ZedPro; Some( h_flex() @@ -557,7 +557,7 @@ impl PickerDelegate for LanguageModelPickerDelegate { window .dispatch_action(Box::new(zed_actions::OpenAccountSettings), cx) }), - Plan::Free | Plan::ZedProTrial => Button::new( + Plan::ZedFree | Plan::ZedProTrial => Button::new( "try-pro", if plan == Plan::ZedProTrial { "Upgrade to Pro" diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 33a240eca1..da7f50076b 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -998,19 +998,6 @@ impl RequestUsage { } } - pub fn from_proto(amount: u32, limit: proto::UsageLimit) -> Option { - let limit = match limit.variant? { - proto::usage_limit::Variant::Limited(limited) => { - UsageLimit::Limited(limited.limit as i32) - } - proto::usage_limit::Variant::Unlimited(_) => UsageLimit::Unlimited, - }; - Some(RequestUsage { - limit, - amount: amount as i32, - }) - } - fn from_headers( limit_name: &str, amount_name: &str, diff --git a/crates/proto/proto/app.proto b/crates/proto/proto/app.proto index fe6f7be1b0..9611b607d0 100644 --- a/crates/proto/proto/app.proto +++ b/crates/proto/proto/app.proto @@ -6,37 +6,6 @@ message UpdateInviteInfo { uint32 count = 2; } -enum Plan { - Free = 0; - ZedPro = 1; - ZedProTrial = 2; -} - -message SubscriptionPeriod { - uint64 started_at = 1; - uint64 ended_at = 2; -} - -message SubscriptionUsage { - uint32 model_requests_usage_amount = 1; - UsageLimit model_requests_usage_limit = 2; - uint32 edit_predictions_usage_amount = 3; - UsageLimit edit_predictions_usage_limit = 4; -} - -message UsageLimit { - oneof variant { - Limited limited = 1; - Unlimited unlimited = 2; - } - - message Limited { - uint32 limit = 1; - } - - message Unlimited {} -} - message AcceptTermsOfService {} message AcceptTermsOfServiceResponse { From 65f64aa5138a4cfcede025648cda973eeae21021 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Fri, 15 Aug 2025 22:21:21 +0200 Subject: [PATCH 387/693] search: Fix recently introduced issues with the search bars (#36271) Follow-up to https://github.com/zed-industries/zed/pull/36233 The above PR simplified the handling but introduced some bugs: The replace buttons were no longer clickable, some buttons also lost their toggle states, some buttons shared their element id and, lastly, some buttons were clickable but would not trigger the right action. This PR fixes all that. Release Notes: - N/A --- crates/search/src/buffer_search.rs | 53 +++++++++++++++----------- crates/search/src/project_search.rs | 59 +++++++++++++++++------------ crates/search/src/search.rs | 55 +++++++++++++++++++-------- crates/search/src/search_bar.rs | 12 +++++- 4 files changed, 114 insertions(+), 65 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index da2d35d74c..189f48e6b6 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -2,9 +2,9 @@ mod registrar; use crate::{ FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOption, - SearchOptions, SelectAllMatches, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, - ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord, - search_bar::{input_base_styles, render_action_button, render_text_input}, + SearchOptions, SearchSource, SelectAllMatches, SelectNextMatch, SelectPreviousMatch, + ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord, + search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input}, }; use any_vec::AnyVec; use anyhow::Context as _; @@ -213,22 +213,25 @@ impl Render for BufferSearchBar { h_flex() .gap_1() .when(case, |div| { - div.child( - SearchOption::CaseSensitive - .as_button(self.search_options, focus_handle.clone()), - ) + div.child(SearchOption::CaseSensitive.as_button( + self.search_options, + SearchSource::Buffer, + focus_handle.clone(), + )) }) .when(word, |div| { - div.child( - SearchOption::WholeWord - .as_button(self.search_options, focus_handle.clone()), - ) + div.child(SearchOption::WholeWord.as_button( + self.search_options, + SearchSource::Buffer, + focus_handle.clone(), + )) }) .when(regex, |div| { - div.child( - SearchOption::Regex - .as_button(self.search_options, focus_handle.clone()), - ) + div.child(SearchOption::Regex.as_button( + self.search_options, + SearchSource::Buffer, + focus_handle.clone(), + )) }), ) }); @@ -240,7 +243,7 @@ impl Render for BufferSearchBar { this.child(render_action_button( "buffer-search-bar-toggle", IconName::Replace, - self.replace_enabled, + self.replace_enabled.then_some(ActionButtonState::Toggled), "Toggle Replace", &ToggleReplace, focus_handle.clone(), @@ -285,7 +288,9 @@ impl Render for BufferSearchBar { .child(render_action_button( "buffer-search-nav-button", ui::IconName::ChevronLeft, - self.active_match_index.is_some(), + self.active_match_index + .is_none() + .then_some(ActionButtonState::Disabled), "Select Previous Match", &SelectPreviousMatch, query_focus.clone(), @@ -293,7 +298,9 @@ impl Render for BufferSearchBar { .child(render_action_button( "buffer-search-nav-button", ui::IconName::ChevronRight, - self.active_match_index.is_some(), + self.active_match_index + .is_none() + .then_some(ActionButtonState::Disabled), "Select Next Match", &SelectNextMatch, query_focus.clone(), @@ -313,7 +320,7 @@ impl Render for BufferSearchBar { el.child(render_action_button( "buffer-search-nav-button", IconName::SelectAll, - true, + Default::default(), "Select All Matches", &SelectAllMatches, query_focus, @@ -324,7 +331,7 @@ impl Render for BufferSearchBar { el.child(render_action_button( "buffer-search", IconName::Close, - true, + Default::default(), "Close Search Bar", &Dismiss, focus_handle.clone(), @@ -352,7 +359,7 @@ impl Render for BufferSearchBar { .child(render_action_button( "buffer-search-replace-button", IconName::ReplaceNext, - true, + Default::default(), "Replace Next Match", &ReplaceNext, focus_handle.clone(), @@ -360,7 +367,7 @@ impl Render for BufferSearchBar { .child(render_action_button( "buffer-search-replace-button", IconName::ReplaceAll, - true, + Default::default(), "Replace All Matches", &ReplaceAll, focus_handle, @@ -394,7 +401,7 @@ impl Render for BufferSearchBar { div.child(h_flex().absolute().right_0().child(render_action_button( "buffer-search", IconName::Close, - true, + Default::default(), "Close Search Bar", &Dismiss, focus_handle.clone(), diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index b791f748ad..056c3556ba 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,9 +1,9 @@ use crate::{ BufferSearchBar, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, - SearchOption, SearchOptions, SelectNextMatch, SelectPreviousMatch, ToggleCaseSensitive, - ToggleIncludeIgnored, ToggleRegex, ToggleReplace, ToggleWholeWord, + SearchOption, SearchOptions, SearchSource, SelectNextMatch, SelectPreviousMatch, + ToggleCaseSensitive, ToggleIncludeIgnored, ToggleRegex, ToggleReplace, ToggleWholeWord, buffer_search::Deploy, - search_bar::{input_base_styles, render_action_button, render_text_input}, + search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input}, }; use anyhow::Context as _; use collections::HashMap; @@ -1665,7 +1665,7 @@ impl ProjectSearchBar { }); } - fn toggle_search_option( + pub(crate) fn toggle_search_option( &mut self, option: SearchOptions, window: &mut Window, @@ -1962,17 +1962,21 @@ impl Render for ProjectSearchBar { .child( h_flex() .gap_1() - .child( - SearchOption::CaseSensitive - .as_button(search.search_options, focus_handle.clone()), - ) - .child( - SearchOption::WholeWord - .as_button(search.search_options, focus_handle.clone()), - ) - .child( - SearchOption::Regex.as_button(search.search_options, focus_handle.clone()), - ), + .child(SearchOption::CaseSensitive.as_button( + search.search_options, + SearchSource::Project(cx), + focus_handle.clone(), + )) + .child(SearchOption::WholeWord.as_button( + search.search_options, + SearchSource::Project(cx), + focus_handle.clone(), + )) + .child(SearchOption::Regex.as_button( + search.search_options, + SearchSource::Project(cx), + focus_handle.clone(), + )), ); let query_focus = search.query_editor.focus_handle(cx); @@ -1985,7 +1989,10 @@ impl Render for ProjectSearchBar { .child(render_action_button( "project-search-nav-button", IconName::ChevronLeft, - search.active_match_index.is_some(), + search + .active_match_index + .is_none() + .then_some(ActionButtonState::Disabled), "Select Previous Match", &SelectPreviousMatch, query_focus.clone(), @@ -1993,7 +2000,10 @@ impl Render for ProjectSearchBar { .child(render_action_button( "project-search-nav-button", IconName::ChevronRight, - search.active_match_index.is_some(), + search + .active_match_index + .is_none() + .then_some(ActionButtonState::Disabled), "Select Next Match", &SelectNextMatch, query_focus, @@ -2054,7 +2064,7 @@ impl Render for ProjectSearchBar { self.active_project_search .as_ref() .map(|search| search.read(cx).replace_enabled) - .unwrap_or_default(), + .and_then(|enabled| enabled.then_some(ActionButtonState::Toggled)), "Toggle Replace", &ToggleReplace, focus_handle.clone(), @@ -2079,7 +2089,7 @@ impl Render for ProjectSearchBar { .child(render_action_button( "project-search-replace-button", IconName::ReplaceNext, - true, + Default::default(), "Replace Next Match", &ReplaceNext, focus_handle.clone(), @@ -2087,7 +2097,7 @@ impl Render for ProjectSearchBar { .child(render_action_button( "project-search-replace-button", IconName::ReplaceAll, - true, + Default::default(), "Replace All Matches", &ReplaceAll, focus_handle, @@ -2129,10 +2139,11 @@ impl Render for ProjectSearchBar { this.toggle_opened_only(window, cx); })), ) - .child( - SearchOption::IncludeIgnored - .as_button(search.search_options, focus_handle.clone()), - ); + .child(SearchOption::IncludeIgnored.as_button( + search.search_options, + SearchSource::Project(cx), + focus_handle.clone(), + )); h_flex() .w_full() .gap_2() diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 89064e0a27..904c74d03c 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -1,7 +1,7 @@ use bitflags::bitflags; pub use buffer_search::BufferSearchBar; use editor::SearchSettings; -use gpui::{Action, App, FocusHandle, IntoElement, actions}; +use gpui::{Action, App, ClickEvent, FocusHandle, IntoElement, actions}; use project::search::SearchQuery; pub use project_search::ProjectSearchView; use ui::{ButtonStyle, IconButton, IconButtonShape}; @@ -11,6 +11,8 @@ use workspace::{Toast, Workspace}; pub use search_status_button::SEARCH_ICON; +use crate::project_search::ProjectSearchBar; + pub mod buffer_search; pub mod project_search; pub(crate) mod search_bar; @@ -83,9 +85,14 @@ pub enum SearchOption { Backwards, } +pub(crate) enum SearchSource<'a, 'b> { + Buffer, + Project(&'a Context<'b, ProjectSearchBar>), +} + impl SearchOption { - pub fn as_options(self) -> SearchOptions { - SearchOptions::from_bits(1 << self as u8).unwrap() + pub fn as_options(&self) -> SearchOptions { + SearchOptions::from_bits(1 << *self as u8).unwrap() } pub fn label(&self) -> &'static str { @@ -119,25 +126,41 @@ impl SearchOption { } } - pub fn as_button(&self, active: SearchOptions, focus_handle: FocusHandle) -> impl IntoElement { + pub(crate) fn as_button( + &self, + active: SearchOptions, + search_source: SearchSource, + focus_handle: FocusHandle, + ) -> impl IntoElement { let action = self.to_toggle_action(); let label = self.label(); - IconButton::new(label, self.icon()) - .on_click({ + IconButton::new( + (label, matches!(search_source, SearchSource::Buffer) as u32), + self.icon(), + ) + .map(|button| match search_source { + SearchSource::Buffer => { let focus_handle = focus_handle.clone(); - move |_, window, cx| { + button.on_click(move |_: &ClickEvent, window, cx| { if !focus_handle.is_focused(&window) { window.focus(&focus_handle); } - window.dispatch_action(action.boxed_clone(), cx) - } - }) - .style(ButtonStyle::Subtle) - .shape(IconButtonShape::Square) - .toggle_state(active.contains(self.as_options())) - .tooltip({ - move |window, cx| Tooltip::for_action_in(label, action, &focus_handle, window, cx) - }) + window.dispatch_action(action.boxed_clone(), cx); + }) + } + SearchSource::Project(cx) => { + let options = self.as_options(); + button.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| { + this.toggle_search_option(options, window, cx); + })) + } + }) + .style(ButtonStyle::Subtle) + .shape(IconButtonShape::Square) + .toggle_state(active.contains(self.as_options())) + .tooltip({ + move |window, cx| Tooltip::for_action_in(label, action, &focus_handle, window, cx) + }) } } diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 094ce3638e..8cc838a8a6 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -5,10 +5,15 @@ use theme::ThemeSettings; use ui::{IconButton, IconButtonShape}; use ui::{Tooltip, prelude::*}; +pub(super) enum ActionButtonState { + Disabled, + Toggled, +} + pub(super) fn render_action_button( id_prefix: &'static str, icon: ui::IconName, - active: bool, + button_state: Option, tooltip: &'static str, action: &'static dyn Action, focus_handle: FocusHandle, @@ -28,7 +33,10 @@ pub(super) fn render_action_button( } }) .tooltip(move |window, cx| Tooltip::for_action_in(tooltip, action, &focus_handle, window, cx)) - .disabled(!active) + .when_some(button_state, |this, state| match state { + ActionButtonState::Toggled => this.toggle_state(true), + ActionButtonState::Disabled => this.disabled(true), + }) } pub(crate) fn input_base_styles(border_color: Hsla, map: impl FnOnce(Div) -> Div) -> Div { From 7199c733b252f62f84135e0b9102fab22d5480e5 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 16:21:45 -0400 Subject: [PATCH 388/693] proto: Remove `AcceptTermsOfService` message (#36272) This PR removes the `AcceptTermsOfService` RPC message. We're no longer using the message after https://github.com/zed-industries/zed/pull/36255. Release Notes: - N/A --- crates/collab/src/rpc.rs | 21 --------------------- crates/proto/proto/app.proto | 6 ------ crates/proto/proto/zed.proto | 3 +-- crates/proto/src/proto.rs | 3 --- 4 files changed, 1 insertion(+), 32 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 957cc30fe6..ef749ac9b7 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -29,7 +29,6 @@ use axum::{ response::IntoResponse, routing::get, }; -use chrono::Utc; use collections::{HashMap, HashSet}; pub use connection_pool::{ConnectionPool, ZedVersion}; use core::fmt::{self, Debug, Formatter}; @@ -449,7 +448,6 @@ impl Server { .add_request_handler(follow) .add_message_handler(unfollow) .add_message_handler(update_followers) - .add_request_handler(accept_terms_of_service) .add_message_handler(acknowledge_channel_message) .add_message_handler(acknowledge_buffer_version) .add_request_handler(get_supermaven_api_key) @@ -3985,25 +3983,6 @@ async fn mark_notification_as_read( Ok(()) } -/// Accept the terms of service (tos) on behalf of the current user -async fn accept_terms_of_service( - _request: proto::AcceptTermsOfService, - response: Response, - session: MessageContext, -) -> Result<()> { - let db = session.db().await; - - let accepted_tos_at = Utc::now(); - db.set_user_accepted_tos_at(session.user_id(), Some(accepted_tos_at.naive_utc())) - .await?; - - response.send(proto::AcceptTermsOfServiceResponse { - accepted_tos_at: accepted_tos_at.timestamp() as u64, - })?; - - Ok(()) -} - fn to_axum_message(message: TungsteniteMessage) -> anyhow::Result { let message = match message { TungsteniteMessage::Text(payload) => AxumMessage::Text(payload.as_str().to_string()), diff --git a/crates/proto/proto/app.proto b/crates/proto/proto/app.proto index 9611b607d0..1f2ab1f539 100644 --- a/crates/proto/proto/app.proto +++ b/crates/proto/proto/app.proto @@ -6,12 +6,6 @@ message UpdateInviteInfo { uint32 count = 2; } -message AcceptTermsOfService {} - -message AcceptTermsOfServiceResponse { - uint64 accepted_tos_at = 1; -} - message ShutdownRemoteServer {} message Toast { diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 4b023a46bc..310fcf584e 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -136,8 +136,6 @@ message Envelope { UpdateFollowers update_followers = 100; Unfollow unfollow = 101; UpdateDiffBases update_diff_bases = 104; - AcceptTermsOfService accept_terms_of_service = 239; - AcceptTermsOfServiceResponse accept_terms_of_service_response = 240; OnTypeFormatting on_type_formatting = 105; OnTypeFormattingResponse on_type_formatting_response = 106; @@ -414,6 +412,7 @@ message Envelope { reserved 224 to 229; reserved 230 to 231; reserved 234 to 236; + reserved 239 to 240; reserved 246; reserved 247 to 254; reserved 255 to 256; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 18abf31c64..802db09590 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -20,8 +20,6 @@ pub const SSH_PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 }; pub const SSH_PROJECT_ID: u64 = 0; messages!( - (AcceptTermsOfService, Foreground), - (AcceptTermsOfServiceResponse, Foreground), (Ack, Foreground), (AckBufferOperation, Background), (AckChannelMessage, Background), @@ -315,7 +313,6 @@ messages!( ); request_messages!( - (AcceptTermsOfService, AcceptTermsOfServiceResponse), (ApplyCodeAction, ApplyCodeActionResponse), ( ApplyCompletionAdditionalEdits, From 3e0a755486201a2fe6e77213af68494a784a4895 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Fri, 15 Aug 2025 22:27:44 +0200 Subject: [PATCH 389/693] Remove some redundant entity clones (#36274) `cx.entity()` already returns an owned entity, so there is no need for these clones. Release Notes: - N/A --- crates/agent_ui/src/context_picker.rs | 2 +- crates/agent_ui/src/inline_assistant.rs | 2 +- crates/agent_ui/src/profile_selector.rs | 2 +- crates/collab_ui/src/chat_panel.rs | 2 +- crates/collab_ui/src/collab_panel.rs | 8 +- .../src/collab_panel/channel_modal.rs | 2 +- crates/collab_ui/src/notification_panel.rs | 4 +- crates/debugger_ui/src/session/running.rs | 4 +- .../src/edit_prediction_button.rs | 6 +- crates/editor/src/editor_tests.rs | 13 +-- crates/editor/src/element.rs | 2 +- crates/extensions_ui/src/extensions_ui.rs | 2 +- crates/git_ui/src/git_panel.rs | 2 +- crates/gpui/examples/input.rs | 4 +- crates/language_tools/src/lsp_log.rs | 2 +- crates/language_tools/src/lsp_tool.rs | 2 +- crates/language_tools/src/syntax_tree_view.rs | 2 +- crates/outline_panel/src/outline_panel.rs | 80 +++++++++---------- crates/project_panel/src/project_panel.rs | 42 +++++----- crates/recent_projects/src/remote_servers.rs | 4 +- crates/repl/src/session.rs | 2 +- crates/storybook/src/stories/indent_guides.rs | 2 +- crates/terminal_view/src/terminal_panel.rs | 4 +- crates/terminal_view/src/terminal_view.rs | 2 +- crates/vim/src/mode_indicator.rs | 4 +- crates/vim/src/normal/search.rs | 2 +- crates/vim/src/vim.rs | 2 +- crates/workspace/src/dock.rs | 2 +- crates/workspace/src/notifications.rs | 2 +- crates/workspace/src/pane.rs | 16 ++-- crates/workspace/src/workspace.rs | 2 +- crates/zed/src/zed.rs | 2 +- 32 files changed, 106 insertions(+), 123 deletions(-) diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index 6c5546c6bb..131023d249 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -228,7 +228,7 @@ impl ContextPicker { } fn build_menu(&mut self, window: &mut Window, cx: &mut Context) -> Entity { - let context_picker = cx.entity().clone(); + let context_picker = cx.entity(); let menu = ContextMenu::build(window, cx, move |menu, _window, cx| { let recent = self.recent_entries(cx); diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 4a4a747899..bbd3595805 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -72,7 +72,7 @@ pub fn init( let Some(window) = window else { return; }; - let workspace = cx.entity().clone(); + let workspace = cx.entity(); InlineAssistant::update_global(cx, |inline_assistant, cx| { inline_assistant.register_workspace(&workspace, window, cx) }); diff --git a/crates/agent_ui/src/profile_selector.rs b/crates/agent_ui/src/profile_selector.rs index 27ca69590f..ce25f531e2 100644 --- a/crates/agent_ui/src/profile_selector.rs +++ b/crates/agent_ui/src/profile_selector.rs @@ -163,7 +163,7 @@ impl Render for ProfileSelector { .unwrap_or_else(|| "Unknown".into()); if self.provider.profiles_supported(cx) { - let this = cx.entity().clone(); + let this = cx.entity(); let focus_handle = self.focus_handle.clone(); let trigger_button = Button::new("profile-selector-model", selected_profile) .label_size(LabelSize::Small) diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 51d9f003f8..2bbaa8446c 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -674,7 +674,7 @@ impl ChatPanel { }) }) .when_some(message_id, |el, message_id| { - let this = cx.entity().clone(); + let this = cx.entity(); el.child( self.render_popover_button( diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 430b447580..c2cc6a7ad5 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -95,7 +95,7 @@ pub fn init(cx: &mut App) { .and_then(|room| room.read(cx).channel_id()); if let Some(channel_id) = channel_id { - let workspace = cx.entity().clone(); + let workspace = cx.entity(); window.defer(cx, move |window, cx| { ChannelView::open(channel_id, None, workspace, window, cx) .detach_and_log_err(cx) @@ -1142,7 +1142,7 @@ impl CollabPanel { window: &mut Window, cx: &mut Context, ) { - let this = cx.entity().clone(); + let this = cx.entity(); if !(role == proto::ChannelRole::Guest || role == proto::ChannelRole::Talker || role == proto::ChannelRole::Member) @@ -1272,7 +1272,7 @@ impl CollabPanel { .channel_for_id(clipboard.channel_id) .map(|channel| channel.name.clone()) }); - let this = cx.entity().clone(); + let this = cx.entity(); let context_menu = ContextMenu::build(window, cx, |mut context_menu, window, cx| { if self.has_subchannels(ix) { @@ -1439,7 +1439,7 @@ impl CollabPanel { window: &mut Window, cx: &mut Context, ) { - let this = cx.entity().clone(); + let this = cx.entity(); let in_room = ActiveCall::global(cx).read(cx).room().is_some(); let context_menu = ContextMenu::build(window, cx, |mut context_menu, _, _| { diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index c0d3130ee9..e558835dba 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -586,7 +586,7 @@ impl ChannelModalDelegate { return; }; let user_id = membership.user.id; - let picker = cx.entity().clone(); + let picker = cx.entity(); let context_menu = ContextMenu::build(window, cx, |mut menu, _window, _cx| { let role = membership.role; diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 3a280ff667..a3420d603b 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -321,7 +321,7 @@ impl NotificationPanel { .justify_end() .child(Button::new("decline", "Decline").on_click({ let notification = notification.clone(); - let entity = cx.entity().clone(); + let entity = cx.entity(); move |_, _, cx| { entity.update(cx, |this, cx| { this.respond_to_notification( @@ -334,7 +334,7 @@ impl NotificationPanel { })) .child(Button::new("accept", "Accept").on_click({ let notification = notification.clone(); - let entity = cx.entity().clone(); + let entity = cx.entity(); move |_, _, cx| { entity.update(cx, |this, cx| { this.respond_to_notification( diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index c8bee42039..f3117aee07 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -291,7 +291,7 @@ pub(crate) fn new_debugger_pane( let Some(project) = project.upgrade() else { return ControlFlow::Break(()); }; - let this_pane = cx.entity().clone(); + let this_pane = cx.entity(); let item = if tab.pane == this_pane { pane.item_for_index(tab.ix) } else { @@ -502,7 +502,7 @@ pub(crate) fn new_debugger_pane( .on_drag( DraggedTab { item: item.boxed_clone(), - pane: cx.entity().clone(), + pane: cx.entity(), detail: 0, is_active: selected, ix, diff --git a/crates/edit_prediction_button/src/edit_prediction_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs index 3d3b43d71b..4632a03daf 100644 --- a/crates/edit_prediction_button/src/edit_prediction_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -127,7 +127,7 @@ impl Render for EditPredictionButton { }), ); } - let this = cx.entity().clone(); + let this = cx.entity(); div().child( PopoverMenu::new("copilot") @@ -182,7 +182,7 @@ impl Render for EditPredictionButton { let icon = status.to_icon(); let tooltip_text = status.to_tooltip(); let has_menu = status.has_menu(); - let this = cx.entity().clone(); + let this = cx.entity(); let fs = self.fs.clone(); return div().child( @@ -331,7 +331,7 @@ impl Render for EditPredictionButton { }) }); - let this = cx.entity().clone(); + let this = cx.entity(); let mut popover_menu = PopoverMenu::new("zeta") .menu(move |window, cx| { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index cf9954bc12..ef2bdc5da3 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -74,7 +74,7 @@ fn test_edit_events(cx: &mut TestAppContext) { let editor1 = cx.add_window({ let events = events.clone(); |window, cx| { - let entity = cx.entity().clone(); + let entity = cx.entity(); cx.subscribe_in( &entity, window, @@ -95,7 +95,7 @@ fn test_edit_events(cx: &mut TestAppContext) { let events = events.clone(); |window, cx| { cx.subscribe_in( - &cx.entity().clone(), + &cx.entity(), window, move |_, _, event: &EditorEvent, _, _| match event { EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")), @@ -19634,13 +19634,8 @@ fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) { editor.insert_creases(Some(crease), cx); let snapshot = editor.snapshot(window, cx); - let _div = snapshot.render_crease_toggle( - MultiBufferRow(1), - false, - cx.entity().clone(), - window, - cx, - ); + let _div = + snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx); snapshot }) .unwrap(); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 8a5c65f994..5edfd7df30 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -7815,7 +7815,7 @@ impl Element for EditorElement { min_lines, max_lines, } => { - let editor_handle = cx.entity().clone(); + let editor_handle = cx.entity(); let max_line_number_width = self.max_line_number_width(&editor.snapshot(window, cx), window); window.request_measured_layout( diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index fe3e94f5c2..4915933920 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -703,7 +703,7 @@ impl ExtensionsPage { extension: &ExtensionMetadata, cx: &mut Context, ) -> ExtensionCard { - let this = cx.entity().clone(); + let this = cx.entity(); let status = Self::extension_status(&extension.id, cx); let has_dev_extension = Self::dev_extension_exists(&extension.id, cx); diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index de308b9dde..70987dd212 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -3410,7 +3410,7 @@ impl GitPanel { * MAX_PANEL_EDITOR_LINES + gap; - let git_panel = cx.entity().clone(); + let git_panel = cx.entity(); let display_name = SharedString::from(Arc::from( active_repository .read(cx) diff --git a/crates/gpui/examples/input.rs b/crates/gpui/examples/input.rs index 52a5b08b96..b0f560e38d 100644 --- a/crates/gpui/examples/input.rs +++ b/crates/gpui/examples/input.rs @@ -595,9 +595,7 @@ impl Render for TextInput { .w_full() .p(px(4.)) .bg(white()) - .child(TextElement { - input: cx.entity().clone(), - }), + .child(TextElement { input: cx.entity() }), ) } } diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 606f3a3f0e..823d59ce12 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -1358,7 +1358,7 @@ impl Render for LspLogToolbarItemView { }) .collect(); - let log_toolbar_view = cx.entity().clone(); + let log_toolbar_view = cx.entity(); let lsp_menu = PopoverMenu::new("LspLogView") .anchor(Corner::TopLeft) diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_tool.rs index 50547253a9..3244350a34 100644 --- a/crates/language_tools/src/lsp_tool.rs +++ b/crates/language_tools/src/lsp_tool.rs @@ -1007,7 +1007,7 @@ impl Render for LspTool { (None, "All Servers Operational") }; - let lsp_tool = cx.entity().clone(); + let lsp_tool = cx.entity(); div().child( PopoverMenu::new("lsp-tool") diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index eadba2c1d2..9946442ec8 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -456,7 +456,7 @@ impl SyntaxTreeToolbarItemView { let active_layer = buffer_state.active_layer.clone()?; let active_buffer = buffer_state.buffer.read(cx).snapshot(); - let view = cx.entity().clone(); + let view = cx.entity(); Some( PopoverMenu::new("Syntax Tree") .trigger(Self::render_header(&active_layer)) diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 1cda3897ec..004a27b0cf 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -4815,51 +4815,45 @@ impl OutlinePanel { .when(show_indent_guides, |list| { list.with_decoration( ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx)) - .with_compute_indents_fn( - cx.entity().clone(), - |outline_panel, range, _, _| { - let entries = outline_panel.cached_entries.get(range); - if let Some(entries) = entries { - entries.into_iter().map(|item| item.depth).collect() - } else { - smallvec::SmallVec::new() - } - }, - ) - .with_render_fn( - cx.entity().clone(), - move |outline_panel, params, _, _| { - const LEFT_OFFSET: Pixels = px(14.); + .with_compute_indents_fn(cx.entity(), |outline_panel, range, _, _| { + let entries = outline_panel.cached_entries.get(range); + if let Some(entries) = entries { + entries.into_iter().map(|item| item.depth).collect() + } else { + smallvec::SmallVec::new() + } + }) + .with_render_fn(cx.entity(), move |outline_panel, params, _, _| { + const LEFT_OFFSET: Pixels = px(14.); - let indent_size = params.indent_size; - let item_height = params.item_height; - let active_indent_guide_ix = find_active_indent_guide_ix( - outline_panel, - ¶ms.indent_guides, - ); + let indent_size = params.indent_size; + let item_height = params.item_height; + let active_indent_guide_ix = find_active_indent_guide_ix( + outline_panel, + ¶ms.indent_guides, + ); - params - .indent_guides - .into_iter() - .enumerate() - .map(|(ix, layout)| { - let bounds = Bounds::new( - point( - layout.offset.x * indent_size + LEFT_OFFSET, - layout.offset.y * item_height, - ), - size(px(1.), layout.length * item_height), - ); - ui::RenderedIndentGuide { - bounds, - layout, - is_active: active_indent_guide_ix == Some(ix), - hitbox: None, - } - }) - .collect() - }, - ), + params + .indent_guides + .into_iter() + .enumerate() + .map(|(ix, layout)| { + let bounds = Bounds::new( + point( + layout.offset.x * indent_size + LEFT_OFFSET, + layout.offset.y * item_height, + ), + size(px(1.), layout.length * item_height), + ); + ui::RenderedIndentGuide { + bounds, + layout, + is_active: active_indent_guide_ix == Some(ix), + hitbox: None, + } + }) + .collect() + }), ) }) }; diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 967df41e23..4d7f2faf62 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -5351,26 +5351,22 @@ impl Render for ProjectPanel { .when(show_indent_guides, |list| { list.with_decoration( ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx)) - .with_compute_indents_fn( - cx.entity().clone(), - |this, range, window, cx| { - let mut items = - SmallVec::with_capacity(range.end - range.start); - this.iter_visible_entries( - range, - window, - cx, - |entry, _, entries, _, _| { - let (depth, _) = - Self::calculate_depth_and_difference( - entry, entries, - ); - items.push(depth); - }, - ); - items - }, - ) + .with_compute_indents_fn(cx.entity(), |this, range, window, cx| { + let mut items = + SmallVec::with_capacity(range.end - range.start); + this.iter_visible_entries( + range, + window, + cx, + |entry, _, entries, _, _| { + let (depth, _) = Self::calculate_depth_and_difference( + entry, entries, + ); + items.push(depth); + }, + ); + items + }) .on_click(cx.listener( |this, active_indent_guide: &IndentGuideLayout, window, cx| { if window.modifiers().secondary() { @@ -5394,7 +5390,7 @@ impl Render for ProjectPanel { } }, )) - .with_render_fn(cx.entity().clone(), move |this, params, _, cx| { + .with_render_fn(cx.entity(), move |this, params, _, cx| { const LEFT_OFFSET: Pixels = px(14.); const PADDING_Y: Pixels = px(4.); const HITBOX_OVERDRAW: Pixels = px(3.); @@ -5447,7 +5443,7 @@ impl Render for ProjectPanel { }) .when(show_sticky_entries, |list| { let sticky_items = ui::sticky_items( - cx.entity().clone(), + cx.entity(), |this, range, window, cx| { let mut items = SmallVec::with_capacity(range.end - range.start); this.iter_visible_entries( @@ -5474,7 +5470,7 @@ impl Render for ProjectPanel { list.with_decoration(if show_indent_guides { sticky_items.with_decoration( ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx)) - .with_render_fn(cx.entity().clone(), move |_, params, _, _| { + .with_render_fn(cx.entity(), move |_, params, _, _| { const LEFT_OFFSET: Pixels = px(14.); let indent_size = params.indent_size; diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 354434a7fc..e5e166cb4c 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -1292,7 +1292,7 @@ impl RemoteServerProjects { let connection_string = connection_string.clone(); move |_, _: &menu::Confirm, window, cx| { remove_ssh_server( - cx.entity().clone(), + cx.entity(), server_index, connection_string.clone(), window, @@ -1312,7 +1312,7 @@ impl RemoteServerProjects { .child(Label::new("Remove Server").color(Color::Error)) .on_click(cx.listener(move |_, _, window, cx| { remove_ssh_server( - cx.entity().clone(), + cx.entity(), server_index, connection_string.clone(), window, diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index 729a616135..f945e5ed9f 100644 --- a/crates/repl/src/session.rs +++ b/crates/repl/src/session.rs @@ -244,7 +244,7 @@ impl Session { repl_session_id = cx.entity_id().to_string(), ); - let session_view = cx.entity().clone(); + let session_view = cx.entity(); let kernel = match self.kernel_specification.clone() { KernelSpecification::Jupyter(kernel_specification) diff --git a/crates/storybook/src/stories/indent_guides.rs b/crates/storybook/src/stories/indent_guides.rs index e4f9669b1f..db23ea79bd 100644 --- a/crates/storybook/src/stories/indent_guides.rs +++ b/crates/storybook/src/stories/indent_guides.rs @@ -65,7 +65,7 @@ impl Render for IndentGuidesStory { }, ) .with_compute_indents_fn( - cx.entity().clone(), + cx.entity(), |this, range, _cx, _context| { this.depths .iter() diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index c9528c39b9..568dc1db2e 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -947,7 +947,7 @@ pub fn new_terminal_pane( cx: &mut Context, ) -> Entity { let is_local = project.read(cx).is_local(); - let terminal_panel = cx.entity().clone(); + let terminal_panel = cx.entity(); let pane = cx.new(|cx| { let mut pane = Pane::new( workspace.clone(), @@ -1009,7 +1009,7 @@ pub fn new_terminal_pane( return ControlFlow::Break(()); }; if let Some(tab) = dropped_item.downcast_ref::() { - let this_pane = cx.entity().clone(); + let this_pane = cx.entity(); let item = if tab.pane == this_pane { pane.item_for_index(tab.ix) } else { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 219238496c..534c0a8051 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1491,7 +1491,7 @@ impl TerminalView { impl Render for TerminalView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let terminal_handle = self.terminal.clone(); - let terminal_view_handle = cx.entity().clone(); + let terminal_view_handle = cx.entity(); let focused = self.focus_handle.is_focused(window); diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index d54b270074..714b74f239 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -20,7 +20,7 @@ impl ModeIndicator { }) .detach(); - let handle = cx.entity().clone(); + let handle = cx.entity(); let window_handle = window.window_handle(); cx.observe_new::(move |_, window, cx| { let Some(window) = window else { @@ -29,7 +29,7 @@ impl ModeIndicator { if window.window_handle() != window_handle { return; } - let vim = cx.entity().clone(); + let vim = cx.entity(); handle.update(cx, |_, cx| { cx.subscribe(&vim, |mode_indicator, vim, event, cx| match event { VimEvent::Focused => { diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index e4e95ca48e..4054c552ae 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -332,7 +332,7 @@ impl Vim { Vim::take_forced_motion(cx); let prior_selections = self.editor_selections(window, cx); let cursor_word = self.editor_cursor_word(window, cx); - let vim = cx.entity().clone(); + let vim = cx.entity(); let searched = pane.update(cx, |pane, cx| { self.search.direction = direction; diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 51bf2dd131..44d9b8f456 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -402,7 +402,7 @@ impl Vim { const NAMESPACE: &'static str = "vim"; pub fn new(window: &mut Window, cx: &mut Context) -> Entity { - let editor = cx.entity().clone(); + let editor = cx.entity(); let mut initial_mode = VimSettings::get_global(cx).default_mode; if initial_mode == Mode::Normal && HelixModeSetting::get_global(cx).0 { diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index ca63d3e553..ae72df3971 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -253,7 +253,7 @@ impl Dock { cx: &mut Context, ) -> Entity { let focus_handle = cx.focus_handle(); - let workspace = cx.entity().clone(); + let workspace = cx.entity(); let dock = cx.new(|cx| { let focus_subscription = cx.on_focus(&focus_handle, window, |dock: &mut Dock, window, cx| { diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 7d8a28b0f1..1356322a5c 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -346,7 +346,7 @@ impl Render for LanguageServerPrompt { ) .child(Label::new(request.message.to_string()).size(LabelSize::Small)) .children(request.actions.iter().enumerate().map(|(ix, action)| { - let this_handle = cx.entity().clone(); + let this_handle = cx.entity(); Button::new(ix, action.title.clone()) .size(ButtonSize::Large) .on_click(move |_, window, cx| { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 759e91f758..860a57c21f 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2198,7 +2198,7 @@ impl Pane { fn update_status_bar(&mut self, window: &mut Window, cx: &mut Context) { let workspace = self.workspace.clone(); - let pane = cx.entity().clone(); + let pane = cx.entity(); window.defer(cx, move |window, cx| { let Ok(status_bar) = @@ -2279,7 +2279,7 @@ impl Pane { cx: &mut Context, ) { maybe!({ - let pane = cx.entity().clone(); + let pane = cx.entity(); let destination_index = match operation { PinOperation::Pin => self.pinned_tab_count.min(ix), @@ -2473,7 +2473,7 @@ impl Pane { .on_drag( DraggedTab { item: item.boxed_clone(), - pane: cx.entity().clone(), + pane: cx.entity(), detail, is_active, ix, @@ -2832,7 +2832,7 @@ impl Pane { let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft) .icon_size(IconSize::Small) .on_click({ - let entity = cx.entity().clone(); + let entity = cx.entity(); move |_, window, cx| { entity.update(cx, |pane, cx| pane.navigate_backward(window, cx)) } @@ -2848,7 +2848,7 @@ impl Pane { let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight) .icon_size(IconSize::Small) .on_click({ - let entity = cx.entity().clone(); + let entity = cx.entity(); move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx)) }) .disabled(!self.can_navigate_forward()) @@ -3054,7 +3054,7 @@ impl Pane { return; } } - let mut to_pane = cx.entity().clone(); + let mut to_pane = cx.entity(); let split_direction = self.drag_split_direction; let item_id = dragged_tab.item.item_id(); if let Some(preview_item_id) = self.preview_item_id { @@ -3163,7 +3163,7 @@ impl Pane { return; } } - let mut to_pane = cx.entity().clone(); + let mut to_pane = cx.entity(); let split_direction = self.drag_split_direction; let project_entry_id = *project_entry_id; self.workspace @@ -3239,7 +3239,7 @@ impl Pane { return; } } - let mut to_pane = cx.entity().clone(); + let mut to_pane = cx.entity(); let mut split_direction = self.drag_split_direction; let paths = paths.paths().to_vec(); let is_remote = self diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ade6838fad..1eaa125ba5 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -6338,7 +6338,7 @@ impl Render for Workspace { .border_b_1() .border_color(colors.border) .child({ - let this = cx.entity().clone(); + let this = cx.entity(); canvas( move |bounds, window, cx| { this.update(cx, |this, cx| { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 84145a1be4..b06652b2ce 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -319,7 +319,7 @@ pub fn initialize_workspace( return; }; - let workspace_handle = cx.entity().clone(); + let workspace_handle = cx.entity(); let center_pane = workspace.active_pane().clone(); initialize_pane(workspace, ¢er_pane, window, cx); From 239e479aedebb45cbc2efd7d0417808a3001710c Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 16:49:56 -0400 Subject: [PATCH 390/693] collab: Remove Stripe code (#36275) This PR removes the code for integrating with Stripe from Collab. All of these concerns are now handled by Cloud. Release Notes: - N/A --- Cargo.lock | 159 +---- Cargo.toml | 14 - crates/collab/Cargo.toml | 5 - crates/collab/k8s/collab.template.yml | 6 - crates/collab/src/api.rs | 1 - crates/collab/src/api/billing.rs | 59 -- .../src/db/tables/billing_subscription.rs | 15 - crates/collab/src/lib.rs | 44 -- crates/collab/src/main.rs | 7 - crates/collab/src/stripe_billing.rs | 156 ----- crates/collab/src/stripe_client.rs | 285 -------- .../src/stripe_client/fake_stripe_client.rs | 247 ------- .../src/stripe_client/real_stripe_client.rs | 612 ------------------ crates/collab/src/tests.rs | 2 - .../collab/src/tests/stripe_billing_tests.rs | 123 ---- crates/collab/src/tests/test_server.rs | 5 - 16 files changed, 2 insertions(+), 1738 deletions(-) delete mode 100644 crates/collab/src/api/billing.rs delete mode 100644 crates/collab/src/stripe_billing.rs delete mode 100644 crates/collab/src/stripe_client.rs delete mode 100644 crates/collab/src/stripe_client/fake_stripe_client.rs delete mode 100644 crates/collab/src/stripe_client/real_stripe_client.rs delete mode 100644 crates/collab/src/tests/stripe_billing_tests.rs diff --git a/Cargo.lock b/Cargo.lock index bfc797d6cd..2be16cc22f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1262,26 +1262,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "async-stripe" -version = "0.40.0" -source = "git+https://github.com/zed-industries/async-stripe?rev=3672dd4efb7181aa597bf580bf5a2f5d23db6735#3672dd4efb7181aa597bf580bf5a2f5d23db6735" -dependencies = [ - "chrono", - "futures-util", - "http-types", - "hyper 0.14.32", - "hyper-rustls 0.24.2", - "serde", - "serde_json", - "serde_path_to_error", - "serde_qs 0.10.1", - "smart-default 0.6.0", - "smol_str 0.1.24", - "thiserror 1.0.69", - "tokio", -] - [[package]] name = "async-tar" version = "0.5.0" @@ -2083,12 +2063,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.7" @@ -3281,7 +3255,6 @@ dependencies = [ "anyhow", "assistant_context", "assistant_slash_command", - "async-stripe", "async-trait", "async-tungstenite", "audio", @@ -3308,7 +3281,6 @@ dependencies = [ "dap_adapters", "dashmap 6.1.0", "debugger_ui", - "derive_more 0.99.19", "editor", "envy", "extension", @@ -3870,7 +3842,7 @@ dependencies = [ "rustc-hash 1.1.0", "rustybuzz 0.14.1", "self_cell", - "smol_str 0.2.2", + "smol_str", "swash", "sys-locale", "ttf-parser 0.21.1", @@ -6374,17 +6346,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.15" @@ -7988,27 +7949,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" -[[package]] -name = "http-types" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" -dependencies = [ - "anyhow", - "async-channel 1.9.0", - "base64 0.13.1", - "futures-lite 1.13.0", - "http 0.2.12", - "infer", - "pin-project-lite", - "rand 0.7.3", - "serde", - "serde_json", - "serde_qs 0.8.5", - "serde_urlencoded", - "url", -] - [[package]] name = "http_client" version = "0.1.0" @@ -8487,12 +8427,6 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" -[[package]] -name = "infer" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" - [[package]] name = "inherent" version = "1.0.12" @@ -10269,7 +10203,7 @@ dependencies = [ "num-traits", "range-map", "scroll", - "smart-default 0.7.1", + "smart-default", ] [[package]] @@ -13143,19 +13077,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", -] - [[package]] name = "rand" version = "0.8.5" @@ -13177,16 +13098,6 @@ dependencies = [ "rand_core 0.9.3", ] -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - [[package]] name = "rand_chacha" version = "0.3.1" @@ -13207,15 +13118,6 @@ dependencies = [ "rand_core 0.9.3", ] -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - [[package]] name = "rand_core" version = "0.6.4" @@ -13234,15 +13136,6 @@ dependencies = [ "getrandom 0.3.2", ] -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - [[package]] name = "range-map" version = "0.2.0" @@ -14897,28 +14790,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_qs" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" -dependencies = [ - "percent-encoding", - "serde", - "thiserror 1.0.69", -] - -[[package]] -name = "serde_qs" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cac3f1e2ca2fe333923a1ae72caca910b98ed0630bb35ef6f8c8517d6e81afa" -dependencies = [ - "percent-encoding", - "serde", - "thiserror 1.0.69", -] - [[package]] name = "serde_repr" version = "0.1.20" @@ -15295,17 +15166,6 @@ dependencies = [ "serde", ] -[[package]] -name = "smart-default" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "smart-default" version = "0.7.1" @@ -15334,15 +15194,6 @@ dependencies = [ "futures-lite 2.6.0", ] -[[package]] -name = "smol_str" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad6c857cbab2627dcf01ec85a623ca4e7dcb5691cbaa3d7fb7653671f0d09c9" -dependencies = [ - "serde", -] - [[package]] name = "smol_str" version = "0.2.2" @@ -18191,12 +18042,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index baa4ee7f4e..644b6c0f40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -667,20 +667,6 @@ workspace-hack = "0.1.0" yawc = { git = "https://github.com/deviant-forks/yawc", rev = "1899688f3e69ace4545aceb97b2a13881cf26142" } zstd = "0.11" -[workspace.dependencies.async-stripe] -git = "https://github.com/zed-industries/async-stripe" -rev = "3672dd4efb7181aa597bf580bf5a2f5d23db6735" -default-features = false -features = [ - "runtime-tokio-hyper-rustls", - "billing", - "checkout", - "events", - # The features below are only enabled to get the `events` feature to build. - "chrono", - "connect", -] - [workspace.dependencies.windows] version = "0.61" features = [ diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 9a867f9e05..6fc591be13 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -19,7 +19,6 @@ test-support = ["sqlite"] [dependencies] anyhow.workspace = true -async-stripe.workspace = true async-trait.workspace = true async-tungstenite.workspace = true aws-config = { version = "1.1.5" } @@ -33,7 +32,6 @@ clock.workspace = true cloud_llm_client.workspace = true collections.workspace = true dashmap.workspace = true -derive_more.workspace = true envy = "0.4.2" futures.workspace = true gpui.workspace = true @@ -134,6 +132,3 @@ util.workspace = true workspace = { workspace = true, features = ["test-support"] } worktree = { workspace = true, features = ["test-support"] } zlog.workspace = true - -[package.metadata.cargo-machete] -ignored = ["async-stripe"] diff --git a/crates/collab/k8s/collab.template.yml b/crates/collab/k8s/collab.template.yml index 45fc018a4a..214b550ac2 100644 --- a/crates/collab/k8s/collab.template.yml +++ b/crates/collab/k8s/collab.template.yml @@ -219,12 +219,6 @@ spec: secretKeyRef: name: slack key: panics_webhook - - name: STRIPE_API_KEY - valueFrom: - secretKeyRef: - name: stripe - key: api_key - optional: true - name: COMPLETE_WITH_LANGUAGE_MODEL_RATE_LIMIT_PER_HOUR value: "1000" - name: SUPERMAVEN_ADMIN_API_KEY diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 078a4469ae..143e764eb3 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -1,4 +1,3 @@ -pub mod billing; pub mod contributors; pub mod events; pub mod extensions; diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs deleted file mode 100644 index a0325d14c4..0000000000 --- a/crates/collab/src/api/billing.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::sync::Arc; -use stripe::SubscriptionStatus; - -use crate::AppState; -use crate::db::billing_subscription::StripeSubscriptionStatus; -use crate::db::{CreateBillingCustomerParams, billing_customer}; -use crate::stripe_client::{StripeClient, StripeCustomerId}; - -impl From for StripeSubscriptionStatus { - fn from(value: SubscriptionStatus) -> Self { - match value { - SubscriptionStatus::Incomplete => Self::Incomplete, - SubscriptionStatus::IncompleteExpired => Self::IncompleteExpired, - SubscriptionStatus::Trialing => Self::Trialing, - SubscriptionStatus::Active => Self::Active, - SubscriptionStatus::PastDue => Self::PastDue, - SubscriptionStatus::Canceled => Self::Canceled, - SubscriptionStatus::Unpaid => Self::Unpaid, - SubscriptionStatus::Paused => Self::Paused, - } - } -} - -/// Finds or creates a billing customer using the provided customer. -pub async fn find_or_create_billing_customer( - app: &Arc, - stripe_client: &dyn StripeClient, - customer_id: &StripeCustomerId, -) -> anyhow::Result> { - // If we already have a billing customer record associated with the Stripe customer, - // there's nothing more we need to do. - if let Some(billing_customer) = app - .db - .get_billing_customer_by_stripe_customer_id(customer_id.0.as_ref()) - .await? - { - return Ok(Some(billing_customer)); - } - - let customer = stripe_client.get_customer(customer_id).await?; - - let Some(email) = customer.email else { - return Ok(None); - }; - - let Some(user) = app.db.get_user_by_email(&email).await? else { - return Ok(None); - }; - - let billing_customer = app - .db - .create_billing_customer(&CreateBillingCustomerParams { - user_id: user.id, - stripe_customer_id: customer.id.to_string(), - }) - .await?; - - Ok(Some(billing_customer)) -} diff --git a/crates/collab/src/db/tables/billing_subscription.rs b/crates/collab/src/db/tables/billing_subscription.rs index 522973dbc9..f5684aeec3 100644 --- a/crates/collab/src/db/tables/billing_subscription.rs +++ b/crates/collab/src/db/tables/billing_subscription.rs @@ -1,5 +1,4 @@ use crate::db::{BillingCustomerId, BillingSubscriptionId}; -use crate::stripe_client; use chrono::{Datelike as _, NaiveDate, Utc}; use sea_orm::entity::prelude::*; use serde::Serialize; @@ -160,17 +159,3 @@ pub enum StripeCancellationReason { #[sea_orm(string_value = "payment_failed")] PaymentFailed, } - -impl From for StripeCancellationReason { - fn from(value: stripe_client::StripeCancellationDetailsReason) -> Self { - match value { - stripe_client::StripeCancellationDetailsReason::CancellationRequested => { - Self::CancellationRequested - } - stripe_client::StripeCancellationDetailsReason::PaymentDisputed => { - Self::PaymentDisputed - } - stripe_client::StripeCancellationDetailsReason::PaymentFailed => Self::PaymentFailed, - } - } -} diff --git a/crates/collab/src/lib.rs b/crates/collab/src/lib.rs index 905859ca69..a68286a5a3 100644 --- a/crates/collab/src/lib.rs +++ b/crates/collab/src/lib.rs @@ -7,8 +7,6 @@ pub mod llm; pub mod migrations; pub mod rpc; pub mod seed; -pub mod stripe_billing; -pub mod stripe_client; pub mod user_backfiller; #[cfg(test)] @@ -27,16 +25,12 @@ use serde::Deserialize; use std::{path::PathBuf, sync::Arc}; use util::ResultExt; -use crate::stripe_billing::StripeBilling; -use crate::stripe_client::{RealStripeClient, StripeClient}; - pub type Result = std::result::Result; pub enum Error { Http(StatusCode, String, HeaderMap), Database(sea_orm::error::DbErr), Internal(anyhow::Error), - Stripe(stripe::StripeError), } impl From for Error { @@ -51,12 +45,6 @@ impl From for Error { } } -impl From for Error { - fn from(error: stripe::StripeError) -> Self { - Self::Stripe(error) - } -} - impl From for Error { fn from(error: axum::Error) -> Self { Self::Internal(error.into()) @@ -104,14 +92,6 @@ impl IntoResponse for Error { ); (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response() } - Error::Stripe(error) => { - log::error!( - "HTTP error {}: {:?}", - StatusCode::INTERNAL_SERVER_ERROR, - &error - ); - (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response() - } } } } @@ -122,7 +102,6 @@ impl std::fmt::Debug for Error { Error::Http(code, message, _headers) => (code, message).fmt(f), Error::Database(error) => error.fmt(f), Error::Internal(error) => error.fmt(f), - Error::Stripe(error) => error.fmt(f), } } } @@ -133,7 +112,6 @@ impl std::fmt::Display for Error { Error::Http(code, message, _) => write!(f, "{code}: {message}"), Error::Database(error) => error.fmt(f), Error::Internal(error) => error.fmt(f), - Error::Stripe(error) => error.fmt(f), } } } @@ -179,7 +157,6 @@ pub struct Config { pub zed_client_checksum_seed: Option, pub slack_panics_webhook: Option, pub auto_join_channel_id: Option, - pub stripe_api_key: Option, pub supermaven_admin_api_key: Option>, pub user_backfiller_github_access_token: Option>, } @@ -234,7 +211,6 @@ impl Config { auto_join_channel_id: None, migrations_path: None, seed_path: None, - stripe_api_key: None, supermaven_admin_api_key: None, user_backfiller_github_access_token: None, kinesis_region: None, @@ -269,11 +245,6 @@ pub struct AppState { pub llm_db: Option>, pub livekit_client: Option>, pub blob_store_client: Option, - /// This is a real instance of the Stripe client; we're working to replace references to this with the - /// [`StripeClient`] trait. - pub real_stripe_client: Option>, - pub stripe_client: Option>, - pub stripe_billing: Option>, pub executor: Executor, pub kinesis_client: Option<::aws_sdk_kinesis::Client>, pub config: Config, @@ -316,18 +287,11 @@ impl AppState { }; let db = Arc::new(db); - let stripe_client = build_stripe_client(&config).map(Arc::new).log_err(); let this = Self { db: db.clone(), llm_db, livekit_client, blob_store_client: build_blob_store_client(&config).await.log_err(), - stripe_billing: stripe_client - .clone() - .map(|stripe_client| Arc::new(StripeBilling::new(stripe_client))), - real_stripe_client: stripe_client.clone(), - stripe_client: stripe_client - .map(|stripe_client| Arc::new(RealStripeClient::new(stripe_client)) as _), executor, kinesis_client: if config.kinesis_access_key.is_some() { build_kinesis_client(&config).await.log_err() @@ -340,14 +304,6 @@ impl AppState { } } -fn build_stripe_client(config: &Config) -> anyhow::Result { - let api_key = config - .stripe_api_key - .as_ref() - .context("missing stripe_api_key")?; - Ok(stripe::Client::new(api_key)) -} - async fn build_blob_store_client(config: &Config) -> anyhow::Result { let keys = aws_sdk_s3::config::Credentials::new( config diff --git a/crates/collab/src/main.rs b/crates/collab/src/main.rs index 20641cb232..177c97f076 100644 --- a/crates/collab/src/main.rs +++ b/crates/collab/src/main.rs @@ -102,13 +102,6 @@ async fn main() -> Result<()> { let state = AppState::new(config, Executor::Production).await?; - if let Some(stripe_billing) = state.stripe_billing.clone() { - let executor = state.executor.clone(); - executor.spawn_detached(async move { - stripe_billing.initialize().await.trace_err(); - }); - } - if mode.is_collab() { state.db.purge_old_embeddings().await.trace_err(); diff --git a/crates/collab/src/stripe_billing.rs b/crates/collab/src/stripe_billing.rs deleted file mode 100644 index ef5bef3e7e..0000000000 --- a/crates/collab/src/stripe_billing.rs +++ /dev/null @@ -1,156 +0,0 @@ -use std::sync::Arc; - -use anyhow::anyhow; -use collections::HashMap; -use stripe::SubscriptionStatus; -use tokio::sync::RwLock; - -use crate::Result; -use crate::stripe_client::{ - RealStripeClient, StripeAutomaticTax, StripeClient, StripeCreateSubscriptionItems, - StripeCreateSubscriptionParams, StripeCustomerId, StripePrice, StripePriceId, - StripeSubscription, -}; - -pub struct StripeBilling { - state: RwLock, - client: Arc, -} - -#[derive(Default)] -struct StripeBillingState { - prices_by_lookup_key: HashMap, -} - -impl StripeBilling { - pub fn new(client: Arc) -> Self { - Self { - client: Arc::new(RealStripeClient::new(client.clone())), - state: RwLock::default(), - } - } - - #[cfg(test)] - pub fn test(client: Arc) -> Self { - Self { - client, - state: RwLock::default(), - } - } - - pub fn client(&self) -> &Arc { - &self.client - } - - pub async fn initialize(&self) -> Result<()> { - log::info!("StripeBilling: initializing"); - - let mut state = self.state.write().await; - - let prices = self.client.list_prices().await?; - - for price in prices { - if let Some(lookup_key) = price.lookup_key.clone() { - state.prices_by_lookup_key.insert(lookup_key, price); - } - } - - log::info!("StripeBilling: initialized"); - - Ok(()) - } - - pub async fn zed_pro_price_id(&self) -> Result { - self.find_price_id_by_lookup_key("zed-pro").await - } - - pub async fn zed_free_price_id(&self) -> Result { - self.find_price_id_by_lookup_key("zed-free").await - } - - pub async fn find_price_id_by_lookup_key(&self, lookup_key: &str) -> Result { - self.state - .read() - .await - .prices_by_lookup_key - .get(lookup_key) - .map(|price| price.id.clone()) - .ok_or_else(|| crate::Error::Internal(anyhow!("no price ID found for {lookup_key:?}"))) - } - - pub async fn find_price_by_lookup_key(&self, lookup_key: &str) -> Result { - self.state - .read() - .await - .prices_by_lookup_key - .get(lookup_key) - .cloned() - .ok_or_else(|| crate::Error::Internal(anyhow!("no price found for {lookup_key:?}"))) - } - - /// Returns the Stripe customer associated with the provided email address, or creates a new customer, if one does - /// not already exist. - /// - /// Always returns a new Stripe customer if the email address is `None`. - pub async fn find_or_create_customer_by_email( - &self, - email_address: Option<&str>, - ) -> Result { - let existing_customer = if let Some(email) = email_address { - let customers = self.client.list_customers_by_email(email).await?; - - customers.first().cloned() - } else { - None - }; - - let customer_id = if let Some(existing_customer) = existing_customer { - existing_customer.id - } else { - let customer = self - .client - .create_customer(crate::stripe_client::CreateCustomerParams { - email: email_address, - }) - .await?; - - customer.id - }; - - Ok(customer_id) - } - - pub async fn subscribe_to_zed_free( - &self, - customer_id: StripeCustomerId, - ) -> Result { - let zed_free_price_id = self.zed_free_price_id().await?; - - let existing_subscriptions = self - .client - .list_subscriptions_for_customer(&customer_id) - .await?; - - let existing_active_subscription = - existing_subscriptions.into_iter().find(|subscription| { - subscription.status == SubscriptionStatus::Active - || subscription.status == SubscriptionStatus::Trialing - }); - if let Some(subscription) = existing_active_subscription { - return Ok(subscription); - } - - let params = StripeCreateSubscriptionParams { - customer: customer_id, - items: vec![StripeCreateSubscriptionItems { - price: Some(zed_free_price_id), - quantity: Some(1), - }], - automatic_tax: Some(StripeAutomaticTax { enabled: true }), - }; - - let subscription = self.client.create_subscription(params).await?; - - Ok(subscription) - } -} diff --git a/crates/collab/src/stripe_client.rs b/crates/collab/src/stripe_client.rs deleted file mode 100644 index 6e75a4d874..0000000000 --- a/crates/collab/src/stripe_client.rs +++ /dev/null @@ -1,285 +0,0 @@ -#[cfg(test)] -mod fake_stripe_client; -mod real_stripe_client; - -use std::collections::HashMap; -use std::sync::Arc; - -use anyhow::Result; -use async_trait::async_trait; - -#[cfg(test)] -pub use fake_stripe_client::*; -pub use real_stripe_client::*; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display, Serialize)] -pub struct StripeCustomerId(pub Arc); - -#[derive(Debug, Clone)] -pub struct StripeCustomer { - pub id: StripeCustomerId, - pub email: Option, -} - -#[derive(Debug)] -pub struct CreateCustomerParams<'a> { - pub email: Option<&'a str>, -} - -#[derive(Debug)] -pub struct UpdateCustomerParams<'a> { - pub email: Option<&'a str>, -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display)] -pub struct StripeSubscriptionId(pub Arc); - -#[derive(Debug, PartialEq, Clone)] -pub struct StripeSubscription { - pub id: StripeSubscriptionId, - pub customer: StripeCustomerId, - // TODO: Create our own version of this enum. - pub status: stripe::SubscriptionStatus, - pub current_period_end: i64, - pub current_period_start: i64, - pub items: Vec, - pub cancel_at: Option, - pub cancellation_details: Option, -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display)] -pub struct StripeSubscriptionItemId(pub Arc); - -#[derive(Debug, PartialEq, Clone)] -pub struct StripeSubscriptionItem { - pub id: StripeSubscriptionItemId, - pub price: Option, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct StripeCancellationDetails { - pub reason: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum StripeCancellationDetailsReason { - CancellationRequested, - PaymentDisputed, - PaymentFailed, -} - -#[derive(Debug)] -pub struct StripeCreateSubscriptionParams { - pub customer: StripeCustomerId, - pub items: Vec, - pub automatic_tax: Option, -} - -#[derive(Debug)] -pub struct StripeCreateSubscriptionItems { - pub price: Option, - pub quantity: Option, -} - -#[derive(Debug, Clone)] -pub struct UpdateSubscriptionParams { - pub items: Option>, - pub trial_settings: Option, -} - -#[derive(Debug, PartialEq, Clone)] -pub struct UpdateSubscriptionItems { - pub price: Option, -} - -#[derive(Debug, PartialEq, Clone)] -pub struct StripeSubscriptionTrialSettings { - pub end_behavior: StripeSubscriptionTrialSettingsEndBehavior, -} - -#[derive(Debug, PartialEq, Clone)] -pub struct StripeSubscriptionTrialSettingsEndBehavior { - pub missing_payment_method: StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod { - Cancel, - CreateInvoice, - Pause, -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display)] -pub struct StripePriceId(pub Arc); - -#[derive(Debug, PartialEq, Clone)] -pub struct StripePrice { - pub id: StripePriceId, - pub unit_amount: Option, - pub lookup_key: Option, - pub recurring: Option, -} - -#[derive(Debug, PartialEq, Clone)] -pub struct StripePriceRecurring { - pub meter: Option, -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display, Deserialize)] -pub struct StripeMeterId(pub Arc); - -#[derive(Debug, Clone, Deserialize)] -pub struct StripeMeter { - pub id: StripeMeterId, - pub event_name: String, -} - -#[derive(Debug, Serialize)] -pub struct StripeCreateMeterEventParams<'a> { - pub identifier: &'a str, - pub event_name: &'a str, - pub payload: StripeCreateMeterEventPayload<'a>, - pub timestamp: Option, -} - -#[derive(Debug, Serialize)] -pub struct StripeCreateMeterEventPayload<'a> { - pub value: u64, - pub stripe_customer_id: &'a StripeCustomerId, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum StripeBillingAddressCollection { - Auto, - Required, -} - -#[derive(Debug, PartialEq, Clone)] -pub struct StripeCustomerUpdate { - pub address: Option, - pub name: Option, - pub shipping: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum StripeCustomerUpdateAddress { - Auto, - Never, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum StripeCustomerUpdateName { - Auto, - Never, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum StripeCustomerUpdateShipping { - Auto, - Never, -} - -#[derive(Debug, Default)] -pub struct StripeCreateCheckoutSessionParams<'a> { - pub customer: Option<&'a StripeCustomerId>, - pub client_reference_id: Option<&'a str>, - pub mode: Option, - pub line_items: Option>, - pub payment_method_collection: Option, - pub subscription_data: Option, - pub success_url: Option<&'a str>, - pub billing_address_collection: Option, - pub customer_update: Option, - pub tax_id_collection: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum StripeCheckoutSessionMode { - Payment, - Setup, - Subscription, -} - -#[derive(Debug, PartialEq, Clone)] -pub struct StripeCreateCheckoutSessionLineItems { - pub price: Option, - pub quantity: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum StripeCheckoutSessionPaymentMethodCollection { - Always, - IfRequired, -} - -#[derive(Debug, PartialEq, Clone)] -pub struct StripeCreateCheckoutSessionSubscriptionData { - pub metadata: Option>, - pub trial_period_days: Option, - pub trial_settings: Option, -} - -#[derive(Debug, PartialEq, Clone)] -pub struct StripeTaxIdCollection { - pub enabled: bool, -} - -#[derive(Debug, Clone)] -pub struct StripeAutomaticTax { - pub enabled: bool, -} - -#[derive(Debug)] -pub struct StripeCheckoutSession { - pub url: Option, -} - -#[async_trait] -pub trait StripeClient: Send + Sync { - async fn list_customers_by_email(&self, email: &str) -> Result>; - - async fn get_customer(&self, customer_id: &StripeCustomerId) -> Result; - - async fn create_customer(&self, params: CreateCustomerParams<'_>) -> Result; - - async fn update_customer( - &self, - customer_id: &StripeCustomerId, - params: UpdateCustomerParams<'_>, - ) -> Result; - - async fn list_subscriptions_for_customer( - &self, - customer_id: &StripeCustomerId, - ) -> Result>; - - async fn get_subscription( - &self, - subscription_id: &StripeSubscriptionId, - ) -> Result; - - async fn create_subscription( - &self, - params: StripeCreateSubscriptionParams, - ) -> Result; - - async fn update_subscription( - &self, - subscription_id: &StripeSubscriptionId, - params: UpdateSubscriptionParams, - ) -> Result<()>; - - async fn cancel_subscription(&self, subscription_id: &StripeSubscriptionId) -> Result<()>; - - async fn list_prices(&self) -> Result>; - - async fn list_meters(&self) -> Result>; - - async fn create_meter_event(&self, params: StripeCreateMeterEventParams<'_>) -> Result<()>; - - async fn create_checkout_session( - &self, - params: StripeCreateCheckoutSessionParams<'_>, - ) -> Result; -} diff --git a/crates/collab/src/stripe_client/fake_stripe_client.rs b/crates/collab/src/stripe_client/fake_stripe_client.rs deleted file mode 100644 index 9bb08443ec..0000000000 --- a/crates/collab/src/stripe_client/fake_stripe_client.rs +++ /dev/null @@ -1,247 +0,0 @@ -use std::sync::Arc; - -use anyhow::{Result, anyhow}; -use async_trait::async_trait; -use chrono::{Duration, Utc}; -use collections::HashMap; -use parking_lot::Mutex; -use uuid::Uuid; - -use crate::stripe_client::{ - CreateCustomerParams, StripeBillingAddressCollection, StripeCheckoutSession, - StripeCheckoutSessionMode, StripeCheckoutSessionPaymentMethodCollection, StripeClient, - StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams, - StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams, - StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeCustomerUpdate, - StripeMeter, StripeMeterId, StripePrice, StripePriceId, StripeSubscription, - StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, StripeTaxIdCollection, - UpdateCustomerParams, UpdateSubscriptionParams, -}; - -#[derive(Debug, Clone)] -pub struct StripeCreateMeterEventCall { - pub identifier: Arc, - pub event_name: Arc, - pub value: u64, - pub stripe_customer_id: StripeCustomerId, - pub timestamp: Option, -} - -#[derive(Debug, Clone)] -pub struct StripeCreateCheckoutSessionCall { - pub customer: Option, - pub client_reference_id: Option, - pub mode: Option, - pub line_items: Option>, - pub payment_method_collection: Option, - pub subscription_data: Option, - pub success_url: Option, - pub billing_address_collection: Option, - pub customer_update: Option, - pub tax_id_collection: Option, -} - -pub struct FakeStripeClient { - pub customers: Arc>>, - pub subscriptions: Arc>>, - pub update_subscription_calls: - Arc>>, - pub prices: Arc>>, - pub meters: Arc>>, - pub create_meter_event_calls: Arc>>, - pub create_checkout_session_calls: Arc>>, -} - -impl FakeStripeClient { - pub fn new() -> Self { - Self { - customers: Arc::new(Mutex::new(HashMap::default())), - subscriptions: Arc::new(Mutex::new(HashMap::default())), - update_subscription_calls: Arc::new(Mutex::new(Vec::new())), - prices: Arc::new(Mutex::new(HashMap::default())), - meters: Arc::new(Mutex::new(HashMap::default())), - create_meter_event_calls: Arc::new(Mutex::new(Vec::new())), - create_checkout_session_calls: Arc::new(Mutex::new(Vec::new())), - } - } -} - -#[async_trait] -impl StripeClient for FakeStripeClient { - async fn list_customers_by_email(&self, email: &str) -> Result> { - Ok(self - .customers - .lock() - .values() - .filter(|customer| customer.email.as_deref() == Some(email)) - .cloned() - .collect()) - } - - async fn get_customer(&self, customer_id: &StripeCustomerId) -> Result { - self.customers - .lock() - .get(customer_id) - .cloned() - .ok_or_else(|| anyhow!("no customer found for {customer_id:?}")) - } - - async fn create_customer(&self, params: CreateCustomerParams<'_>) -> Result { - let customer = StripeCustomer { - id: StripeCustomerId(format!("cus_{}", Uuid::new_v4()).into()), - email: params.email.map(|email| email.to_string()), - }; - - self.customers - .lock() - .insert(customer.id.clone(), customer.clone()); - - Ok(customer) - } - - async fn update_customer( - &self, - customer_id: &StripeCustomerId, - params: UpdateCustomerParams<'_>, - ) -> Result { - let mut customers = self.customers.lock(); - if let Some(customer) = customers.get_mut(customer_id) { - if let Some(email) = params.email { - customer.email = Some(email.to_string()); - } - Ok(customer.clone()) - } else { - Err(anyhow!("no customer found for {customer_id:?}")) - } - } - - async fn list_subscriptions_for_customer( - &self, - customer_id: &StripeCustomerId, - ) -> Result> { - let subscriptions = self - .subscriptions - .lock() - .values() - .filter(|subscription| subscription.customer == *customer_id) - .cloned() - .collect(); - - Ok(subscriptions) - } - - async fn get_subscription( - &self, - subscription_id: &StripeSubscriptionId, - ) -> Result { - self.subscriptions - .lock() - .get(subscription_id) - .cloned() - .ok_or_else(|| anyhow!("no subscription found for {subscription_id:?}")) - } - - async fn create_subscription( - &self, - params: StripeCreateSubscriptionParams, - ) -> Result { - let now = Utc::now(); - - let subscription = StripeSubscription { - id: StripeSubscriptionId(format!("sub_{}", Uuid::new_v4()).into()), - customer: params.customer, - status: stripe::SubscriptionStatus::Active, - current_period_start: now.timestamp(), - current_period_end: (now + Duration::days(30)).timestamp(), - items: params - .items - .into_iter() - .map(|item| StripeSubscriptionItem { - id: StripeSubscriptionItemId(format!("si_{}", Uuid::new_v4()).into()), - price: item - .price - .and_then(|price_id| self.prices.lock().get(&price_id).cloned()), - }) - .collect(), - cancel_at: None, - cancellation_details: None, - }; - - self.subscriptions - .lock() - .insert(subscription.id.clone(), subscription.clone()); - - Ok(subscription) - } - - async fn update_subscription( - &self, - subscription_id: &StripeSubscriptionId, - params: UpdateSubscriptionParams, - ) -> Result<()> { - let subscription = self.get_subscription(subscription_id).await?; - - self.update_subscription_calls - .lock() - .push((subscription.id, params)); - - Ok(()) - } - - async fn cancel_subscription(&self, subscription_id: &StripeSubscriptionId) -> Result<()> { - // TODO: Implement fake subscription cancellation. - let _ = subscription_id; - - Ok(()) - } - - async fn list_prices(&self) -> Result> { - let prices = self.prices.lock().values().cloned().collect(); - - Ok(prices) - } - - async fn list_meters(&self) -> Result> { - let meters = self.meters.lock().values().cloned().collect(); - - Ok(meters) - } - - async fn create_meter_event(&self, params: StripeCreateMeterEventParams<'_>) -> Result<()> { - self.create_meter_event_calls - .lock() - .push(StripeCreateMeterEventCall { - identifier: params.identifier.into(), - event_name: params.event_name.into(), - value: params.payload.value, - stripe_customer_id: params.payload.stripe_customer_id.clone(), - timestamp: params.timestamp, - }); - - Ok(()) - } - - async fn create_checkout_session( - &self, - params: StripeCreateCheckoutSessionParams<'_>, - ) -> Result { - self.create_checkout_session_calls - .lock() - .push(StripeCreateCheckoutSessionCall { - customer: params.customer.cloned(), - client_reference_id: params.client_reference_id.map(|id| id.to_string()), - mode: params.mode, - line_items: params.line_items, - payment_method_collection: params.payment_method_collection, - subscription_data: params.subscription_data, - success_url: params.success_url.map(|url| url.to_string()), - billing_address_collection: params.billing_address_collection, - customer_update: params.customer_update, - tax_id_collection: params.tax_id_collection, - }); - - Ok(StripeCheckoutSession { - url: Some("https://checkout.stripe.com/c/pay/cs_test_1".to_string()), - }) - } -} diff --git a/crates/collab/src/stripe_client/real_stripe_client.rs b/crates/collab/src/stripe_client/real_stripe_client.rs deleted file mode 100644 index 07c191ff30..0000000000 --- a/crates/collab/src/stripe_client/real_stripe_client.rs +++ /dev/null @@ -1,612 +0,0 @@ -use std::str::FromStr as _; -use std::sync::Arc; - -use anyhow::{Context as _, Result, anyhow}; -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use stripe::{ - CancellationDetails, CancellationDetailsReason, CheckoutSession, CheckoutSessionMode, - CheckoutSessionPaymentMethodCollection, CreateCheckoutSession, CreateCheckoutSessionLineItems, - CreateCheckoutSessionSubscriptionData, CreateCheckoutSessionSubscriptionDataTrialSettings, - CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior, - CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod, - CreateCustomer, CreateSubscriptionAutomaticTax, Customer, CustomerId, ListCustomers, Price, - PriceId, Recurring, Subscription, SubscriptionId, SubscriptionItem, SubscriptionItemId, - UpdateCustomer, UpdateSubscriptionItems, UpdateSubscriptionTrialSettings, - UpdateSubscriptionTrialSettingsEndBehavior, - UpdateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, -}; - -use crate::stripe_client::{ - CreateCustomerParams, StripeAutomaticTax, StripeBillingAddressCollection, - StripeCancellationDetails, StripeCancellationDetailsReason, StripeCheckoutSession, - StripeCheckoutSessionMode, StripeCheckoutSessionPaymentMethodCollection, StripeClient, - StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams, - StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams, - StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeCustomerUpdate, - StripeCustomerUpdateAddress, StripeCustomerUpdateName, StripeCustomerUpdateShipping, - StripeMeter, StripePrice, StripePriceId, StripePriceRecurring, StripeSubscription, - StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, - StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior, - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, StripeTaxIdCollection, - UpdateCustomerParams, UpdateSubscriptionParams, -}; - -pub struct RealStripeClient { - client: Arc, -} - -impl RealStripeClient { - pub fn new(client: Arc) -> Self { - Self { client } - } -} - -#[async_trait] -impl StripeClient for RealStripeClient { - async fn list_customers_by_email(&self, email: &str) -> Result> { - let response = Customer::list( - &self.client, - &ListCustomers { - email: Some(email), - ..Default::default() - }, - ) - .await?; - - Ok(response - .data - .into_iter() - .map(StripeCustomer::from) - .collect()) - } - - async fn get_customer(&self, customer_id: &StripeCustomerId) -> Result { - let customer_id = customer_id.try_into()?; - - let customer = Customer::retrieve(&self.client, &customer_id, &[]).await?; - - Ok(StripeCustomer::from(customer)) - } - - async fn create_customer(&self, params: CreateCustomerParams<'_>) -> Result { - let customer = Customer::create( - &self.client, - CreateCustomer { - email: params.email, - ..Default::default() - }, - ) - .await?; - - Ok(StripeCustomer::from(customer)) - } - - async fn update_customer( - &self, - customer_id: &StripeCustomerId, - params: UpdateCustomerParams<'_>, - ) -> Result { - let customer = Customer::update( - &self.client, - &customer_id.try_into()?, - UpdateCustomer { - email: params.email, - ..Default::default() - }, - ) - .await?; - - Ok(StripeCustomer::from(customer)) - } - - async fn list_subscriptions_for_customer( - &self, - customer_id: &StripeCustomerId, - ) -> Result> { - let customer_id = customer_id.try_into()?; - - let subscriptions = stripe::Subscription::list( - &self.client, - &stripe::ListSubscriptions { - customer: Some(customer_id), - status: None, - ..Default::default() - }, - ) - .await?; - - Ok(subscriptions - .data - .into_iter() - .map(StripeSubscription::from) - .collect()) - } - - async fn get_subscription( - &self, - subscription_id: &StripeSubscriptionId, - ) -> Result { - let subscription_id = subscription_id.try_into()?; - - let subscription = Subscription::retrieve(&self.client, &subscription_id, &[]).await?; - - Ok(StripeSubscription::from(subscription)) - } - - async fn create_subscription( - &self, - params: StripeCreateSubscriptionParams, - ) -> Result { - let customer_id = params.customer.try_into()?; - - let mut create_subscription = stripe::CreateSubscription::new(customer_id); - create_subscription.items = Some( - params - .items - .into_iter() - .map(|item| stripe::CreateSubscriptionItems { - price: item.price.map(|price| price.to_string()), - quantity: item.quantity, - ..Default::default() - }) - .collect(), - ); - create_subscription.automatic_tax = params.automatic_tax.map(Into::into); - - let subscription = Subscription::create(&self.client, create_subscription).await?; - - Ok(StripeSubscription::from(subscription)) - } - - async fn update_subscription( - &self, - subscription_id: &StripeSubscriptionId, - params: UpdateSubscriptionParams, - ) -> Result<()> { - let subscription_id = subscription_id.try_into()?; - - stripe::Subscription::update( - &self.client, - &subscription_id, - stripe::UpdateSubscription { - items: params.items.map(|items| { - items - .into_iter() - .map(|item| UpdateSubscriptionItems { - price: item.price.map(|price| price.to_string()), - ..Default::default() - }) - .collect() - }), - trial_settings: params.trial_settings.map(Into::into), - ..Default::default() - }, - ) - .await?; - - Ok(()) - } - - async fn cancel_subscription(&self, subscription_id: &StripeSubscriptionId) -> Result<()> { - let subscription_id = subscription_id.try_into()?; - - Subscription::cancel( - &self.client, - &subscription_id, - stripe::CancelSubscription { - invoice_now: None, - ..Default::default() - }, - ) - .await?; - - Ok(()) - } - - async fn list_prices(&self) -> Result> { - let response = stripe::Price::list( - &self.client, - &stripe::ListPrices { - limit: Some(100), - ..Default::default() - }, - ) - .await?; - - Ok(response.data.into_iter().map(StripePrice::from).collect()) - } - - async fn list_meters(&self) -> Result> { - #[derive(Serialize)] - struct Params { - #[serde(skip_serializing_if = "Option::is_none")] - limit: Option, - } - - let response = self - .client - .get_query::, _>( - "/billing/meters", - Params { limit: Some(100) }, - ) - .await?; - - Ok(response.data) - } - - async fn create_meter_event(&self, params: StripeCreateMeterEventParams<'_>) -> Result<()> { - #[derive(Deserialize)] - struct StripeMeterEvent { - pub identifier: String, - } - - let identifier = params.identifier; - match self - .client - .post_form::("/billing/meter_events", params) - .await - { - Ok(_event) => Ok(()), - Err(stripe::StripeError::Stripe(error)) => { - if error.http_status == 400 - && error - .message - .as_ref() - .map_or(false, |message| message.contains(identifier)) - { - Ok(()) - } else { - Err(anyhow!(stripe::StripeError::Stripe(error))) - } - } - Err(error) => Err(anyhow!("failed to create meter event: {error:?}")), - } - } - - async fn create_checkout_session( - &self, - params: StripeCreateCheckoutSessionParams<'_>, - ) -> Result { - let params = params.try_into()?; - let session = CheckoutSession::create(&self.client, params).await?; - - Ok(session.into()) - } -} - -impl From for StripeCustomerId { - fn from(value: CustomerId) -> Self { - Self(value.as_str().into()) - } -} - -impl TryFrom for CustomerId { - type Error = anyhow::Error; - - fn try_from(value: StripeCustomerId) -> Result { - Self::from_str(value.0.as_ref()).context("failed to parse Stripe customer ID") - } -} - -impl TryFrom<&StripeCustomerId> for CustomerId { - type Error = anyhow::Error; - - fn try_from(value: &StripeCustomerId) -> Result { - Self::from_str(value.0.as_ref()).context("failed to parse Stripe customer ID") - } -} - -impl From for StripeCustomer { - fn from(value: Customer) -> Self { - StripeCustomer { - id: value.id.into(), - email: value.email, - } - } -} - -impl From for StripeSubscriptionId { - fn from(value: SubscriptionId) -> Self { - Self(value.as_str().into()) - } -} - -impl TryFrom<&StripeSubscriptionId> for SubscriptionId { - type Error = anyhow::Error; - - fn try_from(value: &StripeSubscriptionId) -> Result { - Self::from_str(value.0.as_ref()).context("failed to parse Stripe subscription ID") - } -} - -impl From for StripeSubscription { - fn from(value: Subscription) -> Self { - Self { - id: value.id.into(), - customer: value.customer.id().into(), - status: value.status, - current_period_start: value.current_period_start, - current_period_end: value.current_period_end, - items: value.items.data.into_iter().map(Into::into).collect(), - cancel_at: value.cancel_at, - cancellation_details: value.cancellation_details.map(Into::into), - } - } -} - -impl From for StripeCancellationDetails { - fn from(value: CancellationDetails) -> Self { - Self { - reason: value.reason.map(Into::into), - } - } -} - -impl From for StripeCancellationDetailsReason { - fn from(value: CancellationDetailsReason) -> Self { - match value { - CancellationDetailsReason::CancellationRequested => Self::CancellationRequested, - CancellationDetailsReason::PaymentDisputed => Self::PaymentDisputed, - CancellationDetailsReason::PaymentFailed => Self::PaymentFailed, - } - } -} - -impl From for StripeSubscriptionItemId { - fn from(value: SubscriptionItemId) -> Self { - Self(value.as_str().into()) - } -} - -impl From for StripeSubscriptionItem { - fn from(value: SubscriptionItem) -> Self { - Self { - id: value.id.into(), - price: value.price.map(Into::into), - } - } -} - -impl From for CreateSubscriptionAutomaticTax { - fn from(value: StripeAutomaticTax) -> Self { - Self { - enabled: value.enabled, - liability: None, - } - } -} - -impl From for UpdateSubscriptionTrialSettings { - fn from(value: StripeSubscriptionTrialSettings) -> Self { - Self { - end_behavior: value.end_behavior.into(), - } - } -} - -impl From - for UpdateSubscriptionTrialSettingsEndBehavior -{ - fn from(value: StripeSubscriptionTrialSettingsEndBehavior) -> Self { - Self { - missing_payment_method: value.missing_payment_method.into(), - } - } -} - -impl From - for UpdateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod -{ - fn from(value: StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod) -> Self { - match value { - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel => Self::Cancel, - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::CreateInvoice => { - Self::CreateInvoice - } - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Pause => Self::Pause, - } - } -} - -impl From for StripePriceId { - fn from(value: PriceId) -> Self { - Self(value.as_str().into()) - } -} - -impl TryFrom for PriceId { - type Error = anyhow::Error; - - fn try_from(value: StripePriceId) -> Result { - Self::from_str(value.0.as_ref()).context("failed to parse Stripe price ID") - } -} - -impl From for StripePrice { - fn from(value: Price) -> Self { - Self { - id: value.id.into(), - unit_amount: value.unit_amount, - lookup_key: value.lookup_key, - recurring: value.recurring.map(StripePriceRecurring::from), - } - } -} - -impl From for StripePriceRecurring { - fn from(value: Recurring) -> Self { - Self { meter: value.meter } - } -} - -impl<'a> TryFrom> for CreateCheckoutSession<'a> { - type Error = anyhow::Error; - - fn try_from(value: StripeCreateCheckoutSessionParams<'a>) -> Result { - Ok(Self { - customer: value - .customer - .map(|customer_id| customer_id.try_into()) - .transpose()?, - client_reference_id: value.client_reference_id, - mode: value.mode.map(Into::into), - line_items: value - .line_items - .map(|line_items| line_items.into_iter().map(Into::into).collect()), - payment_method_collection: value.payment_method_collection.map(Into::into), - subscription_data: value.subscription_data.map(Into::into), - success_url: value.success_url, - billing_address_collection: value.billing_address_collection.map(Into::into), - customer_update: value.customer_update.map(Into::into), - tax_id_collection: value.tax_id_collection.map(Into::into), - ..Default::default() - }) - } -} - -impl From for CheckoutSessionMode { - fn from(value: StripeCheckoutSessionMode) -> Self { - match value { - StripeCheckoutSessionMode::Payment => Self::Payment, - StripeCheckoutSessionMode::Setup => Self::Setup, - StripeCheckoutSessionMode::Subscription => Self::Subscription, - } - } -} - -impl From for CreateCheckoutSessionLineItems { - fn from(value: StripeCreateCheckoutSessionLineItems) -> Self { - Self { - price: value.price, - quantity: value.quantity, - ..Default::default() - } - } -} - -impl From for CheckoutSessionPaymentMethodCollection { - fn from(value: StripeCheckoutSessionPaymentMethodCollection) -> Self { - match value { - StripeCheckoutSessionPaymentMethodCollection::Always => Self::Always, - StripeCheckoutSessionPaymentMethodCollection::IfRequired => Self::IfRequired, - } - } -} - -impl From for CreateCheckoutSessionSubscriptionData { - fn from(value: StripeCreateCheckoutSessionSubscriptionData) -> Self { - Self { - trial_period_days: value.trial_period_days, - trial_settings: value.trial_settings.map(Into::into), - metadata: value.metadata, - ..Default::default() - } - } -} - -impl From for CreateCheckoutSessionSubscriptionDataTrialSettings { - fn from(value: StripeSubscriptionTrialSettings) -> Self { - Self { - end_behavior: value.end_behavior.into(), - } - } -} - -impl From - for CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior -{ - fn from(value: StripeSubscriptionTrialSettingsEndBehavior) -> Self { - Self { - missing_payment_method: value.missing_payment_method.into(), - } - } -} - -impl From - for CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod -{ - fn from(value: StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod) -> Self { - match value { - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel => Self::Cancel, - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::CreateInvoice => { - Self::CreateInvoice - } - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Pause => Self::Pause, - } - } -} - -impl From for StripeCheckoutSession { - fn from(value: CheckoutSession) -> Self { - Self { url: value.url } - } -} - -impl From for stripe::CheckoutSessionBillingAddressCollection { - fn from(value: StripeBillingAddressCollection) -> Self { - match value { - StripeBillingAddressCollection::Auto => { - stripe::CheckoutSessionBillingAddressCollection::Auto - } - StripeBillingAddressCollection::Required => { - stripe::CheckoutSessionBillingAddressCollection::Required - } - } - } -} - -impl From for stripe::CreateCheckoutSessionCustomerUpdateAddress { - fn from(value: StripeCustomerUpdateAddress) -> Self { - match value { - StripeCustomerUpdateAddress::Auto => { - stripe::CreateCheckoutSessionCustomerUpdateAddress::Auto - } - StripeCustomerUpdateAddress::Never => { - stripe::CreateCheckoutSessionCustomerUpdateAddress::Never - } - } - } -} - -impl From for stripe::CreateCheckoutSessionCustomerUpdateName { - fn from(value: StripeCustomerUpdateName) -> Self { - match value { - StripeCustomerUpdateName::Auto => stripe::CreateCheckoutSessionCustomerUpdateName::Auto, - StripeCustomerUpdateName::Never => { - stripe::CreateCheckoutSessionCustomerUpdateName::Never - } - } - } -} - -impl From for stripe::CreateCheckoutSessionCustomerUpdateShipping { - fn from(value: StripeCustomerUpdateShipping) -> Self { - match value { - StripeCustomerUpdateShipping::Auto => { - stripe::CreateCheckoutSessionCustomerUpdateShipping::Auto - } - StripeCustomerUpdateShipping::Never => { - stripe::CreateCheckoutSessionCustomerUpdateShipping::Never - } - } - } -} - -impl From for stripe::CreateCheckoutSessionCustomerUpdate { - fn from(value: StripeCustomerUpdate) -> Self { - stripe::CreateCheckoutSessionCustomerUpdate { - address: value.address.map(Into::into), - name: value.name.map(Into::into), - shipping: value.shipping.map(Into::into), - } - } -} - -impl From for stripe::CreateCheckoutSessionTaxIdCollection { - fn from(value: StripeTaxIdCollection) -> Self { - stripe::CreateCheckoutSessionTaxIdCollection { - enabled: value.enabled, - } - } -} diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 8d5d076780..ddf245b06f 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -8,7 +8,6 @@ mod channel_buffer_tests; mod channel_guest_tests; mod channel_message_tests; mod channel_tests; -// mod debug_panel_tests; mod editor_tests; mod following_tests; mod git_tests; @@ -18,7 +17,6 @@ mod random_channel_buffer_tests; mod random_project_collaboration_tests; mod randomized_test_helpers; mod remote_editing_collaboration_tests; -mod stripe_billing_tests; mod test_server; use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust}; diff --git a/crates/collab/src/tests/stripe_billing_tests.rs b/crates/collab/src/tests/stripe_billing_tests.rs deleted file mode 100644 index bb84bedfcf..0000000000 --- a/crates/collab/src/tests/stripe_billing_tests.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::sync::Arc; - -use pretty_assertions::assert_eq; - -use crate::stripe_billing::StripeBilling; -use crate::stripe_client::{FakeStripeClient, StripePrice, StripePriceId, StripePriceRecurring}; - -fn make_stripe_billing() -> (StripeBilling, Arc) { - let stripe_client = Arc::new(FakeStripeClient::new()); - let stripe_billing = StripeBilling::test(stripe_client.clone()); - - (stripe_billing, stripe_client) -} - -#[gpui::test] -async fn test_initialize() { - let (stripe_billing, stripe_client) = make_stripe_billing(); - - // Add test prices - let price1 = StripePrice { - id: StripePriceId("price_1".into()), - unit_amount: Some(1_000), - lookup_key: Some("zed-pro".to_string()), - recurring: None, - }; - let price2 = StripePrice { - id: StripePriceId("price_2".into()), - unit_amount: Some(0), - lookup_key: Some("zed-free".to_string()), - recurring: None, - }; - let price3 = StripePrice { - id: StripePriceId("price_3".into()), - unit_amount: Some(500), - lookup_key: None, - recurring: Some(StripePriceRecurring { - meter: Some("meter_1".to_string()), - }), - }; - stripe_client - .prices - .lock() - .insert(price1.id.clone(), price1); - stripe_client - .prices - .lock() - .insert(price2.id.clone(), price2); - stripe_client - .prices - .lock() - .insert(price3.id.clone(), price3); - - // Initialize the billing system - stripe_billing.initialize().await.unwrap(); - - // Verify that prices can be found by lookup key - let zed_pro_price_id = stripe_billing.zed_pro_price_id().await.unwrap(); - assert_eq!(zed_pro_price_id.to_string(), "price_1"); - - let zed_free_price_id = stripe_billing.zed_free_price_id().await.unwrap(); - assert_eq!(zed_free_price_id.to_string(), "price_2"); - - // Verify that a price can be found by lookup key - let zed_pro_price = stripe_billing - .find_price_by_lookup_key("zed-pro") - .await - .unwrap(); - assert_eq!(zed_pro_price.id.to_string(), "price_1"); - assert_eq!(zed_pro_price.unit_amount, Some(1_000)); - - // Verify that finding a non-existent lookup key returns an error - let result = stripe_billing - .find_price_by_lookup_key("non-existent") - .await; - assert!(result.is_err()); -} - -#[gpui::test] -async fn test_find_or_create_customer_by_email() { - let (stripe_billing, stripe_client) = make_stripe_billing(); - - // Create a customer with an email that doesn't yet correspond to a customer. - { - let email = "user@example.com"; - - let customer_id = stripe_billing - .find_or_create_customer_by_email(Some(email)) - .await - .unwrap(); - - let customer = stripe_client - .customers - .lock() - .get(&customer_id) - .unwrap() - .clone(); - assert_eq!(customer.email.as_deref(), Some(email)); - } - - // Create a customer with an email that corresponds to an existing customer. - { - let email = "user2@example.com"; - - let existing_customer_id = stripe_billing - .find_or_create_customer_by_email(Some(email)) - .await - .unwrap(); - - let customer_id = stripe_billing - .find_or_create_customer_by_email(Some(email)) - .await - .unwrap(); - assert_eq!(customer_id, existing_customer_id); - - let customer = stripe_client - .customers - .lock() - .get(&customer_id) - .unwrap() - .clone(); - assert_eq!(customer.email.as_deref(), Some(email)); - } -} diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index f5a0e8ea81..8c545b0670 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -1,4 +1,3 @@ -use crate::stripe_client::FakeStripeClient; use crate::{ AppState, Config, db::{NewUserParams, UserId, tests::TestDb}, @@ -569,9 +568,6 @@ impl TestServer { llm_db: None, livekit_client: Some(Arc::new(livekit_test_server.create_api_client())), blob_store_client: None, - real_stripe_client: None, - stripe_client: Some(Arc::new(FakeStripeClient::new())), - stripe_billing: None, executor, kinesis_client: None, config: Config { @@ -608,7 +604,6 @@ impl TestServer { auto_join_channel_id: None, migrations_path: None, seed_path: None, - stripe_api_key: None, supermaven_admin_api_key: None, user_backfiller_github_access_token: None, kinesis_region: None, From 9eb1ff272693a811c8f3f1b251a67c3a97f856e4 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Fri, 15 Aug 2025 18:03:36 -0300 Subject: [PATCH 391/693] acp thread view: Always use editors for user messages (#36256) This means the cursor will be at the position you clicked: https://github.com/user-attachments/assets/0693950d-7513-4d90-88e2-55817df7213a Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 10 +- .../agent_ui/src/acp/completion_provider.rs | 5 - crates/agent_ui/src/acp/entry_view_state.rs | 387 ++++++----- crates/agent_ui/src/acp/message_editor.rs | 28 +- crates/agent_ui/src/acp/thread_view.rs | 605 ++++++++++++------ crates/agent_ui/src/agent_panel.rs | 10 +- 6 files changed, 671 insertions(+), 374 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 4995ddb9df..2ef94a3cbe 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -109,7 +109,7 @@ pub enum AgentThreadEntry { } impl AgentThreadEntry { - fn to_markdown(&self, cx: &App) -> String { + pub fn to_markdown(&self, cx: &App) -> String { match self { Self::UserMessage(message) => message.to_markdown(cx), Self::AssistantMessage(message) => message.to_markdown(cx), @@ -117,6 +117,14 @@ impl AgentThreadEntry { } } + pub fn user_message(&self) -> Option<&UserMessage> { + if let AgentThreadEntry::UserMessage(message) = self { + Some(message) + } else { + None + } + } + pub fn diffs(&self) -> impl Iterator> { if let AgentThreadEntry::ToolCall(call) = self { itertools::Either::Left(call.diffs()) diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 4ee1eb6948..d7d2cd5d0e 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -80,11 +80,6 @@ impl MentionSet { .chain(self.images.drain().map(|(id, _)| id)) } - pub fn clear(&mut self) { - self.fetch_results.clear(); - self.uri_by_crease_id.clear(); - } - pub fn contents( &self, project: Entity, diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index 2f5f855e90..e99d1f6323 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -1,45 +1,141 @@ -use std::{collections::HashMap, ops::Range}; +use std::ops::Range; -use acp_thread::AcpThread; -use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer}; +use acp_thread::{AcpThread, AgentThreadEntry}; +use agent::{TextThreadStore, ThreadStore}; +use collections::HashMap; +use editor::{Editor, EditorMode, MinimapVisibility}; use gpui::{ - AnyEntity, App, AppContext as _, Entity, EntityId, TextStyleRefinement, WeakEntity, Window, + AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, TextStyleRefinement, + WeakEntity, Window, }; use language::language_settings::SoftWrap; +use project::Project; use settings::Settings as _; use terminal_view::TerminalView; use theme::ThemeSettings; -use ui::TextSize; +use ui::{Context, TextSize}; use workspace::Workspace; -#[derive(Default)] +use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; + pub struct EntryViewState { + workspace: WeakEntity, + project: Entity, + thread_store: Entity, + text_thread_store: Entity, entries: Vec, } impl EntryViewState { + pub fn new( + workspace: WeakEntity, + project: Entity, + thread_store: Entity, + text_thread_store: Entity, + ) -> Self { + Self { + workspace, + project, + thread_store, + text_thread_store, + entries: Vec::new(), + } + } + pub fn entry(&self, index: usize) -> Option<&Entry> { self.entries.get(index) } pub fn sync_entry( &mut self, - workspace: WeakEntity, - thread: Entity, index: usize, + thread: &Entity, window: &mut Window, - cx: &mut App, + cx: &mut Context, ) { - debug_assert!(index <= self.entries.len()); - let entry = if let Some(entry) = self.entries.get_mut(index) { - entry - } else { - self.entries.push(Entry::default()); - self.entries.last_mut().unwrap() + let Some(thread_entry) = thread.read(cx).entries().get(index) else { + return; }; - entry.sync_diff_multibuffers(&thread, index, window, cx); - entry.sync_terminals(&workspace, &thread, index, window, cx); + match thread_entry { + AgentThreadEntry::UserMessage(message) => { + let has_id = message.id.is_some(); + let chunks = message.chunks.clone(); + let message_editor = cx.new(|cx| { + let mut editor = MessageEditor::new( + self.workspace.clone(), + self.project.clone(), + self.thread_store.clone(), + self.text_thread_store.clone(), + editor::EditorMode::AutoHeight { + min_lines: 1, + max_lines: None, + }, + window, + cx, + ); + if !has_id { + editor.set_read_only(true, cx); + } + editor.set_message(chunks, window, cx); + editor + }); + cx.subscribe(&message_editor, move |_, editor, event, cx| { + cx.emit(EntryViewEvent { + entry_index: index, + view_event: ViewEvent::MessageEditorEvent(editor, *event), + }) + }) + .detach(); + self.set_entry(index, Entry::UserMessage(message_editor)); + } + AgentThreadEntry::ToolCall(tool_call) => { + let terminals = tool_call.terminals().cloned().collect::>(); + let diffs = tool_call.diffs().cloned().collect::>(); + + let views = if let Some(Entry::Content(views)) = self.entries.get_mut(index) { + views + } else { + self.set_entry(index, Entry::empty()); + let Some(Entry::Content(views)) = self.entries.get_mut(index) else { + unreachable!() + }; + views + }; + + for terminal in terminals { + views.entry(terminal.entity_id()).or_insert_with(|| { + create_terminal( + self.workspace.clone(), + self.project.clone(), + terminal.clone(), + window, + cx, + ) + .into_any() + }); + } + + for diff in diffs { + views + .entry(diff.entity_id()) + .or_insert_with(|| create_editor_diff(diff.clone(), window, cx).into_any()); + } + } + AgentThreadEntry::AssistantMessage(_) => { + if index == self.entries.len() { + self.entries.push(Entry::empty()) + } + } + }; + } + + fn set_entry(&mut self, index: usize, entry: Entry) { + if index == self.entries.len() { + self.entries.push(entry); + } else { + self.entries[index] = entry; + } } pub fn remove(&mut self, range: Range) { @@ -48,26 +144,51 @@ impl EntryViewState { pub fn settings_changed(&mut self, cx: &mut App) { for entry in self.entries.iter() { - for view in entry.views.values() { - if let Ok(diff_editor) = view.clone().downcast::() { - diff_editor.update(cx, |diff_editor, cx| { - diff_editor - .set_text_style_refinement(diff_editor_text_style_refinement(cx)); - cx.notify(); - }) + match entry { + Entry::UserMessage { .. } => {} + Entry::Content(response_views) => { + for view in response_views.values() { + if let Ok(diff_editor) = view.clone().downcast::() { + diff_editor.update(cx, |diff_editor, cx| { + diff_editor.set_text_style_refinement( + diff_editor_text_style_refinement(cx), + ); + cx.notify(); + }) + } + } } } } } } -pub struct Entry { - views: HashMap, +impl EventEmitter for EntryViewState {} + +pub struct EntryViewEvent { + pub entry_index: usize, + pub view_event: ViewEvent, +} + +pub enum ViewEvent { + MessageEditorEvent(Entity, MessageEditorEvent), +} + +pub enum Entry { + UserMessage(Entity), + Content(HashMap), } impl Entry { - pub fn editor_for_diff(&self, diff: &Entity) -> Option> { - self.views + pub fn message_editor(&self) -> Option<&Entity> { + match self { + Self::UserMessage(editor) => Some(editor), + Entry::Content(_) => None, + } + } + + pub fn editor_for_diff(&self, diff: &Entity) -> Option> { + self.content_map()? .get(&diff.entity_id()) .cloned() .map(|entity| entity.downcast::().unwrap()) @@ -77,118 +198,88 @@ impl Entry { &self, terminal: &Entity, ) -> Option> { - self.views + self.content_map()? .get(&terminal.entity_id()) .cloned() .map(|entity| entity.downcast::().unwrap()) } - fn sync_diff_multibuffers( - &mut self, - thread: &Entity, - index: usize, - window: &mut Window, - cx: &mut App, - ) { - let Some(entry) = thread.read(cx).entries().get(index) else { - return; - }; - - let multibuffers = entry - .diffs() - .map(|diff| diff.read(cx).multibuffer().clone()); - - let multibuffers = multibuffers.collect::>(); - - for multibuffer in multibuffers { - if self.views.contains_key(&multibuffer.entity_id()) { - return; - } - - let editor = cx.new(|cx| { - let mut editor = Editor::new( - EditorMode::Full { - scale_ui_elements_with_buffer_font_size: false, - show_active_line_background: false, - sized_by_content: true, - }, - multibuffer.clone(), - None, - window, - cx, - ); - editor.set_show_gutter(false, cx); - editor.disable_inline_diagnostics(); - editor.disable_expand_excerpt_buttons(cx); - editor.set_show_vertical_scrollbar(false, cx); - editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx); - editor.set_soft_wrap_mode(SoftWrap::None, cx); - editor.scroll_manager.set_forbid_vertical_scroll(true); - editor.set_show_indent_guides(false, cx); - editor.set_read_only(true); - editor.set_show_breakpoints(false, cx); - editor.set_show_code_actions(false, cx); - editor.set_show_git_diff_gutter(false, cx); - editor.set_expand_all_diff_hunks(cx); - editor.set_text_style_refinement(diff_editor_text_style_refinement(cx)); - editor - }); - - let entity_id = multibuffer.entity_id(); - self.views.insert(entity_id, editor.into_any()); + fn content_map(&self) -> Option<&HashMap> { + match self { + Self::Content(map) => Some(map), + _ => None, } } - fn sync_terminals( - &mut self, - workspace: &WeakEntity, - thread: &Entity, - index: usize, - window: &mut Window, - cx: &mut App, - ) { - let Some(entry) = thread.read(cx).entries().get(index) else { - return; - }; - - let terminals = entry - .terminals() - .map(|terminal| terminal.clone()) - .collect::>(); - - for terminal in terminals { - if self.views.contains_key(&terminal.entity_id()) { - return; - } - - let Some(strong_workspace) = workspace.upgrade() else { - return; - }; - - let terminal_view = cx.new(|cx| { - let mut view = TerminalView::new( - terminal.read(cx).inner().clone(), - workspace.clone(), - None, - strong_workspace.read(cx).project().downgrade(), - window, - cx, - ); - view.set_embedded_mode(Some(1000), cx); - view - }); - - let entity_id = terminal.entity_id(); - self.views.insert(entity_id, terminal_view.into_any()); - } + fn empty() -> Self { + Self::Content(HashMap::default()) } #[cfg(test)] - pub fn len(&self) -> usize { - self.views.len() + pub fn has_content(&self) -> bool { + match self { + Self::Content(map) => !map.is_empty(), + Self::UserMessage(_) => false, + } } } +fn create_terminal( + workspace: WeakEntity, + project: Entity, + terminal: Entity, + window: &mut Window, + cx: &mut App, +) -> Entity { + cx.new(|cx| { + let mut view = TerminalView::new( + terminal.read(cx).inner().clone(), + workspace.clone(), + None, + project.downgrade(), + window, + cx, + ); + view.set_embedded_mode(Some(1000), cx); + view + }) +} + +fn create_editor_diff( + diff: Entity, + window: &mut Window, + cx: &mut App, +) -> Entity { + cx.new(|cx| { + let mut editor = Editor::new( + EditorMode::Full { + scale_ui_elements_with_buffer_font_size: false, + show_active_line_background: false, + sized_by_content: true, + }, + diff.read(cx).multibuffer().clone(), + None, + window, + cx, + ); + editor.set_show_gutter(false, cx); + editor.disable_inline_diagnostics(); + editor.disable_expand_excerpt_buttons(cx); + editor.set_show_vertical_scrollbar(false, cx); + editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx); + editor.set_soft_wrap_mode(SoftWrap::None, cx); + editor.scroll_manager.set_forbid_vertical_scroll(true); + editor.set_show_indent_guides(false, cx); + editor.set_read_only(true); + editor.set_show_breakpoints(false, cx); + editor.set_show_code_actions(false, cx); + editor.set_show_git_diff_gutter(false, cx); + editor.set_expand_all_diff_hunks(cx); + editor.set_text_style_refinement(diff_editor_text_style_refinement(cx)); + editor + }) +} + fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement { TextStyleRefinement { font_size: Some( @@ -201,26 +292,20 @@ fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement { } } -impl Default for Entry { - fn default() -> Self { - Self { - // Avoid allocating in the heap by default - views: HashMap::with_capacity(0), - } - } -} - #[cfg(test)] mod tests { use std::{path::Path, rc::Rc}; use acp_thread::{AgentConnection, StubAgentConnection}; + use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol as acp; use agent_settings::AgentSettings; use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; use editor::{EditorSettings, RowInfo}; use fs::FakeFs; - use gpui::{SemanticVersion, TestAppContext}; + use gpui::{AppContext as _, SemanticVersion, TestAppContext}; + + use crate::acp::entry_view_state::EntryViewState; use multi_buffer::MultiBufferRow; use pretty_assertions::assert_matches; use project::Project; @@ -230,8 +315,6 @@ mod tests { use util::path; use workspace::Workspace; - use crate::acp::entry_view_state::EntryViewState; - #[gpui::test] async fn test_diff_sync(cx: &mut TestAppContext) { init_test(cx); @@ -269,7 +352,7 @@ mod tests { .update(|_, cx| { connection .clone() - .new_thread(project, Path::new(path!("/project")), cx) + .new_thread(project.clone(), Path::new(path!("/project")), cx) }) .await .unwrap(); @@ -279,12 +362,23 @@ mod tests { connection.send_update(session_id, acp::SessionUpdate::ToolCall(tool_call), cx) }); - let mut view_state = EntryViewState::default(); - cx.update(|window, cx| { - view_state.sync_entry(workspace.downgrade(), thread.clone(), 0, window, cx); + let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx)); + let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + + let view_state = cx.new(|_cx| { + EntryViewState::new( + workspace.downgrade(), + project.clone(), + thread_store, + text_thread_store, + ) }); - let multibuffer = thread.read_with(cx, |thread, cx| { + view_state.update_in(cx, |view_state, window, cx| { + view_state.sync_entry(0, &thread, window, cx) + }); + + let diff = thread.read_with(cx, |thread, _cx| { thread .entries() .get(0) @@ -292,15 +386,14 @@ mod tests { .diffs() .next() .unwrap() - .read(cx) - .multibuffer() .clone() }); cx.run_until_parked(); - let entry = view_state.entry(0).unwrap(); - let diff_editor = entry.editor_for_diff(&multibuffer).unwrap(); + let diff_editor = view_state.read_with(cx, |view_state, _cx| { + view_state.entry(0).unwrap().editor_for_diff(&diff).unwrap() + }); assert_eq!( diff_editor.read_with(cx, |editor, cx| editor.text(cx)), "hi world\nhello world" diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 32c37da519..90827e5514 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -52,9 +52,11 @@ pub struct MessageEditor { text_thread_store: Entity, } +#[derive(Clone, Copy)] pub enum MessageEditorEvent { Send, Cancel, + Focus, } impl EventEmitter for MessageEditor {} @@ -101,6 +103,11 @@ impl MessageEditor { editor }); + cx.on_focus(&editor.focus_handle(cx), window, |_, _, cx| { + cx.emit(MessageEditorEvent::Focus) + }) + .detach(); + Self { editor, project, @@ -386,11 +393,11 @@ impl MessageEditor { }); } - fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context) { + fn send(&mut self, _: &Chat, _: &mut Window, cx: &mut Context) { cx.emit(MessageEditorEvent::Send) } - fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { + fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context) { cx.emit(MessageEditorEvent::Cancel) } @@ -496,6 +503,13 @@ impl MessageEditor { } } + pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context) { + self.editor.update(cx, |message_editor, cx| { + message_editor.set_read_only(read_only); + cx.notify() + }) + } + fn insert_image( &mut self, excerpt_id: ExcerptId, @@ -572,6 +586,8 @@ impl MessageEditor { window: &mut Window, cx: &mut Context, ) { + self.clear(window, cx); + let mut text = String::new(); let mut mentions = Vec::new(); let mut images = Vec::new(); @@ -609,7 +625,6 @@ impl MessageEditor { editor.buffer().read(cx).snapshot(cx) }); - self.mention_set.clear(); for (range, mention_uri) in mentions { let anchor = snapshot.anchor_before(range.start); let crease_id = crate::context_picker::insert_crease_for_mention( @@ -679,6 +694,11 @@ impl MessageEditor { editor.set_text(text, window, cx); }); } + + #[cfg(test)] + pub fn text(&self, cx: &App) -> String { + self.editor.read(cx).text(cx) + } } impl Focusable for MessageEditor { @@ -691,7 +711,7 @@ impl Render for MessageEditor { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { div() .key_context("MessageEditor") - .on_action(cx.listener(Self::chat)) + .on_action(cx.listener(Self::send)) .on_action(cx.listener(Self::cancel)) .capture_action(cx.listener(Self::paste)) .flex_1() diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index cb1a62fd11..17341e4c8a 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -45,6 +45,7 @@ use zed_actions::assistant::OpenRulesLibrary; use super::entry_view_state::EntryViewState; use crate::acp::AcpModelSelectorPopover; +use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent}; use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; use crate::agent_diff::AgentDiff; use crate::profile_selector::{ProfileProvider, ProfileSelector}; @@ -101,10 +102,8 @@ pub struct AcpThreadView { agent: Rc, workspace: WeakEntity, project: Entity, - thread_store: Entity, - text_thread_store: Entity, thread_state: ThreadState, - entry_view_state: EntryViewState, + entry_view_state: Entity, message_editor: Entity, model_selector: Option>, profile_selector: Option>, @@ -120,16 +119,9 @@ pub struct AcpThreadView { plan_expanded: bool, editor_expanded: bool, terminal_expanded: bool, - editing_message: Option, + editing_message: Option, _cancel_task: Option>, - _subscriptions: [Subscription; 2], -} - -struct EditingMessage { - index: usize, - message_id: UserMessageId, - editor: Entity, - _subscription: Subscription, + _subscriptions: [Subscription; 3], } enum ThreadState { @@ -176,24 +168,32 @@ impl AcpThreadView { let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0)); + let entry_view_state = cx.new(|_| { + EntryViewState::new( + workspace.clone(), + project.clone(), + thread_store.clone(), + text_thread_store.clone(), + ) + }); + let subscriptions = [ cx.observe_global_in::(window, Self::settings_changed), - cx.subscribe_in(&message_editor, window, Self::on_message_editor_event), + cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event), + cx.subscribe_in(&entry_view_state, window, Self::handle_entry_view_event), ]; Self { agent: agent.clone(), workspace: workspace.clone(), project: project.clone(), - thread_store, - text_thread_store, + entry_view_state, thread_state: Self::initial_state(agent, workspace, project, window, cx), message_editor, model_selector: None, profile_selector: None, notifications: Vec::new(), notification_subscriptions: HashMap::default(), - entry_view_state: EntryViewState::default(), list_state: list_state.clone(), scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), thread_error: None, @@ -414,7 +414,7 @@ impl AcpThreadView { cx.notify(); } - pub fn on_message_editor_event( + pub fn handle_message_editor_event( &mut self, _: &Entity, event: &MessageEditorEvent, @@ -424,6 +424,28 @@ impl AcpThreadView { match event { MessageEditorEvent::Send => self.send(window, cx), MessageEditorEvent::Cancel => self.cancel_generation(cx), + MessageEditorEvent::Focus => {} + } + } + + pub fn handle_entry_view_event( + &mut self, + _: &Entity, + event: &EntryViewEvent, + window: &mut Window, + cx: &mut Context, + ) { + match &event.view_event { + ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => { + self.editing_message = Some(event.entry_index); + cx.notify(); + } + ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => { + self.regenerate(event.entry_index, editor, window, cx); + } + ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Cancel) => { + self.cancel_editing(&Default::default(), window, cx); + } } } @@ -494,27 +516,56 @@ impl AcpThreadView { .detach(); } - fn cancel_editing(&mut self, _: &ClickEvent, _window: &mut Window, cx: &mut Context) { - self.editing_message.take(); - cx.notify(); - } - - fn regenerate(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { - let Some(editing_message) = self.editing_message.take() else { - return; - }; - + fn cancel_editing(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { let Some(thread) = self.thread().cloned() else { return; }; - let rewind = thread.update(cx, |thread, cx| { - thread.rewind(editing_message.message_id, cx) - }); + if let Some(index) = self.editing_message.take() { + if let Some(editor) = self + .entry_view_state + .read(cx) + .entry(index) + .and_then(|e| e.message_editor()) + .cloned() + { + editor.update(cx, |editor, cx| { + if let Some(user_message) = thread + .read(cx) + .entries() + .get(index) + .and_then(|e| e.user_message()) + { + editor.set_message(user_message.chunks.clone(), window, cx); + } + }) + } + }; + self.focus_handle(cx).focus(window); + cx.notify(); + } + + fn regenerate( + &mut self, + entry_ix: usize, + message_editor: &Entity, + window: &mut Window, + cx: &mut Context, + ) { + let Some(thread) = self.thread().cloned() else { + return; + }; + + let Some(rewind) = thread.update(cx, |thread, cx| { + let user_message_id = thread.entries().get(entry_ix)?.user_message()?.id.clone()?; + Some(thread.rewind(user_message_id, cx)) + }) else { + return; + }; + + let contents = + message_editor.update(cx, |message_editor, cx| message_editor.contents(window, cx)); - let contents = editing_message - .editor - .update(cx, |message_editor, cx| message_editor.contents(window, cx)); let task = cx.foreground_executor().spawn(async move { rewind.await?; contents.await @@ -570,27 +621,20 @@ impl AcpThreadView { AcpThreadEvent::NewEntry => { let len = thread.read(cx).entries().len(); let index = len - 1; - self.entry_view_state.sync_entry( - self.workspace.clone(), - thread.clone(), - index, - window, - cx, - ); + self.entry_view_state.update(cx, |view_state, cx| { + view_state.sync_entry(index, &thread, window, cx) + }); self.list_state.splice(index..index, 1); } AcpThreadEvent::EntryUpdated(index) => { - self.entry_view_state.sync_entry( - self.workspace.clone(), - thread.clone(), - *index, - window, - cx, - ); + self.entry_view_state.update(cx, |view_state, cx| { + view_state.sync_entry(*index, &thread, window, cx) + }); self.list_state.splice(*index..index + 1, 1); } AcpThreadEvent::EntriesRemoved(range) => { - self.entry_view_state.remove(range.clone()); + self.entry_view_state + .update(cx, |view_state, _cx| view_state.remove(range.clone())); self.list_state.splice(range.clone(), 0); } AcpThreadEvent::ToolAuthorizationRequired => { @@ -722,29 +766,15 @@ impl AcpThreadView { .border_1() .border_color(cx.theme().colors().border) .text_xs() - .id("message") - .on_click(cx.listener({ - move |this, _, window, cx| { - this.start_editing_message(entry_ix, window, cx) - } - })) .children( - if let Some(editing) = self.editing_message.as_ref() - && Some(&editing.message_id) == message.id.as_ref() - { - Some( - self.render_edit_message_editor(editing, cx) - .into_any_element(), - ) - } else { - message.content.markdown().map(|md| { - self.render_markdown( - md.clone(), - user_message_markdown_style(window, cx), - ) - .into_any_element() - }) - }, + self.entry_view_state + .read(cx) + .entry(entry_ix) + .and_then(|entry| entry.message_editor()) + .map(|editor| { + self.render_sent_message_editor(entry_ix, editor, cx) + .into_any_element() + }), ), ) .into_any(), @@ -819,8 +849,8 @@ impl AcpThreadView { primary }; - if let Some(editing) = self.editing_message.as_ref() - && editing.index < entry_ix + if let Some(editing_index) = self.editing_message.as_ref() + && *editing_index < entry_ix { let backdrop = div() .id(("backdrop", entry_ix)) @@ -834,8 +864,8 @@ impl AcpThreadView { div() .relative() - .child(backdrop) .child(primary) + .child(backdrop) .into_any_element() } else { primary @@ -1256,9 +1286,7 @@ impl AcpThreadView { Empty.into_any_element() } } - ToolCallContent::Diff(diff) => { - self.render_diff_editor(entry_ix, &diff.read(cx).multibuffer(), cx) - } + ToolCallContent::Diff(diff) => self.render_diff_editor(entry_ix, &diff, cx), ToolCallContent::Terminal(terminal) => { self.render_terminal_tool_call(entry_ix, terminal, tool_call, window, cx) } @@ -1405,7 +1433,7 @@ impl AcpThreadView { fn render_diff_editor( &self, entry_ix: usize, - multibuffer: &Entity, + diff: &Entity, cx: &Context, ) -> AnyElement { v_flex() @@ -1413,8 +1441,8 @@ impl AcpThreadView { .border_t_1() .border_color(self.tool_card_border_color(cx)) .child( - if let Some(entry) = self.entry_view_state.entry(entry_ix) - && let Some(editor) = entry.editor_for_diff(&multibuffer) + if let Some(entry) = self.entry_view_state.read(cx).entry(entry_ix) + && let Some(editor) = entry.editor_for_diff(&diff) { editor.clone().into_any_element() } else { @@ -1617,6 +1645,7 @@ impl AcpThreadView { let terminal_view = self .entry_view_state + .read(cx) .entry(entry_ix) .and_then(|entry| entry.terminal(&terminal)); let show_output = self.terminal_expanded && terminal_view.is_some(); @@ -2485,82 +2514,38 @@ impl AcpThreadView { ) } - fn start_editing_message(&mut self, index: usize, window: &mut Window, cx: &mut Context) { - let Some(thread) = self.thread() else { - return; - }; - let Some(AgentThreadEntry::UserMessage(message)) = thread.read(cx).entries().get(index) - else { - return; - }; - let Some(message_id) = message.id.clone() else { - return; - }; - - self.list_state.scroll_to_reveal_item(index); - - let chunks = message.chunks.clone(); - let editor = cx.new(|cx| { - let mut editor = MessageEditor::new( - self.workspace.clone(), - self.project.clone(), - self.thread_store.clone(), - self.text_thread_store.clone(), - editor::EditorMode::AutoHeight { - min_lines: 1, - max_lines: None, - }, - window, - cx, - ); - editor.set_message(chunks, window, cx); - editor - }); - let subscription = - cx.subscribe_in(&editor, window, |this, _, event, window, cx| match event { - MessageEditorEvent::Send => { - this.regenerate(&Default::default(), window, cx); - } - MessageEditorEvent::Cancel => { - this.cancel_editing(&Default::default(), window, cx); - } - }); - editor.focus_handle(cx).focus(window); - - self.editing_message.replace(EditingMessage { - index: index, - message_id: message_id.clone(), - editor, - _subscription: subscription, - }); - cx.notify(); - } - - fn render_edit_message_editor(&self, editing: &EditingMessage, cx: &Context) -> Div { - v_flex() - .w_full() - .gap_2() - .child(editing.editor.clone()) - .child( - h_flex() - .gap_1() - .child( - Icon::new(IconName::Warning) - .color(Color::Warning) - .size(IconSize::XSmall), - ) - .child( - Label::new("Editing will restart the thread from this point.") - .color(Color::Muted) - .size(LabelSize::XSmall), - ) - .child(self.render_editing_message_editor_buttons(editing, cx)), - ) - } - - fn render_editing_message_editor_buttons( + fn render_sent_message_editor( &self, - editing: &EditingMessage, + entry_ix: usize, + editor: &Entity, + cx: &Context, + ) -> Div { + v_flex().w_full().gap_2().child(editor.clone()).when( + self.editing_message == Some(entry_ix), + |el| { + el.child( + h_flex() + .gap_1() + .child( + Icon::new(IconName::Warning) + .color(Color::Warning) + .size(IconSize::XSmall), + ) + .child( + Label::new("Editing will restart the thread from this point.") + .color(Color::Muted) + .size(LabelSize::XSmall), + ) + .child(self.render_sent_message_editor_buttons(entry_ix, editor, cx)), + ) + }, + ) + } + + fn render_sent_message_editor_buttons( + &self, + entry_ix: usize, + editor: &Entity, cx: &Context, ) -> Div { h_flex() @@ -2573,7 +2558,7 @@ impl AcpThreadView { .icon_color(Color::Error) .icon_size(IconSize::Small) .tooltip({ - let focus_handle = editing.editor.focus_handle(cx); + let focus_handle = editor.focus_handle(cx); move |window, cx| { Tooltip::for_action_in( "Cancel Edit", @@ -2588,12 +2573,12 @@ impl AcpThreadView { ) .child( IconButton::new("confirm-edit-message", IconName::Return) - .disabled(editing.editor.read(cx).is_empty(cx)) + .disabled(editor.read(cx).is_empty(cx)) .shape(ui::IconButtonShape::Square) .icon_color(Color::Muted) .icon_size(IconSize::Small) .tooltip({ - let focus_handle = editing.editor.focus_handle(cx); + let focus_handle = editor.focus_handle(cx); move |window, cx| { Tooltip::for_action_in( "Regenerate", @@ -2604,7 +2589,12 @@ impl AcpThreadView { ) } }) - .on_click(cx.listener(Self::regenerate)), + .on_click(cx.listener({ + let editor = editor.clone(); + move |this, _, window, cx| { + this.regenerate(entry_ix, &editor, window, cx); + } + })), ) } @@ -3137,7 +3127,9 @@ impl AcpThreadView { } fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context) { - self.entry_view_state.settings_changed(cx); + self.entry_view_state.update(cx, |entry_view_state, cx| { + entry_view_state.settings_changed(cx); + }); } pub(crate) fn insert_dragged_files( @@ -3152,9 +3144,7 @@ impl AcpThreadView { drop(added_worktrees); }) } -} -impl AcpThreadView { fn render_thread_error(&self, window: &mut Window, cx: &mut Context<'_, Self>) -> Option
{ let content = match self.thread_error.as_ref()? { ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx), @@ -3439,35 +3429,6 @@ impl Render for AcpThreadView { } } -fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { - let mut style = default_markdown_style(false, window, cx); - let mut text_style = window.text_style(); - let theme_settings = ThemeSettings::get_global(cx); - - let buffer_font = theme_settings.buffer_font.family.clone(); - let buffer_font_size = TextSize::Small.rems(cx); - - text_style.refine(&TextStyleRefinement { - font_family: Some(buffer_font), - font_size: Some(buffer_font_size.into()), - ..Default::default() - }); - - style.base_text_style = text_style; - style.link_callback = Some(Rc::new(move |url, cx| { - if MentionUri::parse(url).is_ok() { - let colors = cx.theme().colors(); - Some(TextStyleRefinement { - background_color: Some(colors.element_background), - ..Default::default() - }) - } else { - None - } - })); - style -} - fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> MarkdownStyle { let theme_settings = ThemeSettings::get_global(cx); let colors = cx.theme().colors(); @@ -3626,12 +3587,13 @@ pub(crate) mod tests { use agent_client_protocol::SessionId; use editor::EditorSettings; use fs::FakeFs; - use gpui::{SemanticVersion, TestAppContext, VisualTestContext}; + use gpui::{EventEmitter, SemanticVersion, TestAppContext, VisualTestContext}; use project::Project; use serde_json::json; use settings::SettingsStore; use std::any::Any; use std::path::Path; + use workspace::Item; use super::*; @@ -3778,6 +3740,50 @@ pub(crate) mod tests { (thread_view, cx) } + fn add_to_workspace(thread_view: Entity, cx: &mut VisualTestContext) { + let workspace = thread_view.read_with(cx, |thread_view, _cx| thread_view.workspace.clone()); + + workspace + .update_in(cx, |workspace, window, cx| { + workspace.add_item_to_active_pane( + Box::new(cx.new(|_| ThreadViewItem(thread_view.clone()))), + None, + true, + window, + cx, + ); + }) + .unwrap(); + } + + struct ThreadViewItem(Entity); + + impl Item for ThreadViewItem { + type Event = (); + + fn include_in_nav_history() -> bool { + false + } + + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "Test".into() + } + } + + impl EventEmitter<()> for ThreadViewItem {} + + impl Focusable for ThreadViewItem { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.0.read(cx).focus_handle(cx).clone() + } + } + + impl Render for ThreadViewItem { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + self.0.clone().into_any_element() + } + } + struct StubAgentServer { connection: C, } @@ -3799,19 +3805,19 @@ pub(crate) mod tests { C: 'static + AgentConnection + Send + Clone, { fn logo(&self) -> ui::IconName { - unimplemented!() + ui::IconName::Ai } fn name(&self) -> &'static str { - unimplemented!() + "Test" } fn empty_state_headline(&self) -> &'static str { - unimplemented!() + "Test" } fn empty_state_message(&self) -> &'static str { - unimplemented!() + "Test" } fn connect( @@ -3960,9 +3966,17 @@ pub(crate) mod tests { assert_eq!(thread.entries().len(), 2); }); - thread_view.read_with(cx, |view, _| { - assert_eq!(view.entry_view_state.entry(0).unwrap().len(), 0); - assert_eq!(view.entry_view_state.entry(1).unwrap().len(), 1); + thread_view.read_with(cx, |view, cx| { + view.entry_view_state.read_with(cx, |entry_view_state, _| { + assert!( + entry_view_state + .entry(0) + .unwrap() + .message_editor() + .is_some() + ); + assert!(entry_view_state.entry(1).unwrap().has_content()); + }); }); // Second user message @@ -3991,18 +4005,31 @@ pub(crate) mod tests { let second_user_message_id = thread.read_with(cx, |thread, _| { assert_eq!(thread.entries().len(), 4); - let AgentThreadEntry::UserMessage(user_message) = thread.entries().get(2).unwrap() - else { + let AgentThreadEntry::UserMessage(user_message) = &thread.entries()[2] else { panic!(); }; user_message.id.clone().unwrap() }); - thread_view.read_with(cx, |view, _| { - assert_eq!(view.entry_view_state.entry(0).unwrap().len(), 0); - assert_eq!(view.entry_view_state.entry(1).unwrap().len(), 1); - assert_eq!(view.entry_view_state.entry(2).unwrap().len(), 0); - assert_eq!(view.entry_view_state.entry(3).unwrap().len(), 1); + thread_view.read_with(cx, |view, cx| { + view.entry_view_state.read_with(cx, |entry_view_state, _| { + assert!( + entry_view_state + .entry(0) + .unwrap() + .message_editor() + .is_some() + ); + assert!(entry_view_state.entry(1).unwrap().has_content()); + assert!( + entry_view_state + .entry(2) + .unwrap() + .message_editor() + .is_some() + ); + assert!(entry_view_state.entry(3).unwrap().has_content()); + }); }); // Rewind to first message @@ -4017,13 +4044,169 @@ pub(crate) mod tests { assert_eq!(thread.entries().len(), 2); }); - thread_view.read_with(cx, |view, _| { - assert_eq!(view.entry_view_state.entry(0).unwrap().len(), 0); - assert_eq!(view.entry_view_state.entry(1).unwrap().len(), 1); + thread_view.read_with(cx, |view, cx| { + view.entry_view_state.read_with(cx, |entry_view_state, _| { + assert!( + entry_view_state + .entry(0) + .unwrap() + .message_editor() + .is_some() + ); + assert!(entry_view_state.entry(1).unwrap().has_content()); - // Old views should be dropped - assert!(view.entry_view_state.entry(2).is_none()); - assert!(view.entry_view_state.entry(3).is_none()); + // Old views should be dropped + assert!(entry_view_state.entry(2).is_none()); + assert!(entry_view_state.entry(3).is_none()); + }); }); } + + #[gpui::test] + async fn test_message_editing_cancel(cx: &mut TestAppContext) { + init_test(cx); + + let connection = StubAgentConnection::new(); + + connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk { + content: acp::ContentBlock::Text(acp::TextContent { + text: "Response".into(), + annotations: None, + }), + }]); + + let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; + add_to_workspace(thread_view.clone(), cx); + + let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Original message to edit", window, cx); + }); + thread_view.update_in(cx, |thread_view, window, cx| { + thread_view.send(window, cx); + }); + + cx.run_until_parked(); + + let user_message_editor = thread_view.read_with(cx, |view, cx| { + assert_eq!(view.editing_message, None); + + view.entry_view_state + .read(cx) + .entry(0) + .unwrap() + .message_editor() + .unwrap() + .clone() + }); + + // Focus + cx.focus(&user_message_editor); + thread_view.read_with(cx, |view, _cx| { + assert_eq!(view.editing_message, Some(0)); + }); + + // Edit + user_message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Edited message content", window, cx); + }); + + // Cancel + user_message_editor.update_in(cx, |_editor, window, cx| { + window.dispatch_action(Box::new(editor::actions::Cancel), cx); + }); + + thread_view.read_with(cx, |view, _cx| { + assert_eq!(view.editing_message, None); + }); + + user_message_editor.read_with(cx, |editor, cx| { + assert_eq!(editor.text(cx), "Original message to edit"); + }); + } + + #[gpui::test] + async fn test_message_editing_regenerate(cx: &mut TestAppContext) { + init_test(cx); + + let connection = StubAgentConnection::new(); + + connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk { + content: acp::ContentBlock::Text(acp::TextContent { + text: "Response".into(), + annotations: None, + }), + }]); + + let (thread_view, cx) = + setup_thread_view(StubAgentServer::new(connection.clone()), cx).await; + add_to_workspace(thread_view.clone(), cx); + + let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Original message to edit", window, cx); + }); + thread_view.update_in(cx, |thread_view, window, cx| { + thread_view.send(window, cx); + }); + + cx.run_until_parked(); + + let user_message_editor = thread_view.read_with(cx, |view, cx| { + assert_eq!(view.editing_message, None); + assert_eq!(view.thread().unwrap().read(cx).entries().len(), 2); + + view.entry_view_state + .read(cx) + .entry(0) + .unwrap() + .message_editor() + .unwrap() + .clone() + }); + + // Focus + cx.focus(&user_message_editor); + + // Edit + user_message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Edited message content", window, cx); + }); + + // Send + connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk { + content: acp::ContentBlock::Text(acp::TextContent { + text: "New Response".into(), + annotations: None, + }), + }]); + + user_message_editor.update_in(cx, |_editor, window, cx| { + window.dispatch_action(Box::new(Chat), cx); + }); + + cx.run_until_parked(); + + thread_view.read_with(cx, |view, cx| { + assert_eq!(view.editing_message, None); + + let entries = view.thread().unwrap().read(cx).entries(); + assert_eq!(entries.len(), 2); + assert_eq!( + entries[0].to_markdown(cx), + "## User\n\nEdited message content\n\n" + ); + assert_eq!( + entries[1].to_markdown(cx), + "## Assistant\n\nNew Response\n\n" + ); + + let new_editor = view.entry_view_state.read_with(cx, |state, _cx| { + assert!(!state.entry(1).unwrap().has_content()); + state.entry(0).unwrap().message_editor().unwrap().clone() + }); + + assert_eq!(new_editor.read(cx).text(cx), "Edited message content"); + }) + } } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 73915195f5..519f7980ff 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -818,12 +818,10 @@ impl AgentPanel { ActiveView::Thread { thread, .. } => { thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx)); } - ActiveView::ExternalAgentThread { thread_view, .. } => { - thread_view.update(cx, |thread_element, cx| { - thread_element.cancel_generation(cx) - }); - } - ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} + ActiveView::ExternalAgentThread { .. } + | ActiveView::TextThread { .. } + | ActiveView::History + | ActiveView::Configuration => {} } } From f3654036189ba5ca414f9827aee52c0a9f7e95d9 Mon Sep 17 00:00:00 2001 From: Yang Gang Date: Sat, 16 Aug 2025 05:03:50 +0800 Subject: [PATCH 392/693] agent: Update use_modifier_to_send behavior description for Windows (#36230) Release Notes: - N/A Signed-off-by: Yang Gang --- crates/agent_settings/src/agent_settings.rs | 2 +- crates/agent_ui/src/agent_configuration.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index d9557c5d00..fd38ba1f7f 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -309,7 +309,7 @@ pub struct AgentSettingsContent { /// /// Default: true expand_terminal_card: Option, - /// Whether to always use cmd-enter (or ctrl-enter on Linux) to send messages in the agent panel. + /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel. /// /// Default: false use_modifier_to_send: Option, diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 5f72fa58c8..96558f1bea 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -465,7 +465,7 @@ impl AgentConfiguration { "modifier-send", "Use modifier to submit a message", Some( - "Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.".into(), + "Make a modifier (cmd-enter on macOS, ctrl-enter on Linux or Windows) required to send messages.".into(), ), use_modifier_to_send, move |state, _window, cx| { From 3d77ad7e1a8a7afe068aac600d2ab56225fe1fed Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 15 Aug 2025 17:39:33 -0400 Subject: [PATCH 393/693] thread_view: Start loading images as soon as they're added (#36276) Release Notes: - N/A --- .../agent_ui/src/acp/completion_provider.rs | 129 +++------- crates/agent_ui/src/acp/message_editor.rs | 229 +++++++++++------- 2 files changed, 176 insertions(+), 182 deletions(-) diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index d7d2cd5d0e..1a9861d13a 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -1,20 +1,17 @@ -use std::ffi::OsStr; use std::ops::Range; -use std::path::Path; +use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; use acp_thread::MentionUri; use anyhow::{Context as _, Result, anyhow}; -use collections::HashMap; +use collections::{HashMap, HashSet}; use editor::display_map::CreaseId; use editor::{CompletionProvider, Editor, ExcerptId}; use futures::future::{Shared, try_join_all}; use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{App, Entity, ImageFormat, Img, Task, WeakEntity}; -use http_client::HttpClientWithUrl; +use gpui::{App, Entity, ImageFormat, Task, WeakEntity}; use language::{Buffer, CodeLabel, HighlightId}; -use language_model::LanguageModelImage; use lsp::CompletionContext; use project::{ Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId, @@ -43,7 +40,7 @@ use crate::context_picker::{ #[derive(Clone, Debug, Eq, PartialEq)] pub struct MentionImage { - pub abs_path: Option>, + pub abs_path: Option, pub data: SharedString, pub format: ImageFormat, } @@ -88,6 +85,8 @@ impl MentionSet { window: &mut Window, cx: &mut App, ) -> Task>> { + let mut processed_image_creases = HashSet::default(); + let mut contents = self .uri_by_crease_id .iter() @@ -97,59 +96,27 @@ impl MentionSet { // TODO directories let uri = uri.clone(); let abs_path = abs_path.to_path_buf(); - let extension = abs_path.extension().and_then(OsStr::to_str).unwrap_or(""); - if Img::extensions().contains(&extension) && !extension.contains("svg") { - let open_image_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(&abs_path, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_image(path, cx)) + if let Some(task) = self.images.get(&crease_id).cloned() { + processed_image_creases.insert(crease_id); + return cx.spawn(async move |_| { + let image = task.await.map_err(|e| anyhow!("{e}"))?; + anyhow::Ok((crease_id, Mention::Image(image))) }); - - cx.spawn(async move |cx| { - let image_item = open_image_task?.await?; - let (data, format) = image_item.update(cx, |image_item, cx| { - let format = image_item.image.format; - ( - LanguageModelImage::from_image( - image_item.image.clone(), - cx, - ), - format, - ) - })?; - let data = cx.spawn(async move |_| { - if let Some(data) = data.await { - Ok(data.source) - } else { - anyhow::bail!("Failed to convert image") - } - }); - - anyhow::Ok(( - crease_id, - Mention::Image(MentionImage { - abs_path: Some(abs_path.as_path().into()), - data: data.await?, - format, - }), - )) - }) - } else { - let buffer_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(abs_path, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_buffer(path, cx)) - }); - cx.spawn(async move |cx| { - let buffer = buffer_task?.await?; - let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; - - anyhow::Ok((crease_id, Mention::Text { uri, content })) - }) } + + let buffer_task = project.update(cx, |project, cx| { + let path = project + .find_project_path(abs_path, cx) + .context("Failed to find project path")?; + anyhow::Ok(project.open_buffer(path, cx)) + }); + cx.spawn(async move |cx| { + let buffer = buffer_task?.await?; + let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; + + anyhow::Ok((crease_id, Mention::Text { uri, content })) + }) } MentionUri::Symbol { path, line_range, .. @@ -243,15 +210,19 @@ impl MentionSet { }) .collect::>(); - contents.extend(self.images.iter().map(|(crease_id, image)| { + // Handle images that didn't have a mention URI (because they were added by the paste handler). + contents.extend(self.images.iter().filter_map(|(crease_id, image)| { + if processed_image_creases.contains(crease_id) { + return None; + } let crease_id = *crease_id; let image = image.clone(); - cx.spawn(async move |_| { + Some(cx.spawn(async move |_| { Ok(( crease_id, Mention::Image(image.await.map_err(|e| anyhow::anyhow!("{e}"))?), )) - }) + })) })); cx.spawn(async move |_cx| { @@ -753,7 +724,6 @@ impl ContextPickerCompletionProvider { source_range: Range, url_to_fetch: SharedString, message_editor: WeakEntity, - http_client: Arc, cx: &mut App, ) -> Option { let new_text = format!("@fetch {} ", url_to_fetch.clone()); @@ -772,30 +742,13 @@ impl ContextPickerCompletionProvider { source: project::CompletionSource::Custom, icon_path: Some(icon_path.clone()), insert_text_mode: None, - confirm: Some({ - Arc::new(move |_, window, cx| { - let url_to_fetch = url_to_fetch.clone(); - let source_range = source_range.clone(); - let message_editor = message_editor.clone(); - let new_text = new_text.clone(); - let http_client = http_client.clone(); - window.defer(cx, move |window, cx| { - message_editor - .update(cx, |message_editor, cx| { - message_editor.confirm_mention_for_fetch( - new_text, - source_range, - url_to_fetch, - http_client, - window, - cx, - ) - }) - .ok(); - }); - false - }) - }), + confirm: Some(confirm_completion_callback( + url_to_fetch.to_string().into(), + source_range.start, + new_text.len() - 1, + message_editor, + mention_uri, + )), }) } } @@ -843,7 +796,6 @@ impl CompletionProvider for ContextPickerCompletionProvider { }; let project = workspace.read(cx).project().clone(); - let http_client = workspace.read(cx).client().http_client(); let snapshot = buffer.read(cx).snapshot(); let source_range = snapshot.anchor_before(state.source_range.start) ..snapshot.anchor_after(state.source_range.end); @@ -852,8 +804,8 @@ impl CompletionProvider for ContextPickerCompletionProvider { let text_thread_store = self.text_thread_store.clone(); let editor = self.message_editor.clone(); let Ok((exclude_paths, exclude_threads)) = - self.message_editor.update(cx, |message_editor, cx| { - message_editor.mentioned_path_and_threads(cx) + self.message_editor.update(cx, |message_editor, _cx| { + message_editor.mentioned_path_and_threads() }) else { return Task::ready(Ok(Vec::new())); @@ -942,7 +894,6 @@ impl CompletionProvider for ContextPickerCompletionProvider { source_range.clone(), url, editor.clone(), - http_client.clone(), cx, ), diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 90827e5514..a4d74db266 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -16,14 +16,14 @@ use editor::{ use futures::{FutureExt as _, TryFutureExt as _}; use gpui::{ AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, Image, - ImageFormat, Task, TextStyle, WeakEntity, + ImageFormat, Img, Task, TextStyle, WeakEntity, }; -use http_client::HttpClientWithUrl; use language::{Buffer, Language}; use language_model::LanguageModelImage; use project::{CompletionIntent, Project}; use settings::Settings; use std::{ + ffi::OsStr, fmt::Write, ops::Range, path::{Path, PathBuf}, @@ -48,6 +48,7 @@ pub struct MessageEditor { mention_set: MentionSet, editor: Entity, project: Entity, + workspace: WeakEntity, thread_store: Entity, text_thread_store: Entity, } @@ -79,7 +80,7 @@ impl MessageEditor { None, ); let completion_provider = ContextPickerCompletionProvider::new( - workspace, + workspace.clone(), thread_store.downgrade(), text_thread_store.downgrade(), cx.weak_entity(), @@ -114,6 +115,7 @@ impl MessageEditor { mention_set, thread_store, text_thread_store, + workspace, } } @@ -131,7 +133,7 @@ impl MessageEditor { self.editor.read(cx).is_empty(cx) } - pub fn mentioned_path_and_threads(&self, _: &App) -> (HashSet, HashSet) { + pub fn mentioned_path_and_threads(&self) -> (HashSet, HashSet) { let mut excluded_paths = HashSet::default(); let mut excluded_threads = HashSet::default(); @@ -165,8 +167,14 @@ impl MessageEditor { let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else { return; }; + let Some(anchor) = snapshot + .buffer_snapshot + .anchor_in_excerpt(*excerpt_id, start) + else { + return; + }; - if let Some(crease_id) = crate::context_picker::insert_crease_for_mention( + let Some(crease_id) = crate::context_picker::insert_crease_for_mention( *excerpt_id, start, content_len, @@ -175,48 +183,83 @@ impl MessageEditor { self.editor.clone(), window, cx, - ) { - self.mention_set.insert_uri(crease_id, mention_uri.clone()); - } - } - - pub fn confirm_mention_for_fetch( - &mut self, - new_text: String, - source_range: Range, - url: url::Url, - http_client: Arc, - window: &mut Window, - cx: &mut Context, - ) { - let mention_uri = MentionUri::Fetch { url: url.clone() }; - let icon_path = mention_uri.icon_path(cx); - - let start = source_range.start; - let content_len = new_text.len() - 1; - - let snapshot = self - .editor - .update(cx, |editor, cx| editor.snapshot(window, cx)); - let Some((&excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else { - return; - }; - - let Some(crease_id) = crate::context_picker::insert_crease_for_mention( - excerpt_id, - start, - content_len, - url.to_string().into(), - icon_path, - self.editor.clone(), - window, - cx, ) else { return; }; + self.mention_set.insert_uri(crease_id, mention_uri.clone()); - let http_client = http_client.clone(); - let source_range = source_range.clone(); + match mention_uri { + MentionUri::Fetch { url } => { + self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx); + } + MentionUri::File { + abs_path, + is_directory, + } => { + self.confirm_mention_for_file( + crease_id, + anchor, + abs_path, + is_directory, + window, + cx, + ); + } + MentionUri::Symbol { .. } + | MentionUri::Thread { .. } + | MentionUri::TextThread { .. } + | MentionUri::Rule { .. } + | MentionUri::Selection { .. } => {} + } + } + + fn confirm_mention_for_file( + &mut self, + crease_id: CreaseId, + anchor: Anchor, + abs_path: PathBuf, + _is_directory: bool, + window: &mut Window, + cx: &mut Context, + ) { + let extension = abs_path + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default(); + let project = self.project.clone(); + let Some(project_path) = project + .read(cx) + .project_path_for_absolute_path(&abs_path, cx) + else { + return; + }; + + if Img::extensions().contains(&extension) && !extension.contains("svg") { + let image = cx.spawn(async move |_, cx| { + let image = project + .update(cx, |project, cx| project.open_image(project_path, cx))? + .await?; + image.read_with(cx, |image, _cx| image.image.clone()) + }); + self.confirm_mention_for_image(crease_id, anchor, Some(abs_path), image, window, cx); + } + } + + fn confirm_mention_for_fetch( + &mut self, + crease_id: CreaseId, + anchor: Anchor, + url: url::Url, + window: &mut Window, + cx: &mut Context, + ) { + let Some(http_client) = self + .workspace + .update(cx, |workspace, _cx| workspace.client().http_client()) + .ok() + else { + return; + }; let url_string = url.to_string(); let fetch = cx @@ -227,22 +270,18 @@ impl MessageEditor { .await }) .shared(); - self.mention_set.add_fetch_result(url, fetch.clone()); + self.mention_set + .add_fetch_result(url.clone(), fetch.clone()); cx.spawn_in(window, async move |this, cx| { let fetch = fetch.await.notify_async_err(cx); this.update(cx, |this, cx| { + let mention_uri = MentionUri::Fetch { url }; if fetch.is_some() { this.mention_set.insert_uri(crease_id, mention_uri.clone()); } else { // Remove crease if we failed to fetch this.editor.update(cx, |editor, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let Some(anchor) = - snapshot.anchor_in_excerpt(excerpt_id, source_range.start) - else { - return; - }; editor.display_map.update(cx, |display_map, cx| { display_map.unfold_intersecting(vec![anchor..anchor], true, cx); }); @@ -424,27 +463,46 @@ impl MessageEditor { let replacement_text = "image"; for image in images { - let (excerpt_id, anchor) = self.editor.update(cx, |message_editor, cx| { - let snapshot = message_editor.snapshot(window, cx); - let (excerpt_id, _, snapshot) = snapshot.buffer_snapshot.as_singleton().unwrap(); + let (excerpt_id, text_anchor, multibuffer_anchor) = + self.editor.update(cx, |message_editor, cx| { + let snapshot = message_editor.snapshot(window, cx); + let (excerpt_id, _, buffer_snapshot) = + snapshot.buffer_snapshot.as_singleton().unwrap(); - let anchor = snapshot.anchor_before(snapshot.len()); - message_editor.edit( - [( - multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), - format!("{replacement_text} "), - )], - cx, - ); - (*excerpt_id, anchor) - }); + let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len()); + let multibuffer_anchor = snapshot + .buffer_snapshot + .anchor_in_excerpt(*excerpt_id, text_anchor); + message_editor.edit( + [( + multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), + format!("{replacement_text} "), + )], + cx, + ); + (*excerpt_id, text_anchor, multibuffer_anchor) + }); - self.insert_image( + let content_len = replacement_text.len(); + let Some(anchor) = multibuffer_anchor else { + return; + }; + let Some(crease_id) = insert_crease_for_image( excerpt_id, + text_anchor, + content_len, + None.clone(), + self.editor.clone(), + window, + cx, + ) else { + return; + }; + self.confirm_mention_for_image( + crease_id, anchor, - replacement_text.len(), - Arc::new(image), None, + Task::ready(Ok(Arc::new(image))), window, cx, ); @@ -510,34 +568,25 @@ impl MessageEditor { }) } - fn insert_image( + fn confirm_mention_for_image( &mut self, - excerpt_id: ExcerptId, - crease_start: text::Anchor, - content_len: usize, - image: Arc, - abs_path: Option>, + crease_id: CreaseId, + anchor: Anchor, + abs_path: Option, + image: Task>>, window: &mut Window, cx: &mut Context, ) { - let Some(crease_id) = insert_crease_for_image( - excerpt_id, - crease_start, - content_len, - abs_path.clone(), - self.editor.clone(), - window, - cx, - ) else { - return; - }; self.editor.update(cx, |_editor, cx| { - let format = image.format; - let convert = LanguageModelImage::from_image(image, cx); - let task = cx .spawn_in(window, async move |editor, cx| { - if let Some(image) = convert.await { + let image = image.await.map_err(|e| e.to_string())?; + let format = image.format; + let image = cx + .update(|_, cx| LanguageModelImage::from_image(image, cx)) + .map_err(|e| e.to_string())? + .await; + if let Some(image) = image { Ok(MentionImage { abs_path, data: image.source, @@ -546,12 +595,6 @@ impl MessageEditor { } else { editor .update(cx, |editor, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let Some(anchor) = - snapshot.anchor_in_excerpt(excerpt_id, crease_start) - else { - return; - }; editor.display_map.update(cx, |display_map, cx| { display_map.unfold_intersecting(vec![anchor..anchor], true, cx); }); From f642f7615f876f56b1cb5bad90c9ee2bbf574bf0 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Fri, 15 Aug 2025 16:59:57 -0500 Subject: [PATCH 394/693] keymap_ui: Don't try to parse empty action arguments as JSON (#36278) Closes #ISSUE Release Notes: - Keymap Editor: Fixed an issue where leaving the arguments field empty would result in an error even if arguments were optional --- crates/settings_ui/src/keybindings.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 1aaab211aa..b4e871c617 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -2181,6 +2181,7 @@ impl KeybindingEditorModal { let value = action_arguments .as_ref() + .filter(|args| !args.is_empty()) .map(|args| { serde_json::from_str(args).context("Failed to parse action arguments as JSON") }) From b9c110e63e02eea44cde2c1e24d6d332e2a6f0ee Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 18:01:41 -0400 Subject: [PATCH 395/693] collab: Remove `GET /users/look_up` endpoint (#36279) This PR removes the `GET /users/look_up` endpoint from Collab, as it has been moved to Cloud. Release Notes: - N/A --- crates/collab/src/api.rs | 101 +-------------------------------------- 1 file changed, 1 insertion(+), 100 deletions(-) diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 143e764eb3..0cc7e2b2e9 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -4,12 +4,7 @@ pub mod extensions; pub mod ips_file; pub mod slack; -use crate::db::Database; -use crate::{ - AppState, Error, Result, auth, - db::{User, UserId}, - rpc, -}; +use crate::{AppState, Error, Result, auth, db::UserId, rpc}; use anyhow::Context as _; use axum::{ Extension, Json, Router, @@ -96,7 +91,6 @@ impl std::fmt::Display for SystemIdHeader { pub fn routes(rpc_server: Arc) -> Router<(), Body> { Router::new() - .route("/users/look_up", get(look_up_user)) .route("/users/:id/access_tokens", post(create_access_token)) .route("/rpc_server_snapshot", get(get_rpc_server_snapshot)) .merge(contributors::router()) @@ -138,99 +132,6 @@ pub async fn validate_api_token(req: Request, next: Next) -> impl IntoR Ok::<_, Error>(next.run(req).await) } -#[derive(Debug, Deserialize)] -struct LookUpUserParams { - identifier: String, -} - -#[derive(Debug, Serialize)] -struct LookUpUserResponse { - user: Option, -} - -async fn look_up_user( - Query(params): Query, - Extension(app): Extension>, -) -> Result> { - let user = resolve_identifier_to_user(&app.db, ¶ms.identifier).await?; - let user = if let Some(user) = user { - match user { - UserOrId::User(user) => Some(user), - UserOrId::Id(id) => app.db.get_user_by_id(id).await?, - } - } else { - None - }; - - Ok(Json(LookUpUserResponse { user })) -} - -enum UserOrId { - User(User), - Id(UserId), -} - -async fn resolve_identifier_to_user( - db: &Arc, - identifier: &str, -) -> Result> { - if let Some(identifier) = identifier.parse::().ok() { - let user = db.get_user_by_id(UserId(identifier)).await?; - - return Ok(user.map(UserOrId::User)); - } - - if identifier.starts_with("cus_") { - let billing_customer = db - .get_billing_customer_by_stripe_customer_id(&identifier) - .await?; - - return Ok(billing_customer.map(|billing_customer| UserOrId::Id(billing_customer.user_id))); - } - - if identifier.starts_with("sub_") { - let billing_subscription = db - .get_billing_subscription_by_stripe_subscription_id(&identifier) - .await?; - - if let Some(billing_subscription) = billing_subscription { - let billing_customer = db - .get_billing_customer_by_id(billing_subscription.billing_customer_id) - .await?; - - return Ok( - billing_customer.map(|billing_customer| UserOrId::Id(billing_customer.user_id)) - ); - } else { - return Ok(None); - } - } - - if identifier.contains('@') { - let user = db.get_user_by_email(identifier).await?; - - return Ok(user.map(UserOrId::User)); - } - - if let Some(user) = db.get_user_by_github_login(identifier).await? { - return Ok(Some(UserOrId::User(user))); - } - - Ok(None) -} - -#[derive(Deserialize, Debug)] -struct CreateUserParams { - github_user_id: i32, - github_login: String, - email_address: String, - email_confirmation_code: Option, - #[serde(default)] - admin: bool, - #[serde(default)] - invite_count: i32, -} - async fn get_rpc_server_snapshot( Extension(rpc_server): Extension>, ) -> Result { From bf34e185d518f02f032a420f5ed1a59f115b1a9f Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 15 Aug 2025 18:47:36 -0400 Subject: [PATCH 396/693] Move MentionSet to message_editor module (#36281) This is a more natural place for it than its current home next to the completion provider. Release Notes: - N/A --- .../agent_ui/src/acp/completion_provider.rs | 686 +-------------- crates/agent_ui/src/acp/message_editor.rs | 796 ++++++++++++++++-- 2 files changed, 743 insertions(+), 739 deletions(-) diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 1a9861d13a..8a413fc91e 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -1,16 +1,12 @@ use std::ops::Range; -use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; use acp_thread::MentionUri; -use anyhow::{Context as _, Result, anyhow}; -use collections::{HashMap, HashSet}; -use editor::display_map::CreaseId; +use anyhow::Result; use editor::{CompletionProvider, Editor, ExcerptId}; -use futures::future::{Shared, try_join_all}; use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{App, Entity, ImageFormat, Task, WeakEntity}; +use gpui::{App, Entity, Task, WeakEntity}; use language::{Buffer, CodeLabel, HighlightId}; use lsp::CompletionContext; use project::{ @@ -20,7 +16,6 @@ use prompt_store::PromptStore; use rope::Point; use text::{Anchor, ToPoint as _}; use ui::prelude::*; -use url::Url; use workspace::Workspace; use agent::thread_store::{TextThreadStore, ThreadStore}; @@ -38,206 +33,6 @@ use crate::context_picker::{ available_context_picker_entries, recent_context_picker_entries, selection_ranges, }; -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct MentionImage { - pub abs_path: Option, - pub data: SharedString, - pub format: ImageFormat, -} - -#[derive(Default)] -pub struct MentionSet { - pub(crate) uri_by_crease_id: HashMap, - fetch_results: HashMap>>>, - images: HashMap>>>, -} - -impl MentionSet { - pub fn insert_uri(&mut self, crease_id: CreaseId, uri: MentionUri) { - self.uri_by_crease_id.insert(crease_id, uri); - } - - pub fn add_fetch_result(&mut self, url: Url, content: Shared>>) { - self.fetch_results.insert(url, content); - } - - pub fn insert_image( - &mut self, - crease_id: CreaseId, - task: Shared>>, - ) { - self.images.insert(crease_id, task); - } - - pub fn drain(&mut self) -> impl Iterator { - self.fetch_results.clear(); - self.uri_by_crease_id - .drain() - .map(|(id, _)| id) - .chain(self.images.drain().map(|(id, _)| id)) - } - - pub fn contents( - &self, - project: Entity, - thread_store: Entity, - text_thread_store: Entity, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - let mut processed_image_creases = HashSet::default(); - - let mut contents = self - .uri_by_crease_id - .iter() - .map(|(&crease_id, uri)| { - match uri { - MentionUri::File { abs_path, .. } => { - // TODO directories - let uri = uri.clone(); - let abs_path = abs_path.to_path_buf(); - - if let Some(task) = self.images.get(&crease_id).cloned() { - processed_image_creases.insert(crease_id); - return cx.spawn(async move |_| { - let image = task.await.map_err(|e| anyhow!("{e}"))?; - anyhow::Ok((crease_id, Mention::Image(image))) - }); - } - - let buffer_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(abs_path, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_buffer(path, cx)) - }); - cx.spawn(async move |cx| { - let buffer = buffer_task?.await?; - let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; - - anyhow::Ok((crease_id, Mention::Text { uri, content })) - }) - } - MentionUri::Symbol { - path, line_range, .. - } - | MentionUri::Selection { - path, line_range, .. - } => { - let uri = uri.clone(); - let path_buf = path.clone(); - let line_range = line_range.clone(); - - let buffer_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(&path_buf, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_buffer(path, cx)) - }); - - cx.spawn(async move |cx| { - let buffer = buffer_task?.await?; - let content = buffer.read_with(cx, |buffer, _cx| { - buffer - .text_for_range( - Point::new(line_range.start, 0) - ..Point::new( - line_range.end, - buffer.line_len(line_range.end), - ), - ) - .collect() - })?; - - anyhow::Ok((crease_id, Mention::Text { uri, content })) - }) - } - MentionUri::Thread { id: thread_id, .. } => { - let open_task = thread_store.update(cx, |thread_store, cx| { - thread_store.open_thread(&thread_id, window, cx) - }); - - let uri = uri.clone(); - cx.spawn(async move |cx| { - let thread = open_task.await?; - let content = thread.read_with(cx, |thread, _cx| { - thread.latest_detailed_summary_or_text().to_string() - })?; - - anyhow::Ok((crease_id, Mention::Text { uri, content })) - }) - } - MentionUri::TextThread { path, .. } => { - let context = text_thread_store.update(cx, |text_thread_store, cx| { - text_thread_store.open_local_context(path.as_path().into(), cx) - }); - let uri = uri.clone(); - cx.spawn(async move |cx| { - let context = context.await?; - let xml = context.update(cx, |context, cx| context.to_xml(cx))?; - anyhow::Ok((crease_id, Mention::Text { uri, content: xml })) - }) - } - MentionUri::Rule { id: prompt_id, .. } => { - let Some(prompt_store) = thread_store.read(cx).prompt_store().clone() - else { - return Task::ready(Err(anyhow!("missing prompt store"))); - }; - let text_task = prompt_store.read(cx).load(*prompt_id, cx); - let uri = uri.clone(); - cx.spawn(async move |_| { - // TODO: report load errors instead of just logging - let text = text_task.await?; - anyhow::Ok((crease_id, Mention::Text { uri, content: text })) - }) - } - MentionUri::Fetch { url } => { - let Some(content) = self.fetch_results.get(&url).cloned() else { - return Task::ready(Err(anyhow!("missing fetch result"))); - }; - let uri = uri.clone(); - cx.spawn(async move |_| { - Ok(( - crease_id, - Mention::Text { - uri, - content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, - }, - )) - }) - } - } - }) - .collect::>(); - - // Handle images that didn't have a mention URI (because they were added by the paste handler). - contents.extend(self.images.iter().filter_map(|(crease_id, image)| { - if processed_image_creases.contains(crease_id) { - return None; - } - let crease_id = *crease_id; - let image = image.clone(); - Some(cx.spawn(async move |_| { - Ok(( - crease_id, - Mention::Image(image.await.map_err(|e| anyhow::anyhow!("{e}"))?), - )) - })) - })); - - cx.spawn(async move |_cx| { - let contents = try_join_all(contents).await?.into_iter().collect(); - anyhow::Ok(contents) - }) - } -} - -#[derive(Debug, Eq, PartialEq)] -pub enum Mention { - Text { uri: MentionUri, content: String }, - Image(MentionImage), -} - pub(crate) enum Match { File(FileMatch), Symbol(SymbolMatch), @@ -1044,15 +839,6 @@ impl MentionCompletion { #[cfg(test)] mod tests { use super::*; - use editor::{AnchorRangeExt, EditorMode}; - use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext}; - use project::{Project, ProjectPath}; - use serde_json::json; - use settings::SettingsStore; - use smol::stream::StreamExt as _; - use std::{ops::Deref, path::Path}; - use util::path; - use workspace::{AppState, Item}; #[test] fn test_mention_completion_parse() { @@ -1123,472 +909,4 @@ mod tests { assert_eq!(MentionCompletion::try_parse("test@", 0), None); } - - struct MessageEditorItem(Entity); - - impl Item for MessageEditorItem { - type Event = (); - - fn include_in_nav_history() -> bool { - false - } - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "Test".into() - } - } - - impl EventEmitter<()> for MessageEditorItem {} - - impl Focusable for MessageEditorItem { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.0.read(cx).focus_handle(cx).clone() - } - } - - impl Render for MessageEditorItem { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - self.0.clone().into_any_element() - } - } - - #[gpui::test] - async fn test_context_completion_provider(cx: &mut TestAppContext) { - init_test(cx); - - let app_state = cx.update(AppState::test); - - cx.update(|cx| { - language::init(cx); - editor::init(cx); - workspace::init(app_state.clone(), cx); - Project::init_settings(cx); - }); - - app_state - .fs - .as_fake() - .insert_tree( - path!("/dir"), - json!({ - "editor": "", - "a": { - "one.txt": "1", - "two.txt": "2", - "three.txt": "3", - "four.txt": "4" - }, - "b": { - "five.txt": "5", - "six.txt": "6", - "seven.txt": "7", - "eight.txt": "8", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); - - let worktree = project.update(cx, |project, cx| { - let mut worktrees = project.worktrees(cx).collect::>(); - assert_eq!(worktrees.len(), 1); - worktrees.pop().unwrap() - }); - let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); - - let mut cx = VisualTestContext::from_window(*window.deref(), cx); - - let paths = vec![ - path!("a/one.txt"), - path!("a/two.txt"), - path!("a/three.txt"), - path!("a/four.txt"), - path!("b/five.txt"), - path!("b/six.txt"), - path!("b/seven.txt"), - path!("b/eight.txt"), - ]; - - let mut opened_editors = Vec::new(); - for path in paths { - let buffer = workspace - .update_in(&mut cx, |workspace, window, cx| { - workspace.open_path( - ProjectPath { - worktree_id, - path: Path::new(path).into(), - }, - None, - false, - window, - cx, - ) - }) - .await - .unwrap(); - opened_editors.push(buffer); - } - - let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx)); - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - - let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| { - let workspace_handle = cx.weak_entity(); - let message_editor = cx.new(|cx| { - MessageEditor::new( - workspace_handle, - project.clone(), - thread_store.clone(), - text_thread_store.clone(), - EditorMode::AutoHeight { - max_lines: None, - min_lines: 1, - }, - window, - cx, - ) - }); - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item( - Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))), - true, - true, - None, - window, - cx, - ); - }); - message_editor.read(cx).focus_handle(cx).focus(window); - let editor = message_editor.read(cx).editor().clone(); - (message_editor, editor) - }); - - cx.simulate_input("Lorem "); - - editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem "); - assert!(!editor.has_visible_completions_menu()); - }); - - cx.simulate_input("@"); - - editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem @"); - assert!(editor.has_visible_completions_menu()); - assert_eq!( - current_completion_labels(editor), - &[ - "eight.txt dir/b/", - "seven.txt dir/b/", - "six.txt dir/b/", - "five.txt dir/b/", - "Files & Directories", - "Symbols", - "Threads", - "Fetch" - ] - ); - }); - - // Select and confirm "File" - editor.update_in(&mut cx, |editor, window, cx| { - assert!(editor.has_visible_completions_menu()); - editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); - editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); - editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); - editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - cx.run_until_parked(); - - editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem @file "); - assert!(editor.has_visible_completions_menu()); - }); - - cx.simulate_input("one"); - - editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem @file one"); - assert!(editor.has_visible_completions_menu()); - assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]); - }); - - editor.update_in(&mut cx, |editor, window, cx| { - assert!(editor.has_visible_completions_menu()); - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) "); - assert!(!editor.has_visible_completions_menu()); - assert_eq!( - fold_ranges(editor, cx), - vec![Point::new(0, 6)..Point::new(0, 39)] - ); - }); - - let contents = message_editor - .update_in(&mut cx, |message_editor, window, cx| { - message_editor.mention_set().contents( - project.clone(), - thread_store.clone(), - text_thread_store.clone(), - window, - cx, - ) - }) - .await - .unwrap() - .into_values() - .collect::>(); - - pretty_assertions::assert_eq!( - contents, - [Mention::Text { - content: "1".into(), - uri: "file:///dir/a/one.txt".parse().unwrap() - }] - ); - - cx.simulate_input(" "); - - editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) "); - assert!(!editor.has_visible_completions_menu()); - assert_eq!( - fold_ranges(editor, cx), - vec![Point::new(0, 6)..Point::new(0, 39)] - ); - }); - - cx.simulate_input("Ipsum "); - - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum ", - ); - assert!(!editor.has_visible_completions_menu()); - assert_eq!( - fold_ranges(editor, cx), - vec![Point::new(0, 6)..Point::new(0, 39)] - ); - }); - - cx.simulate_input("@file "); - - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum @file ", - ); - assert!(editor.has_visible_completions_menu()); - assert_eq!( - fold_ranges(editor, cx), - vec![Point::new(0, 6)..Point::new(0, 39)] - ); - }); - - editor.update_in(&mut cx, |editor, window, cx| { - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - cx.run_until_parked(); - - let contents = message_editor - .update_in(&mut cx, |message_editor, window, cx| { - message_editor.mention_set().contents( - project.clone(), - thread_store.clone(), - text_thread_store.clone(), - window, - cx, - ) - }) - .await - .unwrap() - .into_values() - .collect::>(); - - assert_eq!(contents.len(), 2); - pretty_assertions::assert_eq!( - contents[1], - Mention::Text { - content: "8".to_string(), - uri: "file:///dir/b/eight.txt".parse().unwrap(), - } - ); - - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) " - ); - assert!(!editor.has_visible_completions_menu()); - assert_eq!( - fold_ranges(editor, cx), - vec![ - Point::new(0, 6)..Point::new(0, 39), - Point::new(0, 47)..Point::new(0, 84) - ] - ); - }); - - let plain_text_language = Arc::new(language::Language::new( - language::LanguageConfig { - name: "Plain Text".into(), - matcher: language::LanguageMatcher { - path_suffixes: vec!["txt".to_string()], - ..Default::default() - }, - ..Default::default() - }, - None, - )); - - // Register the language and fake LSP - let language_registry = project.read_with(&cx, |project, _| project.languages().clone()); - language_registry.add(plain_text_language); - - let mut fake_language_servers = language_registry.register_fake_lsp( - "Plain Text", - language::FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - workspace_symbol_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - }, - ); - - // Open the buffer to trigger LSP initialization - let buffer = project - .update(&mut cx, |project, cx| { - project.open_local_buffer(path!("/dir/a/one.txt"), cx) - }) - .await - .unwrap(); - - // Register the buffer with language servers - let _handle = project.update(&mut cx, |project, cx| { - project.register_buffer_with_language_servers(&buffer, cx) - }); - - cx.run_until_parked(); - - let fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.set_request_handler::( - |_, _| async move { - Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![ - #[allow(deprecated)] - lsp::SymbolInformation { - name: "MySymbol".into(), - location: lsp::Location { - uri: lsp::Url::from_file_path(path!("/dir/a/one.txt")).unwrap(), - range: lsp::Range::new( - lsp::Position::new(0, 0), - lsp::Position::new(0, 1), - ), - }, - kind: lsp::SymbolKind::CONSTANT, - tags: None, - container_name: None, - deprecated: None, - }, - ]))) - }, - ); - - cx.simulate_input("@symbol "); - - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) @symbol " - ); - assert!(editor.has_visible_completions_menu()); - assert_eq!( - current_completion_labels(editor), - &[ - "MySymbol", - ] - ); - }); - - editor.update_in(&mut cx, |editor, window, cx| { - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - let contents = message_editor - .update_in(&mut cx, |message_editor, window, cx| { - message_editor.mention_set().contents( - project.clone(), - thread_store, - text_thread_store, - window, - cx, - ) - }) - .await - .unwrap() - .into_values() - .collect::>(); - - assert_eq!(contents.len(), 3); - pretty_assertions::assert_eq!( - contents[2], - Mention::Text { - content: "1".into(), - uri: "file:///dir/a/one.txt?symbol=MySymbol#L1:1" - .parse() - .unwrap(), - } - ); - - cx.run_until_parked(); - - editor.read_with(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) [@MySymbol](file:///dir/a/one.txt?symbol=MySymbol#L1:1) " - ); - }); - } - - fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec> { - let snapshot = editor.buffer().read(cx).snapshot(cx); - editor.display_map.update(cx, |display_map, cx| { - display_map - .snapshot(cx) - .folds_in_range(0..snapshot.len()) - .map(|fold| fold.range.to_point(&snapshot)) - .collect() - }) - } - - fn current_completion_labels(editor: &Editor) -> Vec { - let completions = editor.current_completions().expect("Missing completions"); - completions - .into_iter() - .map(|completion| completion.label.text.to_string()) - .collect::>() - } - - pub(crate) fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let store = SettingsStore::test(cx); - cx.set_global(store); - theme::init(theme::LoadThemes::JustBase, cx); - client::init_settings(cx); - language::init(cx); - Project::init_settings(cx); - workspace::init_settings(cx); - editor::init_settings(cx); - }); - } } diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index a4d74db266..f6fee3b87e 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1,19 +1,22 @@ use crate::{ - acp::completion_provider::{ContextPickerCompletionProvider, MentionImage, MentionSet}, + acp::completion_provider::ContextPickerCompletionProvider, context_picker::fetch_context_picker::fetch_url_content, }; use acp_thread::{MentionUri, selection_name}; use agent::{TextThreadStore, ThreadId, ThreadStore}; use agent_client_protocol as acp; -use anyhow::Result; -use collections::HashSet; +use anyhow::{Context as _, Result, anyhow}; +use collections::{HashMap, HashSet}; use editor::{ Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, ToOffset, actions::Paste, display_map::{Crease, CreaseId, FoldId}, }; -use futures::{FutureExt as _, TryFutureExt as _}; +use futures::{ + FutureExt as _, TryFutureExt as _, + future::{Shared, try_join_all}, +}; use gpui::{ AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, Task, TextStyle, WeakEntity, @@ -21,6 +24,7 @@ use gpui::{ use language::{Buffer, Language}; use language_model::LanguageModelImage; use project::{CompletionIntent, Project}; +use rope::Point; use settings::Settings; use std::{ ffi::OsStr, @@ -38,12 +42,11 @@ use ui::{ Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div, h_flex, }; +use url::Url; use util::ResultExt; use workspace::{Workspace, notifications::NotifyResultExt as _}; use zed_actions::agent::Chat; -use super::completion_provider::Mention; - pub struct MessageEditor { mention_set: MentionSet, editor: Entity, @@ -186,7 +189,6 @@ impl MessageEditor { ) else { return; }; - self.mention_set.insert_uri(crease_id, mention_uri.clone()); match mention_uri { MentionUri::Fetch { url } => { @@ -209,7 +211,9 @@ impl MessageEditor { | MentionUri::Thread { .. } | MentionUri::TextThread { .. } | MentionUri::Rule { .. } - | MentionUri::Selection { .. } => {} + | MentionUri::Selection { .. } => { + self.mention_set.insert_uri(crease_id, mention_uri.clone()); + } } } @@ -218,7 +222,7 @@ impl MessageEditor { crease_id: CreaseId, anchor: Anchor, abs_path: PathBuf, - _is_directory: bool, + is_directory: bool, window: &mut Window, cx: &mut Context, ) { @@ -226,15 +230,15 @@ impl MessageEditor { .extension() .and_then(OsStr::to_str) .unwrap_or_default(); - let project = self.project.clone(); - let Some(project_path) = project - .read(cx) - .project_path_for_absolute_path(&abs_path, cx) - else { - return; - }; if Img::extensions().contains(&extension) && !extension.contains("svg") { + let project = self.project.clone(); + let Some(project_path) = project + .read(cx) + .project_path_for_absolute_path(&abs_path, cx) + else { + return; + }; let image = cx.spawn(async move |_, cx| { let image = project .update(cx, |project, cx| project.open_image(project_path, cx))? @@ -242,6 +246,14 @@ impl MessageEditor { image.read_with(cx, |image, _cx| image.image.clone()) }); self.confirm_mention_for_image(crease_id, anchor, Some(abs_path), image, window, cx); + } else { + self.mention_set.insert_uri( + crease_id, + MentionUri::File { + abs_path, + is_directory, + }, + ); } } @@ -577,43 +589,54 @@ impl MessageEditor { window: &mut Window, cx: &mut Context, ) { - self.editor.update(cx, |_editor, cx| { - let task = cx - .spawn_in(window, async move |editor, cx| { - let image = image.await.map_err(|e| e.to_string())?; - let format = image.format; - let image = cx - .update(|_, cx| LanguageModelImage::from_image(image, cx)) - .map_err(|e| e.to_string())? - .await; - if let Some(image) = image { - Ok(MentionImage { - abs_path, - data: image.source, - format, + let editor = self.editor.clone(); + let task = cx + .spawn_in(window, async move |this, cx| { + let image = image.await.map_err(|e| e.to_string())?; + let format = image.format; + let image = cx + .update(|_, cx| LanguageModelImage::from_image(image, cx)) + .map_err(|e| e.to_string())? + .await; + if let Some(image) = image { + if let Some(abs_path) = abs_path.clone() { + this.update(cx, |this, _cx| { + this.mention_set.insert_uri( + crease_id, + MentionUri::File { + abs_path, + is_directory: false, + }, + ); }) - } else { - editor - .update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - Err("Failed to convert image".to_string()) + .map_err(|e| e.to_string())?; } - }) - .shared(); - - cx.spawn_in(window, { - let task = task.clone(); - async move |_, cx| task.clone().await.notify_async_err(cx) + Ok(MentionImage { + abs_path, + data: image.source, + format, + }) + } else { + editor + .update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + Err("Failed to convert image".to_string()) + } }) - .detach(); + .shared(); - self.mention_set.insert_image(crease_id, task); - }); + cx.spawn_in(window, { + let task = task.clone(); + async move |_, cx| task.clone().await.notify_async_err(cx) + }) + .detach(); + + self.mention_set.insert_image(crease_id, task); } pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context) { @@ -867,22 +890,230 @@ fn render_image_fold_icon_button( }) } +#[derive(Debug, Eq, PartialEq)] +pub enum Mention { + Text { uri: MentionUri, content: String }, + Image(MentionImage), +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MentionImage { + pub abs_path: Option, + pub data: SharedString, + pub format: ImageFormat, +} + +#[derive(Default)] +pub struct MentionSet { + pub(crate) uri_by_crease_id: HashMap, + fetch_results: HashMap>>>, + images: HashMap>>>, +} + +impl MentionSet { + pub fn insert_uri(&mut self, crease_id: CreaseId, uri: MentionUri) { + self.uri_by_crease_id.insert(crease_id, uri); + } + + pub fn add_fetch_result(&mut self, url: Url, content: Shared>>) { + self.fetch_results.insert(url, content); + } + + pub fn insert_image( + &mut self, + crease_id: CreaseId, + task: Shared>>, + ) { + self.images.insert(crease_id, task); + } + + pub fn drain(&mut self) -> impl Iterator { + self.fetch_results.clear(); + self.uri_by_crease_id + .drain() + .map(|(id, _)| id) + .chain(self.images.drain().map(|(id, _)| id)) + } + + pub fn contents( + &self, + project: Entity, + thread_store: Entity, + text_thread_store: Entity, + window: &mut Window, + cx: &mut App, + ) -> Task>> { + let mut processed_image_creases = HashSet::default(); + + let mut contents = self + .uri_by_crease_id + .iter() + .map(|(&crease_id, uri)| { + match uri { + MentionUri::File { abs_path, .. } => { + // TODO directories + let uri = uri.clone(); + let abs_path = abs_path.to_path_buf(); + + if let Some(task) = self.images.get(&crease_id).cloned() { + processed_image_creases.insert(crease_id); + return cx.spawn(async move |_| { + let image = task.await.map_err(|e| anyhow!("{e}"))?; + anyhow::Ok((crease_id, Mention::Image(image))) + }); + } + + let buffer_task = project.update(cx, |project, cx| { + let path = project + .find_project_path(abs_path, cx) + .context("Failed to find project path")?; + anyhow::Ok(project.open_buffer(path, cx)) + }); + cx.spawn(async move |cx| { + let buffer = buffer_task?.await?; + let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; + + anyhow::Ok((crease_id, Mention::Text { uri, content })) + }) + } + MentionUri::Symbol { + path, line_range, .. + } + | MentionUri::Selection { + path, line_range, .. + } => { + let uri = uri.clone(); + let path_buf = path.clone(); + let line_range = line_range.clone(); + + let buffer_task = project.update(cx, |project, cx| { + let path = project + .find_project_path(&path_buf, cx) + .context("Failed to find project path")?; + anyhow::Ok(project.open_buffer(path, cx)) + }); + + cx.spawn(async move |cx| { + let buffer = buffer_task?.await?; + let content = buffer.read_with(cx, |buffer, _cx| { + buffer + .text_for_range( + Point::new(line_range.start, 0) + ..Point::new( + line_range.end, + buffer.line_len(line_range.end), + ), + ) + .collect() + })?; + + anyhow::Ok((crease_id, Mention::Text { uri, content })) + }) + } + MentionUri::Thread { id: thread_id, .. } => { + let open_task = thread_store.update(cx, |thread_store, cx| { + thread_store.open_thread(&thread_id, window, cx) + }); + + let uri = uri.clone(); + cx.spawn(async move |cx| { + let thread = open_task.await?; + let content = thread.read_with(cx, |thread, _cx| { + thread.latest_detailed_summary_or_text().to_string() + })?; + + anyhow::Ok((crease_id, Mention::Text { uri, content })) + }) + } + MentionUri::TextThread { path, .. } => { + let context = text_thread_store.update(cx, |text_thread_store, cx| { + text_thread_store.open_local_context(path.as_path().into(), cx) + }); + let uri = uri.clone(); + cx.spawn(async move |cx| { + let context = context.await?; + let xml = context.update(cx, |context, cx| context.to_xml(cx))?; + anyhow::Ok((crease_id, Mention::Text { uri, content: xml })) + }) + } + MentionUri::Rule { id: prompt_id, .. } => { + let Some(prompt_store) = thread_store.read(cx).prompt_store().clone() + else { + return Task::ready(Err(anyhow!("missing prompt store"))); + }; + let text_task = prompt_store.read(cx).load(*prompt_id, cx); + let uri = uri.clone(); + cx.spawn(async move |_| { + // TODO: report load errors instead of just logging + let text = text_task.await?; + anyhow::Ok((crease_id, Mention::Text { uri, content: text })) + }) + } + MentionUri::Fetch { url } => { + let Some(content) = self.fetch_results.get(&url).cloned() else { + return Task::ready(Err(anyhow!("missing fetch result"))); + }; + let uri = uri.clone(); + cx.spawn(async move |_| { + Ok(( + crease_id, + Mention::Text { + uri, + content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, + }, + )) + }) + } + } + }) + .collect::>(); + + // Handle images that didn't have a mention URI (because they were added by the paste handler). + contents.extend(self.images.iter().filter_map(|(crease_id, image)| { + if processed_image_creases.contains(crease_id) { + return None; + } + let crease_id = *crease_id; + let image = image.clone(); + Some(cx.spawn(async move |_| { + Ok(( + crease_id, + Mention::Image(image.await.map_err(|e| anyhow::anyhow!("{e}"))?), + )) + })) + })); + + cx.spawn(async move |_cx| { + let contents = try_join_all(contents).await?.into_iter().collect(); + anyhow::Ok(contents) + }) + } +} + #[cfg(test)] mod tests { - use std::path::Path; + use std::{ops::Range, path::Path, sync::Arc}; use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol as acp; - use editor::EditorMode; + use editor::{AnchorRangeExt as _, Editor, EditorMode}; use fs::FakeFs; - use gpui::{AppContext, TestAppContext}; + use futures::StreamExt as _; + use gpui::{ + AppContext, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext, + }; use lsp::{CompletionContext, CompletionTriggerKind}; - use project::{CompletionIntent, Project}; + use project::{CompletionIntent, Project, ProjectPath}; use serde_json::json; + use text::Point; + use ui::{App, Context, IntoElement, Render, SharedString, Window}; use util::path; - use workspace::Workspace; + use workspace::{AppState, Item, Workspace}; - use crate::acp::{message_editor::MessageEditor, thread_view::tests::init_test}; + use crate::acp::{ + message_editor::{Mention, MessageEditor}, + thread_view::tests::init_test, + }; #[gpui::test] async fn test_at_mention_removal(cx: &mut TestAppContext) { @@ -982,4 +1213,459 @@ mod tests { // We don't send a resource link for the deleted crease. pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]); } + + struct MessageEditorItem(Entity); + + impl Item for MessageEditorItem { + type Event = (); + + fn include_in_nav_history() -> bool { + false + } + + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "Test".into() + } + } + + impl EventEmitter<()> for MessageEditorItem {} + + impl Focusable for MessageEditorItem { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.0.read(cx).focus_handle(cx).clone() + } + } + + impl Render for MessageEditorItem { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + self.0.clone().into_any_element() + } + } + + #[gpui::test] + async fn test_context_completion_provider(cx: &mut TestAppContext) { + init_test(cx); + + let app_state = cx.update(AppState::test); + + cx.update(|cx| { + language::init(cx); + editor::init(cx); + workspace::init(app_state.clone(), cx); + Project::init_settings(cx); + }); + + app_state + .fs + .as_fake() + .insert_tree( + path!("/dir"), + json!({ + "editor": "", + "a": { + "one.txt": "1", + "two.txt": "2", + "three.txt": "3", + "four.txt": "4" + }, + "b": { + "five.txt": "5", + "six.txt": "6", + "seven.txt": "7", + "eight.txt": "8", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; + let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let workspace = window.root(cx).unwrap(); + + let worktree = project.update(cx, |project, cx| { + let mut worktrees = project.worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1); + worktrees.pop().unwrap() + }); + let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); + + let mut cx = VisualTestContext::from_window(*window, cx); + + let paths = vec![ + path!("a/one.txt"), + path!("a/two.txt"), + path!("a/three.txt"), + path!("a/four.txt"), + path!("b/five.txt"), + path!("b/six.txt"), + path!("b/seven.txt"), + path!("b/eight.txt"), + ]; + + let mut opened_editors = Vec::new(); + for path in paths { + let buffer = workspace + .update_in(&mut cx, |workspace, window, cx| { + workspace.open_path( + ProjectPath { + worktree_id, + path: Path::new(path).into(), + }, + None, + false, + window, + cx, + ) + }) + .await + .unwrap(); + opened_editors.push(buffer); + } + + let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx)); + let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + + let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| { + let workspace_handle = cx.weak_entity(); + let message_editor = cx.new(|cx| { + MessageEditor::new( + workspace_handle, + project.clone(), + thread_store.clone(), + text_thread_store.clone(), + EditorMode::AutoHeight { + max_lines: None, + min_lines: 1, + }, + window, + cx, + ) + }); + workspace.active_pane().update(cx, |pane, cx| { + pane.add_item( + Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))), + true, + true, + None, + window, + cx, + ); + }); + message_editor.read(cx).focus_handle(cx).focus(window); + let editor = message_editor.read(cx).editor().clone(); + (message_editor, editor) + }); + + cx.simulate_input("Lorem "); + + editor.update(&mut cx, |editor, cx| { + assert_eq!(editor.text(cx), "Lorem "); + assert!(!editor.has_visible_completions_menu()); + }); + + cx.simulate_input("@"); + + editor.update(&mut cx, |editor, cx| { + assert_eq!(editor.text(cx), "Lorem @"); + assert!(editor.has_visible_completions_menu()); + assert_eq!( + current_completion_labels(editor), + &[ + "eight.txt dir/b/", + "seven.txt dir/b/", + "six.txt dir/b/", + "five.txt dir/b/", + "Files & Directories", + "Symbols", + "Threads", + "Fetch" + ] + ); + }); + + // Select and confirm "File" + editor.update_in(&mut cx, |editor, window, cx| { + assert!(editor.has_visible_completions_menu()); + editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); + editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); + editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); + editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); + editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); + }); + + cx.run_until_parked(); + + editor.update(&mut cx, |editor, cx| { + assert_eq!(editor.text(cx), "Lorem @file "); + assert!(editor.has_visible_completions_menu()); + }); + + cx.simulate_input("one"); + + editor.update(&mut cx, |editor, cx| { + assert_eq!(editor.text(cx), "Lorem @file one"); + assert!(editor.has_visible_completions_menu()); + assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]); + }); + + editor.update_in(&mut cx, |editor, window, cx| { + assert!(editor.has_visible_completions_menu()); + editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); + }); + + editor.update(&mut cx, |editor, cx| { + assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) "); + assert!(!editor.has_visible_completions_menu()); + assert_eq!( + fold_ranges(editor, cx), + vec![Point::new(0, 6)..Point::new(0, 39)] + ); + }); + + let contents = message_editor + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( + project.clone(), + thread_store.clone(), + text_thread_store.clone(), + window, + cx, + ) + }) + .await + .unwrap() + .into_values() + .collect::>(); + + pretty_assertions::assert_eq!( + contents, + [Mention::Text { + content: "1".into(), + uri: "file:///dir/a/one.txt".parse().unwrap() + }] + ); + + cx.simulate_input(" "); + + editor.update(&mut cx, |editor, cx| { + assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) "); + assert!(!editor.has_visible_completions_menu()); + assert_eq!( + fold_ranges(editor, cx), + vec![Point::new(0, 6)..Point::new(0, 39)] + ); + }); + + cx.simulate_input("Ipsum "); + + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum ", + ); + assert!(!editor.has_visible_completions_menu()); + assert_eq!( + fold_ranges(editor, cx), + vec![Point::new(0, 6)..Point::new(0, 39)] + ); + }); + + cx.simulate_input("@file "); + + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum @file ", + ); + assert!(editor.has_visible_completions_menu()); + assert_eq!( + fold_ranges(editor, cx), + vec![Point::new(0, 6)..Point::new(0, 39)] + ); + }); + + editor.update_in(&mut cx, |editor, window, cx| { + editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); + }); + + cx.run_until_parked(); + + let contents = message_editor + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( + project.clone(), + thread_store.clone(), + text_thread_store.clone(), + window, + cx, + ) + }) + .await + .unwrap() + .into_values() + .collect::>(); + + assert_eq!(contents.len(), 2); + pretty_assertions::assert_eq!( + contents[1], + Mention::Text { + content: "8".to_string(), + uri: "file:///dir/b/eight.txt".parse().unwrap(), + } + ); + + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) " + ); + assert!(!editor.has_visible_completions_menu()); + assert_eq!( + fold_ranges(editor, cx), + vec![ + Point::new(0, 6)..Point::new(0, 39), + Point::new(0, 47)..Point::new(0, 84) + ] + ); + }); + + let plain_text_language = Arc::new(language::Language::new( + language::LanguageConfig { + name: "Plain Text".into(), + matcher: language::LanguageMatcher { + path_suffixes: vec!["txt".to_string()], + ..Default::default() + }, + ..Default::default() + }, + None, + )); + + // Register the language and fake LSP + let language_registry = project.read_with(&cx, |project, _| project.languages().clone()); + language_registry.add(plain_text_language); + + let mut fake_language_servers = language_registry.register_fake_lsp( + "Plain Text", + language::FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + workspace_symbol_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + }, + ); + + // Open the buffer to trigger LSP initialization + let buffer = project + .update(&mut cx, |project, cx| { + project.open_local_buffer(path!("/dir/a/one.txt"), cx) + }) + .await + .unwrap(); + + // Register the buffer with language servers + let _handle = project.update(&mut cx, |project, cx| { + project.register_buffer_with_language_servers(&buffer, cx) + }); + + cx.run_until_parked(); + + let fake_language_server = fake_language_servers.next().await.unwrap(); + fake_language_server.set_request_handler::( + |_, _| async move { + Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![ + #[allow(deprecated)] + lsp::SymbolInformation { + name: "MySymbol".into(), + location: lsp::Location { + uri: lsp::Url::from_file_path(path!("/dir/a/one.txt")).unwrap(), + range: lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 1), + ), + }, + kind: lsp::SymbolKind::CONSTANT, + tags: None, + container_name: None, + deprecated: None, + }, + ]))) + }, + ); + + cx.simulate_input("@symbol "); + + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) @symbol " + ); + assert!(editor.has_visible_completions_menu()); + assert_eq!( + current_completion_labels(editor), + &[ + "MySymbol", + ] + ); + }); + + editor.update_in(&mut cx, |editor, window, cx| { + editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); + }); + + let contents = message_editor + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( + project.clone(), + thread_store, + text_thread_store, + window, + cx, + ) + }) + .await + .unwrap() + .into_values() + .collect::>(); + + assert_eq!(contents.len(), 3); + pretty_assertions::assert_eq!( + contents[2], + Mention::Text { + content: "1".into(), + uri: "file:///dir/a/one.txt?symbol=MySymbol#L1:1" + .parse() + .unwrap(), + } + ); + + cx.run_until_parked(); + + editor.read_with(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) [@MySymbol](file:///dir/a/one.txt?symbol=MySymbol#L1:1) " + ); + }); + } + + fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec> { + let snapshot = editor.buffer().read(cx).snapshot(cx); + editor.display_map.update(cx, |display_map, cx| { + display_map + .snapshot(cx) + .folds_in_range(0..snapshot.len()) + .map(|fold| fold.range.to_point(&snapshot)) + .collect() + }) + } + + fn current_completion_labels(editor: &Editor) -> Vec { + let completions = editor.current_completions().expect("Missing completions"); + completions + .into_iter() + .map(|completion| completion.label.text.to_string()) + .collect::>() + } } From e664a9bc48dcc0e74d02772acd295ce6356e850b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Aug 2025 18:58:10 -0400 Subject: [PATCH 397/693] collab: Remove unused billing-related database code (#36282) This PR removes a bunch of unused database code related to billing, as we no longer need it. Release Notes: - N/A --- Cargo.lock | 1 - crates/collab/Cargo.toml | 1 - crates/collab/src/db.rs | 5 - crates/collab/src/db/ids.rs | 3 - crates/collab/src/db/queries.rs | 4 - .../src/db/queries/billing_customers.rs | 100 ----------- .../src/db/queries/billing_preferences.rs | 17 -- .../src/db/queries/billing_subscriptions.rs | 158 ----------------- .../src/db/queries/processed_stripe_events.rs | 69 -------- crates/collab/src/db/tables.rs | 4 - .../collab/src/db/tables/billing_customer.rs | 41 ----- .../src/db/tables/billing_preference.rs | 32 ---- .../src/db/tables/billing_subscription.rs | 161 ------------------ .../src/db/tables/processed_stripe_event.rs | 16 -- crates/collab/src/db/tables/user.rs | 8 - crates/collab/src/db/tests.rs | 1 - .../db/tests/processed_stripe_event_tests.rs | 38 ----- crates/collab/src/lib.rs | 17 -- crates/collab/src/llm/db.rs | 74 +------- crates/collab/src/llm/db/ids.rs | 11 -- crates/collab/src/llm/db/queries.rs | 5 - crates/collab/src/llm/db/queries/providers.rs | 134 --------------- .../src/llm/db/queries/subscription_usages.rs | 38 ----- crates/collab/src/llm/db/queries/usages.rs | 44 ----- crates/collab/src/llm/db/seed.rs | 45 ----- crates/collab/src/llm/db/tables.rs | 6 - crates/collab/src/llm/db/tables/model.rs | 48 ------ crates/collab/src/llm/db/tables/provider.rs | 25 --- .../src/llm/db/tables/subscription_usage.rs | 22 --- .../llm/db/tables/subscription_usage_meter.rs | 55 ------ crates/collab/src/llm/db/tables/usage.rs | 52 ------ .../collab/src/llm/db/tables/usage_measure.rs | 36 ---- crates/collab/src/llm/db/tests.rs | 107 ------------ .../collab/src/llm/db/tests/provider_tests.rs | 31 ---- crates/collab/src/main.rs | 10 -- crates/collab/src/tests/test_server.rs | 1 - 36 files changed, 1 insertion(+), 1419 deletions(-) delete mode 100644 crates/collab/src/db/queries/billing_customers.rs delete mode 100644 crates/collab/src/db/queries/billing_preferences.rs delete mode 100644 crates/collab/src/db/queries/billing_subscriptions.rs delete mode 100644 crates/collab/src/db/queries/processed_stripe_events.rs delete mode 100644 crates/collab/src/db/tables/billing_customer.rs delete mode 100644 crates/collab/src/db/tables/billing_preference.rs delete mode 100644 crates/collab/src/db/tables/billing_subscription.rs delete mode 100644 crates/collab/src/db/tables/processed_stripe_event.rs delete mode 100644 crates/collab/src/db/tests/processed_stripe_event_tests.rs delete mode 100644 crates/collab/src/llm/db/ids.rs delete mode 100644 crates/collab/src/llm/db/queries.rs delete mode 100644 crates/collab/src/llm/db/queries/providers.rs delete mode 100644 crates/collab/src/llm/db/queries/subscription_usages.rs delete mode 100644 crates/collab/src/llm/db/queries/usages.rs delete mode 100644 crates/collab/src/llm/db/seed.rs delete mode 100644 crates/collab/src/llm/db/tables.rs delete mode 100644 crates/collab/src/llm/db/tables/model.rs delete mode 100644 crates/collab/src/llm/db/tables/provider.rs delete mode 100644 crates/collab/src/llm/db/tables/subscription_usage.rs delete mode 100644 crates/collab/src/llm/db/tables/subscription_usage_meter.rs delete mode 100644 crates/collab/src/llm/db/tables/usage.rs delete mode 100644 crates/collab/src/llm/db/tables/usage_measure.rs delete mode 100644 crates/collab/src/llm/db/tests.rs delete mode 100644 crates/collab/src/llm/db/tests/provider_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 2be16cc22f..3d72eed42e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3270,7 +3270,6 @@ dependencies = [ "chrono", "client", "clock", - "cloud_llm_client", "collab_ui", "collections", "command_palette_hooks", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 6fc591be13..4fccd3be7f 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -29,7 +29,6 @@ axum-extra = { version = "0.4", features = ["erased-json"] } base64.workspace = true chrono.workspace = true clock.workspace = true -cloud_llm_client.workspace = true collections.workspace = true dashmap.workspace = true envy = "0.4.2" diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 2c22ca2069..774eec5d2c 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -41,12 +41,7 @@ use worktree_settings_file::LocalSettingsKind; pub use tests::TestDb; pub use ids::*; -pub use queries::billing_customers::{CreateBillingCustomerParams, UpdateBillingCustomerParams}; -pub use queries::billing_subscriptions::{ - CreateBillingSubscriptionParams, UpdateBillingSubscriptionParams, -}; pub use queries::contributors::ContributorSelector; -pub use queries::processed_stripe_events::CreateProcessedStripeEventParams; pub use sea_orm::ConnectOptions; pub use tables::user::Model as User; pub use tables::*; diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 2ba7ec1051..8f116cfd63 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -70,9 +70,6 @@ macro_rules! id_type { } id_type!(AccessTokenId); -id_type!(BillingCustomerId); -id_type!(BillingSubscriptionId); -id_type!(BillingPreferencesId); id_type!(BufferId); id_type!(ChannelBufferCollaboratorId); id_type!(ChannelChatParticipantId); diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs index 64b627e475..95e45dc004 100644 --- a/crates/collab/src/db/queries.rs +++ b/crates/collab/src/db/queries.rs @@ -1,9 +1,6 @@ use super::*; pub mod access_tokens; -pub mod billing_customers; -pub mod billing_preferences; -pub mod billing_subscriptions; pub mod buffers; pub mod channels; pub mod contacts; @@ -12,7 +9,6 @@ pub mod embeddings; pub mod extensions; pub mod messages; pub mod notifications; -pub mod processed_stripe_events; pub mod projects; pub mod rooms; pub mod servers; diff --git a/crates/collab/src/db/queries/billing_customers.rs b/crates/collab/src/db/queries/billing_customers.rs deleted file mode 100644 index ead9e6cd32..0000000000 --- a/crates/collab/src/db/queries/billing_customers.rs +++ /dev/null @@ -1,100 +0,0 @@ -use super::*; - -#[derive(Debug)] -pub struct CreateBillingCustomerParams { - pub user_id: UserId, - pub stripe_customer_id: String, -} - -#[derive(Debug, Default)] -pub struct UpdateBillingCustomerParams { - pub user_id: ActiveValue, - pub stripe_customer_id: ActiveValue, - pub has_overdue_invoices: ActiveValue, - pub trial_started_at: ActiveValue>, -} - -impl Database { - /// Creates a new billing customer. - pub async fn create_billing_customer( - &self, - params: &CreateBillingCustomerParams, - ) -> Result { - self.transaction(|tx| async move { - let customer = billing_customer::Entity::insert(billing_customer::ActiveModel { - user_id: ActiveValue::set(params.user_id), - stripe_customer_id: ActiveValue::set(params.stripe_customer_id.clone()), - ..Default::default() - }) - .exec_with_returning(&*tx) - .await?; - - Ok(customer) - }) - .await - } - - /// Updates the specified billing customer. - pub async fn update_billing_customer( - &self, - id: BillingCustomerId, - params: &UpdateBillingCustomerParams, - ) -> Result<()> { - self.transaction(|tx| async move { - billing_customer::Entity::update(billing_customer::ActiveModel { - id: ActiveValue::set(id), - user_id: params.user_id.clone(), - stripe_customer_id: params.stripe_customer_id.clone(), - has_overdue_invoices: params.has_overdue_invoices.clone(), - trial_started_at: params.trial_started_at.clone(), - created_at: ActiveValue::not_set(), - }) - .exec(&*tx) - .await?; - - Ok(()) - }) - .await - } - - pub async fn get_billing_customer_by_id( - &self, - id: BillingCustomerId, - ) -> Result> { - self.transaction(|tx| async move { - Ok(billing_customer::Entity::find() - .filter(billing_customer::Column::Id.eq(id)) - .one(&*tx) - .await?) - }) - .await - } - - /// Returns the billing customer for the user with the specified ID. - pub async fn get_billing_customer_by_user_id( - &self, - user_id: UserId, - ) -> Result> { - self.transaction(|tx| async move { - Ok(billing_customer::Entity::find() - .filter(billing_customer::Column::UserId.eq(user_id)) - .one(&*tx) - .await?) - }) - .await - } - - /// Returns the billing customer for the user with the specified Stripe customer ID. - pub async fn get_billing_customer_by_stripe_customer_id( - &self, - stripe_customer_id: &str, - ) -> Result> { - self.transaction(|tx| async move { - Ok(billing_customer::Entity::find() - .filter(billing_customer::Column::StripeCustomerId.eq(stripe_customer_id)) - .one(&*tx) - .await?) - }) - .await - } -} diff --git a/crates/collab/src/db/queries/billing_preferences.rs b/crates/collab/src/db/queries/billing_preferences.rs deleted file mode 100644 index f370964ecd..0000000000 --- a/crates/collab/src/db/queries/billing_preferences.rs +++ /dev/null @@ -1,17 +0,0 @@ -use super::*; - -impl Database { - /// Returns the billing preferences for the given user, if they exist. - pub async fn get_billing_preferences( - &self, - user_id: UserId, - ) -> Result> { - self.transaction(|tx| async move { - Ok(billing_preference::Entity::find() - .filter(billing_preference::Column::UserId.eq(user_id)) - .one(&*tx) - .await?) - }) - .await - } -} diff --git a/crates/collab/src/db/queries/billing_subscriptions.rs b/crates/collab/src/db/queries/billing_subscriptions.rs deleted file mode 100644 index 8361d6b4d0..0000000000 --- a/crates/collab/src/db/queries/billing_subscriptions.rs +++ /dev/null @@ -1,158 +0,0 @@ -use anyhow::Context as _; - -use crate::db::billing_subscription::{ - StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind, -}; - -use super::*; - -#[derive(Debug)] -pub struct CreateBillingSubscriptionParams { - pub billing_customer_id: BillingCustomerId, - pub kind: Option, - pub stripe_subscription_id: String, - pub stripe_subscription_status: StripeSubscriptionStatus, - pub stripe_cancellation_reason: Option, - pub stripe_current_period_start: Option, - pub stripe_current_period_end: Option, -} - -#[derive(Debug, Default)] -pub struct UpdateBillingSubscriptionParams { - pub billing_customer_id: ActiveValue, - pub kind: ActiveValue>, - pub stripe_subscription_id: ActiveValue, - pub stripe_subscription_status: ActiveValue, - pub stripe_cancel_at: ActiveValue>, - pub stripe_cancellation_reason: ActiveValue>, - pub stripe_current_period_start: ActiveValue>, - pub stripe_current_period_end: ActiveValue>, -} - -impl Database { - /// Creates a new billing subscription. - pub async fn create_billing_subscription( - &self, - params: &CreateBillingSubscriptionParams, - ) -> Result { - self.transaction(|tx| async move { - let id = billing_subscription::Entity::insert(billing_subscription::ActiveModel { - billing_customer_id: ActiveValue::set(params.billing_customer_id), - kind: ActiveValue::set(params.kind), - stripe_subscription_id: ActiveValue::set(params.stripe_subscription_id.clone()), - stripe_subscription_status: ActiveValue::set(params.stripe_subscription_status), - stripe_cancellation_reason: ActiveValue::set(params.stripe_cancellation_reason), - stripe_current_period_start: ActiveValue::set(params.stripe_current_period_start), - stripe_current_period_end: ActiveValue::set(params.stripe_current_period_end), - ..Default::default() - }) - .exec(&*tx) - .await? - .last_insert_id; - - Ok(billing_subscription::Entity::find_by_id(id) - .one(&*tx) - .await? - .context("failed to retrieve inserted billing subscription")?) - }) - .await - } - - /// Updates the specified billing subscription. - pub async fn update_billing_subscription( - &self, - id: BillingSubscriptionId, - params: &UpdateBillingSubscriptionParams, - ) -> Result<()> { - self.transaction(|tx| async move { - billing_subscription::Entity::update(billing_subscription::ActiveModel { - id: ActiveValue::set(id), - billing_customer_id: params.billing_customer_id.clone(), - kind: params.kind.clone(), - stripe_subscription_id: params.stripe_subscription_id.clone(), - stripe_subscription_status: params.stripe_subscription_status.clone(), - stripe_cancel_at: params.stripe_cancel_at.clone(), - stripe_cancellation_reason: params.stripe_cancellation_reason.clone(), - stripe_current_period_start: params.stripe_current_period_start.clone(), - stripe_current_period_end: params.stripe_current_period_end.clone(), - created_at: ActiveValue::not_set(), - }) - .exec(&*tx) - .await?; - - Ok(()) - }) - .await - } - - /// Returns the billing subscription with the specified Stripe subscription ID. - pub async fn get_billing_subscription_by_stripe_subscription_id( - &self, - stripe_subscription_id: &str, - ) -> Result> { - self.transaction(|tx| async move { - Ok(billing_subscription::Entity::find() - .filter( - billing_subscription::Column::StripeSubscriptionId.eq(stripe_subscription_id), - ) - .one(&*tx) - .await?) - }) - .await - } - - pub async fn get_active_billing_subscription( - &self, - user_id: UserId, - ) -> Result> { - self.transaction(|tx| async move { - Ok(billing_subscription::Entity::find() - .inner_join(billing_customer::Entity) - .filter(billing_customer::Column::UserId.eq(user_id)) - .filter( - Condition::all() - .add( - Condition::any() - .add( - billing_subscription::Column::StripeSubscriptionStatus - .eq(StripeSubscriptionStatus::Active), - ) - .add( - billing_subscription::Column::StripeSubscriptionStatus - .eq(StripeSubscriptionStatus::Trialing), - ), - ) - .add(billing_subscription::Column::Kind.is_not_null()), - ) - .one(&*tx) - .await?) - }) - .await - } - - /// Returns whether the user has an active billing subscription. - pub async fn has_active_billing_subscription(&self, user_id: UserId) -> Result { - Ok(self.count_active_billing_subscriptions(user_id).await? > 0) - } - - /// Returns the count of the active billing subscriptions for the user with the specified ID. - pub async fn count_active_billing_subscriptions(&self, user_id: UserId) -> Result { - self.transaction(|tx| async move { - let count = billing_subscription::Entity::find() - .inner_join(billing_customer::Entity) - .filter( - billing_customer::Column::UserId.eq(user_id).and( - billing_subscription::Column::StripeSubscriptionStatus - .eq(StripeSubscriptionStatus::Active) - .or(billing_subscription::Column::StripeSubscriptionStatus - .eq(StripeSubscriptionStatus::Trialing)), - ), - ) - .count(&*tx) - .await?; - - Ok(count as usize) - }) - .await - } -} diff --git a/crates/collab/src/db/queries/processed_stripe_events.rs b/crates/collab/src/db/queries/processed_stripe_events.rs deleted file mode 100644 index f14ad480e0..0000000000 --- a/crates/collab/src/db/queries/processed_stripe_events.rs +++ /dev/null @@ -1,69 +0,0 @@ -use super::*; - -#[derive(Debug)] -pub struct CreateProcessedStripeEventParams { - pub stripe_event_id: String, - pub stripe_event_type: String, - pub stripe_event_created_timestamp: i64, -} - -impl Database { - /// Creates a new processed Stripe event. - pub async fn create_processed_stripe_event( - &self, - params: &CreateProcessedStripeEventParams, - ) -> Result<()> { - self.transaction(|tx| async move { - processed_stripe_event::Entity::insert(processed_stripe_event::ActiveModel { - stripe_event_id: ActiveValue::set(params.stripe_event_id.clone()), - stripe_event_type: ActiveValue::set(params.stripe_event_type.clone()), - stripe_event_created_timestamp: ActiveValue::set( - params.stripe_event_created_timestamp, - ), - ..Default::default() - }) - .exec_without_returning(&*tx) - .await?; - - Ok(()) - }) - .await - } - - /// Returns the processed Stripe event with the specified event ID. - pub async fn get_processed_stripe_event_by_event_id( - &self, - event_id: &str, - ) -> Result> { - self.transaction(|tx| async move { - Ok(processed_stripe_event::Entity::find_by_id(event_id) - .one(&*tx) - .await?) - }) - .await - } - - /// Returns the processed Stripe events with the specified event IDs. - pub async fn get_processed_stripe_events_by_event_ids( - &self, - event_ids: &[&str], - ) -> Result> { - self.transaction(|tx| async move { - Ok(processed_stripe_event::Entity::find() - .filter( - processed_stripe_event::Column::StripeEventId.is_in(event_ids.iter().copied()), - ) - .all(&*tx) - .await?) - }) - .await - } - - /// Returns whether the Stripe event with the specified ID has already been processed. - pub async fn already_processed_stripe_event(&self, event_id: &str) -> Result { - Ok(self - .get_processed_stripe_event_by_event_id(event_id) - .await? - .is_some()) - } -} diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index d87ab174bd..0082a9fb03 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -1,7 +1,4 @@ pub mod access_token; -pub mod billing_customer; -pub mod billing_preference; -pub mod billing_subscription; pub mod buffer; pub mod buffer_operation; pub mod buffer_snapshot; @@ -23,7 +20,6 @@ pub mod notification; pub mod notification_kind; pub mod observed_buffer_edits; pub mod observed_channel_messages; -pub mod processed_stripe_event; pub mod project; pub mod project_collaborator; pub mod project_repository; diff --git a/crates/collab/src/db/tables/billing_customer.rs b/crates/collab/src/db/tables/billing_customer.rs deleted file mode 100644 index e7d4a216e3..0000000000 --- a/crates/collab/src/db/tables/billing_customer.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::db::{BillingCustomerId, UserId}; -use sea_orm::entity::prelude::*; - -/// A billing customer. -#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "billing_customers")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: BillingCustomerId, - pub user_id: UserId, - pub stripe_customer_id: String, - pub has_overdue_invoices: bool, - pub trial_started_at: Option, - pub created_at: DateTime, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::UserId", - to = "super::user::Column::Id" - )] - User, - #[sea_orm(has_many = "super::billing_subscription::Entity")] - BillingSubscription, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::User.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::BillingSubscription.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/billing_preference.rs b/crates/collab/src/db/tables/billing_preference.rs deleted file mode 100644 index c1888d3b2f..0000000000 --- a/crates/collab/src/db/tables/billing_preference.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::db::{BillingPreferencesId, UserId}; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "billing_preferences")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: BillingPreferencesId, - pub created_at: DateTime, - pub user_id: UserId, - pub max_monthly_llm_usage_spending_in_cents: i32, - pub model_request_overages_enabled: bool, - pub model_request_overages_spend_limit_in_cents: i32, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::UserId", - to = "super::user::Column::Id" - )] - User, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::User.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/billing_subscription.rs b/crates/collab/src/db/tables/billing_subscription.rs deleted file mode 100644 index f5684aeec3..0000000000 --- a/crates/collab/src/db/tables/billing_subscription.rs +++ /dev/null @@ -1,161 +0,0 @@ -use crate::db::{BillingCustomerId, BillingSubscriptionId}; -use chrono::{Datelike as _, NaiveDate, Utc}; -use sea_orm::entity::prelude::*; -use serde::Serialize; - -/// A billing subscription. -#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "billing_subscriptions")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: BillingSubscriptionId, - pub billing_customer_id: BillingCustomerId, - pub kind: Option, - pub stripe_subscription_id: String, - pub stripe_subscription_status: StripeSubscriptionStatus, - pub stripe_cancel_at: Option, - pub stripe_cancellation_reason: Option, - pub stripe_current_period_start: Option, - pub stripe_current_period_end: Option, - pub created_at: DateTime, -} - -impl Model { - pub fn current_period_start_at(&self) -> Option { - let period_start = self.stripe_current_period_start?; - chrono::DateTime::from_timestamp(period_start, 0) - } - - pub fn current_period_end_at(&self) -> Option { - let period_end = self.stripe_current_period_end?; - chrono::DateTime::from_timestamp(period_end, 0) - } - - pub fn current_period( - subscription: Option, - is_staff: bool, - ) -> Option<(DateTimeUtc, DateTimeUtc)> { - if is_staff { - let now = Utc::now(); - let year = now.year(); - let month = now.month(); - - let first_day_of_this_month = - NaiveDate::from_ymd_opt(year, month, 1)?.and_hms_opt(0, 0, 0)?; - - let next_month = if month == 12 { 1 } else { month + 1 }; - let next_month_year = if month == 12 { year + 1 } else { year }; - let first_day_of_next_month = - NaiveDate::from_ymd_opt(next_month_year, next_month, 1)?.and_hms_opt(23, 59, 59)?; - - let last_day_of_this_month = first_day_of_next_month - chrono::Days::new(1); - - Some(( - first_day_of_this_month.and_utc(), - last_day_of_this_month.and_utc(), - )) - } else { - let subscription = subscription?; - let period_start_at = subscription.current_period_start_at()?; - let period_end_at = subscription.current_period_end_at()?; - - Some((period_start_at, period_end_at)) - } - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::billing_customer::Entity", - from = "Column::BillingCustomerId", - to = "super::billing_customer::Column::Id" - )] - BillingCustomer, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::BillingCustomer.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} - -#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Hash, Serialize)] -#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")] -#[serde(rename_all = "snake_case")] -pub enum SubscriptionKind { - #[sea_orm(string_value = "zed_pro")] - ZedPro, - #[sea_orm(string_value = "zed_pro_trial")] - ZedProTrial, - #[sea_orm(string_value = "zed_free")] - ZedFree, -} - -impl From for cloud_llm_client::Plan { - fn from(value: SubscriptionKind) -> Self { - match value { - SubscriptionKind::ZedPro => Self::ZedPro, - SubscriptionKind::ZedProTrial => Self::ZedProTrial, - SubscriptionKind::ZedFree => Self::ZedFree, - } - } -} - -/// The status of a Stripe subscription. -/// -/// [Stripe docs](https://docs.stripe.com/api/subscriptions/object#subscription_object-status) -#[derive( - Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash, Serialize, -)] -#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")] -#[serde(rename_all = "snake_case")] -pub enum StripeSubscriptionStatus { - #[default] - #[sea_orm(string_value = "incomplete")] - Incomplete, - #[sea_orm(string_value = "incomplete_expired")] - IncompleteExpired, - #[sea_orm(string_value = "trialing")] - Trialing, - #[sea_orm(string_value = "active")] - Active, - #[sea_orm(string_value = "past_due")] - PastDue, - #[sea_orm(string_value = "canceled")] - Canceled, - #[sea_orm(string_value = "unpaid")] - Unpaid, - #[sea_orm(string_value = "paused")] - Paused, -} - -impl StripeSubscriptionStatus { - pub fn is_cancelable(&self) -> bool { - match self { - Self::Trialing | Self::Active | Self::PastDue => true, - Self::Incomplete - | Self::IncompleteExpired - | Self::Canceled - | Self::Unpaid - | Self::Paused => false, - } - } -} - -/// The cancellation reason for a Stripe subscription. -/// -/// [Stripe docs](https://docs.stripe.com/api/subscriptions/object#subscription_object-cancellation_details-reason) -#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Hash, Serialize)] -#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")] -#[serde(rename_all = "snake_case")] -pub enum StripeCancellationReason { - #[sea_orm(string_value = "cancellation_requested")] - CancellationRequested, - #[sea_orm(string_value = "payment_disputed")] - PaymentDisputed, - #[sea_orm(string_value = "payment_failed")] - PaymentFailed, -} diff --git a/crates/collab/src/db/tables/processed_stripe_event.rs b/crates/collab/src/db/tables/processed_stripe_event.rs deleted file mode 100644 index 7b6f0cdc31..0000000000 --- a/crates/collab/src/db/tables/processed_stripe_event.rs +++ /dev/null @@ -1,16 +0,0 @@ -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "processed_stripe_events")] -pub struct Model { - #[sea_orm(primary_key)] - pub stripe_event_id: String, - pub stripe_event_type: String, - pub stripe_event_created_timestamp: i64, - pub processed_at: DateTime, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/user.rs b/crates/collab/src/db/tables/user.rs index 49fe3eb58f..af43fe300a 100644 --- a/crates/collab/src/db/tables/user.rs +++ b/crates/collab/src/db/tables/user.rs @@ -29,8 +29,6 @@ pub struct Model { pub enum Relation { #[sea_orm(has_many = "super::access_token::Entity")] AccessToken, - #[sea_orm(has_one = "super::billing_customer::Entity")] - BillingCustomer, #[sea_orm(has_one = "super::room_participant::Entity")] RoomParticipant, #[sea_orm(has_many = "super::project::Entity")] @@ -68,12 +66,6 @@ impl Related for Entity { } } -impl Related for Entity { - fn to() -> RelationDef { - Relation::BillingCustomer.def() - } -} - impl Related for Entity { fn to() -> RelationDef { Relation::RoomParticipant.def() diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 6c2f9dc82a..2eb8d377ac 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -8,7 +8,6 @@ mod embedding_tests; mod extension_tests; mod feature_flag_tests; mod message_tests; -mod processed_stripe_event_tests; mod user_tests; use crate::migrations::run_database_migrations; diff --git a/crates/collab/src/db/tests/processed_stripe_event_tests.rs b/crates/collab/src/db/tests/processed_stripe_event_tests.rs deleted file mode 100644 index ad93b5a658..0000000000 --- a/crates/collab/src/db/tests/processed_stripe_event_tests.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::sync::Arc; - -use crate::test_both_dbs; - -use super::{CreateProcessedStripeEventParams, Database}; - -test_both_dbs!( - test_already_processed_stripe_event, - test_already_processed_stripe_event_postgres, - test_already_processed_stripe_event_sqlite -); - -async fn test_already_processed_stripe_event(db: &Arc) { - let unprocessed_event_id = "evt_1PiJOuRxOf7d5PNaw2zzWiyO".to_string(); - let processed_event_id = "evt_1PiIfMRxOf7d5PNakHrAUe8P".to_string(); - - db.create_processed_stripe_event(&CreateProcessedStripeEventParams { - stripe_event_id: processed_event_id.clone(), - stripe_event_type: "customer.created".into(), - stripe_event_created_timestamp: 1722355968, - }) - .await - .unwrap(); - - assert!( - db.already_processed_stripe_event(&processed_event_id) - .await - .unwrap(), - "Expected {processed_event_id} to already be processed" - ); - - assert!( - !db.already_processed_stripe_event(&unprocessed_event_id) - .await - .unwrap(), - "Expected {unprocessed_event_id} to be unprocessed" - ); -} diff --git a/crates/collab/src/lib.rs b/crates/collab/src/lib.rs index a68286a5a3..191025df37 100644 --- a/crates/collab/src/lib.rs +++ b/crates/collab/src/lib.rs @@ -20,7 +20,6 @@ use axum::{ }; use db::{ChannelId, Database}; use executor::Executor; -use llm::db::LlmDatabase; use serde::Deserialize; use std::{path::PathBuf, sync::Arc}; use util::ResultExt; @@ -242,7 +241,6 @@ impl ServiceMode { pub struct AppState { pub db: Arc, - pub llm_db: Option>, pub livekit_client: Option>, pub blob_store_client: Option, pub executor: Executor, @@ -257,20 +255,6 @@ impl AppState { let mut db = Database::new(db_options).await?; db.initialize_notification_kinds().await?; - let llm_db = if let Some((llm_database_url, llm_database_max_connections)) = config - .llm_database_url - .clone() - .zip(config.llm_database_max_connections) - { - let mut llm_db_options = db::ConnectOptions::new(llm_database_url); - llm_db_options.max_connections(llm_database_max_connections); - let mut llm_db = LlmDatabase::new(llm_db_options, executor.clone()).await?; - llm_db.initialize().await?; - Some(Arc::new(llm_db)) - } else { - None - }; - let livekit_client = if let Some(((server, key), secret)) = config .livekit_server .as_ref() @@ -289,7 +273,6 @@ impl AppState { let db = Arc::new(db); let this = Self { db: db.clone(), - llm_db, livekit_client, blob_store_client: build_blob_store_client(&config).await.log_err(), executor, diff --git a/crates/collab/src/llm/db.rs b/crates/collab/src/llm/db.rs index 18ad624dab..b15d5a42b5 100644 --- a/crates/collab/src/llm/db.rs +++ b/crates/collab/src/llm/db.rs @@ -1,30 +1,9 @@ -mod ids; -mod queries; -mod seed; -mod tables; - -#[cfg(test)] -mod tests; - -use cloud_llm_client::LanguageModelProvider; -use collections::HashMap; -pub use ids::*; -pub use seed::*; -pub use tables::*; - -#[cfg(test)] -pub use tests::TestLlmDb; -use usage_measure::UsageMeasure; - use std::future::Future; use std::sync::Arc; use anyhow::Context; pub use sea_orm::ConnectOptions; -use sea_orm::prelude::*; -use sea_orm::{ - ActiveValue, DatabaseConnection, DatabaseTransaction, IsolationLevel, TransactionTrait, -}; +use sea_orm::{DatabaseConnection, DatabaseTransaction, IsolationLevel, TransactionTrait}; use crate::Result; use crate::db::TransactionHandle; @@ -36,9 +15,6 @@ pub struct LlmDatabase { pool: DatabaseConnection, #[allow(unused)] executor: Executor, - provider_ids: HashMap, - models: HashMap<(LanguageModelProvider, String), model::Model>, - usage_measure_ids: HashMap, #[cfg(test)] runtime: Option, } @@ -51,59 +27,11 @@ impl LlmDatabase { options: options.clone(), pool: sea_orm::Database::connect(options).await?, executor, - provider_ids: HashMap::default(), - models: HashMap::default(), - usage_measure_ids: HashMap::default(), #[cfg(test)] runtime: None, }) } - pub async fn initialize(&mut self) -> Result<()> { - self.initialize_providers().await?; - self.initialize_models().await?; - self.initialize_usage_measures().await?; - Ok(()) - } - - /// Returns the list of all known models, with their [`LanguageModelProvider`]. - pub fn all_models(&self) -> Vec<(LanguageModelProvider, model::Model)> { - self.models - .iter() - .map(|((model_provider, _model_name), model)| (*model_provider, model.clone())) - .collect::>() - } - - /// Returns the names of the known models for the given [`LanguageModelProvider`]. - pub fn model_names_for_provider(&self, provider: LanguageModelProvider) -> Vec { - self.models - .keys() - .filter_map(|(model_provider, model_name)| { - if model_provider == &provider { - Some(model_name) - } else { - None - } - }) - .cloned() - .collect::>() - } - - pub fn model(&self, provider: LanguageModelProvider, name: &str) -> Result<&model::Model> { - Ok(self - .models - .get(&(provider, name.to_string())) - .with_context(|| format!("unknown model {provider:?}:{name}"))?) - } - - pub fn model_by_id(&self, id: ModelId) -> Result<&model::Model> { - Ok(self - .models - .values() - .find(|model| model.id == id) - .with_context(|| format!("no model for ID {id:?}"))?) - } - pub fn options(&self) -> &ConnectOptions { &self.options } diff --git a/crates/collab/src/llm/db/ids.rs b/crates/collab/src/llm/db/ids.rs deleted file mode 100644 index 03cab6cee0..0000000000 --- a/crates/collab/src/llm/db/ids.rs +++ /dev/null @@ -1,11 +0,0 @@ -use sea_orm::{DbErr, entity::prelude::*}; -use serde::{Deserialize, Serialize}; - -use crate::id_type; - -id_type!(BillingEventId); -id_type!(ModelId); -id_type!(ProviderId); -id_type!(RevokedAccessTokenId); -id_type!(UsageId); -id_type!(UsageMeasureId); diff --git a/crates/collab/src/llm/db/queries.rs b/crates/collab/src/llm/db/queries.rs deleted file mode 100644 index 0087218b3f..0000000000 --- a/crates/collab/src/llm/db/queries.rs +++ /dev/null @@ -1,5 +0,0 @@ -use super::*; - -pub mod providers; -pub mod subscription_usages; -pub mod usages; diff --git a/crates/collab/src/llm/db/queries/providers.rs b/crates/collab/src/llm/db/queries/providers.rs deleted file mode 100644 index 9c7dbdd184..0000000000 --- a/crates/collab/src/llm/db/queries/providers.rs +++ /dev/null @@ -1,134 +0,0 @@ -use super::*; -use sea_orm::{QueryOrder, sea_query::OnConflict}; -use std::str::FromStr; -use strum::IntoEnumIterator as _; - -pub struct ModelParams { - pub provider: LanguageModelProvider, - pub name: String, - pub max_requests_per_minute: i64, - pub max_tokens_per_minute: i64, - pub max_tokens_per_day: i64, - pub price_per_million_input_tokens: i32, - pub price_per_million_output_tokens: i32, -} - -impl LlmDatabase { - pub async fn initialize_providers(&mut self) -> Result<()> { - self.provider_ids = self - .transaction(|tx| async move { - let existing_providers = provider::Entity::find().all(&*tx).await?; - - let mut new_providers = LanguageModelProvider::iter() - .filter(|provider| { - !existing_providers - .iter() - .any(|p| p.name == provider.to_string()) - }) - .map(|provider| provider::ActiveModel { - name: ActiveValue::set(provider.to_string()), - ..Default::default() - }) - .peekable(); - - if new_providers.peek().is_some() { - provider::Entity::insert_many(new_providers) - .exec(&*tx) - .await?; - } - - let all_providers: HashMap<_, _> = provider::Entity::find() - .all(&*tx) - .await? - .iter() - .filter_map(|provider| { - LanguageModelProvider::from_str(&provider.name) - .ok() - .map(|p| (p, provider.id)) - }) - .collect(); - - Ok(all_providers) - }) - .await?; - Ok(()) - } - - pub async fn initialize_models(&mut self) -> Result<()> { - let all_provider_ids = &self.provider_ids; - self.models = self - .transaction(|tx| async move { - let all_models: HashMap<_, _> = model::Entity::find() - .all(&*tx) - .await? - .into_iter() - .filter_map(|model| { - let provider = all_provider_ids.iter().find_map(|(provider, id)| { - if *id == model.provider_id { - Some(provider) - } else { - None - } - })?; - Some(((*provider, model.name.clone()), model)) - }) - .collect(); - Ok(all_models) - }) - .await?; - Ok(()) - } - - pub async fn insert_models(&mut self, models: &[ModelParams]) -> Result<()> { - let all_provider_ids = &self.provider_ids; - self.transaction(|tx| async move { - model::Entity::insert_many(models.iter().map(|model_params| { - let provider_id = all_provider_ids[&model_params.provider]; - model::ActiveModel { - provider_id: ActiveValue::set(provider_id), - name: ActiveValue::set(model_params.name.clone()), - max_requests_per_minute: ActiveValue::set(model_params.max_requests_per_minute), - max_tokens_per_minute: ActiveValue::set(model_params.max_tokens_per_minute), - max_tokens_per_day: ActiveValue::set(model_params.max_tokens_per_day), - price_per_million_input_tokens: ActiveValue::set( - model_params.price_per_million_input_tokens, - ), - price_per_million_output_tokens: ActiveValue::set( - model_params.price_per_million_output_tokens, - ), - ..Default::default() - } - })) - .on_conflict( - OnConflict::columns([model::Column::ProviderId, model::Column::Name]) - .update_columns([ - model::Column::MaxRequestsPerMinute, - model::Column::MaxTokensPerMinute, - model::Column::MaxTokensPerDay, - model::Column::PricePerMillionInputTokens, - model::Column::PricePerMillionOutputTokens, - ]) - .to_owned(), - ) - .exec_without_returning(&*tx) - .await?; - Ok(()) - }) - .await?; - self.initialize_models().await - } - - /// Returns the list of LLM providers. - pub async fn list_providers(&self) -> Result> { - self.transaction(|tx| async move { - Ok(provider::Entity::find() - .order_by_asc(provider::Column::Name) - .all(&*tx) - .await? - .into_iter() - .filter_map(|p| LanguageModelProvider::from_str(&p.name).ok()) - .collect()) - }) - .await - } -} diff --git a/crates/collab/src/llm/db/queries/subscription_usages.rs b/crates/collab/src/llm/db/queries/subscription_usages.rs deleted file mode 100644 index 8a51979075..0000000000 --- a/crates/collab/src/llm/db/queries/subscription_usages.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::db::UserId; - -use super::*; - -impl LlmDatabase { - pub async fn get_subscription_usage_for_period( - &self, - user_id: UserId, - period_start_at: DateTimeUtc, - period_end_at: DateTimeUtc, - ) -> Result> { - self.transaction(|tx| async move { - self.get_subscription_usage_for_period_in_tx( - user_id, - period_start_at, - period_end_at, - &tx, - ) - .await - }) - .await - } - - async fn get_subscription_usage_for_period_in_tx( - &self, - user_id: UserId, - period_start_at: DateTimeUtc, - period_end_at: DateTimeUtc, - tx: &DatabaseTransaction, - ) -> Result> { - Ok(subscription_usage::Entity::find() - .filter(subscription_usage::Column::UserId.eq(user_id)) - .filter(subscription_usage::Column::PeriodStartAt.eq(period_start_at)) - .filter(subscription_usage::Column::PeriodEndAt.eq(period_end_at)) - .one(tx) - .await?) - } -} diff --git a/crates/collab/src/llm/db/queries/usages.rs b/crates/collab/src/llm/db/queries/usages.rs deleted file mode 100644 index a917703f96..0000000000 --- a/crates/collab/src/llm/db/queries/usages.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::str::FromStr; -use strum::IntoEnumIterator as _; - -use super::*; - -impl LlmDatabase { - pub async fn initialize_usage_measures(&mut self) -> Result<()> { - let all_measures = self - .transaction(|tx| async move { - let existing_measures = usage_measure::Entity::find().all(&*tx).await?; - - let new_measures = UsageMeasure::iter() - .filter(|measure| { - !existing_measures - .iter() - .any(|m| m.name == measure.to_string()) - }) - .map(|measure| usage_measure::ActiveModel { - name: ActiveValue::set(measure.to_string()), - ..Default::default() - }) - .collect::>(); - - if !new_measures.is_empty() { - usage_measure::Entity::insert_many(new_measures) - .exec(&*tx) - .await?; - } - - Ok(usage_measure::Entity::find().all(&*tx).await?) - }) - .await?; - - self.usage_measure_ids = all_measures - .into_iter() - .filter_map(|measure| { - UsageMeasure::from_str(&measure.name) - .ok() - .map(|um| (um, measure.id)) - }) - .collect(); - Ok(()) - } -} diff --git a/crates/collab/src/llm/db/seed.rs b/crates/collab/src/llm/db/seed.rs deleted file mode 100644 index 55c6c30cd5..0000000000 --- a/crates/collab/src/llm/db/seed.rs +++ /dev/null @@ -1,45 +0,0 @@ -use super::*; -use crate::{Config, Result}; -use queries::providers::ModelParams; - -pub async fn seed_database(_config: &Config, db: &mut LlmDatabase, _force: bool) -> Result<()> { - db.insert_models(&[ - ModelParams { - provider: LanguageModelProvider::Anthropic, - name: "claude-3-5-sonnet".into(), - max_requests_per_minute: 5, - max_tokens_per_minute: 20_000, - max_tokens_per_day: 300_000, - price_per_million_input_tokens: 300, // $3.00/MTok - price_per_million_output_tokens: 1500, // $15.00/MTok - }, - ModelParams { - provider: LanguageModelProvider::Anthropic, - name: "claude-3-opus".into(), - max_requests_per_minute: 5, - max_tokens_per_minute: 10_000, - max_tokens_per_day: 300_000, - price_per_million_input_tokens: 1500, // $15.00/MTok - price_per_million_output_tokens: 7500, // $75.00/MTok - }, - ModelParams { - provider: LanguageModelProvider::Anthropic, - name: "claude-3-sonnet".into(), - max_requests_per_minute: 5, - max_tokens_per_minute: 20_000, - max_tokens_per_day: 300_000, - price_per_million_input_tokens: 1500, // $15.00/MTok - price_per_million_output_tokens: 7500, // $75.00/MTok - }, - ModelParams { - provider: LanguageModelProvider::Anthropic, - name: "claude-3-haiku".into(), - max_requests_per_minute: 5, - max_tokens_per_minute: 25_000, - max_tokens_per_day: 300_000, - price_per_million_input_tokens: 25, // $0.25/MTok - price_per_million_output_tokens: 125, // $1.25/MTok - }, - ]) - .await -} diff --git a/crates/collab/src/llm/db/tables.rs b/crates/collab/src/llm/db/tables.rs deleted file mode 100644 index 75ea8f5140..0000000000 --- a/crates/collab/src/llm/db/tables.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod model; -pub mod provider; -pub mod subscription_usage; -pub mod subscription_usage_meter; -pub mod usage; -pub mod usage_measure; diff --git a/crates/collab/src/llm/db/tables/model.rs b/crates/collab/src/llm/db/tables/model.rs deleted file mode 100644 index f0a858b4a6..0000000000 --- a/crates/collab/src/llm/db/tables/model.rs +++ /dev/null @@ -1,48 +0,0 @@ -use sea_orm::entity::prelude::*; - -use crate::llm::db::{ModelId, ProviderId}; - -/// An LLM model. -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "models")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: ModelId, - pub provider_id: ProviderId, - pub name: String, - pub max_requests_per_minute: i64, - pub max_tokens_per_minute: i64, - pub max_input_tokens_per_minute: i64, - pub max_output_tokens_per_minute: i64, - pub max_tokens_per_day: i64, - pub price_per_million_input_tokens: i32, - pub price_per_million_cache_creation_input_tokens: i32, - pub price_per_million_cache_read_input_tokens: i32, - pub price_per_million_output_tokens: i32, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::provider::Entity", - from = "Column::ProviderId", - to = "super::provider::Column::Id" - )] - Provider, - #[sea_orm(has_many = "super::usage::Entity")] - Usages, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Provider.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Usages.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/llm/db/tables/provider.rs b/crates/collab/src/llm/db/tables/provider.rs deleted file mode 100644 index 90838f7c65..0000000000 --- a/crates/collab/src/llm/db/tables/provider.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::llm::db::ProviderId; -use sea_orm::entity::prelude::*; - -/// An LLM provider. -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "providers")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: ProviderId, - pub name: String, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::model::Entity")] - Models, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Models.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/llm/db/tables/subscription_usage.rs b/crates/collab/src/llm/db/tables/subscription_usage.rs deleted file mode 100644 index dd93b03d05..0000000000 --- a/crates/collab/src/llm/db/tables/subscription_usage.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::db::UserId; -use crate::db::billing_subscription::SubscriptionKind; -use sea_orm::entity::prelude::*; -use time::PrimitiveDateTime; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "subscription_usages_v2")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: Uuid, - pub user_id: UserId, - pub period_start_at: PrimitiveDateTime, - pub period_end_at: PrimitiveDateTime, - pub plan: SubscriptionKind, - pub model_requests: i32, - pub edit_predictions: i32, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/llm/db/tables/subscription_usage_meter.rs b/crates/collab/src/llm/db/tables/subscription_usage_meter.rs deleted file mode 100644 index c082cf3bc1..0000000000 --- a/crates/collab/src/llm/db/tables/subscription_usage_meter.rs +++ /dev/null @@ -1,55 +0,0 @@ -use sea_orm::entity::prelude::*; -use serde::Serialize; - -use crate::llm::db::ModelId; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "subscription_usage_meters_v2")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: Uuid, - pub subscription_usage_id: Uuid, - pub model_id: ModelId, - pub mode: CompletionMode, - pub requests: i32, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::subscription_usage::Entity", - from = "Column::SubscriptionUsageId", - to = "super::subscription_usage::Column::Id" - )] - SubscriptionUsage, - #[sea_orm( - belongs_to = "super::model::Entity", - from = "Column::ModelId", - to = "super::model::Column::Id" - )] - Model, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::SubscriptionUsage.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Model.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} - -#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Hash, Serialize)] -#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")] -#[serde(rename_all = "snake_case")] -pub enum CompletionMode { - #[sea_orm(string_value = "normal")] - Normal, - #[sea_orm(string_value = "max")] - Max, -} diff --git a/crates/collab/src/llm/db/tables/usage.rs b/crates/collab/src/llm/db/tables/usage.rs deleted file mode 100644 index 331c94a8a9..0000000000 --- a/crates/collab/src/llm/db/tables/usage.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::{ - db::UserId, - llm::db::{ModelId, UsageId, UsageMeasureId}, -}; -use sea_orm::entity::prelude::*; - -/// An LLM usage record. -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "usages")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: UsageId, - /// The ID of the Zed user. - /// - /// Corresponds to the `users` table in the primary collab database. - pub user_id: UserId, - pub model_id: ModelId, - pub measure_id: UsageMeasureId, - pub timestamp: DateTime, - pub buckets: Vec, - pub is_staff: bool, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::model::Entity", - from = "Column::ModelId", - to = "super::model::Column::Id" - )] - Model, - #[sea_orm( - belongs_to = "super::usage_measure::Entity", - from = "Column::MeasureId", - to = "super::usage_measure::Column::Id" - )] - UsageMeasure, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Model.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::UsageMeasure.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/llm/db/tables/usage_measure.rs b/crates/collab/src/llm/db/tables/usage_measure.rs deleted file mode 100644 index 4f75577ed4..0000000000 --- a/crates/collab/src/llm/db/tables/usage_measure.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::llm::db::UsageMeasureId; -use sea_orm::entity::prelude::*; - -#[derive( - Copy, Clone, Debug, PartialEq, Eq, Hash, strum::EnumString, strum::Display, strum::EnumIter, -)] -#[strum(serialize_all = "snake_case")] -pub enum UsageMeasure { - RequestsPerMinute, - TokensPerMinute, - InputTokensPerMinute, - OutputTokensPerMinute, - TokensPerDay, -} - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "usage_measures")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: UsageMeasureId, - pub name: String, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::usage::Entity")] - Usages, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Usages.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/llm/db/tests.rs b/crates/collab/src/llm/db/tests.rs deleted file mode 100644 index 43a1b8b0d4..0000000000 --- a/crates/collab/src/llm/db/tests.rs +++ /dev/null @@ -1,107 +0,0 @@ -mod provider_tests; - -use gpui::BackgroundExecutor; -use parking_lot::Mutex; -use rand::prelude::*; -use sea_orm::ConnectionTrait; -use sqlx::migrate::MigrateDatabase; -use std::time::Duration; - -use crate::migrations::run_database_migrations; - -use super::*; - -pub struct TestLlmDb { - pub db: Option, - pub connection: Option, -} - -impl TestLlmDb { - pub fn postgres(background: BackgroundExecutor) -> Self { - static LOCK: Mutex<()> = Mutex::new(()); - - let _guard = LOCK.lock(); - let mut rng = StdRng::from_entropy(); - let url = format!( - "postgres://postgres@localhost/zed-llm-test-{}", - rng.r#gen::() - ); - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_io() - .enable_time() - .build() - .unwrap(); - - let mut db = runtime.block_on(async { - sqlx::Postgres::create_database(&url) - .await - .expect("failed to create test db"); - let mut options = ConnectOptions::new(url); - options - .max_connections(5) - .idle_timeout(Duration::from_secs(0)); - let db = LlmDatabase::new(options, Executor::Deterministic(background)) - .await - .unwrap(); - let migrations_path = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations_llm"); - run_database_migrations(db.options(), migrations_path) - .await - .unwrap(); - db - }); - - db.runtime = Some(runtime); - - Self { - db: Some(db), - connection: None, - } - } - - pub fn db(&mut self) -> &mut LlmDatabase { - self.db.as_mut().unwrap() - } -} - -#[macro_export] -macro_rules! test_llm_db { - ($test_name:ident, $postgres_test_name:ident) => { - #[gpui::test] - async fn $postgres_test_name(cx: &mut gpui::TestAppContext) { - if !cfg!(target_os = "macos") { - return; - } - - let mut test_db = $crate::llm::db::TestLlmDb::postgres(cx.executor().clone()); - $test_name(test_db.db()).await; - } - }; -} - -impl Drop for TestLlmDb { - fn drop(&mut self) { - let db = self.db.take().unwrap(); - if let sea_orm::DatabaseBackend::Postgres = db.pool.get_database_backend() { - db.runtime.as_ref().unwrap().block_on(async { - use util::ResultExt; - let query = " - SELECT pg_terminate_backend(pg_stat_activity.pid) - FROM pg_stat_activity - WHERE - pg_stat_activity.datname = current_database() AND - pid <> pg_backend_pid(); - "; - db.pool - .execute(sea_orm::Statement::from_string( - db.pool.get_database_backend(), - query, - )) - .await - .log_err(); - sqlx::Postgres::drop_database(db.options.get_url()) - .await - .log_err(); - }) - } - } -} diff --git a/crates/collab/src/llm/db/tests/provider_tests.rs b/crates/collab/src/llm/db/tests/provider_tests.rs deleted file mode 100644 index f4e1de40ec..0000000000 --- a/crates/collab/src/llm/db/tests/provider_tests.rs +++ /dev/null @@ -1,31 +0,0 @@ -use cloud_llm_client::LanguageModelProvider; -use pretty_assertions::assert_eq; - -use crate::llm::db::LlmDatabase; -use crate::test_llm_db; - -test_llm_db!( - test_initialize_providers, - test_initialize_providers_postgres -); - -async fn test_initialize_providers(db: &mut LlmDatabase) { - let initial_providers = db.list_providers().await.unwrap(); - assert_eq!(initial_providers, vec![]); - - db.initialize_providers().await.unwrap(); - - // Do it twice, to make sure the operation is idempotent. - db.initialize_providers().await.unwrap(); - - let providers = db.list_providers().await.unwrap(); - - assert_eq!( - providers, - &[ - LanguageModelProvider::Anthropic, - LanguageModelProvider::Google, - LanguageModelProvider::OpenAi, - ] - ) -} diff --git a/crates/collab/src/main.rs b/crates/collab/src/main.rs index 177c97f076..cb6f6cad1d 100644 --- a/crates/collab/src/main.rs +++ b/crates/collab/src/main.rs @@ -62,13 +62,6 @@ async fn main() -> Result<()> { db.initialize_notification_kinds().await?; collab::seed::seed(&config, &db, false).await?; - - if let Some(llm_database_url) = config.llm_database_url.clone() { - let db_options = db::ConnectOptions::new(llm_database_url); - let mut db = LlmDatabase::new(db_options.clone(), Executor::Production).await?; - db.initialize().await?; - collab::llm::db::seed_database(&config, &mut db, true).await?; - } } Some("serve") => { let mode = match args.next().as_deref() { @@ -263,9 +256,6 @@ async fn setup_llm_database(config: &Config) -> Result<()> { .llm_database_migrations_path .as_deref() .unwrap_or_else(|| { - #[cfg(feature = "sqlite")] - let default_migrations = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations_llm.sqlite"); - #[cfg(not(feature = "sqlite"))] let default_migrations = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations_llm"); Path::new(default_migrations) diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 8c545b0670..07ea1efc9d 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -565,7 +565,6 @@ impl TestServer { ) -> Arc { Arc::new(AppState { db: test_db.db().clone(), - llm_db: None, livekit_client: Some(Arc::new(livekit_test_server.create_api_client())), blob_store_client: None, executor, From f5f14111ef3203a7e28df531b808f47c2a6a79f0 Mon Sep 17 00:00:00 2001 From: zumbalogy Date: Sat, 16 Aug 2025 08:19:38 +0200 Subject: [PATCH 398/693] Add setting for hiding the status_bar.cursor_position_button (#36288) Release Notes: - Added an option for the status_bar.cursor_position_button. Setting to `false` will hide the button. It defaults to `true`. This builds off the recent work to hide the language selection button (https://github.com/zed-industries/zed/pull/33977). I tried to follow that pattern, and to pick a clear name for the option, but any feedback/change is welcome. --------- Co-authored-by: zumbalogy <3770982+zumbalogy@users.noreply.github.com> --- assets/settings/default.json | 4 +++- crates/editor/src/editor_settings.rs | 8 ++++++++ crates/go_to_line/src/cursor_position.rs | 9 ++++++++- docs/src/configuring-zed.md | 1 + docs/src/visual-customization.md | 4 ++++ 5 files changed, 24 insertions(+), 2 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 2c3bf6930d..1b485a8b28 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1256,7 +1256,9 @@ // Status bar-related settings. "status_bar": { // Whether to show the active language button in the status bar. - "active_language_button": true + "active_language_button": true, + // Whether to show the cursor position button in the status bar. + "cursor_position_button": true }, // Settings specific to the terminal "terminal": { diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 3d132651b8..d3a21c7642 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -132,6 +132,10 @@ pub struct StatusBar { /// /// Default: true pub active_language_button: bool, + /// Whether to show the cursor position button in the status bar. + /// + /// Default: true + pub cursor_position_button: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -585,6 +589,10 @@ pub struct StatusBarContent { /// /// Default: true pub active_language_button: Option, + /// Whether to show the cursor position button in the status bar. + /// + /// Default: true + pub cursor_position_button: Option, } // Toolbar related settings diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index 29064eb29c..af92621378 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -1,4 +1,4 @@ -use editor::{Editor, MultiBufferSnapshot}; +use editor::{Editor, EditorSettings, MultiBufferSnapshot}; use gpui::{App, Entity, FocusHandle, Focusable, Subscription, Task, WeakEntity}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -209,6 +209,13 @@ impl CursorPosition { impl Render for CursorPosition { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + if !EditorSettings::get_global(cx) + .status_bar + .cursor_position_button + { + return div(); + } + div().when_some(self.position, |el, position| { let mut text = format!( "{}{FILE_ROW_COLUMN_DELIMITER}{}", diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index b4cb1fcb9b..9d56130256 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1284,6 +1284,7 @@ Each option controls displaying of a particular toolbar element. If all elements ```json "status_bar": { "active_language_button": true, + "cursor_position_button": true }, ``` diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 7e75f6287d..6e598f4436 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -316,6 +316,10 @@ TBD: Centered layout related settings // Clicking the button brings up the language selector. // Defaults to true. "active_language_button": true, + // Show/hide a button that displays the cursor's position. + // Clicking the button brings up an input for jumping to a line and column. + // Defaults to true. + "cursor_position_button": true, }, ``` From 7784fac288b89b5ffc5edbe634ecbc907325faa6 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Sat, 16 Aug 2025 01:33:32 -0500 Subject: [PATCH 399/693] Separate minidump crashes from panics (#36267) The minidump-based crash reporting is now entirely separate from our legacy panic_hook-based reporting. This should improve the association of minidumps with their metadata and give us more consistent crash reports. Release Notes: - N/A --------- Co-authored-by: Max Brunsfeld --- Cargo.lock | 2 + crates/crashes/Cargo.toml | 2 + crates/crashes/src/crashes.rs | 157 +++++++++++++----- crates/proto/proto/app.proto | 6 +- crates/remote/src/ssh_session.rs | 30 ++-- crates/remote_server/src/unix.rs | 93 +++++------ crates/zed/src/main.rs | 11 +- crates/zed/src/reliability.rs | 262 ++++++++++++++----------------- 8 files changed, 315 insertions(+), 248 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d72eed42e..1bce72b3a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4038,6 +4038,8 @@ dependencies = [ "minidumper", "paths", "release_channel", + "serde", + "serde_json", "smol", "workspace-hack", ] diff --git a/crates/crashes/Cargo.toml b/crates/crashes/Cargo.toml index afb4936b63..2420b499f8 100644 --- a/crates/crashes/Cargo.toml +++ b/crates/crashes/Cargo.toml @@ -12,6 +12,8 @@ minidumper.workspace = true paths.workspace = true release_channel.workspace = true smol.workspace = true +serde.workspace = true +serde_json.workspace = true workspace-hack.workspace = true [lints] diff --git a/crates/crashes/src/crashes.rs b/crates/crashes/src/crashes.rs index 5b9ae0b546..ddf6468be8 100644 --- a/crates/crashes/src/crashes.rs +++ b/crates/crashes/src/crashes.rs @@ -2,15 +2,17 @@ use crash_handler::CrashHandler; use log::info; use minidumper::{Client, LoopAction, MinidumpBinary}; use release_channel::{RELEASE_CHANNEL, ReleaseChannel}; +use serde::{Deserialize, Serialize}; use std::{ env, - fs::File, + fs::{self, File}, io, + panic::Location, path::{Path, PathBuf}, process::{self, Command}, sync::{ - LazyLock, OnceLock, + Arc, OnceLock, atomic::{AtomicBool, Ordering}, }, thread, @@ -18,19 +20,17 @@ use std::{ }; // set once the crash handler has initialized and the client has connected to it -pub static CRASH_HANDLER: AtomicBool = AtomicBool::new(false); +pub static CRASH_HANDLER: OnceLock> = OnceLock::new(); // set when the first minidump request is made to avoid generating duplicate crash reports pub static REQUESTED_MINIDUMP: AtomicBool = AtomicBool::new(false); -const CRASH_HANDLER_TIMEOUT: Duration = Duration::from_secs(60); +const CRASH_HANDLER_PING_TIMEOUT: Duration = Duration::from_secs(60); +const CRASH_HANDLER_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); -pub static GENERATE_MINIDUMPS: LazyLock = LazyLock::new(|| { - *RELEASE_CHANNEL != ReleaseChannel::Dev || env::var("ZED_GENERATE_MINIDUMPS").is_ok() -}); - -pub async fn init(id: String) { - if !*GENERATE_MINIDUMPS { +pub async fn init(crash_init: InitCrashHandler) { + if *RELEASE_CHANNEL == ReleaseChannel::Dev && env::var("ZED_GENERATE_MINIDUMPS").is_err() { return; } + let exe = env::current_exe().expect("unable to find ourselves"); let zed_pid = process::id(); // TODO: we should be able to get away with using 1 crash-handler process per machine, @@ -61,9 +61,11 @@ pub async fn init(id: String) { smol::Timer::after(retry_frequency).await; } let client = maybe_client.unwrap(); - client.send_message(1, id).unwrap(); // set session id on the server + client + .send_message(1, serde_json::to_vec(&crash_init).unwrap()) + .unwrap(); - let client = std::sync::Arc::new(client); + let client = Arc::new(client); let handler = crash_handler::CrashHandler::attach(unsafe { let client = client.clone(); crash_handler::make_crash_event(move |crash_context: &crash_handler::CrashContext| { @@ -72,7 +74,6 @@ pub async fn init(id: String) { .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) .is_ok() { - client.send_message(2, "mistakes were made").unwrap(); client.ping().unwrap(); client.request_dump(crash_context).is_ok() } else { @@ -87,7 +88,7 @@ pub async fn init(id: String) { { handler.set_ptracer(Some(server_pid)); } - CRASH_HANDLER.store(true, Ordering::Release); + CRASH_HANDLER.set(client.clone()).ok(); std::mem::forget(handler); info!("crash handler registered"); @@ -98,14 +99,43 @@ pub async fn init(id: String) { } pub struct CrashServer { - session_id: OnceLock, + initialization_params: OnceLock, + panic_info: OnceLock, + has_connection: Arc, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct CrashInfo { + pub init: InitCrashHandler, + pub panic: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct InitCrashHandler { + pub session_id: String, + pub zed_version: String, + pub release_channel: String, + pub commit_sha: String, + // pub gpu: String, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct CrashPanic { + pub message: String, + pub span: String, } impl minidumper::ServerHandler for CrashServer { fn create_minidump_file(&self) -> Result<(File, PathBuf), io::Error> { - let err_message = "Need to send a message with the ID upon starting the crash handler"; + let err_message = "Missing initialization data"; let dump_path = paths::logs_dir() - .join(self.session_id.get().expect(err_message)) + .join( + &self + .initialization_params + .get() + .expect(err_message) + .session_id, + ) .with_extension("dmp"); let file = File::create(&dump_path)?; Ok((file, dump_path)) @@ -122,38 +152,71 @@ impl minidumper::ServerHandler for CrashServer { info!("failed to write minidump: {:#}", e); } } + + let crash_info = CrashInfo { + init: self + .initialization_params + .get() + .expect("not initialized") + .clone(), + panic: self.panic_info.get().cloned(), + }; + + let crash_data_path = paths::logs_dir() + .join(&crash_info.init.session_id) + .with_extension("json"); + + fs::write(crash_data_path, serde_json::to_vec(&crash_info).unwrap()).ok(); + LoopAction::Exit } fn on_message(&self, kind: u32, buffer: Vec) { - let message = String::from_utf8(buffer).expect("invalid utf-8"); - info!("kind: {kind}, message: {message}",); - if kind == 1 { - self.session_id - .set(message) - .expect("session id already initialized"); + match kind { + 1 => { + let init_data = + serde_json::from_slice::(&buffer).expect("invalid init data"); + self.initialization_params + .set(init_data) + .expect("already initialized"); + } + 2 => { + let panic_data = + serde_json::from_slice::(&buffer).expect("invalid panic data"); + self.panic_info.set(panic_data).expect("already panicked"); + } + _ => { + panic!("invalid message kind"); + } } } - fn on_client_disconnected(&self, clients: usize) -> LoopAction { - info!("client disconnected, {clients} remaining"); - if clients == 0 { - LoopAction::Exit - } else { - LoopAction::Continue - } + fn on_client_disconnected(&self, _clients: usize) -> LoopAction { + LoopAction::Exit + } + + fn on_client_connected(&self, _clients: usize) -> LoopAction { + self.has_connection.store(true, Ordering::SeqCst); + LoopAction::Continue } } -pub fn handle_panic() { - if !*GENERATE_MINIDUMPS { - return; - } +pub fn handle_panic(message: String, span: Option<&Location>) { + let span = span + .map(|loc| format!("{}:{}", loc.file(), loc.line())) + .unwrap_or_default(); + // wait 500ms for the crash handler process to start up // if it's still not there just write panic info and no minidump let retry_frequency = Duration::from_millis(100); for _ in 0..5 { - if CRASH_HANDLER.load(Ordering::Acquire) { + if let Some(client) = CRASH_HANDLER.get() { + client + .send_message( + 2, + serde_json::to_vec(&CrashPanic { message, span }).unwrap(), + ) + .ok(); log::error!("triggering a crash to generate a minidump..."); #[cfg(target_os = "linux")] CrashHandler.simulate_signal(crash_handler::Signal::Trap as u32); @@ -170,14 +233,30 @@ pub fn crash_server(socket: &Path) { log::info!("Couldn't create socket, there may already be a running crash server"); return; }; - let ab = AtomicBool::new(false); + + let shutdown = Arc::new(AtomicBool::new(false)); + let has_connection = Arc::new(AtomicBool::new(false)); + + std::thread::spawn({ + let shutdown = shutdown.clone(); + let has_connection = has_connection.clone(); + move || { + std::thread::sleep(CRASH_HANDLER_CONNECT_TIMEOUT); + if !has_connection.load(Ordering::SeqCst) { + shutdown.store(true, Ordering::SeqCst); + } + } + }); + server .run( Box::new(CrashServer { - session_id: OnceLock::new(), + initialization_params: OnceLock::new(), + panic_info: OnceLock::new(), + has_connection, }), - &ab, - Some(CRASH_HANDLER_TIMEOUT), + &shutdown, + Some(CRASH_HANDLER_PING_TIMEOUT), ) .expect("failed to run server"); } diff --git a/crates/proto/proto/app.proto b/crates/proto/proto/app.proto index 1f2ab1f539..66f8da44f2 100644 --- a/crates/proto/proto/app.proto +++ b/crates/proto/proto/app.proto @@ -28,11 +28,13 @@ message GetCrashFiles { message GetCrashFilesResponse { repeated CrashReport crashes = 1; + repeated string legacy_panics = 2; } message CrashReport { - optional string panic_contents = 1; - optional bytes minidump_contents = 2; + reserved 1, 2; + string metadata = 3; + bytes minidump_contents = 4; } message Extension { diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 2f462a86a5..ea383ac264 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -1490,20 +1490,17 @@ impl RemoteConnection for SshRemoteConnection { identifier = &unique_identifier, ); - if let Some(rust_log) = std::env::var("RUST_LOG").ok() { - start_proxy_command = format!( - "RUST_LOG={} {}", - shlex::try_quote(&rust_log).unwrap(), - start_proxy_command - ) - } - if let Some(rust_backtrace) = std::env::var("RUST_BACKTRACE").ok() { - start_proxy_command = format!( - "RUST_BACKTRACE={} {}", - shlex::try_quote(&rust_backtrace).unwrap(), - start_proxy_command - ) + for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] { + if let Some(value) = std::env::var(env_var).ok() { + start_proxy_command = format!( + "{}={} {} ", + env_var, + shlex::try_quote(&value).unwrap(), + start_proxy_command, + ); + } } + if reconnect { start_proxy_command.push_str(" --reconnect"); } @@ -2241,8 +2238,7 @@ impl SshRemoteConnection { #[cfg(not(target_os = "windows"))] { - run_cmd(Command::new("gzip").args(["-9", "-f", &bin_path.to_string_lossy()])) - .await?; + run_cmd(Command::new("gzip").args(["-f", &bin_path.to_string_lossy()])).await?; } #[cfg(target_os = "windows")] { @@ -2474,7 +2470,7 @@ impl ChannelClient { }, async { smol::Timer::after(timeout).await; - anyhow::bail!("Timeout detected") + anyhow::bail!("Timed out resyncing remote client") }, ) .await @@ -2488,7 +2484,7 @@ impl ChannelClient { }, async { smol::Timer::after(timeout).await; - anyhow::bail!("Timeout detected") + anyhow::bail!("Timed out pinging remote client") }, ) .await diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 9bb5645dc7..dc7fab8c3c 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -34,10 +34,10 @@ use smol::io::AsyncReadExt; use smol::Async; use smol::{net::unix::UnixListener, stream::StreamExt as _}; -use std::collections::HashMap; use std::ffi::OsStr; use std::ops::ControlFlow; use std::str::FromStr; +use std::sync::LazyLock; use std::{env, thread}; use std::{ io::Write, @@ -48,6 +48,13 @@ use std::{ use telemetry_events::LocationData; use util::ResultExt; +pub static VERSION: LazyLock<&str> = LazyLock::new(|| match *RELEASE_CHANNEL { + ReleaseChannel::Stable | ReleaseChannel::Preview => env!("ZED_PKG_VERSION"), + ReleaseChannel::Nightly | ReleaseChannel::Dev => { + option_env!("ZED_COMMIT_SHA").unwrap_or("missing-zed-commit-sha") + } +}); + fn init_logging_proxy() { env_logger::builder() .format(|buf, record| { @@ -113,7 +120,6 @@ fn init_logging_server(log_file_path: PathBuf) -> Result>> { fn init_panic_hook(session_id: String) { std::panic::set_hook(Box::new(move |info| { - crashes::handle_panic(); let payload = info .payload() .downcast_ref::<&str>() @@ -121,6 +127,8 @@ fn init_panic_hook(session_id: String) { .or_else(|| info.payload().downcast_ref::().cloned()) .unwrap_or_else(|| "Box".to_string()); + crashes::handle_panic(payload.clone(), info.location()); + let backtrace = backtrace::Backtrace::new(); let mut backtrace = backtrace .frames() @@ -150,14 +158,6 @@ fn init_panic_hook(session_id: String) { (&backtrace).join("\n") ); - let release_channel = *RELEASE_CHANNEL; - let version = match release_channel { - ReleaseChannel::Stable | ReleaseChannel::Preview => env!("ZED_PKG_VERSION"), - ReleaseChannel::Nightly | ReleaseChannel::Dev => { - option_env!("ZED_COMMIT_SHA").unwrap_or("missing-zed-commit-sha") - } - }; - let panic_data = telemetry_events::Panic { thread: thread_name.into(), payload: payload.clone(), @@ -165,9 +165,9 @@ fn init_panic_hook(session_id: String) { file: location.file().into(), line: location.line(), }), - app_version: format!("remote-server-{version}"), + app_version: format!("remote-server-{}", *VERSION), app_commit_sha: option_env!("ZED_COMMIT_SHA").map(|sha| sha.into()), - release_channel: release_channel.dev_name().into(), + release_channel: RELEASE_CHANNEL.dev_name().into(), target: env!("TARGET").to_owned().into(), os_name: telemetry::os_name(), os_version: Some(telemetry::os_version()), @@ -204,8 +204,8 @@ fn handle_crash_files_requests(project: &Entity, client: &Arc, _cx| async move { + let mut legacy_panics = Vec::new(); let mut crashes = Vec::new(); - let mut minidumps_by_session_id = HashMap::new(); let mut children = smol::fs::read_dir(paths::logs_dir()).await?; while let Some(child) = children.next().await { let child = child?; @@ -227,41 +227,31 @@ fn handle_crash_files_requests(project: &Entity, client: &Arc Result<()> { let server_paths = ServerPaths::new(&identifier)?; let id = std::process::id().to_string(); - smol::spawn(crashes::init(id.clone())).detach(); + smol::spawn(crashes::init(crashes::InitCrashHandler { + session_id: id.clone(), + zed_version: VERSION.to_owned(), + release_channel: release_channel::RELEASE_CHANNEL_NAME.clone(), + commit_sha: option_env!("ZED_COMMIT_SHA").unwrap_or("no_sha").to_owned(), + })) + .detach(); init_panic_hook(id); log::info!("starting proxy process. PID: {}", std::process::id()); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index fd987ef6c5..2a82f81b5b 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -8,6 +8,7 @@ use cli::FORCE_CLI_MODE_ENV_VAR_NAME; use client::{Client, ProxySettings, UserStore, parse_zed_link}; use collab_ui::channel_view::ChannelView; use collections::HashMap; +use crashes::InitCrashHandler; use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE}; use editor::Editor; use extension::ExtensionHostProxy; @@ -269,7 +270,15 @@ pub fn main() { let session = app.background_executor().block(Session::new()); app.background_executor() - .spawn(crashes::init(session_id.clone())) + .spawn(crashes::init(InitCrashHandler { + session_id: session_id.clone(), + zed_version: app_version.to_string(), + release_channel: release_channel::RELEASE_CHANNEL_NAME.clone(), + commit_sha: app_commit_sha + .as_ref() + .map(|sha| sha.full()) + .unwrap_or_else(|| "no sha".to_owned()), + })) .detach(); reliability::init_panic_hook( app_version, diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index fde44344b1..c27f4cb0a8 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -12,6 +12,7 @@ use gpui::{App, AppContext as _, SemanticVersion}; use http_client::{self, HttpClient, HttpClientWithUrl, HttpRequestExt, Method}; use paths::{crashes_dir, crashes_retired_dir}; use project::Project; +use proto::{CrashReport, GetCrashFilesResponse}; use release_channel::{AppCommitSha, RELEASE_CHANNEL, ReleaseChannel}; use reqwest::multipart::{Form, Part}; use settings::Settings; @@ -51,10 +52,6 @@ pub fn init_panic_hook( thread::yield_now(); } } - crashes::handle_panic(); - - let thread = thread::current(); - let thread_name = thread.name().unwrap_or(""); let payload = info .payload() @@ -63,6 +60,11 @@ pub fn init_panic_hook( .or_else(|| info.payload().downcast_ref::().cloned()) .unwrap_or_else(|| "Box".to_string()); + crashes::handle_panic(payload.clone(), info.location()); + + let thread = thread::current(); + let thread_name = thread.name().unwrap_or(""); + if *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev { let location = info.location().unwrap(); let backtrace = Backtrace::new(); @@ -214,45 +216,53 @@ pub fn init( let installation_id = installation_id.clone(); let system_id = system_id.clone(); - if let Some(ssh_client) = project.ssh_client() { - ssh_client.update(cx, |client, cx| { - if TelemetrySettings::get_global(cx).diagnostics { - let request = client.proto_client().request(proto::GetCrashFiles {}); - cx.background_spawn(async move { - let crash_files = request.await?; - for crash in crash_files.crashes { - let mut panic: Option = crash - .panic_contents - .and_then(|s| serde_json::from_str(&s).log_err()); + let Some(ssh_client) = project.ssh_client() else { + return; + }; + ssh_client.update(cx, |client, cx| { + if !TelemetrySettings::get_global(cx).diagnostics { + return; + } + let request = client.proto_client().request(proto::GetCrashFiles {}); + cx.background_spawn(async move { + let GetCrashFilesResponse { + legacy_panics, + crashes, + } = request.await?; - if let Some(panic) = panic.as_mut() { - panic.session_id = session_id.clone(); - panic.system_id = system_id.clone(); - panic.installation_id = installation_id.clone(); - } - - if let Some(minidump) = crash.minidump_contents { - upload_minidump( - http_client.clone(), - minidump.clone(), - panic.as_ref(), - ) - .await - .log_err(); - } - - if let Some(panic) = panic { - upload_panic(&http_client, &panic_report_url, panic, &mut None) - .await?; - } - } - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + for panic in legacy_panics { + if let Some(mut panic) = serde_json::from_str::(&panic).log_err() { + panic.session_id = session_id.clone(); + panic.system_id = system_id.clone(); + panic.installation_id = installation_id.clone(); + upload_panic(&http_client, &panic_report_url, panic, &mut None).await?; + } } + + let Some(endpoint) = MINIDUMP_ENDPOINT.as_ref() else { + return Ok(()); + }; + for CrashReport { + metadata, + minidump_contents, + } in crashes + { + if let Some(metadata) = serde_json::from_str(&metadata).log_err() { + upload_minidump( + http_client.clone(), + endpoint, + minidump_contents, + &metadata, + ) + .await + .log_err(); + } + } + + anyhow::Ok(()) }) - } + .detach_and_log_err(cx); + }) }) .detach(); } @@ -466,16 +476,18 @@ fn upload_panics_and_crashes( installation_id: Option, cx: &App, ) { - let telemetry_settings = *client::TelemetrySettings::get_global(cx); + if !client::TelemetrySettings::get_global(cx).diagnostics { + return; + } cx.background_spawn(async move { - let most_recent_panic = - upload_previous_panics(http.clone(), &panic_report_url, telemetry_settings) - .await - .log_err() - .flatten(); - upload_previous_crashes(http, most_recent_panic, installation_id, telemetry_settings) + upload_previous_minidumps(http.clone()).await.warn_on_err(); + let most_recent_panic = upload_previous_panics(http.clone(), &panic_report_url) .await .log_err() + .flatten(); + upload_previous_crashes(http, most_recent_panic, installation_id) + .await + .log_err(); }) .detach() } @@ -484,7 +496,6 @@ fn upload_panics_and_crashes( async fn upload_previous_panics( http: Arc, panic_report_url: &Url, - telemetry_settings: client::TelemetrySettings, ) -> anyhow::Result> { let mut children = smol::fs::read_dir(paths::logs_dir()).await?; @@ -507,58 +518,41 @@ async fn upload_previous_panics( continue; } - if telemetry_settings.diagnostics { - let panic_file_content = smol::fs::read_to_string(&child_path) - .await - .context("error reading panic file")?; + let panic_file_content = smol::fs::read_to_string(&child_path) + .await + .context("error reading panic file")?; - let panic: Option = serde_json::from_str(&panic_file_content) - .log_err() - .or_else(|| { - panic_file_content - .lines() - .next() - .and_then(|line| serde_json::from_str(line).ok()) - }) - .unwrap_or_else(|| { - log::error!("failed to deserialize panic file {:?}", panic_file_content); - None - }); + let panic: Option = serde_json::from_str(&panic_file_content) + .log_err() + .or_else(|| { + panic_file_content + .lines() + .next() + .and_then(|line| serde_json::from_str(line).ok()) + }) + .unwrap_or_else(|| { + log::error!("failed to deserialize panic file {:?}", panic_file_content); + None + }); - if let Some(panic) = panic { - let minidump_path = paths::logs_dir() - .join(&panic.session_id) - .with_extension("dmp"); - if minidump_path.exists() { - let minidump = smol::fs::read(&minidump_path) - .await - .context("Failed to read minidump")?; - if upload_minidump(http.clone(), minidump, Some(&panic)) - .await - .log_err() - .is_some() - { - fs::remove_file(minidump_path).ok(); - } - } - - if !upload_panic(&http, &panic_report_url, panic, &mut most_recent_panic).await? { - continue; - } - } + if let Some(panic) = panic + && upload_panic(&http, &panic_report_url, panic, &mut most_recent_panic).await? + { + // We've done what we can, delete the file + fs::remove_file(child_path) + .context("error removing panic") + .log_err(); } - - // We've done what we can, delete the file - fs::remove_file(child_path) - .context("error removing panic") - .log_err(); } - if MINIDUMP_ENDPOINT.is_none() { - return Ok(most_recent_panic); - } + Ok(most_recent_panic) +} + +pub async fn upload_previous_minidumps(http: Arc) -> anyhow::Result<()> { + let Some(minidump_endpoint) = MINIDUMP_ENDPOINT.as_ref() else { + return Err(anyhow::anyhow!("Minidump endpoint not set")); + }; - // loop back over the directory again to upload any minidumps that are missing panics let mut children = smol::fs::read_dir(paths::logs_dir()).await?; while let Some(child) = children.next().await { let child = child?; @@ -566,33 +560,35 @@ async fn upload_previous_panics( if child_path.extension() != Some(OsStr::new("dmp")) { continue; } - if upload_minidump( - http.clone(), - smol::fs::read(&child_path) - .await - .context("Failed to read minidump")?, - None, - ) - .await - .log_err() - .is_some() - { - fs::remove_file(child_path).ok(); + let mut json_path = child_path.clone(); + json_path.set_extension("json"); + if let Ok(metadata) = serde_json::from_slice(&smol::fs::read(&json_path).await?) { + if upload_minidump( + http.clone(), + &minidump_endpoint, + smol::fs::read(&child_path) + .await + .context("Failed to read minidump")?, + &metadata, + ) + .await + .log_err() + .is_some() + { + fs::remove_file(child_path).ok(); + fs::remove_file(json_path).ok(); + } } } - - Ok(most_recent_panic) + Ok(()) } async fn upload_minidump( http: Arc, + endpoint: &str, minidump: Vec, - panic: Option<&Panic>, + metadata: &crashes::CrashInfo, ) -> Result<()> { - let minidump_endpoint = MINIDUMP_ENDPOINT - .to_owned() - .ok_or_else(|| anyhow::anyhow!("Minidump endpoint not set"))?; - let mut form = Form::new() .part( "upload_file_minidump", @@ -600,38 +596,22 @@ async fn upload_minidump( .file_name("minidump.dmp") .mime_str("application/octet-stream")?, ) + .text( + "sentry[tags][channel]", + metadata.init.release_channel.clone(), + ) + .text("sentry[tags][version]", metadata.init.zed_version.clone()) + .text("sentry[release]", metadata.init.commit_sha.clone()) .text("platform", "rust"); - if let Some(panic) = panic { - form = form - .text("sentry[tags][channel]", panic.release_channel.clone()) - .text("sentry[tags][version]", panic.app_version.clone()) - .text("sentry[context][os][name]", panic.os_name.clone()) - .text( - "sentry[context][device][architecture]", - panic.architecture.clone(), - ) - .text("sentry[logentry][formatted]", panic.payload.clone()); - - if let Some(sha) = panic.app_commit_sha.clone() { - form = form.text("sentry[release]", sha) - } else { - form = form.text( - "sentry[release]", - format!("{}-{}", panic.release_channel, panic.app_version), - ) - } - if let Some(v) = panic.os_version.clone() { - form = form.text("sentry[context][os][release]", v); - } - if let Some(location) = panic.location_data.as_ref() { - form = form.text("span", format!("{}:{}", location.file, location.line)) - } + if let Some(panic_info) = metadata.panic.as_ref() { + form = form.text("sentry[logentry][formatted]", panic_info.message.clone()); + form = form.text("span", panic_info.span.clone()); // TODO: add gpu-context, feature-flag-context, and more of device-context like gpu // name, screen resolution, available ram, device model, etc } let mut response_text = String::new(); - let mut response = http.send_multipart_form(&minidump_endpoint, form).await?; + let mut response = http.send_multipart_form(endpoint, form).await?; response .body_mut() .read_to_string(&mut response_text) @@ -681,11 +661,7 @@ async fn upload_previous_crashes( http: Arc, most_recent_panic: Option<(i64, String)>, installation_id: Option, - telemetry_settings: client::TelemetrySettings, ) -> Result<()> { - if !telemetry_settings.diagnostics { - return Ok(()); - } let last_uploaded = KEY_VALUE_STORE .read_kvp(LAST_CRASH_UPLOADED)? .unwrap_or("zed-2024-01-17-221900.ips".to_string()); // don't upload old crash reports from before we had this. From 864d4bc1d133e5beb24c64bc0bf7336fc274ed1c Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sat, 16 Aug 2025 09:55:46 +0200 Subject: [PATCH 400/693] editor: Drop multiline targets in navigation buffers (#36291) Release Notes: - N/A --- crates/editor/src/editor.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 85f2e01ed4..0111e91347 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -15909,10 +15909,15 @@ impl Editor { .text_for_range(location.range.clone()) .collect::() }) + .filter(|text| !text.contains('\n')) .unique() .take(3) .join(", "); - format!("{tab_kind} for {target}") + if target.is_empty() { + tab_kind.to_owned() + } else { + format!("{tab_kind} for {target}") + } }) .context("buffer title")?; @@ -16117,10 +16122,15 @@ impl Editor { .text_for_range(location.range.clone()) .collect::() }) + .filter(|text| !text.contains('\n')) .unique() .take(3) .join(", "); - let title = format!("References to {target}"); + let title = if target.is_empty() { + "References".to_owned() + } else { + format!("References to {target}") + }; Self::open_locations_in_multibuffer( workspace, locations, From 6f2e7c355ec4d2b68285047258af7e5d72596b33 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Sat, 16 Aug 2025 13:36:17 +0200 Subject: [PATCH 401/693] Ensure bundled files are opened as read-only (#36299) Closes #36297 While we set the editor as read-only for bundled files, we didn't do this for the underlying buffer. This PR fixes this and adds a test for the corresponding case. Release Notes: - Fixed an issue where bundled files (e.g. the default settings) could be edited in some circumstances --- crates/zed/src/zed.rs | 44 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index b06652b2ce..a324ba0932 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -31,6 +31,7 @@ use gpui::{ px, retain_all, }; use image_viewer::ImageInfo; +use language::Capability; use language_tools::lsp_tool::{self, LspTool}; use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType}; use migrator::{migrate_keymap, migrate_settings}; @@ -1764,7 +1765,11 @@ fn open_bundled_file( workspace.with_local_workspace(window, cx, |workspace, window, cx| { let project = workspace.project(); let buffer = project.update(cx, move |project, cx| { - project.create_local_buffer(text.as_ref(), language, cx) + let buffer = project.create_local_buffer(text.as_ref(), language, cx); + buffer.update(cx, |buffer, cx| { + buffer.set_capability(Capability::ReadOnly, cx); + }); + buffer }); let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into())); @@ -4543,6 +4548,43 @@ mod tests { assert!(has_default_theme); } + #[gpui::test] + async fn test_bundled_files_editor(cx: &mut TestAppContext) { + let app_state = init_test(cx); + cx.update(init); + + let project = Project::test(app_state.fs.clone(), [], cx).await; + let _window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); + + cx.update(|cx| { + cx.dispatch_action(&OpenDefaultSettings); + }); + cx.run_until_parked(); + + assert_eq!(cx.read(|cx| cx.windows().len()), 1); + + let workspace = cx.windows()[0].downcast::().unwrap(); + let active_editor = workspace + .update(cx, |workspace, _, cx| { + workspace.active_item_as::(cx) + }) + .unwrap(); + assert!( + active_editor.is_some(), + "Settings action should have opened an editor with the default file contents" + ); + + let active_editor = active_editor.unwrap(); + assert!( + active_editor.read_with(cx, |editor, cx| editor.read_only(cx)), + "Default settings should be readonly" + ); + assert!( + active_editor.read_with(cx, |editor, cx| editor.buffer().read(cx).read_only()), + "The underlying buffer should also be readonly for the shipped default settings" + ); + } + #[gpui::test] async fn test_bundled_languages(cx: &mut TestAppContext) { env_logger::builder().is_test(true).try_init().ok(); From 5620e359af2c96aa420ade68d017c802012dd005 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sat, 16 Aug 2025 09:09:14 -0400 Subject: [PATCH 402/693] collab: Make `admin` column non-nullable on `users` table (#36307) This PR updates the `admin` column on the `users` table to be non-nullable. We were already treating it like this in practice. All rows in the production database already have a value for the `admin` column. Release Notes: - N/A --- .../migrations/20250816124707_make_admin_required_on_users.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 crates/collab/migrations/20250816124707_make_admin_required_on_users.sql diff --git a/crates/collab/migrations/20250816124707_make_admin_required_on_users.sql b/crates/collab/migrations/20250816124707_make_admin_required_on_users.sql new file mode 100644 index 0000000000..e372723d6d --- /dev/null +++ b/crates/collab/migrations/20250816124707_make_admin_required_on_users.sql @@ -0,0 +1,2 @@ +alter table users +alter column admin set not null; From d1958aa43913889390c171e46d6e59259f7be2c0 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sat, 16 Aug 2025 09:48:38 -0400 Subject: [PATCH 403/693] collab: Add `orb_customer_id` to `billing_customers` (#36310) This PR adds an `orb_customer_id` column to the `billing_customers` table. Release Notes: - N/A --- .../20250816133027_add_orb_customer_id_to_billing_customers.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 crates/collab/migrations/20250816133027_add_orb_customer_id_to_billing_customers.sql diff --git a/crates/collab/migrations/20250816133027_add_orb_customer_id_to_billing_customers.sql b/crates/collab/migrations/20250816133027_add_orb_customer_id_to_billing_customers.sql new file mode 100644 index 0000000000..ea5e4de52a --- /dev/null +++ b/crates/collab/migrations/20250816133027_add_orb_customer_id_to_billing_customers.sql @@ -0,0 +1,2 @@ +alter table billing_customers + add column orb_customer_id text; From ea7bc96c051371f93d7247492a91975608e4e1f7 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sat, 16 Aug 2025 09:52:14 -0400 Subject: [PATCH 404/693] collab: Remove billing-related tables from SQLite schema (#36312) This PR removes the billing-related tables from the SQLite schema, as we don't actually reference these tables anywhere in the Collab codebase anymore. Release Notes: - N/A --- .../20221109000000_test_schema.sql | 50 ------------------- 1 file changed, 50 deletions(-) diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 73d473ab76..63f999b3a7 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -485,56 +485,6 @@ CREATE TABLE rate_buckets ( CREATE INDEX idx_user_id_rate_limit ON rate_buckets (user_id, rate_limit_name); -CREATE TABLE IF NOT EXISTS billing_preferences ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - user_id INTEGER NOT NULL REFERENCES users (id), - max_monthly_llm_usage_spending_in_cents INTEGER NOT NULL, - model_request_overages_enabled bool NOT NULL DEFAULT FALSE, - model_request_overages_spend_limit_in_cents integer NOT NULL DEFAULT 0 -); - -CREATE UNIQUE INDEX "uix_billing_preferences_on_user_id" ON billing_preferences (user_id); - -CREATE TABLE IF NOT EXISTS billing_customers ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - user_id INTEGER NOT NULL REFERENCES users (id), - has_overdue_invoices BOOLEAN NOT NULL DEFAULT FALSE, - stripe_customer_id TEXT NOT NULL, - trial_started_at TIMESTAMP -); - -CREATE UNIQUE INDEX "uix_billing_customers_on_user_id" ON billing_customers (user_id); - -CREATE UNIQUE INDEX "uix_billing_customers_on_stripe_customer_id" ON billing_customers (stripe_customer_id); - -CREATE TABLE IF NOT EXISTS billing_subscriptions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - billing_customer_id INTEGER NOT NULL REFERENCES billing_customers (id), - stripe_subscription_id TEXT NOT NULL, - stripe_subscription_status TEXT NOT NULL, - stripe_cancel_at TIMESTAMP, - stripe_cancellation_reason TEXT, - kind TEXT, - stripe_current_period_start BIGINT, - stripe_current_period_end BIGINT -); - -CREATE INDEX "ix_billing_subscriptions_on_billing_customer_id" ON billing_subscriptions (billing_customer_id); - -CREATE UNIQUE INDEX "uix_billing_subscriptions_on_stripe_subscription_id" ON billing_subscriptions (stripe_subscription_id); - -CREATE TABLE IF NOT EXISTS processed_stripe_events ( - stripe_event_id TEXT PRIMARY KEY, - stripe_event_type TEXT NOT NULL, - stripe_event_created_timestamp INTEGER NOT NULL, - processed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX "ix_processed_stripe_events_on_stripe_event_created_timestamp" ON processed_stripe_events (stripe_event_created_timestamp); - CREATE TABLE IF NOT EXISTS "breakpoints" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, From 36184a71df8766fec6ceebd3c54c42f871abec84 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sat, 16 Aug 2025 10:11:36 -0400 Subject: [PATCH 405/693] collab: Drop `rate_buckets` table (#36315) This PR drops the `rate_buckets` table, as we're no longer using it. Release Notes: - N/A --- .../migrations.sqlite/20221109000000_test_schema.sql | 11 ----------- .../20250816135346_drop_rate_buckets_table.sql | 1 + 2 files changed, 1 insertion(+), 11 deletions(-) create mode 100644 crates/collab/migrations/20250816135346_drop_rate_buckets_table.sql diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 63f999b3a7..170ac7b0a2 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -474,17 +474,6 @@ CREATE UNIQUE INDEX "index_extensions_external_id" ON "extensions" ("external_id CREATE INDEX "index_extensions_total_download_count" ON "extensions" ("total_download_count"); -CREATE TABLE rate_buckets ( - user_id INT NOT NULL, - rate_limit_name VARCHAR(255) NOT NULL, - token_count INT NOT NULL, - last_refill TIMESTAMP WITHOUT TIME ZONE NOT NULL, - PRIMARY KEY (user_id, rate_limit_name), - FOREIGN KEY (user_id) REFERENCES users (id) -); - -CREATE INDEX idx_user_id_rate_limit ON rate_buckets (user_id, rate_limit_name); - CREATE TABLE IF NOT EXISTS "breakpoints" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, diff --git a/crates/collab/migrations/20250816135346_drop_rate_buckets_table.sql b/crates/collab/migrations/20250816135346_drop_rate_buckets_table.sql new file mode 100644 index 0000000000..f51a33ed30 --- /dev/null +++ b/crates/collab/migrations/20250816135346_drop_rate_buckets_table.sql @@ -0,0 +1 @@ +drop table rate_buckets; From 7b3fe0a474f5ead24fb9da976dfde745cc6ba936 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Sat, 16 Aug 2025 16:35:06 +0200 Subject: [PATCH 406/693] Make agent font size inherit the UI font size by default (#36306) Ensures issues like #36242 and #36295 do not arise where users are confused that the agent panel does not follow the default UI font size whilst also keeping the possibility of customization. The agent font size was matching the UI font size previously alredy, which makes it easier to change it for most scenarios. Also cleans up some related logic around modifying the font sizes. Release Notes: - The agent panel font size will now inherit the UI font size by default if not set in your settings. --- assets/settings/default.json | 4 +- crates/agent_ui/src/agent_panel.rs | 6 +-- crates/theme/src/settings.rs | 75 ++++++++++++++++-------------- crates/theme/src/theme.rs | 8 ++++ crates/zed/src/zed.rs | 16 ++----- 5 files changed, 55 insertions(+), 54 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 1b485a8b28..ff000001b5 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -71,8 +71,8 @@ "ui_font_weight": 400, // The default font size for text in the UI "ui_font_size": 16, - // The default font size for text in the agent panel - "agent_font_size": 16, + // The default font size for text in the agent panel. Falls back to the UI font size if unset. + "agent_font_size": null, // How much to fade out unused code. "unnecessary_code_fade": 0.3, // Active pane styling settings. diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 519f7980ff..44d605af57 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1257,13 +1257,11 @@ impl AgentPanel { ThemeSettings::get_global(cx).agent_font_size(cx) + delta; let _ = settings .agent_font_size - .insert(theme::clamp_font_size(agent_font_size).0); + .insert(Some(theme::clamp_font_size(agent_font_size).into())); }, ); } else { - theme::adjust_agent_font_size(cx, |size| { - *size += delta; - }); + theme::adjust_agent_font_size(cx, |size| size + delta); } } WhichFontSize::BufferFont => { diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index f5f1fd5547..df147cfe92 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -19,6 +19,7 @@ use util::ResultExt as _; use util::schemars::replace_subschema; const MIN_FONT_SIZE: Pixels = px(6.0); +const MAX_FONT_SIZE: Pixels = px(100.0); const MIN_LINE_HEIGHT: f32 = 1.0; #[derive( @@ -103,8 +104,8 @@ pub struct ThemeSettings { /// /// The terminal font family can be overridden using it's own setting. pub buffer_font: Font, - /// The agent font size. Determines the size of text in the agent panel. - agent_font_size: Pixels, + /// The agent font size. Determines the size of text in the agent panel. Falls back to the UI font size if unset. + agent_font_size: Option, /// The line height for buffers, and the terminal. /// /// Changing this may affect the spacing of some UI elements. @@ -404,9 +405,9 @@ pub struct ThemeSettingsContent { #[serde(default)] #[schemars(default = "default_font_features")] pub buffer_font_features: Option, - /// The font size for the agent panel. + /// The font size for the agent panel. Falls back to the UI font size if unset. #[serde(default)] - pub agent_font_size: Option, + pub agent_font_size: Option>, /// The name of the Zed theme to use. #[serde(default)] pub theme: Option, @@ -599,13 +600,13 @@ impl ThemeSettings { clamp_font_size(font_size) } - /// Returns the UI font size. + /// Returns the agent panel font size. Falls back to the UI font size if unset. pub fn agent_font_size(&self, cx: &App) -> Pixels { - let font_size = cx - .try_global::() + cx.try_global::() .map(|size| size.0) - .unwrap_or(self.agent_font_size); - clamp_font_size(font_size) + .or(self.agent_font_size) + .map(clamp_font_size) + .unwrap_or_else(|| self.ui_font_size(cx)) } /// Returns the buffer font size, read from the settings. @@ -624,6 +625,14 @@ impl ThemeSettings { self.ui_font_size } + /// Returns the agent font size, read from the settings. + /// + /// The real agent font size is stored in-memory, to support temporary font size changes. + /// Use [`Self::agent_font_size`] to get the real font size. + pub fn agent_font_size_settings(&self) -> Option { + self.agent_font_size + } + // TODO: Rename: `line_height` -> `buffer_line_height` /// Returns the buffer's line height. pub fn line_height(&self) -> f32 { @@ -732,14 +741,12 @@ pub fn adjusted_font_size(size: Pixels, cx: &App) -> Pixels { } /// Adjusts the buffer font size. -pub fn adjust_buffer_font_size(cx: &mut App, mut f: impl FnMut(&mut Pixels)) { +pub fn adjust_buffer_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) { let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size; - let mut adjusted_size = cx + let adjusted_size = cx .try_global::() .map_or(buffer_font_size, |adjusted_size| adjusted_size.0); - - f(&mut adjusted_size); - cx.set_global(BufferFontSize(clamp_font_size(adjusted_size))); + cx.set_global(BufferFontSize(clamp_font_size(f(adjusted_size)))); cx.refresh_windows(); } @@ -765,14 +772,12 @@ pub fn setup_ui_font(window: &mut Window, cx: &mut App) -> gpui::Font { } /// Sets the adjusted UI font size. -pub fn adjust_ui_font_size(cx: &mut App, mut f: impl FnMut(&mut Pixels)) { +pub fn adjust_ui_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) { let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx); - let mut adjusted_size = cx + let adjusted_size = cx .try_global::() .map_or(ui_font_size, |adjusted_size| adjusted_size.0); - - f(&mut adjusted_size); - cx.set_global(UiFontSize(clamp_font_size(adjusted_size))); + cx.set_global(UiFontSize(clamp_font_size(f(adjusted_size)))); cx.refresh_windows(); } @@ -784,19 +789,17 @@ pub fn reset_ui_font_size(cx: &mut App) { } } -/// Sets the adjusted UI font size. -pub fn adjust_agent_font_size(cx: &mut App, mut f: impl FnMut(&mut Pixels)) { +/// Sets the adjusted agent panel font size. +pub fn adjust_agent_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) { let agent_font_size = ThemeSettings::get_global(cx).agent_font_size(cx); - let mut adjusted_size = cx + let adjusted_size = cx .try_global::() .map_or(agent_font_size, |adjusted_size| adjusted_size.0); - - f(&mut adjusted_size); - cx.set_global(AgentFontSize(clamp_font_size(adjusted_size))); + cx.set_global(AgentFontSize(clamp_font_size(f(adjusted_size)))); cx.refresh_windows(); } -/// Resets the UI font size to the default value. +/// Resets the agent panel font size to the default value. pub fn reset_agent_font_size(cx: &mut App) { if cx.has_global::() { cx.remove_global::(); @@ -806,7 +809,7 @@ pub fn reset_agent_font_size(cx: &mut App) { /// Ensures font size is within the valid range. pub fn clamp_font_size(size: Pixels) -> Pixels { - size.max(MIN_FONT_SIZE) + size.clamp(MIN_FONT_SIZE, MAX_FONT_SIZE) } fn clamp_font_weight(weight: f32) -> FontWeight { @@ -860,7 +863,7 @@ impl settings::Settings for ThemeSettings { }, buffer_font_size: defaults.buffer_font_size.unwrap().into(), buffer_line_height: defaults.buffer_line_height.unwrap(), - agent_font_size: defaults.agent_font_size.unwrap().into(), + agent_font_size: defaults.agent_font_size.flatten().map(Into::into), theme_selection: defaults.theme.clone(), active_theme: themes .get(defaults.theme.as_ref().unwrap().theme(*system_appearance)) @@ -959,20 +962,20 @@ impl settings::Settings for ThemeSettings { } } - merge(&mut this.ui_font_size, value.ui_font_size.map(Into::into)); - this.ui_font_size = this.ui_font_size.clamp(px(6.), px(100.)); - + merge( + &mut this.ui_font_size, + value.ui_font_size.map(Into::into).map(clamp_font_size), + ); merge( &mut this.buffer_font_size, - value.buffer_font_size.map(Into::into), + value.buffer_font_size.map(Into::into).map(clamp_font_size), ); - this.buffer_font_size = this.buffer_font_size.clamp(px(6.), px(100.)); - merge( &mut this.agent_font_size, - value.agent_font_size.map(Into::into), + value + .agent_font_size + .map(|value| value.map(Into::into).map(clamp_font_size)), ); - this.agent_font_size = this.agent_font_size.clamp(px(6.), px(100.)); merge(&mut this.buffer_line_height, value.buffer_line_height); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index f04eeade73..e02324a142 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -107,6 +107,8 @@ pub fn init(themes_to_load: LoadThemes, cx: &mut App) { let mut prev_buffer_font_size_settings = ThemeSettings::get_global(cx).buffer_font_size_settings(); let mut prev_ui_font_size_settings = ThemeSettings::get_global(cx).ui_font_size_settings(); + let mut prev_agent_font_size_settings = + ThemeSettings::get_global(cx).agent_font_size_settings(); cx.observe_global::(move |cx| { let buffer_font_size_settings = ThemeSettings::get_global(cx).buffer_font_size_settings(); if buffer_font_size_settings != prev_buffer_font_size_settings { @@ -119,6 +121,12 @@ pub fn init(themes_to_load: LoadThemes, cx: &mut App) { prev_ui_font_size_settings = ui_font_size_settings; reset_ui_font_size(cx); } + + let agent_font_size_settings = ThemeSettings::get_global(cx).agent_font_size_settings(); + if agent_font_size_settings != prev_agent_font_size_settings { + prev_agent_font_size_settings = agent_font_size_settings; + reset_agent_font_size(cx); + } }) .detach(); } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index a324ba0932..cfafbb70f0 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -716,9 +716,7 @@ fn register_actions( .insert(theme::clamp_font_size(ui_font_size).0); }); } else { - theme::adjust_ui_font_size(cx, |size| { - *size += px(1.0); - }); + theme::adjust_ui_font_size(cx, |size| size + px(1.0)); } } }) @@ -733,9 +731,7 @@ fn register_actions( .insert(theme::clamp_font_size(ui_font_size).0); }); } else { - theme::adjust_ui_font_size(cx, |size| { - *size -= px(1.0); - }); + theme::adjust_ui_font_size(cx, |size| size - px(1.0)); } } }) @@ -763,9 +759,7 @@ fn register_actions( .insert(theme::clamp_font_size(buffer_font_size).0); }); } else { - theme::adjust_buffer_font_size(cx, |size| { - *size += px(1.0); - }); + theme::adjust_buffer_font_size(cx, |size| size + px(1.0)); } } }) @@ -781,9 +775,7 @@ fn register_actions( .insert(theme::clamp_font_size(buffer_font_size).0); }); } else { - theme::adjust_buffer_font_size(cx, |size| { - *size -= px(1.0); - }); + theme::adjust_buffer_font_size(cx, |size| size - px(1.0)); } } }) From 332626e5825564e97afc969292c90d9b0fb40b6d Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Sat, 16 Aug 2025 17:04:09 +0200 Subject: [PATCH 407/693] Allow Permission Request to only require a ToolCallUpdate instead of a full tool call (#36319) Release Notes: - N/A --- Cargo.lock | 4 +- Cargo.toml | 2 +- crates/acp_thread/src/acp_thread.rs | 36 ++++++++++------- crates/acp_thread/src/connection.rs | 4 +- crates/agent2/src/agent.rs | 11 ++--- crates/agent2/src/thread.rs | 40 ++++++------------- crates/agent2/src/tools/edit_file_tool.rs | 12 ++++-- crates/agent_servers/src/acp/v0.rs | 6 +-- crates/agent_servers/src/acp/v1.rs | 2 +- crates/agent_servers/src/claude.rs | 3 +- crates/agent_servers/src/claude/mcp_server.rs | 4 +- 11 files changed, 63 insertions(+), 61 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1bce72b3a1..f59d92739b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,9 +172,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.24" +version = "0.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd68bbbef8e424fb8a605c5f0b00c360f682c4528b0a5feb5ec928aaf5ce28e" +checksum = "2ab66add8be8d6a963f5bf4070045c1bbf36472837654c73e2298dd16bda5bf7" dependencies = [ "anyhow", "futures 0.3.31", diff --git a/Cargo.toml b/Cargo.toml index 644b6c0f40..b467e8743e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -426,7 +426,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.24" +agent-client-protocol = "0.0.25" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 2ef94a3cbe..3bb1b99ba1 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -792,7 +792,7 @@ impl AcpThread { &mut self, update: acp::SessionUpdate, cx: &mut Context, - ) -> Result<()> { + ) -> Result<(), acp::Error> { match update { acp::SessionUpdate::UserMessageChunk { content } => { self.push_user_content_block(None, content, cx); @@ -804,7 +804,7 @@ impl AcpThread { self.push_assistant_content_block(content, true, cx); } acp::SessionUpdate::ToolCall(tool_call) => { - self.upsert_tool_call(tool_call, cx); + self.upsert_tool_call(tool_call, cx)?; } acp::SessionUpdate::ToolCallUpdate(tool_call_update) => { self.update_tool_call(tool_call_update, cx)?; @@ -940,32 +940,40 @@ impl AcpThread { } /// Updates a tool call if id matches an existing entry, otherwise inserts a new one. - pub fn upsert_tool_call(&mut self, tool_call: acp::ToolCall, cx: &mut Context) { + pub fn upsert_tool_call( + &mut self, + tool_call: acp::ToolCall, + cx: &mut Context, + ) -> Result<(), acp::Error> { let status = ToolCallStatus::Allowed { status: tool_call.status, }; - self.upsert_tool_call_inner(tool_call, status, cx) + self.upsert_tool_call_inner(tool_call.into(), status, cx) } + /// Fails if id does not match an existing entry. pub fn upsert_tool_call_inner( &mut self, - tool_call: acp::ToolCall, + tool_call_update: acp::ToolCallUpdate, status: ToolCallStatus, cx: &mut Context, - ) { + ) -> Result<(), acp::Error> { let language_registry = self.project.read(cx).languages().clone(); - let call = ToolCall::from_acp(tool_call, status, language_registry, cx); - let id = call.id.clone(); + let id = tool_call_update.id.clone(); - if let Some((ix, current_call)) = self.tool_call_mut(&call.id) { - *current_call = call; + if let Some((ix, current_call)) = self.tool_call_mut(&id) { + current_call.update_fields(tool_call_update.fields, language_registry, cx); + current_call.status = status; cx.emit(AcpThreadEvent::EntryUpdated(ix)); } else { + let call = + ToolCall::from_acp(tool_call_update.try_into()?, status, language_registry, cx); self.push_entry(AgentThreadEntry::ToolCall(call), cx); }; self.resolve_locations(id, cx); + Ok(()) } fn tool_call_mut(&mut self, id: &acp::ToolCallId) -> Option<(usize, &mut ToolCall)> { @@ -1034,10 +1042,10 @@ impl AcpThread { pub fn request_tool_call_authorization( &mut self, - tool_call: acp::ToolCall, + tool_call: acp::ToolCallUpdate, options: Vec, cx: &mut Context, - ) -> oneshot::Receiver { + ) -> Result, acp::Error> { let (tx, rx) = oneshot::channel(); let status = ToolCallStatus::WaitingForConfirmation { @@ -1045,9 +1053,9 @@ impl AcpThread { respond_tx: tx, }; - self.upsert_tool_call_inner(tool_call, status, cx); + self.upsert_tool_call_inner(tool_call, status, cx)?; cx.emit(AcpThreadEvent::ToolAuthorizationRequired); - rx + Ok(rx) } pub fn authorize_tool_call( diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index b2116020fb..7497d2309f 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -286,12 +286,12 @@ mod test_support { if let Some((tool_call, options)) = permission_request { let permission = thread.update(cx, |thread, cx| { thread.request_tool_call_authorization( - tool_call.clone(), + tool_call.clone().into(), options.clone(), cx, ) })?; - permission.await?; + permission?.await?; } thread.update(cx, |thread, cx| { thread.handle_session_update(update.clone(), cx).unwrap(); diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 358365d11f..d63e3f8134 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -514,10 +514,11 @@ impl NativeAgentConnection { thread.request_tool_call_authorization(tool_call, options, cx) })?; cx.background_spawn(async move { - if let Some(option) = recv - .await - .context("authorization sender was dropped") - .log_err() + if let Some(recv) = recv.log_err() + && let Some(option) = recv + .await + .context("authorization sender was dropped") + .log_err() { response .send(option) @@ -530,7 +531,7 @@ impl NativeAgentConnection { AgentResponseEvent::ToolCall(tool_call) => { acp_thread.update(cx, |thread, cx| { thread.upsert_tool_call(tool_call, cx) - })?; + })??; } AgentResponseEvent::ToolCallUpdate(update) => { acp_thread.update(cx, |thread, cx| { diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index cfd67f4b05..0741bb9e08 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -448,7 +448,7 @@ pub enum AgentResponseEvent { #[derive(Debug)] pub struct ToolCallAuthorization { - pub tool_call: acp::ToolCall, + pub tool_call: acp::ToolCallUpdate, pub options: Vec, pub response: oneshot::Sender, } @@ -901,7 +901,7 @@ impl Thread { let fs = self.project.read(cx).fs().clone(); let tool_event_stream = - ToolCallEventStream::new(&tool_use, tool.kind(), event_stream.clone(), Some(fs)); + ToolCallEventStream::new(tool_use.id.clone(), event_stream.clone(), Some(fs)); tool_event_stream.update_fields(acp::ToolCallUpdateFields { status: Some(acp::ToolCallStatus::InProgress), ..Default::default() @@ -1344,8 +1344,6 @@ impl AgentResponseEventStream { #[derive(Clone)] pub struct ToolCallEventStream { tool_use_id: LanguageModelToolUseId, - kind: acp::ToolKind, - input: serde_json::Value, stream: AgentResponseEventStream, fs: Option>, } @@ -1355,32 +1353,19 @@ impl ToolCallEventStream { pub fn test() -> (Self, ToolCallEventStreamReceiver) { let (events_tx, events_rx) = mpsc::unbounded::>(); - let stream = ToolCallEventStream::new( - &LanguageModelToolUse { - id: "test_id".into(), - name: "test_tool".into(), - raw_input: String::new(), - input: serde_json::Value::Null, - is_input_complete: true, - }, - acp::ToolKind::Other, - AgentResponseEventStream(events_tx), - None, - ); + let stream = + ToolCallEventStream::new("test_id".into(), AgentResponseEventStream(events_tx), None); (stream, ToolCallEventStreamReceiver(events_rx)) } fn new( - tool_use: &LanguageModelToolUse, - kind: acp::ToolKind, + tool_use_id: LanguageModelToolUseId, stream: AgentResponseEventStream, fs: Option>, ) -> Self { Self { - tool_use_id: tool_use.id.clone(), - kind, - input: tool_use.input.clone(), + tool_use_id, stream, fs, } @@ -1427,12 +1412,13 @@ impl ToolCallEventStream { .0 .unbounded_send(Ok(AgentResponseEvent::ToolCallAuthorization( ToolCallAuthorization { - tool_call: AgentResponseEventStream::initial_tool_call( - &self.tool_use_id, - title.into(), - self.kind.clone(), - self.input.clone(), - ), + tool_call: acp::ToolCallUpdate { + id: acp::ToolCallId(self.tool_use_id.to_string().into()), + fields: acp::ToolCallUpdateFields { + title: Some(title.into()), + ..Default::default() + }, + }, options: vec![ acp::PermissionOption { id: acp::PermissionOptionId("always_allow".into()), diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index c77b9f6a69..4b4f98daec 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -1001,7 +1001,10 @@ mod tests { }); let event = stream_rx.expect_authorization().await; - assert_eq!(event.tool_call.title, "test 1 (local settings)"); + assert_eq!( + event.tool_call.fields.title, + Some("test 1 (local settings)".into()) + ); // Test 2: Path outside project should require confirmation let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); @@ -1018,7 +1021,7 @@ mod tests { }); let event = stream_rx.expect_authorization().await; - assert_eq!(event.tool_call.title, "test 2"); + assert_eq!(event.tool_call.fields.title, Some("test 2".into())); // Test 3: Relative path without .zed should not require confirmation let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); @@ -1051,7 +1054,10 @@ mod tests { ) }); let event = stream_rx.expect_authorization().await; - assert_eq!(event.tool_call.title, "test 4 (local settings)"); + assert_eq!( + event.tool_call.fields.title, + Some("test 4 (local settings)".into()) + ); // Test 5: When always_allow_tool_actions is enabled, no confirmation needed cx.update(|cx| { diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index e936c87643..74647f7313 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -135,9 +135,9 @@ impl acp_old::Client for OldAcpClientDelegate { let response = cx .update(|cx| { self.thread.borrow().update(cx, |thread, cx| { - thread.request_tool_call_authorization(tool_call, acp_options, cx) + thread.request_tool_call_authorization(tool_call.into(), acp_options, cx) }) - })? + })?? .context("Failed to update thread")? .await; @@ -168,7 +168,7 @@ impl acp_old::Client for OldAcpClientDelegate { cx, ) }) - })? + })?? .context("Failed to update thread")?; Ok(acp_old::PushToolCallResponse { diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index 6cf9801d06..506ae80886 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -233,7 +233,7 @@ impl acp::Client for ClientDelegate { thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx) })?; - let result = rx.await; + let result = rx?.await; let outcome = match result { Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option }, diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 14a179ba3d..4b3a173349 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -560,8 +560,9 @@ impl ClaudeAgentSession { thread.upsert_tool_call( claude_tool.as_acp(acp::ToolCallId(id.into())), cx, - ); + )?; } + anyhow::Ok(()) }) .log_err(); } diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/claude/mcp_server.rs index 53a8556e74..22cb2f8f8d 100644 --- a/crates/agent_servers/src/claude/mcp_server.rs +++ b/crates/agent_servers/src/claude/mcp_server.rs @@ -154,7 +154,7 @@ impl McpServerTool for PermissionTool { let chosen_option = thread .update(cx, |thread, cx| { thread.request_tool_call_authorization( - claude_tool.as_acp(tool_call_id), + claude_tool.as_acp(tool_call_id).into(), vec![ acp::PermissionOption { id: allow_option_id.clone(), @@ -169,7 +169,7 @@ impl McpServerTool for PermissionTool { ], cx, ) - })? + })?? .await?; let response = if chosen_option == allow_option_id { From 15a1eb2a2e3e249eae5ee402fc8a7a3d19260bf6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sat, 16 Aug 2025 13:02:51 -0400 Subject: [PATCH 408/693] emmet: Extract to zed-extensions/emmet repository (#36323) This PR extracts the Emmet extension to the [zed-extensions/emmet](https://github.com/zed-extensions/emmet) repository. Release Notes: - N/A --- .config/hakari.toml | 1 - Cargo.lock | 7 --- Cargo.toml | 1 - docs/src/languages/emmet.md | 2 + extensions/emmet/.gitignore | 3 - extensions/emmet/Cargo.toml | 16 ----- extensions/emmet/LICENSE-APACHE | 1 - extensions/emmet/extension.toml | 24 -------- extensions/emmet/src/emmet.rs | 106 -------------------------------- 9 files changed, 2 insertions(+), 159 deletions(-) delete mode 100644 extensions/emmet/.gitignore delete mode 100644 extensions/emmet/Cargo.toml delete mode 120000 extensions/emmet/LICENSE-APACHE delete mode 100644 extensions/emmet/extension.toml delete mode 100644 extensions/emmet/src/emmet.rs diff --git a/.config/hakari.toml b/.config/hakari.toml index f71e97b45c..8ce0b77490 100644 --- a/.config/hakari.toml +++ b/.config/hakari.toml @@ -34,7 +34,6 @@ workspace-members = [ "zed_extension_api", # exclude all extensions - "zed_emmet", "zed_glsl", "zed_html", "zed_proto", diff --git a/Cargo.lock b/Cargo.lock index f59d92739b..5100a63477 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20520,13 +20520,6 @@ dependencies = [ "workspace-hack", ] -[[package]] -name = "zed_emmet" -version = "0.0.6" -dependencies = [ - "zed_extension_api 0.1.0", -] - [[package]] name = "zed_extension_api" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index b467e8743e..a94db953ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -199,7 +199,6 @@ members = [ # Extensions # - "extensions/emmet", "extensions/glsl", "extensions/html", "extensions/proto", diff --git a/docs/src/languages/emmet.md b/docs/src/languages/emmet.md index 1a76291ad4..73e34c209f 100644 --- a/docs/src/languages/emmet.md +++ b/docs/src/languages/emmet.md @@ -1,5 +1,7 @@ # Emmet +Emmet support is available through the [Emmet extension](https://github.com/zed-extensions/emmet). + [Emmet](https://emmet.io/) is a web-developer’s toolkit that can greatly improve your HTML & CSS workflow. - Language Server: [olrtg/emmet-language-server](https://github.com/olrtg/emmet-language-server) diff --git a/extensions/emmet/.gitignore b/extensions/emmet/.gitignore deleted file mode 100644 index 62c0add260..0000000000 --- a/extensions/emmet/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.wasm -grammars -target diff --git a/extensions/emmet/Cargo.toml b/extensions/emmet/Cargo.toml deleted file mode 100644 index 2fbdf2a7e5..0000000000 --- a/extensions/emmet/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "zed_emmet" -version = "0.0.6" -edition.workspace = true -publish.workspace = true -license = "Apache-2.0" - -[lints] -workspace = true - -[lib] -path = "src/emmet.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = "0.1.0" diff --git a/extensions/emmet/LICENSE-APACHE b/extensions/emmet/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/extensions/emmet/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/emmet/extension.toml b/extensions/emmet/extension.toml deleted file mode 100644 index a1848400b8..0000000000 --- a/extensions/emmet/extension.toml +++ /dev/null @@ -1,24 +0,0 @@ -id = "emmet" -name = "Emmet" -description = "Emmet support" -version = "0.0.6" -schema_version = 1 -authors = ["Piotr Osiewicz "] -repository = "https://github.com/zed-industries/zed" - -[language_servers.emmet-language-server] -name = "Emmet Language Server" -language = "HTML" -languages = ["HTML", "PHP", "ERB", "HTML/ERB", "JavaScript", "TSX", "CSS", "HEEX", "Elixir", "Vue.js"] - -[language_servers.emmet-language-server.language_ids] -"HTML" = "html" -"PHP" = "php" -"ERB" = "eruby" -"HTML/ERB" = "eruby" -"JavaScript" = "javascriptreact" -"TSX" = "typescriptreact" -"CSS" = "css" -"HEEX" = "heex" -"Elixir" = "heex" -"Vue.js" = "vue" diff --git a/extensions/emmet/src/emmet.rs b/extensions/emmet/src/emmet.rs deleted file mode 100644 index 1434e16e88..0000000000 --- a/extensions/emmet/src/emmet.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::{env, fs}; -use zed_extension_api::{self as zed, Result}; - -struct EmmetExtension { - did_find_server: bool, -} - -const SERVER_PATH: &str = "node_modules/@olrtg/emmet-language-server/dist/index.js"; -const PACKAGE_NAME: &str = "@olrtg/emmet-language-server"; - -impl EmmetExtension { - fn server_exists(&self) -> bool { - fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file()) - } - - fn server_script_path(&mut self, language_server_id: &zed::LanguageServerId) -> Result { - let server_exists = self.server_exists(); - if self.did_find_server && server_exists { - return Ok(SERVER_PATH.to_string()); - } - - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::CheckingForUpdate, - ); - let version = zed::npm_package_latest_version(PACKAGE_NAME)?; - - if !server_exists - || zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version) - { - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::Downloading, - ); - let result = zed::npm_install_package(PACKAGE_NAME, &version); - match result { - Ok(()) => { - if !self.server_exists() { - Err(format!( - "installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'", - ))?; - } - } - Err(error) => { - if !self.server_exists() { - Err(error)?; - } - } - } - } - - self.did_find_server = true; - Ok(SERVER_PATH.to_string()) - } -} - -impl zed::Extension for EmmetExtension { - fn new() -> Self { - Self { - did_find_server: false, - } - } - - fn language_server_command( - &mut self, - language_server_id: &zed::LanguageServerId, - _worktree: &zed::Worktree, - ) -> Result { - let server_path = self.server_script_path(language_server_id)?; - Ok(zed::Command { - command: zed::node_binary_path()?, - args: vec![ - zed_ext::sanitize_windows_path(env::current_dir().unwrap()) - .join(&server_path) - .to_string_lossy() - .to_string(), - "--stdio".to_string(), - ], - env: Default::default(), - }) - } -} - -zed::register_extension!(EmmetExtension); - -/// Extensions to the Zed extension API that have not yet stabilized. -mod zed_ext { - /// Sanitizes the given path to remove the leading `/` on Windows. - /// - /// On macOS and Linux this is a no-op. - /// - /// This is a workaround for https://github.com/bytecodealliance/wasmtime/issues/10415. - pub fn sanitize_windows_path(path: std::path::PathBuf) -> std::path::PathBuf { - use zed_extension_api::{Os, current_platform}; - - let (os, _arch) = current_platform(); - match os { - Os::Mac | Os::Linux => path, - Os::Windows => path - .to_string_lossy() - .to_string() - .trim_start_matches('/') - .into(), - } - } -} From f17f63ec84424f772bfdb7c7998db598829596bf Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sat, 16 Aug 2025 15:00:31 -0400 Subject: [PATCH 409/693] Remove `/docs` slash command (#36325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR removes the `/docs` slash command. We never fully shipped this—with it requiring explicit opt-in via a setting—and it doesn't seem like the feature is needed in an agentic world. Release Notes: - Removed the `/docs` slash command. --- Cargo.lock | 30 - Cargo.toml | 2 - assets/settings/default.json | 5 - crates/agent_ui/Cargo.toml | 1 - crates/agent_ui/src/agent_configuration.rs | 1 - crates/agent_ui/src/agent_ui.rs | 7 - crates/agent_ui/src/slash_command_settings.rs | 11 - crates/agent_ui/src/text_thread_editor.rs | 86 +-- crates/assistant_slash_commands/Cargo.toml | 1 - .../src/assistant_slash_commands.rs | 2 - .../src/docs_command.rs | 543 --------------- crates/extension/src/extension_host_proxy.rs | 34 - crates/extension/src/extension_manifest.rs | 7 - crates/extension_cli/src/main.rs | 4 - .../extension_compilation_benchmark.rs | 1 - .../extension_host/src/capability_granter.rs | 1 - crates/extension_host/src/extension_host.rs | 15 +- .../src/extension_store_test.rs | 3 - crates/indexed_docs/Cargo.toml | 38 -- crates/indexed_docs/LICENSE-GPL | 1 - .../src/extension_indexed_docs_provider.rs | 81 --- crates/indexed_docs/src/indexed_docs.rs | 16 - crates/indexed_docs/src/providers.rs | 1 - crates/indexed_docs/src/providers/rustdoc.rs | 291 --------- .../src/providers/rustdoc/item.rs | 82 --- .../src/providers/rustdoc/popular_crates.txt | 252 ------- .../src/providers/rustdoc/to_markdown.rs | 618 ------------------ crates/indexed_docs/src/registry.rs | 62 -- crates/indexed_docs/src/store.rs | 346 ---------- typos.toml | 3 - 30 files changed, 6 insertions(+), 2539 deletions(-) delete mode 100644 crates/assistant_slash_commands/src/docs_command.rs delete mode 100644 crates/indexed_docs/Cargo.toml delete mode 120000 crates/indexed_docs/LICENSE-GPL delete mode 100644 crates/indexed_docs/src/extension_indexed_docs_provider.rs delete mode 100644 crates/indexed_docs/src/indexed_docs.rs delete mode 100644 crates/indexed_docs/src/providers.rs delete mode 100644 crates/indexed_docs/src/providers/rustdoc.rs delete mode 100644 crates/indexed_docs/src/providers/rustdoc/item.rs delete mode 100644 crates/indexed_docs/src/providers/rustdoc/popular_crates.txt delete mode 100644 crates/indexed_docs/src/providers/rustdoc/to_markdown.rs delete mode 100644 crates/indexed_docs/src/registry.rs delete mode 100644 crates/indexed_docs/src/store.rs diff --git a/Cargo.lock b/Cargo.lock index 5100a63477..b4bf705eb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -347,7 +347,6 @@ dependencies = [ "gpui", "html_to_markdown", "http_client", - "indexed_docs", "indoc", "inventory", "itertools 0.14.0", @@ -872,7 +871,6 @@ dependencies = [ "gpui", "html_to_markdown", "http_client", - "indexed_docs", "language", "pretty_assertions", "project", @@ -8383,34 +8381,6 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" -[[package]] -name = "indexed_docs" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "cargo_metadata", - "collections", - "derive_more 0.99.19", - "extension", - "fs", - "futures 0.3.31", - "fuzzy", - "gpui", - "heed", - "html_to_markdown", - "http_client", - "indexmap", - "indoc", - "parking_lot", - "paths", - "pretty_assertions", - "serde", - "strum 0.27.1", - "util", - "workspace-hack", -] - [[package]] name = "indexmap" version = "2.9.0" diff --git a/Cargo.toml b/Cargo.toml index a94db953ab..b3105bd97c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,7 +81,6 @@ members = [ "crates/http_client_tls", "crates/icons", "crates/image_viewer", - "crates/indexed_docs", "crates/edit_prediction", "crates/edit_prediction_button", "crates/inspector_ui", @@ -305,7 +304,6 @@ http_client = { path = "crates/http_client" } http_client_tls = { path = "crates/http_client_tls" } icons = { path = "crates/icons" } image_viewer = { path = "crates/image_viewer" } -indexed_docs = { path = "crates/indexed_docs" } edit_prediction = { path = "crates/edit_prediction" } edit_prediction_button = { path = "crates/edit_prediction_button" } inspector_ui = { path = "crates/inspector_ui" } diff --git a/assets/settings/default.json b/assets/settings/default.json index ff000001b5..6a8b034268 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -887,11 +887,6 @@ }, // The settings for slash commands. "slash_commands": { - // Settings for the `/docs` slash command. - "docs": { - // Whether `/docs` is enabled. - "enabled": false - }, // Settings for the `/project` slash command. "project": { // Whether `/project` is enabled. diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 13fd9d13c5..fbf8590e68 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -50,7 +50,6 @@ fuzzy.workspace = true gpui.workspace = true html_to_markdown.workspace = true http_client.workspace = true -indexed_docs.workspace = true indoc.workspace = true inventory.workspace = true itertools.workspace = true diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 96558f1bea..4a2dd88c33 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -1035,7 +1035,6 @@ fn extension_only_provides_context_server(manifest: &ExtensionManifest) -> bool && manifest.grammars.is_empty() && manifest.language_servers.is_empty() && manifest.slash_commands.is_empty() - && manifest.indexed_docs_providers.is_empty() && manifest.snippets.is_none() && manifest.debug_locators.is_empty() } diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 4f5f022593..f25b576886 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -242,7 +242,6 @@ pub fn init( client.telemetry().clone(), cx, ); - indexed_docs::init(cx); cx.observe_new(move |workspace, window, cx| { ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx) }) @@ -409,12 +408,6 @@ fn update_slash_commands_from_settings(cx: &mut App) { let slash_command_registry = SlashCommandRegistry::global(cx); let settings = SlashCommandSettings::get_global(cx); - if settings.docs.enabled { - slash_command_registry.register_command(assistant_slash_commands::DocsSlashCommand, true); - } else { - slash_command_registry.unregister_command(assistant_slash_commands::DocsSlashCommand); - } - if settings.cargo_workspace.enabled { slash_command_registry .register_command(assistant_slash_commands::CargoWorkspaceSlashCommand, true); diff --git a/crates/agent_ui/src/slash_command_settings.rs b/crates/agent_ui/src/slash_command_settings.rs index f254d00ec6..73e5622aa9 100644 --- a/crates/agent_ui/src/slash_command_settings.rs +++ b/crates/agent_ui/src/slash_command_settings.rs @@ -7,22 +7,11 @@ use settings::{Settings, SettingsSources}; /// Settings for slash commands. #[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)] pub struct SlashCommandSettings { - /// Settings for the `/docs` slash command. - #[serde(default)] - pub docs: DocsCommandSettings, /// Settings for the `/cargo-workspace` slash command. #[serde(default)] pub cargo_workspace: CargoWorkspaceCommandSettings, } -/// Settings for the `/docs` slash command. -#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)] -pub struct DocsCommandSettings { - /// Whether `/docs` is enabled. - #[serde(default)] - pub enabled: bool, -} - /// Settings for the `/cargo-workspace` slash command. #[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)] pub struct CargoWorkspaceCommandSettings { diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 2e3b4ed890..8c1e163eca 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -5,10 +5,7 @@ use crate::{ use agent_settings::{AgentSettings, CompletionMode}; use anyhow::Result; use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet}; -use assistant_slash_commands::{ - DefaultSlashCommand, DocsSlashCommand, DocsSlashCommandArgs, FileSlashCommand, - selections_creases, -}; +use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases}; use client::{proto, zed_urls}; use collections::{BTreeSet, HashMap, HashSet, hash_map}; use editor::{ @@ -30,7 +27,6 @@ use gpui::{ StatefulInteractiveElement, Styled, Subscription, Task, Transformation, WeakEntity, actions, div, img, percentage, point, prelude::*, pulsating_between, size, }; -use indexed_docs::IndexedDocsStore; use language::{ BufferSnapshot, LspAdapterDelegate, ToOffset, language_settings::{SoftWrap, all_language_settings}, @@ -77,7 +73,7 @@ use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker} use assistant_context::{ AssistantContext, CacheStatus, Content, ContextEvent, ContextId, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId, MessageMetadata, MessageStatus, - ParsedSlashCommand, PendingSlashCommandStatus, ThoughtProcessOutputSection, + PendingSlashCommandStatus, ThoughtProcessOutputSection, }; actions!( @@ -701,19 +697,7 @@ impl TextThreadEditor { } }; let render_trailer = { - let command = command.clone(); - move |row, _unfold, _window: &mut Window, cx: &mut App| { - // TODO: In the future we should investigate how we can expose - // this as a hook on the `SlashCommand` trait so that we don't - // need to special-case it here. - if command.name == DocsSlashCommand::NAME { - return render_docs_slash_command_trailer( - row, - command.clone(), - cx, - ); - } - + move |_row, _unfold, _window: &mut Window, _cx: &mut App| { Empty.into_any() } }; @@ -2398,70 +2382,6 @@ fn render_pending_slash_command_gutter_decoration( icon.into_any_element() } -fn render_docs_slash_command_trailer( - row: MultiBufferRow, - command: ParsedSlashCommand, - cx: &mut App, -) -> AnyElement { - if command.arguments.is_empty() { - return Empty.into_any(); - } - let args = DocsSlashCommandArgs::parse(&command.arguments); - - let Some(store) = args - .provider() - .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok()) - else { - return Empty.into_any(); - }; - - let Some(package) = args.package() else { - return Empty.into_any(); - }; - - let mut children = Vec::new(); - - if store.is_indexing(&package) { - children.push( - div() - .id(("crates-being-indexed", row.0)) - .child(Icon::new(IconName::ArrowCircle).with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(4)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - )) - .tooltip({ - let package = package.clone(); - Tooltip::text(format!("Indexing {package}…")) - }) - .into_any_element(), - ); - } - - if let Some(latest_error) = store.latest_error_for_package(&package) { - children.push( - div() - .id(("latest-error", row.0)) - .child( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .color(Color::Warning), - ) - .tooltip(Tooltip::text(format!("Failed to index: {latest_error}"))) - .into_any_element(), - ) - } - - let is_indexing = store.is_indexing(&package); - let latest_error = store.latest_error_for_package(&package); - - if !is_indexing && latest_error.is_none() { - return Empty.into_any(); - } - - h_flex().gap_2().children(children).into_any_element() -} - #[derive(Debug, Clone, Serialize, Deserialize)] struct CopyMetadata { creases: Vec, diff --git a/crates/assistant_slash_commands/Cargo.toml b/crates/assistant_slash_commands/Cargo.toml index f703a753f5..c054c3ced8 100644 --- a/crates/assistant_slash_commands/Cargo.toml +++ b/crates/assistant_slash_commands/Cargo.toml @@ -27,7 +27,6 @@ globset.workspace = true gpui.workspace = true html_to_markdown.workspace = true http_client.workspace = true -indexed_docs.workspace = true language.workspace = true project.workspace = true prompt_store.workspace = true diff --git a/crates/assistant_slash_commands/src/assistant_slash_commands.rs b/crates/assistant_slash_commands/src/assistant_slash_commands.rs index fa5dd8b683..fb00a91219 100644 --- a/crates/assistant_slash_commands/src/assistant_slash_commands.rs +++ b/crates/assistant_slash_commands/src/assistant_slash_commands.rs @@ -3,7 +3,6 @@ mod context_server_command; mod default_command; mod delta_command; mod diagnostics_command; -mod docs_command; mod fetch_command; mod file_command; mod now_command; @@ -18,7 +17,6 @@ pub use crate::context_server_command::*; pub use crate::default_command::*; pub use crate::delta_command::*; pub use crate::diagnostics_command::*; -pub use crate::docs_command::*; pub use crate::fetch_command::*; pub use crate::file_command::*; pub use crate::now_command::*; diff --git a/crates/assistant_slash_commands/src/docs_command.rs b/crates/assistant_slash_commands/src/docs_command.rs deleted file mode 100644 index bd87c72849..0000000000 --- a/crates/assistant_slash_commands/src/docs_command.rs +++ /dev/null @@ -1,543 +0,0 @@ -use std::path::Path; -use std::sync::Arc; -use std::sync::atomic::AtomicBool; -use std::time::Duration; - -use anyhow::{Context as _, Result, anyhow, bail}; -use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, - SlashCommandResult, -}; -use gpui::{App, BackgroundExecutor, Entity, Task, WeakEntity}; -use indexed_docs::{ - DocsDotRsProvider, IndexedDocsRegistry, IndexedDocsStore, LocalRustdocProvider, PackageName, - ProviderId, -}; -use language::{BufferSnapshot, LspAdapterDelegate}; -use project::{Project, ProjectPath}; -use ui::prelude::*; -use util::{ResultExt, maybe}; -use workspace::Workspace; - -pub struct DocsSlashCommand; - -impl DocsSlashCommand { - pub const NAME: &'static str = "docs"; - - fn path_to_cargo_toml(project: Entity, cx: &mut App) -> Option> { - let worktree = project.read(cx).worktrees(cx).next()?; - let worktree = worktree.read(cx); - let entry = worktree.entry_for_path("Cargo.toml")?; - let path = ProjectPath { - worktree_id: worktree.id(), - path: entry.path.clone(), - }; - Some(Arc::from( - project.read(cx).absolute_path(&path, cx)?.as_path(), - )) - } - - /// Ensures that the indexed doc providers for Rust are registered. - /// - /// Ideally we would do this sooner, but we need to wait until we're able to - /// access the workspace so we can read the project. - fn ensure_rust_doc_providers_are_registered( - &self, - workspace: Option>, - cx: &mut App, - ) { - let indexed_docs_registry = IndexedDocsRegistry::global(cx); - if indexed_docs_registry - .get_provider_store(LocalRustdocProvider::id()) - .is_none() - { - let index_provider_deps = maybe!({ - let workspace = workspace - .as_ref() - .context("no workspace")? - .upgrade() - .context("workspace dropped")?; - let project = workspace.read(cx).project().clone(); - let fs = project.read(cx).fs().clone(); - let cargo_workspace_root = Self::path_to_cargo_toml(project, cx) - .and_then(|path| path.parent().map(|path| path.to_path_buf())) - .context("no Cargo workspace root found")?; - - anyhow::Ok((fs, cargo_workspace_root)) - }); - - if let Some((fs, cargo_workspace_root)) = index_provider_deps.log_err() { - indexed_docs_registry.register_provider(Box::new(LocalRustdocProvider::new( - fs, - cargo_workspace_root, - ))); - } - } - - if indexed_docs_registry - .get_provider_store(DocsDotRsProvider::id()) - .is_none() - { - let http_client = maybe!({ - let workspace = workspace - .as_ref() - .context("no workspace")? - .upgrade() - .context("workspace was dropped")?; - let project = workspace.read(cx).project().clone(); - anyhow::Ok(project.read(cx).client().http_client()) - }); - - if let Some(http_client) = http_client.log_err() { - indexed_docs_registry - .register_provider(Box::new(DocsDotRsProvider::new(http_client))); - } - } - } - - /// Runs just-in-time indexing for a given package, in case the slash command - /// is run without any entries existing in the index. - fn run_just_in_time_indexing( - store: Arc, - key: String, - package: PackageName, - executor: BackgroundExecutor, - ) -> Task<()> { - executor.clone().spawn(async move { - let (prefix, needs_full_index) = if let Some((prefix, _)) = key.split_once('*') { - // If we have a wildcard in the search, we want to wait until - // we've completely finished indexing so we get a full set of - // results for the wildcard. - (prefix.to_string(), true) - } else { - (key, false) - }; - - // If we already have some entries, we assume that we've indexed the package before - // and don't need to do it again. - let has_any_entries = store - .any_with_prefix(prefix.clone()) - .await - .unwrap_or_default(); - if has_any_entries { - return (); - }; - - let index_task = store.clone().index(package.clone()); - - if needs_full_index { - _ = index_task.await; - } else { - loop { - executor.timer(Duration::from_millis(200)).await; - - if store - .any_with_prefix(prefix.clone()) - .await - .unwrap_or_default() - || !store.is_indexing(&package) - { - break; - } - } - } - }) - } -} - -impl SlashCommand for DocsSlashCommand { - fn name(&self) -> String { - Self::NAME.into() - } - - fn description(&self) -> String { - "insert docs".into() - } - - fn menu_text(&self) -> String { - "Insert Documentation".into() - } - - fn requires_argument(&self) -> bool { - true - } - - fn complete_argument( - self: Arc, - arguments: &[String], - _cancel: Arc, - workspace: Option>, - _: &mut Window, - cx: &mut App, - ) -> Task>> { - self.ensure_rust_doc_providers_are_registered(workspace, cx); - - let indexed_docs_registry = IndexedDocsRegistry::global(cx); - let args = DocsSlashCommandArgs::parse(arguments); - let store = args - .provider() - .context("no docs provider specified") - .and_then(|provider| IndexedDocsStore::try_global(provider, cx)); - cx.background_spawn(async move { - fn build_completions(items: Vec) -> Vec { - items - .into_iter() - .map(|item| ArgumentCompletion { - label: item.clone().into(), - new_text: item.to_string(), - after_completion: assistant_slash_command::AfterCompletion::Run, - replace_previous_arguments: false, - }) - .collect() - } - - match args { - DocsSlashCommandArgs::NoProvider => { - let providers = indexed_docs_registry.list_providers(); - if providers.is_empty() { - return Ok(vec![ArgumentCompletion { - label: "No available docs providers.".into(), - new_text: String::new(), - after_completion: false.into(), - replace_previous_arguments: false, - }]); - } - - Ok(providers - .into_iter() - .map(|provider| ArgumentCompletion { - label: provider.to_string().into(), - new_text: provider.to_string(), - after_completion: false.into(), - replace_previous_arguments: false, - }) - .collect()) - } - DocsSlashCommandArgs::SearchPackageDocs { - provider, - package, - index, - } => { - let store = store?; - - if index { - // We don't need to hold onto this task, as the `IndexedDocsStore` will hold it - // until it completes. - drop(store.clone().index(package.as_str().into())); - } - - let suggested_packages = store.clone().suggest_packages().await?; - let search_results = store.search(package).await; - - let mut items = build_completions(search_results); - let workspace_crate_completions = suggested_packages - .into_iter() - .filter(|package_name| { - !items - .iter() - .any(|item| item.label.text() == package_name.as_ref()) - }) - .map(|package_name| ArgumentCompletion { - label: format!("{package_name} (unindexed)").into(), - new_text: format!("{package_name}"), - after_completion: true.into(), - replace_previous_arguments: false, - }) - .collect::>(); - items.extend(workspace_crate_completions); - - if items.is_empty() { - return Ok(vec![ArgumentCompletion { - label: format!( - "Enter a {package_term} name.", - package_term = package_term(&provider) - ) - .into(), - new_text: provider.to_string(), - after_completion: false.into(), - replace_previous_arguments: false, - }]); - } - - Ok(items) - } - DocsSlashCommandArgs::SearchItemDocs { item_path, .. } => { - let store = store?; - let items = store.search(item_path).await; - Ok(build_completions(items)) - } - } - }) - } - - fn run( - self: Arc, - arguments: &[String], - _context_slash_command_output_sections: &[SlashCommandOutputSection], - _context_buffer: BufferSnapshot, - _workspace: WeakEntity, - _delegate: Option>, - _: &mut Window, - cx: &mut App, - ) -> Task { - if arguments.is_empty() { - return Task::ready(Err(anyhow!("missing an argument"))); - }; - - let args = DocsSlashCommandArgs::parse(arguments); - let executor = cx.background_executor().clone(); - let task = cx.background_spawn({ - let store = args - .provider() - .context("no docs provider specified") - .and_then(|provider| IndexedDocsStore::try_global(provider, cx)); - async move { - let (provider, key) = match args.clone() { - DocsSlashCommandArgs::NoProvider => bail!("no docs provider specified"), - DocsSlashCommandArgs::SearchPackageDocs { - provider, package, .. - } => (provider, package), - DocsSlashCommandArgs::SearchItemDocs { - provider, - item_path, - .. - } => (provider, item_path), - }; - - if key.trim().is_empty() { - bail!( - "no {package_term} name provided", - package_term = package_term(&provider) - ); - } - - let store = store?; - - if let Some(package) = args.package() { - Self::run_just_in_time_indexing(store.clone(), key.clone(), package, executor) - .await; - } - - let (text, ranges) = if let Some((prefix, _)) = key.split_once('*') { - let docs = store.load_many_by_prefix(prefix.to_string()).await?; - - let mut text = String::new(); - let mut ranges = Vec::new(); - - for (key, docs) in docs { - let prev_len = text.len(); - - text.push_str(&docs.0); - text.push_str("\n"); - ranges.push((key, prev_len..text.len())); - text.push_str("\n"); - } - - (text, ranges) - } else { - let item_docs = store.load(key.clone()).await?; - let text = item_docs.to_string(); - let range = 0..text.len(); - - (text, vec![(key, range)]) - }; - - anyhow::Ok((provider, text, ranges)) - } - }); - - cx.foreground_executor().spawn(async move { - let (provider, text, ranges) = task.await?; - Ok(SlashCommandOutput { - text, - sections: ranges - .into_iter() - .map(|(key, range)| SlashCommandOutputSection { - range, - icon: IconName::FileDoc, - label: format!("docs ({provider}): {key}",).into(), - metadata: None, - }) - .collect(), - run_commands_in_text: false, - } - .to_event_stream()) - }) - } -} - -fn is_item_path_delimiter(char: char) -> bool { - !char.is_alphanumeric() && char != '-' && char != '_' -} - -#[derive(Debug, PartialEq, Clone)] -pub enum DocsSlashCommandArgs { - NoProvider, - SearchPackageDocs { - provider: ProviderId, - package: String, - index: bool, - }, - SearchItemDocs { - provider: ProviderId, - package: String, - item_path: String, - }, -} - -impl DocsSlashCommandArgs { - pub fn parse(arguments: &[String]) -> Self { - let Some(provider) = arguments - .get(0) - .cloned() - .filter(|arg| !arg.trim().is_empty()) - else { - return Self::NoProvider; - }; - let provider = ProviderId(provider.into()); - let Some(argument) = arguments.get(1) else { - return Self::NoProvider; - }; - - if let Some((package, rest)) = argument.split_once(is_item_path_delimiter) { - if rest.trim().is_empty() { - Self::SearchPackageDocs { - provider, - package: package.to_owned(), - index: true, - } - } else { - Self::SearchItemDocs { - provider, - package: package.to_owned(), - item_path: argument.to_owned(), - } - } - } else { - Self::SearchPackageDocs { - provider, - package: argument.to_owned(), - index: false, - } - } - } - - pub fn provider(&self) -> Option { - match self { - Self::NoProvider => None, - Self::SearchPackageDocs { provider, .. } | Self::SearchItemDocs { provider, .. } => { - Some(provider.clone()) - } - } - } - - pub fn package(&self) -> Option { - match self { - Self::NoProvider => None, - Self::SearchPackageDocs { package, .. } | Self::SearchItemDocs { package, .. } => { - Some(package.as_str().into()) - } - } - } -} - -/// Returns the term used to refer to a package. -fn package_term(provider: &ProviderId) -> &'static str { - if provider == &DocsDotRsProvider::id() || provider == &LocalRustdocProvider::id() { - return "crate"; - } - - "package" -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_docs_slash_command_args() { - assert_eq!( - DocsSlashCommandArgs::parse(&["".to_string()]), - DocsSlashCommandArgs::NoProvider - ); - assert_eq!( - DocsSlashCommandArgs::parse(&["rustdoc".to_string()]), - DocsSlashCommandArgs::NoProvider - ); - - assert_eq!( - DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "".to_string()]), - DocsSlashCommandArgs::SearchPackageDocs { - provider: ProviderId("rustdoc".into()), - package: "".into(), - index: false - } - ); - assert_eq!( - DocsSlashCommandArgs::parse(&["gleam".to_string(), "".to_string()]), - DocsSlashCommandArgs::SearchPackageDocs { - provider: ProviderId("gleam".into()), - package: "".into(), - index: false - } - ); - - assert_eq!( - DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "gpui".to_string()]), - DocsSlashCommandArgs::SearchPackageDocs { - provider: ProviderId("rustdoc".into()), - package: "gpui".into(), - index: false, - } - ); - assert_eq!( - DocsSlashCommandArgs::parse(&["gleam".to_string(), "gleam_stdlib".to_string()]), - DocsSlashCommandArgs::SearchPackageDocs { - provider: ProviderId("gleam".into()), - package: "gleam_stdlib".into(), - index: false - } - ); - - // Adding an item path delimiter indicates we can start indexing. - assert_eq!( - DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "gpui:".to_string()]), - DocsSlashCommandArgs::SearchPackageDocs { - provider: ProviderId("rustdoc".into()), - package: "gpui".into(), - index: true, - } - ); - assert_eq!( - DocsSlashCommandArgs::parse(&["gleam".to_string(), "gleam_stdlib/".to_string()]), - DocsSlashCommandArgs::SearchPackageDocs { - provider: ProviderId("gleam".into()), - package: "gleam_stdlib".into(), - index: true - } - ); - - assert_eq!( - DocsSlashCommandArgs::parse(&[ - "rustdoc".to_string(), - "gpui::foo::bar::Baz".to_string() - ]), - DocsSlashCommandArgs::SearchItemDocs { - provider: ProviderId("rustdoc".into()), - package: "gpui".into(), - item_path: "gpui::foo::bar::Baz".into() - } - ); - assert_eq!( - DocsSlashCommandArgs::parse(&[ - "gleam".to_string(), - "gleam_stdlib/gleam/int".to_string() - ]), - DocsSlashCommandArgs::SearchItemDocs { - provider: ProviderId("gleam".into()), - package: "gleam_stdlib".into(), - item_path: "gleam_stdlib/gleam/int".into() - } - ); - } -} diff --git a/crates/extension/src/extension_host_proxy.rs b/crates/extension/src/extension_host_proxy.rs index 917739759f..6a24e3ba3f 100644 --- a/crates/extension/src/extension_host_proxy.rs +++ b/crates/extension/src/extension_host_proxy.rs @@ -28,7 +28,6 @@ pub struct ExtensionHostProxy { snippet_proxy: RwLock>>, slash_command_proxy: RwLock>>, context_server_proxy: RwLock>>, - indexed_docs_provider_proxy: RwLock>>, debug_adapter_provider_proxy: RwLock>>, } @@ -54,7 +53,6 @@ impl ExtensionHostProxy { snippet_proxy: RwLock::default(), slash_command_proxy: RwLock::default(), context_server_proxy: RwLock::default(), - indexed_docs_provider_proxy: RwLock::default(), debug_adapter_provider_proxy: RwLock::default(), } } @@ -87,14 +85,6 @@ impl ExtensionHostProxy { self.context_server_proxy.write().replace(Arc::new(proxy)); } - pub fn register_indexed_docs_provider_proxy( - &self, - proxy: impl ExtensionIndexedDocsProviderProxy, - ) { - self.indexed_docs_provider_proxy - .write() - .replace(Arc::new(proxy)); - } pub fn register_debug_adapter_proxy(&self, proxy: impl ExtensionDebugAdapterProviderProxy) { self.debug_adapter_provider_proxy .write() @@ -408,30 +398,6 @@ impl ExtensionContextServerProxy for ExtensionHostProxy { } } -pub trait ExtensionIndexedDocsProviderProxy: Send + Sync + 'static { - fn register_indexed_docs_provider(&self, extension: Arc, provider_id: Arc); - - fn unregister_indexed_docs_provider(&self, provider_id: Arc); -} - -impl ExtensionIndexedDocsProviderProxy for ExtensionHostProxy { - fn register_indexed_docs_provider(&self, extension: Arc, provider_id: Arc) { - let Some(proxy) = self.indexed_docs_provider_proxy.read().clone() else { - return; - }; - - proxy.register_indexed_docs_provider(extension, provider_id) - } - - fn unregister_indexed_docs_provider(&self, provider_id: Arc) { - let Some(proxy) = self.indexed_docs_provider_proxy.read().clone() else { - return; - }; - - proxy.unregister_indexed_docs_provider(provider_id) - } -} - pub trait ExtensionDebugAdapterProviderProxy: Send + Sync + 'static { fn register_debug_adapter( &self, diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index 5852b3e3fc..f5296198b0 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -84,8 +84,6 @@ pub struct ExtensionManifest { #[serde(default)] pub slash_commands: BTreeMap, SlashCommandManifestEntry>, #[serde(default)] - pub indexed_docs_providers: BTreeMap, IndexedDocsProviderEntry>, - #[serde(default)] pub snippets: Option, #[serde(default)] pub capabilities: Vec, @@ -195,9 +193,6 @@ pub struct SlashCommandManifestEntry { pub requires_argument: bool, } -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct IndexedDocsProviderEntry {} - #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] pub struct DebugAdapterManifestEntry { pub schema_path: Option, @@ -271,7 +266,6 @@ fn manifest_from_old_manifest( language_servers: Default::default(), context_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), - indexed_docs_providers: BTreeMap::default(), snippets: None, capabilities: Vec::new(), debug_adapters: Default::default(), @@ -304,7 +298,6 @@ mod tests { language_servers: BTreeMap::default(), context_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), - indexed_docs_providers: BTreeMap::default(), snippets: None, capabilities: vec![], debug_adapters: Default::default(), diff --git a/crates/extension_cli/src/main.rs b/crates/extension_cli/src/main.rs index ab4a9cddb0..d6c0501efd 100644 --- a/crates/extension_cli/src/main.rs +++ b/crates/extension_cli/src/main.rs @@ -144,10 +144,6 @@ fn extension_provides(manifest: &ExtensionManifest) -> BTreeSet ExtensionManifest { .collect(), context_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), - indexed_docs_providers: BTreeMap::default(), snippets: None, capabilities: vec![ExtensionCapability::ProcessExec( extension::ProcessExecCapability { diff --git a/crates/extension_host/src/capability_granter.rs b/crates/extension_host/src/capability_granter.rs index c77e5ecba1..5a2093c1dd 100644 --- a/crates/extension_host/src/capability_granter.rs +++ b/crates/extension_host/src/capability_granter.rs @@ -108,7 +108,6 @@ mod tests { language_servers: BTreeMap::default(), context_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), - indexed_docs_providers: BTreeMap::default(), snippets: None, capabilities: vec![], debug_adapters: Default::default(), diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 67baf4e692..46deacfe69 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -16,9 +16,9 @@ pub use extension::ExtensionManifest; use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; use extension::{ ExtensionContextServerProxy, ExtensionDebugAdapterProviderProxy, ExtensionEvents, - ExtensionGrammarProxy, ExtensionHostProxy, ExtensionIndexedDocsProviderProxy, - ExtensionLanguageProxy, ExtensionLanguageServerProxy, ExtensionSlashCommandProxy, - ExtensionSnippetProxy, ExtensionThemeProxy, + ExtensionGrammarProxy, ExtensionHostProxy, ExtensionLanguageProxy, + ExtensionLanguageServerProxy, ExtensionSlashCommandProxy, ExtensionSnippetProxy, + ExtensionThemeProxy, }; use fs::{Fs, RemoveOptions}; use futures::future::join_all; @@ -1192,10 +1192,6 @@ impl ExtensionStore { for (command_name, _) in &extension.manifest.slash_commands { self.proxy.unregister_slash_command(command_name.clone()); } - for (provider_id, _) in &extension.manifest.indexed_docs_providers { - self.proxy - .unregister_indexed_docs_provider(provider_id.clone()); - } } self.wasm_extensions @@ -1399,11 +1395,6 @@ impl ExtensionStore { .register_context_server(extension.clone(), id.clone(), cx); } - for (provider_id, _provider) in &manifest.indexed_docs_providers { - this.proxy - .register_indexed_docs_provider(extension.clone(), provider_id.clone()); - } - for (debug_adapter, meta) in &manifest.debug_adapters { let mut path = root_dir.clone(); path.push(Path::new(manifest.id.as_ref())); diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index c31774c20d..347a610439 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -160,7 +160,6 @@ async fn test_extension_store(cx: &mut TestAppContext) { language_servers: BTreeMap::default(), context_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), - indexed_docs_providers: BTreeMap::default(), snippets: None, capabilities: Vec::new(), debug_adapters: Default::default(), @@ -191,7 +190,6 @@ async fn test_extension_store(cx: &mut TestAppContext) { language_servers: BTreeMap::default(), context_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), - indexed_docs_providers: BTreeMap::default(), snippets: None, capabilities: Vec::new(), debug_adapters: Default::default(), @@ -371,7 +369,6 @@ async fn test_extension_store(cx: &mut TestAppContext) { language_servers: BTreeMap::default(), context_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), - indexed_docs_providers: BTreeMap::default(), snippets: None, capabilities: Vec::new(), debug_adapters: Default::default(), diff --git a/crates/indexed_docs/Cargo.toml b/crates/indexed_docs/Cargo.toml deleted file mode 100644 index eb269ad939..0000000000 --- a/crates/indexed_docs/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "indexed_docs" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/indexed_docs.rs" - -[dependencies] -anyhow.workspace = true -async-trait.workspace = true -cargo_metadata.workspace = true -collections.workspace = true -derive_more.workspace = true -extension.workspace = true -fs.workspace = true -futures.workspace = true -fuzzy.workspace = true -gpui.workspace = true -heed.workspace = true -html_to_markdown.workspace = true -http_client.workspace = true -indexmap.workspace = true -parking_lot.workspace = true -paths.workspace = true -serde.workspace = true -strum.workspace = true -util.workspace = true -workspace-hack.workspace = true - -[dev-dependencies] -indoc.workspace = true -pretty_assertions.workspace = true diff --git a/crates/indexed_docs/LICENSE-GPL b/crates/indexed_docs/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/indexed_docs/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/indexed_docs/src/extension_indexed_docs_provider.rs b/crates/indexed_docs/src/extension_indexed_docs_provider.rs deleted file mode 100644 index c77ea4066d..0000000000 --- a/crates/indexed_docs/src/extension_indexed_docs_provider.rs +++ /dev/null @@ -1,81 +0,0 @@ -use std::path::PathBuf; -use std::sync::Arc; - -use anyhow::Result; -use async_trait::async_trait; -use extension::{Extension, ExtensionHostProxy, ExtensionIndexedDocsProviderProxy}; -use gpui::App; - -use crate::{ - IndexedDocsDatabase, IndexedDocsProvider, IndexedDocsRegistry, PackageName, ProviderId, -}; - -pub fn init(cx: &mut App) { - let proxy = ExtensionHostProxy::default_global(cx); - proxy.register_indexed_docs_provider_proxy(IndexedDocsRegistryProxy { - indexed_docs_registry: IndexedDocsRegistry::global(cx), - }); -} - -struct IndexedDocsRegistryProxy { - indexed_docs_registry: Arc, -} - -impl ExtensionIndexedDocsProviderProxy for IndexedDocsRegistryProxy { - fn register_indexed_docs_provider(&self, extension: Arc, provider_id: Arc) { - self.indexed_docs_registry - .register_provider(Box::new(ExtensionIndexedDocsProvider::new( - extension, - ProviderId(provider_id), - ))); - } - - fn unregister_indexed_docs_provider(&self, provider_id: Arc) { - self.indexed_docs_registry - .unregister_provider(&ProviderId(provider_id)); - } -} - -pub struct ExtensionIndexedDocsProvider { - extension: Arc, - id: ProviderId, -} - -impl ExtensionIndexedDocsProvider { - pub fn new(extension: Arc, id: ProviderId) -> Self { - Self { extension, id } - } -} - -#[async_trait] -impl IndexedDocsProvider for ExtensionIndexedDocsProvider { - fn id(&self) -> ProviderId { - self.id.clone() - } - - fn database_path(&self) -> PathBuf { - let mut database_path = PathBuf::from(self.extension.work_dir().as_ref()); - database_path.push("docs"); - database_path.push(format!("{}.0.mdb", self.id)); - - database_path - } - - async fn suggest_packages(&self) -> Result> { - let packages = self - .extension - .suggest_docs_packages(self.id.0.clone()) - .await?; - - Ok(packages - .into_iter() - .map(|package| PackageName::from(package.as_str())) - .collect()) - } - - async fn index(&self, package: PackageName, database: Arc) -> Result<()> { - self.extension - .index_docs(self.id.0.clone(), package.as_ref().into(), database) - .await - } -} diff --git a/crates/indexed_docs/src/indexed_docs.rs b/crates/indexed_docs/src/indexed_docs.rs deleted file mode 100644 index 97538329d4..0000000000 --- a/crates/indexed_docs/src/indexed_docs.rs +++ /dev/null @@ -1,16 +0,0 @@ -mod extension_indexed_docs_provider; -mod providers; -mod registry; -mod store; - -use gpui::App; - -pub use crate::extension_indexed_docs_provider::ExtensionIndexedDocsProvider; -pub use crate::providers::rustdoc::*; -pub use crate::registry::*; -pub use crate::store::*; - -pub fn init(cx: &mut App) { - IndexedDocsRegistry::init_global(cx); - extension_indexed_docs_provider::init(cx); -} diff --git a/crates/indexed_docs/src/providers.rs b/crates/indexed_docs/src/providers.rs deleted file mode 100644 index c6505a2ab6..0000000000 --- a/crates/indexed_docs/src/providers.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod rustdoc; diff --git a/crates/indexed_docs/src/providers/rustdoc.rs b/crates/indexed_docs/src/providers/rustdoc.rs deleted file mode 100644 index ac6dc3a10b..0000000000 --- a/crates/indexed_docs/src/providers/rustdoc.rs +++ /dev/null @@ -1,291 +0,0 @@ -mod item; -mod to_markdown; - -use cargo_metadata::MetadataCommand; -use futures::future::BoxFuture; -pub use item::*; -use parking_lot::RwLock; -pub use to_markdown::convert_rustdoc_to_markdown; - -use std::collections::BTreeSet; -use std::path::PathBuf; -use std::sync::{Arc, LazyLock}; -use std::time::{Duration, Instant}; - -use anyhow::{Context as _, Result, bail}; -use async_trait::async_trait; -use collections::{HashSet, VecDeque}; -use fs::Fs; -use futures::{AsyncReadExt, FutureExt}; -use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; - -use crate::{IndexedDocsDatabase, IndexedDocsProvider, PackageName, ProviderId}; - -#[derive(Debug)] -struct RustdocItemWithHistory { - pub item: RustdocItem, - #[cfg(debug_assertions)] - pub history: Vec, -} - -pub struct LocalRustdocProvider { - fs: Arc, - cargo_workspace_root: PathBuf, -} - -impl LocalRustdocProvider { - pub fn id() -> ProviderId { - ProviderId("rustdoc".into()) - } - - pub fn new(fs: Arc, cargo_workspace_root: PathBuf) -> Self { - Self { - fs, - cargo_workspace_root, - } - } -} - -#[async_trait] -impl IndexedDocsProvider for LocalRustdocProvider { - fn id(&self) -> ProviderId { - Self::id() - } - - fn database_path(&self) -> PathBuf { - paths::data_dir().join("docs/rust/rustdoc-db.1.mdb") - } - - async fn suggest_packages(&self) -> Result> { - static WORKSPACE_CRATES: LazyLock, Instant)>>> = - LazyLock::new(|| RwLock::new(None)); - - if let Some((crates, fetched_at)) = &*WORKSPACE_CRATES.read() { - if fetched_at.elapsed() < Duration::from_secs(300) { - return Ok(crates.iter().cloned().collect()); - } - } - - let workspace = MetadataCommand::new() - .manifest_path(self.cargo_workspace_root.join("Cargo.toml")) - .exec() - .context("failed to load cargo metadata")?; - - let workspace_crates = workspace - .packages - .into_iter() - .map(|package| PackageName::from(package.name.as_str())) - .collect::>(); - - *WORKSPACE_CRATES.write() = Some((workspace_crates.clone(), Instant::now())); - - Ok(workspace_crates.into_iter().collect()) - } - - async fn index(&self, package: PackageName, database: Arc) -> Result<()> { - index_rustdoc(package, database, { - move |crate_name, item| { - let fs = self.fs.clone(); - let cargo_workspace_root = self.cargo_workspace_root.clone(); - let crate_name = crate_name.clone(); - let item = item.cloned(); - async move { - let target_doc_path = cargo_workspace_root.join("target/doc"); - let mut local_cargo_doc_path = target_doc_path.join(crate_name.as_ref().replace('-', "_")); - - if !fs.is_dir(&local_cargo_doc_path).await { - let cargo_doc_exists_at_all = fs.is_dir(&target_doc_path).await; - if cargo_doc_exists_at_all { - bail!( - "no docs directory for '{crate_name}'. if this is a valid crate name, try running `cargo doc`" - ); - } else { - bail!("no cargo doc directory. run `cargo doc`"); - } - } - - if let Some(item) = item { - local_cargo_doc_path.push(item.url_path()); - } else { - local_cargo_doc_path.push("index.html"); - } - - let Ok(contents) = fs.load(&local_cargo_doc_path).await else { - return Ok(None); - }; - - Ok(Some(contents)) - } - .boxed() - } - }) - .await - } -} - -pub struct DocsDotRsProvider { - http_client: Arc, -} - -impl DocsDotRsProvider { - pub fn id() -> ProviderId { - ProviderId("docs-rs".into()) - } - - pub fn new(http_client: Arc) -> Self { - Self { http_client } - } -} - -#[async_trait] -impl IndexedDocsProvider for DocsDotRsProvider { - fn id(&self) -> ProviderId { - Self::id() - } - - fn database_path(&self) -> PathBuf { - paths::data_dir().join("docs/rust/docs-rs-db.1.mdb") - } - - async fn suggest_packages(&self) -> Result> { - static POPULAR_CRATES: LazyLock> = LazyLock::new(|| { - include_str!("./rustdoc/popular_crates.txt") - .lines() - .filter(|line| !line.starts_with('#')) - .map(|line| PackageName::from(line.trim())) - .collect() - }); - - Ok(POPULAR_CRATES.clone()) - } - - async fn index(&self, package: PackageName, database: Arc) -> Result<()> { - index_rustdoc(package, database, { - move |crate_name, item| { - let http_client = self.http_client.clone(); - let crate_name = crate_name.clone(); - let item = item.cloned(); - async move { - let version = "latest"; - let path = format!( - "{crate_name}/{version}/{crate_name}{item_path}", - item_path = item - .map(|item| format!("/{}", item.url_path())) - .unwrap_or_default() - ); - - let mut response = http_client - .get( - &format!("https://docs.rs/{path}"), - AsyncBody::default(), - true, - ) - .await?; - - let mut body = Vec::new(); - response - .body_mut() - .read_to_end(&mut body) - .await - .context("error reading docs.rs response body")?; - - if response.status().is_client_error() { - let text = String::from_utf8_lossy(body.as_slice()); - bail!( - "status error {}, response: {text:?}", - response.status().as_u16() - ); - } - - Ok(Some(String::from_utf8(body)?)) - } - .boxed() - } - }) - .await - } -} - -async fn index_rustdoc( - package: PackageName, - database: Arc, - fetch_page: impl Fn( - &PackageName, - Option<&RustdocItem>, - ) -> BoxFuture<'static, Result>> - + Send - + Sync, -) -> Result<()> { - let Some(package_root_content) = fetch_page(&package, None).await? else { - return Ok(()); - }; - - let (crate_root_markdown, items) = - convert_rustdoc_to_markdown(package_root_content.as_bytes())?; - - database - .insert(package.to_string(), crate_root_markdown) - .await?; - - let mut seen_items = HashSet::from_iter(items.clone()); - let mut items_to_visit: VecDeque = - VecDeque::from_iter(items.into_iter().map(|item| RustdocItemWithHistory { - item, - #[cfg(debug_assertions)] - history: Vec::new(), - })); - - while let Some(item_with_history) = items_to_visit.pop_front() { - let item = &item_with_history.item; - - let Some(result) = fetch_page(&package, Some(item)).await.with_context(|| { - #[cfg(debug_assertions)] - { - format!( - "failed to fetch {item:?}: {history:?}", - history = item_with_history.history - ) - } - - #[cfg(not(debug_assertions))] - { - format!("failed to fetch {item:?}") - } - })? - else { - continue; - }; - - let (markdown, referenced_items) = convert_rustdoc_to_markdown(result.as_bytes())?; - - database - .insert(format!("{package}::{}", item.display()), markdown) - .await?; - - let parent_item = item; - for mut item in referenced_items { - if seen_items.contains(&item) { - continue; - } - - seen_items.insert(item.clone()); - - item.path.extend(parent_item.path.clone()); - if parent_item.kind == RustdocItemKind::Mod { - item.path.push(parent_item.name.clone()); - } - - items_to_visit.push_back(RustdocItemWithHistory { - #[cfg(debug_assertions)] - history: { - let mut history = item_with_history.history.clone(); - history.push(item.url_path()); - history - }, - item, - }); - } - } - - Ok(()) -} diff --git a/crates/indexed_docs/src/providers/rustdoc/item.rs b/crates/indexed_docs/src/providers/rustdoc/item.rs deleted file mode 100644 index 7d9023ef3e..0000000000 --- a/crates/indexed_docs/src/providers/rustdoc/item.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::sync::Arc; - -use serde::{Deserialize, Serialize}; -use strum::EnumIter; - -#[derive( - Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize, EnumIter, -)] -#[serde(rename_all = "snake_case")] -pub enum RustdocItemKind { - Mod, - Macro, - Struct, - Enum, - Constant, - Trait, - Function, - TypeAlias, - AttributeMacro, - DeriveMacro, -} - -impl RustdocItemKind { - pub(crate) const fn class(&self) -> &'static str { - match self { - Self::Mod => "mod", - Self::Macro => "macro", - Self::Struct => "struct", - Self::Enum => "enum", - Self::Constant => "constant", - Self::Trait => "trait", - Self::Function => "fn", - Self::TypeAlias => "type", - Self::AttributeMacro => "attr", - Self::DeriveMacro => "derive", - } - } -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] -pub struct RustdocItem { - pub kind: RustdocItemKind, - /// The item path, up until the name of the item. - pub path: Vec>, - /// The name of the item. - pub name: Arc, -} - -impl RustdocItem { - pub fn display(&self) -> String { - let mut path_segments = self.path.clone(); - path_segments.push(self.name.clone()); - - path_segments.join("::") - } - - pub fn url_path(&self) -> String { - let name = &self.name; - let mut path_components = self.path.clone(); - - match self.kind { - RustdocItemKind::Mod => { - path_components.push(name.clone()); - path_components.push("index.html".into()); - } - RustdocItemKind::Macro - | RustdocItemKind::Struct - | RustdocItemKind::Enum - | RustdocItemKind::Constant - | RustdocItemKind::Trait - | RustdocItemKind::Function - | RustdocItemKind::TypeAlias - | RustdocItemKind::AttributeMacro - | RustdocItemKind::DeriveMacro => { - path_components - .push(format!("{kind}.{name}.html", kind = self.kind.class()).into()); - } - } - - path_components.join("/") - } -} diff --git a/crates/indexed_docs/src/providers/rustdoc/popular_crates.txt b/crates/indexed_docs/src/providers/rustdoc/popular_crates.txt deleted file mode 100644 index ce2c3d51d8..0000000000 --- a/crates/indexed_docs/src/providers/rustdoc/popular_crates.txt +++ /dev/null @@ -1,252 +0,0 @@ -# A list of the most popular Rust crates. -# Sourced from https://lib.rs/std. -serde -serde_json -syn -clap -thiserror -rand -log -tokio -anyhow -regex -quote -proc-macro2 -base64 -itertools -chrono -lazy_static -once_cell -libc -reqwest -futures -bitflags -tracing -url -bytes -toml -tempfile -uuid -indexmap -env_logger -num-traits -async-trait -sha2 -hex -tracing-subscriber -http -parking_lot -cfg-if -futures-util -cc -hashbrown -rayon -hyper -getrandom -semver -strum -flate2 -tokio-util -smallvec -criterion -paste -heck -rand_core -nom -rustls -nix -glob -time -byteorder -strum_macros -serde_yaml -wasm-bindgen -ahash -either -num_cpus -rand_chacha -prost -percent-encoding -pin-project-lite -tokio-stream -bincode -walkdir -bindgen -axum -windows-sys -futures-core -ring -digest -num-bigint -rustls-pemfile -serde_with -crossbeam-channel -tokio-rustls -hmac -fastrand -dirs -zeroize -socket2 -pin-project -tower -derive_more -memchr -toml_edit -static_assertions -pretty_assertions -js-sys -convert_case -unicode-width -pkg-config -itoa -colored -rustc-hash -darling -mime -web-sys -image -bytemuck -which -sha1 -dashmap -arrayvec -fnv -tonic -humantime -libloading -winapi -rustc_version -http-body -indoc -num -home -serde_urlencoded -http-body-util -unicode-segmentation -num-integer -webpki-roots -phf -futures-channel -indicatif -petgraph -ordered-float -strsim -zstd -console -encoding_rs -wasm-bindgen-futures -urlencoding -subtle -crc32fast -slab -rustix -predicates -spin -hyper-rustls -backtrace -rustversion -mio -scopeguard -proc-macro-error -hyper-util -ryu -prost-types -textwrap -memmap2 -zip -zerocopy -generic-array -tar -pyo3 -async-stream -quick-xml -memoffset -csv -crossterm -windows -num_enum -tokio-tungstenite -crossbeam-utils -async-channel -lru -aes -futures-lite -tracing-core -prettyplease -httparse -serde_bytes -tracing-log -tower-service -cargo_metadata -pest -mime_guess -tower-http -data-encoding -native-tls -prost-build -proptest -derivative -serial_test -libm -half -futures-io -bitvec -rustls-native-certs -ureq -object -anstyle -tonic-build -form_urlencoded -num-derive -pest_derive -schemars -proc-macro-crate -rstest -futures-executor -assert_cmd -termcolor -serde_repr -ctrlc -sha3 -clap_complete -flume -mockall -ipnet -aho-corasick -atty -signal-hook -async-std -filetime -num-complex -opentelemetry -cmake -arc-swap -derive_builder -async-recursion -dyn-clone -bumpalo -fs_extra -git2 -sysinfo -shlex -instant -approx -rmp-serde -rand_distr -rustls-pki-types -maplit -sqlx -blake3 -hyper-tls -dotenvy -jsonwebtoken -openssl-sys -crossbeam -camino -winreg -config -rsa -bit-vec -chrono-tz -async-lock -bstr diff --git a/crates/indexed_docs/src/providers/rustdoc/to_markdown.rs b/crates/indexed_docs/src/providers/rustdoc/to_markdown.rs deleted file mode 100644 index 87e3863728..0000000000 --- a/crates/indexed_docs/src/providers/rustdoc/to_markdown.rs +++ /dev/null @@ -1,618 +0,0 @@ -use std::cell::RefCell; -use std::io::Read; -use std::rc::Rc; - -use anyhow::Result; -use html_to_markdown::markdown::{ - HeadingHandler, ListHandler, ParagraphHandler, StyledTextHandler, TableHandler, -}; -use html_to_markdown::{ - HandleTag, HandlerOutcome, HtmlElement, MarkdownWriter, StartTagOutcome, TagHandler, - convert_html_to_markdown, -}; -use indexmap::IndexSet; -use strum::IntoEnumIterator; - -use crate::{RustdocItem, RustdocItemKind}; - -/// Converts the provided rustdoc HTML to Markdown. -pub fn convert_rustdoc_to_markdown(html: impl Read) -> Result<(String, Vec)> { - let item_collector = Rc::new(RefCell::new(RustdocItemCollector::new())); - - let mut handlers: Vec = vec![ - Rc::new(RefCell::new(ParagraphHandler)), - Rc::new(RefCell::new(HeadingHandler)), - Rc::new(RefCell::new(ListHandler)), - Rc::new(RefCell::new(TableHandler::new())), - Rc::new(RefCell::new(StyledTextHandler)), - Rc::new(RefCell::new(RustdocChromeRemover)), - Rc::new(RefCell::new(RustdocHeadingHandler)), - Rc::new(RefCell::new(RustdocCodeHandler)), - Rc::new(RefCell::new(RustdocItemHandler)), - item_collector.clone(), - ]; - - let markdown = convert_html_to_markdown(html, &mut handlers)?; - - let items = item_collector - .borrow() - .items - .iter() - .cloned() - .collect::>(); - - Ok((markdown, items)) -} - -pub struct RustdocHeadingHandler; - -impl HandleTag for RustdocHeadingHandler { - fn should_handle(&self, _tag: &str) -> bool { - // We're only handling text, so we don't need to visit any tags. - false - } - - fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome { - if writer.is_inside("h1") - || writer.is_inside("h2") - || writer.is_inside("h3") - || writer.is_inside("h4") - || writer.is_inside("h5") - || writer.is_inside("h6") - { - let text = text - .trim_matches(|char| char == '\n' || char == '\r') - .replace('\n', " "); - writer.push_str(&text); - - return HandlerOutcome::Handled; - } - - HandlerOutcome::NoOp - } -} - -pub struct RustdocCodeHandler; - -impl HandleTag for RustdocCodeHandler { - fn should_handle(&self, tag: &str) -> bool { - matches!(tag, "pre" | "code") - } - - fn handle_tag_start( - &mut self, - tag: &HtmlElement, - writer: &mut MarkdownWriter, - ) -> StartTagOutcome { - match tag.tag() { - "code" => { - if !writer.is_inside("pre") { - writer.push_str("`"); - } - } - "pre" => { - let classes = tag.classes(); - let is_rust = classes.iter().any(|class| class == "rust"); - let language = is_rust - .then_some("rs") - .or_else(|| { - classes.iter().find_map(|class| { - if let Some((_, language)) = class.split_once("language-") { - Some(language.trim()) - } else { - None - } - }) - }) - .unwrap_or(""); - - writer.push_str(&format!("\n\n```{language}\n")); - } - _ => {} - } - - StartTagOutcome::Continue - } - - fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) { - match tag.tag() { - "code" => { - if !writer.is_inside("pre") { - writer.push_str("`"); - } - } - "pre" => writer.push_str("\n```\n"), - _ => {} - } - } - - fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome { - if writer.is_inside("pre") { - writer.push_str(text); - return HandlerOutcome::Handled; - } - - HandlerOutcome::NoOp - } -} - -const RUSTDOC_ITEM_NAME_CLASS: &str = "item-name"; - -pub struct RustdocItemHandler; - -impl RustdocItemHandler { - /// Returns whether we're currently inside of an `.item-name` element, which - /// rustdoc uses to display Rust items in a list. - fn is_inside_item_name(writer: &MarkdownWriter) -> bool { - writer - .current_element_stack() - .iter() - .any(|element| element.has_class(RUSTDOC_ITEM_NAME_CLASS)) - } -} - -impl HandleTag for RustdocItemHandler { - fn should_handle(&self, tag: &str) -> bool { - matches!(tag, "div" | "span") - } - - fn handle_tag_start( - &mut self, - tag: &HtmlElement, - writer: &mut MarkdownWriter, - ) -> StartTagOutcome { - match tag.tag() { - "div" | "span" => { - if Self::is_inside_item_name(writer) && tag.has_class("stab") { - writer.push_str(" ["); - } - } - _ => {} - } - - StartTagOutcome::Continue - } - - fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) { - match tag.tag() { - "div" | "span" => { - if tag.has_class(RUSTDOC_ITEM_NAME_CLASS) { - writer.push_str(": "); - } - - if Self::is_inside_item_name(writer) && tag.has_class("stab") { - writer.push_str("]"); - } - } - _ => {} - } - } - - fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome { - if Self::is_inside_item_name(writer) - && !writer.is_inside("span") - && !writer.is_inside("code") - { - writer.push_str(&format!("`{text}`")); - return HandlerOutcome::Handled; - } - - HandlerOutcome::NoOp - } -} - -pub struct RustdocChromeRemover; - -impl HandleTag for RustdocChromeRemover { - fn should_handle(&self, tag: &str) -> bool { - matches!( - tag, - "head" | "script" | "nav" | "summary" | "button" | "a" | "div" | "span" - ) - } - - fn handle_tag_start( - &mut self, - tag: &HtmlElement, - _writer: &mut MarkdownWriter, - ) -> StartTagOutcome { - match tag.tag() { - "head" | "script" | "nav" => return StartTagOutcome::Skip, - "summary" => { - if tag.has_class("hideme") { - return StartTagOutcome::Skip; - } - } - "button" => { - if tag.attr("id").as_deref() == Some("copy-path") { - return StartTagOutcome::Skip; - } - } - "a" => { - if tag.has_any_classes(&["anchor", "doc-anchor", "src"]) { - return StartTagOutcome::Skip; - } - } - "div" | "span" => { - if tag.has_any_classes(&["nav-container", "sidebar-elems", "out-of-band"]) { - return StartTagOutcome::Skip; - } - } - - _ => {} - } - - StartTagOutcome::Continue - } -} - -pub struct RustdocItemCollector { - pub items: IndexSet, -} - -impl RustdocItemCollector { - pub fn new() -> Self { - Self { - items: IndexSet::new(), - } - } - - fn parse_item(tag: &HtmlElement) -> Option { - if tag.tag() != "a" { - return None; - } - - let href = tag.attr("href")?; - if href.starts_with('#') || href.starts_with("https://") || href.starts_with("../") { - return None; - } - - for kind in RustdocItemKind::iter() { - if tag.has_class(kind.class()) { - let mut parts = href.trim_end_matches("/index.html").split('/'); - - if let Some(last_component) = parts.next_back() { - let last_component = match last_component.split_once('#') { - Some((component, _fragment)) => component, - None => last_component, - }; - - let name = last_component - .trim_start_matches(&format!("{}.", kind.class())) - .trim_end_matches(".html"); - - return Some(RustdocItem { - kind, - name: name.into(), - path: parts.map(Into::into).collect(), - }); - } - } - } - - None - } -} - -impl HandleTag for RustdocItemCollector { - fn should_handle(&self, tag: &str) -> bool { - tag == "a" - } - - fn handle_tag_start( - &mut self, - tag: &HtmlElement, - writer: &mut MarkdownWriter, - ) -> StartTagOutcome { - if tag.tag() == "a" { - let is_reexport = writer.current_element_stack().iter().any(|element| { - if let Some(id) = element.attr("id") { - id.starts_with("reexport.") || id.starts_with("method.") - } else { - false - } - }); - - if !is_reexport { - if let Some(item) = Self::parse_item(tag) { - self.items.insert(item); - } - } - } - - StartTagOutcome::Continue - } -} - -#[cfg(test)] -mod tests { - use html_to_markdown::{TagHandler, convert_html_to_markdown}; - use indoc::indoc; - use pretty_assertions::assert_eq; - - use super::*; - - fn rustdoc_handlers() -> Vec { - vec![ - Rc::new(RefCell::new(ParagraphHandler)), - Rc::new(RefCell::new(HeadingHandler)), - Rc::new(RefCell::new(ListHandler)), - Rc::new(RefCell::new(TableHandler::new())), - Rc::new(RefCell::new(StyledTextHandler)), - Rc::new(RefCell::new(RustdocChromeRemover)), - Rc::new(RefCell::new(RustdocHeadingHandler)), - Rc::new(RefCell::new(RustdocCodeHandler)), - Rc::new(RefCell::new(RustdocItemHandler)), - ] - } - - #[test] - fn test_main_heading_buttons_get_removed() { - let html = indoc! {r##" - - "##}; - let expected = indoc! {" - # Crate serde - "} - .trim(); - - assert_eq!( - convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(), - expected - ) - } - - #[test] - fn test_single_paragraph() { - let html = indoc! {r#" -

In particular, the last point is what sets axum apart from other frameworks. - axum doesn’t have its own middleware system but instead uses - tower::Service. This means axum gets timeouts, tracing, compression, - authorization, and more, for free. It also enables you to share middleware with - applications written using hyper or tonic.

- "#}; - let expected = indoc! {" - In particular, the last point is what sets `axum` apart from other frameworks. `axum` doesn’t have its own middleware system but instead uses `tower::Service`. This means `axum` gets timeouts, tracing, compression, authorization, and more, for free. It also enables you to share middleware with applications written using `hyper` or `tonic`. - "} - .trim(); - - assert_eq!( - convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(), - expected - ) - } - - #[test] - fn test_multiple_paragraphs() { - let html = indoc! {r##" -

§Serde

-

Serde is a framework for serializing and deserializing Rust data - structures efficiently and generically.

-

The Serde ecosystem consists of data structures that know how to serialize - and deserialize themselves along with data formats that know how to - serialize and deserialize other things. Serde provides the layer by which - these two groups interact with each other, allowing any supported data - structure to be serialized and deserialized using any supported data format.

-

See the Serde website https://serde.rs/ for additional documentation and - usage examples.

-

§Design

-

Where many other languages rely on runtime reflection for serializing data, - Serde is instead built on Rust’s powerful trait system. A data structure - that knows how to serialize and deserialize itself is one that implements - Serde’s Serialize and Deserialize traits (or uses Serde’s derive - attribute to automatically generate implementations at compile time). This - avoids any overhead of reflection or runtime type information. In fact in - many situations the interaction between data structure and data format can - be completely optimized away by the Rust compiler, leaving Serde - serialization to perform the same speed as a handwritten serializer for the - specific selection of data structure and data format.

- "##}; - let expected = indoc! {" - ## Serde - - Serde is a framework for _**ser**_ializing and _**de**_serializing Rust data structures efficiently and generically. - - The Serde ecosystem consists of data structures that know how to serialize and deserialize themselves along with data formats that know how to serialize and deserialize other things. Serde provides the layer by which these two groups interact with each other, allowing any supported data structure to be serialized and deserialized using any supported data format. - - See the Serde website https://serde.rs/ for additional documentation and usage examples. - - ### Design - - Where many other languages rely on runtime reflection for serializing data, Serde is instead built on Rust’s powerful trait system. A data structure that knows how to serialize and deserialize itself is one that implements Serde’s `Serialize` and `Deserialize` traits (or uses Serde’s derive attribute to automatically generate implementations at compile time). This avoids any overhead of reflection or runtime type information. In fact in many situations the interaction between data structure and data format can be completely optimized away by the Rust compiler, leaving Serde serialization to perform the same speed as a handwritten serializer for the specific selection of data structure and data format. - "} - .trim(); - - assert_eq!( - convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(), - expected - ) - } - - #[test] - fn test_styled_text() { - let html = indoc! {r#" -

This text is bolded.

-

This text is italicized.

- "#}; - let expected = indoc! {" - This text is **bolded**. - - This text is _italicized_. - "} - .trim(); - - assert_eq!( - convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(), - expected - ) - } - - #[test] - fn test_rust_code_block() { - let html = indoc! {r#" -
use axum::extract::{Path, Query, Json};
-            use std::collections::HashMap;
-
-            // `Path` gives you the path parameters and deserializes them.
-            async fn path(Path(user_id): Path<u32>) {}
-
-            // `Query` gives you the query parameters and deserializes them.
-            async fn query(Query(params): Query<HashMap<String, String>>) {}
-
-            // Buffer the request body and deserialize it as JSON into a
-            // `serde_json::Value`. `Json` supports any type that implements
-            // `serde::Deserialize`.
-            async fn json(Json(payload): Json<serde_json::Value>) {}
- "#}; - let expected = indoc! {" - ```rs - use axum::extract::{Path, Query, Json}; - use std::collections::HashMap; - - // `Path` gives you the path parameters and deserializes them. - async fn path(Path(user_id): Path) {} - - // `Query` gives you the query parameters and deserializes them. - async fn query(Query(params): Query>) {} - - // Buffer the request body and deserialize it as JSON into a - // `serde_json::Value`. `Json` supports any type that implements - // `serde::Deserialize`. - async fn json(Json(payload): Json) {} - ``` - "} - .trim(); - - assert_eq!( - convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(), - expected - ) - } - - #[test] - fn test_toml_code_block() { - let html = indoc! {r##" -

§Required dependencies

-

To use axum there are a few dependencies you have to pull in as well:

-
[dependencies]
-            axum = "<latest-version>"
-            tokio = { version = "<latest-version>", features = ["full"] }
-            tower = "<latest-version>"
-            
- "##}; - let expected = indoc! {r#" - ## Required dependencies - - To use axum there are a few dependencies you have to pull in as well: - - ```toml - [dependencies] - axum = "" - tokio = { version = "", features = ["full"] } - tower = "" - - ``` - "#} - .trim(); - - assert_eq!( - convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(), - expected - ) - } - - #[test] - fn test_item_table() { - let html = indoc! {r##" -

Structs§

-
    -
  • Errors that can happen when using axum.
  • -
  • Extractor and response for extensions.
  • -
  • Formform
    URL encoded extractor and response.
  • -
  • Jsonjson
    JSON Extractor / Response.
  • -
  • The router type for composing handlers and services.
-

Functions§

-
    -
  • servetokio and (http1 or http2)
    Serve the service with the supplied listener.
  • -
- "##}; - let expected = indoc! {r#" - ## Structs - - - `Error`: Errors that can happen when using axum. - - `Extension`: Extractor and response for extensions. - - `Form` [`form`]: URL encoded extractor and response. - - `Json` [`json`]: JSON Extractor / Response. - - `Router`: The router type for composing handlers and services. - - ## Functions - - - `serve` [`tokio` and (`http1` or `http2`)]: Serve the service with the supplied listener. - "#} - .trim(); - - assert_eq!( - convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(), - expected - ) - } - - #[test] - fn test_table() { - let html = indoc! {r##" -

§Feature flags

-

axum uses a set of feature flags to reduce the amount of compiled and - optional dependencies.

-

The following optional features are available:

-
- - - - - - - - - - - - - -
NameDescriptionDefault?
http1Enables hyper’s http1 featureYes
http2Enables hyper’s http2 featureNo
jsonEnables the Json type and some similar convenience functionalityYes
macrosEnables optional utility macrosNo
matched-pathEnables capturing of every request’s router path and the MatchedPath extractorYes
multipartEnables parsing multipart/form-data requests with MultipartNo
original-uriEnables capturing of every request’s original URI and the OriginalUri extractorYes
tokioEnables tokio as a dependency and axum::serve, SSE and extract::connect_info types.Yes
tower-logEnables tower’s log featureYes
tracingLog rejections from built-in extractorsYes
wsEnables WebSockets support via extract::wsNo
formEnables the Form extractorYes
queryEnables the Query extractorYes
- "##}; - let expected = indoc! {r#" - ## Feature flags - - axum uses a set of feature flags to reduce the amount of compiled and optional dependencies. - - The following optional features are available: - - | Name | Description | Default? | - | --- | --- | --- | - | `http1` | Enables hyper’s `http1` feature | Yes | - | `http2` | Enables hyper’s `http2` feature | No | - | `json` | Enables the `Json` type and some similar convenience functionality | Yes | - | `macros` | Enables optional utility macros | No | - | `matched-path` | Enables capturing of every request’s router path and the `MatchedPath` extractor | Yes | - | `multipart` | Enables parsing `multipart/form-data` requests with `Multipart` | No | - | `original-uri` | Enables capturing of every request’s original URI and the `OriginalUri` extractor | Yes | - | `tokio` | Enables `tokio` as a dependency and `axum::serve`, `SSE` and `extract::connect_info` types. | Yes | - | `tower-log` | Enables `tower`’s `log` feature | Yes | - | `tracing` | Log rejections from built-in extractors | Yes | - | `ws` | Enables WebSockets support via `extract::ws` | No | - | `form` | Enables the `Form` extractor | Yes | - | `query` | Enables the `Query` extractor | Yes | - "#} - .trim(); - - assert_eq!( - convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(), - expected - ) - } -} diff --git a/crates/indexed_docs/src/registry.rs b/crates/indexed_docs/src/registry.rs deleted file mode 100644 index 6757cd9c1a..0000000000 --- a/crates/indexed_docs/src/registry.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::sync::Arc; - -use collections::HashMap; -use gpui::{App, BackgroundExecutor, Global, ReadGlobal, UpdateGlobal}; -use parking_lot::RwLock; - -use crate::{IndexedDocsProvider, IndexedDocsStore, ProviderId}; - -struct GlobalIndexedDocsRegistry(Arc); - -impl Global for GlobalIndexedDocsRegistry {} - -pub struct IndexedDocsRegistry { - executor: BackgroundExecutor, - stores_by_provider: RwLock>>, -} - -impl IndexedDocsRegistry { - pub fn global(cx: &App) -> Arc { - GlobalIndexedDocsRegistry::global(cx).0.clone() - } - - pub(crate) fn init_global(cx: &mut App) { - GlobalIndexedDocsRegistry::set_global( - cx, - GlobalIndexedDocsRegistry(Arc::new(Self::new(cx.background_executor().clone()))), - ); - } - - pub fn new(executor: BackgroundExecutor) -> Self { - Self { - executor, - stores_by_provider: RwLock::new(HashMap::default()), - } - } - - pub fn list_providers(&self) -> Vec { - self.stores_by_provider - .read() - .keys() - .cloned() - .collect::>() - } - - pub fn register_provider( - &self, - provider: Box, - ) { - self.stores_by_provider.write().insert( - provider.id(), - Arc::new(IndexedDocsStore::new(provider, self.executor.clone())), - ); - } - - pub fn unregister_provider(&self, provider_id: &ProviderId) { - self.stores_by_provider.write().remove(provider_id); - } - - pub fn get_provider_store(&self, provider_id: ProviderId) -> Option> { - self.stores_by_provider.read().get(&provider_id).cloned() - } -} diff --git a/crates/indexed_docs/src/store.rs b/crates/indexed_docs/src/store.rs deleted file mode 100644 index 1407078efa..0000000000 --- a/crates/indexed_docs/src/store.rs +++ /dev/null @@ -1,346 +0,0 @@ -use std::path::PathBuf; -use std::sync::Arc; -use std::sync::atomic::AtomicBool; - -use anyhow::{Context as _, Result, anyhow}; -use async_trait::async_trait; -use collections::HashMap; -use derive_more::{Deref, Display}; -use futures::FutureExt; -use futures::future::{self, BoxFuture, Shared}; -use fuzzy::StringMatchCandidate; -use gpui::{App, BackgroundExecutor, Task}; -use heed::Database; -use heed::types::SerdeBincode; -use parking_lot::RwLock; -use serde::{Deserialize, Serialize}; -use util::ResultExt; - -use crate::IndexedDocsRegistry; - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Deref, Display)] -pub struct ProviderId(pub Arc); - -/// The name of a package. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Deref, Display)] -pub struct PackageName(Arc); - -impl From<&str> for PackageName { - fn from(value: &str) -> Self { - Self(value.into()) - } -} - -#[async_trait] -pub trait IndexedDocsProvider { - /// Returns the ID of this provider. - fn id(&self) -> ProviderId; - - /// Returns the path to the database for this provider. - fn database_path(&self) -> PathBuf; - - /// Returns a list of packages as suggestions to be included in the search - /// results. - /// - /// This can be used to provide completions for known packages (e.g., from the - /// local project or a registry) before a package has been indexed. - async fn suggest_packages(&self) -> Result>; - - /// Indexes the package with the given name. - async fn index(&self, package: PackageName, database: Arc) -> Result<()>; -} - -/// A store for indexed docs. -pub struct IndexedDocsStore { - executor: BackgroundExecutor, - database_future: - Shared, Arc>>>, - provider: Box, - indexing_tasks_by_package: - RwLock>>>>>, - latest_errors_by_package: RwLock>>, -} - -impl IndexedDocsStore { - pub fn try_global(provider: ProviderId, cx: &App) -> Result> { - let registry = IndexedDocsRegistry::global(cx); - registry - .get_provider_store(provider.clone()) - .with_context(|| format!("no indexed docs store found for {provider}")) - } - - pub fn new( - provider: Box, - executor: BackgroundExecutor, - ) -> Self { - let database_future = executor - .spawn({ - let executor = executor.clone(); - let database_path = provider.database_path(); - async move { IndexedDocsDatabase::new(database_path, executor) } - }) - .then(|result| future::ready(result.map(Arc::new).map_err(Arc::new))) - .boxed() - .shared(); - - Self { - executor, - database_future, - provider, - indexing_tasks_by_package: RwLock::new(HashMap::default()), - latest_errors_by_package: RwLock::new(HashMap::default()), - } - } - - pub fn latest_error_for_package(&self, package: &PackageName) -> Option> { - self.latest_errors_by_package.read().get(package).cloned() - } - - /// Returns whether the package with the given name is currently being indexed. - pub fn is_indexing(&self, package: &PackageName) -> bool { - self.indexing_tasks_by_package.read().contains_key(package) - } - - pub async fn load(&self, key: String) -> Result { - self.database_future - .clone() - .await - .map_err(|err| anyhow!(err))? - .load(key) - .await - } - - pub async fn load_many_by_prefix(&self, prefix: String) -> Result> { - self.database_future - .clone() - .await - .map_err(|err| anyhow!(err))? - .load_many_by_prefix(prefix) - .await - } - - /// Returns whether any entries exist with the given prefix. - pub async fn any_with_prefix(&self, prefix: String) -> Result { - self.database_future - .clone() - .await - .map_err(|err| anyhow!(err))? - .any_with_prefix(prefix) - .await - } - - pub fn suggest_packages(self: Arc) -> Task>> { - let this = self.clone(); - self.executor - .spawn(async move { this.provider.suggest_packages().await }) - } - - pub fn index( - self: Arc, - package: PackageName, - ) -> Shared>>> { - if let Some(existing_task) = self.indexing_tasks_by_package.read().get(&package) { - return existing_task.clone(); - } - - let indexing_task = self - .executor - .spawn({ - let this = self.clone(); - let package = package.clone(); - async move { - let _finally = util::defer({ - let this = this.clone(); - let package = package.clone(); - move || { - this.indexing_tasks_by_package.write().remove(&package); - } - }); - - let index_task = { - let package = package.clone(); - async { - let database = this - .database_future - .clone() - .await - .map_err(|err| anyhow!(err))?; - this.provider.index(package, database).await - } - }; - - let result = index_task.await.map_err(Arc::new); - match &result { - Ok(_) => { - this.latest_errors_by_package.write().remove(&package); - } - Err(err) => { - this.latest_errors_by_package - .write() - .insert(package, err.to_string().into()); - } - } - - result - } - }) - .shared(); - - self.indexing_tasks_by_package - .write() - .insert(package, indexing_task.clone()); - - indexing_task - } - - pub fn search(&self, query: String) -> Task> { - let executor = self.executor.clone(); - let database_future = self.database_future.clone(); - self.executor.spawn(async move { - let Some(database) = database_future.await.map_err(|err| anyhow!(err)).log_err() else { - return Vec::new(); - }; - - let Some(items) = database.keys().await.log_err() else { - return Vec::new(); - }; - - let candidates = items - .iter() - .enumerate() - .map(|(ix, item_path)| StringMatchCandidate::new(ix, &item_path)) - .collect::>(); - - let matches = fuzzy::match_strings( - &candidates, - &query, - false, - true, - 100, - &AtomicBool::default(), - executor, - ) - .await; - - matches - .into_iter() - .map(|mat| items[mat.candidate_id].clone()) - .collect() - }) - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Display, Serialize, Deserialize)] -pub struct MarkdownDocs(pub String); - -pub struct IndexedDocsDatabase { - executor: BackgroundExecutor, - env: heed::Env, - entries: Database, SerdeBincode>, -} - -impl IndexedDocsDatabase { - pub fn new(path: PathBuf, executor: BackgroundExecutor) -> Result { - std::fs::create_dir_all(&path)?; - - const ONE_GB_IN_BYTES: usize = 1024 * 1024 * 1024; - let env = unsafe { - heed::EnvOpenOptions::new() - .map_size(ONE_GB_IN_BYTES) - .max_dbs(1) - .open(path)? - }; - - let mut txn = env.write_txn()?; - let entries = env.create_database(&mut txn, Some("rustdoc_entries"))?; - txn.commit()?; - - Ok(Self { - executor, - env, - entries, - }) - } - - pub fn keys(&self) -> Task>> { - let env = self.env.clone(); - let entries = self.entries; - - self.executor.spawn(async move { - let txn = env.read_txn()?; - let mut iter = entries.iter(&txn)?; - let mut keys = Vec::new(); - while let Some((key, _value)) = iter.next().transpose()? { - keys.push(key); - } - - Ok(keys) - }) - } - - pub fn load(&self, key: String) -> Task> { - let env = self.env.clone(); - let entries = self.entries; - - self.executor.spawn(async move { - let txn = env.read_txn()?; - entries - .get(&txn, &key)? - .with_context(|| format!("no docs found for {key}")) - }) - } - - pub fn load_many_by_prefix(&self, prefix: String) -> Task>> { - let env = self.env.clone(); - let entries = self.entries; - - self.executor.spawn(async move { - let txn = env.read_txn()?; - let results = entries - .iter(&txn)? - .filter_map(|entry| { - let (key, value) = entry.ok()?; - if key.starts_with(&prefix) { - Some((key, value)) - } else { - None - } - }) - .collect::>(); - - Ok(results) - }) - } - - /// Returns whether any entries exist with the given prefix. - pub fn any_with_prefix(&self, prefix: String) -> Task> { - let env = self.env.clone(); - let entries = self.entries; - - self.executor.spawn(async move { - let txn = env.read_txn()?; - let any = entries - .iter(&txn)? - .any(|entry| entry.map_or(false, |(key, _value)| key.starts_with(&prefix))); - Ok(any) - }) - } - - pub fn insert(&self, key: String, docs: String) -> Task> { - let env = self.env.clone(); - let entries = self.entries; - - self.executor.spawn(async move { - let mut txn = env.write_txn()?; - entries.put(&mut txn, &key, &MarkdownDocs(docs))?; - txn.commit()?; - Ok(()) - }) - } -} - -impl extension::KeyValueStoreDelegate for IndexedDocsDatabase { - fn insert(&self, key: String, docs: String) -> Task> { - IndexedDocsDatabase::insert(&self, key, docs) - } -} diff --git a/typos.toml b/typos.toml index 336a829a44..e5f02b6415 100644 --- a/typos.toml +++ b/typos.toml @@ -16,9 +16,6 @@ extend-exclude = [ "crates/google_ai/src/supported_countries.rs", "crates/open_ai/src/supported_countries.rs", - # Some crate names are flagged as typos. - "crates/indexed_docs/src/providers/rustdoc/popular_crates.txt", - # Some mock data is flagged as typos. "crates/assistant_tools/src/web_search_tool.rs", From da8a692ec0aab6d050fe9ab20f2f7d4a793b5b00 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sun, 17 Aug 2025 08:52:05 -0400 Subject: [PATCH 410/693] docs: Remove link to openSUSE Tumbleweed (#36355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR removes the link to Zed on openSUSE Tumbleweed, as it has been removed: https://en.opensuse.org/index.php?title=Archive:Zed&action=history Screenshot 2025-08-17 at 8 48 59 AM Release Notes: - N/A --- docs/src/linux.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/src/linux.md b/docs/src/linux.md index 309354de6d..4a66445b78 100644 --- a/docs/src/linux.md +++ b/docs/src/linux.md @@ -48,7 +48,6 @@ There are several third-party Zed packages for various Linux distributions and p - Manjaro: [`zed`](https://packages.manjaro.org/?query=zed) - ALT Linux (Sisyphus): [`zed`](https://packages.altlinux.org/en/sisyphus/srpms/zed/) - AOSC OS: [`zed`](https://packages.aosc.io/packages/zed) -- openSUSE Tumbleweed: [`zed`](https://en.opensuse.org/Zed) See [Repology](https://repology.org/project/zed-editor/versions) for a list of Zed packages in various repositories. From 5895fac377b3c9abd3f14fd8e48188451b02d215 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Sun, 17 Aug 2025 15:05:23 +0200 Subject: [PATCH 411/693] Refactor ToolCallStatus enum to flat variants (#36356) Replace nested Allowed variant with distinct statuses for clearer status handling. Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 74 ++++++++++++++------------ crates/agent_servers/src/e2e_tests.rs | 12 +++-- crates/agent_ui/src/acp/thread_view.rs | 56 ++++++++----------- 3 files changed, 72 insertions(+), 70 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 3bb1b99ba1..c1c634612b 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -223,7 +223,7 @@ impl ToolCall { } if let Some(status) = status { - self.status = ToolCallStatus::Allowed { status }; + self.status = status.into(); } if let Some(title) = title { @@ -344,30 +344,48 @@ impl ToolCall { #[derive(Debug)] pub enum ToolCallStatus { + /// The tool call hasn't started running yet, but we start showing it to + /// the user. + Pending, + /// The tool call is waiting for confirmation from the user. WaitingForConfirmation { options: Vec, respond_tx: oneshot::Sender, }, - Allowed { - status: acp::ToolCallStatus, - }, + /// The tool call is currently running. + InProgress, + /// The tool call completed successfully. + Completed, + /// The tool call failed. + Failed, + /// The user rejected the tool call. Rejected, + /// The user cancelled generation so the tool call was cancelled. Canceled, } +impl From for ToolCallStatus { + fn from(status: acp::ToolCallStatus) -> Self { + match status { + acp::ToolCallStatus::Pending => Self::Pending, + acp::ToolCallStatus::InProgress => Self::InProgress, + acp::ToolCallStatus::Completed => Self::Completed, + acp::ToolCallStatus::Failed => Self::Failed, + } + } +} + impl Display for ToolCallStatus { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { + ToolCallStatus::Pending => "Pending", ToolCallStatus::WaitingForConfirmation { .. } => "Waiting for confirmation", - ToolCallStatus::Allowed { status } => match status { - acp::ToolCallStatus::Pending => "Pending", - acp::ToolCallStatus::InProgress => "In Progress", - acp::ToolCallStatus::Completed => "Completed", - acp::ToolCallStatus::Failed => "Failed", - }, + ToolCallStatus::InProgress => "In Progress", + ToolCallStatus::Completed => "Completed", + ToolCallStatus::Failed => "Failed", ToolCallStatus::Rejected => "Rejected", ToolCallStatus::Canceled => "Canceled", } @@ -759,11 +777,7 @@ impl AcpThread { AgentThreadEntry::UserMessage(_) => return false, AgentThreadEntry::ToolCall( call @ ToolCall { - status: - ToolCallStatus::Allowed { - status: - acp::ToolCallStatus::InProgress | acp::ToolCallStatus::Pending, - }, + status: ToolCallStatus::InProgress | ToolCallStatus::Pending, .. }, ) if call.diffs().next().is_some() => { @@ -945,9 +959,7 @@ impl AcpThread { tool_call: acp::ToolCall, cx: &mut Context, ) -> Result<(), acp::Error> { - let status = ToolCallStatus::Allowed { - status: tool_call.status, - }; + let status = tool_call.status.into(); self.upsert_tool_call_inner(tool_call.into(), status, cx) } @@ -1074,9 +1086,7 @@ impl AcpThread { ToolCallStatus::Rejected } acp::PermissionOptionKind::AllowOnce | acp::PermissionOptionKind::AllowAlways => { - ToolCallStatus::Allowed { - status: acp::ToolCallStatus::InProgress, - } + ToolCallStatus::InProgress } }; @@ -1097,7 +1107,10 @@ impl AcpThread { match &entry { AgentThreadEntry::ToolCall(call) => match call.status { ToolCallStatus::WaitingForConfirmation { .. } => return true, - ToolCallStatus::Allowed { .. } + ToolCallStatus::Pending + | ToolCallStatus::InProgress + | ToolCallStatus::Completed + | ToolCallStatus::Failed | ToolCallStatus::Rejected | ToolCallStatus::Canceled => continue, }, @@ -1290,10 +1303,9 @@ impl AcpThread { if let AgentThreadEntry::ToolCall(call) = entry { let cancel = matches!( call.status, - ToolCallStatus::WaitingForConfirmation { .. } - | ToolCallStatus::Allowed { - status: acp::ToolCallStatus::InProgress - } + ToolCallStatus::Pending + | ToolCallStatus::WaitingForConfirmation { .. } + | ToolCallStatus::InProgress ); if cancel { @@ -1939,10 +1951,7 @@ mod tests { assert!(matches!( thread.entries[1], AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Allowed { - status: acp::ToolCallStatus::InProgress, - .. - }, + status: ToolCallStatus::InProgress, .. }) )); @@ -1981,10 +1990,7 @@ mod tests { assert!(matches!( thread.entries[1], AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Allowed { - status: acp::ToolCallStatus::Completed, - .. - }, + status: ToolCallStatus::Completed, .. }) )); diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 5af7010f26..2b32edcd4f 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -134,7 +134,9 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp matches!( entry, AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Allowed { .. }, + status: ToolCallStatus::Pending + | ToolCallStatus::InProgress + | ToolCallStatus::Completed, .. }) ) @@ -212,7 +214,9 @@ pub async fn test_tool_call_with_permission( assert!(thread.entries().iter().any(|entry| matches!( entry, AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Allowed { .. }, + status: ToolCallStatus::Pending + | ToolCallStatus::InProgress + | ToolCallStatus::Completed, .. }) ))); @@ -223,7 +227,9 @@ pub async fn test_tool_call_with_permission( thread.read_with(cx, |thread, cx| { let AgentThreadEntry::ToolCall(ToolCall { content, - status: ToolCallStatus::Allowed { .. }, + status: ToolCallStatus::Pending + | ToolCallStatus::InProgress + | ToolCallStatus::Completed, .. }) = thread .entries() diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 17341e4c8a..7c1f3cf4ae 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1053,14 +1053,10 @@ impl AcpThreadView { let card_header_id = SharedString::from("inner-tool-call-header"); let status_icon = match &tool_call.status { - ToolCallStatus::Allowed { - status: acp::ToolCallStatus::Pending, - } - | ToolCallStatus::WaitingForConfirmation { .. } => None, - ToolCallStatus::Allowed { - status: acp::ToolCallStatus::InProgress, - .. - } => Some( + ToolCallStatus::Pending + | ToolCallStatus::WaitingForConfirmation { .. } + | ToolCallStatus::Completed => None, + ToolCallStatus::InProgress => Some( Icon::new(IconName::ArrowCircle) .color(Color::Accent) .size(IconSize::Small) @@ -1071,16 +1067,7 @@ impl AcpThreadView { ) .into_any(), ), - ToolCallStatus::Allowed { - status: acp::ToolCallStatus::Completed, - .. - } => None, - ToolCallStatus::Rejected - | ToolCallStatus::Canceled - | ToolCallStatus::Allowed { - status: acp::ToolCallStatus::Failed, - .. - } => Some( + ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => Some( Icon::new(IconName::Close) .color(Color::Error) .size(IconSize::Small) @@ -1146,15 +1133,23 @@ impl AcpThreadView { tool_call.content.is_empty(), cx, )), - ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => v_flex() - .w_full() - .children(tool_call.content.iter().map(|content| { - div() - .child( - self.render_tool_call_content(entry_ix, content, tool_call, window, cx), - ) - .into_any_element() - })), + ToolCallStatus::Pending + | ToolCallStatus::InProgress + | ToolCallStatus::Completed + | ToolCallStatus::Failed + | ToolCallStatus::Canceled => { + v_flex() + .w_full() + .children(tool_call.content.iter().map(|content| { + div() + .child( + self.render_tool_call_content( + entry_ix, content, tool_call, window, cx, + ), + ) + .into_any_element() + })) + } ToolCallStatus::Rejected => v_flex().size_0(), }; @@ -1467,12 +1462,7 @@ impl AcpThreadView { let tool_failed = matches!( &tool_call.status, - ToolCallStatus::Rejected - | ToolCallStatus::Canceled - | ToolCallStatus::Allowed { - status: acp::ToolCallStatus::Failed, - .. - } + ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed ); let output = terminal_data.output(); From addc4f4a11a816fd6d116be379bf249aa203f535 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sun, 17 Aug 2025 17:19:38 +0200 Subject: [PATCH 412/693] agent_ui: Ensure that all configuration views get rendered with full width (#36362) Closes #36097 Release Notes: - Fixed API key input fields getting shrunk in Agent Panel settings view on low panel widths paired with high UI font sizes. --- crates/agent_ui/src/agent_configuration.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 4a2dd88c33..b4ebb8206c 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -300,6 +300,7 @@ impl AgentConfiguration { ) .child( div() + .w_full() .px_2() .when(is_expanded, |parent| match configuration_view { Some(configuration_view) => parent.child(configuration_view), From faaaf02bf211e71743912b77cd6e7911e73965ff Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Sun, 17 Aug 2025 13:25:05 -0300 Subject: [PATCH 413/693] ui: Reduce icons stroke width (#36361) After redesigning all Zed icons (https://github.com/zed-industries/zed/pull/35856), it felt like using 1.5 for stroke width didn't really flow well with the default typeface default font weight. Reducing it to 1.2 makes the UI much sharper, less burry, and more cohesive overall. Release Notes: - N/A --- assets/icons/ai.svg | 2 +- assets/icons/arrow_circle.svg | 8 ++++---- assets/icons/arrow_down.svg | 2 +- assets/icons/arrow_down10.svg | 2 +- assets/icons/arrow_down_right.svg | 4 ++-- assets/icons/arrow_left.svg | 2 +- assets/icons/arrow_right.svg | 2 +- assets/icons/arrow_right_left.svg | 8 ++++---- assets/icons/arrow_up.svg | 2 +- assets/icons/arrow_up_right.svg | 4 ++-- assets/icons/audio_off.svg | 10 +++++----- assets/icons/audio_on.svg | 6 +++--- assets/icons/backspace.svg | 6 +++--- assets/icons/bell.svg | 4 ++-- assets/icons/bell_dot.svg | 4 ++-- assets/icons/bell_off.svg | 6 +++--- assets/icons/bell_ring.svg | 8 ++++---- assets/icons/binary.svg | 2 +- assets/icons/blocks.svg | 2 +- assets/icons/bolt_outlined.svg | 2 +- assets/icons/book.svg | 2 +- assets/icons/book_copy.svg | 2 +- assets/icons/chat.svg | 4 ++-- assets/icons/check.svg | 2 +- assets/icons/check_circle.svg | 4 ++-- assets/icons/check_double.svg | 4 ++-- assets/icons/chevron_down.svg | 2 +- assets/icons/chevron_left.svg | 2 +- assets/icons/chevron_right.svg | 2 +- assets/icons/chevron_up.svg | 2 +- assets/icons/chevron_up_down.svg | 4 ++-- assets/icons/circle_help.svg | 6 +++--- assets/icons/close.svg | 2 +- assets/icons/cloud_download.svg | 2 +- assets/icons/code.svg | 2 +- assets/icons/cog.svg | 2 +- assets/icons/command.svg | 2 +- assets/icons/control.svg | 2 +- assets/icons/copilot.svg | 6 +++--- assets/icons/copy.svg | 2 +- assets/icons/countdown_timer.svg | 2 +- assets/icons/crosshair.svg | 10 +++++----- assets/icons/cursor_i_beam.svg | 4 ++-- assets/icons/dash.svg | 2 +- assets/icons/database_zap.svg | 2 +- assets/icons/debug.svg | 20 +++++++++---------- assets/icons/debug_breakpoint.svg | 2 +- assets/icons/debug_continue.svg | 2 +- assets/icons/debug_detach.svg | 2 +- assets/icons/debug_disabled_breakpoint.svg | 2 +- .../icons/debug_disabled_log_breakpoint.svg | 6 +++--- assets/icons/debug_ignore_breakpoints.svg | 2 +- assets/icons/debug_step_back.svg | 2 +- assets/icons/debug_step_into.svg | 2 +- assets/icons/debug_step_out.svg | 2 +- assets/icons/debug_step_over.svg | 2 +- assets/icons/diff.svg | 2 +- assets/icons/disconnected.svg | 2 +- assets/icons/download.svg | 2 +- assets/icons/envelope.svg | 4 ++-- assets/icons/eraser.svg | 2 +- assets/icons/escape.svg | 2 +- assets/icons/exit.svg | 6 +++--- assets/icons/expand_down.svg | 4 ++-- assets/icons/expand_up.svg | 4 ++-- assets/icons/expand_vertical.svg | 2 +- assets/icons/eye.svg | 4 ++-- assets/icons/file.svg | 2 +- assets/icons/file_code.svg | 2 +- assets/icons/file_diff.svg | 2 +- assets/icons/file_doc.svg | 6 +++--- assets/icons/file_generic.svg | 6 +++--- assets/icons/file_git.svg | 8 ++++---- assets/icons/file_icons/ai.svg | 2 +- assets/icons/file_icons/audio.svg | 12 +++++------ assets/icons/file_icons/book.svg | 6 +++--- assets/icons/file_icons/bun.svg | 2 +- assets/icons/file_icons/chevron_down.svg | 2 +- assets/icons/file_icons/chevron_left.svg | 2 +- assets/icons/file_icons/chevron_right.svg | 2 +- assets/icons/file_icons/chevron_up.svg | 2 +- assets/icons/file_icons/code.svg | 4 ++-- assets/icons/file_icons/coffeescript.svg | 2 +- assets/icons/file_icons/conversations.svg | 2 +- assets/icons/file_icons/dart.svg | 2 +- assets/icons/file_icons/database.svg | 6 +++--- assets/icons/file_icons/diff.svg | 6 +++--- assets/icons/file_icons/eslint.svg | 2 +- assets/icons/file_icons/file.svg | 6 +++--- assets/icons/file_icons/folder.svg | 2 +- assets/icons/file_icons/folder_open.svg | 4 ++-- assets/icons/file_icons/font.svg | 2 +- assets/icons/file_icons/git.svg | 8 ++++---- assets/icons/file_icons/gleam.svg | 4 ++-- assets/icons/file_icons/graphql.svg | 4 ++-- assets/icons/file_icons/hash.svg | 8 ++++---- assets/icons/file_icons/heroku.svg | 2 +- assets/icons/file_icons/html.svg | 6 +++--- assets/icons/file_icons/image.svg | 6 +++--- assets/icons/file_icons/java.svg | 10 +++++----- assets/icons/file_icons/lock.svg | 2 +- assets/icons/file_icons/magnifying_glass.svg | 2 +- assets/icons/file_icons/nix.svg | 12 +++++------ assets/icons/file_icons/notebook.svg | 10 +++++----- assets/icons/file_icons/package.svg | 2 +- assets/icons/file_icons/phoenix.svg | 2 +- assets/icons/file_icons/plus.svg | 2 +- assets/icons/file_icons/prettier.svg | 20 +++++++++---------- assets/icons/file_icons/project.svg | 2 +- assets/icons/file_icons/python.svg | 4 ++-- assets/icons/file_icons/replace.svg | 2 +- assets/icons/file_icons/replace_next.svg | 2 +- assets/icons/file_icons/rust.svg | 2 +- assets/icons/file_icons/scala.svg | 2 +- assets/icons/file_icons/settings.svg | 2 +- assets/icons/file_icons/tcl.svg | 2 +- assets/icons/file_icons/toml.svg | 6 +++--- assets/icons/file_icons/video.svg | 4 ++-- assets/icons/file_icons/vue.svg | 2 +- assets/icons/file_lock.svg | 2 +- assets/icons/file_markdown.svg | 2 +- assets/icons/file_rust.svg | 2 +- assets/icons/file_text_outlined.svg | 8 ++++---- assets/icons/file_toml.svg | 6 +++--- assets/icons/file_tree.svg | 2 +- assets/icons/filter.svg | 2 +- assets/icons/flame.svg | 2 +- assets/icons/folder.svg | 2 +- assets/icons/folder_open.svg | 4 ++-- assets/icons/folder_search.svg | 2 +- assets/icons/font.svg | 2 +- assets/icons/font_size.svg | 2 +- assets/icons/font_weight.svg | 2 +- assets/icons/forward_arrow.svg | 4 ++-- assets/icons/git_branch.svg | 2 +- assets/icons/git_branch_alt.svg | 10 +++++----- assets/icons/github.svg | 2 +- assets/icons/hash.svg | 2 +- assets/icons/history_rerun.svg | 6 +++--- assets/icons/image.svg | 2 +- assets/icons/info.svg | 4 ++-- assets/icons/json.svg | 4 ++-- assets/icons/keyboard.svg | 2 +- assets/icons/knockouts/x_fg.svg | 2 +- assets/icons/library.svg | 8 ++++---- assets/icons/line_height.svg | 2 +- assets/icons/list_collapse.svg | 2 +- assets/icons/list_todo.svg | 2 +- assets/icons/list_tree.svg | 10 +++++----- assets/icons/list_x.svg | 10 +++++----- assets/icons/load_circle.svg | 2 +- assets/icons/location_edit.svg | 2 +- assets/icons/lock_outlined.svg | 6 +++--- assets/icons/magnifying_glass.svg | 2 +- assets/icons/maximize.svg | 8 ++++---- assets/icons/menu.svg | 2 +- assets/icons/menu_alt.svg | 2 +- assets/icons/mic.svg | 6 +++--- assets/icons/mic_mute.svg | 12 +++++------ assets/icons/minimize.svg | 8 ++++---- assets/icons/notepad.svg | 2 +- assets/icons/option.svg | 4 ++-- assets/icons/pencil.svg | 4 ++-- assets/icons/person.svg | 2 +- assets/icons/pin.svg | 4 ++-- assets/icons/play_filled.svg | 2 +- assets/icons/play_outlined.svg | 2 +- assets/icons/plus.svg | 4 ++-- assets/icons/power.svg | 2 +- assets/icons/public.svg | 2 +- assets/icons/pull_request.svg | 2 +- assets/icons/quote.svg | 2 +- assets/icons/reader.svg | 6 +++--- assets/icons/refresh_title.svg | 2 +- assets/icons/regex.svg | 2 +- assets/icons/repl_neutral.svg | 8 ++++---- assets/icons/repl_off.svg | 18 ++++++++--------- assets/icons/repl_pause.svg | 12 +++++------ assets/icons/repl_play.svg | 10 +++++----- assets/icons/replace.svg | 2 +- assets/icons/replace_next.svg | 2 +- assets/icons/rerun.svg | 2 +- assets/icons/return.svg | 4 ++-- assets/icons/rotate_ccw.svg | 2 +- assets/icons/rotate_cw.svg | 2 +- assets/icons/scissors.svg | 2 +- assets/icons/screen.svg | 6 +++--- assets/icons/select_all.svg | 2 +- assets/icons/send.svg | 2 +- assets/icons/server.svg | 8 ++++---- assets/icons/settings.svg | 2 +- assets/icons/shield_check.svg | 4 ++-- assets/icons/shift.svg | 2 +- assets/icons/slash.svg | 2 +- assets/icons/sliders.svg | 12 +++++------ assets/icons/space.svg | 2 +- assets/icons/sparkle.svg | 2 +- assets/icons/split.svg | 4 ++-- assets/icons/split_alt.svg | 2 +- assets/icons/square_dot.svg | 2 +- assets/icons/square_minus.svg | 4 ++-- assets/icons/square_plus.svg | 6 +++--- assets/icons/star.svg | 2 +- assets/icons/star_filled.svg | 2 +- assets/icons/stop.svg | 2 +- assets/icons/swatch_book.svg | 2 +- assets/icons/tab.svg | 6 +++--- assets/icons/terminal_alt.svg | 6 +++--- assets/icons/text_snippet.svg | 2 +- assets/icons/text_thread.svg | 10 +++++----- assets/icons/thread.svg | 2 +- assets/icons/thread_from_summary.svg | 8 ++++---- assets/icons/thumbs_down.svg | 2 +- assets/icons/thumbs_up.svg | 2 +- assets/icons/todo_complete.svg | 2 +- assets/icons/todo_pending.svg | 16 +++++++-------- assets/icons/todo_progress.svg | 18 ++++++++--------- assets/icons/tool_copy.svg | 4 ++-- assets/icons/tool_delete_file.svg | 6 +++--- assets/icons/tool_diagnostics.svg | 6 +++--- assets/icons/tool_folder.svg | 2 +- assets/icons/tool_hammer.svg | 6 +++--- assets/icons/tool_notification.svg | 4 ++-- assets/icons/tool_pencil.svg | 4 ++-- assets/icons/tool_read.svg | 10 +++++----- assets/icons/tool_regex.svg | 2 +- assets/icons/tool_search.svg | 4 ++-- assets/icons/tool_terminal.svg | 6 +++--- assets/icons/tool_think.svg | 2 +- assets/icons/tool_web.svg | 6 +++--- assets/icons/trash.svg | 6 +++--- assets/icons/undo.svg | 2 +- assets/icons/user_check.svg | 2 +- assets/icons/user_group.svg | 6 +++--- assets/icons/user_round_pen.svg | 2 +- assets/icons/warning.svg | 2 +- assets/icons/whole_word.svg | 2 +- assets/icons/x_circle.svg | 2 +- assets/icons/zed_assistant.svg | 2 +- assets/icons/zed_burn_mode.svg | 2 +- assets/icons/zed_burn_mode_on.svg | 2 +- assets/icons/zed_mcp_custom.svg | 2 +- assets/icons/zed_mcp_extension.svg | 2 +- assets/icons/zed_predict.svg | 6 +++--- assets/icons/zed_predict_down.svg | 6 +++--- assets/icons/zed_predict_error.svg | 4 ++-- assets/icons/zed_predict_up.svg | 6 +++--- crates/icons/README.md | 2 +- 248 files changed, 499 insertions(+), 499 deletions(-) diff --git a/assets/icons/ai.svg b/assets/icons/ai.svg index d60396ad47..4236d50337 100644 --- a/assets/icons/ai.svg +++ b/assets/icons/ai.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/arrow_circle.svg b/assets/icons/arrow_circle.svg index 76363c6270..cdfa939795 100644 --- a/assets/icons/arrow_circle.svg +++ b/assets/icons/arrow_circle.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/arrow_down.svg b/assets/icons/arrow_down.svg index c71e5437f8..60e6584c45 100644 --- a/assets/icons/arrow_down.svg +++ b/assets/icons/arrow_down.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_down10.svg b/assets/icons/arrow_down10.svg index 8eed82276c..5933b758d9 100644 --- a/assets/icons/arrow_down10.svg +++ b/assets/icons/arrow_down10.svg @@ -1 +1 @@ - + diff --git a/assets/icons/arrow_down_right.svg b/assets/icons/arrow_down_right.svg index 73f72a2c38..ebdb06d77b 100644 --- a/assets/icons/arrow_down_right.svg +++ b/assets/icons/arrow_down_right.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/arrow_left.svg b/assets/icons/arrow_left.svg index ca441497a0..f7eacb2a77 100644 --- a/assets/icons/arrow_left.svg +++ b/assets/icons/arrow_left.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_right.svg b/assets/icons/arrow_right.svg index ae14888563..b9324af5a2 100644 --- a/assets/icons/arrow_right.svg +++ b/assets/icons/arrow_right.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_right_left.svg b/assets/icons/arrow_right_left.svg index cfeee0cc24..2c1211056a 100644 --- a/assets/icons/arrow_right_left.svg +++ b/assets/icons/arrow_right_left.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/arrow_up.svg b/assets/icons/arrow_up.svg index b98c710374..ff3ad44123 100644 --- a/assets/icons/arrow_up.svg +++ b/assets/icons/arrow_up.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_up_right.svg b/assets/icons/arrow_up_right.svg index fb065bc9ce..a948ef8f81 100644 --- a/assets/icons/arrow_up_right.svg +++ b/assets/icons/arrow_up_right.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/audio_off.svg b/assets/icons/audio_off.svg index dfb5a1c458..43d2a04344 100644 --- a/assets/icons/audio_off.svg +++ b/assets/icons/audio_off.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/audio_on.svg b/assets/icons/audio_on.svg index d1bef0d337..6e183bd585 100644 --- a/assets/icons/audio_on.svg +++ b/assets/icons/audio_on.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/backspace.svg b/assets/icons/backspace.svg index 679ef1ade1..9ef4432b6f 100644 --- a/assets/icons/backspace.svg +++ b/assets/icons/backspace.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/bell.svg b/assets/icons/bell.svg index f9b2a97fb3..70225bb105 100644 --- a/assets/icons/bell.svg +++ b/assets/icons/bell.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/bell_dot.svg b/assets/icons/bell_dot.svg index 09a17401da..959a7773cf 100644 --- a/assets/icons/bell_dot.svg +++ b/assets/icons/bell_dot.svg @@ -1,5 +1,5 @@ - - + + diff --git a/assets/icons/bell_off.svg b/assets/icons/bell_off.svg index 98cbd1eb60..5c3c1a0d68 100644 --- a/assets/icons/bell_off.svg +++ b/assets/icons/bell_off.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/bell_ring.svg b/assets/icons/bell_ring.svg index e411e7511b..838056cc03 100644 --- a/assets/icons/bell_ring.svg +++ b/assets/icons/bell_ring.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/binary.svg b/assets/icons/binary.svg index bbc375617f..3c15e9b547 100644 --- a/assets/icons/binary.svg +++ b/assets/icons/binary.svg @@ -1 +1 @@ - + diff --git a/assets/icons/blocks.svg b/assets/icons/blocks.svg index e1690e2642..84725d7892 100644 --- a/assets/icons/blocks.svg +++ b/assets/icons/blocks.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/bolt_outlined.svg b/assets/icons/bolt_outlined.svg index 58fccf7788..ca9c75fbfd 100644 --- a/assets/icons/bolt_outlined.svg +++ b/assets/icons/bolt_outlined.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/book.svg b/assets/icons/book.svg index 8b0f89e82d..a2ab394be4 100644 --- a/assets/icons/book.svg +++ b/assets/icons/book.svg @@ -1 +1 @@ - + diff --git a/assets/icons/book_copy.svg b/assets/icons/book_copy.svg index f509beffe6..b7afd1df5c 100644 --- a/assets/icons/book_copy.svg +++ b/assets/icons/book_copy.svg @@ -1 +1 @@ - + diff --git a/assets/icons/chat.svg b/assets/icons/chat.svg index a0548c3d3e..c64f6b5e0e 100644 --- a/assets/icons/chat.svg +++ b/assets/icons/chat.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/check.svg b/assets/icons/check.svg index 4563505aaa..21e2137965 100644 --- a/assets/icons/check.svg +++ b/assets/icons/check.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/check_circle.svg b/assets/icons/check_circle.svg index e6ec5d11ef..f9b88c4ce1 100644 --- a/assets/icons/check_circle.svg +++ b/assets/icons/check_circle.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/check_double.svg b/assets/icons/check_double.svg index b52bef81a4..fabc700520 100644 --- a/assets/icons/check_double.svg +++ b/assets/icons/check_double.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/chevron_down.svg b/assets/icons/chevron_down.svg index 7894aae764..e4ca142a91 100644 --- a/assets/icons/chevron_down.svg +++ b/assets/icons/chevron_down.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/chevron_left.svg b/assets/icons/chevron_left.svg index 4be4c95dca..fbe438fd4b 100644 --- a/assets/icons/chevron_left.svg +++ b/assets/icons/chevron_left.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/chevron_right.svg b/assets/icons/chevron_right.svg index c8ff847177..4f170717c9 100644 --- a/assets/icons/chevron_right.svg +++ b/assets/icons/chevron_right.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/chevron_up.svg b/assets/icons/chevron_up.svg index 8e575e2e8d..bbe6b9762d 100644 --- a/assets/icons/chevron_up.svg +++ b/assets/icons/chevron_up.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/chevron_up_down.svg b/assets/icons/chevron_up_down.svg index c7af01d4a3..299f6bce5a 100644 --- a/assets/icons/chevron_up_down.svg +++ b/assets/icons/chevron_up_down.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/circle_help.svg b/assets/icons/circle_help.svg index 4e2890d3e1..0e623bd1da 100644 --- a/assets/icons/circle_help.svg +++ b/assets/icons/circle_help.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/close.svg b/assets/icons/close.svg index ad487e0a4f..846b3a703d 100644 --- a/assets/icons/close.svg +++ b/assets/icons/close.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/cloud_download.svg b/assets/icons/cloud_download.svg index 0efcbe10f1..70cda55856 100644 --- a/assets/icons/cloud_download.svg +++ b/assets/icons/cloud_download.svg @@ -1 +1 @@ - + diff --git a/assets/icons/code.svg b/assets/icons/code.svg index 6a1795b59c..72d145224a 100644 --- a/assets/icons/code.svg +++ b/assets/icons/code.svg @@ -1 +1 @@ - + diff --git a/assets/icons/cog.svg b/assets/icons/cog.svg index 4f3ada11a6..7dd3a8beff 100644 --- a/assets/icons/cog.svg +++ b/assets/icons/cog.svg @@ -1 +1 @@ - + diff --git a/assets/icons/command.svg b/assets/icons/command.svg index 6602af8e1f..f361ca2d05 100644 --- a/assets/icons/command.svg +++ b/assets/icons/command.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/control.svg b/assets/icons/control.svg index e831968df6..f9341b6256 100644 --- a/assets/icons/control.svg +++ b/assets/icons/control.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/copilot.svg b/assets/icons/copilot.svg index 57c0a5f91a..2584cd6310 100644 --- a/assets/icons/copilot.svg +++ b/assets/icons/copilot.svg @@ -1,9 +1,9 @@ - - - + + + diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg index dfd8d9dbb9..bca13f8d56 100644 --- a/assets/icons/copy.svg +++ b/assets/icons/copy.svg @@ -1 +1 @@ - + diff --git a/assets/icons/countdown_timer.svg b/assets/icons/countdown_timer.svg index 5e69f1bfb4..5d1e775e68 100644 --- a/assets/icons/countdown_timer.svg +++ b/assets/icons/countdown_timer.svg @@ -1 +1 @@ - + diff --git a/assets/icons/crosshair.svg b/assets/icons/crosshair.svg index 1492bf9245..3af6aa9fa3 100644 --- a/assets/icons/crosshair.svg +++ b/assets/icons/crosshair.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/cursor_i_beam.svg b/assets/icons/cursor_i_beam.svg index 3790de6f49..2d513181f9 100644 --- a/assets/icons/cursor_i_beam.svg +++ b/assets/icons/cursor_i_beam.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/dash.svg b/assets/icons/dash.svg index 9270f80781..3928ee7cfa 100644 --- a/assets/icons/dash.svg +++ b/assets/icons/dash.svg @@ -1 +1 @@ - + diff --git a/assets/icons/database_zap.svg b/assets/icons/database_zap.svg index 160ffa5041..76af0f9251 100644 --- a/assets/icons/database_zap.svg +++ b/assets/icons/database_zap.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug.svg b/assets/icons/debug.svg index 900caf4b98..6423a2b090 100644 --- a/assets/icons/debug.svg +++ b/assets/icons/debug.svg @@ -1,12 +1,12 @@ - - - - - - - - - - + + + + + + + + + + diff --git a/assets/icons/debug_breakpoint.svg b/assets/icons/debug_breakpoint.svg index 9cab42eecd..c09a3c159f 100644 --- a/assets/icons/debug_breakpoint.svg +++ b/assets/icons/debug_breakpoint.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/debug_continue.svg b/assets/icons/debug_continue.svg index f663a5a041..f03a8b2364 100644 --- a/assets/icons/debug_continue.svg +++ b/assets/icons/debug_continue.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug_detach.svg b/assets/icons/debug_detach.svg index a34a0e8171..8b34845571 100644 --- a/assets/icons/debug_detach.svg +++ b/assets/icons/debug_detach.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug_disabled_breakpoint.svg b/assets/icons/debug_disabled_breakpoint.svg index 8b80623b02..9a7c896f47 100644 --- a/assets/icons/debug_disabled_breakpoint.svg +++ b/assets/icons/debug_disabled_breakpoint.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/debug_disabled_log_breakpoint.svg b/assets/icons/debug_disabled_log_breakpoint.svg index 2ccc37623d..f477f4f32d 100644 --- a/assets/icons/debug_disabled_log_breakpoint.svg +++ b/assets/icons/debug_disabled_log_breakpoint.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/debug_ignore_breakpoints.svg b/assets/icons/debug_ignore_breakpoints.svg index b2a345d314..bc95329c7a 100644 --- a/assets/icons/debug_ignore_breakpoints.svg +++ b/assets/icons/debug_ignore_breakpoints.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/debug_step_back.svg b/assets/icons/debug_step_back.svg index d1112d6b8e..61d45866f6 100644 --- a/assets/icons/debug_step_back.svg +++ b/assets/icons/debug_step_back.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug_step_into.svg b/assets/icons/debug_step_into.svg index 02bdd63cb4..9a517fc7ca 100644 --- a/assets/icons/debug_step_into.svg +++ b/assets/icons/debug_step_into.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug_step_out.svg b/assets/icons/debug_step_out.svg index 48190b704b..147a44f930 100644 --- a/assets/icons/debug_step_out.svg +++ b/assets/icons/debug_step_out.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug_step_over.svg b/assets/icons/debug_step_over.svg index 54afac001f..336abc11de 100644 --- a/assets/icons/debug_step_over.svg +++ b/assets/icons/debug_step_over.svg @@ -1 +1 @@ - + diff --git a/assets/icons/diff.svg b/assets/icons/diff.svg index 61aa617f5b..9d93b2d5b4 100644 --- a/assets/icons/diff.svg +++ b/assets/icons/diff.svg @@ -1 +1 @@ - + diff --git a/assets/icons/disconnected.svg b/assets/icons/disconnected.svg index f3069798d0..47bd1db478 100644 --- a/assets/icons/disconnected.svg +++ b/assets/icons/disconnected.svg @@ -1 +1 @@ - + diff --git a/assets/icons/download.svg b/assets/icons/download.svg index 6ddcb1e100..6c105d3fd7 100644 --- a/assets/icons/download.svg +++ b/assets/icons/download.svg @@ -1 +1 @@ - + diff --git a/assets/icons/envelope.svg b/assets/icons/envelope.svg index 0f5e95f968..273cc6de26 100644 --- a/assets/icons/envelope.svg +++ b/assets/icons/envelope.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/eraser.svg b/assets/icons/eraser.svg index 601f2b9b90..ca6209785f 100644 --- a/assets/icons/eraser.svg +++ b/assets/icons/eraser.svg @@ -1 +1 @@ - + diff --git a/assets/icons/escape.svg b/assets/icons/escape.svg index a87f03d2fa..1898588a67 100644 --- a/assets/icons/escape.svg +++ b/assets/icons/escape.svg @@ -1 +1 @@ - + diff --git a/assets/icons/exit.svg b/assets/icons/exit.svg index 1ff9d78824..3619a55c87 100644 --- a/assets/icons/exit.svg +++ b/assets/icons/exit.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/expand_down.svg b/assets/icons/expand_down.svg index 07390aad18..9f85ee6720 100644 --- a/assets/icons/expand_down.svg +++ b/assets/icons/expand_down.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/expand_up.svg b/assets/icons/expand_up.svg index 73c1358b99..49b084fa8f 100644 --- a/assets/icons/expand_up.svg +++ b/assets/icons/expand_up.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/expand_vertical.svg b/assets/icons/expand_vertical.svg index e2a6dd227e..5a5fa8ccb5 100644 --- a/assets/icons/expand_vertical.svg +++ b/assets/icons/expand_vertical.svg @@ -1 +1 @@ - + diff --git a/assets/icons/eye.svg b/assets/icons/eye.svg index 7f10f73801..327fa751e9 100644 --- a/assets/icons/eye.svg +++ b/assets/icons/eye.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/file.svg b/assets/icons/file.svg index 85f3f543a5..60cf2537d9 100644 --- a/assets/icons/file.svg +++ b/assets/icons/file.svg @@ -1 +1 @@ - + diff --git a/assets/icons/file_code.svg b/assets/icons/file_code.svg index b0e632b67f..548d5a153b 100644 --- a/assets/icons/file_code.svg +++ b/assets/icons/file_code.svg @@ -1 +1 @@ - + diff --git a/assets/icons/file_diff.svg b/assets/icons/file_diff.svg index d6cb4440ea..193dd7392f 100644 --- a/assets/icons/file_diff.svg +++ b/assets/icons/file_diff.svg @@ -1 +1 @@ - + diff --git a/assets/icons/file_doc.svg b/assets/icons/file_doc.svg index 3b11995f36..ccd5eeea01 100644 --- a/assets/icons/file_doc.svg +++ b/assets/icons/file_doc.svg @@ -1,6 +1,6 @@ - + - - + + diff --git a/assets/icons/file_generic.svg b/assets/icons/file_generic.svg index 3c72bd3320..790a5f18d7 100644 --- a/assets/icons/file_generic.svg +++ b/assets/icons/file_generic.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_git.svg b/assets/icons/file_git.svg index 197db2e9e6..2b36b0ffd3 100644 --- a/assets/icons/file_git.svg +++ b/assets/icons/file_git.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/file_icons/ai.svg b/assets/icons/file_icons/ai.svg index d60396ad47..4236d50337 100644 --- a/assets/icons/file_icons/ai.svg +++ b/assets/icons/file_icons/ai.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/audio.svg b/assets/icons/file_icons/audio.svg index 672f736c95..7948b04616 100644 --- a/assets/icons/file_icons/audio.svg +++ b/assets/icons/file_icons/audio.svg @@ -1,8 +1,8 @@ - - - - - - + + + + + + diff --git a/assets/icons/file_icons/book.svg b/assets/icons/file_icons/book.svg index 3b11995f36..ccd5eeea01 100644 --- a/assets/icons/file_icons/book.svg +++ b/assets/icons/file_icons/book.svg @@ -1,6 +1,6 @@ - + - - + + diff --git a/assets/icons/file_icons/bun.svg b/assets/icons/file_icons/bun.svg index 48af8b3088..ca1ec900bc 100644 --- a/assets/icons/file_icons/bun.svg +++ b/assets/icons/file_icons/bun.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/chevron_down.svg b/assets/icons/file_icons/chevron_down.svg index 9e60e40cf4..9918f6c9f7 100644 --- a/assets/icons/file_icons/chevron_down.svg +++ b/assets/icons/file_icons/chevron_down.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/chevron_left.svg b/assets/icons/file_icons/chevron_left.svg index a2aa9ad996..3299ee7168 100644 --- a/assets/icons/file_icons/chevron_left.svg +++ b/assets/icons/file_icons/chevron_left.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/chevron_right.svg b/assets/icons/file_icons/chevron_right.svg index 06608c95ee..140f644127 100644 --- a/assets/icons/file_icons/chevron_right.svg +++ b/assets/icons/file_icons/chevron_right.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/chevron_up.svg b/assets/icons/file_icons/chevron_up.svg index fd3d5e4470..ae8c12a989 100644 --- a/assets/icons/file_icons/chevron_up.svg +++ b/assets/icons/file_icons/chevron_up.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/code.svg b/assets/icons/file_icons/code.svg index 5f012f8838..af2f6c5dc0 100644 --- a/assets/icons/file_icons/code.svg +++ b/assets/icons/file_icons/code.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/file_icons/coffeescript.svg b/assets/icons/file_icons/coffeescript.svg index fc49df62c0..e91d187615 100644 --- a/assets/icons/file_icons/coffeescript.svg +++ b/assets/icons/file_icons/coffeescript.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/conversations.svg b/assets/icons/file_icons/conversations.svg index cef764661f..e25ed973ef 100644 --- a/assets/icons/file_icons/conversations.svg +++ b/assets/icons/file_icons/conversations.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/dart.svg b/assets/icons/file_icons/dart.svg index fd3ab01c93..c9ec3de51a 100644 --- a/assets/icons/file_icons/dart.svg +++ b/assets/icons/file_icons/dart.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/database.svg b/assets/icons/file_icons/database.svg index 10fbdcbff4..a8226110d3 100644 --- a/assets/icons/file_icons/database.svg +++ b/assets/icons/file_icons/database.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_icons/diff.svg b/assets/icons/file_icons/diff.svg index 07c46f1799..ec59a0aabe 100644 --- a/assets/icons/file_icons/diff.svg +++ b/assets/icons/file_icons/diff.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_icons/eslint.svg b/assets/icons/file_icons/eslint.svg index 0f42abe691..ba72d9166b 100644 --- a/assets/icons/file_icons/eslint.svg +++ b/assets/icons/file_icons/eslint.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/file.svg b/assets/icons/file_icons/file.svg index 3c72bd3320..790a5f18d7 100644 --- a/assets/icons/file_icons/file.svg +++ b/assets/icons/file_icons/file.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_icons/folder.svg b/assets/icons/file_icons/folder.svg index a76dc63d1a..e40613000d 100644 --- a/assets/icons/file_icons/folder.svg +++ b/assets/icons/file_icons/folder.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/folder_open.svg b/assets/icons/file_icons/folder_open.svg index ef37f55f83..55231fb6ab 100644 --- a/assets/icons/file_icons/folder_open.svg +++ b/assets/icons/file_icons/folder_open.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/file_icons/font.svg b/assets/icons/file_icons/font.svg index 4cb01a28f2..6f2b734b26 100644 --- a/assets/icons/file_icons/font.svg +++ b/assets/icons/file_icons/font.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/git.svg b/assets/icons/file_icons/git.svg index 197db2e9e6..2b36b0ffd3 100644 --- a/assets/icons/file_icons/git.svg +++ b/assets/icons/file_icons/git.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/file_icons/gleam.svg b/assets/icons/file_icons/gleam.svg index 6a3dc2c96f..0399bb4dd2 100644 --- a/assets/icons/file_icons/gleam.svg +++ b/assets/icons/file_icons/gleam.svg @@ -1,7 +1,7 @@ - - + + diff --git a/assets/icons/file_icons/graphql.svg b/assets/icons/file_icons/graphql.svg index 9688472599..e6c0368182 100644 --- a/assets/icons/file_icons/graphql.svg +++ b/assets/icons/file_icons/graphql.svg @@ -1,6 +1,6 @@ - - + + diff --git a/assets/icons/file_icons/hash.svg b/assets/icons/file_icons/hash.svg index 2241904266..77e6c60072 100644 --- a/assets/icons/file_icons/hash.svg +++ b/assets/icons/file_icons/hash.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/file_icons/heroku.svg b/assets/icons/file_icons/heroku.svg index 826a88646b..732adf72cb 100644 --- a/assets/icons/file_icons/heroku.svg +++ b/assets/icons/file_icons/heroku.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/html.svg b/assets/icons/file_icons/html.svg index 41f254dd68..8832bcba3a 100644 --- a/assets/icons/file_icons/html.svg +++ b/assets/icons/file_icons/html.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_icons/image.svg b/assets/icons/file_icons/image.svg index 75e64c0a43..c89de1b128 100644 --- a/assets/icons/file_icons/image.svg +++ b/assets/icons/file_icons/image.svg @@ -1,7 +1,7 @@ - - - + + + diff --git a/assets/icons/file_icons/java.svg b/assets/icons/file_icons/java.svg index 63ce6e768c..70d2d10ed7 100644 --- a/assets/icons/file_icons/java.svg +++ b/assets/icons/file_icons/java.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/file_icons/lock.svg b/assets/icons/file_icons/lock.svg index 6bfef249b4..10ae33869a 100644 --- a/assets/icons/file_icons/lock.svg +++ b/assets/icons/file_icons/lock.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/magnifying_glass.svg b/assets/icons/file_icons/magnifying_glass.svg index 75c3e76c80..d0440d905c 100644 --- a/assets/icons/file_icons/magnifying_glass.svg +++ b/assets/icons/file_icons/magnifying_glass.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/nix.svg b/assets/icons/file_icons/nix.svg index 879a4d76aa..215d58a035 100644 --- a/assets/icons/file_icons/nix.svg +++ b/assets/icons/file_icons/nix.svg @@ -1,8 +1,8 @@ - - - - - - + + + + + + diff --git a/assets/icons/file_icons/notebook.svg b/assets/icons/file_icons/notebook.svg index b72ebc3967..968d5c5982 100644 --- a/assets/icons/file_icons/notebook.svg +++ b/assets/icons/file_icons/notebook.svg @@ -1,8 +1,8 @@ - - - - - + + + + + diff --git a/assets/icons/file_icons/package.svg b/assets/icons/file_icons/package.svg index 12889e8084..16bbccb2e6 100644 --- a/assets/icons/file_icons/package.svg +++ b/assets/icons/file_icons/package.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/phoenix.svg b/assets/icons/file_icons/phoenix.svg index b61b8beda7..5db68b4e44 100644 --- a/assets/icons/file_icons/phoenix.svg +++ b/assets/icons/file_icons/phoenix.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/plus.svg b/assets/icons/file_icons/plus.svg index f343d5dd87..3449da3ecd 100644 --- a/assets/icons/file_icons/plus.svg +++ b/assets/icons/file_icons/plus.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/prettier.svg b/assets/icons/file_icons/prettier.svg index 835bd3a126..f01230c33c 100644 --- a/assets/icons/file_icons/prettier.svg +++ b/assets/icons/file_icons/prettier.svg @@ -1,12 +1,12 @@ - - - - - - - - - - + + + + + + + + + + diff --git a/assets/icons/file_icons/project.svg b/assets/icons/file_icons/project.svg index 86a15d41bc..509cc5f4d0 100644 --- a/assets/icons/file_icons/project.svg +++ b/assets/icons/file_icons/project.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/python.svg b/assets/icons/file_icons/python.svg index de904d8e04..b44fdc539d 100644 --- a/assets/icons/file_icons/python.svg +++ b/assets/icons/file_icons/python.svg @@ -1,6 +1,6 @@ - - + + diff --git a/assets/icons/file_icons/replace.svg b/assets/icons/file_icons/replace.svg index 837cb23b66..287328e82e 100644 --- a/assets/icons/file_icons/replace.svg +++ b/assets/icons/file_icons/replace.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/replace_next.svg b/assets/icons/file_icons/replace_next.svg index 72511be70a..a9a9fc91f5 100644 --- a/assets/icons/file_icons/replace_next.svg +++ b/assets/icons/file_icons/replace_next.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/rust.svg b/assets/icons/file_icons/rust.svg index 5db753628a..9e4dc57adb 100644 --- a/assets/icons/file_icons/rust.svg +++ b/assets/icons/file_icons/rust.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/scala.svg b/assets/icons/file_icons/scala.svg index 9e89d1fa82..0884cc96f4 100644 --- a/assets/icons/file_icons/scala.svg +++ b/assets/icons/file_icons/scala.svg @@ -1,7 +1,7 @@ - + diff --git a/assets/icons/file_icons/settings.svg b/assets/icons/file_icons/settings.svg index 081d25bf48..d308135ff1 100644 --- a/assets/icons/file_icons/settings.svg +++ b/assets/icons/file_icons/settings.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/tcl.svg b/assets/icons/file_icons/tcl.svg index bb15b0f8e7..1bd7c4a551 100644 --- a/assets/icons/file_icons/tcl.svg +++ b/assets/icons/file_icons/tcl.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/toml.svg b/assets/icons/file_icons/toml.svg index 9ab78af50f..ae31911d6a 100644 --- a/assets/icons/file_icons/toml.svg +++ b/assets/icons/file_icons/toml.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_icons/video.svg b/assets/icons/file_icons/video.svg index b96e359edb..c249d4c82b 100644 --- a/assets/icons/file_icons/video.svg +++ b/assets/icons/file_icons/video.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/file_icons/vue.svg b/assets/icons/file_icons/vue.svg index 1cbe08dff5..1f993e90ef 100644 --- a/assets/icons/file_icons/vue.svg +++ b/assets/icons/file_icons/vue.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_lock.svg b/assets/icons/file_lock.svg index 6bfef249b4..10ae33869a 100644 --- a/assets/icons/file_lock.svg +++ b/assets/icons/file_lock.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_markdown.svg b/assets/icons/file_markdown.svg index e26d7a532d..26688a3db0 100644 --- a/assets/icons/file_markdown.svg +++ b/assets/icons/file_markdown.svg @@ -1 +1 @@ - + diff --git a/assets/icons/file_rust.svg b/assets/icons/file_rust.svg index 5db753628a..9e4dc57adb 100644 --- a/assets/icons/file_rust.svg +++ b/assets/icons/file_rust.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_text_outlined.svg b/assets/icons/file_text_outlined.svg index bb9b85d62f..d2e8897251 100644 --- a/assets/icons/file_text_outlined.svg +++ b/assets/icons/file_text_outlined.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/file_toml.svg b/assets/icons/file_toml.svg index 9ab78af50f..ae31911d6a 100644 --- a/assets/icons/file_toml.svg +++ b/assets/icons/file_toml.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_tree.svg b/assets/icons/file_tree.svg index 74acb1fc25..baf0e26ce6 100644 --- a/assets/icons/file_tree.svg +++ b/assets/icons/file_tree.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/filter.svg b/assets/icons/filter.svg index 7391fea132..4aa14e93c0 100644 --- a/assets/icons/filter.svg +++ b/assets/icons/filter.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/flame.svg b/assets/icons/flame.svg index 3215f0d5ae..89fc6cab1e 100644 --- a/assets/icons/flame.svg +++ b/assets/icons/flame.svg @@ -1 +1 @@ - + diff --git a/assets/icons/folder.svg b/assets/icons/folder.svg index 0d76b7e3f8..35f4c1f8ac 100644 --- a/assets/icons/folder.svg +++ b/assets/icons/folder.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/folder_open.svg b/assets/icons/folder_open.svg index ef37f55f83..55231fb6ab 100644 --- a/assets/icons/folder_open.svg +++ b/assets/icons/folder_open.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/folder_search.svg b/assets/icons/folder_search.svg index d1bc537c98..207ea5c10e 100644 --- a/assets/icons/folder_search.svg +++ b/assets/icons/folder_search.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/font.svg b/assets/icons/font.svg index 1cc569ecb7..47633a58c9 100644 --- a/assets/icons/font.svg +++ b/assets/icons/font.svg @@ -1 +1 @@ - + diff --git a/assets/icons/font_size.svg b/assets/icons/font_size.svg index fd983cb5d3..4286277bd9 100644 --- a/assets/icons/font_size.svg +++ b/assets/icons/font_size.svg @@ -1 +1 @@ - + diff --git a/assets/icons/font_weight.svg b/assets/icons/font_weight.svg index 73b9852e2f..410f43ec6e 100644 --- a/assets/icons/font_weight.svg +++ b/assets/icons/font_weight.svg @@ -1 +1 @@ - + diff --git a/assets/icons/forward_arrow.svg b/assets/icons/forward_arrow.svg index 503b0b309b..e51796e554 100644 --- a/assets/icons/forward_arrow.svg +++ b/assets/icons/forward_arrow.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/git_branch.svg b/assets/icons/git_branch.svg index 811bc74762..fc6dcfe1b2 100644 --- a/assets/icons/git_branch.svg +++ b/assets/icons/git_branch.svg @@ -1 +1 @@ - + diff --git a/assets/icons/git_branch_alt.svg b/assets/icons/git_branch_alt.svg index d18b072512..cf40195d8b 100644 --- a/assets/icons/git_branch_alt.svg +++ b/assets/icons/git_branch_alt.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/github.svg b/assets/icons/github.svg index fe9186872b..0a12c9b656 100644 --- a/assets/icons/github.svg +++ b/assets/icons/github.svg @@ -1 +1 @@ - + diff --git a/assets/icons/hash.svg b/assets/icons/hash.svg index 9e4dd7c068..afc1f9c0b5 100644 --- a/assets/icons/hash.svg +++ b/assets/icons/hash.svg @@ -1 +1 @@ - + diff --git a/assets/icons/history_rerun.svg b/assets/icons/history_rerun.svg index 9ade606b31..e11e754318 100644 --- a/assets/icons/history_rerun.svg +++ b/assets/icons/history_rerun.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/image.svg b/assets/icons/image.svg index 0a26c35182..e0d73d7621 100644 --- a/assets/icons/image.svg +++ b/assets/icons/image.svg @@ -1 +1 @@ - + diff --git a/assets/icons/info.svg b/assets/icons/info.svg index f3d2e6644f..c000f25867 100644 --- a/assets/icons/info.svg +++ b/assets/icons/info.svg @@ -1,5 +1,5 @@ - - + + diff --git a/assets/icons/json.svg b/assets/icons/json.svg index 5f012f8838..af2f6c5dc0 100644 --- a/assets/icons/json.svg +++ b/assets/icons/json.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/keyboard.svg b/assets/icons/keyboard.svg index de9afd9561..82791cda3f 100644 --- a/assets/icons/keyboard.svg +++ b/assets/icons/keyboard.svg @@ -1 +1 @@ - + diff --git a/assets/icons/knockouts/x_fg.svg b/assets/icons/knockouts/x_fg.svg index a3d47f1373..f459954f72 100644 --- a/assets/icons/knockouts/x_fg.svg +++ b/assets/icons/knockouts/x_fg.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/library.svg b/assets/icons/library.svg index ed59e1818b..fc7f5afcd2 100644 --- a/assets/icons/library.svg +++ b/assets/icons/library.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/line_height.svg b/assets/icons/line_height.svg index 7afa70f767..3929fc4080 100644 --- a/assets/icons/line_height.svg +++ b/assets/icons/line_height.svg @@ -1 +1 @@ - + diff --git a/assets/icons/list_collapse.svg b/assets/icons/list_collapse.svg index 938799b151..f18bc550b9 100644 --- a/assets/icons/list_collapse.svg +++ b/assets/icons/list_collapse.svg @@ -1 +1 @@ - + diff --git a/assets/icons/list_todo.svg b/assets/icons/list_todo.svg index 019af95734..709f26d89d 100644 --- a/assets/icons/list_todo.svg +++ b/assets/icons/list_todo.svg @@ -1 +1 @@ - + diff --git a/assets/icons/list_tree.svg b/assets/icons/list_tree.svg index 09872a60f7..de3e0f3a57 100644 --- a/assets/icons/list_tree.svg +++ b/assets/icons/list_tree.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/list_x.svg b/assets/icons/list_x.svg index 206faf2ce4..0fa3bd68fb 100644 --- a/assets/icons/list_x.svg +++ b/assets/icons/list_x.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/load_circle.svg b/assets/icons/load_circle.svg index 825aa335b0..eecf099310 100644 --- a/assets/icons/load_circle.svg +++ b/assets/icons/load_circle.svg @@ -1 +1 @@ - + diff --git a/assets/icons/location_edit.svg b/assets/icons/location_edit.svg index 02cd6f3389..e342652eb1 100644 --- a/assets/icons/location_edit.svg +++ b/assets/icons/location_edit.svg @@ -1 +1 @@ - + diff --git a/assets/icons/lock_outlined.svg b/assets/icons/lock_outlined.svg index 0bfd2fdc82..d69a245603 100644 --- a/assets/icons/lock_outlined.svg +++ b/assets/icons/lock_outlined.svg @@ -1,6 +1,6 @@ - - + + - + diff --git a/assets/icons/magnifying_glass.svg b/assets/icons/magnifying_glass.svg index b7c22e64bd..24f00bb51b 100644 --- a/assets/icons/magnifying_glass.svg +++ b/assets/icons/magnifying_glass.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/maximize.svg b/assets/icons/maximize.svg index ee03a2c021..7b6d26fed8 100644 --- a/assets/icons/maximize.svg +++ b/assets/icons/maximize.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/menu.svg b/assets/icons/menu.svg index 0724fb2816..f12ce47f7e 100644 --- a/assets/icons/menu.svg +++ b/assets/icons/menu.svg @@ -1 +1 @@ - + diff --git a/assets/icons/menu_alt.svg b/assets/icons/menu_alt.svg index b605e094e3..f73102e286 100644 --- a/assets/icons/menu_alt.svg +++ b/assets/icons/menu_alt.svg @@ -1 +1 @@ - + diff --git a/assets/icons/mic.svg b/assets/icons/mic.svg index 1d9c5bc9ed..000d135ea5 100644 --- a/assets/icons/mic.svg +++ b/assets/icons/mic.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/mic_mute.svg b/assets/icons/mic_mute.svg index 8c61ae2f1c..8bc63be610 100644 --- a/assets/icons/mic_mute.svg +++ b/assets/icons/mic_mute.svg @@ -1,8 +1,8 @@ - - - - - - + + + + + + diff --git a/assets/icons/minimize.svg b/assets/icons/minimize.svg index ea825f054e..082ade47db 100644 --- a/assets/icons/minimize.svg +++ b/assets/icons/minimize.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/notepad.svg b/assets/icons/notepad.svg index 48875eedee..27fd35566e 100644 --- a/assets/icons/notepad.svg +++ b/assets/icons/notepad.svg @@ -1 +1 @@ - + diff --git a/assets/icons/option.svg b/assets/icons/option.svg index 676c10c93b..47201f7c67 100644 --- a/assets/icons/option.svg +++ b/assets/icons/option.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/pencil.svg b/assets/icons/pencil.svg index b913015c08..c4d289e9c0 100644 --- a/assets/icons/pencil.svg +++ b/assets/icons/pencil.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/person.svg b/assets/icons/person.svg index c641678303..a1c29e4acb 100644 --- a/assets/icons/person.svg +++ b/assets/icons/person.svg @@ -1 +1 @@ - + diff --git a/assets/icons/pin.svg b/assets/icons/pin.svg index f3f50cc659..d23daff8b9 100644 --- a/assets/icons/pin.svg +++ b/assets/icons/pin.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/play_filled.svg b/assets/icons/play_filled.svg index c632434305..8075197ad2 100644 --- a/assets/icons/play_filled.svg +++ b/assets/icons/play_filled.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/play_outlined.svg b/assets/icons/play_outlined.svg index 7e1cacd5af..ba1ea2693d 100644 --- a/assets/icons/play_outlined.svg +++ b/assets/icons/play_outlined.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg index e26d430320..8ac57d8cdd 100644 --- a/assets/icons/plus.svg +++ b/assets/icons/plus.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/power.svg b/assets/icons/power.svg index 23f6f48f30..29bd2127c5 100644 --- a/assets/icons/power.svg +++ b/assets/icons/power.svg @@ -1 +1 @@ - + diff --git a/assets/icons/public.svg b/assets/icons/public.svg index 574ee1010d..5659b5419f 100644 --- a/assets/icons/public.svg +++ b/assets/icons/public.svg @@ -1 +1 @@ - + diff --git a/assets/icons/pull_request.svg b/assets/icons/pull_request.svg index ccfaaacfdc..515462ab64 100644 --- a/assets/icons/pull_request.svg +++ b/assets/icons/pull_request.svg @@ -1 +1 @@ - + diff --git a/assets/icons/quote.svg b/assets/icons/quote.svg index 5564a60f95..a958bc67f2 100644 --- a/assets/icons/quote.svg +++ b/assets/icons/quote.svg @@ -1 +1 @@ - + diff --git a/assets/icons/reader.svg b/assets/icons/reader.svg index 2ccc37623d..f477f4f32d 100644 --- a/assets/icons/reader.svg +++ b/assets/icons/reader.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/refresh_title.svg b/assets/icons/refresh_title.svg index 8a8fdb04f3..c9e670bfab 100644 --- a/assets/icons/refresh_title.svg +++ b/assets/icons/refresh_title.svg @@ -1 +1 @@ - + diff --git a/assets/icons/regex.svg b/assets/icons/regex.svg index 0432cd570f..818c2ba360 100644 --- a/assets/icons/regex.svg +++ b/assets/icons/regex.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/repl_neutral.svg b/assets/icons/repl_neutral.svg index d9c8b001df..2842e2c421 100644 --- a/assets/icons/repl_neutral.svg +++ b/assets/icons/repl_neutral.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/repl_off.svg b/assets/icons/repl_off.svg index ac249ad5ff..3018ceaf85 100644 --- a/assets/icons/repl_off.svg +++ b/assets/icons/repl_off.svg @@ -1,11 +1,11 @@ - - - - - - - - - + + + + + + + + + diff --git a/assets/icons/repl_pause.svg b/assets/icons/repl_pause.svg index 5273ed60bb..5a69a576c1 100644 --- a/assets/icons/repl_pause.svg +++ b/assets/icons/repl_pause.svg @@ -1,8 +1,8 @@ - - - - - - + + + + + + diff --git a/assets/icons/repl_play.svg b/assets/icons/repl_play.svg index 76c292a382..0c8f4b0832 100644 --- a/assets/icons/repl_play.svg +++ b/assets/icons/repl_play.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/replace.svg b/assets/icons/replace.svg index 837cb23b66..287328e82e 100644 --- a/assets/icons/replace.svg +++ b/assets/icons/replace.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/replace_next.svg b/assets/icons/replace_next.svg index 72511be70a..a9a9fc91f5 100644 --- a/assets/icons/replace_next.svg +++ b/assets/icons/replace_next.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/rerun.svg b/assets/icons/rerun.svg index a5daa5de1d..1a03a01ae6 100644 --- a/assets/icons/rerun.svg +++ b/assets/icons/rerun.svg @@ -1 +1 @@ - + diff --git a/assets/icons/return.svg b/assets/icons/return.svg index aed9242a95..c605eb6512 100644 --- a/assets/icons/return.svg +++ b/assets/icons/return.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/rotate_ccw.svg b/assets/icons/rotate_ccw.svg index 8f6bd6346a..cdfa8d0ab4 100644 --- a/assets/icons/rotate_ccw.svg +++ b/assets/icons/rotate_ccw.svg @@ -1 +1 @@ - + diff --git a/assets/icons/rotate_cw.svg b/assets/icons/rotate_cw.svg index b082096ee4..2adfa7f972 100644 --- a/assets/icons/rotate_cw.svg +++ b/assets/icons/rotate_cw.svg @@ -1 +1 @@ - + diff --git a/assets/icons/scissors.svg b/assets/icons/scissors.svg index 430293f913..a19580bd89 100644 --- a/assets/icons/scissors.svg +++ b/assets/icons/scissors.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/screen.svg b/assets/icons/screen.svg index 4b686b58f9..4bcdf19528 100644 --- a/assets/icons/screen.svg +++ b/assets/icons/screen.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/select_all.svg b/assets/icons/select_all.svg index c15973c419..4fa17dcf63 100644 --- a/assets/icons/select_all.svg +++ b/assets/icons/select_all.svg @@ -1 +1 @@ - + diff --git a/assets/icons/send.svg b/assets/icons/send.svg index 1403a43ff5..5ceeef2af4 100644 --- a/assets/icons/send.svg +++ b/assets/icons/send.svg @@ -1 +1 @@ - + diff --git a/assets/icons/server.svg b/assets/icons/server.svg index bde19efd75..8d851d1328 100644 --- a/assets/icons/server.svg +++ b/assets/icons/server.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/settings.svg b/assets/icons/settings.svg index 617b14b3cd..33ac74f230 100644 --- a/assets/icons/settings.svg +++ b/assets/icons/settings.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/shield_check.svg b/assets/icons/shield_check.svg index 6e58c31468..43b52f43a8 100644 --- a/assets/icons/shield_check.svg +++ b/assets/icons/shield_check.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/shift.svg b/assets/icons/shift.svg index 35dc2f144c..c38807d8b0 100644 --- a/assets/icons/shift.svg +++ b/assets/icons/shift.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/slash.svg b/assets/icons/slash.svg index e2313f0099..1ebf01eb9f 100644 --- a/assets/icons/slash.svg +++ b/assets/icons/slash.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/sliders.svg b/assets/icons/sliders.svg index 8ab83055ee..20a6a367dc 100644 --- a/assets/icons/sliders.svg +++ b/assets/icons/sliders.svg @@ -1,8 +1,8 @@ - - - - - - + + + + + + diff --git a/assets/icons/space.svg b/assets/icons/space.svg index 86bd55cd53..0294c9bf1e 100644 --- a/assets/icons/space.svg +++ b/assets/icons/space.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/sparkle.svg b/assets/icons/sparkle.svg index e5cce9fafd..535c447723 100644 --- a/assets/icons/sparkle.svg +++ b/assets/icons/sparkle.svg @@ -1 +1 @@ - + diff --git a/assets/icons/split.svg b/assets/icons/split.svg index eb031ab790..b2be46a875 100644 --- a/assets/icons/split.svg +++ b/assets/icons/split.svg @@ -1,5 +1,5 @@ - - + + diff --git a/assets/icons/split_alt.svg b/assets/icons/split_alt.svg index 5b99b7a26a..2f99e1436f 100644 --- a/assets/icons/split_alt.svg +++ b/assets/icons/split_alt.svg @@ -1 +1 @@ - + diff --git a/assets/icons/square_dot.svg b/assets/icons/square_dot.svg index 4bb684afb2..72b3273439 100644 --- a/assets/icons/square_dot.svg +++ b/assets/icons/square_dot.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/square_minus.svg b/assets/icons/square_minus.svg index 4b8fc4d982..5ba458e8b5 100644 --- a/assets/icons/square_minus.svg +++ b/assets/icons/square_minus.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/square_plus.svg b/assets/icons/square_plus.svg index e0ee106b52..063c7dbf82 100644 --- a/assets/icons/square_plus.svg +++ b/assets/icons/square_plus.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/star.svg b/assets/icons/star.svg index fd1502ede8..b39638e386 100644 --- a/assets/icons/star.svg +++ b/assets/icons/star.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/star_filled.svg b/assets/icons/star_filled.svg index d7de9939db..16f64e5cb3 100644 --- a/assets/icons/star_filled.svg +++ b/assets/icons/star_filled.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/stop.svg b/assets/icons/stop.svg index 41e4fd35e9..cc2bbe9207 100644 --- a/assets/icons/stop.svg +++ b/assets/icons/stop.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/swatch_book.svg b/assets/icons/swatch_book.svg index 99a1c88bd5..b37d5df8c1 100644 --- a/assets/icons/swatch_book.svg +++ b/assets/icons/swatch_book.svg @@ -1 +1 @@ - + diff --git a/assets/icons/tab.svg b/assets/icons/tab.svg index f16d51ccf5..db93be4df5 100644 --- a/assets/icons/tab.svg +++ b/assets/icons/tab.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/terminal_alt.svg b/assets/icons/terminal_alt.svg index 82d88167b2..d03c05423e 100644 --- a/assets/icons/terminal_alt.svg +++ b/assets/icons/terminal_alt.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/text_snippet.svg b/assets/icons/text_snippet.svg index 12f131fdd5..b8987546d3 100644 --- a/assets/icons/text_snippet.svg +++ b/assets/icons/text_snippet.svg @@ -1 +1 @@ - + diff --git a/assets/icons/text_thread.svg b/assets/icons/text_thread.svg index 75afa934a0..aa078c72a2 100644 --- a/assets/icons/text_thread.svg +++ b/assets/icons/text_thread.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/thread.svg b/assets/icons/thread.svg index 8c2596a4c9..496cf42e3a 100644 --- a/assets/icons/thread.svg +++ b/assets/icons/thread.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/thread_from_summary.svg b/assets/icons/thread_from_summary.svg index 7519935aff..94ce9562da 100644 --- a/assets/icons/thread_from_summary.svg +++ b/assets/icons/thread_from_summary.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/thumbs_down.svg b/assets/icons/thumbs_down.svg index 334115a014..a396ff14f6 100644 --- a/assets/icons/thumbs_down.svg +++ b/assets/icons/thumbs_down.svg @@ -1 +1 @@ - + diff --git a/assets/icons/thumbs_up.svg b/assets/icons/thumbs_up.svg index b1e435936b..73c859c355 100644 --- a/assets/icons/thumbs_up.svg +++ b/assets/icons/thumbs_up.svg @@ -1 +1 @@ - + diff --git a/assets/icons/todo_complete.svg b/assets/icons/todo_complete.svg index d50044e435..5bf70841a8 100644 --- a/assets/icons/todo_complete.svg +++ b/assets/icons/todo_complete.svg @@ -1 +1 @@ - + diff --git a/assets/icons/todo_pending.svg b/assets/icons/todo_pending.svg index dfb013b52b..e5e9776f11 100644 --- a/assets/icons/todo_pending.svg +++ b/assets/icons/todo_pending.svg @@ -1,10 +1,10 @@ - - - - - - - - + + + + + + + + diff --git a/assets/icons/todo_progress.svg b/assets/icons/todo_progress.svg index 9b2ed7375d..b4a3e8c50e 100644 --- a/assets/icons/todo_progress.svg +++ b/assets/icons/todo_progress.svg @@ -1,11 +1,11 @@ - - - - - - - - - + + + + + + + + + diff --git a/assets/icons/tool_copy.svg b/assets/icons/tool_copy.svg index e722d8a022..a497a5c9cb 100644 --- a/assets/icons/tool_copy.svg +++ b/assets/icons/tool_copy.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/tool_delete_file.svg b/assets/icons/tool_delete_file.svg index 3276f3d78e..e15c0cb568 100644 --- a/assets/icons/tool_delete_file.svg +++ b/assets/icons/tool_delete_file.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/tool_diagnostics.svg b/assets/icons/tool_diagnostics.svg index c659d96781..414810628d 100644 --- a/assets/icons/tool_diagnostics.svg +++ b/assets/icons/tool_diagnostics.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/tool_folder.svg b/assets/icons/tool_folder.svg index 0d76b7e3f8..35f4c1f8ac 100644 --- a/assets/icons/tool_folder.svg +++ b/assets/icons/tool_folder.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/tool_hammer.svg b/assets/icons/tool_hammer.svg index e66173ce70..f725012cdf 100644 --- a/assets/icons/tool_hammer.svg +++ b/assets/icons/tool_hammer.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/tool_notification.svg b/assets/icons/tool_notification.svg index 7510b32040..7903a3369a 100644 --- a/assets/icons/tool_notification.svg +++ b/assets/icons/tool_notification.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/tool_pencil.svg b/assets/icons/tool_pencil.svg index b913015c08..c4d289e9c0 100644 --- a/assets/icons/tool_pencil.svg +++ b/assets/icons/tool_pencil.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/tool_read.svg b/assets/icons/tool_read.svg index 458cbb3660..d22e9d8c7d 100644 --- a/assets/icons/tool_read.svg +++ b/assets/icons/tool_read.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/tool_regex.svg b/assets/icons/tool_regex.svg index 0432cd570f..818c2ba360 100644 --- a/assets/icons/tool_regex.svg +++ b/assets/icons/tool_regex.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/tool_search.svg b/assets/icons/tool_search.svg index 4f2750cfa2..b225a1298e 100644 --- a/assets/icons/tool_search.svg +++ b/assets/icons/tool_search.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/tool_terminal.svg b/assets/icons/tool_terminal.svg index 3c4ab42a4d..24da5e3a10 100644 --- a/assets/icons/tool_terminal.svg +++ b/assets/icons/tool_terminal.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/tool_think.svg b/assets/icons/tool_think.svg index 595f8070d8..efd5908a90 100644 --- a/assets/icons/tool_think.svg +++ b/assets/icons/tool_think.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/tool_web.svg b/assets/icons/tool_web.svg index 6250a9f05a..288b54c432 100644 --- a/assets/icons/tool_web.svg +++ b/assets/icons/tool_web.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/trash.svg b/assets/icons/trash.svg index 1322e90f9f..4a9e9add02 100644 --- a/assets/icons/trash.svg +++ b/assets/icons/trash.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/undo.svg b/assets/icons/undo.svg index b2407456dc..c714b58747 100644 --- a/assets/icons/undo.svg +++ b/assets/icons/undo.svg @@ -1 +1 @@ - + diff --git a/assets/icons/user_check.svg b/assets/icons/user_check.svg index cd682b5eda..ee32a52590 100644 --- a/assets/icons/user_check.svg +++ b/assets/icons/user_check.svg @@ -1 +1 @@ - + diff --git a/assets/icons/user_group.svg b/assets/icons/user_group.svg index ac1f7bdc63..30d2e5a7ea 100644 --- a/assets/icons/user_group.svg +++ b/assets/icons/user_group.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/user_round_pen.svg b/assets/icons/user_round_pen.svg index eb75517323..e684fd1a20 100644 --- a/assets/icons/user_round_pen.svg +++ b/assets/icons/user_round_pen.svg @@ -1 +1 @@ - + diff --git a/assets/icons/warning.svg b/assets/icons/warning.svg index 456799fa5a..5af37dab9d 100644 --- a/assets/icons/warning.svg +++ b/assets/icons/warning.svg @@ -1 +1 @@ - + diff --git a/assets/icons/whole_word.svg b/assets/icons/whole_word.svg index 77cecce38c..ce0d1606c8 100644 --- a/assets/icons/whole_word.svg +++ b/assets/icons/whole_word.svg @@ -1 +1 @@ - + diff --git a/assets/icons/x_circle.svg b/assets/icons/x_circle.svg index 69aaa3f6a1..8807e5fa1f 100644 --- a/assets/icons/x_circle.svg +++ b/assets/icons/x_circle.svg @@ -1 +1 @@ - + diff --git a/assets/icons/zed_assistant.svg b/assets/icons/zed_assistant.svg index d21252de8c..470eb0fede 100644 --- a/assets/icons/zed_assistant.svg +++ b/assets/icons/zed_assistant.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/zed_burn_mode.svg b/assets/icons/zed_burn_mode.svg index f6192d16e7..cad6ed666b 100644 --- a/assets/icons/zed_burn_mode.svg +++ b/assets/icons/zed_burn_mode.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/zed_burn_mode_on.svg b/assets/icons/zed_burn_mode_on.svg index 29a74a3e63..10e0e42b13 100644 --- a/assets/icons/zed_burn_mode_on.svg +++ b/assets/icons/zed_burn_mode_on.svg @@ -1 +1 @@ - + diff --git a/assets/icons/zed_mcp_custom.svg b/assets/icons/zed_mcp_custom.svg index 6410a26fca..feff2d7d34 100644 --- a/assets/icons/zed_mcp_custom.svg +++ b/assets/icons/zed_mcp_custom.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/zed_mcp_extension.svg b/assets/icons/zed_mcp_extension.svg index 996e0c1920..00117efcf4 100644 --- a/assets/icons/zed_mcp_extension.svg +++ b/assets/icons/zed_mcp_extension.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/zed_predict.svg b/assets/icons/zed_predict.svg index 79fd8c8fc1..605a0584d5 100644 --- a/assets/icons/zed_predict.svg +++ b/assets/icons/zed_predict.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/zed_predict_down.svg b/assets/icons/zed_predict_down.svg index 4532ad7e26..79eef9b0b4 100644 --- a/assets/icons/zed_predict_down.svg +++ b/assets/icons/zed_predict_down.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/zed_predict_error.svg b/assets/icons/zed_predict_error.svg index b2dc339fe9..6f75326179 100644 --- a/assets/icons/zed_predict_error.svg +++ b/assets/icons/zed_predict_error.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/zed_predict_up.svg b/assets/icons/zed_predict_up.svg index 61ec143022..f77001e4bd 100644 --- a/assets/icons/zed_predict_up.svg +++ b/assets/icons/zed_predict_up.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/crates/icons/README.md b/crates/icons/README.md index 71bc5c8545..e340a00277 100644 --- a/crates/icons/README.md +++ b/crates/icons/README.md @@ -6,7 +6,7 @@ Icons are a big part of Zed, and they're how we convey hundreds of actions witho When introducing a new icon, it's important to ensure consistency with the existing set, which follows these guidelines: 1. The SVG view box should be 16x16. -2. For outlined icons, use a 1.5px stroke width. +2. For outlined icons, use a 1.2px stroke width. 3. Not all icons are mathematically aligned; there's quite a bit of optical adjustment. However, try to keep the icon within an internal 12x12 bounding box as much as possible while ensuring proper visibility. 4. Use the `filled` and `outlined` terminology when introducing icons that will have these two variants. 5. Icons that are deeply contextual may have the feature context as their name prefix. For example, `ToolWeb`, `ReplPlay`, `DebugStepInto`, etc. From 9cd13a35de2fb658fee3af30a4863333816828b8 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Sun, 17 Aug 2025 13:39:14 -0300 Subject: [PATCH 414/693] agent2: Experiment with new toolbar design (#36366) Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 72 ++++++++++++++------------- crates/agent_ui/src/thread_history.rs | 1 + 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 44d605af57..b01bf39728 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -65,8 +65,8 @@ use theme::ThemeSettings; use time::UtcOffset; use ui::utils::WithRemSize; use ui::{ - Banner, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding, PopoverMenu, - PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*, + Banner, Callout, ContextMenu, ContextMenuEntry, Divider, ElevationIndex, KeyBinding, + PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*, }; use util::ResultExt as _; use workspace::{ @@ -243,9 +243,9 @@ pub enum AgentType { impl AgentType { fn label(self) -> impl Into { match self { - Self::Zed | Self::TextThread => "Zed", + Self::Zed | Self::TextThread => "Zed Agent", Self::NativeAgent => "Agent 2", - Self::Gemini => "Gemini", + Self::Gemini => "Google Gemini", Self::ClaudeCode => "Claude Code", } } @@ -1784,7 +1784,8 @@ impl AgentPanel { .w_full() .child(change_title_editor.clone()) .child( - ui::IconButton::new("retry-summary-generation", IconName::RotateCcw) + IconButton::new("retry-summary-generation", IconName::RotateCcw) + .icon_size(IconSize::Small) .on_click({ let active_thread = active_thread.clone(); move |_, _window, cx| { @@ -1836,7 +1837,8 @@ impl AgentPanel { .w_full() .child(title_editor.clone()) .child( - ui::IconButton::new("retry-summary-generation", IconName::RotateCcw) + IconButton::new("retry-summary-generation", IconName::RotateCcw) + .icon_size(IconSize::Small) .on_click({ let context_editor = context_editor.clone(); move |_, _window, cx| { @@ -1974,21 +1976,17 @@ impl AgentPanel { }) } - fn render_recent_entries_menu( - &self, - icon: IconName, - cx: &mut Context, - ) -> impl IntoElement { + fn render_recent_entries_menu(&self, cx: &mut Context) -> impl IntoElement { let focus_handle = self.focus_handle(cx); PopoverMenu::new("agent-nav-menu") .trigger_with_tooltip( - IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small), + IconButton::new("agent-nav-menu", IconName::MenuAlt).icon_size(IconSize::Small), { let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( - "Toggle Panel Menu", + "Toggle Recent Threads", &ToggleNavigationMenu, &focus_handle, window, @@ -2124,9 +2122,7 @@ impl AgentPanel { .pl(DynamicSpacing::Base04.rems(cx)) .child(self.render_toolbar_back_button(cx)) .into_any_element(), - _ => self - .render_recent_entries_menu(IconName::MenuAlt, cx) - .into_any_element(), + _ => self.render_recent_entries_menu(cx).into_any_element(), }) .child(self.render_title_view(window, cx)), ) @@ -2364,6 +2360,22 @@ impl AgentPanel { } }); + let selected_agent_label = self.selected_agent.label().into(); + let selected_agent = div() + .id("selected_agent_icon") + .px(DynamicSpacing::Base02.rems(cx)) + .child(Icon::new(self.selected_agent.icon()).color(Color::Muted)) + .tooltip(move |window, cx| { + Tooltip::with_meta( + selected_agent_label.clone(), + None, + "Selected Agent", + window, + cx, + ) + }) + .into_any_element(); + h_flex() .id("agent-panel-toolbar") .h(Tab::container_height(cx)) @@ -2377,26 +2389,17 @@ impl AgentPanel { .child( h_flex() .size_full() - .gap(DynamicSpacing::Base08.rems(cx)) + .gap(DynamicSpacing::Base04.rems(cx)) + .pl(DynamicSpacing::Base04.rems(cx)) .child(match &self.active_view { - ActiveView::History | ActiveView::Configuration => div() - .pl(DynamicSpacing::Base04.rems(cx)) - .child(self.render_toolbar_back_button(cx)) - .into_any_element(), + ActiveView::History | ActiveView::Configuration => { + self.render_toolbar_back_button(cx).into_any_element() + } _ => h_flex() - .h_full() - .px(DynamicSpacing::Base04.rems(cx)) - .border_r_1() - .border_color(cx.theme().colors().border) - .child( - h_flex() - .px_0p5() - .gap_1p5() - .child( - Icon::new(self.selected_agent.icon()).color(Color::Muted), - ) - .child(Label::new(self.selected_agent.label())), - ) + .gap_1() + .child(self.render_recent_entries_menu(cx)) + .child(Divider::vertical()) + .child(selected_agent) .into_any_element(), }) .child(self.render_title_view(window, cx)), @@ -2415,7 +2418,6 @@ impl AgentPanel { .border_l_1() .border_color(cx.theme().colors().border) .child(new_thread_menu) - .child(self.render_recent_entries_menu(IconName::HistoryRerun, cx)) .child(self.render_panel_options_menu(window, cx)), ), ) diff --git a/crates/agent_ui/src/thread_history.rs b/crates/agent_ui/src/thread_history.rs index b8d1db88d6..66afe2c2c5 100644 --- a/crates/agent_ui/src/thread_history.rs +++ b/crates/agent_ui/src/thread_history.rs @@ -541,6 +541,7 @@ impl Render for ThreadHistory { v_flex() .key_context("ThreadHistory") .size_full() + .bg(cx.theme().colors().panel_background) .on_action(cx.listener(Self::select_previous)) .on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_first)) From 46a2d8d95aad9e0070f683050703bec384f2fec4 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Sun, 17 Aug 2025 14:03:58 -0300 Subject: [PATCH 415/693] git: Refine clone repo modal design (#36369) Release Notes: - N/A --- crates/git_ui/src/git_ui.rs | 87 +++++++++----------- crates/recent_projects/src/remote_servers.rs | 7 +- 2 files changed, 41 insertions(+), 53 deletions(-) diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 79aa4a6bd0..3b4196b8ec 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -3,7 +3,7 @@ use std::any::Any; use ::settings::Settings; use command_palette_hooks::CommandPaletteFilter; use commit_modal::CommitModal; -use editor::{Editor, EditorElement, EditorStyle, actions::DiffClipboardWithSelectionData}; +use editor::{Editor, actions::DiffClipboardWithSelectionData}; mod blame_ui; use git::{ repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus}, @@ -11,12 +11,11 @@ use git::{ }; use git_panel_settings::GitPanelSettings; use gpui::{ - Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, TextStyle, - Window, actions, + Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Window, + actions, }; use onboarding::GitOnboardingModal; use project_diff::ProjectDiff; -use theme::ThemeSettings; use ui::prelude::*; use workspace::{ModalView, Workspace}; use zed_actions; @@ -637,7 +636,7 @@ impl GitCloneModal { pub fn show(panel: Entity, window: &mut Window, cx: &mut Context) -> Self { let repo_input = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Enter repository", cx); + editor.set_placeholder_text("Enter repository URL…", cx); editor }); let focus_handle = repo_input.focus_handle(cx); @@ -650,46 +649,6 @@ impl GitCloneModal { focus_handle, } } - - fn render_editor(&self, window: &Window, cx: &App) -> impl IntoElement { - let settings = ThemeSettings::get_global(cx); - let theme = cx.theme(); - - let text_style = TextStyle { - color: cx.theme().colors().text, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features.clone(), - font_size: settings.buffer_font_size(cx).into(), - font_weight: settings.buffer_font.weight, - line_height: relative(settings.buffer_line_height.value()), - background_color: Some(theme.colors().editor_background), - ..Default::default() - }; - - let element = EditorElement::new( - &self.repo_input, - EditorStyle { - background: theme.colors().editor_background, - local_player: theme.players().local(), - text: text_style, - ..Default::default() - }, - ); - - div() - .rounded_md() - .p_1() - .border_1() - .border_color(theme.colors().border_variant) - .when( - self.repo_input - .focus_handle(cx) - .contains_focused(window, cx), - |this| this.border_color(theme.colors().border_focused), - ) - .child(element) - .bg(theme.colors().editor_background) - } } impl Focusable for GitCloneModal { @@ -699,12 +658,42 @@ impl Focusable for GitCloneModal { } impl Render for GitCloneModal { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { div() - .size_full() - .w(rems(34.)) .elevation_3(cx) - .child(self.render_editor(window, cx)) + .w(rems(34.)) + .flex_1() + .overflow_hidden() + .child( + div() + .w_full() + .p_2() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .child(self.repo_input.clone()), + ) + .child( + h_flex() + .w_full() + .p_2() + .gap_0p5() + .rounded_b_sm() + .bg(cx.theme().colors().editor_background) + .child( + Label::new("Clone a repository from GitHub or other sources.") + .color(Color::Muted) + .size(LabelSize::Small), + ) + .child( + Button::new("learn-more", "Learn More") + .label_size(LabelSize::Small) + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::XSmall) + .on_click(|_, _, cx| { + cx.open_url("https://github.com/git-guides/git-clone"); + }), + ), + ) .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| { cx.emit(DismissEvent); })) diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index e5e166cb4c..81259c1aac 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -1094,11 +1094,10 @@ impl RemoteServerProjects { .size(LabelSize::Small), ) .child( - Button::new("learn-more", "Learn more…") + Button::new("learn-more", "Learn More") .label_size(LabelSize::Small) - .size(ButtonSize::None) - .color(Color::Accent) - .style(ButtonStyle::Transparent) + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::XSmall) .on_click(|_, _, cx| { cx.open_url( "https://zed.dev/docs/remote-development", From 8282b9cf000d3636fd69d29a00260edb6edecd63 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Sun, 17 Aug 2025 14:27:42 -0300 Subject: [PATCH 416/693] project panel: Add git clone action to empty state (#36371) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds the git clone action to the project panel. It also changes the "open" button to open a folder instead of the recent projects modal, which feels faster to start with, more intuitive, and also consistent with VS Code (which I think is good in this specific case). CleanShot 2025-08-17 at 2  10 01@2x Release Notes: - Improved the project panel empty state by including the git clone action and allowing users to quickly open a local folder. --- crates/onboarding/src/welcome.rs | 2 +- crates/project_panel/src/project_panel.rs | 41 ++++++++++++++++++----- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index ba0053a3b6..610f6a98e3 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -37,7 +37,7 @@ const CONTENT: (Section<4>, Section<3>) = ( }, SectionEntry { icon: IconName::CloudDownload, - title: "Clone a Repo", + title: "Clone Repository", action: &git::Clone, }, SectionEntry { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 4d7f2faf62..d5ddd89419 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -57,9 +57,9 @@ use std::{ }; use theme::ThemeSettings; use ui::{ - Color, ContextMenu, DecoratedIcon, Icon, IconDecoration, IconDecorationKind, IndentGuideColors, - IndentGuideLayout, KeyBinding, Label, LabelSize, ListItem, ListItemSpacing, ScrollableHandle, - Scrollbar, ScrollbarState, StickyCandidate, Tooltip, prelude::*, v_flex, + Color, ContextMenu, DecoratedIcon, Divider, Icon, IconDecoration, IconDecorationKind, + IndentGuideColors, IndentGuideLayout, KeyBinding, Label, LabelSize, ListItem, ListItemSpacing, + ScrollableHandle, Scrollbar, ScrollbarState, StickyCandidate, Tooltip, prelude::*, v_flex, }; use util::{ResultExt, TakeUntilExt, TryFutureExt, maybe, paths::compare_paths}; use workspace::{ @@ -69,7 +69,6 @@ use workspace::{ notifications::{DetachAndPromptErr, NotifyTaskExt}, }; use worktree::CreatedEntry; -use zed_actions::OpenRecent; const PROJECT_PANEL_KEY: &str = "ProjectPanel"; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; @@ -5521,24 +5520,48 @@ impl Render for ProjectPanel { .with_priority(3) })) } else { + let focus_handle = self.focus_handle(cx).clone(); + v_flex() .id("empty-project_panel") - .size_full() .p_4() + .size_full() + .items_center() + .justify_center() + .gap_1() .track_focus(&self.focus_handle(cx)) .child( - Button::new("open_project", "Open a project") + Button::new("open_project", "Open Project") .full_width() .key_binding(KeyBinding::for_action_in( - &OpenRecent::default(), - &self.focus_handle, + &workspace::Open, + &focus_handle, window, cx, )) .on_click(cx.listener(|this, _, window, cx| { this.workspace .update(cx, |_, cx| { - window.dispatch_action(OpenRecent::default().boxed_clone(), cx); + window.dispatch_action(workspace::Open.boxed_clone(), cx); + }) + .log_err(); + })), + ) + .child( + h_flex() + .w_1_2() + .gap_2() + .child(Divider::horizontal()) + .child(Label::new("or").size(LabelSize::XSmall).color(Color::Muted)) + .child(Divider::horizontal()), + ) + .child( + Button::new("clone_repo", "Clone Repository") + .full_width() + .on_click(cx.listener(|this, _, window, cx| { + this.workspace + .update(cx, |_, cx| { + window.dispatch_action(git::Clone.boxed_clone(), cx); }) .log_err(); })), From 2dbc951058fe0b2325bca2452da330f2bafa34d7 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Sun, 17 Aug 2025 16:38:07 -0400 Subject: [PATCH 417/693] agent2: Start loading mentioned threads and text threads as soon as they're added (#36374) Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/agent_ui/src/acp/message_editor.rs | 300 ++++++++++++++++------ 1 file changed, 217 insertions(+), 83 deletions(-) diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index f6fee3b87e..12766ef458 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -207,11 +207,13 @@ impl MessageEditor { cx, ); } - MentionUri::Symbol { .. } - | MentionUri::Thread { .. } - | MentionUri::TextThread { .. } - | MentionUri::Rule { .. } - | MentionUri::Selection { .. } => { + MentionUri::Thread { id, name } => { + self.confirm_mention_for_thread(crease_id, anchor, id, name, window, cx); + } + MentionUri::TextThread { path, name } => { + self.confirm_mention_for_text_thread(crease_id, anchor, path, name, window, cx); + } + MentionUri::Symbol { .. } | MentionUri::Rule { .. } | MentionUri::Selection { .. } => { self.mention_set.insert_uri(crease_id, mention_uri.clone()); } } @@ -363,13 +365,9 @@ impl MessageEditor { window: &mut Window, cx: &mut Context, ) -> Task>> { - let contents = self.mention_set.contents( - self.project.clone(), - self.thread_store.clone(), - self.text_thread_store.clone(), - window, - cx, - ); + let contents = + self.mention_set + .contents(self.project.clone(), self.thread_store.clone(), window, cx); let editor = self.editor.clone(); cx.spawn(async move |_, cx| { @@ -591,52 +589,154 @@ impl MessageEditor { ) { let editor = self.editor.clone(); let task = cx - .spawn_in(window, async move |this, cx| { - let image = image.await.map_err(|e| e.to_string())?; - let format = image.format; - let image = cx - .update(|_, cx| LanguageModelImage::from_image(image, cx)) - .map_err(|e| e.to_string())? - .await; - if let Some(image) = image { - if let Some(abs_path) = abs_path.clone() { - this.update(cx, |this, _cx| { - this.mention_set.insert_uri( - crease_id, - MentionUri::File { - abs_path, - is_directory: false, - }, - ); + .spawn_in(window, { + let abs_path = abs_path.clone(); + async move |_, cx| { + let image = image.await.map_err(|e| e.to_string())?; + let format = image.format; + let image = cx + .update(|_, cx| LanguageModelImage::from_image(image, cx)) + .map_err(|e| e.to_string())? + .await; + if let Some(image) = image { + Ok(MentionImage { + abs_path, + data: image.source, + format, }) - .map_err(|e| e.to_string())?; + } else { + Err("Failed to convert image".into()) } - Ok(MentionImage { - abs_path, - data: image.source, - format, - }) - } else { - editor - .update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - Err("Failed to convert image".to_string()) } }) .shared(); - cx.spawn_in(window, { - let task = task.clone(); - async move |_, cx| task.clone().await.notify_async_err(cx) + self.mention_set.insert_image(crease_id, task.clone()); + + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_some() { + if let Some(abs_path) = abs_path.clone() { + this.update(cx, |this, _cx| { + this.mention_set.insert_uri( + crease_id, + MentionUri::File { + abs_path, + is_directory: false, + }, + ); + }) + .ok(); + } + } else { + editor + .update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + } }) .detach(); + } - self.mention_set.insert_image(crease_id, task); + fn confirm_mention_for_thread( + &mut self, + crease_id: CreaseId, + anchor: Anchor, + id: ThreadId, + name: String, + window: &mut Window, + cx: &mut Context, + ) { + let uri = MentionUri::Thread { + id: id.clone(), + name, + }; + let open_task = self.thread_store.update(cx, |thread_store, cx| { + thread_store.open_thread(&id, window, cx) + }); + let task = cx + .spawn(async move |_, cx| { + let thread = open_task.await.map_err(|e| e.to_string())?; + let content = thread + .read_with(cx, |thread, _cx| thread.latest_detailed_summary_or_text()) + .map_err(|e| e.to_string())?; + Ok(content) + }) + .shared(); + + self.mention_set.insert_thread(id, task.clone()); + + let editor = self.editor.clone(); + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_some() { + this.update(cx, |this, _| { + this.mention_set.insert_uri(crease_id, uri); + }) + .ok(); + } else { + editor + .update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + } + }) + .detach(); + } + + fn confirm_mention_for_text_thread( + &mut self, + crease_id: CreaseId, + anchor: Anchor, + path: PathBuf, + name: String, + window: &mut Window, + cx: &mut Context, + ) { + let uri = MentionUri::TextThread { + path: path.clone(), + name, + }; + let context = self.text_thread_store.update(cx, |text_thread_store, cx| { + text_thread_store.open_local_context(path.as_path().into(), cx) + }); + let task = cx + .spawn(async move |_, cx| { + let context = context.await.map_err(|e| e.to_string())?; + let xml = context + .update(cx, |context, cx| context.to_xml(cx)) + .map_err(|e| e.to_string())?; + Ok(xml) + }) + .shared(); + + self.mention_set.insert_text_thread(path, task.clone()); + + let editor = self.editor.clone(); + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_some() { + this.update(cx, |this, _| { + this.mention_set.insert_uri(crease_id, uri); + }) + .ok(); + } else { + editor + .update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + } + }) + .detach(); } pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context) { @@ -671,7 +771,7 @@ impl MessageEditor { let start = text.len(); write!(&mut text, "{}", mention_uri.as_link()).ok(); let end = text.len(); - mentions.push((start..end, mention_uri)); + mentions.push((start..end, mention_uri, resource.text)); } } acp::ContentBlock::Image(content) => { @@ -691,7 +791,7 @@ impl MessageEditor { editor.buffer().read(cx).snapshot(cx) }); - for (range, mention_uri) in mentions { + for (range, mention_uri, text) in mentions { let anchor = snapshot.anchor_before(range.start); let crease_id = crate::context_picker::insert_crease_for_mention( anchor.excerpt_id, @@ -705,7 +805,26 @@ impl MessageEditor { ); if let Some(crease_id) = crease_id { - self.mention_set.insert_uri(crease_id, mention_uri); + self.mention_set.insert_uri(crease_id, mention_uri.clone()); + } + + match mention_uri { + MentionUri::Thread { id, .. } => { + self.mention_set + .insert_thread(id, Task::ready(Ok(text.into())).shared()); + } + MentionUri::TextThread { path, .. } => { + self.mention_set + .insert_text_thread(path, Task::ready(Ok(text)).shared()); + } + MentionUri::Fetch { url } => { + self.mention_set + .add_fetch_result(url, Task::ready(Ok(text)).shared()); + } + MentionUri::File { .. } + | MentionUri::Symbol { .. } + | MentionUri::Rule { .. } + | MentionUri::Selection { .. } => {} } } for (range, content) in images { @@ -905,9 +1024,11 @@ pub struct MentionImage { #[derive(Default)] pub struct MentionSet { - pub(crate) uri_by_crease_id: HashMap, + uri_by_crease_id: HashMap, fetch_results: HashMap>>>, images: HashMap>>>, + thread_summaries: HashMap>>>, + text_thread_summaries: HashMap>>>, } impl MentionSet { @@ -927,8 +1048,18 @@ impl MentionSet { self.images.insert(crease_id, task); } + fn insert_thread(&mut self, id: ThreadId, task: Shared>>) { + self.thread_summaries.insert(id, task); + } + + fn insert_text_thread(&mut self, path: PathBuf, task: Shared>>) { + self.text_thread_summaries.insert(path, task); + } + pub fn drain(&mut self) -> impl Iterator { self.fetch_results.clear(); + self.thread_summaries.clear(); + self.text_thread_summaries.clear(); self.uri_by_crease_id .drain() .map(|(id, _)| id) @@ -939,8 +1070,7 @@ impl MentionSet { &self, project: Entity, thread_store: Entity, - text_thread_store: Entity, - window: &mut Window, + _window: &mut Window, cx: &mut App, ) -> Task>> { let mut processed_image_creases = HashSet::default(); @@ -1010,30 +1140,40 @@ impl MentionSet { anyhow::Ok((crease_id, Mention::Text { uri, content })) }) } - MentionUri::Thread { id: thread_id, .. } => { - let open_task = thread_store.update(cx, |thread_store, cx| { - thread_store.open_thread(&thread_id, window, cx) - }); - + MentionUri::Thread { id, .. } => { + let Some(content) = self.thread_summaries.get(id).cloned() else { + return Task::ready(Err(anyhow!("missing thread summary"))); + }; let uri = uri.clone(); - cx.spawn(async move |cx| { - let thread = open_task.await?; - let content = thread.read_with(cx, |thread, _cx| { - thread.latest_detailed_summary_or_text().to_string() - })?; - - anyhow::Ok((crease_id, Mention::Text { uri, content })) + cx.spawn(async move |_| { + Ok(( + crease_id, + Mention::Text { + uri, + content: content + .await + .map_err(|e| anyhow::anyhow!("{e}"))? + .to_string(), + }, + )) }) } MentionUri::TextThread { path, .. } => { - let context = text_thread_store.update(cx, |text_thread_store, cx| { - text_thread_store.open_local_context(path.as_path().into(), cx) - }); + let Some(content) = self.text_thread_summaries.get(path).cloned() else { + return Task::ready(Err(anyhow!("missing text thread summary"))); + }; let uri = uri.clone(); - cx.spawn(async move |cx| { - let context = context.await?; - let xml = context.update(cx, |context, cx| context.to_xml(cx))?; - anyhow::Ok((crease_id, Mention::Text { uri, content: xml })) + cx.spawn(async move |_| { + Ok(( + crease_id, + Mention::Text { + uri, + content: content + .await + .map_err(|e| anyhow::anyhow!("{e}"))? + .to_string(), + }, + )) }) } MentionUri::Rule { id: prompt_id, .. } => { @@ -1427,7 +1567,6 @@ mod tests { message_editor.mention_set().contents( project.clone(), thread_store.clone(), - text_thread_store.clone(), window, cx, ) @@ -1495,7 +1634,6 @@ mod tests { message_editor.mention_set().contents( project.clone(), thread_store.clone(), - text_thread_store.clone(), window, cx, ) @@ -1616,13 +1754,9 @@ mod tests { let contents = message_editor .update_in(&mut cx, |message_editor, window, cx| { - message_editor.mention_set().contents( - project.clone(), - thread_store, - text_thread_store, - window, - cx, - ) + message_editor + .mention_set() + .contents(project.clone(), thread_store, window, cx) }) .await .unwrap() From 7dc4adbd4027b9b3ba80db589f93be25dcaaa64d Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Mon, 18 Aug 2025 08:16:17 +0530 Subject: [PATCH 418/693] gpui: Fix crash when starting Zed on macOS during texture creation (#36382) Closes #36229 Fix zero-sized texture creation that triggers a SIGABRT in the Metal renderer. Not sure why this happens yet, but it likely occurs when `native_window.contentView()` returns a zero `NSSize` during initial window creation, before the view size is computed. Release Notes: - Fixed a rare startup crash on macOS. --- crates/gpui/src/platform/mac/metal_renderer.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index 629654014d..a686d8c45b 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -314,6 +314,15 @@ impl MetalRenderer { } fn update_path_intermediate_textures(&mut self, size: Size) { + // We are uncertain when this happens, but sometimes size can be 0 here. Most likely before + // the layout pass on window creation. Zero-sized texture creation causes SIGABRT. + // https://github.com/zed-industries/zed/issues/36229 + if size.width.0 <= 0 || size.height.0 <= 0 { + self.path_intermediate_texture = None; + self.path_intermediate_msaa_texture = None; + return; + } + let texture_descriptor = metal::TextureDescriptor::new(); texture_descriptor.set_width(size.width.0 as u64); texture_descriptor.set_height(size.height.0 as u64); From b3969ed427d44077595a329032969f35dc28c0fb Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Mon, 18 Aug 2025 06:07:32 +0200 Subject: [PATCH 419/693] Standardize on canceled instead of cancelled (#36385) Release Notes: - N/A --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- crates/acp_thread/src/acp_thread.rs | 14 +++++++------- crates/agent/src/thread.rs | 4 ++-- crates/agent/src/tool_use.rs | 6 +++--- crates/agent_servers/src/acp/v1.rs | 2 +- crates/agent_servers/src/claude.rs | 17 ++++++++--------- crates/agent_ui/src/active_thread.rs | 2 +- crates/agent_ui/src/message_editor.rs | 6 +++--- 9 files changed, 28 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b4bf705eb9..a4f8c521a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,9 +172,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.25" +version = "0.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab66add8be8d6a963f5bf4070045c1bbf36472837654c73e2298dd16bda5bf7" +checksum = "160971bb53ca0b2e70ebc857c21e24eb448745f1396371015f4c59e9a9e51ed0" dependencies = [ "anyhow", "futures 0.3.31", diff --git a/Cargo.toml b/Cargo.toml index b3105bd97c..14691cf8a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -423,7 +423,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.25" +agent-client-protocol = "0.0.26" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index c1c634612b..fb31265326 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -360,7 +360,7 @@ pub enum ToolCallStatus { Failed, /// The user rejected the tool call. Rejected, - /// The user cancelled generation so the tool call was cancelled. + /// The user canceled generation so the tool call was canceled. Canceled, } @@ -1269,19 +1269,19 @@ impl AcpThread { Err(e) } result => { - let cancelled = matches!( + let canceled = matches!( result, Ok(Ok(acp::PromptResponse { - stop_reason: acp::StopReason::Cancelled + stop_reason: acp::StopReason::Canceled })) ); - // We only take the task if the current prompt wasn't cancelled. + // We only take the task if the current prompt wasn't canceled. // - // This prompt may have been cancelled because another one was sent + // This prompt may have been canceled because another one was sent // while it was still generating. In these cases, dropping `send_task` - // would cause the next generation to be cancelled. - if !cancelled { + // would cause the next generation to be canceled. + if !canceled { this.send_task.take(); } diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index f3f1088483..5491842185 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -5337,7 +5337,7 @@ fn main() {{ } #[gpui::test] - async fn test_retry_cancelled_on_stop(cx: &mut TestAppContext) { + async fn test_retry_canceled_on_stop(cx: &mut TestAppContext) { init_test_settings(cx); let project = create_test_project(cx, json!({})).await; @@ -5393,7 +5393,7 @@ fn main() {{ "Should have no pending completions after cancellation" ); - // Verify the retry was cancelled by checking retry state + // Verify the retry was canceled by checking retry state thread.read_with(cx, |thread, _| { if let Some(retry_state) = &thread.retry_state { panic!( diff --git a/crates/agent/src/tool_use.rs b/crates/agent/src/tool_use.rs index 7392c0878d..74dfaf9a85 100644 --- a/crates/agent/src/tool_use.rs +++ b/crates/agent/src/tool_use.rs @@ -137,7 +137,7 @@ impl ToolUseState { } pub fn cancel_pending(&mut self) -> Vec { - let mut cancelled_tool_uses = Vec::new(); + let mut canceled_tool_uses = Vec::new(); self.pending_tool_uses_by_id .retain(|tool_use_id, tool_use| { if matches!(tool_use.status, PendingToolUseStatus::Error { .. }) { @@ -155,10 +155,10 @@ impl ToolUseState { is_error: true, }, ); - cancelled_tool_uses.push(tool_use.clone()); + canceled_tool_uses.push(tool_use.clone()); false }); - cancelled_tool_uses + canceled_tool_uses } pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> { diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index 506ae80886..b77b5ef36d 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -237,7 +237,7 @@ impl acp::Client for ClientDelegate { let outcome = match result { Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option }, - Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled, + Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Canceled, }; Ok(acp::RequestPermissionResponse { outcome }) diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 4b3a173349..d15cc1dd89 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -285,7 +285,7 @@ impl AgentConnection for ClaudeAgentConnection { let turn_state = session.turn_state.take(); let TurnState::InProgress { end_tx } = turn_state else { - // Already cancelled or idle, put it back + // Already canceled or idle, put it back session.turn_state.replace(turn_state); return; }; @@ -389,7 +389,7 @@ enum TurnState { } impl TurnState { - fn is_cancelled(&self) -> bool { + fn is_canceled(&self) -> bool { matches!(self, TurnState::CancelConfirmed { .. }) } @@ -439,7 +439,7 @@ impl ClaudeAgentSession { for chunk in message.content.chunks() { match chunk { ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => { - if !turn_state.borrow().is_cancelled() { + if !turn_state.borrow().is_canceled() { thread .update(cx, |thread, cx| { thread.push_user_content_block(None, text.into(), cx) @@ -458,8 +458,8 @@ impl ClaudeAgentSession { acp::ToolCallUpdate { id: acp::ToolCallId(tool_use_id.into()), fields: acp::ToolCallUpdateFields { - status: if turn_state.borrow().is_cancelled() { - // Do not set to completed if turn was cancelled + status: if turn_state.borrow().is_canceled() { + // Do not set to completed if turn was canceled None } else { Some(acp::ToolCallStatus::Completed) @@ -592,14 +592,13 @@ impl ClaudeAgentSession { .. } => { let turn_state = turn_state.take(); - let was_cancelled = turn_state.is_cancelled(); + let was_canceled = turn_state.is_canceled(); let Some(end_turn_tx) = turn_state.end_tx() else { debug_panic!("Received `SdkMessage::Result` but there wasn't an active turn"); return; }; - if is_error || (!was_cancelled && subtype == ResultErrorType::ErrorDuringExecution) - { + if is_error || (!was_canceled && subtype == ResultErrorType::ErrorDuringExecution) { end_turn_tx .send(Err(anyhow!( "Error: {}", @@ -610,7 +609,7 @@ impl ClaudeAgentSession { let stop_reason = match subtype { ResultErrorType::Success => acp::StopReason::EndTurn, ResultErrorType::ErrorMaxTurns => acp::StopReason::MaxTurnRequests, - ResultErrorType::ErrorDuringExecution => acp::StopReason::Cancelled, + ResultErrorType::ErrorDuringExecution => acp::StopReason::Canceled, }; end_turn_tx .send(Ok(acp::PromptResponse { stop_reason })) diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index ffed62d41f..116c2b901b 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -4020,7 +4020,7 @@ mod tests { cx.run_until_parked(); - // Verify that the previous completion was cancelled + // Verify that the previous completion was canceled assert_eq!(cancellation_events.lock().unwrap().len(), 1); // Verify that a new request was started after cancellation diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 127e9256be..d6c9a778a6 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -441,11 +441,11 @@ impl MessageEditor { thread.cancel_editing(cx); }); - let cancelled = self.thread.update(cx, |thread, cx| { + let canceled = self.thread.update(cx, |thread, cx| { thread.cancel_last_completion(Some(window.window_handle()), cx) }); - if cancelled { + if canceled { self.set_editor_is_expanded(false, cx); self.send_to_model(window, cx); } @@ -1404,7 +1404,7 @@ impl MessageEditor { }) .ok(); }); - // Replace existing load task, if any, causing it to be cancelled. + // Replace existing load task, if any, causing it to be canceled. let load_task = load_task.shared(); self.load_context_task = Some(load_task.clone()); cx.spawn(async move |this, cx| { From ea828c0c597a00bd84941ca163dc1f063d14ae89 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Mon, 18 Aug 2025 09:58:30 +0200 Subject: [PATCH 420/693] agent2: Emit cancellation stop reason on cancel (#36381) Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra --- crates/agent2/src/tests/mod.rs | 66 +++++++++- crates/agent2/src/thread.rs | 218 +++++++++++++++++++-------------- 2 files changed, 191 insertions(+), 93 deletions(-) diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index cc8bd483bb..48a16bf685 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -941,7 +941,15 @@ async fn test_cancellation(cx: &mut TestAppContext) { // Cancel the current send and ensure that the event stream is closed, even // if one of the tools is still running. thread.update(cx, |thread, _cx| thread.cancel()); - events.collect::>().await; + let events = events.collect::>().await; + let last_event = events.last(); + assert!( + matches!( + last_event, + Some(Ok(AgentResponseEvent::Stop(acp::StopReason::Canceled))) + ), + "unexpected event {last_event:?}" + ); // Ensure we can still send a new message after cancellation. let events = thread @@ -965,6 +973,62 @@ async fn test_cancellation(cx: &mut TestAppContext) { assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); } +#[gpui::test] +async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let events_1 = thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello 1"], cx) + }); + cx.run_until_parked(); + fake_model.send_last_completion_stream_text_chunk("Hey 1!"); + cx.run_until_parked(); + + let events_2 = thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello 2"], cx) + }); + cx.run_until_parked(); + fake_model.send_last_completion_stream_text_chunk("Hey 2!"); + fake_model + .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)); + fake_model.end_last_completion_stream(); + + let events_1 = events_1.collect::>().await; + assert_eq!(stop_events(events_1), vec![acp::StopReason::Canceled]); + let events_2 = events_2.collect::>().await; + assert_eq!(stop_events(events_2), vec![acp::StopReason::EndTurn]); +} + +#[gpui::test] +async fn test_subsequent_successful_sends_dont_cancel(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let events_1 = thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello 1"], cx) + }); + cx.run_until_parked(); + fake_model.send_last_completion_stream_text_chunk("Hey 1!"); + fake_model + .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)); + fake_model.end_last_completion_stream(); + let events_1 = events_1.collect::>().await; + + let events_2 = thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello 2"], cx) + }); + cx.run_until_parked(); + fake_model.send_last_completion_stream_text_chunk("Hey 2!"); + fake_model + .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)); + fake_model.end_last_completion_stream(); + let events_2 = events_2.collect::>().await; + + assert_eq!(stop_events(events_1), vec![acp::StopReason::EndTurn]); + assert_eq!(stop_events(events_2), vec![acp::StopReason::EndTurn]); +} + #[gpui::test] async fn test_refusal(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 0741bb9e08..d8b6286f60 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -461,7 +461,7 @@ pub struct Thread { /// Holds the task that handles agent interaction until the end of the turn. /// Survives across multiple requests as the model performs tool calls and /// we run tools, report their results. - running_turn: Option>, + running_turn: Option, pending_message: Option, tools: BTreeMap>, tool_use_limit_reached: bool, @@ -554,8 +554,9 @@ impl Thread { } pub fn cancel(&mut self) { - // TODO: do we need to emit a stop::cancel for ACP? - self.running_turn.take(); + if let Some(running_turn) = self.running_turn.take() { + running_turn.cancel(); + } self.flush_pending_message(); } @@ -616,108 +617,118 @@ impl Thread { &mut self, cx: &mut Context, ) -> mpsc::UnboundedReceiver> { + self.cancel(); + let model = self.model.clone(); let (events_tx, events_rx) = mpsc::unbounded::>(); let event_stream = AgentResponseEventStream(events_tx); let message_ix = self.messages.len().saturating_sub(1); self.tool_use_limit_reached = false; - self.running_turn = Some(cx.spawn(async move |this, cx| { - log::info!("Starting agent turn execution"); - let turn_result: Result<()> = async { - let mut completion_intent = CompletionIntent::UserPrompt; - loop { - log::debug!( - "Building completion request with intent: {:?}", - completion_intent - ); - let request = this.update(cx, |this, cx| { - this.build_completion_request(completion_intent, cx) - })?; + self.running_turn = Some(RunningTurn { + event_stream: event_stream.clone(), + _task: cx.spawn(async move |this, cx| { + log::info!("Starting agent turn execution"); + let turn_result: Result<()> = async { + let mut completion_intent = CompletionIntent::UserPrompt; + loop { + log::debug!( + "Building completion request with intent: {:?}", + completion_intent + ); + let request = this.update(cx, |this, cx| { + this.build_completion_request(completion_intent, cx) + })?; - log::info!("Calling model.stream_completion"); - let mut events = model.stream_completion(request, cx).await?; - log::debug!("Stream completion started successfully"); + log::info!("Calling model.stream_completion"); + let mut events = model.stream_completion(request, cx).await?; + log::debug!("Stream completion started successfully"); - let mut tool_use_limit_reached = false; - let mut tool_uses = FuturesUnordered::new(); - while let Some(event) = events.next().await { - match event? { - LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::ToolUseLimitReached, - ) => { - tool_use_limit_reached = true; - } - LanguageModelCompletionEvent::Stop(reason) => { - event_stream.send_stop(reason); - if reason == StopReason::Refusal { - this.update(cx, |this, _cx| { - this.flush_pending_message(); - this.messages.truncate(message_ix); - })?; - return Ok(()); + let mut tool_use_limit_reached = false; + let mut tool_uses = FuturesUnordered::new(); + while let Some(event) = events.next().await { + match event? { + LanguageModelCompletionEvent::StatusUpdate( + CompletionRequestStatus::ToolUseLimitReached, + ) => { + tool_use_limit_reached = true; + } + LanguageModelCompletionEvent::Stop(reason) => { + event_stream.send_stop(reason); + if reason == StopReason::Refusal { + this.update(cx, |this, _cx| { + this.flush_pending_message(); + this.messages.truncate(message_ix); + })?; + return Ok(()); + } + } + event => { + log::trace!("Received completion event: {:?}", event); + this.update(cx, |this, cx| { + tool_uses.extend(this.handle_streamed_completion_event( + event, + &event_stream, + cx, + )); + }) + .ok(); } } - event => { - log::trace!("Received completion event: {:?}", event); - this.update(cx, |this, cx| { - tool_uses.extend(this.handle_streamed_completion_event( - event, - &event_stream, - cx, - )); - }) - .ok(); - } + } + + let used_tools = tool_uses.is_empty(); + while let Some(tool_result) = tool_uses.next().await { + log::info!("Tool finished {:?}", tool_result); + + event_stream.update_tool_call_fields( + &tool_result.tool_use_id, + acp::ToolCallUpdateFields { + status: Some(if tool_result.is_error { + acp::ToolCallStatus::Failed + } else { + acp::ToolCallStatus::Completed + }), + raw_output: tool_result.output.clone(), + ..Default::default() + }, + ); + this.update(cx, |this, _cx| { + this.pending_message() + .tool_results + .insert(tool_result.tool_use_id.clone(), tool_result); + }) + .ok(); + } + + if tool_use_limit_reached { + log::info!("Tool use limit reached, completing turn"); + this.update(cx, |this, _cx| this.tool_use_limit_reached = true)?; + return Err(language_model::ToolUseLimitReachedError.into()); + } else if used_tools { + log::info!("No tool uses found, completing turn"); + return Ok(()); + } else { + this.update(cx, |this, _| this.flush_pending_message())?; + completion_intent = CompletionIntent::ToolResults; } } - - let used_tools = tool_uses.is_empty(); - while let Some(tool_result) = tool_uses.next().await { - log::info!("Tool finished {:?}", tool_result); - - event_stream.update_tool_call_fields( - &tool_result.tool_use_id, - acp::ToolCallUpdateFields { - status: Some(if tool_result.is_error { - acp::ToolCallStatus::Failed - } else { - acp::ToolCallStatus::Completed - }), - raw_output: tool_result.output.clone(), - ..Default::default() - }, - ); - this.update(cx, |this, _cx| { - this.pending_message() - .tool_results - .insert(tool_result.tool_use_id.clone(), tool_result); - }) - .ok(); - } - - if tool_use_limit_reached { - log::info!("Tool use limit reached, completing turn"); - this.update(cx, |this, _cx| this.tool_use_limit_reached = true)?; - return Err(language_model::ToolUseLimitReachedError.into()); - } else if used_tools { - log::info!("No tool uses found, completing turn"); - return Ok(()); - } else { - this.update(cx, |this, _| this.flush_pending_message())?; - completion_intent = CompletionIntent::ToolResults; - } } - } - .await; + .await; - this.update(cx, |this, _| this.flush_pending_message()).ok(); - if let Err(error) = turn_result { - log::error!("Turn execution failed: {:?}", error); - event_stream.send_error(error); - } else { - log::info!("Turn execution completed successfully"); - } - })); + if let Err(error) = turn_result { + log::error!("Turn execution failed: {:?}", error); + event_stream.send_error(error); + } else { + log::info!("Turn execution completed successfully"); + } + + this.update(cx, |this, _| { + this.flush_pending_message(); + this.running_turn.take(); + }) + .ok(); + }), + }); events_rx } @@ -1125,6 +1136,23 @@ impl Thread { } } +struct RunningTurn { + /// Holds the task that handles agent interaction until the end of the turn. + /// Survives across multiple requests as the model performs tool calls and + /// we run tools, report their results. + _task: Task<()>, + /// The current event stream for the running turn. Used to report a final + /// cancellation event if we cancel the turn. + event_stream: AgentResponseEventStream, +} + +impl RunningTurn { + fn cancel(self) { + log::debug!("Cancelling in progress turn"); + self.event_stream.send_canceled(); + } +} + pub trait AgentTool where Self: 'static + Sized, @@ -1336,6 +1364,12 @@ impl AgentResponseEventStream { } } + fn send_canceled(&self) { + self.0 + .unbounded_send(Ok(AgentResponseEvent::Stop(acp::StopReason::Canceled))) + .ok(); + } + fn send_error(&self, error: impl Into) { self.0.unbounded_send(Err(error.into())).ok(); } From 61ce07a91b06f3edf58d5fb7f76cdc1e79b6ae76 Mon Sep 17 00:00:00 2001 From: Cale Sennett Date: Mon, 18 Aug 2025 03:36:52 -0500 Subject: [PATCH 421/693] Add capabilities to OpenAI-compatible model settings (#36370) ### TL;DR * Adds `capabilities` configuration for OpenAI-compatible models * Relates to https://github.com/zed-industries/zed/issues/36215#issuecomment-3193920491 ### Summary This PR introduces support for configuring model capabilities for OpenAI-compatible language models. The implementation addresses the issue that not all OpenAI-compatible APIs support the same features - for example, Cerebras' API explicitly does not support `parallel_tool_calls` as documented in their [OpenAI compatibility guide](https://inference-docs.cerebras.ai/resources/openai#currently-unsupported-openai-features). ### Changes 1. **Model Capabilities Structure**: - Added `ModelCapabilityToggles` struct for UI representation with boolean toggle states - Implemented proper parsing of capability toggles into `ModelCapabilities` 2. **UI Updates**: - Modified the "Add LLM Provider" modal to include checkboxes for each capability - Each OpenAI-compatible model can now be configured with its specific capabilities through the UI 3. **Configuration File Structure**: - Updated the settings schema to support a `capabilities` object for each `openai_compatible` model - Each capability (`tools`, `images`, `parallel_tool_calls`, `prompt_cache_key`) can be individually specified per model ### Example Configuration ```json { "openai_compatible": { "Cerebras": { "api_url": "https://api.cerebras.ai/v1", "available_models": [ { "name": "gpt-oss-120b", "max_tokens": 131000, "capabilities": { "tools": true, "images": false, "parallel_tool_calls": false, "prompt_cache_key": false } } ] } } } ``` ### Tests Added - Added tests to verify default capability values are correctly applied - Added tests to verify that deselected toggles are properly parsed as `false` - Added tests to verify that mixed capability selections work correctly Thanks to @osyvokon for the desired `capabilities` configuration structure! Release Notes: - OpenAI-compatible models now have configurable capabilities (#36370; thanks @calesennett) --------- Co-authored-by: Oleksiy Syvokon --- .../add_llm_provider_modal.rs | 168 +++++++++++++++++- .../src/provider/open_ai_compatible.rs | 35 +++- docs/src/ai/llm-providers.md | 17 +- 3 files changed, 208 insertions(+), 12 deletions(-) diff --git a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs index 401a633488..c68c9c2730 100644 --- a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs +++ b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs @@ -7,10 +7,12 @@ use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, T use language_model::LanguageModelRegistry; use language_models::{ AllLanguageModelSettings, OpenAiCompatibleSettingsContent, - provider::open_ai_compatible::AvailableModel, + provider::open_ai_compatible::{AvailableModel, ModelCapabilities}, }; use settings::update_settings_file; -use ui::{Banner, KeyBinding, Modal, ModalFooter, ModalHeader, Section, prelude::*}; +use ui::{ + Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, prelude::*, +}; use ui_input::SingleLineInput; use workspace::{ModalView, Workspace}; @@ -69,11 +71,19 @@ impl AddLlmProviderInput { } } +struct ModelCapabilityToggles { + pub supports_tools: ToggleState, + pub supports_images: ToggleState, + pub supports_parallel_tool_calls: ToggleState, + pub supports_prompt_cache_key: ToggleState, +} + struct ModelInput { name: Entity, max_completion_tokens: Entity, max_output_tokens: Entity, max_tokens: Entity, + capabilities: ModelCapabilityToggles, } impl ModelInput { @@ -100,11 +110,23 @@ impl ModelInput { cx, ); let max_tokens = single_line_input("Max Tokens", "Max Tokens", Some("200000"), window, cx); + let ModelCapabilities { + tools, + images, + parallel_tool_calls, + prompt_cache_key, + } = ModelCapabilities::default(); Self { name: model_name, max_completion_tokens, max_output_tokens, max_tokens, + capabilities: ModelCapabilityToggles { + supports_tools: tools.into(), + supports_images: images.into(), + supports_parallel_tool_calls: parallel_tool_calls.into(), + supports_prompt_cache_key: prompt_cache_key.into(), + }, } } @@ -136,6 +158,12 @@ impl ModelInput { .text(cx) .parse::() .map_err(|_| SharedString::from("Max Tokens must be a number"))?, + capabilities: ModelCapabilities { + tools: self.capabilities.supports_tools.selected(), + images: self.capabilities.supports_images.selected(), + parallel_tool_calls: self.capabilities.supports_parallel_tool_calls.selected(), + prompt_cache_key: self.capabilities.supports_prompt_cache_key.selected(), + }, }) } } @@ -322,6 +350,55 @@ impl AddLlmProviderModal { .child(model.max_output_tokens.clone()), ) .child(model.max_tokens.clone()) + .child( + v_flex() + .gap_1() + .child( + Checkbox::new(("supports-tools", ix), model.capabilities.supports_tools) + .label("Supports tools") + .on_click(cx.listener(move |this, checked, _window, cx| { + this.input.models[ix].capabilities.supports_tools = *checked; + cx.notify(); + })), + ) + .child( + Checkbox::new(("supports-images", ix), model.capabilities.supports_images) + .label("Supports images") + .on_click(cx.listener(move |this, checked, _window, cx| { + this.input.models[ix].capabilities.supports_images = *checked; + cx.notify(); + })), + ) + .child( + Checkbox::new( + ("supports-parallel-tool-calls", ix), + model.capabilities.supports_parallel_tool_calls, + ) + .label("Supports parallel_tool_calls") + .on_click(cx.listener( + move |this, checked, _window, cx| { + this.input.models[ix] + .capabilities + .supports_parallel_tool_calls = *checked; + cx.notify(); + }, + )), + ) + .child( + Checkbox::new( + ("supports-prompt-cache-key", ix), + model.capabilities.supports_prompt_cache_key, + ) + .label("Supports prompt_cache_key") + .on_click(cx.listener( + move |this, checked, _window, cx| { + this.input.models[ix].capabilities.supports_prompt_cache_key = + *checked; + cx.notify(); + }, + )), + ), + ) .when(has_more_than_one_model, |this| { this.child( Button::new(("remove-model", ix), "Remove Model") @@ -562,6 +639,93 @@ mod tests { ); } + #[gpui::test] + async fn test_model_input_default_capabilities(cx: &mut TestAppContext) { + let cx = setup_test(cx).await; + + cx.update(|window, cx| { + let model_input = ModelInput::new(window, cx); + model_input.name.update(cx, |input, cx| { + input.editor().update(cx, |editor, cx| { + editor.set_text("somemodel", window, cx); + }); + }); + assert_eq!( + model_input.capabilities.supports_tools, + ToggleState::Selected + ); + assert_eq!( + model_input.capabilities.supports_images, + ToggleState::Unselected + ); + assert_eq!( + model_input.capabilities.supports_parallel_tool_calls, + ToggleState::Unselected + ); + assert_eq!( + model_input.capabilities.supports_prompt_cache_key, + ToggleState::Unselected + ); + + let parsed_model = model_input.parse(cx).unwrap(); + assert_eq!(parsed_model.capabilities.tools, true); + assert_eq!(parsed_model.capabilities.images, false); + assert_eq!(parsed_model.capabilities.parallel_tool_calls, false); + assert_eq!(parsed_model.capabilities.prompt_cache_key, false); + }); + } + + #[gpui::test] + async fn test_model_input_deselected_capabilities(cx: &mut TestAppContext) { + let cx = setup_test(cx).await; + + cx.update(|window, cx| { + let mut model_input = ModelInput::new(window, cx); + model_input.name.update(cx, |input, cx| { + input.editor().update(cx, |editor, cx| { + editor.set_text("somemodel", window, cx); + }); + }); + + model_input.capabilities.supports_tools = ToggleState::Unselected; + model_input.capabilities.supports_images = ToggleState::Unselected; + model_input.capabilities.supports_parallel_tool_calls = ToggleState::Unselected; + model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected; + + let parsed_model = model_input.parse(cx).unwrap(); + assert_eq!(parsed_model.capabilities.tools, false); + assert_eq!(parsed_model.capabilities.images, false); + assert_eq!(parsed_model.capabilities.parallel_tool_calls, false); + assert_eq!(parsed_model.capabilities.prompt_cache_key, false); + }); + } + + #[gpui::test] + async fn test_model_input_with_name_and_capabilities(cx: &mut TestAppContext) { + let cx = setup_test(cx).await; + + cx.update(|window, cx| { + let mut model_input = ModelInput::new(window, cx); + model_input.name.update(cx, |input, cx| { + input.editor().update(cx, |editor, cx| { + editor.set_text("somemodel", window, cx); + }); + }); + + model_input.capabilities.supports_tools = ToggleState::Selected; + model_input.capabilities.supports_images = ToggleState::Unselected; + model_input.capabilities.supports_parallel_tool_calls = ToggleState::Selected; + model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected; + + let parsed_model = model_input.parse(cx).unwrap(); + assert_eq!(parsed_model.name, "somemodel"); + assert_eq!(parsed_model.capabilities.tools, true); + assert_eq!(parsed_model.capabilities.images, false); + assert_eq!(parsed_model.capabilities.parallel_tool_calls, true); + assert_eq!(parsed_model.capabilities.prompt_cache_key, false); + }); + } + async fn setup_test(cx: &mut TestAppContext) -> &mut VisualTestContext { cx.update(|cx| { let store = SettingsStore::test(cx); diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs index 5f546f5219..e2d3adb198 100644 --- a/crates/language_models/src/provider/open_ai_compatible.rs +++ b/crates/language_models/src/provider/open_ai_compatible.rs @@ -38,6 +38,27 @@ pub struct AvailableModel { pub max_tokens: u64, pub max_output_tokens: Option, pub max_completion_tokens: Option, + #[serde(default)] + pub capabilities: ModelCapabilities, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct ModelCapabilities { + pub tools: bool, + pub images: bool, + pub parallel_tool_calls: bool, + pub prompt_cache_key: bool, +} + +impl Default for ModelCapabilities { + fn default() -> Self { + Self { + tools: true, + images: false, + parallel_tool_calls: false, + prompt_cache_key: false, + } + } } pub struct OpenAiCompatibleLanguageModelProvider { @@ -293,17 +314,17 @@ impl LanguageModel for OpenAiCompatibleLanguageModel { } fn supports_tools(&self) -> bool { - true + self.model.capabilities.tools } fn supports_images(&self) -> bool { - false + self.model.capabilities.images } fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { match choice { - LanguageModelToolChoice::Auto => true, - LanguageModelToolChoice::Any => true, + LanguageModelToolChoice::Auto => self.model.capabilities.tools, + LanguageModelToolChoice::Any => self.model.capabilities.tools, LanguageModelToolChoice::None => true, } } @@ -355,13 +376,11 @@ impl LanguageModel for OpenAiCompatibleLanguageModel { LanguageModelCompletionError, >, > { - let supports_parallel_tool_call = true; - let supports_prompt_cache_key = false; let request = into_open_ai( request, &self.model.name, - supports_parallel_tool_call, - supports_prompt_cache_key, + self.model.capabilities.parallel_tool_calls, + self.model.capabilities.prompt_cache_key, self.max_output_tokens(), None, ); diff --git a/docs/src/ai/llm-providers.md b/docs/src/ai/llm-providers.md index 58c9230760..5ef6081421 100644 --- a/docs/src/ai/llm-providers.md +++ b/docs/src/ai/llm-providers.md @@ -427,7 +427,7 @@ Custom models will be listed in the model dropdown in the Agent Panel. Zed supports using [OpenAI compatible APIs](https://platform.openai.com/docs/api-reference/chat) by specifying a custom `api_url` and `available_models` for the OpenAI provider. This is useful for connecting to other hosted services (like Together AI, Anyscale, etc.) or local models. -You can add a custom, OpenAI-compatible model via either via the UI or by editing your `settings.json`. +You can add a custom, OpenAI-compatible model either via the UI or by editing your `settings.json`. To do it via the UI, go to the Agent Panel settings (`agent: open settings`) and look for the "Add Provider" button to the right of the "LLM Providers" section title. Then, fill up the input fields available in the modal. @@ -443,7 +443,13 @@ To do it via your `settings.json`, add the following snippet under `language_mod { "name": "mistralai/Mixtral-8x7B-Instruct-v0.1", "display_name": "Together Mixtral 8x7B", - "max_tokens": 32768 + "max_tokens": 32768, + "capabilities": { + "tools": true, + "images": false, + "parallel_tool_calls": false, + "prompt_cache_key": false + } } ] } @@ -451,6 +457,13 @@ To do it via your `settings.json`, add the following snippet under `language_mod } ``` +By default, OpenAI-compatible models inherit the following capabilities: + +- `tools`: true (supports tool/function calling) +- `images`: false (does not support image inputs) +- `parallel_tool_calls`: false (does not support `parallel_tool_calls` parameter) +- `prompt_cache_key`: false (does not support `prompt_cache_key` parameter) + Note that LLM API keys aren't stored in your settings file. So, ensure you have it set in your environment variables (`OPENAI_API_KEY=`) so your settings can pick it up. From 42ffa8900afaa6ec6bd954bdde08f1686d729019 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Mon, 18 Aug 2025 11:54:31 +0300 Subject: [PATCH 422/693] open_ai: Fix error response parsing (#36390) Closes #35925 Release Notes: - Fixed OpenAI error response parsing in some cases --- crates/open_ai/src/open_ai.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 604e8fe622..1fb9a1342c 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -432,11 +432,16 @@ pub struct ChoiceDelta { pub finish_reason: Option, } +#[derive(Serialize, Deserialize, Debug)] +pub struct OpenAiError { + message: String, +} + #[derive(Serialize, Deserialize, Debug)] #[serde(untagged)] pub enum ResponseStreamResult { Ok(ResponseStreamEvent), - Err { error: String }, + Err { error: OpenAiError }, } #[derive(Serialize, Deserialize, Debug)] @@ -475,7 +480,7 @@ pub async fn stream_completion( match serde_json::from_str(line) { Ok(ResponseStreamResult::Ok(response)) => Some(Ok(response)), Ok(ResponseStreamResult::Err { error }) => { - Some(Err(anyhow!(error))) + Some(Err(anyhow!(error.message))) } Err(error) => { log::error!( @@ -502,11 +507,6 @@ pub async fn stream_completion( error: OpenAiError, } - #[derive(Deserialize)] - struct OpenAiError { - message: String, - } - match serde_json::from_str::(&body) { Ok(response) if !response.error.message.is_empty() => Err(anyhow!( "API request to {} failed: {}", From b8a106632fca78d6f07f88b003464e6573f90702 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 18 Aug 2025 11:43:52 +0200 Subject: [PATCH 423/693] lsp: Identify language servers by their configuration (#35270) - **WIP: reorganize dispositions** - **Introduce a LocalToolchainStore trait and use it for LspAdapter methods** Closes #35782 Closes #27331 Release Notes: - Python: Improved propagation of a selected virtual environment into the LSP configuration. This should the make all language-related features such as Go to definition or Find all references more reliable. --------- Co-authored-by: Cole Miller Co-authored-by: Lukas Wirth --- crates/editor/src/editor.rs | 46 +- crates/extension_host/src/extension_host.rs | 1 + crates/extension_host/src/headless_host.rs | 1 + .../src/wasm_host/wit/since_v0_6_0.rs | 2 +- crates/language/src/buffer.rs | 1 + crates/language/src/language.rs | 40 +- crates/language/src/language_registry.rs | 12 +- crates/language/src/manifest.rs | 6 + crates/language/src/toolchain.rs | 33 +- .../src/extension_lsp_adapter.rs | 10 +- .../src/language_extension.rs | 2 +- crates/languages/src/c.rs | 2 +- crates/languages/src/css.rs | 6 +- crates/languages/src/go.rs | 2 +- crates/languages/src/json.rs | 10 +- crates/languages/src/lib.rs | 11 +- crates/languages/src/python.rs | 70 +- crates/languages/src/rust.rs | 6 +- crates/languages/src/tailwind.rs | 6 +- crates/languages/src/typescript.rs | 6 +- crates/languages/src/vtsls.rs | 6 +- crates/languages/src/yaml.rs | 8 +- crates/project/src/lsp_command.rs | 26 +- crates/project/src/lsp_store.rs | 1182 ++++++++--------- crates/project/src/manifest_tree.rs | 111 +- .../src/manifest_tree/manifest_store.rs | 13 +- .../project/src/manifest_tree/server_tree.rs | 386 +++--- crates/project/src/project.rs | 18 +- crates/project/src/project_settings.rs | 8 +- crates/project/src/project_tests.rs | 7 +- crates/project/src/toolchain_store.rs | 78 +- crates/remote_server/src/headless_project.rs | 6 +- 32 files changed, 1037 insertions(+), 1085 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0111e91347..e645bfee67 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -16022,38 +16022,24 @@ impl Editor { cx.spawn_in(window, async move |editor, cx| { let location_task = editor.update(cx, |_, cx| { project.update(cx, |project, cx| { - let language_server_name = project - .language_server_statuses(cx) - .find(|(id, _)| server_id == *id) - .map(|(_, status)| status.name.clone()); - language_server_name.map(|language_server_name| { - project.open_local_buffer_via_lsp( - lsp_location.uri.clone(), - server_id, - language_server_name, - cx, - ) - }) + project.open_local_buffer_via_lsp(lsp_location.uri.clone(), server_id, cx) }) })?; - let location = match location_task { - Some(task) => Some({ - let target_buffer_handle = task.await.context("open local buffer")?; - let range = target_buffer_handle.read_with(cx, |target_buffer, _| { - let target_start = target_buffer - .clip_point_utf16(point_from_lsp(lsp_location.range.start), Bias::Left); - let target_end = target_buffer - .clip_point_utf16(point_from_lsp(lsp_location.range.end), Bias::Left); - target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end) - })?; - Location { - buffer: target_buffer_handle, - range, - } - }), - None => None, - }; + let location = Some({ + let target_buffer_handle = location_task.await.context("open local buffer")?; + let range = target_buffer_handle.read_with(cx, |target_buffer, _| { + let target_start = target_buffer + .clip_point_utf16(point_from_lsp(lsp_location.range.start), Bias::Left); + let target_end = target_buffer + .clip_point_utf16(point_from_lsp(lsp_location.range.end), Bias::Left); + target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end) + })?; + Location { + buffer: target_buffer_handle, + range, + } + }); Ok(location) }) } diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 46deacfe69..e795fa5ac5 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1275,6 +1275,7 @@ impl ExtensionStore { queries, context_provider, toolchain_provider: None, + manifest_name: None, }) }), ); diff --git a/crates/extension_host/src/headless_host.rs b/crates/extension_host/src/headless_host.rs index adc9638c29..8ce3847376 100644 --- a/crates/extension_host/src/headless_host.rs +++ b/crates/extension_host/src/headless_host.rs @@ -163,6 +163,7 @@ impl HeadlessExtensionStore { queries: LanguageQueries::default(), context_provider: None, toolchain_provider: None, + manifest_name: None, }) }), ); diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs index 767b9033ad..84794d5386 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs @@ -938,7 +938,7 @@ impl ExtensionImports for WasmState { binary: settings.binary.map(|binary| settings::CommandSettings { path: binary.path, arguments: binary.arguments, - env: binary.env, + env: binary.env.map(|env| env.into_iter().collect()), }), settings: settings.settings, initialization_options: settings.initialization_options, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 83517accc2..2080513f49 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1571,6 +1571,7 @@ impl Buffer { diagnostics: diagnostics.iter().cloned().collect(), lamport_timestamp, }; + self.apply_diagnostic_update(server_id, diagnostics, lamport_timestamp, cx); self.send_operation(op, true, cx); } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index b9933dfcec..f299dee345 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -44,6 +44,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use serde_json::Value; use settings::WorktreeId; use smol::future::FutureExt as _; +use std::num::NonZeroU32; use std::{ any::Any, ffi::OsStr, @@ -59,7 +60,6 @@ use std::{ atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst}, }, }; -use std::{num::NonZeroU32, sync::OnceLock}; use syntax_map::{QueryCursorHandle, SyntaxSnapshot}; use task::RunnableTag; pub use task_context::{ContextLocation, ContextProvider, RunnableRange}; @@ -67,7 +67,9 @@ pub use text_diff::{ DiffOptions, apply_diff_patch, line_diff, text_diff, text_diff_with_options, unified_diff, }; use theme::SyntaxTheme; -pub use toolchain::{LanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister}; +pub use toolchain::{ + LanguageToolchainStore, LocalLanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister, +}; use tree_sitter::{self, Query, QueryCursor, WasmStore, wasmtime}; use util::serde::default_true; @@ -165,7 +167,6 @@ pub struct CachedLspAdapter { pub adapter: Arc, pub reinstall_attempt_count: AtomicU64, cached_binary: futures::lock::Mutex>, - manifest_name: OnceLock>, } impl Debug for CachedLspAdapter { @@ -201,7 +202,6 @@ impl CachedLspAdapter { adapter, cached_binary: Default::default(), reinstall_attempt_count: AtomicU64::new(0), - manifest_name: Default::default(), }) } @@ -212,7 +212,7 @@ impl CachedLspAdapter { pub async fn get_language_server_command( self: Arc, delegate: Arc, - toolchains: Arc, + toolchains: Option, binary_options: LanguageServerBinaryOptions, cx: &mut AsyncApp, ) -> Result { @@ -281,12 +281,6 @@ impl CachedLspAdapter { .cloned() .unwrap_or_else(|| language_name.lsp_id()) } - - pub fn manifest_name(&self) -> Option { - self.manifest_name - .get_or_init(|| self.adapter.manifest_name()) - .clone() - } } /// Determines what gets sent out as a workspace folders content @@ -327,7 +321,7 @@ pub trait LspAdapter: 'static + Send + Sync { fn get_language_server_command<'a>( self: Arc, delegate: Arc, - toolchains: Arc, + toolchains: Option, binary_options: LanguageServerBinaryOptions, mut cached_binary: futures::lock::MutexGuard<'a, Option>, cx: &'a mut AsyncApp, @@ -402,7 +396,7 @@ pub trait LspAdapter: 'static + Send + Sync { async fn check_if_user_installed( &self, _: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { None @@ -535,7 +529,7 @@ pub trait LspAdapter: 'static + Send + Sync { self: Arc, _: &dyn Fs, _: &Arc, - _: Arc, + _: Option, _cx: &mut AsyncApp, ) -> Result { Ok(serde_json::json!({})) @@ -555,7 +549,6 @@ pub trait LspAdapter: 'static + Send + Sync { _target_language_server_id: LanguageServerName, _: &dyn Fs, _: &Arc, - _: Arc, _cx: &mut AsyncApp, ) -> Result> { Ok(None) @@ -594,10 +587,6 @@ pub trait LspAdapter: 'static + Send + Sync { WorkspaceFoldersContent::SubprojectRoots } - fn manifest_name(&self) -> Option { - None - } - /// Method only implemented by the default JSON language server adapter. /// Used to provide dynamic reloading of the JSON schemas used to /// provide autocompletion and diagnostics in Zed setting and keybind @@ -1108,6 +1097,7 @@ pub struct Language { pub(crate) grammar: Option>, pub(crate) context_provider: Option>, pub(crate) toolchain: Option>, + pub(crate) manifest_name: Option, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] @@ -1318,6 +1308,7 @@ impl Language { }), context_provider: None, toolchain: None, + manifest_name: None, } } @@ -1331,6 +1322,10 @@ impl Language { self } + pub fn with_manifest(mut self, name: Option) -> Self { + self.manifest_name = name; + self + } pub fn with_queries(mut self, queries: LanguageQueries) -> Result { if let Some(query) = queries.highlights { self = self @@ -1764,6 +1759,9 @@ impl Language { pub fn name(&self) -> LanguageName { self.config.name.clone() } + pub fn manifest(&self) -> Option<&ManifestName> { + self.manifest_name.as_ref() + } pub fn code_fence_block_name(&self) -> Arc { self.config @@ -2209,7 +2207,7 @@ impl LspAdapter for FakeLspAdapter { async fn check_if_user_installed( &self, _: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { Some(self.language_server_binary.clone()) @@ -2218,7 +2216,7 @@ impl LspAdapter for FakeLspAdapter { fn get_language_server_command<'a>( self: Arc, _: Arc, - _: Arc, + _: Option, _: LanguageServerBinaryOptions, _: futures::lock::MutexGuard<'a, Option>, _: &'a mut AsyncApp, diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index ea988e8098..6a89b90462 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -1,6 +1,6 @@ use crate::{ CachedLspAdapter, File, Language, LanguageConfig, LanguageId, LanguageMatcher, - LanguageServerName, LspAdapter, PLAIN_TEXT, ToolchainLister, + LanguageServerName, LspAdapter, ManifestName, PLAIN_TEXT, ToolchainLister, language_settings::{ AllLanguageSettingsContent, LanguageSettingsContent, all_language_settings, }, @@ -172,6 +172,7 @@ pub struct AvailableLanguage { hidden: bool, load: Arc Result + 'static + Send + Sync>, loaded: bool, + manifest_name: Option, } impl AvailableLanguage { @@ -259,6 +260,7 @@ pub struct LoadedLanguage { pub queries: LanguageQueries, pub context_provider: Option>, pub toolchain_provider: Option>, + pub manifest_name: Option, } impl LanguageRegistry { @@ -349,12 +351,14 @@ impl LanguageRegistry { config.grammar.clone(), config.matcher.clone(), config.hidden, + None, Arc::new(move || { Ok(LoadedLanguage { config: config.clone(), queries: Default::default(), toolchain_provider: None, context_provider: None, + manifest_name: None, }) }), ) @@ -487,6 +491,7 @@ impl LanguageRegistry { grammar_name: Option>, matcher: LanguageMatcher, hidden: bool, + manifest_name: Option, load: Arc Result + 'static + Send + Sync>, ) { let state = &mut *self.state.write(); @@ -496,6 +501,7 @@ impl LanguageRegistry { existing_language.grammar = grammar_name; existing_language.matcher = matcher; existing_language.load = load; + existing_language.manifest_name = manifest_name; return; } } @@ -508,6 +514,7 @@ impl LanguageRegistry { load, hidden, loaded: false, + manifest_name, }); state.version += 1; state.reload_count += 1; @@ -575,6 +582,7 @@ impl LanguageRegistry { grammar: language.config.grammar.clone(), matcher: language.config.matcher.clone(), hidden: language.config.hidden, + manifest_name: None, load: Arc::new(|| Err(anyhow!("already loaded"))), loaded: true, }); @@ -914,10 +922,12 @@ impl LanguageRegistry { Language::new_with_id(id, loaded_language.config, grammar) .with_context_provider(loaded_language.context_provider) .with_toolchain_lister(loaded_language.toolchain_provider) + .with_manifest(loaded_language.manifest_name) .with_queries(loaded_language.queries) } else { Ok(Language::new_with_id(id, loaded_language.config, None) .with_context_provider(loaded_language.context_provider) + .with_manifest(loaded_language.manifest_name) .with_toolchain_lister(loaded_language.toolchain_provider)) } } diff --git a/crates/language/src/manifest.rs b/crates/language/src/manifest.rs index 37505fec3b..3ca0ddf71d 100644 --- a/crates/language/src/manifest.rs +++ b/crates/language/src/manifest.rs @@ -12,6 +12,12 @@ impl Borrow for ManifestName { } } +impl Borrow for ManifestName { + fn borrow(&self) -> &str { + &self.0 + } +} + impl From for ManifestName { fn from(value: SharedString) -> Self { Self(value) diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index 1f4b038f68..979513bc96 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -17,7 +17,7 @@ use settings::WorktreeId; use crate::{LanguageName, ManifestName}; /// Represents a single toolchain. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq)] pub struct Toolchain { /// User-facing label pub name: SharedString, @@ -27,6 +27,14 @@ pub struct Toolchain { pub as_json: serde_json::Value, } +impl std::hash::Hash for Toolchain { + fn hash(&self, state: &mut H) { + self.name.hash(state); + self.path.hash(state); + self.language_name.hash(state); + } +} + impl PartialEq for Toolchain { fn eq(&self, other: &Self) -> bool { // Do not use as_json for comparisons; it shouldn't impact equality, as it's not user-surfaced. @@ -64,6 +72,29 @@ pub trait LanguageToolchainStore: Send + Sync + 'static { ) -> Option; } +pub trait LocalLanguageToolchainStore: Send + Sync + 'static { + fn active_toolchain( + self: Arc, + worktree_id: WorktreeId, + relative_path: &Arc, + language_name: LanguageName, + cx: &mut AsyncApp, + ) -> Option; +} + +#[async_trait(?Send )] +impl LanguageToolchainStore for T { + async fn active_toolchain( + self: Arc, + worktree_id: WorktreeId, + relative_path: Arc, + language_name: LanguageName, + cx: &mut AsyncApp, + ) -> Option { + self.active_toolchain(worktree_id, &relative_path, language_name, cx) + } +} + type DefaultIndex = usize; #[derive(Default, Clone)] pub struct ToolchainList { diff --git a/crates/language_extension/src/extension_lsp_adapter.rs b/crates/language_extension/src/extension_lsp_adapter.rs index 98b6fd4b5a..e465a8dd0a 100644 --- a/crates/language_extension/src/extension_lsp_adapter.rs +++ b/crates/language_extension/src/extension_lsp_adapter.rs @@ -12,8 +12,8 @@ use fs::Fs; use futures::{Future, FutureExt, future::join_all}; use gpui::{App, AppContext, AsyncApp, Task}; use language::{ - BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LanguageToolchainStore, - LspAdapter, LspAdapterDelegate, + BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LspAdapter, LspAdapterDelegate, + Toolchain, }; use lsp::{ CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName, @@ -159,7 +159,7 @@ impl LspAdapter for ExtensionLspAdapter { fn get_language_server_command<'a>( self: Arc, delegate: Arc, - _: Arc, + _: Option, _: LanguageServerBinaryOptions, _: futures::lock::MutexGuard<'a, Option>, _: &'a mut AsyncApp, @@ -288,7 +288,7 @@ impl LspAdapter for ExtensionLspAdapter { self: Arc, _: &dyn Fs, delegate: &Arc, - _: Arc, + _: Option, _cx: &mut AsyncApp, ) -> Result { let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _; @@ -336,7 +336,7 @@ impl LspAdapter for ExtensionLspAdapter { target_language_server_id: LanguageServerName, _: &dyn Fs, delegate: &Arc, - _: Arc, + _cx: &mut AsyncApp, ) -> Result> { let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _; diff --git a/crates/language_extension/src/language_extension.rs b/crates/language_extension/src/language_extension.rs index 1915eae2d1..7bca0eb485 100644 --- a/crates/language_extension/src/language_extension.rs +++ b/crates/language_extension/src/language_extension.rs @@ -52,7 +52,7 @@ impl ExtensionLanguageProxy for LanguageServerRegistryProxy { load: Arc Result + Send + Sync + 'static>, ) { self.language_registry - .register_language(language, grammar, matcher, hidden, load); + .register_language(language, grammar, matcher, hidden, None, load); } fn remove_languages( diff --git a/crates/languages/src/c.rs b/crates/languages/src/c.rs index aee1abee95..999d4a74c3 100644 --- a/crates/languages/src/c.rs +++ b/crates/languages/src/c.rs @@ -28,7 +28,7 @@ impl super::LspAdapter for CLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; diff --git a/crates/languages/src/css.rs b/crates/languages/src/css.rs index ffd9006c76..a1a5418220 100644 --- a/crates/languages/src/css.rs +++ b/crates/languages/src/css.rs @@ -2,7 +2,7 @@ use anyhow::{Context as _, Result}; use async_trait::async_trait; use futures::StreamExt; use gpui::AsyncApp; -use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; +use language::{LspAdapter, LspAdapterDelegate, Toolchain}; use lsp::{LanguageServerBinary, LanguageServerName}; use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; @@ -43,7 +43,7 @@ impl LspAdapter for CssLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { let path = delegate @@ -144,7 +144,7 @@ impl LspAdapter for CssLspAdapter { self: Arc, _: &dyn Fs, delegate: &Arc, - _: Arc, + _: Option, cx: &mut AsyncApp, ) -> Result { let mut default_config = json!({ diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 14f646133b..f739c5c4c6 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -75,7 +75,7 @@ impl super::LspAdapter for GoLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 484631d01f..4db48c67f0 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -8,8 +8,8 @@ use futures::StreamExt; use gpui::{App, AsyncApp, Task}; use http_client::github::{GitHubLspBinaryVersion, latest_github_release}; use language::{ - ContextProvider, LanguageName, LanguageRegistry, LanguageToolchainStore, LocalFile as _, - LspAdapter, LspAdapterDelegate, + ContextProvider, LanguageName, LanguageRegistry, LocalFile as _, LspAdapter, + LspAdapterDelegate, Toolchain, }; use lsp::{LanguageServerBinary, LanguageServerName}; use node_runtime::{NodeRuntime, VersionStrategy}; @@ -303,7 +303,7 @@ impl LspAdapter for JsonLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { let path = delegate @@ -404,7 +404,7 @@ impl LspAdapter for JsonLspAdapter { self: Arc, _: &dyn Fs, delegate: &Arc, - _: Arc, + _: Option, cx: &mut AsyncApp, ) -> Result { let mut config = self.get_or_init_workspace_config(cx).await?; @@ -529,7 +529,7 @@ impl LspAdapter for NodeVersionAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 195ba79e1d..e446f22713 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -1,6 +1,6 @@ use anyhow::Context as _; use feature_flags::{FeatureFlag, FeatureFlagAppExt as _}; -use gpui::{App, UpdateGlobal}; +use gpui::{App, SharedString, UpdateGlobal}; use node_runtime::NodeRuntime; use python::PyprojectTomlManifestProvider; use rust::CargoManifestProvider; @@ -177,11 +177,13 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { adapters: vec![python_lsp_adapter.clone(), py_lsp_adapter.clone()], context: Some(python_context_provider), toolchain: Some(python_toolchain_provider), + manifest_name: Some(SharedString::new_static("pyproject.toml").into()), }, LanguageInfo { name: "rust", adapters: vec![rust_lsp_adapter], context: Some(rust_context_provider), + manifest_name: Some(SharedString::new_static("Cargo.toml").into()), ..Default::default() }, LanguageInfo { @@ -234,6 +236,7 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { registration.adapters, registration.context, registration.toolchain, + registration.manifest_name, ); } @@ -340,7 +343,7 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { Arc::from(PyprojectTomlManifestProvider), ]; for provider in manifest_providers { - project::ManifestProviders::global(cx).register(provider); + project::ManifestProvidersStore::global(cx).register(provider); } } @@ -350,6 +353,7 @@ struct LanguageInfo { adapters: Vec>, context: Option>, toolchain: Option>, + manifest_name: Option, } fn register_language( @@ -358,6 +362,7 @@ fn register_language( adapters: Vec>, context: Option>, toolchain: Option>, + manifest_name: Option, ) { let config = load_config(name); for adapter in adapters { @@ -368,12 +373,14 @@ fn register_language( config.grammar.clone(), config.matcher.clone(), config.hidden, + manifest_name.clone(), Arc::new(move || { Ok(LoadedLanguage { config: config.clone(), queries: load_queries(name), context_provider: context.clone(), toolchain_provider: toolchain.clone(), + manifest_name: manifest_name.clone(), }) }), ); diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 40131089d1..b61ad2d36c 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -127,7 +127,7 @@ impl LspAdapter for PythonLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { if let Some(pyright_bin) = delegate.which("pyright-langserver".as_ref()).await { @@ -319,17 +319,9 @@ impl LspAdapter for PythonLspAdapter { self: Arc, _: &dyn Fs, adapter: &Arc, - toolchains: Arc, + toolchain: Option, cx: &mut AsyncApp, ) -> Result { - let toolchain = toolchains - .active_toolchain( - adapter.worktree_id(), - Arc::from("".as_ref()), - LanguageName::new("Python"), - cx, - ) - .await; cx.update(move |cx| { let mut user_settings = language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx) @@ -397,9 +389,7 @@ impl LspAdapter for PythonLspAdapter { user_settings }) } - fn manifest_name(&self) -> Option { - Some(SharedString::new_static("pyproject.toml").into()) - } + fn workspace_folders_content(&self) -> WorkspaceFoldersContent { WorkspaceFoldersContent::WorktreeRoot } @@ -1046,8 +1036,8 @@ impl LspAdapter for PyLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - toolchains: Arc, - cx: &AsyncApp, + toolchain: Option, + _: &AsyncApp, ) -> Option { if let Some(pylsp_bin) = delegate.which(Self::SERVER_NAME.as_ref()).await { let env = delegate.shell_env().await; @@ -1057,14 +1047,7 @@ impl LspAdapter for PyLspAdapter { arguments: vec![], }) } else { - let venv = toolchains - .active_toolchain( - delegate.worktree_id(), - Arc::from("".as_ref()), - LanguageName::new("Python"), - &mut cx.clone(), - ) - .await?; + let venv = toolchain?; let pylsp_path = Path::new(venv.path.as_ref()).parent()?.join("pylsp"); pylsp_path.exists().then(|| LanguageServerBinary { path: venv.path.to_string().into(), @@ -1211,17 +1194,9 @@ impl LspAdapter for PyLspAdapter { self: Arc, _: &dyn Fs, adapter: &Arc, - toolchains: Arc, + toolchain: Option, cx: &mut AsyncApp, ) -> Result { - let toolchain = toolchains - .active_toolchain( - adapter.worktree_id(), - Arc::from("".as_ref()), - LanguageName::new("Python"), - cx, - ) - .await; cx.update(move |cx| { let mut user_settings = language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx) @@ -1282,9 +1257,6 @@ impl LspAdapter for PyLspAdapter { user_settings }) } - fn manifest_name(&self) -> Option { - Some(SharedString::new_static("pyproject.toml").into()) - } fn workspace_folders_content(&self) -> WorkspaceFoldersContent { WorkspaceFoldersContent::WorktreeRoot } @@ -1377,8 +1349,8 @@ impl LspAdapter for BasedPyrightLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - toolchains: Arc, - cx: &AsyncApp, + toolchain: Option, + _: &AsyncApp, ) -> Option { if let Some(bin) = delegate.which(Self::BINARY_NAME.as_ref()).await { let env = delegate.shell_env().await; @@ -1388,15 +1360,7 @@ impl LspAdapter for BasedPyrightLspAdapter { arguments: vec!["--stdio".into()], }) } else { - let venv = toolchains - .active_toolchain( - delegate.worktree_id(), - Arc::from("".as_ref()), - LanguageName::new("Python"), - &mut cx.clone(), - ) - .await?; - let path = Path::new(venv.path.as_ref()) + let path = Path::new(toolchain?.path.as_ref()) .parent()? .join(Self::BINARY_NAME); path.exists().then(|| LanguageServerBinary { @@ -1543,17 +1507,9 @@ impl LspAdapter for BasedPyrightLspAdapter { self: Arc, _: &dyn Fs, adapter: &Arc, - toolchains: Arc, + toolchain: Option, cx: &mut AsyncApp, ) -> Result { - let toolchain = toolchains - .active_toolchain( - adapter.worktree_id(), - Arc::from("".as_ref()), - LanguageName::new("Python"), - cx, - ) - .await; cx.update(move |cx| { let mut user_settings = language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx) @@ -1622,10 +1578,6 @@ impl LspAdapter for BasedPyrightLspAdapter { }) } - fn manifest_name(&self) -> Option { - Some(SharedString::new_static("pyproject.toml").into()) - } - fn workspace_folders_content(&self) -> WorkspaceFoldersContent { WorkspaceFoldersContent::WorktreeRoot } diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 3baaec1842..3ef7c1ba34 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -109,14 +109,10 @@ impl LspAdapter for RustLspAdapter { SERVER_NAME.clone() } - fn manifest_name(&self) -> Option { - Some(SharedString::new_static("Cargo.toml").into()) - } - async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { let path = delegate.which("rust-analyzer".as_ref()).await?; diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index 0d647f07cf..27939c645c 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -3,7 +3,7 @@ use async_trait::async_trait; use collections::HashMap; use futures::StreamExt; use gpui::AsyncApp; -use language::{LanguageName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; +use language::{LanguageName, LspAdapter, LspAdapterDelegate, Toolchain}; use lsp::{LanguageServerBinary, LanguageServerName}; use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; @@ -50,7 +50,7 @@ impl LspAdapter for TailwindLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; @@ -155,7 +155,7 @@ impl LspAdapter for TailwindLspAdapter { self: Arc, _: &dyn Fs, delegate: &Arc, - _: Arc, + _: Option, cx: &mut AsyncApp, ) -> Result { let mut tailwind_user_settings = cx.update(|cx| { diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index 1877c86dc5..dec7df4060 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -7,7 +7,7 @@ use gpui::{App, AppContext, AsyncApp, Task}; use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url}; use language::{ ContextLocation, ContextProvider, File, LanguageName, LanguageToolchainStore, LspAdapter, - LspAdapterDelegate, + LspAdapterDelegate, Toolchain, }; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName}; use node_runtime::{NodeRuntime, VersionStrategy}; @@ -722,7 +722,7 @@ impl LspAdapter for TypeScriptLspAdapter { self: Arc, _: &dyn Fs, delegate: &Arc, - _: Arc, + _: Option, cx: &mut AsyncApp, ) -> Result { let override_options = cx.update(|cx| { @@ -822,7 +822,7 @@ impl LspAdapter for EsLintLspAdapter { self: Arc, _: &dyn Fs, delegate: &Arc, - _: Arc, + _: Option, cx: &mut AsyncApp, ) -> Result { let workspace_root = delegate.worktree_root_path(); diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index 90faf883ba..fd227e267d 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -2,7 +2,7 @@ use anyhow::Result; use async_trait::async_trait; use collections::HashMap; use gpui::AsyncApp; -use language::{LanguageName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; +use language::{LanguageName, LspAdapter, LspAdapterDelegate, Toolchain}; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName}; use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; @@ -86,7 +86,7 @@ impl LspAdapter for VtslsLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { let env = delegate.shell_env().await; @@ -211,7 +211,7 @@ impl LspAdapter for VtslsLspAdapter { self: Arc, fs: &dyn Fs, delegate: &Arc, - _: Arc, + _: Option, cx: &mut AsyncApp, ) -> Result { let tsdk_path = Self::tsdk_path(fs, delegate).await; diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 15a4d590bc..137a9c2282 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -2,9 +2,7 @@ use anyhow::{Context as _, Result}; use async_trait::async_trait; use futures::StreamExt; use gpui::AsyncApp; -use language::{ - LanguageToolchainStore, LspAdapter, LspAdapterDelegate, language_settings::AllLanguageSettings, -}; +use language::{LspAdapter, LspAdapterDelegate, Toolchain, language_settings::AllLanguageSettings}; use lsp::{LanguageServerBinary, LanguageServerName}; use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; @@ -57,7 +55,7 @@ impl LspAdapter for YamlLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; @@ -135,7 +133,7 @@ impl LspAdapter for YamlLspAdapter { self: Arc, _: &dyn Fs, delegate: &Arc, - _: Arc, + _: Option, cx: &mut AsyncApp, ) -> Result { let location = SettingsLocation { diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index c458b6b300..fcfeb9c660 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -500,13 +500,12 @@ impl LspCommand for PerformRename { mut cx: AsyncApp, ) -> Result { if let Some(edit) = message { - let (lsp_adapter, lsp_server) = + let (_, lsp_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; LocalLspStore::deserialize_workspace_edit( lsp_store, edit, self.push_to_history, - lsp_adapter, lsp_server, &mut cx, ) @@ -1116,18 +1115,12 @@ pub async fn location_links_from_lsp( } } - let (lsp_adapter, language_server) = - language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; + let (_, language_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; let mut definitions = Vec::new(); for (origin_range, target_uri, target_range) in unresolved_links { let target_buffer_handle = lsp_store .update(&mut cx, |this, cx| { - this.open_local_buffer_via_lsp( - target_uri, - language_server.server_id(), - lsp_adapter.name.clone(), - cx, - ) + this.open_local_buffer_via_lsp(target_uri, language_server.server_id(), cx) })? .await?; @@ -1172,8 +1165,7 @@ pub async fn location_link_from_lsp( server_id: LanguageServerId, cx: &mut AsyncApp, ) -> Result { - let (lsp_adapter, language_server) = - language_server_for_buffer(&lsp_store, &buffer, server_id, cx)?; + let (_, language_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, cx)?; let (origin_range, target_uri, target_range) = ( link.origin_selection_range, @@ -1183,12 +1175,7 @@ pub async fn location_link_from_lsp( let target_buffer_handle = lsp_store .update(cx, |lsp_store, cx| { - lsp_store.open_local_buffer_via_lsp( - target_uri, - language_server.server_id(), - lsp_adapter.name.clone(), - cx, - ) + lsp_store.open_local_buffer_via_lsp(target_uri, language_server.server_id(), cx) })? .await?; @@ -1326,7 +1313,7 @@ impl LspCommand for GetReferences { mut cx: AsyncApp, ) -> Result> { let mut references = Vec::new(); - let (lsp_adapter, language_server) = + let (_, language_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; if let Some(locations) = locations { @@ -1336,7 +1323,6 @@ impl LspCommand for GetReferences { lsp_store.open_local_buffer_via_lsp( lsp_location.uri, language_server.server_id(), - lsp_adapter.name.clone(), cx, ) })? diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 196f55171a..8ea41a100b 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -1,3 +1,14 @@ +//! LSP store provides unified access to the language server protocol. +//! The consumers of LSP store can interact with language servers without knowing exactly which language server they're interacting with. +//! +//! # Local/Remote LSP Stores +//! This module is split up into three distinct parts: +//! - [`LocalLspStore`], which is ran on the host machine (either project host or SSH host), that manages the lifecycle of language servers. +//! - [`RemoteLspStore`], which is ran on the remote machine (project guests) which is mostly about passing through the requests via RPC. +//! The remote stores don't really care about which language server they're running against - they don't usually get to decide which language server is going to responsible for handling their request. +//! - [`LspStore`], which unifies the two under one consistent interface for interacting with language servers. +//! +//! Most of the interesting work happens at the local layer, as bulk of the complexity is with managing the lifecycle of language servers. The actual implementation of the LSP protocol is handled by [`lsp`] crate. pub mod clangd_ext; pub mod json_language_server_ext; pub mod lsp_ext_command; @@ -6,20 +17,20 @@ pub mod rust_analyzer_ext; use crate::{ CodeAction, ColorPresentation, Completion, CompletionResponse, CompletionSource, CoreCompletion, DocumentColor, Hover, InlayHint, LocationLink, LspAction, LspPullDiagnostics, - ProjectItem, ProjectPath, ProjectTransaction, PulledDiagnostics, ResolveState, Symbol, - ToolchainStore, + ManifestProvidersStore, ProjectItem, ProjectPath, ProjectTransaction, PulledDiagnostics, + ResolveState, Symbol, buffer_store::{BufferStore, BufferStoreEvent}, environment::ProjectEnvironment, lsp_command::{self, *}, lsp_store, manifest_tree::{ - AdapterQuery, LanguageServerTree, LanguageServerTreeNode, LaunchDisposition, - ManifestQueryDelegate, ManifestTree, + LanguageServerTree, LanguageServerTreeNode, LaunchDisposition, ManifestQueryDelegate, + ManifestTree, }, prettier_store::{self, PrettierStore, PrettierStoreEvent}, project_settings::{LspSettings, ProjectSettings}, relativize_path, resolve_path, - toolchain_store::{EmptyToolchainStore, ToolchainStoreEvent}, + toolchain_store::{LocalToolchainStore, ToolchainStoreEvent}, worktree_store::{WorktreeStore, WorktreeStoreEvent}, yarn::YarnPathStore, }; @@ -44,9 +55,9 @@ use itertools::Itertools as _; use language::{ Bias, BinaryStatus, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, Diff, File as _, Language, LanguageName, - LanguageRegistry, LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch, - PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, - WorkspaceFoldersContent, + LanguageRegistry, LocalFile, LspAdapter, LspAdapterDelegate, ManifestDelegate, ManifestName, + Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Toolchain, Transaction, + Unclipped, WorkspaceFoldersContent, language_settings::{ FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings, }, @@ -140,6 +151,20 @@ impl FormatTrigger { } } +#[derive(Clone)] +struct UnifiedLanguageServer { + id: LanguageServerId, + project_roots: HashSet>, +} + +#[derive(Clone, Hash, PartialEq, Eq)] +struct LanguageServerSeed { + worktree_id: WorktreeId, + name: LanguageServerName, + toolchain: Option, + settings: Arc, +} + #[derive(Debug)] pub struct DocumentDiagnosticsUpdate<'a, D> { pub diagnostics: D, @@ -157,17 +182,18 @@ pub struct DocumentDiagnostics { pub struct LocalLspStore { weak: WeakEntity, worktree_store: Entity, - toolchain_store: Entity, + toolchain_store: Entity, http_client: Arc, environment: Entity, fs: Arc, languages: Arc, - language_server_ids: HashMap<(WorktreeId, LanguageServerName), BTreeSet>, + language_server_ids: HashMap, yarn: Entity, pub language_servers: HashMap, buffers_being_formatted: HashSet, last_workspace_edits_by_language_server: HashMap, language_server_watched_paths: HashMap, + watched_manifest_filenames: HashSet, language_server_paths_watched_for_rename: HashMap, language_server_watcher_registrations: @@ -188,7 +214,7 @@ pub struct LocalLspStore { >, buffer_snapshots: HashMap>>, // buffer_id -> server_id -> vec of snapshots _subscription: gpui::Subscription, - lsp_tree: Entity, + lsp_tree: LanguageServerTree, registered_buffers: HashMap, buffers_opened_in_servers: HashMap>, buffer_pull_diagnostics_result_ids: HashMap>>, @@ -208,19 +234,63 @@ impl LocalLspStore { } } + fn get_or_insert_language_server( + &mut self, + worktree_handle: &Entity, + delegate: Arc, + disposition: &Arc, + language_name: &LanguageName, + cx: &mut App, + ) -> LanguageServerId { + let key = LanguageServerSeed { + worktree_id: worktree_handle.read(cx).id(), + name: disposition.server_name.clone(), + settings: disposition.settings.clone(), + toolchain: disposition.toolchain.clone(), + }; + if let Some(state) = self.language_server_ids.get_mut(&key) { + state.project_roots.insert(disposition.path.path.clone()); + state.id + } else { + let adapter = self + .languages + .lsp_adapters(language_name) + .into_iter() + .find(|adapter| adapter.name() == disposition.server_name) + .expect("To find LSP adapter"); + let new_language_server_id = self.start_language_server( + worktree_handle, + delegate, + adapter, + disposition.settings.clone(), + key.clone(), + cx, + ); + if let Some(state) = self.language_server_ids.get_mut(&key) { + state.project_roots.insert(disposition.path.path.clone()); + } else { + debug_assert!( + false, + "Expected `start_language_server` to ensure that `key` exists in a map" + ); + } + new_language_server_id + } + } + fn start_language_server( &mut self, worktree_handle: &Entity, delegate: Arc, adapter: Arc, settings: Arc, + key: LanguageServerSeed, cx: &mut App, ) -> LanguageServerId { let worktree = worktree_handle.read(cx); - let worktree_id = worktree.id(); - let root_path = worktree.abs_path(); - let key = (worktree_id, adapter.name.clone()); + let root_path = worktree.abs_path(); + let toolchain = key.toolchain.clone(); let override_options = settings.initialization_options.clone(); let stderr_capture = Arc::new(Mutex::new(Some(String::new()))); @@ -231,7 +301,14 @@ impl LocalLspStore { adapter.name.0 ); - let binary = self.get_language_server_binary(adapter.clone(), delegate.clone(), true, cx); + let binary = self.get_language_server_binary( + adapter.clone(), + settings, + toolchain.clone(), + delegate.clone(), + true, + cx, + ); let pending_workspace_folders: Arc>> = Default::default(); let pending_server = cx.spawn({ @@ -290,15 +367,13 @@ impl LocalLspStore { .enabled; cx.spawn(async move |cx| { let result = async { - let toolchains = - lsp_store.update(cx, |lsp_store, cx| lsp_store.toolchain_store(cx))?; let language_server = pending_server.await?; let workspace_config = Self::workspace_configuration_for_adapter( adapter.adapter.clone(), fs.as_ref(), &delegate, - toolchains.clone(), + toolchain, cx, ) .await?; @@ -417,31 +492,26 @@ impl LocalLspStore { self.language_servers.insert(server_id, state); self.language_server_ids .entry(key) - .or_default() - .insert(server_id); + .or_insert(UnifiedLanguageServer { + id: server_id, + project_roots: Default::default(), + }); server_id } fn get_language_server_binary( &self, adapter: Arc, + settings: Arc, + toolchain: Option, delegate: Arc, allow_binary_download: bool, cx: &mut App, ) -> Task> { - let settings = ProjectSettings::get( - Some(SettingsLocation { - worktree_id: delegate.worktree_id(), - path: Path::new(""), - }), - cx, - ) - .lsp - .get(&adapter.name) - .and_then(|s| s.binary.clone()); - - if settings.as_ref().is_some_and(|b| b.path.is_some()) { - let settings = settings.unwrap(); + if let Some(settings) = settings.binary.as_ref() + && settings.path.is_some() + { + let settings = settings.clone(); return cx.background_spawn(async move { let mut env = delegate.shell_env().await; @@ -461,16 +531,17 @@ impl LocalLspStore { } let lsp_binary_options = LanguageServerBinaryOptions { allow_path_lookup: !settings + .binary .as_ref() .and_then(|b| b.ignore_system_version) .unwrap_or_default(), allow_binary_download, }; - let toolchains = self.toolchain_store.read(cx).as_language_toolchain_store(); + cx.spawn(async move |cx| { let binary_result = adapter .clone() - .get_language_server_command(delegate.clone(), toolchains, lsp_binary_options, cx) + .get_language_server_command(delegate.clone(), toolchain, lsp_binary_options, cx) .await; delegate.update_status(adapter.name.clone(), BinaryStatus::None); @@ -480,12 +551,12 @@ impl LocalLspStore { shell_env.extend(binary.env.unwrap_or_default()); - if let Some(settings) = settings { - if let Some(arguments) = settings.arguments { + if let Some(settings) = settings.binary.as_ref() { + if let Some(arguments) = &settings.arguments { binary.arguments = arguments.into_iter().map(Into::into).collect(); } - if let Some(env) = settings.env { - shell_env.extend(env); + if let Some(env) = &settings.env { + shell_env.extend(env.iter().map(|(k, v)| (k.clone(), v.clone()))); } } @@ -559,14 +630,20 @@ impl LocalLspStore { let fs = fs.clone(); let mut cx = cx.clone(); async move { - let toolchains = - this.update(&mut cx, |this, cx| this.toolchain_store(cx))?; - + let toolchain_for_id = this + .update(&mut cx, |this, _| { + this.as_local()?.language_server_ids.iter().find_map( + |(seed, value)| { + (value.id == server_id).then(|| seed.toolchain.clone()) + }, + ) + })? + .context("Expected the LSP store to be in a local mode")?; let workspace_config = Self::workspace_configuration_for_adapter( adapter.clone(), fs.as_ref(), &delegate, - toolchains.clone(), + toolchain_for_id, &mut cx, ) .await?; @@ -700,18 +777,15 @@ impl LocalLspStore { language_server .on_request::({ - let adapter = adapter.clone(); let this = this.clone(); move |params, cx| { let mut cx = cx.clone(); let this = this.clone(); - let adapter = adapter.clone(); async move { LocalLspStore::on_lsp_workspace_edit( this.clone(), params, server_id, - adapter.clone(), &mut cx, ) .await @@ -960,19 +1034,18 @@ impl LocalLspStore { ) -> impl Iterator> { self.language_server_ids .iter() - .flat_map(move |((language_server_path, _), ids)| { - ids.iter().filter_map(move |id| { - if *language_server_path != worktree_id { - return None; - } - if let Some(LanguageServerState::Running { server, .. }) = - self.language_servers.get(id) - { - return Some(server); - } else { - None - } - }) + .filter_map(move |(seed, state)| { + if seed.worktree_id != worktree_id { + return None; + } + + if let Some(LanguageServerState::Running { server, .. }) = + self.language_servers.get(&state.id) + { + return Some(server); + } else { + None + } }) } @@ -989,17 +1062,18 @@ impl LocalLspStore { else { return Vec::new(); }; - let delegate = Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); - let root = self.lsp_tree.update(cx, |this, cx| { - this.get( + let delegate: Arc = + Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); + let root = self + .lsp_tree + .get( project_path, - AdapterQuery::Language(&language.name()), - delegate, + language.name(), + language.manifest(), + &delegate, cx, ) - .filter_map(|node| node.server_id()) - .collect::>() - }); + .collect::>(); root } @@ -1083,7 +1157,7 @@ impl LocalLspStore { .collect::>() }) })?; - for (lsp_adapter, language_server) in adapters_and_servers.iter() { + for (_, language_server) in adapters_and_servers.iter() { let actions = Self::get_server_code_actions_from_action_kinds( &lsp_store, language_server.server_id(), @@ -1095,7 +1169,6 @@ impl LocalLspStore { Self::execute_code_actions_on_server( &lsp_store, language_server, - lsp_adapter, actions, push_to_history, &mut project_transaction, @@ -2038,13 +2111,14 @@ impl LocalLspStore { let buffer = buffer_handle.read(cx); let file = buffer.file().cloned(); + let Some(file) = File::from_dyn(file.as_ref()) else { return; }; if !file.is_local() { return; } - + let path = ProjectPath::from_file(file, cx); let worktree_id = file.worktree_id(cx); let language = buffer.language().cloned(); @@ -2067,46 +2141,52 @@ impl LocalLspStore { let Some(language) = language else { return; }; - for adapter in self.languages.lsp_adapters(&language.name()) { - let servers = self - .language_server_ids - .get(&(worktree_id, adapter.name.clone())); - if let Some(server_ids) = servers { - for server_id in server_ids { - let server = self - .language_servers - .get(server_id) - .and_then(|server_state| { - if let LanguageServerState::Running { server, .. } = server_state { - Some(server.clone()) - } else { - None - } - }); - let server = match server { - Some(server) => server, - None => continue, - }; + let Some(snapshot) = self + .worktree_store + .read(cx) + .worktree_for_id(worktree_id, cx) + .map(|worktree| worktree.read(cx).snapshot()) + else { + return; + }; + let delegate: Arc = Arc::new(ManifestQueryDelegate::new(snapshot)); - buffer_handle.update(cx, |buffer, cx| { - buffer.set_completion_triggers( - server.server_id(), - server - .capabilities() - .completion_provider + for server_id in + self.lsp_tree + .get(path, language.name(), language.manifest(), &delegate, cx) + { + let server = self + .language_servers + .get(&server_id) + .and_then(|server_state| { + if let LanguageServerState::Running { server, .. } = server_state { + Some(server.clone()) + } else { + None + } + }); + let server = match server { + Some(server) => server, + None => continue, + }; + + buffer_handle.update(cx, |buffer, cx| { + buffer.set_completion_triggers( + server.server_id(), + server + .capabilities() + .completion_provider + .as_ref() + .and_then(|provider| { + provider + .trigger_characters .as_ref() - .and_then(|provider| { - provider - .trigger_characters - .as_ref() - .map(|characters| characters.iter().cloned().collect()) - }) - .unwrap_or_default(), - cx, - ); - }); - } - } + .map(|characters| characters.iter().cloned().collect()) + }) + .unwrap_or_default(), + cx, + ); + }); } } @@ -2216,6 +2296,31 @@ impl LocalLspStore { Ok(()) } + fn register_language_server_for_invisible_worktree( + &mut self, + worktree: &Entity, + language_server_id: LanguageServerId, + cx: &mut App, + ) { + let worktree = worktree.read(cx); + let worktree_id = worktree.id(); + debug_assert!(!worktree.is_visible()); + let Some(mut origin_seed) = self + .language_server_ids + .iter() + .find_map(|(seed, state)| (state.id == language_server_id).then(|| seed.clone())) + else { + return; + }; + origin_seed.worktree_id = worktree_id; + self.language_server_ids + .entry(origin_seed) + .or_insert_with(|| UnifiedLanguageServer { + id: language_server_id, + project_roots: Default::default(), + }); + } + fn register_buffer_with_language_servers( &mut self, buffer_handle: &Entity, @@ -2256,27 +2361,23 @@ impl LocalLspStore { }; let language_name = language.name(); let (reused, delegate, servers) = self - .lsp_tree - .update(cx, |lsp_tree, cx| { - self.reuse_existing_language_server(lsp_tree, &worktree, &language_name, cx) - }) - .map(|(delegate, servers)| (true, delegate, servers)) + .reuse_existing_language_server(&self.lsp_tree, &worktree, &language_name, cx) + .map(|(delegate, apply)| (true, delegate, apply(&mut self.lsp_tree))) .unwrap_or_else(|| { let lsp_delegate = LocalLspAdapterDelegate::from_local_lsp(self, &worktree, cx); - let delegate = Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); + let delegate: Arc = + Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); + let servers = self .lsp_tree - .clone() - .update(cx, |language_server_tree, cx| { - language_server_tree - .get( - ProjectPath { worktree_id, path }, - AdapterQuery::Language(&language.name()), - delegate.clone(), - cx, - ) - .collect::>() - }); + .walk( + ProjectPath { worktree_id, path }, + language.name(), + language.manifest(), + &delegate, + cx, + ) + .collect::>(); (false, lsp_delegate, servers) }); let servers_and_adapters = servers @@ -2298,55 +2399,35 @@ impl LocalLspStore { } } - let server_id = server_node.server_id_or_init( - |LaunchDisposition { - server_name, - path, - settings, - }| { - let server_id = - { - let uri = Url::from_file_path( - worktree.read(cx).abs_path().join(&path.path), - ); - let key = (worktree_id, server_name.clone()); - if !self.language_server_ids.contains_key(&key) { - let language_name = language.name(); - let adapter = self.languages - .lsp_adapters(&language_name) - .into_iter() - .find(|adapter| &adapter.name() == server_name) - .expect("To find LSP adapter"); - self.start_language_server( - &worktree, - delegate.clone(), - adapter, - settings, - cx, - ); - } - if let Some(server_ids) = self - .language_server_ids - .get(&key) - { - debug_assert_eq!(server_ids.len(), 1); - let server_id = server_ids.iter().cloned().next().unwrap(); - if let Some(state) = self.language_servers.get(&server_id) { - if let Ok(uri) = uri { - state.add_workspace_folder(uri); - }; - } - server_id - } else { - unreachable!("Language server ID should be available, as it's registered on demand") - } + let server_id = server_node.server_id_or_init(|disposition| { + let path = &disposition.path; + let server_id = { + let uri = + Url::from_file_path(worktree.read(cx).abs_path().join(&path.path)); - }; + let server_id = self.get_or_insert_language_server( + &worktree, + delegate.clone(), + disposition, + &language_name, + cx, + ); + + if let Some(state) = self.language_servers.get(&server_id) { + if let Ok(uri) = uri { + state.add_workspace_folder(uri); + }; + } server_id - }, - )?; + }; + + server_id + })?; let server_state = self.language_servers.get(&server_id)?; - if let LanguageServerState::Running { server, adapter, .. } = server_state { + if let LanguageServerState::Running { + server, adapter, .. + } = server_state + { Some((server.clone(), adapter.clone())) } else { None @@ -2413,13 +2494,16 @@ impl LocalLspStore { } } - fn reuse_existing_language_server( + fn reuse_existing_language_server<'lang_name>( &self, - server_tree: &mut LanguageServerTree, + server_tree: &LanguageServerTree, worktree: &Entity, - language_name: &LanguageName, + language_name: &'lang_name LanguageName, cx: &mut App, - ) -> Option<(Arc, Vec)> { + ) -> Option<( + Arc, + impl FnOnce(&mut LanguageServerTree) -> Vec + use<'lang_name>, + )> { if worktree.read(cx).is_visible() { return None; } @@ -2458,16 +2542,16 @@ impl LocalLspStore { .into_values() .max_by_key(|servers| servers.len())?; - for server_node in &servers { - server_tree.register_reused( - worktree.read(cx).id(), - language_name.clone(), - server_node.clone(), - ); - } + let worktree_id = worktree.read(cx).id(); + let apply = move |tree: &mut LanguageServerTree| { + for server_node in &servers { + tree.register_reused(worktree_id, language_name.clone(), server_node.clone()); + } + servers + }; let delegate = LocalLspAdapterDelegate::from_local_lsp(self, worktree, cx); - Some((delegate, servers)) + Some((delegate, apply)) } pub(crate) fn unregister_old_buffer_from_language_servers( @@ -2568,7 +2652,7 @@ impl LocalLspStore { pub async fn execute_code_actions_on_server( lsp_store: &WeakEntity, language_server: &Arc, - lsp_adapter: &Arc, + actions: Vec, push_to_history: bool, project_transaction: &mut ProjectTransaction, @@ -2588,7 +2672,6 @@ impl LocalLspStore { lsp_store.upgrade().context("project dropped")?, edit.clone(), push_to_history, - lsp_adapter.clone(), language_server.clone(), cx, ) @@ -2769,7 +2852,6 @@ impl LocalLspStore { this: Entity, edit: lsp::WorkspaceEdit, push_to_history: bool, - lsp_adapter: Arc, language_server: Arc, cx: &mut AsyncApp, ) -> Result { @@ -2870,7 +2952,6 @@ impl LocalLspStore { this.open_local_buffer_via_lsp( op.text_document.uri.clone(), language_server.server_id(), - lsp_adapter.name.clone(), cx, ) })? @@ -2995,7 +3076,6 @@ impl LocalLspStore { this: WeakEntity, params: lsp::ApplyWorkspaceEditParams, server_id: LanguageServerId, - adapter: Arc, cx: &mut AsyncApp, ) -> Result { let this = this.upgrade().context("project project closed")?; @@ -3006,7 +3086,6 @@ impl LocalLspStore { this.clone(), params.edit, true, - adapter.clone(), language_server.clone(), cx, ) @@ -3037,23 +3116,19 @@ impl LocalLspStore { prettier_store.remove_worktree(id_to_remove, cx); }); - let mut servers_to_remove = BTreeMap::default(); + let mut servers_to_remove = BTreeSet::default(); let mut servers_to_preserve = HashSet::default(); - for ((path, server_name), ref server_ids) in &self.language_server_ids { - if *path == id_to_remove { - servers_to_remove.extend(server_ids.iter().map(|id| (*id, server_name.clone()))); + for (seed, ref state) in &self.language_server_ids { + if seed.worktree_id == id_to_remove { + servers_to_remove.insert(state.id); } else { - servers_to_preserve.extend(server_ids.iter().cloned()); + servers_to_preserve.insert(state.id); } } - servers_to_remove.retain(|server_id, _| !servers_to_preserve.contains(server_id)); - - for (server_id_to_remove, _) in &servers_to_remove { - self.language_server_ids - .values_mut() - .for_each(|server_ids| { - server_ids.remove(server_id_to_remove); - }); + servers_to_remove.retain(|server_id| !servers_to_preserve.contains(server_id)); + self.language_server_ids + .retain(|_, state| !servers_to_remove.contains(&state.id)); + for server_id_to_remove in &servers_to_remove { self.language_server_watched_paths .remove(server_id_to_remove); self.language_server_paths_watched_for_rename @@ -3068,7 +3143,7 @@ impl LocalLspStore { } cx.emit(LspStoreEvent::LanguageServerRemoved(*server_id_to_remove)); } - servers_to_remove.into_keys().collect() + servers_to_remove.into_iter().collect() } fn rebuild_watched_paths_inner<'a>( @@ -3326,16 +3401,20 @@ impl LocalLspStore { Ok(Some(initialization_config)) } + fn toolchain_store(&self) -> &Entity { + &self.toolchain_store + } + async fn workspace_configuration_for_adapter( adapter: Arc, fs: &dyn Fs, delegate: &Arc, - toolchains: Arc, + toolchain: Option, cx: &mut AsyncApp, ) -> Result { let mut workspace_config = adapter .clone() - .workspace_configuration(fs, delegate, toolchains.clone(), cx) + .workspace_configuration(fs, delegate, toolchain, cx) .await?; for other_adapter in delegate.registered_lsp_adapters() { @@ -3344,13 +3423,7 @@ impl LocalLspStore { } if let Ok(Some(target_config)) = other_adapter .clone() - .additional_workspace_configuration( - adapter.name(), - fs, - delegate, - toolchains.clone(), - cx, - ) + .additional_workspace_configuration(adapter.name(), fs, delegate, cx) .await { merge_json_value_into(target_config.clone(), &mut workspace_config); @@ -3416,7 +3489,6 @@ pub struct LspStore { nonce: u128, buffer_store: Entity, worktree_store: Entity, - toolchain_store: Option>, pub languages: Arc, language_server_statuses: BTreeMap, active_entry: Option, @@ -3607,7 +3679,7 @@ impl LspStore { buffer_store: Entity, worktree_store: Entity, prettier_store: Entity, - toolchain_store: Entity, + toolchain_store: Entity, environment: Entity, manifest_tree: Entity, languages: Arc, @@ -3649,7 +3721,7 @@ impl LspStore { mode: LspStoreMode::Local(LocalLspStore { weak: cx.weak_entity(), worktree_store: worktree_store.clone(), - toolchain_store: toolchain_store.clone(), + supplementary_language_servers: Default::default(), languages: languages.clone(), language_server_ids: Default::default(), @@ -3672,16 +3744,22 @@ impl LspStore { .unwrap() .shutdown_language_servers_on_quit(cx) }), - lsp_tree: LanguageServerTree::new(manifest_tree, languages.clone(), cx), + lsp_tree: LanguageServerTree::new( + manifest_tree, + languages.clone(), + toolchain_store.clone(), + ), + toolchain_store, registered_buffers: HashMap::default(), buffers_opened_in_servers: HashMap::default(), buffer_pull_diagnostics_result_ids: HashMap::default(), + watched_manifest_filenames: ManifestProvidersStore::global(cx) + .manifest_file_names(), }), last_formatting_failure: None, downstream_client: None, buffer_store, worktree_store, - toolchain_store: Some(toolchain_store), languages: languages.clone(), language_server_statuses: Default::default(), nonce: StdRng::from_entropy().r#gen(), @@ -3719,7 +3797,6 @@ impl LspStore { pub(super) fn new_remote( buffer_store: Entity, worktree_store: Entity, - toolchain_store: Option>, languages: Arc, upstream_client: AnyProtoClient, project_id: u64, @@ -3752,7 +3829,7 @@ impl LspStore { lsp_document_colors: HashMap::default(), lsp_code_lens: HashMap::default(), active_entry: None, - toolchain_store, + _maintain_workspace_config, _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), } @@ -3851,7 +3928,7 @@ impl LspStore { fn on_toolchain_store_event( &mut self, - _: Entity, + _: Entity, event: &ToolchainStoreEvent, _: &mut Context, ) { @@ -3930,9 +4007,9 @@ impl LspStore { let local = this.as_local()?; let mut servers = Vec::new(); - for ((worktree_id, _), server_ids) in &local.language_server_ids { - for server_id in server_ids { - let Some(states) = local.language_servers.get(server_id) else { + for (seed, state) in &local.language_server_ids { + + let Some(states) = local.language_servers.get(&state.id) else { continue; }; let (json_adapter, json_server) = match states { @@ -3947,7 +4024,7 @@ impl LspStore { let Some(worktree) = this .worktree_store .read(cx) - .worktree_for_id(*worktree_id, cx) + .worktree_for_id(seed.worktree_id, cx) else { continue; }; @@ -3963,7 +4040,7 @@ impl LspStore { ); servers.push((json_adapter, json_server, json_delegate)); - } + } return Some(servers); }) @@ -3974,9 +4051,9 @@ impl LspStore { return; }; - let Ok(Some((fs, toolchain_store))) = this.read_with(cx, |this, cx| { + let Ok(Some((fs, _))) = this.read_with(cx, |this, _| { let local = this.as_local()?; - let toolchain_store = this.toolchain_store(cx); + let toolchain_store = local.toolchain_store().clone(); return Some((local.fs.clone(), toolchain_store)); }) else { return; @@ -3988,7 +4065,7 @@ impl LspStore { adapter, fs.as_ref(), &delegate, - toolchain_store.clone(), + None, cx, ) .await @@ -4533,7 +4610,7 @@ impl LspStore { } } - self.refresh_server_tree(cx); + self.request_workspace_config_refresh(); if let Some(prettier_store) = self.as_local().map(|s| s.prettier_store.clone()) { prettier_store.update(cx, |prettier_store, cx| { @@ -4546,158 +4623,150 @@ impl LspStore { fn refresh_server_tree(&mut self, cx: &mut Context) { let buffer_store = self.buffer_store.clone(); - if let Some(local) = self.as_local_mut() { - let mut adapters = BTreeMap::default(); - let get_adapter = { - let languages = local.languages.clone(); - let environment = local.environment.clone(); - let weak = local.weak.clone(); - let worktree_store = local.worktree_store.clone(); - let http_client = local.http_client.clone(); - let fs = local.fs.clone(); - move |worktree_id, cx: &mut App| { - let worktree = worktree_store.read(cx).worktree_for_id(worktree_id, cx)?; - Some(LocalLspAdapterDelegate::new( - languages.clone(), - &environment, - weak.clone(), - &worktree, - http_client.clone(), - fs.clone(), - cx, - )) - } - }; + let Some(local) = self.as_local_mut() else { + return; + }; + let mut adapters = BTreeMap::default(); + let get_adapter = { + let languages = local.languages.clone(); + let environment = local.environment.clone(); + let weak = local.weak.clone(); + let worktree_store = local.worktree_store.clone(); + let http_client = local.http_client.clone(); + let fs = local.fs.clone(); + move |worktree_id, cx: &mut App| { + let worktree = worktree_store.read(cx).worktree_for_id(worktree_id, cx)?; + Some(LocalLspAdapterDelegate::new( + languages.clone(), + &environment, + weak.clone(), + &worktree, + http_client.clone(), + fs.clone(), + cx, + )) + } + }; - let mut messages_to_report = Vec::new(); - let to_stop = local.lsp_tree.clone().update(cx, |lsp_tree, cx| { - let mut rebase = lsp_tree.rebase(); - for buffer_handle in buffer_store.read(cx).buffers().sorted_by_key(|buffer| { - Reverse( - File::from_dyn(buffer.read(cx).file()) - .map(|file| file.worktree.read(cx).is_visible()), - ) - }) { - let buffer = buffer_handle.read(cx); - let buffer_id = buffer.remote_id(); - if !local.registered_buffers.contains_key(&buffer_id) { - continue; - } - if let Some((file, language)) = File::from_dyn(buffer.file()) - .cloned() - .zip(buffer.language().map(|l| l.name())) + let mut messages_to_report = Vec::new(); + let (new_tree, to_stop) = { + let mut rebase = local.lsp_tree.rebase(); + let buffers = buffer_store + .read(cx) + .buffers() + .filter_map(|buffer| { + let raw_buffer = buffer.read(cx); + if !local + .registered_buffers + .contains_key(&raw_buffer.remote_id()) { - let worktree_id = file.worktree_id(cx); - let Some(worktree) = local - .worktree_store - .read(cx) - .worktree_for_id(worktree_id, cx) - else { - continue; - }; + return None; + } + let file = File::from_dyn(raw_buffer.file()).cloned()?; + let language = raw_buffer.language().cloned()?; + Some((file, language, raw_buffer.remote_id())) + }) + .sorted_by_key(|(file, _, _)| Reverse(file.worktree.read(cx).is_visible())); - let Some((reused, delegate, nodes)) = local - .reuse_existing_language_server( - rebase.server_tree(), + for (file, language, buffer_id) in buffers { + let worktree_id = file.worktree_id(cx); + let Some(worktree) = local + .worktree_store + .read(cx) + .worktree_for_id(worktree_id, cx) + else { + continue; + }; + + if let Some((_, apply)) = local.reuse_existing_language_server( + rebase.server_tree(), + &worktree, + &language.name(), + cx, + ) { + (apply)(rebase.server_tree()); + } else if let Some(lsp_delegate) = adapters + .entry(worktree_id) + .or_insert_with(|| get_adapter(worktree_id, cx)) + .clone() + { + let delegate = + Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); + let path = file + .path() + .parent() + .map(Arc::from) + .unwrap_or_else(|| file.path().clone()); + let worktree_path = ProjectPath { worktree_id, path }; + let abs_path = file.abs_path(cx); + let worktree_root = worktree.read(cx).abs_path(); + let nodes = rebase + .walk( + worktree_path, + language.name(), + language.manifest(), + delegate.clone(), + cx, + ) + .collect::>(); + + for node in nodes { + let server_id = node.server_id_or_init(|disposition| { + let path = &disposition.path; + let uri = Url::from_file_path(worktree_root.join(&path.path)); + let key = LanguageServerSeed { + worktree_id, + name: disposition.server_name.clone(), + settings: disposition.settings.clone(), + toolchain: local.toolchain_store.read(cx).active_toolchain( + path.worktree_id, + &path.path, + language.name(), + ), + }; + local.language_server_ids.remove(&key); + + let server_id = local.get_or_insert_language_server( &worktree, - &language, + lsp_delegate.clone(), + disposition, + &language.name(), cx, - ) - .map(|(delegate, servers)| (true, delegate, servers)) - .or_else(|| { - let lsp_delegate = adapters - .entry(worktree_id) - .or_insert_with(|| get_adapter(worktree_id, cx)) - .clone()?; - let delegate = Arc::new(ManifestQueryDelegate::new( - worktree.read(cx).snapshot(), - )); - let path = file - .path() - .parent() - .map(Arc::from) - .unwrap_or_else(|| file.path().clone()); - let worktree_path = ProjectPath { worktree_id, path }; - - let nodes = rebase.get( - worktree_path, - AdapterQuery::Language(&language), - delegate.clone(), - cx, - ); - - Some((false, lsp_delegate, nodes.collect())) - }) - else { - continue; - }; - - let abs_path = file.abs_path(cx); - for node in nodes { - if !reused { - let server_id = node.server_id_or_init( - |LaunchDisposition { - server_name, - - path, - settings, - }| - { - let uri = Url::from_file_path( - worktree.read(cx).abs_path().join(&path.path), - ); - let key = (worktree_id, server_name.clone()); - local.language_server_ids.remove(&key); - - let adapter = local - .languages - .lsp_adapters(&language) - .into_iter() - .find(|adapter| &adapter.name() == server_name) - .expect("To find LSP adapter"); - let server_id = local.start_language_server( - &worktree, - delegate.clone(), - adapter, - settings, - cx, - ); - if let Some(state) = - local.language_servers.get(&server_id) - { - if let Ok(uri) = uri { - state.add_workspace_folder(uri); - }; - } - server_id - } - ); - - if let Some(language_server_id) = server_id { - messages_to_report.push(LspStoreEvent::LanguageServerUpdate { - language_server_id, - name: node.name(), - message: - proto::update_language_server::Variant::RegisteredForBuffer( - proto::RegisteredForBuffer { - buffer_abs_path: abs_path.to_string_lossy().to_string(), - buffer_id: buffer_id.to_proto(), - }, - ), - }); - } + ); + if let Some(state) = local.language_servers.get(&server_id) { + if let Ok(uri) = uri { + state.add_workspace_folder(uri); + }; } + server_id + }); + + if let Some(language_server_id) = server_id { + messages_to_report.push(LspStoreEvent::LanguageServerUpdate { + language_server_id, + name: node.name(), + message: + proto::update_language_server::Variant::RegisteredForBuffer( + proto::RegisteredForBuffer { + buffer_abs_path: abs_path.to_string_lossy().to_string(), + buffer_id: buffer_id.to_proto(), + }, + ), + }); } } + } else { + continue; } - rebase.finish() - }); - for message in messages_to_report { - cx.emit(message); - } - for (id, _) in to_stop { - self.stop_local_language_server(id, cx).detach(); } + rebase.finish() + }; + for message in messages_to_report { + cx.emit(message); + } + local.lsp_tree = new_tree; + for (id, _) in to_stop { + self.stop_local_language_server(id, cx).detach(); } } @@ -4729,7 +4798,7 @@ impl LspStore { .await }) } else if self.mode.is_local() { - let Some((lsp_adapter, lang_server)) = buffer_handle.update(cx, |buffer, cx| { + let Some((_, lang_server)) = buffer_handle.update(cx, |buffer, cx| { self.language_server_for_local_buffer(buffer, action.server_id, cx) .map(|(adapter, server)| (adapter.clone(), server.clone())) }) else { @@ -4745,7 +4814,7 @@ impl LspStore { this.upgrade().context("no app present")?, edit.clone(), push_to_history, - lsp_adapter.clone(), + lang_server.clone(), cx, ) @@ -7073,11 +7142,11 @@ impl LspStore { let mut requests = Vec::new(); let mut requested_servers = BTreeSet::new(); - 'next_server: for ((worktree_id, _), server_ids) in local.language_server_ids.iter() { + for (seed, state) in local.language_server_ids.iter() { let Some(worktree_handle) = self .worktree_store .read(cx) - .worktree_for_id(*worktree_id, cx) + .worktree_for_id(seed.worktree_id, cx) else { continue; }; @@ -7086,31 +7155,30 @@ impl LspStore { continue; } - let mut servers_to_query = server_ids - .difference(&requested_servers) - .cloned() - .collect::>(); - for server_id in &servers_to_query { - let (lsp_adapter, server) = match local.language_servers.get(server_id) { - Some(LanguageServerState::Running { - adapter, server, .. - }) => (adapter.clone(), server), + if !requested_servers.insert(state.id) { + continue; + } - _ => continue 'next_server, + let (lsp_adapter, server) = match local.language_servers.get(&state.id) { + Some(LanguageServerState::Running { + adapter, server, .. + }) => (adapter.clone(), server), + + _ => continue, + }; + let supports_workspace_symbol_request = + match server.capabilities().workspace_symbol_provider { + Some(OneOf::Left(supported)) => supported, + Some(OneOf::Right(_)) => true, + None => false, }; - let supports_workspace_symbol_request = - match server.capabilities().workspace_symbol_provider { - Some(OneOf::Left(supported)) => supported, - Some(OneOf::Right(_)) => true, - None => false, - }; - if !supports_workspace_symbol_request { - continue 'next_server; - } - let worktree_abs_path = worktree.abs_path().clone(); - let worktree_handle = worktree_handle.clone(); - let server_id = server.server_id(); - requests.push( + if !supports_workspace_symbol_request { + continue; + } + let worktree_abs_path = worktree.abs_path().clone(); + let worktree_handle = worktree_handle.clone(); + let server_id = server.server_id(); + requests.push( server .request::( lsp::WorkspaceSymbolParams { @@ -7152,8 +7220,6 @@ impl LspStore { } }), ); - } - requested_servers.append(&mut servers_to_query); } cx.spawn(async move |this, cx| { @@ -7416,7 +7482,7 @@ impl LspStore { None } - pub(crate) async fn refresh_workspace_configurations( + async fn refresh_workspace_configurations( lsp_store: &WeakEntity, fs: Arc, cx: &mut AsyncApp, @@ -7425,71 +7491,70 @@ impl LspStore { let mut refreshed_servers = HashSet::default(); let servers = lsp_store .update(cx, |lsp_store, cx| { - let toolchain_store = lsp_store.toolchain_store(cx); - let Some(local) = lsp_store.as_local() else { - return Vec::default(); - }; - local + let local = lsp_store.as_local()?; + + let servers = local .language_server_ids .iter() - .flat_map(|((worktree_id, _), server_ids)| { + .filter_map(|(seed, state)| { let worktree = lsp_store .worktree_store .read(cx) - .worktree_for_id(*worktree_id, cx); - let delegate = worktree.map(|worktree| { - LocalLspAdapterDelegate::new( - local.languages.clone(), - &local.environment, - cx.weak_entity(), - &worktree, - local.http_client.clone(), - local.fs.clone(), - cx, - ) - }); + .worktree_for_id(seed.worktree_id, cx); + let delegate: Arc = + worktree.map(|worktree| { + LocalLspAdapterDelegate::new( + local.languages.clone(), + &local.environment, + cx.weak_entity(), + &worktree, + local.http_client.clone(), + local.fs.clone(), + cx, + ) + })?; + let server_id = state.id; - let fs = fs.clone(); - let toolchain_store = toolchain_store.clone(); - server_ids.iter().filter_map(|server_id| { - let delegate = delegate.clone()? as Arc; - let states = local.language_servers.get(server_id)?; + let states = local.language_servers.get(&server_id)?; - match states { - LanguageServerState::Starting { .. } => None, - LanguageServerState::Running { - adapter, server, .. - } => { - let fs = fs.clone(); - let toolchain_store = toolchain_store.clone(); - let adapter = adapter.clone(); - let server = server.clone(); - refreshed_servers.insert(server.name()); - Some(cx.spawn(async move |_, cx| { - let settings = - LocalLspStore::workspace_configuration_for_adapter( - adapter.adapter.clone(), - fs.as_ref(), - &delegate, - toolchain_store, - cx, - ) - .await - .ok()?; - server - .notify::( - &lsp::DidChangeConfigurationParams { settings }, - ) - .ok()?; - Some(()) - })) - } + match states { + LanguageServerState::Starting { .. } => None, + LanguageServerState::Running { + adapter, server, .. + } => { + let fs = fs.clone(); + + let adapter = adapter.clone(); + let server = server.clone(); + refreshed_servers.insert(server.name()); + let toolchain = seed.toolchain.clone(); + Some(cx.spawn(async move |_, cx| { + let settings = + LocalLspStore::workspace_configuration_for_adapter( + adapter.adapter.clone(), + fs.as_ref(), + &delegate, + toolchain, + cx, + ) + .await + .ok()?; + server + .notify::( + &lsp::DidChangeConfigurationParams { settings }, + ) + .ok()?; + Some(()) + })) } - }).collect::>() + } }) - .collect::>() + .collect::>(); + + Some(servers) }) - .ok()?; + .ok() + .flatten()?; log::info!("Refreshing workspace configurations for servers {refreshed_servers:?}"); // TODO this asynchronous job runs concurrently with extension (de)registration and may take enough time for a certain extension @@ -7497,18 +7562,12 @@ impl LspStore { // This is racy : an extension might have already removed all `local.language_servers` state, but here we `.clone()` and hold onto it anyway. // This now causes errors in the logs, we should find a way to remove such servers from the processing everywhere. let _: Vec> = join_all(servers).await; + Some(()) }) .await; } - fn toolchain_store(&self, cx: &App) -> Arc { - if let Some(toolchain_store) = self.toolchain_store.as_ref() { - toolchain_store.read(cx).as_language_toolchain_store() - } else { - Arc::new(EmptyToolchainStore) - } - } fn maintain_workspace_config( fs: Arc, external_refresh_requests: watch::Receiver<()>, @@ -7523,8 +7582,19 @@ impl LspStore { let mut joint_future = futures::stream::select(settings_changed_rx, external_refresh_requests); + // Multiple things can happen when a workspace environment (selected toolchain + settings) change: + // - We might shut down a language server if it's no longer enabled for a given language (and there are no buffers using it otherwise). + // - We might also shut it down when the workspace configuration of all of the users of a given language server converges onto that of the other. + // - In the same vein, we might also decide to start a new language server if the workspace configuration *diverges* from the other. + // - In the easiest case (where we're not wrangling the lifetime of a language server anyhow), if none of the roots of a single language server diverge in their configuration, + // but it is still different to what we had before, we're gonna send out a workspace configuration update. cx.spawn(async move |this, cx| { while let Some(()) = joint_future.next().await { + this.update(cx, |this, cx| { + this.refresh_server_tree(cx); + }) + .ok(); + Self::refresh_workspace_configurations(&this, fs.clone(), cx).await; } @@ -7642,47 +7712,6 @@ impl LspStore { .collect(); } - fn register_local_language_server( - &mut self, - worktree: Entity, - language_server_name: LanguageServerName, - language_server_id: LanguageServerId, - cx: &mut App, - ) { - let Some(local) = self.as_local_mut() else { - return; - }; - - let worktree_id = worktree.read(cx).id(); - if worktree.read(cx).is_visible() { - let path = ProjectPath { - worktree_id, - path: Arc::from("".as_ref()), - }; - let delegate = Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); - local.lsp_tree.update(cx, |language_server_tree, cx| { - for node in language_server_tree.get( - path, - AdapterQuery::Adapter(&language_server_name), - delegate, - cx, - ) { - node.server_id_or_init(|disposition| { - assert_eq!(disposition.server_name, &language_server_name); - - language_server_id - }); - } - }); - } - - local - .language_server_ids - .entry((worktree_id, language_server_name)) - .or_default() - .insert(language_server_id); - } - #[cfg(test)] pub fn update_diagnostic_entries( &mut self, @@ -7912,17 +7941,12 @@ impl LspStore { .await }) } else if let Some(local) = self.as_local() { - let Some(language_server_id) = local - .language_server_ids - .get(&( - symbol.source_worktree_id, - symbol.language_server_name.clone(), - )) - .and_then(|ids| { - ids.contains(&symbol.source_language_server_id) - .then_some(symbol.source_language_server_id) - }) - else { + let is_valid = local.language_server_ids.iter().any(|(seed, state)| { + seed.worktree_id == symbol.source_worktree_id + && state.id == symbol.source_language_server_id + && symbol.language_server_name == seed.name + }); + if !is_valid { return Task::ready(Err(anyhow!( "language server for worktree and language not found" ))); @@ -7946,22 +7970,16 @@ impl LspStore { return Task::ready(Err(anyhow!("invalid symbol path"))); }; - self.open_local_buffer_via_lsp( - symbol_uri, - language_server_id, - symbol.language_server_name.clone(), - cx, - ) + self.open_local_buffer_via_lsp(symbol_uri, symbol.source_language_server_id, cx) } else { Task::ready(Err(anyhow!("no upstream client or local store"))) } } - pub fn open_local_buffer_via_lsp( + pub(crate) fn open_local_buffer_via_lsp( &mut self, mut abs_path: lsp::Url, language_server_id: LanguageServerId, - language_server_name: LanguageServerName, cx: &mut Context, ) -> Task>> { cx.spawn(async move |lsp_store, cx| { @@ -8012,12 +8030,13 @@ impl LspStore { if worktree.read_with(cx, |worktree, _| worktree.is_local())? { lsp_store .update(cx, |lsp_store, cx| { - lsp_store.register_local_language_server( - worktree.clone(), - language_server_name, - language_server_id, - cx, - ) + if let Some(local) = lsp_store.as_local_mut() { + local.register_language_server_for_invisible_worktree( + &worktree, + language_server_id, + cx, + ) + } }) .ok(); } @@ -9202,11 +9221,7 @@ impl LspStore { else { continue; }; - let Some(adapter) = - this.language_server_adapter_for_id(language_server.server_id()) - else { - continue; - }; + if filter.should_send_will_rename(&old_uri, is_dir) { let apply_edit = cx.spawn({ let old_uri = old_uri.clone(); @@ -9227,7 +9242,6 @@ impl LspStore { this.upgrade()?, edit, false, - adapter.clone(), language_server.clone(), cx, ) @@ -10290,28 +10304,18 @@ impl LspStore { &mut self, server_id: LanguageServerId, cx: &mut Context, - ) -> Task> { + ) -> Task<()> { let local = match &mut self.mode { LspStoreMode::Local(local) => local, _ => { - return Task::ready(Vec::new()); + return Task::ready(()); } }; - let mut orphaned_worktrees = Vec::new(); // Remove this server ID from all entries in the given worktree. - local.language_server_ids.retain(|(worktree, _), ids| { - if !ids.remove(&server_id) { - return true; - } - - if ids.is_empty() { - orphaned_worktrees.push(*worktree); - false - } else { - true - } - }); + local + .language_server_ids + .retain(|_, state| state.id != server_id); self.buffer_store.update(cx, |buffer_store, cx| { for buffer in buffer_store.buffers() { buffer.update(cx, |buffer, cx| { @@ -10390,14 +10394,13 @@ impl LspStore { cx.notify(); }) .ok(); - orphaned_worktrees }); } if server_state.is_some() { cx.emit(LspStoreEvent::LanguageServerRemoved(server_id)); } - Task::ready(orphaned_worktrees) + Task::ready(()) } pub fn stop_all_language_servers(&mut self, cx: &mut Context) { @@ -10416,12 +10419,9 @@ impl LspStore { let language_servers_to_stop = local .language_server_ids .values() - .flatten() - .copied() + .map(|state| state.id) .collect(); - local.lsp_tree.update(cx, |this, _| { - this.remove_nodes(&language_servers_to_stop); - }); + local.lsp_tree.remove_nodes(&language_servers_to_stop); let tasks = language_servers_to_stop .into_iter() .map(|server| self.stop_local_language_server(server, cx)) @@ -10571,34 +10571,28 @@ impl LspStore { if let Some(worktree_id) = buffer.file().map(|f| f.worktree_id(cx)) { if covered_worktrees.insert(worktree_id) { language_server_names_to_stop.retain(|name| { - match local.language_server_ids.get(&(worktree_id, name.clone())) { - Some(server_ids) => { - language_servers_to_stop - .extend(server_ids.into_iter().copied()); - false - } - None => true, - } + let old_ids_count = language_servers_to_stop.len(); + let all_language_servers_with_this_name = local + .language_server_ids + .iter() + .filter_map(|(seed, state)| seed.name.eq(name).then(|| state.id)); + language_servers_to_stop.extend(all_language_servers_with_this_name); + old_ids_count == language_servers_to_stop.len() }); } } }); } for name in language_server_names_to_stop { - if let Some(server_ids) = local - .language_server_ids - .iter() - .filter(|((_, server_name), _)| server_name == &name) - .map(|((_, _), server_ids)| server_ids) - .max_by_key(|server_ids| server_ids.len()) - { - language_servers_to_stop.extend(server_ids.into_iter().copied()); - } + language_servers_to_stop.extend( + local + .language_server_ids + .iter() + .filter_map(|(seed, v)| seed.name.eq(&name).then(|| v.id)), + ); } - local.lsp_tree.update(cx, |this, _| { - this.remove_nodes(&language_servers_to_stop); - }); + local.lsp_tree.remove_nodes(&language_servers_to_stop); let tasks = language_servers_to_stop .into_iter() .map(|server| self.stop_local_language_server(server, cx)) @@ -10821,7 +10815,7 @@ impl LspStore { adapter: Arc, language_server: Arc, server_id: LanguageServerId, - key: (WorktreeId, LanguageServerName), + key: LanguageServerSeed, workspace_folders: Arc>>, cx: &mut Context, ) { @@ -10833,7 +10827,7 @@ impl LspStore { if local .language_server_ids .get(&key) - .map(|ids| !ids.contains(&server_id)) + .map(|state| state.id != server_id) .unwrap_or(false) { return; @@ -10890,7 +10884,7 @@ impl LspStore { cx.emit(LspStoreEvent::LanguageServerAdded( server_id, language_server.name(), - Some(key.0), + Some(key.worktree_id), )); cx.emit(LspStoreEvent::RefreshInlayHints); @@ -10902,7 +10896,7 @@ impl LspStore { server: Some(proto::LanguageServer { id: server_id.to_proto(), name: language_server.name().to_string(), - worktree_id: Some(key.0.to_proto()), + worktree_id: Some(key.worktree_id.to_proto()), }), capabilities: serde_json::to_string(&server_capabilities) .expect("serializing server LSP capabilities"), @@ -10914,13 +10908,13 @@ impl LspStore { // Tell the language server about every open buffer in the worktree that matches the language. // Also check for buffers in worktrees that reused this server - let mut worktrees_using_server = vec![key.0]; + let mut worktrees_using_server = vec![key.worktree_id]; if let Some(local) = self.as_local() { // Find all worktrees that have this server in their language server tree - for (worktree_id, servers) in &local.lsp_tree.read(cx).instances { - if *worktree_id != key.0 { + for (worktree_id, servers) in &local.lsp_tree.instances { + if *worktree_id != key.worktree_id { for (_, server_map) in &servers.roots { - if server_map.contains_key(&key.1) { + if server_map.contains_key(&key.name) { worktrees_using_server.push(*worktree_id); } } @@ -10946,7 +10940,7 @@ impl LspStore { .languages .lsp_adapters(&language.name()) .iter() - .any(|a| a.name == key.1) + .any(|a| a.name == key.name) { continue; } @@ -11191,11 +11185,7 @@ impl LspStore { let mut language_server_ids = local .language_server_ids .iter() - .flat_map(|((server_worktree, _), server_ids)| { - server_ids - .iter() - .filter_map(|server_id| server_worktree.eq(&worktree_id).then(|| *server_id)) - }) + .filter_map(|(seed, v)| seed.worktree_id.eq(&worktree_id).then(|| v.id)) .collect::>(); language_server_ids.sort(); language_server_ids.dedup(); @@ -11239,6 +11229,14 @@ impl LspStore { } } } + for (path, _, _) in changes { + if let Some(file_name) = path.file_name().and_then(|file_name| file_name.to_str()) + && local.watched_manifest_filenames.contains(file_name) + { + self.request_workspace_config_refresh(); + break; + } + } } pub fn wait_for_remote_buffer( @@ -12785,7 +12783,7 @@ impl LspAdapter for SshLspAdapter { async fn check_if_user_installed( &self, _: &dyn LspAdapterDelegate, - _: Arc, + _: Option, _: &AsyncApp, ) -> Option { Some(self.binary.clone()) diff --git a/crates/project/src/manifest_tree.rs b/crates/project/src/manifest_tree.rs index 7266acb5b4..8621d24d06 100644 --- a/crates/project/src/manifest_tree.rs +++ b/crates/project/src/manifest_tree.rs @@ -7,18 +7,12 @@ mod manifest_store; mod path_trie; mod server_tree; -use std::{ - borrow::Borrow, - collections::{BTreeMap, hash_map::Entry}, - ops::ControlFlow, - path::Path, - sync::Arc, -}; +use std::{borrow::Borrow, collections::hash_map::Entry, ops::ControlFlow, path::Path, sync::Arc}; use collections::HashMap; -use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription}; +use gpui::{App, AppContext as _, Context, Entity, Subscription}; use language::{ManifestDelegate, ManifestName, ManifestQuery}; -pub use manifest_store::ManifestProviders; +pub use manifest_store::ManifestProvidersStore; use path_trie::{LabelPresence, RootPathTrie, TriePath}; use settings::{SettingsStore, WorktreeId}; use worktree::{Event as WorktreeEvent, Snapshot, Worktree}; @@ -28,9 +22,7 @@ use crate::{ worktree_store::{WorktreeStore, WorktreeStoreEvent}, }; -pub(crate) use server_tree::{ - AdapterQuery, LanguageServerTree, LanguageServerTreeNode, LaunchDisposition, -}; +pub(crate) use server_tree::{LanguageServerTree, LanguageServerTreeNode, LaunchDisposition}; struct WorktreeRoots { roots: RootPathTrie, @@ -81,14 +73,6 @@ pub struct ManifestTree { _subscriptions: [Subscription; 2], } -#[derive(PartialEq)] -pub(crate) enum ManifestTreeEvent { - WorktreeRemoved(WorktreeId), - Cleared, -} - -impl EventEmitter for ManifestTree {} - impl ManifestTree { pub fn new(worktree_store: Entity, cx: &mut App) -> Entity { cx.new(|cx| Self { @@ -101,30 +85,28 @@ impl ManifestTree { worktree_roots.roots = RootPathTrie::new(); }) } - cx.emit(ManifestTreeEvent::Cleared); }), ], worktree_store, }) } + pub(crate) fn root_for_path( &mut self, - ProjectPath { worktree_id, path }: ProjectPath, - manifests: &mut dyn Iterator, - delegate: Arc, + ProjectPath { worktree_id, path }: &ProjectPath, + manifest_name: &ManifestName, + delegate: &Arc, cx: &mut App, - ) -> BTreeMap { - debug_assert_eq!(delegate.worktree_id(), worktree_id); - let mut roots = BTreeMap::from_iter( - manifests.map(|manifest| (manifest, (None, LabelPresence::KnownAbsent))), - ); - let worktree_roots = match self.root_points.entry(worktree_id) { + ) -> Option { + debug_assert_eq!(delegate.worktree_id(), *worktree_id); + let (mut marked_path, mut current_presence) = (None, LabelPresence::KnownAbsent); + let worktree_roots = match self.root_points.entry(*worktree_id) { Entry::Occupied(occupied_entry) => occupied_entry.get().clone(), Entry::Vacant(vacant_entry) => { let Some(worktree) = self .worktree_store .read(cx) - .worktree_for_id(worktree_id, cx) + .worktree_for_id(*worktree_id, cx) else { return Default::default(); }; @@ -133,16 +115,16 @@ impl ManifestTree { } }; - let key = TriePath::from(&*path); + let key = TriePath::from(&**path); worktree_roots.read_with(cx, |this, _| { this.roots.walk(&key, &mut |path, labels| { for (label, presence) in labels { - if let Some((marked_path, current_presence)) = roots.get_mut(label) { - if *current_presence > *presence { + if label == manifest_name { + if current_presence > *presence { debug_assert!(false, "RootPathTrie precondition violation; while walking the tree label presence is only allowed to increase"); } - *marked_path = Some(ProjectPath {worktree_id, path: path.clone()}); - *current_presence = *presence; + marked_path = Some(ProjectPath {worktree_id: *worktree_id, path: path.clone()}); + current_presence = *presence; } } @@ -150,12 +132,9 @@ impl ManifestTree { }); }); - for (manifest_name, (root_path, presence)) in &mut roots { - if *presence == LabelPresence::Present { - continue; - } - - let depth = root_path + if current_presence == LabelPresence::KnownAbsent { + // Some part of the path is unexplored. + let depth = marked_path .as_ref() .map(|root_path| { path.strip_prefix(&root_path.path) @@ -165,13 +144,10 @@ impl ManifestTree { }) .unwrap_or_else(|| path.components().count() + 1); - if depth > 0 { - let Some(provider) = ManifestProviders::global(cx).get(manifest_name.borrow()) - else { - log::warn!("Manifest provider `{}` not found", manifest_name.as_ref()); - continue; - }; - + if depth > 0 + && let Some(provider) = + ManifestProvidersStore::global(cx).get(manifest_name.borrow()) + { let root = provider.search(ManifestQuery { path: path.clone(), depth, @@ -182,9 +158,9 @@ impl ManifestTree { let root = TriePath::from(&*known_root); this.roots .insert(&root, manifest_name.clone(), LabelPresence::Present); - *presence = LabelPresence::Present; - *root_path = Some(ProjectPath { - worktree_id, + current_presence = LabelPresence::Present; + marked_path = Some(ProjectPath { + worktree_id: *worktree_id, path: known_root, }); }), @@ -195,25 +171,35 @@ impl ManifestTree { } } } - - roots - .into_iter() - .filter_map(|(k, (path, presence))| { - let path = path?; - presence.eq(&LabelPresence::Present).then(|| (k, path)) - }) - .collect() + marked_path.filter(|_| current_presence.eq(&LabelPresence::Present)) } + + pub(crate) fn root_for_path_or_worktree_root( + &mut self, + project_path: &ProjectPath, + manifest_name: Option<&ManifestName>, + delegate: &Arc, + cx: &mut App, + ) -> ProjectPath { + let worktree_id = project_path.worktree_id; + // Backwards-compat: Fill in any adapters for which we did not detect the root as having the project root at the root of a worktree. + manifest_name + .and_then(|manifest_name| self.root_for_path(project_path, manifest_name, delegate, cx)) + .unwrap_or_else(|| ProjectPath { + worktree_id, + path: Arc::from(Path::new("")), + }) + } + fn on_worktree_store_event( &mut self, _: Entity, evt: &WorktreeStoreEvent, - cx: &mut Context, + _: &mut Context, ) { match evt { WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => { self.root_points.remove(&worktree_id); - cx.emit(ManifestTreeEvent::WorktreeRemoved(*worktree_id)); } _ => {} } @@ -223,6 +209,7 @@ impl ManifestTree { pub(crate) struct ManifestQueryDelegate { worktree: Snapshot, } + impl ManifestQueryDelegate { pub fn new(worktree: Snapshot) -> Self { Self { worktree } diff --git a/crates/project/src/manifest_tree/manifest_store.rs b/crates/project/src/manifest_tree/manifest_store.rs index 0462b25798..cf9f81aee4 100644 --- a/crates/project/src/manifest_tree/manifest_store.rs +++ b/crates/project/src/manifest_tree/manifest_store.rs @@ -1,4 +1,4 @@ -use collections::HashMap; +use collections::{HashMap, HashSet}; use gpui::{App, Global, SharedString}; use parking_lot::RwLock; use std::{ops::Deref, sync::Arc}; @@ -11,13 +11,13 @@ struct ManifestProvidersState { } #[derive(Clone, Default)] -pub struct ManifestProviders(Arc>); +pub struct ManifestProvidersStore(Arc>); #[derive(Default)] -struct GlobalManifestProvider(ManifestProviders); +struct GlobalManifestProvider(ManifestProvidersStore); impl Deref for GlobalManifestProvider { - type Target = ManifestProviders; + type Target = ManifestProvidersStore; fn deref(&self) -> &Self::Target { &self.0 @@ -26,7 +26,7 @@ impl Deref for GlobalManifestProvider { impl Global for GlobalManifestProvider {} -impl ManifestProviders { +impl ManifestProvidersStore { /// Returns the global [`ManifestStore`]. /// /// Inserts a default [`ManifestStore`] if one does not yet exist. @@ -45,4 +45,7 @@ impl ManifestProviders { pub(super) fn get(&self, name: &SharedString) -> Option> { self.0.read().providers.get(name).cloned() } + pub(crate) fn manifest_file_names(&self) -> HashSet { + self.0.read().providers.keys().cloned().collect() + } } diff --git a/crates/project/src/manifest_tree/server_tree.rs b/crates/project/src/manifest_tree/server_tree.rs index 81cb1c450c..49c0cff730 100644 --- a/crates/project/src/manifest_tree/server_tree.rs +++ b/crates/project/src/manifest_tree/server_tree.rs @@ -4,8 +4,7 @@ //! //! ## RPC //! LSP Tree is transparent to RPC peers; when clients ask host to spawn a new language server, the host will perform LSP Tree lookup for provided path; it may decide -//! to reuse existing language server. The client maintains it's own LSP Tree that is a subset of host LSP Tree. Done this way, the client does not need to -//! ask about suitable language server for each path it interacts with; it can resolve most of the queries locally. +//! to reuse existing language server. use std::{ collections::{BTreeMap, BTreeSet}, @@ -14,20 +13,23 @@ use std::{ }; use collections::IndexMap; -use gpui::{App, AppContext as _, Entity, Subscription}; +use gpui::{App, Entity}; use language::{ - CachedLspAdapter, LanguageName, LanguageRegistry, ManifestDelegate, + CachedLspAdapter, LanguageName, LanguageRegistry, ManifestDelegate, ManifestName, Toolchain, language_settings::AllLanguageSettings, }; use lsp::LanguageServerName; use settings::{Settings, SettingsLocation, WorktreeId}; use std::sync::OnceLock; -use crate::{LanguageServerId, ProjectPath, project_settings::LspSettings}; +use crate::{ + LanguageServerId, ProjectPath, project_settings::LspSettings, + toolchain_store::LocalToolchainStore, +}; -use super::{ManifestTree, ManifestTreeEvent}; +use super::ManifestTree; -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default)] pub(crate) struct ServersForWorktree { pub(crate) roots: BTreeMap< Arc, @@ -39,7 +41,7 @@ pub struct LanguageServerTree { manifest_tree: Entity, pub(crate) instances: BTreeMap, languages: Arc, - _subscriptions: Subscription, + toolchains: Entity, } /// A node in language server tree represents either: @@ -49,22 +51,15 @@ pub struct LanguageServerTree { pub struct LanguageServerTreeNode(Weak); /// Describes a request to launch a language server. -#[derive(Debug)] -pub(crate) struct LaunchDisposition<'a> { - pub(crate) server_name: &'a LanguageServerName, +#[derive(Clone, Debug)] +pub(crate) struct LaunchDisposition { + pub(crate) server_name: LanguageServerName, + /// Path to the root directory of a subproject. pub(crate) path: ProjectPath, pub(crate) settings: Arc, + pub(crate) toolchain: Option, } -impl<'a> From<&'a InnerTreeNode> for LaunchDisposition<'a> { - fn from(value: &'a InnerTreeNode) -> Self { - LaunchDisposition { - server_name: &value.name, - path: value.path.clone(), - settings: value.settings.clone(), - } - } -} impl LanguageServerTreeNode { /// Returns a language server ID for this node if there is one. /// Returns None if this node has not been initialized yet or it is no longer in the tree. @@ -76,19 +71,17 @@ impl LanguageServerTreeNode { /// May return None if the node no longer belongs to the server tree it was created in. pub(crate) fn server_id_or_init( &self, - init: impl FnOnce(LaunchDisposition) -> LanguageServerId, + init: impl FnOnce(&Arc) -> LanguageServerId, ) -> Option { let this = self.0.upgrade()?; - Some( - *this - .id - .get_or_init(|| init(LaunchDisposition::from(&*this))), - ) + Some(*this.id.get_or_init(|| init(&this.disposition))) } /// Returns a language server name as the language server adapter would return. pub fn name(&self) -> Option { - self.0.upgrade().map(|node| node.name.clone()) + self.0 + .upgrade() + .map(|node| node.disposition.server_name.clone()) } } @@ -101,160 +94,149 @@ impl From> for LanguageServerTreeNode { #[derive(Debug)] pub struct InnerTreeNode { id: OnceLock, - name: LanguageServerName, - path: ProjectPath, - settings: Arc, + disposition: Arc, } impl InnerTreeNode { fn new( - name: LanguageServerName, + server_name: LanguageServerName, path: ProjectPath, - settings: impl Into>, + settings: LspSettings, + toolchain: Option, ) -> Self { InnerTreeNode { id: Default::default(), - name, - path, - settings: settings.into(), + disposition: Arc::new(LaunchDisposition { + server_name, + path, + settings: settings.into(), + toolchain, + }), } } } -/// Determines how the list of adapters to query should be constructed. -pub(crate) enum AdapterQuery<'a> { - /// Search for roots of all adapters associated with a given language name. - /// Layman: Look for all project roots along the queried path that have any - /// language server associated with this language running. - Language(&'a LanguageName), - /// Search for roots of adapter with a given name. - /// Layman: Look for all project roots along the queried path that have this server running. - Adapter(&'a LanguageServerName), -} - impl LanguageServerTree { pub(crate) fn new( manifest_tree: Entity, languages: Arc, - cx: &mut App, - ) -> Entity { - cx.new(|cx| Self { - _subscriptions: cx.subscribe(&manifest_tree, |_: &mut Self, _, event, _| { - if event == &ManifestTreeEvent::Cleared {} - }), + toolchains: Entity, + ) -> Self { + Self { manifest_tree, instances: Default::default(), - languages, - }) + toolchains, + } + } + + /// Get all initialized language server IDs for a given path. + pub(crate) fn get<'a>( + &'a self, + path: ProjectPath, + language_name: LanguageName, + manifest_name: Option<&ManifestName>, + delegate: &Arc, + cx: &mut App, + ) -> impl Iterator + 'a { + let manifest_location = self.manifest_location_for_path(&path, manifest_name, delegate, cx); + let adapters = self.adapters_for_language(&manifest_location, &language_name, cx); + self.get_with_adapters(manifest_location, adapters) } /// Get all language server root points for a given path and language; the language servers might already be initialized at a given path. - pub(crate) fn get<'a>( + pub(crate) fn walk<'a>( &'a mut self, path: ProjectPath, - query: AdapterQuery<'_>, - delegate: Arc, - cx: &mut App, + language_name: LanguageName, + manifest_name: Option<&ManifestName>, + delegate: &Arc, + cx: &'a mut App, ) -> impl Iterator + 'a { - let settings_location = SettingsLocation { - worktree_id: path.worktree_id, - path: &path.path, - }; - let adapters = match query { - AdapterQuery::Language(language_name) => { - self.adapters_for_language(settings_location, language_name, cx) - } - AdapterQuery::Adapter(language_server_name) => { - IndexMap::from_iter(self.adapter_for_name(language_server_name).map(|adapter| { - ( + let manifest_location = self.manifest_location_for_path(&path, manifest_name, delegate, cx); + let adapters = self.adapters_for_language(&manifest_location, &language_name, cx); + self.init_with_adapters(manifest_location, language_name, adapters, cx) + } + + fn init_with_adapters<'a>( + &'a mut self, + root_path: ProjectPath, + language_name: LanguageName, + adapters: IndexMap)>, + cx: &'a App, + ) -> impl Iterator + 'a { + adapters.into_iter().map(move |(_, (settings, adapter))| { + let root_path = root_path.clone(); + let inner_node = self + .instances + .entry(root_path.worktree_id) + .or_default() + .roots + .entry(root_path.path.clone()) + .or_default() + .entry(adapter.name()); + let (node, languages) = inner_node.or_insert_with(|| { + let toolchain = self.toolchains.read(cx).active_toolchain( + root_path.worktree_id, + &root_path.path, + language_name.clone(), + ); + ( + Arc::new(InnerTreeNode::new( adapter.name(), - (LspSettings::default(), BTreeSet::new(), adapter), - ) - })) - } - }; - self.get_with_adapters(path, adapters, delegate, cx) + root_path.clone(), + settings.clone(), + toolchain, + )), + Default::default(), + ) + }); + languages.insert(language_name.clone()); + Arc::downgrade(&node).into() + }) } fn get_with_adapters<'a>( - &'a mut self, - path: ProjectPath, - adapters: IndexMap< - LanguageServerName, - (LspSettings, BTreeSet, Arc), - >, - delegate: Arc, - cx: &mut App, - ) -> impl Iterator + 'a { - let worktree_id = path.worktree_id; - - let mut manifest_to_adapters = BTreeMap::default(); - for (_, _, adapter) in adapters.values() { - if let Some(manifest_name) = adapter.manifest_name() { - manifest_to_adapters - .entry(manifest_name) - .or_insert_with(Vec::default) - .push(adapter.clone()); - } - } - - let roots = self.manifest_tree.update(cx, |this, cx| { - this.root_for_path( - path, - &mut manifest_to_adapters.keys().cloned(), - delegate, - cx, - ) - }); - let root_path = std::cell::LazyCell::new(move || ProjectPath { - worktree_id, - path: Arc::from("".as_ref()), - }); - adapters - .into_iter() - .map(move |(_, (settings, new_languages, adapter))| { - // Backwards-compat: Fill in any adapters for which we did not detect the root as having the project root at the root of a worktree. - let root_path = adapter - .manifest_name() - .and_then(|name| roots.get(&name)) - .cloned() - .unwrap_or_else(|| root_path.clone()); - - let inner_node = self - .instances - .entry(root_path.worktree_id) - .or_default() - .roots - .entry(root_path.path.clone()) - .or_default() - .entry(adapter.name()); - let (node, languages) = inner_node.or_insert_with(|| { - ( - Arc::new(InnerTreeNode::new( - adapter.name(), - root_path.clone(), - settings.clone(), - )), - Default::default(), - ) - }); - languages.extend(new_languages.iter().cloned()); - Arc::downgrade(&node).into() - }) + &'a self, + root_path: ProjectPath, + adapters: IndexMap)>, + ) -> impl Iterator + 'a { + adapters.into_iter().filter_map(move |(_, (_, adapter))| { + let root_path = root_path.clone(); + let inner_node = self + .instances + .get(&root_path.worktree_id)? + .roots + .get(&root_path.path)? + .get(&adapter.name())?; + inner_node.0.id.get().copied() + }) } - fn adapter_for_name(&self, name: &LanguageServerName) -> Option> { - self.languages.adapter_for_name(name) + fn manifest_location_for_path( + &self, + path: &ProjectPath, + manifest_name: Option<&ManifestName>, + delegate: &Arc, + cx: &mut App, + ) -> ProjectPath { + // Find out what the root location of our subproject is. + // That's where we'll look for language settings (that include a set of language servers). + self.manifest_tree.update(cx, |this, cx| { + this.root_for_path_or_worktree_root(path, manifest_name, delegate, cx) + }) } fn adapters_for_language( &self, - settings_location: SettingsLocation, + manifest_location: &ProjectPath, language_name: &LanguageName, cx: &App, - ) -> IndexMap, Arc)> - { + ) -> IndexMap)> { + let settings_location = SettingsLocation { + worktree_id: manifest_location.worktree_id, + path: &manifest_location.path, + }; let settings = AllLanguageSettings::get(Some(settings_location), cx).language( Some(settings_location), Some(language_name), @@ -295,14 +277,7 @@ impl LanguageServerTree { ) .cloned() .unwrap_or_default(); - Some(( - adapter.name(), - ( - adapter_settings, - BTreeSet::from_iter([language_name.clone()]), - adapter, - ), - )) + Some((adapter.name(), (adapter_settings, adapter))) }) .collect::>(); // After starting all the language servers, reorder them to reflect the desired order @@ -315,17 +290,23 @@ impl LanguageServerTree { &language_name, adapters_with_settings .values() - .map(|(_, _, adapter)| adapter.clone()) + .map(|(_, adapter)| adapter.clone()) .collect(), ); adapters_with_settings } - // Rebasing a tree: - // - Clears it out - // - Provides you with the indirect access to the old tree while you're reinitializing a new one (by querying it). - pub(crate) fn rebase(&mut self) -> ServerTreeRebase<'_> { + /// Server Tree is built up incrementally via queries for distinct paths of the worktree. + /// Results of these queries have to be invalidated when data used to build the tree changes. + /// + /// The environment of a server tree is a set of all user settings. + /// Rebasing a tree means invalidating it and building up a new one while reusing the old tree where applicable. + /// We want to reuse the old tree in order to preserve as many of the running language servers as possible. + /// E.g. if the user disables one of their language servers for Python, we don't want to shut down any language servers unaffected by this settings change. + /// + /// Thus, [`ServerTreeRebase`] mimics the interface of a [`ServerTree`], except that it tries to find a matching language server in the old tree before handing out an uninitialized node. + pub(crate) fn rebase(&mut self) -> ServerTreeRebase { ServerTreeRebase::new(self) } @@ -354,16 +335,16 @@ impl LanguageServerTree { .roots .entry(Arc::from(Path::new(""))) .or_default() - .entry(node.name.clone()) + .entry(node.disposition.server_name.clone()) .or_insert_with(|| (node, BTreeSet::new())) .1 .insert(language_name); } } -pub(crate) struct ServerTreeRebase<'a> { +pub(crate) struct ServerTreeRebase { old_contents: BTreeMap, - new_tree: &'a mut LanguageServerTree, + new_tree: LanguageServerTree, /// All server IDs seen in the old tree. all_server_ids: BTreeMap, /// Server IDs we've preserved for a new iteration of the tree. `all_server_ids - rebased_server_ids` is the @@ -371,9 +352,9 @@ pub(crate) struct ServerTreeRebase<'a> { rebased_server_ids: BTreeSet, } -impl<'tree> ServerTreeRebase<'tree> { - fn new(new_tree: &'tree mut LanguageServerTree) -> Self { - let old_contents = std::mem::take(&mut new_tree.instances); +impl ServerTreeRebase { + fn new(old_tree: &LanguageServerTree) -> Self { + let old_contents = old_tree.instances.clone(); let all_server_ids = old_contents .values() .flat_map(|nodes| { @@ -384,69 +365,68 @@ impl<'tree> ServerTreeRebase<'tree> { .id .get() .copied() - .map(|id| (id, server.0.name.clone())) + .map(|id| (id, server.0.disposition.server_name.clone())) }) }) }) .collect(); + let new_tree = LanguageServerTree::new( + old_tree.manifest_tree.clone(), + old_tree.languages.clone(), + old_tree.toolchains.clone(), + ); Self { old_contents, - new_tree, all_server_ids, + new_tree, rebased_server_ids: BTreeSet::new(), } } - pub(crate) fn get<'a>( + pub(crate) fn walk<'a>( &'a mut self, path: ProjectPath, - query: AdapterQuery<'_>, + language_name: LanguageName, + manifest_name: Option<&ManifestName>, delegate: Arc, - cx: &mut App, + cx: &'a mut App, ) -> impl Iterator + 'a { - let settings_location = SettingsLocation { - worktree_id: path.worktree_id, - path: &path.path, - }; - let adapters = match query { - AdapterQuery::Language(language_name) => { - self.new_tree - .adapters_for_language(settings_location, language_name, cx) - } - AdapterQuery::Adapter(language_server_name) => { - IndexMap::from_iter(self.new_tree.adapter_for_name(language_server_name).map( - |adapter| { - ( - adapter.name(), - (LspSettings::default(), BTreeSet::new(), adapter), - ) - }, - )) - } - }; + let manifest = + self.new_tree + .manifest_location_for_path(&path, manifest_name, &delegate, cx); + let adapters = self + .new_tree + .adapters_for_language(&manifest, &language_name, cx); self.new_tree - .get_with_adapters(path, adapters, delegate, cx) + .init_with_adapters(manifest, language_name, adapters, cx) .filter_map(|node| { // Inspect result of the query and initialize it ourselves before // handing it off to the caller. - let disposition = node.0.upgrade()?; + let live_node = node.0.upgrade()?; - if disposition.id.get().is_some() { + if live_node.id.get().is_some() { return Some(node); } + let disposition = &live_node.disposition; let Some((existing_node, _)) = self .old_contents .get(&disposition.path.worktree_id) .and_then(|worktree_nodes| worktree_nodes.roots.get(&disposition.path.path)) - .and_then(|roots| roots.get(&disposition.name)) - .filter(|(old_node, _)| disposition.settings == old_node.settings) + .and_then(|roots| roots.get(&disposition.server_name)) + .filter(|(old_node, _)| { + (&disposition.toolchain, &disposition.settings) + == ( + &old_node.disposition.toolchain, + &old_node.disposition.settings, + ) + }) else { return Some(node); }; if let Some(existing_id) = existing_node.id.get() { self.rebased_server_ids.insert(*existing_id); - disposition.id.set(*existing_id).ok(); + live_node.id.set(*existing_id).ok(); } Some(node) @@ -454,11 +434,19 @@ impl<'tree> ServerTreeRebase<'tree> { } /// Returns IDs of servers that are no longer referenced (and can be shut down). - pub(crate) fn finish(self) -> BTreeMap { - self.all_server_ids - .into_iter() - .filter(|(id, _)| !self.rebased_server_ids.contains(id)) - .collect() + pub(crate) fn finish( + self, + ) -> ( + LanguageServerTree, + BTreeMap, + ) { + ( + self.new_tree, + self.all_server_ids + .into_iter() + .filter(|(id, _)| !self.rebased_server_ids.contains(id)) + .collect(), + ) } pub(crate) fn server_tree(&mut self) -> &mut LanguageServerTree { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 27ab55d53e..57afaceeca 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -84,7 +84,7 @@ use lsp::{ }; use lsp_command::*; use lsp_store::{CompletionDocumentation, LspFormatTarget, OpenLspBufferHandle}; -pub use manifest_tree::ManifestProviders; +pub use manifest_tree::ManifestProvidersStore; use node_runtime::NodeRuntime; use parking_lot::Mutex; pub use prettier_store::PrettierStore; @@ -1115,7 +1115,11 @@ impl Project { buffer_store.clone(), worktree_store.clone(), prettier_store.clone(), - toolchain_store.clone(), + toolchain_store + .read(cx) + .as_local_store() + .expect("Toolchain store to be local") + .clone(), environment.clone(), manifest_tree, languages.clone(), @@ -1260,7 +1264,6 @@ impl Project { LspStore::new_remote( buffer_store.clone(), worktree_store.clone(), - Some(toolchain_store.clone()), languages.clone(), ssh_proto.clone(), SSH_PROJECT_ID, @@ -1485,7 +1488,6 @@ impl Project { let mut lsp_store = LspStore::new_remote( buffer_store.clone(), worktree_store.clone(), - None, languages.clone(), client.clone().into(), remote_id, @@ -3596,16 +3598,10 @@ impl Project { &mut self, abs_path: lsp::Url, language_server_id: LanguageServerId, - language_server_name: LanguageServerName, cx: &mut Context, ) -> Task>> { self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.open_local_buffer_via_lsp( - abs_path, - language_server_id, - language_server_name, - cx, - ) + lsp_store.open_local_buffer_via_lsp(abs_path, language_server_id, cx) }) } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 12e3aa88ad..d78526ddd0 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -22,6 +22,7 @@ use settings::{ SettingsStore, parse_json_with_comments, watch_config_file, }; use std::{ + collections::BTreeMap, path::{Path, PathBuf}, sync::Arc, time::Duration, @@ -518,16 +519,15 @@ impl Default for InlineBlameSettings { } } -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] pub struct BinarySettings { pub path: Option, pub arguments: Option>, - // this can't be an FxHashMap because the extension APIs require the default SipHash - pub env: Option>, + pub env: Option>, pub ignore_system_version: Option, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] #[serde(rename_all = "snake_case")] pub struct LspSettings { pub binary: Option, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index cb3c9efe60..5b3827b42b 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1099,9 +1099,9 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon let prev_read_dir_count = fs.read_dir_call_count(); let fake_server = fake_servers.next().await.unwrap(); - let (server_id, server_name) = lsp_store.read_with(cx, |lsp_store, _| { - let (id, status) = lsp_store.language_server_statuses().next().unwrap(); - (id, status.name.clone()) + let server_id = lsp_store.read_with(cx, |lsp_store, _| { + let (id, _) = lsp_store.language_server_statuses().next().unwrap(); + id }); // Simulate jumping to a definition in a dependency outside of the worktree. @@ -1110,7 +1110,6 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon project.open_local_buffer_via_lsp( lsp::Url::from_file_path(path!("/the-registry/dep1/src/dep1.rs")).unwrap(), server_id, - server_name.clone(), cx, ) }) diff --git a/crates/project/src/toolchain_store.rs b/crates/project/src/toolchain_store.rs index 61a005520d..05531ebe9a 100644 --- a/crates/project/src/toolchain_store.rs +++ b/crates/project/src/toolchain_store.rs @@ -11,7 +11,10 @@ use collections::BTreeMap; use gpui::{ App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, }; -use language::{LanguageName, LanguageRegistry, LanguageToolchainStore, Toolchain, ToolchainList}; +use language::{ + LanguageName, LanguageRegistry, LanguageToolchainStore, ManifestDelegate, Toolchain, + ToolchainList, +}; use rpc::{ AnyProtoClient, TypedEnvelope, proto::{self, FromProto, ToProto}, @@ -104,9 +107,11 @@ impl ToolchainStore { cx: &App, ) -> Task> { match &self.0 { - ToolchainStoreInner::Local(local, _) => { - local.read(cx).active_toolchain(path, language_name, cx) - } + ToolchainStoreInner::Local(local, _) => Task::ready(local.read(cx).active_toolchain( + path.worktree_id, + &path.path, + language_name, + )), ToolchainStoreInner::Remote(remote) => { remote.read(cx).active_toolchain(path, language_name, cx) } @@ -232,9 +237,15 @@ impl ToolchainStore { ToolchainStoreInner::Remote(remote) => Arc::new(RemoteStore(remote.downgrade())), } } + pub fn as_local_store(&self) -> Option<&Entity> { + match &self.0 { + ToolchainStoreInner::Local(local, _) => Some(local), + ToolchainStoreInner::Remote(_) => None, + } + } } -struct LocalToolchainStore { +pub struct LocalToolchainStore { languages: Arc, worktree_store: Entity, project_environment: Entity, @@ -243,20 +254,19 @@ struct LocalToolchainStore { } #[async_trait(?Send)] -impl language::LanguageToolchainStore for LocalStore { - async fn active_toolchain( +impl language::LocalLanguageToolchainStore for LocalStore { + fn active_toolchain( self: Arc, worktree_id: WorktreeId, - path: Arc, + path: &Arc, language_name: LanguageName, cx: &mut AsyncApp, ) -> Option { self.0 - .update(cx, |this, cx| { - this.active_toolchain(ProjectPath { worktree_id, path }, language_name, cx) + .update(cx, |this, _| { + this.active_toolchain(worktree_id, path, language_name) }) .ok()? - .await } } @@ -279,19 +289,18 @@ impl language::LanguageToolchainStore for RemoteStore { } pub struct EmptyToolchainStore; -#[async_trait(?Send)] -impl language::LanguageToolchainStore for EmptyToolchainStore { - async fn active_toolchain( +impl language::LocalLanguageToolchainStore for EmptyToolchainStore { + fn active_toolchain( self: Arc, _: WorktreeId, - _: Arc, + _: &Arc, _: LanguageName, _: &mut AsyncApp, ) -> Option { None } } -struct LocalStore(WeakEntity); +pub(crate) struct LocalStore(WeakEntity); struct RemoteStore(WeakEntity); #[derive(Clone)] @@ -349,17 +358,13 @@ impl LocalToolchainStore { .flatten()?; let worktree_id = snapshot.id(); let worktree_root = snapshot.abs_path().to_path_buf(); + let delegate = + Arc::from(ManifestQueryDelegate::new(snapshot)) as Arc; let relative_path = manifest_tree .update(cx, |this, cx| { - this.root_for_path( - path, - &mut std::iter::once(manifest_name.clone()), - Arc::new(ManifestQueryDelegate::new(snapshot)), - cx, - ) + this.root_for_path(&path, &manifest_name, &delegate, cx) }) .ok()? - .remove(&manifest_name) .unwrap_or_else(|| ProjectPath { path: Arc::from(Path::new("")), worktree_id, @@ -394,21 +399,20 @@ impl LocalToolchainStore { } pub(crate) fn active_toolchain( &self, - path: ProjectPath, + worktree_id: WorktreeId, + relative_path: &Arc, language_name: LanguageName, - _: &App, - ) -> Task> { - let ancestors = path.path.ancestors(); - Task::ready( - self.active_toolchains - .get(&(path.worktree_id, language_name)) - .and_then(|paths| { - ancestors - .into_iter() - .find_map(|root_path| paths.get(root_path)) - }) - .cloned(), - ) + ) -> Option { + let ancestors = relative_path.ancestors(); + + self.active_toolchains + .get(&(worktree_id, language_name)) + .and_then(|paths| { + ancestors + .into_iter() + .find_map(|root_path| paths.get(root_path)) + }) + .cloned() } } struct RemoteToolchainStore { diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index b4d3162641..ac1737ba4b 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -171,7 +171,11 @@ impl HeadlessProject { buffer_store.clone(), worktree_store.clone(), prettier_store.clone(), - toolchain_store.clone(), + toolchain_store + .read(cx) + .as_local_store() + .expect("Toolchain store to be local") + .clone(), environment, manifest_tree, languages.clone(), From 2075627d6c31a6661816335afc69e662ef0b60e2 Mon Sep 17 00:00:00 2001 From: Mahmud Ridwan Date: Mon, 18 Aug 2025 15:54:45 +0600 Subject: [PATCH 424/693] Suggest single tracked commit message only when nothing else is staged (#36347) Closes #36341 image In the case where commit message was suggested based on single tracked entry, this PR adds a clause to the condition to ensure there are no staged entries. Release Notes: - Fixed commit message suggestion when there is one unstaged tracked file, but multiple untracked files are staged. --- crates/git_ui/src/git_panel.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 70987dd212..b346f4d216 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1833,7 +1833,9 @@ impl GitPanel { let git_status_entry = if let Some(staged_entry) = &self.single_staged_entry { Some(staged_entry) - } else if let Some(single_tracked_entry) = &self.single_tracked_entry { + } else if self.total_staged_count() == 0 + && let Some(single_tracked_entry) = &self.single_tracked_entry + { Some(single_tracked_entry) } else { None From 2eadd5a3962e250fc14820ef60dbe94804959b41 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 18 Aug 2025 11:56:02 +0200 Subject: [PATCH 425/693] agent2: Make `model` of `Thread` optional (#36395) Related to #36394 Release Notes: - N/A --- crates/agent2/src/agent.rs | 42 ++--- crates/agent2/src/tests/mod.rs | 200 ++++++++++++++-------- crates/agent2/src/thread.rs | 45 ++--- crates/agent2/src/tools/edit_file_tool.rs | 32 ++-- crates/agent_ui/src/acp/thread_view.rs | 14 +- 5 files changed, 195 insertions(+), 138 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index d63e3f8134..0ad90753e1 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -427,9 +427,11 @@ impl NativeAgent { self.models.refresh_list(cx); for session in self.sessions.values_mut() { session.thread.update(cx, |thread, _| { - let model_id = LanguageModels::model_id(&thread.model()); - if let Some(model) = self.models.model_from_id(&model_id) { - thread.set_model(model.clone()); + if let Some(model) = thread.model() { + let model_id = LanguageModels::model_id(model); + if let Some(model) = self.models.model_from_id(&model_id) { + thread.set_model(model.clone()); + } } }); } @@ -622,13 +624,15 @@ impl AgentModelSelector for NativeAgentConnection { else { return Task::ready(Err(anyhow!("Session not found"))); }; - let model = thread.read(cx).model().clone(); + let Some(model) = thread.read(cx).model() else { + return Task::ready(Err(anyhow!("Model not found"))); + }; let Some(provider) = LanguageModelRegistry::read_global(cx).provider(&model.provider_id()) else { return Task::ready(Err(anyhow!("Provider not found"))); }; Task::ready(Ok(LanguageModels::map_language_model_to_info( - &model, &provider, + model, &provider, ))) } @@ -679,19 +683,11 @@ impl acp_thread::AgentConnection for NativeAgentConnection { let available_count = registry.available_models(cx).count(); log::debug!("Total available models: {}", available_count); - let default_model = registry - .default_model() - .and_then(|default_model| { - agent - .models - .model_from_id(&LanguageModels::model_id(&default_model.model)) - }) - .ok_or_else(|| { - log::warn!("No default model configured in settings"); - anyhow!( - "No default model. Please configure a default model in settings." - ) - })?; + let default_model = registry.default_model().and_then(|default_model| { + agent + .models + .model_from_id(&LanguageModels::model_id(&default_model.model)) + }); let thread = cx.new(|cx| { let mut thread = Thread::new( @@ -777,13 +773,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { log::debug!("Message id: {:?}", id); log::debug!("Message content: {:?}", content); - Ok(thread.update(cx, |thread, cx| { - log::info!( - "Sending message to thread with model: {:?}", - thread.model().name() - ); - thread.send(id, content, cx) - })) + thread.update(cx, |thread, cx| thread.send(id, content, cx)) }) } @@ -1008,7 +998,7 @@ mod tests { agent.read_with(cx, |agent, _| { let session = agent.sessions.get(&session_id).unwrap(); session.thread.read_with(cx, |thread, _| { - assert_eq!(thread.model().id().0, "fake"); + assert_eq!(thread.model().unwrap().id().0, "fake"); }); }); diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 48a16bf685..e3e3050d49 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -40,6 +40,7 @@ async fn test_echo(cx: &mut TestAppContext) { .update(cx, |thread, cx| { thread.send(UserMessageId::new(), ["Testing: Reply with 'Hello'"], cx) }) + .unwrap() .collect() .await; thread.update(cx, |thread, _cx| { @@ -73,6 +74,7 @@ async fn test_thinking(cx: &mut TestAppContext) { cx, ) }) + .unwrap() .collect() .await; thread.update(cx, |thread, _cx| { @@ -101,9 +103,11 @@ async fn test_system_prompt(cx: &mut TestAppContext) { project_context.borrow_mut().shell = "test-shell".into(); thread.update(cx, |thread, _| thread.add_tool(EchoTool)); - thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["abc"], cx) - }); + thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["abc"], cx) + }) + .unwrap(); cx.run_until_parked(); let mut pending_completions = fake_model.pending_completions(); assert_eq!( @@ -136,9 +140,11 @@ async fn test_prompt_caching(cx: &mut TestAppContext) { let fake_model = model.as_fake(); // Send initial user message and verify it's cached - thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Message 1"], cx) - }); + thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Message 1"], cx) + }) + .unwrap(); cx.run_until_parked(); let completion = fake_model.pending_completions().pop().unwrap(); @@ -157,9 +163,11 @@ async fn test_prompt_caching(cx: &mut TestAppContext) { cx.run_until_parked(); // Send another user message and verify only the latest is cached - thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Message 2"], cx) - }); + thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Message 2"], cx) + }) + .unwrap(); cx.run_until_parked(); let completion = fake_model.pending_completions().pop().unwrap(); @@ -191,9 +199,11 @@ async fn test_prompt_caching(cx: &mut TestAppContext) { // Simulate a tool call and verify that the latest tool result is cached thread.update(cx, |thread, _| thread.add_tool(EchoTool)); - thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Use the echo tool"], cx) - }); + thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Use the echo tool"], cx) + }) + .unwrap(); cx.run_until_parked(); let tool_use = LanguageModelToolUse { @@ -273,6 +283,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) { cx, ) }) + .unwrap() .collect() .await; assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); @@ -291,6 +302,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) { cx, ) }) + .unwrap() .collect() .await; assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); @@ -322,10 +334,12 @@ async fn test_streaming_tool_calls(cx: &mut TestAppContext) { let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; // Test a tool call that's likely to complete *before* streaming stops. - let mut events = thread.update(cx, |thread, cx| { - thread.add_tool(WordListTool); - thread.send(UserMessageId::new(), ["Test the word_list tool."], cx) - }); + let mut events = thread + .update(cx, |thread, cx| { + thread.add_tool(WordListTool); + thread.send(UserMessageId::new(), ["Test the word_list tool."], cx) + }) + .unwrap(); let mut saw_partial_tool_use = false; while let Some(event) = events.next().await { @@ -371,10 +385,12 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); - let mut events = thread.update(cx, |thread, cx| { - thread.add_tool(ToolRequiringPermission); - thread.send(UserMessageId::new(), ["abc"], cx) - }); + let mut events = thread + .update(cx, |thread, cx| { + thread.add_tool(ToolRequiringPermission); + thread.send(UserMessageId::new(), ["abc"], cx) + }) + .unwrap(); cx.run_until_parked(); fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { @@ -501,9 +517,11 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); - let mut events = thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["abc"], cx) - }); + let mut events = thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["abc"], cx) + }) + .unwrap(); cx.run_until_parked(); fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { @@ -528,10 +546,12 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); - let events = thread.update(cx, |thread, cx| { - thread.add_tool(EchoTool); - thread.send(UserMessageId::new(), ["abc"], cx) - }); + let events = thread + .update(cx, |thread, cx| { + thread.add_tool(EchoTool); + thread.send(UserMessageId::new(), ["abc"], cx) + }) + .unwrap(); cx.run_until_parked(); let tool_use = LanguageModelToolUse { id: "tool_id_1".into(), @@ -644,10 +664,12 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); - let events = thread.update(cx, |thread, cx| { - thread.add_tool(EchoTool); - thread.send(UserMessageId::new(), ["abc"], cx) - }); + let events = thread + .update(cx, |thread, cx| { + thread.add_tool(EchoTool); + thread.send(UserMessageId::new(), ["abc"], cx) + }) + .unwrap(); cx.run_until_parked(); let tool_use = LanguageModelToolUse { @@ -677,9 +699,11 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) { .is::() ); - thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), vec!["ghi"], cx) - }); + thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), vec!["ghi"], cx) + }) + .unwrap(); cx.run_until_parked(); let completion = fake_model.pending_completions().pop().unwrap(); assert_eq!( @@ -790,6 +814,7 @@ async fn test_concurrent_tool_calls(cx: &mut TestAppContext) { cx, ) }) + .unwrap() .collect() .await; @@ -857,10 +882,12 @@ async fn test_profiles(cx: &mut TestAppContext) { cx.run_until_parked(); // Test that test-1 profile (default) has echo and delay tools - thread.update(cx, |thread, cx| { - thread.set_profile(AgentProfileId("test-1".into())); - thread.send(UserMessageId::new(), ["test"], cx); - }); + thread + .update(cx, |thread, cx| { + thread.set_profile(AgentProfileId("test-1".into())); + thread.send(UserMessageId::new(), ["test"], cx) + }) + .unwrap(); cx.run_until_parked(); let mut pending_completions = fake_model.pending_completions(); @@ -875,10 +902,12 @@ async fn test_profiles(cx: &mut TestAppContext) { fake_model.end_last_completion_stream(); // Switch to test-2 profile, and verify that it has only the infinite tool. - thread.update(cx, |thread, cx| { - thread.set_profile(AgentProfileId("test-2".into())); - thread.send(UserMessageId::new(), ["test2"], cx) - }); + thread + .update(cx, |thread, cx| { + thread.set_profile(AgentProfileId("test-2".into())); + thread.send(UserMessageId::new(), ["test2"], cx) + }) + .unwrap(); cx.run_until_parked(); let mut pending_completions = fake_model.pending_completions(); assert_eq!(pending_completions.len(), 1); @@ -896,15 +925,17 @@ async fn test_profiles(cx: &mut TestAppContext) { async fn test_cancellation(cx: &mut TestAppContext) { let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; - let mut events = thread.update(cx, |thread, cx| { - thread.add_tool(InfiniteTool); - thread.add_tool(EchoTool); - thread.send( - UserMessageId::new(), - ["Call the echo tool, then call the infinite tool, then explain their output"], - cx, - ) - }); + let mut events = thread + .update(cx, |thread, cx| { + thread.add_tool(InfiniteTool); + thread.add_tool(EchoTool); + thread.send( + UserMessageId::new(), + ["Call the echo tool, then call the infinite tool, then explain their output"], + cx, + ) + }) + .unwrap(); // Wait until both tools are called. let mut expected_tools = vec!["Echo", "Infinite Tool"]; @@ -960,6 +991,7 @@ async fn test_cancellation(cx: &mut TestAppContext) { cx, ) }) + .unwrap() .collect::>() .await; thread.update(cx, |thread, _cx| { @@ -978,16 +1010,20 @@ async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); - let events_1 = thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello 1"], cx) - }); + let events_1 = thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello 1"], cx) + }) + .unwrap(); cx.run_until_parked(); fake_model.send_last_completion_stream_text_chunk("Hey 1!"); cx.run_until_parked(); - let events_2 = thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello 2"], cx) - }); + let events_2 = thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello 2"], cx) + }) + .unwrap(); cx.run_until_parked(); fake_model.send_last_completion_stream_text_chunk("Hey 2!"); fake_model @@ -1005,9 +1041,11 @@ async fn test_subsequent_successful_sends_dont_cancel(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); - let events_1 = thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello 1"], cx) - }); + let events_1 = thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello 1"], cx) + }) + .unwrap(); cx.run_until_parked(); fake_model.send_last_completion_stream_text_chunk("Hey 1!"); fake_model @@ -1015,9 +1053,11 @@ async fn test_subsequent_successful_sends_dont_cancel(cx: &mut TestAppContext) { fake_model.end_last_completion_stream(); let events_1 = events_1.collect::>().await; - let events_2 = thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello 2"], cx) - }); + let events_2 = thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello 2"], cx) + }) + .unwrap(); cx.run_until_parked(); fake_model.send_last_completion_stream_text_chunk("Hey 2!"); fake_model @@ -1034,9 +1074,11 @@ async fn test_refusal(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); - let events = thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello"], cx) - }); + let events = thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello"], cx) + }) + .unwrap(); cx.run_until_parked(); thread.read_with(cx, |thread, _| { assert_eq!( @@ -1082,9 +1124,11 @@ async fn test_truncate(cx: &mut TestAppContext) { let fake_model = model.as_fake(); let message_id = UserMessageId::new(); - thread.update(cx, |thread, cx| { - thread.send(message_id.clone(), ["Hello"], cx) - }); + thread + .update(cx, |thread, cx| { + thread.send(message_id.clone(), ["Hello"], cx) + }) + .unwrap(); cx.run_until_parked(); thread.read_with(cx, |thread, _| { assert_eq!( @@ -1123,9 +1167,11 @@ async fn test_truncate(cx: &mut TestAppContext) { }); // Ensure we can still send a new message after truncation. - thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hi"], cx) - }); + thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hi"], cx) + }) + .unwrap(); thread.update(cx, |thread, _cx| { assert_eq!( thread.to_markdown(), @@ -1291,9 +1337,11 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) { thread.update(cx, |thread, _cx| thread.add_tool(ThinkingTool)); let fake_model = model.as_fake(); - let mut events = thread.update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Think"], cx) - }); + let mut events = thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Think"], cx) + }) + .unwrap(); cx.run_until_parked(); // Simulate streaming partial input. @@ -1506,7 +1554,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { context_server_registry, action_log, templates, - model.clone(), + Some(model.clone()), cx, ) }); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index d8b6286f60..c4181a1f42 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -469,7 +469,7 @@ pub struct Thread { profile_id: AgentProfileId, project_context: Rc>, templates: Arc, - model: Arc, + model: Option>, project: Entity, action_log: Entity, } @@ -481,7 +481,7 @@ impl Thread { context_server_registry: Entity, action_log: Entity, templates: Arc, - model: Arc, + model: Option>, cx: &mut Context, ) -> Self { let profile_id = AgentSettings::get_global(cx).default_profile.clone(); @@ -512,12 +512,12 @@ impl Thread { &self.action_log } - pub fn model(&self) -> &Arc { - &self.model + pub fn model(&self) -> Option<&Arc> { + self.model.as_ref() } pub fn set_model(&mut self, model: Arc) { - self.model = model; + self.model = Some(model); } pub fn completion_mode(&self) -> CompletionMode { @@ -575,6 +575,7 @@ impl Thread { &mut self, cx: &mut Context, ) -> Result>> { + anyhow::ensure!(self.model.is_some(), "Model not set"); anyhow::ensure!( self.tool_use_limit_reached, "can only resume after tool use limit is reached" @@ -584,7 +585,7 @@ impl Thread { cx.notify(); log::info!("Total messages in thread: {}", self.messages.len()); - Ok(self.run_turn(cx)) + self.run_turn(cx) } /// Sending a message results in the model streaming a response, which could include tool calls. @@ -595,11 +596,13 @@ impl Thread { id: UserMessageId, content: impl IntoIterator, cx: &mut Context, - ) -> mpsc::UnboundedReceiver> + ) -> Result>> where T: Into, { - log::info!("Thread::send called with model: {:?}", self.model.name()); + let model = self.model().context("No language model configured")?; + + log::info!("Thread::send called with model: {:?}", model.name()); self.advance_prompt_id(); let content = content.into_iter().map(Into::into).collect::>(); @@ -616,10 +619,10 @@ impl Thread { fn run_turn( &mut self, cx: &mut Context, - ) -> mpsc::UnboundedReceiver> { + ) -> Result>> { self.cancel(); - let model = self.model.clone(); + let model = self.model.clone().context("No language model configured")?; let (events_tx, events_rx) = mpsc::unbounded::>(); let event_stream = AgentResponseEventStream(events_tx); let message_ix = self.messages.len().saturating_sub(1); @@ -637,7 +640,7 @@ impl Thread { ); let request = this.update(cx, |this, cx| { this.build_completion_request(completion_intent, cx) - })?; + })??; log::info!("Calling model.stream_completion"); let mut events = model.stream_completion(request, cx).await?; @@ -729,7 +732,7 @@ impl Thread { .ok(); }), }); - events_rx + Ok(events_rx) } pub fn build_system_message(&self) -> LanguageModelRequestMessage { @@ -917,7 +920,7 @@ impl Thread { status: Some(acp::ToolCallStatus::InProgress), ..Default::default() }); - let supports_images = self.model.supports_images(); + let supports_images = self.model().map_or(false, |model| model.supports_images()); let tool_result = tool.run(tool_use.input, tool_event_stream, cx); log::info!("Running tool {}", tool_use.name); Some(cx.foreground_executor().spawn(async move { @@ -1005,7 +1008,9 @@ impl Thread { &self, completion_intent: CompletionIntent, cx: &mut App, - ) -> LanguageModelRequest { + ) -> Result { + let model = self.model().context("No language model configured")?; + log::debug!("Building completion request"); log::debug!("Completion intent: {:?}", completion_intent); log::debug!("Completion mode: {:?}", self.completion_mode); @@ -1021,9 +1026,7 @@ impl Thread { Some(LanguageModelRequestTool { name: tool_name, description: tool.description().to_string(), - input_schema: tool - .input_schema(self.model.tool_input_format()) - .log_err()?, + input_schema: tool.input_schema(model.tool_input_format()).log_err()?, }) }) .collect() @@ -1042,20 +1045,22 @@ impl Thread { tools, tool_choice: None, stop: Vec::new(), - temperature: AgentSettings::temperature_for_model(self.model(), cx), + temperature: AgentSettings::temperature_for_model(&model, cx), thinking_allowed: true, }; log::debug!("Completion request built successfully"); - request + Ok(request) } fn tools<'a>(&'a self, cx: &'a App) -> Result>> { + let model = self.model().context("No language model configured")?; + let profile = AgentSettings::get_global(cx) .profiles .get(&self.profile_id) .context("profile not found")?; - let provider_id = self.model.provider_id(); + let provider_id = model.provider_id(); Ok(self .tools diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 4b4f98daec..c55e503d76 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -237,11 +237,17 @@ impl AgentTool for EditFileTool { }); } - let request = self.thread.update(cx, |thread, cx| { - thread.build_completion_request(CompletionIntent::ToolResults, cx) - }); + let Some(request) = self.thread.update(cx, |thread, cx| { + thread + .build_completion_request(CompletionIntent::ToolResults, cx) + .ok() + }) else { + return Task::ready(Err(anyhow!("Failed to build completion request"))); + }; let thread = self.thread.read(cx); - let model = thread.model().clone(); + let Some(model) = thread.model().cloned() else { + return Task::ready(Err(anyhow!("No language model configured"))); + }; let action_log = thread.action_log().clone(); let authorize = self.authorize(&input, &event_stream, cx); @@ -520,7 +526,7 @@ mod tests { context_server_registry, action_log, Templates::new(), - model, + Some(model), cx, ) }); @@ -717,7 +723,7 @@ mod tests { context_server_registry, action_log.clone(), Templates::new(), - model.clone(), + Some(model.clone()), cx, ) }); @@ -853,7 +859,7 @@ mod tests { context_server_registry, action_log.clone(), Templates::new(), - model.clone(), + Some(model.clone()), cx, ) }); @@ -979,7 +985,7 @@ mod tests { context_server_registry, action_log.clone(), Templates::new(), - model.clone(), + Some(model.clone()), cx, ) }); @@ -1116,7 +1122,7 @@ mod tests { context_server_registry, action_log.clone(), Templates::new(), - model.clone(), + Some(model.clone()), cx, ) }); @@ -1226,7 +1232,7 @@ mod tests { context_server_registry.clone(), action_log.clone(), Templates::new(), - model.clone(), + Some(model.clone()), cx, ) }); @@ -1307,7 +1313,7 @@ mod tests { context_server_registry.clone(), action_log.clone(), Templates::new(), - model.clone(), + Some(model.clone()), cx, ) }); @@ -1391,7 +1397,7 @@ mod tests { context_server_registry.clone(), action_log.clone(), Templates::new(), - model.clone(), + Some(model.clone()), cx, ) }); @@ -1472,7 +1478,7 @@ mod tests { context_server_registry, action_log.clone(), Templates::new(), - model.clone(), + Some(model.clone()), cx, ) }); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 7c1f3cf4ae..f011d72d3c 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -94,7 +94,9 @@ impl ProfileProvider for Entity { } fn profiles_supported(&self, cx: &App) -> bool { - self.read(cx).model().supports_tools() + self.read(cx) + .model() + .map_or(false, |model| model.supports_tools()) } } @@ -2475,7 +2477,10 @@ impl AcpThreadView { fn render_burn_mode_toggle(&self, cx: &mut Context) -> Option { let thread = self.as_native_thread(cx)?.read(cx); - if !thread.model().supports_burn_mode() { + if thread + .model() + .map_or(true, |model| !model.supports_burn_mode()) + { return None; } @@ -3219,7 +3224,10 @@ impl AcpThreadView { cx: &mut Context, ) -> Option { let thread = self.as_native_thread(cx)?; - let supports_burn_mode = thread.read(cx).model().supports_burn_mode(); + let supports_burn_mode = thread + .read(cx) + .model() + .map_or(false, |model| model.supports_burn_mode()); let focus_handle = self.focus_handle(cx); From 5591fc810e8c5cf31463bac2127cc89008c0599b Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 18 Aug 2025 12:22:00 +0200 Subject: [PATCH 426/693] agent: Restore last used agent session on startup (#36401) Release Notes: - N/A --- crates/agent2/src/agent.rs | 17 ++++--- crates/agent2/src/thread.rs | 5 ++- crates/agent_ui/src/agent_panel.rs | 71 ++++++++++++++++++------------ crates/agent_ui/src/agent_ui.rs | 2 +- 4 files changed, 60 insertions(+), 35 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 0ad90753e1..af740d9901 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -425,13 +425,18 @@ impl NativeAgent { cx: &mut Context, ) { self.models.refresh_list(cx); + + let default_model = LanguageModelRegistry::read_global(cx) + .default_model() + .map(|m| m.model.clone()); + for session in self.sessions.values_mut() { - session.thread.update(cx, |thread, _| { - if let Some(model) = thread.model() { - let model_id = LanguageModels::model_id(model); - if let Some(model) = self.models.model_from_id(&model_id) { - thread.set_model(model.clone()); - } + session.thread.update(cx, |thread, cx| { + if thread.model().is_none() + && let Some(model) = default_model.clone() + { + thread.set_model(model); + cx.notify(); } }); } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index c4181a1f42..429832010b 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -622,7 +622,10 @@ impl Thread { ) -> Result>> { self.cancel(); - let model = self.model.clone().context("No language model configured")?; + let model = self + .model() + .cloned() + .context("No language model configured")?; let (events_tx, events_rx) = mpsc::unbounded::>(); let event_stream = AgentResponseEventStream(events_tx); let message_ix = self.messages.len().saturating_sub(1); diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index b01bf39728..391d6aa6e9 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -573,6 +573,7 @@ impl AgentPanel { panel.width = serialized_panel.width.map(|w| w.round()); if let Some(selected_agent) = serialized_panel.selected_agent { panel.selected_agent = selected_agent; + panel.new_agent_thread(selected_agent, window, cx); } cx.notify(); }); @@ -1631,16 +1632,53 @@ impl AgentPanel { menu } - pub fn set_selected_agent(&mut self, agent: AgentType, cx: &mut Context) { + pub fn set_selected_agent( + &mut self, + agent: AgentType, + window: &mut Window, + cx: &mut Context, + ) { if self.selected_agent != agent { self.selected_agent = agent; self.serialize(cx); + self.new_agent_thread(agent, window, cx); } } pub fn selected_agent(&self) -> AgentType { self.selected_agent } + + pub fn new_agent_thread( + &mut self, + agent: AgentType, + window: &mut Window, + cx: &mut Context, + ) { + match agent { + AgentType::Zed => { + window.dispatch_action( + NewThread { + from_thread_id: None, + } + .boxed_clone(), + cx, + ); + } + AgentType::TextThread => { + window.dispatch_action(NewTextThread.boxed_clone(), cx); + } + AgentType::NativeAgent => { + self.new_external_thread(Some(crate::ExternalAgent::NativeAgent), window, cx) + } + AgentType::Gemini => { + self.new_external_thread(Some(crate::ExternalAgent::Gemini), window, cx) + } + AgentType::ClaudeCode => { + self.new_external_thread(Some(crate::ExternalAgent::ClaudeCode), window, cx) + } + } + } } impl Focusable for AgentPanel { @@ -2221,16 +2259,13 @@ impl AgentPanel { panel.update(cx, |panel, cx| { panel.set_selected_agent( AgentType::Zed, + window, cx, ); }); } }); } - window.dispatch_action( - NewThread::default().boxed_clone(), - cx, - ); } }), ) @@ -2250,13 +2285,13 @@ impl AgentPanel { panel.update(cx, |panel, cx| { panel.set_selected_agent( AgentType::TextThread, + window, cx, ); }); } }); } - window.dispatch_action(NewTextThread.boxed_clone(), cx); } }), ) @@ -2275,19 +2310,13 @@ impl AgentPanel { panel.update(cx, |panel, cx| { panel.set_selected_agent( AgentType::NativeAgent, + window, cx, ); }); } }); } - window.dispatch_action( - NewExternalAgentThread { - agent: Some(crate::ExternalAgent::NativeAgent), - } - .boxed_clone(), - cx, - ); } }), ) @@ -2308,19 +2337,13 @@ impl AgentPanel { panel.update(cx, |panel, cx| { panel.set_selected_agent( AgentType::Gemini, + window, cx, ); }); } }); } - window.dispatch_action( - NewExternalAgentThread { - agent: Some(crate::ExternalAgent::Gemini), - } - .boxed_clone(), - cx, - ); } }), ) @@ -2339,19 +2362,13 @@ impl AgentPanel { panel.update(cx, |panel, cx| { panel.set_selected_agent( AgentType::ClaudeCode, + window, cx, ); }); } }); } - window.dispatch_action( - NewExternalAgentThread { - agent: Some(crate::ExternalAgent::ClaudeCode), - } - .boxed_clone(), - cx, - ); } }), ); diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index f25b576886..ce1c2203bf 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -146,7 +146,7 @@ pub struct NewExternalAgentThread { agent: Option, } -#[derive(Default, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] enum ExternalAgent { #[default] From 472f1a8cc21a4754c12f9a0e125a3242e3c9937a Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 18 Aug 2025 12:40:39 +0200 Subject: [PATCH 427/693] editor: Add right click context menu to buffer headers (#36398) This adds a context menu to buffer headers mimicking that of pane tabs, notably being able to copy the relative and absolute paths of the buffer as well as opening a terminal in the parent. Confusingly prior to this right clicking a buffer header used to open the context menu of the underlying editor. Release Notes: - Added context menu for buffer titles --- crates/editor/src/element.rs | 418 ++++++++++++++++++++++------------- 1 file changed, 260 insertions(+), 158 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 5edfd7df30..c15ff3e509 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -40,14 +40,15 @@ use git::{ }; use gpui::{ Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle, - Bounds, ClickEvent, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges, - Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, - HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, Keystroke, Length, - ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, - MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle, ScrollWheelEvent, - ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, TextRun, - TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop, - linear_gradient, outline, point, px, quad, relative, size, solid_background, transparent_black, + Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, + DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, + GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, + Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, + MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle, + ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, + TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, + linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background, + transparent_black, }; use itertools::Itertools; use language::language_settings::{ @@ -60,7 +61,7 @@ use multi_buffer::{ }; use project::{ - ProjectPath, + Entry, ProjectPath, debugger::breakpoint_store::{Breakpoint, BreakpointSessionState}, project_settings::{GitGutterSetting, GitHunkStyleSetting, ProjectSettings}, }; @@ -80,11 +81,17 @@ use std::{ use sum_tree::Bias; use text::{BufferId, SelectionGoal}; use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor}; -use ui::{ButtonLike, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*}; +use ui::{ + ButtonLike, ContextMenu, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*, + right_click_menu, +}; use unicode_segmentation::UnicodeSegmentation; use util::post_inc; use util::{RangeExt, ResultExt, debug_panic}; -use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt}; +use workspace::{ + CollaboratorId, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, item::Item, + notifications::NotifyTaskExt, +}; /// Determines what kinds of highlights should be applied to a lines background. #[derive(Clone, Copy, Default)] @@ -3556,7 +3563,7 @@ impl EditorElement { jump_data: JumpData, window: &mut Window, cx: &mut App, - ) -> Div { + ) -> impl IntoElement { let editor = self.editor.read(cx); let file_status = editor .buffer @@ -3577,126 +3584,125 @@ impl EditorElement { .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) .unwrap_or_default(); let can_open_excerpts = Editor::can_open_excerpts_in_file(for_excerpt.buffer.file()); - let path = for_excerpt.buffer.resolve_file_path(cx, include_root); - let filename = path + let relative_path = for_excerpt.buffer.resolve_file_path(cx, include_root); + let filename = relative_path .as_ref() .and_then(|path| Some(path.file_name()?.to_string_lossy().to_string())); - let parent_path = path.as_ref().and_then(|path| { + let parent_path = relative_path.as_ref().and_then(|path| { Some(path.parent()?.to_string_lossy().to_string() + std::path::MAIN_SEPARATOR_STR) }); let focus_handle = editor.focus_handle(cx); let colors = cx.theme().colors(); - div() - .p_1() - .w_full() - .h(FILE_HEADER_HEIGHT as f32 * window.line_height()) - .child( - h_flex() - .size_full() - .gap_2() - .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) - .pl_0p5() - .pr_5() - .rounded_sm() - .when(is_sticky, |el| el.shadow_md()) - .border_1() - .map(|div| { - let border_color = if is_selected - && is_folded - && focus_handle.contains_focused(window, cx) - { - colors.border_focused - } else { - colors.border - }; - div.border_color(border_color) - }) - .bg(colors.editor_subheader_background) - .hover(|style| style.bg(colors.element_hover)) - .map(|header| { - let editor = self.editor.clone(); - let buffer_id = for_excerpt.buffer_id; - let toggle_chevron_icon = - FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path); - header.child( - div() - .hover(|style| style.bg(colors.element_selected)) - .rounded_xs() - .child( - ButtonLike::new("toggle-buffer-fold") - .style(ui::ButtonStyle::Transparent) - .height(px(28.).into()) - .width(px(28.)) - .children(toggle_chevron_icon) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::with_meta_in( - "Toggle Excerpt Fold", - Some(&ToggleFold), - "Alt+click to toggle all", - &focus_handle, - window, - cx, - ) - } - }) - .on_click(move |event, window, cx| { - if event.modifiers().alt { - // Alt+click toggles all buffers - editor.update(cx, |editor, cx| { - editor.toggle_fold_all( - &ToggleFoldAll, + let header = + div() + .p_1() + .w_full() + .h(FILE_HEADER_HEIGHT as f32 * window.line_height()) + .child( + h_flex() + .size_full() + .gap_2() + .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) + .pl_0p5() + .pr_5() + .rounded_sm() + .when(is_sticky, |el| el.shadow_md()) + .border_1() + .map(|div| { + let border_color = if is_selected + && is_folded + && focus_handle.contains_focused(window, cx) + { + colors.border_focused + } else { + colors.border + }; + div.border_color(border_color) + }) + .bg(colors.editor_subheader_background) + .hover(|style| style.bg(colors.element_hover)) + .map(|header| { + let editor = self.editor.clone(); + let buffer_id = for_excerpt.buffer_id; + let toggle_chevron_icon = + FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path); + header.child( + div() + .hover(|style| style.bg(colors.element_selected)) + .rounded_xs() + .child( + ButtonLike::new("toggle-buffer-fold") + .style(ui::ButtonStyle::Transparent) + .height(px(28.).into()) + .width(px(28.)) + .children(toggle_chevron_icon) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::with_meta_in( + "Toggle Excerpt Fold", + Some(&ToggleFold), + "Alt+click to toggle all", + &focus_handle, window, cx, - ); - }); - } else { - // Regular click toggles single buffer - if is_folded { + ) + } + }) + .on_click(move |event, window, cx| { + if event.modifiers().alt { + // Alt+click toggles all buffers editor.update(cx, |editor, cx| { - editor.unfold_buffer(buffer_id, cx); + editor.toggle_fold_all( + &ToggleFoldAll, + window, + cx, + ); }); } else { - editor.update(cx, |editor, cx| { - editor.fold_buffer(buffer_id, cx); - }); + // Regular click toggles single buffer + if is_folded { + editor.update(cx, |editor, cx| { + editor.unfold_buffer(buffer_id, cx); + }); + } else { + editor.update(cx, |editor, cx| { + editor.fold_buffer(buffer_id, cx); + }); + } } - } - }), - ), + }), + ), + ) + }) + .children( + editor + .addons + .values() + .filter_map(|addon| { + addon.render_buffer_header_controls(for_excerpt, window, cx) + }) + .take(1), ) - }) - .children( - editor - .addons - .values() - .filter_map(|addon| { - addon.render_buffer_header_controls(for_excerpt, window, cx) - }) - .take(1), - ) - .child( - h_flex() - .cursor_pointer() - .id("path header block") - .size_full() - .justify_between() - .overflow_hidden() - .child( - h_flex() - .gap_2() - .child( - Label::new( - filename - .map(SharedString::from) - .unwrap_or_else(|| "untitled".into()), - ) - .single_line() - .when_some( - file_status, - |el, status| { + .child( + h_flex() + .cursor_pointer() + .id("path header block") + .size_full() + .justify_between() + .overflow_hidden() + .child( + h_flex() + .gap_2() + .child( + Label::new( + filename + .map(SharedString::from) + .unwrap_or_else(|| "untitled".into()), + ) + .single_line() + .when_some(file_status, |el, status| { el.color(if status.is_conflicted() { Color::Conflict } else if status.is_modified() { @@ -3707,49 +3713,145 @@ impl EditorElement { Color::Created }) .when(status.is_deleted(), |el| el.strikethrough()) - }, - ), - ) - .when_some(parent_path, |then, path| { - then.child(div().child(path).text_color( - if file_status.is_some_and(FileStatus::is_deleted) { - colors.text_disabled - } else { - colors.text_muted - }, - )) + }), + ) + .when_some(parent_path, |then, path| { + then.child(div().child(path).text_color( + if file_status.is_some_and(FileStatus::is_deleted) { + colors.text_disabled + } else { + colors.text_muted + }, + )) + }), + ) + .when( + can_open_excerpts && is_selected && relative_path.is_some(), + |el| { + el.child( + h_flex() + .id("jump-to-file-button") + .gap_2p5() + .child(Label::new("Jump To File")) + .children( + KeyBinding::for_action_in( + &OpenExcerpts, + &focus_handle, + window, + cx, + ) + .map(|binding| binding.into_any_element()), + ), + ) + }, + ) + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .on_click(window.listener_for(&self.editor, { + move |editor, e: &ClickEvent, window, cx| { + editor.open_excerpts_common( + Some(jump_data.clone()), + e.modifiers().secondary(), + window, + cx, + ); + } + })), + ), + ); + + let file = for_excerpt.buffer.file().cloned(); + let editor = self.editor.clone(); + right_click_menu("buffer-header-context-menu") + .trigger(move |_, _, _| header) + .menu(move |window, cx| { + let menu_context = focus_handle.clone(); + let editor = editor.clone(); + let file = file.clone(); + ContextMenu::build(window, cx, move |mut menu, window, cx| { + if let Some(file) = file + && let Some(project) = editor.read(cx).project() + && let Some(worktree) = + project.read(cx).worktree_for_id(file.worktree_id(cx), cx) + { + let relative_path = file.path(); + let entry_for_path = worktree.read(cx).entry_for_path(relative_path); + let abs_path = entry_for_path.and_then(|e| e.canonical_path.as_deref()); + let has_relative_path = + worktree.read(cx).root_entry().is_some_and(Entry::is_dir); + + let parent_abs_path = + abs_path.and_then(|abs_path| Some(abs_path.parent()?.to_path_buf())); + let relative_path = has_relative_path + .then_some(relative_path) + .map(ToOwned::to_owned); + + let visible_in_project_panel = + relative_path.is_some() && worktree.read(cx).is_visible(); + let reveal_in_project_panel = entry_for_path + .filter(|_| visible_in_project_panel) + .map(|entry| entry.id); + menu = menu + .when_some(abs_path.map(ToOwned::to_owned), |menu, abs_path| { + menu.entry( + "Copy Path", + Some(Box::new(zed_actions::workspace::CopyPath)), + window.handler_for(&editor, move |_, _, cx| { + cx.write_to_clipboard(ClipboardItem::new_string( + abs_path.to_string_lossy().to_string(), + )); }), - ) - .when(can_open_excerpts && is_selected && path.is_some(), |el| { - el.child( - h_flex() - .id("jump-to-file-button") - .gap_2p5() - .child(Label::new("Jump To File")) - .children( - KeyBinding::for_action_in( - &OpenExcerpts, - &focus_handle, - window, - cx, - ) - .map(|binding| binding.into_any_element()), - ), ) }) - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .on_click(window.listener_for(&self.editor, { - move |editor, e: &ClickEvent, window, cx| { - editor.open_excerpts_common( - Some(jump_data.clone()), - e.modifiers().secondary(), - window, - cx, - ); - } - })), - ), - ) + .when_some(relative_path, |menu, relative_path| { + menu.entry( + "Copy Relative Path", + Some(Box::new(zed_actions::workspace::CopyRelativePath)), + window.handler_for(&editor, move |_, _, cx| { + cx.write_to_clipboard(ClipboardItem::new_string( + relative_path.to_string_lossy().to_string(), + )); + }), + ) + }) + .when( + reveal_in_project_panel.is_some() || parent_abs_path.is_some(), + |menu| menu.separator(), + ) + .when_some(reveal_in_project_panel, |menu, entry_id| { + menu.entry( + "Reveal In Project Panel", + Some(Box::new(RevealInProjectPanel::default())), + window.handler_for(&editor, move |editor, _, cx| { + if let Some(project) = &mut editor.project { + project.update(cx, |_, cx| { + cx.emit(project::Event::RevealInProjectPanel( + entry_id, + )) + }); + } + }), + ) + }) + .when_some(parent_abs_path, |menu, parent_abs_path| { + menu.entry( + "Open in Terminal", + Some(Box::new(OpenInTerminal)), + window.handler_for(&editor, move |_, window, cx| { + window.dispatch_action( + OpenTerminal { + working_directory: parent_abs_path.clone(), + } + .boxed_clone(), + cx, + ); + }), + ) + }); + } + + menu.context(menu_context) + }) + }) } fn render_blocks( From d83f341d273394140c6052dcc404fe8b332570e1 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 18 Aug 2025 13:45:51 +0300 Subject: [PATCH 428/693] Silence "minidump endpoint not set" errors' backtraces in the logs (#36404) bad Release Notes: - N/A --- crates/zed/src/reliability.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index c27f4cb0a8..0a54572f6b 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -550,7 +550,8 @@ async fn upload_previous_panics( pub async fn upload_previous_minidumps(http: Arc) -> anyhow::Result<()> { let Some(minidump_endpoint) = MINIDUMP_ENDPOINT.as_ref() else { - return Err(anyhow::anyhow!("Minidump endpoint not set")); + log::warn!("Minidump endpoint not set"); + return Ok(()); }; let mut children = smol::fs::read_dir(paths::logs_dir()).await?; From 843336970ad65fcb12c73f45f8d23823ed1167d5 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Mon, 18 Aug 2025 13:01:32 +0200 Subject: [PATCH 429/693] keymap_ui: Ensure keybind with empty arguments can be saved (#36393) Follow up to #36278 to ensure this bug is actually fixed. Also fixes this on two layers and adds a test for the lower layer, as we cannot properly test it in the UI. Furthermore, this improves the error message to show some more context and ensures the status toast is actually only shown when the keybind was successfully updated: Before, we would show the success toast whilst also showing an error in the editor. Lastly, this also fixes some issues with the status toast (and animations) where no status toast or no animation would show in certain scenarios. Release Notes: - N/A --- crates/settings/src/keymap_file.rs | 24 +++++++- crates/settings_ui/src/keybindings.rs | 84 +++++++++++++-------------- crates/ui/src/styles/animation.rs | 27 +++++---- crates/workspace/src/toast_layer.rs | 32 +++++----- 4 files changed, 93 insertions(+), 74 deletions(-) diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 7802671fec..fb03662290 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -928,14 +928,14 @@ impl<'a> KeybindUpdateTarget<'a> { } let action_name: Value = self.action_name.into(); let value = match self.action_arguments { - Some(args) => { + Some(args) if !args.is_empty() => { let args = serde_json::from_str::(args) .context("Failed to parse action arguments as JSON")?; serde_json::json!([action_name, args]) } - None => action_name, + _ => action_name, }; - return Ok(value); + Ok(value) } fn keystrokes_unparsed(&self) -> String { @@ -1084,6 +1084,24 @@ mod tests { .unindent(), ); + check_keymap_update( + "[]", + KeybindUpdateOperation::add(KeybindUpdateTarget { + keystrokes: &parse_keystrokes("ctrl-a"), + action_name: "zed::SomeAction", + context: None, + action_arguments: Some(""), + }), + r#"[ + { + "bindings": { + "ctrl-a": "zed::SomeAction" + } + } + ]"# + .unindent(), + ); + check_keymap_update( r#"[ { diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index b4e871c617..5181d86a78 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -2177,11 +2177,11 @@ impl KeybindingEditorModal { let action_arguments = self .action_arguments_editor .as_ref() - .map(|editor| editor.read(cx).editor.read(cx).text(cx)); + .map(|arguments_editor| arguments_editor.read(cx).editor.read(cx).text(cx)) + .filter(|args| !args.is_empty()); let value = action_arguments .as_ref() - .filter(|args| !args.is_empty()) .map(|args| { serde_json::from_str(args).context("Failed to parse action arguments as JSON") }) @@ -2289,29 +2289,11 @@ impl KeybindingEditorModal { let create = self.creating; - let status_toast = StatusToast::new( - format!( - "Saved edits to the {} action.", - &self.editing_keybind.action().humanized_name - ), - cx, - move |this, _cx| { - this.icon(ToastIcon::new(IconName::Check).color(Color::Success)) - .dismiss_button(true) - // .action("Undo", f) todo: wire the undo functionality - }, - ); - - self.workspace - .update(cx, |workspace, cx| { - workspace.toggle_status_toast(status_toast, cx); - }) - .log_err(); - cx.spawn(async move |this, cx| { let action_name = existing_keybind.action().name; + let humanized_action_name = existing_keybind.action().humanized_name.clone(); - if let Err(err) = save_keybinding_update( + match save_keybinding_update( create, existing_keybind, &action_mapping, @@ -2321,25 +2303,43 @@ impl KeybindingEditorModal { ) .await { - this.update(cx, |this, cx| { - this.set_error(InputError::error(err), cx); - }) - .log_err(); - } else { - this.update(cx, |this, cx| { - this.keymap_editor.update(cx, |keymap, cx| { - keymap.previous_edit = Some(PreviousEdit::Keybinding { - action_mapping, - action_name, - fallback: keymap - .table_interaction_state - .read(cx) - .get_scrollbar_offset(Axis::Vertical), - }) - }); - cx.emit(DismissEvent); - }) - .ok(); + Ok(_) => { + this.update(cx, |this, cx| { + this.keymap_editor.update(cx, |keymap, cx| { + keymap.previous_edit = Some(PreviousEdit::Keybinding { + action_mapping, + action_name, + fallback: keymap + .table_interaction_state + .read(cx) + .get_scrollbar_offset(Axis::Vertical), + }); + let status_toast = StatusToast::new( + format!("Saved edits to the {} action.", humanized_action_name), + cx, + move |this, _cx| { + this.icon(ToastIcon::new(IconName::Check).color(Color::Success)) + .dismiss_button(true) + // .action("Undo", f) todo: wire the undo functionality + }, + ); + + this.workspace + .update(cx, |workspace, cx| { + workspace.toggle_status_toast(status_toast, cx); + }) + .log_err(); + }); + cx.emit(DismissEvent); + }) + .ok(); + } + Err(err) => { + this.update(cx, |this, cx| { + this.set_error(InputError::error(err), cx); + }) + .log_err(); + } } }) .detach(); @@ -3011,7 +3011,7 @@ async fn save_keybinding_update( let updated_keymap_contents = settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) - .context("Failed to update keybinding")?; + .map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?; fs.write( paths::keymap_file().as_path(), updated_keymap_contents.as_bytes(), diff --git a/crates/ui/src/styles/animation.rs b/crates/ui/src/styles/animation.rs index ee5352d454..acea834548 100644 --- a/crates/ui/src/styles/animation.rs +++ b/crates/ui/src/styles/animation.rs @@ -31,7 +31,7 @@ pub enum AnimationDirection { FromTop, } -pub trait DefaultAnimations: Styled + Sized { +pub trait DefaultAnimations: Styled + Sized + Element { fn animate_in( self, animation_type: AnimationDirection, @@ -44,8 +44,13 @@ pub trait DefaultAnimations: Styled + Sized { AnimationDirection::FromTop => "animate_from_top", }; + let animation_id = self.id().map_or_else( + || ElementId::from(animation_name), + |id| (id, animation_name).into(), + ); + self.with_animation( - animation_name, + animation_id, gpui::Animation::new(AnimationDuration::Fast.into()).with_easing(ease_out_quint()), move |mut this, delta| { let start_opacity = 0.4; @@ -91,7 +96,7 @@ pub trait DefaultAnimations: Styled + Sized { } } -impl DefaultAnimations for E {} +impl DefaultAnimations for E {} // Don't use this directly, it only exists to show animation previews #[derive(RegisterComponent)] @@ -132,7 +137,7 @@ impl Component for Animation { .left(px(offset)) .rounded_md() .bg(gpui::red()) - .animate_in(AnimationDirection::FromBottom, false), + .animate_in_from_bottom(false), ) .into_any_element(), ), @@ -151,7 +156,7 @@ impl Component for Animation { .left(px(offset)) .rounded_md() .bg(gpui::blue()) - .animate_in(AnimationDirection::FromTop, false), + .animate_in_from_top(false), ) .into_any_element(), ), @@ -170,7 +175,7 @@ impl Component for Animation { .top(px(offset)) .rounded_md() .bg(gpui::green()) - .animate_in(AnimationDirection::FromLeft, false), + .animate_in_from_left(false), ) .into_any_element(), ), @@ -189,7 +194,7 @@ impl Component for Animation { .top(px(offset)) .rounded_md() .bg(gpui::yellow()) - .animate_in(AnimationDirection::FromRight, false), + .animate_in_from_right(false), ) .into_any_element(), ), @@ -214,7 +219,7 @@ impl Component for Animation { .left(px(offset)) .rounded_md() .bg(gpui::red()) - .animate_in(AnimationDirection::FromBottom, true), + .animate_in_from_bottom(true), ) .into_any_element(), ), @@ -233,7 +238,7 @@ impl Component for Animation { .left(px(offset)) .rounded_md() .bg(gpui::blue()) - .animate_in(AnimationDirection::FromTop, true), + .animate_in_from_top(true), ) .into_any_element(), ), @@ -252,7 +257,7 @@ impl Component for Animation { .top(px(offset)) .rounded_md() .bg(gpui::green()) - .animate_in(AnimationDirection::FromLeft, true), + .animate_in_from_left(true), ) .into_any_element(), ), @@ -271,7 +276,7 @@ impl Component for Animation { .top(px(offset)) .rounded_md() .bg(gpui::yellow()) - .animate_in(AnimationDirection::FromRight, true), + .animate_in_from_right(true), ) .into_any_element(), ), diff --git a/crates/workspace/src/toast_layer.rs b/crates/workspace/src/toast_layer.rs index 28be3e7e47..5157945548 100644 --- a/crates/workspace/src/toast_layer.rs +++ b/crates/workspace/src/toast_layer.rs @@ -3,7 +3,7 @@ use std::{ time::{Duration, Instant}, }; -use gpui::{AnyView, DismissEvent, Entity, FocusHandle, ManagedView, Subscription, Task}; +use gpui::{AnyView, DismissEvent, Entity, EntityId, FocusHandle, ManagedView, Subscription, Task}; use ui::{animation::DefaultAnimations, prelude::*}; use zed_actions::toast; @@ -76,6 +76,7 @@ impl ToastViewHandle for Entity { } pub struct ActiveToast { + id: EntityId, toast: Box, action: Option, _subscriptions: [Subscription; 1], @@ -113,9 +114,9 @@ impl ToastLayer { V: ToastView, { if let Some(active_toast) = &self.active_toast { - let is_close = active_toast.toast.view().downcast::().is_ok(); - let did_close = self.hide_toast(cx); - if is_close || !did_close { + let show_new = active_toast.id != new_toast.entity_id(); + self.hide_toast(cx); + if !show_new { return; } } @@ -130,11 +131,12 @@ impl ToastLayer { let focus_handle = cx.focus_handle(); self.active_toast = Some(ActiveToast { - toast: Box::new(new_toast.clone()), - action, _subscriptions: [cx.subscribe(&new_toast, |this, _, _: &DismissEvent, cx| { this.hide_toast(cx); })], + id: new_toast.entity_id(), + toast: Box::new(new_toast), + action, focus_handle, }); @@ -143,11 +145,9 @@ impl ToastLayer { cx.notify(); } - pub fn hide_toast(&mut self, cx: &mut Context) -> bool { + pub fn hide_toast(&mut self, cx: &mut Context) { self.active_toast.take(); cx.notify(); - - true } pub fn active_toast(&self) -> Option> @@ -218,11 +218,10 @@ impl Render for ToastLayer { let Some(active_toast) = &self.active_toast else { return div(); }; - let handle = cx.weak_entity(); div().absolute().size_full().bottom_0().left_0().child( v_flex() - .id("toast-layer-container") + .id(("toast-layer-container", active_toast.id)) .absolute() .w_full() .bottom(px(0.)) @@ -234,17 +233,14 @@ impl Render for ToastLayer { h_flex() .id("active-toast-container") .occlude() - .on_hover(move |hover_start, _window, cx| { - let Some(this) = handle.upgrade() else { - return; - }; + .on_hover(cx.listener(|this, hover_start, _window, cx| { if *hover_start { - this.update(cx, |this, _| this.pause_dismiss_timer()); + this.pause_dismiss_timer(); } else { - this.update(cx, |this, cx| this.restart_dismiss_timer(cx)); + this.restart_dismiss_timer(cx); } cx.stop_propagation(); - }) + })) .on_click(|_, _, cx| { cx.stop_propagation(); }) From d5711d44a5cda4bd9f76849ca3e4904a1aed7c75 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Mon, 18 Aug 2025 16:32:01 +0530 Subject: [PATCH 430/693] editor: Fix panic in inlay hint while padding (#36405) Closes #36247 Fix a panic when padding inlay hints if the last character is a multi-byte character. Regressed in https://github.com/zed-industries/zed/pull/35786. Release Notes: - Fixed a crash that could occur when an inlay hint ended with `...`. --- crates/editor/src/display_map/inlay_map.rs | 25 +++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index b296b3e62a..76148af587 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -48,7 +48,7 @@ pub struct Inlay { impl Inlay { pub fn hint(id: usize, position: Anchor, hint: &project::InlayHint) -> Self { let mut text = hint.text(); - if hint.padding_right && text.chars_at(text.len().saturating_sub(1)).next() != Some(' ') { + if hint.padding_right && text.reversed_chars_at(text.len()).next() != Some(' ') { text.push(" "); } if hint.padding_left && text.chars_at(0).next() != Some(' ') { @@ -1305,6 +1305,29 @@ mod tests { ); } + #[gpui::test] + fn test_inlay_hint_padding_with_multibyte_chars() { + assert_eq!( + Inlay::hint( + 0, + Anchor::min(), + &InlayHint { + label: InlayHintLabel::String("🎨".to_string()), + position: text::Anchor::default(), + padding_left: true, + padding_right: true, + tooltip: None, + kind: None, + resolve_state: ResolveState::Resolved, + }, + ) + .text + .to_string(), + " 🎨 ", + "Should pad single emoji correctly" + ); + } + #[gpui::test] fn test_basic_inlays(cx: &mut App) { let buffer = MultiBuffer::build_simple("abcdefghi", cx); From 57198f33c46f79a8520049ad9de69498e449d533 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 18 Aug 2025 13:12:17 +0200 Subject: [PATCH 431/693] agent2: Show Zed AI onboarding (#36406) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 7 +++++-- crates/agent_ui/src/agent_panel.rs | 11 +++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index f011d72d3c..271d9e5d4c 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2444,12 +2444,15 @@ impl AcpThreadView { .into_any() } - fn as_native_connection(&self, cx: &App) -> Option> { + pub(crate) fn as_native_connection( + &self, + cx: &App, + ) -> Option> { let acp_thread = self.thread()?.read(cx); acp_thread.connection().clone().downcast() } - fn as_native_thread(&self, cx: &App) -> Option> { + pub(crate) fn as_native_thread(&self, cx: &App) -> Option> { let acp_thread = self.thread()?.read(cx); self.as_native_connection(cx)? .thread(acp_thread.session_id(), cx) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 391d6aa6e9..4cb231f357 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -2619,7 +2619,13 @@ impl AgentPanel { } match &self.active_view { - ActiveView::Thread { .. } | ActiveView::TextThread { .. } => { + ActiveView::History | ActiveView::Configuration => false, + ActiveView::ExternalAgentThread { thread_view, .. } + if thread_view.read(cx).as_native_thread(cx).is_none() => + { + false + } + _ => { let history_is_empty = self .history_store .update(cx, |store, cx| store.recent_entries(1, cx).is_empty()); @@ -2634,9 +2640,6 @@ impl AgentPanel { history_is_empty || !has_configured_non_zed_providers } - ActiveView::ExternalAgentThread { .. } - | ActiveView::History - | ActiveView::Configuration => false, } } From 5225844c9edc5a43c426b04cb05dc59289ba085b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 18 Aug 2025 13:48:21 +0200 Subject: [PATCH 432/693] lsp: Always report innermost workspace_folders (#36407) Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/language/src/language.rs | 16 ---------------- crates/languages/src/python.rs | 13 +------------ crates/project/src/lsp_store.rs | 7 ++----- 3 files changed, 3 insertions(+), 33 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index f299dee345..6fa31da860 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -283,15 +283,6 @@ impl CachedLspAdapter { } } -/// Determines what gets sent out as a workspace folders content -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum WorkspaceFoldersContent { - /// Send out a single entry with the root of the workspace. - WorktreeRoot, - /// Send out a list of subproject roots. - SubprojectRoots, -} - /// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application // e.g. to display a notification or fetch data from the web. #[async_trait] @@ -580,13 +571,6 @@ pub trait LspAdapter: 'static + Send + Sync { Ok(original) } - /// Determines whether a language server supports workspace folders. - /// - /// And does not trip over itself in the process. - fn workspace_folders_content(&self) -> WorkspaceFoldersContent { - WorkspaceFoldersContent::SubprojectRoots - } - /// Method only implemented by the default JSON language server adapter. /// Used to provide dynamic reloading of the JSON schemas used to /// provide autocompletion and diagnostics in Zed setting and keybind diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index b61ad2d36c..222e3f1946 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -4,13 +4,13 @@ use async_trait::async_trait; use collections::HashMap; use gpui::{App, Task}; use gpui::{AsyncApp, SharedString}; +use language::Toolchain; use language::ToolchainList; use language::ToolchainLister; use language::language_settings::language_settings; use language::{ContextLocation, LanguageToolchainStore}; use language::{ContextProvider, LspAdapter, LspAdapterDelegate}; use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery}; -use language::{Toolchain, WorkspaceFoldersContent}; use lsp::LanguageServerBinary; use lsp::LanguageServerName; use node_runtime::{NodeRuntime, VersionStrategy}; @@ -389,10 +389,6 @@ impl LspAdapter for PythonLspAdapter { user_settings }) } - - fn workspace_folders_content(&self) -> WorkspaceFoldersContent { - WorkspaceFoldersContent::WorktreeRoot - } } async fn get_cached_server_binary( @@ -1257,9 +1253,6 @@ impl LspAdapter for PyLspAdapter { user_settings }) } - fn workspace_folders_content(&self) -> WorkspaceFoldersContent { - WorkspaceFoldersContent::WorktreeRoot - } } pub(crate) struct BasedPyrightLspAdapter { @@ -1577,10 +1570,6 @@ impl LspAdapter for BasedPyrightLspAdapter { user_settings }) } - - fn workspace_folders_content(&self) -> WorkspaceFoldersContent { - WorkspaceFoldersContent::WorktreeRoot - } } #[cfg(test)] diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 8ea41a100b..802b304e94 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -57,7 +57,7 @@ use language::{ DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, Diff, File as _, Language, LanguageName, LanguageRegistry, LocalFile, LspAdapter, LspAdapterDelegate, ManifestDelegate, ManifestName, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Toolchain, Transaction, - Unclipped, WorkspaceFoldersContent, + Unclipped, language_settings::{ FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings, }, @@ -344,10 +344,7 @@ impl LocalLspStore { binary, &root_path, code_action_kinds, - Some(pending_workspace_folders).filter(|_| { - adapter.adapter.workspace_folders_content() - == WorkspaceFoldersContent::SubprojectRoots - }), + Some(pending_workspace_folders), cx, ) } From 1add1d042dc59d82ed9089bd792e5192e71b5e0f Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Mon, 18 Aug 2025 14:21:33 +0200 Subject: [PATCH 433/693] Add option to disable auto indentation (#36259) Closes https://github.com/zed-industries/zed/issues/11780 While auto indentation is generally nice to have, there are cases where it is currently just not good enough for some languages (e.g. Haskell) or users just straight up do not want their editor to auto indent for them. Hence, this PR adds the possibilty to disable auto indentation for either all language or on a per-language basis. Manual invocation via the `editor: auto indent` action will continue to work. Also takes a similar approach as https://github.com/zed-industries/zed/pull/31569 to ensure performance is fine for larger multicursor edits. Release Notes: - Added the possibility to configure auto indentation for all languages and per language. Add `"auto_indent": false"` to your settings or desired language to disable the feature. --- assets/settings/default.json | 2 + crates/editor/src/editor_tests.rs | 210 +++++++++++++++++++++++ crates/language/src/buffer.rs | 45 +++-- crates/language/src/language_settings.rs | 7 + 4 files changed, 250 insertions(+), 14 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 6a8b034268..72e4dcbf4f 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -286,6 +286,8 @@ // bracket, brace, single or double quote characters. // For example, when you select text and type (, Zed will surround the text with (). "use_auto_surround": true, + /// Whether indentation should be adjusted based on the context whilst typing. + "auto_indent": true, // Whether indentation of pasted content should be adjusted based on the context. "auto_indent_on_paste": true, // Controls how the editor handles the autoclosed characters. diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index ef2bdc5da3..f97dcd712c 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -8214,6 +8214,216 @@ async fn test_autoindent(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_autoindent_disabled(cx: &mut TestAppContext) { + init_test(cx, |settings| settings.defaults.auto_indent = Some(false)); + + let language = Arc::new( + Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: false, + surround: false, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: false, + surround: false, + newline: true, + }, + ], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + ) + .with_indents_query( + r#" + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap(), + ); + + let text = "fn a() {}"; + + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); + editor + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .await; + + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([5..5, 8..8, 9..9]) + }); + editor.newline(&Newline, window, cx); + assert_eq!( + editor.text(cx), + indoc!( + " + fn a( + + ) { + + } + " + ) + ); + assert_eq!( + editor.selections.ranges(cx), + &[ + Point::new(1, 0)..Point::new(1, 0), + Point::new(3, 0)..Point::new(3, 0), + Point::new(5, 0)..Point::new(5, 0) + ] + ); + }); +} + +#[gpui::test] +async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) { + init_test(cx, |settings| { + settings.defaults.auto_indent = Some(true); + settings.languages.0.insert( + "python".into(), + LanguageSettingsContent { + auto_indent: Some(false), + ..Default::default() + }, + ); + }); + + let mut cx = EditorTestContext::new(cx).await; + + let injected_language = Arc::new( + Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: false, + surround: false, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: true, + surround: false, + newline: true, + }, + ], + ..Default::default() + }, + name: "python".into(), + ..Default::default() + }, + Some(tree_sitter_python::LANGUAGE.into()), + ) + .with_indents_query( + r#" + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap(), + ); + + let language = Arc::new( + Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: false, + surround: false, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: true, + surround: false, + newline: true, + }, + ], + ..Default::default() + }, + name: LanguageName::new("rust"), + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + ) + .with_indents_query( + r#" + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap() + .with_injection_query( + r#" + (macro_invocation + macro: (identifier) @_macro_name + (token_tree) @injection.content + (#set! injection.language "python")) + "#, + ) + .unwrap(), + ); + + cx.language_registry().add(injected_language); + cx.language_registry().add(language.clone()); + + cx.update_buffer(|buffer, cx| { + buffer.set_language(Some(language), cx); + }); + + cx.set_state(&r#"struct A {ˇ}"#); + + cx.update_editor(|editor, window, cx| { + editor.newline(&Default::default(), window, cx); + }); + + cx.assert_editor_state(indoc!( + "struct A { + ˇ + }" + )); + + cx.set_state(&r#"select_biased!(ˇ)"#); + + cx.update_editor(|editor, window, cx| { + editor.newline(&Default::default(), window, cx); + editor.handle_input("def ", window, cx); + editor.handle_input("(", window, cx); + editor.newline(&Default::default(), window, cx); + editor.handle_input("a", window, cx); + }); + + cx.assert_editor_state(indoc!( + "select_biased!( + def ( + aˇ + ) + )" + )); +} + #[gpui::test] async fn test_autoindent_selections(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 2080513f49..e2bcc938fa 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2271,13 +2271,11 @@ impl Buffer { } let new_text = new_text.into(); if !new_text.is_empty() || !range.is_empty() { - if let Some((prev_range, prev_text)) = edits.last_mut() { - if prev_range.end >= range.start { - prev_range.end = cmp::max(prev_range.end, range.end); - *prev_text = format!("{prev_text}{new_text}").into(); - } else { - edits.push((range, new_text)); - } + if let Some((prev_range, prev_text)) = edits.last_mut() + && prev_range.end >= range.start + { + prev_range.end = cmp::max(prev_range.end, range.end); + *prev_text = format!("{prev_text}{new_text}").into(); } else { edits.push((range, new_text)); } @@ -2297,10 +2295,27 @@ impl Buffer { if let Some((before_edit, mode)) = autoindent_request { let mut delta = 0isize; - let entries = edits + let mut previous_setting = None; + let entries: Vec<_> = edits .into_iter() .enumerate() .zip(&edit_operation.as_edit().unwrap().new_text) + .filter(|((_, (range, _)), _)| { + let language = before_edit.language_at(range.start); + let language_id = language.map(|l| l.id()); + if let Some((cached_language_id, auto_indent)) = previous_setting + && cached_language_id == language_id + { + auto_indent + } else { + // The auto-indent setting is not present in editorconfigs, hence + // we can avoid passing the file here. + let auto_indent = + language_settings(language.map(|l| l.name()), None, cx).auto_indent; + previous_setting = Some((language_id, auto_indent)); + auto_indent + } + }) .map(|((ix, (range, _)), new_text)| { let new_text_length = new_text.len(); let old_start = range.start.to_point(&before_edit); @@ -2374,12 +2389,14 @@ impl Buffer { }) .collect(); - self.autoindent_requests.push(Arc::new(AutoindentRequest { - before_edit, - entries, - is_block_mode: matches!(mode, AutoindentMode::Block { .. }), - ignore_empty_lines: false, - })); + if !entries.is_empty() { + self.autoindent_requests.push(Arc::new(AutoindentRequest { + before_edit, + entries, + is_block_mode: matches!(mode, AutoindentMode::Block { .. }), + ignore_empty_lines: false, + })); + } } self.end_transaction(cx); diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 1aae0b2f7e..29669ba2a0 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -133,6 +133,8 @@ pub struct LanguageSettings { /// Whether to use additional LSP queries to format (and amend) the code after /// every "trigger" symbol input, defined by LSP server capabilities. pub use_on_type_format: bool, + /// Whether indentation should be adjusted based on the context whilst typing. + pub auto_indent: bool, /// Whether indentation of pasted content should be adjusted based on the context. pub auto_indent_on_paste: bool, /// Controls how the editor handles the autoclosed characters. @@ -561,6 +563,10 @@ pub struct LanguageSettingsContent { /// /// Default: true pub linked_edits: Option, + /// Whether indentation should be adjusted based on the context whilst typing. + /// + /// Default: true + pub auto_indent: Option, /// Whether indentation of pasted content should be adjusted based on the context. /// /// Default: true @@ -1517,6 +1523,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent merge(&mut settings.use_autoclose, src.use_autoclose); merge(&mut settings.use_auto_surround, src.use_auto_surround); merge(&mut settings.use_on_type_format, src.use_on_type_format); + merge(&mut settings.auto_indent, src.auto_indent); merge(&mut settings.auto_indent_on_paste, src.auto_indent_on_paste); merge( &mut settings.always_treat_brackets_as_autoclosed, From 58f7006898d2f67f038f6305f08a9fb990f7a771 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Mon, 18 Aug 2025 14:35:54 +0200 Subject: [PATCH 434/693] editor: Add tests to ensure no horizontal scrolling is possible in soft wrap mode (#36411) Prior to https://github.com/zed-industries/zed/pull/34564 as well as https://github.com/zed-industries/zed/pull/26893, we would have cases where editors would be scrollable even if `soft_wrap` was set to `editor_width`. This has regressed and improved quite a few times back and forth. The issue was only within the editor code, the code for the wrap map was functioning and tested properly. Hence, this PR adds two tests to the editor rendering code in an effort to ensure that we maintain the current correct behavior. Release Notes: - N/A --- crates/editor/src/element.rs | 65 ++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c15ff3e509..e56ac45fab 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -10187,6 +10187,71 @@ mod tests { use std::num::NonZeroU32; use util::test::sample_text; + #[gpui::test] + async fn test_soft_wrap_editor_width_auto_height_editor(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let window = cx.add_window(|window, cx| { + let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx); + let mut editor = Editor::new( + EditorMode::AutoHeight { + min_lines: 1, + max_lines: None, + }, + buffer, + None, + window, + cx, + ); + editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); + editor + }); + let cx = &mut VisualTestContext::from_window(*window, cx); + let editor = window.root(cx).unwrap(); + let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone()); + + for x in 1..=100 { + let (_, state) = cx.draw( + Default::default(), + size(px(200. + 0.13 * x as f32), px(500.)), + |_, _| EditorElement::new(&editor, style.clone()), + ); + + assert!( + state.position_map.scroll_max.x == 0., + "Soft wrapped editor should have no horizontal scrolling!" + ); + } + } + + #[gpui::test] + async fn test_soft_wrap_editor_width_full_editor(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let window = cx.add_window(|window, cx| { + let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx); + let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx); + editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); + editor + }); + let cx = &mut VisualTestContext::from_window(*window, cx); + let editor = window.root(cx).unwrap(); + let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone()); + + for x in 1..=100 { + let (_, state) = cx.draw( + Default::default(), + size(px(200. + 0.13 * x as f32), px(500.)), + |_, _| EditorElement::new(&editor, style.clone()), + ); + + assert!( + state.position_map.scroll_max.x == 0., + "Soft wrapped editor should have no horizontal scrolling!" + ); + } + } + #[gpui::test] fn test_shape_line_numbers(cx: &mut TestAppContext) { init_test(cx, |_| {}); From e2db434920cc22e9905e84a50ffec2f0f01da67b Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Mon, 18 Aug 2025 09:50:29 -0300 Subject: [PATCH 435/693] acp thread view: Floating editing message controls (#36283) Prevents layout shift when focusing the editor Release Notes: - N/A --------- Co-authored-by: Danilo Leal --- crates/agent_ui/src/acp/entry_view_state.rs | 1 + crates/agent_ui/src/acp/message_editor.rs | 5 +- crates/agent_ui/src/acp/thread_view.rs | 235 +++++++++----------- 3 files changed, 105 insertions(+), 136 deletions(-) diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index e99d1f6323..c7ab2353f1 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -67,6 +67,7 @@ impl EntryViewState { self.project.clone(), self.thread_store.clone(), self.text_thread_store.clone(), + "Edit message - @ to include context", editor::EditorMode::AutoHeight { min_lines: 1, max_lines: None, diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 12766ef458..299f0c30be 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -71,6 +71,7 @@ impl MessageEditor { project: Entity, thread_store: Entity, text_thread_store: Entity, + placeholder: impl Into>, mode: EditorMode, window: &mut Window, cx: &mut Context, @@ -94,7 +95,7 @@ impl MessageEditor { let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let mut editor = Editor::new(mode, buffer, None, window, cx); - editor.set_placeholder_text("Message the agent - @ to include files", cx); + editor.set_placeholder_text(placeholder, cx); editor.set_show_indent_guides(false, cx); editor.set_soft_wrap(); editor.set_use_modal_editing(true); @@ -1276,6 +1277,7 @@ mod tests { project.clone(), thread_store.clone(), text_thread_store.clone(), + "Test", EditorMode::AutoHeight { min_lines: 1, max_lines: None, @@ -1473,6 +1475,7 @@ mod tests { project.clone(), thread_store.clone(), text_thread_store.clone(), + "Test", EditorMode::AutoHeight { max_lines: None, min_lines: 1, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 271d9e5d4c..3be6e355a9 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -159,6 +159,7 @@ impl AcpThreadView { project.clone(), thread_store.clone(), text_thread_store.clone(), + "Message the agent - @ to include context", editor::EditorMode::AutoHeight { min_lines: MIN_EDITOR_LINES, max_lines: Some(MAX_EDITOR_LINES), @@ -426,7 +427,9 @@ impl AcpThreadView { match event { MessageEditorEvent::Send => self.send(window, cx), MessageEditorEvent::Cancel => self.cancel_generation(cx), - MessageEditorEvent::Focus => {} + MessageEditorEvent::Focus => { + self.cancel_editing(&Default::default(), window, cx); + } } } @@ -742,44 +745,98 @@ impl AcpThreadView { cx: &Context, ) -> AnyElement { let primary = match &entry { - AgentThreadEntry::UserMessage(message) => div() - .id(("user_message", entry_ix)) - .py_4() - .px_2() - .children(message.id.clone().and_then(|message_id| { - message.checkpoint.as_ref()?.show.then(|| { - Button::new("restore-checkpoint", "Restore Checkpoint") - .icon(IconName::Undo) - .icon_size(IconSize::XSmall) - .icon_position(IconPosition::Start) - .label_size(LabelSize::XSmall) - .on_click(cx.listener(move |this, _, _window, cx| { - this.rewind(&message_id, cx); - })) - }) - })) - .child( - v_flex() - .p_3() - .gap_1p5() - .rounded_lg() - .shadow_md() - .bg(cx.theme().colors().editor_background) - .border_1() - .border_color(cx.theme().colors().border) - .text_xs() - .children( - self.entry_view_state - .read(cx) - .entry(entry_ix) - .and_then(|entry| entry.message_editor()) - .map(|editor| { - self.render_sent_message_editor(entry_ix, editor, cx) - .into_any_element() - }), - ), - ) - .into_any(), + AgentThreadEntry::UserMessage(message) => { + let Some(editor) = self + .entry_view_state + .read(cx) + .entry(entry_ix) + .and_then(|entry| entry.message_editor()) + .cloned() + else { + return Empty.into_any_element(); + }; + + let editing = self.editing_message == Some(entry_ix); + let editor_focus = editor.focus_handle(cx).is_focused(window); + let focus_border = cx.theme().colors().border_focused; + + div() + .id(("user_message", entry_ix)) + .py_4() + .px_2() + .children(message.id.clone().and_then(|message_id| { + message.checkpoint.as_ref()?.show.then(|| { + Button::new("restore-checkpoint", "Restore Checkpoint") + .icon(IconName::Undo) + .icon_size(IconSize::XSmall) + .icon_position(IconPosition::Start) + .label_size(LabelSize::XSmall) + .on_click(cx.listener(move |this, _, _window, cx| { + this.rewind(&message_id, cx); + })) + }) + })) + .child( + div() + .relative() + .child( + div() + .p_3() + .rounded_lg() + .shadow_md() + .bg(cx.theme().colors().editor_background) + .border_1() + .when(editing && !editor_focus, |this| this.border_dashed()) + .border_color(cx.theme().colors().border) + .map(|this|{ + if editor_focus { + this.border_color(focus_border) + } else { + this.hover(|s| s.border_color(focus_border.opacity(0.8))) + } + }) + .text_xs() + .child(editor.clone().into_any_element()), + ) + .when(editor_focus, |this| + this.child( + h_flex() + .absolute() + .top_neg_3p5() + .right_3() + .gap_1() + .rounded_sm() + .border_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().editor_background) + .overflow_hidden() + .child( + IconButton::new("cancel", IconName::Close) + .icon_color(Color::Error) + .icon_size(IconSize::XSmall) + .on_click(cx.listener(Self::cancel_editing)) + ) + .child( + IconButton::new("regenerate", IconName::Return) + .icon_color(Color::Muted) + .icon_size(IconSize::XSmall) + .tooltip(Tooltip::text( + "Editing will restart the thread from this point." + )) + .on_click(cx.listener({ + let editor = editor.clone(); + move |this, _, window, cx| { + this.regenerate( + entry_ix, &editor, window, cx, + ); + } + })), + ) + ) + ), + ) + .into_any() + } AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => { let style = default_markdown_style(false, window, cx); let message_body = v_flex() @@ -854,20 +911,12 @@ impl AcpThreadView { if let Some(editing_index) = self.editing_message.as_ref() && *editing_index < entry_ix { - let backdrop = div() - .id(("backdrop", entry_ix)) - .size_full() - .absolute() - .inset_0() - .bg(cx.theme().colors().panel_background) - .opacity(0.8) - .block_mouse_except_scroll() - .on_click(cx.listener(Self::cancel_editing)); - div() - .relative() .child(primary) - .child(backdrop) + .opacity(0.2) + .block_mouse_except_scroll() + .id("overlay") + .on_click(cx.listener(Self::cancel_editing)) .into_any_element() } else { primary @@ -2512,90 +2561,6 @@ impl AcpThreadView { ) } - fn render_sent_message_editor( - &self, - entry_ix: usize, - editor: &Entity, - cx: &Context, - ) -> Div { - v_flex().w_full().gap_2().child(editor.clone()).when( - self.editing_message == Some(entry_ix), - |el| { - el.child( - h_flex() - .gap_1() - .child( - Icon::new(IconName::Warning) - .color(Color::Warning) - .size(IconSize::XSmall), - ) - .child( - Label::new("Editing will restart the thread from this point.") - .color(Color::Muted) - .size(LabelSize::XSmall), - ) - .child(self.render_sent_message_editor_buttons(entry_ix, editor, cx)), - ) - }, - ) - } - - fn render_sent_message_editor_buttons( - &self, - entry_ix: usize, - editor: &Entity, - cx: &Context, - ) -> Div { - h_flex() - .gap_0p5() - .flex_1() - .justify_end() - .child( - IconButton::new("cancel-edit-message", IconName::Close) - .shape(ui::IconButtonShape::Square) - .icon_color(Color::Error) - .icon_size(IconSize::Small) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Cancel Edit", - &menu::Cancel, - &focus_handle, - window, - cx, - ) - } - }) - .on_click(cx.listener(Self::cancel_editing)), - ) - .child( - IconButton::new("confirm-edit-message", IconName::Return) - .disabled(editor.read(cx).is_empty(cx)) - .shape(ui::IconButtonShape::Square) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Regenerate", - &menu::Confirm, - &focus_handle, - window, - cx, - ) - } - }) - .on_click(cx.listener({ - let editor = editor.clone(); - move |this, _, window, cx| { - this.regenerate(entry_ix, &editor, window, cx); - } - })), - ) - } - fn render_send_button(&self, cx: &mut Context) -> AnyElement { if self.thread().map_or(true, |thread| { thread.read(cx).status() == ThreadStatus::Idle From 6f56ac50fecf360a2983adc88fc1e164ac8f9dcc Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Mon, 18 Aug 2025 18:45:52 +0530 Subject: [PATCH 436/693] Use upstream version of yawc (#36412) As this was merged in upstream: https://github.com/infinitefield/yawc/pull/16. It's safe to point yawc to upstream instead of fork. cc @maxdeviant Release Notes: - N/A --- Cargo.lock | 5 +++-- Cargo.toml | 4 +--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a4f8c521a1..98f10eff41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20196,8 +20196,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yawc" -version = "0.2.4" -source = "git+https://github.com/deviant-forks/yawc?rev=1899688f3e69ace4545aceb97b2a13881cf26142#1899688f3e69ace4545aceb97b2a13881cf26142" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a5d82922135b4ae73a079a4ffb5501e9aadb4d785b8c660eaa0a8b899028c5" dependencies = [ "base64 0.22.1", "bytes 1.10.1", diff --git a/Cargo.toml b/Cargo.toml index 14691cf8a4..83d6da5cd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -659,9 +659,7 @@ which = "6.0.0" windows-core = "0.61" wit-component = "0.221" workspace-hack = "0.1.0" -# We can switch back to the published version once https://github.com/infinitefield/yawc/pull/16 is merged and a new -# version is released. -yawc = { git = "https://github.com/deviant-forks/yawc", rev = "1899688f3e69ace4545aceb97b2a13881cf26142" } +yawc = "0.2.5" zstd = "0.11" [workspace.dependencies.windows] From 6bf666958c7a2cf931ae22690c1affa069c5bbd1 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 18 Aug 2025 10:49:17 -0300 Subject: [PATCH 437/693] agent2: Allow to interrupt and send a new message (#36185) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 77 +++++++++++++++++++------- 1 file changed, 58 insertions(+), 19 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 3be6e355a9..2fc30e3007 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -474,12 +474,41 @@ impl AcpThreadView { } fn send(&mut self, window: &mut Window, cx: &mut Context) { + if let Some(thread) = self.thread() { + if thread.read(cx).status() != ThreadStatus::Idle { + self.stop_current_and_send_new_message(window, cx); + return; + } + } + let contents = self .message_editor .update(cx, |message_editor, cx| message_editor.contents(window, cx)); self.send_impl(contents, window, cx) } + fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context) { + let Some(thread) = self.thread().cloned() else { + return; + }; + + let cancelled = thread.update(cx, |thread, cx| thread.cancel(cx)); + + let contents = self + .message_editor + .update(cx, |message_editor, cx| message_editor.contents(window, cx)); + + cx.spawn_in(window, async move |this, cx| { + cancelled.await; + + this.update_in(cx, |this, window, cx| { + this.send_impl(contents, window, cx); + }) + .ok(); + }) + .detach(); + } + fn send_impl( &mut self, contents: Task>>, @@ -2562,25 +2591,12 @@ impl AcpThreadView { } fn render_send_button(&self, cx: &mut Context) -> AnyElement { - if self.thread().map_or(true, |thread| { - thread.read(cx).status() == ThreadStatus::Idle - }) { - let is_editor_empty = self.message_editor.read(cx).is_empty(cx); - IconButton::new("send-message", IconName::Send) - .icon_color(Color::Accent) - .style(ButtonStyle::Filled) - .disabled(self.thread().is_none() || is_editor_empty) - .when(!is_editor_empty, |button| { - button.tooltip(move |window, cx| Tooltip::for_action("Send", &Chat, window, cx)) - }) - .when(is_editor_empty, |button| { - button.tooltip(Tooltip::text("Type a message to submit")) - }) - .on_click(cx.listener(|this, _, window, cx| { - this.send(window, cx); - })) - .into_any_element() - } else { + let is_editor_empty = self.message_editor.read(cx).is_empty(cx); + let is_generating = self.thread().map_or(false, |thread| { + thread.read(cx).status() != ThreadStatus::Idle + }); + + if is_generating && is_editor_empty { IconButton::new("stop-generation", IconName::Stop) .icon_color(Color::Error) .style(ButtonStyle::Tinted(ui::TintColor::Error)) @@ -2589,6 +2605,29 @@ impl AcpThreadView { }) .on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx))) .into_any_element() + } else { + let send_btn_tooltip = if is_editor_empty && !is_generating { + "Type to Send" + } else if is_generating { + "Stop and Send Message" + } else { + "Send" + }; + + IconButton::new("send-message", IconName::Send) + .style(ButtonStyle::Filled) + .map(|this| { + if is_editor_empty && !is_generating { + this.disabled(true).icon_color(Color::Muted) + } else { + this.icon_color(Color::Accent) + } + }) + .tooltip(move |window, cx| Tooltip::for_action(send_btn_tooltip, &Chat, window, cx)) + .on_click(cx.listener(|this, _, window, cx| { + this.send(window, cx); + })) + .into_any_element() } } From db31fa67f301b0b22f029e455ddad86b28b28371 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Mon, 18 Aug 2025 11:37:28 -0300 Subject: [PATCH 438/693] acp: Stay in edit mode when current completion ends (#36413) When a turn ends and the checkpoint is updated, `AcpThread` emits `EntryUpdated` with the index of the user message. This was causing the message editor to be recreated and, therefore, lose focus. Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 1 + crates/acp_thread/src/connection.rs | 121 ++++++++++++++------ crates/agent_ui/src/acp/entry_view_state.rs | 66 ++++++----- crates/agent_ui/src/acp/thread_view.rs | 96 +++++++++++++++- 4 files changed, 214 insertions(+), 70 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index fb31265326..3762c553cc 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -670,6 +670,7 @@ pub struct AcpThread { session_id: acp::SessionId, } +#[derive(Debug)] pub enum AcpThreadEvent { NewEntry, EntryUpdated(usize), diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 7497d2309f..48310f07ce 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -186,7 +186,7 @@ mod test_support { use std::sync::Arc; use collections::HashMap; - use futures::future::try_join_all; + use futures::{channel::oneshot, future::try_join_all}; use gpui::{AppContext as _, WeakEntity}; use parking_lot::Mutex; @@ -194,11 +194,16 @@ mod test_support { #[derive(Clone, Default)] pub struct StubAgentConnection { - sessions: Arc>>>, + sessions: Arc>>, permission_requests: HashMap>, next_prompt_updates: Arc>>, } + struct Session { + thread: WeakEntity, + response_tx: Option>, + } + impl StubAgentConnection { pub fn new() -> Self { Self { @@ -226,15 +231,33 @@ mod test_support { update: acp::SessionUpdate, cx: &mut App, ) { + assert!( + self.next_prompt_updates.lock().is_empty(), + "Use either send_update or set_next_prompt_updates" + ); + self.sessions .lock() .get(&session_id) .unwrap() + .thread .update(cx, |thread, cx| { thread.handle_session_update(update.clone(), cx).unwrap(); }) .unwrap(); } + + pub fn end_turn(&self, session_id: acp::SessionId) { + self.sessions + .lock() + .get_mut(&session_id) + .unwrap() + .response_tx + .take() + .expect("No pending turn") + .send(()) + .unwrap(); + } } impl AgentConnection for StubAgentConnection { @@ -251,7 +274,13 @@ mod test_support { let session_id = acp::SessionId(self.sessions.lock().len().to_string().into()); let thread = cx.new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx)); - self.sessions.lock().insert(session_id, thread.downgrade()); + self.sessions.lock().insert( + session_id, + Session { + thread: thread.downgrade(), + response_tx: None, + }, + ); Task::ready(Ok(thread)) } @@ -269,43 +298,59 @@ mod test_support { params: acp::PromptRequest, cx: &mut App, ) -> Task> { - let sessions = self.sessions.lock(); - let thread = sessions.get(¶ms.session_id).unwrap(); + let mut sessions = self.sessions.lock(); + let Session { + thread, + response_tx, + } = sessions.get_mut(¶ms.session_id).unwrap(); let mut tasks = vec![]; - for update in self.next_prompt_updates.lock().drain(..) { - let thread = thread.clone(); - let update = update.clone(); - let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) = &update - && let Some(options) = self.permission_requests.get(&tool_call.id) - { - Some((tool_call.clone(), options.clone())) - } else { - None - }; - let task = cx.spawn(async move |cx| { - if let Some((tool_call, options)) = permission_request { - let permission = thread.update(cx, |thread, cx| { - thread.request_tool_call_authorization( - tool_call.clone().into(), - options.clone(), - cx, - ) - })?; - permission?.await?; - } - thread.update(cx, |thread, cx| { - thread.handle_session_update(update.clone(), cx).unwrap(); - })?; - anyhow::Ok(()) - }); - tasks.push(task); - } - cx.spawn(async move |_| { - try_join_all(tasks).await?; - Ok(acp::PromptResponse { - stop_reason: acp::StopReason::EndTurn, + if self.next_prompt_updates.lock().is_empty() { + let (tx, rx) = oneshot::channel(); + response_tx.replace(tx); + cx.spawn(async move |_| { + rx.await?; + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) }) - }) + } else { + for update in self.next_prompt_updates.lock().drain(..) { + let thread = thread.clone(); + let update = update.clone(); + let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) = + &update + && let Some(options) = self.permission_requests.get(&tool_call.id) + { + Some((tool_call.clone(), options.clone())) + } else { + None + }; + let task = cx.spawn(async move |cx| { + if let Some((tool_call, options)) = permission_request { + let permission = thread.update(cx, |thread, cx| { + thread.request_tool_call_authorization( + tool_call.clone().into(), + options.clone(), + cx, + ) + })?; + permission?.await?; + } + thread.update(cx, |thread, cx| { + thread.handle_session_update(update.clone(), cx).unwrap(); + })?; + anyhow::Ok(()) + }); + tasks.push(task); + } + + cx.spawn(async move |_| { + try_join_all(tasks).await?; + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) + }) + } } fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) { diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index c7ab2353f1..18ef1ce2ab 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -5,8 +5,8 @@ use agent::{TextThreadStore, ThreadStore}; use collections::HashMap; use editor::{Editor, EditorMode, MinimapVisibility}; use gpui::{ - AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, TextStyleRefinement, - WeakEntity, Window, + AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, + TextStyleRefinement, WeakEntity, Window, }; use language::language_settings::SoftWrap; use project::Project; @@ -61,34 +61,44 @@ impl EntryViewState { AgentThreadEntry::UserMessage(message) => { let has_id = message.id.is_some(); let chunks = message.chunks.clone(); - let message_editor = cx.new(|cx| { - let mut editor = MessageEditor::new( - self.workspace.clone(), - self.project.clone(), - self.thread_store.clone(), - self.text_thread_store.clone(), - "Edit message - @ to include context", - editor::EditorMode::AutoHeight { - min_lines: 1, - max_lines: None, - }, - window, - cx, - ); - if !has_id { - editor.set_read_only(true, cx); + if let Some(Entry::UserMessage(editor)) = self.entries.get_mut(index) { + if !editor.focus_handle(cx).is_focused(window) { + // Only update if we are not editing. + // If we are, cancelling the edit will set the message to the newest content. + editor.update(cx, |editor, cx| { + editor.set_message(chunks, window, cx); + }); } - editor.set_message(chunks, window, cx); - editor - }); - cx.subscribe(&message_editor, move |_, editor, event, cx| { - cx.emit(EntryViewEvent { - entry_index: index, - view_event: ViewEvent::MessageEditorEvent(editor, *event), + } else { + let message_editor = cx.new(|cx| { + let mut editor = MessageEditor::new( + self.workspace.clone(), + self.project.clone(), + self.thread_store.clone(), + self.text_thread_store.clone(), + "Edit message - @ to include context", + editor::EditorMode::AutoHeight { + min_lines: 1, + max_lines: None, + }, + window, + cx, + ); + if !has_id { + editor.set_read_only(true, cx); + } + editor.set_message(chunks, window, cx); + editor + }); + cx.subscribe(&message_editor, move |_, editor, event, cx| { + cx.emit(EntryViewEvent { + entry_index: index, + view_event: ViewEvent::MessageEditorEvent(editor, *event), + }) }) - }) - .detach(); - self.set_entry(index, Entry::UserMessage(message_editor)); + .detach(); + self.set_entry(index, Entry::UserMessage(message_editor)); + } } AgentThreadEntry::ToolCall(tool_call) => { let terminals = tool_call.terminals().cloned().collect::>(); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 2fc30e3007..4760677fa1 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -3606,7 +3606,7 @@ pub(crate) mod tests { async fn test_drop(cx: &mut TestAppContext) { init_test(cx); - let (thread_view, _cx) = setup_thread_view(StubAgentServer::default(), cx).await; + let (thread_view, _cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; let weak_view = thread_view.downgrade(); drop(thread_view); assert!(!weak_view.is_upgradable()); @@ -3616,7 +3616,7 @@ pub(crate) mod tests { async fn test_notification_for_stop_event(cx: &mut TestAppContext) { init_test(cx); - let (thread_view, cx) = setup_thread_view(StubAgentServer::default(), cx).await; + let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); message_editor.update_in(cx, |editor, window, cx| { @@ -3800,8 +3800,12 @@ pub(crate) mod tests { } impl StubAgentServer { - fn default() -> Self { - Self::new(StubAgentConnection::default()) + fn default_response() -> Self { + let conn = StubAgentConnection::new(); + conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk { + content: "Default response".into(), + }]); + Self::new(conn) } } @@ -4214,4 +4218,88 @@ pub(crate) mod tests { assert_eq!(new_editor.read(cx).text(cx), "Edited message content"); }) } + + #[gpui::test] + async fn test_message_editing_while_generating(cx: &mut TestAppContext) { + init_test(cx); + + let connection = StubAgentConnection::new(); + + let (thread_view, cx) = + setup_thread_view(StubAgentServer::new(connection.clone()), cx).await; + add_to_workspace(thread_view.clone(), cx); + + let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Original message to edit", window, cx); + }); + thread_view.update_in(cx, |thread_view, window, cx| { + thread_view.send(window, cx); + }); + + cx.run_until_parked(); + + let (user_message_editor, session_id) = thread_view.read_with(cx, |view, cx| { + let thread = view.thread().unwrap().read(cx); + assert_eq!(thread.entries().len(), 1); + + let editor = view + .entry_view_state + .read(cx) + .entry(0) + .unwrap() + .message_editor() + .unwrap() + .clone(); + + (editor, thread.session_id().clone()) + }); + + // Focus + cx.focus(&user_message_editor); + + thread_view.read_with(cx, |view, _cx| { + assert_eq!(view.editing_message, Some(0)); + }); + + // Edit + user_message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Edited message content", window, cx); + }); + + thread_view.read_with(cx, |view, _cx| { + assert_eq!(view.editing_message, Some(0)); + }); + + // Finish streaming response + cx.update(|_, cx| { + connection.send_update( + session_id.clone(), + acp::SessionUpdate::AgentMessageChunk { + content: acp::ContentBlock::Text(acp::TextContent { + text: "Response".into(), + annotations: None, + }), + }, + cx, + ); + connection.end_turn(session_id); + }); + + thread_view.read_with(cx, |view, _cx| { + assert_eq!(view.editing_message, Some(0)); + }); + + cx.run_until_parked(); + + // Should still be editing + cx.update(|window, cx| { + assert!(user_message_editor.focus_handle(cx).is_focused(window)); + assert_eq!(thread_view.read(cx).editing_message, Some(0)); + assert_eq!( + user_message_editor.read(cx).text(cx), + "Edited message content" + ); + }); + } } From 9b78c4690208367444699f1e3a58e96437cdecd1 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 18 Aug 2025 16:48:38 +0200 Subject: [PATCH 439/693] python: Use pip provided by our 'base' venv (#36414) Closes #36218 Release Notes: - Debugger: Python debugger installation no longer assumes that pip is available in global Python installation --- crates/dap_adapters/src/python.rs | 58 +++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index a2bd934311..7b90f80fe2 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -24,6 +24,7 @@ use util::{ResultExt, maybe}; #[derive(Default)] pub(crate) struct PythonDebugAdapter { + base_venv_path: OnceCell, String>>, debugpy_whl_base_path: OnceCell, String>>, } @@ -91,14 +92,12 @@ impl PythonDebugAdapter { }) } - async fn fetch_wheel(delegate: &Arc) -> Result, String> { - let system_python = Self::system_python_name(delegate) - .await - .ok_or_else(|| String::from("Could not find a Python installation"))?; - let command: &OsStr = system_python.as_ref(); + async fn fetch_wheel(&self, delegate: &Arc) -> Result, String> { let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME).join("wheels"); std::fs::create_dir_all(&download_dir).map_err(|e| e.to_string())?; - let installation_succeeded = util::command::new_smol_command(command) + let system_python = self.base_venv_path(delegate).await?; + + let installation_succeeded = util::command::new_smol_command(system_python.as_ref()) .args([ "-m", "pip", @@ -114,7 +113,7 @@ impl PythonDebugAdapter { .status .success(); if !installation_succeeded { - return Err("debugpy installation failed".into()); + return Err("debugpy installation failed (could not fetch Debugpy's wheel)".into()); } let wheel_path = std::fs::read_dir(&download_dir) @@ -139,7 +138,7 @@ impl PythonDebugAdapter { Ok(Arc::from(wheel_path.path())) } - async fn maybe_fetch_new_wheel(delegate: &Arc) { + async fn maybe_fetch_new_wheel(&self, delegate: &Arc) { let latest_release = delegate .http_client() .get( @@ -191,7 +190,7 @@ impl PythonDebugAdapter { ) .await .ok()?; - Self::fetch_wheel(delegate).await.ok()?; + self.fetch_wheel(delegate).await.ok()?; } Some(()) }) @@ -204,7 +203,7 @@ impl PythonDebugAdapter { ) -> Result, String> { self.debugpy_whl_base_path .get_or_init(|| async move { - Self::maybe_fetch_new_wheel(delegate).await; + self.maybe_fetch_new_wheel(delegate).await; Ok(Arc::from( debug_adapters_dir() .join(Self::ADAPTER_NAME) @@ -217,6 +216,45 @@ impl PythonDebugAdapter { .clone() } + async fn base_venv_path(&self, delegate: &Arc) -> Result, String> { + self.base_venv_path + .get_or_init(|| async { + let base_python = Self::system_python_name(delegate) + .await + .ok_or_else(|| String::from("Could not find a Python installation"))?; + + let did_succeed = util::command::new_smol_command(base_python) + .args(["-m", "venv", "zed_base_venv"]) + .current_dir( + paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref()), + ) + .spawn() + .map_err(|e| format!("{e:#?}"))? + .status() + .await + .map_err(|e| format!("{e:#?}"))? + .success(); + if !did_succeed { + return Err("Failed to create base virtual environment".into()); + } + + const DIR: &'static str = if cfg!(target_os = "windows") { + "Scripts" + } else { + "bin" + }; + Ok(Arc::from( + paths::debug_adapters_dir() + .join(Self::DEBUG_ADAPTER_NAME.as_ref()) + .join("zed_base_venv") + .join(DIR) + .join("python3") + .as_ref(), + )) + }) + .await + .clone() + } async fn system_python_name(delegate: &Arc) -> Option { const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"]; let mut name = None; From 48fed866e60f1951bd8aa6ccec000670ce839b7f Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Mon, 18 Aug 2025 12:34:27 -0300 Subject: [PATCH 440/693] acp: Have `AcpThread` handle all interrupting (#36417) The view was cancelling the generation, but `AcpThread` already handles that, so we removed that extra code and fixed a bug where an update from the first user message would appear after the second one. Release Notes: - N/A Co-authored-by: Danilo --- crates/acp_thread/src/acp_thread.rs | 22 ++-- crates/acp_thread/src/connection.rs | 27 +++-- crates/agent_ui/src/acp/thread_view.rs | 135 ++++++++++++++++++++++++- 3 files changed, 164 insertions(+), 20 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 3762c553cc..e104c40bf2 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1200,17 +1200,21 @@ impl AcpThread { } else { None }; - self.push_entry( - AgentThreadEntry::UserMessage(UserMessage { - id: message_id.clone(), - content: block, - chunks: message, - checkpoint: None, - }), - cx, - ); self.run_turn(cx, async move |this, cx| { + this.update(cx, |this, cx| { + this.push_entry( + AgentThreadEntry::UserMessage(UserMessage { + id: message_id.clone(), + content: block, + chunks: message, + checkpoint: None, + }), + cx, + ); + }) + .ok(); + let old_checkpoint = git_store .update(cx, |git, cx| git.checkpoint(cx))? .await diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 48310f07ce..a328499bbc 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -201,7 +201,7 @@ mod test_support { struct Session { thread: WeakEntity, - response_tx: Option>, + response_tx: Option>, } impl StubAgentConnection { @@ -242,12 +242,12 @@ mod test_support { .unwrap() .thread .update(cx, |thread, cx| { - thread.handle_session_update(update.clone(), cx).unwrap(); + thread.handle_session_update(update, cx).unwrap(); }) .unwrap(); } - pub fn end_turn(&self, session_id: acp::SessionId) { + pub fn end_turn(&self, session_id: acp::SessionId, stop_reason: acp::StopReason) { self.sessions .lock() .get_mut(&session_id) @@ -255,7 +255,7 @@ mod test_support { .response_tx .take() .expect("No pending turn") - .send(()) + .send(stop_reason) .unwrap(); } } @@ -308,10 +308,8 @@ mod test_support { let (tx, rx) = oneshot::channel(); response_tx.replace(tx); cx.spawn(async move |_| { - rx.await?; - Ok(acp::PromptResponse { - stop_reason: acp::StopReason::EndTurn, - }) + let stop_reason = rx.await?; + Ok(acp::PromptResponse { stop_reason }) }) } else { for update in self.next_prompt_updates.lock().drain(..) { @@ -353,8 +351,17 @@ mod test_support { } } - fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) { - unimplemented!() + fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) { + if let Some(end_turn_tx) = self + .sessions + .lock() + .get_mut(session_id) + .unwrap() + .response_tx + .take() + { + end_turn_tx.send(acp::StopReason::Canceled).unwrap(); + } } fn session_editor( diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 4760677fa1..2c02027c4d 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -4283,7 +4283,7 @@ pub(crate) mod tests { }, cx, ); - connection.end_turn(session_id); + connection.end_turn(session_id, acp::StopReason::EndTurn); }); thread_view.read_with(cx, |view, _cx| { @@ -4302,4 +4302,137 @@ pub(crate) mod tests { ); }); } + + #[gpui::test] + async fn test_interrupt(cx: &mut TestAppContext) { + init_test(cx); + + let connection = StubAgentConnection::new(); + + let (thread_view, cx) = + setup_thread_view(StubAgentServer::new(connection.clone()), cx).await; + add_to_workspace(thread_view.clone(), cx); + + let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Message 1", window, cx); + }); + thread_view.update_in(cx, |thread_view, window, cx| { + thread_view.send(window, cx); + }); + + let (thread, session_id) = thread_view.read_with(cx, |view, cx| { + let thread = view.thread().unwrap(); + + (thread.clone(), thread.read(cx).session_id().clone()) + }); + + cx.run_until_parked(); + + cx.update(|_, cx| { + connection.send_update( + session_id.clone(), + acp::SessionUpdate::AgentMessageChunk { + content: "Message 1 resp".into(), + }, + cx, + ); + }); + + cx.run_until_parked(); + + thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc::indoc! {" + ## User + + Message 1 + + ## Assistant + + Message 1 resp + + "} + ) + }); + + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Message 2", window, cx); + }); + thread_view.update_in(cx, |thread_view, window, cx| { + thread_view.send(window, cx); + }); + + cx.update(|_, cx| { + // Simulate a response sent after beginning to cancel + connection.send_update( + session_id.clone(), + acp::SessionUpdate::AgentMessageChunk { + content: "onse".into(), + }, + cx, + ); + }); + + cx.run_until_parked(); + + // Last Message 1 response should appear before Message 2 + thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc::indoc! {" + ## User + + Message 1 + + ## Assistant + + Message 1 response + + ## User + + Message 2 + + "} + ) + }); + + cx.update(|_, cx| { + connection.send_update( + session_id.clone(), + acp::SessionUpdate::AgentMessageChunk { + content: "Message 2 response".into(), + }, + cx, + ); + connection.end_turn(session_id.clone(), acp::StopReason::EndTurn); + }); + + cx.run_until_parked(); + + thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc::indoc! {" + ## User + + Message 1 + + ## Assistant + + Message 1 response + + ## User + + Message 2 + + ## Assistant + + Message 2 response + + "} + ) + }); + } } From e1d31cfcc3360bf50f6230d6dd5d1aafc3295c4c Mon Sep 17 00:00:00 2001 From: AidanV <84053180+AidanV@users.noreply.github.com> Date: Mon, 18 Aug 2025 08:52:25 -0700 Subject: [PATCH 441/693] vim: Display invisibles in mode indicator (#35760) Release Notes: - Fixes bug where `ctrl-k enter` while in `INSERT` mode would put a newline in the Vim mode indicator #### Old OldVimModeIndicator #### New NewVimModeIndicator --- crates/vim/src/state.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index c4be034871..423859dadc 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -1028,13 +1028,21 @@ impl Operator { } pub fn status(&self) -> String { + fn make_visible(c: &str) -> &str { + match c { + "\n" => "enter", + "\t" => "tab", + " " => "space", + c => c, + } + } match self { Operator::Digraph { first_char: Some(first_char), - } => format!("^K{first_char}"), + } => format!("^K{}", make_visible(&first_char.to_string())), Operator::Literal { prefix: Some(prefix), - } => format!("^V{prefix}"), + } => format!("^V{}", make_visible(&prefix)), Operator::AutoIndent => "=".to_string(), Operator::ShellCommand => "=".to_string(), _ => self.id().to_string(), From 768b2de368697a559a038f65e61aff81dc99f041 Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Mon, 18 Aug 2025 12:57:53 -0300 Subject: [PATCH 442/693] vim: Fix `ap` text object selection when there is line wrapping (#35485) In Vim mode, `ap` text object (used in `vap`, `dap`, `cap`) was selecting multiple paragraphs when soft wrap was enabled. The bug was caused by using DisplayRow coordinates for arithmetic instead of buffer row coordinates in the paragraph boundary calculation. Fix by converting to buffer coordinates before arithmetic, then back to display coordinates for the final result. Closes #35085 --------- Co-authored-by: Conrad Irwin --- crates/vim/src/normal/delete.rs | 22 +++++ crates/vim/src/object.rs | 93 ++++++++++++++++++- crates/vim/src/visual.rs | 33 +++++++ ...hange_paragraph_object_with_soft_wrap.json | 72 ++++++++++++++ ...elete_paragraph_object_with_soft_wrap.json | 72 ++++++++++++++ .../test_delete_paragraph_whitespace.json | 5 + ...isual_paragraph_object_with_soft_wrap.json | 72 ++++++++++++++ 7 files changed, 365 insertions(+), 4 deletions(-) create mode 100644 crates/vim/test_data/test_change_paragraph_object_with_soft_wrap.json create mode 100644 crates/vim/test_data/test_delete_paragraph_object_with_soft_wrap.json create mode 100644 crates/vim/test_data/test_delete_paragraph_whitespace.json create mode 100644 crates/vim/test_data/test_visual_paragraph_object_with_soft_wrap.json diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index 1b7557371a..d7a6932baa 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -2,6 +2,7 @@ use crate::{ Vim, motion::{Motion, MotionKind}, object::Object, + state::Mode, }; use collections::{HashMap, HashSet}; use editor::{ @@ -102,8 +103,20 @@ impl Vim { // Emulates behavior in vim where if we expanded backwards to include a newline // the cursor gets set back to the start of the line let mut should_move_to_start: HashSet<_> = Default::default(); + + // Emulates behavior in vim where after deletion the cursor should try to move + // to the same column it was before deletion if the line is not empty or only + // contains whitespace + let mut column_before_move: HashMap<_, _> = Default::default(); + let target_mode = object.target_visual_mode(vim.mode, around); + editor.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { + let cursor_point = selection.head().to_point(map); + if target_mode == Mode::VisualLine { + column_before_move.insert(selection.id, cursor_point.column); + } + object.expand_selection(map, selection, around, times); let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range(); let mut move_selection_start_to_previous_line = @@ -164,6 +177,15 @@ impl Vim { let mut cursor = selection.head(); if should_move_to_start.contains(&selection.id) { *cursor.column_mut() = 0; + } else if let Some(column) = column_before_move.get(&selection.id) + && *column > 0 + { + let mut cursor_point = cursor.to_point(map); + cursor_point.column = *column; + cursor = map + .buffer_snapshot + .clip_point(cursor_point, Bias::Left) + .to_display_point(map); } cursor = map.clip_point(cursor, Bias::Left); selection.collapse_to(cursor, selection.goal) diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 63139d7e94..cff23c4bd4 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -1444,14 +1444,15 @@ fn paragraph( return None; } - let paragraph_start_row = paragraph_start.row(); - if paragraph_start_row.0 != 0 { + let paragraph_start_buffer_point = paragraph_start.to_point(map); + if paragraph_start_buffer_point.row != 0 { let previous_paragraph_last_line_start = - Point::new(paragraph_start_row.0 - 1, 0).to_display_point(map); + Point::new(paragraph_start_buffer_point.row - 1, 0).to_display_point(map); paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start); } } else { - let mut start_row = paragraph_end_row.0 + 1; + let paragraph_end_buffer_point = paragraph_end.to_point(map); + let mut start_row = paragraph_end_buffer_point.row + 1; if i > 0 { start_row += 1; } @@ -1903,6 +1904,90 @@ mod test { } } + #[gpui::test] + async fn test_change_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + const WRAPPING_EXAMPLE: &str = indoc! {" + ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines. + + ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly. + + ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ + "}; + + cx.set_shared_wrap(20).await; + + cx.simulate_at_each_offset("c i p", WRAPPING_EXAMPLE) + .await + .assert_matches(); + cx.simulate_at_each_offset("c a p", WRAPPING_EXAMPLE) + .await + .assert_matches(); + } + + #[gpui::test] + async fn test_delete_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + const WRAPPING_EXAMPLE: &str = indoc! {" + ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines. + + ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly. + + ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ + "}; + + cx.set_shared_wrap(20).await; + + cx.simulate_at_each_offset("d i p", WRAPPING_EXAMPLE) + .await + .assert_matches(); + cx.simulate_at_each_offset("d a p", WRAPPING_EXAMPLE) + .await + .assert_matches(); + } + + #[gpui::test] + async fn test_delete_paragraph_whitespace(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + a + ˇ• + aaaaaaaaaaaaa + "}) + .await; + + cx.simulate_shared_keystrokes("d i p").await; + cx.shared_state().await.assert_eq(indoc! {" + a + aaaaaaaˇaaaaaa + "}); + } + + #[gpui::test] + async fn test_visual_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + const WRAPPING_EXAMPLE: &str = indoc! {" + ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines. + + ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly. + + ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ + "}; + + cx.set_shared_wrap(20).await; + + cx.simulate_at_each_offset("v i p", WRAPPING_EXAMPLE) + .await + .assert_matches(); + cx.simulate_at_each_offset("v a p", WRAPPING_EXAMPLE) + .await + .assert_matches(); + } + // Test string with "`" for opening surrounders and "'" for closing surrounders const SURROUNDING_MARKER_STRING: &str = indoc! {" ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn` diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 7bfd8dc8be..3b789b1f3e 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -414,6 +414,8 @@ impl Vim { ); } + let original_point = selection.tail().to_point(&map); + if let Some(range) = object.range(map, mut_selection, around, count) { if !range.is_empty() { let expand_both_ways = object.always_expands_both_ways() @@ -462,6 +464,37 @@ impl Vim { }; selection.end = new_selection_end.to_display_point(map); } + + // To match vim, if the range starts of the same line as it originally + // did, we keep the tail of the selection in the same place instead of + // snapping it to the start of the line + if target_mode == Mode::VisualLine { + let new_start_point = selection.start.to_point(map); + if new_start_point.row == original_point.row { + if selection.end.to_point(map).row > new_start_point.row { + if original_point.column + == map + .buffer_snapshot + .line_len(MultiBufferRow(original_point.row)) + { + selection.start = movement::saturating_left( + map, + original_point.to_display_point(map), + ) + } else { + selection.start = original_point.to_display_point(map) + } + } else { + selection.end = movement::saturating_right( + map, + original_point.to_display_point(map), + ); + if original_point.column > 0 { + selection.reversed = true + } + } + } + } } }); }); diff --git a/crates/vim/test_data/test_change_paragraph_object_with_soft_wrap.json b/crates/vim/test_data/test_change_paragraph_object_with_soft_wrap.json new file mode 100644 index 0000000000..47d68e13a6 --- /dev/null +++ b/crates/vim/test_data/test_change_paragraph_object_with_soft_wrap.json @@ -0,0 +1,72 @@ +{"SetOption":{"value":"wrap"}} +{"SetOption":{"value":"columns=20"}} +{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"ˇ\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"ˇ\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ\n","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ\n","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ\n","mode":"Insert"}} +{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Insert"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_delete_paragraph_object_with_soft_wrap.json b/crates/vim/test_data/test_delete_paragraph_object_with_soft_wrap.json new file mode 100644 index 0000000000..19dcd175b3 --- /dev/null +++ b/crates/vim/test_data/test_delete_paragraph_object_with_soft_wrap.json @@ -0,0 +1,72 @@ +{"SetOption":{"value":"wrap"}} +{"SetOption":{"value":"columns=20"}} +{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Normal"}} +{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"Second paragraph that is also quite long and will definitely wrap under soft wrap conditions andˇ should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nThird paragraph with additional long text content that will also wrap when line length is constraˇined by the wrapping settings.\n","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\nˇ","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\nˇ","mode":"Normal"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\nˇ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_paragraph_whitespace.json b/crates/vim/test_data/test_delete_paragraph_whitespace.json new file mode 100644 index 0000000000..e07b18eaa3 --- /dev/null +++ b/crates/vim/test_data/test_delete_paragraph_whitespace.json @@ -0,0 +1,5 @@ +{"Put":{"state":"a\n ˇ•\naaaaaaaaaaaaa\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"a\naaaaaaaˇaaaaaa\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_visual_paragraph_object_with_soft_wrap.json b/crates/vim/test_data/test_visual_paragraph_object_with_soft_wrap.json new file mode 100644 index 0000000000..6bfce2f955 --- /dev/null +++ b/crates/vim/test_data/test_visual_paragraph_object_with_soft_wrap.json @@ -0,0 +1,72 @@ +{"SetOption":{"value":"wrap"}} +{"SetOption":{"value":"columns=20"}} +{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"«Fˇ»irst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"«ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is l»imited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\n«Sˇ»econd paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\n«ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and s»hould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\n«Tˇ»hird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\n«ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping s»ettings.\n","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\n«ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.»\n","mode":"VisualLine"}} +{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"«First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ»Second paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is «limited making it span multiple display lines.\n\nˇ»Second paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\n«Second paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ»Third paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and «should be handled correctly.\n\nˇ»Third paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\n«Third paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\nˇ»","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping «settings.\nˇ»","mode":"VisualLine"}} +{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"p"} +{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings«.\nˇ»","mode":"VisualLine"}} From e1d8e3bf6d74f260f8fc5b8d0ec3aa89fb3f6985 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Mon, 18 Aug 2025 18:58:12 +0300 Subject: [PATCH 443/693] language: Clean up allocations (#36418) - Correctly pre-allocate `Vec` when deserializing regexes - Simplify manual `Vec::with_capacity` calls by using `Iterator::unzip` - Collect directly into `Arc<[T]>` (uses `Vec` internally anyway, but simplifies code) - Remove unnecessary `LazyLock` around Atomics by not using const incompatible `Default` for initialization. Release Notes: - N/A --- crates/language/src/language.rs | 42 +++++++++++++++------------------ crates/language/src/proto.rs | 10 ++++---- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 6fa31da860..c377d7440a 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -121,8 +121,8 @@ where func(cursor.deref_mut()) } -static NEXT_LANGUAGE_ID: LazyLock = LazyLock::new(Default::default); -static NEXT_GRAMMAR_ID: LazyLock = LazyLock::new(Default::default); +static NEXT_LANGUAGE_ID: AtomicUsize = AtomicUsize::new(0); +static NEXT_GRAMMAR_ID: AtomicUsize = AtomicUsize::new(0); static WASM_ENGINE: LazyLock = LazyLock::new(|| { wasmtime::Engine::new(&wasmtime::Config::new()).expect("Failed to create Wasmtime engine") }); @@ -964,11 +964,11 @@ where fn deserialize_regex_vec<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { let sources = Vec::::deserialize(d)?; - let mut regexes = Vec::new(); - for source in sources { - regexes.push(regex::Regex::new(&source).map_err(de::Error::custom)?); - } - Ok(regexes) + sources + .into_iter() + .map(|source| regex::Regex::new(&source)) + .collect::>() + .map_err(de::Error::custom) } fn regex_vec_json_schema(_: &mut SchemaGenerator) -> schemars::Schema { @@ -1034,12 +1034,10 @@ impl<'de> Deserialize<'de> for BracketPairConfig { D: Deserializer<'de>, { let result = Vec::::deserialize(deserializer)?; - let mut brackets = Vec::with_capacity(result.len()); - let mut disabled_scopes_by_bracket_ix = Vec::with_capacity(result.len()); - for entry in result { - brackets.push(entry.bracket_pair); - disabled_scopes_by_bracket_ix.push(entry.not_in); - } + let (brackets, disabled_scopes_by_bracket_ix) = result + .into_iter() + .map(|entry| (entry.bracket_pair, entry.not_in)) + .unzip(); Ok(BracketPairConfig { pairs: brackets, @@ -1379,16 +1377,14 @@ impl Language { let grammar = self.grammar_mut().context("cannot mutate grammar")?; let query = Query::new(&grammar.ts_language, source)?; - let mut extra_captures = Vec::with_capacity(query.capture_names().len()); - - for name in query.capture_names().iter() { - let kind = if *name == "run" { - RunnableCapture::Run - } else { - RunnableCapture::Named(name.to_string().into()) - }; - extra_captures.push(kind); - } + let extra_captures: Vec<_> = query + .capture_names() + .iter() + .map(|&name| match name { + "run" => RunnableCapture::Run, + name => RunnableCapture::Named(name.to_string().into()), + }) + .collect(); grammar.runnable_config = Some(RunnableConfig { extra_captures, diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 18f6bb8709..acae97019f 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -385,12 +385,10 @@ pub fn deserialize_undo_map_entry( /// Deserializes selections from the RPC representation. pub fn deserialize_selections(selections: Vec) -> Arc<[Selection]> { - Arc::from( - selections - .into_iter() - .filter_map(deserialize_selection) - .collect::>(), - ) + selections + .into_iter() + .filter_map(deserialize_selection) + .collect() } /// Deserializes a [`Selection`] from the RPC representation. From ed155ceba9e8add2193dc77220bf1a20bf7c5288 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 18 Aug 2025 18:27:26 +0200 Subject: [PATCH 444/693] title_bar: Fix screensharing errors not being shown to the user (#36424) Release Notes: - N/A --- crates/title_bar/src/collab.rs | 95 ++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 45 deletions(-) diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index 74d60a6d66..b458c64b5f 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -14,7 +14,6 @@ use ui::{ Avatar, AvatarAudioStatusIndicator, ContextMenu, ContextMenuItem, Divider, DividerColor, Facepile, PopoverMenu, SplitButton, SplitButtonStyle, TintColor, Tooltip, prelude::*, }; -use util::maybe; use workspace::notifications::DetachAndPromptErr; use crate::TitleBar; @@ -32,52 +31,59 @@ actions!( ); fn toggle_screen_sharing( - screen: Option>, + screen: anyhow::Result>>, window: &mut Window, cx: &mut App, ) { let call = ActiveCall::global(cx).read(cx); - if let Some(room) = call.room().cloned() { - let toggle_screen_sharing = room.update(cx, |room, cx| { - let clicked_on_currently_shared_screen = - room.shared_screen_id().is_some_and(|screen_id| { - Some(screen_id) - == screen - .as_deref() - .and_then(|s| s.metadata().ok().map(|meta| meta.id)) - }); - let should_unshare_current_screen = room.is_sharing_screen(); - let unshared_current_screen = should_unshare_current_screen.then(|| { - telemetry::event!( - "Screen Share Disabled", - room_id = room.id(), - channel_id = room.channel_id(), - ); - room.unshare_screen(clicked_on_currently_shared_screen || screen.is_none(), cx) - }); - if let Some(screen) = screen { - if !should_unshare_current_screen { + let toggle_screen_sharing = match screen { + Ok(screen) => { + let Some(room) = call.room().cloned() else { + return; + }; + let toggle_screen_sharing = room.update(cx, |room, cx| { + let clicked_on_currently_shared_screen = + room.shared_screen_id().is_some_and(|screen_id| { + Some(screen_id) + == screen + .as_deref() + .and_then(|s| s.metadata().ok().map(|meta| meta.id)) + }); + let should_unshare_current_screen = room.is_sharing_screen(); + let unshared_current_screen = should_unshare_current_screen.then(|| { telemetry::event!( - "Screen Share Enabled", + "Screen Share Disabled", room_id = room.id(), channel_id = room.channel_id(), ); - } - cx.spawn(async move |room, cx| { - unshared_current_screen.transpose()?; - if !clicked_on_currently_shared_screen { - room.update(cx, |room, cx| room.share_screen(screen, cx))? - .await - } else { - Ok(()) + room.unshare_screen(clicked_on_currently_shared_screen || screen.is_none(), cx) + }); + if let Some(screen) = screen { + if !should_unshare_current_screen { + telemetry::event!( + "Screen Share Enabled", + room_id = room.id(), + channel_id = room.channel_id(), + ); } - }) - } else { - Task::ready(Ok(())) - } - }); - toggle_screen_sharing.detach_and_prompt_err("Sharing Screen Failed", window, cx, |e, _, _| Some(format!("{:?}\n\nPlease check that you have given Zed permissions to record your screen in Settings.", e))); - } + cx.spawn(async move |room, cx| { + unshared_current_screen.transpose()?; + if !clicked_on_currently_shared_screen { + room.update(cx, |room, cx| room.share_screen(screen, cx))? + .await + } else { + Ok(()) + } + }) + } else { + Task::ready(Ok(())) + } + }); + toggle_screen_sharing + } + Err(e) => Task::ready(Err(e)), + }; + toggle_screen_sharing.detach_and_prompt_err("Sharing Screen Failed", window, cx, |e, _, _| Some(format!("{:?}\n\nPlease check that you have given Zed permissions to record your screen in Settings.", e))); } fn toggle_mute(_: &ToggleMute, cx: &mut App) { @@ -483,9 +489,8 @@ impl TitleBar { let screen = if should_share { cx.update(|_, cx| pick_default_screen(cx))?.await } else { - None + Ok(None) }; - cx.update(|window, cx| toggle_screen_sharing(screen, window, cx))?; Result::<_, anyhow::Error>::Ok(()) @@ -571,7 +576,7 @@ impl TitleBar { selectable: true, documentation_aside: None, handler: Rc::new(move |_, window, cx| { - toggle_screen_sharing(Some(screen.clone()), window, cx); + toggle_screen_sharing(Ok(Some(screen.clone())), window, cx); }), }); } @@ -585,11 +590,11 @@ impl TitleBar { } /// Picks the screen to share when clicking on the main screen sharing button. -fn pick_default_screen(cx: &App) -> Task>> { +fn pick_default_screen(cx: &App) -> Task>>> { let source = cx.screen_capture_sources(); cx.spawn(async move |_| { - let available_sources = maybe!(async move { source.await? }).await.ok()?; - available_sources + let available_sources = source.await??; + Ok(available_sources .iter() .find(|it| { it.as_ref() @@ -597,6 +602,6 @@ fn pick_default_screen(cx: &App) -> Task>> { .is_ok_and(|meta| meta.is_main.unwrap_or_default()) }) .or_else(|| available_sources.iter().next()) - .cloned() + .cloned()) }) } From fa61c3e24d8893a8a62ba0e46dba48e9cc4ae8bd Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 18 Aug 2025 13:27:23 -0400 Subject: [PATCH 445/693] gpui: Fix typo in `handle_gpui_events` (#36431) This PR fixes a typo I noticed in the `handle_gpui_events` method name. Release Notes: - N/A --- crates/gpui/src/platform/windows/platform.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index c1fb0cabc4..ee0babf7cb 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -227,7 +227,7 @@ impl WindowsPlatform { | WM_GPUI_CLOSE_ONE_WINDOW | WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD | WM_GPUI_DOCK_MENU_ACTION => { - if self.handle_gpui_evnets(msg.message, msg.wParam, msg.lParam, &msg) { + if self.handle_gpui_events(msg.message, msg.wParam, msg.lParam, &msg) { return; } } @@ -240,7 +240,7 @@ impl WindowsPlatform { } // Returns true if the app should quit. - fn handle_gpui_evnets( + fn handle_gpui_events( &self, message: u32, wparam: WPARAM, From 3a3df5c0118e942893dd3f12aa0c2f734ffae0af Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Mon, 18 Aug 2025 15:48:02 -0400 Subject: [PATCH 446/693] gpui: Add support for custom prompt text in PathPromptOptions (#36410) This will be used to improve the clarity of the git clone UI ### MacOS image ### Windows image ### Linux Screenshot From 2025-08-18 15-32-06 Release Notes: - N/A --- crates/extensions_ui/src/extensions_ui.rs | 1 + crates/git_ui/src/git_panel.rs | 1 + crates/gpui/src/platform.rs | 4 +++- crates/gpui/src/platform/linux/platform.rs | 1 + crates/gpui/src/platform/mac/platform.rs | 6 ++++++ crates/gpui/src/platform/windows/platform.rs | 6 ++++++ crates/gpui/src/shared_string.rs | 5 +++++ crates/workspace/src/workspace.rs | 3 +++ crates/zed/src/zed.rs | 2 ++ 9 files changed, 28 insertions(+), 1 deletion(-) diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 4915933920..7c7f9e6836 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -116,6 +116,7 @@ pub fn init(cx: &mut App) { files: false, directories: true, multiple: false, + prompt: None, }, DirectoryLister::Local( workspace.project().clone(), diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index b346f4d216..754812cbdf 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -2088,6 +2088,7 @@ impl GitPanel { files: false, directories: true, multiple: false, + prompt: Some("Select as Repository Destination".into()), }); let workspace = self.workspace.clone(); diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index bf6ce68703..ffd68d60e6 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1278,7 +1278,7 @@ pub enum WindowBackgroundAppearance { } /// The options that can be configured for a file dialog prompt -#[derive(Copy, Clone, Debug)] +#[derive(Clone, Debug)] pub struct PathPromptOptions { /// Should the prompt allow files to be selected? pub files: bool, @@ -1286,6 +1286,8 @@ pub struct PathPromptOptions { pub directories: bool, /// Should the prompt allow multiple files to be selected? pub multiple: bool, + /// The prompt to show to a user when selecting a path + pub prompt: Option, } /// What kind of prompt styling to show diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 31d445be52..86e5a79e8a 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -294,6 +294,7 @@ impl Platform for P { let request = match ashpd::desktop::file_chooser::OpenFileRequest::default() .modal(true) .title(title) + .accept_label(options.prompt.as_ref().map(crate::SharedString::as_str)) .multiple(options.multiple) .directory(options.directories) .send() diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 533423229c..79177fb2c9 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -705,6 +705,7 @@ impl Platform for MacPlatform { panel.setCanChooseDirectories_(options.directories.to_objc()); panel.setCanChooseFiles_(options.files.to_objc()); panel.setAllowsMultipleSelection_(options.multiple.to_objc()); + panel.setCanCreateDirectories(true.to_objc()); panel.setResolvesAliases_(false.to_objc()); let done_tx = Cell::new(Some(done_tx)); @@ -730,6 +731,11 @@ impl Platform for MacPlatform { } }); let block = block.copy(); + + if let Some(prompt) = options.prompt { + let _: () = msg_send![panel, setPrompt: ns_string(&prompt)]; + } + let _: () = msg_send![panel, beginWithCompletionHandler: block]; } }) diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index ee0babf7cb..856187fa57 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -787,6 +787,12 @@ fn file_open_dialog( unsafe { folder_dialog.SetOptions(dialog_options)?; + + if let Some(prompt) = options.prompt { + let prompt: &str = &prompt; + folder_dialog.SetOkButtonLabel(&HSTRING::from(prompt))?; + } + if folder_dialog.Show(window).is_err() { // User cancelled return Ok(None); diff --git a/crates/gpui/src/shared_string.rs b/crates/gpui/src/shared_string.rs index c325f98cd2..a34b7502f0 100644 --- a/crates/gpui/src/shared_string.rs +++ b/crates/gpui/src/shared_string.rs @@ -23,6 +23,11 @@ impl SharedString { pub fn new(str: impl Into>) -> Self { SharedString(ArcCow::Owned(str.into())) } + + /// Get a &str from the underlying string. + pub fn as_str(&self) -> &str { + &self.0 + } } impl JsonSchema for SharedString { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1eaa125ba5..02eac1665b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -561,6 +561,7 @@ pub fn init(app_state: Arc, cx: &mut App) { files: true, directories: true, multiple: true, + prompt: None, }, cx, ); @@ -578,6 +579,7 @@ pub fn init(app_state: Arc, cx: &mut App) { files: true, directories, multiple: true, + prompt: None, }, cx, ); @@ -2655,6 +2657,7 @@ impl Workspace { files: false, directories: true, multiple: true, + prompt: None, }, DirectoryLister::Project(self.project.clone()), window, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index cfafbb70f0..6d5aecba70 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -645,6 +645,7 @@ fn register_actions( files: true, directories: true, multiple: true, + prompt: None, }, DirectoryLister::Local( workspace.project().clone(), @@ -685,6 +686,7 @@ fn register_actions( files: true, directories: true, multiple: true, + prompt: None, }, DirectoryLister::Project(workspace.project().clone()), window, From 50819a9d208917344d913800e818fe37e71974a8 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 18 Aug 2025 15:57:28 -0400 Subject: [PATCH 447/693] client: Parse auth callback query parameters before showing sign-in success page (#36440) This PR fixes an issue where we would redirect the user's browser to the sign-in success page even if the OAuth callback was malformed. We now parse the OAuth callback parameters from the query string and only redirect to the sign-in success page when they are valid. Release Notes: - Updated the sign-in flow to not show the sign-in success page prematurely. --- Cargo.lock | 1 + Cargo.toml | 1 + crates/client/Cargo.toml | 1 + crates/client/src/client.rs | 24 +++++++++++++----------- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98f10eff41..3158a61ad8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3070,6 +3070,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "serde_urlencoded", "settings", "sha2", "smol", diff --git a/Cargo.toml b/Cargo.toml index 83d6da5cd7..914f9e6837 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -582,6 +582,7 @@ serde_json_lenient = { version = "0.2", features = [ "raw_value", ] } serde_repr = "0.1" +serde_urlencoded = "0.7" sha2 = "0.10" shellexpand = "2.1.0" shlex = "1.3.0" diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 365625b445..5c6d1157fd 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -44,6 +44,7 @@ rpc = { workspace = true, features = ["gpui"] } schemars.workspace = true serde.workspace = true serde_json.workspace = true +serde_urlencoded.workspace = true settings.workspace = true sha2.workspace = true smol.workspace = true diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index f09c012a85..0f00471356 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1410,6 +1410,12 @@ impl Client { open_url_tx.send(url).log_err(); + #[derive(Deserialize)] + struct CallbackParams { + pub user_id: String, + pub access_token: String, + } + // Receive the HTTP request from the user's browser. Retrieve the user id and encrypted // access token from the query params. // @@ -1420,17 +1426,13 @@ impl Client { for _ in 0..100 { if let Some(req) = server.recv_timeout(Duration::from_secs(1))? { let path = req.url(); - let mut user_id = None; - let mut access_token = None; let url = Url::parse(&format!("http://example.com{}", path)) .context("failed to parse login notification url")?; - for (key, value) in url.query_pairs() { - if key == "access_token" { - access_token = Some(value.to_string()); - } else if key == "user_id" { - user_id = Some(value.to_string()); - } - } + let callback_params: CallbackParams = + serde_urlencoded::from_str(url.query().unwrap_or_default()) + .context( + "failed to parse sign-in callback query parameters", + )?; let post_auth_url = http.build_url("/native_app_signin_succeeded"); @@ -1445,8 +1447,8 @@ impl Client { ) .context("failed to respond to login http request")?; return Ok(( - user_id.context("missing user_id parameter")?, - access_token.context("missing access_token parameter")?, + callback_params.user_id, + callback_params.access_token, )); } } From 8b89ea1a801af6190b1b6e6557a69fadb08db93f Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Mon, 18 Aug 2025 17:40:59 -0300 Subject: [PATCH 448/693] Handle auth for claude (#36442) We'll now use the anthropic provider to get credentials for `claude` and embed its configuration view in the panel when they are not present. Release Notes: - N/A --- Cargo.lock | 3 + crates/acp_thread/Cargo.toml | 1 + crates/acp_thread/src/connection.rs | 27 ++- crates/agent_servers/Cargo.toml | 2 + crates/agent_servers/src/acp/v0.rs | 2 +- crates/agent_servers/src/acp/v1.rs | 8 +- crates/agent_servers/src/claude.rs | 54 ++++-- crates/agent_ui/src/acp/thread_view.rs | 182 +++++++++++++----- crates/agent_ui/src/agent_configuration.rs | 6 +- crates/agent_ui/src/agent_ui.rs | 2 +- .../agent_ui/src/language_model_selector.rs | 2 +- .../src/agent_api_keys_onboarding.rs | 2 +- .../src/agent_panel_onboarding_content.rs | 2 +- crates/gpui/src/subscription.rs | 6 + crates/language_model/src/fake_provider.rs | 15 +- crates/language_model/src/language_model.rs | 14 +- crates/language_model/src/registry.rs | 9 +- .../language_models/src/provider/anthropic.rs | 90 ++++++--- .../language_models/src/provider/bedrock.rs | 7 +- crates/language_models/src/provider/cloud.rs | 7 +- .../src/provider/copilot_chat.rs | 7 +- .../language_models/src/provider/deepseek.rs | 7 +- crates/language_models/src/provider/google.rs | 7 +- .../language_models/src/provider/lmstudio.rs | 7 +- .../language_models/src/provider/mistral.rs | 7 +- crates/language_models/src/provider/ollama.rs | 7 +- .../language_models/src/provider/open_ai.rs | 7 +- .../src/provider/open_ai_compatible.rs | 7 +- .../src/provider/open_router.rs | 7 +- crates/language_models/src/provider/vercel.rs | 7 +- crates/language_models/src/provider/x_ai.rs | 7 +- crates/onboarding/src/ai_setup_page.rs | 6 +- 32 files changed, 400 insertions(+), 124 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3158a61ad8..3bc2b63843 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,7 @@ dependencies = [ "indoc", "itertools 0.14.0", "language", + "language_model", "markdown", "parking_lot", "project", @@ -267,6 +268,8 @@ dependencies = [ "indoc", "itertools 0.14.0", "language", + "language_model", + "language_models", "libc", "log", "nix 0.29.0", diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index 2b9a6513c8..173f4c4208 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -28,6 +28,7 @@ futures.workspace = true gpui.workspace = true itertools.workspace = true language.workspace = true +language_model.workspace = true markdown.workspace = true parking_lot = { workspace = true, optional = true } project.workspace = true diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index a328499bbc..0d4116321d 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -3,6 +3,7 @@ use agent_client_protocol::{self as acp}; use anyhow::Result; use collections::IndexMap; use gpui::{Entity, SharedString, Task}; +use language_model::LanguageModelProviderId; use project::Project; use std::{any::Any, error::Error, fmt, path::Path, rc::Rc, sync::Arc}; use ui::{App, IconName}; @@ -80,12 +81,34 @@ pub trait AgentSessionResume { } #[derive(Debug)] -pub struct AuthRequired; +pub struct AuthRequired { + pub description: Option, + pub provider_id: Option, +} + +impl AuthRequired { + pub fn new() -> Self { + Self { + description: None, + provider_id: None, + } + } + + pub fn with_description(mut self, description: String) -> Self { + self.description = Some(description); + self + } + + pub fn with_language_model_provider(mut self, provider_id: LanguageModelProviderId) -> Self { + self.provider_id = Some(provider_id); + self + } +} impl Error for AuthRequired {} impl fmt::Display for AuthRequired { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "AuthRequired") + write!(f, "Authentication required") } } diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index 81c97c8aa6..f894bb15bf 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -27,6 +27,8 @@ futures.workspace = true gpui.workspace = true indoc.workspace = true itertools.workspace = true +language_model.workspace = true +language_models.workspace = true log.workspace = true paths.workspace = true project.workspace = true diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index 74647f7313..551e9fa01a 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -437,7 +437,7 @@ impl AgentConnection for AcpConnection { let result = acp_old::InitializeParams::response_from_any(result)?; if !result.is_authenticated { - anyhow::bail!(AuthRequired) + anyhow::bail!(AuthRequired::new()) } cx.update(|cx| { diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index b77b5ef36d..93a5ae757a 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -140,7 +140,13 @@ impl AgentConnection for AcpConnection { .await .map_err(|err| { if err.code == acp::ErrorCode::AUTH_REQUIRED.code { - anyhow!(AuthRequired) + let mut error = AuthRequired::new(); + + if err.message != acp::ErrorCode::AUTH_REQUIRED.message { + error = error.with_description(err.message); + } + + anyhow!(error) } else { anyhow!(err) } diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index d15cc1dd89..d80d040aad 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -3,6 +3,7 @@ pub mod tools; use collections::HashMap; use context_server::listener::McpServerTool; +use language_models::provider::anthropic::AnthropicLanguageModelProvider; use project::Project; use settings::SettingsStore; use smol::process::Child; @@ -30,7 +31,7 @@ use util::{ResultExt, debug_panic}; use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig}; use crate::claude::tools::ClaudeTool; use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings}; -use acp_thread::{AcpThread, AgentConnection}; +use acp_thread::{AcpThread, AgentConnection, AuthRequired}; #[derive(Clone)] pub struct ClaudeCode; @@ -79,6 +80,36 @@ impl AgentConnection for ClaudeAgentConnection { ) -> Task>> { let cwd = cwd.to_owned(); cx.spawn(async move |cx| { + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings.get::(None).claude.clone() + })?; + + let Some(command) = AgentServerCommand::resolve( + "claude", + &[], + Some(&util::paths::home_dir().join(".claude/local/claude")), + settings, + &project, + cx, + ) + .await + else { + anyhow::bail!("Failed to find claude binary"); + }; + + let api_key = + cx.update(AnthropicLanguageModelProvider::api_key)? + .await + .map_err(|err| { + if err.is::() { + anyhow!(AuthRequired::new().with_language_model_provider( + language_model::ANTHROPIC_PROVIDER_ID + )) + } else { + anyhow!(err) + } + })?; + let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid()); let permission_mcp_server = ClaudeZedMcpServer::new(thread_rx.clone(), cx).await?; @@ -98,23 +129,6 @@ impl AgentConnection for ClaudeAgentConnection { .await?; mcp_config_file.flush().await?; - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings.get::(None).claude.clone() - })?; - - let Some(command) = AgentServerCommand::resolve( - "claude", - &[], - Some(&util::paths::home_dir().join(".claude/local/claude")), - settings, - &project, - cx, - ) - .await - else { - anyhow::bail!("Failed to find claude binary"); - }; - let (incoming_message_tx, mut incoming_message_rx) = mpsc::unbounded(); let (outgoing_tx, outgoing_rx) = mpsc::unbounded(); @@ -126,6 +140,7 @@ impl AgentConnection for ClaudeAgentConnection { &command, ClaudeSessionMode::Start, session_id.clone(), + api_key, &mcp_config_path, &cwd, )?; @@ -320,6 +335,7 @@ fn spawn_claude( command: &AgentServerCommand, mode: ClaudeSessionMode, session_id: acp::SessionId, + api_key: language_models::provider::anthropic::ApiKey, mcp_config_path: &Path, root_dir: &Path, ) -> Result { @@ -355,6 +371,8 @@ fn spawn_claude( ClaudeSessionMode::Resume => ["--resume".to_string(), session_id.to_string()], }) .args(command.args.iter().map(|arg| arg.as_str())) + .envs(command.env.iter().flatten()) + .env("ANTHROPIC_API_KEY", api_key.key) .current_dir(root_dir) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 2c02027c4d..e2e5820812 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1,6 +1,7 @@ use acp_thread::{ AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, - LoadError, MentionUri, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, UserMessageId, + AuthRequired, LoadError, MentionUri, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, + UserMessageId, }; use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; @@ -18,13 +19,16 @@ use editor::{Editor, EditorMode, MultiBuffer, PathKey, SelectionEffects}; use file_icons::FileIcons; use fs::Fs; use gpui::{ - Action, Animation, AnimationExt, App, BorderStyle, ClickEvent, ClipboardItem, EdgesRefinement, - Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, - PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, - TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, - linear_color_stop, linear_gradient, list, percentage, point, prelude::*, pulsating_between, + Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, + EdgesRefinement, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, + MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, Task, + TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, + WindowHandle, div, linear_color_stop, linear_gradient, list, percentage, point, prelude::*, + pulsating_between, }; use language::Buffer; + +use language_model::LanguageModelRegistry; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; use project::Project; use prompt_store::PromptId; @@ -137,6 +141,9 @@ enum ThreadState { LoadError(LoadError), Unauthenticated { connection: Rc, + description: Option>, + configuration_view: Option, + _subscription: Option, }, ServerExited { status: ExitStatus, @@ -267,19 +274,16 @@ impl AcpThreadView { }; let result = match result.await { - Err(e) => { - let mut cx = cx.clone(); - if e.is::() { - this.update(&mut cx, |this, cx| { - this.thread_state = ThreadState::Unauthenticated { connection }; - cx.notify(); + Err(e) => match e.downcast::() { + Ok(err) => { + cx.update(|window, cx| { + Self::handle_auth_required(this, err, agent, connection, window, cx) }) - .ok(); + .log_err(); return; - } else { - Err(e) } - } + Err(err) => Err(err), + }, Ok(thread) => Ok(thread), }; @@ -345,6 +349,68 @@ impl AcpThreadView { ThreadState::Loading { _task: load_task } } + fn handle_auth_required( + this: WeakEntity, + err: AuthRequired, + agent: Rc, + connection: Rc, + window: &mut Window, + cx: &mut App, + ) { + let agent_name = agent.name(); + let (configuration_view, subscription) = if let Some(provider_id) = err.provider_id { + let registry = LanguageModelRegistry::global(cx); + + let sub = window.subscribe(®istry, cx, { + let provider_id = provider_id.clone(); + let this = this.clone(); + move |_, ev, window, cx| { + if let language_model::Event::ProviderStateChanged(updated_provider_id) = &ev { + if &provider_id == updated_provider_id { + this.update(cx, |this, cx| { + this.thread_state = Self::initial_state( + agent.clone(), + this.workspace.clone(), + this.project.clone(), + window, + cx, + ); + cx.notify(); + }) + .ok(); + } + } + } + }); + + let view = registry.read(cx).provider(&provider_id).map(|provider| { + provider.configuration_view( + language_model::ConfigurationViewTargetAgent::Other(agent_name), + window, + cx, + ) + }); + + (view, Some(sub)) + } else { + (None, None) + }; + + this.update(cx, |this, cx| { + this.thread_state = ThreadState::Unauthenticated { + connection, + configuration_view, + description: err + .description + .clone() + .map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))), + _subscription: subscription, + }; + cx.notify(); + }) + .ok(); + } + fn handle_load_error(&mut self, err: anyhow::Error, cx: &mut Context) { if let Some(load_err) = err.downcast_ref::() { self.thread_state = ThreadState::LoadError(load_err.clone()); @@ -369,7 +435,7 @@ impl AcpThreadView { ThreadState::Ready { thread, .. } => thread.read(cx).title(), ThreadState::Loading { .. } => "Loading…".into(), ThreadState::LoadError(_) => "Failed to load".into(), - ThreadState::Unauthenticated { .. } => "Not authenticated".into(), + ThreadState::Unauthenticated { .. } => "Authentication Required".into(), ThreadState::ServerExited { .. } => "Server exited unexpectedly".into(), } } @@ -708,7 +774,7 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) { - let ThreadState::Unauthenticated { ref connection } = self.thread_state else { + let ThreadState::Unauthenticated { ref connection, .. } = self.thread_state else { return; }; @@ -1841,19 +1907,53 @@ impl AcpThreadView { .into_any() } - fn render_pending_auth_state(&self) -> AnyElement { + fn render_auth_required_state( + &self, + connection: &Rc, + description: Option<&Entity>, + configuration_view: Option<&AnyView>, + window: &mut Window, + cx: &Context, + ) -> Div { v_flex() + .p_2() + .gap_2() + .flex_1() .items_center() .justify_center() - .child(self.render_error_agent_logo()) .child( - h_flex() - .mt_4() - .mb_1() + v_flex() + .items_center() .justify_center() - .child(Headline::new("Not Authenticated").size(HeadlineSize::Medium)), + .child(self.render_error_agent_logo()) + .child( + h_flex().mt_4().mb_1().justify_center().child( + Headline::new("Authentication Required").size(HeadlineSize::Medium), + ), + ) + .into_any(), ) - .into_any() + .children(description.map(|desc| { + div().text_ui(cx).text_center().child( + self.render_markdown(desc.clone(), default_markdown_style(false, window, cx)), + ) + })) + .children( + configuration_view + .cloned() + .map(|view| div().px_4().w_full().max_w_128().child(view)), + ) + .child(h_flex().mt_1p5().justify_center().children( + connection.auth_methods().into_iter().map(|method| { + Button::new(SharedString::from(method.id.0.clone()), method.name.clone()) + .on_click({ + let method_id = method.id.clone(); + cx.listener(move |this, _, window, cx| { + this.authenticate(method_id.clone(), window, cx) + }) + }) + }), + )) } fn render_server_exited(&self, status: ExitStatus, _cx: &Context) -> AnyElement { @@ -3347,26 +3447,18 @@ impl Render for AcpThreadView { .on_action(cx.listener(Self::toggle_burn_mode)) .bg(cx.theme().colors().panel_background) .child(match &self.thread_state { - ThreadState::Unauthenticated { connection } => v_flex() - .p_2() - .flex_1() - .items_center() - .justify_center() - .child(self.render_pending_auth_state()) - .child(h_flex().mt_1p5().justify_center().children( - connection.auth_methods().into_iter().map(|method| { - Button::new( - SharedString::from(method.id.0.clone()), - method.name.clone(), - ) - .on_click({ - let method_id = method.id.clone(); - cx.listener(move |this, _, window, cx| { - this.authenticate(method_id.clone(), window, cx) - }) - }) - }), - )), + ThreadState::Unauthenticated { + connection, + description, + configuration_view, + .. + } => self.render_auth_required_state( + &connection, + description.as_ref(), + configuration_view.as_ref(), + window, + cx, + ), ThreadState::Loading { .. } => v_flex().flex_1().child(self.render_empty_state(cx)), ThreadState::LoadError(e) => v_flex() .p_2() diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index b4ebb8206c..a0584f9e2e 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -137,7 +137,11 @@ impl AgentConfiguration { window: &mut Window, cx: &mut Context, ) { - let configuration_view = provider.configuration_view(window, cx); + let configuration_view = provider.configuration_view( + language_model::ConfigurationViewTargetAgent::ZedAgent, + window, + cx, + ); self.configuration_views_by_provider .insert(provider.id(), configuration_view); } diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index ce1c2203bf..8525d7f9e5 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -320,7 +320,7 @@ fn init_language_model_settings(cx: &mut App) { cx.subscribe( &LanguageModelRegistry::global(cx), |_, event: &language_model::Event, cx| match event { - language_model::Event::ProviderStateChanged + language_model::Event::ProviderStateChanged(_) | language_model::Event::AddedProvider(_) | language_model::Event::RemovedProvider(_) => { update_active_language_model_from_settings(cx); diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index bb8514a224..fa8ca490d8 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -104,7 +104,7 @@ impl LanguageModelPickerDelegate { window, |picker, _, event, window, cx| { match event { - language_model::Event::ProviderStateChanged + language_model::Event::ProviderStateChanged(_) | language_model::Event::AddedProvider(_) | language_model::Event::RemovedProvider(_) => { let query = picker.query(cx); diff --git a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs index b55ad4c895..0a34a29068 100644 --- a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs +++ b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs @@ -11,7 +11,7 @@ impl ApiKeysWithProviders { cx.subscribe( &LanguageModelRegistry::global(cx), |this: &mut Self, _registry, event: &language_model::Event, cx| match event { - language_model::Event::ProviderStateChanged + language_model::Event::ProviderStateChanged(_) | language_model::Event::AddedProvider(_) | language_model::Event::RemovedProvider(_) => { this.configured_providers = Self::compute_configured_providers(cx) diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs index f1629eeff8..23810b74f3 100644 --- a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs +++ b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs @@ -25,7 +25,7 @@ impl AgentPanelOnboarding { cx.subscribe( &LanguageModelRegistry::global(cx), |this: &mut Self, _registry, event: &language_model::Event, cx| match event { - language_model::Event::ProviderStateChanged + language_model::Event::ProviderStateChanged(_) | language_model::Event::AddedProvider(_) | language_model::Event::RemovedProvider(_) => { this.configured_providers = Self::compute_available_providers(cx) diff --git a/crates/gpui/src/subscription.rs b/crates/gpui/src/subscription.rs index a584f1a45f..bd869f8d32 100644 --- a/crates/gpui/src/subscription.rs +++ b/crates/gpui/src/subscription.rs @@ -201,3 +201,9 @@ impl Drop for Subscription { } } } + +impl std::fmt::Debug for Subscription { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Subscription").finish() + } +} diff --git a/crates/language_model/src/fake_provider.rs b/crates/language_model/src/fake_provider.rs index a9c7d5c034..67fba44887 100644 --- a/crates/language_model/src/fake_provider.rs +++ b/crates/language_model/src/fake_provider.rs @@ -1,8 +1,8 @@ use crate::{ - AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, - LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, - LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, - LanguageModelToolChoice, + AuthenticateError, ConfigurationViewTargetAgent, LanguageModel, LanguageModelCompletionError, + LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, + LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, + LanguageModelRequest, LanguageModelToolChoice, }; use futures::{FutureExt, StreamExt, channel::mpsc, future::BoxFuture, stream::BoxStream}; use gpui::{AnyView, App, AsyncApp, Entity, Task, Window}; @@ -62,7 +62,12 @@ impl LanguageModelProvider for FakeLanguageModelProvider { Task::ready(Ok(())) } - fn configuration_view(&self, _window: &mut Window, _: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: ConfigurationViewTargetAgent, + _window: &mut Window, + _: &mut App, + ) -> AnyView { unimplemented!() } diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index 1637d2de8a..70e42cb02d 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -634,7 +634,12 @@ pub trait LanguageModelProvider: 'static { } fn is_authenticated(&self, cx: &App) -> bool; fn authenticate(&self, cx: &mut App) -> Task>; - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView; + fn configuration_view( + &self, + target_agent: ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView; fn must_accept_terms(&self, _cx: &App) -> bool { false } @@ -648,6 +653,13 @@ pub trait LanguageModelProvider: 'static { fn reset_credentials(&self, cx: &mut App) -> Task>; } +#[derive(Default, Clone, Copy)] +pub enum ConfigurationViewTargetAgent { + #[default] + ZedAgent, + Other(&'static str), +} + #[derive(PartialEq, Eq)] pub enum LanguageModelProviderTosView { /// When there are some past interactions in the Agent Panel. diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index 7cf071808a..078b90a291 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -107,7 +107,7 @@ pub enum Event { InlineAssistantModelChanged, CommitMessageModelChanged, ThreadSummaryModelChanged, - ProviderStateChanged, + ProviderStateChanged(LanguageModelProviderId), AddedProvider(LanguageModelProviderId), RemovedProvider(LanguageModelProviderId), } @@ -148,8 +148,11 @@ impl LanguageModelRegistry { ) { let id = provider.id(); - let subscription = provider.subscribe(cx, |_, cx| { - cx.emit(Event::ProviderStateChanged); + let subscription = provider.subscribe(cx, { + let id = id.clone(); + move |_, cx| { + cx.emit(Event::ProviderStateChanged(id.clone())); + } }); if let Some(subscription) = subscription { subscription.detach(); diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index ef21e85f71..810d4a5f44 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -15,11 +15,11 @@ use gpui::{ }; use http_client::HttpClient; use language_model::{ - AuthenticateError, LanguageModel, LanguageModelCacheConfiguration, - LanguageModelCompletionError, LanguageModelId, LanguageModelName, LanguageModelProvider, - LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, - LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent, MessageContent, - RateLimiter, Role, + AuthenticateError, ConfigurationViewTargetAgent, LanguageModel, + LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelId, + LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, + LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, + LanguageModelToolResultContent, MessageContent, RateLimiter, Role, }; use language_model::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason}; use schemars::JsonSchema; @@ -153,29 +153,14 @@ impl State { return Task::ready(Ok(())); } - let credentials_provider = ::global(cx); - let api_url = AllLanguageModelSettings::get_global(cx) - .anthropic - .api_url - .clone(); + let key = AnthropicLanguageModelProvider::api_key(cx); cx.spawn(async move |this, cx| { - let (api_key, from_env) = if let Ok(api_key) = std::env::var(ANTHROPIC_API_KEY_VAR) { - (api_key, true) - } else { - let (_, api_key) = credentials_provider - .read_credentials(&api_url, &cx) - .await? - .ok_or(AuthenticateError::CredentialsNotFound)?; - ( - String::from_utf8(api_key).context("invalid {PROVIDER_NAME} API key")?, - false, - ) - }; + let key = key.await?; this.update(cx, |this, cx| { - this.api_key = Some(api_key); - this.api_key_from_env = from_env; + this.api_key = Some(key.key); + this.api_key_from_env = key.from_env; cx.notify(); })?; @@ -184,6 +169,11 @@ impl State { } } +pub struct ApiKey { + pub key: String, + pub from_env: bool, +} + impl AnthropicLanguageModelProvider { pub fn new(http_client: Arc, cx: &mut App) -> Self { let state = cx.new(|cx| State { @@ -206,6 +196,33 @@ impl AnthropicLanguageModelProvider { request_limiter: RateLimiter::new(4), }) } + + pub fn api_key(cx: &mut App) -> Task> { + let credentials_provider = ::global(cx); + let api_url = AllLanguageModelSettings::get_global(cx) + .anthropic + .api_url + .clone(); + + if let Ok(key) = std::env::var(ANTHROPIC_API_KEY_VAR) { + Task::ready(Ok(ApiKey { + key, + from_env: true, + })) + } else { + cx.spawn(async move |cx| { + let (_, api_key) = credentials_provider + .read_credentials(&api_url, &cx) + .await? + .ok_or(AuthenticateError::CredentialsNotFound)?; + + Ok(ApiKey { + key: String::from_utf8(api_key).context("invalid {PROVIDER_NAME} API key")?, + from_env: false, + }) + }) + } + } } impl LanguageModelProviderState for AnthropicLanguageModelProvider { @@ -299,8 +316,13 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { - cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) + fn configuration_view( + &self, + target_agent: ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { + cx.new(|cx| ConfigurationView::new(self.state.clone(), target_agent, window, cx)) .into() } @@ -902,12 +924,18 @@ struct ConfigurationView { api_key_editor: Entity, state: gpui::Entity, load_credentials_task: Option>, + target_agent: ConfigurationViewTargetAgent, } impl ConfigurationView { const PLACEHOLDER_TEXT: &'static str = "sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; - fn new(state: gpui::Entity, window: &mut Window, cx: &mut Context) -> Self { + fn new( + state: gpui::Entity, + target_agent: ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut Context, + ) -> Self { cx.observe(&state, |_, _, cx| { cx.notify(); }) @@ -939,6 +967,7 @@ impl ConfigurationView { }), state, load_credentials_task, + target_agent, } } @@ -1012,7 +1041,10 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new("To use Zed's agent with Anthropic, you need to add an API key. Follow these steps:")) + .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match self.target_agent { + ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic", + ConfigurationViewTargetAgent::Other(agent) => agent, + }))) .child( List::new() .child( @@ -1023,7 +1055,7 @@ impl Render for ConfigurationView { ) ) .child( - InstructionListItem::text_only("Paste your API key below and hit enter to start using the assistant") + InstructionListItem::text_only("Paste your API key below and hit enter to start using the agent") ) ) .child( diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index 6df96c5c56..4e6744d745 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -348,7 +348,12 @@ impl LanguageModelProvider for BedrockLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index c1337399f9..c3f4399832 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -391,7 +391,12 @@ impl LanguageModelProvider for CloudLanguageModelProvider { Task::ready(Ok(())) } - fn configuration_view(&self, _: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + _: &mut Window, + cx: &mut App, + ) -> AnyView { cx.new(|_| ConfigurationView::new(self.state.clone())) .into() } diff --git a/crates/language_models/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs index 73f73a9a31..eb12c0056f 100644 --- a/crates/language_models/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -176,7 +176,12 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider { Task::ready(Err(err.into())) } - fn configuration_view(&self, _: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + _: &mut Window, + cx: &mut App, + ) -> AnyView { let state = self.state.clone(); cx.new(|cx| ConfigurationView::new(state, cx)).into() } diff --git a/crates/language_models/src/provider/deepseek.rs b/crates/language_models/src/provider/deepseek.rs index a568ef4034..2b30d456ee 100644 --- a/crates/language_models/src/provider/deepseek.rs +++ b/crates/language_models/src/provider/deepseek.rs @@ -229,7 +229,12 @@ impl LanguageModelProvider for DeepSeekLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index b287e8181a..32f8838df7 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -277,7 +277,12 @@ impl LanguageModelProvider for GoogleLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } diff --git a/crates/language_models/src/provider/lmstudio.rs b/crates/language_models/src/provider/lmstudio.rs index 36a32ab941..7ac08f2c15 100644 --- a/crates/language_models/src/provider/lmstudio.rs +++ b/crates/language_models/src/provider/lmstudio.rs @@ -226,7 +226,12 @@ impl LanguageModelProvider for LmStudioLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, _window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + _window: &mut Window, + cx: &mut App, + ) -> AnyView { let state = self.state.clone(); cx.new(|cx| ConfigurationView::new(state, cx)).into() } diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index 4a0d740334..e1d55801eb 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/crates/language_models/src/provider/mistral.rs @@ -243,7 +243,12 @@ impl LanguageModelProvider for MistralLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index 0c2b1107b1..93844542ea 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -255,7 +255,12 @@ impl LanguageModelProvider for OllamaLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { let state = self.state.clone(); cx.new(|cx| ConfigurationView::new(state, window, cx)) .into() diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index eaf8d885b3..04d89f2db1 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -233,7 +233,12 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs index e2d3adb198..c6b980c3ec 100644 --- a/crates/language_models/src/provider/open_ai_compatible.rs +++ b/crates/language_models/src/provider/open_ai_compatible.rs @@ -243,7 +243,12 @@ impl LanguageModelProvider for OpenAiCompatibleLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index 3a492086f1..5d8bace6d3 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -306,7 +306,12 @@ impl LanguageModelProvider for OpenRouterLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } diff --git a/crates/language_models/src/provider/vercel.rs b/crates/language_models/src/provider/vercel.rs index 9f447cb68b..98e4f60b6b 100644 --- a/crates/language_models/src/provider/vercel.rs +++ b/crates/language_models/src/provider/vercel.rs @@ -230,7 +230,12 @@ impl LanguageModelProvider for VercelLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } diff --git a/crates/language_models/src/provider/x_ai.rs b/crates/language_models/src/provider/x_ai.rs index fed6fe92bf..2b8238cc5c 100644 --- a/crates/language_models/src/provider/x_ai.rs +++ b/crates/language_models/src/provider/x_ai.rs @@ -230,7 +230,12 @@ impl LanguageModelProvider for XAiLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + fn configuration_view( + &self, + _target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut App, + ) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index bb1932bdf2..d700fa08bd 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -329,7 +329,11 @@ impl AiConfigurationModal { cx: &mut Context, ) -> Self { let focus_handle = cx.focus_handle(); - let configuration_view = selected_provider.configuration_view(window, cx); + let configuration_view = selected_provider.configuration_view( + language_model::ConfigurationViewTargetAgent::ZedAgent, + window, + cx, + ); Self { focus_handle, From c5991e74bb6f305c299684dc7ac3f6ee9055efcd Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 19 Aug 2025 02:27:40 +0530 Subject: [PATCH 449/693] project: Handle `textDocument/didSave` and `textDocument/didChange` (un)registration and usage correctly (#36441) Follow-up of https://github.com/zed-industries/zed/pull/35306 This PR contains two changes: Both changes are inspired from: https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/textSynchronization.ts 1. Handling `textDocument/didSave` and `textDocument/didChange` registration and unregistration correctly: ```rs #[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] #[serde(untagged)] pub enum TextDocumentSyncCapability { Kind(TextDocumentSyncKind), Options(TextDocumentSyncOptions), } ``` - `textDocument/didSave` dynamic registration contains "includeText" - `textDocument/didChange` dynamic registration contains "syncKind" While storing this to Language Server, we use `TextDocumentSyncCapability::Options` instead of `TextDocumentSyncCapability::Kind` since it also include [change](https://github.com/gluon-lang/lsp-types/blob/be7336e92a6ad23f214df19bcdceab17f39531a9/src/lib.rs#L1714-L1717) field as `TextDocumentSyncCapability::Kind` as well as [save](https://github.com/gluon-lang/lsp-types/blob/be7336e92a6ad23f214df19bcdceab17f39531a9/src/lib.rs#L1727-L1729) field as `TextDocumentSyncSaveOptions`. This way while registering or unregistering both of them, we don't accidentaly mess with other data. So, if at intialization we end up getting `TextDocumentSyncCapability::Kind` and we receive any above kind of dynamic registration, we change `TextDocumentSyncCapability::Kind` to `TextDocumentSyncCapability::Options` so we can store more data anyway. 2. Modify `include_text` method to only depend on `TextDocumentSyncSaveOptions`, instead of depending on `TextDocumentSyncKind`. Idea behind this is, `TextDocumentSyncSaveOptions` should be responsible for "textDocument/didSave" notification, and `TextDocumentSyncKind` should be responsible for "textDocument/didChange", which it already is: https://github.com/zed-industries/zed/blob/4b79eade1da2f5f7dfa18208cf882c8e6ca8a97f/crates/project/src/lsp_store.rs#L7324-L7331 Release Notes: - N/A --- crates/project/src/lsp_store.rs | 72 +++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 802b304e94..11c78aad8d 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -11820,8 +11820,28 @@ impl LspStore { .transpose()? { server.update_capabilities(|capabilities| { + let mut sync_options = + Self::take_text_document_sync_options(capabilities); + sync_options.change = Some(sync_kind); capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)); + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + }); + notify_server_capabilities_updated(&server, cx); + } + } + "textDocument/didSave" => { + if let Some(save_options) = reg + .register_options + .and_then(|opts| opts.get("includeText").cloned()) + .map(serde_json::from_value::) + .transpose()? + { + server.update_capabilities(|capabilities| { + let mut sync_options = + Self::take_text_document_sync_options(capabilities); + sync_options.save = Some(save_options); + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); }); notify_server_capabilities_updated(&server, cx); } @@ -11973,7 +11993,19 @@ impl LspStore { } "textDocument/didChange" => { server.update_capabilities(|capabilities| { - capabilities.text_document_sync = None; + let mut sync_options = Self::take_text_document_sync_options(capabilities); + sync_options.change = None; + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/didSave" => { + server.update_capabilities(|capabilities| { + let mut sync_options = Self::take_text_document_sync_options(capabilities); + sync_options.save = None; + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); }); notify_server_capabilities_updated(&server, cx); } @@ -12001,6 +12033,20 @@ impl LspStore { Ok(()) } + + fn take_text_document_sync_options( + capabilities: &mut lsp::ServerCapabilities, + ) -> lsp::TextDocumentSyncOptions { + match capabilities.text_document_sync.take() { + Some(lsp::TextDocumentSyncCapability::Options(sync_options)) => sync_options, + Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)) => { + let mut sync_options = lsp::TextDocumentSyncOptions::default(); + sync_options.change = Some(sync_kind); + sync_options + } + None => lsp::TextDocumentSyncOptions::default(), + } + } } // Registration with empty capabilities should be ignored. @@ -13103,24 +13149,18 @@ async fn populate_labels_for_symbols( fn include_text(server: &lsp::LanguageServer) -> Option { match server.capabilities().text_document_sync.as_ref()? { - lsp::TextDocumentSyncCapability::Kind(kind) => match *kind { - lsp::TextDocumentSyncKind::NONE => None, - lsp::TextDocumentSyncKind::FULL => Some(true), - lsp::TextDocumentSyncKind::INCREMENTAL => Some(false), - _ => None, - }, - lsp::TextDocumentSyncCapability::Options(options) => match options.save.as_ref()? { - lsp::TextDocumentSyncSaveOptions::Supported(supported) => { - if *supported { - Some(true) - } else { - None - } - } + lsp::TextDocumentSyncCapability::Options(opts) => match opts.save.as_ref()? { + // Server wants didSave but didn't specify includeText. + lsp::TextDocumentSyncSaveOptions::Supported(true) => Some(false), + // Server doesn't want didSave at all. + lsp::TextDocumentSyncSaveOptions::Supported(false) => None, + // Server provided SaveOptions. lsp::TextDocumentSyncSaveOptions::SaveOptions(save_options) => { Some(save_options.include_text.unwrap_or(false)) } }, + // We do not have any save info. Kind affects didChange only. + lsp::TextDocumentSyncCapability::Kind(_) => None, } } From 97f784dedf58a1f1337f6824918d73deb5abab97 Mon Sep 17 00:00:00 2001 From: localcc Date: Mon, 18 Aug 2025 23:30:02 +0200 Subject: [PATCH 450/693] Fix early dispatch crash on windows (#36445) Closes #36384 Release Notes: - N/A --- crates/gpui/src/platform/windows/events.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 4ab257d27a..9b25ab360e 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -100,6 +100,7 @@ impl WindowsWindowInner { WM_SETCURSOR => self.handle_set_cursor(handle, lparam), WM_SETTINGCHANGE => self.handle_system_settings_changed(handle, wparam, lparam), WM_INPUTLANGCHANGE => self.handle_input_language_changed(lparam), + WM_SHOWWINDOW => self.handle_window_visibility_changed(handle, wparam), WM_GPUI_CURSOR_STYLE_CHANGED => self.handle_cursor_changed(lparam), WM_GPUI_FORCE_UPDATE_WINDOW => self.draw_window(handle, true), _ => None, @@ -1160,6 +1161,13 @@ impl WindowsWindowInner { Some(0) } + fn handle_window_visibility_changed(&self, handle: HWND, wparam: WPARAM) -> Option { + if wparam.0 == 1 { + self.draw_window(handle, false); + } + None + } + fn handle_device_change_msg(&self, handle: HWND, wparam: WPARAM) -> Option { if wparam.0 == DBT_DEVNODES_CHANGED as usize { // The reason for sending this message is to actually trigger a redraw of the window. From eecf142f06a1f7d073242946b98e389dd94d0011 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Tue, 19 Aug 2025 00:49:22 +0300 Subject: [PATCH 451/693] Explicitly allow `clippy::new_without_default` style lint (#36434) Discussed in #36432 Release Notes: - N/A --------- Co-authored-by: Marshall Bowers --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 914f9e6837..3edd8d802c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -832,6 +832,8 @@ new_ret_no_self = { level = "allow" } # compared to Iterator::next. Yet, clippy complains about those. should_implement_trait = { level = "allow" } let_underscore_future = "allow" +# It doesn't make sense to implement `Default` unilaterally. +new_without_default = "allow" # in Rust it can be very tedious to reduce argument count without # running afoul of the borrow checker. From 9e0e233319d06956bb28fb0609bb843e89d1a812 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 18 Aug 2025 23:54:35 +0200 Subject: [PATCH 452/693] Fix clippy::needless_borrow lint violations (#36444) Release Notes: - N/A --- Cargo.toml | 1 + crates/acp_thread/src/acp_thread.rs | 8 +- crates/acp_thread/src/diff.rs | 10 +-- crates/acp_thread/src/mention.rs | 2 +- crates/action_log/src/action_log.rs | 8 +- .../src/activity_indicator.rs | 6 +- crates/agent/src/thread.rs | 16 ++-- crates/agent2/src/agent.rs | 2 +- crates/agent2/src/templates.rs | 2 +- crates/agent2/src/thread.rs | 10 +-- .../src/tools/context_server_registry.rs | 2 +- crates/agent2/src/tools/edit_file_tool.rs | 2 +- crates/agent2/src/tools/terminal_tool.rs | 2 +- crates/agent_servers/src/acp.rs | 4 +- crates/agent_servers/src/claude.rs | 2 +- .../agent_ui/src/acp/completion_provider.rs | 4 +- crates/agent_ui/src/acp/message_editor.rs | 2 +- crates/agent_ui/src/acp/model_selector.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 16 ++-- crates/agent_ui/src/active_thread.rs | 6 +- crates/agent_ui/src/agent_diff.rs | 20 ++--- crates/agent_ui/src/agent_panel.rs | 6 +- crates/agent_ui/src/buffer_codegen.rs | 6 +- .../src/context_picker/completion_provider.rs | 4 +- .../src/context_picker/file_context_picker.rs | 4 +- .../context_picker/symbol_context_picker.rs | 2 +- .../context_picker/thread_context_picker.rs | 6 +- crates/agent_ui/src/context_strip.rs | 4 +- crates/agent_ui/src/inline_assistant.rs | 8 +- crates/agent_ui/src/inline_prompt_editor.rs | 2 +- .../agent_ui/src/language_model_selector.rs | 2 +- crates/agent_ui/src/message_editor.rs | 11 ++- crates/agent_ui/src/slash_command_picker.rs | 2 +- crates/agent_ui/src/terminal_codegen.rs | 2 +- crates/agent_ui/src/ui/context_pill.rs | 6 +- .../src/assistant_context.rs | 16 ++-- .../src/assistant_context_tests.rs | 2 +- .../src/context_server_command.rs | 2 +- .../src/diagnostics_command.rs | 6 +- crates/assistant_tools/src/edit_file_tool.rs | 8 +- crates/assistant_tools/src/grep_tool.rs | 22 ++--- .../src/project_notifications_tool.rs | 2 +- crates/assistant_tools/src/terminal_tool.rs | 2 +- crates/breadcrumbs/src/breadcrumbs.rs | 2 +- crates/buffer_diff/src/buffer_diff.rs | 26 +++--- crates/cli/src/main.rs | 4 +- crates/client/src/client.rs | 16 ++-- crates/collab/src/db/queries/projects.rs | 6 +- crates/collab/src/db/queries/rooms.rs | 6 +- .../src/chat_panel/message_editor.rs | 4 +- crates/collab_ui/src/collab_panel.rs | 2 +- crates/collab_ui/src/notification_panel.rs | 2 +- crates/context_server/src/listener.rs | 4 +- crates/context_server/src/types.rs | 2 +- crates/copilot/src/copilot_chat.rs | 4 +- crates/dap/src/adapters.rs | 2 +- crates/dap_adapters/src/go.rs | 2 +- crates/dap_adapters/src/javascript.rs | 2 +- crates/dap_adapters/src/python.rs | 6 +- crates/db/src/db.rs | 10 +-- crates/debugger_tools/src/dap_log.rs | 8 +- crates/debugger_ui/src/debugger_panel.rs | 18 ++-- crates/debugger_ui/src/new_process_modal.rs | 13 +-- crates/debugger_ui/src/persistence.rs | 2 +- crates/debugger_ui/src/session/running.rs | 12 +-- .../src/session/running/breakpoint_list.rs | 4 +- .../src/session/running/console.rs | 4 +- .../src/session/running/memory_view.rs | 4 +- .../src/session/running/variable_list.rs | 4 +- crates/debugger_ui/src/tests/attach_modal.rs | 4 +- .../src/tests/new_process_modal.rs | 2 +- crates/diagnostics/src/diagnostic_renderer.rs | 4 +- crates/diagnostics/src/diagnostics.rs | 4 +- crates/docs_preprocessor/src/main.rs | 6 +- crates/editor/src/clangd_ext.rs | 2 +- crates/editor/src/code_completion_tests.rs | 4 +- crates/editor/src/code_context_menus.rs | 2 +- .../src/display_map/custom_highlights.rs | 8 +- crates/editor/src/display_map/invisibles.rs | 6 +- crates/editor/src/display_map/wrap_map.rs | 2 +- crates/editor/src/editor.rs | 67 +++++++-------- crates/editor/src/editor_tests.rs | 42 ++++----- crates/editor/src/element.rs | 22 ++--- crates/editor/src/hover_links.rs | 2 +- crates/editor/src/items.rs | 12 +-- crates/editor/src/jsx_tag_auto_close.rs | 2 +- crates/editor/src/lsp_colors.rs | 2 +- crates/editor/src/lsp_ext.rs | 2 +- crates/editor/src/mouse_context_menu.rs | 4 +- crates/editor/src/movement.rs | 12 +-- crates/editor/src/proposed_changes_editor.rs | 6 +- crates/editor/src/rust_analyzer_ext.rs | 12 +-- crates/editor/src/signature_help.rs | 2 +- crates/editor/src/test.rs | 2 +- crates/eval/src/eval.rs | 2 +- crates/eval/src/example.rs | 4 +- crates/eval/src/instance.rs | 14 +-- crates/extension/src/extension_builder.rs | 2 +- crates/extension_host/src/extension_host.rs | 4 +- crates/extension_host/src/headless_host.rs | 2 +- crates/file_finder/src/file_finder.rs | 10 +-- crates/file_finder/src/file_finder_tests.rs | 4 +- crates/file_finder/src/open_path_prompt.rs | 2 +- crates/fs/src/fs.rs | 6 +- crates/git/src/repository.rs | 8 +- crates/git/src/status.rs | 2 +- .../src/git_hosting_providers.rs | 2 +- .../src/providers/chromium.rs | 2 +- .../src/providers/github.rs | 4 +- crates/git_ui/src/commit_view.rs | 6 +- crates/git_ui/src/conflict_view.rs | 10 +-- crates/git_ui/src/file_diff_view.rs | 8 +- crates/git_ui/src/git_panel.rs | 16 ++-- crates/git_ui/src/picker_prompt.rs | 2 +- crates/git_ui/src/project_diff.rs | 6 +- crates/git_ui/src/text_diff_view.rs | 4 +- crates/gpui/build.rs | 4 +- crates/gpui/examples/input.rs | 4 +- crates/gpui/src/app.rs | 4 +- crates/gpui/src/app/entity_map.rs | 2 +- crates/gpui/src/elements/div.rs | 2 +- crates/gpui/src/inspector.rs | 2 +- crates/gpui/src/key_dispatch.rs | 4 +- crates/gpui/src/keymap.rs | 2 +- crates/gpui/src/path_builder.rs | 2 +- crates/gpui/src/platform.rs | 2 +- crates/gpui/src/platform/linux/platform.rs | 2 +- .../gpui/src/platform/linux/wayland/client.rs | 15 ++-- .../gpui/src/platform/linux/wayland/cursor.rs | 2 +- crates/gpui/src/platform/linux/x11/client.rs | 6 +- crates/gpui/src/platform/linux/x11/event.rs | 4 +- crates/gpui/src/platform/linux/x11/window.rs | 6 +- .../gpui/src/platform/mac/metal_renderer.rs | 16 ++-- crates/gpui/src/platform/mac/platform.rs | 10 +-- crates/gpui/src/platform/mac/window.rs | 8 +- .../gpui/src/platform/windows/direct_write.rs | 4 +- .../src/platform/windows/directx_renderer.rs | 4 +- crates/gpui/src/tab_stop.rs | 2 +- crates/gpui_macros/src/test.rs | 2 +- crates/install_cli/src/install_cli.rs | 2 +- crates/jj/src/jj_store.rs | 2 +- crates/language/src/buffer.rs | 8 +- crates/language/src/language.rs | 2 +- crates/language/src/language_registry.rs | 2 +- crates/language/src/language_settings.rs | 4 +- crates/language/src/syntax_map.rs | 2 +- crates/language/src/text_diff.rs | 8 +- .../src/language_extension.rs | 2 +- crates/language_model/src/request.rs | 6 +- .../language_models/src/provider/anthropic.rs | 6 +- .../language_models/src/provider/bedrock.rs | 8 +- crates/language_models/src/provider/cloud.rs | 2 +- .../language_models/src/provider/deepseek.rs | 6 +- crates/language_models/src/provider/google.rs | 6 +- .../language_models/src/provider/mistral.rs | 6 +- .../language_models/src/provider/open_ai.rs | 6 +- .../src/provider/open_ai_compatible.rs | 6 +- .../src/provider/open_router.rs | 6 +- crates/language_models/src/provider/vercel.rs | 6 +- crates/language_models/src/provider/x_ai.rs | 6 +- crates/language_tools/src/lsp_log.rs | 2 +- crates/languages/src/css.rs | 2 +- crates/languages/src/github_download.rs | 6 +- crates/languages/src/json.rs | 2 +- crates/languages/src/python.rs | 2 +- crates/languages/src/rust.rs | 6 +- crates/languages/src/tailwind.rs | 2 +- crates/languages/src/typescript.rs | 2 +- crates/languages/src/yaml.rs | 2 +- crates/livekit_client/src/test.rs | 4 +- crates/markdown/src/markdown.rs | 2 +- crates/markdown/src/parser.rs | 2 +- .../markdown_preview/src/markdown_renderer.rs | 4 +- crates/migrator/src/migrator.rs | 8 +- crates/multi_buffer/src/anchor.rs | 4 +- crates/multi_buffer/src/multi_buffer.rs | 38 ++++---- crates/multi_buffer/src/multi_buffer_tests.rs | 19 ++-- crates/multi_buffer/src/position.rs | 6 +- crates/onboarding/src/onboarding.rs | 2 +- crates/onboarding/src/welcome.rs | 2 +- crates/outline_panel/src/outline_panel.rs | 86 +++++++++---------- crates/project/src/context_server_store.rs | 6 +- .../project/src/debugger/breakpoint_store.rs | 6 +- crates/project/src/debugger/dap_store.rs | 4 +- crates/project/src/debugger/locators/cargo.rs | 4 +- crates/project/src/debugger/session.rs | 4 +- crates/project/src/environment.rs | 2 +- crates/project/src/git_store.rs | 20 ++--- crates/project/src/git_store/git_traversal.rs | 2 +- crates/project/src/image_store.rs | 2 +- crates/project/src/lsp_command.rs | 2 +- crates/project/src/lsp_store.rs | 26 +++--- crates/project/src/manifest_tree.rs | 2 +- .../project/src/manifest_tree/server_tree.rs | 8 +- crates/project/src/project.rs | 17 ++-- crates/project/src/project_settings.rs | 4 +- crates/project/src/task_inventory.rs | 2 +- crates/project_panel/src/project_panel.rs | 10 +-- crates/project_symbols/src/project_symbols.rs | 2 +- crates/recent_projects/src/remote_servers.rs | 2 +- crates/remote/src/ssh_session.rs | 30 +++---- crates/remote_server/src/headless_project.rs | 4 +- crates/remote_server/src/unix.rs | 9 +- crates/repl/src/notebook/notebook_ui.rs | 2 +- crates/rope/src/chunk.rs | 4 +- crates/search/src/search.rs | 2 +- crates/search/src/search_bar.rs | 2 +- crates/semantic_index/src/summary_index.rs | 4 +- crates/settings/src/keymap_file.rs | 6 +- crates/settings_ui/src/keybindings.rs | 25 +++--- crates/settings_ui/src/ui_components/table.rs | 2 +- crates/streaming_diff/src/streaming_diff.rs | 30 +++---- crates/task/src/vscode_debug_format.rs | 2 +- crates/terminal/src/terminal.rs | 6 +- crates/terminal_view/src/terminal_panel.rs | 6 +- crates/terminal_view/src/terminal_view.rs | 6 +- crates/title_bar/src/title_bar.rs | 4 +- crates/ui/src/components/indent_guides.rs | 4 +- crates/ui/src/components/keybinding.rs | 4 +- crates/vim/src/command.rs | 2 +- crates/vim/src/motion.rs | 10 +-- crates/vim/src/normal/increment.rs | 4 +- crates/vim/src/normal/scroll.rs | 4 +- crates/vim/src/state.rs | 8 +- crates/vim/src/test/neovim_connection.rs | 4 +- crates/vim/src/vim.rs | 6 +- crates/vim/src/visual.rs | 2 +- crates/workspace/src/notifications.rs | 2 +- crates/workspace/src/pane.rs | 5 +- crates/workspace/src/workspace.rs | 22 ++--- crates/worktree/src/worktree.rs | 12 +-- crates/zed/src/main.rs | 32 +++---- crates/zed/src/reliability.rs | 4 +- crates/zed/src/zed.rs | 4 +- crates/zed/src/zed/component_preview.rs | 2 +- .../zed/src/zed/edit_prediction_registry.rs | 4 +- crates/zed/src/zed/open_listener.rs | 4 +- crates/zed/src/zed/quick_action_bar.rs | 2 +- crates/zeta/src/license_detection.rs | 2 +- crates/zeta/src/zeta.rs | 14 +-- crates/zeta_cli/src/main.rs | 6 +- crates/zlog/src/filter.rs | 2 +- 242 files changed, 801 insertions(+), 821 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3edd8d802c..3854ebe010 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -824,6 +824,7 @@ module_inception = { level = "deny" } question_mark = { level = "deny" } redundant_closure = { level = "deny" } declare_interior_mutable_const = { level = "deny" } +needless_borrow = { level = "warn"} # Individual rules that have violations in the codebase: type_complexity = "allow" # We often return trait objects from `new` functions. diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index e104c40bf2..8bc0635475 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -485,7 +485,7 @@ impl ContentBlock { } fn resource_link_md(uri: &str) -> String { - if let Some(uri) = MentionUri::parse(&uri).log_err() { + if let Some(uri) = MentionUri::parse(uri).log_err() { uri.as_link().to_string() } else { uri.to_string() @@ -1416,7 +1416,7 @@ impl AcpThread { fn user_message(&self, id: &UserMessageId) -> Option<&UserMessage> { self.entries.iter().find_map(|entry| { if let AgentThreadEntry::UserMessage(message) = entry { - if message.id.as_ref() == Some(&id) { + if message.id.as_ref() == Some(id) { Some(message) } else { None @@ -1430,7 +1430,7 @@ impl AcpThread { fn user_message_mut(&mut self, id: &UserMessageId) -> Option<(usize, &mut UserMessage)> { self.entries.iter_mut().enumerate().find_map(|(ix, entry)| { if let AgentThreadEntry::UserMessage(message) = entry { - if message.id.as_ref() == Some(&id) { + if message.id.as_ref() == Some(id) { Some((ix, message)) } else { None @@ -2356,7 +2356,7 @@ mod tests { fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { let sessions = self.sessions.lock(); - let thread = sessions.get(&session_id).unwrap().clone(); + let thread = sessions.get(session_id).unwrap().clone(); cx.spawn(async move |cx| { thread diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs index a2c2d6c322..e5f71d2109 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -71,8 +71,8 @@ impl Diff { let hunk_ranges = { let buffer = new_buffer.read(cx); let diff = buffer_diff.read(cx); - diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, cx) - .map(|diff_hunk| diff_hunk.buffer_range.to_point(&buffer)) + diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx) + .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer)) .collect::>() }; @@ -306,13 +306,13 @@ impl PendingDiff { let buffer = self.buffer.read(cx); let diff = self.diff.read(cx); let mut ranges = diff - .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, cx) - .map(|diff_hunk| diff_hunk.buffer_range.to_point(&buffer)) + .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx) + .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer)) .collect::>(); ranges.extend( self.revealed_ranges .iter() - .map(|range| range.to_point(&buffer)), + .map(|range| range.to_point(buffer)), ); ranges.sort_unstable_by_key(|range| (range.start, Reverse(range.end))); diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index b9b021c4ca..17bc265fac 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -146,7 +146,7 @@ impl MentionUri { FileIcons::get_folder_icon(false, cx) .unwrap_or_else(|| IconName::Folder.path().into()) } else { - FileIcons::get_icon(&abs_path, cx) + FileIcons::get_icon(abs_path, cx) .unwrap_or_else(|| IconName::File.path().into()) } } diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs index c4eaffc228..20ba9586ea 100644 --- a/crates/action_log/src/action_log.rs +++ b/crates/action_log/src/action_log.rs @@ -290,7 +290,7 @@ impl ActionLog { } _ = git_diff_updates_rx.changed().fuse() => { if let Some(git_diff) = git_diff.as_ref() { - Self::keep_committed_edits(&this, &buffer, &git_diff, cx).await?; + Self::keep_committed_edits(&this, &buffer, git_diff, cx).await?; } } } @@ -498,7 +498,7 @@ impl ActionLog { new: new_range, }, &new_diff_base, - &buffer_snapshot.as_rope(), + buffer_snapshot.as_rope(), )); } unreviewed_edits @@ -964,7 +964,7 @@ impl TrackedBuffer { fn has_edits(&self, cx: &App) -> bool { self.diff .read(cx) - .hunks(&self.buffer.read(cx), cx) + .hunks(self.buffer.read(cx), cx) .next() .is_some() } @@ -2268,7 +2268,7 @@ mod tests { log::info!("quiescing..."); cx.run_until_parked(); action_log.update(cx, |log, cx| { - let tracked_buffer = log.tracked_buffers.get(&buffer).unwrap(); + let tracked_buffer = log.tracked_buffers.get(buffer).unwrap(); let mut old_text = tracked_buffer.diff_base.clone(); let new_text = buffer.read(cx).as_rope(); for edit in tracked_buffer.unreviewed_edits.edits() { diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 7c562aaba4..090252d338 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -702,7 +702,7 @@ impl ActivityIndicator { on_click: Some(Arc::new(|this, window, cx| { this.dismiss_error_message(&DismissErrorMessage, window, cx) })), - tooltip_message: Some(Self::version_tooltip_message(&version)), + tooltip_message: Some(Self::version_tooltip_message(version)), }), AutoUpdateStatus::Installing { version } => Some(Content { icon: Some( @@ -714,13 +714,13 @@ impl ActivityIndicator { on_click: Some(Arc::new(|this, window, cx| { this.dismiss_error_message(&DismissErrorMessage, window, cx) })), - tooltip_message: Some(Self::version_tooltip_message(&version)), + tooltip_message: Some(Self::version_tooltip_message(version)), }), AutoUpdateStatus::Updated { version } => Some(Content { icon: None, message: "Click to restart and update Zed".to_string(), on_click: Some(Arc::new(move |_, _, cx| workspace::reload(cx))), - tooltip_message: Some(Self::version_tooltip_message(&version)), + tooltip_message: Some(Self::version_tooltip_message(version)), }), AutoUpdateStatus::Errored => Some(Content { icon: Some( diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 5491842185..469135a967 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -1692,7 +1692,7 @@ impl Thread { self.last_received_chunk_at = Some(Instant::now()); let task = cx.spawn(async move |thread, cx| { - let stream_completion_future = model.stream_completion(request, &cx); + let stream_completion_future = model.stream_completion(request, cx); let initial_token_usage = thread.read_with(cx, |thread, _cx| thread.cumulative_token_usage); let stream_completion = async { @@ -1824,7 +1824,7 @@ impl Thread { let streamed_input = if tool_use.is_input_complete { None } else { - Some((&tool_use.input).clone()) + Some(tool_use.input.clone()) }; let ui_text = thread.tool_use.request_tool_use( @@ -2051,7 +2051,7 @@ impl Thread { retry_scheduled = thread .handle_retryable_error_with_delay( - &completion_error, + completion_error, Some(retry_strategy), model.clone(), intent, @@ -2130,7 +2130,7 @@ impl Thread { self.pending_summary = cx.spawn(async move |this, cx| { let result = async { - let mut messages = model.model.stream_completion(request, &cx).await?; + let mut messages = model.model.stream_completion(request, cx).await?; let mut new_summary = String::new(); while let Some(event) = messages.next().await { @@ -2456,7 +2456,7 @@ impl Thread { // which result to prefer (the old task could complete after the new one, resulting in a // stale summary). self.detailed_summary_task = cx.spawn(async move |thread, cx| { - let stream = model.stream_completion_text(request, &cx); + let stream = model.stream_completion_text(request, cx); let Some(mut messages) = stream.await.log_err() else { thread .update(cx, |thread, _cx| { @@ -4043,7 +4043,7 @@ fn main() {{ }); let fake_model = model.as_fake(); - simulate_successful_response(&fake_model, cx); + simulate_successful_response(fake_model, cx); // Should start generating summary when there are >= 2 messages thread.read_with(cx, |thread, _| { @@ -4138,7 +4138,7 @@ fn main() {{ }); let fake_model = model.as_fake(); - simulate_successful_response(&fake_model, cx); + simulate_successful_response(fake_model, cx); thread.read_with(cx, |thread, _| { // State is still Error, not Generating @@ -5420,7 +5420,7 @@ fn main() {{ }); let fake_model = model.as_fake(); - simulate_successful_response(&fake_model, cx); + simulate_successful_response(fake_model, cx); thread.read_with(cx, |thread, _| { assert!(matches!(thread.summary(), ThreadSummary::Generating)); diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index af740d9901..985de4d123 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -91,7 +91,7 @@ impl LanguageModels { for provider in &providers { for model in provider.recommended_models(cx) { recommended_models.insert(model.id()); - recommended.push(Self::map_language_model_to_info(&model, &provider)); + recommended.push(Self::map_language_model_to_info(&model, provider)); } } if !recommended.is_empty() { diff --git a/crates/agent2/src/templates.rs b/crates/agent2/src/templates.rs index a63f0ad206..72a8f6633c 100644 --- a/crates/agent2/src/templates.rs +++ b/crates/agent2/src/templates.rs @@ -62,7 +62,7 @@ fn contains( handlebars::RenderError::new("contains: missing or invalid query parameter") })?; - if list.contains(&query) { + if list.contains(query) { out.write("true")?; } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 429832010b..eed374e396 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -173,7 +173,7 @@ impl UserMessage { &mut symbol_context, "\n{}", MarkdownCodeBlock { - tag: &codeblock_tag(&abs_path, None), + tag: &codeblock_tag(abs_path, None), text: &content.to_string(), } ) @@ -189,8 +189,8 @@ impl UserMessage { &mut rules_context, "\n{}", MarkdownCodeBlock { - tag: &codeblock_tag(&path, Some(line_range)), - text: &content + tag: &codeblock_tag(path, Some(line_range)), + text: content } ) .ok(); @@ -207,7 +207,7 @@ impl UserMessage { "\n{}", MarkdownCodeBlock { tag: "", - text: &content + text: content } ) .ok(); @@ -1048,7 +1048,7 @@ impl Thread { tools, tool_choice: None, stop: Vec::new(), - temperature: AgentSettings::temperature_for_model(&model, cx), + temperature: AgentSettings::temperature_for_model(model, cx), thinking_allowed: true, }; diff --git a/crates/agent2/src/tools/context_server_registry.rs b/crates/agent2/src/tools/context_server_registry.rs index db39e9278c..ddeb08a046 100644 --- a/crates/agent2/src/tools/context_server_registry.rs +++ b/crates/agent2/src/tools/context_server_registry.rs @@ -103,7 +103,7 @@ impl ContextServerRegistry { self.reload_tools_for_server(server_id.clone(), cx); } ContextServerStatus::Stopped | ContextServerStatus::Error(_) => { - self.registered_servers.remove(&server_id); + self.registered_servers.remove(server_id); cx.notify(); } } diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index c55e503d76..e70e5e8a14 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -471,7 +471,7 @@ fn resolve_path( let parent_entry = parent_project_path .as_ref() - .and_then(|path| project.entry_for_path(&path, cx)) + .and_then(|path| project.entry_for_path(path, cx)) .context("Can't create file: parent directory doesn't exist")?; anyhow::ensure!( diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs index ecb855ac34..ac79874c36 100644 --- a/crates/agent2/src/tools/terminal_tool.rs +++ b/crates/agent2/src/tools/terminal_tool.rs @@ -80,7 +80,7 @@ impl AgentTool for TerminalTool { let first_line = lines.next().unwrap_or_default(); let remaining_line_count = lines.count(); match remaining_line_count { - 0 => MarkdownInlineCode(&first_line).to_string().into(), + 0 => MarkdownInlineCode(first_line).to_string().into(), 1 => MarkdownInlineCode(&format!( "{} - {} more line", first_line, remaining_line_count diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index 00e3e3df50..1cfb1fcabf 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -19,14 +19,14 @@ pub async fn connect( root_dir: &Path, cx: &mut AsyncApp, ) -> Result> { - let conn = v1::AcpConnection::stdio(server_name, command.clone(), &root_dir, cx).await; + let conn = v1::AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await; match conn { Ok(conn) => Ok(Rc::new(conn) as _), Err(err) if err.is::() => { // Consider re-using initialize response and subprocess when adding another version here let conn: Rc = - Rc::new(v0::AcpConnection::stdio(server_name, command, &root_dir, cx).await?); + Rc::new(v0::AcpConnection::stdio(server_name, command, root_dir, cx).await?); Ok(conn) } Err(err) => Err(err), diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index d80d040aad..354bda494d 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -291,7 +291,7 @@ impl AgentConnection for ClaudeAgentConnection { fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) { let sessions = self.sessions.borrow(); - let Some(session) = sessions.get(&session_id) else { + let Some(session) = sessions.get(session_id) else { log::warn!("Attempted to cancel nonexistent session {}", session_id); return; }; diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 8a413fc91e..e2ddd03f27 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -552,11 +552,11 @@ fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId); let mut label = CodeLabel::default(); - label.push_str(&file_name, None); + label.push_str(file_name, None); label.push_str(" ", None); if let Some(directory) = directory { - label.push_str(&directory, comment_id); + label.push_str(directory, comment_id); } label.filter_range = 0..label.text().len(); diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 299f0c30be..d592231726 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1191,7 +1191,7 @@ impl MentionSet { }) } MentionUri::Fetch { url } => { - let Some(content) = self.fetch_results.get(&url).cloned() else { + let Some(content) = self.fetch_results.get(url).cloned() else { return Task::ready(Err(anyhow!("missing fetch result"))); }; let uri = uri.clone(); diff --git a/crates/agent_ui/src/acp/model_selector.rs b/crates/agent_ui/src/acp/model_selector.rs index 563afee65f..77c88c461d 100644 --- a/crates/agent_ui/src/acp/model_selector.rs +++ b/crates/agent_ui/src/acp/model_selector.rs @@ -330,7 +330,7 @@ async fn fuzzy_search( .collect::>(); let mut matches = match_strings( &candidates, - &query, + query, false, true, 100, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index e2e5820812..4a8f9bf209 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -696,7 +696,7 @@ impl AcpThreadView { }; diff.update(cx, |diff, cx| { - diff.move_to_path(PathKey::for_buffer(&buffer, cx), window, cx) + diff.move_to_path(PathKey::for_buffer(buffer, cx), window, cx) }) } @@ -722,13 +722,13 @@ impl AcpThreadView { let len = thread.read(cx).entries().len(); let index = len - 1; self.entry_view_state.update(cx, |view_state, cx| { - view_state.sync_entry(index, &thread, window, cx) + view_state.sync_entry(index, thread, window, cx) }); self.list_state.splice(index..index, 1); } AcpThreadEvent::EntryUpdated(index) => { self.entry_view_state.update(cx, |view_state, cx| { - view_state.sync_entry(*index, &thread, window, cx) + view_state.sync_entry(*index, thread, window, cx) }); self.list_state.splice(*index..index + 1, 1); } @@ -1427,7 +1427,7 @@ impl AcpThreadView { Empty.into_any_element() } } - ToolCallContent::Diff(diff) => self.render_diff_editor(entry_ix, &diff, cx), + ToolCallContent::Diff(diff) => self.render_diff_editor(entry_ix, diff, cx), ToolCallContent::Terminal(terminal) => { self.render_terminal_tool_call(entry_ix, terminal, tool_call, window, cx) } @@ -1583,7 +1583,7 @@ impl AcpThreadView { .border_color(self.tool_card_border_color(cx)) .child( if let Some(entry) = self.entry_view_state.read(cx).entry(entry_ix) - && let Some(editor) = entry.editor_for_diff(&diff) + && let Some(editor) = entry.editor_for_diff(diff) { editor.clone().into_any_element() } else { @@ -1783,7 +1783,7 @@ impl AcpThreadView { .entry_view_state .read(cx) .entry(entry_ix) - .and_then(|entry| entry.terminal(&terminal)); + .and_then(|entry| entry.terminal(terminal)); let show_output = self.terminal_expanded && terminal_view.is_some(); v_flex() @@ -2420,7 +2420,7 @@ impl AcpThreadView { .buffer_font(cx) }); - let file_icon = FileIcons::get_icon(&path, cx) + let file_icon = FileIcons::get_icon(path, cx) .map(Icon::from_path) .map(|icon| icon.color(Color::Muted).size(IconSize::Small)) .unwrap_or_else(|| { @@ -3453,7 +3453,7 @@ impl Render for AcpThreadView { configuration_view, .. } => self.render_auth_required_state( - &connection, + connection, description.as_ref(), configuration_view.as_ref(), window, diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 116c2b901b..38be2b193c 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -1044,12 +1044,12 @@ impl ActiveThread { ); } ThreadEvent::StreamedAssistantText(message_id, text) => { - if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) { + if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(message_id) { rendered_message.append_text(text, cx); } } ThreadEvent::StreamedAssistantThinking(message_id, text) => { - if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) { + if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(message_id) { rendered_message.append_thinking(text, cx); } } @@ -2473,7 +2473,7 @@ impl ActiveThread { message_id, index, content.clone(), - &scroll_handle, + scroll_handle, Some(index) == pending_thinking_segment_index, window, cx, diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index b9e1ea5d0a..85e7297810 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -207,7 +207,7 @@ impl AgentDiffPane { ), match &thread { AgentDiffThread::Native(thread) => { - Some(cx.subscribe(&thread, |this, _thread, event, cx| { + Some(cx.subscribe(thread, |this, _thread, event, cx| { this.handle_thread_event(event, cx) })) } @@ -398,7 +398,7 @@ fn keep_edits_in_selection( .disjoint_anchor_ranges() .collect::>(); - keep_edits_in_ranges(editor, buffer_snapshot, &thread, ranges, window, cx) + keep_edits_in_ranges(editor, buffer_snapshot, thread, ranges, window, cx) } fn reject_edits_in_selection( @@ -412,7 +412,7 @@ fn reject_edits_in_selection( .selections .disjoint_anchor_ranges() .collect::>(); - reject_edits_in_ranges(editor, buffer_snapshot, &thread, ranges, window, cx) + reject_edits_in_ranges(editor, buffer_snapshot, thread, ranges, window, cx) } fn keep_edits_in_ranges( @@ -1001,7 +1001,7 @@ impl AgentDiffToolbar { return; }; - *state = agent_diff.read(cx).editor_state(&editor); + *state = agent_diff.read(cx).editor_state(editor); self.update_location(cx); cx.notify(); } @@ -1343,13 +1343,13 @@ impl AgentDiff { }); let thread_subscription = match &thread { - AgentDiffThread::Native(thread) => cx.subscribe_in(&thread, window, { + AgentDiffThread::Native(thread) => cx.subscribe_in(thread, window, { let workspace = workspace.clone(); move |this, _thread, event, window, cx| { this.handle_native_thread_event(&workspace, event, window, cx) } }), - AgentDiffThread::AcpThread(thread) => cx.subscribe_in(&thread, window, { + AgentDiffThread::AcpThread(thread) => cx.subscribe_in(thread, window, { let workspace = workspace.clone(); move |this, thread, event, window, cx| { this.handle_acp_thread_event(&workspace, thread, event, window, cx) @@ -1357,11 +1357,11 @@ impl AgentDiff { }), }; - if let Some(workspace_thread) = self.workspace_threads.get_mut(&workspace) { + if let Some(workspace_thread) = self.workspace_threads.get_mut(workspace) { // replace thread and action log subscription, but keep editors workspace_thread.thread = thread.downgrade(); workspace_thread._thread_subscriptions = (action_log_subscription, thread_subscription); - self.update_reviewing_editors(&workspace, window, cx); + self.update_reviewing_editors(workspace, window, cx); return; } @@ -1677,7 +1677,7 @@ impl AgentDiff { editor.register_addon(EditorAgentDiffAddon); }); } else { - unaffected.remove(&weak_editor); + unaffected.remove(weak_editor); } if new_state == EditorState::Reviewing && previous_state != Some(new_state) { @@ -1730,7 +1730,7 @@ impl AgentDiff { fn editor_state(&self, editor: &WeakEntity) -> EditorState { self.reviewing_editors - .get(&editor) + .get(editor) .cloned() .unwrap_or(EditorState::Idle) } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 4cb231f357..e1174a4191 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -2923,7 +2923,7 @@ impl AgentPanel { .style(ButtonStyle::Tinted(ui::TintColor::Warning)) .label_size(LabelSize::Small) .key_binding( - KeyBinding::for_action_in(&OpenSettings, &focus_handle, window, cx) + KeyBinding::for_action_in(&OpenSettings, focus_handle, window, cx) .map(|kb| kb.size(rems_from_px(12.))), ) .on_click(|_event, window, cx| { @@ -3329,7 +3329,7 @@ impl AgentPanel { .paths() .into_iter() .map(|path| { - Workspace::project_path_for_path(this.project.clone(), &path, false, cx) + Workspace::project_path_for_path(this.project.clone(), path, false, cx) }) .collect::>(); cx.spawn_in(window, async move |this, cx| { @@ -3599,7 +3599,7 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist { let text_thread_store = None; let context_store = cx.new(|_| ContextStore::new(project.clone(), None)); assistant.assist( - &prompt_editor, + prompt_editor, self.workspace.clone(), context_store, project, diff --git a/crates/agent_ui/src/buffer_codegen.rs b/crates/agent_ui/src/buffer_codegen.rs index 615142b73d..23e04266db 100644 --- a/crates/agent_ui/src/buffer_codegen.rs +++ b/crates/agent_ui/src/buffer_codegen.rs @@ -388,7 +388,7 @@ impl CodegenAlternative { } else { let request = self.build_request(&model, user_prompt, cx)?; cx.spawn(async move |_, cx| { - Ok(model.stream_completion_text(request.await, &cx).await?) + Ok(model.stream_completion_text(request.await, cx).await?) }) .boxed_local() }; @@ -447,7 +447,7 @@ impl CodegenAlternative { } }); - let temperature = AgentSettings::temperature_for_model(&model, cx); + let temperature = AgentSettings::temperature_for_model(model, cx); Ok(cx.spawn(async move |_cx| { let mut request_message = LanguageModelRequestMessage { @@ -1028,7 +1028,7 @@ where chunk.push('\n'); } - chunk.push_str(&line); + chunk.push_str(line); } consumed += line.len(); diff --git a/crates/agent_ui/src/context_picker/completion_provider.rs b/crates/agent_ui/src/context_picker/completion_provider.rs index 962c0df03d..79e56acacf 100644 --- a/crates/agent_ui/src/context_picker/completion_provider.rs +++ b/crates/agent_ui/src/context_picker/completion_provider.rs @@ -728,11 +728,11 @@ fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId); let mut label = CodeLabel::default(); - label.push_str(&file_name, None); + label.push_str(file_name, None); label.push_str(" ", None); if let Some(directory) = directory { - label.push_str(&directory, comment_id); + label.push_str(directory, comment_id); } label.filter_range = 0..label.text().len(); diff --git a/crates/agent_ui/src/context_picker/file_context_picker.rs b/crates/agent_ui/src/context_picker/file_context_picker.rs index eaf9ed16d6..4f74e2cea4 100644 --- a/crates/agent_ui/src/context_picker/file_context_picker.rs +++ b/crates/agent_ui/src/context_picker/file_context_picker.rs @@ -315,7 +315,7 @@ pub fn render_file_context_entry( context_store: WeakEntity, cx: &App, ) -> Stateful
{ - let (file_name, directory) = extract_file_name_and_directory(&path, path_prefix); + let (file_name, directory) = extract_file_name_and_directory(path, path_prefix); let added = context_store.upgrade().and_then(|context_store| { let project_path = ProjectPath { @@ -334,7 +334,7 @@ pub fn render_file_context_entry( let file_icon = if is_directory { FileIcons::get_folder_icon(false, cx) } else { - FileIcons::get_icon(&path, cx) + FileIcons::get_icon(path, cx) } .map(Icon::from_path) .unwrap_or_else(|| Icon::new(IconName::File)); diff --git a/crates/agent_ui/src/context_picker/symbol_context_picker.rs b/crates/agent_ui/src/context_picker/symbol_context_picker.rs index 05e77deece..805c10c965 100644 --- a/crates/agent_ui/src/context_picker/symbol_context_picker.rs +++ b/crates/agent_ui/src/context_picker/symbol_context_picker.rs @@ -289,7 +289,7 @@ pub(crate) fn search_symbols( .iter() .enumerate() .map(|(id, symbol)| { - StringMatchCandidate::new(id, &symbol.label.filter_text()) + StringMatchCandidate::new(id, symbol.label.filter_text()) }) .partition(|candidate| { project diff --git a/crates/agent_ui/src/context_picker/thread_context_picker.rs b/crates/agent_ui/src/context_picker/thread_context_picker.rs index 15cc731f8f..e660e64ae3 100644 --- a/crates/agent_ui/src/context_picker/thread_context_picker.rs +++ b/crates/agent_ui/src/context_picker/thread_context_picker.rs @@ -167,7 +167,7 @@ impl PickerDelegate for ThreadContextPickerDelegate { return; }; let open_thread_task = - thread_store.update(cx, |this, cx| this.open_thread(&id, window, cx)); + thread_store.update(cx, |this, cx| this.open_thread(id, window, cx)); cx.spawn(async move |this, cx| { let thread = open_thread_task.await?; @@ -236,7 +236,7 @@ pub fn render_thread_context_entry( let is_added = match entry { ThreadContextEntry::Thread { id, .. } => context_store .upgrade() - .map_or(false, |ctx_store| ctx_store.read(cx).includes_thread(&id)), + .map_or(false, |ctx_store| ctx_store.read(cx).includes_thread(id)), ThreadContextEntry::Context { path, .. } => { context_store.upgrade().map_or(false, |ctx_store| { ctx_store.read(cx).includes_text_thread(path) @@ -338,7 +338,7 @@ pub(crate) fn search_threads( let candidates = threads .iter() .enumerate() - .map(|(id, (_, thread))| StringMatchCandidate::new(id, &thread.title())) + .map(|(id, (_, thread))| StringMatchCandidate::new(id, thread.title())) .collect::>(); let matches = fuzzy::match_strings( &candidates, diff --git a/crates/agent_ui/src/context_strip.rs b/crates/agent_ui/src/context_strip.rs index 369964f165..51ed3a5e11 100644 --- a/crates/agent_ui/src/context_strip.rs +++ b/crates/agent_ui/src/context_strip.rs @@ -145,7 +145,7 @@ impl ContextStrip { } let file_name = active_buffer.file()?.file_name(cx); - let icon_path = FileIcons::get_icon(&Path::new(&file_name), cx); + let icon_path = FileIcons::get_icon(Path::new(&file_name), cx); Some(SuggestedContext::File { name: file_name.to_string_lossy().into_owned().into(), buffer: active_buffer_entity.downgrade(), @@ -377,7 +377,7 @@ impl ContextStrip { fn add_suggested_context(&mut self, suggested: &SuggestedContext, cx: &mut Context) { self.context_store.update(cx, |context_store, cx| { - context_store.add_suggested_context(&suggested, cx) + context_store.add_suggested_context(suggested, cx) }); cx.notify(); } diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index bbd3595805..781e242fba 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -526,9 +526,9 @@ impl InlineAssistant { if assist_to_focus.is_none() { let focus_assist = if newest_selection.reversed { - range.start.to_point(&snapshot) == newest_selection.start + range.start.to_point(snapshot) == newest_selection.start } else { - range.end.to_point(&snapshot) == newest_selection.end + range.end.to_point(snapshot) == newest_selection.end }; if focus_assist { assist_to_focus = Some(assist_id); @@ -550,7 +550,7 @@ impl InlineAssistant { let editor_assists = self .assists_by_editor .entry(editor.downgrade()) - .or_insert_with(|| EditorInlineAssists::new(&editor, window, cx)); + .or_insert_with(|| EditorInlineAssists::new(editor, window, cx)); let mut assist_group = InlineAssistGroup::new(); for (assist_id, range, prompt_editor, prompt_block_id, end_block_id) in assists { let codegen = prompt_editor.read(cx).codegen().clone(); @@ -649,7 +649,7 @@ impl InlineAssistant { let editor_assists = self .assists_by_editor .entry(editor.downgrade()) - .or_insert_with(|| EditorInlineAssists::new(&editor, window, cx)); + .or_insert_with(|| EditorInlineAssists::new(editor, window, cx)); let mut assist_group = InlineAssistGroup::new(); self.assists.insert( diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index e6fca16984..6f12050f88 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -75,7 +75,7 @@ impl Render for PromptEditor { let codegen = codegen.read(cx); if codegen.alternative_count(cx) > 1 { - buttons.push(self.render_cycle_controls(&codegen, cx)); + buttons.push(self.render_cycle_controls(codegen, cx)); } let editor_margins = editor_margins.lock(); diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index fa8ca490d8..845540979a 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -296,7 +296,7 @@ impl ModelMatcher { pub fn fuzzy_search(&self, query: &str) -> Vec { let mut matches = self.bg_executor.block(match_strings( &self.candidates, - &query, + query, false, true, 100, diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index d6c9a778a6..181a0dd5d2 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -1166,7 +1166,7 @@ impl MessageEditor { .buffer_font(cx) }); - let file_icon = FileIcons::get_icon(&path, cx) + let file_icon = FileIcons::get_icon(path, cx) .map(Icon::from_path) .map(|icon| icon.color(Color::Muted).size(IconSize::Small)) .unwrap_or_else(|| { @@ -1559,9 +1559,8 @@ impl ContextCreasesAddon { cx: &mut Context, ) { self.creases.entry(key).or_default().extend(creases); - self._subscription = Some(cx.subscribe( - &context_store, - |editor, _, event, cx| match event { + self._subscription = Some( + cx.subscribe(context_store, |editor, _, event, cx| match event { ContextStoreEvent::ContextRemoved(key) => { let Some(this) = editor.addon_mut::() else { return; @@ -1581,8 +1580,8 @@ impl ContextCreasesAddon { editor.edit(ranges.into_iter().zip(replacement_texts), cx); cx.notify(); } - }, - )) + }), + ) } pub fn into_inner(self) -> HashMap> { diff --git a/crates/agent_ui/src/slash_command_picker.rs b/crates/agent_ui/src/slash_command_picker.rs index 678562e059..bab2364679 100644 --- a/crates/agent_ui/src/slash_command_picker.rs +++ b/crates/agent_ui/src/slash_command_picker.rs @@ -214,7 +214,7 @@ impl PickerDelegate for SlashCommandDelegate { let mut label = format!("{}", info.name); if let Some(args) = info.args.as_ref().filter(|_| selected) { - label.push_str(&args); + label.push_str(args); } Label::new(label) .single_line() diff --git a/crates/agent_ui/src/terminal_codegen.rs b/crates/agent_ui/src/terminal_codegen.rs index 54f5b52f58..5a4a9d560a 100644 --- a/crates/agent_ui/src/terminal_codegen.rs +++ b/crates/agent_ui/src/terminal_codegen.rs @@ -48,7 +48,7 @@ impl TerminalCodegen { let prompt = prompt_task.await; let model_telemetry_id = model.telemetry_id(); let model_provider_id = model.provider_id(); - let response = model.stream_completion_text(prompt, &cx).await; + let response = model.stream_completion_text(prompt, cx).await; let generate = async { let message_id = response .as_ref() diff --git a/crates/agent_ui/src/ui/context_pill.rs b/crates/agent_ui/src/ui/context_pill.rs index 5dd57de244..4e33e151cd 100644 --- a/crates/agent_ui/src/ui/context_pill.rs +++ b/crates/agent_ui/src/ui/context_pill.rs @@ -353,7 +353,7 @@ impl AddedContext { name, parent, tooltip: Some(full_path_string), - icon_path: FileIcons::get_icon(&full_path, cx), + icon_path: FileIcons::get_icon(full_path, cx), status: ContextStatus::Ready, render_hover: None, handle: AgentContextHandle::File(handle), @@ -615,7 +615,7 @@ impl AddedContext { let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into(); let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, &full_path_string); - let icon_path = FileIcons::get_icon(&full_path, cx); + let icon_path = FileIcons::get_icon(full_path, cx); (name, parent, icon_path) } else { ("Image".into(), None, None) @@ -706,7 +706,7 @@ impl ContextFileExcerpt { .and_then(|p| p.file_name()) .map(|n| n.to_string_lossy().into_owned().into()); - let icon_path = FileIcons::get_icon(&full_path, cx); + let icon_path = FileIcons::get_icon(full_path, cx); ContextFileExcerpt { file_name_and_range: file_name_and_range.into(), diff --git a/crates/assistant_context/src/assistant_context.rs b/crates/assistant_context/src/assistant_context.rs index 557f9592e4..06abbad39f 100644 --- a/crates/assistant_context/src/assistant_context.rs +++ b/crates/assistant_context/src/assistant_context.rs @@ -592,7 +592,7 @@ impl MessageMetadata { pub fn is_cache_valid(&self, buffer: &BufferSnapshot, range: &Range) -> bool { let result = match &self.cache { Some(MessageCacheMetadata { cached_at, .. }) => !buffer.has_edits_since_in_range( - &cached_at, + cached_at, Range { start: buffer.anchor_at(range.start, Bias::Right), end: buffer.anchor_at(range.end, Bias::Left), @@ -1413,7 +1413,7 @@ impl AssistantContext { } let request = { - let mut req = self.to_completion_request(Some(&model), cx); + let mut req = self.to_completion_request(Some(model), cx); // Skip the last message because it's likely to change and // therefore would be a waste to cache. req.messages.pop(); @@ -1428,7 +1428,7 @@ impl AssistantContext { let model = Arc::clone(model); self.pending_cache_warming_task = cx.spawn(async move |this, cx| { async move { - match model.stream_completion(request, &cx).await { + match model.stream_completion(request, cx).await { Ok(mut stream) => { stream.next().await; log::info!("Cache warming completed successfully"); @@ -1661,12 +1661,12 @@ impl AssistantContext { ) -> Range { let buffer = self.buffer.read(cx); let start_ix = match all_annotations - .binary_search_by(|probe| probe.range().end.cmp(&range.start, &buffer)) + .binary_search_by(|probe| probe.range().end.cmp(&range.start, buffer)) { Ok(ix) | Err(ix) => ix, }; let end_ix = match all_annotations - .binary_search_by(|probe| probe.range().start.cmp(&range.end, &buffer)) + .binary_search_by(|probe| probe.range().start.cmp(&range.end, buffer)) { Ok(ix) => ix + 1, Err(ix) => ix, @@ -2045,7 +2045,7 @@ impl AssistantContext { let task = cx.spawn({ async move |this, cx| { - let stream = model.stream_completion(request, &cx); + let stream = model.stream_completion(request, cx); let assistant_message_id = assistant_message.id; let mut response_latency = None; let stream_completion = async { @@ -2708,7 +2708,7 @@ impl AssistantContext { self.summary_task = cx.spawn(async move |this, cx| { let result = async { - let stream = model.model.stream_completion_text(request, &cx); + let stream = model.model.stream_completion_text(request, cx); let mut messages = stream.await?; let mut replaced = !replace_old; @@ -2927,7 +2927,7 @@ impl AssistantContext { if let Some(old_path) = old_path.as_ref() { if new_path.as_path() != old_path.as_ref() { fs.rename( - &old_path, + old_path, &new_path, RenameOptions { overwrite: true, diff --git a/crates/assistant_context/src/assistant_context_tests.rs b/crates/assistant_context/src/assistant_context_tests.rs index efcad8ed96..eae7741358 100644 --- a/crates/assistant_context/src/assistant_context_tests.rs +++ b/crates/assistant_context/src/assistant_context_tests.rs @@ -1300,7 +1300,7 @@ fn test_summarize_error( context.assist(cx); }); - simulate_successful_response(&model, cx); + simulate_successful_response(model, cx); context.read_with(cx, |context, _| { assert!(!context.summary().content().unwrap().done); diff --git a/crates/assistant_slash_commands/src/context_server_command.rs b/crates/assistant_slash_commands/src/context_server_command.rs index f223d3b184..15f3901bfb 100644 --- a/crates/assistant_slash_commands/src/context_server_command.rs +++ b/crates/assistant_slash_commands/src/context_server_command.rs @@ -44,7 +44,7 @@ impl SlashCommand for ContextServerSlashCommand { parts.push(arg.name.as_str()); } } - create_label_for_command(&parts[0], &parts[1..], cx) + create_label_for_command(parts[0], &parts[1..], cx) } fn description(&self) -> String { diff --git a/crates/assistant_slash_commands/src/diagnostics_command.rs b/crates/assistant_slash_commands/src/diagnostics_command.rs index 2feabd8b1e..31014f8fb8 100644 --- a/crates/assistant_slash_commands/src/diagnostics_command.rs +++ b/crates/assistant_slash_commands/src/diagnostics_command.rs @@ -249,7 +249,7 @@ fn collect_diagnostics( let worktree = worktree.read(cx); let worktree_root_path = Path::new(worktree.root_name()); let relative_path = path.strip_prefix(worktree_root_path).ok()?; - worktree.absolutize(&relative_path).ok() + worktree.absolutize(relative_path).ok() }) }) .is_some() @@ -365,7 +365,7 @@ pub fn collect_buffer_diagnostics( ) { for (_, group) in snapshot.diagnostic_groups(None) { let entry = &group.entries[group.primary_ix]; - collect_diagnostic(output, entry, &snapshot, include_warnings) + collect_diagnostic(output, entry, snapshot, include_warnings) } } @@ -396,7 +396,7 @@ fn collect_diagnostic( let start_row = range.start.row.saturating_sub(EXCERPT_EXPANSION_SIZE); let end_row = (range.end.row + EXCERPT_EXPANSION_SIZE).min(snapshot.max_point().row) + 1; let excerpt_range = - Point::new(start_row, 0).to_offset(&snapshot)..Point::new(end_row, 0).to_offset(&snapshot); + Point::new(start_row, 0).to_offset(snapshot)..Point::new(end_row, 0).to_offset(snapshot); output.text.push_str("```"); if let Some(language_name) = snapshot.language().map(|l| l.code_fence_block_name()) { diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index e819c51e1e..039f9d9316 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -536,7 +536,7 @@ fn resolve_path( let parent_entry = parent_project_path .as_ref() - .and_then(|path| project.entry_for_path(&path, cx)) + .and_then(|path| project.entry_for_path(path, cx)) .context("Can't create file: parent directory doesn't exist")?; anyhow::ensure!( @@ -723,13 +723,13 @@ impl EditFileToolCard { let buffer = buffer.read(cx); let diff = diff.read(cx); let mut ranges = diff - .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, cx) - .map(|diff_hunk| diff_hunk.buffer_range.to_point(&buffer)) + .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx) + .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer)) .collect::>(); ranges.extend( self.revealed_ranges .iter() - .map(|range| range.to_point(&buffer)), + .map(|range| range.to_point(buffer)), ); ranges.sort_unstable_by_key(|range| (range.start, Reverse(range.end))); diff --git a/crates/assistant_tools/src/grep_tool.rs b/crates/assistant_tools/src/grep_tool.rs index a5ce07823f..1f00332c5a 100644 --- a/crates/assistant_tools/src/grep_tool.rs +++ b/crates/assistant_tools/src/grep_tool.rs @@ -894,7 +894,7 @@ mod tests { }) .await; let results = result.unwrap(); - let paths = extract_paths_from_results(&results.content.as_str().unwrap()); + let paths = extract_paths_from_results(results.content.as_str().unwrap()); assert!( paths.is_empty(), "grep_tool should not find files outside the project worktree" @@ -920,7 +920,7 @@ mod tests { }) .await; let results = result.unwrap(); - let paths = extract_paths_from_results(&results.content.as_str().unwrap()); + let paths = extract_paths_from_results(results.content.as_str().unwrap()); assert!( paths.iter().any(|p| p.contains("allowed_file.rs")), "grep_tool should be able to search files inside worktrees" @@ -946,7 +946,7 @@ mod tests { }) .await; let results = result.unwrap(); - let paths = extract_paths_from_results(&results.content.as_str().unwrap()); + let paths = extract_paths_from_results(results.content.as_str().unwrap()); assert!( paths.is_empty(), "grep_tool should not search files in .secretdir (file_scan_exclusions)" @@ -971,7 +971,7 @@ mod tests { }) .await; let results = result.unwrap(); - let paths = extract_paths_from_results(&results.content.as_str().unwrap()); + let paths = extract_paths_from_results(results.content.as_str().unwrap()); assert!( paths.is_empty(), "grep_tool should not search .mymetadata files (file_scan_exclusions)" @@ -997,7 +997,7 @@ mod tests { }) .await; let results = result.unwrap(); - let paths = extract_paths_from_results(&results.content.as_str().unwrap()); + let paths = extract_paths_from_results(results.content.as_str().unwrap()); assert!( paths.is_empty(), "grep_tool should not search .mysecrets (private_files)" @@ -1022,7 +1022,7 @@ mod tests { }) .await; let results = result.unwrap(); - let paths = extract_paths_from_results(&results.content.as_str().unwrap()); + let paths = extract_paths_from_results(results.content.as_str().unwrap()); assert!( paths.is_empty(), "grep_tool should not search .privatekey files (private_files)" @@ -1047,7 +1047,7 @@ mod tests { }) .await; let results = result.unwrap(); - let paths = extract_paths_from_results(&results.content.as_str().unwrap()); + let paths = extract_paths_from_results(results.content.as_str().unwrap()); assert!( paths.is_empty(), "grep_tool should not search .mysensitive files (private_files)" @@ -1073,7 +1073,7 @@ mod tests { }) .await; let results = result.unwrap(); - let paths = extract_paths_from_results(&results.content.as_str().unwrap()); + let paths = extract_paths_from_results(results.content.as_str().unwrap()); assert!( paths.iter().any(|p| p.contains("normal_file.rs")), "Should be able to search normal files" @@ -1100,7 +1100,7 @@ mod tests { }) .await; let results = result.unwrap(); - let paths = extract_paths_from_results(&results.content.as_str().unwrap()); + let paths = extract_paths_from_results(results.content.as_str().unwrap()); assert!( paths.is_empty(), "grep_tool should not allow escaping project boundaries with relative paths" @@ -1206,7 +1206,7 @@ mod tests { .unwrap(); let content = result.content.as_str().unwrap(); - let paths = extract_paths_from_results(&content); + let paths = extract_paths_from_results(content); // Should find matches in non-private files assert!( @@ -1271,7 +1271,7 @@ mod tests { .unwrap(); let content = result.content.as_str().unwrap(); - let paths = extract_paths_from_results(&content); + let paths = extract_paths_from_results(content); // Should only find matches in worktree1 *.rs files (excluding private ones) assert!( diff --git a/crates/assistant_tools/src/project_notifications_tool.rs b/crates/assistant_tools/src/project_notifications_tool.rs index c65cfd0ca7..e30d80207d 100644 --- a/crates/assistant_tools/src/project_notifications_tool.rs +++ b/crates/assistant_tools/src/project_notifications_tool.rs @@ -81,7 +81,7 @@ fn fit_patch_to_size(patch: &str, max_size: usize) -> String { // Compression level 1: remove context lines in diff bodies, but // leave the counts and positions of inserted/deleted lines let mut current_size = patch.len(); - let mut file_patches = split_patch(&patch); + let mut file_patches = split_patch(patch); file_patches.sort_by_key(|patch| patch.len()); let compressed_patches = file_patches .iter() diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index 46227f130d..3de22ad28d 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -105,7 +105,7 @@ impl Tool for TerminalTool { let first_line = lines.next().unwrap_or_default(); let remaining_line_count = lines.count(); match remaining_line_count { - 0 => MarkdownInlineCode(&first_line).to_string(), + 0 => MarkdownInlineCode(first_line).to_string(), 1 => MarkdownInlineCode(&format!( "{} - {} more line", first_line, remaining_line_count diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 8eed7497da..990fc27fbd 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -231,7 +231,7 @@ fn apply_dirty_filename_style( let highlight = vec![(filename_position..text.len(), highlight_style)]; Some( StyledText::new(text) - .with_default_highlights(&text_style, highlight) + .with_default_highlights(text_style, highlight) .into_any(), ) } diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 97f529fe37..e20ea9713f 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -928,7 +928,7 @@ impl BufferDiff { let new_index_text = self.inner.stage_or_unstage_hunks_impl( &self.secondary_diff.as_ref()?.read(cx).inner, stage, - &hunks, + hunks, buffer, file_exists, ); @@ -952,12 +952,12 @@ impl BufferDiff { cx: &App, ) -> Option> { let start = self - .hunks_intersecting_range(range.clone(), &buffer, cx) + .hunks_intersecting_range(range.clone(), buffer, cx) .next()? .buffer_range .start; let end = self - .hunks_intersecting_range_rev(range.clone(), &buffer) + .hunks_intersecting_range_rev(range.clone(), buffer) .next()? .buffer_range .end; @@ -1031,18 +1031,18 @@ impl BufferDiff { && state.base_text.syntax_update_count() == new_state.base_text.syntax_update_count() => { - (false, new_state.compare(&state, buffer)) + (false, new_state.compare(state, buffer)) } _ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)), }; if let Some(secondary_changed_range) = secondary_diff_change { if let Some(secondary_hunk_range) = - self.range_to_hunk_range(secondary_changed_range, &buffer, cx) + self.range_to_hunk_range(secondary_changed_range, buffer, cx) { if let Some(range) = &mut changed_range { - range.start = secondary_hunk_range.start.min(&range.start, &buffer); - range.end = secondary_hunk_range.end.max(&range.end, &buffer); + range.start = secondary_hunk_range.start.min(&range.start, buffer); + range.end = secondary_hunk_range.end.max(&range.end, buffer); } else { changed_range = Some(secondary_hunk_range); } @@ -1057,8 +1057,8 @@ impl BufferDiff { if let Some((first, last)) = state.pending_hunks.first().zip(state.pending_hunks.last()) { if let Some(range) = &mut changed_range { - range.start = range.start.min(&first.buffer_range.start, &buffer); - range.end = range.end.max(&last.buffer_range.end, &buffer); + range.start = range.start.min(&first.buffer_range.start, buffer); + range.end = range.end.max(&last.buffer_range.end, buffer); } else { changed_range = Some(first.buffer_range.start..last.buffer_range.end); } @@ -1797,7 +1797,7 @@ mod tests { uncommitted_diff.update(cx, |diff, cx| { let hunks = diff - .hunks_intersecting_range(hunk_range.clone(), &buffer, &cx) + .hunks_intersecting_range(hunk_range.clone(), &buffer, cx) .collect::>(); for hunk in &hunks { assert_ne!( @@ -1812,7 +1812,7 @@ mod tests { .to_string(); let hunks = diff - .hunks_intersecting_range(hunk_range.clone(), &buffer, &cx) + .hunks_intersecting_range(hunk_range.clone(), &buffer, cx) .collect::>(); for hunk in &hunks { assert_eq!( @@ -1870,7 +1870,7 @@ mod tests { .to_string(); assert_eq!(new_index_text, buffer_text); - let hunk = diff.hunks(&buffer, &cx).next().unwrap(); + let hunk = diff.hunks(&buffer, cx).next().unwrap(); assert_eq!( hunk.secondary_status, DiffHunkSecondaryStatus::SecondaryHunkRemovalPending @@ -1882,7 +1882,7 @@ mod tests { .to_string(); assert_eq!(index_text, head_text); - let hunk = diff.hunks(&buffer, &cx).next().unwrap(); + let hunk = diff.hunks(&buffer, cx).next().unwrap(); // optimistically unstaged (fine, could also be HasSecondaryHunk) assert_eq!( hunk.secondary_status, diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 67591167df..a61d8e0911 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -518,11 +518,11 @@ mod linux { ) -> Result<(), std::io::Error> { for _ in 0..100 { thread::sleep(Duration::from_millis(10)); - if sock.connect_addr(&sock_addr).is_ok() { + if sock.connect_addr(sock_addr).is_ok() { return Ok(()); } } - sock.connect_addr(&sock_addr) + sock.connect_addr(sock_addr) } } } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 0f00471356..91bdf001d8 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -162,7 +162,7 @@ pub fn init(client: &Arc, cx: &mut App) { let client = client.clone(); move |_: &SignIn, cx| { if let Some(client) = client.upgrade() { - cx.spawn(async move |cx| client.sign_in_with_optional_connect(true, &cx).await) + cx.spawn(async move |cx| client.sign_in_with_optional_connect(true, cx).await) .detach_and_log_err(cx); } } @@ -173,7 +173,7 @@ pub fn init(client: &Arc, cx: &mut App) { move |_: &SignOut, cx| { if let Some(client) = client.upgrade() { cx.spawn(async move |cx| { - client.sign_out(&cx).await; + client.sign_out(cx).await; }) .detach(); } @@ -185,7 +185,7 @@ pub fn init(client: &Arc, cx: &mut App) { move |_: &Reconnect, cx| { if let Some(client) = client.upgrade() { cx.spawn(async move |cx| { - client.reconnect(&cx); + client.reconnect(cx); }) .detach(); } @@ -677,7 +677,7 @@ impl Client { let mut delay = INITIAL_RECONNECTION_DELAY; loop { - match client.connect(true, &cx).await { + match client.connect(true, cx).await { ConnectionResult::Timeout => { log::error!("client connect attempt timed out") } @@ -701,7 +701,7 @@ impl Client { Status::ReconnectionError { next_reconnection: Instant::now() + delay, }, - &cx, + cx, ); let jitter = Duration::from_millis(rng.gen_range(0..delay.as_millis() as u64)); @@ -1151,7 +1151,7 @@ impl Client { let this = self.clone(); async move |cx| { while let Some(message) = incoming.next().await { - this.handle_message(message, &cx); + this.handle_message(message, cx); // Don't starve the main thread when receiving lots of messages at once. smol::future::yield_now().await; } @@ -1169,12 +1169,12 @@ impl Client { peer_id, }) { - this.set_status(Status::SignedOut, &cx); + this.set_status(Status::SignedOut, cx); } } Err(err) => { log::error!("connection error: {:?}", err); - this.set_status(Status::ConnectionLost, &cx); + this.set_status(Status::ConnectionLost, cx); } } }) diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 82f74d910b..6783d8ed2a 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -943,21 +943,21 @@ impl Database { let current_merge_conflicts = db_repository_entry .current_merge_conflicts .as_ref() - .map(|conflicts| serde_json::from_str(&conflicts)) + .map(|conflicts| serde_json::from_str(conflicts)) .transpose()? .unwrap_or_default(); let branch_summary = db_repository_entry .branch_summary .as_ref() - .map(|branch_summary| serde_json::from_str(&branch_summary)) + .map(|branch_summary| serde_json::from_str(branch_summary)) .transpose()? .unwrap_or_default(); let head_commit_details = db_repository_entry .head_commit_details .as_ref() - .map(|head_commit_details| serde_json::from_str(&head_commit_details)) + .map(|head_commit_details| serde_json::from_str(head_commit_details)) .transpose()? .unwrap_or_default(); diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index c63d7133be..1b128e3a23 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -746,21 +746,21 @@ impl Database { let current_merge_conflicts = db_repository .current_merge_conflicts .as_ref() - .map(|conflicts| serde_json::from_str(&conflicts)) + .map(|conflicts| serde_json::from_str(conflicts)) .transpose()? .unwrap_or_default(); let branch_summary = db_repository .branch_summary .as_ref() - .map(|branch_summary| serde_json::from_str(&branch_summary)) + .map(|branch_summary| serde_json::from_str(branch_summary)) .transpose()? .unwrap_or_default(); let head_commit_details = db_repository .head_commit_details .as_ref() - .map(|head_commit_details| serde_json::from_str(&head_commit_details)) + .map(|head_commit_details| serde_json::from_str(head_commit_details)) .transpose()? .unwrap_or_default(); diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index 03d39cb8ce..28d60d9221 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -245,7 +245,7 @@ impl MessageEditor { if !candidates.is_empty() { return cx.spawn(async move |_, cx| { let completion_response = Self::completions_for_candidates( - &cx, + cx, query.as_str(), &candidates, start_anchor..end_anchor, @@ -263,7 +263,7 @@ impl MessageEditor { if !candidates.is_empty() { return cx.spawn(async move |_, cx| { let completion_response = Self::completions_for_candidates( - &cx, + cx, query.as_str(), candidates, start_anchor..end_anchor, diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index c2cc6a7ad5..8016481f6f 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2317,7 +2317,7 @@ impl CollabPanel { let client = this.client.clone(); cx.spawn_in(window, async move |_, cx| { client - .connect(true, &cx) + .connect(true, cx) .await .into_response() .notify_async_err(cx); diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index a3420d603b..01ca533c10 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -643,7 +643,7 @@ impl Render for NotificationPanel { let client = client.clone(); window .spawn(cx, async move |cx| { - match client.connect(true, &cx).await { + match client.connect(true, cx).await { util::ConnectionResult::Timeout => { log::error!("Connection timeout"); } diff --git a/crates/context_server/src/listener.rs b/crates/context_server/src/listener.rs index 0e85fb2129..f3c199a14e 100644 --- a/crates/context_server/src/listener.rs +++ b/crates/context_server/src/listener.rs @@ -315,12 +315,12 @@ impl McpServer { Self::send_err( request_id, format!("Tool not found: {}", params.name), - &outgoing_tx, + outgoing_tx, ); } } Err(err) => { - Self::send_err(request_id, err.to_string(), &outgoing_tx); + Self::send_err(request_id, err.to_string(), outgoing_tx); } } } diff --git a/crates/context_server/src/types.rs b/crates/context_server/src/types.rs index 5fa2420a3d..e92a18c763 100644 --- a/crates/context_server/src/types.rs +++ b/crates/context_server/src/types.rs @@ -691,7 +691,7 @@ impl CallToolResponse { let mut text = String::new(); for chunk in &self.content { if let ToolResponseContent::Text { text: chunk } = chunk { - text.push_str(&chunk) + text.push_str(chunk) }; } text diff --git a/crates/copilot/src/copilot_chat.rs b/crates/copilot/src/copilot_chat.rs index 4c91b4fedb..e8e2251648 100644 --- a/crates/copilot/src/copilot_chat.rs +++ b/crates/copilot/src/copilot_chat.rs @@ -484,7 +484,7 @@ impl CopilotChat { }; if this.oauth_token.is_some() { - cx.spawn(async move |this, mut cx| Self::update_models(&this, &mut cx).await) + cx.spawn(async move |this, cx| Self::update_models(&this, cx).await) .detach_and_log_err(cx); } @@ -863,7 +863,7 @@ mod tests { "object": "list" }"#; - let schema: ModelSchema = serde_json::from_str(&json).unwrap(); + let schema: ModelSchema = serde_json::from_str(json).unwrap(); assert_eq!(schema.data.len(), 2); assert_eq!(schema.data[0].id, "gpt-4"); diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 687305ae94..2cef266677 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -285,7 +285,7 @@ pub async fn download_adapter_from_github( } if !adapter_path.exists() { - fs.create_dir(&adapter_path.as_path()) + fs.create_dir(adapter_path.as_path()) .await .context("Failed creating adapter path")?; } diff --git a/crates/dap_adapters/src/go.rs b/crates/dap_adapters/src/go.rs index 22d8262b93..db8a45ceb4 100644 --- a/crates/dap_adapters/src/go.rs +++ b/crates/dap_adapters/src/go.rs @@ -36,7 +36,7 @@ impl GoDebugAdapter { delegate: &Arc, ) -> Result { let release = latest_github_release( - &"zed-industries/delve-shim-dap", + "zed-industries/delve-shim-dap", true, false, delegate.http_client(), diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 2d19921a0f..70b0638120 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -514,7 +514,7 @@ impl DebugAdapter for JsDebugAdapter { } } - self.get_installed_binary(delegate, &config, user_installed_path, user_args, cx) + self.get_installed_binary(delegate, config, user_installed_path, user_args, cx) .await } diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 7b90f80fe2..6e80ec484c 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -717,7 +717,7 @@ impl DebugAdapter for PythonDebugAdapter { local_path.display() ); return self - .get_installed_binary(delegate, &config, Some(local_path.clone()), user_args, None) + .get_installed_binary(delegate, config, Some(local_path.clone()), user_args, None) .await; } @@ -754,7 +754,7 @@ impl DebugAdapter for PythonDebugAdapter { return self .get_installed_binary( delegate, - &config, + config, None, user_args, Some(toolchain.path.to_string()), @@ -762,7 +762,7 @@ impl DebugAdapter for PythonDebugAdapter { .await; } - self.get_installed_binary(delegate, &config, None, user_args, None) + self.get_installed_binary(delegate, config, None, user_args, None) .await } diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index de55212cba..7fed761f5a 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -238,7 +238,7 @@ mod tests { .unwrap(); let _bad_db = open_db::( tempdir.path(), - &release_channel::ReleaseChannel::Dev.dev_name(), + release_channel::ReleaseChannel::Dev.dev_name(), ) .await; } @@ -279,7 +279,7 @@ mod tests { { let corrupt_db = open_db::( tempdir.path(), - &release_channel::ReleaseChannel::Dev.dev_name(), + release_channel::ReleaseChannel::Dev.dev_name(), ) .await; assert!(corrupt_db.persistent()); @@ -287,7 +287,7 @@ mod tests { let good_db = open_db::( tempdir.path(), - &release_channel::ReleaseChannel::Dev.dev_name(), + release_channel::ReleaseChannel::Dev.dev_name(), ) .await; assert!( @@ -334,7 +334,7 @@ mod tests { // Setup the bad database let corrupt_db = open_db::( tempdir.path(), - &release_channel::ReleaseChannel::Dev.dev_name(), + release_channel::ReleaseChannel::Dev.dev_name(), ) .await; assert!(corrupt_db.persistent()); @@ -347,7 +347,7 @@ mod tests { let guard = thread::spawn(move || { let good_db = smol::block_on(open_db::( tmp_path.as_path(), - &release_channel::ReleaseChannel::Dev.dev_name(), + release_channel::ReleaseChannel::Dev.dev_name(), )); assert!( good_db.select_row::("SELECT * FROM test2").unwrap()() diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index b806381d25..14154e5b39 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -485,7 +485,7 @@ impl LogStore { &mut self, id: &LogStoreEntryIdentifier<'_>, ) -> Option<&Vec> { - self.get_debug_adapter_state(&id) + self.get_debug_adapter_state(id) .map(|state| &state.rpc_messages.initialization_sequence) } } @@ -536,11 +536,11 @@ impl Render for DapLogToolbarItemView { }) .unwrap_or_else(|| "No adapter selected".into()), )) - .menu(move |mut window, cx| { + .menu(move |window, cx| { let log_view = log_view.clone(); let menu_rows = menu_rows.clone(); let project = project.clone(); - ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| { + ContextMenu::build(window, cx, move |mut menu, window, _cx| { for row in menu_rows.into_iter() { menu = menu.custom_row(move |_window, _cx| { div() @@ -1131,7 +1131,7 @@ impl LogStore { project: &WeakEntity, session_id: SessionId, ) -> Vec { - self.projects.get(&project).map_or(vec![], |state| { + self.projects.get(project).map_or(vec![], |state| { state .debug_sessions .get(&session_id) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 1d44c5c244..cf038871bc 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -693,7 +693,7 @@ impl DebugPanel { ) .icon_size(IconSize::Small) .on_click(window.listener_for( - &running_state, + running_state, |this, _, _window, cx| { this.pause_thread(cx); }, @@ -719,7 +719,7 @@ impl DebugPanel { ) .icon_size(IconSize::Small) .on_click(window.listener_for( - &running_state, + running_state, |this, _, _window, cx| this.continue_thread(cx), )) .disabled(thread_status != ThreadStatus::Stopped) @@ -742,7 +742,7 @@ impl DebugPanel { IconButton::new("debug-step-over", IconName::ArrowRight) .icon_size(IconSize::Small) .on_click(window.listener_for( - &running_state, + running_state, |this, _, _window, cx| { this.step_over(cx); }, @@ -768,7 +768,7 @@ impl DebugPanel { ) .icon_size(IconSize::Small) .on_click(window.listener_for( - &running_state, + running_state, |this, _, _window, cx| { this.step_in(cx); }, @@ -791,7 +791,7 @@ impl DebugPanel { IconButton::new("debug-step-out", IconName::ArrowUpRight) .icon_size(IconSize::Small) .on_click(window.listener_for( - &running_state, + running_state, |this, _, _window, cx| { this.step_out(cx); }, @@ -815,7 +815,7 @@ impl DebugPanel { IconButton::new("debug-restart", IconName::RotateCcw) .icon_size(IconSize::Small) .on_click(window.listener_for( - &running_state, + running_state, |this, _, window, cx| { this.rerun_session(window, cx); }, @@ -837,7 +837,7 @@ impl DebugPanel { IconButton::new("debug-stop", IconName::Power) .icon_size(IconSize::Small) .on_click(window.listener_for( - &running_state, + running_state, |this, _, _window, cx| { if this.session().read(cx).is_building() { this.session().update(cx, |session, cx| { @@ -892,7 +892,7 @@ impl DebugPanel { ) .icon_size(IconSize::Small) .on_click(window.listener_for( - &running_state, + running_state, |this, _, _, cx| { this.detach_client(cx); }, @@ -1160,7 +1160,7 @@ impl DebugPanel { workspace .project() .read(cx) - .project_path_for_absolute_path(&path, cx) + .project_path_for_absolute_path(path, cx) .context( "Couldn't get project path for .zed/debug.json in active worktree", ) diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index 4ac8e371a1..51ea25a5cb 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -413,7 +413,7 @@ impl NewProcessModal { let Some(adapter) = self.debugger.as_ref() else { return; }; - let scenario = self.debug_scenario(&adapter, cx); + let scenario = self.debug_scenario(adapter, cx); cx.spawn_in(window, async move |this, cx| { let scenario = scenario.await.context("no scenario to save")?; let worktree_id = task_contexts @@ -659,12 +659,7 @@ impl Render for NewProcessModal { this.mode = NewProcessMode::Attach; if let Some(debugger) = this.debugger.as_ref() { - Self::update_attach_picker( - &this.attach_mode, - &debugger, - window, - cx, - ); + Self::update_attach_picker(&this.attach_mode, debugger, window, cx); } this.mode_focus_handle(cx).focus(window); cx.notify(); @@ -1083,7 +1078,7 @@ impl DebugDelegate { .into_iter() .map(|(scenario, context)| { let (kind, scenario) = - Self::get_scenario_kind(&languages, &dap_registry, scenario); + Self::get_scenario_kind(&languages, dap_registry, scenario); (kind, scenario, Some(context)) }) .chain( @@ -1100,7 +1095,7 @@ impl DebugDelegate { .filter(|(_, scenario)| valid_adapters.contains(&scenario.adapter)) .map(|(kind, scenario)| { let (language, scenario) = - Self::get_scenario_kind(&languages, &dap_registry, scenario); + Self::get_scenario_kind(&languages, dap_registry, scenario); (language.or(Some(kind)), scenario, None) }), ) diff --git a/crates/debugger_ui/src/persistence.rs b/crates/debugger_ui/src/persistence.rs index 3a0ad7a40e..f0d7fd6fdd 100644 --- a/crates/debugger_ui/src/persistence.rs +++ b/crates/debugger_ui/src/persistence.rs @@ -341,7 +341,7 @@ impl SerializedPaneLayout { pub(crate) fn in_order(&self) -> Vec { let mut panes = vec![]; - Self::inner_in_order(&self, &mut panes); + Self::inner_in_order(self, &mut panes); panes } diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index f3117aee07..3c1d35cdd3 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -102,7 +102,7 @@ impl Render for RunningState { .find(|pane| pane.read(cx).is_zoomed()); let active = self.panes.panes().into_iter().next(); - let pane = if let Some(ref zoomed_pane) = zoomed_pane { + let pane = if let Some(zoomed_pane) = zoomed_pane { zoomed_pane.update(cx, |pane, cx| pane.render(window, cx).into_any_element()) } else if let Some(active) = active { self.panes @@ -627,7 +627,7 @@ impl RunningState { if s.starts_with("\"$ZED_") && s.ends_with('"') { *s = s[1..s.len() - 1].to_string(); } - if let Some(substituted) = substitute_variables_in_str(&s, context) { + if let Some(substituted) = substitute_variables_in_str(s, context) { *s = substituted; } } @@ -657,7 +657,7 @@ impl RunningState { } resolve_path(s); - if let Some(substituted) = substitute_variables_in_str(&s, context) { + if let Some(substituted) = substitute_variables_in_str(s, context) { *s = substituted; } } @@ -954,7 +954,7 @@ impl RunningState { inventory.read(cx).task_template_by_label( buffer, worktree_id, - &label, + label, cx, ) }) @@ -1310,7 +1310,7 @@ impl RunningState { let mut pane_item_status = IndexMap::from_iter( DebuggerPaneItem::all() .iter() - .filter(|kind| kind.is_supported(&caps)) + .filter(|kind| kind.is_supported(caps)) .map(|kind| (*kind, false)), ); self.panes.panes().iter().for_each(|pane| { @@ -1371,7 +1371,7 @@ impl RunningState { this.serialize_layout(window, cx); match event { Event::Remove { .. } => { - let _did_find_pane = this.panes.remove(&source_pane).is_ok(); + let _did_find_pane = this.panes.remove(source_pane).is_ok(); debug_assert!(_did_find_pane); cx.notify(); } diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index 38108dbfbc..9768f02e8e 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -494,7 +494,7 @@ impl BreakpointList { fn toggle_data_breakpoint(&mut self, id: &str, cx: &mut Context) { if let Some(session) = &self.session { session.update(cx, |this, cx| { - this.toggle_data_breakpoint(&id, cx); + this.toggle_data_breakpoint(id, cx); }); } } @@ -502,7 +502,7 @@ impl BreakpointList { fn toggle_exception_breakpoint(&mut self, id: &str, cx: &mut Context) { if let Some(session) = &self.session { session.update(cx, |this, cx| { - this.toggle_exception_breakpoint(&id, cx); + this.toggle_exception_breakpoint(id, cx); }); cx.notify(); const EXCEPTION_SERIALIZATION_INTERVAL: Duration = Duration::from_secs(1); diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index e6308518e4..42989ddc20 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -697,7 +697,7 @@ impl ConsoleQueryBarCompletionProvider { new_bytes: &[u8], snapshot: &TextBufferSnapshot, ) -> Range { - let buffer_offset = buffer_position.to_offset(&snapshot); + let buffer_offset = buffer_position.to_offset(snapshot); let buffer_bytes = &buffer_text.as_bytes()[0..buffer_offset]; let mut prefix_len = 0; @@ -977,7 +977,7 @@ mod tests { &cx.buffer_text(), snapshot.anchor_before(buffer_position), replacement.as_bytes(), - &snapshot, + snapshot, ); cx.update_editor(|editor, _, cx| { diff --git a/crates/debugger_ui/src/session/running/memory_view.rs b/crates/debugger_ui/src/session/running/memory_view.rs index f936d908b1..a09df6e728 100644 --- a/crates/debugger_ui/src/session/running/memory_view.rs +++ b/crates/debugger_ui/src/session/running/memory_view.rs @@ -262,7 +262,7 @@ impl MemoryView { cx: &mut Context, ) { use parse_int::parse; - let Ok(as_address) = parse::(&memory_reference) else { + let Ok(as_address) = parse::(memory_reference) else { return; }; let access_size = evaluate_name @@ -931,7 +931,7 @@ impl Render for MemoryView { v_flex() .size_full() .on_drag_move(cx.listener(|this, evt, _, _| { - this.handle_memory_drag(&evt); + this.handle_memory_drag(evt); })) .child(self.render_memory(cx).size_full()) .children(self.open_context_menu.as_ref().map(|(menu, position, _)| { diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index efbc72e8cf..b54ee29e15 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -1289,7 +1289,7 @@ impl VariableList { }), ) .child(self.render_variable_value( - &entry, + entry, &variable_color, watcher.value.to_string(), cx, @@ -1494,7 +1494,7 @@ impl VariableList { }), ) .child(self.render_variable_value( - &variable, + variable, &variable_color, dap.value.clone(), cx, diff --git a/crates/debugger_ui/src/tests/attach_modal.rs b/crates/debugger_ui/src/tests/attach_modal.rs index 906a7a0d4b..80e2b73d5a 100644 --- a/crates/debugger_ui/src/tests/attach_modal.rs +++ b/crates/debugger_ui/src/tests/attach_modal.rs @@ -139,7 +139,7 @@ async fn test_show_attach_modal_and_select_process( workspace .update(cx, |_, window, cx| { let names = - attach_modal.update(cx, |modal, cx| attach_modal::_process_names(&modal, cx)); + attach_modal.update(cx, |modal, cx| attach_modal::_process_names(modal, cx)); // Initially all processes are visible. assert_eq!(3, names.len()); attach_modal.update(cx, |this, cx| { @@ -154,7 +154,7 @@ async fn test_show_attach_modal_and_select_process( workspace .update(cx, |_, _, cx| { let names = - attach_modal.update(cx, |modal, cx| attach_modal::_process_names(&modal, cx)); + attach_modal.update(cx, |modal, cx| attach_modal::_process_names(modal, cx)); // Initially all processes are visible. assert_eq!(2, names.len()); }) diff --git a/crates/debugger_ui/src/tests/new_process_modal.rs b/crates/debugger_ui/src/tests/new_process_modal.rs index d6b0dfa004..5ac6af389d 100644 --- a/crates/debugger_ui/src/tests/new_process_modal.rs +++ b/crates/debugger_ui/src/tests/new_process_modal.rs @@ -107,7 +107,7 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths( let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") { input_path - .replace("$ZED_WORKTREE_ROOT", &path!("/test/worktree/path")) + .replace("$ZED_WORKTREE_ROOT", path!("/test/worktree/path")) .to_owned() } else { input_path.to_string() diff --git a/crates/diagnostics/src/diagnostic_renderer.rs b/crates/diagnostics/src/diagnostic_renderer.rs index ce7b253702..cb1c052925 100644 --- a/crates/diagnostics/src/diagnostic_renderer.rs +++ b/crates/diagnostics/src/diagnostic_renderer.rs @@ -46,7 +46,7 @@ impl DiagnosticRenderer { markdown.push_str(" ("); } if let Some(source) = diagnostic.source.as_ref() { - markdown.push_str(&Markdown::escape(&source)); + markdown.push_str(&Markdown::escape(source)); } if diagnostic.source.is_some() && diagnostic.code.is_some() { markdown.push(' '); @@ -306,7 +306,7 @@ impl DiagnosticBlock { cx: &mut Context, ) { let snapshot = &editor.buffer().read(cx).snapshot(cx); - let range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot); + let range = range.start.to_offset(snapshot)..range.end.to_offset(snapshot); editor.unfold_ranges(&[range.start..range.end], true, false, cx); editor.change_selections(Default::default(), window, cx, |s| { diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index e7660920da..23dbf33322 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -528,7 +528,7 @@ impl ProjectDiagnosticsEditor { lsp::DiagnosticSeverity::ERROR }; - cx.spawn_in(window, async move |this, mut cx| { + cx.spawn_in(window, async move |this, cx| { let diagnostics = buffer_snapshot .diagnostics_in_range::<_, text::Anchor>( Point::zero()..buffer_snapshot.max_point(), @@ -595,7 +595,7 @@ impl ProjectDiagnosticsEditor { b.initial_range.clone(), DEFAULT_MULTIBUFFER_CONTEXT, buffer_snapshot.clone(), - &mut cx, + cx, ) .await; let i = excerpt_ranges diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index 17804b4281..29011352fb 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -129,7 +129,7 @@ fn handle_frontmatter(book: &mut Book, errors: &mut HashSet) let Some((name, value)) = line.split_once(':') else { errors.insert(PreprocessorError::InvalidFrontmatterLine(format!( "{}: {}", - chapter_breadcrumbs(&chapter), + chapter_breadcrumbs(chapter), line ))); continue; @@ -402,11 +402,11 @@ fn handle_postprocessing() -> Result<()> { path: &'a std::path::PathBuf, root: &'a std::path::PathBuf, ) -> &'a std::path::Path { - &path.strip_prefix(&root).unwrap_or(&path) + path.strip_prefix(&root).unwrap_or(path) } fn extract_title_from_page(contents: &str, pretty_path: &std::path::Path) -> String { let title_tag_contents = &title_regex() - .captures(&contents) + .captures(contents) .with_context(|| format!("Failed to find title in {:?}", pretty_path)) .expect("Page has element")[1]; let title = title_tag_contents diff --git a/crates/editor/src/clangd_ext.rs b/crates/editor/src/clangd_ext.rs index 3239fdc653..07be9ea9e9 100644 --- a/crates/editor/src/clangd_ext.rs +++ b/crates/editor/src/clangd_ext.rs @@ -104,6 +104,6 @@ pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: & .filter_map(|buffer| buffer.read(cx).language()) .any(|language| is_c_language(language)) { - register_action(&editor, window, switch_source_header); + register_action(editor, window, switch_source_header); } } diff --git a/crates/editor/src/code_completion_tests.rs b/crates/editor/src/code_completion_tests.rs index fd8db29584..a1d9f04a9c 100644 --- a/crates/editor/src/code_completion_tests.rs +++ b/crates/editor/src/code_completion_tests.rs @@ -317,7 +317,7 @@ async fn filter_and_sort_matches( let candidates: Arc<[StringMatchCandidate]> = completions .iter() .enumerate() - .map(|(id, completion)| StringMatchCandidate::new(id, &completion.label.filter_text())) + .map(|(id, completion)| StringMatchCandidate::new(id, completion.label.filter_text())) .collect(); let cancel_flag = Arc::new(AtomicBool::new(false)); let background_executor = cx.executor(); @@ -331,5 +331,5 @@ async fn filter_and_sort_matches( background_executor, ) .await; - CompletionsMenu::sort_string_matches(matches, Some(query), snippet_sort_order, &completions) + CompletionsMenu::sort_string_matches(matches, Some(query), snippet_sort_order, completions) } diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 4ae2a14ca7..24d2cfddcb 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -321,7 +321,7 @@ impl CompletionsMenu { let match_candidates = choices .iter() .enumerate() - .map(|(id, completion)| StringMatchCandidate::new(id, &completion)) + .map(|(id, completion)| StringMatchCandidate::new(id, completion)) .collect(); let entries = choices .iter() diff --git a/crates/editor/src/display_map/custom_highlights.rs b/crates/editor/src/display_map/custom_highlights.rs index ae69e9cf8c..f3737ea4b7 100644 --- a/crates/editor/src/display_map/custom_highlights.rs +++ b/crates/editor/src/display_map/custom_highlights.rs @@ -77,7 +77,7 @@ fn create_highlight_endpoints( let ranges = &text_highlights.1; let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&start, &buffer); + let cmp = probe.end.cmp(&start, buffer); if cmp.is_gt() { cmp::Ordering::Greater } else { @@ -88,18 +88,18 @@ fn create_highlight_endpoints( }; for range in &ranges[start_ix..] { - if range.start.cmp(&end, &buffer).is_ge() { + if range.start.cmp(&end, buffer).is_ge() { break; } highlight_endpoints.push(HighlightEndpoint { - offset: range.start.to_offset(&buffer), + offset: range.start.to_offset(buffer), is_start: true, tag, style, }); highlight_endpoints.push(HighlightEndpoint { - offset: range.end.to_offset(&buffer), + offset: range.end.to_offset(buffer), is_start: false, tag, style, diff --git a/crates/editor/src/display_map/invisibles.rs b/crates/editor/src/display_map/invisibles.rs index 199986f2a4..19e4c2b42a 100644 --- a/crates/editor/src/display_map/invisibles.rs +++ b/crates/editor/src/display_map/invisibles.rs @@ -36,8 +36,8 @@ pub fn is_invisible(c: char) -> bool { } else if c >= '\u{7f}' { c <= '\u{9f}' || (c.is_whitespace() && c != IDEOGRAPHIC_SPACE) - || contains(c, &FORMAT) - || contains(c, &OTHER) + || contains(c, FORMAT) + || contains(c, OTHER) } else { false } @@ -50,7 +50,7 @@ pub fn replacement(c: char) -> Option<&'static str> { Some(C0_SYMBOLS[c as usize]) } else if c == '\x7f' { Some(DEL) - } else if contains(c, &PRESERVE) { + } else if contains(c, PRESERVE) { None } else { Some("\u{2007}") // fixed width space diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index caa4882a6e..0d2d1c4a4c 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1461,7 +1461,7 @@ mod tests { } let mut prev_ix = 0; - for boundary in line_wrapper.wrap_line(&[LineFragment::text(&line)], wrap_width) { + for boundary in line_wrapper.wrap_line(&[LineFragment::text(line)], wrap_width) { wrapped_text.push_str(&line[prev_ix..boundary.ix]); wrapped_text.push('\n'); wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize)); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e645bfee67..6edd4e9d8c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2379,7 +2379,7 @@ impl Editor { pending_selection .selection .range() - .includes(&range, &snapshot) + .includes(range, &snapshot) }) { return true; @@ -3342,9 +3342,9 @@ impl Editor { let old_cursor_position = &state.old_cursor_position; - self.selections_did_change(true, &old_cursor_position, state.effects, window, cx); + self.selections_did_change(true, old_cursor_position, state.effects, window, cx); - if self.should_open_signature_help_automatically(&old_cursor_position, cx) { + if self.should_open_signature_help_automatically(old_cursor_position, cx) { self.show_signature_help(&ShowSignatureHelp, window, cx); } } @@ -3764,9 +3764,9 @@ impl Editor { ColumnarSelectionState::FromMouse { selection_tail, display_point, - } => display_point.unwrap_or_else(|| selection_tail.to_display_point(&display_map)), + } => display_point.unwrap_or_else(|| selection_tail.to_display_point(display_map)), ColumnarSelectionState::FromSelection { selection_tail } => { - selection_tail.to_display_point(&display_map) + selection_tail.to_display_point(display_map) } }; @@ -6082,7 +6082,7 @@ impl Editor { if let Some(tasks) = &tasks { if let Some(project) = project { task_context_task = - Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx); + Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx); } } @@ -6864,7 +6864,7 @@ impl Editor { for (buffer_snapshot, search_range, excerpt_id) in buffer_ranges { match_ranges.extend( regex - .search(&buffer_snapshot, Some(search_range.clone())) + .search(buffer_snapshot, Some(search_range.clone())) .await .into_iter() .filter_map(|match_range| { @@ -7206,7 +7206,7 @@ impl Editor { return Some(false); } let provider = self.edit_prediction_provider()?; - if !provider.is_enabled(&buffer, buffer_position, cx) { + if !provider.is_enabled(buffer, buffer_position, cx) { return Some(false); } let buffer = buffer.read(cx); @@ -7966,7 +7966,7 @@ impl Editor { let multi_buffer_anchor = Anchor::in_buffer(excerpt_id, buffer_snapshot.remote_id(), breakpoint.position); let position = multi_buffer_anchor - .to_point(&multi_buffer_snapshot) + .to_point(multi_buffer_snapshot) .to_display_point(&snapshot); breakpoint_display_points.insert( @@ -8859,7 +8859,7 @@ impl Editor { } let highlighted_edits = if let Some(edit_preview) = edit_preview.as_ref() { - crate::edit_prediction_edit_text(&snapshot, edits, edit_preview, false, cx) + crate::edit_prediction_edit_text(snapshot, edits, edit_preview, false, cx) } else { // Fallback for providers without edit_preview crate::edit_prediction_fallback_text(edits, cx) @@ -9222,7 +9222,7 @@ impl Editor { .child(div().px_1p5().child(match &prediction.completion { EditPrediction::Move { target, snapshot } => { use text::ToPoint as _; - if target.text_anchor.to_point(&snapshot).row > cursor_point.row + if target.text_anchor.to_point(snapshot).row > cursor_point.row { Icon::new(IconName::ZedPredictDown) } else { @@ -9424,7 +9424,7 @@ impl Editor { .gap_2() .flex_1() .child( - if target.text_anchor.to_point(&snapshot).row > cursor_point.row { + if target.text_anchor.to_point(snapshot).row > cursor_point.row { Icon::new(IconName::ZedPredictDown) } else { Icon::new(IconName::ZedPredictUp) @@ -9440,14 +9440,14 @@ impl Editor { snapshot, display_mode: _, } => { - let first_edit_row = edits.first()?.0.start.text_anchor.to_point(&snapshot).row; + let first_edit_row = edits.first()?.0.start.text_anchor.to_point(snapshot).row; let (highlighted_edits, has_more_lines) = if let Some(edit_preview) = edit_preview.as_ref() { - crate::edit_prediction_edit_text(&snapshot, &edits, edit_preview, true, cx) + crate::edit_prediction_edit_text(snapshot, edits, edit_preview, true, cx) .first_line_preview() } else { - crate::edit_prediction_fallback_text(&edits, cx).first_line_preview() + crate::edit_prediction_fallback_text(edits, cx).first_line_preview() }; let styled_text = gpui::StyledText::new(highlighted_edits.text) @@ -9770,7 +9770,7 @@ impl Editor { if let Some(choices) = &snippet.choices[snippet.active_index] { if let Some(selection) = current_ranges.first() { - self.show_snippet_choices(&choices, selection.clone(), cx); + self.show_snippet_choices(choices, selection.clone(), cx); } } @@ -12284,7 +12284,7 @@ impl Editor { let trigger_in_words = this.show_edit_predictions_in_menu() || !had_active_edit_prediction; - this.trigger_completion_on_input(&text, trigger_in_words, window, cx); + this.trigger_completion_on_input(text, trigger_in_words, window, cx); }); } @@ -17896,7 +17896,7 @@ impl Editor { ranges: &[Range<Anchor>], snapshot: &MultiBufferSnapshot, ) -> bool { - let mut hunks = self.diff_hunks_in_ranges(ranges, &snapshot); + let mut hunks = self.diff_hunks_in_ranges(ranges, snapshot); hunks.any(|hunk| hunk.status().has_secondary_hunk()) } @@ -19042,8 +19042,8 @@ impl Editor { buffer_ranges.last() }?; - let selection = text::ToPoint::to_point(&range.start, &buffer).row - ..text::ToPoint::to_point(&range.end, &buffer).row; + let selection = text::ToPoint::to_point(&range.start, buffer).row + ..text::ToPoint::to_point(&range.end, buffer).row; Some(( multi_buffer.buffer(buffer.remote_id()).unwrap().clone(), selection, @@ -20055,8 +20055,7 @@ impl Editor { self.registered_buffers .entry(edited_buffer.read(cx).remote_id()) .or_insert_with(|| { - project - .register_buffer_with_language_servers(&edited_buffer, cx) + project.register_buffer_with_language_servers(edited_buffer, cx) }); }); } @@ -21079,7 +21078,7 @@ impl Editor { }; if let Some((workspace, path)) = workspace.as_ref().zip(path) { let Some(task) = cx - .update_window_entity(&workspace, |workspace, window, cx| { + .update_window_entity(workspace, |workspace, window, cx| { workspace .open_path_preview(path, None, false, false, false, window, cx) }) @@ -21303,14 +21302,14 @@ fn process_completion_for_edit( debug_assert!( insert_range .start - .cmp(&cursor_position, &buffer_snapshot) + .cmp(cursor_position, &buffer_snapshot) .is_le(), "insert_range should start before or at cursor position" ); debug_assert!( replace_range .start - .cmp(&cursor_position, &buffer_snapshot) + .cmp(cursor_position, &buffer_snapshot) .is_le(), "replace_range should start before or at cursor position" ); @@ -21344,7 +21343,7 @@ fn process_completion_for_edit( LspInsertMode::ReplaceSuffix => { if replace_range .end - .cmp(&cursor_position, &buffer_snapshot) + .cmp(cursor_position, &buffer_snapshot) .is_gt() { let range_after_cursor = *cursor_position..replace_range.end; @@ -21380,7 +21379,7 @@ fn process_completion_for_edit( if range_to_replace .end - .cmp(&cursor_position, &buffer_snapshot) + .cmp(cursor_position, &buffer_snapshot) .is_lt() { range_to_replace.end = *cursor_position; @@ -21388,7 +21387,7 @@ fn process_completion_for_edit( CompletionEdit { new_text, - replace_range: range_to_replace.to_offset(&buffer), + replace_range: range_to_replace.to_offset(buffer), snippet, } } @@ -22137,7 +22136,7 @@ fn snippet_completions( snippet .prefix .iter() - .map(move |prefix| StringMatchCandidate::new(ix, &prefix)) + .map(move |prefix| StringMatchCandidate::new(ix, prefix)) }) .collect::<Vec<StringMatchCandidate>>(); @@ -22366,10 +22365,10 @@ impl SemanticsProvider for Entity<Project> { cx: &mut App, ) -> Option<Task<Result<Vec<LocationLink>>>> { Some(self.update(cx, |project, cx| match kind { - GotoDefinitionKind::Symbol => project.definitions(&buffer, position, cx), - GotoDefinitionKind::Declaration => project.declarations(&buffer, position, cx), - GotoDefinitionKind::Type => project.type_definitions(&buffer, position, cx), - GotoDefinitionKind::Implementation => project.implementations(&buffer, position, cx), + GotoDefinitionKind::Symbol => project.definitions(buffer, position, cx), + GotoDefinitionKind::Declaration => project.declarations(buffer, position, cx), + GotoDefinitionKind::Type => project.type_definitions(buffer, position, cx), + GotoDefinitionKind::Implementation => project.implementations(buffer, position, cx), })) } @@ -23778,7 +23777,7 @@ fn all_edits_insertions_or_deletions( let mut all_deletions = true; for (range, new_text) in edits.iter() { - let range_is_empty = range.to_offset(&snapshot).is_empty(); + let range_is_empty = range.to_offset(snapshot).is_empty(); let text_is_empty = new_text.is_empty(); if range_is_empty != text_is_empty { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index f97dcd712c..189bdd1bf7 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -8393,7 +8393,7 @@ async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) buffer.set_language(Some(language), cx); }); - cx.set_state(&r#"struct A {ˇ}"#); + cx.set_state(r#"struct A {ˇ}"#); cx.update_editor(|editor, window, cx| { editor.newline(&Default::default(), window, cx); @@ -8405,7 +8405,7 @@ async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) }" )); - cx.set_state(&r#"select_biased!(ˇ)"#); + cx.set_state(r#"select_biased!(ˇ)"#); cx.update_editor(|editor, window, cx| { editor.newline(&Default::default(), window, cx); @@ -12319,7 +12319,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) let counter = Arc::new(AtomicUsize::new(0)); handle_completion_request_with_insert_and_replace( &mut cx, - &buffer_marked_text, + buffer_marked_text, vec![(completion_text, completion_text)], counter.clone(), ) @@ -12333,7 +12333,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) .confirm_completion_replace(&ConfirmCompletionReplace, window, cx) .unwrap() }); - cx.assert_editor_state(&expected_with_replace_mode); + cx.assert_editor_state(expected_with_replace_mode); handle_resolve_completion_request(&mut cx, None).await; apply_additional_edits.await.unwrap(); @@ -12353,7 +12353,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) }); handle_completion_request_with_insert_and_replace( &mut cx, - &buffer_marked_text, + buffer_marked_text, vec![(completion_text, completion_text)], counter.clone(), ) @@ -12367,7 +12367,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) .confirm_completion_insert(&ConfirmCompletionInsert, window, cx) .unwrap() }); - cx.assert_editor_state(&expected_with_insert_mode); + cx.assert_editor_state(expected_with_insert_mode); handle_resolve_completion_request(&mut cx, None).await; apply_additional_edits.await.unwrap(); } @@ -13141,7 +13141,7 @@ async fn test_word_completion(cx: &mut TestAppContext) { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(&menu), + completion_menu_entries(menu), &["first", "last"], "When LSP server is fast to reply, no fallback word completions are used" ); @@ -13164,7 +13164,7 @@ async fn test_word_completion(cx: &mut TestAppContext) { cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { - assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"], + assert_eq!(completion_menu_entries(menu), &["one", "three", "two"], "When LSP server is slow, document words can be shown instead, if configured accordingly"); } else { panic!("expected completion menu to be open"); @@ -13225,7 +13225,7 @@ async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(&menu), + completion_menu_entries(menu), &["first", "last", "second"], "Word completions that has the same edit as the any of the LSP ones, should not be proposed" ); @@ -13281,7 +13281,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(&menu), + completion_menu_entries(menu), &["first", "last", "second"], "`ShowWordCompletions` action should show word completions" ); @@ -13298,7 +13298,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(&menu), + completion_menu_entries(menu), &["last"], "After showing word completions, further editing should filter them and not query the LSP" ); @@ -13337,7 +13337,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(&menu), + completion_menu_entries(menu), &["let"], "With no digits in the completion query, no digits should be in the word completions" ); @@ -13362,7 +13362,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { - assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \ + assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \ return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)"); } else { panic!("expected completion menu to be open"); @@ -13599,7 +13599,7 @@ async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) { cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { - assert_eq!(completion_menu_entries(&menu), &["first", "last"]); + assert_eq!(completion_menu_entries(menu), &["first", "last"]); } else { panic!("expected completion menu to be open"); } @@ -16702,7 +16702,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(&menu), + completion_menu_entries(menu), &["bg-blue", "bg-red", "bg-yellow"] ); } else { @@ -16715,7 +16715,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { - assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]); + assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]); } else { panic!("expected completion menu to be open"); } @@ -16729,7 +16729,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { - assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]); + assert_eq!(completion_menu_entries(menu), &["bg-yellow"]); } else { panic!("expected completion menu to be open"); } @@ -17298,7 +17298,7 @@ async fn test_multibuffer_reverts(cx: &mut TestAppContext) { (buffer_2.clone(), base_text_2), (buffer_3.clone(), base_text_3), ] { - let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx)); + let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx)); editor .buffer .update(cx, |buffer, cx| buffer.add_diff(diff, cx)); @@ -17919,7 +17919,7 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) { (buffer_2.clone(), file_2_old), (buffer_3.clone(), file_3_old), ] { - let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx)); + let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx)); editor .buffer .update(cx, |buffer, cx| buffer.add_diff(diff, cx)); @@ -21024,7 +21024,7 @@ async fn assert_highlighted_edits( cx.update(|_window, cx| { let highlighted_edits = edit_prediction_edit_text( - &snapshot.as_singleton().unwrap().2, + snapshot.as_singleton().unwrap().2, &edits, &edit_preview, include_deletions, @@ -21091,7 +21091,7 @@ fn add_log_breakpoint_at_cursor( .buffer_snapshot .anchor_before(Point::new(cursor_position.row, 0)); - (breakpoint_position, Breakpoint::new_log(&log_message)) + (breakpoint_position, Breakpoint::new_log(log_message)) }); editor.edit_breakpoint_at_anchor( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e56ac45fab..927a207358 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1162,7 +1162,7 @@ impl EditorElement { .map_or(false, |state| state.keyboard_grace); if mouse_over_inline_blame || mouse_over_popover { - editor.show_blame_popover(&blame_entry, event.position, false, cx); + editor.show_blame_popover(blame_entry, event.position, false, cx); } else if !keyboard_grace { editor.hide_blame_popover(cx); } @@ -2818,7 +2818,7 @@ impl EditorElement { } let row = - MultiBufferRow(DisplayPoint::new(display_row, 0).to_point(&snapshot).row); + MultiBufferRow(DisplayPoint::new(display_row, 0).to_point(snapshot).row); if snapshot.is_line_folded(row) { return None; } @@ -3312,7 +3312,7 @@ impl EditorElement { let chunks = snapshot.highlighted_chunks(rows.clone(), true, style); LineWithInvisibles::from_chunks( chunks, - &style, + style, MAX_LINE_LEN, rows.len(), &snapshot.mode, @@ -3393,7 +3393,7 @@ impl EditorElement { let line_ix = align_to.row().0.checked_sub(rows.start.0); x_position = if let Some(layout) = line_ix.and_then(|ix| line_layouts.get(ix as usize)) { - x_and_width(&layout) + x_and_width(layout) } else { x_and_width(&layout_line( align_to.row(), @@ -5549,9 +5549,9 @@ impl EditorElement { // In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor. // In multi buffers, we open file at the line number clicked, so use a pointing hand cursor. if is_singleton { - window.set_cursor_style(CursorStyle::IBeam, &hitbox); + window.set_cursor_style(CursorStyle::IBeam, hitbox); } else { - window.set_cursor_style(CursorStyle::PointingHand, &hitbox); + window.set_cursor_style(CursorStyle::PointingHand, hitbox); } } } @@ -5570,7 +5570,7 @@ impl EditorElement { &layout.position_map.snapshot, line_height, layout.gutter_hitbox.bounds, - &hunk, + hunk, ); Some(( hunk_bounds, @@ -6092,10 +6092,10 @@ impl EditorElement { if axis == ScrollbarAxis::Vertical { let fast_markers = - self.collect_fast_scrollbar_markers(layout, &scrollbar_layout, cx); + self.collect_fast_scrollbar_markers(layout, scrollbar_layout, cx); // Refresh slow scrollbar markers in the background. Below, we // paint whatever markers have already been computed. - self.refresh_slow_scrollbar_markers(layout, &scrollbar_layout, window, cx); + self.refresh_slow_scrollbar_markers(layout, scrollbar_layout, window, cx); let markers = self.editor.read(cx).scrollbar_marker_state.markers.clone(); for marker in markers.iter().chain(&fast_markers) { @@ -6129,7 +6129,7 @@ impl EditorElement { if any_scrollbar_dragged { window.set_window_cursor_style(CursorStyle::Arrow); } else { - window.set_cursor_style(CursorStyle::Arrow, &hitbox); + window.set_cursor_style(CursorStyle::Arrow, hitbox); } } }) @@ -9782,7 +9782,7 @@ pub fn layout_line( let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style); LineWithInvisibles::from_chunks( chunks, - &style, + style, MAX_LINE_LEN, 1, &snapshot.mode, diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 02f93e6829..8b6e2cea84 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -794,7 +794,7 @@ pub(crate) async fn find_file( ) -> Option<ResolvedPath> { project .update(cx, |project, cx| { - project.resolve_path_in_buffer(&candidate_file_path, buffer, cx) + project.resolve_path_in_buffer(candidate_file_path, buffer, cx) }) .ok()? .await diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 34533002ff..22430ab5e1 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -524,8 +524,8 @@ fn serialize_selection( ) -> proto::Selection { proto::Selection { id: selection.id as u64, - start: Some(serialize_anchor(&selection.start, &buffer)), - end: Some(serialize_anchor(&selection.end, &buffer)), + start: Some(serialize_anchor(&selection.start, buffer)), + end: Some(serialize_anchor(&selection.end, buffer)), reversed: selection.reversed, } } @@ -1010,7 +1010,7 @@ impl Item for Editor { self.workspace = Some((workspace.weak_handle(), workspace.database_id())); if let Some(workspace) = &workspace.weak_handle().upgrade() { cx.subscribe( - &workspace, + workspace, |editor, _, event: &workspace::Event, _cx| match event { workspace::Event::ModalOpened => { editor.mouse_context_menu.take(); @@ -1296,7 +1296,7 @@ impl SerializableItem for Editor { project .read(cx) .worktree_for_id(worktree_id, cx) - .and_then(|worktree| worktree.read(cx).absolutize(&file.path()).ok()) + .and_then(|worktree| worktree.read(cx).absolutize(file.path()).ok()) .or_else(|| { let full_path = file.full_path(cx); let project_path = project.read(cx).find_project_path(&full_path, cx)?; @@ -1385,14 +1385,14 @@ impl ProjectItem for Editor { }) { editor.fold_ranges( - clip_ranges(&restoration_data.folds, &snapshot), + clip_ranges(&restoration_data.folds, snapshot), false, window, cx, ); if !restoration_data.selections.is_empty() { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges(clip_ranges(&restoration_data.selections, &snapshot)); + s.select_ranges(clip_ranges(&restoration_data.selections, snapshot)); }); } let (top_row, offset) = restoration_data.scroll_position; diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index 95a7925839..f358ab7b93 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -37,7 +37,7 @@ pub(crate) fn should_auto_close( let text = buffer .text_for_range(edited_range.clone()) .collect::<String>(); - let edited_range = edited_range.to_offset(&buffer); + let edited_range = edited_range.to_offset(buffer); if !text.ends_with(">") { continue; } diff --git a/crates/editor/src/lsp_colors.rs b/crates/editor/src/lsp_colors.rs index 08cf9078f2..29eb9f249a 100644 --- a/crates/editor/src/lsp_colors.rs +++ b/crates/editor/src/lsp_colors.rs @@ -207,7 +207,7 @@ impl Editor { .entry(buffer_snapshot.remote_id()) .or_insert_with(Vec::new); let excerpt_point_range = - excerpt_range.context.to_point_utf16(&buffer_snapshot); + excerpt_range.context.to_point_utf16(buffer_snapshot); excerpt_data.push(( excerpt_id, buffer_snapshot.clone(), diff --git a/crates/editor/src/lsp_ext.rs b/crates/editor/src/lsp_ext.rs index 6161afbbc0..d02fc0f901 100644 --- a/crates/editor/src/lsp_ext.rs +++ b/crates/editor/src/lsp_ext.rs @@ -76,7 +76,7 @@ async fn lsp_task_context( let project_env = project .update(cx, |project, cx| { - project.buffer_environment(&buffer, &worktree_store, cx) + project.buffer_environment(buffer, &worktree_store, cx) }) .ok()? .await; diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 9d5145dec1..7f9eb374e8 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -102,11 +102,11 @@ impl MouseContextMenu { let display_snapshot = &editor .display_map .update(cx, |display_map, cx| display_map.snapshot(cx)); - let selection_init_range = selection_init.display_range(&display_snapshot); + let selection_init_range = selection_init.display_range(display_snapshot); let selection_now_range = editor .selections .newest_anchor() - .display_range(&display_snapshot); + .display_range(display_snapshot); if selection_now_range == selection_init_range { return; } diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index fdda0e82bc..0bf875095b 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -439,17 +439,17 @@ pub fn start_of_excerpt( }; match direction { Direction::Prev => { - let mut start = excerpt.start_anchor().to_display_point(&map); + let mut start = excerpt.start_anchor().to_display_point(map); if start >= display_point && start.row() > DisplayRow(0) { let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else { return display_point; }; - start = excerpt.start_anchor().to_display_point(&map); + start = excerpt.start_anchor().to_display_point(map); } start } Direction::Next => { - let mut end = excerpt.end_anchor().to_display_point(&map); + let mut end = excerpt.end_anchor().to_display_point(map); *end.row_mut() += 1; map.clip_point(end, Bias::Right) } @@ -467,7 +467,7 @@ pub fn end_of_excerpt( }; match direction { Direction::Prev => { - let mut start = excerpt.start_anchor().to_display_point(&map); + let mut start = excerpt.start_anchor().to_display_point(map); if start.row() > DisplayRow(0) { *start.row_mut() -= 1; } @@ -476,7 +476,7 @@ pub fn end_of_excerpt( start } Direction::Next => { - let mut end = excerpt.end_anchor().to_display_point(&map); + let mut end = excerpt.end_anchor().to_display_point(map); *end.column_mut() = 0; if end <= display_point { *end.row_mut() += 1; @@ -485,7 +485,7 @@ pub fn end_of_excerpt( else { return display_point; }; - end = excerpt.end_anchor().to_display_point(&map); + end = excerpt.end_anchor().to_display_point(map); *end.column_mut() = 0; } end diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index 1ead45b3de..e549f64758 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -478,7 +478,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { } fn supports_inlay_hints(&self, buffer: &Entity<Buffer>, cx: &mut App) -> bool { - if let Some(buffer) = self.to_base(&buffer, &[], cx) { + if let Some(buffer) = self.to_base(buffer, &[], cx) { self.0.supports_inlay_hints(&buffer, cx) } else { false @@ -491,7 +491,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { position: text::Anchor, cx: &mut App, ) -> Option<Task<anyhow::Result<Vec<project::DocumentHighlight>>>> { - let buffer = self.to_base(&buffer, &[position], cx)?; + let buffer = self.to_base(buffer, &[position], cx)?; self.0.document_highlights(&buffer, position, cx) } @@ -502,7 +502,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { kind: crate::GotoDefinitionKind, cx: &mut App, ) -> Option<Task<anyhow::Result<Vec<project::LocationLink>>>> { - let buffer = self.to_base(&buffer, &[position], cx)?; + let buffer = self.to_base(buffer, &[position], cx)?; self.0.definitions(&buffer, position, kind, cx) } diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index 2b8150de67..bee9464124 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -35,12 +35,12 @@ pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: & .filter_map(|buffer| buffer.read(cx).language()) .any(|language| is_rust_language(language)) { - register_action(&editor, window, go_to_parent_module); - register_action(&editor, window, expand_macro_recursively); - register_action(&editor, window, open_docs); - register_action(&editor, window, cancel_flycheck_action); - register_action(&editor, window, run_flycheck_action); - register_action(&editor, window, clear_flycheck_action); + register_action(editor, window, go_to_parent_module); + register_action(editor, window, expand_macro_recursively); + register_action(editor, window, open_docs); + register_action(editor, window, cancel_flycheck_action); + register_action(editor, window, run_flycheck_action); + register_action(editor, window, clear_flycheck_action); } } diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs index e0736a6e9f..5c9800ab55 100644 --- a/crates/editor/src/signature_help.rs +++ b/crates/editor/src/signature_help.rs @@ -196,7 +196,7 @@ impl Editor { .highlight_text(&text, 0..signature.label.len()) .into_iter() .flat_map(|(range, highlight_id)| { - Some((range, highlight_id.style(&cx.theme().syntax())?)) + Some((range, highlight_id.style(cx.theme().syntax())?)) }); signature.highlights = combine_highlights(signature.highlights.clone(), highlights) diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index f328945dbe..819d6d9fed 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -189,7 +189,7 @@ pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestCo continue; } }; - let content = block_content_for_tests(&editor, custom_block.id, cx) + let content = block_content_for_tests(editor, custom_block.id, cx) .expect("block content not found"); // 2: "related info 1 for diagnostic 0" if let Some(height) = custom_block.height { diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index 6558222d89..53c9113934 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -520,7 +520,7 @@ async fn judge_example( enable_telemetry: bool, cx: &AsyncApp, ) -> JudgeOutput { - let judge_output = example.judge(model.clone(), &run_output, cx).await; + let judge_output = example.judge(model.clone(), run_output, cx).await; if enable_telemetry { telemetry::event!( diff --git a/crates/eval/src/example.rs b/crates/eval/src/example.rs index 23c8814916..82e95728a1 100644 --- a/crates/eval/src/example.rs +++ b/crates/eval/src/example.rs @@ -64,7 +64,7 @@ impl ExampleMetadata { self.url .split('/') .next_back() - .unwrap_or(&"") + .unwrap_or("") .trim_end_matches(".git") .into() } @@ -255,7 +255,7 @@ impl ExampleContext { thread.update(cx, |thread, _cx| { if let Some(tool_use) = pending_tool_use { let mut tool_metrics = tool_metrics.lock().unwrap(); - if let Some(tool_result) = thread.tool_result(&tool_use_id) { + if let Some(tool_result) = thread.tool_result(tool_use_id) { let message = if tool_result.is_error { format!("✖︎ {}", tool_use.name) } else { diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs index 0f2b4c18ea..e3b67ed355 100644 --- a/crates/eval/src/instance.rs +++ b/crates/eval/src/instance.rs @@ -459,8 +459,8 @@ impl ExampleInstance { let mut output_file = File::create(self.run_directory.join("judge.md")).expect("failed to create judge.md"); - let diff_task = self.judge_diff(model.clone(), &run_output, cx); - let thread_task = self.judge_thread(model.clone(), &run_output, cx); + let diff_task = self.judge_diff(model.clone(), run_output, cx); + let thread_task = self.judge_thread(model.clone(), run_output, cx); let (diff_result, thread_result) = futures::join!(diff_task, thread_task); @@ -661,7 +661,7 @@ pub fn wait_for_lang_server( .update(cx, |buffer, cx| { lsp_store.update(cx, |lsp_store, cx| { lsp_store - .language_servers_for_local_buffer(&buffer, cx) + .language_servers_for_local_buffer(buffer, cx) .next() .is_some() }) @@ -693,7 +693,7 @@ pub fn wait_for_lang_server( _ => {} } }), - cx.subscribe(&project, { + cx.subscribe(project, { let buffer = buffer.clone(); move |project, event, cx| match event { project::Event::LanguageServerAdded(_, _, _) => { @@ -838,7 +838,7 @@ fn messages_to_markdown<'a>(message_iter: impl IntoIterator<Item = &'a Message>) for segment in &message.segments { match segment { MessageSegment::Text(text) => { - messages.push_str(&text); + messages.push_str(text); messages.push_str("\n\n"); } MessageSegment::Thinking { text, signature } => { @@ -846,7 +846,7 @@ fn messages_to_markdown<'a>(message_iter: impl IntoIterator<Item = &'a Message>) if let Some(sig) = signature { messages.push_str(&format!("Signature: {}\n\n", sig)); } - messages.push_str(&text); + messages.push_str(text); messages.push_str("\n"); } MessageSegment::RedactedThinking(items) => { @@ -878,7 +878,7 @@ pub async fn send_language_model_request( request: LanguageModelRequest, cx: &AsyncApp, ) -> anyhow::Result<String> { - match model.stream_completion_text(request, &cx).await { + match model.stream_completion_text(request, cx).await { Ok(mut stream) => { let mut full_response = String::new(); while let Some(chunk_result) = stream.stream.next().await { diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs index 621ba9250c..b80525798b 100644 --- a/crates/extension/src/extension_builder.rs +++ b/crates/extension/src/extension_builder.rs @@ -452,7 +452,7 @@ impl ExtensionBuilder { let mut output = Vec::new(); let mut stack = Vec::new(); - for payload in Parser::new(0).parse_all(&input) { + for payload in Parser::new(0).parse_all(input) { let payload = payload?; // Track nesting depth, so that we don't mess with inner producer sections: diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index e795fa5ac5..4ee948dda8 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1341,7 +1341,7 @@ impl ExtensionStore { &extension_path, &extension.manifest, wasm_host.clone(), - &cx, + cx, ) .await .with_context(|| format!("Loading extension from {extension_path:?}")); @@ -1776,7 +1776,7 @@ impl ExtensionStore { })?; for client in clients { - Self::sync_extensions_over_ssh(&this, client, cx) + Self::sync_extensions_over_ssh(this, client, cx) .await .log_err(); } diff --git a/crates/extension_host/src/headless_host.rs b/crates/extension_host/src/headless_host.rs index 8ce3847376..a6305118cd 100644 --- a/crates/extension_host/src/headless_host.rs +++ b/crates/extension_host/src/headless_host.rs @@ -175,7 +175,7 @@ impl HeadlessExtensionStore { } let wasm_extension: Arc<dyn Extension> = - Arc::new(WasmExtension::load(&extension_dir, &manifest, wasm_host.clone(), &cx).await?); + Arc::new(WasmExtension::load(&extension_dir, &manifest, wasm_host.clone(), cx).await?); for (language_server_id, language_server_config) in &manifest.language_servers { for language in language_server_config.languages() { diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index c6997ccdc0..e8f80e5ef2 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -210,7 +210,7 @@ impl FileFinder { return; }; if self.picker.read(cx).delegate.has_changed_selected_index { - if !event.modified() || !init_modifiers.is_subset_of(&event) { + if !event.modified() || !init_modifiers.is_subset_of(event) { self.init_modifiers = None; window.dispatch_action(menu::Confirm.boxed_clone(), cx); } @@ -497,7 +497,7 @@ impl Match { fn panel_match(&self) -> Option<&ProjectPanelOrdMatch> { match self { Match::History { panel_match, .. } => panel_match.as_ref(), - Match::Search(panel_match) => Some(&panel_match), + Match::Search(panel_match) => Some(panel_match), Match::CreateNew(_) => None, } } @@ -537,7 +537,7 @@ impl Matches { self.matches.binary_search_by(|m| { // `reverse()` since if cmp_matches(a, b) == Ordering::Greater, then a is better than b. // And we want the better entries go first. - Self::cmp_matches(self.separate_history, currently_opened, &m, &entry).reverse() + Self::cmp_matches(self.separate_history, currently_opened, m, entry).reverse() }) } } @@ -1082,7 +1082,7 @@ impl FileFinderDelegate { if let Some(user_home_path) = std::env::var("HOME").ok() { let user_home_path = user_home_path.trim(); if !user_home_path.is_empty() { - if (&full_path).starts_with(user_home_path) { + if full_path.starts_with(user_home_path) { full_path.replace_range(0..user_home_path.len(), "~"); full_path_positions.retain_mut(|pos| { if *pos >= user_home_path.len() { @@ -1402,7 +1402,7 @@ impl PickerDelegate for FileFinderDelegate { cx.notify(); Task::ready(()) } else { - let path_position = PathWithPosition::parse_str(&raw_query); + let path_position = PathWithPosition::parse_str(raw_query); #[cfg(windows)] let raw_query = raw_query.trim().to_owned().replace("/", "\\"); diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index db259ccef8..8203d1b1fd 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -1614,7 +1614,7 @@ async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppCon let picker = open_file_picker(&workspace, cx); picker.update(cx, |finder, _| { - assert_match_selection(&finder, 0, "1_qw"); + assert_match_selection(finder, 0, "1_qw"); }); } @@ -2623,7 +2623,7 @@ async fn open_queried_buffer( workspace: &Entity<Workspace>, cx: &mut gpui::VisualTestContext, ) -> Vec<FoundPath> { - let picker = open_file_picker(&workspace, cx); + let picker = open_file_picker(workspace, cx); cx.simulate_input(input); let history_items = picker.update(cx, |finder, _| { diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index 68ba7a78b5..7235568e4f 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -637,7 +637,7 @@ impl PickerDelegate for OpenPathDelegate { FileIcons::get_folder_icon(false, cx)? } else { let path = path::Path::new(&candidate.path.string); - FileIcons::get_icon(&path, cx)? + FileIcons::get_icon(path, cx)? }; Some(Icon::from_path(icon).color(Color::Muted)) }); diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 22bfdbcd66..64eeae99d1 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -776,7 +776,7 @@ impl Fs for RealFs { } // Check if path is a symlink and follow the target parent - if let Some(mut target) = self.read_link(&path).await.ok() { + if let Some(mut target) = self.read_link(path).await.ok() { // Check if symlink target is relative path, if so make it absolute if target.is_relative() { if let Some(parent) = path.parent() { @@ -1677,7 +1677,7 @@ impl FakeFs { /// by mutating the head, index, and unmerged state. pub fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&Path, FileStatus)]) { let workdir_path = dot_git.parent().unwrap(); - let workdir_contents = self.files_with_contents(&workdir_path); + let workdir_contents = self.files_with_contents(workdir_path); self.with_git_state(dot_git, true, |state| { state.index_contents.clear(); state.head_contents.clear(); @@ -2244,7 +2244,7 @@ impl Fs for FakeFs { async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>> { self.simulate_random_delay().await; let mut state = self.state.lock(); - let inode = match state.entry(&path)? { + let inode = match state.entry(path)? { FakeFsEntry::File { inode, .. } => *inode, FakeFsEntry::Dir { inode, .. } => *inode, _ => unreachable!(), diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 49eee84840..ae8c5f849c 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -858,7 +858,7 @@ impl GitRepository for RealGitRepository { let output = new_smol_command(&git_binary_path) .current_dir(&working_directory) .envs(env.iter()) - .args(["update-index", "--add", "--cacheinfo", "100644", &sha]) + .args(["update-index", "--add", "--cacheinfo", "100644", sha]) .arg(path.to_unix_style()) .output() .await?; @@ -959,7 +959,7 @@ impl GitRepository for RealGitRepository { Ok(working_directory) => working_directory, Err(e) => return Task::ready(Err(e)), }; - let args = git_status_args(&path_prefixes); + let args = git_status_args(path_prefixes); log::debug!("Checking for git status in {path_prefixes:?}"); self.executor.spawn(async move { let output = new_std_command(&git_binary_path) @@ -1056,7 +1056,7 @@ impl GitRepository for RealGitRepository { let (_, branch_name) = name.split_once("/").context("Unexpected branch format")?; let revision = revision.get(); let branch_commit = revision.peel_to_commit()?; - let mut branch = repo.branch(&branch_name, &branch_commit, false)?; + let mut branch = repo.branch(branch_name, &branch_commit, false)?; branch.set_upstream(Some(&name))?; branch } else { @@ -2349,7 +2349,7 @@ mod tests { #[allow(clippy::octal_escapes)] let input = "*\0060964da10574cd9bf06463a53bf6e0769c5c45e\0\0refs/heads/zed-patches\0refs/remotes/origin/zed-patches\0\01733187470\0generated protobuf\n"; assert_eq!( - parse_branch_input(&input).unwrap(), + parse_branch_input(input).unwrap(), vec![Branch { is_head: true, ref_name: "refs/heads/zed-patches".into(), diff --git a/crates/git/src/status.rs b/crates/git/src/status.rs index 6158b51798..92836042f2 100644 --- a/crates/git/src/status.rs +++ b/crates/git/src/status.rs @@ -468,7 +468,7 @@ impl FromStr for GitStatus { Some((path, status)) }) .collect::<Vec<_>>(); - entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b)); + entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(b)); // When a file exists in HEAD, is deleted in the index, and exists again in the working copy, // git produces two lines for it, one reading `D ` (deleted in index, unmodified in working copy) // and the other reading `??` (untracked). Merge these two into the equivalent of `DA`. diff --git a/crates/git_hosting_providers/src/git_hosting_providers.rs b/crates/git_hosting_providers/src/git_hosting_providers.rs index b31412ed4a..d4b3a59375 100644 --- a/crates/git_hosting_providers/src/git_hosting_providers.rs +++ b/crates/git_hosting_providers/src/git_hosting_providers.rs @@ -55,7 +55,7 @@ pub fn get_host_from_git_remote_url(remote_url: &str) -> Result<String> { } } - Url::parse(&remote_url) + Url::parse(remote_url) .ok() .and_then(|remote_url| remote_url.host_str().map(|host| host.to_string())) }) diff --git a/crates/git_hosting_providers/src/providers/chromium.rs b/crates/git_hosting_providers/src/providers/chromium.rs index b68c629ec7..5d940fb496 100644 --- a/crates/git_hosting_providers/src/providers/chromium.rs +++ b/crates/git_hosting_providers/src/providers/chromium.rs @@ -292,7 +292,7 @@ mod tests { assert_eq!( Chromium - .extract_pull_request(&remote, &message) + .extract_pull_request(&remote, message) .unwrap() .url .as_str(), diff --git a/crates/git_hosting_providers/src/providers/github.rs b/crates/git_hosting_providers/src/providers/github.rs index 30f8d058a7..4475afeb49 100644 --- a/crates/git_hosting_providers/src/providers/github.rs +++ b/crates/git_hosting_providers/src/providers/github.rs @@ -474,7 +474,7 @@ mod tests { assert_eq!( github - .extract_pull_request(&remote, &message) + .extract_pull_request(&remote, message) .unwrap() .url .as_str(), @@ -488,6 +488,6 @@ mod tests { See the original PR, this is a fix. "# }; - assert_eq!(github.extract_pull_request(&remote, &message), None); + assert_eq!(github.extract_pull_request(&remote, message), None); } } diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index c8c237fe90..07896b0c01 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -160,7 +160,7 @@ impl CommitView { }); } - cx.spawn(async move |this, mut cx| { + cx.spawn(async move |this, cx| { for file in commit_diff.files { let is_deleted = file.new_text.is_none(); let new_text = file.new_text.unwrap_or_default(); @@ -179,9 +179,9 @@ impl CommitView { worktree_id, }) as Arc<dyn language::File>; - let buffer = build_buffer(new_text, file, &language_registry, &mut cx).await?; + let buffer = build_buffer(new_text, file, &language_registry, cx).await?; let buffer_diff = - build_buffer_diff(old_text, &buffer, &language_registry, &mut cx).await?; + build_buffer_diff(old_text, &buffer, &language_registry, cx).await?; this.update(cx, |this, cx| { this.multibuffer.update(cx, |multibuffer, cx| { diff --git a/crates/git_ui/src/conflict_view.rs b/crates/git_ui/src/conflict_view.rs index 6482ebb9f8..5c1b1325a5 100644 --- a/crates/git_ui/src/conflict_view.rs +++ b/crates/git_ui/src/conflict_view.rs @@ -156,7 +156,7 @@ fn buffers_removed(editor: &mut Editor, removed_buffer_ids: &[BufferId], cx: &mu .unwrap() .buffers .retain(|buffer_id, buffer| { - if removed_buffer_ids.contains(&buffer_id) { + if removed_buffer_ids.contains(buffer_id) { removed_block_ids.extend(buffer.block_ids.iter().map(|(_, block_id)| *block_id)); false } else { @@ -222,12 +222,12 @@ fn conflicts_updated( let precedes_start = range .context .start - .cmp(&conflict_range.start, &buffer_snapshot) + .cmp(&conflict_range.start, buffer_snapshot) .is_le(); let follows_end = range .context .end - .cmp(&conflict_range.start, &buffer_snapshot) + .cmp(&conflict_range.start, buffer_snapshot) .is_ge(); precedes_start && follows_end }) else { @@ -268,12 +268,12 @@ fn conflicts_updated( let precedes_start = range .context .start - .cmp(&conflict.range.start, &buffer_snapshot) + .cmp(&conflict.range.start, buffer_snapshot) .is_le(); let follows_end = range .context .end - .cmp(&conflict.range.start, &buffer_snapshot) + .cmp(&conflict.range.start, buffer_snapshot) .is_ge(); precedes_start && follows_end }) else { diff --git a/crates/git_ui/src/file_diff_view.rs b/crates/git_ui/src/file_diff_view.rs index 2f8a744ed8..f7d29cdfa7 100644 --- a/crates/git_ui/src/file_diff_view.rs +++ b/crates/git_ui/src/file_diff_view.rs @@ -398,7 +398,7 @@ mod tests { let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; - let (workspace, mut cx) = + let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); let diff_view = workspace @@ -417,7 +417,7 @@ mod tests { // Verify initial diff assert_state_with_diff( &diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()), - &mut cx, + cx, &unindent( " - old line 1 @@ -452,7 +452,7 @@ mod tests { cx.executor().advance_clock(RECALCULATE_DIFF_DEBOUNCE); assert_state_with_diff( &diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()), - &mut cx, + cx, &unindent( " - old line 1 @@ -487,7 +487,7 @@ mod tests { cx.executor().advance_clock(RECALCULATE_DIFF_DEBOUNCE); assert_state_with_diff( &diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()), - &mut cx, + cx, &unindent( " ˇnew line 1 diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 754812cbdf..c21ac286cb 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -103,7 +103,7 @@ fn prompt<T>( where T: IntoEnumIterator + VariantNames + 'static, { - let rx = window.prompt(PromptLevel::Info, msg, detail, &T::VARIANTS, cx); + let rx = window.prompt(PromptLevel::Info, msg, detail, T::VARIANTS, cx); cx.spawn(async move |_| Ok(T::iter().nth(rx.await?).unwrap())) } @@ -652,14 +652,14 @@ impl GitPanel { if GitPanelSettings::get_global(cx).sort_by_path { return self .entries - .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path)) + .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(path)) .ok(); } if self.conflicted_count > 0 { let conflicted_start = 1; if let Ok(ix) = self.entries[conflicted_start..conflicted_start + self.conflicted_count] - .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path)) + .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(path)) { return Some(conflicted_start + ix); } @@ -671,7 +671,7 @@ impl GitPanel { 0 } + 1; if let Ok(ix) = self.entries[tracked_start..tracked_start + self.tracked_count] - .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path)) + .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(path)) { return Some(tracked_start + ix); } @@ -687,7 +687,7 @@ impl GitPanel { 0 } + 1; if let Ok(ix) = self.entries[untracked_start..untracked_start + self.new_count] - .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path)) + .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(path)) { return Some(untracked_start + ix); } @@ -1341,7 +1341,7 @@ impl GitPanel { .iter() .filter_map(|entry| entry.status_entry()) .filter(|status_entry| { - section.contains(&status_entry, repository) + section.contains(status_entry, repository) && status_entry.staging.as_bool() != Some(goal_staged_state) }) .map(|status_entry| status_entry.clone()) @@ -1952,7 +1952,7 @@ impl GitPanel { thinking_allowed: false, }; - let stream = model.stream_completion_text(request, &cx); + let stream = model.stream_completion_text(request, cx); match stream.await { Ok(mut messages) => { if !text_empty { @@ -4620,7 +4620,7 @@ impl editor::Addon for GitPanelAddon { git_panel .read(cx) - .render_buffer_header_controls(&git_panel, &file, window, cx) + .render_buffer_header_controls(&git_panel, file, window, cx) } } diff --git a/crates/git_ui/src/picker_prompt.rs b/crates/git_ui/src/picker_prompt.rs index 4077e0f362..3f1d507c42 100644 --- a/crates/git_ui/src/picker_prompt.rs +++ b/crates/git_ui/src/picker_prompt.rs @@ -152,7 +152,7 @@ impl PickerDelegate for PickerPromptDelegate { .all_options .iter() .enumerate() - .map(|(ix, option)| StringMatchCandidate::new(ix, &option)) + .map(|(ix, option)| StringMatchCandidate::new(ix, option)) .collect::<Vec<StringMatchCandidate>>() }); let Some(candidates) = candidates.log_err() else { diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index d6a4e27286..e312d6a2aa 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -1173,7 +1173,7 @@ impl RenderOnce for ProjectDiffEmptyState { .child(Label::new("No Changes").color(Color::Muted)) } else { this.when_some(self.current_branch.as_ref(), |this, branch| { - this.child(has_branch_container(&branch)) + this.child(has_branch_container(branch)) }) } }), @@ -1332,14 +1332,14 @@ fn merge_anchor_ranges<'a>( loop { if let Some(left_range) = left .peek() - .filter(|range| range.start.cmp(&next_range.end, &snapshot).is_le()) + .filter(|range| range.start.cmp(&next_range.end, snapshot).is_le()) .cloned() { left.next(); next_range.end = left_range.end; } else if let Some(right_range) = right .peek() - .filter(|range| range.start.cmp(&next_range.end, &snapshot).is_le()) + .filter(|range| range.start.cmp(&next_range.end, snapshot).is_le()) .cloned() { right.next(); diff --git a/crates/git_ui/src/text_diff_view.rs b/crates/git_ui/src/text_diff_view.rs index 005c1e18b4..d07868c3e1 100644 --- a/crates/git_ui/src/text_diff_view.rs +++ b/crates/git_ui/src/text_diff_view.rs @@ -686,7 +686,7 @@ mod tests { let project = Project::test(fs, [project_root.as_ref()], cx).await; - let (workspace, mut cx) = + let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); let buffer = project @@ -725,7 +725,7 @@ mod tests { assert_state_with_diff( &diff_view.read_with(cx, |diff_view, _| diff_view.diff_editor.clone()), - &mut cx, + cx, expected_diff, ); diff --git a/crates/gpui/build.rs b/crates/gpui/build.rs index 93a1c15c41..3a80ee12a0 100644 --- a/crates/gpui/build.rs +++ b/crates/gpui/build.rs @@ -374,7 +374,7 @@ mod windows { shader_path, "vs_4_1", ); - generate_rust_binding(&const_name, &output_file, &rust_binding_path); + generate_rust_binding(&const_name, &output_file, rust_binding_path); // Compile fragment shader let output_file = format!("{}/{}_ps.h", out_dir, module); @@ -387,7 +387,7 @@ mod windows { shader_path, "ps_4_1", ); - generate_rust_binding(&const_name, &output_file, &rust_binding_path); + generate_rust_binding(&const_name, &output_file, rust_binding_path); } fn compile_shader_impl( diff --git a/crates/gpui/examples/input.rs b/crates/gpui/examples/input.rs index b0f560e38d..170df3cad7 100644 --- a/crates/gpui/examples/input.rs +++ b/crates/gpui/examples/input.rs @@ -137,14 +137,14 @@ impl TextInput { fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) { if !self.selected_range.is_empty() { cx.write_to_clipboard(ClipboardItem::new_string( - (&self.content[self.selected_range.clone()]).to_string(), + self.content[self.selected_range.clone()].to_string(), )); } } fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) { if !self.selected_range.is_empty() { cx.write_to_clipboard(ClipboardItem::new_string( - (&self.content[self.selected_range.clone()]).to_string(), + self.content[self.selected_range.clone()].to_string(), )); self.replace_text_in_range(None, "", window, cx) } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index e1df6d0be4..ed1b935c58 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1310,7 +1310,7 @@ impl App { T: 'static, { let window_handle = window.handle; - self.observe_release(&handle, move |entity, cx| { + self.observe_release(handle, move |entity, cx| { let _ = window_handle.update(cx, |_, window, cx| on_release(entity, window, cx)); }) } @@ -1917,7 +1917,7 @@ impl AppContext for App { G: Global, { let mut g = self.global::<G>(); - callback(&g, self) + callback(g, self) } } diff --git a/crates/gpui/src/app/entity_map.rs b/crates/gpui/src/app/entity_map.rs index fccb417caa..48b2bcaf98 100644 --- a/crates/gpui/src/app/entity_map.rs +++ b/crates/gpui/src/app/entity_map.rs @@ -661,7 +661,7 @@ pub struct WeakEntity<T> { impl<T> std::fmt::Debug for WeakEntity<T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct(&type_name::<Self>()) + f.debug_struct(type_name::<Self>()) .field("entity_id", &self.any_entity.entity_id) .field("entity_type", &type_name::<T>()) .finish() diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 09afbff929..78114b7ecf 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -2785,7 +2785,7 @@ fn handle_tooltip_check_visible_and_update( match action { Action::None => {} - Action::Hide => clear_active_tooltip(&active_tooltip, window), + Action::Hide => clear_active_tooltip(active_tooltip, window), Action::ScheduleHide(tooltip) => { let delayed_hide_task = window.spawn(cx, { let active_tooltip = active_tooltip.clone(); diff --git a/crates/gpui/src/inspector.rs b/crates/gpui/src/inspector.rs index 23c46edcc1..9f86576a59 100644 --- a/crates/gpui/src/inspector.rs +++ b/crates/gpui/src/inspector.rs @@ -164,7 +164,7 @@ mod conditional { if let Some(render_inspector) = cx .inspector_element_registry .renderers_by_type_id - .remove(&type_id) + .remove(type_id) { let mut element = (render_inspector)( active_element.id.clone(), diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index c3f5d18603..f682b78c41 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -408,7 +408,7 @@ impl DispatchTree { keymap .bindings_for_action(action) .filter(|binding| { - Self::binding_matches_predicate_and_not_shadowed(&keymap, &binding, context_stack) + Self::binding_matches_predicate_and_not_shadowed(&keymap, binding, context_stack) }) .cloned() .collect() @@ -426,7 +426,7 @@ impl DispatchTree { .bindings_for_action(action) .rev() .find(|binding| { - Self::binding_matches_predicate_and_not_shadowed(&keymap, &binding, context_stack) + Self::binding_matches_predicate_and_not_shadowed(&keymap, binding, context_stack) }) .cloned() } diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index 83d7479a04..66f191ca5d 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -148,7 +148,7 @@ impl Keymap { let mut pending_bindings = SmallVec::<[(BindingIndex, &KeyBinding); 1]>::new(); for (ix, binding) in self.bindings().enumerate().rev() { - let Some(depth) = self.binding_enabled(binding, &context_stack) else { + let Some(depth) = self.binding_enabled(binding, context_stack) else { continue; }; let Some(pending) = binding.match_keystrokes(input) else { diff --git a/crates/gpui/src/path_builder.rs b/crates/gpui/src/path_builder.rs index 6c8cfddd52..38903ea588 100644 --- a/crates/gpui/src/path_builder.rs +++ b/crates/gpui/src/path_builder.rs @@ -278,7 +278,7 @@ impl PathBuilder { options: &StrokeOptions, ) -> Result<Path<Pixels>, Error> { let path = if let Some(dash_array) = dash_array { - let measurements = lyon::algorithms::measure::PathMeasurements::from_path(&path, 0.01); + let measurements = lyon::algorithms::measure::PathMeasurements::from_path(path, 0.01); let mut sampler = measurements .create_sampler(path, lyon::algorithms::measure::SampleType::Normalized); let mut builder = lyon::path::Path::builder(); diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index ffd68d60e6..3e002309e4 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1508,7 +1508,7 @@ impl ClipboardItem { for entry in self.entries.iter() { if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry { - answer.push_str(&text); + answer.push_str(text); any_entries = true; } } diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 86e5a79e8a..a1da088b75 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -642,7 +642,7 @@ pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::S let mut state: Option<xkb::compose::State> = None; for locale in locales { if let Ok(table) = - xkb::compose::Table::new_from_locale(&cx, &locale, xkb::compose::COMPILE_NO_FLAGS) + xkb::compose::Table::new_from_locale(cx, &locale, xkb::compose::COMPILE_NO_FLAGS) { state = Some(xkb::compose::State::new( &table, diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 72e4477ecf..0ab61fbf0c 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -1145,7 +1145,7 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr { .globals .text_input_manager .as_ref() - .map(|text_input_manager| text_input_manager.get_text_input(&seat, qh, ())); + .map(|text_input_manager| text_input_manager.get_text_input(seat, qh, ())); if let Some(wl_keyboard) = &state.wl_keyboard { wl_keyboard.release(); @@ -1294,7 +1294,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr { match key_state { wl_keyboard::KeyState::Pressed if !keysym.is_modifier_key() => { let mut keystroke = - Keystroke::from_xkb(&keymap_state, state.modifiers, keycode); + Keystroke::from_xkb(keymap_state, state.modifiers, keycode); if let Some(mut compose) = state.compose_state.take() { compose.feed(keysym); match compose.status() { @@ -1538,12 +1538,9 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr { cursor_shape_device.set_shape(serial, style.to_shape()); } else { let scale = window.primary_output_scale(); - state.cursor.set_icon( - &wl_pointer, - serial, - style.to_icon_names(), - scale, - ); + state + .cursor + .set_icon(wl_pointer, serial, style.to_icon_names(), scale); } } drop(state); @@ -1580,7 +1577,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr { if state .keyboard_focused_window .as_ref() - .map_or(false, |keyboard_window| window.ptr_eq(&keyboard_window)) + .map_or(false, |keyboard_window| window.ptr_eq(keyboard_window)) { state.enter_token = None; } diff --git a/crates/gpui/src/platform/linux/wayland/cursor.rs b/crates/gpui/src/platform/linux/wayland/cursor.rs index 2a24d0e1ba..bfbedf234d 100644 --- a/crates/gpui/src/platform/linux/wayland/cursor.rs +++ b/crates/gpui/src/platform/linux/wayland/cursor.rs @@ -144,7 +144,7 @@ impl Cursor { hot_y as i32 / scale, ); - self.surface.attach(Some(&buffer), 0, 0); + self.surface.attach(Some(buffer), 0, 0); self.surface.damage(0, 0, width as i32, height as i32); self.surface.commit(); } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 053cd0387b..dd0cea3290 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1212,7 +1212,7 @@ impl X11Client { state = self.0.borrow_mut(); if let Some(mut pointer) = state.pointer_device_states.get_mut(&event.sourceid) { - let scroll_delta = get_scroll_delta_and_update_state(&mut pointer, &event); + let scroll_delta = get_scroll_delta_and_update_state(pointer, &event); drop(state); if let Some(scroll_delta) = scroll_delta { window.handle_input(PlatformInput::ScrollWheel(make_scroll_wheel_event( @@ -1271,7 +1271,7 @@ impl X11Client { Event::XinputDeviceChanged(event) => { let mut state = self.0.borrow_mut(); if let Some(mut pointer) = state.pointer_device_states.get_mut(&event.sourceid) { - reset_pointer_device_scroll_positions(&mut pointer); + reset_pointer_device_scroll_positions(pointer); } } _ => {} @@ -2038,7 +2038,7 @@ fn xdnd_get_supported_atom( { if let Some(atoms) = reply.value32() { for atom in atoms { - if xdnd_is_atom_supported(atom, &supported_atoms) { + if xdnd_is_atom_supported(atom, supported_atoms) { return atom; } } diff --git a/crates/gpui/src/platform/linux/x11/event.rs b/crates/gpui/src/platform/linux/x11/event.rs index cd4cef24a3..a566762c54 100644 --- a/crates/gpui/src/platform/linux/x11/event.rs +++ b/crates/gpui/src/platform/linux/x11/event.rs @@ -73,8 +73,8 @@ pub(crate) fn get_valuator_axis_index( // valuator present in this event's axisvalues. Axisvalues is ordered from // lowest valuator number to highest, so counting bits before the 1 bit for // this valuator yields the index in axisvalues. - if bit_is_set_in_vec(&valuator_mask, valuator_number) { - Some(popcount_upto_bit_index(&valuator_mask, valuator_number) as usize) + if bit_is_set_in_vec(valuator_mask, valuator_number) { + Some(popcount_upto_bit_index(valuator_mask, valuator_number) as usize) } else { None } diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 1a3c323c35..2bf58d6184 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -397,7 +397,7 @@ impl X11WindowState { .display_id .map_or(x_main_screen_index, |did| did.0 as usize); - let visual_set = find_visuals(&xcb, x_screen_index); + let visual_set = find_visuals(xcb, x_screen_index); let visual = match visual_set.transparent { Some(visual) => visual, @@ -604,7 +604,7 @@ impl X11WindowState { ), )?; - xcb_flush(&xcb); + xcb_flush(xcb); let renderer = { let raw_window = RawWindow { @@ -664,7 +664,7 @@ impl X11WindowState { || "X11 DestroyWindow failed while cleaning it up after setup failure.", xcb.destroy_window(x_window), )?; - xcb_flush(&xcb); + xcb_flush(xcb); } setup_result diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index a686d8c45b..49a5edceb2 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -445,14 +445,14 @@ impl MetalRenderer { instance_buffer, &mut instance_offset, viewport_size, - &command_encoder, + command_encoder, ), PrimitiveBatch::Quads(quads) => self.draw_quads( quads, instance_buffer, &mut instance_offset, viewport_size, - &command_encoder, + command_encoder, ), PrimitiveBatch::Paths(paths) => { command_encoder.end_encoding(); @@ -480,7 +480,7 @@ impl MetalRenderer { instance_buffer, &mut instance_offset, viewport_size, - &command_encoder, + command_encoder, ) } else { false @@ -491,7 +491,7 @@ impl MetalRenderer { instance_buffer, &mut instance_offset, viewport_size, - &command_encoder, + command_encoder, ), PrimitiveBatch::MonochromeSprites { texture_id, @@ -502,7 +502,7 @@ impl MetalRenderer { instance_buffer, &mut instance_offset, viewport_size, - &command_encoder, + command_encoder, ), PrimitiveBatch::PolychromeSprites { texture_id, @@ -513,14 +513,14 @@ impl MetalRenderer { instance_buffer, &mut instance_offset, viewport_size, - &command_encoder, + command_encoder, ), PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces( surfaces, instance_buffer, &mut instance_offset, viewport_size, - &command_encoder, + command_encoder, ), }; if !ok { @@ -763,7 +763,7 @@ impl MetalRenderer { viewport_size: Size<DevicePixels>, command_encoder: &metal::RenderCommandEncoderRef, ) -> bool { - let Some(ref first_path) = paths.first() else { + let Some(first_path) = paths.first() else { return true; }; diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 79177fb2c9..f094ed9f30 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -371,7 +371,7 @@ impl MacPlatform { item = NSMenuItem::alloc(nil) .initWithTitle_action_keyEquivalent_( - ns_string(&name), + ns_string(name), selector, ns_string(key_to_native(&keystroke.key).as_ref()), ) @@ -383,7 +383,7 @@ impl MacPlatform { } else { item = NSMenuItem::alloc(nil) .initWithTitle_action_keyEquivalent_( - ns_string(&name), + ns_string(name), selector, ns_string(""), ) @@ -392,7 +392,7 @@ impl MacPlatform { } else { item = NSMenuItem::alloc(nil) .initWithTitle_action_keyEquivalent_( - ns_string(&name), + ns_string(name), selector, ns_string(""), ) @@ -412,7 +412,7 @@ impl MacPlatform { submenu.addItem_(Self::create_menu_item(item, delegate, actions, keymap)); } item.setSubmenu_(submenu); - item.setTitle_(ns_string(&name)); + item.setTitle_(ns_string(name)); item } MenuItem::SystemMenu(OsMenu { name, menu_type }) => { @@ -420,7 +420,7 @@ impl MacPlatform { let submenu = NSMenu::new(nil).autorelease(); submenu.setDelegate_(delegate); item.setSubmenu_(submenu); - item.setTitle_(ns_string(&name)); + item.setTitle_(ns_string(name)); match menu_type { SystemMenuType::Services => { diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index aedf131909..40a03b6c4a 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1480,9 +1480,9 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: if key_down_event.is_held { if let Some(key_char) = key_down_event.keystroke.key_char.as_ref() { - let handled = with_input_handler(&this, |input_handler| { + let handled = with_input_handler(this, |input_handler| { if !input_handler.apple_press_and_hold_enabled() { - input_handler.replace_text_in_range(None, &key_char); + input_handler.replace_text_in_range(None, key_char); return YES; } NO @@ -1949,7 +1949,7 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS let text = text.to_str(); let replacement_range = replacement_range.to_range(); with_input_handler(this, |input_handler| { - input_handler.replace_text_in_range(replacement_range, &text) + input_handler.replace_text_in_range(replacement_range, text) }); } } @@ -1973,7 +1973,7 @@ extern "C" fn set_marked_text( let replacement_range = replacement_range.to_range(); let text = text.to_str(); with_input_handler(this, |input_handler| { - input_handler.replace_and_mark_text_in_range(replacement_range, &text, selected_range) + input_handler.replace_and_mark_text_in_range(replacement_range, text, selected_range) }); } } diff --git a/crates/gpui/src/platform/windows/direct_write.rs b/crates/gpui/src/platform/windows/direct_write.rs index 75cb50243b..a86a1fab62 100644 --- a/crates/gpui/src/platform/windows/direct_write.rs +++ b/crates/gpui/src/platform/windows/direct_write.rs @@ -850,7 +850,7 @@ impl DirectWriteState { } let bitmap_data = if params.is_emoji { - if let Ok(color) = self.rasterize_color(¶ms, glyph_bounds) { + if let Ok(color) = self.rasterize_color(params, glyph_bounds) { color } else { let monochrome = self.rasterize_monochrome(params, glyph_bounds)?; @@ -1784,7 +1784,7 @@ fn apply_font_features( } unsafe { - direct_write_features.AddFontFeature(make_direct_write_feature(&tag, *value))?; + direct_write_features.AddFontFeature(make_direct_write_feature(tag, *value))?; } } unsafe { diff --git a/crates/gpui/src/platform/windows/directx_renderer.rs b/crates/gpui/src/platform/windows/directx_renderer.rs index 4e72ded534..f84a1c1b6d 100644 --- a/crates/gpui/src/platform/windows/directx_renderer.rs +++ b/crates/gpui/src/platform/windows/directx_renderer.rs @@ -758,7 +758,7 @@ impl DirectXRenderPipelines { impl DirectComposition { pub fn new(dxgi_device: &IDXGIDevice, hwnd: HWND) -> Result<Self> { - let comp_device = get_comp_device(&dxgi_device)?; + let comp_device = get_comp_device(dxgi_device)?; let comp_target = unsafe { comp_device.CreateTargetForHwnd(hwnd, true) }?; let comp_visual = unsafe { comp_device.CreateVisual() }?; @@ -1144,7 +1144,7 @@ fn create_resources( [D3D11_VIEWPORT; 1], )> { let (render_target, render_target_view) = - create_render_target_and_its_view(&swap_chain, &devices.device)?; + create_render_target_and_its_view(swap_chain, &devices.device)?; let (path_intermediate_texture, path_intermediate_srv) = create_path_intermediate_texture(&devices.device, width, height)?; let (path_intermediate_msaa_texture, path_intermediate_msaa_view) = diff --git a/crates/gpui/src/tab_stop.rs b/crates/gpui/src/tab_stop.rs index 7dde42efed..30d24e85e7 100644 --- a/crates/gpui/src/tab_stop.rs +++ b/crates/gpui/src/tab_stop.rs @@ -90,7 +90,7 @@ mod tests { ]; for handle in focus_handles.iter() { - tab.insert(&handle); + tab.insert(handle); } assert_eq!( tab.handles diff --git a/crates/gpui_macros/src/test.rs b/crates/gpui_macros/src/test.rs index adb27f42ea..5a8b1cf7fc 100644 --- a/crates/gpui_macros/src/test.rs +++ b/crates/gpui_macros/src/test.rs @@ -73,7 +73,7 @@ impl Parse for Args { (Meta::NameValue(meta), "seed") => { seeds = vec![parse_usize_from_expr(&meta.value)? as u64] } - (Meta::List(list), "seeds") => seeds = parse_u64_array(&list)?, + (Meta::List(list), "seeds") => seeds = parse_u64_array(list)?, (Meta::Path(_), _) => { return Err(syn::Error::new(meta.span(), "invalid path argument")); } diff --git a/crates/install_cli/src/install_cli.rs b/crates/install_cli/src/install_cli.rs index 12c094448b..dc9e0e31ab 100644 --- a/crates/install_cli/src/install_cli.rs +++ b/crates/install_cli/src/install_cli.rs @@ -105,7 +105,7 @@ pub fn install_cli(window: &mut Window, cx: &mut Context<Workspace>) { cx, ) })?; - register_zed_scheme(&cx).await.log_err(); + register_zed_scheme(cx).await.log_err(); Ok(()) }) .detach_and_prompt_err("Error installing zed cli", window, cx, |_, _, _| None); diff --git a/crates/jj/src/jj_store.rs b/crates/jj/src/jj_store.rs index a10f06fad4..2d2d958d7f 100644 --- a/crates/jj/src/jj_store.rs +++ b/crates/jj/src/jj_store.rs @@ -16,7 +16,7 @@ pub struct JujutsuStore { impl JujutsuStore { pub fn init_global(cx: &mut App) { - let Some(repository) = RealJujutsuRepository::new(&Path::new(".")).ok() else { + let Some(repository) = RealJujutsuRepository::new(Path::new(".")).ok() else { return; }; diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index e2bcc938fa..abb8d3b151 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -716,7 +716,7 @@ impl EditPreview { &self.applied_edits_snapshot, &self.syntax_snapshot, None, - &syntax_theme, + syntax_theme, ); } @@ -727,7 +727,7 @@ impl EditPreview { ¤t_snapshot.text, ¤t_snapshot.syntax, Some(deletion_highlight_style), - &syntax_theme, + syntax_theme, ); } @@ -737,7 +737,7 @@ impl EditPreview { &self.applied_edits_snapshot, &self.syntax_snapshot, Some(insertion_highlight_style), - &syntax_theme, + syntax_theme, ); } @@ -749,7 +749,7 @@ impl EditPreview { &self.applied_edits_snapshot, &self.syntax_snapshot, None, - &syntax_theme, + syntax_theme, ); highlighted_text.build() diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index c377d7440a..3a41733191 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -1830,7 +1830,7 @@ impl Language { impl LanguageScope { pub fn path_suffixes(&self) -> &[String] { - &self.language.path_suffixes() + self.language.path_suffixes() } pub fn language_name(&self) -> LanguageName { diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 6a89b90462..83c16f4558 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -1102,7 +1102,7 @@ impl LanguageRegistry { use gpui::AppContext as _; let mut state = self.state.write(); - let fake_entry = state.fake_server_entries.get_mut(&name)?; + let fake_entry = state.fake_server_entries.get_mut(name)?; let (server, mut fake_server) = lsp::FakeLanguageServer::new( server_id, binary, diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 29669ba2a0..62fe75b6a8 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -187,8 +187,8 @@ impl LanguageSettings { let rest = available_language_servers .iter() .filter(|&available_language_server| { - !disabled_language_servers.contains(&available_language_server) - && !enabled_language_servers.contains(&available_language_server) + !disabled_language_servers.contains(available_language_server) + && !enabled_language_servers.contains(available_language_server) }) .cloned() .collect::<Vec<_>>(); diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index c56ffed066..30bbc88f7e 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -1297,7 +1297,7 @@ fn parse_text( ) -> anyhow::Result<Tree> { with_parser(|parser| { let mut chunks = text.chunks_in_range(start_byte..text.len()); - parser.set_included_ranges(&ranges)?; + parser.set_included_ranges(ranges)?; parser.set_language(&grammar.ts_language)?; parser .parse_with_options( diff --git a/crates/language/src/text_diff.rs b/crates/language/src/text_diff.rs index f9221f571a..af8ce60881 100644 --- a/crates/language/src/text_diff.rs +++ b/crates/language/src/text_diff.rs @@ -154,19 +154,19 @@ fn diff_internal( input, |old_tokens: Range<u32>, new_tokens: Range<u32>| { old_offset += token_len( - &input, + input, &input.before[old_token_ix as usize..old_tokens.start as usize], ); new_offset += token_len( - &input, + input, &input.after[new_token_ix as usize..new_tokens.start as usize], ); let old_len = token_len( - &input, + input, &input.before[old_tokens.start as usize..old_tokens.end as usize], ); let new_len = token_len( - &input, + input, &input.after[new_tokens.start as usize..new_tokens.end as usize], ); let old_byte_range = old_offset..old_offset + old_len; diff --git a/crates/language_extension/src/language_extension.rs b/crates/language_extension/src/language_extension.rs index 7bca0eb485..510f870ce8 100644 --- a/crates/language_extension/src/language_extension.rs +++ b/crates/language_extension/src/language_extension.rs @@ -61,6 +61,6 @@ impl ExtensionLanguageProxy for LanguageServerRegistryProxy { grammars_to_remove: &[Arc<str>], ) { self.language_registry - .remove_languages(&languages_to_remove, &grammars_to_remove); + .remove_languages(languages_to_remove, grammars_to_remove); } } diff --git a/crates/language_model/src/request.rs b/crates/language_model/src/request.rs index edce3d03b7..8c2d169973 100644 --- a/crates/language_model/src/request.rs +++ b/crates/language_model/src/request.rs @@ -220,7 +220,7 @@ impl<'de> Deserialize<'de> for LanguageModelToolResultContent { // Accept wrapped text format: { "type": "text", "text": "..." } if let (Some(type_value), Some(text_value)) = - (get_field(&obj, "type"), get_field(&obj, "text")) + (get_field(obj, "type"), get_field(obj, "text")) { if let Some(type_str) = type_value.as_str() { if type_str.to_lowercase() == "text" { @@ -255,7 +255,7 @@ impl<'de> Deserialize<'de> for LanguageModelToolResultContent { } // Try as direct Image (object with "source" and "size" fields) - if let Some(image) = LanguageModelImage::from_json(&obj) { + if let Some(image) = LanguageModelImage::from_json(obj) { return Ok(Self::Image(image)); } } @@ -272,7 +272,7 @@ impl<'de> Deserialize<'de> for LanguageModelToolResultContent { impl LanguageModelToolResultContent { pub fn to_str(&self) -> Option<&str> { match self { - Self::Text(text) => Some(&text), + Self::Text(text) => Some(text), Self::Image(_) => None, } } diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index 810d4a5f44..7ba56ec775 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -114,7 +114,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .delete_credentials(&api_url, &cx) + .delete_credentials(&api_url, cx) .await .ok(); this.update(cx, |this, cx| { @@ -133,7 +133,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx) + .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx) .await .ok(); @@ -212,7 +212,7 @@ impl AnthropicLanguageModelProvider { } else { cx.spawn(async move |cx| { let (_, api_key) = credentials_provider - .read_credentials(&api_url, &cx) + .read_credentials(&api_url, cx) .await? .ok_or(AuthenticateError::CredentialsNotFound)?; diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index 4e6744d745..f33a00972d 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -150,7 +150,7 @@ impl State { let credentials_provider = <dyn CredentialsProvider>::global(cx); cx.spawn(async move |this, cx| { credentials_provider - .delete_credentials(AMAZON_AWS_URL, &cx) + .delete_credentials(AMAZON_AWS_URL, cx) .await .log_err(); this.update(cx, |this, cx| { @@ -174,7 +174,7 @@ impl State { AMAZON_AWS_URL, "Bearer", &serde_json::to_vec(&credentials)?, - &cx, + cx, ) .await?; this.update(cx, |this, cx| { @@ -206,7 +206,7 @@ impl State { (credentials, true) } else { let (_, credentials) = credentials_provider - .read_credentials(AMAZON_AWS_URL, &cx) + .read_credentials(AMAZON_AWS_URL, cx) .await? .ok_or_else(|| AuthenticateError::CredentialsNotFound)?; ( @@ -465,7 +465,7 @@ impl BedrockModel { Result<BoxStream<'static, Result<BedrockStreamingResponse, BedrockError>>>, > { let Ok(runtime_client) = self - .get_or_init_client(&cx) + .get_or_init_client(cx) .cloned() .context("Bedrock client not initialized") else { diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index c3f4399832..f226d0c6a8 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -193,7 +193,7 @@ impl State { fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<()>> { let client = self.client.clone(); cx.spawn(async move |state, cx| { - client.sign_in_with_optional_connect(true, &cx).await?; + client.sign_in_with_optional_connect(true, cx).await?; state.update(cx, |_, cx| cx.notify()) }) } diff --git a/crates/language_models/src/provider/deepseek.rs b/crates/language_models/src/provider/deepseek.rs index 2b30d456ee..8c7f8bcc35 100644 --- a/crates/language_models/src/provider/deepseek.rs +++ b/crates/language_models/src/provider/deepseek.rs @@ -77,7 +77,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .delete_credentials(&api_url, &cx) + .delete_credentials(&api_url, cx) .await .log_err(); this.update(cx, |this, cx| { @@ -96,7 +96,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx) + .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx) .await?; this.update(cx, |this, cx| { this.api_key = Some(api_key); @@ -120,7 +120,7 @@ impl State { (api_key, true) } else { let (_, api_key) = credentials_provider - .read_credentials(&api_url, &cx) + .read_credentials(&api_url, cx) .await? .ok_or(AuthenticateError::CredentialsNotFound)?; ( diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index 32f8838df7..1bb9f3fa00 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -110,7 +110,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .delete_credentials(&api_url, &cx) + .delete_credentials(&api_url, cx) .await .log_err(); this.update(cx, |this, cx| { @@ -129,7 +129,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx) + .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx) .await?; this.update(cx, |this, cx| { this.api_key = Some(api_key); @@ -156,7 +156,7 @@ impl State { (api_key, true) } else { let (_, api_key) = credentials_provider - .read_credentials(&api_url, &cx) + .read_credentials(&api_url, cx) .await? .ok_or(AuthenticateError::CredentialsNotFound)?; ( diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index e1d55801eb..3f8c2e2a67 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/crates/language_models/src/provider/mistral.rs @@ -76,7 +76,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .delete_credentials(&api_url, &cx) + .delete_credentials(&api_url, cx) .await .log_err(); this.update(cx, |this, cx| { @@ -95,7 +95,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx) + .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx) .await?; this.update(cx, |this, cx| { this.api_key = Some(api_key); @@ -119,7 +119,7 @@ impl State { (api_key, true) } else { let (_, api_key) = credentials_provider - .read_credentials(&api_url, &cx) + .read_credentials(&api_url, cx) .await? .ok_or(AuthenticateError::CredentialsNotFound)?; ( diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 04d89f2db1..1a5c09cdc4 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -75,7 +75,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .delete_credentials(&api_url, &cx) + .delete_credentials(&api_url, cx) .await .log_err(); this.update(cx, |this, cx| { @@ -94,7 +94,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx) + .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx) .await .log_err(); this.update(cx, |this, cx| { @@ -119,7 +119,7 @@ impl State { (api_key, true) } else { let (_, api_key) = credentials_provider - .read_credentials(&api_url, &cx) + .read_credentials(&api_url, cx) .await? .ok_or(AuthenticateError::CredentialsNotFound)?; ( diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs index c6b980c3ec..55df534cc9 100644 --- a/crates/language_models/src/provider/open_ai_compatible.rs +++ b/crates/language_models/src/provider/open_ai_compatible.rs @@ -87,7 +87,7 @@ impl State { let api_url = self.settings.api_url.clone(); cx.spawn(async move |this, cx| { credentials_provider - .delete_credentials(&api_url, &cx) + .delete_credentials(&api_url, cx) .await .log_err(); this.update(cx, |this, cx| { @@ -103,7 +103,7 @@ impl State { let api_url = self.settings.api_url.clone(); cx.spawn(async move |this, cx| { credentials_provider - .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx) + .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx) .await .log_err(); this.update(cx, |this, cx| { @@ -126,7 +126,7 @@ impl State { (api_key, true) } else { let (_, api_key) = credentials_provider - .read_credentials(&api_url, &cx) + .read_credentials(&api_url, cx) .await? .ok_or(AuthenticateError::CredentialsNotFound)?; ( diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index 5d8bace6d3..8f2abfce35 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -112,7 +112,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .delete_credentials(&api_url, &cx) + .delete_credentials(&api_url, cx) .await .log_err(); this.update(cx, |this, cx| { @@ -131,7 +131,7 @@ impl State { .clone(); cx.spawn(async move |this, cx| { credentials_provider - .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx) + .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx) .await .log_err(); this.update(cx, |this, cx| { @@ -157,7 +157,7 @@ impl State { (api_key, true) } else { let (_, api_key) = credentials_provider - .read_credentials(&api_url, &cx) + .read_credentials(&api_url, cx) .await? .ok_or(AuthenticateError::CredentialsNotFound)?; ( diff --git a/crates/language_models/src/provider/vercel.rs b/crates/language_models/src/provider/vercel.rs index 98e4f60b6b..84f3175d1e 100644 --- a/crates/language_models/src/provider/vercel.rs +++ b/crates/language_models/src/provider/vercel.rs @@ -71,7 +71,7 @@ impl State { }; cx.spawn(async move |this, cx| { credentials_provider - .delete_credentials(&api_url, &cx) + .delete_credentials(&api_url, cx) .await .log_err(); this.update(cx, |this, cx| { @@ -92,7 +92,7 @@ impl State { }; cx.spawn(async move |this, cx| { credentials_provider - .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx) + .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx) .await .log_err(); this.update(cx, |this, cx| { @@ -119,7 +119,7 @@ impl State { (api_key, true) } else { let (_, api_key) = credentials_provider - .read_credentials(&api_url, &cx) + .read_credentials(&api_url, cx) .await? .ok_or(AuthenticateError::CredentialsNotFound)?; ( diff --git a/crates/language_models/src/provider/x_ai.rs b/crates/language_models/src/provider/x_ai.rs index 2b8238cc5c..b37a55e19f 100644 --- a/crates/language_models/src/provider/x_ai.rs +++ b/crates/language_models/src/provider/x_ai.rs @@ -71,7 +71,7 @@ impl State { }; cx.spawn(async move |this, cx| { credentials_provider - .delete_credentials(&api_url, &cx) + .delete_credentials(&api_url, cx) .await .log_err(); this.update(cx, |this, cx| { @@ -92,7 +92,7 @@ impl State { }; cx.spawn(async move |this, cx| { credentials_provider - .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx) + .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx) .await .log_err(); this.update(cx, |this, cx| { @@ -119,7 +119,7 @@ impl State { (api_key, true) } else { let (_, api_key) = credentials_provider - .read_credentials(&api_url, &cx) + .read_credentials(&api_url, cx) .await? .ok_or(AuthenticateError::CredentialsNotFound)?; ( diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 823d59ce12..c303a8c305 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -661,7 +661,7 @@ impl LogStore { IoKind::StdOut => true, IoKind::StdIn => false, IoKind::StdErr => { - self.add_language_server_log(language_server_id, MessageType::LOG, &message, cx); + self.add_language_server_log(language_server_id, MessageType::LOG, message, cx); return Some(()); } }; diff --git a/crates/languages/src/css.rs b/crates/languages/src/css.rs index a1a5418220..2480d40268 100644 --- a/crates/languages/src/css.rs +++ b/crates/languages/src/css.rs @@ -106,7 +106,7 @@ impl LspAdapter for CssLspAdapter { .should_install_npm_package( Self::PACKAGE_NAME, &server_path, - &container_dir, + container_dir, VersionStrategy::Latest(version), ) .await; diff --git a/crates/languages/src/github_download.rs b/crates/languages/src/github_download.rs index 5b0f1d0729..766c894fbb 100644 --- a/crates/languages/src/github_download.rs +++ b/crates/languages/src/github_download.rs @@ -96,7 +96,7 @@ async fn stream_response_archive( AssetKind::TarGz => extract_tar_gz(destination_path, url, response).await?, AssetKind::Gz => extract_gz(destination_path, url, response).await?, AssetKind::Zip => { - util::archive::extract_zip(&destination_path, response).await?; + util::archive::extract_zip(destination_path, response).await?; } }; Ok(()) @@ -113,11 +113,11 @@ async fn stream_file_archive( AssetKind::Gz => extract_gz(destination_path, url, file_archive).await?, #[cfg(not(windows))] AssetKind::Zip => { - util::archive::extract_seekable_zip(&destination_path, file_archive).await?; + util::archive::extract_seekable_zip(destination_path, file_archive).await?; } #[cfg(windows)] AssetKind::Zip => { - util::archive::extract_zip(&destination_path, file_archive).await?; + util::archive::extract_zip(destination_path, file_archive).await?; } }; Ok(()) diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 4db48c67f0..6f57ace488 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -343,7 +343,7 @@ impl LspAdapter for JsonLspAdapter { .should_install_npm_package( Self::PACKAGE_NAME, &server_path, - &container_dir, + container_dir, VersionStrategy::Latest(version), ) .await; diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 222e3f1946..17d0d98fad 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -204,7 +204,7 @@ impl LspAdapter for PythonLspAdapter { .should_install_npm_package( Self::SERVER_NAME.as_ref(), &server_path, - &container_dir, + container_dir, VersionStrategy::Latest(version), ) .await; diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 3ef7c1ba34..bbdfcdb499 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -581,7 +581,7 @@ impl ContextProvider for RustContextProvider { if let (Some(path), Some(stem)) = (&local_abs_path, task_variables.get(&VariableName::Stem)) { - let fragment = test_fragment(&variables, &path, stem); + let fragment = test_fragment(&variables, path, stem); variables.insert(RUST_TEST_FRAGMENT_TASK_VARIABLE, fragment); }; if let Some(test_name) = @@ -607,7 +607,7 @@ impl ContextProvider for RustContextProvider { } if let Some(path) = local_abs_path.as_ref() && let Some((target, manifest_path)) = - target_info_from_abs_path(&path, project_env.as_ref()).await + target_info_from_abs_path(path, project_env.as_ref()).await { if let Some(target) = target { variables.extend(TaskVariables::from_iter([ @@ -1570,7 +1570,7 @@ mod tests { let found = test_fragment( &TaskVariables::from_iter(variables.into_iter().map(|(k, v)| (k, v.to_owned()))), path, - &path.file_stem().unwrap().to_str().unwrap(), + path.file_stem().unwrap().to_str().unwrap(), ); assert_eq!(expected, found); } diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index 27939c645c..29a96d9515 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -111,7 +111,7 @@ impl LspAdapter for TailwindLspAdapter { .should_install_npm_package( Self::PACKAGE_NAME, &server_path, - &container_dir, + container_dir, VersionStrategy::Latest(version), ) .await; diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index dec7df4060..d477acc7f6 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -587,7 +587,7 @@ impl LspAdapter for TypeScriptLspAdapter { .should_install_npm_package( Self::PACKAGE_NAME, &server_path, - &container_dir, + container_dir, VersionStrategy::Latest(version.typescript_version.as_str()), ) .await; diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 137a9c2282..6ac92e0b2b 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -105,7 +105,7 @@ impl LspAdapter for YamlLspAdapter { .should_install_npm_package( Self::PACKAGE_NAME, &server_path, - &container_dir, + container_dir, VersionStrategy::Latest(version), ) .await; diff --git a/crates/livekit_client/src/test.rs b/crates/livekit_client/src/test.rs index e02c4d876f..e0058d1163 100644 --- a/crates/livekit_client/src/test.rs +++ b/crates/livekit_client/src/test.rs @@ -421,7 +421,7 @@ impl TestServer { track_sid: &TrackSid, muted: bool, ) -> Result<()> { - let claims = livekit_api::token::validate(&token, &self.secret_key)?; + let claims = livekit_api::token::validate(token, &self.secret_key)?; let room_name = claims.video.room.unwrap(); let identity = ParticipantIdentity(claims.sub.unwrap().to_string()); let mut server_rooms = self.rooms.lock(); @@ -475,7 +475,7 @@ impl TestServer { } pub(crate) fn is_track_muted(&self, token: &str, track_sid: &TrackSid) -> Option<bool> { - let claims = livekit_api::token::validate(&token, &self.secret_key).ok()?; + let claims = livekit_api::token::validate(token, &self.secret_key).ok()?; let room_name = claims.video.room.unwrap(); let mut server_rooms = self.rooms.lock(); diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index a3235a9773..e5709bc07c 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -875,7 +875,7 @@ impl Element for MarkdownElement { (CodeBlockRenderer::Custom { render, .. }, _) => { let parent_container = render( kind, - &parsed_markdown, + parsed_markdown, range.clone(), metadata.clone(), window, diff --git a/crates/markdown/src/parser.rs b/crates/markdown/src/parser.rs index 1035335ccb..3720e5b1ef 100644 --- a/crates/markdown/src/parser.rs +++ b/crates/markdown/src/parser.rs @@ -247,7 +247,7 @@ pub fn parse_markdown( events.push(event_for( text, range.source_range.start..range.source_range.start + prefix_len, - &head, + head, )); range.parsed = CowStr::Boxed(tail.into()); range.merged_range.start += prefix_len; diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 37d2ca2110..3acc4b5600 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -459,13 +459,13 @@ fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) - let mut max_lengths: Vec<usize> = vec![0; parsed.header.children.len()]; for (index, cell) in parsed.header.children.iter().enumerate() { - let length = paragraph_len(&cell); + let length = paragraph_len(cell); max_lengths[index] = length; } for row in &parsed.body { for (index, cell) in row.children.iter().enumerate() { - let length = paragraph_len(&cell); + let length = paragraph_len(cell); if length > max_lengths[index] { max_lengths[index] = length; diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index b425f7f1d5..88e3e12f02 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -37,7 +37,7 @@ fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Opt let mut edits = vec![]; while let Some(mat) = matches.next() { if let Some((_, callback)) = patterns.get(mat.pattern_index) { - edits.extend(callback(&text, &mat, query)); + edits.extend(callback(text, mat, query)); } } @@ -170,7 +170,7 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> { pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> { migrate( - &text, + text, &[( SETTINGS_NESTED_KEY_VALUE_PATTERN, migrations::m_2025_01_29::replace_edit_prediction_provider_setting, @@ -293,12 +293,12 @@ mod tests { use super::*; fn assert_migrate_keymap(input: &str, output: Option<&str>) { - let migrated = migrate_keymap(&input).unwrap(); + let migrated = migrate_keymap(input).unwrap(); pretty_assertions::assert_eq!(migrated.as_deref(), output); } fn assert_migrate_settings(input: &str, output: Option<&str>) { - let migrated = migrate_settings(&input).unwrap(); + let migrated = migrate_settings(input).unwrap(); pretty_assertions::assert_eq!(migrated.as_deref(), output); } diff --git a/crates/multi_buffer/src/anchor.rs b/crates/multi_buffer/src/anchor.rs index 1305328d38..8584519d56 100644 --- a/crates/multi_buffer/src/anchor.rs +++ b/crates/multi_buffer/src/anchor.rs @@ -145,7 +145,7 @@ impl Anchor { .map(|diff| diff.base_text()) { if a.buffer_id == Some(base_text.remote_id()) { - return a.bias_right(&base_text); + return a.bias_right(base_text); } } a @@ -212,7 +212,7 @@ impl AnchorRangeExt for Range<Anchor> { } fn includes(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool { - self.start.cmp(&other.start, &buffer).is_le() && other.end.cmp(&self.end, &buffer).is_le() + self.start.cmp(&other.start, buffer).is_le() && other.end.cmp(&self.end, buffer).is_le() } fn overlaps(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool { diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index eb12e6929c..59eaa9934d 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1686,7 +1686,7 @@ impl MultiBuffer { cx: &mut Context<Self>, ) -> (Vec<Range<Anchor>>, bool) { let (excerpt_ids, added_a_new_excerpt) = - self.update_path_excerpts(path, buffer, &buffer_snapshot, new, cx); + self.update_path_excerpts(path, buffer, buffer_snapshot, new, cx); let mut result = Vec::new(); let mut ranges = ranges.into_iter(); @@ -1784,7 +1784,7 @@ impl MultiBuffer { } Some(( *existing_id, - excerpt.range.context.to_point(&buffer_snapshot), + excerpt.range.context.to_point(buffer_snapshot), )) } else { None @@ -3056,7 +3056,7 @@ impl MultiBuffer { snapshot.has_conflict = has_conflict; for (id, diff) in self.diffs.iter() { - if snapshot.diffs.get(&id).is_none() { + if snapshot.diffs.get(id).is_none() { snapshot.diffs.insert(*id, diff.diff.read(cx).snapshot(cx)); } } @@ -3177,7 +3177,7 @@ impl MultiBuffer { &mut new_diff_transforms, &mut end_of_current_insert, &mut old_expanded_hunks, - &snapshot, + snapshot, change_kind, ); @@ -3223,7 +3223,7 @@ impl MultiBuffer { old_expanded_hunks.clear(); self.push_buffer_content_transform( - &snapshot, + snapshot, &mut new_diff_transforms, excerpt_offset, end_of_current_insert, @@ -3916,8 +3916,8 @@ impl MultiBufferSnapshot { &self, range: Range<T>, ) -> Vec<(&BufferSnapshot, Range<usize>, ExcerptId)> { - let start = range.start.to_offset(&self); - let end = range.end.to_offset(&self); + let start = range.start.to_offset(self); + let end = range.end.to_offset(self); let mut cursor = self.cursor::<usize>(); cursor.seek(&start); @@ -3955,8 +3955,8 @@ impl MultiBufferSnapshot { &self, range: Range<T>, ) -> impl Iterator<Item = (&BufferSnapshot, Range<usize>, ExcerptId, Option<Anchor>)> + '_ { - let start = range.start.to_offset(&self); - let end = range.end.to_offset(&self); + let start = range.start.to_offset(self); + let end = range.end.to_offset(self); let mut cursor = self.cursor::<usize>(); cursor.seek(&start); @@ -4186,7 +4186,7 @@ impl MultiBufferSnapshot { } let start = Anchor::in_buffer(excerpt.id, excerpt.buffer_id, hunk.buffer_range.start) - .to_point(&self); + .to_point(self); return Some(MultiBufferRow(start.row)); } } @@ -4204,7 +4204,7 @@ impl MultiBufferSnapshot { continue; }; let start = Anchor::in_buffer(excerpt.id, excerpt.buffer_id, hunk.buffer_range.start) - .to_point(&self); + .to_point(self); return Some(MultiBufferRow(start.row)); } } @@ -4455,7 +4455,7 @@ impl MultiBufferSnapshot { let mut buffer_position = region.buffer_range.start; buffer_position.add_assign(&overshoot); let clipped_buffer_position = - clip_buffer_position(®ion.buffer, buffer_position, bias); + clip_buffer_position(region.buffer, buffer_position, bias); let mut position = region.range.start; position.add_assign(&(clipped_buffer_position - region.buffer_range.start)); position @@ -4485,7 +4485,7 @@ impl MultiBufferSnapshot { let buffer_start_value = region.buffer_range.start.value.unwrap(); let mut buffer_key = buffer_start_key; buffer_key.add_assign(&(key - start_key)); - let buffer_value = convert_buffer_dimension(®ion.buffer, buffer_key); + let buffer_value = convert_buffer_dimension(region.buffer, buffer_key); let mut result = start_value; result.add_assign(&(buffer_value - buffer_start_value)); result @@ -4633,7 +4633,7 @@ impl MultiBufferSnapshot { .as_str() == **delimiter { - indent.push_str(&delimiter); + indent.push_str(delimiter); break; } } @@ -4897,8 +4897,8 @@ impl MultiBufferSnapshot { if let Some(base_text) = self.diffs.get(buffer_id).map(|diff| diff.base_text()) { - if base_text.can_resolve(&diff_base_anchor) { - let base_text_offset = diff_base_anchor.to_offset(&base_text); + if base_text.can_resolve(diff_base_anchor) { + let base_text_offset = diff_base_anchor.to_offset(base_text); if base_text_offset >= base_text_byte_range.start && base_text_offset <= base_text_byte_range.end { @@ -6418,7 +6418,7 @@ impl MultiBufferSnapshot { for (ix, entry) in excerpt_ids.iter().enumerate() { if ix == 0 { - if entry.id.cmp(&ExcerptId::min(), &self).is_le() { + if entry.id.cmp(&ExcerptId::min(), self).is_le() { panic!("invalid first excerpt id {:?}", entry.id); } } else if entry.id <= excerpt_ids[ix - 1].id { @@ -6648,7 +6648,7 @@ where hunk_info, .. } => { - let diff = self.diffs.get(&buffer_id)?; + let diff = self.diffs.get(buffer_id)?; let buffer = diff.base_text(); let mut rope_cursor = buffer.as_rope().cursor(0); let buffer_start = rope_cursor.summary::<D>(base_text_byte_range.start); @@ -7767,7 +7767,7 @@ impl<'a> Iterator for MultiBufferChunks<'a> { } chunks } else { - let base_buffer = &self.diffs.get(&buffer_id)?.base_text(); + let base_buffer = &self.diffs.get(buffer_id)?.base_text(); base_buffer.chunks(base_text_start..base_text_end, self.language_aware) }; diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index 824efa559f..fefeddb4da 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -473,7 +473,7 @@ fn test_editing_text_in_diff_hunks(cx: &mut TestAppContext) { let base_text = "one\ntwo\nfour\nfive\nsix\nseven\n"; let text = "one\ntwo\nTHREE\nfour\nfive\nseven\n"; let buffer = cx.new(|cx| Buffer::local(text, cx)); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer, cx)); + let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { @@ -2265,14 +2265,14 @@ impl ReferenceMultibuffer { } if !excerpt.expanded_diff_hunks.iter().any(|expanded_anchor| { - expanded_anchor.to_offset(&buffer).max(buffer_range.start) + expanded_anchor.to_offset(buffer).max(buffer_range.start) == hunk_range.start.max(buffer_range.start) }) { log::trace!("skipping a hunk that's not marked as expanded"); continue; } - if !hunk.buffer_range.start.is_valid(&buffer) { + if !hunk.buffer_range.start.is_valid(buffer) { log::trace!("skipping hunk with deleted start: {:?}", hunk.range); continue; } @@ -2449,7 +2449,7 @@ impl ReferenceMultibuffer { return false; } while let Some(hunk) = hunks.peek() { - match hunk.buffer_range.start.cmp(&hunk_anchor, &buffer) { + match hunk.buffer_range.start.cmp(hunk_anchor, &buffer) { cmp::Ordering::Less => { hunks.next(); } @@ -2519,8 +2519,8 @@ async fn test_random_set_ranges(cx: &mut TestAppContext, mut rng: StdRng) { let mut seen_ranges = Vec::default(); for (_, buf, range) in snapshot.excerpts() { - let start = range.context.start.to_point(&buf); - let end = range.context.end.to_point(&buf); + let start = range.context.start.to_point(buf); + let end = range.context.end.to_point(buf); seen_ranges.push(start..end); if let Some(last_end) = last_end.take() { @@ -2739,9 +2739,8 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { let id = buffer_handle.read(cx).remote_id(); if multibuffer.diff_for(id).is_none() { let base_text = base_texts.get(&id).unwrap(); - let diff = cx.new(|cx| { - BufferDiff::new_with_base_text(base_text, &buffer_handle, cx) - }); + let diff = cx + .new(|cx| BufferDiff::new_with_base_text(base_text, buffer_handle, cx)); reference.add_diff(diff.clone(), cx); multibuffer.add_diff(diff, cx) } @@ -3604,7 +3603,7 @@ fn assert_position_translation(snapshot: &MultiBufferSnapshot) { offsets[ix - 1], ); assert!( - prev_anchor.cmp(&anchor, snapshot).is_lt(), + prev_anchor.cmp(anchor, snapshot).is_lt(), "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_lt()", offsets[ix - 1], offsets[ix], diff --git a/crates/multi_buffer/src/position.rs b/crates/multi_buffer/src/position.rs index 0650875059..8a3ce78d0d 100644 --- a/crates/multi_buffer/src/position.rs +++ b/crates/multi_buffer/src/position.rs @@ -126,17 +126,17 @@ impl<T> Default for TypedRow<T> { impl<T> PartialOrd for TypedOffset<T> { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { - Some(self.cmp(&other)) + Some(self.cmp(other)) } } impl<T> PartialOrd for TypedPoint<T> { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { - Some(self.cmp(&other)) + Some(self.cmp(other)) } } impl<T> PartialOrd for TypedRow<T> { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { - Some(self.cmp(&other)) + Some(self.cmp(other)) } } diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index e07a8dc9fb..884374a72f 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -494,7 +494,7 @@ impl Onboarding { window .spawn(cx, async move |cx| { client - .sign_in_with_optional_connect(true, &cx) + .sign_in_with_optional_connect(true, cx) .await .notify_async_err(cx); }) diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index 610f6a98e3..3fe9c32a48 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -104,7 +104,7 @@ impl<const COLS: usize> Section<COLS> { self.entries .iter() .enumerate() - .map(|(index, entry)| entry.render(index_offset + index, &focus, window, cx)), + .map(|(index, entry)| entry.render(index_offset + index, focus, window, cx)), ) } } diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 004a27b0cf..9514fd7e36 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -5498,7 +5498,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5514,7 +5514,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5532,7 +5532,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5569,7 +5569,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5583,7 +5583,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5602,7 +5602,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5630,7 +5630,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5718,7 +5718,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, None, cx, @@ -5741,7 +5741,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, None, cx, @@ -5767,7 +5767,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, None, cx, @@ -5873,7 +5873,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5896,7 +5896,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5933,7 +5933,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5970,7 +5970,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6073,7 +6073,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6099,7 +6099,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6123,7 +6123,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6144,7 +6144,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6232,7 +6232,7 @@ struct OutlineEntryExcerpt { assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6259,7 +6259,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6286,7 +6286,7 @@ outline: struct OutlineEntryExcerpt <==== selected assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6313,7 +6313,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6340,7 +6340,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6367,7 +6367,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6394,7 +6394,7 @@ outline: struct OutlineEntryExcerpt <==== selected assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6421,7 +6421,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6448,7 +6448,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6475,7 +6475,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6502,7 +6502,7 @@ outline: struct OutlineEntryExcerpt <==== selected assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6608,7 +6608,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6645,7 +6645,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6673,7 +6673,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6705,7 +6705,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6736,7 +6736,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6864,7 +6864,7 @@ outline: struct OutlineEntryExcerpt .render_data .get_or_init(|| SearchData::new( &search_entry.match_range, - &multi_buffer_snapshot + multi_buffer_snapshot )) .context_text ) @@ -7255,7 +7255,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7314,7 +7314,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7338,7 +7338,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7403,7 +7403,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7544,7 +7544,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7582,7 +7582,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7616,7 +7616,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7648,7 +7648,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(&outline_panel, cx), + &snapshot(outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index c96ab4e8f3..f80f24bb71 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -368,7 +368,7 @@ impl ContextServerStore { } pub fn restart_server(&mut self, id: &ContextServerId, cx: &mut Context<Self>) -> Result<()> { - if let Some(state) = self.servers.get(&id) { + if let Some(state) = self.servers.get(id) { let configuration = state.configuration(); self.stop_server(&state.server().id(), cx)?; @@ -397,7 +397,7 @@ impl ContextServerStore { let server = server.clone(); let configuration = configuration.clone(); async move |this, cx| { - match server.clone().start(&cx).await { + match server.clone().start(cx).await { Ok(_) => { log::info!("Started {} context server", id); debug_assert!(server.client().is_some()); @@ -588,7 +588,7 @@ impl ContextServerStore { for server_id in this.servers.keys() { // All servers that are not in desired_servers should be removed from the store. // This can happen if the user removed a server from the context server settings. - if !configured_servers.contains_key(&server_id) { + if !configured_servers.contains_key(server_id) { if disabled_servers.contains_key(&server_id.0) { servers_to_stop.insert(server_id.clone()); } else { diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index 025dca4100..091189db7c 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -317,8 +317,8 @@ impl BreakpointStore { .iter() .filter_map(|breakpoint| { breakpoint.bp.bp.to_proto( - &path, - &breakpoint.position(), + path, + breakpoint.position(), &breakpoint.session_state, ) }) @@ -753,7 +753,7 @@ impl BreakpointStore { .iter() .map(|breakpoint| { let position = snapshot - .summary_for_anchor::<PointUtf16>(&breakpoint.position()) + .summary_for_anchor::<PointUtf16>(breakpoint.position()) .row; let breakpoint = &breakpoint.bp; SourceBreakpoint { diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 6f834b5dc0..ccda64fba8 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -215,7 +215,7 @@ impl DapStore { dap_settings.and_then(|s| s.binary.as_ref().map(PathBuf::from)); let user_args = dap_settings.map(|s| s.args.clone()); - let delegate = self.delegate(&worktree, console, cx); + let delegate = self.delegate(worktree, console, cx); let cwd: Arc<Path> = worktree.read(cx).abs_path().as_ref().into(); cx.spawn(async move |this, cx| { @@ -902,7 +902,7 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate { } fn worktree_root_path(&self) -> &Path { - &self.worktree.abs_path() + self.worktree.abs_path() } fn http_client(&self) -> Arc<dyn HttpClient> { self.http_client.clone() diff --git a/crates/project/src/debugger/locators/cargo.rs b/crates/project/src/debugger/locators/cargo.rs index fa265dae58..9a36584e71 100644 --- a/crates/project/src/debugger/locators/cargo.rs +++ b/crates/project/src/debugger/locators/cargo.rs @@ -187,12 +187,12 @@ impl DapLocator for CargoLocator { .cloned(); } let executable = { - if let Some(ref name) = test_name.as_ref().and_then(|name| { + if let Some(name) = test_name.as_ref().and_then(|name| { name.strip_prefix('$') .map(|name| build_config.env.get(name)) .unwrap_or(Some(name)) }) { - find_best_executable(&executables, &name).await + find_best_executable(&executables, name).await } else { None } diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index d9c28df497..b5ae714841 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -1630,7 +1630,7 @@ impl Session { + 'static, cx: &mut Context<Self>, ) -> Task<Option<T::Response>> { - if !T::is_supported(&capabilities) { + if !T::is_supported(capabilities) { log::warn!( "Attempted to send a DAP request that isn't supported: {:?}", request @@ -1688,7 +1688,7 @@ impl Session { self.requests .entry((&*key.0 as &dyn Any).type_id()) .and_modify(|request_map| { - request_map.remove(&key); + request_map.remove(key); }); } diff --git a/crates/project/src/environment.rs b/crates/project/src/environment.rs index 7379a7ef72..d109e307a8 100644 --- a/crates/project/src/environment.rs +++ b/crates/project/src/environment.rs @@ -198,7 +198,7 @@ async fn load_directory_shell_environment( ); }; - load_shell_environment(&dir, load_direnv).await + load_shell_environment(dir, load_direnv).await } Err(err) => ( None, diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 3163a10239..e8ba2425d1 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -561,7 +561,7 @@ impl GitStore { pub fn active_repository(&self) -> Option<Entity<Repository>> { self.active_repo_id .as_ref() - .map(|id| self.repositories[&id].clone()) + .map(|id| self.repositories[id].clone()) } pub fn open_unstaged_diff( @@ -1277,7 +1277,7 @@ impl GitStore { ) { match event { BufferStoreEvent::BufferAdded(buffer) => { - cx.subscribe(&buffer, |this, buffer, event, cx| { + cx.subscribe(buffer, |this, buffer, event, cx| { if let BufferEvent::LanguageChanged = event { let buffer_id = buffer.read(cx).remote_id(); if let Some(diff_state) = this.diffs.get(&buffer_id) { @@ -1295,7 +1295,7 @@ impl GitStore { } } BufferStoreEvent::BufferDropped(buffer_id) => { - self.diffs.remove(&buffer_id); + self.diffs.remove(buffer_id); for diffs in self.shared_diffs.values_mut() { diffs.remove(buffer_id); } @@ -1384,8 +1384,8 @@ impl GitStore { repository.update(cx, |repository, cx| { let repo_abs_path = &repository.work_directory_abs_path; if changed_repos.iter().any(|update| { - update.old_work_directory_abs_path.as_ref() == Some(&repo_abs_path) - || update.new_work_directory_abs_path.as_ref() == Some(&repo_abs_path) + update.old_work_directory_abs_path.as_ref() == Some(repo_abs_path) + || update.new_work_directory_abs_path.as_ref() == Some(repo_abs_path) }) { repository.reload_buffer_diff_bases(cx); } @@ -1536,7 +1536,7 @@ impl GitStore { }); if is_new { this._subscriptions - .push(cx.subscribe(&repo, Self::on_repository_event)) + .push(cx.subscribe(repo, Self::on_repository_event)) } repo.update(cx, { @@ -2353,7 +2353,7 @@ impl GitStore { // All paths prefixed by a given repo will constitute a continuous range. while let Some(path) = entries.get(ix) && let Some(repo_path) = - RepositorySnapshot::abs_path_to_repo_path_inner(&repo_path, &path) + RepositorySnapshot::abs_path_to_repo_path_inner(&repo_path, path) { paths.push((repo_path, ix)); ix += 1; @@ -2875,14 +2875,14 @@ impl RepositorySnapshot { } pub fn had_conflict_on_last_merge_head_change(&self, repo_path: &RepoPath) -> bool { - self.merge.conflicted_paths.contains(&repo_path) + self.merge.conflicted_paths.contains(repo_path) } pub fn has_conflict(&self, repo_path: &RepoPath) -> bool { let had_conflict_on_last_merge_head_change = - self.merge.conflicted_paths.contains(&repo_path); + self.merge.conflicted_paths.contains(repo_path); let has_conflict_currently = self - .status_for_path(&repo_path) + .status_for_path(repo_path) .map_or(false, |entry| entry.status.is_conflicted()); had_conflict_on_last_merge_head_change || has_conflict_currently } diff --git a/crates/project/src/git_store/git_traversal.rs b/crates/project/src/git_store/git_traversal.rs index bbcffe046d..de5ff9b935 100644 --- a/crates/project/src/git_store/git_traversal.rs +++ b/crates/project/src/git_store/git_traversal.rs @@ -211,7 +211,7 @@ impl Deref for GitEntryRef<'_> { type Target = Entry; fn deref(&self) -> &Self::Target { - &self.entry + self.entry } } diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs index 79f134b91a..54d87d230c 100644 --- a/crates/project/src/image_store.rs +++ b/crates/project/src/image_store.rs @@ -224,7 +224,7 @@ impl ProjectItem for ImageItem { path: &ProjectPath, cx: &mut App, ) -> Option<Task<anyhow::Result<Entity<Self>>>> { - if is_image_file(&project, &path, cx) { + if is_image_file(project, path, cx) { Some(cx.spawn({ let path = path.clone(); let project = project.clone(); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index fcfeb9c660..d5c3cc424f 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1165,7 +1165,7 @@ pub async fn location_link_from_lsp( server_id: LanguageServerId, cx: &mut AsyncApp, ) -> Result<LocationLink> { - let (_, language_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, cx)?; + let (_, language_server) = language_server_for_buffer(lsp_store, buffer, server_id, cx)?; let (origin_range, target_uri, target_range) = ( link.origin_selection_range, diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 11c78aad8d..1bc6770d4e 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -442,14 +442,14 @@ impl LocalLspStore { match result { Ok(server) => { lsp_store - .update(cx, |lsp_store, mut cx| { + .update(cx, |lsp_store, cx| { lsp_store.insert_newly_running_language_server( adapter, server.clone(), server_id, key, pending_workspace_folders, - &mut cx, + cx, ); }) .ok(); @@ -1927,7 +1927,7 @@ impl LocalLspStore { if let Some(lsp_edits) = lsp_edits { this.update(cx, |this, cx| { this.as_local_mut().unwrap().edits_from_lsp( - &buffer_handle, + buffer_handle, lsp_edits, language_server.server_id(), None, @@ -3115,7 +3115,7 @@ impl LocalLspStore { let mut servers_to_remove = BTreeSet::default(); let mut servers_to_preserve = HashSet::default(); - for (seed, ref state) in &self.language_server_ids { + for (seed, state) in &self.language_server_ids { if seed.worktree_id == id_to_remove { servers_to_remove.insert(state.id); } else { @@ -3169,7 +3169,7 @@ impl LocalLspStore { for watcher in watchers { if let Some((worktree, literal_prefix, pattern)) = - self.worktree_and_path_for_file_watcher(&worktrees, &watcher, cx) + self.worktree_and_path_for_file_watcher(&worktrees, watcher, cx) { worktree.update(cx, |worktree, _| { if let Some((tree, glob)) = @@ -4131,7 +4131,7 @@ impl LspStore { local.registered_buffers.remove(&buffer_id); local.buffers_opened_in_servers.remove(&buffer_id); if let Some(file) = File::from_dyn(buffer.read(cx).file()).cloned() { - local.unregister_old_buffer_from_language_servers(&buffer, &file, cx); + local.unregister_old_buffer_from_language_servers(buffer, &file, cx); } } }) @@ -4453,7 +4453,7 @@ impl LspStore { .contains(&server_status.name) .then_some(server_id) }) - .filter_map(|server_id| self.lsp_server_capabilities.get(&server_id)) + .filter_map(|server_id| self.lsp_server_capabilities.get(server_id)) .any(check) } @@ -5419,7 +5419,7 @@ impl LspStore { ) -> Task<Result<Vec<LocationLink>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetTypeDefinitions { position }; - if !self.is_capable_for_proto_request(&buffer, &request, cx) { + if !self.is_capable_for_proto_request(buffer, &request, cx) { return Task::ready(Ok(Vec::new())); } let request_task = upstream_client.request(proto::MultiLspQuery { @@ -5573,7 +5573,7 @@ impl LspStore { ) -> Task<Result<Vec<Location>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetReferences { position }; - if !self.is_capable_for_proto_request(&buffer, &request, cx) { + if !self.is_capable_for_proto_request(buffer, &request, cx) { return Task::ready(Ok(Vec::new())); } let request_task = upstream_client.request(proto::MultiLspQuery { @@ -5755,7 +5755,7 @@ impl LspStore { let lsp_data = self.lsp_code_lens.entry(buffer_id).or_default(); if let Some((updating_for, running_update)) = &lsp_data.update { - if !version_queried_for.changed_since(&updating_for) { + if !version_queried_for.changed_since(updating_for) { return running_update.clone(); } } @@ -6786,7 +6786,7 @@ impl LspStore { let lsp_data = self.lsp_document_colors.entry(buffer_id).or_default(); if let Some((updating_for, running_update)) = &lsp_data.colors_update { - if !version_queried_for.changed_since(&updating_for) { + if !version_queried_for.changed_since(updating_for) { return Some(running_update.clone()); } } @@ -10057,7 +10057,7 @@ impl LspStore { ) -> Shared<Task<Option<HashMap<String, String>>>> { if let Some(environment) = &self.as_local().map(|local| local.environment.clone()) { environment.update(cx, |env, cx| { - env.get_buffer_environment(&buffer, &self.worktree_store, cx) + env.get_buffer_environment(buffer, &self.worktree_store, cx) }) } else { Task::ready(None).shared() @@ -11175,7 +11175,7 @@ impl LspStore { let Some(local) = self.as_local() else { return }; local.prettier_store.update(cx, |prettier_store, cx| { - prettier_store.update_prettier_settings(&worktree_handle, changes, cx) + prettier_store.update_prettier_settings(worktree_handle, changes, cx) }); let worktree_id = worktree_handle.read(cx).id(); diff --git a/crates/project/src/manifest_tree.rs b/crates/project/src/manifest_tree.rs index 8621d24d06..f68905d14c 100644 --- a/crates/project/src/manifest_tree.rs +++ b/crates/project/src/manifest_tree.rs @@ -199,7 +199,7 @@ impl ManifestTree { ) { match evt { WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => { - self.root_points.remove(&worktree_id); + self.root_points.remove(worktree_id); } _ => {} } diff --git a/crates/project/src/manifest_tree/server_tree.rs b/crates/project/src/manifest_tree/server_tree.rs index 49c0cff730..7da43feeef 100644 --- a/crates/project/src/manifest_tree/server_tree.rs +++ b/crates/project/src/manifest_tree/server_tree.rs @@ -192,7 +192,7 @@ impl LanguageServerTree { ) }); languages.insert(language_name.clone()); - Arc::downgrade(&node).into() + Arc::downgrade(node).into() }) } @@ -245,7 +245,7 @@ impl LanguageServerTree { if !settings.enable_language_server { return Default::default(); } - let available_lsp_adapters = self.languages.lsp_adapters(&language_name); + let available_lsp_adapters = self.languages.lsp_adapters(language_name); let available_language_servers = available_lsp_adapters .iter() .map(|lsp_adapter| lsp_adapter.name.clone()) @@ -287,7 +287,7 @@ impl LanguageServerTree { // (e.g., native vs extension) still end up in the right order at the end, rather than // it being based on which language server happened to be loaded in first. self.languages.reorder_language_servers( - &language_name, + language_name, adapters_with_settings .values() .map(|(_, adapter)| adapter.clone()) @@ -314,7 +314,7 @@ impl LanguageServerTree { pub(crate) fn remove_nodes(&mut self, ids: &BTreeSet<LanguageServerId>) { for (_, servers) in &mut self.instances { for (_, nodes) in &mut servers.roots { - nodes.retain(|_, (node, _)| node.id.get().map_or(true, |id| !ids.contains(&id))); + nodes.retain(|_, (node, _)| node.id.get().map_or(true, |id| !ids.contains(id))); } } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 57afaceeca..17997850b6 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1848,7 +1848,7 @@ impl Project { cx: &'a mut App, ) -> Shared<Task<Option<HashMap<String, String>>>> { self.environment.update(cx, |environment, cx| { - environment.get_buffer_environment(&buffer, &worktree_store, cx) + environment.get_buffer_environment(buffer, worktree_store, cx) }) } @@ -2592,7 +2592,7 @@ impl Project { cx: &mut App, ) -> OpenLspBufferHandle { self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.register_buffer_with_language_servers(&buffer, HashSet::default(), false, cx) + lsp_store.register_buffer_with_language_servers(buffer, HashSet::default(), false, cx) }) } @@ -4167,15 +4167,14 @@ impl Project { }) .collect(); - cx.spawn(async move |_, mut cx| { + cx.spawn(async move |_, cx| { if let Some(buffer_worktree_id) = buffer_worktree_id { if let Some((worktree, _)) = worktrees_with_ids .iter() .find(|(_, id)| *id == buffer_worktree_id) { for candidate in candidates.iter() { - if let Some(path) = - Self::resolve_path_in_worktree(&worktree, candidate, &mut cx) + if let Some(path) = Self::resolve_path_in_worktree(worktree, candidate, cx) { return Some(path); } @@ -4187,9 +4186,7 @@ impl Project { continue; } for candidate in candidates.iter() { - if let Some(path) = - Self::resolve_path_in_worktree(&worktree, candidate, &mut cx) - { + if let Some(path) = Self::resolve_path_in_worktree(&worktree, candidate, cx) { return Some(path); } } @@ -5329,7 +5326,7 @@ impl ResolvedPath { pub fn project_path(&self) -> Option<&ProjectPath> { match self { - Self::ProjectPath { project_path, .. } => Some(&project_path), + Self::ProjectPath { project_path, .. } => Some(project_path), _ => None, } } @@ -5399,7 +5396,7 @@ impl Completion { _ => None, }) .unwrap_or(DEFAULT_KIND_KEY); - (kind_key, &self.label.filter_text()) + (kind_key, self.label.filter_text()) } /// Whether this completion is a snippet. diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index d78526ddd0..050ca60e7a 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -1105,7 +1105,7 @@ impl SettingsObserver { cx: &mut Context<Self>, ) -> Task<()> { let mut user_tasks_file_rx = - watch_config_file(&cx.background_executor(), fs, file_path.clone()); + watch_config_file(cx.background_executor(), fs, file_path.clone()); let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next()); let weak_entry = cx.weak_entity(); cx.spawn(async move |settings_observer, cx| { @@ -1160,7 +1160,7 @@ impl SettingsObserver { cx: &mut Context<Self>, ) -> Task<()> { let mut user_tasks_file_rx = - watch_config_file(&cx.background_executor(), fs, file_path.clone()); + watch_config_file(cx.background_executor(), fs, file_path.clone()); let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next()); let weak_entry = cx.weak_entity(); cx.spawn(async move |settings_observer, cx| { diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index d0f1c71daf..8d8a1bd008 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -333,7 +333,7 @@ impl Inventory { for locator in locators.values() { if let Some(scenario) = locator - .create_scenario(&task.original_task(), &task.display_label(), &adapter) + .create_scenario(task.original_task(), task.display_label(), &adapter) .await { scenarios.push((kind, scenario)); diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index d5ddd89419..892847a380 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -503,7 +503,7 @@ impl ProjectPanel { if let Some((worktree, expanded_dir_ids)) = project .read(cx) .worktree_for_id(*worktree_id, cx) - .zip(this.expanded_dir_ids.get_mut(&worktree_id)) + .zip(this.expanded_dir_ids.get_mut(worktree_id)) { let worktree = worktree.read(cx); @@ -3043,7 +3043,7 @@ impl ProjectPanel { if hide_root && Some(entry.entry) == worktree.read(cx).root_entry() { if new_entry_parent_id == Some(entry.id) { visible_worktree_entries.push(Self::create_new_git_entry( - &entry.entry, + entry.entry, entry.git_summary, new_entry_kind, )); @@ -3106,7 +3106,7 @@ impl ProjectPanel { }; if precedes_new_entry && (!hide_gitignore || !entry.is_ignored) { visible_worktree_entries.push(Self::create_new_git_entry( - &entry.entry, + entry.entry, entry.git_summary, new_entry_kind, )); @@ -3503,7 +3503,7 @@ impl ProjectPanel { let base_index = ix + entry_range.start; for (i, entry) in visible.entries[entry_range].iter().enumerate() { let global_index = base_index + i; - callback(&entry, global_index, entries, window, cx); + callback(entry, global_index, entries, window, cx); } ix = end_ix; } @@ -4669,7 +4669,7 @@ impl ProjectPanel { }; let (depth, difference) = - ProjectPanel::calculate_depth_and_difference(&entry, entries_paths); + ProjectPanel::calculate_depth_and_difference(entry, entries_paths); let filename = match difference { diff if diff > 1 => entry diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 47aed8f470..9fffbde5f7 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -191,7 +191,7 @@ impl PickerDelegate for ProjectSymbolsDelegate { .iter() .enumerate() .map(|(id, symbol)| { - StringMatchCandidate::new(id, &symbol.label.filter_text()) + StringMatchCandidate::new(id, symbol.label.filter_text()) }) .partition(|candidate| { project diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 81259c1aac..bc837b1a1e 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -1490,7 +1490,7 @@ impl RemoteServerProjects { .track_focus(&self.focus_handle(cx)) .id("ssh-server-list") .overflow_y_scroll() - .track_scroll(&scroll_handle) + .track_scroll(scroll_handle) .size_full() .child(connect_button) .child( diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index ea383ac264..71e8f6e8e7 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -730,7 +730,7 @@ impl SshRemoteClient { cx, ); - let multiplex_task = Self::monitor(this.downgrade(), io_task, &cx); + let multiplex_task = Self::monitor(this.downgrade(), io_task, cx); if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await { log::error!("failed to establish connection: {}", error); @@ -918,8 +918,8 @@ impl SshRemoteClient { } }; - let multiplex_task = Self::monitor(this.clone(), io_task, &cx); - client.reconnect(incoming_rx, outgoing_tx, &cx); + let multiplex_task = Self::monitor(this.clone(), io_task, cx); + client.reconnect(incoming_rx, outgoing_tx, cx); if let Err(error) = client.resync(HEARTBEAT_TIMEOUT).await { failed!(error, attempts, ssh_connection, delegate); @@ -1005,8 +1005,8 @@ impl SshRemoteClient { if missed_heartbeats != 0 { missed_heartbeats = 0; - let _ =this.update(cx, |this, mut cx| { - this.handle_heartbeat_result(missed_heartbeats, &mut cx) + let _ =this.update(cx, |this, cx| { + this.handle_heartbeat_result(missed_heartbeats, cx) })?; } } @@ -1036,8 +1036,8 @@ impl SshRemoteClient { continue; } - let result = this.update(cx, |this, mut cx| { - this.handle_heartbeat_result(missed_heartbeats, &mut cx) + let result = this.update(cx, |this, cx| { + this.handle_heartbeat_result(missed_heartbeats, cx) })?; if result.is_break() { return Ok(()); @@ -1214,7 +1214,7 @@ impl SshRemoteClient { .await .unwrap(); - connection.simulate_disconnect(&cx); + connection.simulate_disconnect(cx); }) } @@ -1523,7 +1523,7 @@ impl RemoteConnection for SshRemoteConnection { incoming_tx, outgoing_rx, connection_activity_tx, - &cx, + cx, ) } @@ -1908,8 +1908,8 @@ impl SshRemoteConnection { "-H", "Content-Type: application/json", "-d", - &body, - &url, + body, + url, "-o", &tmp_path_gz.to_string(), ], @@ -1930,8 +1930,8 @@ impl SshRemoteConnection { "--method=GET", "--header=Content-Type: application/json", "--body-data", - &body, - &url, + body, + url, "-O", &tmp_path_gz.to_string(), ], @@ -1982,7 +1982,7 @@ impl SshRemoteConnection { tmp_path_gz, size / 1024 ); - self.upload_file(&src_path, &tmp_path_gz) + self.upload_file(src_path, tmp_path_gz) .await .context("failed to upload server binary")?; log::info!("uploaded remote development server in {:?}", t0.elapsed()); @@ -2654,7 +2654,7 @@ mod fake { let (outgoing_tx, _) = mpsc::unbounded::<Envelope>(); let (_, incoming_rx) = mpsc::unbounded::<Envelope>(); self.server_channel - .reconnect(incoming_rx, outgoing_tx, &self.server_cx.get(&cx)); + .reconnect(incoming_rx, outgoing_tx, &self.server_cx.get(cx)); } fn start_proxy( diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index ac1737ba4b..6b0cc2219f 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -348,7 +348,7 @@ impl HeadlessProject { .iter() .map(|action| action.title.to_string()) .collect(), - level: Some(prompt_to_proto(&prompt)), + level: Some(prompt_to_proto(prompt)), lsp_name: prompt.lsp_name.clone(), message: prompt.message.clone(), }); @@ -388,7 +388,7 @@ impl HeadlessProject { let parent = fs.canonicalize(parent).await.map_err(|_| { anyhow!( proto::ErrorCode::DevServerProjectPathDoesNotExist - .with_tag("path", &path.to_string_lossy().as_ref()) + .with_tag("path", path.to_string_lossy().as_ref()) ) })?; parent.join(path.file_name().unwrap()) diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index dc7fab8c3c..4daacb3eec 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -155,7 +155,7 @@ fn init_panic_hook(session_id: String) { log::error!( "panic occurred: {}\nBacktrace:\n{}", &payload, - (&backtrace).join("\n") + backtrace.join("\n") ); let panic_data = telemetry_events::Panic { @@ -796,11 +796,8 @@ fn initialize_settings( fs: Arc<dyn Fs>, cx: &mut App, ) -> watch::Receiver<Option<NodeBinaryOptions>> { - let user_settings_file_rx = watch_config_file( - &cx.background_executor(), - fs, - paths::settings_file().clone(), - ); + let user_settings_file_rx = + watch_config_file(cx.background_executor(), fs, paths::settings_file().clone()); handle_settings_file_changes(user_settings_file_rx, cx, { let session = session.clone(); diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index 36a0af30d0..a84f147dd2 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -575,7 +575,7 @@ impl project::ProjectItem for NotebookItem { .with_context(|| format!("finding the absolute path of {path:?}"))?; // todo: watch for changes to the file - let file_content = fs.load(&abs_path.as_path()).await?; + let file_content = fs.load(abs_path.as_path()).await?; let notebook = nbformat::parse_notebook(&file_content); let notebook = match notebook { diff --git a/crates/rope/src/chunk.rs b/crates/rope/src/chunk.rs index dc00674380..96f7d1db11 100644 --- a/crates/rope/src/chunk.rs +++ b/crates/rope/src/chunk.rs @@ -49,7 +49,7 @@ impl Chunk { self.chars_utf16 |= slice.chars_utf16 << base_ix; self.newlines |= slice.newlines << base_ix; self.tabs |= slice.tabs << base_ix; - self.text.push_str(&slice.text); + self.text.push_str(slice.text); } #[inline(always)] @@ -623,7 +623,7 @@ mod tests { let text = &text[..ix]; log::info!("Chunk: {:?}", text); - let chunk = Chunk::new(&text); + let chunk = Chunk::new(text); verify_chunk(chunk.as_slice(), text); for _ in 0..10 { diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 904c74d03c..1afbc2c23b 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -142,7 +142,7 @@ impl SearchOption { SearchSource::Buffer => { let focus_handle = focus_handle.clone(); button.on_click(move |_: &ClickEvent, window, cx| { - if !focus_handle.is_focused(&window) { + if !focus_handle.is_focused(window) { window.focus(&focus_handle); } window.dispatch_action(action.boxed_clone(), cx); diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 8cc838a8a6..44f6b3fdd2 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -26,7 +26,7 @@ pub(super) fn render_action_button( .on_click({ let focus_handle = focus_handle.clone(); move |_, window, cx| { - if !focus_handle.is_focused(&window) { + if !focus_handle.is_focused(window) { window.focus(&focus_handle); } window.dispatch_action(action.boxed_clone(), cx) diff --git a/crates/semantic_index/src/summary_index.rs b/crates/semantic_index/src/summary_index.rs index 6e3aae1344..20858c8d3f 100644 --- a/crates/semantic_index/src/summary_index.rs +++ b/crates/semantic_index/src/summary_index.rs @@ -324,7 +324,7 @@ impl SummaryIndex { ) -> Vec<(Arc<Path>, Option<MTime>)> { let entry_db_key = db_key_for_path(&entry.path); - match digest_db.get(&txn, &entry_db_key) { + match digest_db.get(txn, &entry_db_key) { Ok(opt_saved_digest) => { // The file path is the same, but the mtime is different. (Or there was no mtime.) // It needs updating, so add it to the backlog! Then, if the backlog is full, drain it and summarize its contents. @@ -575,7 +575,7 @@ impl SummaryIndex { let code_len = code.len(); cx.spawn(async move |cx| { - let stream = model.stream_completion(request, &cx); + let stream = model.stream_completion(request, cx); cx.background_spawn(async move { let answer: String = stream .await? diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index fb03662290..b0f7d2449e 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -358,11 +358,11 @@ impl KeymapFile { let action_input = items[1].clone(); let action_input_string = action_input.to_string(); ( - cx.build_action(&name, Some(action_input)), + cx.build_action(name, Some(action_input)), Some(action_input_string), ) } - Value::String(name) => (cx.build_action(&name, None), None), + Value::String(name) => (cx.build_action(name, None), None), Value::Null => (Ok(NoAction.boxed_clone()), None), _ => { return Err(format!( @@ -839,7 +839,7 @@ impl KeymapFile { if &action.0 != target_action_value { continue; } - return Some((index, &keystrokes_str)); + return Some((index, keystrokes_str)); } } None diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 5181d86a78..58090d2060 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -270,7 +270,7 @@ impl ConflictState { for origin in indices.iter() { conflicts[origin.index] = - origin.get_conflict_with(if origin == fst { &snd } else { &fst }) + origin.get_conflict_with(if origin == fst { snd } else { fst }) } has_user_conflicts |= fst.override_source == KeybindSource::User @@ -673,8 +673,8 @@ impl KeymapEditor { action_name, action_arguments, &actions_with_schemas, - &action_documentation, - &humanized_action_names, + action_documentation, + humanized_action_names, ); let index = processed_bindings.len(); @@ -696,8 +696,8 @@ impl KeymapEditor { action_name, None, &actions_with_schemas, - &action_documentation, - &humanized_action_names, + action_documentation, + humanized_action_names, ); let string_match_candidate = StringMatchCandidate::new(index, &action_information.humanized_name); @@ -2187,7 +2187,7 @@ impl KeybindingEditorModal { }) .transpose()?; - cx.build_action(&self.editing_keybind.action().name, value) + cx.build_action(self.editing_keybind.action().name, value) .context("Failed to validate action arguments")?; Ok(action_arguments) } @@ -2862,11 +2862,8 @@ impl CompletionProvider for KeyContextCompletionProvider { break; } } - let start_anchor = buffer.anchor_before( - buffer_position - .to_offset(&buffer) - .saturating_sub(count_back), - ); + let start_anchor = + buffer.anchor_before(buffer_position.to_offset(buffer).saturating_sub(count_back)); let replace_range = start_anchor..buffer_position; gpui::Task::ready(Ok(vec![project::CompletionResponse { completions: self @@ -2983,14 +2980,14 @@ async fn save_keybinding_update( let target = settings::KeybindUpdateTarget { context: existing_context, keystrokes: existing_keystrokes, - action_name: &existing.action().name, + action_name: existing.action().name, action_arguments: existing_args, }; let source = settings::KeybindUpdateTarget { context: action_mapping.context.as_ref().map(|a| &***a), keystrokes: &action_mapping.keystrokes, - action_name: &existing.action().name, + action_name: existing.action().name, action_arguments: new_args, }; @@ -3044,7 +3041,7 @@ async fn remove_keybinding( target: settings::KeybindUpdateTarget { context: existing.context().and_then(KeybindContextString::local_str), keystrokes, - action_name: &existing.action().name, + action_name: existing.action().name, action_arguments: existing .action() .arguments diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index 2b3e815f36..66dd636d21 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/src/ui_components/table.rs @@ -343,7 +343,7 @@ impl TableInteractionState { .on_any_mouse_down(|_, _, cx| { cx.stop_propagation(); }) - .on_scroll_wheel(Self::listener(&this, |_, _, _, cx| { + .on_scroll_wheel(Self::listener(this, |_, _, _, cx| { cx.notify(); })) .children(Scrollbar::vertical( diff --git a/crates/streaming_diff/src/streaming_diff.rs b/crates/streaming_diff/src/streaming_diff.rs index f7649b1bf1..704164e01e 100644 --- a/crates/streaming_diff/src/streaming_diff.rs +++ b/crates/streaming_diff/src/streaming_diff.rs @@ -303,10 +303,10 @@ impl LineDiff { self.flush_insert(old_text); self.buffered_insert.push_str(suffix); } else { - self.buffered_insert.push_str(&text); + self.buffered_insert.push_str(text); } } else { - self.buffered_insert.push_str(&text); + self.buffered_insert.push_str(text); if !text.ends_with('\n') { self.flush_insert(old_text); } @@ -523,7 +523,7 @@ mod tests { apply_line_operations(old_text, &new_text, &expected_line_ops) ); - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!(line_ops, expected_line_ops); } @@ -534,7 +534,7 @@ mod tests { CharOperation::Keep { bytes: 5 }, CharOperation::Delete { bytes: 4 }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -559,7 +559,7 @@ mod tests { text: "\ncccc".into(), }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -582,7 +582,7 @@ mod tests { CharOperation::Delete { bytes: 5 }, CharOperation::Keep { bytes: 4 }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -609,7 +609,7 @@ mod tests { }, CharOperation::Keep { bytes: 5 }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -638,7 +638,7 @@ mod tests { text: "\nEEEE".into(), }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -664,7 +664,7 @@ mod tests { CharOperation::Insert { text: "A".into() }, CharOperation::Keep { bytes: 10 }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -689,7 +689,7 @@ mod tests { CharOperation::Keep { bytes: 4 }, ]; let new_text = apply_char_operations(old_text, &char_ops); - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -710,7 +710,7 @@ mod tests { CharOperation::Insert { text: "\n".into() }, CharOperation::Keep { bytes: 9 }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -733,7 +733,7 @@ mod tests { CharOperation::Delete { bytes: 1 }, CharOperation::Keep { bytes: 4 }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -759,7 +759,7 @@ mod tests { }, CharOperation::Keep { bytes: 4 }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -783,7 +783,7 @@ mod tests { CharOperation::Delete { bytes: 2 }, CharOperation::Keep { bytes: 4 }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ @@ -814,7 +814,7 @@ mod tests { }, CharOperation::Keep { bytes: 6 }, ]; - let line_ops = char_ops_to_line_ops(&old_text, &char_ops); + let line_ops = char_ops_to_line_ops(old_text, &char_ops); assert_eq!( line_ops, vec![ diff --git a/crates/task/src/vscode_debug_format.rs b/crates/task/src/vscode_debug_format.rs index 2990716686..e688760a5e 100644 --- a/crates/task/src/vscode_debug_format.rs +++ b/crates/task/src/vscode_debug_format.rs @@ -131,7 +131,7 @@ mod tests { } "#; let parsed: VsCodeDebugTaskFile = - serde_json_lenient::from_str(&raw).expect("deserializing launch.json"); + serde_json_lenient::from_str(raw).expect("deserializing launch.json"); let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates"); pretty_assertions::assert_eq!( zed, diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 86728cc11c..2f3b7aa28d 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -890,15 +890,15 @@ impl Terminal { if self.vi_mode_enabled { match *scroll { AlacScroll::Delta(delta) => { - term.vi_mode_cursor = term.vi_mode_cursor.scroll(&term, delta); + term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, delta); } AlacScroll::PageUp => { let lines = term.screen_lines() as i32; - term.vi_mode_cursor = term.vi_mode_cursor.scroll(&term, lines); + term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, lines); } AlacScroll::PageDown => { let lines = -(term.screen_lines() as i32); - term.vi_mode_cursor = term.vi_mode_cursor.scroll(&term, lines); + term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, lines); } AlacScroll::Top => { let point = AlacPoint::new(term.topmost_line(), Column(0)); diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 568dc1db2e..cdf405b642 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -346,7 +346,7 @@ impl TerminalPanel { pane::Event::RemovedItem { .. } => self.serialize(cx), pane::Event::Remove { focus_on_pane } => { let pane_count_before_removal = self.center.panes().len(); - let _removal_result = self.center.remove(&pane); + let _removal_result = self.center.remove(pane); if pane_count_before_removal == 1 { self.center.first_pane().update(cx, |pane, cx| { pane.set_zoomed(false, cx); @@ -1181,10 +1181,10 @@ impl Render for TerminalPanel { registrar.size_full().child(self.center.render( workspace.zoomed_item(), &workspace::PaneRenderContext { - follower_states: &&HashMap::default(), + follower_states: &HashMap::default(), active_call: workspace.active_call(), active_pane: &self.active_pane, - app_state: &workspace.app_state(), + app_state: workspace.app_state(), project: workspace.project(), workspace: &workspace.weak_handle(), }, diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 534c0a8051..559faea42a 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1604,15 +1604,15 @@ impl Item for TerminalView { TaskStatus::Running => ( IconName::PlayFilled, Color::Disabled, - TerminalView::rerun_button(&terminal_task), + TerminalView::rerun_button(terminal_task), ), TaskStatus::Unknown => ( IconName::Warning, Color::Warning, - TerminalView::rerun_button(&terminal_task), + TerminalView::rerun_button(terminal_task), ), TaskStatus::Completed { success } => { - let rerun_button = TerminalView::rerun_button(&terminal_task); + let rerun_button = TerminalView::rerun_button(terminal_task); if *success { (IconName::Check, Color::Success, rerun_button) diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index eb317a5616..84622888f1 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -478,7 +478,7 @@ impl TitleBar { repo.branch .as_ref() .map(|branch| branch.name()) - .map(|name| util::truncate_and_trailoff(&name, MAX_BRANCH_NAME_LENGTH)) + .map(|name| util::truncate_and_trailoff(name, MAX_BRANCH_NAME_LENGTH)) .or_else(|| { repo.head_commit.as_ref().map(|commit| { commit @@ -617,7 +617,7 @@ impl TitleBar { window .spawn(cx, async move |cx| { client - .sign_in_with_optional_connect(true, &cx) + .sign_in_with_optional_connect(true, cx) .await .notify_async_err(cx); }) diff --git a/crates/ui/src/components/indent_guides.rs b/crates/ui/src/components/indent_guides.rs index e3dc1f35fa..5e6f4ee8ba 100644 --- a/crates/ui/src/components/indent_guides.rs +++ b/crates/ui/src/components/indent_guides.rs @@ -216,7 +216,7 @@ mod uniform_list { }; let visible_entries = &compute_indents_fn(visible_range.clone(), window, cx); let indent_guides = compute_indent_guides( - &visible_entries, + visible_entries, visible_range.start, includes_trailing_indent, ); @@ -241,7 +241,7 @@ mod sticky_items { window: &mut Window, cx: &mut App, ) -> AnyElement { - let indent_guides = compute_indent_guides(&indents, 0, false); + let indent_guides = compute_indent_guides(indents, 0, false); self.render_from_layout(indent_guides, bounds, item_height, window, cx) } } diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index 56be867796..bbce6101f4 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -163,7 +163,7 @@ pub fn render_keystroke( let size = size.into(); if use_text { - let element = Key::new(keystroke_text(&keystroke, platform_style, vim_mode), color) + let element = Key::new(keystroke_text(keystroke, platform_style, vim_mode), color) .size(size) .into_any_element(); vec![element] @@ -176,7 +176,7 @@ pub fn render_keystroke( size, true, )); - elements.push(render_key(&keystroke, color, platform_style, size)); + elements.push(render_key(keystroke, color, platform_style, size)); elements } } diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index ce5e5a0300..fe1537684c 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -95,7 +95,7 @@ impl VimOption { } } - Self::possibilities(&prefix) + Self::possibilities(prefix) .map(|possible| { let mut options = prefix_of_options.clone(); options.push(possible); diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index a6a07e7b2f..367b5130b6 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -2280,8 +2280,8 @@ fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) - } let mut last_position = None; for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() { - let excerpt_range = language::ToOffset::to_offset(&range.context.start, &buffer) - ..language::ToOffset::to_offset(&range.context.end, &buffer); + let excerpt_range = language::ToOffset::to_offset(&range.context.start, buffer) + ..language::ToOffset::to_offset(&range.context.end, buffer); if offset >= excerpt_range.start && offset <= excerpt_range.end { let text_anchor = buffer.anchor_after(offset); let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor); @@ -2882,7 +2882,7 @@ fn method_motion( } else { possibilities.min().unwrap_or(offset) }; - let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left); + let new_point = map.clip_point(dest.to_display_point(map), Bias::Left); if new_point == display_point { break; } @@ -2936,7 +2936,7 @@ fn comment_motion( } else { possibilities.min().unwrap_or(offset) }; - let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left); + let new_point = map.clip_point(dest.to_display_point(map), Bias::Left); if new_point == display_point { break; } @@ -3003,7 +3003,7 @@ fn section_motion( possibilities.min().unwrap_or(map.buffer_snapshot.len()) }; - let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left); + let new_point = map.clip_point(offset.to_display_point(map), Bias::Left); if new_point == display_point { break; } diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index 007514e472..115aef1dab 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -155,7 +155,7 @@ fn increment_decimal_string(num: &str, delta: i64) -> String { } fn increment_hex_string(num: &str, delta: i64) -> String { - let result = if let Ok(val) = u64::from_str_radix(&num, 16) { + let result = if let Ok(val) = u64::from_str_radix(num, 16) { val.wrapping_add_signed(delta) } else { u64::MAX @@ -181,7 +181,7 @@ fn should_use_lowercase(num: &str) -> bool { } fn increment_binary_string(num: &str, delta: i64) -> String { - let result = if let Ok(val) = u64::from_str_radix(&num, 2) { + let result = if let Ok(val) = u64::from_str_radix(num, 2) { val.wrapping_add_signed(delta) } else { u64::MAX diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index af13bc0fd0..9eb8367f57 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -549,7 +549,7 @@ mod test { cx.set_neovim_option("nowrap").await; let content = "ˇ01234567890123456789"; - cx.set_shared_state(&content).await; + cx.set_shared_state(content).await; cx.simulate_shared_keystrokes("z shift-l").await; cx.shared_state().await.assert_eq("012345ˇ67890123456789"); @@ -560,7 +560,7 @@ mod test { cx.shared_state().await.assert_eq("012345ˇ67890123456789"); let content = "ˇ01234567890123456789"; - cx.set_shared_state(&content).await; + cx.set_shared_state(content).await; cx.simulate_shared_keystrokes("z l").await; cx.shared_state().await.assert_eq("0ˇ1234567890123456789"); diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 423859dadc..2e8e2f76bd 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -540,7 +540,7 @@ impl MarksState { cx: &mut Context<Self>, ) { let buffer = multibuffer.read(cx).as_singleton(); - let abs_path = buffer.as_ref().and_then(|b| self.path_for_buffer(&b, cx)); + let abs_path = buffer.as_ref().and_then(|b| self.path_for_buffer(b, cx)); let Some(abs_path) = abs_path else { self.multibuffer_marks @@ -606,7 +606,7 @@ impl MarksState { match target? { MarkLocation::Buffer(entity_id) => { - let anchors = self.multibuffer_marks.get(&entity_id)?; + let anchors = self.multibuffer_marks.get(entity_id)?; return Some(Mark::Buffer(*entity_id, anchors.get(name)?.clone())); } MarkLocation::Path(path) => { @@ -636,7 +636,7 @@ impl MarksState { match target { MarkLocation::Buffer(entity_id) => { self.multibuffer_marks - .get_mut(&entity_id) + .get_mut(entity_id) .map(|m| m.remove(&mark_name.clone())); return; } @@ -1042,7 +1042,7 @@ impl Operator { } => format!("^K{}", make_visible(&first_char.to_string())), Operator::Literal { prefix: Some(prefix), - } => format!("^V{}", make_visible(&prefix)), + } => format!("^V{}", make_visible(prefix)), Operator::AutoIndent => "=".to_string(), Operator::ShellCommand => "=".to_string(), _ => self.id().to_string(), diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 46ea261cd6..45cef3a2b9 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -67,7 +67,7 @@ impl NeovimConnection { // Ensure we don't create neovim connections in parallel let _lock = NEOVIM_LOCK.lock(); let (nvim, join_handle, child) = new_child_cmd( - &mut Command::new("nvim") + Command::new("nvim") .arg("--embed") .arg("--clean") // disable swap (otherwise after about 1000 test runs you run out of swap file names) @@ -161,7 +161,7 @@ impl NeovimConnection { #[cfg(feature = "neovim")] pub async fn set_state(&mut self, marked_text: &str) { - let (text, selections) = parse_state(&marked_text); + let (text, selections) = parse_state(marked_text); let nvim_buffer = self .nvim diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 44d9b8f456..15b0b443b5 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -265,7 +265,7 @@ pub fn init(cx: &mut App) { workspace.register_action(|workspace, _: &MaximizePane, window, cx| { let pane = workspace.active_pane(); - let Some(size) = workspace.bounding_box_for_pane(&pane) else { + let Some(size) = workspace.bounding_box_for_pane(pane) else { return; }; @@ -1599,7 +1599,7 @@ impl Vim { second_char, smartcase: VimSettings::get_global(cx).use_smartcase_find, }; - Vim::globals(cx).last_find = Some((&sneak).clone()); + Vim::globals(cx).last_find = Some(sneak.clone()); self.motion(sneak, window, cx) } } else { @@ -1616,7 +1616,7 @@ impl Vim { second_char, smartcase: VimSettings::get_global(cx).use_smartcase_find, }; - Vim::globals(cx).last_find = Some((&sneak).clone()); + Vim::globals(cx).last_find = Some(sneak.clone()); self.motion(sneak, window, cx) } } else { diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 3b789b1f3e..ffbae3ff76 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -414,7 +414,7 @@ impl Vim { ); } - let original_point = selection.tail().to_point(&map); + let original_point = selection.tail().to_point(map); if let Some(range) = object.range(map, mut_selection, around, count) { if !range.is_empty() { diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 1356322a5c..8af39be3e7 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -1038,7 +1038,7 @@ where { fn detach_and_notify_err(self, window: &mut Window, cx: &mut App) { window - .spawn(cx, async move |mut cx| self.await.notify_async_err(&mut cx)) + .spawn(cx, async move |cx| self.await.notify_async_err(cx)) .detach(); } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 860a57c21f..0a40dbc12c 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1627,8 +1627,7 @@ impl Pane { items_to_close .iter() .filter(|item| { - item.is_dirty(cx) - && !Self::skip_save_on_close(item.as_ref(), &workspace, cx) + item.is_dirty(cx) && !Self::skip_save_on_close(item.as_ref(), workspace, cx) }) .map(|item| item.boxed_clone()) .collect::<Vec<_>>() @@ -1657,7 +1656,7 @@ impl Pane { let mut should_save = true; if save_intent == SaveIntent::Close { workspace.update(cx, |workspace, cx| { - if Self::skip_save_on_close(item_to_close.as_ref(), &workspace, cx) { + if Self::skip_save_on_close(item_to_close.as_ref(), workspace, cx) { should_save = false; } })?; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 02eac1665b..8ec61b6f10 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -647,7 +647,7 @@ impl ProjectItemRegistry { .build_project_item_for_path_fns .iter() .rev() - .find_map(|open_project_item| open_project_item(&project, &path, window, cx)) + .find_map(|open_project_item| open_project_item(project, path, window, cx)) else { return Task::ready(Err(anyhow!("cannot open file {:?}", path.path))); }; @@ -2431,7 +2431,7 @@ impl Workspace { ); window.prompt( PromptLevel::Warning, - &"Do you want to save all changes in the following files?", + "Do you want to save all changes in the following files?", Some(&detail), &["Save all", "Discard all", "Cancel"], cx, @@ -2767,9 +2767,9 @@ impl Workspace { let item = pane.read(cx).active_item(); let pane = pane.downgrade(); - window.spawn(cx, async move |mut cx| { + window.spawn(cx, async move |cx| { if let Some(item) = item { - Pane::save_item(project, &pane, item.as_ref(), save_intent, &mut cx) + Pane::save_item(project, &pane, item.as_ref(), save_intent, cx) .await .map(|_| ()) } else { @@ -3889,14 +3889,14 @@ impl Workspace { pane.track_alternate_file_items(); }); if *local { - self.unfollow_in_pane(&pane, window, cx); + self.unfollow_in_pane(pane, window, cx); } serialize_workspace = *focus_changed || pane != self.active_pane(); if pane == self.active_pane() { self.active_item_path_changed(window, cx); self.update_active_view_for_followers(window, cx); } else if *local { - self.set_active_pane(&pane, window, cx); + self.set_active_pane(pane, window, cx); } } pane::Event::UserSavedItem { item, save_intent } => { @@ -7182,9 +7182,9 @@ pub fn open_paths( .collect::<Vec<_>>(); cx.update(|cx| { - for window in local_workspace_windows(&cx) { - if let Ok(workspace) = window.read(&cx) { - let m = workspace.project.read(&cx).visibility_for_paths( + for window in local_workspace_windows(cx) { + if let Ok(workspace) = window.read(cx) { + let m = workspace.project.read(cx).visibility_for_paths( &abs_paths, &all_metadatas, open_options.open_new_workspace == None, @@ -7341,7 +7341,7 @@ pub fn open_ssh_project_with_new_connection( ) -> Task<Result<()>> { cx.spawn(async move |cx| { let (serialized_ssh_project, workspace_id, serialized_workspace) = - serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?; + serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?; let session = match cx .update(|cx| { @@ -7395,7 +7395,7 @@ pub fn open_ssh_project_with_existing_connection( ) -> Task<Result<()>> { cx.spawn(async move |cx| { let (serialized_ssh_project, workspace_id, serialized_workspace) = - serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?; + serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?; open_ssh_project_inner( project, diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index b5a0f71e81..f110726afd 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3199,7 +3199,7 @@ impl BackgroundScannerState { } async fn is_git_dir(path: &Path, fs: &dyn Fs) -> bool { - if path.file_name() == Some(&*DOT_GIT) { + if path.file_name() == Some(*DOT_GIT) { return true; } @@ -3575,7 +3575,7 @@ impl<'a> cursor_location: &Dimensions<TraversalProgress<'a>, GitSummary>, _: &(), ) -> Ordering { - self.cmp_path(&cursor_location.0.max_path) + self.cmp_path(cursor_location.0.max_path) } } @@ -5364,13 +5364,13 @@ impl PathTarget<'_> { impl<'a, S: Summary> SeekTarget<'a, PathSummary<S>, PathProgress<'a>> for PathTarget<'_> { fn cmp(&self, cursor_location: &PathProgress<'a>, _: &S::Context) -> Ordering { - self.cmp_path(&cursor_location.max_path) + self.cmp_path(cursor_location.max_path) } } impl<'a, S: Summary> SeekTarget<'a, PathSummary<S>, TraversalProgress<'a>> for PathTarget<'_> { fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: &S::Context) -> Ordering { - self.cmp_path(&cursor_location.max_path) + self.cmp_path(cursor_location.max_path) } } @@ -5396,7 +5396,7 @@ impl<'a> TraversalTarget<'a> { fn cmp_progress(&self, progress: &TraversalProgress) -> Ordering { match self { - TraversalTarget::Path(path) => path.cmp_path(&progress.max_path), + TraversalTarget::Path(path) => path.cmp_path(progress.max_path), TraversalTarget::Count { count, include_files, @@ -5551,7 +5551,7 @@ fn discover_git_paths(dot_git_abs_path: &Arc<Path>, fs: &dyn Fs) -> (Arc<Path>, let mut repository_dir_abs_path = dot_git_abs_path.clone(); let mut common_dir_abs_path = dot_git_abs_path.clone(); - if let Some(path) = smol::block_on(fs.load(&dot_git_abs_path)) + if let Some(path) = smol::block_on(fs.load(dot_git_abs_path)) .ok() .as_ref() .and_then(|contents| parse_gitfile(contents).log_err()) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 2a82f81b5b..a66b30c44a 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -371,9 +371,9 @@ pub fn main() { { cx.spawn({ let app_state = app_state.clone(); - async move |mut cx| { - if let Err(e) = restore_or_create_workspace(app_state, &mut cx).await { - fail_to_open_window_async(e, &mut cx) + async move |cx| { + if let Err(e) = restore_or_create_workspace(app_state, cx).await { + fail_to_open_window_async(e, cx) } } }) @@ -690,7 +690,7 @@ pub fn main() { cx.spawn({ let client = app_state.client.clone(); - async move |cx| authenticate(client, &cx).await + async move |cx| authenticate(client, cx).await }) .detach_and_log_err(cx); @@ -722,9 +722,9 @@ pub fn main() { None => { cx.spawn({ let app_state = app_state.clone(); - async move |mut cx| { - if let Err(e) = restore_or_create_workspace(app_state, &mut cx).await { - fail_to_open_window_async(e, &mut cx) + async move |cx| { + if let Err(e) = restore_or_create_workspace(app_state, cx).await { + fail_to_open_window_async(e, cx) } } }) @@ -795,14 +795,14 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut } if let Some(connection_options) = request.ssh_connection { - cx.spawn(async move |mut cx| { + cx.spawn(async move |cx| { let paths: Vec<PathBuf> = request.open_paths.into_iter().map(PathBuf::from).collect(); open_ssh_project( connection_options, paths, app_state, workspace::OpenOptions::default(), - &mut cx, + cx, ) .await }) @@ -813,7 +813,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut let mut task = None; if !request.open_paths.is_empty() || !request.diff_paths.is_empty() { let app_state = app_state.clone(); - task = Some(cx.spawn(async move |mut cx| { + task = Some(cx.spawn(async move |cx| { let paths_with_position = derive_paths_with_position(app_state.fs.as_ref(), request.open_paths).await; let (_window, results) = open_paths_with_positions( @@ -821,7 +821,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut &request.diff_paths, app_state, workspace::OpenOptions::default(), - &mut cx, + cx, ) .await?; for result in results.into_iter().flatten() { @@ -834,7 +834,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut } if !request.open_channel_notes.is_empty() || request.join_channel.is_some() { - cx.spawn(async move |mut cx| { + cx.spawn(async move |cx| { let result = maybe!(async { if let Some(task) = task { task.await?; @@ -842,7 +842,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut let client = app_state.client.clone(); // we continue even if authentication fails as join_channel/ open channel notes will // show a visible error message. - authenticate(client, &cx).await.log_err(); + authenticate(client, cx).await.log_err(); if let Some(channel_id) = request.join_channel { cx.update(|cx| { @@ -878,14 +878,14 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut }) .await; if let Err(err) = result { - fail_to_open_window_async(err, &mut cx); + fail_to_open_window_async(err, cx); } }) .detach() } else if let Some(task) = task { - cx.spawn(async move |mut cx| { + cx.spawn(async move |cx| { if let Err(err) = task.await { - fail_to_open_window_async(err, &mut cx); + fail_to_open_window_async(err, cx); } }) .detach(); diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index 0a54572f6b..f2e65b4f53 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -536,7 +536,7 @@ async fn upload_previous_panics( }); if let Some(panic) = panic - && upload_panic(&http, &panic_report_url, panic, &mut most_recent_panic).await? + && upload_panic(&http, panic_report_url, panic, &mut most_recent_panic).await? { // We've done what we can, delete the file fs::remove_file(child_path) @@ -566,7 +566,7 @@ pub async fn upload_previous_minidumps(http: Arc<HttpClientWithUrl>) -> anyhow:: if let Ok(metadata) = serde_json::from_slice(&smol::fs::read(&json_path).await?) { if upload_minidump( http.clone(), - &minidump_endpoint, + minidump_endpoint, smol::fs::read(&child_path) .await .context("Failed to read minidump")?, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 6d5aecba70..535cb12e1a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -327,7 +327,7 @@ pub fn initialize_workspace( cx.subscribe_in(&workspace_handle, window, { move |workspace, _, event, window, cx| match event { workspace::Event::PaneAdded(pane) => { - initialize_pane(workspace, &pane, window, cx); + initialize_pane(workspace, pane, window, cx); } workspace::Event::OpenBundledFile { text, @@ -796,7 +796,7 @@ fn register_actions( .register_action(install_cli) .register_action(|_, _: &install_cli::RegisterZedScheme, window, cx| { cx.spawn_in(window, async move |workspace, cx| { - install_cli::register_zed_scheme(&cx).await?; + install_cli::register_zed_scheme(cx).await?; workspace.update_in(cx, |workspace, _, cx| { struct RegisterZedScheme; diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index 4609ecce9b..915c40030a 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/crates/zed/src/zed/component_preview.rs @@ -650,7 +650,7 @@ impl ComponentPreview { _window: &mut Window, _cx: &mut Context<Self>, ) -> impl IntoElement { - let component = self.component_map.get(&component_id); + let component = self.component_map.get(component_id); if let Some(component) = component { v_flex() diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index 5b0826413b..587786fe8f 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -147,7 +147,7 @@ fn assign_edit_prediction_providers( assign_edit_prediction_provider( editor, provider, - &client, + client, user_store.clone(), window, cx, @@ -248,7 +248,7 @@ fn assign_edit_prediction_provider( if let Some(buffer) = &singleton_buffer { if buffer.read(cx).file().is_some() { zeta.update(cx, |zeta, cx| { - zeta.register_buffer(&buffer, cx); + zeta.register_buffer(buffer, cx); }); } } diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 82d3795e94..f282860e2c 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -432,13 +432,13 @@ async fn open_workspaces( .connection_options_for(ssh.host, ssh.port, ssh.user) }); if let Ok(connection_options) = connection_options { - cx.spawn(async move |mut cx| { + cx.spawn(async move |cx| { open_ssh_project( connection_options, ssh.paths.into_iter().map(PathBuf::from).collect(), app_state, OpenOptions::default(), - &mut cx, + cx, ) .await .log_err(); diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 2b7c38f997..d65053c05f 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -182,7 +182,7 @@ impl Render for QuickActionBar { let code_action_element = if is_deployed { editor.update(cx, |editor, cx| { if let Some(style) = editor.style() { - editor.render_context_menu(&style, MAX_CODE_ACTION_MENU_LINES, window, cx) + editor.render_context_menu(style, MAX_CODE_ACTION_MENU_LINES, window, cx) } else { None } diff --git a/crates/zeta/src/license_detection.rs b/crates/zeta/src/license_detection.rs index fa1eabf524..3dd025c1e1 100644 --- a/crates/zeta/src/license_detection.rs +++ b/crates/zeta/src/license_detection.rs @@ -198,7 +198,7 @@ mod tests { #[test] fn test_mit_positive_detection() { - assert!(is_license_eligible_for_data_collection(&MIT_LICENSE)); + assert!(is_license_eligible_for_data_collection(MIT_LICENSE)); } #[test] diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 1a6a8c2934..956e416fe9 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -505,7 +505,7 @@ impl Zeta { input_events, input_excerpt, buffer_snapshotted_at, - &cx, + cx, ) .await; @@ -981,7 +981,7 @@ and then another old_text, new_text, editable_range.start, - &snapshot, + snapshot, )) } @@ -991,7 +991,7 @@ and then another offset: usize, snapshot: &BufferSnapshot, ) -> Vec<(Range<Anchor>, String)> { - text_diff(&old_text, &new_text) + text_diff(&old_text, new_text) .into_iter() .map(|(mut old_range, new_text)| { old_range.start += offset; @@ -1182,7 +1182,7 @@ pub fn gather_context( .filter_map(|(language_server_id, diagnostic_group)| { let language_server = local_lsp_store.running_language_server_for_id(language_server_id)?; - let diagnostic_group = diagnostic_group.resolve::<usize>(&snapshot); + let diagnostic_group = diagnostic_group.resolve::<usize>(snapshot); let language_server_name = language_server.name().to_string(); let serialized = serde_json::to_value(diagnostic_group).unwrap(); Some((language_server_name, serialized)) @@ -1313,10 +1313,10 @@ impl CurrentEditPrediction { return true; } - let Some(old_edits) = old_completion.completion.interpolate(&snapshot) else { + let Some(old_edits) = old_completion.completion.interpolate(snapshot) else { return true; }; - let Some(new_edits) = self.completion.interpolate(&snapshot) else { + let Some(new_edits) = self.completion.interpolate(snapshot) else { return false; }; @@ -1664,7 +1664,7 @@ impl edit_prediction::EditPredictionProvider for ZetaEditPredictionProvider { if let Some(old_completion) = this.current_completion.as_ref() { let snapshot = buffer.read(cx).snapshot(); - if new_completion.should_replace_completion(&old_completion, &snapshot) { + if new_completion.should_replace_completion(old_completion, &snapshot) { this.zeta.update(cx, |zeta, cx| { zeta.completion_shown(&new_completion.completion, cx); }); diff --git a/crates/zeta_cli/src/main.rs b/crates/zeta_cli/src/main.rs index d78035bc9d..ba854b8732 100644 --- a/crates/zeta_cli/src/main.rs +++ b/crates/zeta_cli/src/main.rs @@ -131,7 +131,7 @@ async fn get_context( let (project, _lsp_open_handle, buffer) = if use_language_server { let (project, lsp_open_handle, buffer) = - open_buffer_with_language_server(&worktree_path, &cursor.path, &app_state, cx).await?; + open_buffer_with_language_server(&worktree_path, &cursor.path, app_state, cx).await?; (Some(project), Some(lsp_open_handle), buffer) } else { let abs_path = worktree_path.join(&cursor.path); @@ -260,7 +260,7 @@ pub fn wait_for_lang_server( .update(cx, |buffer, cx| { lsp_store.update(cx, |lsp_store, cx| { lsp_store - .language_servers_for_local_buffer(&buffer, cx) + .language_servers_for_local_buffer(buffer, cx) .next() .is_some() }) @@ -291,7 +291,7 @@ pub fn wait_for_lang_server( _ => {} } }), - cx.subscribe(&project, { + cx.subscribe(project, { let buffer = buffer.clone(); move |project, event, cx| match event { project::Event::LanguageServerAdded(_, _, _) => { diff --git a/crates/zlog/src/filter.rs b/crates/zlog/src/filter.rs index 56350d34c3..cf1604bd9f 100644 --- a/crates/zlog/src/filter.rs +++ b/crates/zlog/src/filter.rs @@ -82,7 +82,7 @@ pub fn is_scope_enabled(scope: &Scope, module_path: Option<&str>, level: log::Le // if no scopes are enabled, return false because it's not <= LEVEL_ENABLED_MAX_STATIC return is_enabled_by_default; } - let enabled_status = map.is_enabled(&scope, module_path, level); + let enabled_status = map.is_enabled(scope, module_path, level); return match enabled_status { EnabledStatus::NotConfigured => is_enabled_by_default, EnabledStatus::Enabled => true, From bb640c6a1c8b77031b8aabf1d5100945bf1d00ff Mon Sep 17 00:00:00 2001 From: Gregor <mail@watwa.re> Date: Tue, 19 Aug 2025 00:01:46 +0200 Subject: [PATCH 453/693] Add multi selection support to UnwrapSyntaxNode (#35991) Closes #35932 Closes #35933 I only intended to fix multi select in this, I accidentally drive-by fixed the VIM issue as well. `replace_text_in_range` which I was using before has two, to me unexpected, side-effects: - it no-ops when input is disabled, which is the case in VIM's Insert/Visual modes - it takes the current selection into account, and does not just operate on the given range (which I erroneously assumed before) Now the code is using `buffer.edit` instead, which seems more lower level, and does not have those side-effects. I was enthused to see that it accepts a vec of edits, so I didn't have to calculate offsets for following edits... until I also wanted to set selections, where I do need to do it by hand. I'm still wondering if there is a simpler way to do it, but for now it at least passes my muster Release Notes: - Added multiple selection support to UnwrapSyntaxNode action - Fixed UnwrapSyntaxNode not working in VIM Insert/Visual modes --- crates/editor/src/editor.rs | 85 +++++++++++++++++-------------- crates/editor/src/editor_tests.rs | 17 ++----- 2 files changed, 50 insertions(+), 52 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6edd4e9d8c..365cd1ea5a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -14834,15 +14834,18 @@ impl Editor { self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let buffer = self.buffer.read(cx).snapshot(cx); - let old_selections: Box<[_]> = self.selections.all::<usize>(cx).into(); + let selections = self + .selections + .all::<usize>(cx) + .into_iter() + // subtracting the offset requires sorting + .sorted_by_key(|i| i.start); - let edits = old_selections - .iter() - // only consider the first selection for now - .take(1) - .map(|selection| { + let full_edits = selections + .into_iter() + .filter_map(|selection| { // Only requires two branches once if-let-chains stabilize (#53667) - let selection_range = if !selection.is_empty() { + let child = if !selection.is_empty() { selection.range() } else if let Some((_, ancestor_range)) = buffer.syntax_ancestor(selection.start..selection.end) @@ -14855,48 +14858,52 @@ impl Editor { selection.range() }; - let mut new_range = selection_range.clone(); - while let Some((_, ancestor_range)) = buffer.syntax_ancestor(new_range.clone()) { - new_range = match ancestor_range { + let mut parent = child.clone(); + while let Some((_, ancestor_range)) = buffer.syntax_ancestor(parent.clone()) { + parent = match ancestor_range { MultiOrSingleBufferOffsetRange::Single(range) => range, MultiOrSingleBufferOffsetRange::Multi(range) => range, }; - if new_range.start < selection_range.start - || new_range.end > selection_range.end - { + if parent.start < child.start || parent.end > child.end { break; } } - (selection, selection_range, new_range) + if parent == child { + return None; + } + let text = buffer.text_for_range(child.clone()).collect::<String>(); + Some((selection.id, parent, text)) }) .collect::<Vec<_>>(); - self.transact(window, cx, |editor, window, cx| { - for (_, child, parent) in &edits { - let text = buffer.text_for_range(child.clone()).collect::<String>(); - editor.replace_text_in_range(Some(parent.clone()), &text, window, cx); - } - - editor.change_selections( - SelectionEffects::scroll(Autoscroll::fit()), - window, - cx, - |s| { - s.select( - edits - .iter() - .map(|(s, old, new)| Selection { - id: s.id, - start: new.start, - end: new.start + old.len(), - goal: SelectionGoal::None, - reversed: s.reversed, - }) - .collect(), - ); - }, - ); + self.transact(window, cx, |this, window, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.edit( + full_edits + .iter() + .map(|(_, p, t)| (p.clone(), t.clone())) + .collect::<Vec<_>>(), + None, + cx, + ); + }); + this.change_selections(Default::default(), window, cx, |s| { + let mut offset = 0; + let mut selections = vec![]; + for (id, parent, text) in full_edits { + let start = parent.start - offset; + offset += parent.len() - text.len(); + selections.push(Selection { + id: id, + start, + end: start + text.len(), + reversed: false, + goal: Default::default(), + }); + } + s.select(selections); + }); }); } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 189bdd1bf7..685cc47cdb 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -8015,7 +8015,7 @@ async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppConte } #[gpui::test] -async fn test_unwrap_syntax_node(cx: &mut gpui::TestAppContext) { +async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; @@ -8029,21 +8029,12 @@ async fn test_unwrap_syntax_node(cx: &mut gpui::TestAppContext) { buffer.set_language(Some(language), cx); }); - cx.set_state( - &r#" - use mod1::mod2::{«mod3ˇ», mod4}; - "# - .unindent(), - ); + cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# }); cx.update_editor(|editor, window, cx| { editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx); }); - cx.assert_editor_state( - &r#" - use mod1::mod2::«mod3ˇ»; - "# - .unindent(), - ); + + cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# }); } #[gpui::test] From e7b7c206a0233c19c982cf0ef95c87d98a2fd8a9 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Tue, 19 Aug 2025 01:03:16 +0300 Subject: [PATCH 454/693] terminal: Fix python venv path when spawning tasks on windows (#35909) I haven't found any issues related to this, but it seems like currently the wrong directory is added to the path when spawning tasks on windows with a python virtual environment. I also deduplicated the logic at a few places. The same constant exists in the languages crate, but we don't want to pull an additional dependency just for this. -1 papercut Release Notes: - Fix python venv path when spawning tasks on windows --- crates/project/src/terminals.rs | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 5ea7b87fbe..f5d08990b5 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -23,6 +23,13 @@ use util::{ paths::{PathStyle, RemotePathBuf}, }; +/// The directory inside a Python virtual environment that contains executables +const PYTHON_VENV_BIN_DIR: &str = if cfg!(target_os = "windows") { + "Scripts" +} else { + "bin" +}; + pub struct Terminals { pub(crate) local_handles: Vec<WeakEntity<terminal::Terminal>>, } @@ -368,7 +375,8 @@ impl Project { } None => { if let Some(venv_path) = &python_venv_directory { - add_environment_path(&mut env, &venv_path.join("bin")).log_err(); + add_environment_path(&mut env, &venv_path.join(PYTHON_VENV_BIN_DIR)) + .log_err(); } let shell = if let Some(program) = spawn_task.command { @@ -478,16 +486,12 @@ impl Project { venv_settings: &terminal_settings::VenvSettingsContent, cx: &App, ) -> Option<PathBuf> { - let bin_dir_name = match std::env::consts::OS { - "windows" => "Scripts", - _ => "bin", - }; venv_settings .directories .iter() .map(|name| abs_path.join(name)) .find(|venv_path| { - let bin_path = venv_path.join(bin_dir_name); + let bin_path = venv_path.join(PYTHON_VENV_BIN_DIR); self.find_worktree(&bin_path, cx) .and_then(|(worktree, relative_path)| { worktree.read(cx).entry_for_path(&relative_path) @@ -504,16 +508,12 @@ impl Project { ) -> Option<PathBuf> { let (worktree, _) = self.find_worktree(abs_path, cx)?; let fs = worktree.read(cx).as_local()?.fs(); - let bin_dir_name = match std::env::consts::OS { - "windows" => "Scripts", - _ => "bin", - }; venv_settings .directories .iter() .map(|name| abs_path.join(name)) .find(|venv_path| { - let bin_path = venv_path.join(bin_dir_name); + let bin_path = venv_path.join(PYTHON_VENV_BIN_DIR); // One-time synchronous check is acceptable for terminal/task initialization smol::block_on(fs.metadata(&bin_path)) .ok() @@ -589,10 +589,7 @@ impl Project { if venv_settings.venv_name.is_empty() { let path = venv_base_directory - .join(match std::env::consts::OS { - "windows" => "Scripts", - _ => "bin", - }) + .join(PYTHON_VENV_BIN_DIR) .join(activate_script_name) .to_string_lossy() .to_string(); From 3648dbe939172bba070be8b29e64cb5b7d749a0c Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Mon, 18 Aug 2025 18:09:30 -0400 Subject: [PATCH 455/693] terminal: Temporarily disable `test_basic_terminal` test (#36447) This PR temporarily disables the `test_basic_terminal` test, as it flakes on macOS. Release Notes: - N/A --- crates/terminal/src/terminal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 2f3b7aa28d..3dfde8a9af 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -2160,7 +2160,7 @@ mod tests { use gpui::{Pixels, Point, TestAppContext, bounds, point, size}; use rand::{Rng, distributions::Alphanumeric, rngs::ThreadRng, thread_rng}; - #[cfg_attr(windows, ignore = "TODO: fix on windows")] + #[ignore = "Test is flaky on macOS, and doesn't run on Windows"] #[gpui::test] async fn test_basic_terminal(cx: &mut TestAppContext) { cx.executor().allow_parking(); From 567ceffd429d8711a0ef6674bd01f22dfc0e98ff Mon Sep 17 00:00:00 2001 From: Kirill Bulatov <kirill@zed.dev> Date: Tue, 19 Aug 2025 01:54:37 +0300 Subject: [PATCH 456/693] Remove an unused struct (#36448) Release Notes: - N/A --- crates/workspace/src/workspace.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8ec61b6f10..babf2ac1d5 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -256,11 +256,6 @@ actions!( ] ); -#[derive(Clone, PartialEq)] -pub struct OpenPaths { - pub paths: Vec<PathBuf>, -} - /// Activates a specific pane by its index. #[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)] #[action(namespace = workspace)] @@ -6823,14 +6818,6 @@ impl WorkspaceHandle for Entity<Workspace> { } } -impl std::fmt::Debug for OpenPaths { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("OpenPaths") - .field("paths", &self.paths) - .finish() - } -} - pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> { DB.last_workspace().await.log_err().flatten() } From 33fbe53d48291c6c622e8d3b4bbf4d0210d41025 Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Mon, 18 Aug 2025 19:16:28 -0400 Subject: [PATCH 457/693] client: Make `Client::sign_in_with_optional_connect` a no-op when already connected to Collab (#36449) This PR makes it so `Client::sign_in_with_optional_connect` does nothing when the user is already connected to Collab. This fixes the issue where clicking on a channel link would temporarily disconnect you from Collab. Release Notes: - N/A --- crates/client/src/client.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 91bdf001d8..66d5fd89b1 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -973,6 +973,11 @@ impl Client { try_provider: bool, cx: &AsyncApp, ) -> Result<()> { + // Don't try to sign in again if we're already connected to Collab, as it will temporarily disconnect us. + if self.status().borrow().is_connected() { + return Ok(()); + } + let (is_staff_tx, is_staff_rx) = oneshot::channel::<bool>(); let mut is_staff_tx = Some(is_staff_tx); cx.update(|cx| { From b578031120b7ab294e86877656d54bd95157683c Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Mon, 18 Aug 2025 20:27:08 -0300 Subject: [PATCH 458/693] claude: Respect always allow setting (#36450) Claude will now respect the `agent.always_allow_tool_actions` setting and will set it when "Always Allow" is clicked. Release Notes: - N/A --- Cargo.lock | 1 + crates/agent_servers/Cargo.toml | 1 + crates/agent_servers/src/claude.rs | 3 +- crates/agent_servers/src/claude/mcp_server.rs | 70 ++++++++++++++++--- 4 files changed, 64 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3bc2b63843..6c05839ef3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -258,6 +258,7 @@ version = "0.1.0" dependencies = [ "acp_thread", "agent-client-protocol", + "agent_settings", "agentic-coding-protocol", "anyhow", "collections", diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index f894bb15bf..886f650470 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -19,6 +19,7 @@ doctest = false [dependencies] acp_thread.workspace = true agent-client-protocol.workspace = true +agent_settings.workspace = true agentic-coding-protocol.workspace = true anyhow.workspace = true collections.workspace = true diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 354bda494d..9b273cb091 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -111,7 +111,8 @@ impl AgentConnection for ClaudeAgentConnection { })?; let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid()); - let permission_mcp_server = ClaudeZedMcpServer::new(thread_rx.clone(), cx).await?; + let fs = project.read_with(cx, |project, _cx| project.fs().clone())?; + let permission_mcp_server = ClaudeZedMcpServer::new(thread_rx.clone(), fs, cx).await?; let mut mcp_servers = HashMap::default(); mcp_servers.insert( diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/claude/mcp_server.rs index 22cb2f8f8d..38587574db 100644 --- a/crates/agent_servers/src/claude/mcp_server.rs +++ b/crates/agent_servers/src/claude/mcp_server.rs @@ -1,8 +1,10 @@ use std::path::PathBuf; +use std::sync::Arc; use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams}; use acp_thread::AcpThread; use agent_client_protocol as acp; +use agent_settings::AgentSettings; use anyhow::{Context, Result}; use collections::HashMap; use context_server::listener::{McpServerTool, ToolResponse}; @@ -11,8 +13,11 @@ use context_server::types::{ ToolAnnotations, ToolResponseContent, ToolsCapabilities, requests, }; use gpui::{App, AsyncApp, Task, WeakEntity}; +use project::Fs; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use settings::{Settings as _, update_settings_file}; +use util::debug_panic; pub struct ClaudeZedMcpServer { server: context_server::listener::McpServer, @@ -23,6 +28,7 @@ pub const SERVER_NAME: &str = "zed"; impl ClaudeZedMcpServer { pub async fn new( thread_rx: watch::Receiver<WeakEntity<AcpThread>>, + fs: Arc<dyn Fs>, cx: &AsyncApp, ) -> Result<Self> { let mut mcp_server = context_server::listener::McpServer::new(cx).await?; @@ -30,6 +36,7 @@ impl ClaudeZedMcpServer { mcp_server.add_tool(PermissionTool { thread_rx: thread_rx.clone(), + fs: fs.clone(), }); mcp_server.add_tool(ReadTool { thread_rx: thread_rx.clone(), @@ -102,6 +109,7 @@ pub struct McpServerConfig { #[derive(Clone)] pub struct PermissionTool { + fs: Arc<dyn Fs>, thread_rx: watch::Receiver<WeakEntity<AcpThread>>, } @@ -141,6 +149,24 @@ impl McpServerTool for PermissionTool { input: Self::Input, cx: &mut AsyncApp, ) -> Result<ToolResponse<Self::Output>> { + if agent_settings::AgentSettings::try_read_global(cx, |settings| { + settings.always_allow_tool_actions + }) + .unwrap_or(false) + { + let response = PermissionToolResponse { + behavior: PermissionToolBehavior::Allow, + updated_input: input.input, + }; + + return Ok(ToolResponse { + content: vec![ToolResponseContent::Text { + text: serde_json::to_string(&response)?, + }], + structured_content: (), + }); + } + let mut thread_rx = self.thread_rx.clone(); let Some(thread) = thread_rx.recv().await?.upgrade() else { anyhow::bail!("Thread closed"); @@ -148,8 +174,10 @@ impl McpServerTool for PermissionTool { let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone()); let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into()); - let allow_option_id = acp::PermissionOptionId("allow".into()); - let reject_option_id = acp::PermissionOptionId("reject".into()); + + const ALWAYS_ALLOW: &'static str = "always_allow"; + const ALLOW: &'static str = "allow"; + const REJECT: &'static str = "reject"; let chosen_option = thread .update(cx, |thread, cx| { @@ -157,12 +185,17 @@ impl McpServerTool for PermissionTool { claude_tool.as_acp(tool_call_id).into(), vec![ acp::PermissionOption { - id: allow_option_id.clone(), + id: acp::PermissionOptionId(ALWAYS_ALLOW.into()), + name: "Always Allow".into(), + kind: acp::PermissionOptionKind::AllowAlways, + }, + acp::PermissionOption { + id: acp::PermissionOptionId(ALLOW.into()), name: "Allow".into(), kind: acp::PermissionOptionKind::AllowOnce, }, acp::PermissionOption { - id: reject_option_id.clone(), + id: acp::PermissionOptionId(REJECT.into()), name: "Reject".into(), kind: acp::PermissionOptionKind::RejectOnce, }, @@ -172,16 +205,33 @@ impl McpServerTool for PermissionTool { })?? .await?; - let response = if chosen_option == allow_option_id { - PermissionToolResponse { + let response = match chosen_option.0.as_ref() { + ALWAYS_ALLOW => { + cx.update(|cx| { + update_settings_file::<AgentSettings>(self.fs.clone(), cx, |settings, _| { + settings.set_always_allow_tool_actions(true); + }); + })?; + + PermissionToolResponse { + behavior: PermissionToolBehavior::Allow, + updated_input: input.input, + } + } + ALLOW => PermissionToolResponse { behavior: PermissionToolBehavior::Allow, updated_input: input.input, - } - } else { - debug_assert_eq!(chosen_option, reject_option_id); - PermissionToolResponse { + }, + REJECT => PermissionToolResponse { behavior: PermissionToolBehavior::Deny, updated_input: input.input, + }, + opt => { + debug_panic!("Unexpected option: {}", opt); + PermissionToolResponse { + behavior: PermissionToolBehavior::Deny, + updated_input: input.input, + } } }; From b7edc89a87e2589fbe69c13a53aba57260371a5f Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:44:07 -0300 Subject: [PATCH 459/693] agent: Improve error and warnings display (#36425) This PR refactors the callout component and improves how we display errors and warnings in the agent panel, along with improvements for specific cases (e.g., you have `zed.dev` as your LLM provider and is signed out). Still a work in progress, though, wrapping up some details. Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 145 ++++--- crates/agent_ui/src/active_thread.rs | 2 +- .../add_llm_provider_modal.rs | 2 +- crates/agent_ui/src/agent_panel.rs | 357 +++++++++--------- crates/agent_ui/src/message_editor.rs | 50 +-- .../agent_ui/src/ui/preview/usage_callouts.rs | 14 +- .../ai_onboarding/src/young_account_banner.rs | 2 +- crates/language_model/src/registry.rs | 2 +- crates/settings_ui/src/keybindings.rs | 14 +- crates/ui/src/components/banner.rs | 9 - crates/ui/src/components/callout.rs | 217 +++++++---- crates/ui/src/prelude.rs | 4 +- crates/ui/src/styles.rs | 2 + crates/ui/src/styles/severity.rs | 10 + 14 files changed, 436 insertions(+), 394 deletions(-) create mode 100644 crates/ui/src/styles/severity.rs diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 4a8f9bf209..0d15e27e0c 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -3259,44 +3259,33 @@ impl AcpThreadView { } }; - Some( - div() - .border_t_1() - .border_color(cx.theme().colors().border) - .child(content), - ) + Some(div().child(content)) } fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout { - let icon = Icon::new(IconName::XCircle) - .size(IconSize::Small) - .color(Color::Error); - Callout::new() - .icon(icon) + .severity(Severity::Error) .title("Error") .description(error.clone()) - .secondary_action(self.create_copy_button(error.to_string())) - .primary_action(self.dismiss_error_button(cx)) - .bg_color(self.error_callout_bg(cx)) + .actions_slot(self.create_copy_button(error.to_string())) + .dismiss_action(self.dismiss_error_button(cx)) } fn render_payment_required_error(&self, cx: &mut Context<Self>) -> Callout { const ERROR_MESSAGE: &str = "You reached your free usage limit. Upgrade to Zed Pro for more prompts."; - let icon = Icon::new(IconName::XCircle) - .size(IconSize::Small) - .color(Color::Error); - Callout::new() - .icon(icon) + .severity(Severity::Error) .title("Free Usage Exceeded") .description(ERROR_MESSAGE) - .tertiary_action(self.upgrade_button(cx)) - .secondary_action(self.create_copy_button(ERROR_MESSAGE)) - .primary_action(self.dismiss_error_button(cx)) - .bg_color(self.error_callout_bg(cx)) + .actions_slot( + h_flex() + .gap_0p5() + .child(self.upgrade_button(cx)) + .child(self.create_copy_button(ERROR_MESSAGE)), + ) + .dismiss_action(self.dismiss_error_button(cx)) } fn render_model_request_limit_reached_error( @@ -3311,18 +3300,17 @@ impl AcpThreadView { } }; - let icon = Icon::new(IconName::XCircle) - .size(IconSize::Small) - .color(Color::Error); - Callout::new() - .icon(icon) + .severity(Severity::Error) .title("Model Prompt Limit Reached") .description(error_message) - .tertiary_action(self.upgrade_button(cx)) - .secondary_action(self.create_copy_button(error_message)) - .primary_action(self.dismiss_error_button(cx)) - .bg_color(self.error_callout_bg(cx)) + .actions_slot( + h_flex() + .gap_0p5() + .child(self.upgrade_button(cx)) + .child(self.create_copy_button(error_message)), + ) + .dismiss_action(self.dismiss_error_button(cx)) } fn render_tool_use_limit_reached_error( @@ -3338,52 +3326,59 @@ impl AcpThreadView { let focus_handle = self.focus_handle(cx); - let icon = Icon::new(IconName::Info) - .size(IconSize::Small) - .color(Color::Info); - Some( Callout::new() - .icon(icon) + .icon(IconName::Info) .title("Consecutive tool use limit reached.") - .when(supports_burn_mode, |this| { - this.secondary_action( - Button::new("continue-burn-mode", "Continue with Burn Mode") - .style(ButtonStyle::Filled) - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .layer(ElevationIndex::ModalSurface) - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in( - &ContinueWithBurnMode, - &focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(10.))), + .actions_slot( + h_flex() + .gap_0p5() + .when(supports_burn_mode, |this| { + this.child( + Button::new("continue-burn-mode", "Continue with Burn Mode") + .style(ButtonStyle::Filled) + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .layer(ElevationIndex::ModalSurface) + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in( + &ContinueWithBurnMode, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(10.))), + ) + .tooltip(Tooltip::text( + "Enable Burn Mode for unlimited tool use.", + )) + .on_click({ + cx.listener(move |this, _, _window, cx| { + thread.update(cx, |thread, _cx| { + thread.set_completion_mode(CompletionMode::Burn); + }); + this.resume_chat(cx); + }) + }), ) - .tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use.")) - .on_click({ - cx.listener(move |this, _, _window, cx| { - thread.update(cx, |thread, _cx| { - thread.set_completion_mode(CompletionMode::Burn); - }); + }) + .child( + Button::new("continue-conversation", "Continue") + .layer(ElevationIndex::ModalSurface) + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in( + &ContinueThread, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(10.))), + ) + .on_click(cx.listener(|this, _, _window, cx| { this.resume_chat(cx); - }) - }), - ) - }) - .primary_action( - Button::new("continue-conversation", "Continue") - .layer(ElevationIndex::ModalSurface) - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in(&ContinueThread, &focus_handle, window, cx) - .map(|kb| kb.size(rems_from_px(10.))), - ) - .on_click(cx.listener(|this, _, _window, cx| { - this.resume_chat(cx); - })), + })), + ), ), ) } @@ -3424,10 +3419,6 @@ impl AcpThreadView { } })) } - - fn error_callout_bg(&self, cx: &Context<Self>) -> Hsla { - cx.theme().status().error.opacity(0.08) - } } impl Focusable for AcpThreadView { diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 38be2b193c..d2f448635e 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -2597,7 +2597,7 @@ impl ActiveThread { .id(("message-container", ix)) .py_1() .px_2p5() - .child(Banner::new().severity(ui::Severity::Warning).child(message)) + .child(Banner::new().severity(Severity::Warning).child(message)) } fn render_message_thinking_segment( diff --git a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs index c68c9c2730..998641bf01 100644 --- a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs +++ b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs @@ -454,7 +454,7 @@ impl Render for AddLlmProviderModal { this.section( Section::new().child( Banner::new() - .severity(ui::Severity::Warning) + .severity(Severity::Warning) .child(div().text_xs().child(error)), ), ) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index e1174a4191..cb354222b6 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -48,9 +48,8 @@ use feature_flags::{self, FeatureFlagAppExt}; use fs::Fs; use gpui::{ Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem, - Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, Hsla, - KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, - pulsating_between, + Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, + Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between, }; use language::LanguageRegistry; use language_model::{ @@ -2712,20 +2711,22 @@ impl AgentPanel { action_slot: Option<AnyElement>, cx: &mut Context<Self>, ) -> impl IntoElement { - h_flex() - .mt_2() - .pl_1p5() - .pb_1() - .w_full() - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .child( - Label::new(label.into()) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .children(action_slot) + div().pl_1().pr_1p5().child( + h_flex() + .mt_2() + .pl_1p5() + .pb_1() + .w_full() + .justify_between() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .child( + Label::new(label.into()) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .children(action_slot), + ) } fn render_thread_empty_state( @@ -2831,22 +2832,12 @@ impl AgentPanel { }), ), ) - }) - .when_some(configuration_error.as_ref(), |this, err| { - this.child(self.render_configuration_error( - err, - &focus_handle, - window, - cx, - )) }), ) }) .when(!recent_history.is_empty(), |parent| { - let focus_handle = focus_handle.clone(); parent .overflow_hidden() - .p_1p5() .justify_end() .gap_1() .child( @@ -2874,10 +2865,11 @@ impl AgentPanel { ), ) .child( - v_flex() - .gap_1() - .children(recent_history.into_iter().enumerate().map( - |(index, entry)| { + v_flex().p_1().pr_1p5().gap_1().children( + recent_history + .into_iter() + .enumerate() + .map(|(index, entry)| { // TODO: Add keyboard navigation. let is_hovered = self.hovered_recent_history_item == Some(index); @@ -2896,30 +2888,68 @@ impl AgentPanel { }, )) .into_any_element() - }, - )), + }), + ), ) - .when_some(configuration_error.as_ref(), |this, err| { - this.child(self.render_configuration_error(err, &focus_handle, window, cx)) - }) + }) + .when_some(configuration_error.as_ref(), |this, err| { + this.child(self.render_configuration_error(false, err, &focus_handle, window, cx)) }) } fn render_configuration_error( &self, + border_bottom: bool, configuration_error: &ConfigurationError, focus_handle: &FocusHandle, window: &mut Window, cx: &mut App, ) -> impl IntoElement { - match configuration_error { - ConfigurationError::ModelNotFound - | ConfigurationError::ProviderNotAuthenticated(_) - | ConfigurationError::NoProvider => Banner::new() - .severity(ui::Severity::Warning) - .child(Label::new(configuration_error.to_string())) - .action_slot( - Button::new("settings", "Configure Provider") + let zed_provider_configured = AgentSettings::get_global(cx) + .default_model + .as_ref() + .map_or(false, |selection| { + selection.provider.0.as_str() == "zed.dev" + }); + + let callout = if zed_provider_configured { + Callout::new() + .icon(IconName::Warning) + .severity(Severity::Warning) + .when(border_bottom, |this| { + this.border_position(ui::BorderPosition::Bottom) + }) + .title("Sign in to continue using Zed as your LLM provider.") + .actions_slot( + Button::new("sign_in", "Sign In") + .style(ButtonStyle::Tinted(ui::TintColor::Warning)) + .label_size(LabelSize::Small) + .on_click({ + let workspace = self.workspace.clone(); + move |_, _, cx| { + let Ok(client) = + workspace.update(cx, |workspace, _| workspace.client().clone()) + else { + return; + }; + + cx.spawn(async move |cx| { + client.sign_in_with_optional_connect(true, cx).await + }) + .detach_and_log_err(cx); + } + }), + ) + } else { + Callout::new() + .icon(IconName::Warning) + .severity(Severity::Warning) + .when(border_bottom, |this| { + this.border_position(ui::BorderPosition::Bottom) + }) + .title(configuration_error.to_string()) + .actions_slot( + Button::new("settings", "Configure") .style(ButtonStyle::Tinted(ui::TintColor::Warning)) .label_size(LabelSize::Small) .key_binding( @@ -2929,16 +2959,23 @@ impl AgentPanel { .on_click(|_event, window, cx| { window.dispatch_action(OpenSettings.boxed_clone(), cx) }), - ), + ) + }; + + match configuration_error { + ConfigurationError::ModelNotFound + | ConfigurationError::ProviderNotAuthenticated(_) + | ConfigurationError::NoProvider => callout.into_any_element(), ConfigurationError::ProviderPendingTermsAcceptance(provider) => { - Banner::new().severity(ui::Severity::Warning).child( - h_flex().w_full().children( + Banner::new() + .severity(Severity::Warning) + .child(h_flex().w_full().children( provider.render_accept_terms( LanguageModelProviderTosView::ThreadEmptyState, cx, ), - ), - ) + )) + .into_any_element() } } } @@ -2970,7 +3007,7 @@ impl AgentPanel { let focus_handle = self.focus_handle(cx); let banner = Banner::new() - .severity(ui::Severity::Info) + .severity(Severity::Info) .child(Label::new("Consecutive tool use limit reached.").size(LabelSize::Small)) .action_slot( h_flex() @@ -3081,10 +3118,6 @@ impl AgentPanel { })) } - fn error_callout_bg(&self, cx: &Context<Self>) -> Hsla { - cx.theme().status().error.opacity(0.08) - } - fn render_payment_required_error( &self, thread: &Entity<ActiveThread>, @@ -3093,23 +3126,18 @@ impl AgentPanel { const ERROR_MESSAGE: &str = "You reached your free usage limit. Upgrade to Zed Pro for more prompts."; - let icon = Icon::new(IconName::XCircle) - .size(IconSize::Small) - .color(Color::Error); - - div() - .border_t_1() - .border_color(cx.theme().colors().border) - .child( - Callout::new() - .icon(icon) - .title("Free Usage Exceeded") - .description(ERROR_MESSAGE) - .tertiary_action(self.upgrade_button(thread, cx)) - .secondary_action(self.create_copy_button(ERROR_MESSAGE)) - .primary_action(self.dismiss_error_button(thread, cx)) - .bg_color(self.error_callout_bg(cx)), + Callout::new() + .severity(Severity::Error) + .icon(IconName::XCircle) + .title("Free Usage Exceeded") + .description(ERROR_MESSAGE) + .actions_slot( + h_flex() + .gap_0p5() + .child(self.upgrade_button(thread, cx)) + .child(self.create_copy_button(ERROR_MESSAGE)), ) + .dismiss_action(self.dismiss_error_button(thread, cx)) .into_any_element() } @@ -3124,23 +3152,37 @@ impl AgentPanel { Plan::ZedProTrial | Plan::ZedFree => "Upgrade to Zed Pro for more prompts.", }; - let icon = Icon::new(IconName::XCircle) - .size(IconSize::Small) - .color(Color::Error); - - div() - .border_t_1() - .border_color(cx.theme().colors().border) - .child( - Callout::new() - .icon(icon) - .title("Model Prompt Limit Reached") - .description(error_message) - .tertiary_action(self.upgrade_button(thread, cx)) - .secondary_action(self.create_copy_button(error_message)) - .primary_action(self.dismiss_error_button(thread, cx)) - .bg_color(self.error_callout_bg(cx)), + Callout::new() + .severity(Severity::Error) + .title("Model Prompt Limit Reached") + .description(error_message) + .actions_slot( + h_flex() + .gap_0p5() + .child(self.upgrade_button(thread, cx)) + .child(self.create_copy_button(error_message)), ) + .dismiss_action(self.dismiss_error_button(thread, cx)) + .into_any_element() + } + + fn render_retry_button(&self, thread: &Entity<ActiveThread>) -> AnyElement { + Button::new("retry", "Retry") + .icon(IconName::RotateCw) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .label_size(LabelSize::Small) + .on_click({ + let thread = thread.clone(); + move |_, window, cx| { + thread.update(cx, |thread, cx| { + thread.clear_last_error(); + thread.thread().update(cx, |thread, cx| { + thread.retry_last_completion(Some(window.window_handle()), cx); + }); + }); + } + }) .into_any_element() } @@ -3153,40 +3195,18 @@ impl AgentPanel { ) -> AnyElement { let message_with_header = format!("{}\n{}", header, message); - let icon = Icon::new(IconName::XCircle) - .size(IconSize::Small) - .color(Color::Error); - - let retry_button = Button::new("retry", "Retry") - .icon(IconName::RotateCw) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .label_size(LabelSize::Small) - .on_click({ - let thread = thread.clone(); - move |_, window, cx| { - thread.update(cx, |thread, cx| { - thread.clear_last_error(); - thread.thread().update(cx, |thread, cx| { - thread.retry_last_completion(Some(window.window_handle()), cx); - }); - }); - } - }); - - div() - .border_t_1() - .border_color(cx.theme().colors().border) - .child( - Callout::new() - .icon(icon) - .title(header) - .description(message.clone()) - .primary_action(retry_button) - .secondary_action(self.dismiss_error_button(thread, cx)) - .tertiary_action(self.create_copy_button(message_with_header)) - .bg_color(self.error_callout_bg(cx)), + Callout::new() + .severity(Severity::Error) + .icon(IconName::XCircle) + .title(header) + .description(message.clone()) + .actions_slot( + h_flex() + .gap_0p5() + .child(self.render_retry_button(thread)) + .child(self.create_copy_button(message_with_header)), ) + .dismiss_action(self.dismiss_error_button(thread, cx)) .into_any_element() } @@ -3195,60 +3215,39 @@ impl AgentPanel { message: SharedString, can_enable_burn_mode: bool, thread: &Entity<ActiveThread>, - cx: &mut Context<Self>, ) -> AnyElement { - let icon = Icon::new(IconName::XCircle) - .size(IconSize::Small) - .color(Color::Error); - - let retry_button = Button::new("retry", "Retry") - .icon(IconName::RotateCw) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .label_size(LabelSize::Small) - .on_click({ - let thread = thread.clone(); - move |_, window, cx| { - thread.update(cx, |thread, cx| { - thread.clear_last_error(); - thread.thread().update(cx, |thread, cx| { - thread.retry_last_completion(Some(window.window_handle()), cx); - }); - }); - } - }); - - let mut callout = Callout::new() - .icon(icon) + Callout::new() + .severity(Severity::Error) .title("Error") .description(message.clone()) - .bg_color(self.error_callout_bg(cx)) - .primary_action(retry_button); - - if can_enable_burn_mode { - let burn_mode_button = Button::new("enable_burn_retry", "Enable Burn Mode and Retry") - .icon(IconName::ZedBurnMode) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .label_size(LabelSize::Small) - .on_click({ - let thread = thread.clone(); - move |_, window, cx| { - thread.update(cx, |thread, cx| { - thread.clear_last_error(); - thread.thread().update(cx, |thread, cx| { - thread.enable_burn_mode_and_retry(Some(window.window_handle()), cx); - }); - }); - } - }); - callout = callout.secondary_action(burn_mode_button); - } - - div() - .border_t_1() - .border_color(cx.theme().colors().border) - .child(callout) + .actions_slot( + h_flex() + .gap_0p5() + .when(can_enable_burn_mode, |this| { + this.child( + Button::new("enable_burn_retry", "Enable Burn Mode and Retry") + .icon(IconName::ZedBurnMode) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .label_size(LabelSize::Small) + .on_click({ + let thread = thread.clone(); + move |_, window, cx| { + thread.update(cx, |thread, cx| { + thread.clear_last_error(); + thread.thread().update(cx, |thread, cx| { + thread.enable_burn_mode_and_retry( + Some(window.window_handle()), + cx, + ); + }); + }); + } + }), + ) + }) + .child(self.render_retry_button(thread)), + ) .into_any_element() } @@ -3503,7 +3502,6 @@ impl Render for AgentPanel { message, can_enable_burn_mode, thread, - cx, ), }) .into_any(), @@ -3531,16 +3529,13 @@ impl Render for AgentPanel { if !self.should_render_onboarding(cx) && let Some(err) = configuration_error.as_ref() { - this.child( - div().bg(cx.theme().colors().editor_background).p_2().child( - self.render_configuration_error( - err, - &self.focus_handle(cx), - window, - cx, - ), - ), - ) + this.child(self.render_configuration_error( + true, + err, + &self.focus_handle(cx), + window, + cx, + )) } else { this } diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 181a0dd5d2..ddb51154f5 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -1323,14 +1323,10 @@ impl MessageEditor { token_usage_ratio: TokenUsageRatio, cx: &mut Context<Self>, ) -> Option<Div> { - let icon = if token_usage_ratio == TokenUsageRatio::Exceeded { - Icon::new(IconName::Close) - .color(Color::Error) - .size(IconSize::XSmall) + let (icon, severity) = if token_usage_ratio == TokenUsageRatio::Exceeded { + (IconName::Close, Severity::Error) } else { - Icon::new(IconName::Warning) - .color(Color::Warning) - .size(IconSize::XSmall) + (IconName::Warning, Severity::Warning) }; let title = if token_usage_ratio == TokenUsageRatio::Exceeded { @@ -1345,30 +1341,34 @@ impl MessageEditor { "To continue, start a new thread from a summary." }; - let mut callout = Callout::new() + let callout = Callout::new() .line_height(line_height) + .severity(severity) .icon(icon) .title(title) .description(description) - .primary_action( - Button::new("start-new-thread", "Start New Thread") - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - let from_thread_id = Some(this.thread.read(cx).id().clone()); - window.dispatch_action(Box::new(NewThread { from_thread_id }), cx); - })), + .actions_slot( + h_flex() + .gap_0p5() + .when(self.is_using_zed_provider(cx), |this| { + this.child( + IconButton::new("burn-mode-callout", IconName::ZedBurnMode) + .icon_size(IconSize::XSmall) + .on_click(cx.listener(|this, _event, window, cx| { + this.toggle_burn_mode(&ToggleBurnMode, window, cx); + })), + ) + }) + .child( + Button::new("start-new-thread", "Start New Thread") + .label_size(LabelSize::Small) + .on_click(cx.listener(|this, _, window, cx| { + let from_thread_id = Some(this.thread.read(cx).id().clone()); + window.dispatch_action(Box::new(NewThread { from_thread_id }), cx); + })), + ), ); - if self.is_using_zed_provider(cx) { - callout = callout.secondary_action( - IconButton::new("burn-mode-callout", IconName::ZedBurnMode) - .icon_size(IconSize::XSmall) - .on_click(cx.listener(|this, _event, window, cx| { - this.toggle_burn_mode(&ToggleBurnMode, window, cx); - })), - ); - } - Some( div() .border_t_1() diff --git a/crates/agent_ui/src/ui/preview/usage_callouts.rs b/crates/agent_ui/src/ui/preview/usage_callouts.rs index eef878a9d1..29b12ea627 100644 --- a/crates/agent_ui/src/ui/preview/usage_callouts.rs +++ b/crates/agent_ui/src/ui/preview/usage_callouts.rs @@ -80,14 +80,10 @@ impl RenderOnce for UsageCallout { } }; - let icon = if is_limit_reached { - Icon::new(IconName::Close) - .color(Color::Error) - .size(IconSize::XSmall) + let (icon, severity) = if is_limit_reached { + (IconName::Close, Severity::Error) } else { - Icon::new(IconName::Warning) - .color(Color::Warning) - .size(IconSize::XSmall) + (IconName::Warning, Severity::Warning) }; div() @@ -95,10 +91,12 @@ impl RenderOnce for UsageCallout { .border_color(cx.theme().colors().border) .child( Callout::new() + .icon(icon) + .severity(severity) .icon(icon) .title(title) .description(message) - .primary_action( + .actions_slot( Button::new("upgrade", button_text) .label_size(LabelSize::Small) .on_click(move |_, _, cx| { diff --git a/crates/ai_onboarding/src/young_account_banner.rs b/crates/ai_onboarding/src/young_account_banner.rs index 54f563e4aa..ed9a6b3b35 100644 --- a/crates/ai_onboarding/src/young_account_banner.rs +++ b/crates/ai_onboarding/src/young_account_banner.rs @@ -17,6 +17,6 @@ impl RenderOnce for YoungAccountBanner { div() .max_w_full() .my_1() - .child(Banner::new().severity(ui::Severity::Warning).child(label)) + .child(Banner::new().severity(Severity::Warning).child(label)) } } diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index 078b90a291..8f52f8c1c3 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -21,7 +21,7 @@ impl Global for GlobalLanguageModelRegistry {} pub enum ConfigurationError { #[error("Configure at least one LLM provider to start using the panel.")] NoProvider, - #[error("LLM Provider is not configured or does not support the configured model.")] + #[error("LLM provider is not configured or does not support the configured model.")] ModelNotFound, #[error("{} LLM provider is not configured.", .0.name().0)] ProviderNotAuthenticated(Arc<dyn LanguageModelProvider>), diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 58090d2060..757a0ca226 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -2021,21 +2021,21 @@ impl RenderOnce for SyntaxHighlightedText { #[derive(PartialEq)] struct InputError { - severity: ui::Severity, + severity: Severity, content: SharedString, } impl InputError { fn warning(message: impl Into<SharedString>) -> Self { Self { - severity: ui::Severity::Warning, + severity: Severity::Warning, content: message.into(), } } fn error(message: anyhow::Error) -> Self { Self { - severity: ui::Severity::Error, + severity: Severity::Error, content: message.to_string().into(), } } @@ -2162,9 +2162,11 @@ impl KeybindingEditorModal { } fn set_error(&mut self, error: InputError, cx: &mut Context<Self>) -> bool { - if self.error.as_ref().is_some_and(|old_error| { - old_error.severity == ui::Severity::Warning && *old_error == error - }) { + if self + .error + .as_ref() + .is_some_and(|old_error| old_error.severity == Severity::Warning && *old_error == error) + { false } else { self.error = Some(error); diff --git a/crates/ui/src/components/banner.rs b/crates/ui/src/components/banner.rs index d493e8a0d3..7458ad8eb0 100644 --- a/crates/ui/src/components/banner.rs +++ b/crates/ui/src/components/banner.rs @@ -1,15 +1,6 @@ use crate::prelude::*; use gpui::{AnyElement, IntoElement, ParentElement, Styled}; -/// Severity levels that determine the style of the banner. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Severity { - Info, - Success, - Warning, - Error, -} - /// Banners provide informative and brief messages without interrupting the user. /// This component offers four severity levels that can be used depending on the message. /// diff --git a/crates/ui/src/components/callout.rs b/crates/ui/src/components/callout.rs index abb03198ab..22ba0468cd 100644 --- a/crates/ui/src/components/callout.rs +++ b/crates/ui/src/components/callout.rs @@ -1,7 +1,13 @@ -use gpui::{AnyElement, Hsla}; +use gpui::AnyElement; use crate::prelude::*; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BorderPosition { + Top, + Bottom, +} + /// A callout component for displaying important information that requires user attention. /// /// # Usage Example @@ -10,42 +16,48 @@ use crate::prelude::*; /// use ui::{Callout}; /// /// Callout::new() -/// .icon(Icon::new(IconName::Warning).color(Color::Warning)) +/// .severity(Severity::Warning) +/// .icon(IconName::Warning) /// .title(Label::new("Be aware of your subscription!")) /// .description(Label::new("Your subscription is about to expire. Renew now!")) -/// .primary_action(Button::new("renew", "Renew Now")) -/// .secondary_action(Button::new("remind", "Remind Me Later")) +/// .actions_slot(Button::new("renew", "Renew Now")) /// ``` /// #[derive(IntoElement, RegisterComponent)] pub struct Callout { - icon: Option<Icon>, + severity: Severity, + icon: Option<IconName>, title: Option<SharedString>, description: Option<SharedString>, - primary_action: Option<AnyElement>, - secondary_action: Option<AnyElement>, - tertiary_action: Option<AnyElement>, + actions_slot: Option<AnyElement>, + dismiss_action: Option<AnyElement>, line_height: Option<Pixels>, - bg_color: Option<Hsla>, + border_position: BorderPosition, } impl Callout { /// Creates a new `Callout` component with default styling. pub fn new() -> Self { Self { + severity: Severity::Info, icon: None, title: None, description: None, - primary_action: None, - secondary_action: None, - tertiary_action: None, + actions_slot: None, + dismiss_action: None, line_height: None, - bg_color: None, + border_position: BorderPosition::Top, } } + /// Sets the severity of the callout. + pub fn severity(mut self, severity: Severity) -> Self { + self.severity = severity; + self + } + /// Sets the icon to display in the callout. - pub fn icon(mut self, icon: Icon) -> Self { + pub fn icon(mut self, icon: IconName) -> Self { self.icon = Some(icon); self } @@ -64,20 +76,14 @@ impl Callout { } /// Sets the primary call-to-action button. - pub fn primary_action(mut self, action: impl IntoElement) -> Self { - self.primary_action = Some(action.into_any_element()); - self - } - - /// Sets an optional secondary call-to-action button. - pub fn secondary_action(mut self, action: impl IntoElement) -> Self { - self.secondary_action = Some(action.into_any_element()); + pub fn actions_slot(mut self, action: impl IntoElement) -> Self { + self.actions_slot = Some(action.into_any_element()); self } /// Sets an optional tertiary call-to-action button. - pub fn tertiary_action(mut self, action: impl IntoElement) -> Self { - self.tertiary_action = Some(action.into_any_element()); + pub fn dismiss_action(mut self, action: impl IntoElement) -> Self { + self.dismiss_action = Some(action.into_any_element()); self } @@ -87,9 +93,9 @@ impl Callout { self } - /// Sets a custom background color for the callout content. - pub fn bg_color(mut self, color: Hsla) -> Self { - self.bg_color = Some(color); + /// Sets the border position in the callout. + pub fn border_position(mut self, border_position: BorderPosition) -> Self { + self.border_position = border_position; self } } @@ -97,21 +103,51 @@ impl Callout { impl RenderOnce for Callout { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let line_height = self.line_height.unwrap_or(window.line_height()); - let bg_color = self - .bg_color - .unwrap_or(cx.theme().colors().panel_background); - let has_actions = self.primary_action.is_some() - || self.secondary_action.is_some() - || self.tertiary_action.is_some(); + + let has_actions = self.actions_slot.is_some() || self.dismiss_action.is_some(); + + let (icon, icon_color, bg_color) = match self.severity { + Severity::Info => ( + IconName::Info, + Color::Muted, + cx.theme().colors().panel_background.opacity(0.), + ), + Severity::Success => ( + IconName::Check, + Color::Success, + cx.theme().status().success.opacity(0.1), + ), + Severity::Warning => ( + IconName::Warning, + Color::Warning, + cx.theme().status().warning_background.opacity(0.2), + ), + Severity::Error => ( + IconName::XCircle, + Color::Error, + cx.theme().status().error.opacity(0.08), + ), + }; h_flex() + .min_w_0() .p_2() .gap_2() .items_start() + .map(|this| match self.border_position { + BorderPosition::Top => this.border_t_1(), + BorderPosition::Bottom => this.border_b_1(), + }) + .border_color(cx.theme().colors().border) .bg(bg_color) .overflow_x_hidden() - .when_some(self.icon, |this, icon| { - this.child(h_flex().h(line_height).justify_center().child(icon)) + .when(self.icon.is_some(), |this| { + this.child( + h_flex() + .h(line_height) + .justify_center() + .child(Icon::new(icon).size(IconSize::Small).color(icon_color)), + ) }) .child( v_flex() @@ -119,10 +155,11 @@ impl RenderOnce for Callout { .w_full() .child( h_flex() - .h(line_height) + .min_h(line_height) .w_full() .gap_1() .justify_between() + .flex_wrap() .when_some(self.title, |this, title| { this.child(h_flex().child(Label::new(title).size(LabelSize::Small))) }) @@ -130,13 +167,10 @@ impl RenderOnce for Callout { this.child( h_flex() .gap_0p5() - .when_some(self.tertiary_action, |this, action| { + .when_some(self.actions_slot, |this, action| { this.child(action) }) - .when_some(self.secondary_action, |this, action| { - this.child(action) - }) - .when_some(self.primary_action, |this, action| { + .when_some(self.dismiss_action, |this, action| { this.child(action) }), ) @@ -168,84 +202,101 @@ impl Component for Callout { } fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> { - let callout_examples = vec![ + let single_action = || Button::new("got-it", "Got it").label_size(LabelSize::Small); + let multiple_actions = || { + h_flex() + .gap_0p5() + .child(Button::new("update", "Backup & Update").label_size(LabelSize::Small)) + .child(Button::new("dismiss", "Dismiss").label_size(LabelSize::Small)) + }; + + let basic_examples = vec![ single_example( "Simple with Title Only", Callout::new() - .icon( - Icon::new(IconName::Info) - .color(Color::Accent) - .size(IconSize::Small), - ) + .icon(IconName::Info) .title("System maintenance scheduled for tonight") - .primary_action(Button::new("got-it", "Got it").label_size(LabelSize::Small)) + .actions_slot(single_action()) .into_any_element(), ) .width(px(580.)), single_example( "With Title and Description", Callout::new() - .icon( - Icon::new(IconName::Warning) - .color(Color::Warning) - .size(IconSize::Small), - ) + .icon(IconName::Warning) .title("Your settings contain deprecated values") .description( "We'll backup your current settings and update them to the new format.", ) - .primary_action( - Button::new("update", "Backup & Update").label_size(LabelSize::Small), - ) - .secondary_action( - Button::new("dismiss", "Dismiss").label_size(LabelSize::Small), - ) + .actions_slot(single_action()) .into_any_element(), ) .width(px(580.)), single_example( "Error with Multiple Actions", Callout::new() - .icon( - Icon::new(IconName::Close) - .color(Color::Error) - .size(IconSize::Small), - ) + .icon(IconName::Close) .title("Thread reached the token limit") .description("Start a new thread from a summary to continue the conversation.") - .primary_action( - Button::new("new-thread", "Start New Thread").label_size(LabelSize::Small), - ) - .secondary_action( - Button::new("view-summary", "View Summary").label_size(LabelSize::Small), - ) + .actions_slot(multiple_actions()) .into_any_element(), ) .width(px(580.)), single_example( "Multi-line Description", Callout::new() - .icon( - Icon::new(IconName::Sparkle) - .color(Color::Accent) - .size(IconSize::Small), - ) + .icon(IconName::Sparkle) .title("Upgrade to Pro") .description("• Unlimited threads\n• Priority support\n• Advanced analytics") - .primary_action( - Button::new("upgrade", "Upgrade Now").label_size(LabelSize::Small), - ) - .secondary_action( - Button::new("learn-more", "Learn More").label_size(LabelSize::Small), - ) + .actions_slot(multiple_actions()) .into_any_element(), ) .width(px(580.)), ]; + let severity_examples = vec![ + single_example( + "Info", + Callout::new() + .icon(IconName::Info) + .title("System maintenance scheduled for tonight") + .actions_slot(single_action()) + .into_any_element(), + ), + single_example( + "Warning", + Callout::new() + .severity(Severity::Warning) + .icon(IconName::Triangle) + .title("System maintenance scheduled for tonight") + .actions_slot(single_action()) + .into_any_element(), + ), + single_example( + "Error", + Callout::new() + .severity(Severity::Error) + .icon(IconName::XCircle) + .title("System maintenance scheduled for tonight") + .actions_slot(single_action()) + .into_any_element(), + ), + single_example( + "Success", + Callout::new() + .severity(Severity::Success) + .icon(IconName::Check) + .title("System maintenance scheduled for tonight") + .actions_slot(single_action()) + .into_any_element(), + ), + ]; + Some( - example_group(callout_examples) - .vertical() + v_flex() + .gap_4() + .child(example_group(basic_examples).vertical()) + .child(example_group_with_title("Severity", severity_examples).vertical()) .into_any_element(), ) } diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index 80f8f863f8..0357e498bb 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -14,7 +14,9 @@ pub use ui_macros::RegisterComponent; pub use crate::DynamicSpacing; pub use crate::animation::{AnimationDirection, AnimationDuration, DefaultAnimations}; -pub use crate::styles::{PlatformStyle, StyledTypography, TextSize, rems_from_px, vh, vw}; +pub use crate::styles::{ + PlatformStyle, Severity, StyledTypography, TextSize, rems_from_px, vh, vw, +}; pub use crate::traits::clickable::*; pub use crate::traits::disableable::*; pub use crate::traits::fixed::*; diff --git a/crates/ui/src/styles.rs b/crates/ui/src/styles.rs index af6ab57029..bc2399f54b 100644 --- a/crates/ui/src/styles.rs +++ b/crates/ui/src/styles.rs @@ -3,6 +3,7 @@ mod appearance; mod color; mod elevation; mod platform; +mod severity; mod spacing; mod typography; mod units; @@ -11,6 +12,7 @@ pub use appearance::*; pub use color::*; pub use elevation::*; pub use platform::*; +pub use severity::*; pub use spacing::*; pub use typography::*; pub use units::*; diff --git a/crates/ui/src/styles/severity.rs b/crates/ui/src/styles/severity.rs new file mode 100644 index 0000000000..464f835186 --- /dev/null +++ b/crates/ui/src/styles/severity.rs @@ -0,0 +1,10 @@ +/// Severity levels that determine the style of the component. +/// Usually, it affects the background. Most of the time, +/// it also follows with an icon corresponding the severity level. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Severity { + Info, + Success, + Warning, + Error, +} From 6ee06bf2a0f0db312e4ec916e2802bd5bef034e8 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:53:05 -0300 Subject: [PATCH 460/693] ai onboarding: Adjust the Zed Pro banner (#36452) Release Notes: - N/A --- crates/ai_onboarding/src/ai_onboarding.rs | 30 ++++++++++++++--------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index 75177d4bd2..717abebfd1 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -332,17 +332,25 @@ impl ZedAiOnboarding { .mb_2(), ) .child(plan_definitions.pro_plan(false)) - .child( - Button::new("pro", "Continue with Zed Pro") - .full_width() - .style(ButtonStyle::Outlined) - .on_click({ - let callback = self.continue_with_zed_ai.clone(); - move |_, window, cx| { - telemetry::event!("Banner Dismissed", source = "AI Onboarding"); - callback(window, cx) - } - }), + .when_some( + self.dismiss_onboarding.as_ref(), + |this, dismiss_callback| { + let callback = dismiss_callback.clone(); + this.child( + h_flex().absolute().top_0().right_0().child( + IconButton::new("dismiss_onboarding", IconName::Close) + .icon_size(IconSize::Small) + .tooltip(Tooltip::text("Dismiss")) + .on_click(move |_, window, cx| { + telemetry::event!( + "Banner Dismissed", + source = "AI Onboarding", + ); + callback(window, cx) + }), + ), + ) + }, ) .into_any_element() } From 4abfcbaff987c0b42081e501aa431935e5dad27d Mon Sep 17 00:00:00 2001 From: Ben Kunkle <ben@zed.dev> Date: Mon, 18 Aug 2025 21:08:20 -0500 Subject: [PATCH 461/693] git: Suggest merge commit message in remote (#36430) Closes #ISSUE Adds `merge_message` field to the `UpdateRepository` proto message so that suggested merge messages are displayed in remote projects. Release Notes: - git: Fixed an issue where suggested merge commit messages would not appear for remote projects --- .../migrations.sqlite/20221109000000_test_schema.sql | 1 + .../migrations/20250818192156_add_git_merge_message.sql | 1 + crates/collab/src/db/queries/projects.rs | 7 +++++-- crates/collab/src/db/queries/rooms.rs | 1 + crates/collab/src/db/tables/project_repository.rs | 2 ++ crates/project/src/git_store.rs | 3 +++ crates/proto/proto/git.proto | 1 + 7 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 crates/collab/migrations/20250818192156_add_git_merge_message.sql diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 170ac7b0a2..43581fd942 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -116,6 +116,7 @@ CREATE TABLE "project_repositories" ( "scan_id" INTEGER NOT NULL, "is_deleted" BOOL NOT NULL, "current_merge_conflicts" VARCHAR, + "merge_message" VARCHAR, "branch_summary" VARCHAR, "head_commit_details" VARCHAR, PRIMARY KEY (project_id, id) diff --git a/crates/collab/migrations/20250818192156_add_git_merge_message.sql b/crates/collab/migrations/20250818192156_add_git_merge_message.sql new file mode 100644 index 0000000000..335ea2f824 --- /dev/null +++ b/crates/collab/migrations/20250818192156_add_git_merge_message.sql @@ -0,0 +1 @@ +ALTER TABLE "project_repositories" ADD COLUMN "merge_message" VARCHAR; diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 6783d8ed2a..9abab25ede 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -349,11 +349,11 @@ impl Database { serde_json::to_string(&repository.current_merge_conflicts) .unwrap(), )), - - // Old clients do not use abs path, entry ids or head_commit_details. + // Old clients do not use abs path, entry ids, head_commit_details, or merge_message. abs_path: ActiveValue::set(String::new()), entry_ids: ActiveValue::set("[]".into()), head_commit_details: ActiveValue::set(None), + merge_message: ActiveValue::set(None), } }), ) @@ -502,6 +502,7 @@ impl Database { current_merge_conflicts: ActiveValue::Set(Some( serde_json::to_string(&update.current_merge_conflicts).unwrap(), )), + merge_message: ActiveValue::set(update.merge_message.clone()), }) .on_conflict( OnConflict::columns([ @@ -515,6 +516,7 @@ impl Database { project_repository::Column::AbsPath, project_repository::Column::CurrentMergeConflicts, project_repository::Column::HeadCommitDetails, + project_repository::Column::MergeMessage, ]) .to_owned(), ) @@ -990,6 +992,7 @@ impl Database { head_commit_details, scan_id: db_repository_entry.scan_id as u64, is_last_update: true, + merge_message: db_repository_entry.merge_message, }); } } diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 1b128e3a23..9e7cabf9b2 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -793,6 +793,7 @@ impl Database { abs_path: db_repository.abs_path, scan_id: db_repository.scan_id as u64, is_last_update: true, + merge_message: db_repository.merge_message, }); } } diff --git a/crates/collab/src/db/tables/project_repository.rs b/crates/collab/src/db/tables/project_repository.rs index 665e87cd1f..eb653ecee3 100644 --- a/crates/collab/src/db/tables/project_repository.rs +++ b/crates/collab/src/db/tables/project_repository.rs @@ -16,6 +16,8 @@ pub struct Model { pub is_deleted: bool, // JSON array typed string pub current_merge_conflicts: Option<String>, + // The suggested merge commit message + pub merge_message: Option<String>, // A JSON object representing the current Branch values pub branch_summary: Option<String>, // A JSON object representing the current Head commit values diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index e8ba2425d1..9539008530 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -2774,6 +2774,7 @@ impl RepositorySnapshot { .iter() .map(|repo_path| repo_path.to_proto()) .collect(), + merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()), project_id, id: self.id.to_proto(), abs_path: self.work_directory_abs_path.to_proto(), @@ -2836,6 +2837,7 @@ impl RepositorySnapshot { .iter() .map(|path| path.as_ref().to_proto()) .collect(), + merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()), project_id, id: self.id.to_proto(), abs_path: self.work_directory_abs_path.to_proto(), @@ -4266,6 +4268,7 @@ impl Repository { .map(proto_to_commit_details); self.snapshot.merge.conflicted_paths = conflicted_paths; + self.snapshot.merge.message = update.merge_message.map(SharedString::from); let edits = update .removed_statuses diff --git a/crates/proto/proto/git.proto b/crates/proto/proto/git.proto index f2c388a3a3..cfb0369875 100644 --- a/crates/proto/proto/git.proto +++ b/crates/proto/proto/git.proto @@ -121,6 +121,7 @@ message UpdateRepository { uint64 scan_id = 9; bool is_last_update = 10; optional GitCommitDetails head_commit_details = 11; + optional string merge_message = 12; } message RemoveRepository { From 5004cb647bd843e46c47c830085f3564771f476e Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Mon, 18 Aug 2025 22:43:27 -0400 Subject: [PATCH 462/693] collab: Add `orb_subscription_id` to `billing_subscriptions` (#36455) This PR adds an `orb_subscription_id` column to the `billing_subscriptions` table. Release Notes: - N/A --- ...9022421_add_orb_subscription_id_to_billing_subscriptions.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 crates/collab/migrations/20250819022421_add_orb_subscription_id_to_billing_subscriptions.sql diff --git a/crates/collab/migrations/20250819022421_add_orb_subscription_id_to_billing_subscriptions.sql b/crates/collab/migrations/20250819022421_add_orb_subscription_id_to_billing_subscriptions.sql new file mode 100644 index 0000000000..317f6a7653 --- /dev/null +++ b/crates/collab/migrations/20250819022421_add_orb_subscription_id_to_billing_subscriptions.sql @@ -0,0 +1,2 @@ +alter table billing_subscriptions + add column orb_subscription_id text; From 1b6fd996f8bd0ed1934c99495f36e7f9b16c41fd Mon Sep 17 00:00:00 2001 From: Michael Sloan <michael@zed.dev> Date: Mon, 18 Aug 2025 21:23:07 -0600 Subject: [PATCH 463/693] Fix `InlineCompletion` -> `EditPrediction` keymap migration (#36457) Accidentally regressed this in #35512, causing this migration to not work and an error log to appear when one of these actions is in the user keymap Release Notes: - N/A --- .../migrator/src/migrations/m_2025_01_29/keymap.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/migrator/src/migrations/m_2025_01_29/keymap.rs b/crates/migrator/src/migrations/m_2025_01_29/keymap.rs index 646af8f63d..c32da88229 100644 --- a/crates/migrator/src/migrations/m_2025_01_29/keymap.rs +++ b/crates/migrator/src/migrations/m_2025_01_29/keymap.rs @@ -242,22 +242,22 @@ static STRING_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| { "inline_completion::ToggleMenu", "edit_prediction::ToggleMenu", ), - ("editor::NextEditPrediction", "editor::NextEditPrediction"), + ("editor::NextInlineCompletion", "editor::NextEditPrediction"), ( - "editor::PreviousEditPrediction", + "editor::PreviousInlineCompletion", "editor::PreviousEditPrediction", ), ( - "editor::AcceptPartialEditPrediction", + "editor::AcceptPartialInlineCompletion", "editor::AcceptPartialEditPrediction", ), - ("editor::ShowEditPrediction", "editor::ShowEditPrediction"), + ("editor::ShowInlineCompletion", "editor::ShowEditPrediction"), ( - "editor::AcceptEditPrediction", + "editor::AcceptInlineCompletion", "editor::AcceptEditPrediction", ), ( - "editor::ToggleEditPredictions", + "editor::ToggleInlineCompletions", "editor::ToggleEditPrediction", ), ]) From 821e97a392d9ec8c9cf736f26fae86d188dcb409 Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Mon, 18 Aug 2025 23:26:15 -0400 Subject: [PATCH 464/693] agent2: Add hover preview for image creases (#36427) Note that (at least for now) this only works for creases in the "new message" editor, not when editing past messages. That's because we don't have the original image available when putting together the creases for past messages, only the base64-encoded language model content. Release Notes: - N/A --- crates/agent_ui/src/acp/message_editor.rs | 162 +++++++++++------- .../ui/src/components/button/button_like.rs | 13 ++ 2 files changed, 111 insertions(+), 64 deletions(-) diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index d592231726..441ca9cf18 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -178,6 +178,56 @@ impl MessageEditor { return; }; + if let MentionUri::File { abs_path, .. } = &mention_uri { + let extension = abs_path + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default(); + + if Img::extensions().contains(&extension) && !extension.contains("svg") { + let project = self.project.clone(); + let Some(project_path) = project + .read(cx) + .project_path_for_absolute_path(abs_path, cx) + else { + return; + }; + let image = cx + .spawn(async move |_, cx| { + let image = project + .update(cx, |project, cx| project.open_image(project_path, cx)) + .map_err(|e| e.to_string())? + .await + .map_err(|e| e.to_string())?; + image + .read_with(cx, |image, _cx| image.image.clone()) + .map_err(|e| e.to_string()) + }) + .shared(); + let Some(crease_id) = insert_crease_for_image( + *excerpt_id, + start, + content_len, + Some(abs_path.as_path().into()), + image.clone(), + self.editor.clone(), + window, + cx, + ) else { + return; + }; + self.confirm_mention_for_image( + crease_id, + anchor, + Some(abs_path.clone()), + image, + window, + cx, + ); + return; + } + } + let Some(crease_id) = crate::context_picker::insert_crease_for_mention( *excerpt_id, start, @@ -195,71 +245,21 @@ impl MessageEditor { MentionUri::Fetch { url } => { self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx); } - MentionUri::File { - abs_path, - is_directory, - } => { - self.confirm_mention_for_file( - crease_id, - anchor, - abs_path, - is_directory, - window, - cx, - ); - } MentionUri::Thread { id, name } => { self.confirm_mention_for_thread(crease_id, anchor, id, name, window, cx); } MentionUri::TextThread { path, name } => { self.confirm_mention_for_text_thread(crease_id, anchor, path, name, window, cx); } - MentionUri::Symbol { .. } | MentionUri::Rule { .. } | MentionUri::Selection { .. } => { + MentionUri::File { .. } + | MentionUri::Symbol { .. } + | MentionUri::Rule { .. } + | MentionUri::Selection { .. } => { self.mention_set.insert_uri(crease_id, mention_uri.clone()); } } } - fn confirm_mention_for_file( - &mut self, - crease_id: CreaseId, - anchor: Anchor, - abs_path: PathBuf, - is_directory: bool, - window: &mut Window, - cx: &mut Context<Self>, - ) { - let extension = abs_path - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default(); - - if Img::extensions().contains(&extension) && !extension.contains("svg") { - let project = self.project.clone(); - let Some(project_path) = project - .read(cx) - .project_path_for_absolute_path(&abs_path, cx) - else { - return; - }; - let image = cx.spawn(async move |_, cx| { - let image = project - .update(cx, |project, cx| project.open_image(project_path, cx))? - .await?; - image.read_with(cx, |image, _cx| image.image.clone()) - }); - self.confirm_mention_for_image(crease_id, anchor, Some(abs_path), image, window, cx); - } else { - self.mention_set.insert_uri( - crease_id, - MentionUri::File { - abs_path, - is_directory, - }, - ); - } - } - fn confirm_mention_for_fetch( &mut self, crease_id: CreaseId, @@ -498,25 +498,20 @@ impl MessageEditor { let Some(anchor) = multibuffer_anchor else { return; }; + let task = Task::ready(Ok(Arc::new(image))).shared(); let Some(crease_id) = insert_crease_for_image( excerpt_id, text_anchor, content_len, None.clone(), + task.clone(), self.editor.clone(), window, cx, ) else { return; }; - self.confirm_mention_for_image( - crease_id, - anchor, - None, - Task::ready(Ok(Arc::new(image))), - window, - cx, - ); + self.confirm_mention_for_image(crease_id, anchor, None, task, window, cx); } } @@ -584,7 +579,7 @@ impl MessageEditor { crease_id: CreaseId, anchor: Anchor, abs_path: Option<PathBuf>, - image: Task<Result<Arc<Image>>>, + image: Shared<Task<Result<Arc<Image>, String>>>, window: &mut Window, cx: &mut Context<Self>, ) { @@ -937,6 +932,7 @@ pub(crate) fn insert_crease_for_image( anchor: text::Anchor, content_len: usize, abs_path: Option<Arc<Path>>, + image: Shared<Task<Result<Arc<Image>, String>>>, editor: Entity<Editor>, window: &mut Window, cx: &mut App, @@ -956,7 +952,7 @@ pub(crate) fn insert_crease_for_image( let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len); let placeholder = FoldPlaceholder { - render: render_image_fold_icon_button(crease_label, cx.weak_entity()), + render: render_image_fold_icon_button(crease_label, image, cx.weak_entity()), merge_adjacent: false, ..Default::default() }; @@ -978,9 +974,11 @@ pub(crate) fn insert_crease_for_image( fn render_image_fold_icon_button( label: SharedString, + image_task: Shared<Task<Result<Arc<Image>, String>>>, editor: WeakEntity<Editor>, ) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> { Arc::new({ + let image_task = image_task.clone(); move |fold_id, fold_range, cx| { let is_in_text_selection = editor .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx)) @@ -1005,11 +1003,47 @@ fn render_image_fold_icon_button( .single_line(), ), ) + .hoverable_tooltip({ + let image_task = image_task.clone(); + move |_, cx| { + let image = image_task.peek().cloned().transpose().ok().flatten(); + let image_task = image_task.clone(); + cx.new::<ImageHover>(|cx| ImageHover { + image, + _task: cx.spawn(async move |this, cx| { + if let Ok(image) = image_task.clone().await { + this.update(cx, |this, cx| { + if this.image.replace(image).is_none() { + cx.notify(); + } + }) + .ok(); + } + }), + }) + .into() + } + }) .into_any_element() } }) } +struct ImageHover { + image: Option<Arc<Image>>, + _task: Task<()>, +} + +impl Render for ImageHover { + fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { + if let Some(image) = self.image.clone() { + gpui::img(image).max_w_96().max_h_96().into_any_element() + } else { + gpui::Empty.into_any_element() + } + } +} + #[derive(Debug, Eq, PartialEq)] pub enum Mention { Text { uri: MentionUri, content: String }, diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index 0b30007e44..31bf76e843 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -400,6 +400,7 @@ pub struct ButtonLike { size: ButtonSize, rounding: Option<ButtonLikeRounding>, tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>, + hoverable_tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>, cursor_style: CursorStyle, on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>, on_right_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>, @@ -420,6 +421,7 @@ impl ButtonLike { size: ButtonSize::Default, rounding: Some(ButtonLikeRounding::All), tooltip: None, + hoverable_tooltip: None, children: SmallVec::new(), cursor_style: CursorStyle::PointingHand, on_click: None, @@ -463,6 +465,14 @@ impl ButtonLike { self.on_right_click = Some(Box::new(handler)); self } + + pub fn hoverable_tooltip( + mut self, + tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, + ) -> Self { + self.hoverable_tooltip = Some(Box::new(tooltip)); + self + } } impl Disableable for ButtonLike { @@ -654,6 +664,9 @@ impl RenderOnce for ButtonLike { .when_some(self.tooltip, |this, tooltip| { this.tooltip(move |window, cx| tooltip(window, cx)) }) + .when_some(self.hoverable_tooltip, |this, tooltip| { + this.hoverable_tooltip(move |window, cx| tooltip(window, cx)) + }) .children(self.children) } } From 7bcea7dc2c0fbeb6d9f42cddc55fa1e4bdf97744 Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Tue, 19 Aug 2025 00:09:43 -0400 Subject: [PATCH 465/693] agent2: Support directories in @file mentions (#36416) Release Notes: - N/A --- crates/acp_thread/src/mention.rs | 66 ++-- crates/agent2/src/thread.rs | 14 +- .../agent_ui/src/acp/completion_provider.rs | 13 +- crates/agent_ui/src/acp/message_editor.rs | 369 ++++++++++++------ crates/agent_ui/src/acp/thread_view.rs | 31 +- 5 files changed, 325 insertions(+), 168 deletions(-) diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 17bc265fac..25e64acbee 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -15,7 +15,9 @@ use url::Url; pub enum MentionUri { File { abs_path: PathBuf, - is_directory: bool, + }, + Directory { + abs_path: PathBuf, }, Symbol { path: PathBuf, @@ -79,14 +81,14 @@ impl MentionUri { }) } } else { - let file_path = + let abs_path = PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path)); - let is_directory = input.ends_with("/"); - Ok(Self::File { - abs_path: file_path, - is_directory, - }) + if input.ends_with("/") { + Ok(Self::Directory { abs_path }) + } else { + Ok(Self::File { abs_path }) + } } } "zed" => { @@ -120,7 +122,7 @@ impl MentionUri { pub fn name(&self) -> String { match self { - MentionUri::File { abs_path, .. } => abs_path + MentionUri::File { abs_path, .. } | MentionUri::Directory { abs_path, .. } => abs_path .file_name() .unwrap_or_default() .to_string_lossy() @@ -138,18 +140,11 @@ impl MentionUri { pub fn icon_path(&self, cx: &mut App) -> SharedString { match self { - MentionUri::File { - abs_path, - is_directory, - } => { - if *is_directory { - FileIcons::get_folder_icon(false, cx) - .unwrap_or_else(|| IconName::Folder.path().into()) - } else { - FileIcons::get_icon(abs_path, cx) - .unwrap_or_else(|| IconName::File.path().into()) - } + MentionUri::File { abs_path } => { + FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into()) } + MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx) + .unwrap_or_else(|| IconName::Folder.path().into()), MentionUri::Symbol { .. } => IconName::Code.path().into(), MentionUri::Thread { .. } => IconName::Thread.path().into(), MentionUri::TextThread { .. } => IconName::Thread.path().into(), @@ -165,13 +160,16 @@ impl MentionUri { pub fn to_uri(&self) -> Url { match self { - MentionUri::File { - abs_path, - is_directory, - } => { + MentionUri::File { abs_path } => { + let mut url = Url::parse("file:///").unwrap(); + let path = abs_path.to_string_lossy(); + url.set_path(&path); + url + } + MentionUri::Directory { abs_path } => { let mut url = Url::parse("file:///").unwrap(); let mut path = abs_path.to_string_lossy().to_string(); - if *is_directory && !path.ends_with("/") { + if !path.ends_with("/") { path.push_str("/"); } url.set_path(&path); @@ -274,12 +272,8 @@ mod tests { let file_uri = "file:///path/to/file.rs"; let parsed = MentionUri::parse(file_uri).unwrap(); match &parsed { - MentionUri::File { - abs_path, - is_directory, - } => { + MentionUri::File { abs_path } => { assert_eq!(abs_path.to_str().unwrap(), "/path/to/file.rs"); - assert!(!is_directory); } _ => panic!("Expected File variant"), } @@ -291,32 +285,26 @@ mod tests { let file_uri = "file:///path/to/dir/"; let parsed = MentionUri::parse(file_uri).unwrap(); match &parsed { - MentionUri::File { - abs_path, - is_directory, - } => { + MentionUri::Directory { abs_path } => { assert_eq!(abs_path.to_str().unwrap(), "/path/to/dir/"); - assert!(is_directory); } - _ => panic!("Expected File variant"), + _ => panic!("Expected Directory variant"), } assert_eq!(parsed.to_uri().to_string(), file_uri); } #[test] fn test_to_directory_uri_with_slash() { - let uri = MentionUri::File { + let uri = MentionUri::Directory { abs_path: PathBuf::from("/path/to/dir/"), - is_directory: true, }; assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/"); } #[test] fn test_to_directory_uri_without_slash() { - let uri = MentionUri::File { + let uri = MentionUri::Directory { abs_path: PathBuf::from("/path/to/dir"), - is_directory: true, }; assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/"); } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index eed374e396..e0819abcc5 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -146,6 +146,7 @@ impl UserMessage { They are up-to-date and don't need to be re-read.\n\n"; const OPEN_FILES_TAG: &str = "<files>"; + const OPEN_DIRECTORIES_TAG: &str = "<directories>"; const OPEN_SYMBOLS_TAG: &str = "<symbols>"; const OPEN_THREADS_TAG: &str = "<threads>"; const OPEN_FETCH_TAG: &str = "<fetched_urls>"; @@ -153,6 +154,7 @@ impl UserMessage { "<rules>\nThe user has specified the following rules that should be applied:\n"; let mut file_context = OPEN_FILES_TAG.to_string(); + let mut directory_context = OPEN_DIRECTORIES_TAG.to_string(); let mut symbol_context = OPEN_SYMBOLS_TAG.to_string(); let mut thread_context = OPEN_THREADS_TAG.to_string(); let mut fetch_context = OPEN_FETCH_TAG.to_string(); @@ -168,7 +170,7 @@ impl UserMessage { } UserMessageContent::Mention { uri, content } => { match uri { - MentionUri::File { abs_path, .. } => { + MentionUri::File { abs_path } => { write!( &mut symbol_context, "\n{}", @@ -179,6 +181,9 @@ impl UserMessage { ) .ok(); } + MentionUri::Directory { .. } => { + write!(&mut directory_context, "\n{}\n", content).ok(); + } MentionUri::Symbol { path, line_range, .. } @@ -233,6 +238,13 @@ impl UserMessage { .push(language_model::MessageContent::Text(file_context)); } + if directory_context.len() > OPEN_DIRECTORIES_TAG.len() { + directory_context.push_str("</directories>\n"); + message + .content + .push(language_model::MessageContent::Text(directory_context)); + } + if symbol_context.len() > OPEN_SYMBOLS_TAG.len() { symbol_context.push_str("</symbols>\n"); message diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index e2ddd03f27..d2af2a880d 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -445,19 +445,20 @@ impl ContextPickerCompletionProvider { let abs_path = project.read(cx).absolute_path(&project_path, cx)?; - let file_uri = MentionUri::File { - abs_path, - is_directory, + let uri = if is_directory { + MentionUri::Directory { abs_path } + } else { + MentionUri::File { abs_path } }; - let crease_icon_path = file_uri.icon_path(cx); + let crease_icon_path = uri.icon_path(cx); let completion_icon_path = if is_recent { IconName::HistoryRerun.path().into() } else { crease_icon_path.clone() }; - let new_text = format!("{} ", file_uri.as_link()); + let new_text = format!("{} ", uri.as_link()); let new_text_len = new_text.len(); Some(Completion { replace_range: source_range.clone(), @@ -472,7 +473,7 @@ impl ContextPickerCompletionProvider { source_range.start, new_text_len - 1, message_editor, - file_uri, + uri, )), }) } diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 441ca9cf18..e5ecf43ef5 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -6,6 +6,7 @@ use acp_thread::{MentionUri, selection_name}; use agent::{TextThreadStore, ThreadId, ThreadStore}; use agent_client_protocol as acp; use anyhow::{Context as _, Result, anyhow}; +use assistant_slash_commands::codeblock_fence_for_path; use collections::{HashMap, HashSet}; use editor::{ Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, @@ -15,7 +16,7 @@ use editor::{ }; use futures::{ FutureExt as _, TryFutureExt as _, - future::{Shared, try_join_all}, + future::{Shared, join_all, try_join_all}, }; use gpui::{ AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, Image, @@ -23,12 +24,12 @@ use gpui::{ }; use language::{Buffer, Language}; use language_model::LanguageModelImage; -use project::{CompletionIntent, Project}; +use project::{CompletionIntent, Project, ProjectPath, Worktree}; use rope::Point; use settings::Settings; use std::{ ffi::OsStr, - fmt::Write, + fmt::{Display, Write}, ops::Range, path::{Path, PathBuf}, rc::Rc, @@ -245,6 +246,9 @@ impl MessageEditor { MentionUri::Fetch { url } => { self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx); } + MentionUri::Directory { abs_path } => { + self.confirm_mention_for_directory(crease_id, anchor, abs_path, window, cx); + } MentionUri::Thread { id, name } => { self.confirm_mention_for_thread(crease_id, anchor, id, name, window, cx); } @@ -260,6 +264,124 @@ impl MessageEditor { } } + fn confirm_mention_for_directory( + &mut self, + crease_id: CreaseId, + anchor: Anchor, + abs_path: PathBuf, + window: &mut Window, + cx: &mut Context<Self>, + ) { + fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc<Path>, PathBuf)> { + let mut files = Vec::new(); + + for entry in worktree.child_entries(path) { + if entry.is_dir() { + files.extend(collect_files_in_path(worktree, &entry.path)); + } else if entry.is_file() { + files.push((entry.path.clone(), worktree.full_path(&entry.path))); + } + } + + files + } + + let uri = MentionUri::Directory { + abs_path: abs_path.clone(), + }; + let Some(project_path) = self + .project + .read(cx) + .project_path_for_absolute_path(&abs_path, cx) + else { + return; + }; + let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else { + return; + }; + let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else { + return; + }; + let project = self.project.clone(); + let task = cx.spawn(async move |_, cx| { + let directory_path = entry.path.clone(); + + let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?; + let file_paths = worktree.read_with(cx, |worktree, _cx| { + collect_files_in_path(worktree, &directory_path) + })?; + let descendants_future = cx.update(|cx| { + join_all(file_paths.into_iter().map(|(worktree_path, full_path)| { + let rel_path = worktree_path + .strip_prefix(&directory_path) + .log_err() + .map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into()); + + let open_task = project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + let project_path = ProjectPath { + worktree_id, + path: worktree_path, + }; + buffer_store.open_buffer(project_path, cx) + }) + }); + + // TODO: report load errors instead of just logging + let rope_task = cx.spawn(async move |cx| { + let buffer = open_task.await.log_err()?; + let rope = buffer + .read_with(cx, |buffer, _cx| buffer.as_rope().clone()) + .log_err()?; + Some(rope) + }); + + cx.background_spawn(async move { + let rope = rope_task.await?; + Some((rel_path, full_path, rope.to_string())) + }) + })) + })?; + + let contents = cx + .background_spawn(async move { + let contents = descendants_future.await.into_iter().flatten(); + contents.collect() + }) + .await; + anyhow::Ok(contents) + }); + let task = cx + .spawn(async move |_, _| { + task.await + .map(|contents| DirectoryContents(contents).to_string()) + .map_err(|e| e.to_string()) + }) + .shared(); + + self.mention_set.directories.insert(abs_path, task.clone()); + + let editor = self.editor.clone(); + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_some() { + this.update(cx, |this, _| { + this.mention_set.insert_uri(crease_id, uri); + }) + .ok(); + } else { + editor + .update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + } + }) + .detach(); + } + fn confirm_mention_for_fetch( &mut self, crease_id: CreaseId, @@ -361,6 +483,104 @@ impl MessageEditor { } } + fn confirm_mention_for_thread( + &mut self, + crease_id: CreaseId, + anchor: Anchor, + id: ThreadId, + name: String, + window: &mut Window, + cx: &mut Context<Self>, + ) { + let uri = MentionUri::Thread { + id: id.clone(), + name, + }; + let open_task = self.thread_store.update(cx, |thread_store, cx| { + thread_store.open_thread(&id, window, cx) + }); + let task = cx + .spawn(async move |_, cx| { + let thread = open_task.await.map_err(|e| e.to_string())?; + let content = thread + .read_with(cx, |thread, _cx| thread.latest_detailed_summary_or_text()) + .map_err(|e| e.to_string())?; + Ok(content) + }) + .shared(); + + self.mention_set.insert_thread(id, task.clone()); + + let editor = self.editor.clone(); + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_some() { + this.update(cx, |this, _| { + this.mention_set.insert_uri(crease_id, uri); + }) + .ok(); + } else { + editor + .update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + } + }) + .detach(); + } + + fn confirm_mention_for_text_thread( + &mut self, + crease_id: CreaseId, + anchor: Anchor, + path: PathBuf, + name: String, + window: &mut Window, + cx: &mut Context<Self>, + ) { + let uri = MentionUri::TextThread { + path: path.clone(), + name, + }; + let context = self.text_thread_store.update(cx, |text_thread_store, cx| { + text_thread_store.open_local_context(path.as_path().into(), cx) + }); + let task = cx + .spawn(async move |_, cx| { + let context = context.await.map_err(|e| e.to_string())?; + let xml = context + .update(cx, |context, cx| context.to_xml(cx)) + .map_err(|e| e.to_string())?; + Ok(xml) + }) + .shared(); + + self.mention_set.insert_text_thread(path, task.clone()); + + let editor = self.editor.clone(); + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_some() { + this.update(cx, |this, _| { + this.mention_set.insert_uri(crease_id, uri); + }) + .ok(); + } else { + editor + .update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting(vec![anchor..anchor], true, cx); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + } + }) + .detach(); + } + pub fn contents( &self, window: &mut Window, @@ -613,13 +833,8 @@ impl MessageEditor { if task.await.notify_async_err(cx).is_some() { if let Some(abs_path) = abs_path.clone() { this.update(cx, |this, _cx| { - this.mention_set.insert_uri( - crease_id, - MentionUri::File { - abs_path, - is_directory: false, - }, - ); + this.mention_set + .insert_uri(crease_id, MentionUri::File { abs_path }); }) .ok(); } @@ -637,104 +852,6 @@ impl MessageEditor { .detach(); } - fn confirm_mention_for_thread( - &mut self, - crease_id: CreaseId, - anchor: Anchor, - id: ThreadId, - name: String, - window: &mut Window, - cx: &mut Context<Self>, - ) { - let uri = MentionUri::Thread { - id: id.clone(), - name, - }; - let open_task = self.thread_store.update(cx, |thread_store, cx| { - thread_store.open_thread(&id, window, cx) - }); - let task = cx - .spawn(async move |_, cx| { - let thread = open_task.await.map_err(|e| e.to_string())?; - let content = thread - .read_with(cx, |thread, _cx| thread.latest_detailed_summary_or_text()) - .map_err(|e| e.to_string())?; - Ok(content) - }) - .shared(); - - self.mention_set.insert_thread(id, task.clone()); - - let editor = self.editor.clone(); - cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_some() { - this.update(cx, |this, _| { - this.mention_set.insert_uri(crease_id, uri); - }) - .ok(); - } else { - editor - .update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - } - }) - .detach(); - } - - fn confirm_mention_for_text_thread( - &mut self, - crease_id: CreaseId, - anchor: Anchor, - path: PathBuf, - name: String, - window: &mut Window, - cx: &mut Context<Self>, - ) { - let uri = MentionUri::TextThread { - path: path.clone(), - name, - }; - let context = self.text_thread_store.update(cx, |text_thread_store, cx| { - text_thread_store.open_local_context(path.as_path().into(), cx) - }); - let task = cx - .spawn(async move |_, cx| { - let context = context.await.map_err(|e| e.to_string())?; - let xml = context - .update(cx, |context, cx| context.to_xml(cx)) - .map_err(|e| e.to_string())?; - Ok(xml) - }) - .shared(); - - self.mention_set.insert_text_thread(path, task.clone()); - - let editor = self.editor.clone(); - cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_some() { - this.update(cx, |this, _| { - this.mention_set.insert_uri(crease_id, uri); - }) - .ok(); - } else { - editor - .update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - } - }) - .detach(); - } - pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) { self.editor.update(cx, |editor, cx| { editor.set_mode(mode); @@ -817,6 +934,10 @@ impl MessageEditor { self.mention_set .add_fetch_result(url, Task::ready(Ok(text)).shared()); } + MentionUri::Directory { abs_path } => { + let task = Task::ready(Ok(text)).shared(); + self.mention_set.directories.insert(abs_path, task); + } MentionUri::File { .. } | MentionUri::Symbol { .. } | MentionUri::Rule { .. } @@ -882,6 +1003,18 @@ impl MessageEditor { } } +struct DirectoryContents(Arc<[(Arc<Path>, PathBuf, String)]>); + +impl Display for DirectoryContents { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (_relative_path, full_path, content) in self.0.iter() { + let fence = codeblock_fence_for_path(Some(full_path), None); + write!(f, "\n{fence}\n{content}\n```")?; + } + Ok(()) + } +} + impl Focusable for MessageEditor { fn focus_handle(&self, cx: &App) -> FocusHandle { self.editor.focus_handle(cx) @@ -1064,6 +1197,7 @@ pub struct MentionSet { images: HashMap<CreaseId, Shared<Task<Result<MentionImage, String>>>>, thread_summaries: HashMap<ThreadId, Shared<Task<Result<SharedString, String>>>>, text_thread_summaries: HashMap<PathBuf, Shared<Task<Result<String, String>>>>, + directories: HashMap<PathBuf, Shared<Task<Result<String, String>>>>, } impl MentionSet { @@ -1116,7 +1250,6 @@ impl MentionSet { .map(|(&crease_id, uri)| { match uri { MentionUri::File { abs_path, .. } => { - // TODO directories let uri = uri.clone(); let abs_path = abs_path.to_path_buf(); @@ -1141,6 +1274,24 @@ impl MentionSet { anyhow::Ok((crease_id, Mention::Text { uri, content })) }) } + MentionUri::Directory { abs_path } => { + let Some(content) = self.directories.get(abs_path).cloned() else { + return Task::ready(Err(anyhow!("missing directory load task"))); + }; + let uri = uri.clone(); + cx.spawn(async move |_| { + Ok(( + crease_id, + Mention::Text { + uri, + content: content + .await + .map_err(|e| anyhow::anyhow!("{e}"))? + .to_string(), + }, + )) + }) + } MentionUri::Symbol { path, line_range, .. } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 0d15e27e0c..b3ebe86674 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2790,25 +2790,30 @@ impl AcpThreadView { if let Some(mention) = MentionUri::parse(&url).log_err() { workspace.update(cx, |workspace, cx| match mention { - MentionUri::File { abs_path, .. } => { + MentionUri::File { abs_path } => { let project = workspace.project(); - let Some((path, entry)) = project.update(cx, |project, cx| { + let Some(path) = + project.update(cx, |project, cx| project.find_project_path(abs_path, cx)) + else { + return; + }; + + workspace + .open_path(path, None, true, window, cx) + .detach_and_log_err(cx); + } + MentionUri::Directory { abs_path } => { + let project = workspace.project(); + let Some(entry) = project.update(cx, |project, cx| { let path = project.find_project_path(abs_path, cx)?; - let entry = project.entry_for_path(&path, cx)?; - Some((path, entry)) + project.entry_for_path(&path, cx) }) else { return; }; - if entry.is_dir() { - project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(entry.id)); - }); - } else { - workspace - .open_path(path, None, true, window, cx) - .detach_and_log_err(cx); - } + project.update(cx, |_, cx| { + cx.emit(project::Event::RevealInProjectPanel(entry.id)); + }); } MentionUri::Symbol { path, line_range, .. From d30b017d1f7dda921ebd1ab6a3ef726e1f796571 Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Tue, 19 Aug 2025 02:00:41 -0400 Subject: [PATCH 466/693] Prevent sending slash commands in CC threads (#36453) Highlight them as errors in the editor, and add a leading space when sending them so users don't hit the odd behavior when sending these commands to the SDK. Release Notes: - N/A --- crates/agent2/src/native_agent_server.rs | 6 +- crates/agent_servers/src/agent_servers.rs | 9 + crates/agent_servers/src/claude.rs | 4 + crates/agent_servers/src/gemini.rs | 6 +- crates/agent_ui/src/acp/entry_view_state.rs | 5 + crates/agent_ui/src/acp/message_editor.rs | 223 +++++++++++++++++++- crates/agent_ui/src/acp/thread_view.rs | 9 +- crates/editor/src/hover_popover.rs | 17 +- 8 files changed, 263 insertions(+), 16 deletions(-) diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index cadd88a846..6f09ee1175 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -1,4 +1,4 @@ -use std::{path::Path, rc::Rc, sync::Arc}; +use std::{any::Any, path::Path, rc::Rc, sync::Arc}; use agent_servers::AgentServer; use anyhow::Result; @@ -66,4 +66,8 @@ impl AgentServer for NativeAgentServer { Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>) }) } + + fn into_any(self: Rc<Self>) -> Rc<dyn Any> { + self + } } diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index b3b8a33170..8f8aa1d788 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -18,6 +18,7 @@ use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{ + any::Any, path::{Path, PathBuf}, rc::Rc, sync::Arc, @@ -40,6 +41,14 @@ pub trait AgentServer: Send { project: &Entity<Project>, cx: &mut App, ) -> Task<Result<Rc<dyn AgentConnection>>>; + + fn into_any(self: Rc<Self>) -> Rc<dyn Any>; +} + +impl dyn AgentServer { + pub fn downcast<T: 'static + AgentServer + Sized>(self: Rc<Self>) -> Option<Rc<T>> { + self.into_any().downcast().ok() + } } impl std::fmt::Debug for AgentServerCommand { diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 9b273cb091..7034d6fbce 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -65,6 +65,10 @@ impl AgentServer for ClaudeCode { Task::ready(Ok(Rc::new(connection) as _)) } + + fn into_any(self: Rc<Self>) -> Rc<dyn Any> { + self + } } struct ClaudeAgentConnection { diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index ad883f6da8..167e632d79 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -1,5 +1,5 @@ -use std::path::Path; use std::rc::Rc; +use std::{any::Any, path::Path}; use crate::{AgentServer, AgentServerCommand}; use acp_thread::{AgentConnection, LoadError}; @@ -86,6 +86,10 @@ impl AgentServer for Gemini { result }) } + + fn into_any(self: Rc<Self>) -> Rc<dyn Any> { + self + } } #[cfg(test)] diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index 18ef1ce2ab..0b0b8471a7 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -24,6 +24,7 @@ pub struct EntryViewState { thread_store: Entity<ThreadStore>, text_thread_store: Entity<TextThreadStore>, entries: Vec<Entry>, + prevent_slash_commands: bool, } impl EntryViewState { @@ -32,6 +33,7 @@ impl EntryViewState { project: Entity<Project>, thread_store: Entity<ThreadStore>, text_thread_store: Entity<TextThreadStore>, + prevent_slash_commands: bool, ) -> Self { Self { workspace, @@ -39,6 +41,7 @@ impl EntryViewState { thread_store, text_thread_store, entries: Vec::new(), + prevent_slash_commands, } } @@ -77,6 +80,7 @@ impl EntryViewState { self.thread_store.clone(), self.text_thread_store.clone(), "Edit message - @ to include context", + self.prevent_slash_commands, editor::EditorMode::AutoHeight { min_lines: 1, max_lines: None, @@ -382,6 +386,7 @@ mod tests { project.clone(), thread_store, text_thread_store, + false, ) }); diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index e5ecf43ef5..a32d0ce6ce 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -10,7 +10,8 @@ use assistant_slash_commands::codeblock_fence_for_path; use collections::{HashMap, HashSet}; use editor::{ Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, - EditorMode, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, ToOffset, + EditorEvent, EditorMode, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, + SemanticsProvider, ToOffset, actions::Paste, display_map::{Crease, CreaseId, FoldId}, }; @@ -19,8 +20,9 @@ use futures::{ future::{Shared, join_all, try_join_all}, }; use gpui::{ - AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, Image, - ImageFormat, Img, Task, TextStyle, WeakEntity, + AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, + HighlightStyle, Image, ImageFormat, Img, Subscription, Task, TextStyle, UnderlineStyle, + WeakEntity, }; use language::{Buffer, Language}; use language_model::LanguageModelImage; @@ -28,26 +30,30 @@ use project::{CompletionIntent, Project, ProjectPath, Worktree}; use rope::Point; use settings::Settings; use std::{ + cell::Cell, ffi::OsStr, fmt::{Display, Write}, ops::Range, path::{Path, PathBuf}, rc::Rc, sync::Arc, + time::Duration, }; -use text::OffsetRangeExt; +use text::{OffsetRangeExt, ToOffset as _}; use theme::ThemeSettings; use ui::{ ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement, Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div, - h_flex, + h_flex, px, }; use url::Url; use util::ResultExt; use workspace::{Workspace, notifications::NotifyResultExt as _}; use zed_actions::agent::Chat; +const PARSE_SLASH_COMMAND_DEBOUNCE: Duration = Duration::from_millis(50); + pub struct MessageEditor { mention_set: MentionSet, editor: Entity<Editor>, @@ -55,6 +61,9 @@ pub struct MessageEditor { workspace: WeakEntity<Workspace>, thread_store: Entity<ThreadStore>, text_thread_store: Entity<TextThreadStore>, + prevent_slash_commands: bool, + _subscriptions: Vec<Subscription>, + _parse_slash_command_task: Task<()>, } #[derive(Clone, Copy)] @@ -73,6 +82,7 @@ impl MessageEditor { thread_store: Entity<ThreadStore>, text_thread_store: Entity<TextThreadStore>, placeholder: impl Into<Arc<str>>, + prevent_slash_commands: bool, mode: EditorMode, window: &mut Window, cx: &mut Context<Self>, @@ -90,6 +100,9 @@ impl MessageEditor { text_thread_store.downgrade(), cx.weak_entity(), ); + let semantics_provider = Rc::new(SlashCommandSemanticsProvider { + range: Cell::new(None), + }); let mention_set = MentionSet::default(); let editor = cx.new(|cx| { let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx)); @@ -106,6 +119,9 @@ impl MessageEditor { max_entries_visible: 12, placement: Some(ContextMenuPlacement::Above), }); + if prevent_slash_commands { + editor.set_semantics_provider(Some(semantics_provider.clone())); + } editor }); @@ -114,6 +130,24 @@ impl MessageEditor { }) .detach(); + let mut subscriptions = Vec::new(); + if prevent_slash_commands { + subscriptions.push(cx.subscribe_in(&editor, window, { + let semantics_provider = semantics_provider.clone(); + move |this, editor, event, window, cx| match event { + EditorEvent::Edited { .. } => { + this.highlight_slash_command( + semantics_provider.clone(), + editor.clone(), + window, + cx, + ); + } + _ => {} + } + })); + } + Self { editor, project, @@ -121,6 +155,9 @@ impl MessageEditor { thread_store, text_thread_store, workspace, + prevent_slash_commands, + _subscriptions: subscriptions, + _parse_slash_command_task: Task::ready(()), } } @@ -590,6 +627,7 @@ impl MessageEditor { self.mention_set .contents(self.project.clone(), self.thread_store.clone(), window, cx); let editor = self.editor.clone(); + let prevent_slash_commands = self.prevent_slash_commands; cx.spawn(async move |_, cx| { let contents = contents.await?; @@ -612,7 +650,15 @@ impl MessageEditor { let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot); if crease_range.start > ix { - chunks.push(text[ix..crease_range.start].into()); + let chunk = if prevent_slash_commands + && ix == 0 + && parse_slash_command(&text[ix..]).is_some() + { + format!(" {}", &text[ix..crease_range.start]).into() + } else { + text[ix..crease_range.start].into() + }; + chunks.push(chunk); } let chunk = match mention { Mention::Text { uri, content } => { @@ -644,7 +690,14 @@ impl MessageEditor { } if ix < text.len() { - let last_chunk = text[ix..].trim_end(); + let last_chunk = if prevent_slash_commands + && ix == 0 + && parse_slash_command(&text[ix..]).is_some() + { + format!(" {}", text[ix..].trim_end()) + } else { + text[ix..].trim_end().to_owned() + }; if !last_chunk.is_empty() { chunks.push(last_chunk.into()); } @@ -990,6 +1043,48 @@ impl MessageEditor { cx.notify(); } + fn highlight_slash_command( + &mut self, + semantics_provider: Rc<SlashCommandSemanticsProvider>, + editor: Entity<Editor>, + window: &mut Window, + cx: &mut Context<Self>, + ) { + struct InvalidSlashCommand; + + self._parse_slash_command_task = cx.spawn_in(window, async move |_, cx| { + cx.background_executor() + .timer(PARSE_SLASH_COMMAND_DEBOUNCE) + .await; + editor + .update_in(cx, |editor, window, cx| { + let snapshot = editor.snapshot(window, cx); + let range = parse_slash_command(&editor.text(cx)); + semantics_provider.range.set(range); + if let Some((start, end)) = range { + editor.highlight_text::<InvalidSlashCommand>( + vec![ + snapshot.buffer_snapshot.anchor_after(start) + ..snapshot.buffer_snapshot.anchor_before(end), + ], + HighlightStyle { + underline: Some(UnderlineStyle { + thickness: px(1.), + color: Some(gpui::red()), + wavy: true, + }), + ..Default::default() + }, + cx, + ); + } else { + editor.clear_highlights::<InvalidSlashCommand>(cx); + } + }) + .ok(); + }) + } + #[cfg(test)] pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) { self.editor.update(cx, |editor, cx| { @@ -1416,6 +1511,118 @@ impl MentionSet { } } +struct SlashCommandSemanticsProvider { + range: Cell<Option<(usize, usize)>>, +} + +impl SemanticsProvider for SlashCommandSemanticsProvider { + fn hover( + &self, + buffer: &Entity<Buffer>, + position: text::Anchor, + cx: &mut App, + ) -> Option<Task<Vec<project::Hover>>> { + let snapshot = buffer.read(cx).snapshot(); + let offset = position.to_offset(&snapshot); + let (start, end) = self.range.get()?; + if !(start..end).contains(&offset) { + return None; + } + let range = snapshot.anchor_after(start)..snapshot.anchor_after(end); + return Some(Task::ready(vec![project::Hover { + contents: vec![project::HoverBlock { + text: "Slash commands are not supported".into(), + kind: project::HoverBlockKind::PlainText, + }], + range: Some(range), + language: None, + }])); + } + + fn inline_values( + &self, + _buffer_handle: Entity<Buffer>, + _range: Range<text::Anchor>, + _cx: &mut App, + ) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> { + None + } + + fn inlay_hints( + &self, + _buffer_handle: Entity<Buffer>, + _range: Range<text::Anchor>, + _cx: &mut App, + ) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> { + None + } + + fn resolve_inlay_hint( + &self, + _hint: project::InlayHint, + _buffer_handle: Entity<Buffer>, + _server_id: lsp::LanguageServerId, + _cx: &mut App, + ) -> Option<Task<anyhow::Result<project::InlayHint>>> { + None + } + + fn supports_inlay_hints(&self, _buffer: &Entity<Buffer>, _cx: &mut App) -> bool { + false + } + + fn document_highlights( + &self, + _buffer: &Entity<Buffer>, + _position: text::Anchor, + _cx: &mut App, + ) -> Option<Task<Result<Vec<project::DocumentHighlight>>>> { + None + } + + fn definitions( + &self, + _buffer: &Entity<Buffer>, + _position: text::Anchor, + _kind: editor::GotoDefinitionKind, + _cx: &mut App, + ) -> Option<Task<Result<Vec<project::LocationLink>>>> { + None + } + + fn range_for_rename( + &self, + _buffer: &Entity<Buffer>, + _position: text::Anchor, + _cx: &mut App, + ) -> Option<Task<Result<Option<Range<text::Anchor>>>>> { + None + } + + fn perform_rename( + &self, + _buffer: &Entity<Buffer>, + _position: text::Anchor, + _new_name: String, + _cx: &mut App, + ) -> Option<Task<Result<project::ProjectTransaction>>> { + None + } +} + +fn parse_slash_command(text: &str) -> Option<(usize, usize)> { + if let Some(remainder) = text.strip_prefix('/') { + let pos = remainder + .find(char::is_whitespace) + .unwrap_or(remainder.len()); + let command = &remainder[..pos]; + if !command.is_empty() && command.chars().all(char::is_alphanumeric) { + return Some((0, 1 + command.len())); + } + } + None +} + #[cfg(test)] mod tests { use std::{ops::Range, path::Path, sync::Arc}; @@ -1463,6 +1670,7 @@ mod tests { thread_store.clone(), text_thread_store.clone(), "Test", + false, EditorMode::AutoHeight { min_lines: 1, max_lines: None, @@ -1661,6 +1869,7 @@ mod tests { thread_store.clone(), text_thread_store.clone(), "Test", + false, EditorMode::AutoHeight { max_lines: None, min_lines: 1, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index b3ebe86674..2cfedfe840 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -7,7 +7,7 @@ use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::{self as acp}; -use agent_servers::AgentServer; +use agent_servers::{AgentServer, ClaudeCode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; use anyhow::bail; use audio::{Audio, Sound}; @@ -160,6 +160,7 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context<Self>, ) -> Self { + let prevent_slash_commands = agent.clone().downcast::<ClaudeCode>().is_some(); let message_editor = cx.new(|cx| { MessageEditor::new( workspace.clone(), @@ -167,6 +168,7 @@ impl AcpThreadView { thread_store.clone(), text_thread_store.clone(), "Message the agent - @ to include context", + prevent_slash_commands, editor::EditorMode::AutoHeight { min_lines: MIN_EDITOR_LINES, max_lines: Some(MAX_EDITOR_LINES), @@ -184,6 +186,7 @@ impl AcpThreadView { project.clone(), thread_store.clone(), text_thread_store.clone(), + prevent_slash_commands, ) }); @@ -3925,6 +3928,10 @@ pub(crate) mod tests { ) -> Task<gpui::Result<Rc<dyn AgentConnection>>> { Task::ready(Ok(Rc::new(self.connection.clone()))) } + + fn into_any(self: Rc<Self>) -> Rc<dyn Any> { + self + } } #[derive(Clone)] diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 3fc673bad9..6fe981fd6e 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -167,7 +167,8 @@ pub fn hover_at_inlay( let language_registry = project.read_with(cx, |p, _| p.languages().clone())?; let blocks = vec![inlay_hover.tooltip]; - let parsed_content = parse_blocks(&blocks, &language_registry, None, cx).await; + let parsed_content = + parse_blocks(&blocks, Some(&language_registry), None, cx).await; let scroll_handle = ScrollHandle::new(); @@ -251,7 +252,9 @@ fn show_hover( let (excerpt_id, _, _) = editor.buffer().read(cx).excerpt_containing(anchor, cx)?; - let language_registry = editor.project()?.read(cx).languages().clone(); + let language_registry = editor + .project() + .map(|project| project.read(cx).languages().clone()); let provider = editor.semantics_provider.clone()?; if !ignore_timeout { @@ -443,7 +446,8 @@ fn show_hover( text: format!("Unicode character U+{:02X}", invisible as u32), kind: HoverBlockKind::PlainText, }]; - let parsed_content = parse_blocks(&blocks, &language_registry, None, cx).await; + let parsed_content = + parse_blocks(&blocks, language_registry.as_ref(), None, cx).await; let scroll_handle = ScrollHandle::new(); let subscription = this .update(cx, |_, cx| { @@ -493,7 +497,8 @@ fn show_hover( let blocks = hover_result.contents; let language = hover_result.language; - let parsed_content = parse_blocks(&blocks, &language_registry, language, cx).await; + let parsed_content = + parse_blocks(&blocks, language_registry.as_ref(), language, cx).await; let scroll_handle = ScrollHandle::new(); hover_highlights.push(range.clone()); let subscription = this @@ -583,7 +588,7 @@ fn same_diagnostic_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anc async fn parse_blocks( blocks: &[HoverBlock], - language_registry: &Arc<LanguageRegistry>, + language_registry: Option<&Arc<LanguageRegistry>>, language: Option<Arc<Language>>, cx: &mut AsyncWindowContext, ) -> Option<Entity<Markdown>> { @@ -603,7 +608,7 @@ async fn parse_blocks( .new_window_entity(|_window, cx| { Markdown::new( combined_text.into(), - Some(language_registry.clone()), + language_registry.cloned(), language.map(|language| language.name()), cx, ) From 176c445817c431ec2557d2df074d97e600983b96 Mon Sep 17 00:00:00 2001 From: 0x5457 <0x5457@protonmail.com> Date: Tue, 19 Aug 2025 15:28:24 +0800 Subject: [PATCH 467/693] Avoid symlink conflicts when re-extracting `eslint-xx.tar.gz` (#36068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #34325 **Background** When upgrading/reinstalling the ESLint language server, extracting the archive over an existing version directory that contains symlinks can fail and interrupt the installation. ``` failed to unpack .../vscode-eslint-2.4.4/.../client/src/shared File exists (os error 17) when symlinking ../../$shared/ to .../client/src/shared ``` **Root cause** Extracting into a non-empty directory conflicts with leftover files/symlinks (e.g., `client/src/shared -> ../../$shared`), causing “File exists (os error 17)”. When `fs::metadata(&server_path).await.is_err()` is true, the code falls back to cached_server_binary, but that still targets the same (potentially corrupted/half-installed) directory and does not run `npm install` or `npm run compile`, so the system cannot recover and remains broken. **Change** Before downloading and extracting, delete the target version directory (vscode-eslint-<version>) to ensure an empty extraction destination and avoid conflicts. **Alternative approaches** temp directory + rename: extract into a clean temp directory and rename into place to avoid half-installed states [async-tar](https://github.com/dignifiedquire/async-tar) enhancement: tolerate already-existing symlinks (or add a “replace-existing” option). Release Notes: - Fixed eslint installation not clearing files after previous attempts' --- crates/languages/src/typescript.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index d477acc7f6..7937adbc09 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -910,7 +910,7 @@ impl LspAdapter for EsLintLspAdapter { let server_path = destination_path.join(Self::SERVER_PATH); if fs::metadata(&server_path).await.is_err() { - remove_matching(&container_dir, |entry| entry != destination_path).await; + remove_matching(&container_dir, |_| true).await; download_server_binary( delegate, From 1fbb318714624e5fa1e7fdd5e97cfa325ae0b5ca Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Tue, 19 Aug 2025 11:06:35 +0300 Subject: [PATCH 468/693] Fix iterator related clippy style lint violations (#36437) Release Notes: - N/A --- Cargo.toml | 5 +++++ crates/agent_ui/src/agent_diff.rs | 3 +-- crates/agent_ui/src/message_editor.rs | 6 +----- crates/agent_ui/src/text_thread_editor.rs | 6 +----- .../debugger_ui/src/session/running/variable_list.rs | 2 +- crates/editor/src/editor.rs | 2 +- crates/git_ui/src/git_panel.rs | 10 +++++----- crates/git_ui/src/project_diff.rs | 2 +- crates/language/src/proto.rs | 2 +- crates/language_tools/src/key_context_view.rs | 4 +--- crates/settings_ui/src/keybindings.rs | 8 +------- crates/title_bar/src/collab.rs | 2 +- crates/vim/src/normal.rs | 2 +- 13 files changed, 21 insertions(+), 33 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3854ebe010..b61eb3c260 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -820,6 +820,11 @@ single_range_in_vec_init = "allow" style = { level = "allow", priority = -1 } # Temporary list of style lints that we've fixed so far. +iter_cloned_collect = "warn" +iter_next_slice = "warn" +iter_nth = "warn" +iter_nth_zero = "warn" +iter_skip_next = "warn" module_inception = { level = "deny" } question_mark = { level = "deny" } redundant_closure = { level = "deny" } diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 85e7297810..3522a0c9ab 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -503,8 +503,7 @@ fn update_editor_selection( &[last_kept_hunk_end..editor::Anchor::max()], buffer_snapshot, ) - .skip(1) - .next() + .nth(1) }) .or_else(|| { let first_kept_hunk = diff_hunks.first()?; diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index ddb51154f5..64c9a873f5 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -690,11 +690,7 @@ impl MessageEditor { .as_ref() .map(|model| { self.incompatible_tools_state.update(cx, |state, cx| { - state - .incompatible_tools(&model.model, cx) - .iter() - .cloned() - .collect::<Vec<_>>() + state.incompatible_tools(&model.model, cx).to_vec() }) }) .unwrap_or_default(); diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 8c1e163eca..376d3c54fd 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -747,11 +747,7 @@ impl TextThreadEditor { self.context.read(cx).invoked_slash_command(&command_id) { if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status { - let run_commands_in_ranges = invoked_slash_command - .run_commands_in_ranges - .iter() - .cloned() - .collect::<Vec<_>>(); + let run_commands_in_ranges = invoked_slash_command.run_commands_in_ranges.clone(); for range in run_commands_in_ranges { let commands = self.context.update(cx, |context, cx| { context.reparse(cx); diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index b54ee29e15..3cc5fbc272 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -272,7 +272,7 @@ impl VariableList { let mut entries = vec![]; let scopes: Vec<_> = self.session.update(cx, |session, cx| { - session.scopes(stack_frame_id, cx).iter().cloned().collect() + session.scopes(stack_frame_id, cx).to_vec() }); let mut contains_local_scope = false; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 365cd1ea5a..a49f1dba86 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -20932,7 +20932,7 @@ impl Editor { let existing_pending = self .text_highlights::<PendingInput>(cx) - .map(|(_, ranges)| ranges.iter().cloned().collect::<Vec<_>>()); + .map(|(_, ranges)| ranges.to_vec()); if existing_pending.is_none() && pending.is_empty() { return; } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index c21ac286cb..b1bdcdc3e0 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -2756,7 +2756,7 @@ impl GitPanel { for pending in self.pending.iter() { if pending.target_status == TargetStatus::Staged { pending_staged_count += pending.entries.len(); - last_pending_staged = pending.entries.iter().next().cloned(); + last_pending_staged = pending.entries.first().cloned(); } if let Some(single_staged) = &single_staged_entry { if pending @@ -5261,7 +5261,7 @@ mod tests { project .read(cx) .worktrees(cx) - .nth(0) + .next() .unwrap() .read(cx) .as_local() @@ -5386,7 +5386,7 @@ mod tests { project .read(cx) .worktrees(cx) - .nth(0) + .next() .unwrap() .read(cx) .as_local() @@ -5437,7 +5437,7 @@ mod tests { project .read(cx) .worktrees(cx) - .nth(0) + .next() .unwrap() .read(cx) .as_local() @@ -5486,7 +5486,7 @@ mod tests { project .read(cx) .worktrees(cx) - .nth(0) + .next() .unwrap() .read(cx) .as_local() diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index e312d6a2aa..09c5ce1152 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -280,7 +280,7 @@ impl ProjectDiff { fn button_states(&self, cx: &App) -> ButtonStates { let editor = self.editor.read(cx); let snapshot = self.multibuffer.read(cx).snapshot(cx); - let prev_next = snapshot.diff_hunks().skip(1).next().is_some(); + let prev_next = snapshot.diff_hunks().nth(1).is_some(); let mut selection = true; let mut ranges = editor diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index acae97019f..3be189cea0 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -86,7 +86,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation { proto::operation::UpdateCompletionTriggers { replica_id: lamport_timestamp.replica_id as u32, lamport_timestamp: lamport_timestamp.value, - triggers: triggers.iter().cloned().collect(), + triggers: triggers.clone(), language_server_id: server_id.to_proto(), }, ), diff --git a/crates/language_tools/src/key_context_view.rs b/crates/language_tools/src/key_context_view.rs index 88131781ec..320668cfc2 100644 --- a/crates/language_tools/src/key_context_view.rs +++ b/crates/language_tools/src/key_context_view.rs @@ -98,9 +98,7 @@ impl KeyContextView { cx.notify(); }); let sub2 = cx.observe_pending_input(window, |this, window, cx| { - this.pending_keystrokes = window - .pending_input_keystrokes() - .map(|k| k.iter().cloned().collect()); + this.pending_keystrokes = window.pending_input_keystrokes().map(|k| k.to_vec()); if this.pending_keystrokes.is_some() { this.last_keystrokes.take(); } diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 757a0ca226..b8c52602a6 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -472,13 +472,7 @@ impl KeymapEditor { fn current_keystroke_query(&self, cx: &App) -> Vec<Keystroke> { match self.search_mode { - SearchMode::KeyStroke { .. } => self - .keystroke_editor - .read(cx) - .keystrokes() - .iter() - .cloned() - .collect(), + SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(), SearchMode::Normal => Default::default(), } } diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index b458c64b5f..c2171d3899 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -601,7 +601,7 @@ fn pick_default_screen(cx: &App) -> Task<anyhow::Result<Option<Rc<dyn ScreenCapt .metadata() .is_ok_and(|meta| meta.is_main.unwrap_or_default()) }) - .or_else(|| available_sources.iter().next()) + .or_else(|| available_sources.first()) .cloned()) }) } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index b74d85b7c5..0c7b6e55a1 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -221,7 +221,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) { return; }; - let anchors = last_change.iter().cloned().collect::<Vec<_>>(); + let anchors = last_change.to_vec(); let mut last_row = None; let ranges: Vec<_> = anchors .iter() From ed14ab8c02e6c96e67053764da1f012df3ad7f74 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 19 Aug 2025 10:26:37 +0200 Subject: [PATCH 469/693] gpui: Introduce stacker to address stack overflows with deep layout trees (#35813) Co-authored-by: Anthony Eid <hello@anthonyeid.me> Co-authored-by: Lukas Wirth <lukas@zed.dev> Co-authored-by: Ben Kunkle <ben@zed.dev> Release Notes: - N/A Co-authored-by: Anthony Eid <hello@anthonyeid.me> Co-authored-by: Lukas Wirth <lukas@zed.dev> Co-authored-by: Ben Kunkle <ben@zed.dev> --- Cargo.lock | 35 +++++++++++++++++++++++++++++++++ Cargo.toml | 1 + crates/gpui/Cargo.toml | 1 + crates/gpui/src/elements/div.rs | 9 +++++++-- crates/gpui/src/taffy.rs | 15 +++++++++++--- 5 files changed, 56 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c05839ef3..2ef91c79c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7482,6 +7482,7 @@ dependencies = [ "slotmap", "smallvec", "smol", + "stacksafe", "strum 0.27.1", "sum_tree", "taffy", @@ -15541,6 +15542,40 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "stacker" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + +[[package]] +name = "stacksafe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9c1172965d317e87ddb6d364a040d958b40a1db82b6ef97da26253a8b3d090" +dependencies = [ + "stacker", + "stacksafe-macro", +] + +[[package]] +name = "stacksafe-macro" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172175341049678163e979d9107ca3508046d4d2a7c6682bee46ac541b17db69" +dependencies = [ + "proc-macro-error2", + "quote", + "syn 2.0.101", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index b61eb3c260..f326090b51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -590,6 +590,7 @@ simplelog = "0.12.2" smallvec = { version = "1.6", features = ["union"] } smol = "2.0" sqlformat = "0.2" +stacksafe = "0.1" streaming-iterator = "0.1" strsim = "0.11" strum = { version = "0.27.0", features = ["derive"] } diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 6be8c5fd1f..9f5b66087d 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -119,6 +119,7 @@ serde_json.workspace = true slotmap = "1.0.6" smallvec.workspace = true smol.workspace = true +stacksafe.workspace = true strum.workspace = true sum_tree.workspace = true taffy = "=0.9.0" diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 78114b7ecf..f553bf55f6 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -27,6 +27,7 @@ use crate::{ use collections::HashMap; use refineable::Refineable; use smallvec::SmallVec; +use stacksafe::{StackSafe, stacksafe}; use std::{ any::{Any, TypeId}, cell::RefCell, @@ -1195,7 +1196,7 @@ pub fn div() -> Div { /// A [`Div`] element, the all-in-one element for building complex UIs in GPUI pub struct Div { interactivity: Interactivity, - children: SmallVec<[AnyElement; 2]>, + children: SmallVec<[StackSafe<AnyElement>; 2]>, prepaint_listener: Option<Box<dyn Fn(Vec<Bounds<Pixels>>, &mut Window, &mut App) + 'static>>, image_cache: Option<Box<dyn ImageCacheProvider>>, } @@ -1256,7 +1257,8 @@ impl InteractiveElement for Div { impl ParentElement for Div { fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) { - self.children.extend(elements) + self.children + .extend(elements.into_iter().map(StackSafe::new)) } } @@ -1272,6 +1274,7 @@ impl Element for Div { self.interactivity.source_location() } + #[stacksafe] fn request_layout( &mut self, global_id: Option<&GlobalElementId>, @@ -1307,6 +1310,7 @@ impl Element for Div { (layout_id, DivFrameState { child_layout_ids }) } + #[stacksafe] fn prepaint( &mut self, global_id: Option<&GlobalElementId>, @@ -1376,6 +1380,7 @@ impl Element for Div { ) } + #[stacksafe] fn paint( &mut self, global_id: Option<&GlobalElementId>, diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index ee21ecd8c4..f78d6b30c7 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -3,6 +3,7 @@ use crate::{ }; use collections::{FxHashMap, FxHashSet}; use smallvec::SmallVec; +use stacksafe::{StackSafe, stacksafe}; use std::{fmt::Debug, ops::Range}; use taffy::{ TaffyTree, TraversePartialTree as _, @@ -11,8 +12,15 @@ use taffy::{ tree::NodeId, }; -type NodeMeasureFn = Box< - dyn FnMut(Size<Option<Pixels>>, Size<AvailableSpace>, &mut Window, &mut App) -> Size<Pixels>, +type NodeMeasureFn = StackSafe< + Box< + dyn FnMut( + Size<Option<Pixels>>, + Size<AvailableSpace>, + &mut Window, + &mut App, + ) -> Size<Pixels>, + >, >; struct NodeContext { @@ -88,7 +96,7 @@ impl TaffyLayoutEngine { .new_leaf_with_context( taffy_style, NodeContext { - measure: Box::new(measure), + measure: StackSafe::new(Box::new(measure)), }, ) .expect(EXPECT_MESSAGE) @@ -143,6 +151,7 @@ impl TaffyLayoutEngine { Ok(edges) } + #[stacksafe] pub fn compute_layout( &mut self, id: LayoutId, From b8ddb0141c0625a47fdc7b68aa8a8a782c439f62 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Tue, 19 Aug 2025 11:12:57 +0200 Subject: [PATCH 470/693] agent2: Port rules UI (#36429) Release Notes: - N/A --- crates/agent2/src/agent.rs | 19 +-- crates/agent2/src/tests/mod.rs | 10 +- crates/agent2/src/thread.rs | 20 +-- crates/agent2/src/tools/edit_file_tool.rs | 20 +-- crates/agent_ui/src/acp/thread_view.rs | 160 +++++++++++++++++++++- 5 files changed, 197 insertions(+), 32 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 985de4d123..6347f5f9a4 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -22,7 +22,6 @@ use prompt_store::{ }; use settings::update_settings_file; use std::any::Any; -use std::cell::RefCell; use std::collections::HashMap; use std::path::Path; use std::rc::Rc; @@ -156,7 +155,7 @@ pub struct NativeAgent { /// Session ID -> Session mapping sessions: HashMap<acp::SessionId, Session>, /// Shared project context for all threads - project_context: Rc<RefCell<ProjectContext>>, + project_context: Entity<ProjectContext>, project_context_needs_refresh: watch::Sender<()>, _maintain_project_context: Task<Result<()>>, context_server_registry: Entity<ContextServerRegistry>, @@ -200,7 +199,7 @@ impl NativeAgent { watch::channel(()); Self { sessions: HashMap::new(), - project_context: Rc::new(RefCell::new(project_context)), + project_context: cx.new(|_| project_context), project_context_needs_refresh: project_context_needs_refresh_tx, _maintain_project_context: cx.spawn(async move |this, cx| { Self::maintain_project_context(this, project_context_needs_refresh_rx, cx).await @@ -233,7 +232,9 @@ impl NativeAgent { Self::build_project_context(&this.project, this.prompt_store.as_ref(), cx) })? .await; - this.update(cx, |this, _| this.project_context.replace(project_context))?; + this.update(cx, |this, cx| { + this.project_context = cx.new(|_| project_context); + })?; } Ok(()) @@ -872,8 +873,8 @@ mod tests { ) .await .unwrap(); - agent.read_with(cx, |agent, _| { - assert_eq!(agent.project_context.borrow().worktrees, vec![]) + agent.read_with(cx, |agent, cx| { + assert_eq!(agent.project_context.read(cx).worktrees, vec![]) }); let worktree = project @@ -881,9 +882,9 @@ mod tests { .await .unwrap(); cx.run_until_parked(); - agent.read_with(cx, |agent, _| { + agent.read_with(cx, |agent, cx| { assert_eq!( - agent.project_context.borrow().worktrees, + agent.project_context.read(cx).worktrees, vec![WorktreeContext { root_name: "a".into(), abs_path: Path::new("/a").into(), @@ -898,7 +899,7 @@ mod tests { agent.read_with(cx, |agent, cx| { let rules_entry = worktree.read(cx).entry_for_path(".rules").unwrap(); assert_eq!( - agent.project_context.borrow().worktrees, + agent.project_context.read(cx).worktrees, vec![WorktreeContext { root_name: "a".into(), abs_path: Path::new("/a").into(), diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index e3e3050d49..13b37fbaa2 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -25,7 +25,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use settings::SettingsStore; use smol::stream::StreamExt; -use std::{cell::RefCell, path::Path, rc::Rc, sync::Arc, time::Duration}; +use std::{path::Path, rc::Rc, sync::Arc, time::Duration}; use util::path; mod test_tools; @@ -101,7 +101,9 @@ async fn test_system_prompt(cx: &mut TestAppContext) { } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); - project_context.borrow_mut().shell = "test-shell".into(); + project_context.update(cx, |project_context, _cx| { + project_context.shell = "test-shell".into() + }); thread.update(cx, |thread, _| thread.add_tool(EchoTool)); thread .update(cx, |thread, cx| { @@ -1447,7 +1449,7 @@ fn stop_events(result_events: Vec<Result<AgentResponseEvent>>) -> Vec<acp::StopR struct ThreadTest { model: Arc<dyn LanguageModel>, thread: Entity<Thread>, - project_context: Rc<RefCell<ProjectContext>>, + project_context: Entity<ProjectContext>, fs: Arc<FakeFs>, } @@ -1543,7 +1545,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { }) .await; - let project_context = Rc::new(RefCell::new(ProjectContext::default())); + let project_context = cx.new(|_cx| ProjectContext::default()); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let action_log = cx.new(|_| ActionLog::new(project.clone())); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index e0819abcc5..7f0465f5ce 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -25,7 +25,7 @@ use schemars::{JsonSchema, Schema}; use serde::{Deserialize, Serialize}; use settings::{Settings, update_settings_file}; use smol::stream::StreamExt; -use std::{cell::RefCell, collections::BTreeMap, path::Path, rc::Rc, sync::Arc}; +use std::{collections::BTreeMap, path::Path, sync::Arc}; use std::{fmt::Write, ops::Range}; use util::{ResultExt, markdown::MarkdownCodeBlock}; use uuid::Uuid; @@ -479,7 +479,7 @@ pub struct Thread { tool_use_limit_reached: bool, context_server_registry: Entity<ContextServerRegistry>, profile_id: AgentProfileId, - project_context: Rc<RefCell<ProjectContext>>, + project_context: Entity<ProjectContext>, templates: Arc<Templates>, model: Option<Arc<dyn LanguageModel>>, project: Entity<Project>, @@ -489,7 +489,7 @@ pub struct Thread { impl Thread { pub fn new( project: Entity<Project>, - project_context: Rc<RefCell<ProjectContext>>, + project_context: Entity<ProjectContext>, context_server_registry: Entity<ContextServerRegistry>, action_log: Entity<ActionLog>, templates: Arc<Templates>, @@ -520,6 +520,10 @@ impl Thread { &self.project } + pub fn project_context(&self) -> &Entity<ProjectContext> { + &self.project_context + } + pub fn action_log(&self) -> &Entity<ActionLog> { &self.action_log } @@ -750,10 +754,10 @@ impl Thread { Ok(events_rx) } - pub fn build_system_message(&self) -> LanguageModelRequestMessage { + pub fn build_system_message(&self, cx: &App) -> LanguageModelRequestMessage { log::debug!("Building system message"); let prompt = SystemPromptTemplate { - project: &self.project_context.borrow(), + project: &self.project_context.read(cx), available_tools: self.tools.keys().cloned().collect(), } .render(&self.templates) @@ -1030,7 +1034,7 @@ impl Thread { log::debug!("Completion intent: {:?}", completion_intent); log::debug!("Completion mode: {:?}", self.completion_mode); - let messages = self.build_request_messages(); + let messages = self.build_request_messages(cx); log::info!("Request will include {} messages", messages.len()); let tools = if let Some(tools) = self.tools(cx).log_err() { @@ -1101,12 +1105,12 @@ impl Thread { ))) } - fn build_request_messages(&self) -> Vec<LanguageModelRequestMessage> { + fn build_request_messages(&self, cx: &App) -> Vec<LanguageModelRequestMessage> { log::trace!( "Building request messages from {} thread messages", self.messages.len() ); - let mut messages = vec![self.build_system_message()]; + let mut messages = vec![self.build_system_message(cx)]; for message in &self.messages { match message { Message::User(message) => messages.push(message.to_request()), diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index e70e5e8a14..8ebd2936a5 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -503,9 +503,9 @@ mod tests { use fs::Fs; use gpui::{TestAppContext, UpdateGlobal}; use language_model::fake_provider::FakeLanguageModel; + use prompt_store::ProjectContext; use serde_json::json; use settings::SettingsStore; - use std::rc::Rc; use util::path; #[gpui::test] @@ -522,7 +522,7 @@ mod tests { let thread = cx.new(|cx| { Thread::new( project, - Rc::default(), + cx.new(|_cx| ProjectContext::default()), context_server_registry, action_log, Templates::new(), @@ -719,7 +719,7 @@ mod tests { let thread = cx.new(|cx| { Thread::new( project, - Rc::default(), + cx.new(|_cx| ProjectContext::default()), context_server_registry, action_log.clone(), Templates::new(), @@ -855,7 +855,7 @@ mod tests { let thread = cx.new(|cx| { Thread::new( project, - Rc::default(), + cx.new(|_cx| ProjectContext::default()), context_server_registry, action_log.clone(), Templates::new(), @@ -981,7 +981,7 @@ mod tests { let thread = cx.new(|cx| { Thread::new( project, - Rc::default(), + cx.new(|_cx| ProjectContext::default()), context_server_registry, action_log.clone(), Templates::new(), @@ -1118,7 +1118,7 @@ mod tests { let thread = cx.new(|cx| { Thread::new( project, - Rc::default(), + cx.new(|_cx| ProjectContext::default()), context_server_registry, action_log.clone(), Templates::new(), @@ -1228,7 +1228,7 @@ mod tests { let thread = cx.new(|cx| { Thread::new( project.clone(), - Rc::default(), + cx.new(|_cx| ProjectContext::default()), context_server_registry.clone(), action_log.clone(), Templates::new(), @@ -1309,7 +1309,7 @@ mod tests { let thread = cx.new(|cx| { Thread::new( project.clone(), - Rc::default(), + cx.new(|_cx| ProjectContext::default()), context_server_registry.clone(), action_log.clone(), Templates::new(), @@ -1393,7 +1393,7 @@ mod tests { let thread = cx.new(|cx| { Thread::new( project.clone(), - Rc::default(), + cx.new(|_cx| ProjectContext::default()), context_server_registry.clone(), action_log.clone(), Templates::new(), @@ -1474,7 +1474,7 @@ mod tests { let thread = cx.new(|cx| { Thread::new( project.clone(), - Rc::default(), + cx.new(|_cx| ProjectContext::default()), context_server_registry, action_log.clone(), Templates::new(), diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 2cfedfe840..2fffe1b179 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -30,7 +30,7 @@ use language::Buffer; use language_model::LanguageModelRegistry; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; -use project::Project; +use project::{Project, ProjectEntryId}; use prompt_store::PromptId; use rope::Point; use settings::{Settings as _, SettingsStore}; @@ -703,6 +703,38 @@ impl AcpThreadView { }) } + fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) { + let Some(thread) = self.as_native_thread(cx) else { + return; + }; + let project_context = thread.read(cx).project_context().read(cx); + + let project_entry_ids = project_context + .worktrees + .iter() + .flat_map(|worktree| worktree.rules_file.as_ref()) + .map(|rules_file| ProjectEntryId::from_usize(rules_file.project_entry_id)) + .collect::<Vec<_>>(); + + self.workspace + .update(cx, move |workspace, cx| { + // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules + // files clear. For example, if rules file 1 is already open but rules file 2 is not, + // this would open and focus rules file 2 in a tab that is not next to rules file 1. + let project = workspace.project().read(cx); + let project_paths = project_entry_ids + .into_iter() + .flat_map(|entry_id| project.path_for_entry(entry_id, cx)) + .collect::<Vec<_>>(); + for project_path in project_paths { + workspace + .open_path(project_path, None, true, window, cx) + .detach_and_log_err(cx); + } + }) + .ok(); + } + fn handle_thread_error(&mut self, error: anyhow::Error, cx: &mut Context<Self>) { self.thread_error = Some(ThreadError::from_err(error)); cx.notify(); @@ -858,6 +890,12 @@ impl AcpThreadView { let editor_focus = editor.focus_handle(cx).is_focused(window); let focus_border = cx.theme().colors().border_focused; + let rules_item = if entry_ix == 0 { + self.render_rules_item(cx) + } else { + None + }; + div() .id(("user_message", entry_ix)) .py_4() @@ -874,6 +912,7 @@ impl AcpThreadView { })) }) })) + .children(rules_item) .child( div() .relative() @@ -1862,6 +1901,125 @@ impl AcpThreadView { .into_any_element() } + fn render_rules_item(&self, cx: &Context<Self>) -> Option<AnyElement> { + let project_context = self + .as_native_thread(cx)? + .read(cx) + .project_context() + .read(cx); + + let user_rules_text = if project_context.user_rules.is_empty() { + None + } else if project_context.user_rules.len() == 1 { + let user_rules = &project_context.user_rules[0]; + + match user_rules.title.as_ref() { + Some(title) => Some(format!("Using \"{title}\" user rule")), + None => Some("Using user rule".into()), + } + } else { + Some(format!( + "Using {} user rules", + project_context.user_rules.len() + )) + }; + + let first_user_rules_id = project_context + .user_rules + .first() + .map(|user_rules| user_rules.uuid.0); + + let rules_files = project_context + .worktrees + .iter() + .filter_map(|worktree| worktree.rules_file.as_ref()) + .collect::<Vec<_>>(); + + let rules_file_text = match rules_files.as_slice() { + &[] => None, + &[rules_file] => Some(format!( + "Using project {:?} file", + rules_file.path_in_worktree + )), + rules_files => Some(format!("Using {} project rules files", rules_files.len())), + }; + + if user_rules_text.is_none() && rules_file_text.is_none() { + return None; + } + + Some( + v_flex() + .pt_2() + .px_2p5() + .gap_1() + .when_some(user_rules_text, |parent, user_rules_text| { + parent.child( + h_flex() + .w_full() + .child( + Icon::new(IconName::Reader) + .size(IconSize::XSmall) + .color(Color::Disabled), + ) + .child( + Label::new(user_rules_text) + .size(LabelSize::XSmall) + .color(Color::Muted) + .truncate() + .buffer_font(cx) + .ml_1p5() + .mr_0p5(), + ) + .child( + IconButton::new("open-prompt-library", IconName::ArrowUpRight) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(Color::Ignored) + // TODO: Figure out a way to pass focus handle here so we can display the `OpenRulesLibrary` keybinding + .tooltip(Tooltip::text("View User Rules")) + .on_click(move |_event, window, cx| { + window.dispatch_action( + Box::new(OpenRulesLibrary { + prompt_to_select: first_user_rules_id, + }), + cx, + ) + }), + ), + ) + }) + .when_some(rules_file_text, |parent, rules_file_text| { + parent.child( + h_flex() + .w_full() + .child( + Icon::new(IconName::File) + .size(IconSize::XSmall) + .color(Color::Disabled), + ) + .child( + Label::new(rules_file_text) + .size(LabelSize::XSmall) + .color(Color::Muted) + .buffer_font(cx) + .ml_1p5() + .mr_0p5(), + ) + .child( + IconButton::new("open-rule", IconName::ArrowUpRight) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(Color::Ignored) + .on_click(cx.listener(Self::handle_open_rules)) + .tooltip(Tooltip::text("View Rules")), + ), + ) + }) + .into_any(), + ) + } + fn render_empty_state(&self, cx: &App) -> AnyElement { let loading = matches!(&self.thread_state, ThreadState::Loading { .. }); From 47e1d4511cda45a2044435523209282ffd2f8627 Mon Sep 17 00:00:00 2001 From: Smit Barmase <heysmitbarmase@gmail.com> Date: Tue, 19 Aug 2025 14:43:41 +0530 Subject: [PATCH 471/693] editor: Fix `edit_predictions_disabled_in` not disabling predictions (#36469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #25744 Only setting changes and editor init determined whether to show predictions, so glob patterns and toggles correctly disabled them. On cursor changes we call `update_visible_edit_prediction`, but we weren’t discarding predictions when the scope changed. This PR fixes that. Release Notes: - Fixed an issue where the `edit_predictions_disabled_in` setting was ignored in some cases. --- crates/editor/src/edit_prediction_tests.rs | 42 +++++++++++++++++++++- crates/editor/src/editor.rs | 8 +++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/edit_prediction_tests.rs b/crates/editor/src/edit_prediction_tests.rs index 7bf51e45d7..bba632e81f 100644 --- a/crates/editor/src/edit_prediction_tests.rs +++ b/crates/editor/src/edit_prediction_tests.rs @@ -7,7 +7,9 @@ use std::ops::Range; use text::{Point, ToOffset}; use crate::{ - EditPrediction, editor_tests::init_test, test::editor_test_context::EditorTestContext, + EditPrediction, + editor_tests::{init_test, update_test_language_settings}, + test::editor_test_context::EditorTestContext, }; #[gpui::test] @@ -271,6 +273,44 @@ async fn test_edit_prediction_jump_disabled_for_non_zed_providers(cx: &mut gpui: }); } +#[gpui::test] +async fn test_edit_predictions_disabled_in_scope(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + update_test_language_settings(cx, |settings| { + settings.defaults.edit_predictions_disabled_in = Some(vec!["string".to_string()]); + }); + + let mut cx = EditorTestContext::new(cx).await; + let provider = cx.new(|_| FakeEditPredictionProvider::default()); + assign_editor_completion_provider(provider.clone(), &mut cx); + + let language = languages::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into()); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + // Test disabled inside of string + cx.set_state("const x = \"hello ˇworld\";"); + propose_edits(&provider, vec![(17..17, "beautiful ")], &mut cx); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); + cx.editor(|editor, _, _| { + assert!( + editor.active_edit_prediction.is_none(), + "Edit predictions should be disabled in string scopes when configured in edit_predictions_disabled_in" + ); + }); + + // Test enabled outside of string + cx.set_state("const x = \"hello world\"; ˇ"); + propose_edits(&provider, vec![(24..24, "// comment")], &mut cx); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); + cx.editor(|editor, _, _| { + assert!( + editor.active_edit_prediction.is_some(), + "Edit predictions should work outside of disabled scopes" + ); + }); +} + fn assert_editor_active_edit_completion( cx: &mut EditorTestContext, assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range<Anchor>, String)>), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a49f1dba86..c52a59a909 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7764,6 +7764,14 @@ impl Editor { self.edit_prediction_settings = self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx); + match self.edit_prediction_settings { + EditPredictionSettings::Disabled => { + self.discard_edit_prediction(false, cx); + return None; + } + _ => {} + }; + self.edit_prediction_indent_conflict = multibuffer.is_line_whitespace_upto(cursor); if self.edit_prediction_indent_conflict { From 0ea0d466d289ff2c57bdac3ab4b20f61c9ab7494 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Tue, 19 Aug 2025 11:41:55 +0200 Subject: [PATCH 472/693] agent2: Port retry logic (#36421) Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 15 ++ crates/agent2/src/agent.rs | 5 + crates/agent2/src/tests/mod.rs | 166 +++++++++++- crates/agent2/src/thread.rs | 286 ++++++++++++++++++--- crates/agent_ui/src/acp/thread_view.rs | 61 ++++- crates/agent_ui/src/agent_diff.rs | 1 + crates/language_model/src/fake_provider.rs | 32 ++- 7 files changed, 514 insertions(+), 52 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 8bc0635475..916f48cbe0 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -24,6 +24,7 @@ use std::fmt::{Formatter, Write}; use std::ops::Range; use std::process::ExitStatus; use std::rc::Rc; +use std::time::{Duration, Instant}; use std::{fmt::Display, mem, path::PathBuf, sync::Arc}; use ui::App; use util::ResultExt; @@ -658,6 +659,15 @@ impl PlanEntry { } } +#[derive(Debug, Clone)] +pub struct RetryStatus { + pub last_error: SharedString, + pub attempt: usize, + pub max_attempts: usize, + pub started_at: Instant, + pub duration: Duration, +} + pub struct AcpThread { title: SharedString, entries: Vec<AgentThreadEntry>, @@ -676,6 +686,7 @@ pub enum AcpThreadEvent { EntryUpdated(usize), EntriesRemoved(Range<usize>), ToolAuthorizationRequired, + Retry(RetryStatus), Stopped, Error, ServerExited(ExitStatus), @@ -916,6 +927,10 @@ impl AcpThread { cx.emit(AcpThreadEvent::NewEntry); } + pub fn update_retry_status(&mut self, status: RetryStatus, cx: &mut Context<Self>) { + cx.emit(AcpThreadEvent::Retry(status)); + } + pub fn update_tool_call( &mut self, update: impl Into<ToolCallUpdate>, diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 6347f5f9a4..480b2baa95 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -546,6 +546,11 @@ impl NativeAgentConnection { thread.update_tool_call(update, cx) })??; } + AgentResponseEvent::Retry(status) => { + acp_thread.update(cx, |thread, cx| { + thread.update_retry_status(status, cx) + })?; + } AgentResponseEvent::Stop(stop_reason) => { log::debug!("Assistant message complete: {:?}", stop_reason); return Ok(acp::PromptResponse { stop_reason }); diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 13b37fbaa2..c83479f2cf 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -6,15 +6,16 @@ use agent_settings::AgentProfileId; use anyhow::Result; use client::{Client, UserStore}; use fs::{FakeFs, Fs}; -use futures::channel::mpsc::UnboundedReceiver; +use futures::{StreamExt, channel::mpsc::UnboundedReceiver}; use gpui::{ App, AppContext, Entity, Task, TestAppContext, UpdateGlobal, http_client::FakeHttpClient, }; use indoc::indoc; use language_model::{ - LanguageModel, LanguageModelCompletionEvent, LanguageModelId, LanguageModelRegistry, - LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse, MessageContent, - Role, StopReason, fake_provider::FakeLanguageModel, + LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, + LanguageModelProviderName, LanguageModelRegistry, LanguageModelRequestMessage, + LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role, StopReason, + fake_provider::FakeLanguageModel, }; use pretty_assertions::assert_eq; use project::Project; @@ -24,7 +25,6 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::json; use settings::SettingsStore; -use smol::stream::StreamExt; use std::{path::Path, rc::Rc, sync::Arc, time::Duration}; use util::path; @@ -1435,6 +1435,162 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) { ); } +#[gpui::test] +async fn test_send_no_retry_on_success(cx: &mut TestAppContext) { + let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let mut events = thread + .update(cx, |thread, cx| { + thread.set_completion_mode(agent_settings::CompletionMode::Burn); + thread.send(UserMessageId::new(), ["Hello!"], cx) + }) + .unwrap(); + cx.run_until_parked(); + + fake_model.send_last_completion_stream_text_chunk("Hey!"); + fake_model.end_last_completion_stream(); + + let mut retry_events = Vec::new(); + while let Some(Ok(event)) = events.next().await { + match event { + AgentResponseEvent::Retry(retry_status) => { + retry_events.push(retry_status); + } + AgentResponseEvent::Stop(..) => break, + _ => {} + } + } + + assert_eq!(retry_events.len(), 0); + thread.read_with(cx, |thread, _cx| { + assert_eq!( + thread.to_markdown(), + indoc! {" + ## User + + Hello! + + ## Assistant + + Hey! + "} + ) + }); +} + +#[gpui::test] +async fn test_send_retry_on_error(cx: &mut TestAppContext) { + let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let mut events = thread + .update(cx, |thread, cx| { + thread.set_completion_mode(agent_settings::CompletionMode::Burn); + thread.send(UserMessageId::new(), ["Hello!"], cx) + }) + .unwrap(); + cx.run_until_parked(); + + fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded { + provider: LanguageModelProviderName::new("Anthropic"), + retry_after: Some(Duration::from_secs(3)), + }); + fake_model.end_last_completion_stream(); + + cx.executor().advance_clock(Duration::from_secs(3)); + cx.run_until_parked(); + + fake_model.send_last_completion_stream_text_chunk("Hey!"); + fake_model.end_last_completion_stream(); + + let mut retry_events = Vec::new(); + while let Some(Ok(event)) = events.next().await { + match event { + AgentResponseEvent::Retry(retry_status) => { + retry_events.push(retry_status); + } + AgentResponseEvent::Stop(..) => break, + _ => {} + } + } + + assert_eq!(retry_events.len(), 1); + assert!(matches!( + retry_events[0], + acp_thread::RetryStatus { attempt: 1, .. } + )); + thread.read_with(cx, |thread, _cx| { + assert_eq!( + thread.to_markdown(), + indoc! {" + ## User + + Hello! + + ## Assistant + + Hey! + "} + ) + }); +} + +#[gpui::test] +async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) { + let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let mut events = thread + .update(cx, |thread, cx| { + thread.set_completion_mode(agent_settings::CompletionMode::Burn); + thread.send(UserMessageId::new(), ["Hello!"], cx) + }) + .unwrap(); + cx.run_until_parked(); + + for _ in 0..crate::thread::MAX_RETRY_ATTEMPTS + 1 { + fake_model.send_last_completion_stream_error( + LanguageModelCompletionError::ServerOverloaded { + provider: LanguageModelProviderName::new("Anthropic"), + retry_after: Some(Duration::from_secs(3)), + }, + ); + fake_model.end_last_completion_stream(); + cx.executor().advance_clock(Duration::from_secs(3)); + cx.run_until_parked(); + } + + let mut errors = Vec::new(); + let mut retry_events = Vec::new(); + while let Some(event) = events.next().await { + match event { + Ok(AgentResponseEvent::Retry(retry_status)) => { + retry_events.push(retry_status); + } + Ok(AgentResponseEvent::Stop(..)) => break, + Err(error) => errors.push(error), + _ => {} + } + } + + assert_eq!( + retry_events.len(), + crate::thread::MAX_RETRY_ATTEMPTS as usize + ); + for i in 0..crate::thread::MAX_RETRY_ATTEMPTS as usize { + assert_eq!(retry_events[i].attempt, i + 1); + } + assert_eq!(errors.len(), 1); + let error = errors[0] + .downcast_ref::<LanguageModelCompletionError>() + .unwrap(); + assert!(matches!( + error, + LanguageModelCompletionError::ServerOverloaded { .. } + )); +} + /// Filters out the stop events for asserting against in tests fn stop_events(result_events: Vec<Result<AgentResponseEvent>>) -> Vec<acp::StopReason> { result_events diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 7f0465f5ce..beb780850c 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -12,12 +12,12 @@ use futures::{ channel::{mpsc, oneshot}, stream::FuturesUnordered, }; -use gpui::{App, Context, Entity, SharedString, Task}; +use gpui::{App, AsyncApp, Context, Entity, SharedString, Task, WeakEntity}; use language_model::{ - LanguageModel, LanguageModelCompletionEvent, LanguageModelImage, LanguageModelProviderId, - LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, - LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, - LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason, + LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage, + LanguageModelProviderId, LanguageModelRequest, LanguageModelRequestMessage, + LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent, + LanguageModelToolSchemaFormat, LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason, }; use project::Project; use prompt_store::ProjectContext; @@ -25,7 +25,12 @@ use schemars::{JsonSchema, Schema}; use serde::{Deserialize, Serialize}; use settings::{Settings, update_settings_file}; use smol::stream::StreamExt; -use std::{collections::BTreeMap, path::Path, sync::Arc}; +use std::{ + collections::BTreeMap, + path::Path, + sync::Arc, + time::{Duration, Instant}, +}; use std::{fmt::Write, ops::Range}; use util::{ResultExt, markdown::MarkdownCodeBlock}; use uuid::Uuid; @@ -71,6 +76,21 @@ impl std::fmt::Display for PromptId { } } +pub(crate) const MAX_RETRY_ATTEMPTS: u8 = 4; +pub(crate) const BASE_RETRY_DELAY: Duration = Duration::from_secs(5); + +#[derive(Debug, Clone)] +enum RetryStrategy { + ExponentialBackoff { + initial_delay: Duration, + max_attempts: u8, + }, + Fixed { + delay: Duration, + max_attempts: u8, + }, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum Message { User(UserMessage), @@ -455,6 +475,7 @@ pub enum AgentResponseEvent { ToolCall(acp::ToolCall), ToolCallUpdate(acp_thread::ToolCallUpdate), ToolCallAuthorization(ToolCallAuthorization), + Retry(acp_thread::RetryStatus), Stop(acp::StopReason), } @@ -662,41 +683,18 @@ impl Thread { })??; log::info!("Calling model.stream_completion"); - let mut events = model.stream_completion(request, cx).await?; - log::debug!("Stream completion started successfully"); let mut tool_use_limit_reached = false; - let mut tool_uses = FuturesUnordered::new(); - while let Some(event) = events.next().await { - match event? { - LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::ToolUseLimitReached, - ) => { - tool_use_limit_reached = true; - } - LanguageModelCompletionEvent::Stop(reason) => { - event_stream.send_stop(reason); - if reason == StopReason::Refusal { - this.update(cx, |this, _cx| { - this.flush_pending_message(); - this.messages.truncate(message_ix); - })?; - return Ok(()); - } - } - event => { - log::trace!("Received completion event: {:?}", event); - this.update(cx, |this, cx| { - tool_uses.extend(this.handle_streamed_completion_event( - event, - &event_stream, - cx, - )); - }) - .ok(); - } - } - } + let mut tool_uses = Self::stream_completion_with_retries( + this.clone(), + model.clone(), + request, + message_ix, + &event_stream, + &mut tool_use_limit_reached, + cx, + ) + .await?; let used_tools = tool_uses.is_empty(); while let Some(tool_result) = tool_uses.next().await { @@ -754,10 +752,105 @@ impl Thread { Ok(events_rx) } + async fn stream_completion_with_retries( + this: WeakEntity<Self>, + model: Arc<dyn LanguageModel>, + request: LanguageModelRequest, + message_ix: usize, + event_stream: &AgentResponseEventStream, + tool_use_limit_reached: &mut bool, + cx: &mut AsyncApp, + ) -> Result<FuturesUnordered<Task<LanguageModelToolResult>>> { + log::debug!("Stream completion started successfully"); + + let mut attempt = None; + 'retry: loop { + let mut events = model.stream_completion(request.clone(), cx).await?; + let mut tool_uses = FuturesUnordered::new(); + while let Some(event) = events.next().await { + match event { + Ok(LanguageModelCompletionEvent::StatusUpdate( + CompletionRequestStatus::ToolUseLimitReached, + )) => { + *tool_use_limit_reached = true; + } + Ok(LanguageModelCompletionEvent::Stop(reason)) => { + event_stream.send_stop(reason); + if reason == StopReason::Refusal { + this.update(cx, |this, _cx| { + this.flush_pending_message(); + this.messages.truncate(message_ix); + })?; + return Ok(tool_uses); + } + } + Ok(event) => { + log::trace!("Received completion event: {:?}", event); + this.update(cx, |this, cx| { + tool_uses.extend(this.handle_streamed_completion_event( + event, + event_stream, + cx, + )); + }) + .ok(); + } + Err(error) => { + let completion_mode = + this.read_with(cx, |thread, _cx| thread.completion_mode())?; + if completion_mode == CompletionMode::Normal { + return Err(error.into()); + } + + let Some(strategy) = Self::retry_strategy_for(&error) else { + return Err(error.into()); + }; + + let max_attempts = match &strategy { + RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts, + RetryStrategy::Fixed { max_attempts, .. } => *max_attempts, + }; + + let attempt = attempt.get_or_insert(0u8); + + *attempt += 1; + + let attempt = *attempt; + if attempt > max_attempts { + return Err(error.into()); + } + + let delay = match &strategy { + RetryStrategy::ExponentialBackoff { initial_delay, .. } => { + let delay_secs = + initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32); + Duration::from_secs(delay_secs) + } + RetryStrategy::Fixed { delay, .. } => *delay, + }; + log::debug!("Retry attempt {attempt} with delay {delay:?}"); + + event_stream.send_retry(acp_thread::RetryStatus { + last_error: error.to_string().into(), + attempt: attempt as usize, + max_attempts: max_attempts as usize, + started_at: Instant::now(), + duration: delay, + }); + + cx.background_executor().timer(delay).await; + continue 'retry; + } + } + } + return Ok(tool_uses); + } + } + pub fn build_system_message(&self, cx: &App) -> LanguageModelRequestMessage { log::debug!("Building system message"); let prompt = SystemPromptTemplate { - project: &self.project_context.read(cx), + project: self.project_context.read(cx), available_tools: self.tools.keys().cloned().collect(), } .render(&self.templates) @@ -1158,6 +1251,113 @@ impl Thread { fn advance_prompt_id(&mut self) { self.prompt_id = PromptId::new(); } + + fn retry_strategy_for(error: &LanguageModelCompletionError) -> Option<RetryStrategy> { + use LanguageModelCompletionError::*; + use http_client::StatusCode; + + // General strategy here: + // - If retrying won't help (e.g. invalid API key or payload too large), return None so we don't retry at all. + // - If it's a time-based issue (e.g. server overloaded, rate limit exceeded), retry up to 4 times with exponential backoff. + // - If it's an issue that *might* be fixed by retrying (e.g. internal server error), retry up to 3 times. + match error { + HttpResponseError { + status_code: StatusCode::TOO_MANY_REQUESTS, + .. + } => Some(RetryStrategy::ExponentialBackoff { + initial_delay: BASE_RETRY_DELAY, + max_attempts: MAX_RETRY_ATTEMPTS, + }), + ServerOverloaded { retry_after, .. } | RateLimitExceeded { retry_after, .. } => { + Some(RetryStrategy::Fixed { + delay: retry_after.unwrap_or(BASE_RETRY_DELAY), + max_attempts: MAX_RETRY_ATTEMPTS, + }) + } + UpstreamProviderError { + status, + retry_after, + .. + } => match *status { + StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE => { + Some(RetryStrategy::Fixed { + delay: retry_after.unwrap_or(BASE_RETRY_DELAY), + max_attempts: MAX_RETRY_ATTEMPTS, + }) + } + StatusCode::INTERNAL_SERVER_ERROR => Some(RetryStrategy::Fixed { + delay: retry_after.unwrap_or(BASE_RETRY_DELAY), + // Internal Server Error could be anything, retry up to 3 times. + max_attempts: 3, + }), + status => { + // There is no StatusCode variant for the unofficial HTTP 529 ("The service is overloaded"), + // but we frequently get them in practice. See https://http.dev/529 + if status.as_u16() == 529 { + Some(RetryStrategy::Fixed { + delay: retry_after.unwrap_or(BASE_RETRY_DELAY), + max_attempts: MAX_RETRY_ATTEMPTS, + }) + } else { + Some(RetryStrategy::Fixed { + delay: retry_after.unwrap_or(BASE_RETRY_DELAY), + max_attempts: 2, + }) + } + } + }, + ApiInternalServerError { .. } => Some(RetryStrategy::Fixed { + delay: BASE_RETRY_DELAY, + max_attempts: 3, + }), + ApiReadResponseError { .. } + | HttpSend { .. } + | DeserializeResponse { .. } + | BadRequestFormat { .. } => Some(RetryStrategy::Fixed { + delay: BASE_RETRY_DELAY, + max_attempts: 3, + }), + // Retrying these errors definitely shouldn't help. + HttpResponseError { + status_code: + StatusCode::PAYLOAD_TOO_LARGE | StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED, + .. + } + | AuthenticationError { .. } + | PermissionError { .. } + | NoApiKey { .. } + | ApiEndpointNotFound { .. } + | PromptTooLarge { .. } => None, + // These errors might be transient, so retry them + SerializeRequest { .. } | BuildRequestBody { .. } => Some(RetryStrategy::Fixed { + delay: BASE_RETRY_DELAY, + max_attempts: 1, + }), + // Retry all other 4xx and 5xx errors once. + HttpResponseError { status_code, .. } + if status_code.is_client_error() || status_code.is_server_error() => + { + Some(RetryStrategy::Fixed { + delay: BASE_RETRY_DELAY, + max_attempts: 3, + }) + } + Other(err) + if err.is::<language_model::PaymentRequiredError>() + || err.is::<language_model::ModelRequestLimitReachedError>() => + { + // Retrying won't help for Payment Required or Model Request Limit errors (where + // the user must upgrade to usage-based billing to get more requests, or else wait + // for a significant amount of time for the request limit to reset). + None + } + // Conservatively assume that any other errors are non-retryable + HttpResponseError { .. } | Other(..) => Some(RetryStrategy::Fixed { + delay: BASE_RETRY_DELAY, + max_attempts: 2, + }), + } + } } struct RunningTurn { @@ -1367,6 +1567,12 @@ impl AgentResponseEventStream { .ok(); } + fn send_retry(&self, status: acp_thread::RetryStatus) { + self.0 + .unbounded_send(Ok(AgentResponseEvent::Retry(status))) + .ok(); + } + fn send_stop(&self, reason: StopReason) { match reason { StopReason::EndTurn => { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 2fffe1b179..370dae53e4 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1,7 +1,7 @@ use acp_thread::{ AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, - AuthRequired, LoadError, MentionUri, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, - UserMessageId, + AuthRequired, LoadError, MentionUri, RetryStatus, ThreadStatus, ToolCall, ToolCallContent, + ToolCallStatus, UserMessageId, }; use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; @@ -35,6 +35,7 @@ use prompt_store::PromptId; use rope::Point; use settings::{Settings as _, SettingsStore}; use std::sync::Arc; +use std::time::Instant; use std::{collections::BTreeMap, process::ExitStatus, rc::Rc, time::Duration}; use text::Anchor; use theme::ThemeSettings; @@ -115,6 +116,7 @@ pub struct AcpThreadView { profile_selector: Option<Entity<ProfileSelector>>, notifications: Vec<WindowHandle<AgentNotification>>, notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>, + thread_retry_status: Option<RetryStatus>, thread_error: Option<ThreadError>, list_state: ListState, scrollbar_state: ScrollbarState, @@ -209,6 +211,7 @@ impl AcpThreadView { notification_subscriptions: HashMap::default(), list_state: list_state.clone(), scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), + thread_retry_status: None, thread_error: None, auth_task: None, expanded_tool_calls: HashSet::default(), @@ -445,6 +448,7 @@ impl AcpThreadView { pub fn cancel_generation(&mut self, cx: &mut Context<Self>) { self.thread_error.take(); + self.thread_retry_status.take(); if let Some(thread) = self.thread() { self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx))); @@ -775,7 +779,11 @@ impl AcpThreadView { AcpThreadEvent::ToolAuthorizationRequired => { self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx); } + AcpThreadEvent::Retry(retry) => { + self.thread_retry_status = Some(retry.clone()); + } AcpThreadEvent::Stopped => { + self.thread_retry_status.take(); let used_tools = thread.read(cx).used_tools_since_last_user_message(); self.notify_with_sound( if used_tools { @@ -789,6 +797,7 @@ impl AcpThreadView { ); } AcpThreadEvent::Error => { + self.thread_retry_status.take(); self.notify_with_sound( "Agent stopped due to an error", IconName::Warning, @@ -797,6 +806,7 @@ impl AcpThreadView { ); } AcpThreadEvent::ServerExited(status) => { + self.thread_retry_status.take(); self.thread_state = ThreadState::ServerExited { status: *status }; } } @@ -3413,7 +3423,51 @@ impl AcpThreadView { }) } - fn render_thread_error(&self, window: &mut Window, cx: &mut Context<'_, Self>) -> Option<Div> { + fn render_thread_retry_status_callout( + &self, + _window: &mut Window, + _cx: &mut Context<Self>, + ) -> Option<Callout> { + let state = self.thread_retry_status.as_ref()?; + + let next_attempt_in = state + .duration + .saturating_sub(Instant::now().saturating_duration_since(state.started_at)); + if next_attempt_in.is_zero() { + return None; + } + + let next_attempt_in_secs = next_attempt_in.as_secs() + 1; + + let retry_message = if state.max_attempts == 1 { + if next_attempt_in_secs == 1 { + "Retrying. Next attempt in 1 second.".to_string() + } else { + format!("Retrying. Next attempt in {next_attempt_in_secs} seconds.") + } + } else { + if next_attempt_in_secs == 1 { + format!( + "Retrying. Next attempt in 1 second (Attempt {} of {}).", + state.attempt, state.max_attempts, + ) + } else { + format!( + "Retrying. Next attempt in {next_attempt_in_secs} seconds (Attempt {} of {}).", + state.attempt, state.max_attempts, + ) + } + }; + + Some( + Callout::new() + .severity(Severity::Warning) + .title(state.last_error.clone()) + .description(retry_message), + ) + } + + fn render_thread_error(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> { let content = match self.thread_error.as_ref()? { ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx), ThreadError::PaymentRequired => self.render_payment_required_error(cx), @@ -3678,6 +3732,7 @@ impl Render for AcpThreadView { } _ => this, }) + .children(self.render_thread_retry_status_callout(window, cx)) .children(self.render_thread_error(window, cx)) .child(self.render_message_editor(window, cx)) } diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 3522a0c9ab..b0b06583a4 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1523,6 +1523,7 @@ impl AgentDiff { AcpThreadEvent::EntriesRemoved(_) | AcpThreadEvent::Stopped | AcpThreadEvent::ToolAuthorizationRequired + | AcpThreadEvent::Retry(_) | AcpThreadEvent::Error | AcpThreadEvent::ServerExited(_) => {} } diff --git a/crates/language_model/src/fake_provider.rs b/crates/language_model/src/fake_provider.rs index 67fba44887..ebfd37d16c 100644 --- a/crates/language_model/src/fake_provider.rs +++ b/crates/language_model/src/fake_provider.rs @@ -4,10 +4,11 @@ use crate::{ LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, }; -use futures::{FutureExt, StreamExt, channel::mpsc, future::BoxFuture, stream::BoxStream}; +use futures::{FutureExt, channel::mpsc, future::BoxFuture, stream::BoxStream}; use gpui::{AnyView, App, AsyncApp, Entity, Task, Window}; use http_client::Result; use parking_lot::Mutex; +use smol::stream::StreamExt; use std::sync::Arc; #[derive(Clone)] @@ -100,7 +101,9 @@ pub struct FakeLanguageModel { current_completion_txs: Mutex< Vec<( LanguageModelRequest, - mpsc::UnboundedSender<LanguageModelCompletionEvent>, + mpsc::UnboundedSender< + Result<LanguageModelCompletionEvent, LanguageModelCompletionError>, + >, )>, >, } @@ -150,7 +153,21 @@ impl FakeLanguageModel { .find(|(req, _)| req == request) .map(|(_, tx)| tx) .unwrap(); - tx.unbounded_send(event.into()).unwrap(); + tx.unbounded_send(Ok(event.into())).unwrap(); + } + + pub fn send_completion_stream_error( + &self, + request: &LanguageModelRequest, + error: impl Into<LanguageModelCompletionError>, + ) { + let current_completion_txs = self.current_completion_txs.lock(); + let tx = current_completion_txs + .iter() + .find(|(req, _)| req == request) + .map(|(_, tx)| tx) + .unwrap(); + tx.unbounded_send(Err(error.into())).unwrap(); } pub fn end_completion_stream(&self, request: &LanguageModelRequest) { @@ -170,6 +187,13 @@ impl FakeLanguageModel { self.send_completion_stream_event(self.pending_completions().last().unwrap(), event); } + pub fn send_last_completion_stream_error( + &self, + error: impl Into<LanguageModelCompletionError>, + ) { + self.send_completion_stream_error(self.pending_completions().last().unwrap(), error); + } + pub fn end_last_completion_stream(&self) { self.end_completion_stream(self.pending_completions().last().unwrap()); } @@ -229,7 +253,7 @@ impl LanguageModel for FakeLanguageModel { > { let (tx, rx) = mpsc::unbounded(); self.current_completion_txs.lock().push((request, tx)); - async move { Ok(rx.map(Ok).boxed()) }.boxed() + async move { Ok(rx.boxed()) }.boxed() } fn as_fake(&self) -> &Self { From 5df9c7c1c22f60cf48abbc9bb7b8519481923ed7 Mon Sep 17 00:00:00 2001 From: Lukas Wirth <lukas@zed.dev> Date: Tue, 19 Aug 2025 12:16:49 +0200 Subject: [PATCH 473/693] search: Fix project search query flickering (#36470) Release Notes: - N/A Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com> --- crates/search/src/project_search.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 056c3556ba..443bbb0427 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1925,13 +1925,15 @@ impl Render for ProjectSearchBar { let limit_reached = project_search.limit_reached; let color_override = match ( + &project_search.pending_search, project_search.no_results, &project_search.active_query, &project_search.last_search_query_text, ) { - (Some(true), Some(q), Some(p)) if q.as_str() == p => Some(Color::Error), + (None, Some(true), Some(q), Some(p)) if q.as_str() == p => Some(Color::Error), _ => None, }; + let match_text = search .active_match_index .and_then(|index| { From 97a31c59c99781e33143321849e7613c62acd482 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Tue, 19 Aug 2025 12:22:17 +0200 Subject: [PATCH 474/693] agent2: Fix agent location still being present after thread stopped (#36471) Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 2 ++ crates/agent_ui/src/agent_diff.rs | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 916f48cbe0..b86696d437 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1282,6 +1282,8 @@ impl AcpThread { .await?; this.update(cx, |this, cx| { + this.project + .update(cx, |project, cx| project.set_agent_location(None, cx)); match response { Ok(Err(e)) => { this.send_task.take(); diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index b0b06583a4..b010f8a424 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1520,12 +1520,12 @@ impl AgentDiff { self.update_reviewing_editors(workspace, window, cx); } } + AcpThreadEvent::Stopped | AcpThreadEvent::Error | AcpThreadEvent::ServerExited(_) => { + self.update_reviewing_editors(workspace, window, cx); + } AcpThreadEvent::EntriesRemoved(_) - | AcpThreadEvent::Stopped | AcpThreadEvent::ToolAuthorizationRequired - | AcpThreadEvent::Retry(_) - | AcpThreadEvent::Error - | AcpThreadEvent::ServerExited(_) => {} + | AcpThreadEvent::Retry(_) => {} } } From 790a2a0cfa603b0fcf1ddff29eab9434fcdc1e65 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Tue, 19 Aug 2025 12:40:02 +0200 Subject: [PATCH 475/693] agent2: Support `preferred_completion_mode` setting (#36473) Release Notes: - N/A --- crates/agent2/src/thread.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index beb780850c..f0b5d2f08a 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -522,7 +522,7 @@ impl Thread { id: ThreadId::new(), prompt_id: PromptId::new(), messages: Vec::new(), - completion_mode: CompletionMode::Normal, + completion_mode: AgentSettings::get_global(cx).preferred_completion_mode, running_turn: None, pending_message: None, tools: BTreeMap::default(), From e6d5a6a4fdb0ebcdfdc6c1f903cf98469934dcce Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Tue, 19 Aug 2025 12:59:34 +0200 Subject: [PATCH 476/693] agent: Remove `thread-auto-capture` feature (#36474) We never ended up using this in practice (the feature flag is not enabled for anyone, not even staff) Release Notes: - N/A --- Cargo.lock | 1 - crates/agent/Cargo.toml | 1 - crates/agent/src/thread.rs | 55 ----------------------- crates/feature_flags/src/feature_flags.rs | 8 ---- 4 files changed, 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ef91c79c9..d7edc54257 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,7 +131,6 @@ dependencies = [ "component", "context_server", "convert_case 0.8.0", - "feature_flags", "fs", "futures 0.3.31", "git", diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index 53ad2f4967..391abb38fe 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -31,7 +31,6 @@ collections.workspace = true component.workspace = true context_server.workspace = true convert_case.workspace = true -feature_flags.workspace = true fs.workspace = true futures.workspace = true git.workspace = true diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 469135a967..a3f903a60d 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -16,7 +16,6 @@ use chrono::{DateTime, Utc}; use client::{ModelRequestUsage, RequestUsage}; use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit}; use collections::HashMap; -use feature_flags::{self, FeatureFlagAppExt}; use futures::{FutureExt, StreamExt as _, future::Shared}; use git::repository::DiffType; use gpui::{ @@ -388,7 +387,6 @@ pub struct Thread { feedback: Option<ThreadFeedback>, retry_state: Option<RetryState>, message_feedback: HashMap<MessageId, ThreadFeedback>, - last_auto_capture_at: Option<Instant>, last_received_chunk_at: Option<Instant>, request_callback: Option< Box<dyn FnMut(&LanguageModelRequest, &[Result<LanguageModelCompletionEvent, String>])>, @@ -489,7 +487,6 @@ impl Thread { feedback: None, retry_state: None, message_feedback: HashMap::default(), - last_auto_capture_at: None, last_error_context: None, last_received_chunk_at: None, request_callback: None, @@ -614,7 +611,6 @@ impl Thread { tool_use_limit_reached: serialized.tool_use_limit_reached, feedback: None, message_feedback: HashMap::default(), - last_auto_capture_at: None, last_error_context: None, last_received_chunk_at: None, request_callback: None, @@ -1033,8 +1029,6 @@ impl Thread { }); } - self.auto_capture_telemetry(cx); - message_id } @@ -1906,7 +1900,6 @@ impl Thread { cx.emit(ThreadEvent::StreamedCompletion); cx.notify(); - thread.auto_capture_telemetry(cx); Ok(()) })??; @@ -2081,8 +2074,6 @@ impl Thread { request_callback(request, response_events); } - thread.auto_capture_telemetry(cx); - if let Ok(initial_usage) = initial_token_usage { let usage = thread.cumulative_token_usage - initial_usage; @@ -2536,7 +2527,6 @@ impl Thread { model: Arc<dyn LanguageModel>, cx: &mut Context<Self>, ) -> Vec<PendingToolUse> { - self.auto_capture_telemetry(cx); let request = Arc::new(self.to_completion_request(model.clone(), CompletionIntent::ToolResults, cx)); let pending_tool_uses = self @@ -2745,7 +2735,6 @@ impl Thread { if !canceled { self.send_to_model(model.clone(), CompletionIntent::ToolResults, window, cx); } - self.auto_capture_telemetry(cx); } } @@ -3147,50 +3136,6 @@ impl Thread { &self.project } - pub fn auto_capture_telemetry(&mut self, cx: &mut Context<Self>) { - if !cx.has_flag::<feature_flags::ThreadAutoCaptureFeatureFlag>() { - return; - } - - let now = Instant::now(); - if let Some(last) = self.last_auto_capture_at { - if now.duration_since(last).as_secs() < 10 { - return; - } - } - - self.last_auto_capture_at = Some(now); - - let thread_id = self.id().clone(); - let github_login = self - .project - .read(cx) - .user_store() - .read(cx) - .current_user() - .map(|user| user.github_login.clone()); - let client = self.project.read(cx).client(); - let serialize_task = self.serialize(cx); - - cx.background_executor() - .spawn(async move { - if let Ok(serialized_thread) = serialize_task.await { - if let Ok(thread_data) = serde_json::to_value(serialized_thread) { - telemetry::event!( - "Agent Thread Auto-Captured", - thread_id = thread_id.to_string(), - thread_data = thread_data, - auto_capture_reason = "tracked_user", - github_login = github_login - ); - - client.telemetry().flush_events().await; - } - } - }) - .detach(); - } - pub fn cumulative_token_usage(&self) -> TokenUsage { self.cumulative_token_usage } diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index ef357adf35..f87932bfaf 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -77,14 +77,6 @@ impl FeatureFlag for NotebookFeatureFlag { const NAME: &'static str = "notebooks"; } -pub struct ThreadAutoCaptureFeatureFlag {} -impl FeatureFlag for ThreadAutoCaptureFeatureFlag { - const NAME: &'static str = "thread-auto-capture"; - - fn enabled_for_staff() -> bool { - false - } -} pub struct PanicFeatureFlag; impl FeatureFlag for PanicFeatureFlag { From 2fb89c9b3eb0f76f57f179a3cc4f0b37f2007b42 Mon Sep 17 00:00:00 2001 From: Vincent Durewski <vincent.durewski@gmail.com> Date: Tue, 19 Aug 2025 13:08:10 +0200 Subject: [PATCH 477/693] chore: Default settings: Comments: dock option (#36476) Minor tweak in the wording of the comments for the default settings regarding the `dock` option of the panels, in order to make them congruent across all panels. Release Notes: - N/A --- assets/settings/default.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 72e4dcbf4f..c290baf003 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -717,7 +717,7 @@ // Can be 'never', 'always', or 'when_in_call', // or a boolean (interpreted as 'never'/'always'). "button": "when_in_call", - // Where to the chat panel. Can be 'left' or 'right'. + // Where to dock the chat panel. Can be 'left' or 'right'. "dock": "right", // Default width of the chat panel. "default_width": 240 @@ -725,7 +725,7 @@ "git_panel": { // Whether to show the git panel button in the status bar. "button": true, - // Where to show the git panel. Can be 'left' or 'right'. + // Where to dock the git panel. Can be 'left' or 'right'. "dock": "left", // Default width of the git panel. "default_width": 360, From 9e8ec72bd5c697edc6b61f4e18542afc4e343a1b Mon Sep 17 00:00:00 2001 From: Lukas Wirth <lukas@zed.dev> Date: Tue, 19 Aug 2025 14:32:26 +0200 Subject: [PATCH 478/693] Revert "project: Handle `textDocument/didSave` and `textDocument/didChange` (un)registration and usage correctly (#36441)" (#36480) This reverts commit c5991e74bb6f305c299684dc7ac3f6ee9055efcd. This PR broke rust-analyzer's check on save function, so reverting for now Release Notes: - N/A --- crates/lsp/src/lsp.rs | 2 +- crates/project/src/lsp_store.rs | 72 ++++++++------------------------- 2 files changed, 17 insertions(+), 57 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index ce9e2fe229..366005a4ab 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -827,7 +827,7 @@ impl LanguageServer { }), synchronization: Some(TextDocumentSyncClientCapabilities { did_save: Some(true), - dynamic_registration: Some(true), + dynamic_registration: Some(false), ..TextDocumentSyncClientCapabilities::default() }), code_lens: Some(CodeLensClientCapabilities { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 1bc6770d4e..9410eea742 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -11820,28 +11820,8 @@ impl LspStore { .transpose()? { server.update_capabilities(|capabilities| { - let mut sync_options = - Self::take_text_document_sync_options(capabilities); - sync_options.change = Some(sync_kind); capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); - }); - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/didSave" => { - if let Some(save_options) = reg - .register_options - .and_then(|opts| opts.get("includeText").cloned()) - .map(serde_json::from_value::<lsp::TextDocumentSyncSaveOptions>) - .transpose()? - { - server.update_capabilities(|capabilities| { - let mut sync_options = - Self::take_text_document_sync_options(capabilities); - sync_options.save = Some(save_options); - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)); }); notify_server_capabilities_updated(&server, cx); } @@ -11993,19 +11973,7 @@ impl LspStore { } "textDocument/didChange" => { server.update_capabilities(|capabilities| { - let mut sync_options = Self::take_text_document_sync_options(capabilities); - sync_options.change = None; - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/didSave" => { - server.update_capabilities(|capabilities| { - let mut sync_options = Self::take_text_document_sync_options(capabilities); - sync_options.save = None; - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + capabilities.text_document_sync = None; }); notify_server_capabilities_updated(&server, cx); } @@ -12033,20 +12001,6 @@ impl LspStore { Ok(()) } - - fn take_text_document_sync_options( - capabilities: &mut lsp::ServerCapabilities, - ) -> lsp::TextDocumentSyncOptions { - match capabilities.text_document_sync.take() { - Some(lsp::TextDocumentSyncCapability::Options(sync_options)) => sync_options, - Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)) => { - let mut sync_options = lsp::TextDocumentSyncOptions::default(); - sync_options.change = Some(sync_kind); - sync_options - } - None => lsp::TextDocumentSyncOptions::default(), - } - } } // Registration with empty capabilities should be ignored. @@ -13149,18 +13103,24 @@ async fn populate_labels_for_symbols( fn include_text(server: &lsp::LanguageServer) -> Option<bool> { match server.capabilities().text_document_sync.as_ref()? { - lsp::TextDocumentSyncCapability::Options(opts) => match opts.save.as_ref()? { - // Server wants didSave but didn't specify includeText. - lsp::TextDocumentSyncSaveOptions::Supported(true) => Some(false), - // Server doesn't want didSave at all. - lsp::TextDocumentSyncSaveOptions::Supported(false) => None, - // Server provided SaveOptions. + lsp::TextDocumentSyncCapability::Kind(kind) => match *kind { + lsp::TextDocumentSyncKind::NONE => None, + lsp::TextDocumentSyncKind::FULL => Some(true), + lsp::TextDocumentSyncKind::INCREMENTAL => Some(false), + _ => None, + }, + lsp::TextDocumentSyncCapability::Options(options) => match options.save.as_ref()? { + lsp::TextDocumentSyncSaveOptions::Supported(supported) => { + if *supported { + Some(true) + } else { + None + } + } lsp::TextDocumentSyncSaveOptions::SaveOptions(save_options) => { Some(save_options.include_text.unwrap_or(false)) } }, - // We do not have any save info. Kind affects didChange only. - lsp::TextDocumentSyncCapability::Kind(_) => None, } } From 8f567383e4bef1914c2e349fc8e984cfa5aae397 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 19 Aug 2025 15:27:24 +0200 Subject: [PATCH 479/693] Auto-fix clippy::collapsible_if violations (#36428) Release Notes: - N/A --- Cargo.toml | 1 + crates/acp_thread/src/acp_thread.rs | 25 +- crates/action_log/src/action_log.rs | 8 +- .../src/activity_indicator.rs | 65 +- crates/agent/src/context.rs | 34 +- crates/agent/src/context_store.rs | 8 +- crates/agent/src/thread.rs | 49 +- crates/agent/src/thread_store.rs | 58 +- crates/agent/src/tool_use.rs | 20 +- crates/agent2/src/thread.rs | 12 +- crates/agent2/src/tools/edit_file_tool.rs | 14 +- crates/agent2/src/tools/grep_tool.rs | 10 +- crates/agent_servers/src/claude.rs | 16 +- crates/agent_settings/src/agent_settings.rs | 16 +- crates/agent_ui/src/acp/thread_view.rs | 158 ++-- crates/agent_ui/src/active_thread.rs | 124 ++- .../configure_context_server_modal.rs | 32 +- .../src/agent_configuration/tool_picker.rs | 8 +- crates/agent_ui/src/agent_diff.rs | 62 +- crates/agent_ui/src/agent_panel.rs | 13 +- crates/agent_ui/src/buffer_codegen.rs | 66 +- crates/agent_ui/src/context_strip.rs | 8 +- crates/agent_ui/src/inline_assistant.rs | 109 ++- .../agent_ui/src/terminal_inline_assistant.rs | 55 +- crates/agent_ui/src/text_thread_editor.rs | 39 +- crates/agent_ui/src/thread_history.rs | 9 +- .../src/assistant_context.rs | 122 ++- crates/assistant_context/src/context_store.rs | 47 +- .../src/context_server_command.rs | 8 +- .../src/delta_command.rs | 69 +- .../src/diagnostics_command.rs | 8 +- .../src/tab_command.rs | 16 +- crates/assistant_tool/src/tool_schema.rs | 38 +- crates/assistant_tools/src/edit_agent.rs | 43 +- .../assistant_tools/src/edit_agent/evals.rs | 16 +- crates/assistant_tools/src/edit_file_tool.rs | 16 +- crates/assistant_tools/src/grep_tool.rs | 10 +- crates/assistant_tools/src/schema.rs | 11 +- crates/auto_update_helper/src/dialog.rs | 10 +- crates/breadcrumbs/src/breadcrumbs.rs | 11 +- crates/buffer_diff/src/buffer_diff.rs | 29 +- crates/call/src/call_impl/room.rs | 62 +- crates/channel/src/channel_buffer.rs | 11 +- crates/channel/src/channel_chat.rs | 119 ++- crates/channel/src/channel_store.rs | 78 +- crates/cli/src/main.rs | 9 +- crates/client/src/client.rs | 29 +- crates/client/src/user.rs | 8 +- crates/collab/src/api/events.rs | 160 ++-- crates/collab/src/api/extensions.rs | 14 +- crates/collab/src/auth.rs | 40 +- crates/collab/src/db/queries/extensions.rs | 16 +- crates/collab/src/db/queries/projects.rs | 8 +- crates/collab/src/rpc.rs | 93 +- .../random_project_collaboration_tests.rs | 5 +- .../src/tests/randomized_test_helpers.rs | 30 +- crates/collab/src/user_backfiller.rs | 22 +- crates/collab_ui/src/channel_view.rs | 80 +- crates/collab_ui/src/chat_panel.rs | 81 +- .../src/chat_panel/message_editor.rs | 65 +- crates/collab_ui/src/collab_panel.rs | 187 ++-- crates/collab_ui/src/notification_panel.rs | 76 +- crates/context_server/src/client.rs | 8 +- crates/copilot/src/copilot.rs | 121 ++- crates/dap_adapters/src/javascript.rs | 8 +- crates/debugger_tools/src/dap_log.rs | 10 +- crates/debugger_ui/src/debugger_panel.rs | 13 +- crates/debugger_ui/src/new_process_modal.rs | 8 +- .../src/session/running/breakpoint_list.rs | 40 +- .../src/session/running/console.rs | 17 +- crates/diagnostics/src/diagnostics.rs | 38 +- crates/editor/src/display_map.rs | 36 +- crates/editor/src/display_map/block_map.rs | 148 ++-- crates/editor/src/display_map/fold_map.rs | 60 +- crates/editor/src/display_map/inlay_map.rs | 10 +- crates/editor/src/display_map/wrap_map.rs | 94 +- crates/editor/src/editor.rs | 834 +++++++++--------- crates/editor/src/element.rs | 261 +++--- crates/editor/src/git/blame.rs | 31 +- crates/editor/src/hover_links.rs | 41 +- crates/editor/src/hover_popover.rs | 139 ++- crates/editor/src/indent_guides.rs | 10 +- crates/editor/src/inlay_hint_cache.rs | 121 ++- crates/editor/src/items.rs | 61 +- crates/editor/src/jsx_tag_auto_close.rs | 17 +- crates/editor/src/lsp_ext.rs | 19 +- crates/editor/src/movement.rs | 50 +- crates/editor/src/rust_analyzer_ext.rs | 10 +- crates/editor/src/scroll.rs | 24 +- crates/editor/src/scroll/autoscroll.rs | 12 +- crates/editor/src/test.rs | 8 +- crates/eval/src/eval.rs | 5 +- crates/eval/src/explorer.rs | 28 +- crates/eval/src/instance.rs | 5 +- crates/extension/src/extension.rs | 17 +- crates/extension_host/src/extension_host.rs | 71 +- crates/extension_host/src/wasm_host.rs | 17 +- crates/extensions_ui/src/extensions_ui.rs | 15 +- crates/file_finder/src/file_finder.rs | 399 ++++----- crates/file_finder/src/open_path_prompt.rs | 20 +- crates/fs/src/fs.rs | 82 +- crates/fs/src/mac_watcher.rs | 5 +- crates/fsevent/src/fsevent.rs | 61 +- crates/git/src/blame.rs | 12 +- crates/git/src/repository.rs | 16 +- .../src/git_hosting_providers.rs | 8 +- crates/git_ui/src/commit_modal.rs | 19 +- crates/git_ui/src/git_panel.rs | 42 +- crates/git_ui/src/project_diff.rs | 8 +- crates/go_to_line/src/cursor_position.rs | 16 +- crates/go_to_line/src/go_to_line.rs | 10 +- crates/google_ai/src/google_ai.rs | 5 +- crates/gpui/build.rs | 15 +- crates/gpui/examples/input.rs | 8 +- crates/gpui/src/app.rs | 42 +- crates/gpui/src/app/context.rs | 20 +- crates/gpui/src/element.rs | 6 +- crates/gpui/src/elements/div.rs | 229 +++-- crates/gpui/src/elements/image_cache.rs | 8 +- crates/gpui/src/elements/img.rs | 13 +- crates/gpui/src/elements/list.rs | 74 +- crates/gpui/src/elements/text.rs | 32 +- crates/gpui/src/keymap/binding.rs | 8 +- .../gpui/src/platform/blade/blade_renderer.rs | 30 +- .../gpui/src/platform/linux/wayland/client.rs | 52 +- .../gpui/src/platform/linux/wayland/cursor.rs | 9 +- .../gpui/src/platform/linux/wayland/window.rs | 27 +- crates/gpui/src/platform/linux/x11/client.rs | 29 +- .../gpui/src/platform/linux/x11/clipboard.rs | 38 +- crates/gpui/src/platform/linux/x11/window.rs | 49 +- crates/gpui/src/platform/mac/open_type.rs | 16 +- crates/gpui/src/platform/mac/platform.rs | 27 +- crates/gpui/src/platform/mac/window.rs | 41 +- crates/gpui/src/platform/test/dispatcher.rs | 10 +- crates/gpui/src/platform/test/platform.rs | 8 +- crates/gpui/src/platform/windows/events.rs | 75 +- crates/gpui/src/platform/windows/platform.rs | 16 +- crates/gpui/src/text_system.rs | 43 +- crates/gpui/src/text_system/line.rs | 24 +- crates/gpui/src/text_system/line_layout.rs | 8 +- crates/gpui/src/view.rs | 29 +- crates/gpui/src/window.rs | 68 +- .../src/derive_inspector_reflection.rs | 18 +- crates/gpui_macros/src/test.rs | 116 +-- crates/html_to_markdown/src/markdown.rs | 17 +- crates/http_client/src/github.rs | 8 +- crates/journal/src/journal.rs | 32 +- crates/language/src/buffer.rs | 215 +++-- crates/language/src/language.rs | 33 +- crates/language/src/syntax_map.rs | 104 +-- crates/language/src/text_diff.rs | 10 +- crates/language_model/src/request.rs | 41 +- .../language_models/src/provider/anthropic.rs | 10 +- .../language_models/src/provider/bedrock.rs | 18 +- crates/language_models/src/provider/cloud.rs | 52 +- .../src/active_buffer_language.rs | 8 +- crates/language_tools/src/lsp_log.rs | 116 ++- crates/language_tools/src/syntax_tree_view.rs | 23 +- crates/languages/src/go.rs | 24 +- crates/languages/src/lib.rs | 7 +- crates/languages/src/python.rs | 96 +- crates/languages/src/rust.rs | 8 +- crates/languages/src/typescript.rs | 8 +- crates/livekit_client/src/test.rs | 16 +- crates/markdown/src/markdown.rs | 71 +- .../markdown_preview/src/markdown_parser.rs | 21 +- .../src/markdown_preview_view.rs | 52 +- .../src/migrations/m_2025_06_16/settings.rs | 28 +- .../src/migrations/m_2025_06_25/settings.rs | 16 +- .../src/migrations/m_2025_06_27/settings.rs | 25 +- crates/multi_buffer/src/anchor.rs | 113 ++- crates/multi_buffer/src/multi_buffer.rs | 475 +++++----- crates/multi_buffer/src/multi_buffer_tests.rs | 32 +- .../notifications/src/notification_store.rs | 51 +- crates/open_router/src/open_router.rs | 8 +- crates/outline_panel/src/outline_panel.rs | 238 +++-- crates/prettier/src/prettier.rs | 38 +- crates/project/src/buffer_store.rs | 32 +- .../project/src/debugger/breakpoint_store.rs | 11 +- crates/project/src/debugger/memory.rs | 13 +- crates/project/src/git_store.rs | 152 ++-- crates/project/src/git_store/git_traversal.rs | 10 +- crates/project/src/lsp_command.rs | 26 +- crates/project/src/lsp_store.rs | 392 ++++---- crates/project/src/manifest_tree/path_trie.rs | 10 +- crates/project/src/prettier_store.rs | 21 +- crates/project/src/project.rs | 147 ++- crates/project/src/search.rs | 16 +- crates/project/src/search_history.rs | 23 +- crates/project/src/terminals.rs | 52 +- crates/project_panel/src/project_panel.rs | 497 +++++------ crates/prompt_store/src/prompts.rs | 19 +- crates/proto/src/error.rs | 8 +- crates/recent_projects/src/remote_servers.rs | 8 +- .../src/derive_refineable.rs | 12 +- crates/remote/src/ssh_session.rs | 41 +- crates/remote_server/src/unix.rs | 14 +- crates/repl/src/kernels/native_kernel.rs | 24 +- crates/repl/src/outputs.rs | 39 +- crates/repl/src/repl_editor.rs | 8 +- crates/repl/src/repl_sessions_ui.rs | 25 +- crates/repl/src/repl_store.rs | 8 +- crates/reqwest_client/src/reqwest_client.rs | 11 +- crates/rich_text/src/rich_text.rs | 21 +- crates/rope/src/rope.rs | 41 +- crates/rpc/src/notification.rs | 8 +- crates/rpc/src/peer.rs | 8 +- crates/rules_library/src/rules_library.rs | 34 +- crates/search/src/buffer_search.rs | 180 ++-- crates/search/src/project_search.rs | 45 +- crates/semantic_index/src/embedding_index.rs | 10 +- crates/semantic_index/src/project_index.rs | 8 +- crates/semantic_index/src/semantic_index.rs | 15 +- crates/semantic_index/src/summary_index.rs | 18 +- crates/session/src/session.rs | 10 +- crates/settings/src/keymap_file.rs | 48 +- crates/settings/src/settings_file.rs | 27 +- crates/settings/src/settings_json.rs | 17 +- crates/settings/src/settings_store.rs | 109 ++- crates/snippets_ui/src/snippets_ui.rs | 13 +- crates/sqlez/src/connection.rs | 63 +- crates/sum_tree/src/cursor.rs | 8 +- crates/sum_tree/src/sum_tree.rs | 30 +- crates/svg_preview/src/svg_preview_view.rs | 160 ++-- crates/task/src/debug_format.rs | 9 +- crates/task/src/vscode_debug_format.rs | 12 +- crates/tasks_ui/src/tasks_ui.rs | 17 +- crates/terminal/src/terminal.rs | 36 +- crates/terminal/src/terminal_settings.rs | 8 +- crates/terminal_view/src/terminal_element.rs | 29 +- crates/terminal_view/src/terminal_panel.rs | 23 +- crates/terminal_view/src/terminal_view.rs | 80 +- crates/text/src/text.rs | 26 +- crates/theme/src/fallback_themes.rs | 8 +- crates/title_bar/src/application_menu.rs | 41 +- crates/title_bar/src/title_bar.rs | 10 +- .../src/active_toolchain.rs | 47 +- .../src/toolchain_selector.rs | 9 +- .../src/components/label/highlighted_label.rs | 12 +- crates/ui/src/components/popover_menu.rs | 32 +- crates/ui/src/components/right_click_menu.rs | 9 +- crates/util/src/fs.rs | 27 +- crates/util/src/schemars.rs | 13 +- crates/util/src/util.rs | 8 +- crates/vim/src/command.rs | 55 +- crates/vim/src/digraph.rs | 16 +- crates/vim/src/motion.rs | 24 +- crates/vim/src/normal/delete.rs | 8 +- crates/vim/src/normal/mark.rs | 6 +- crates/vim/src/normal/repeat.rs | 24 +- crates/vim/src/object.rs | 56 +- crates/vim/src/state.rs | 71 +- crates/vim/src/surrounds.rs | 22 +- crates/vim/src/test/neovim_connection.rs | 9 +- crates/vim/src/vim.rs | 54 +- crates/workspace/src/dock.rs | 41 +- crates/workspace/src/history_manager.rs | 10 +- crates/workspace/src/item.rs | 26 +- crates/workspace/src/modal_layer.rs | 8 +- crates/workspace/src/pane.rs | 133 ++- crates/workspace/src/pane_group.rs | 55 +- crates/workspace/src/workspace.rs | 432 +++++---- crates/workspace/src/workspace_settings.rs | 28 +- crates/worktree/src/worktree.rs | 126 ++- crates/zed/build.rs | 26 +- crates/zed/src/main.rs | 113 ++- crates/zed/src/reliability.rs | 43 +- crates/zed/src/zed.rs | 73 +- crates/zed/src/zed/component_preview.rs | 93 +- .../zed/src/zed/edit_prediction_registry.rs | 42 +- crates/zed/src/zed/mac_only_instance.rs | 23 +- crates/zed/src/zed/open_listener.rs | 63 +- crates/zeta/src/rate_completion_modal.rs | 12 +- crates/zeta/src/zeta.rs | 9 +- crates/zlog/src/sink.rs | 8 +- crates/zlog/src/zlog.rs | 30 +- extensions/glsl/src/glsl.rs | 8 +- extensions/ruff/src/ruff.rs | 14 +- extensions/snippets/src/snippets.rs | 8 +- .../test-extension/src/test_extension.rs | 8 +- extensions/toml/src/toml.rs | 14 +- 281 files changed, 6628 insertions(+), 7089 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f326090b51..89aadbcba0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -830,6 +830,7 @@ module_inception = { level = "deny" } question_mark = { level = "deny" } redundant_closure = { level = "deny" } declare_interior_mutable_const = { level = "deny" } +collapsible_if = { level = "warn"} needless_borrow = { level = "warn"} # Individual rules that have violations in the codebase: type_complexity = "allow" diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index b86696d437..227ca984d4 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -249,14 +249,13 @@ impl ToolCall { } if let Some(raw_output) = raw_output { - if self.content.is_empty() { - if let Some(markdown) = markdown_for_raw_output(&raw_output, &language_registry, cx) - { - self.content - .push(ToolCallContent::ContentBlock(ContentBlock::Markdown { - markdown, - })); - } + if self.content.is_empty() + && let Some(markdown) = markdown_for_raw_output(&raw_output, &language_registry, cx) + { + self.content + .push(ToolCallContent::ContentBlock(ContentBlock::Markdown { + markdown, + })); } self.raw_output = Some(raw_output); } @@ -430,11 +429,11 @@ impl ContentBlock { language_registry: &Arc<LanguageRegistry>, cx: &mut App, ) { - if matches!(self, ContentBlock::Empty) { - if let acp::ContentBlock::ResourceLink(resource_link) = block { - *self = ContentBlock::ResourceLink { resource_link }; - return; - } + if matches!(self, ContentBlock::Empty) + && let acp::ContentBlock::ResourceLink(resource_link) = block + { + *self = ContentBlock::ResourceLink { resource_link }; + return; } let new_content = self.block_string_contents(block); diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs index 20ba9586ea..ceced1bcdd 100644 --- a/crates/action_log/src/action_log.rs +++ b/crates/action_log/src/action_log.rs @@ -614,10 +614,10 @@ impl ActionLog { false } }); - if tracked_buffer.unreviewed_edits.is_empty() { - if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status { - tracked_buffer.status = TrackedBufferStatus::Modified; - } + if tracked_buffer.unreviewed_edits.is_empty() + && let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status + { + tracked_buffer.status = TrackedBufferStatus::Modified; } tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); } diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 090252d338..8faf74736a 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -458,26 +458,24 @@ impl ActivityIndicator { .map(|r| r.read(cx)) .and_then(Repository::current_job); // Show any long-running git command - if let Some(job_info) = current_job { - if Instant::now() - job_info.start >= GIT_OPERATION_DELAY { - return Some(Content { - icon: Some( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage(delta))) - }, - ) - .into_any_element(), - ), - message: job_info.message.into(), - on_click: None, - tooltip_message: None, - }); - } + if let Some(job_info) = current_job + && Instant::now() - job_info.start >= GIT_OPERATION_DELAY + { + return Some(Content { + icon: Some( + Icon::new(IconName::ArrowCircle) + .size(IconSize::Small) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + ) + .into_any_element(), + ), + message: job_info.message.into(), + on_click: None, + tooltip_message: None, + }); } // Show any language server installation info. @@ -740,21 +738,20 @@ impl ActivityIndicator { if let Some(extension_store) = ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx)) + && let Some(extension_id) = extension_store.outstanding_operations().keys().next() { - if let Some(extension_id) = extension_store.outstanding_operations().keys().next() { - return Some(Content { - icon: Some( - Icon::new(IconName::Download) - .size(IconSize::Small) - .into_any_element(), - ), - message: format!("Updating {extension_id} extension…"), - on_click: Some(Arc::new(|this, window, cx| { - this.dismiss_error_message(&DismissErrorMessage, window, cx) - })), - tooltip_message: None, - }); - } + return Some(Content { + icon: Some( + Icon::new(IconName::Download) + .size(IconSize::Small) + .into_any_element(), + ), + message: format!("Updating {extension_id} extension…"), + on_click: Some(Arc::new(|this, window, cx| { + this.dismiss_error_message(&DismissErrorMessage, window, cx) + })), + tooltip_message: None, + }); } None diff --git a/crates/agent/src/context.rs b/crates/agent/src/context.rs index 8cdb87ef8d..9bb8fc0eae 100644 --- a/crates/agent/src/context.rs +++ b/crates/agent/src/context.rs @@ -201,24 +201,24 @@ impl FileContextHandle { parse_status.changed().await.log_err(); } - if let Ok(snapshot) = buffer.read_with(cx, |buffer, _| buffer.snapshot()) { - if let Some(outline) = snapshot.outline(None) { - let items = outline - .items - .into_iter() - .map(|item| item.to_point(&snapshot)); + if let Ok(snapshot) = buffer.read_with(cx, |buffer, _| buffer.snapshot()) + && let Some(outline) = snapshot.outline(None) + { + let items = outline + .items + .into_iter() + .map(|item| item.to_point(&snapshot)); - if let Ok(outline_text) = - outline::render_outline(items, None, 0, usize::MAX).await - { - let context = AgentContext::File(FileContext { - handle: self, - full_path, - text: outline_text.into(), - is_outline: true, - }); - return Some((context, vec![buffer])); - } + if let Ok(outline_text) = + outline::render_outline(items, None, 0, usize::MAX).await + { + let context = AgentContext::File(FileContext { + handle: self, + full_path, + text: outline_text.into(), + is_outline: true, + }); + return Some((context, vec![buffer])); } } } diff --git a/crates/agent/src/context_store.rs b/crates/agent/src/context_store.rs index 60ba5527dc..b531852a18 100644 --- a/crates/agent/src/context_store.rs +++ b/crates/agent/src/context_store.rs @@ -338,11 +338,9 @@ impl ContextStore { image_task, context_id: self.next_context_id.post_inc(), }); - if self.has_context(&context) { - if remove_if_exists { - self.remove_context(&context, cx); - return None; - } + if self.has_context(&context) && remove_if_exists { + self.remove_context(&context, cx); + return None; } self.insert_context(context.clone(), cx); diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index a3f903a60d..5c4b2b8ebf 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -1967,11 +1967,9 @@ impl Thread { if let Some(prev_message) = thread.messages.get(ix - 1) - { - if prev_message.role == Role::Assistant { + && prev_message.role == Role::Assistant { break; } - } } } @@ -2476,13 +2474,13 @@ impl Thread { .ok()?; // Save thread so its summary can be reused later - if let Some(thread) = thread.upgrade() { - if let Ok(Ok(save_task)) = cx.update(|cx| { + if let Some(thread) = thread.upgrade() + && let Ok(Ok(save_task)) = cx.update(|cx| { thread_store .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx)) - }) { - save_task.await.log_err(); - } + }) + { + save_task.await.log_err(); } Some(()) @@ -2730,12 +2728,11 @@ impl Thread { window: Option<AnyWindowHandle>, cx: &mut Context<Self>, ) { - if self.all_tools_finished() { - if let Some(ConfiguredModel { model, .. }) = self.configured_model.as_ref() { - if !canceled { - self.send_to_model(model.clone(), CompletionIntent::ToolResults, window, cx); - } - } + if self.all_tools_finished() + && let Some(ConfiguredModel { model, .. }) = self.configured_model.as_ref() + && !canceled + { + self.send_to_model(model.clone(), CompletionIntent::ToolResults, window, cx); } cx.emit(ThreadEvent::ToolFinished { @@ -2922,11 +2919,11 @@ impl Thread { let buffer_store = project.read(app_cx).buffer_store(); for buffer_handle in buffer_store.read(app_cx).buffers() { let buffer = buffer_handle.read(app_cx); - if buffer.is_dirty() { - if let Some(file) = buffer.file() { - let path = file.path().to_string_lossy().to_string(); - unsaved_buffers.push(path); - } + if buffer.is_dirty() + && let Some(file) = buffer.file() + { + let path = file.path().to_string_lossy().to_string(); + unsaved_buffers.push(path); } } }) @@ -3178,13 +3175,13 @@ impl Thread { .model .max_token_count_for_mode(self.completion_mode().into()); - if let Some(exceeded_error) = &self.exceeded_window_error { - if model.model.id() == exceeded_error.model_id { - return Some(TotalTokenUsage { - total: exceeded_error.token_count, - max, - }); - } + if let Some(exceeded_error) = &self.exceeded_window_error + && model.model.id() == exceeded_error.model_id + { + return Some(TotalTokenUsage { + total: exceeded_error.token_count, + max, + }); } let total = self diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index 12c94a522d..96bf639306 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -581,33 +581,32 @@ impl ThreadStore { return; }; - if protocol.capable(context_server::protocol::ServerCapability::Tools) { - if let Some(response) = protocol + if protocol.capable(context_server::protocol::ServerCapability::Tools) + && let Some(response) = protocol .request::<context_server::types::requests::ListTools>(()) .await .log_err() - { - let tool_ids = tool_working_set - .update(cx, |tool_working_set, cx| { - tool_working_set.extend( - response.tools.into_iter().map(|tool| { - Arc::new(ContextServerTool::new( - context_server_store.clone(), - server.id(), - tool, - )) as Arc<dyn Tool> - }), - cx, - ) - }) - .log_err(); + { + let tool_ids = tool_working_set + .update(cx, |tool_working_set, cx| { + tool_working_set.extend( + response.tools.into_iter().map(|tool| { + Arc::new(ContextServerTool::new( + context_server_store.clone(), + server.id(), + tool, + )) as Arc<dyn Tool> + }), + cx, + ) + }) + .log_err(); - if let Some(tool_ids) = tool_ids { - this.update(cx, |this, _| { - this.context_server_tool_ids.insert(server_id, tool_ids); - }) - .log_err(); - } + if let Some(tool_ids) = tool_ids { + this.update(cx, |this, _| { + this.context_server_tool_ids.insert(server_id, tool_ids); + }) + .log_err(); } } }) @@ -697,13 +696,14 @@ impl SerializedThreadV0_1_0 { let mut messages: Vec<SerializedMessage> = Vec::with_capacity(self.0.messages.len()); for message in self.0.messages { - if message.role == Role::User && !message.tool_results.is_empty() { - if let Some(last_message) = messages.last_mut() { - debug_assert!(last_message.role == Role::Assistant); + if message.role == Role::User + && !message.tool_results.is_empty() + && let Some(last_message) = messages.last_mut() + { + debug_assert!(last_message.role == Role::Assistant); - last_message.tool_results = message.tool_results; - continue; - } + last_message.tool_results = message.tool_results; + continue; } messages.push(message); diff --git a/crates/agent/src/tool_use.rs b/crates/agent/src/tool_use.rs index 74dfaf9a85..d109891bf2 100644 --- a/crates/agent/src/tool_use.rs +++ b/crates/agent/src/tool_use.rs @@ -112,19 +112,13 @@ impl ToolUseState { }, ); - if let Some(window) = &mut window { - if let Some(tool) = this.tools.read(cx).tool(tool_use, cx) { - if let Some(output) = tool_result.output.clone() { - if let Some(card) = tool.deserialize_card( - output, - project.clone(), - window, - cx, - ) { - this.tool_result_cards.insert(tool_use_id, card); - } - } - } + if let Some(window) = &mut window + && let Some(tool) = this.tools.read(cx).tool(tool_use, cx) + && let Some(output) = tool_result.output.clone() + && let Some(card) = + tool.deserialize_card(output, project.clone(), window, cx) + { + this.tool_result_cards.insert(tool_use_id, card); } } } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index f0b5d2f08a..856e70ce59 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1037,12 +1037,12 @@ impl Thread { log::info!("Running tool {}", tool_use.name); Some(cx.foreground_executor().spawn(async move { let tool_result = tool_result.await.and_then(|output| { - if let LanguageModelToolResultContent::Image(_) = &output.llm_output { - if !supports_images { - return Err(anyhow!( - "Attempted to read an image, but this model doesn't support it.", - )); - } + if let LanguageModelToolResultContent::Image(_) = &output.llm_output + && !supports_images + { + return Err(anyhow!( + "Attempted to read an image, but this model doesn't support it.", + )); } Ok(output) }); diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 8ebd2936a5..7687d68702 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -156,13 +156,13 @@ impl EditFileTool { // It's also possible that the global config dir is configured to be inside the project, // so check for that edge case too. - if let Ok(canonical_path) = std::fs::canonicalize(&input.path) { - if canonical_path.starts_with(paths::config_dir()) { - return event_stream.authorize( - format!("{} (global settings)", input.display_description), - cx, - ); - } + if let Ok(canonical_path) = std::fs::canonicalize(&input.path) + && canonical_path.starts_with(paths::config_dir()) + { + return event_stream.authorize( + format!("{} (global settings)", input.display_description), + cx, + ); } // Check if path is inside the global config directory diff --git a/crates/agent2/src/tools/grep_tool.rs b/crates/agent2/src/tools/grep_tool.rs index e5d92b3c1d..6d7c05d211 100644 --- a/crates/agent2/src/tools/grep_tool.rs +++ b/crates/agent2/src/tools/grep_tool.rs @@ -179,15 +179,14 @@ impl AgentTool for GrepTool { // Check if this file should be excluded based on its worktree settings if let Ok(Some(project_path)) = project.read_with(cx, |project, cx| { project.find_project_path(&path, cx) - }) { - if cx.update(|cx| { + }) + && cx.update(|cx| { let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx); worktree_settings.is_path_excluded(&project_path.path) || worktree_settings.is_path_private(&project_path.path) }).unwrap_or(false) { continue; } - } while *parse_status.borrow() != ParseStatus::Idle { parse_status.changed().await?; @@ -275,12 +274,11 @@ impl AgentTool for GrepTool { output.extend(snapshot.text_for_range(range)); output.push_str("\n```\n"); - if let Some(ancestor_range) = ancestor_range { - if end_row < ancestor_range.end.row { + if let Some(ancestor_range) = ancestor_range + && end_row < ancestor_range.end.row { let remaining_lines = ancestor_range.end.row - end_row; writeln!(output, "\n{} lines remaining in ancestor node. Read the file to see all.", remaining_lines)?; } - } matches_found += 1; } diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 7034d6fbce..34d55f39dc 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -203,14 +203,14 @@ impl AgentConnection for ClaudeAgentConnection { .await } - if let Some(status) = child.status().await.log_err() { - if let Some(thread) = thread_rx.recv().await.ok() { - thread - .update(cx, |thread, cx| { - thread.emit_server_exited(status, cx); - }) - .ok(); - } + if let Some(status) = child.status().await.log_err() + && let Some(thread) = thread_rx.recv().await.ok() + { + thread + .update(cx, |thread, cx| { + thread.emit_server_exited(status, cx); + }) + .ok(); } } }); diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index fd38ba1f7f..afc834cdd8 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -116,15 +116,15 @@ pub struct LanguageModelParameters { impl LanguageModelParameters { pub fn matches(&self, model: &Arc<dyn LanguageModel>) -> bool { - if let Some(provider) = &self.provider { - if provider.0 != model.provider_id().0 { - return false; - } + if let Some(provider) = &self.provider + && provider.0 != model.provider_id().0 + { + return false; } - if let Some(setting_model) = &self.model { - if *setting_model != model.id().0 { - return false; - } + if let Some(setting_model) = &self.model + && *setting_model != model.id().0 + { + return false; } true } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 370dae53e4..ad0920bc4a 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -371,20 +371,20 @@ impl AcpThreadView { let provider_id = provider_id.clone(); let this = this.clone(); move |_, ev, window, cx| { - if let language_model::Event::ProviderStateChanged(updated_provider_id) = &ev { - if &provider_id == updated_provider_id { - this.update(cx, |this, cx| { - this.thread_state = Self::initial_state( - agent.clone(), - this.workspace.clone(), - this.project.clone(), - window, - cx, - ); - cx.notify(); - }) - .ok(); - } + if let language_model::Event::ProviderStateChanged(updated_provider_id) = &ev + && &provider_id == updated_provider_id + { + this.update(cx, |this, cx| { + this.thread_state = Self::initial_state( + agent.clone(), + this.workspace.clone(), + this.project.clone(), + window, + cx, + ); + cx.notify(); + }) + .ok(); } } }); @@ -547,11 +547,11 @@ impl AcpThreadView { } fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) { - if let Some(thread) = self.thread() { - if thread.read(cx).status() != ThreadStatus::Idle { - self.stop_current_and_send_new_message(window, cx); - return; - } + if let Some(thread) = self.thread() + && thread.read(cx).status() != ThreadStatus::Idle + { + self.stop_current_and_send_new_message(window, cx); + return; } let contents = self @@ -628,25 +628,24 @@ impl AcpThreadView { return; }; - if let Some(index) = self.editing_message.take() { - if let Some(editor) = self + if let Some(index) = self.editing_message.take() + && let Some(editor) = self .entry_view_state .read(cx) .entry(index) .and_then(|e| e.message_editor()) .cloned() - { - editor.update(cx, |editor, cx| { - if let Some(user_message) = thread - .read(cx) - .entries() - .get(index) - .and_then(|e| e.user_message()) - { - editor.set_message(user_message.chunks.clone(), window, cx); - } - }) - } + { + editor.update(cx, |editor, cx| { + if let Some(user_message) = thread + .read(cx) + .entries() + .get(index) + .and_then(|e| e.user_message()) + { + editor.set_message(user_message.chunks.clone(), window, cx); + } + }) }; self.focus_handle(cx).focus(window); cx.notify(); @@ -3265,62 +3264,61 @@ impl AcpThreadView { }) }) .log_err() + && let Some(pop_up) = screen_window.entity(cx).log_err() { - if let Some(pop_up) = screen_window.entity(cx).log_err() { - self.notification_subscriptions - .entry(screen_window) - .or_insert_with(Vec::new) - .push(cx.subscribe_in(&pop_up, window, { - |this, _, event, window, cx| match event { - AgentNotificationEvent::Accepted => { - let handle = window.window_handle(); - cx.activate(true); + self.notification_subscriptions + .entry(screen_window) + .or_insert_with(Vec::new) + .push(cx.subscribe_in(&pop_up, window, { + |this, _, event, window, cx| match event { + AgentNotificationEvent::Accepted => { + let handle = window.window_handle(); + cx.activate(true); - let workspace_handle = this.workspace.clone(); + let workspace_handle = this.workspace.clone(); - // If there are multiple Zed windows, activate the correct one. - cx.defer(move |cx| { - handle - .update(cx, |_view, window, _cx| { - window.activate_window(); + // If there are multiple Zed windows, activate the correct one. + cx.defer(move |cx| { + handle + .update(cx, |_view, window, _cx| { + window.activate_window(); - if let Some(workspace) = workspace_handle.upgrade() { - workspace.update(_cx, |workspace, cx| { - workspace.focus_panel::<AgentPanel>(window, cx); - }); - } - }) - .log_err(); - }); + if let Some(workspace) = workspace_handle.upgrade() { + workspace.update(_cx, |workspace, cx| { + workspace.focus_panel::<AgentPanel>(window, cx); + }); + } + }) + .log_err(); + }); - this.dismiss_notifications(cx); - } - AgentNotificationEvent::Dismissed => { - this.dismiss_notifications(cx); - } + this.dismiss_notifications(cx); } - })); + AgentNotificationEvent::Dismissed => { + this.dismiss_notifications(cx); + } + } + })); - self.notifications.push(screen_window); + self.notifications.push(screen_window); - // If the user manually refocuses the original window, dismiss the popup. - self.notification_subscriptions - .entry(screen_window) - .or_insert_with(Vec::new) - .push({ - let pop_up_weak = pop_up.downgrade(); + // If the user manually refocuses the original window, dismiss the popup. + self.notification_subscriptions + .entry(screen_window) + .or_insert_with(Vec::new) + .push({ + let pop_up_weak = pop_up.downgrade(); - cx.observe_window_activation(window, move |_, window, cx| { - if window.is_window_active() { - if let Some(pop_up) = pop_up_weak.upgrade() { - pop_up.update(cx, |_, cx| { - cx.emit(AgentNotificationEvent::Dismissed); - }); - } - } - }) - }); - } + cx.observe_window_activation(window, move |_, window, cx| { + if window.is_window_active() + && let Some(pop_up) = pop_up_weak.upgrade() + { + pop_up.update(cx, |_, cx| { + cx.emit(AgentNotificationEvent::Dismissed); + }); + } + }) + }); } } diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index d2f448635e..3defa42d17 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -1072,8 +1072,8 @@ impl ActiveThread { } ThreadEvent::MessageEdited(message_id) => { self.clear_last_error(); - if let Some(index) = self.messages.iter().position(|id| id == message_id) { - if let Some(rendered_message) = self.thread.update(cx, |thread, cx| { + if let Some(index) = self.messages.iter().position(|id| id == message_id) + && let Some(rendered_message) = self.thread.update(cx, |thread, cx| { thread.message(*message_id).map(|message| { let mut rendered_message = RenderedMessage { language_registry: self.language_registry.clone(), @@ -1084,14 +1084,14 @@ impl ActiveThread { } rendered_message }) - }) { - self.list_state.splice(index..index + 1, 1); - self.rendered_messages_by_id - .insert(*message_id, rendered_message); - self.scroll_to_bottom(cx); - self.save_thread(cx); - cx.notify(); - } + }) + { + self.list_state.splice(index..index + 1, 1); + self.rendered_messages_by_id + .insert(*message_id, rendered_message); + self.scroll_to_bottom(cx); + self.save_thread(cx); + cx.notify(); } } ThreadEvent::MessageDeleted(message_id) => { @@ -1272,62 +1272,61 @@ impl ActiveThread { }) }) .log_err() + && let Some(pop_up) = screen_window.entity(cx).log_err() { - if let Some(pop_up) = screen_window.entity(cx).log_err() { - self.notification_subscriptions - .entry(screen_window) - .or_insert_with(Vec::new) - .push(cx.subscribe_in(&pop_up, window, { - |this, _, event, window, cx| match event { - AgentNotificationEvent::Accepted => { - let handle = window.window_handle(); - cx.activate(true); + self.notification_subscriptions + .entry(screen_window) + .or_insert_with(Vec::new) + .push(cx.subscribe_in(&pop_up, window, { + |this, _, event, window, cx| match event { + AgentNotificationEvent::Accepted => { + let handle = window.window_handle(); + cx.activate(true); - let workspace_handle = this.workspace.clone(); + let workspace_handle = this.workspace.clone(); - // If there are multiple Zed windows, activate the correct one. - cx.defer(move |cx| { - handle - .update(cx, |_view, window, _cx| { - window.activate_window(); + // If there are multiple Zed windows, activate the correct one. + cx.defer(move |cx| { + handle + .update(cx, |_view, window, _cx| { + window.activate_window(); - if let Some(workspace) = workspace_handle.upgrade() { - workspace.update(_cx, |workspace, cx| { - workspace.focus_panel::<AgentPanel>(window, cx); - }); - } - }) - .log_err(); - }); + if let Some(workspace) = workspace_handle.upgrade() { + workspace.update(_cx, |workspace, cx| { + workspace.focus_panel::<AgentPanel>(window, cx); + }); + } + }) + .log_err(); + }); - this.dismiss_notifications(cx); - } - AgentNotificationEvent::Dismissed => { - this.dismiss_notifications(cx); - } + this.dismiss_notifications(cx); } - })); + AgentNotificationEvent::Dismissed => { + this.dismiss_notifications(cx); + } + } + })); - self.notifications.push(screen_window); + self.notifications.push(screen_window); - // If the user manually refocuses the original window, dismiss the popup. - self.notification_subscriptions - .entry(screen_window) - .or_insert_with(Vec::new) - .push({ - let pop_up_weak = pop_up.downgrade(); + // If the user manually refocuses the original window, dismiss the popup. + self.notification_subscriptions + .entry(screen_window) + .or_insert_with(Vec::new) + .push({ + let pop_up_weak = pop_up.downgrade(); - cx.observe_window_activation(window, move |_, window, cx| { - if window.is_window_active() { - if let Some(pop_up) = pop_up_weak.upgrade() { - pop_up.update(cx, |_, cx| { - cx.emit(AgentNotificationEvent::Dismissed); - }); - } - } - }) - }); - } + cx.observe_window_activation(window, move |_, window, cx| { + if window.is_window_active() + && let Some(pop_up) = pop_up_weak.upgrade() + { + pop_up.update(cx, |_, cx| { + cx.emit(AgentNotificationEvent::Dismissed); + }); + } + }) + }); } } @@ -2269,13 +2268,12 @@ impl ActiveThread { let mut error = None; if let Some(last_restore_checkpoint) = self.thread.read(cx).last_restore_checkpoint() + && last_restore_checkpoint.message_id() == message_id { - if last_restore_checkpoint.message_id() == message_id { - match last_restore_checkpoint { - LastRestoreCheckpoint::Pending { .. } => is_pending = true, - LastRestoreCheckpoint::Error { error: err, .. } => { - error = Some(err.clone()); - } + match last_restore_checkpoint { + LastRestoreCheckpoint::Pending { .. } => is_pending = true, + LastRestoreCheckpoint::Error { error: err, .. } => { + error = Some(err.clone()); } } } diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs index 32360dd56e..311f75af3b 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs @@ -163,10 +163,10 @@ impl ConfigurationSource { .read(cx) .text(cx); let settings = serde_json_lenient::from_str::<serde_json::Value>(&text)?; - if let Some(settings_validator) = settings_validator { - if let Err(error) = settings_validator.validate(&settings) { - return Err(anyhow::anyhow!(error.to_string())); - } + if let Some(settings_validator) = settings_validator + && let Err(error) = settings_validator.validate(&settings) + { + return Err(anyhow::anyhow!(error.to_string())); } Ok(( id.clone(), @@ -716,24 +716,24 @@ fn wait_for_context_server( project::context_server_store::Event::ServerStatusChanged { server_id, status } => { match status { ContextServerStatus::Running => { - if server_id == &context_server_id { - if let Some(tx) = tx.lock().unwrap().take() { - let _ = tx.send(Ok(())); - } + if server_id == &context_server_id + && let Some(tx) = tx.lock().unwrap().take() + { + let _ = tx.send(Ok(())); } } ContextServerStatus::Stopped => { - if server_id == &context_server_id { - if let Some(tx) = tx.lock().unwrap().take() { - let _ = tx.send(Err("Context server stopped running".into())); - } + if server_id == &context_server_id + && let Some(tx) = tx.lock().unwrap().take() + { + let _ = tx.send(Err("Context server stopped running".into())); } } ContextServerStatus::Error(error) => { - if server_id == &context_server_id { - if let Some(tx) = tx.lock().unwrap().take() { - let _ = tx.send(Err(error.clone())); - } + if server_id == &context_server_id + && let Some(tx) = tx.lock().unwrap().take() + { + let _ = tx.send(Err(error.clone())); } } _ => {} diff --git a/crates/agent_ui/src/agent_configuration/tool_picker.rs b/crates/agent_ui/src/agent_configuration/tool_picker.rs index 8f1e0d71c0..25947a1e58 100644 --- a/crates/agent_ui/src/agent_configuration/tool_picker.rs +++ b/crates/agent_ui/src/agent_configuration/tool_picker.rs @@ -191,10 +191,10 @@ impl PickerDelegate for ToolPickerDelegate { BTreeMap::default(); for item in all_items.iter() { - if let PickerItem::Tool { server_id, name } = item.clone() { - if name.contains(&query) { - tools_by_provider.entry(server_id).or_default().push(name); - } + if let PickerItem::Tool { server_id, name } = item.clone() + && name.contains(&query) + { + tools_by_provider.entry(server_id).or_default().push(name); } } diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index b010f8a424..f474fdf3ae 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1043,18 +1043,18 @@ impl ToolbarItemView for AgentDiffToolbar { return self.location(cx); } - if let Some(editor) = item.act_as::<Editor>(cx) { - if editor.read(cx).mode().is_full() { - let agent_diff = AgentDiff::global(cx); + if let Some(editor) = item.act_as::<Editor>(cx) + && editor.read(cx).mode().is_full() + { + let agent_diff = AgentDiff::global(cx); - self.active_item = Some(AgentDiffToolbarItem::Editor { - editor: editor.downgrade(), - state: agent_diff.read(cx).editor_state(&editor.downgrade()), - _diff_subscription: cx.observe(&agent_diff, Self::handle_diff_notify), - }); + self.active_item = Some(AgentDiffToolbarItem::Editor { + editor: editor.downgrade(), + state: agent_diff.read(cx).editor_state(&editor.downgrade()), + _diff_subscription: cx.observe(&agent_diff, Self::handle_diff_notify), + }); - return self.location(cx); - } + return self.location(cx); } } @@ -1538,16 +1538,10 @@ impl AgentDiff { ) { match event { workspace::Event::ItemAdded { item } => { - if let Some(editor) = item.downcast::<Editor>() { - if let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx) { - self.register_editor( - workspace.downgrade(), - buffer.clone(), - editor, - window, - cx, - ); - } + if let Some(editor) = item.downcast::<Editor>() + && let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx) + { + self.register_editor(workspace.downgrade(), buffer.clone(), editor, window, cx); } } _ => {} @@ -1850,22 +1844,22 @@ impl AgentDiff { let thread = thread.upgrade()?; - if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx) { - if let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton() { - let changed_buffers = thread.action_log(cx).read(cx).changed_buffers(cx); + if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx) + && let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton() + { + let changed_buffers = thread.action_log(cx).read(cx).changed_buffers(cx); - let mut keys = changed_buffers.keys().cycle(); - keys.find(|k| *k == &curr_buffer); - let next_project_path = keys - .next() - .filter(|k| *k != &curr_buffer) - .and_then(|after| after.read(cx).project_path(cx)); + let mut keys = changed_buffers.keys().cycle(); + keys.find(|k| *k == &curr_buffer); + let next_project_path = keys + .next() + .filter(|k| *k != &curr_buffer) + .and_then(|after| after.read(cx).project_path(cx)); - if let Some(path) = next_project_path { - let task = workspace.open_path(path, None, true, window, cx); - let task = cx.spawn(async move |_, _cx| task.await.map(|_| ())); - return Some(task); - } + if let Some(path) = next_project_path { + let task = workspace.open_path(path, None, true, window, cx); + let task = cx.spawn(async move |_, _cx| task.await.map(|_| ())); + return Some(task); } } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index cb354222b6..55d07ed495 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1398,14 +1398,13 @@ impl AgentPanel { if LanguageModelRegistry::read_global(cx) .default_model() .map_or(true, |model| model.provider.id() != provider.id()) + && let Some(model) = provider.default_model(cx) { - if let Some(model) = provider.default_model(cx) { - update_settings_file::<AgentSettings>( - self.fs.clone(), - cx, - move |settings, _| settings.set_model(model), - ); - } + update_settings_file::<AgentSettings>( + self.fs.clone(), + cx, + move |settings, _| settings.set_model(model), + ); } self.new_thread(&NewThread::default(), window, cx); diff --git a/crates/agent_ui/src/buffer_codegen.rs b/crates/agent_ui/src/buffer_codegen.rs index 23e04266db..ff5e9362dd 100644 --- a/crates/agent_ui/src/buffer_codegen.rs +++ b/crates/agent_ui/src/buffer_codegen.rs @@ -352,12 +352,12 @@ impl CodegenAlternative { event: &multi_buffer::Event, cx: &mut Context<Self>, ) { - if let multi_buffer::Event::TransactionUndone { transaction_id } = event { - if self.transformation_transaction_id == Some(*transaction_id) { - self.transformation_transaction_id = None; - self.generation = Task::ready(()); - cx.emit(CodegenEvent::Undone); - } + if let multi_buffer::Event::TransactionUndone { transaction_id } = event + && self.transformation_transaction_id == Some(*transaction_id) + { + self.transformation_transaction_id = None; + self.generation = Task::ready(()); + cx.emit(CodegenEvent::Undone); } } @@ -576,38 +576,34 @@ impl CodegenAlternative { let mut lines = chunk.split('\n').peekable(); while let Some(line) = lines.next() { new_text.push_str(line); - if line_indent.is_none() { - if let Some(non_whitespace_ch_ix) = + if line_indent.is_none() + && let Some(non_whitespace_ch_ix) = new_text.find(|ch: char| !ch.is_whitespace()) - { - line_indent = Some(non_whitespace_ch_ix); - base_indent = base_indent.or(line_indent); + { + line_indent = Some(non_whitespace_ch_ix); + base_indent = base_indent.or(line_indent); - let line_indent = line_indent.unwrap(); - let base_indent = base_indent.unwrap(); - let indent_delta = - line_indent as i32 - base_indent as i32; - let mut corrected_indent_len = cmp::max( - 0, - suggested_line_indent.len as i32 + indent_delta, - ) - as usize; - if first_line { - corrected_indent_len = corrected_indent_len - .saturating_sub( - selection_start.column as usize, - ); - } - - let indent_char = suggested_line_indent.char(); - let mut indent_buffer = [0; 4]; - let indent_str = - indent_char.encode_utf8(&mut indent_buffer); - new_text.replace_range( - ..line_indent, - &indent_str.repeat(corrected_indent_len), - ); + let line_indent = line_indent.unwrap(); + let base_indent = base_indent.unwrap(); + let indent_delta = line_indent as i32 - base_indent as i32; + let mut corrected_indent_len = cmp::max( + 0, + suggested_line_indent.len as i32 + indent_delta, + ) + as usize; + if first_line { + corrected_indent_len = corrected_indent_len + .saturating_sub(selection_start.column as usize); } + + let indent_char = suggested_line_indent.char(); + let mut indent_buffer = [0; 4]; + let indent_str = + indent_char.encode_utf8(&mut indent_buffer); + new_text.replace_range( + ..line_indent, + &indent_str.repeat(corrected_indent_len), + ); } if line_indent.is_some() { diff --git a/crates/agent_ui/src/context_strip.rs b/crates/agent_ui/src/context_strip.rs index 51ed3a5e11..d25d7d3544 100644 --- a/crates/agent_ui/src/context_strip.rs +++ b/crates/agent_ui/src/context_strip.rs @@ -368,10 +368,10 @@ impl ContextStrip { _window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(suggested) = self.suggested_context(cx) { - if self.is_suggested_focused(&self.added_contexts(cx)) { - self.add_suggested_context(&suggested, cx); - } + if let Some(suggested) = self.suggested_context(cx) + && self.is_suggested_focused(&self.added_contexts(cx)) + { + self.add_suggested_context(&suggested, cx); } } diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 781e242fba..101eb899b2 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -182,13 +182,13 @@ impl InlineAssistant { match event { workspace::Event::UserSavedItem { item, .. } => { // When the user manually saves an editor, automatically accepts all finished transformations. - if let Some(editor) = item.upgrade().and_then(|item| item.act_as::<Editor>(cx)) { - if let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) { - for assist_id in editor_assists.assist_ids.clone() { - let assist = &self.assists[&assist_id]; - if let CodegenStatus::Done = assist.codegen.read(cx).status(cx) { - self.finish_assist(assist_id, false, window, cx) - } + if let Some(editor) = item.upgrade().and_then(|item| item.act_as::<Editor>(cx)) + && let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) + { + for assist_id in editor_assists.assist_ids.clone() { + let assist = &self.assists[&assist_id]; + if let CodegenStatus::Done = assist.codegen.read(cx).status(cx) { + self.finish_assist(assist_id, false, window, cx) } } } @@ -342,13 +342,11 @@ impl InlineAssistant { ) .await .ok(); - if let Some(answer) = answer { - if answer == 0 { - cx.update(|window, cx| { - window.dispatch_action(Box::new(OpenSettings), cx) - }) + if let Some(answer) = answer + && answer == 0 + { + cx.update(|window, cx| window.dispatch_action(Box::new(OpenSettings), cx)) .ok(); - } } anyhow::Ok(()) }) @@ -435,11 +433,11 @@ impl InlineAssistant { } } - if let Some(prev_selection) = selections.last_mut() { - if selection.start <= prev_selection.end { - prev_selection.end = selection.end; - continue; - } + if let Some(prev_selection) = selections.last_mut() + && selection.start <= prev_selection.end + { + prev_selection.end = selection.end; + continue; } let latest_selection = newest_selection.get_or_insert_with(|| selection.clone()); @@ -985,14 +983,13 @@ impl InlineAssistant { EditorEvent::SelectionsChanged { .. } => { for assist_id in editor_assists.assist_ids.clone() { let assist = &self.assists[&assist_id]; - if let Some(decorations) = assist.decorations.as_ref() { - if decorations + if let Some(decorations) = assist.decorations.as_ref() + && decorations .prompt_editor .focus_handle(cx) .is_focused(window) - { - return; - } + { + return; } } @@ -1503,20 +1500,18 @@ impl InlineAssistant { window: &mut Window, cx: &mut App, ) -> Option<InlineAssistTarget> { - if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx) { - if terminal_panel + if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx) + && terminal_panel .read(cx) .focus_handle(cx) .contains_focused(window, cx) - { - if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| { - pane.read(cx) - .active_item() - .and_then(|t| t.downcast::<TerminalView>()) - }) { - return Some(InlineAssistTarget::Terminal(terminal_view)); - } - } + && let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| { + pane.read(cx) + .active_item() + .and_then(|t| t.downcast::<TerminalView>()) + }) + { + return Some(InlineAssistTarget::Terminal(terminal_view)); } let context_editor = agent_panel @@ -1741,22 +1736,20 @@ impl InlineAssist { return; }; - if let CodegenStatus::Error(error) = codegen.read(cx).status(cx) { - if assist.decorations.is_none() { - if let Some(workspace) = assist.workspace.upgrade() { - let error = format!("Inline assistant error: {}", error); - workspace.update(cx, |workspace, cx| { - struct InlineAssistantError; + if let CodegenStatus::Error(error) = codegen.read(cx).status(cx) + && assist.decorations.is_none() + && let Some(workspace) = assist.workspace.upgrade() + { + let error = format!("Inline assistant error: {}", error); + workspace.update(cx, |workspace, cx| { + struct InlineAssistantError; - let id = - NotificationId::composite::<InlineAssistantError>( - assist_id.0, - ); + let id = NotificationId::composite::<InlineAssistantError>( + assist_id.0, + ); - workspace.show_toast(Toast::new(id, error), cx); - }) - } - } + workspace.show_toast(Toast::new(id, error), cx); + }) } if assist.decorations.is_none() { @@ -1821,18 +1814,18 @@ impl CodeActionProvider for AssistantCodeActionProvider { has_diagnostics = true; } if has_diagnostics { - if let Some(symbols_containing_start) = snapshot.symbols_containing(range.start, None) { - if let Some(symbol) = symbols_containing_start.last() { - range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot)); - range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot)); - } + if let Some(symbols_containing_start) = snapshot.symbols_containing(range.start, None) + && let Some(symbol) = symbols_containing_start.last() + { + range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot)); + range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot)); } - if let Some(symbols_containing_end) = snapshot.symbols_containing(range.end, None) { - if let Some(symbol) = symbols_containing_end.last() { - range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot)); - range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot)); - } + if let Some(symbols_containing_end) = snapshot.symbols_containing(range.end, None) + && let Some(symbol) = symbols_containing_end.last() + { + range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot)); + range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot)); } Task::ready(Ok(vec![CodeAction { diff --git a/crates/agent_ui/src/terminal_inline_assistant.rs b/crates/agent_ui/src/terminal_inline_assistant.rs index bcbc308c99..3859863ebe 100644 --- a/crates/agent_ui/src/terminal_inline_assistant.rs +++ b/crates/agent_ui/src/terminal_inline_assistant.rs @@ -388,20 +388,20 @@ impl TerminalInlineAssistant { window: &mut Window, cx: &mut App, ) { - if let Some(assist) = self.assists.get_mut(&assist_id) { - if let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned() { - assist - .terminal - .update(cx, |terminal, cx| { - terminal.clear_block_below_cursor(cx); - let block = terminal_view::BlockProperties { - height, - render: Box::new(move |_| prompt_editor.clone().into_any_element()), - }; - terminal.set_block_below_cursor(block, window, cx); - }) - .log_err(); - } + if let Some(assist) = self.assists.get_mut(&assist_id) + && let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned() + { + assist + .terminal + .update(cx, |terminal, cx| { + terminal.clear_block_below_cursor(cx); + let block = terminal_view::BlockProperties { + height, + render: Box::new(move |_| prompt_editor.clone().into_any_element()), + }; + terminal.set_block_below_cursor(block, window, cx); + }) + .log_err(); } } } @@ -450,23 +450,20 @@ impl TerminalInlineAssist { return; }; - if let CodegenStatus::Error(error) = &codegen.read(cx).status { - if assist.prompt_editor.is_none() { - if let Some(workspace) = assist.workspace.upgrade() { - let error = - format!("Terminal inline assistant error: {}", error); - workspace.update(cx, |workspace, cx| { - struct InlineAssistantError; + if let CodegenStatus::Error(error) = &codegen.read(cx).status + && assist.prompt_editor.is_none() + && let Some(workspace) = assist.workspace.upgrade() + { + let error = format!("Terminal inline assistant error: {}", error); + workspace.update(cx, |workspace, cx| { + struct InlineAssistantError; - let id = - NotificationId::composite::<InlineAssistantError>( - assist_id.0, - ); + let id = NotificationId::composite::<InlineAssistantError>( + assist_id.0, + ); - workspace.show_toast(Toast::new(id, error), cx); - }) - } - } + workspace.show_toast(Toast::new(id, error), cx); + }) } if assist.prompt_editor.is_none() { diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 376d3c54fd..3b5f2e5069 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -745,28 +745,27 @@ impl TextThreadEditor { ) { if let Some(invoked_slash_command) = self.context.read(cx).invoked_slash_command(&command_id) + && let InvokedSlashCommandStatus::Finished = invoked_slash_command.status { - if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status { - let run_commands_in_ranges = invoked_slash_command.run_commands_in_ranges.clone(); - for range in run_commands_in_ranges { - let commands = self.context.update(cx, |context, cx| { - context.reparse(cx); - context - .pending_commands_for_range(range.clone(), cx) - .to_vec() - }); + let run_commands_in_ranges = invoked_slash_command.run_commands_in_ranges.clone(); + for range in run_commands_in_ranges { + let commands = self.context.update(cx, |context, cx| { + context.reparse(cx); + context + .pending_commands_for_range(range.clone(), cx) + .to_vec() + }); - for command in commands { - self.run_command( - command.source_range, - &command.name, - &command.arguments, - false, - self.workspace.clone(), - window, - cx, - ); - } + for command in commands { + self.run_command( + command.source_range, + &command.name, + &command.arguments, + false, + self.workspace.clone(), + window, + cx, + ); } } } diff --git a/crates/agent_ui/src/thread_history.rs b/crates/agent_ui/src/thread_history.rs index 66afe2c2c5..4ec2078e5d 100644 --- a/crates/agent_ui/src/thread_history.rs +++ b/crates/agent_ui/src/thread_history.rs @@ -166,14 +166,13 @@ impl ThreadHistory { this.all_entries.len().saturating_sub(1), cx, ); - } else if let Some(prev_id) = previously_selected_entry { - if let Some(new_ix) = this + } else if let Some(prev_id) = previously_selected_entry + && let Some(new_ix) = this .all_entries .iter() .position(|probe| probe.id() == prev_id) - { - this.set_selected_entry_index(new_ix, cx); - } + { + this.set_selected_entry_index(new_ix, cx); } } SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => { diff --git a/crates/assistant_context/src/assistant_context.rs b/crates/assistant_context/src/assistant_context.rs index 06abbad39f..151586564f 100644 --- a/crates/assistant_context/src/assistant_context.rs +++ b/crates/assistant_context/src/assistant_context.rs @@ -1076,20 +1076,20 @@ impl AssistantContext { timestamp, .. } => { - if let Some(slash_command) = self.invoked_slash_commands.get_mut(&id) { - if timestamp > slash_command.timestamp { - slash_command.timestamp = timestamp; - match error_message { - Some(message) => { - slash_command.status = - InvokedSlashCommandStatus::Error(message.into()); - } - None => { - slash_command.status = InvokedSlashCommandStatus::Finished; - } + if let Some(slash_command) = self.invoked_slash_commands.get_mut(&id) + && timestamp > slash_command.timestamp + { + slash_command.timestamp = timestamp; + match error_message { + Some(message) => { + slash_command.status = + InvokedSlashCommandStatus::Error(message.into()); + } + None => { + slash_command.status = InvokedSlashCommandStatus::Finished; } - cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id: id }); } + cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id: id }); } } ContextOperation::BufferOperation(_) => unreachable!(), @@ -1368,10 +1368,10 @@ impl AssistantContext { continue; } - if let Some(last_anchor) = last_anchor { - if message.id == last_anchor { - hit_last_anchor = true; - } + if let Some(last_anchor) = last_anchor + && message.id == last_anchor + { + hit_last_anchor = true; } new_anchor_needs_caching = new_anchor_needs_caching @@ -1406,10 +1406,10 @@ impl AssistantContext { if !self.pending_completions.is_empty() { return; } - if let Some(cache_configuration) = cache_configuration { - if !cache_configuration.should_speculate { - return; - } + if let Some(cache_configuration) = cache_configuration + && !cache_configuration.should_speculate + { + return; } let request = { @@ -1552,25 +1552,24 @@ impl AssistantContext { }) .map(ToOwned::to_owned) .collect::<SmallVec<_>>(); - if let Some(command) = self.slash_commands.command(name, cx) { - if !command.requires_argument() || !arguments.is_empty() { - let start_ix = offset + command_line.name.start - 1; - let end_ix = offset - + command_line - .arguments - .last() - .map_or(command_line.name.end, |argument| argument.end); - let source_range = - buffer.anchor_after(start_ix)..buffer.anchor_after(end_ix); - let pending_command = ParsedSlashCommand { - name: name.to_string(), - arguments, - source_range, - status: PendingSlashCommandStatus::Idle, - }; - updated.push(pending_command.clone()); - new_commands.push(pending_command); - } + if let Some(command) = self.slash_commands.command(name, cx) + && (!command.requires_argument() || !arguments.is_empty()) + { + let start_ix = offset + command_line.name.start - 1; + let end_ix = offset + + command_line + .arguments + .last() + .map_or(command_line.name.end, |argument| argument.end); + let source_range = buffer.anchor_after(start_ix)..buffer.anchor_after(end_ix); + let pending_command = ParsedSlashCommand { + name: name.to_string(), + arguments, + source_range, + status: PendingSlashCommandStatus::Idle, + }; + updated.push(pending_command.clone()); + new_commands.push(pending_command); } } @@ -1799,14 +1798,13 @@ impl AssistantContext { }); let end = this.buffer.read(cx).anchor_before(insert_position); - if run_commands_in_text { - if let Some(invoked_slash_command) = + if run_commands_in_text + && let Some(invoked_slash_command) = this.invoked_slash_commands.get_mut(&command_id) - { - invoked_slash_command - .run_commands_in_ranges - .push(start..end); - } + { + invoked_slash_command + .run_commands_in_ranges + .push(start..end); } } SlashCommandEvent::EndSection => { @@ -2741,10 +2739,10 @@ impl AssistantContext { } this.read_with(cx, |this, _cx| { - if let Some(summary) = this.summary.content() { - if summary.text.is_empty() { - bail!("Model generated an empty summary"); - } + if let Some(summary) = this.summary.content() + && summary.text.is_empty() + { + bail!("Model generated an empty summary"); } Ok(()) })??; @@ -2924,18 +2922,18 @@ impl AssistantContext { fs.create_dir(contexts_dir().as_ref()).await?; // rename before write ensures that only one file exists - if let Some(old_path) = old_path.as_ref() { - if new_path.as_path() != old_path.as_ref() { - fs.rename( - old_path, - &new_path, - RenameOptions { - overwrite: true, - ignore_if_exists: true, - }, - ) - .await?; - } + if let Some(old_path) = old_path.as_ref() + && new_path.as_path() != old_path.as_ref() + { + fs.rename( + old_path, + &new_path, + RenameOptions { + overwrite: true, + ignore_if_exists: true, + }, + ) + .await?; } // update path before write in case it fails diff --git a/crates/assistant_context/src/context_store.rs b/crates/assistant_context/src/context_store.rs index 622d8867a7..af43b912e9 100644 --- a/crates/assistant_context/src/context_store.rs +++ b/crates/assistant_context/src/context_store.rs @@ -894,34 +894,33 @@ impl ContextStore { return; }; - if protocol.capable(context_server::protocol::ServerCapability::Prompts) { - if let Some(response) = protocol + if protocol.capable(context_server::protocol::ServerCapability::Prompts) + && let Some(response) = protocol .request::<context_server::types::requests::PromptsList>(()) .await .log_err() - { - let slash_command_ids = response - .prompts - .into_iter() - .filter(assistant_slash_commands::acceptable_prompt) - .map(|prompt| { - log::info!("registering context server command: {:?}", prompt.name); - slash_command_working_set.insert(Arc::new( - assistant_slash_commands::ContextServerSlashCommand::new( - context_server_store.clone(), - server.id(), - prompt, - ), - )) - }) - .collect::<Vec<_>>(); - - this.update(cx, |this, _cx| { - this.context_server_slash_command_ids - .insert(server_id.clone(), slash_command_ids); + { + let slash_command_ids = response + .prompts + .into_iter() + .filter(assistant_slash_commands::acceptable_prompt) + .map(|prompt| { + log::info!("registering context server command: {:?}", prompt.name); + slash_command_working_set.insert(Arc::new( + assistant_slash_commands::ContextServerSlashCommand::new( + context_server_store.clone(), + server.id(), + prompt, + ), + )) }) - .log_err(); - } + .collect::<Vec<_>>(); + + this.update(cx, |this, _cx| { + this.context_server_slash_command_ids + .insert(server_id.clone(), slash_command_ids); + }) + .log_err(); } }) .detach(); diff --git a/crates/assistant_slash_commands/src/context_server_command.rs b/crates/assistant_slash_commands/src/context_server_command.rs index 15f3901bfb..219c3b30bc 100644 --- a/crates/assistant_slash_commands/src/context_server_command.rs +++ b/crates/assistant_slash_commands/src/context_server_command.rs @@ -39,10 +39,10 @@ impl SlashCommand for ContextServerSlashCommand { fn label(&self, cx: &App) -> language::CodeLabel { let mut parts = vec![self.prompt.name.as_str()]; - if let Some(args) = &self.prompt.arguments { - if let Some(arg) = args.first() { - parts.push(arg.name.as_str()); - } + if let Some(args) = &self.prompt.arguments + && let Some(arg) = args.first() + { + parts.push(arg.name.as_str()); } create_label_for_command(parts[0], &parts[1..], cx) } diff --git a/crates/assistant_slash_commands/src/delta_command.rs b/crates/assistant_slash_commands/src/delta_command.rs index 8c840c17b2..2cc4591386 100644 --- a/crates/assistant_slash_commands/src/delta_command.rs +++ b/crates/assistant_slash_commands/src/delta_command.rs @@ -66,23 +66,22 @@ impl SlashCommand for DeltaSlashCommand { .metadata .as_ref() .and_then(|value| serde_json::from_value::<FileCommandMetadata>(value.clone()).ok()) + && paths.insert(metadata.path.clone()) { - if paths.insert(metadata.path.clone()) { - file_command_old_outputs.push( - context_buffer - .as_rope() - .slice(section.range.to_offset(&context_buffer)), - ); - file_command_new_outputs.push(Arc::new(FileSlashCommand).run( - std::slice::from_ref(&metadata.path), - context_slash_command_output_sections, - context_buffer.clone(), - workspace.clone(), - delegate.clone(), - window, - cx, - )); - } + file_command_old_outputs.push( + context_buffer + .as_rope() + .slice(section.range.to_offset(&context_buffer)), + ); + file_command_new_outputs.push(Arc::new(FileSlashCommand).run( + std::slice::from_ref(&metadata.path), + context_slash_command_output_sections, + context_buffer.clone(), + workspace.clone(), + delegate.clone(), + window, + cx, + )); } } @@ -95,25 +94,25 @@ impl SlashCommand for DeltaSlashCommand { .into_iter() .zip(file_command_new_outputs) { - if let Ok(new_output) = new_output { - if let Ok(new_output) = SlashCommandOutput::from_event_stream(new_output).await - { - if let Some(file_command_range) = new_output.sections.first() { - let new_text = &new_output.text[file_command_range.range.clone()]; - if old_text.chars().ne(new_text.chars()) { - changes_detected = true; - output.sections.extend(new_output.sections.into_iter().map( - |section| SlashCommandOutputSection { - range: output.text.len() + section.range.start - ..output.text.len() + section.range.end, - icon: section.icon, - label: section.label, - metadata: section.metadata, - }, - )); - output.text.push_str(&new_output.text); - } - } + if let Ok(new_output) = new_output + && let Ok(new_output) = SlashCommandOutput::from_event_stream(new_output).await + && let Some(file_command_range) = new_output.sections.first() + { + let new_text = &new_output.text[file_command_range.range.clone()]; + if old_text.chars().ne(new_text.chars()) { + changes_detected = true; + output + .sections + .extend(new_output.sections.into_iter().map(|section| { + SlashCommandOutputSection { + range: output.text.len() + section.range.start + ..output.text.len() + section.range.end, + icon: section.icon, + label: section.label, + metadata: section.metadata, + } + })); + output.text.push_str(&new_output.text); } } } diff --git a/crates/assistant_slash_commands/src/diagnostics_command.rs b/crates/assistant_slash_commands/src/diagnostics_command.rs index 31014f8fb8..45c976c826 100644 --- a/crates/assistant_slash_commands/src/diagnostics_command.rs +++ b/crates/assistant_slash_commands/src/diagnostics_command.rs @@ -280,10 +280,10 @@ fn collect_diagnostics( let mut project_summary = DiagnosticSummary::default(); for (project_path, path, summary) in diagnostic_summaries { - if let Some(path_matcher) = &options.path_matcher { - if !path_matcher.is_match(&path) { - continue; - } + if let Some(path_matcher) = &options.path_matcher + && !path_matcher.is_match(&path) + { + continue; } project_summary.error_count += summary.error_count; diff --git a/crates/assistant_slash_commands/src/tab_command.rs b/crates/assistant_slash_commands/src/tab_command.rs index ca7601bc4c..e4ae391a9c 100644 --- a/crates/assistant_slash_commands/src/tab_command.rs +++ b/crates/assistant_slash_commands/src/tab_command.rs @@ -195,16 +195,14 @@ fn tab_items_for_queries( } for editor in workspace.items_of_type::<Editor>(cx) { - if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() { - if let Some(timestamp) = + if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() + && let Some(timestamp) = timestamps_by_entity_id.get(&editor.entity_id()) - { - if visited_buffers.insert(buffer.read(cx).remote_id()) { - let snapshot = buffer.read(cx).snapshot(); - let full_path = snapshot.resolve_file_path(cx, true); - open_buffers.push((full_path, snapshot, *timestamp)); - } - } + && visited_buffers.insert(buffer.read(cx).remote_id()) + { + let snapshot = buffer.read(cx).snapshot(); + let full_path = snapshot.resolve_file_path(cx, true); + open_buffers.push((full_path, snapshot, *timestamp)); } } diff --git a/crates/assistant_tool/src/tool_schema.rs b/crates/assistant_tool/src/tool_schema.rs index 7b48f93ba6..192f7c8a2b 100644 --- a/crates/assistant_tool/src/tool_schema.rs +++ b/crates/assistant_tool/src/tool_schema.rs @@ -24,16 +24,16 @@ pub fn adapt_schema_to_format( fn preprocess_json_schema(json: &mut Value) -> Result<()> { // `additionalProperties` defaults to `false` unless explicitly specified. // This prevents models from hallucinating tool parameters. - if let Value::Object(obj) = json { - if matches!(obj.get("type"), Some(Value::String(s)) if s == "object") { - if !obj.contains_key("additionalProperties") { - obj.insert("additionalProperties".to_string(), Value::Bool(false)); - } + if let Value::Object(obj) = json + && matches!(obj.get("type"), Some(Value::String(s)) if s == "object") + { + if !obj.contains_key("additionalProperties") { + obj.insert("additionalProperties".to_string(), Value::Bool(false)); + } - // OpenAI API requires non-missing `properties` - if !obj.contains_key("properties") { - obj.insert("properties".to_string(), Value::Object(Default::default())); - } + // OpenAI API requires non-missing `properties` + if !obj.contains_key("properties") { + obj.insert("properties".to_string(), Value::Object(Default::default())); } } Ok(()) @@ -59,10 +59,10 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> { ("optional", |value| value.is_boolean()), ]; for (key, predicate) in KEYS_TO_REMOVE { - if let Some(value) = obj.get(key) { - if predicate(value) { - obj.remove(key); - } + if let Some(value) = obj.get(key) + && predicate(value) + { + obj.remove(key); } } @@ -77,12 +77,12 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> { } // Handle oneOf -> anyOf conversion - if let Some(subschemas) = obj.get_mut("oneOf") { - if subschemas.is_array() { - let subschemas_clone = subschemas.clone(); - obj.remove("oneOf"); - obj.insert("anyOf".to_string(), subschemas_clone); - } + if let Some(subschemas) = obj.get_mut("oneOf") + && subschemas.is_array() + { + let subschemas_clone = subschemas.clone(); + obj.remove("oneOf"); + obj.insert("anyOf".to_string(), subschemas_clone); } // Recursively process all nested objects and arrays diff --git a/crates/assistant_tools/src/edit_agent.rs b/crates/assistant_tools/src/edit_agent.rs index aa321aa8f3..665ece2baa 100644 --- a/crates/assistant_tools/src/edit_agent.rs +++ b/crates/assistant_tools/src/edit_agent.rs @@ -672,29 +672,30 @@ impl EditAgent { cx: &mut AsyncApp, ) -> Result<BoxStream<'static, Result<String, LanguageModelCompletionError>>> { let mut messages_iter = conversation.messages.iter_mut(); - if let Some(last_message) = messages_iter.next_back() { - if last_message.role == Role::Assistant { - let old_content_len = last_message.content.len(); - last_message - .content - .retain(|content| !matches!(content, MessageContent::ToolUse(_))); - let new_content_len = last_message.content.len(); + if let Some(last_message) = messages_iter.next_back() + && last_message.role == Role::Assistant + { + let old_content_len = last_message.content.len(); + last_message + .content + .retain(|content| !matches!(content, MessageContent::ToolUse(_))); + let new_content_len = last_message.content.len(); - // We just removed pending tool uses from the content of the - // last message, so it doesn't make sense to cache it anymore - // (e.g., the message will look very different on the next - // request). Thus, we move the flag to the message prior to it, - // as it will still be a valid prefix of the conversation. - if old_content_len != new_content_len && last_message.cache { - if let Some(prev_message) = messages_iter.next_back() { - last_message.cache = false; - prev_message.cache = true; - } - } + // We just removed pending tool uses from the content of the + // last message, so it doesn't make sense to cache it anymore + // (e.g., the message will look very different on the next + // request). Thus, we move the flag to the message prior to it, + // as it will still be a valid prefix of the conversation. + if old_content_len != new_content_len + && last_message.cache + && let Some(prev_message) = messages_iter.next_back() + { + last_message.cache = false; + prev_message.cache = true; + } - if last_message.content.is_empty() { - conversation.messages.pop(); - } + if last_message.content.is_empty() { + conversation.messages.pop(); } } diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/assistant_tools/src/edit_agent/evals.rs index 9a8e762455..0d529a5573 100644 --- a/crates/assistant_tools/src/edit_agent/evals.rs +++ b/crates/assistant_tools/src/edit_agent/evals.rs @@ -1283,14 +1283,14 @@ impl EvalAssertion { // Parse the score from the response let re = regex::Regex::new(r"<score>(\d+)</score>").unwrap(); - if let Some(captures) = re.captures(&output) { - if let Some(score_match) = captures.get(1) { - let score = score_match.as_str().parse().unwrap_or(0); - return Ok(EvalAssertionOutcome { - score, - message: Some(output), - }); - } + if let Some(captures) = re.captures(&output) + && let Some(score_match) = captures.get(1) + { + let score = score_match.as_str().parse().unwrap_or(0); + return Ok(EvalAssertionOutcome { + score, + message: Some(output), + }); } anyhow::bail!("No score found in response. Raw output: {output}"); diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 039f9d9316..2d6b5ce924 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -155,10 +155,10 @@ impl Tool for EditFileTool { // It's also possible that the global config dir is configured to be inside the project, // so check for that edge case too. - if let Ok(canonical_path) = std::fs::canonicalize(&input.path) { - if canonical_path.starts_with(paths::config_dir()) { - return true; - } + if let Ok(canonical_path) = std::fs::canonicalize(&input.path) + && canonical_path.starts_with(paths::config_dir()) + { + return true; } // Check if path is inside the global config directory @@ -199,10 +199,10 @@ impl Tool for EditFileTool { .any(|c| c.as_os_str() == local_settings_folder.as_os_str()) { description.push_str(" (local settings)"); - } else if let Ok(canonical_path) = std::fs::canonicalize(&input.path) { - if canonical_path.starts_with(paths::config_dir()) { - description.push_str(" (global settings)"); - } + } else if let Ok(canonical_path) = std::fs::canonicalize(&input.path) + && canonical_path.starts_with(paths::config_dir()) + { + description.push_str(" (global settings)"); } description diff --git a/crates/assistant_tools/src/grep_tool.rs b/crates/assistant_tools/src/grep_tool.rs index 1f00332c5a..1dd74b99e7 100644 --- a/crates/assistant_tools/src/grep_tool.rs +++ b/crates/assistant_tools/src/grep_tool.rs @@ -188,15 +188,14 @@ impl Tool for GrepTool { // Check if this file should be excluded based on its worktree settings if let Ok(Some(project_path)) = project.read_with(cx, |project, cx| { project.find_project_path(&path, cx) - }) { - if cx.update(|cx| { + }) + && cx.update(|cx| { let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx); worktree_settings.is_path_excluded(&project_path.path) || worktree_settings.is_path_private(&project_path.path) }).unwrap_or(false) { continue; } - } while *parse_status.borrow() != ParseStatus::Idle { parse_status.changed().await?; @@ -284,12 +283,11 @@ impl Tool for GrepTool { output.extend(snapshot.text_for_range(range)); output.push_str("\n```\n"); - if let Some(ancestor_range) = ancestor_range { - if end_row < ancestor_range.end.row { + if let Some(ancestor_range) = ancestor_range + && end_row < ancestor_range.end.row { let remaining_lines = ancestor_range.end.row - end_row; writeln!(output, "\n{} lines remaining in ancestor node. Read the file to see all.", remaining_lines)?; } - } matches_found += 1; } diff --git a/crates/assistant_tools/src/schema.rs b/crates/assistant_tools/src/schema.rs index 10a8bf0acd..dab7384efd 100644 --- a/crates/assistant_tools/src/schema.rs +++ b/crates/assistant_tools/src/schema.rs @@ -43,12 +43,11 @@ impl Transform for ToJsonSchemaSubsetTransform { fn transform(&mut self, schema: &mut Schema) { // Ensure that the type field is not an array, this happens when we use // Option<T>, the type will be [T, "null"]. - if let Some(type_field) = schema.get_mut("type") { - if let Some(types) = type_field.as_array() { - if let Some(first_type) = types.first() { - *type_field = first_type.clone(); - } - } + if let Some(type_field) = schema.get_mut("type") + && let Some(types) = type_field.as_array() + && let Some(first_type) = types.first() + { + *type_field = first_type.clone(); } // oneOf is not supported, use anyOf instead diff --git a/crates/auto_update_helper/src/dialog.rs b/crates/auto_update_helper/src/dialog.rs index 757819df51..903ac34da2 100644 --- a/crates/auto_update_helper/src/dialog.rs +++ b/crates/auto_update_helper/src/dialog.rs @@ -186,11 +186,11 @@ unsafe extern "system" fn wnd_proc( }), WM_TERMINATE => { with_dialog_data(hwnd, |data| { - if let Ok(result) = data.borrow_mut().rx.recv() { - if let Err(e) = result { - log::error!("Failed to update Zed: {:?}", e); - show_error(format!("Error: {:?}", e)); - } + if let Ok(result) = data.borrow_mut().rx.recv() + && let Err(e) = result + { + log::error!("Failed to update Zed: {:?}", e); + show_error(format!("Error: {:?}", e)); } }); unsafe { PostQuitMessage(0) }; diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 990fc27fbd..a6b27476fe 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -82,11 +82,12 @@ impl Render for Breadcrumbs { } text_style.color = Color::Muted.color(cx); - if index == 0 && !TabBarSettings::get_global(cx).show && active_item.is_dirty(cx) { - if let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx) - { - return styled_element; - } + if index == 0 + && !TabBarSettings::get_global(cx).show + && active_item.is_dirty(cx) + && let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx) + { + return styled_element; } StyledText::new(segment.text.replace('\n', "⏎")) diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index e20ea9713f..6b38fe5576 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -572,14 +572,14 @@ impl BufferDiffInner { pending_range.end.column = 0; } - if pending_range == (start_point..end_point) { - if !buffer.has_edits_since_in_range( + if pending_range == (start_point..end_point) + && !buffer.has_edits_since_in_range( &pending_hunk.buffer_version, start_anchor..end_anchor, - ) { - has_pending = true; - secondary_status = pending_hunk.new_status; - } + ) + { + has_pending = true; + secondary_status = pending_hunk.new_status; } } @@ -1036,16 +1036,15 @@ impl BufferDiff { _ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)), }; - if let Some(secondary_changed_range) = secondary_diff_change { - if let Some(secondary_hunk_range) = + if let Some(secondary_changed_range) = secondary_diff_change + && let Some(secondary_hunk_range) = self.range_to_hunk_range(secondary_changed_range, buffer, cx) - { - if let Some(range) = &mut changed_range { - range.start = secondary_hunk_range.start.min(&range.start, buffer); - range.end = secondary_hunk_range.end.max(&range.end, buffer); - } else { - changed_range = Some(secondary_hunk_range); - } + { + if let Some(range) = &mut changed_range { + range.start = secondary_hunk_range.start.min(&range.start, buffer); + range.end = secondary_hunk_range.end.max(&range.end, buffer); + } else { + changed_range = Some(secondary_hunk_range); } } diff --git a/crates/call/src/call_impl/room.rs b/crates/call/src/call_impl/room.rs index 73cb8518a6..bab99cd3f3 100644 --- a/crates/call/src/call_impl/room.rs +++ b/crates/call/src/call_impl/room.rs @@ -827,24 +827,23 @@ impl Room { ); Audio::play_sound(Sound::Joined, cx); - if let Some(livekit_participants) = &livekit_participants { - if let Some(livekit_participant) = livekit_participants + if let Some(livekit_participants) = &livekit_participants + && let Some(livekit_participant) = livekit_participants .get(&ParticipantIdentity(user.id.to_string())) + { + for publication in + livekit_participant.track_publications().into_values() { - for publication in - livekit_participant.track_publications().into_values() - { - if let Some(track) = publication.track() { - this.livekit_room_updated( - RoomEvent::TrackSubscribed { - track, - publication, - participant: livekit_participant.clone(), - }, - cx, - ) - .warn_on_err(); - } + if let Some(track) = publication.track() { + this.livekit_room_updated( + RoomEvent::TrackSubscribed { + track, + publication, + participant: livekit_participant.clone(), + }, + cx, + ) + .warn_on_err(); } } } @@ -940,10 +939,9 @@ impl Room { self.client.user_id() ) })?; - if self.live_kit.as_ref().map_or(true, |kit| kit.deafened) { - if publication.is_audio() { - publication.set_enabled(false, cx); - } + if self.live_kit.as_ref().map_or(true, |kit| kit.deafened) && publication.is_audio() + { + publication.set_enabled(false, cx); } match track { livekit_client::RemoteTrack::Audio(track) => { @@ -1005,10 +1003,10 @@ impl Room { for (sid, participant) in &mut self.remote_participants { participant.speaking = speaker_ids.binary_search(sid).is_ok(); } - if let Some(id) = self.client.user_id() { - if let Some(room) = &mut self.live_kit { - room.speaking = speaker_ids.binary_search(&id).is_ok(); - } + if let Some(id) = self.client.user_id() + && let Some(room) = &mut self.live_kit + { + room.speaking = speaker_ids.binary_search(&id).is_ok(); } } @@ -1042,18 +1040,16 @@ impl Room { if let LocalTrack::Published { track_publication, .. } = &room.microphone_track + && track_publication.sid() == publication.sid() { - if track_publication.sid() == publication.sid() { - room.microphone_track = LocalTrack::None; - } + room.microphone_track = LocalTrack::None; } if let LocalTrack::Published { track_publication, .. } = &room.screen_track + && track_publication.sid() == publication.sid() { - if track_publication.sid() == publication.sid() { - room.screen_track = LocalTrack::None; - } + room.screen_track = LocalTrack::None; } } } @@ -1484,10 +1480,8 @@ impl Room { self.set_deafened(deafened, cx); - if should_change_mute { - if let Some(task) = self.set_mute(deafened, cx) { - task.detach_and_log_err(cx); - } + if should_change_mute && let Some(task) = self.set_mute(deafened, cx) { + task.detach_and_log_err(cx); } } } diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 183f7eb3c6..a367ffbf09 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -191,12 +191,11 @@ impl ChannelBuffer { operation, is_local: true, } => { - if *ZED_ALWAYS_ACTIVE { - if let language::Operation::UpdateSelections { selections, .. } = operation { - if selections.is_empty() { - return; - } - } + if *ZED_ALWAYS_ACTIVE + && let language::Operation::UpdateSelections { selections, .. } = operation + && selections.is_empty() + { + return; } let operation = language::proto::serialize_operation(operation); self.client diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index 4ac37ffd14..02b5ccec68 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -329,24 +329,24 @@ impl ChannelChat { loop { let step = chat .update(&mut cx, |chat, cx| { - if let Some(first_id) = chat.first_loaded_message_id() { - if first_id <= message_id { - let mut cursor = chat - .messages - .cursor::<Dimensions<ChannelMessageId, Count>>(&()); - let message_id = ChannelMessageId::Saved(message_id); - cursor.seek(&message_id, Bias::Left); - return ControlFlow::Break( - if cursor - .item() - .map_or(false, |message| message.id == message_id) - { - Some(cursor.start().1.0) - } else { - None - }, - ); - } + if let Some(first_id) = chat.first_loaded_message_id() + && first_id <= message_id + { + let mut cursor = chat + .messages + .cursor::<Dimensions<ChannelMessageId, Count>>(&()); + let message_id = ChannelMessageId::Saved(message_id); + cursor.seek(&message_id, Bias::Left); + return ControlFlow::Break( + if cursor + .item() + .map_or(false, |message| message.id == message_id) + { + Some(cursor.start().1.0) + } else { + None + }, + ); } ControlFlow::Continue(chat.load_more_messages(cx)) }) @@ -359,22 +359,21 @@ impl ChannelChat { } pub fn acknowledge_last_message(&mut self, cx: &mut Context<Self>) { - if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id { - if self + if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id + && self .last_acknowledged_id .map_or(true, |acknowledged_id| acknowledged_id < latest_message_id) - { - self.rpc - .send(proto::AckChannelMessage { - channel_id: self.channel_id.0, - message_id: latest_message_id, - }) - .ok(); - self.last_acknowledged_id = Some(latest_message_id); - self.channel_store.update(cx, |store, cx| { - store.acknowledge_message_id(self.channel_id, latest_message_id, cx); - }); - } + { + self.rpc + .send(proto::AckChannelMessage { + channel_id: self.channel_id.0, + message_id: latest_message_id, + }) + .ok(); + self.last_acknowledged_id = Some(latest_message_id); + self.channel_store.update(cx, |store, cx| { + store.acknowledge_message_id(self.channel_id, latest_message_id, cx); + }); } } @@ -407,10 +406,10 @@ impl ChannelChat { let missing_ancestors = loaded_messages .iter() .filter_map(|message| { - if let Some(ancestor_id) = message.reply_to_message_id { - if !loaded_message_ids.contains(&ancestor_id) { - return Some(ancestor_id); - } + if let Some(ancestor_id) = message.reply_to_message_id + && !loaded_message_ids.contains(&ancestor_id) + { + return Some(ancestor_id); } None }) @@ -646,32 +645,32 @@ impl ChannelChat { fn message_removed(&mut self, id: u64, cx: &mut Context<Self>) { let mut cursor = self.messages.cursor::<ChannelMessageId>(&()); let mut messages = cursor.slice(&ChannelMessageId::Saved(id), Bias::Left); - if let Some(item) = cursor.item() { - if item.id == ChannelMessageId::Saved(id) { - let deleted_message_ix = messages.summary().count; - cursor.next(); - messages.append(cursor.suffix(), &()); - drop(cursor); - self.messages = messages; + if let Some(item) = cursor.item() + && item.id == ChannelMessageId::Saved(id) + { + let deleted_message_ix = messages.summary().count; + cursor.next(); + messages.append(cursor.suffix(), &()); + drop(cursor); + self.messages = messages; - // If the message that was deleted was the last acknowledged message, - // replace the acknowledged message with an earlier one. - self.channel_store.update(cx, |store, _| { - let summary = self.messages.summary(); - if summary.count == 0 { - store.set_acknowledged_message_id(self.channel_id, None); - } else if deleted_message_ix == summary.count { - if let ChannelMessageId::Saved(id) = summary.max_id { - store.set_acknowledged_message_id(self.channel_id, Some(id)); - } - } - }); + // If the message that was deleted was the last acknowledged message, + // replace the acknowledged message with an earlier one. + self.channel_store.update(cx, |store, _| { + let summary = self.messages.summary(); + if summary.count == 0 { + store.set_acknowledged_message_id(self.channel_id, None); + } else if deleted_message_ix == summary.count + && let ChannelMessageId::Saved(id) = summary.max_id + { + store.set_acknowledged_message_id(self.channel_id, Some(id)); + } + }); - cx.emit(ChannelChatEvent::MessagesUpdated { - old_range: deleted_message_ix..deleted_message_ix + 1, - new_count: 0, - }); - } + cx.emit(ChannelChatEvent::MessagesUpdated { + old_range: deleted_message_ix..deleted_message_ix + 1, + new_count: 0, + }); } } diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 4ad156b9fb..6d1716a7ea 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -262,13 +262,12 @@ impl ChannelStore { } } status = status_receiver.next().fuse() => { - if let Some(status) = status { - if status.is_connected() { + if let Some(status) = status + && status.is_connected() { this.update(cx, |this, _cx| { this.initialize(); }).ok(); } - } continue; } _ = timer => { @@ -336,10 +335,10 @@ impl ChannelStore { } pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &App) -> bool { - if let Some(buffer) = self.opened_buffers.get(&channel_id) { - if let OpenEntityHandle::Open(buffer) = buffer { - return buffer.upgrade().is_some(); - } + if let Some(buffer) = self.opened_buffers.get(&channel_id) + && let OpenEntityHandle::Open(buffer) = buffer + { + return buffer.upgrade().is_some(); } false } @@ -408,13 +407,12 @@ impl ChannelStore { pub fn last_acknowledge_message_id(&self, channel_id: ChannelId) -> Option<u64> { self.channel_states.get(&channel_id).and_then(|state| { - if let Some(last_message_id) = state.latest_chat_message { - if state + if let Some(last_message_id) = state.latest_chat_message + && state .last_acknowledged_message_id() .is_some_and(|id| id < last_message_id) - { - return state.last_acknowledged_message_id(); - } + { + return state.last_acknowledged_message_id(); } None @@ -962,27 +960,27 @@ impl ChannelStore { self.disconnect_channel_buffers_task.take(); for chat in self.opened_chats.values() { - if let OpenEntityHandle::Open(chat) = chat { - if let Some(chat) = chat.upgrade() { - chat.update(cx, |chat, cx| { - chat.rejoin(cx); - }); - } + if let OpenEntityHandle::Open(chat) = chat + && let Some(chat) = chat.upgrade() + { + chat.update(cx, |chat, cx| { + chat.rejoin(cx); + }); } } let mut buffer_versions = Vec::new(); for buffer in self.opened_buffers.values() { - if let OpenEntityHandle::Open(buffer) = buffer { - if let Some(buffer) = buffer.upgrade() { - let channel_buffer = buffer.read(cx); - let buffer = channel_buffer.buffer().read(cx); - buffer_versions.push(proto::ChannelBufferVersion { - channel_id: channel_buffer.channel_id.0, - epoch: channel_buffer.epoch(), - version: language::proto::serialize_version(&buffer.version()), - }); - } + if let OpenEntityHandle::Open(buffer) = buffer + && let Some(buffer) = buffer.upgrade() + { + let channel_buffer = buffer.read(cx); + let buffer = channel_buffer.buffer().read(cx); + buffer_versions.push(proto::ChannelBufferVersion { + channel_id: channel_buffer.channel_id.0, + epoch: channel_buffer.epoch(), + version: language::proto::serialize_version(&buffer.version()), + }); } } @@ -1078,10 +1076,10 @@ impl ChannelStore { if let Some(this) = this.upgrade() { this.update(cx, |this, cx| { for (_, buffer) in &this.opened_buffers { - if let OpenEntityHandle::Open(buffer) = &buffer { - if let Some(buffer) = buffer.upgrade() { - buffer.update(cx, |buffer, cx| buffer.disconnect(cx)); - } + if let OpenEntityHandle::Open(buffer) = &buffer + && let Some(buffer) = buffer.upgrade() + { + buffer.update(cx, |buffer, cx| buffer.disconnect(cx)); } } }) @@ -1157,10 +1155,9 @@ impl ChannelStore { } if let Some(OpenEntityHandle::Open(buffer)) = self.opened_buffers.remove(&channel_id) + && let Some(buffer) = buffer.upgrade() { - if let Some(buffer) = buffer.upgrade() { - buffer.update(cx, ChannelBuffer::disconnect); - } + buffer.update(cx, ChannelBuffer::disconnect); } } } @@ -1170,12 +1167,11 @@ impl ChannelStore { let id = ChannelId(channel.id); let channel_changed = index.insert(channel); - if channel_changed { - if let Some(OpenEntityHandle::Open(buffer)) = self.opened_buffers.get(&id) { - if let Some(buffer) = buffer.upgrade() { - buffer.update(cx, ChannelBuffer::channel_changed); - } - } + if channel_changed + && let Some(OpenEntityHandle::Open(buffer)) = self.opened_buffers.get(&id) + && let Some(buffer) = buffer.upgrade() + { + buffer.update(cx, ChannelBuffer::channel_changed); } } diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index a61d8e0911..d8b46dabb6 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -587,13 +587,10 @@ mod flatpak { pub fn set_bin_if_no_escape(mut args: super::Args) -> super::Args { if env::var(NO_ESCAPE_ENV_NAME).is_ok() && env::var("FLATPAK_ID").map_or(false, |id| id.starts_with("dev.zed.Zed")) + && args.zed.is_none() { - if args.zed.is_none() { - args.zed = Some("/app/libexec/zed-editor".into()); - unsafe { - env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed") - }; - } + args.zed = Some("/app/libexec/zed-editor".into()); + unsafe { env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed") }; } args } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 66d5fd89b1..d7d8b60211 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -864,22 +864,23 @@ impl Client { let mut credentials = None; let old_credentials = self.state.read().credentials.clone(); - if let Some(old_credentials) = old_credentials { - if self.validate_credentials(&old_credentials, cx).await? { - credentials = Some(old_credentials); - } + if let Some(old_credentials) = old_credentials + && self.validate_credentials(&old_credentials, cx).await? + { + credentials = Some(old_credentials); } - if credentials.is_none() && try_provider { - if let Some(stored_credentials) = self.credentials_provider.read_credentials(cx).await { - if self.validate_credentials(&stored_credentials, cx).await? { - credentials = Some(stored_credentials); - } else { - self.credentials_provider - .delete_credentials(cx) - .await - .log_err(); - } + if credentials.is_none() + && try_provider + && let Some(stored_credentials) = self.credentials_provider.read_credentials(cx).await + { + if self.validate_credentials(&stored_credentials, cx).await? { + credentials = Some(stored_credentials); + } else { + self.credentials_provider + .delete_credentials(cx) + .await + .log_err(); } } diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index da7f50076b..3509a8c57f 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -894,10 +894,10 @@ impl UserStore { let mut ret = Vec::with_capacity(users.len()); for user in users { let user = User::new(user); - if let Some(old) = self.users.insert(user.id, user.clone()) { - if old.github_login != user.github_login { - self.by_github_login.remove(&old.github_login); - } + if let Some(old) = self.users.insert(user.id, user.clone()) + && old.github_login != user.github_login + { + self.by_github_login.remove(&old.github_login); } self.by_github_login .insert(user.github_login.clone(), user.id); diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index cd1dc42e64..c500872fd7 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -149,35 +149,35 @@ pub async fn post_crash( "crash report" ); - if let Some(kinesis_client) = app.kinesis_client.clone() { - if let Some(stream) = app.config.kinesis_stream.clone() { - let properties = json!({ - "app_version": report.header.app_version, - "os_version": report.header.os_version, - "os_name": "macOS", - "bundle_id": report.header.bundle_id, - "incident_id": report.header.incident_id, - "installation_id": installation_id, - "description": description, - "backtrace": summary, - }); - let row = SnowflakeRow::new( - "Crash Reported", - None, - false, - Some(installation_id), - properties, - ); - let data = serde_json::to_vec(&row)?; - kinesis_client - .put_record() - .stream_name(stream) - .partition_key(row.insert_id.unwrap_or_default()) - .data(data.into()) - .send() - .await - .log_err(); - } + if let Some(kinesis_client) = app.kinesis_client.clone() + && let Some(stream) = app.config.kinesis_stream.clone() + { + let properties = json!({ + "app_version": report.header.app_version, + "os_version": report.header.os_version, + "os_name": "macOS", + "bundle_id": report.header.bundle_id, + "incident_id": report.header.incident_id, + "installation_id": installation_id, + "description": description, + "backtrace": summary, + }); + let row = SnowflakeRow::new( + "Crash Reported", + None, + false, + Some(installation_id), + properties, + ); + let data = serde_json::to_vec(&row)?; + kinesis_client + .put_record() + .stream_name(stream) + .partition_key(row.insert_id.unwrap_or_default()) + .data(data.into()) + .send() + .await + .log_err(); } if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() { @@ -359,34 +359,34 @@ pub async fn post_panic( "panic report" ); - if let Some(kinesis_client) = app.kinesis_client.clone() { - if let Some(stream) = app.config.kinesis_stream.clone() { - let properties = json!({ - "app_version": panic.app_version, - "os_name": panic.os_name, - "os_version": panic.os_version, - "incident_id": incident_id, - "installation_id": panic.installation_id, - "description": panic.payload, - "backtrace": backtrace, - }); - let row = SnowflakeRow::new( - "Panic Reported", - None, - false, - panic.installation_id.clone(), - properties, - ); - let data = serde_json::to_vec(&row)?; - kinesis_client - .put_record() - .stream_name(stream) - .partition_key(row.insert_id.unwrap_or_default()) - .data(data.into()) - .send() - .await - .log_err(); - } + if let Some(kinesis_client) = app.kinesis_client.clone() + && let Some(stream) = app.config.kinesis_stream.clone() + { + let properties = json!({ + "app_version": panic.app_version, + "os_name": panic.os_name, + "os_version": panic.os_version, + "incident_id": incident_id, + "installation_id": panic.installation_id, + "description": panic.payload, + "backtrace": backtrace, + }); + let row = SnowflakeRow::new( + "Panic Reported", + None, + false, + panic.installation_id.clone(), + properties, + ); + let data = serde_json::to_vec(&row)?; + kinesis_client + .put_record() + .stream_name(stream) + .partition_key(row.insert_id.unwrap_or_default()) + .data(data.into()) + .send() + .await + .log_err(); } if !report_to_slack(&panic) { @@ -518,31 +518,31 @@ pub async fn post_events( let first_event_at = chrono::Utc::now() - chrono::Duration::milliseconds(last_event.milliseconds_since_first_event); - if let Some(kinesis_client) = app.kinesis_client.clone() { - if let Some(stream) = app.config.kinesis_stream.clone() { - let mut request = kinesis_client.put_records().stream_name(stream); - let mut has_records = false; - for row in for_snowflake( - request_body.clone(), - first_event_at, - country_code.clone(), - checksum_matched, - ) { - if let Some(data) = serde_json::to_vec(&row).log_err() { - request = request.records( - aws_sdk_kinesis::types::PutRecordsRequestEntry::builder() - .partition_key(request_body.system_id.clone().unwrap_or_default()) - .data(data.into()) - .build() - .unwrap(), - ); - has_records = true; - } - } - if has_records { - request.send().await.log_err(); + if let Some(kinesis_client) = app.kinesis_client.clone() + && let Some(stream) = app.config.kinesis_stream.clone() + { + let mut request = kinesis_client.put_records().stream_name(stream); + let mut has_records = false; + for row in for_snowflake( + request_body.clone(), + first_event_at, + country_code.clone(), + checksum_matched, + ) { + if let Some(data) = serde_json::to_vec(&row).log_err() { + request = request.records( + aws_sdk_kinesis::types::PutRecordsRequestEntry::builder() + .partition_key(request_body.system_id.clone().unwrap_or_default()) + .data(data.into()) + .build() + .unwrap(), + ); + has_records = true; } } + if has_records { + request.send().await.log_err(); + } }; Ok(()) diff --git a/crates/collab/src/api/extensions.rs b/crates/collab/src/api/extensions.rs index 9170c39e47..1ace433db2 100644 --- a/crates/collab/src/api/extensions.rs +++ b/crates/collab/src/api/extensions.rs @@ -337,8 +337,7 @@ async fn fetch_extensions_from_blob_store( if known_versions .binary_search_by_key(&published_version, |known_version| known_version) .is_err() - { - if let Some(extension) = fetch_extension_manifest( + && let Some(extension) = fetch_extension_manifest( blob_store_client, blob_store_bucket, extension_id, @@ -346,12 +345,11 @@ async fn fetch_extensions_from_blob_store( ) .await .log_err() - { - new_versions - .entry(extension_id) - .or_default() - .push(extension); - } + { + new_versions + .entry(extension_id) + .or_default() + .push(extension); } } } diff --git a/crates/collab/src/auth.rs b/crates/collab/src/auth.rs index 00f37c6758..5a2a1329bb 100644 --- a/crates/collab/src/auth.rs +++ b/crates/collab/src/auth.rs @@ -79,27 +79,27 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into verify_access_token(access_token, user_id, &state.db).await }; - if let Ok(validate_result) = validate_result { - if validate_result.is_valid { - let user = state - .db - .get_user_by_id(user_id) - .await? - .with_context(|| format!("user {user_id} not found"))?; + if let Ok(validate_result) = validate_result + && validate_result.is_valid + { + let user = state + .db + .get_user_by_id(user_id) + .await? + .with_context(|| format!("user {user_id} not found"))?; - if let Some(impersonator_id) = validate_result.impersonator_id { - let admin = state - .db - .get_user_by_id(impersonator_id) - .await? - .with_context(|| format!("user {impersonator_id} not found"))?; - req.extensions_mut() - .insert(Principal::Impersonated { user, admin }); - } else { - req.extensions_mut().insert(Principal::User(user)); - }; - return Ok::<_, Error>(next.run(req).await); - } + if let Some(impersonator_id) = validate_result.impersonator_id { + let admin = state + .db + .get_user_by_id(impersonator_id) + .await? + .with_context(|| format!("user {impersonator_id} not found"))?; + req.extensions_mut() + .insert(Principal::Impersonated { user, admin }); + } else { + req.extensions_mut().insert(Principal::User(user)); + }; + return Ok::<_, Error>(next.run(req).await); } Err(Error::http( diff --git a/crates/collab/src/db/queries/extensions.rs b/crates/collab/src/db/queries/extensions.rs index 7d8aad2be4..f218ff2850 100644 --- a/crates/collab/src/db/queries/extensions.rs +++ b/crates/collab/src/db/queries/extensions.rs @@ -87,10 +87,10 @@ impl Database { continue; }; - if let Some((_, max_extension_version)) = &max_versions.get(&version.extension_id) { - if max_extension_version > &extension_version { - continue; - } + if let Some((_, max_extension_version)) = &max_versions.get(&version.extension_id) + && max_extension_version > &extension_version + { + continue; } if let Some(constraints) = constraints { @@ -331,10 +331,10 @@ impl Database { .exec_without_returning(&*tx) .await?; - if let Ok(db_version) = semver::Version::parse(&extension.latest_version) { - if db_version >= latest_version.version { - continue; - } + if let Ok(db_version) = semver::Version::parse(&extension.latest_version) + && db_version >= latest_version.version + { + continue; } let mut extension = extension.into_active_model(); diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 9abab25ede..393f2c80f8 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -1321,10 +1321,10 @@ impl Database { .await?; let mut connection_ids = HashSet::default(); - if let Some(host_connection) = project.host_connection().log_err() { - if !exclude_dev_server { - connection_ids.insert(host_connection); - } + if let Some(host_connection) = project.host_connection().log_err() + && !exclude_dev_server + { + connection_ids.insert(host_connection); } while let Some(collaborator) = collaborators.next().await { diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index ef749ac9b7..01f553edf2 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -616,10 +616,10 @@ impl Server { } } - if let Some(live_kit) = livekit_client.as_ref() { - if delete_livekit_room { - live_kit.delete_room(livekit_room).await.trace_err(); - } + if let Some(live_kit) = livekit_client.as_ref() + && delete_livekit_room + { + live_kit.delete_room(livekit_room).await.trace_err(); } } } @@ -1015,47 +1015,47 @@ impl Server { inviter_id: UserId, invitee_id: UserId, ) -> Result<()> { - if let Some(user) = self.app_state.db.get_user_by_id(inviter_id).await? { - if let Some(code) = &user.invite_code { - let pool = self.connection_pool.lock(); - let invitee_contact = contact_for_user(invitee_id, false, &pool); - for connection_id in pool.user_connection_ids(inviter_id) { - self.peer.send( - connection_id, - proto::UpdateContacts { - contacts: vec![invitee_contact.clone()], - ..Default::default() - }, - )?; - self.peer.send( - connection_id, - proto::UpdateInviteInfo { - url: format!("{}{}", self.app_state.config.invite_link_prefix, &code), - count: user.invite_count as u32, - }, - )?; - } + if let Some(user) = self.app_state.db.get_user_by_id(inviter_id).await? + && let Some(code) = &user.invite_code + { + let pool = self.connection_pool.lock(); + let invitee_contact = contact_for_user(invitee_id, false, &pool); + for connection_id in pool.user_connection_ids(inviter_id) { + self.peer.send( + connection_id, + proto::UpdateContacts { + contacts: vec![invitee_contact.clone()], + ..Default::default() + }, + )?; + self.peer.send( + connection_id, + proto::UpdateInviteInfo { + url: format!("{}{}", self.app_state.config.invite_link_prefix, &code), + count: user.invite_count as u32, + }, + )?; } } Ok(()) } pub async fn invite_count_updated(self: &Arc<Self>, user_id: UserId) -> Result<()> { - if let Some(user) = self.app_state.db.get_user_by_id(user_id).await? { - if let Some(invite_code) = &user.invite_code { - let pool = self.connection_pool.lock(); - for connection_id in pool.user_connection_ids(user_id) { - self.peer.send( - connection_id, - proto::UpdateInviteInfo { - url: format!( - "{}{}", - self.app_state.config.invite_link_prefix, invite_code - ), - count: user.invite_count as u32, - }, - )?; - } + if let Some(user) = self.app_state.db.get_user_by_id(user_id).await? + && let Some(invite_code) = &user.invite_code + { + let pool = self.connection_pool.lock(); + for connection_id in pool.user_connection_ids(user_id) { + self.peer.send( + connection_id, + proto::UpdateInviteInfo { + url: format!( + "{}{}", + self.app_state.config.invite_link_prefix, invite_code + ), + count: user.invite_count as u32, + }, + )?; } } Ok(()) @@ -1101,10 +1101,10 @@ fn broadcast<F>( F: FnMut(ConnectionId) -> anyhow::Result<()>, { for receiver_id in receiver_ids { - if Some(receiver_id) != sender_id { - if let Err(error) = f(receiver_id) { - tracing::error!("failed to send to {:?} {}", receiver_id, error); - } + if Some(receiver_id) != sender_id + && let Err(error) = f(receiver_id) + { + tracing::error!("failed to send to {:?} {}", receiver_id, error); } } } @@ -2294,11 +2294,10 @@ async fn update_language_server( let db = session.db().await; if let Some(proto::update_language_server::Variant::MetadataUpdated(update)) = &request.variant + && let Some(capabilities) = update.capabilities.clone() { - if let Some(capabilities) = update.capabilities.clone() { - db.update_server_capabilities(project_id, request.language_server_id, capabilities) - .await?; - } + db.update_server_capabilities(project_id, request.language_server_id, capabilities) + .await?; } let project_connection_ids = db diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index 4d94d041b9..ca8a42d54d 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -1162,8 +1162,8 @@ impl RandomizedTest for ProjectCollaborationTest { Some((project, cx)) }); - if !guest_project.is_disconnected(cx) { - if let Some((host_project, host_cx)) = host_project { + if !guest_project.is_disconnected(cx) + && let Some((host_project, host_cx)) = host_project { let host_worktree_snapshots = host_project.read_with(host_cx, |host_project, cx| { host_project @@ -1235,7 +1235,6 @@ impl RandomizedTest for ProjectCollaborationTest { ); } } - } for buffer in guest_project.opened_buffers(cx) { let buffer = buffer.read(cx); diff --git a/crates/collab/src/tests/randomized_test_helpers.rs b/crates/collab/src/tests/randomized_test_helpers.rs index cabf10cfbc..d6c299a6a9 100644 --- a/crates/collab/src/tests/randomized_test_helpers.rs +++ b/crates/collab/src/tests/randomized_test_helpers.rs @@ -198,11 +198,11 @@ pub async fn run_randomized_test<T: RandomizedTest>( } pub fn save_randomized_test_plan() { - if let Some(serialize_plan) = LAST_PLAN.lock().take() { - if let Some(path) = plan_save_path() { - eprintln!("saved test plan to path {:?}", path); - std::fs::write(path, serialize_plan()).unwrap(); - } + if let Some(serialize_plan) = LAST_PLAN.lock().take() + && let Some(path) = plan_save_path() + { + eprintln!("saved test plan to path {:?}", path); + std::fs::write(path, serialize_plan()).unwrap(); } } @@ -290,10 +290,9 @@ impl<T: RandomizedTest> TestPlan<T> { if let StoredOperation::Client { user_id, batch_id, .. } = operation + && batch_id == current_batch_id { - if batch_id == current_batch_id { - return Some(user_id); - } + return Some(user_id); } None })); @@ -366,10 +365,9 @@ impl<T: RandomizedTest> TestPlan<T> { }, applied, ) = stored_operation + && user_id == ¤t_user_id { - if user_id == ¤t_user_id { - return Some((operation.clone(), applied.clone())); - } + return Some((operation.clone(), applied.clone())); } } None @@ -550,11 +548,11 @@ impl<T: RandomizedTest> TestPlan<T> { .unwrap(); let pool = server.connection_pool.lock(); for contact in contacts { - if let db::Contact::Accepted { user_id, busy, .. } = contact { - if user_id == removed_user_id { - assert!(!pool.is_user_online(user_id)); - assert!(!busy); - } + if let db::Contact::Accepted { user_id, busy, .. } = contact + && user_id == removed_user_id + { + assert!(!pool.is_user_online(user_id)); + assert!(!busy); } } } diff --git a/crates/collab/src/user_backfiller.rs b/crates/collab/src/user_backfiller.rs index 71b99a3d4c..569a298c9c 100644 --- a/crates/collab/src/user_backfiller.rs +++ b/crates/collab/src/user_backfiller.rs @@ -130,17 +130,17 @@ impl UserBackfiller { .and_then(|value| value.parse::<i64>().ok()) .and_then(|value| DateTime::from_timestamp(value, 0)); - if rate_limit_remaining == Some(0) { - if let Some(reset_at) = rate_limit_reset { - let now = Utc::now(); - if reset_at > now { - let sleep_duration = reset_at - now; - log::info!( - "rate limit reached. Sleeping for {} seconds", - sleep_duration.num_seconds() - ); - self.executor.sleep(sleep_duration.to_std().unwrap()).await; - } + if rate_limit_remaining == Some(0) + && let Some(reset_at) = rate_limit_reset + { + let now = Utc::now(); + if reset_at > now { + let sleep_duration = reset_at - now; + log::info!( + "rate limit reached. Sleeping for {} seconds", + sleep_duration.num_seconds() + ); + self.executor.sleep(sleep_duration.to_std().unwrap()).await; } } diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index b86d72d92f..9993c0841c 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -107,43 +107,32 @@ impl ChannelView { .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id); // If this channel buffer is already open in this pane, just return it. - if let Some(existing_view) = existing_view.clone() { - if existing_view.read(cx).channel_buffer == channel_view.read(cx).channel_buffer - { - if let Some(link_position) = link_position { - existing_view.update(cx, |channel_view, cx| { - channel_view.focus_position_from_link( - link_position, - true, - window, - cx, - ) - }); - } - return existing_view; + if let Some(existing_view) = existing_view.clone() + && existing_view.read(cx).channel_buffer == channel_view.read(cx).channel_buffer + { + if let Some(link_position) = link_position { + existing_view.update(cx, |channel_view, cx| { + channel_view.focus_position_from_link(link_position, true, window, cx) + }); } + return existing_view; } // If the pane contained a disconnected view for this channel buffer, // replace that. - if let Some(existing_item) = existing_view { - if let Some(ix) = pane.index_for_item(&existing_item) { - pane.close_item_by_id( - existing_item.entity_id(), - SaveIntent::Skip, - window, - cx, - ) + if let Some(existing_item) = existing_view + && let Some(ix) = pane.index_for_item(&existing_item) + { + pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, window, cx) .detach(); - pane.add_item( - Box::new(channel_view.clone()), - true, - true, - Some(ix), - window, - cx, - ); - } + pane.add_item( + Box::new(channel_view.clone()), + true, + true, + Some(ix), + window, + cx, + ); } if let Some(link_position) = link_position { @@ -259,26 +248,21 @@ impl ChannelView { .editor .update(cx, |editor, cx| editor.snapshot(window, cx)); - if let Some(outline) = snapshot.buffer_snapshot.outline(None) { - if let Some(item) = outline + if let Some(outline) = snapshot.buffer_snapshot.outline(None) + && let Some(item) = outline .items .iter() .find(|item| &Channel::slug(&item.text).to_lowercase() == &position) - { - self.editor.update(cx, |editor, cx| { - editor.change_selections( - SelectionEffects::scroll(Autoscroll::focused()), - window, - cx, - |s| { - s.replace_cursors_with(|map| { - vec![item.range.start.to_display_point(map)] - }) - }, - ) - }); - return; - } + { + self.editor.update(cx, |editor, cx| { + editor.change_selections( + SelectionEffects::scroll(Autoscroll::focused()), + window, + cx, + |s| s.replace_cursors_with(|map| vec![item.range.start.to_display_point(map)]), + ) + }); + return; } if !first_attempt { diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 2bbaa8446c..77ce74d581 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -287,19 +287,20 @@ impl ChatPanel { } fn acknowledge_last_message(&mut self, cx: &mut Context<Self>) { - if self.active && self.is_scrolled_to_bottom { - if let Some((chat, _)) = &self.active_chat { - if let Some(channel_id) = self.channel_id(cx) { - self.last_acknowledged_message_id = self - .channel_store - .read(cx) - .last_acknowledge_message_id(channel_id); - } - - chat.update(cx, |chat, cx| { - chat.acknowledge_last_message(cx); - }); + if self.active + && self.is_scrolled_to_bottom + && let Some((chat, _)) = &self.active_chat + { + if let Some(channel_id) = self.channel_id(cx) { + self.last_acknowledged_message_id = self + .channel_store + .read(cx) + .last_acknowledge_message_id(channel_id); } + + chat.update(cx, |chat, cx| { + chat.acknowledge_last_message(cx); + }); } } @@ -405,14 +406,13 @@ impl ChatPanel { && last_message.id != this_message.id && duration_since_last_message < Duration::from_secs(5 * 60); - if let ChannelMessageId::Saved(id) = this_message.id { - if this_message + if let ChannelMessageId::Saved(id) = this_message.id + && this_message .mentions .iter() .any(|(_, user_id)| Some(*user_id) == self.client.user_id()) - { - active_chat.acknowledge_message(id); - } + { + active_chat.acknowledge_message(id); } (this_message, is_continuation_from_previous, is_admin) @@ -871,34 +871,33 @@ impl ChatPanel { scroll_to_message_id.or(this.last_acknowledged_message_id) })?; - if let Some(message_id) = scroll_to_message_id { - if let Some(item_ix) = + if let Some(message_id) = scroll_to_message_id + && let Some(item_ix) = ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone()) .await - { - this.update(cx, |this, cx| { - if let Some(highlight_message_id) = highlight_message_id { - let task = cx.spawn(async move |this, cx| { - cx.background_executor().timer(Duration::from_secs(2)).await; - this.update(cx, |this, cx| { - this.highlighted_message.take(); - cx.notify(); - }) - .ok(); - }); + { + this.update(cx, |this, cx| { + if let Some(highlight_message_id) = highlight_message_id { + let task = cx.spawn(async move |this, cx| { + cx.background_executor().timer(Duration::from_secs(2)).await; + this.update(cx, |this, cx| { + this.highlighted_message.take(); + cx.notify(); + }) + .ok(); + }); - this.highlighted_message = Some((highlight_message_id, task)); - } + this.highlighted_message = Some((highlight_message_id, task)); + } - if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) { - this.message_list.scroll_to(ListOffset { - item_ix, - offset_in_item: px(0.0), - }); - cx.notify(); - } - })?; - } + if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) { + this.message_list.scroll_to(ListOffset { + item_ix, + offset_in_item: px(0.0), + }); + cx.notify(); + } + })?; } Ok(()) diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index 28d60d9221..57f6341297 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -241,38 +241,36 @@ impl MessageEditor { ) -> Task<Result<Vec<CompletionResponse>>> { if let Some((start_anchor, query, candidates)) = self.collect_mention_candidates(buffer, end_anchor, cx) + && !candidates.is_empty() { - if !candidates.is_empty() { - return cx.spawn(async move |_, cx| { - let completion_response = Self::completions_for_candidates( - cx, - query.as_str(), - &candidates, - start_anchor..end_anchor, - Self::completion_for_mention, - ) - .await; - Ok(vec![completion_response]) - }); - } + return cx.spawn(async move |_, cx| { + let completion_response = Self::completions_for_candidates( + cx, + query.as_str(), + &candidates, + start_anchor..end_anchor, + Self::completion_for_mention, + ) + .await; + Ok(vec![completion_response]) + }); } if let Some((start_anchor, query, candidates)) = self.collect_emoji_candidates(buffer, end_anchor, cx) + && !candidates.is_empty() { - if !candidates.is_empty() { - return cx.spawn(async move |_, cx| { - let completion_response = Self::completions_for_candidates( - cx, - query.as_str(), - candidates, - start_anchor..end_anchor, - Self::completion_for_emoji, - ) - .await; - Ok(vec![completion_response]) - }); - } + return cx.spawn(async move |_, cx| { + let completion_response = Self::completions_for_candidates( + cx, + query.as_str(), + candidates, + start_anchor..end_anchor, + Self::completion_for_emoji, + ) + .await; + Ok(vec![completion_response]) + }); } Task::ready(Ok(vec![CompletionResponse { @@ -474,18 +472,17 @@ impl MessageEditor { for range in ranges { text.clear(); text.extend(buffer.text_for_range(range.clone())); - if let Some(username) = text.strip_prefix('@') { - if let Some(user) = this + if let Some(username) = text.strip_prefix('@') + && let Some(user) = this .user_store .read(cx) .cached_user_by_github_login(username) - { - let start = multi_buffer.anchor_after(range.start); - let end = multi_buffer.anchor_after(range.end); + { + let start = multi_buffer.anchor_after(range.start); + let end = multi_buffer.anchor_after(range.end); - mentioned_user_ids.push(user.id); - anchor_ranges.push(start..end); - } + mentioned_user_ids.push(user.id); + anchor_ranges.push(start..end); } } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 8016481f6f..526aacf066 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -311,10 +311,10 @@ impl CollabPanel { window, |this: &mut Self, _, event, window, cx| { if let editor::EditorEvent::Blurred = event { - if let Some(state) = &this.channel_editing_state { - if state.pending_name().is_some() { - return; - } + if let Some(state) = &this.channel_editing_state + && state.pending_name().is_some() + { + return; } this.take_editing_state(window, cx); this.update_entries(false, cx); @@ -491,11 +491,11 @@ impl CollabPanel { if !self.collapsed_sections.contains(&Section::ActiveCall) { let room = room.read(cx); - if query.is_empty() { - if let Some(channel_id) = room.channel_id() { - self.entries.push(ListEntry::ChannelNotes { channel_id }); - self.entries.push(ListEntry::ChannelChat { channel_id }); - } + if query.is_empty() + && let Some(channel_id) = room.channel_id() + { + self.entries.push(ListEntry::ChannelNotes { channel_id }); + self.entries.push(ListEntry::ChannelChat { channel_id }); } // Populate the active user. @@ -639,10 +639,10 @@ impl CollabPanel { &Default::default(), executor.clone(), )); - if let Some(state) = &self.channel_editing_state { - if matches!(state, ChannelEditingState::Create { location: None, .. }) { - self.entries.push(ListEntry::ChannelEditor { depth: 0 }); - } + if let Some(state) = &self.channel_editing_state + && matches!(state, ChannelEditingState::Create { location: None, .. }) + { + self.entries.push(ListEntry::ChannelEditor { depth: 0 }); } let mut collapse_depth = None; for mat in matches { @@ -1552,98 +1552,93 @@ impl CollabPanel { return; } - if let Some(selection) = self.selection { - if let Some(entry) = self.entries.get(selection) { - match entry { - ListEntry::Header(section) => match section { - Section::ActiveCall => Self::leave_call(window, cx), - Section::Channels => self.new_root_channel(window, cx), - Section::Contacts => self.toggle_contact_finder(window, cx), - Section::ContactRequests - | Section::Online - | Section::Offline - | Section::ChannelInvites => { - self.toggle_section_expanded(*section, cx); - } - }, - ListEntry::Contact { contact, calling } => { - if contact.online && !contact.busy && !calling { - self.call(contact.user.id, window, cx); - } + if let Some(selection) = self.selection + && let Some(entry) = self.entries.get(selection) + { + match entry { + ListEntry::Header(section) => match section { + Section::ActiveCall => Self::leave_call(window, cx), + Section::Channels => self.new_root_channel(window, cx), + Section::Contacts => self.toggle_contact_finder(window, cx), + Section::ContactRequests + | Section::Online + | Section::Offline + | Section::ChannelInvites => { + self.toggle_section_expanded(*section, cx); } - ListEntry::ParticipantProject { - project_id, - host_user_id, - .. - } => { - if let Some(workspace) = self.workspace.upgrade() { - let app_state = workspace.read(cx).app_state().clone(); - workspace::join_in_room_project( - *project_id, - *host_user_id, - app_state, - cx, - ) + }, + ListEntry::Contact { contact, calling } => { + if contact.online && !contact.busy && !calling { + self.call(contact.user.id, window, cx); + } + } + ListEntry::ParticipantProject { + project_id, + host_user_id, + .. + } => { + if let Some(workspace) = self.workspace.upgrade() { + let app_state = workspace.read(cx).app_state().clone(); + workspace::join_in_room_project(*project_id, *host_user_id, app_state, cx) .detach_and_prompt_err( "Failed to join project", window, cx, |_, _, _| None, ); - } } - ListEntry::ParticipantScreen { peer_id, .. } => { - let Some(peer_id) = peer_id else { - return; - }; - if let Some(workspace) = self.workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - workspace.open_shared_screen(*peer_id, window, cx) - }); - } - } - ListEntry::Channel { channel, .. } => { - let is_active = maybe!({ - let call_channel = ActiveCall::global(cx) - .read(cx) - .room()? - .read(cx) - .channel_id()?; - - Some(call_channel == channel.id) - }) - .unwrap_or(false); - if is_active { - self.open_channel_notes(channel.id, window, cx) - } else { - self.join_channel(channel.id, window, cx) - } - } - ListEntry::ContactPlaceholder => self.toggle_contact_finder(window, cx), - ListEntry::CallParticipant { user, peer_id, .. } => { - if Some(user) == self.user_store.read(cx).current_user().as_ref() { - Self::leave_call(window, cx); - } else if let Some(peer_id) = peer_id { - self.workspace - .update(cx, |workspace, cx| workspace.follow(*peer_id, window, cx)) - .ok(); - } - } - ListEntry::IncomingRequest(user) => { - self.respond_to_contact_request(user.id, true, window, cx) - } - ListEntry::ChannelInvite(channel) => { - self.respond_to_channel_invite(channel.id, true, cx) - } - ListEntry::ChannelNotes { channel_id } => { - self.open_channel_notes(*channel_id, window, cx) - } - ListEntry::ChannelChat { channel_id } => { - self.join_channel_chat(*channel_id, window, cx) - } - ListEntry::OutgoingRequest(_) => {} - ListEntry::ChannelEditor { .. } => {} } + ListEntry::ParticipantScreen { peer_id, .. } => { + let Some(peer_id) = peer_id else { + return; + }; + if let Some(workspace) = self.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + workspace.open_shared_screen(*peer_id, window, cx) + }); + } + } + ListEntry::Channel { channel, .. } => { + let is_active = maybe!({ + let call_channel = ActiveCall::global(cx) + .read(cx) + .room()? + .read(cx) + .channel_id()?; + + Some(call_channel == channel.id) + }) + .unwrap_or(false); + if is_active { + self.open_channel_notes(channel.id, window, cx) + } else { + self.join_channel(channel.id, window, cx) + } + } + ListEntry::ContactPlaceholder => self.toggle_contact_finder(window, cx), + ListEntry::CallParticipant { user, peer_id, .. } => { + if Some(user) == self.user_store.read(cx).current_user().as_ref() { + Self::leave_call(window, cx); + } else if let Some(peer_id) = peer_id { + self.workspace + .update(cx, |workspace, cx| workspace.follow(*peer_id, window, cx)) + .ok(); + } + } + ListEntry::IncomingRequest(user) => { + self.respond_to_contact_request(user.id, true, window, cx) + } + ListEntry::ChannelInvite(channel) => { + self.respond_to_channel_invite(channel.id, true, cx) + } + ListEntry::ChannelNotes { channel_id } => { + self.open_channel_notes(*channel_id, window, cx) + } + ListEntry::ChannelChat { channel_id } => { + self.join_channel_chat(*channel_id, window, cx) + } + ListEntry::OutgoingRequest(_) => {} + ListEntry::ChannelEditor { .. } => {} } } } diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 01ca533c10..00c3bbf623 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -121,13 +121,12 @@ impl NotificationPanel { let notification_list = ListState::new(0, ListAlignment::Top, px(1000.)); notification_list.set_scroll_handler(cx.listener( |this, event: &ListScrollEvent, _, cx| { - if event.count.saturating_sub(event.visible_range.end) < LOADING_THRESHOLD { - if let Some(task) = this + if event.count.saturating_sub(event.visible_range.end) < LOADING_THRESHOLD + && let Some(task) = this .notification_store .update(cx, |store, cx| store.load_more_notifications(false, cx)) - { - task.detach(); - } + { + task.detach(); } }, )); @@ -469,20 +468,19 @@ impl NotificationPanel { channel_id, .. } = notification.clone() + && let Some(workspace) = self.workspace.upgrade() { - if let Some(workspace) = self.workspace.upgrade() { - window.defer(cx, move |window, cx| { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = workspace.focus_panel::<ChatPanel>(window, cx) { - panel.update(cx, |panel, cx| { - panel - .select_channel(ChannelId(channel_id), Some(message_id), cx) - .detach_and_log_err(cx); - }); - } - }); + window.defer(cx, move |window, cx| { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = workspace.focus_panel::<ChatPanel>(window, cx) { + panel.update(cx, |panel, cx| { + panel + .select_channel(ChannelId(channel_id), Some(message_id), cx) + .detach_and_log_err(cx); + }); + } }); - } + }); } } @@ -491,18 +489,18 @@ impl NotificationPanel { return false; } - if let Notification::ChannelMessageMention { channel_id, .. } = ¬ification { - if let Some(workspace) = self.workspace.upgrade() { - return if let Some(panel) = workspace.read(cx).panel::<ChatPanel>(cx) { - let panel = panel.read(cx); - panel.is_scrolled_to_bottom() - && panel - .active_chat() - .map_or(false, |chat| chat.read(cx).channel_id.0 == *channel_id) - } else { - false - }; - } + if let Notification::ChannelMessageMention { channel_id, .. } = ¬ification + && let Some(workspace) = self.workspace.upgrade() + { + return if let Some(panel) = workspace.read(cx).panel::<ChatPanel>(cx) { + let panel = panel.read(cx); + panel.is_scrolled_to_bottom() + && panel + .active_chat() + .map_or(false, |chat| chat.read(cx).channel_id.0 == *channel_id) + } else { + false + }; } false @@ -582,16 +580,16 @@ impl NotificationPanel { } fn remove_toast(&mut self, notification_id: u64, cx: &mut Context<Self>) { - if let Some((current_id, _)) = &self.current_notification_toast { - if *current_id == notification_id { - self.current_notification_toast.take(); - self.workspace - .update(cx, |workspace, cx| { - let id = NotificationId::unique::<NotificationToast>(); - workspace.dismiss_notification(&id, cx) - }) - .ok(); - } + if let Some((current_id, _)) = &self.current_notification_toast + && *current_id == notification_id + { + self.current_notification_toast.take(); + self.workspace + .update(cx, |workspace, cx| { + let id = NotificationId::unique::<NotificationToast>(); + workspace.dismiss_notification(&id, cx) + }) + .ok(); } } diff --git a/crates/context_server/src/client.rs b/crates/context_server/src/client.rs index 65283afa87..609d2c43e3 100644 --- a/crates/context_server/src/client.rs +++ b/crates/context_server/src/client.rs @@ -271,10 +271,10 @@ impl Client { ); } } else if let Ok(response) = serde_json::from_str::<AnyResponse>(&message) { - if let Some(handlers) = response_handlers.lock().as_mut() { - if let Some(handler) = handlers.remove(&response.id) { - handler(Ok(message.to_string())); - } + if let Some(handlers) = response_handlers.lock().as_mut() + && let Some(handler) = handlers.remove(&response.id) + { + handler(Ok(message.to_string())); } } else if let Ok(notification) = serde_json::from_str::<AnyNotification>(&message) { let mut notification_handlers = notification_handlers.lock(); diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index dcebeae721..1916853a69 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -608,15 +608,13 @@ impl Copilot { sign_in_status: status, .. }) = &mut this.server - { - if let SignInStatus::SigningIn { + && let SignInStatus::SigningIn { prompt: prompt_flow, .. } = status - { - *prompt_flow = Some(flow.clone()); - cx.notify(); - } + { + *prompt_flow = Some(flow.clone()); + cx.notify(); } })?; let response = lsp @@ -782,59 +780,58 @@ impl Copilot { event: &language::BufferEvent, cx: &mut Context<Self>, ) -> Result<()> { - if let Ok(server) = self.server.as_running() { - if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.entity_id()) - { - match event { - language::BufferEvent::Edited => { - drop(registered_buffer.report_changes(&buffer, cx)); - } - language::BufferEvent::Saved => { + if let Ok(server) = self.server.as_running() + && let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.entity_id()) + { + match event { + language::BufferEvent::Edited => { + drop(registered_buffer.report_changes(&buffer, cx)); + } + language::BufferEvent::Saved => { + server + .lsp + .notify::<lsp::notification::DidSaveTextDocument>( + &lsp::DidSaveTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new( + registered_buffer.uri.clone(), + ), + text: None, + }, + )?; + } + language::BufferEvent::FileHandleChanged + | language::BufferEvent::LanguageChanged => { + let new_language_id = id_for_language(buffer.read(cx).language()); + let Ok(new_uri) = uri_for_buffer(&buffer, cx) else { + return Ok(()); + }; + if new_uri != registered_buffer.uri + || new_language_id != registered_buffer.language_id + { + let old_uri = mem::replace(&mut registered_buffer.uri, new_uri); + registered_buffer.language_id = new_language_id; server .lsp - .notify::<lsp::notification::DidSaveTextDocument>( - &lsp::DidSaveTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new( + .notify::<lsp::notification::DidCloseTextDocument>( + &lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(old_uri), + }, + )?; + server + .lsp + .notify::<lsp::notification::DidOpenTextDocument>( + &lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( registered_buffer.uri.clone(), + registered_buffer.language_id.clone(), + registered_buffer.snapshot_version, + registered_buffer.snapshot.text(), ), - text: None, }, )?; } - language::BufferEvent::FileHandleChanged - | language::BufferEvent::LanguageChanged => { - let new_language_id = id_for_language(buffer.read(cx).language()); - let Ok(new_uri) = uri_for_buffer(&buffer, cx) else { - return Ok(()); - }; - if new_uri != registered_buffer.uri - || new_language_id != registered_buffer.language_id - { - let old_uri = mem::replace(&mut registered_buffer.uri, new_uri); - registered_buffer.language_id = new_language_id; - server - .lsp - .notify::<lsp::notification::DidCloseTextDocument>( - &lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new(old_uri), - }, - )?; - server - .lsp - .notify::<lsp::notification::DidOpenTextDocument>( - &lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - registered_buffer.uri.clone(), - registered_buffer.language_id.clone(), - registered_buffer.snapshot_version, - registered_buffer.snapshot.text(), - ), - }, - )?; - } - } - _ => {} } + _ => {} } } @@ -842,17 +839,17 @@ impl Copilot { } fn unregister_buffer(&mut self, buffer: &WeakEntity<Buffer>) { - if let Ok(server) = self.server.as_running() { - if let Some(buffer) = server.registered_buffers.remove(&buffer.entity_id()) { - server - .lsp - .notify::<lsp::notification::DidCloseTextDocument>( - &lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new(buffer.uri), - }, - ) - .ok(); - } + if let Ok(server) = self.server.as_running() + && let Some(buffer) = server.registered_buffers.remove(&buffer.entity_id()) + { + server + .lsp + .notify::<lsp::notification::DidCloseTextDocument>( + &lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(buffer.uri), + }, + ) + .ok(); } } diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 70b0638120..a8826d563b 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -99,10 +99,10 @@ impl JsDebugAdapter { } } - if let Some(env) = configuration.get("env").cloned() { - if let Ok(env) = serde_json::from_value(env) { - envs = env; - } + if let Some(env) = configuration.get("env").cloned() + && let Ok(env) = serde_json::from_value(env) + { + envs = env; } configuration diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 14154e5b39..e60c08cd0f 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -661,11 +661,11 @@ impl ToolbarItemView for DapLogToolbarItemView { _window: &mut Window, cx: &mut Context<Self>, ) -> workspace::ToolbarItemLocation { - if let Some(item) = active_pane_item { - if let Some(log_view) = item.downcast::<DapLogView>() { - self.log_view = Some(log_view.clone()); - return workspace::ToolbarItemLocation::PrimaryLeft; - } + if let Some(item) = active_pane_item + && let Some(log_view) = item.downcast::<DapLogView>() + { + self.log_view = Some(log_view.clone()); + return workspace::ToolbarItemLocation::PrimaryLeft; } self.log_view = None; diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index cf038871bc..4e1b0d19e2 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -530,10 +530,9 @@ impl DebugPanel { .active_session .as_ref() .map(|session| session.entity_id()) + && active_session_id == entity_id { - if active_session_id == entity_id { - this.active_session = this.sessions_with_children.keys().next().cloned(); - } + this.active_session = this.sessions_with_children.keys().next().cloned(); } cx.notify() }) @@ -1302,10 +1301,10 @@ impl DebugPanel { cx: &mut Context<'_, Self>, ) -> Option<SharedString> { let adapter = parent_session.read(cx).adapter(); - if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) { - if let Some(label) = adapter.label_for_child_session(request) { - return Some(label.into()); - } + if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) + && let Some(label) = adapter.label_for_child_session(request) + { + return Some(label.into()); } None } diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index 51ea25a5cb..eb0ad92dcc 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -343,10 +343,10 @@ impl NewProcessModal { return; } - if let NewProcessMode::Launch = &self.mode { - if self.configure_mode.read(cx).save_to_debug_json.selected() { - self.save_debug_scenario(window, cx); - } + if let NewProcessMode::Launch = &self.mode + && self.configure_mode.read(cx).save_to_debug_json.selected() + { + self.save_debug_scenario(window, cx); } let Some(debugger) = self.debugger.clone() else { diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index 9768f02e8e..095b069fa3 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -239,11 +239,9 @@ impl BreakpointList { } fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) { - if self.strip_mode.is_some() { - if self.input.focus_handle(cx).contains_focused(window, cx) { - cx.propagate(); - return; - } + if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) { + cx.propagate(); + return; } let ix = match self.selected_ix { _ if self.breakpoints.len() == 0 => None, @@ -265,11 +263,9 @@ impl BreakpointList { window: &mut Window, cx: &mut Context<Self>, ) { - if self.strip_mode.is_some() { - if self.input.focus_handle(cx).contains_focused(window, cx) { - cx.propagate(); - return; - } + if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) { + cx.propagate(); + return; } let ix = match self.selected_ix { _ if self.breakpoints.len() == 0 => None, @@ -286,11 +282,9 @@ impl BreakpointList { } fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) { - if self.strip_mode.is_some() { - if self.input.focus_handle(cx).contains_focused(window, cx) { - cx.propagate(); - return; - } + if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) { + cx.propagate(); + return; } let ix = if self.breakpoints.len() > 0 { Some(0) @@ -301,11 +295,9 @@ impl BreakpointList { } fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) { - if self.strip_mode.is_some() { - if self.input.focus_handle(cx).contains_focused(window, cx) { - cx.propagate(); - return; - } + if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) { + cx.propagate(); + return; } let ix = if self.breakpoints.len() > 0 { Some(self.breakpoints.len() - 1) @@ -401,11 +393,9 @@ impl BreakpointList { let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else { return; }; - if self.strip_mode.is_some() { - if self.input.focus_handle(cx).contains_focused(window, cx) { - cx.propagate(); - return; - } + if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) { + cx.propagate(); + return; } match &mut entry.kind { diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index 42989ddc20..05d2231da4 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -611,17 +611,16 @@ impl ConsoleQueryBarCompletionProvider { for variable in console.variable_list.update(cx, |variable_list, cx| { variable_list.completion_variables(cx) }) { - if let Some(evaluate_name) = &variable.evaluate_name { - if variables + if let Some(evaluate_name) = &variable.evaluate_name + && variables .insert(evaluate_name.clone(), variable.value.clone()) .is_none() - { - string_matches.push(StringMatchCandidate { - id: 0, - string: evaluate_name.clone(), - char_bag: evaluate_name.chars().collect(), - }); - } + { + string_matches.push(StringMatchCandidate { + id: 0, + string: evaluate_name.clone(), + char_bag: evaluate_name.chars().collect(), + }); } if variables diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 23dbf33322..c15c0f2493 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -639,17 +639,15 @@ impl ProjectDiagnosticsEditor { #[cfg(test)] let cloned_blocks = blocks.clone(); - if was_empty { - if let Some(anchor_range) = anchor_ranges.first() { - let range_to_select = anchor_range.start..anchor_range.start; - this.editor.update(cx, |editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.select_anchor_ranges([range_to_select]); - }) - }); - if this.focus_handle.is_focused(window) { - this.editor.read(cx).focus_handle(cx).focus(window); - } + if was_empty && let Some(anchor_range) = anchor_ranges.first() { + let range_to_select = anchor_range.start..anchor_range.start; + this.editor.update(cx, |editor, cx| { + editor.change_selections(Default::default(), window, cx, |s| { + s.select_anchor_ranges([range_to_select]); + }) + }); + if this.focus_handle.is_focused(window) { + this.editor.read(cx).focus_handle(cx).focus(window); } } @@ -980,18 +978,16 @@ async fn heuristic_syntactic_expand( // Remove blank lines from start and end if let Some(start_row) = (outline_range.start.row..outline_range.end.row) .find(|row| !snapshot.line_indent_for_row(*row).is_line_blank()) - { - if let Some(end_row) = (outline_range.start.row..outline_range.end.row + 1) + && let Some(end_row) = (outline_range.start.row..outline_range.end.row + 1) .rev() .find(|row| !snapshot.line_indent_for_row(*row).is_line_blank()) - { - let row_count = end_row.saturating_sub(start_row); - if row_count <= max_row_count { - return Some(RangeInclusive::new( - outline_range.start.row, - outline_range.end.row, - )); - } + { + let row_count = end_row.saturating_sub(start_row); + if row_count <= max_row_count { + return Some(RangeInclusive::new( + outline_range.start.row, + outline_range.end.row, + )); } } } diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index a16e516a70..cc1cc2c440 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -969,13 +969,13 @@ impl DisplaySnapshot { if let Some(chunk_highlight) = chunk.highlight_style { // For color inlays, blend the color with the editor background let mut processed_highlight = chunk_highlight; - if chunk.is_inlay { - if let Some(inlay_color) = chunk_highlight.color { - // Only blend if the color has transparency (alpha < 1.0) - if inlay_color.a < 1.0 { - let blended_color = editor_style.background.blend(inlay_color); - processed_highlight.color = Some(blended_color); - } + if chunk.is_inlay + && let Some(inlay_color) = chunk_highlight.color + { + // Only blend if the color has transparency (alpha < 1.0) + if inlay_color.a < 1.0 { + let blended_color = editor_style.background.blend(inlay_color); + processed_highlight.color = Some(blended_color); } } @@ -2351,11 +2351,12 @@ pub mod tests { .highlight_style .and_then(|style| style.color) .map_or(black, |color| color.to_rgb()); - if let Some((last_chunk, last_severity, last_color)) = chunks.last_mut() { - if *last_severity == chunk.diagnostic_severity && *last_color == color { - last_chunk.push_str(chunk.text); - continue; - } + if let Some((last_chunk, last_severity, last_color)) = chunks.last_mut() + && *last_severity == chunk.diagnostic_severity + && *last_color == color + { + last_chunk.push_str(chunk.text); + continue; } chunks.push((chunk.text.to_string(), chunk.diagnostic_severity, color)); @@ -2901,11 +2902,12 @@ pub mod tests { .syntax_highlight_id .and_then(|id| id.style(theme)?.color); let highlight_color = chunk.highlight_style.and_then(|style| style.color); - if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() { - if syntax_color == *last_syntax_color && highlight_color == *last_highlight_color { - last_chunk.push_str(chunk.text); - continue; - } + if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() + && syntax_color == *last_syntax_color + && highlight_color == *last_highlight_color + { + last_chunk.push_str(chunk.text); + continue; } chunks.push((chunk.text.to_string(), syntax_color, highlight_color)); } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index c4c9f2004a..5ae37d20fa 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -525,26 +525,25 @@ impl BlockMap { // * Below blocks that end at the start of the edit // However, if we hit a replace block that ends at the start of the edit we want to reconstruct it. new_transforms.append(cursor.slice(&old_start, Bias::Left), &()); - if let Some(transform) = cursor.item() { - if transform.summary.input_rows > 0 - && cursor.end() == old_start - && transform - .block - .as_ref() - .map_or(true, |b| !b.is_replacement()) - { - // Preserve the transform (push and next) - new_transforms.push(transform.clone(), &()); - cursor.next(); + if let Some(transform) = cursor.item() + && transform.summary.input_rows > 0 + && cursor.end() == old_start + && transform + .block + .as_ref() + .map_or(true, |b| !b.is_replacement()) + { + // Preserve the transform (push and next) + new_transforms.push(transform.clone(), &()); + cursor.next(); - // Preserve below blocks at end of edit - while let Some(transform) = cursor.item() { - if transform.block.as_ref().map_or(false, |b| b.place_below()) { - new_transforms.push(transform.clone(), &()); - cursor.next(); - } else { - break; - } + // Preserve below blocks at end of edit + while let Some(transform) = cursor.item() { + if transform.block.as_ref().map_or(false, |b| b.place_below()) { + new_transforms.push(transform.clone(), &()); + cursor.next(); + } else { + break; } } } @@ -657,10 +656,10 @@ impl BlockMap { .iter() .filter_map(|block| { let placement = block.placement.to_wrap_row(wrap_snapshot)?; - if let BlockPlacement::Above(row) = placement { - if row < new_start { - return None; - } + if let BlockPlacement::Above(row) = placement + && row < new_start + { + return None; } Some((placement, Block::Custom(block.clone()))) }), @@ -977,10 +976,10 @@ impl BlockMapReader<'_> { break; } - if let Some(BlockId::Custom(id)) = transform.block.as_ref().map(|block| block.id()) { - if id == block_id { - return Some(cursor.start().1); - } + if let Some(BlockId::Custom(id)) = transform.block.as_ref().map(|block| block.id()) + && id == block_id + { + return Some(cursor.start().1); } cursor.next(); } @@ -1299,14 +1298,14 @@ impl BlockSnapshot { let mut input_start = transform_input_start; let mut input_end = transform_input_start; - if let Some(transform) = cursor.item() { - if transform.block.is_none() { - input_start += rows.start - transform_output_start; - input_end += cmp::min( - rows.end - transform_output_start, - transform.summary.input_rows, - ); - } + if let Some(transform) = cursor.item() + && transform.block.is_none() + { + input_start += rows.start - transform_output_start; + input_end += cmp::min( + rows.end - transform_output_start, + transform.summary.input_rows, + ); } BlockChunks { @@ -1472,18 +1471,18 @@ impl BlockSnapshot { longest_row_chars = summary.longest_row_chars; } - if let Some(transform) = cursor.item() { - if transform.block.is_none() { - let Dimensions(output_start, input_start, _) = cursor.start(); - let overshoot = range.end.0 - output_start.0; - let wrap_start_row = input_start.0; - let wrap_end_row = input_start.0 + overshoot; - let summary = self - .wrap_snapshot - .text_summary_for_range(wrap_start_row..wrap_end_row); - if summary.longest_row_chars > longest_row_chars { - longest_row = BlockRow(output_start.0 + summary.longest_row); - } + if let Some(transform) = cursor.item() + && transform.block.is_none() + { + let Dimensions(output_start, input_start, _) = cursor.start(); + let overshoot = range.end.0 - output_start.0; + let wrap_start_row = input_start.0; + let wrap_end_row = input_start.0 + overshoot; + let summary = self + .wrap_snapshot + .text_summary_for_range(wrap_start_row..wrap_end_row); + if summary.longest_row_chars > longest_row_chars { + longest_row = BlockRow(output_start.0 + summary.longest_row); } } } @@ -1557,12 +1556,11 @@ impl BlockSnapshot { match transform.block.as_ref() { Some(block) => { - if block.is_replacement() { - if ((bias == Bias::Left || search_left) && output_start <= point.0) - || (!search_left && output_start >= point.0) - { - return BlockPoint(output_start); - } + if block.is_replacement() + && (((bias == Bias::Left || search_left) && output_start <= point.0) + || (!search_left && output_start >= point.0)) + { + return BlockPoint(output_start); } } None => { @@ -3228,34 +3226,32 @@ mod tests { let mut is_in_replace_block = false; if let Some((BlockPlacement::Replace(replace_range), block)) = sorted_blocks_iter.peek() + && wrap_row >= replace_range.start().0 { - if wrap_row >= replace_range.start().0 { - is_in_replace_block = true; + is_in_replace_block = true; - if wrap_row == replace_range.start().0 { - if matches!(block, Block::FoldedBuffer { .. }) { - expected_buffer_rows.push(None); - } else { - expected_buffer_rows - .push(input_buffer_rows[multibuffer_row as usize]); - } + if wrap_row == replace_range.start().0 { + if matches!(block, Block::FoldedBuffer { .. }) { + expected_buffer_rows.push(None); + } else { + expected_buffer_rows.push(input_buffer_rows[multibuffer_row as usize]); } + } - if wrap_row == replace_range.end().0 { - expected_block_positions.push((block_row, block.id())); - let text = "\n".repeat((block.height() - 1) as usize); - if block_row > 0 { - expected_text.push('\n'); - } - expected_text.push_str(&text); - - for _ in 1..block.height() { - expected_buffer_rows.push(None); - } - block_row += block.height(); - - sorted_blocks_iter.next(); + if wrap_row == replace_range.end().0 { + expected_block_positions.push((block_row, block.id())); + let text = "\n".repeat((block.height() - 1) as usize); + if block_row > 0 { + expected_text.push('\n'); } + expected_text.push_str(&text); + + for _ in 1..block.height() { + expected_buffer_rows.push(None); + } + block_row += block.height(); + + sorted_blocks_iter.next(); } } diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index c4e53a0f43..3509bcbba8 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -289,25 +289,25 @@ impl FoldMapWriter<'_> { let ChunkRendererId::Fold(id) = id else { continue; }; - if let Some(metadata) = self.0.snapshot.fold_metadata_by_id.get(&id).cloned() { - if Some(new_width) != metadata.width { - let buffer_start = metadata.range.start.to_offset(buffer); - let buffer_end = metadata.range.end.to_offset(buffer); - let inlay_range = inlay_snapshot.to_inlay_offset(buffer_start) - ..inlay_snapshot.to_inlay_offset(buffer_end); - edits.push(InlayEdit { - old: inlay_range.clone(), - new: inlay_range.clone(), - }); + if let Some(metadata) = self.0.snapshot.fold_metadata_by_id.get(&id).cloned() + && Some(new_width) != metadata.width + { + let buffer_start = metadata.range.start.to_offset(buffer); + let buffer_end = metadata.range.end.to_offset(buffer); + let inlay_range = inlay_snapshot.to_inlay_offset(buffer_start) + ..inlay_snapshot.to_inlay_offset(buffer_end); + edits.push(InlayEdit { + old: inlay_range.clone(), + new: inlay_range.clone(), + }); - self.0.snapshot.fold_metadata_by_id.insert( - id, - FoldMetadata { - range: metadata.range, - width: Some(new_width), - }, - ); - } + self.0.snapshot.fold_metadata_by_id.insert( + id, + FoldMetadata { + range: metadata.range, + width: Some(new_width), + }, + ); } } @@ -417,18 +417,18 @@ impl FoldMap { cursor.seek(&InlayOffset(0), Bias::Right); while let Some(mut edit) = inlay_edits_iter.next() { - if let Some(item) = cursor.item() { - if !item.is_fold() { - new_transforms.update_last( - |transform| { - if !transform.is_fold() { - transform.summary.add_summary(&item.summary, &()); - cursor.next(); - } - }, - &(), - ); - } + if let Some(item) = cursor.item() + && !item.is_fold() + { + new_transforms.update_last( + |transform| { + if !transform.is_fold() { + transform.summary.add_summary(&item.summary, &()); + cursor.next(); + } + }, + &(), + ); } new_transforms.append(cursor.slice(&edit.old.start, Bias::Left), &()); edit.new.start -= edit.old.start - *cursor.start(); diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 76148af587..626dbf5cba 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -557,11 +557,11 @@ impl InlayMap { let mut buffer_edits_iter = buffer_edits.iter().peekable(); while let Some(buffer_edit) = buffer_edits_iter.next() { new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left), &()); - if let Some(Transform::Isomorphic(transform)) = cursor.item() { - if cursor.end().0 == buffer_edit.old.start { - push_isomorphic(&mut new_transforms, *transform); - cursor.next(); - } + if let Some(Transform::Isomorphic(transform)) = cursor.item() + && cursor.end().0 == buffer_edit.old.start + { + push_isomorphic(&mut new_transforms, *transform); + cursor.next(); } // Remove all the inlays and transforms contained by the edit. diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 0d2d1c4a4c..7aa252a7f3 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -249,48 +249,48 @@ impl WrapMap { return; } - if let Some(wrap_width) = self.wrap_width { - if self.background_task.is_none() { - let pending_edits = self.pending_edits.clone(); - let mut snapshot = self.snapshot.clone(); - let text_system = cx.text_system().clone(); - let (font, font_size) = self.font_with_size.clone(); - let update_task = cx.background_spawn(async move { - let mut edits = Patch::default(); - let mut line_wrapper = text_system.line_wrapper(font, font_size); - for (tab_snapshot, tab_edits) in pending_edits { - let wrap_edits = snapshot - .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper) - .await; - edits = edits.compose(&wrap_edits); - } - (snapshot, edits) - }); + if let Some(wrap_width) = self.wrap_width + && self.background_task.is_none() + { + let pending_edits = self.pending_edits.clone(); + let mut snapshot = self.snapshot.clone(); + let text_system = cx.text_system().clone(); + let (font, font_size) = self.font_with_size.clone(); + let update_task = cx.background_spawn(async move { + let mut edits = Patch::default(); + let mut line_wrapper = text_system.line_wrapper(font, font_size); + for (tab_snapshot, tab_edits) in pending_edits { + let wrap_edits = snapshot + .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper) + .await; + edits = edits.compose(&wrap_edits); + } + (snapshot, edits) + }); - match cx - .background_executor() - .block_with_timeout(Duration::from_millis(1), update_task) - { - Ok((snapshot, output_edits)) => { - self.snapshot = snapshot; - self.edits_since_sync = self.edits_since_sync.compose(&output_edits); - } - Err(update_task) => { - self.background_task = Some(cx.spawn(async move |this, cx| { - let (snapshot, edits) = update_task.await; - this.update(cx, |this, cx| { - this.snapshot = snapshot; - this.edits_since_sync = this - .edits_since_sync - .compose(mem::take(&mut this.interpolated_edits).invert()) - .compose(&edits); - this.background_task = None; - this.flush_edits(cx); - cx.notify(); - }) - .ok(); - })); - } + match cx + .background_executor() + .block_with_timeout(Duration::from_millis(1), update_task) + { + Ok((snapshot, output_edits)) => { + self.snapshot = snapshot; + self.edits_since_sync = self.edits_since_sync.compose(&output_edits); + } + Err(update_task) => { + self.background_task = Some(cx.spawn(async move |this, cx| { + let (snapshot, edits) = update_task.await; + this.update(cx, |this, cx| { + this.snapshot = snapshot; + this.edits_since_sync = this + .edits_since_sync + .compose(mem::take(&mut this.interpolated_edits).invert()) + .compose(&edits); + this.background_task = None; + this.flush_edits(cx); + cx.notify(); + }) + .ok(); + })); } } } @@ -1065,12 +1065,12 @@ impl sum_tree::Item for Transform { } fn push_isomorphic(transforms: &mut Vec<Transform>, summary: TextSummary) { - if let Some(last_transform) = transforms.last_mut() { - if last_transform.is_isomorphic() { - last_transform.summary.input += &summary; - last_transform.summary.output += &summary; - return; - } + if let Some(last_transform) = transforms.last_mut() + && last_transform.is_isomorphic() + { + last_transform.summary.input += &summary; + last_transform.summary.output += &summary; + return; } transforms.push(Transform::isomorphic(summary)); } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c52a59a909..ca1f1f8828 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -942,10 +942,10 @@ impl ChangeList { } pub fn invert_last_group(&mut self) { - if let Some(last) = self.changes.last_mut() { - if let Some(current) = last.current.as_mut() { - mem::swap(&mut last.original, current); - } + if let Some(last) = self.changes.last_mut() + && let Some(current) = last.current.as_mut() + { + mem::swap(&mut last.original, current); } } } @@ -1861,114 +1861,110 @@ impl Editor { .then(|| language_settings::SoftWrap::None); let mut project_subscriptions = Vec::new(); - if full_mode { - if let Some(project) = project.as_ref() { - project_subscriptions.push(cx.subscribe_in( - project, - window, - |editor, _, event, window, cx| match event { - project::Event::RefreshCodeLens => { - // we always query lens with actions, without storing them, always refreshing them + if full_mode && let Some(project) = project.as_ref() { + project_subscriptions.push(cx.subscribe_in( + project, + window, + |editor, _, event, window, cx| match event { + project::Event::RefreshCodeLens => { + // we always query lens with actions, without storing them, always refreshing them + } + project::Event::RefreshInlayHints => { + editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); + } + project::Event::LanguageServerAdded(..) + | project::Event::LanguageServerRemoved(..) => { + if editor.tasks_update_task.is_none() { + editor.tasks_update_task = Some(editor.refresh_runnables(window, cx)); } - project::Event::RefreshInlayHints => { - editor - .refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); - } - project::Event::LanguageServerAdded(..) - | project::Event::LanguageServerRemoved(..) => { - if editor.tasks_update_task.is_none() { - editor.tasks_update_task = - Some(editor.refresh_runnables(window, cx)); - } - } - project::Event::SnippetEdit(id, snippet_edits) => { - if let Some(buffer) = editor.buffer.read(cx).buffer(*id) { - let focus_handle = editor.focus_handle(cx); - if focus_handle.is_focused(window) { - let snapshot = buffer.read(cx).snapshot(); - for (range, snippet) in snippet_edits { - let editor_range = - language::range_from_lsp(*range).to_offset(&snapshot); - editor - .insert_snippet( - &[editor_range], - snippet.clone(), - window, - cx, - ) - .ok(); - } + } + project::Event::SnippetEdit(id, snippet_edits) => { + if let Some(buffer) = editor.buffer.read(cx).buffer(*id) { + let focus_handle = editor.focus_handle(cx); + if focus_handle.is_focused(window) { + let snapshot = buffer.read(cx).snapshot(); + for (range, snippet) in snippet_edits { + let editor_range = + language::range_from_lsp(*range).to_offset(&snapshot); + editor + .insert_snippet( + &[editor_range], + snippet.clone(), + window, + cx, + ) + .ok(); } } } - project::Event::LanguageServerBufferRegistered { buffer_id, .. } => { - if editor.buffer().read(cx).buffer(*buffer_id).is_some() { - editor.update_lsp_data(false, Some(*buffer_id), window, cx); - } - } - _ => {} - }, - )); - if let Some(task_inventory) = project - .read(cx) - .task_store() - .read(cx) - .task_inventory() - .cloned() - { - project_subscriptions.push(cx.observe_in( - &task_inventory, - window, - |editor, _, window, cx| { - editor.tasks_update_task = Some(editor.refresh_runnables(window, cx)); - }, - )); - }; - - project_subscriptions.push(cx.subscribe_in( - &project.read(cx).breakpoint_store(), - window, - |editor, _, event, window, cx| match event { - BreakpointStoreEvent::ClearDebugLines => { - editor.clear_row_highlights::<ActiveDebugLine>(); - editor.refresh_inline_values(cx); - } - BreakpointStoreEvent::SetDebugLine => { - if editor.go_to_active_debug_line(window, cx) { - cx.stop_propagation(); - } - - editor.refresh_inline_values(cx); - } - _ => {} - }, - )); - let git_store = project.read(cx).git_store().clone(); - let project = project.clone(); - project_subscriptions.push(cx.subscribe(&git_store, move |this, _, event, cx| { - match event { - GitStoreEvent::RepositoryUpdated( - _, - RepositoryEvent::Updated { - new_instance: true, .. - }, - _, - ) => { - this.load_diff_task = Some( - update_uncommitted_diff_for_buffer( - cx.entity(), - &project, - this.buffer.read(cx).all_buffers(), - this.buffer.clone(), - cx, - ) - .shared(), - ); - } - _ => {} } - })); - } + project::Event::LanguageServerBufferRegistered { buffer_id, .. } => { + if editor.buffer().read(cx).buffer(*buffer_id).is_some() { + editor.update_lsp_data(false, Some(*buffer_id), window, cx); + } + } + _ => {} + }, + )); + if let Some(task_inventory) = project + .read(cx) + .task_store() + .read(cx) + .task_inventory() + .cloned() + { + project_subscriptions.push(cx.observe_in( + &task_inventory, + window, + |editor, _, window, cx| { + editor.tasks_update_task = Some(editor.refresh_runnables(window, cx)); + }, + )); + }; + + project_subscriptions.push(cx.subscribe_in( + &project.read(cx).breakpoint_store(), + window, + |editor, _, event, window, cx| match event { + BreakpointStoreEvent::ClearDebugLines => { + editor.clear_row_highlights::<ActiveDebugLine>(); + editor.refresh_inline_values(cx); + } + BreakpointStoreEvent::SetDebugLine => { + if editor.go_to_active_debug_line(window, cx) { + cx.stop_propagation(); + } + + editor.refresh_inline_values(cx); + } + _ => {} + }, + )); + let git_store = project.read(cx).git_store().clone(); + let project = project.clone(); + project_subscriptions.push(cx.subscribe(&git_store, move |this, _, event, cx| { + match event { + GitStoreEvent::RepositoryUpdated( + _, + RepositoryEvent::Updated { + new_instance: true, .. + }, + _, + ) => { + this.load_diff_task = Some( + update_uncommitted_diff_for_buffer( + cx.entity(), + &project, + this.buffer.read(cx).all_buffers(), + this.buffer.clone(), + cx, + ) + .shared(), + ); + } + _ => {} + } + })); } let buffer_snapshot = buffer.read(cx).snapshot(cx); @@ -2323,15 +2319,15 @@ impl Editor { editor.go_to_active_debug_line(window, cx); - if let Some(buffer) = buffer.read(cx).as_singleton() { - if let Some(project) = editor.project() { - let handle = project.update(cx, |project, cx| { - project.register_buffer_with_language_servers(&buffer, cx) - }); - editor - .registered_buffers - .insert(buffer.read(cx).remote_id(), handle); - } + if let Some(buffer) = buffer.read(cx).as_singleton() + && let Some(project) = editor.project() + { + let handle = project.update(cx, |project, cx| { + project.register_buffer_with_language_servers(&buffer, cx) + }); + editor + .registered_buffers + .insert(buffer.read(cx).remote_id(), handle); } editor.minimap = @@ -3035,20 +3031,19 @@ impl Editor { } if local { - if let Some(buffer_id) = new_cursor_position.buffer_id { - if !self.registered_buffers.contains_key(&buffer_id) { - if let Some(project) = self.project.as_ref() { - project.update(cx, |project, cx| { - let Some(buffer) = self.buffer.read(cx).buffer(buffer_id) else { - return; - }; - self.registered_buffers.insert( - buffer_id, - project.register_buffer_with_language_servers(&buffer, cx), - ); - }) - } - } + if let Some(buffer_id) = new_cursor_position.buffer_id + && !self.registered_buffers.contains_key(&buffer_id) + && let Some(project) = self.project.as_ref() + { + project.update(cx, |project, cx| { + let Some(buffer) = self.buffer.read(cx).buffer(buffer_id) else { + return; + }; + self.registered_buffers.insert( + buffer_id, + project.register_buffer_with_language_servers(&buffer, cx), + ); + }) } let mut context_menu = self.context_menu.borrow_mut(); @@ -3063,28 +3058,28 @@ impl Editor { let completion_position = completion_menu.map(|menu| menu.initial_position); drop(context_menu); - if effects.completions { - if let Some(completion_position) = completion_position { - let start_offset = selection_start.to_offset(buffer); - let position_matches = start_offset == completion_position.to_offset(buffer); - let continue_showing = if position_matches { - if self.snippet_stack.is_empty() { - buffer.char_kind_before(start_offset, true) == Some(CharKind::Word) - } else { - // Snippet choices can be shown even when the cursor is in whitespace. - // Dismissing the menu with actions like backspace is handled by - // invalidation regions. - true - } + if effects.completions + && let Some(completion_position) = completion_position + { + let start_offset = selection_start.to_offset(buffer); + let position_matches = start_offset == completion_position.to_offset(buffer); + let continue_showing = if position_matches { + if self.snippet_stack.is_empty() { + buffer.char_kind_before(start_offset, true) == Some(CharKind::Word) } else { - false - }; - - if continue_showing { - self.show_completions(&ShowCompletions { trigger: None }, window, cx); - } else { - self.hide_context_menu(window, cx); + // Snippet choices can be shown even when the cursor is in whitespace. + // Dismissing the menu with actions like backspace is handled by + // invalidation regions. + true } + } else { + false + }; + + if continue_showing { + self.show_completions(&ShowCompletions { trigger: None }, window, cx); + } else { + self.hide_context_menu(window, cx); } } @@ -3115,30 +3110,27 @@ impl Editor { if selections.len() == 1 { cx.emit(SearchEvent::ActiveMatchChanged) } - if local { - if let Some((_, _, buffer_snapshot)) = buffer.as_singleton() { - let inmemory_selections = selections - .iter() - .map(|s| { - text::ToPoint::to_point(&s.range().start.text_anchor, buffer_snapshot) - ..text::ToPoint::to_point(&s.range().end.text_anchor, buffer_snapshot) - }) - .collect(); - self.update_restoration_data(cx, |data| { - data.selections = inmemory_selections; - }); + if local && let Some((_, _, buffer_snapshot)) = buffer.as_singleton() { + let inmemory_selections = selections + .iter() + .map(|s| { + text::ToPoint::to_point(&s.range().start.text_anchor, buffer_snapshot) + ..text::ToPoint::to_point(&s.range().end.text_anchor, buffer_snapshot) + }) + .collect(); + self.update_restoration_data(cx, |data| { + data.selections = inmemory_selections; + }); - if WorkspaceSettings::get(None, cx).restore_on_startup - != RestoreOnStartupBehavior::None - { - if let Some(workspace_id) = - self.workspace.as_ref().and_then(|workspace| workspace.1) - { - let snapshot = self.buffer().read(cx).snapshot(cx); - let selections = selections.clone(); - let background_executor = cx.background_executor().clone(); - let editor_id = cx.entity().entity_id().as_u64() as ItemId; - self.serialize_selections = cx.background_spawn(async move { + if WorkspaceSettings::get(None, cx).restore_on_startup != RestoreOnStartupBehavior::None + && let Some(workspace_id) = + self.workspace.as_ref().and_then(|workspace| workspace.1) + { + let snapshot = self.buffer().read(cx).snapshot(cx); + let selections = selections.clone(); + let background_executor = cx.background_executor().clone(); + let editor_id = cx.entity().entity_id().as_u64() as ItemId; + self.serialize_selections = cx.background_spawn(async move { background_executor.timer(SERIALIZATION_THROTTLE_TIME).await; let db_selections = selections .iter() @@ -3155,8 +3147,6 @@ impl Editor { .with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}")) .log_err(); }); - } - } } } @@ -4154,42 +4144,38 @@ impl Editor { if self.auto_replace_emoji_shortcode && selection.is_empty() && text.as_ref().ends_with(':') - { - if let Some(possible_emoji_short_code) = + && let Some(possible_emoji_short_code) = Self::find_possible_emoji_shortcode_at_position(&snapshot, selection.start) - { - if !possible_emoji_short_code.is_empty() { - if let Some(emoji) = emojis::get_by_shortcode(&possible_emoji_short_code) { - let emoji_shortcode_start = Point::new( - selection.start.row, - selection.start.column - possible_emoji_short_code.len() as u32 - 1, - ); + && !possible_emoji_short_code.is_empty() + && let Some(emoji) = emojis::get_by_shortcode(&possible_emoji_short_code) + { + let emoji_shortcode_start = Point::new( + selection.start.row, + selection.start.column - possible_emoji_short_code.len() as u32 - 1, + ); - // Remove shortcode from buffer - edits.push(( - emoji_shortcode_start..selection.start, - "".to_string().into(), - )); - new_selections.push(( - Selection { - id: selection.id, - start: snapshot.anchor_after(emoji_shortcode_start), - end: snapshot.anchor_before(selection.start), - reversed: selection.reversed, - goal: selection.goal, - }, - 0, - )); + // Remove shortcode from buffer + edits.push(( + emoji_shortcode_start..selection.start, + "".to_string().into(), + )); + new_selections.push(( + Selection { + id: selection.id, + start: snapshot.anchor_after(emoji_shortcode_start), + end: snapshot.anchor_before(selection.start), + reversed: selection.reversed, + goal: selection.goal, + }, + 0, + )); - // Insert emoji - let selection_start_anchor = snapshot.anchor_after(selection.start); - new_selections.push((selection.map(|_| selection_start_anchor), 0)); - edits.push((selection.start..selection.end, emoji.to_string().into())); + // Insert emoji + let selection_start_anchor = snapshot.anchor_after(selection.start); + new_selections.push((selection.map(|_| selection_start_anchor), 0)); + edits.push((selection.start..selection.end, emoji.to_string().into())); - continue; - } - } - } + continue; } // If not handling any auto-close operation, then just replace the selected @@ -4303,12 +4289,11 @@ impl Editor { |s| s.select(new_selections), ); - if !bracket_inserted { - if let Some(on_type_format_task) = + if !bracket_inserted + && let Some(on_type_format_task) = this.trigger_on_type_formatting(text.to_string(), window, cx) - { - on_type_format_task.detach_and_log_err(cx); - } + { + on_type_format_task.detach_and_log_err(cx); } let editor_settings = EditorSettings::get_global(cx); @@ -5274,10 +5259,10 @@ impl Editor { } let language = buffer.language()?; - if let Some(restrict_to_languages) = restrict_to_languages { - if !restrict_to_languages.contains(language) { - return None; - } + if let Some(restrict_to_languages) = restrict_to_languages + && !restrict_to_languages.contains(language) + { + return None; } Some(( excerpt_id, @@ -5605,15 +5590,15 @@ impl Editor { // that having one source with `is_incomplete: true` doesn't cause all to be re-queried. let mut completions = Vec::new(); let mut is_incomplete = false; - if let Some(provider_responses) = provider_responses.await.log_err() { - if !provider_responses.is_empty() { - for response in provider_responses { - completions.extend(response.completions); - is_incomplete = is_incomplete || response.is_incomplete; - } - if completion_settings.words == WordsCompletionMode::Fallback { - words = Task::ready(BTreeMap::default()); - } + if let Some(provider_responses) = provider_responses.await.log_err() + && !provider_responses.is_empty() + { + for response in provider_responses { + completions.extend(response.completions); + is_incomplete = is_incomplete || response.is_incomplete; + } + if completion_settings.words == WordsCompletionMode::Fallback { + words = Task::ready(BTreeMap::default()); } } @@ -5718,21 +5703,21 @@ impl Editor { editor .update_in(cx, |editor, window, cx| { - if editor.focus_handle.is_focused(window) { - if let Some(menu) = menu { - *editor.context_menu.borrow_mut() = - Some(CodeContextMenu::Completions(menu)); + if editor.focus_handle.is_focused(window) + && let Some(menu) = menu + { + *editor.context_menu.borrow_mut() = + Some(CodeContextMenu::Completions(menu)); - crate::hover_popover::hide_hover(editor, cx); - if editor.show_edit_predictions_in_menu() { - editor.update_visible_edit_prediction(window, cx); - } else { - editor.discard_edit_prediction(false, cx); - } - - cx.notify(); - return; + crate::hover_popover::hide_hover(editor, cx); + if editor.show_edit_predictions_in_menu() { + editor.update_visible_edit_prediction(window, cx); + } else { + editor.discard_edit_prediction(false, cx); } + + cx.notify(); + return; } if editor.completion_tasks.len() <= 1 { @@ -6079,11 +6064,11 @@ impl Editor { Some(CodeActionSource::Indicator(_)) => Task::ready(Ok(Default::default())), _ => { let mut task_context_task = Task::ready(None); - if let Some(tasks) = &tasks { - if let Some(project) = project { - task_context_task = - Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx); - } + if let Some(tasks) = &tasks + && let Some(project) = project + { + task_context_task = + Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx); } cx.spawn_in(window, { @@ -6148,14 +6133,14 @@ impl Editor { deployed_from, })); cx.notify(); - if spawn_straight_away { - if let Some(task) = editor.confirm_code_action( + if spawn_straight_away + && let Some(task) = editor.confirm_code_action( &ConfirmCodeAction { item_ix: Some(0) }, window, cx, - ) { - return task; - } + ) + { + return task; } Task::ready(Ok(())) @@ -6342,21 +6327,20 @@ impl Editor { .read(cx) .excerpt_containing(editor.selections.newest_anchor().head(), cx) })?; - if let Some((_, excerpted_buffer, excerpt_range)) = excerpt { - if excerpted_buffer == *buffer { - let all_edits_within_excerpt = buffer.read_with(cx, |buffer, _| { - let excerpt_range = excerpt_range.to_offset(buffer); - buffer - .edited_ranges_for_transaction::<usize>(transaction) - .all(|range| { - excerpt_range.start <= range.start - && excerpt_range.end >= range.end - }) - })?; + if let Some((_, excerpted_buffer, excerpt_range)) = excerpt + && excerpted_buffer == *buffer + { + let all_edits_within_excerpt = buffer.read_with(cx, |buffer, _| { + let excerpt_range = excerpt_range.to_offset(buffer); + buffer + .edited_ranges_for_transaction::<usize>(transaction) + .all(|range| { + excerpt_range.start <= range.start && excerpt_range.end >= range.end + }) + })?; - if all_edits_within_excerpt { - return Ok(()); - } + if all_edits_within_excerpt { + return Ok(()); } } } @@ -7779,10 +7763,10 @@ impl Editor { let indents = multibuffer.suggested_indents(cursor_point.row..cursor_point.row + 1, cx); - if let Some((_, indent)) = indents.iter().next() { - if indent.len == cursor_point.column { - self.edit_prediction_indent_conflict = false; - } + if let Some((_, indent)) = indents.iter().next() + && indent.len == cursor_point.column + { + self.edit_prediction_indent_conflict = false; } } @@ -9531,10 +9515,10 @@ impl Editor { let context_menu = self.context_menu.borrow_mut().take(); self.stale_edit_prediction_in_menu.take(); self.update_visible_edit_prediction(window, cx); - if let Some(CodeContextMenu::Completions(_)) = &context_menu { - if let Some(completion_provider) = &self.completion_provider { - completion_provider.selection_changed(None, window, cx); - } + if let Some(CodeContextMenu::Completions(_)) = &context_menu + && let Some(completion_provider) = &self.completion_provider + { + completion_provider.selection_changed(None, window, cx); } context_menu } @@ -9639,10 +9623,10 @@ impl Editor { s.select_ranges(tabstop.ranges.iter().rev().cloned()); }); - if let Some(choices) = &tabstop.choices { - if let Some(selection) = tabstop.ranges.first() { - self.show_snippet_choices(choices, selection.clone(), cx) - } + if let Some(choices) = &tabstop.choices + && let Some(selection) = tabstop.ranges.first() + { + self.show_snippet_choices(choices, selection.clone(), cx) } // If we're already at the last tabstop and it's at the end of the snippet, @@ -9776,10 +9760,10 @@ impl Editor { s.select_ranges(current_ranges.iter().rev().cloned()) }); - if let Some(choices) = &snippet.choices[snippet.active_index] { - if let Some(selection) = current_ranges.first() { - self.show_snippet_choices(choices, selection.clone(), cx); - } + if let Some(choices) = &snippet.choices[snippet.active_index] + && let Some(selection) = current_ranges.first() + { + self.show_snippet_choices(choices, selection.clone(), cx); } // If snippet state is not at the last tabstop, push it back on the stack @@ -10176,10 +10160,10 @@ impl Editor { // Avoid re-outdenting a row that has already been outdented by a // previous selection. - if let Some(last_row) = last_outdent { - if last_row == rows.start { - rows.start = rows.start.next_row(); - } + if let Some(last_row) = last_outdent + && last_row == rows.start + { + rows.start = rows.start.next_row(); } let has_multiple_rows = rows.len() > 1; for row in rows.iter_rows() { @@ -10357,11 +10341,11 @@ impl Editor { MultiBufferRow(selection.end.row) }; - if let Some(last_row_range) = row_ranges.last_mut() { - if start <= last_row_range.end { - last_row_range.end = end; - continue; - } + if let Some(last_row_range) = row_ranges.last_mut() + && start <= last_row_range.end + { + last_row_range.end = end; + continue; } row_ranges.push(start..end); } @@ -15331,17 +15315,15 @@ impl Editor { if direction == ExpandExcerptDirection::Down { let multi_buffer = self.buffer.read(cx); let snapshot = multi_buffer.snapshot(cx); - if let Some(buffer_id) = snapshot.buffer_id_for_excerpt(excerpt) { - if let Some(buffer) = multi_buffer.buffer(buffer_id) { - if let Some(excerpt_range) = snapshot.buffer_range_for_excerpt(excerpt) { - let buffer_snapshot = buffer.read(cx).snapshot(); - let excerpt_end_row = - Point::from_anchor(&excerpt_range.end, &buffer_snapshot).row; - let last_row = buffer_snapshot.max_point().row; - let lines_below = last_row.saturating_sub(excerpt_end_row); - should_scroll_up = lines_below >= lines_to_expand; - } - } + if let Some(buffer_id) = snapshot.buffer_id_for_excerpt(excerpt) + && let Some(buffer) = multi_buffer.buffer(buffer_id) + && let Some(excerpt_range) = snapshot.buffer_range_for_excerpt(excerpt) + { + let buffer_snapshot = buffer.read(cx).snapshot(); + let excerpt_end_row = Point::from_anchor(&excerpt_range.end, &buffer_snapshot).row; + let last_row = buffer_snapshot.max_point().row; + let lines_below = last_row.saturating_sub(excerpt_end_row); + should_scroll_up = lines_below >= lines_to_expand; } } @@ -15426,10 +15408,10 @@ impl Editor { let selection = self.selections.newest::<usize>(cx); let mut active_group_id = None; - if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics { - if active_group.active_range.start.to_offset(&buffer) == selection.start { - active_group_id = Some(active_group.group_id); - } + if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics + && active_group.active_range.start.to_offset(&buffer) == selection.start + { + active_group_id = Some(active_group.group_id); } fn filtered( @@ -16674,10 +16656,10 @@ impl Editor { buffer .update(cx, |buffer, cx| { - if let Some(transaction) = transaction { - if !buffer.is_singleton() { - buffer.push_transaction(&transaction.0, cx); - } + if let Some(transaction) = transaction + && !buffer.is_singleton() + { + buffer.push_transaction(&transaction.0, cx); } cx.notify(); }) @@ -16743,10 +16725,10 @@ impl Editor { buffer .update(cx, |buffer, cx| { // check if we need this - if let Some(transaction) = transaction { - if !buffer.is_singleton() { - buffer.push_transaction(&transaction.0, cx); - } + if let Some(transaction) = transaction + && !buffer.is_singleton() + { + buffer.push_transaction(&transaction.0, cx); } cx.notify(); }) @@ -17378,12 +17360,12 @@ impl Editor { } for row in (0..=range.start.row).rev() { - if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) { - if crease.range().end.row >= buffer_start_row { - to_fold.push(crease); - if row <= range.start.row { - break; - } + if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) + && crease.range().end.row >= buffer_start_row + { + to_fold.push(crease); + if row <= range.start.row { + break; } } } @@ -18693,10 +18675,10 @@ impl Editor { pub fn working_directory(&self, cx: &App) -> Option<PathBuf> { if let Some(buffer) = self.buffer().read(cx).as_singleton() { - if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) { - if let Some(dir) = file.abs_path(cx).parent() { - return Some(dir.to_owned()); - } + if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) + && let Some(dir) = file.abs_path(cx).parent() + { + return Some(dir.to_owned()); } if let Some(project_path) = buffer.read(cx).project_path(cx) { @@ -18756,10 +18738,10 @@ impl Editor { _window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(path) = self.target_file_abs_path(cx) { - if let Some(path) = path.to_str() { - cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); - } + if let Some(path) = self.target_file_abs_path(cx) + && let Some(path) = path.to_str() + { + cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); } } @@ -18769,10 +18751,10 @@ impl Editor { _window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(path) = self.target_file_path(cx) { - if let Some(path) = path.to_str() { - cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); - } + if let Some(path) = self.target_file_path(cx) + && let Some(path) = path.to_str() + { + cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); } } @@ -18841,22 +18823,20 @@ impl Editor { _: &mut Window, cx: &mut Context<Self>, ) { - if let Some(file) = self.target_file(cx) { - if let Some(file_stem) = file.path().file_stem() { - if let Some(name) = file_stem.to_str() { - cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); - } - } + if let Some(file) = self.target_file(cx) + && let Some(file_stem) = file.path().file_stem() + && let Some(name) = file_stem.to_str() + { + cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); } } pub fn copy_file_name(&mut self, _: &CopyFileName, _: &mut Window, cx: &mut Context<Self>) { - if let Some(file) = self.target_file(cx) { - if let Some(file_name) = file.path().file_name() { - if let Some(name) = file_name.to_str() { - cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); - } - } + if let Some(file) = self.target_file(cx) + && let Some(file_name) = file.path().file_name() + && let Some(name) = file_name.to_str() + { + cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); } } @@ -19126,10 +19106,10 @@ impl Editor { cx: &mut Context<Self>, ) { let selection = self.selections.newest::<Point>(cx).start.row + 1; - if let Some(file) = self.target_file(cx) { - if let Some(path) = file.path().to_str() { - cx.write_to_clipboard(ClipboardItem::new_string(format!("{path}:{selection}"))); - } + if let Some(file) = self.target_file(cx) + && let Some(path) = file.path().to_str() + { + cx.write_to_clipboard(ClipboardItem::new_string(format!("{path}:{selection}"))); } } @@ -19769,10 +19749,10 @@ impl Editor { break; } let end = range.end.to_point(&display_snapshot.buffer_snapshot); - if let Some(current_row) = &end_row { - if end.row == current_row.row { - continue; - } + if let Some(current_row) = &end_row + && end.row == current_row.row + { + continue; } let start = range.start.to_point(&display_snapshot.buffer_snapshot); if start_row.is_none() { @@ -20064,16 +20044,16 @@ impl Editor { if self.has_active_edit_prediction() { self.update_visible_edit_prediction(window, cx); } - if let Some(project) = self.project.as_ref() { - if let Some(edited_buffer) = edited_buffer { - project.update(cx, |project, cx| { - self.registered_buffers - .entry(edited_buffer.read(cx).remote_id()) - .or_insert_with(|| { - project.register_buffer_with_language_servers(edited_buffer, cx) - }); - }); - } + if let Some(project) = self.project.as_ref() + && let Some(edited_buffer) = edited_buffer + { + project.update(cx, |project, cx| { + self.registered_buffers + .entry(edited_buffer.read(cx).remote_id()) + .or_insert_with(|| { + project.register_buffer_with_language_servers(edited_buffer, cx) + }); + }); } cx.emit(EditorEvent::BufferEdited); cx.emit(SearchEvent::MatchesInvalidated); @@ -20083,10 +20063,10 @@ impl Editor { } if *singleton_buffer_edited { - if let Some(buffer) = edited_buffer { - if buffer.read(cx).file().is_none() { - cx.emit(EditorEvent::TitleChanged); - } + if let Some(buffer) = edited_buffer + && buffer.read(cx).file().is_none() + { + cx.emit(EditorEvent::TitleChanged); } if let Some(project) = &self.project { #[allow(clippy::mutable_key_type)] @@ -20132,17 +20112,17 @@ impl Editor { } => { self.tasks_update_task = Some(self.refresh_runnables(window, cx)); let buffer_id = buffer.read(cx).remote_id(); - if self.buffer.read(cx).diff_for(buffer_id).is_none() { - if let Some(project) = &self.project { - update_uncommitted_diff_for_buffer( - cx.entity(), - project, - [buffer.clone()], - self.buffer.clone(), - cx, - ) - .detach(); - } + if self.buffer.read(cx).diff_for(buffer_id).is_none() + && let Some(project) = &self.project + { + update_uncommitted_diff_for_buffer( + cx.entity(), + project, + [buffer.clone()], + self.buffer.clone(), + cx, + ) + .detach(); } self.update_lsp_data(false, Some(buffer_id), window, cx); cx.emit(EditorEvent::ExcerptsAdded { @@ -20746,11 +20726,11 @@ impl Editor { let mut chunk_lines = chunk.text.split('\n').peekable(); while let Some(text) = chunk_lines.next() { let mut merged_with_last_token = false; - if let Some(last_token) = line.back_mut() { - if last_token.highlight == highlight { - last_token.text.push_str(text); - merged_with_last_token = true; - } + if let Some(last_token) = line.back_mut() + && last_token.highlight == highlight + { + last_token.text.push_str(text); + merged_with_last_token = true; } if !merged_with_last_token { @@ -21209,39 +21189,37 @@ impl Editor { { let buffer_snapshot = OnceCell::new(); - if let Some(folds) = DB.get_editor_folds(item_id, workspace_id).log_err() { - if !folds.is_empty() { - let snapshot = - buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); - self.fold_ranges( - folds - .into_iter() - .map(|(start, end)| { - snapshot.clip_offset(start, Bias::Left) - ..snapshot.clip_offset(end, Bias::Right) - }) - .collect(), - false, - window, - cx, - ); - } - } - - if let Some(selections) = DB.get_editor_selections(item_id, workspace_id).log_err() { - if !selections.is_empty() { - let snapshot = - buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); - // skip adding the initial selection to selection history - self.selection_history.mode = SelectionHistoryMode::Skipping; - self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges(selections.into_iter().map(|(start, end)| { + if let Some(folds) = DB.get_editor_folds(item_id, workspace_id).log_err() + && !folds.is_empty() + { + let snapshot = buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); + self.fold_ranges( + folds + .into_iter() + .map(|(start, end)| { snapshot.clip_offset(start, Bias::Left) ..snapshot.clip_offset(end, Bias::Right) - })); - }); - self.selection_history.mode = SelectionHistoryMode::Normal; - } + }) + .collect(), + false, + window, + cx, + ); + } + + if let Some(selections) = DB.get_editor_selections(item_id, workspace_id).log_err() + && !selections.is_empty() + { + let snapshot = buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); + // skip adding the initial selection to selection history + self.selection_history.mode = SelectionHistoryMode::Skipping; + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges(selections.into_iter().map(|(start, end)| { + snapshot.clip_offset(start, Bias::Left) + ..snapshot.clip_offset(end, Bias::Right) + })); + }); + self.selection_history.mode = SelectionHistoryMode::Normal; }; } @@ -21283,17 +21261,15 @@ fn process_completion_for_edit( let mut snippet_source = completion.new_text.clone(); let mut previous_point = text::ToPoint::to_point(cursor_position, buffer); previous_point.column = previous_point.column.saturating_sub(1); - if let Some(scope) = buffer_snapshot.language_scope_at(previous_point) { - if scope.prefers_label_for_snippet_in_completion() { - if let Some(label) = completion.label() { - if matches!( - completion.kind(), - Some(CompletionItemKind::FUNCTION) | Some(CompletionItemKind::METHOD) - ) { - snippet_source = label; - } - } - } + if let Some(scope) = buffer_snapshot.language_scope_at(previous_point) + && scope.prefers_label_for_snippet_in_completion() + && let Some(label) = completion.label() + && matches!( + completion.kind(), + Some(CompletionItemKind::FUNCTION) | Some(CompletionItemKind::METHOD) + ) + { + snippet_source = label; } match Snippet::parse(&snippet_source).log_err() { Some(parsed_snippet) => (Some(parsed_snippet.clone()), parsed_snippet.text), @@ -21347,10 +21323,10 @@ fn process_completion_for_edit( ); let mut current_needle = text_to_replace.next(); for haystack_ch in completion.label.text.chars() { - if let Some(needle_ch) = current_needle { - if haystack_ch.eq_ignore_ascii_case(&needle_ch) { - current_needle = text_to_replace.next(); - } + if let Some(needle_ch) = current_needle + && haystack_ch.eq_ignore_ascii_case(&needle_ch) + { + current_needle = text_to_replace.next(); } } current_needle.is_none() @@ -21604,11 +21580,11 @@ impl<'a> Iterator for WordBreakingTokenizer<'a> { offset += first_grapheme.len(); grapheme_len += 1; if is_grapheme_ideographic(first_grapheme) && !is_whitespace { - if let Some(grapheme) = iter.peek().copied() { - if should_stay_with_preceding_ideograph(grapheme) { - offset += grapheme.len(); - grapheme_len += 1; - } + if let Some(grapheme) = iter.peek().copied() + && should_stay_with_preceding_ideograph(grapheme) + { + offset += grapheme.len(); + grapheme_len += 1; } } else { let mut words = self.input[offset..].split_word_bound_indices().peekable(); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 927a207358..915a3cdc38 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -917,6 +917,10 @@ impl EditorElement { } else if cfg!(any(target_os = "linux", target_os = "freebsd")) && event.button == MouseButton::Middle { + #[allow( + clippy::collapsible_if, + reason = "The cfg-block below makes this a false positive" + )] if !text_hitbox.is_hovered(window) || editor.read_only(cx) { return; } @@ -1387,29 +1391,27 @@ impl EditorElement { ref drop_cursor, ref hide_drop_cursor, } = editor.selection_drag_state + && !hide_drop_cursor + && (drop_cursor + .start + .cmp(&selection.start, &snapshot.buffer_snapshot) + .eq(&Ordering::Less) + || drop_cursor + .end + .cmp(&selection.end, &snapshot.buffer_snapshot) + .eq(&Ordering::Greater)) { - if !hide_drop_cursor - && (drop_cursor - .start - .cmp(&selection.start, &snapshot.buffer_snapshot) - .eq(&Ordering::Less) - || drop_cursor - .end - .cmp(&selection.end, &snapshot.buffer_snapshot) - .eq(&Ordering::Greater)) - { - let drag_cursor_layout = SelectionLayout::new( - drop_cursor.clone(), - false, - CursorShape::Bar, - &snapshot.display_snapshot, - false, - false, - None, - ); - let absent_color = cx.theme().players().absent(); - selections.push((absent_color, vec![drag_cursor_layout])); - } + let drag_cursor_layout = SelectionLayout::new( + drop_cursor.clone(), + false, + CursorShape::Bar, + &snapshot.display_snapshot, + false, + false, + None, + ); + let absent_color = cx.theme().players().absent(); + selections.push((absent_color, vec![drag_cursor_layout])); } } @@ -1420,19 +1422,15 @@ impl EditorElement { CollaboratorId::PeerId(peer_id) => { if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&peer_id) - { - if let Some(participant_index) = collaboration_hub + && let Some(participant_index) = collaboration_hub .user_participant_indices(cx) .get(&collaborator.user_id) - { - if let Some((local_selection_style, _)) = selections.first_mut() - { - *local_selection_style = cx - .theme() - .players() - .color_for_participant(participant_index.0); - } - } + && let Some((local_selection_style, _)) = selections.first_mut() + { + *local_selection_style = cx + .theme() + .players() + .color_for_participant(participant_index.0); } } CollaboratorId::Agent => { @@ -3518,33 +3516,33 @@ impl EditorElement { let mut x_offset = px(0.); let mut is_block = true; - if let BlockId::Custom(custom_block_id) = block_id { - if block.has_height() { - if block.place_near() { - if let Some((x_target, line_width)) = x_position { - let margin = em_width * 2; - if line_width + final_size.width + margin - < editor_width + editor_margins.gutter.full_width() - && !row_block_types.contains_key(&(row - 1)) - && element_height_in_lines == 1 - { - x_offset = line_width + margin; - row = row - 1; - is_block = false; - element_height_in_lines = 0; - row_block_types.insert(row, is_block); - } else { - let max_offset = editor_width + editor_margins.gutter.full_width() - - final_size.width; - let min_offset = (x_target + em_width - final_size.width) - .max(editor_margins.gutter.full_width()); - x_offset = x_target.min(max_offset).max(min_offset); - } - } - }; - if element_height_in_lines != block.height() { - resized_blocks.insert(custom_block_id, element_height_in_lines); + if let BlockId::Custom(custom_block_id) = block_id + && block.has_height() + { + if block.place_near() + && let Some((x_target, line_width)) = x_position + { + let margin = em_width * 2; + if line_width + final_size.width + margin + < editor_width + editor_margins.gutter.full_width() + && !row_block_types.contains_key(&(row - 1)) + && element_height_in_lines == 1 + { + x_offset = line_width + margin; + row = row - 1; + is_block = false; + element_height_in_lines = 0; + row_block_types.insert(row, is_block); + } else { + let max_offset = + editor_width + editor_margins.gutter.full_width() - final_size.width; + let min_offset = (x_target + em_width - final_size.width) + .max(editor_margins.gutter.full_width()); + x_offset = x_target.min(max_offset).max(min_offset); } + }; + if element_height_in_lines != block.height() { + resized_blocks.insert(custom_block_id, element_height_in_lines); } } for i in 0..element_height_in_lines { @@ -3987,60 +3985,58 @@ impl EditorElement { } } - if let Some(focused_block) = focused_block { - if let Some(focus_handle) = focused_block.focus_handle.upgrade() { - if focus_handle.is_focused(window) { - if let Some(block) = snapshot.block_for_id(focused_block.id) { - let style = block.style(); - let width = match style { - BlockStyle::Fixed => AvailableSpace::MinContent, - BlockStyle::Flex => AvailableSpace::Definite( - hitbox - .size - .width - .max(fixed_block_max_width) - .max(editor_margins.gutter.width + *scroll_width), - ), - BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width), - }; + if let Some(focused_block) = focused_block + && let Some(focus_handle) = focused_block.focus_handle.upgrade() + && focus_handle.is_focused(window) + && let Some(block) = snapshot.block_for_id(focused_block.id) + { + let style = block.style(); + let width = match style { + BlockStyle::Fixed => AvailableSpace::MinContent, + BlockStyle::Flex => AvailableSpace::Definite( + hitbox + .size + .width + .max(fixed_block_max_width) + .max(editor_margins.gutter.width + *scroll_width), + ), + BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width), + }; - if let Some((element, element_size, _, x_offset)) = self.render_block( - &block, - width, - focused_block.id, - rows.end, - snapshot, - text_x, - &rows, - line_layouts, - editor_margins, - line_height, - em_width, - text_hitbox, - editor_width, - scroll_width, - &mut resized_blocks, - &mut row_block_types, - selections, - selected_buffer_ids, - is_row_soft_wrapped, - sticky_header_excerpt_id, - window, - cx, - ) { - blocks.push(BlockLayout { - id: block.id(), - x_offset, - row: None, - element, - available_space: size(width, element_size.height.into()), - style, - overlaps_gutter: true, - is_buffer_header: block.is_buffer_header(), - }); - } - } - } + if let Some((element, element_size, _, x_offset)) = self.render_block( + &block, + width, + focused_block.id, + rows.end, + snapshot, + text_x, + &rows, + line_layouts, + editor_margins, + line_height, + em_width, + text_hitbox, + editor_width, + scroll_width, + &mut resized_blocks, + &mut row_block_types, + selections, + selected_buffer_ids, + is_row_soft_wrapped, + sticky_header_excerpt_id, + window, + cx, + ) { + blocks.push(BlockLayout { + id: block.id(), + x_offset, + row: None, + element, + available_space: size(width, element_size.height.into()), + style, + overlaps_gutter: true, + is_buffer_header: block.is_buffer_header(), + }); } } @@ -4203,19 +4199,19 @@ impl EditorElement { edit_prediction_popover_visible = true; } - if editor.context_menu_visible() { - if let Some(crate::ContextMenuOrigin::Cursor) = editor.context_menu_origin() { - let (min_height_in_lines, max_height_in_lines) = editor - .context_menu_options - .as_ref() - .map_or((3, 12), |options| { - (options.min_entries_visible, options.max_entries_visible) - }); + if editor.context_menu_visible() + && let Some(crate::ContextMenuOrigin::Cursor) = editor.context_menu_origin() + { + let (min_height_in_lines, max_height_in_lines) = editor + .context_menu_options + .as_ref() + .map_or((3, 12), |options| { + (options.min_entries_visible, options.max_entries_visible) + }); - min_menu_height += line_height * min_height_in_lines as f32 + POPOVER_Y_PADDING; - max_menu_height += line_height * max_height_in_lines as f32 + POPOVER_Y_PADDING; - context_menu_visible = true; - } + min_menu_height += line_height * min_height_in_lines as f32 + POPOVER_Y_PADDING; + max_menu_height += line_height * max_height_in_lines as f32 + POPOVER_Y_PADDING; + context_menu_visible = true; } context_menu_placement = editor .context_menu_options @@ -5761,16 +5757,15 @@ impl EditorElement { cx: &mut App, ) { for (_, hunk_hitbox) in &layout.display_hunks { - if let Some(hunk_hitbox) = hunk_hitbox { - if !self + if let Some(hunk_hitbox) = hunk_hitbox + && !self .editor .read(cx) .buffer() .read(cx) .all_diff_hunks_expanded() - { - window.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox); - } + { + window.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox); } } @@ -10152,10 +10147,10 @@ fn compute_auto_height_layout( let overscroll = size(em_width, px(0.)); let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width; - if !matches!(editor.soft_wrap_mode(cx), SoftWrap::None) { - if editor.set_wrap_width(Some(editor_width), cx) { - snapshot = editor.snapshot(window, cx); - } + if !matches!(editor.soft_wrap_mode(cx), SoftWrap::None) + && editor.set_wrap_width(Some(editor_width), cx) + { + snapshot = editor.snapshot(window, cx); } let scroll_height = (snapshot.max_point().row().next_row().0 as f32) * line_height; diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index fc350a5a15..712325f339 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -312,10 +312,10 @@ impl GitBlame { .as_ref() .and_then(|entry| entry.author.as_ref()) .map(|author| author.len()); - if let Some(author_len) = author_len { - if author_len > max_author_length { - max_author_length = author_len; - } + if let Some(author_len) = author_len + && author_len > max_author_length + { + max_author_length = author_len; } } @@ -416,20 +416,19 @@ impl GitBlame { if row_edits .peek() .map_or(true, |next_edit| next_edit.old.start >= old_end) + && let Some(entry) = cursor.item() { - if let Some(entry) = cursor.item() { - if old_end > edit.old.end { - new_entries.push( - GitBlameEntry { - rows: cursor.end() - edit.old.end, - blame: entry.blame.clone(), - }, - &(), - ); - } - - cursor.next(); + if old_end > edit.old.end { + new_entries.push( + GitBlameEntry { + rows: cursor.end() - edit.old.end, + blame: entry.blame.clone(), + }, + &(), + ); } + + cursor.next(); } } new_entries.append(cursor.suffix(), &()); diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 8b6e2cea84..b431834d35 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -418,24 +418,22 @@ pub fn update_inlay_link_and_hover_points( } if let Some((language_server_id, location)) = hovered_hint_part.location + && secondary_held + && !editor.has_pending_nonempty_selection() { - if secondary_held - && !editor.has_pending_nonempty_selection() - { - go_to_definition_updated = true; - show_link_definition( - shift_held, - editor, - TriggerPoint::InlayHint( - highlight, - location, - language_server_id, - ), - snapshot, - window, - cx, - ); - } + go_to_definition_updated = true; + show_link_definition( + shift_held, + editor, + TriggerPoint::InlayHint( + highlight, + location, + language_server_id, + ), + snapshot, + window, + cx, + ); } } } @@ -766,10 +764,11 @@ pub(crate) fn find_url_from_range( let mut finder = LinkFinder::new(); finder.kinds(&[LinkKind::Url]); - if let Some(link) = finder.links(&text).next() { - if link.start() == 0 && link.end() == text.len() { - return Some(link.as_str().to_string()); - } + if let Some(link) = finder.links(&text).next() + && link.start() == 0 + && link.end() == text.len() + { + return Some(link.as_str().to_string()); } None diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 6fe981fd6e..a8cdfa99df 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -142,11 +142,11 @@ pub fn hover_at_inlay( .info_popovers .iter() .any(|InfoPopover { symbol_range, .. }| { - if let RangeInEditor::Inlay(range) = symbol_range { - if range == &inlay_hover.range { - // Hover triggered from same location as last time. Don't show again. - return true; - } + if let RangeInEditor::Inlay(range) = symbol_range + && range == &inlay_hover.range + { + // Hover triggered from same location as last time. Don't show again. + return true; } false }) @@ -270,13 +270,12 @@ fn show_hover( } // Don't request again if the location is the same as the previous request - if let Some(triggered_from) = &editor.hover_state.triggered_from { - if triggered_from + if let Some(triggered_from) = &editor.hover_state.triggered_from + && triggered_from .cmp(&anchor, &snapshot.buffer_snapshot) .is_eq() - { - return None; - } + { + return None; } let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay; @@ -717,59 +716,54 @@ pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { } pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App) { - if let Ok(uri) = Url::parse(&link) { - if uri.scheme() == "file" { - if let Some(workspace) = window.root::<Workspace>().flatten() { - workspace.update(cx, |workspace, cx| { - let task = workspace.open_abs_path( - PathBuf::from(uri.path()), - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - window, - cx, - ); + if let Ok(uri) = Url::parse(&link) + && uri.scheme() == "file" + && let Some(workspace) = window.root::<Workspace>().flatten() + { + workspace.update(cx, |workspace, cx| { + let task = workspace.open_abs_path( + PathBuf::from(uri.path()), + OpenOptions { + visible: Some(OpenVisible::None), + ..Default::default() + }, + window, + cx, + ); - cx.spawn_in(window, async move |_, cx| { - let item = task.await?; - // Ruby LSP uses URLs with #L1,1-4,4 - // we'll just take the first number and assume it's a line number - let Some(fragment) = uri.fragment() else { - return anyhow::Ok(()); - }; - let mut accum = 0u32; - for c in fragment.chars() { - if c >= '0' && c <= '9' && accum < u32::MAX / 2 { - accum *= 10; - accum += c as u32 - '0' as u32; - } else if accum > 0 { - break; - } - } - if accum == 0 { - return Ok(()); - } - let Some(editor) = cx.update(|_, cx| item.act_as::<Editor>(cx))? else { - return Ok(()); - }; - editor.update_in(cx, |editor, window, cx| { - editor.change_selections( - Default::default(), - window, - cx, - |selections| { - selections.select_ranges([text::Point::new(accum - 1, 0) - ..text::Point::new(accum - 1, 0)]); - }, - ); - }) - }) - .detach_and_log_err(cx); - }); - return; - } - } + cx.spawn_in(window, async move |_, cx| { + let item = task.await?; + // Ruby LSP uses URLs with #L1,1-4,4 + // we'll just take the first number and assume it's a line number + let Some(fragment) = uri.fragment() else { + return anyhow::Ok(()); + }; + let mut accum = 0u32; + for c in fragment.chars() { + if c >= '0' && c <= '9' && accum < u32::MAX / 2 { + accum *= 10; + accum += c as u32 - '0' as u32; + } else if accum > 0 { + break; + } + } + if accum == 0 { + return Ok(()); + } + let Some(editor) = cx.update(|_, cx| item.act_as::<Editor>(cx))? else { + return Ok(()); + }; + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(Default::default(), window, cx, |selections| { + selections.select_ranges([ + text::Point::new(accum - 1, 0)..text::Point::new(accum - 1, 0) + ]); + }); + }) + }) + .detach_and_log_err(cx); + }); + return; } cx.open_url(&link); } @@ -839,21 +833,20 @@ impl HoverState { pub fn focused(&self, window: &mut Window, cx: &mut Context<Editor>) -> bool { let mut hover_popover_is_focused = false; for info_popover in &self.info_popovers { - if let Some(markdown_view) = &info_popover.parsed_content { - if markdown_view.focus_handle(cx).is_focused(window) { - hover_popover_is_focused = true; - } - } - } - if let Some(diagnostic_popover) = &self.diagnostic_popover { - if diagnostic_popover - .markdown - .focus_handle(cx) - .is_focused(window) + if let Some(markdown_view) = &info_popover.parsed_content + && markdown_view.focus_handle(cx).is_focused(window) { hover_popover_is_focused = true; } } + if let Some(diagnostic_popover) = &self.diagnostic_popover + && diagnostic_popover + .markdown + .focus_handle(cx) + .is_focused(window) + { + hover_popover_is_focused = true; + } hover_popover_is_focused } } diff --git a/crates/editor/src/indent_guides.rs b/crates/editor/src/indent_guides.rs index f6d51c929a..a1de2b604b 100644 --- a/crates/editor/src/indent_guides.rs +++ b/crates/editor/src/indent_guides.rs @@ -168,11 +168,11 @@ pub fn indent_guides_in_range( while let Some(fold) = folds.next() { let start = fold.range.start.to_point(&snapshot.buffer_snapshot); let end = fold.range.end.to_point(&snapshot.buffer_snapshot); - if let Some(last_range) = fold_ranges.last_mut() { - if last_range.end >= start { - last_range.end = last_range.end.max(end); - continue; - } + if let Some(last_range) = fold_ranges.last_mut() + && last_range.end >= start + { + last_range.end = last_range.end.max(end); + continue; } fold_ranges.push(start..end); } diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 60ad0e5bf6..cea0e32d7f 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -498,16 +498,14 @@ impl InlayHintCache { cmp::Ordering::Less | cmp::Ordering::Equal => { if !old_kinds.contains(&cached_hint.kind) && new_kinds.contains(&cached_hint.kind) - { - if let Some(anchor) = multi_buffer_snapshot + && let Some(anchor) = multi_buffer_snapshot .anchor_in_excerpt(*excerpt_id, cached_hint.position) - { - to_insert.push(Inlay::hint( - cached_hint_id.id(), - anchor, - cached_hint, - )); - } + { + to_insert.push(Inlay::hint( + cached_hint_id.id(), + anchor, + cached_hint, + )); } excerpt_cache.next(); } @@ -522,16 +520,16 @@ impl InlayHintCache { for cached_hint_id in excerpt_cache { let maybe_missed_cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id]; let cached_hint_kind = maybe_missed_cached_hint.kind; - if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) { - if let Some(anchor) = multi_buffer_snapshot + if !old_kinds.contains(&cached_hint_kind) + && new_kinds.contains(&cached_hint_kind) + && let Some(anchor) = multi_buffer_snapshot .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position) - { - to_insert.push(Inlay::hint( - cached_hint_id.id(), - anchor, - maybe_missed_cached_hint, - )); - } + { + to_insert.push(Inlay::hint( + cached_hint_id.id(), + anchor, + maybe_missed_cached_hint, + )); } } } @@ -620,44 +618,44 @@ impl InlayHintCache { ) { if let Some(excerpt_hints) = self.hints.get(&excerpt_id) { let mut guard = excerpt_hints.write(); - if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) { - if let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state { - let hint_to_resolve = cached_hint.clone(); - let server_id = *server_id; - cached_hint.resolve_state = ResolveState::Resolving; - drop(guard); - cx.spawn_in(window, async move |editor, cx| { - let resolved_hint_task = editor.update(cx, |editor, cx| { - let buffer = editor.buffer().read(cx).buffer(buffer_id)?; - editor.semantics_provider.as_ref()?.resolve_inlay_hint( - hint_to_resolve, - buffer, - server_id, - cx, - ) - })?; - if let Some(resolved_hint_task) = resolved_hint_task { - let mut resolved_hint = - resolved_hint_task.await.context("hint resolve task")?; - editor.read_with(cx, |editor, _| { - if let Some(excerpt_hints) = - editor.inlay_hint_cache.hints.get(&excerpt_id) + if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) + && let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state + { + let hint_to_resolve = cached_hint.clone(); + let server_id = *server_id; + cached_hint.resolve_state = ResolveState::Resolving; + drop(guard); + cx.spawn_in(window, async move |editor, cx| { + let resolved_hint_task = editor.update(cx, |editor, cx| { + let buffer = editor.buffer().read(cx).buffer(buffer_id)?; + editor.semantics_provider.as_ref()?.resolve_inlay_hint( + hint_to_resolve, + buffer, + server_id, + cx, + ) + })?; + if let Some(resolved_hint_task) = resolved_hint_task { + let mut resolved_hint = + resolved_hint_task.await.context("hint resolve task")?; + editor.read_with(cx, |editor, _| { + if let Some(excerpt_hints) = + editor.inlay_hint_cache.hints.get(&excerpt_id) + { + let mut guard = excerpt_hints.write(); + if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) + && cached_hint.resolve_state == ResolveState::Resolving { - let mut guard = excerpt_hints.write(); - if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) { - if cached_hint.resolve_state == ResolveState::Resolving { - resolved_hint.resolve_state = ResolveState::Resolved; - *cached_hint = resolved_hint; - } - } + resolved_hint.resolve_state = ResolveState::Resolved; + *cached_hint = resolved_hint; } - })?; - } + } + })?; + } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } } } @@ -990,8 +988,8 @@ fn fetch_and_update_hints( let buffer = editor.buffer().read(cx).buffer(query.buffer_id)?; - if !editor.registered_buffers.contains_key(&query.buffer_id) { - if let Some(project) = editor.project.as_ref() { + if !editor.registered_buffers.contains_key(&query.buffer_id) + && let Some(project) = editor.project.as_ref() { project.update(cx, |project, cx| { editor.registered_buffers.insert( query.buffer_id, @@ -999,7 +997,6 @@ fn fetch_and_update_hints( ); }) } - } editor .semantics_provider @@ -1240,14 +1237,12 @@ fn apply_hint_update( .inlay_hint_cache .allowed_hint_kinds .contains(&new_hint.kind) - { - if let Some(new_hint_position) = + && let Some(new_hint_position) = multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position) - { - splice - .to_insert - .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint)); - } + { + splice + .to_insert + .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint)); } let new_id = InlayId::Hint(new_inlay_id); cached_excerpt_hints.hints_by_id.insert(new_id, new_hint); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 22430ab5e1..136b0b314d 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -930,10 +930,10 @@ impl Item for Editor { })?; buffer .update(cx, |buffer, cx| { - if let Some(transaction) = transaction { - if !buffer.is_singleton() { - buffer.push_transaction(&transaction.0, cx); - } + if let Some(transaction) = transaction + && !buffer.is_singleton() + { + buffer.push_transaction(&transaction.0, cx); } }) .ok(); @@ -1374,36 +1374,33 @@ impl ProjectItem for Editor { let mut editor = Self::for_buffer(buffer.clone(), Some(project), window, cx); if let Some((excerpt_id, buffer_id, snapshot)) = editor.buffer().read(cx).snapshot(cx).as_singleton() + && WorkspaceSettings::get(None, cx).restore_on_file_reopen + && let Some(restoration_data) = Self::project_item_kind() + .and_then(|kind| pane.as_ref()?.project_item_restoration_data.get(&kind)) + .and_then(|data| data.downcast_ref::<EditorRestorationData>()) + .and_then(|data| { + let file = project::File::from_dyn(buffer.read(cx).file())?; + data.entries.get(&file.abs_path(cx)) + }) { - if WorkspaceSettings::get(None, cx).restore_on_file_reopen { - if let Some(restoration_data) = Self::project_item_kind() - .and_then(|kind| pane.as_ref()?.project_item_restoration_data.get(&kind)) - .and_then(|data| data.downcast_ref::<EditorRestorationData>()) - .and_then(|data| { - let file = project::File::from_dyn(buffer.read(cx).file())?; - data.entries.get(&file.abs_path(cx)) - }) - { - editor.fold_ranges( - clip_ranges(&restoration_data.folds, snapshot), - false, - window, - cx, - ); - if !restoration_data.selections.is_empty() { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges(clip_ranges(&restoration_data.selections, snapshot)); - }); - } - let (top_row, offset) = restoration_data.scroll_position; - let anchor = Anchor::in_buffer( - *excerpt_id, - buffer_id, - snapshot.anchor_before(Point::new(top_row, 0)), - ); - editor.set_scroll_anchor(ScrollAnchor { anchor, offset }, window, cx); - } + editor.fold_ranges( + clip_ranges(&restoration_data.folds, snapshot), + false, + window, + cx, + ); + if !restoration_data.selections.is_empty() { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges(clip_ranges(&restoration_data.selections, snapshot)); + }); } + let (top_row, offset) = restoration_data.scroll_position; + let anchor = Anchor::in_buffer( + *excerpt_id, + buffer_id, + snapshot.anchor_before(Point::new(top_row, 0)), + ); + editor.set_scroll_anchor(ScrollAnchor { anchor, offset }, window, cx); } editor diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index f358ab7b93..cae4b565b4 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -51,12 +51,11 @@ pub(crate) fn should_auto_close( continue; }; let mut jsx_open_tag_node = node; - if node.grammar_name() != config.open_tag_node_name { - if let Some(parent) = node.parent() { - if parent.grammar_name() == config.open_tag_node_name { - jsx_open_tag_node = parent; - } - } + if node.grammar_name() != config.open_tag_node_name + && let Some(parent) = node.parent() + && parent.grammar_name() == config.open_tag_node_name + { + jsx_open_tag_node = parent; } if jsx_open_tag_node.grammar_name() != config.open_tag_node_name { continue; @@ -284,10 +283,8 @@ pub(crate) fn generate_auto_close_edits( unclosed_open_tag_count -= 1; } } else if has_erroneous_close_tag && kind == erroneous_close_tag_node_name { - if tag_node_name_equals(&node, &tag_name) { - if !is_after_open_tag(&node) { - unclosed_open_tag_count -= 1; - } + if tag_node_name_equals(&node, &tag_name) && !is_after_open_tag(&node) { + unclosed_open_tag_count -= 1; } } else if kind == config.jsx_element_node_name { // perf: filter only open,close,element,erroneous nodes diff --git a/crates/editor/src/lsp_ext.rs b/crates/editor/src/lsp_ext.rs index d02fc0f901..18ad2d71c8 100644 --- a/crates/editor/src/lsp_ext.rs +++ b/crates/editor/src/lsp_ext.rs @@ -147,16 +147,15 @@ pub fn lsp_tasks( }, cx, ) - }) { - if let Some(new_runnables) = runnables_task.await.log_err() { - new_lsp_tasks.extend(new_runnables.runnables.into_iter().filter_map( - |(location, runnable)| { - let resolved_task = - runnable.resolve_task(&id_base, &lsp_buffer_context)?; - Some((location, resolved_task)) - }, - )); - } + }) && let Some(new_runnables) = runnables_task.await.log_err() + { + new_lsp_tasks.extend(new_runnables.runnables.into_iter().filter_map( + |(location, runnable)| { + let resolved_task = + runnable.resolve_task(&id_base, &lsp_buffer_context)?; + Some((location, resolved_task)) + }, + )); } lsp_tasks .entry(source_kind) diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 0bf875095b..7a008e3ba2 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -510,10 +510,10 @@ pub fn find_preceding_boundary_point( if find_range == FindRange::SingleLine && ch == '\n' { break; } - if let Some(prev_ch) = prev_ch { - if is_boundary(ch, prev_ch) { - break; - } + if let Some(prev_ch) = prev_ch + && is_boundary(ch, prev_ch) + { + break; } offset -= ch.len_utf8(); @@ -562,13 +562,13 @@ pub fn find_boundary_point( if find_range == FindRange::SingleLine && ch == '\n' { break; } - if let Some(prev_ch) = prev_ch { - if is_boundary(prev_ch, ch) { - if return_point_before_boundary { - return map.clip_point(prev_offset.to_display_point(map), Bias::Right); - } else { - break; - } + if let Some(prev_ch) = prev_ch + && is_boundary(prev_ch, ch) + { + if return_point_before_boundary { + return map.clip_point(prev_offset.to_display_point(map), Bias::Right); + } else { + break; } } prev_offset = offset; @@ -603,13 +603,13 @@ pub fn find_preceding_boundary_trail( // Find the boundary let start_offset = offset; for ch in forward { - if let Some(prev_ch) = prev_ch { - if is_boundary(prev_ch, ch) { - if start_offset == offset { - trail_offset = Some(offset); - } else { - break; - } + if let Some(prev_ch) = prev_ch + && is_boundary(prev_ch, ch) + { + if start_offset == offset { + trail_offset = Some(offset); + } else { + break; } } offset -= ch.len_utf8(); @@ -651,13 +651,13 @@ pub fn find_boundary_trail( // Find the boundary let start_offset = offset; for ch in forward { - if let Some(prev_ch) = prev_ch { - if is_boundary(prev_ch, ch) { - if start_offset == offset { - trail_offset = Some(offset); - } else { - break; - } + if let Some(prev_ch) = prev_ch + && is_boundary(prev_ch, ch) + { + if start_offset == offset { + trail_offset = Some(offset); + } else { + break; } } offset += ch.len_utf8(); diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index bee9464124..e3d83ab160 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -285,11 +285,11 @@ pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mu workspace.update(cx, |_workspace, cx| { // Check if the local document exists, otherwise fallback to the online document. // Open with the default browser. - if let Some(local_url) = docs_urls.local { - if fs::metadata(Path::new(&local_url[8..])).is_ok() { - cx.open_url(&local_url); - return; - } + if let Some(local_url) = docs_urls.local + && fs::metadata(Path::new(&local_url[8..])).is_ok() + { + cx.open_url(&local_url); + return; } if let Some(web_url) = docs_urls.web { diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 08ff23f8f7..b47f1cd711 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -703,20 +703,20 @@ impl Editor { if matches!( settings.defaults.soft_wrap, SoftWrap::PreferredLineLength | SoftWrap::Bounded - ) { - if (settings.defaults.preferred_line_length as f32) < visible_column_count { - visible_column_count = settings.defaults.preferred_line_length as f32; - } + ) && (settings.defaults.preferred_line_length as f32) < visible_column_count + { + visible_column_count = settings.defaults.preferred_line_length as f32; } // If the scroll position is currently at the left edge of the document // (x == 0.0) and the intent is to scroll right, the gutter's margin // should first be added to the current position, otherwise the cursor // will end at the column position minus the margin, which looks off. - if current_position.x == 0.0 && amount.columns(visible_column_count) > 0. { - if let Some(last_position_map) = &self.last_position_map { - current_position.x += self.gutter_dimensions.margin / last_position_map.em_advance; - } + if current_position.x == 0.0 + && amount.columns(visible_column_count) > 0. + && let Some(last_position_map) = &self.last_position_map + { + current_position.x += self.gutter_dimensions.margin / last_position_map.em_advance; } let new_position = current_position + point( @@ -749,12 +749,10 @@ impl Editor { if let (Some(visible_lines), Some(visible_columns)) = (self.visible_line_count(), self.visible_column_count()) + && newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32) + && newest_head.column() <= screen_top.column() + visible_columns as u32 { - if newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32) - && newest_head.column() <= screen_top.column() + visible_columns as u32 - { - return Ordering::Equal; - } + return Ordering::Equal; } Ordering::Greater diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index 88d3b52d76..057d622903 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -116,12 +116,12 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut scroll_position = self.scroll_manager.scroll_position(&display_map); let original_y = scroll_position.y; - if let Some(last_bounds) = self.expect_bounds_change.take() { - if scroll_position.y != 0. { - scroll_position.y += (bounds.top() - last_bounds.top()) / line_height; - if scroll_position.y < 0. { - scroll_position.y = 0.; - } + if let Some(last_bounds) = self.expect_bounds_change.take() + && scroll_position.y != 0. + { + scroll_position.y += (bounds.top() - last_bounds.top()) / line_height; + if scroll_position.y < 0. { + scroll_position.y = 0.; } } if scroll_position.y > max_scroll_top { diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 819d6d9fed..d388e8f3b7 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -184,10 +184,10 @@ pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestCo for (row, block) in blocks { match block { Block::Custom(custom_block) => { - if let BlockPlacement::Near(x) = &custom_block.placement { - if snapshot.intersects_fold(x.to_point(&snapshot.buffer_snapshot)) { - continue; - } + if let BlockPlacement::Near(x) = &custom_block.placement + && snapshot.intersects_fold(x.to_point(&snapshot.buffer_snapshot)) + { + continue; }; let content = block_content_for_tests(editor, custom_block.id, cx) .expect("block content not found"); diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index 53c9113934..809b530ed7 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -167,15 +167,14 @@ fn main() { continue; } - if let Some(language) = meta.language_server { - if !languages.contains(&language.file_extension) { + if let Some(language) = meta.language_server + && !languages.contains(&language.file_extension) { panic!( "Eval for {:?} could not be run because no language server was found for extension {:?}", meta.name, language.file_extension ); } - } // TODO: This creates a worktree per repetition. Ideally these examples should // either be run sequentially on the same worktree, or reuse worktrees when there diff --git a/crates/eval/src/explorer.rs b/crates/eval/src/explorer.rs index ee1dfa95c3..3326070cea 100644 --- a/crates/eval/src/explorer.rs +++ b/crates/eval/src/explorer.rs @@ -46,27 +46,25 @@ fn find_target_files_recursive( max_depth, found_files, )?; - } else if path.is_file() { - if let Some(filename_osstr) = path.file_name() { - if let Some(filename_str) = filename_osstr.to_str() { - if filename_str == target_filename { - found_files.push(path); - } - } - } + } else if path.is_file() + && let Some(filename_osstr) = path.file_name() + && let Some(filename_str) = filename_osstr.to_str() + && filename_str == target_filename + { + found_files.push(path); } } Ok(()) } pub fn generate_explorer_html(input_paths: &[PathBuf], output_path: &PathBuf) -> Result<String> { - if let Some(parent) = output_path.parent() { - if !parent.exists() { - fs::create_dir_all(parent).context(format!( - "Failed to create output directory: {}", - parent.display() - ))?; - } + if let Some(parent) = output_path.parent() + && !parent.exists() + { + fs::create_dir_all(parent).context(format!( + "Failed to create output directory: {}", + parent.display() + ))?; } let template_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/explorer.html"); diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs index e3b67ed355..dd9b4f8bba 100644 --- a/crates/eval/src/instance.rs +++ b/crates/eval/src/instance.rs @@ -376,11 +376,10 @@ impl ExampleInstance { ); let result = this.thread.conversation(&mut example_cx).await; - if let Err(err) = result { - if !err.is::<FailedAssertion>() { + if let Err(err) = result + && !err.is::<FailedAssertion>() { return Err(err); } - } println!("{}Stopped", this.log_prefix); diff --git a/crates/extension/src/extension.rs b/crates/extension/src/extension.rs index 35f7f41938..6af793253b 100644 --- a/crates/extension/src/extension.rs +++ b/crates/extension/src/extension.rs @@ -178,16 +178,15 @@ pub fn parse_wasm_extension_version( for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) { if let wasmparser::Payload::CustomSection(s) = part.context("error parsing wasm extension")? + && s.name() == "zed:api-version" { - if s.name() == "zed:api-version" { - version = parse_wasm_extension_version_custom_section(s.data()); - if version.is_none() { - bail!( - "extension {} has invalid zed:api-version section: {:?}", - extension_id, - s.data() - ); - } + version = parse_wasm_extension_version_custom_section(s.data()); + if version.is_none() { + bail!( + "extension {} has invalid zed:api-version section: {:?}", + extension_id, + s.data() + ); } } } diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 4ee948dda8..01edb5c033 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -93,10 +93,9 @@ pub fn is_version_compatible( .wasm_api_version .as_ref() .and_then(|wasm_api_version| SemanticVersion::from_str(wasm_api_version).ok()) + && !is_supported_wasm_api_version(release_channel, wasm_api_version) { - if !is_supported_wasm_api_version(release_channel, wasm_api_version) { - return false; - } + return false; } true @@ -292,19 +291,17 @@ impl ExtensionStore { // it must be asynchronously rebuilt. let mut extension_index = ExtensionIndex::default(); let mut extension_index_needs_rebuild = true; - if let Ok(index_content) = index_content { - if let Some(index) = serde_json::from_str(&index_content).log_err() { - extension_index = index; - if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) = - (index_metadata, extensions_metadata) - { - if index_metadata - .mtime - .bad_is_greater_than(extensions_metadata.mtime) - { - extension_index_needs_rebuild = false; - } - } + if let Ok(index_content) = index_content + && let Some(index) = serde_json::from_str(&index_content).log_err() + { + extension_index = index; + if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) = + (index_metadata, extensions_metadata) + && index_metadata + .mtime + .bad_is_greater_than(extensions_metadata.mtime) + { + extension_index_needs_rebuild = false; } } @@ -392,10 +389,9 @@ impl ExtensionStore { if let Some(path::Component::Normal(extension_dir_name)) = event_path.components().next() + && let Some(extension_id) = extension_dir_name.to_str() { - if let Some(extension_id) = extension_dir_name.to_str() { - reload_tx.unbounded_send(Some(extension_id.into())).ok(); - } + reload_tx.unbounded_send(Some(extension_id.into())).ok(); } } } @@ -763,8 +759,8 @@ impl ExtensionStore { if let ExtensionOperation::Install = operation { this.update( cx, |this, cx| { cx.emit(Event::ExtensionInstalled(extension_id.clone())); - if let Some(events) = ExtensionEvents::try_global(cx) { - if let Some(manifest) = this.extension_manifest_for_id(&extension_id) { + if let Some(events) = ExtensionEvents::try_global(cx) + && let Some(manifest) = this.extension_manifest_for_id(&extension_id) { events.update(cx, |this, cx| { this.emit( extension::Event::ExtensionInstalled(manifest.clone()), @@ -772,7 +768,6 @@ impl ExtensionStore { ) }); } - } }) .ok(); } @@ -912,12 +907,12 @@ impl ExtensionStore { extension_store.update(cx, |_, cx| { cx.emit(Event::ExtensionUninstalled(extension_id.clone())); - if let Some(events) = ExtensionEvents::try_global(cx) { - if let Some(manifest) = extension_manifest { - events.update(cx, |this, cx| { - this.emit(extension::Event::ExtensionUninstalled(manifest.clone()), cx) - }); - } + if let Some(events) = ExtensionEvents::try_global(cx) + && let Some(manifest) = extension_manifest + { + events.update(cx, |this, cx| { + this.emit(extension::Event::ExtensionUninstalled(manifest.clone()), cx) + }); } })?; @@ -997,12 +992,12 @@ impl ExtensionStore { this.update(cx, |this, cx| this.reload(None, cx))?.await; this.update(cx, |this, cx| { cx.emit(Event::ExtensionInstalled(extension_id.clone())); - if let Some(events) = ExtensionEvents::try_global(cx) { - if let Some(manifest) = this.extension_manifest_for_id(&extension_id) { - events.update(cx, |this, cx| { - this.emit(extension::Event::ExtensionInstalled(manifest.clone()), cx) - }); - } + if let Some(events) = ExtensionEvents::try_global(cx) + && let Some(manifest) = this.extension_manifest_for_id(&extension_id) + { + events.update(cx, |this, cx| { + this.emit(extension::Event::ExtensionInstalled(manifest.clone()), cx) + }); } })?; @@ -1788,10 +1783,10 @@ impl ExtensionStore { let connection_options = client.read(cx).connection_options(); let ssh_url = connection_options.ssh_url(); - if let Some(existing_client) = self.ssh_clients.get(&ssh_url) { - if existing_client.upgrade().is_some() { - return; - } + if let Some(existing_client) = self.ssh_clients.get(&ssh_url) + && existing_client.upgrade().is_some() + { + return; } self.ssh_clients.insert(ssh_url, client.downgrade()); diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index d990b670f4..4fe27aedc9 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -701,16 +701,15 @@ pub fn parse_wasm_extension_version( for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) { if let wasmparser::Payload::CustomSection(s) = part.context("error parsing wasm extension")? + && s.name() == "zed:api-version" { - if s.name() == "zed:api-version" { - version = parse_wasm_extension_version_custom_section(s.data()); - if version.is_none() { - bail!( - "extension {} has invalid zed:api-version section: {:?}", - extension_id, - s.data() - ); - } + version = parse_wasm_extension_version_custom_section(s.data()); + if version.is_none() { + bail!( + "extension {} has invalid zed:api-version section: {:?}", + extension_id, + s.data() + ); } } } diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 7c7f9e6836..7f0e8171f6 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -1031,15 +1031,14 @@ impl ExtensionsPage { .read(cx) .extension_manifest_for_id(&extension_id) .cloned() + && let Some(events) = extension::ExtensionEvents::try_global(cx) { - if let Some(events) = extension::ExtensionEvents::try_global(cx) { - events.update(cx, |this, cx| { - this.emit( - extension::Event::ConfigureExtensionRequested(manifest), - cx, - ) - }); - } + events.update(cx, |this, cx| { + this.emit( + extension::Event::ConfigureExtensionRequested(manifest), + cx, + ) + }); } } }) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index e8f80e5ef2..aebc262af0 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -209,11 +209,11 @@ impl FileFinder { let Some(init_modifiers) = self.init_modifiers.take() else { return; }; - if self.picker.read(cx).delegate.has_changed_selected_index { - if !event.modified() || !init_modifiers.is_subset_of(event) { - self.init_modifiers = None; - window.dispatch_action(menu::Confirm.boxed_clone(), cx); - } + if self.picker.read(cx).delegate.has_changed_selected_index + && (!event.modified() || !init_modifiers.is_subset_of(event)) + { + self.init_modifiers = None; + window.dispatch_action(menu::Confirm.boxed_clone(), cx); } } @@ -323,27 +323,27 @@ impl FileFinder { ) { self.picker.update(cx, |picker, cx| { let delegate = &mut picker.delegate; - if let Some(workspace) = delegate.workspace.upgrade() { - if let Some(m) = delegate.matches.get(delegate.selected_index()) { - let path = match &m { - Match::History { path, .. } => { - let worktree_id = path.project.worktree_id; - ProjectPath { - worktree_id, - path: Arc::clone(&path.project.path), - } + if let Some(workspace) = delegate.workspace.upgrade() + && let Some(m) = delegate.matches.get(delegate.selected_index()) + { + let path = match &m { + Match::History { path, .. } => { + let worktree_id = path.project.worktree_id; + ProjectPath { + worktree_id, + path: Arc::clone(&path.project.path), } - Match::Search(m) => ProjectPath { - worktree_id: WorktreeId::from_usize(m.0.worktree_id), - path: m.0.path.clone(), - }, - Match::CreateNew(p) => p.clone(), - }; - let open_task = workspace.update(cx, move |workspace, cx| { - workspace.split_path_preview(path, false, Some(split_direction), window, cx) - }); - open_task.detach_and_log_err(cx); - } + } + Match::Search(m) => ProjectPath { + worktree_id: WorktreeId::from_usize(m.0.worktree_id), + path: m.0.path.clone(), + }, + Match::CreateNew(p) => p.clone(), + }; + let open_task = workspace.update(cx, move |workspace, cx| { + workspace.split_path_preview(path, false, Some(split_direction), window, cx) + }); + open_task.detach_and_log_err(cx); } }) } @@ -675,17 +675,17 @@ impl Matches { let path_str = panel_match.0.path.to_string_lossy(); let filename_str = filename.to_string_lossy(); - if let Some(filename_pos) = path_str.rfind(&*filename_str) { - if panel_match.0.positions[0] >= filename_pos { - let mut prev_position = panel_match.0.positions[0]; - for p in &panel_match.0.positions[1..] { - if *p != prev_position + 1 { - return false; - } - prev_position = *p; + if let Some(filename_pos) = path_str.rfind(&*filename_str) + && panel_match.0.positions[0] >= filename_pos + { + let mut prev_position = panel_match.0.positions[0]; + for p in &panel_match.0.positions[1..] { + if *p != prev_position + 1 { + return false; } - return true; + prev_position = *p; } + return true; } } @@ -1045,10 +1045,10 @@ impl FileFinderDelegate { ) } else { let mut path = Arc::clone(project_relative_path); - if project_relative_path.as_ref() == Path::new("") { - if let Some(absolute_path) = &entry_path.absolute { - path = Arc::from(absolute_path.as_path()); - } + if project_relative_path.as_ref() == Path::new("") + && let Some(absolute_path) = &entry_path.absolute + { + path = Arc::from(absolute_path.as_path()); } let mut path_match = PathMatch { @@ -1078,23 +1078,21 @@ impl FileFinderDelegate { ), }; - if file_name_positions.is_empty() { - if let Some(user_home_path) = std::env::var("HOME").ok() { - let user_home_path = user_home_path.trim(); - if !user_home_path.is_empty() { - if full_path.starts_with(user_home_path) { - full_path.replace_range(0..user_home_path.len(), "~"); - full_path_positions.retain_mut(|pos| { - if *pos >= user_home_path.len() { - *pos -= user_home_path.len(); - *pos += 1; - true - } else { - false - } - }) + if file_name_positions.is_empty() + && let Some(user_home_path) = std::env::var("HOME").ok() + { + let user_home_path = user_home_path.trim(); + if !user_home_path.is_empty() && full_path.starts_with(user_home_path) { + full_path.replace_range(0..user_home_path.len(), "~"); + full_path_positions.retain_mut(|pos| { + if *pos >= user_home_path.len() { + *pos -= user_home_path.len(); + *pos += 1; + true + } else { + false } - } + }) } } @@ -1242,14 +1240,13 @@ impl FileFinderDelegate { /// Skips first history match (that is displayed topmost) if it's currently opened. fn calculate_selected_index(&self, cx: &mut Context<Picker<Self>>) -> usize { - if FileFinderSettings::get_global(cx).skip_focus_for_active_in_search { - if let Some(Match::History { path, .. }) = self.matches.get(0) { - if Some(path) == self.currently_opened_path.as_ref() { - let elements_after_first = self.matches.len() - 1; - if elements_after_first > 0 { - return 1; - } - } + if FileFinderSettings::get_global(cx).skip_focus_for_active_in_search + && let Some(Match::History { path, .. }) = self.matches.get(0) + && Some(path) == self.currently_opened_path.as_ref() + { + let elements_after_first = self.matches.len() - 1; + if elements_after_first > 0 { + return 1; } } @@ -1310,10 +1307,10 @@ impl PickerDelegate for FileFinderDelegate { .enumerate() .find(|(_, m)| !matches!(m, Match::History { .. })) .map(|(i, _)| i); - if let Some(first_non_history_index) = first_non_history_index { - if first_non_history_index > 0 { - return vec![first_non_history_index - 1]; - } + if let Some(first_non_history_index) = first_non_history_index + && first_non_history_index > 0 + { + return vec![first_non_history_index - 1]; } } Vec::new() @@ -1436,69 +1433,101 @@ impl PickerDelegate for FileFinderDelegate { window: &mut Window, cx: &mut Context<Picker<FileFinderDelegate>>, ) { - if let Some(m) = self.matches.get(self.selected_index()) { - if let Some(workspace) = self.workspace.upgrade() { - let open_task = workspace.update(cx, |workspace, cx| { - let split_or_open = - |workspace: &mut Workspace, - project_path, - window: &mut Window, - cx: &mut Context<Workspace>| { - let allow_preview = - PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder; - if secondary { - workspace.split_path_preview( - project_path, - allow_preview, - None, - window, - cx, - ) - } else { - workspace.open_path_preview( - project_path, - None, - true, - allow_preview, - true, - window, - cx, - ) - } - }; - match &m { - Match::CreateNew(project_path) => { - // Create a new file with the given filename - if secondary { - workspace.split_path_preview( - project_path.clone(), - false, - None, - window, - cx, - ) - } else { - workspace.open_path_preview( - project_path.clone(), - None, - true, - false, - true, - window, - cx, - ) - } + if let Some(m) = self.matches.get(self.selected_index()) + && let Some(workspace) = self.workspace.upgrade() + { + let open_task = workspace.update(cx, |workspace, cx| { + let split_or_open = + |workspace: &mut Workspace, + project_path, + window: &mut Window, + cx: &mut Context<Workspace>| { + let allow_preview = + PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder; + if secondary { + workspace.split_path_preview( + project_path, + allow_preview, + None, + window, + cx, + ) + } else { + workspace.open_path_preview( + project_path, + None, + true, + allow_preview, + true, + window, + cx, + ) } + }; + match &m { + Match::CreateNew(project_path) => { + // Create a new file with the given filename + if secondary { + workspace.split_path_preview( + project_path.clone(), + false, + None, + window, + cx, + ) + } else { + workspace.open_path_preview( + project_path.clone(), + None, + true, + false, + true, + window, + cx, + ) + } + } - Match::History { path, .. } => { - let worktree_id = path.project.worktree_id; - if workspace - .project() - .read(cx) - .worktree_for_id(worktree_id, cx) - .is_some() - { - split_or_open( + Match::History { path, .. } => { + let worktree_id = path.project.worktree_id; + if workspace + .project() + .read(cx) + .worktree_for_id(worktree_id, cx) + .is_some() + { + split_or_open( + workspace, + ProjectPath { + worktree_id, + path: Arc::clone(&path.project.path), + }, + window, + cx, + ) + } else { + match path.absolute.as_ref() { + Some(abs_path) => { + if secondary { + workspace.split_abs_path( + abs_path.to_path_buf(), + false, + window, + cx, + ) + } else { + workspace.open_abs_path( + abs_path.to_path_buf(), + OpenOptions { + visible: Some(OpenVisible::None), + ..Default::default() + }, + window, + cx, + ) + } + } + None => split_or_open( workspace, ProjectPath { worktree_id, @@ -1506,88 +1535,52 @@ impl PickerDelegate for FileFinderDelegate { }, window, cx, - ) - } else { - match path.absolute.as_ref() { - Some(abs_path) => { - if secondary { - workspace.split_abs_path( - abs_path.to_path_buf(), - false, - window, - cx, - ) - } else { - workspace.open_abs_path( - abs_path.to_path_buf(), - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - window, - cx, - ) - } - } - None => split_or_open( - workspace, - ProjectPath { - worktree_id, - path: Arc::clone(&path.project.path), - }, - window, - cx, - ), - } + ), } } - Match::Search(m) => split_or_open( - workspace, - ProjectPath { - worktree_id: WorktreeId::from_usize(m.0.worktree_id), - path: m.0.path.clone(), - }, - window, - cx, - ), } - }); + Match::Search(m) => split_or_open( + workspace, + ProjectPath { + worktree_id: WorktreeId::from_usize(m.0.worktree_id), + path: m.0.path.clone(), + }, + window, + cx, + ), + } + }); - let row = self - .latest_search_query - .as_ref() - .and_then(|query| query.path_position.row) - .map(|row| row.saturating_sub(1)); - let col = self - .latest_search_query - .as_ref() - .and_then(|query| query.path_position.column) - .unwrap_or(0) - .saturating_sub(1); - let finder = self.file_finder.clone(); + let row = self + .latest_search_query + .as_ref() + .and_then(|query| query.path_position.row) + .map(|row| row.saturating_sub(1)); + let col = self + .latest_search_query + .as_ref() + .and_then(|query| query.path_position.column) + .unwrap_or(0) + .saturating_sub(1); + let finder = self.file_finder.clone(); - cx.spawn_in(window, async move |_, cx| { - let item = open_task.await.notify_async_err(cx)?; - if let Some(row) = row { - if let Some(active_editor) = item.downcast::<Editor>() { - active_editor - .downgrade() - .update_in(cx, |editor, window, cx| { - editor.go_to_singleton_buffer_point( - Point::new(row, col), - window, - cx, - ); - }) - .log_err(); - } - } - finder.update(cx, |_, cx| cx.emit(DismissEvent)).ok()?; + cx.spawn_in(window, async move |_, cx| { + let item = open_task.await.notify_async_err(cx)?; + if let Some(row) = row + && let Some(active_editor) = item.downcast::<Editor>() + { + active_editor + .downgrade() + .update_in(cx, |editor, window, cx| { + editor.go_to_singleton_buffer_point(Point::new(row, col), window, cx); + }) + .log_err(); + } + finder.update(cx, |_, cx| cx.emit(DismissEvent)).ok()?; - Some(()) - }) - .detach(); - } + Some(()) + }) + .detach(); } } diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index 7235568e4f..3a99afc8cb 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -75,16 +75,16 @@ impl OpenPathDelegate { .. } => { let mut i = selected_match_index; - if let Some(user_input) = user_input { - if !user_input.exists || !user_input.is_dir { - if i == 0 { - return Some(CandidateInfo { - path: user_input.file.clone(), - is_dir: false, - }); - } else { - i -= 1; - } + if let Some(user_input) = user_input + && (!user_input.exists || !user_input.is_dir) + { + if i == 0 { + return Some(CandidateInfo { + path: user_input.file.clone(), + is_dir: false, + }); + } else { + i -= 1; } } let id = self.string_matches.get(i)?.candidate_id; diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 64eeae99d1..847e98d6c4 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -420,18 +420,19 @@ impl Fs for RealFs { async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> { #[cfg(windows)] - if let Ok(Some(metadata)) = self.metadata(path).await { - if metadata.is_symlink && metadata.is_dir { - self.remove_dir( - path, - RemoveOptions { - recursive: false, - ignore_if_not_exists: true, - }, - ) - .await?; - return Ok(()); - } + if let Ok(Some(metadata)) = self.metadata(path).await + && metadata.is_symlink + && metadata.is_dir + { + self.remove_dir( + path, + RemoveOptions { + recursive: false, + ignore_if_not_exists: true, + }, + ) + .await?; + return Ok(()); } match smol::fs::remove_file(path).await { @@ -467,11 +468,11 @@ impl Fs for RealFs { #[cfg(any(target_os = "linux", target_os = "freebsd"))] async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> { - if let Ok(Some(metadata)) = self.metadata(path).await { - if metadata.is_symlink { - // TODO: trash_file does not support trashing symlinks yet - https://github.com/bilelmoussaoui/ashpd/issues/255 - return self.remove_file(path, RemoveOptions::default()).await; - } + if let Ok(Some(metadata)) = self.metadata(path).await + && metadata.is_symlink + { + // TODO: trash_file does not support trashing symlinks yet - https://github.com/bilelmoussaoui/ashpd/issues/255 + return self.remove_file(path, RemoveOptions::default()).await; } let file = smol::fs::File::open(path).await?; match trash::trash_file(&file.as_fd()).await { @@ -766,24 +767,23 @@ impl Fs for RealFs { let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default(); let watcher = Arc::new(fs_watcher::FsWatcher::new(tx, pending_paths.clone())); - if watcher.add(path).is_err() { - // If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created. - if let Some(parent) = path.parent() { - if let Err(e) = watcher.add(parent) { - log::warn!("Failed to watch: {e}"); - } - } + // If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created. + if watcher.add(path).is_err() + && let Some(parent) = path.parent() + && let Err(e) = watcher.add(parent) + { + log::warn!("Failed to watch: {e}"); } // Check if path is a symlink and follow the target parent if let Some(mut target) = self.read_link(path).await.ok() { // Check if symlink target is relative path, if so make it absolute - if target.is_relative() { - if let Some(parent) = path.parent() { - target = parent.join(target); - if let Ok(canonical) = self.canonicalize(&target).await { - target = SanitizedPath::from(canonical).as_path().to_path_buf(); - } + if target.is_relative() + && let Some(parent) = path.parent() + { + target = parent.join(target); + if let Ok(canonical) = self.canonicalize(&target).await { + target = SanitizedPath::from(canonical).as_path().to_path_buf(); } } watcher.add(&target).ok(); @@ -1068,13 +1068,13 @@ impl FakeFsState { let current_entry = *entry_stack.last()?; if let FakeFsEntry::Dir { entries, .. } = current_entry { let entry = entries.get(name.to_str().unwrap())?; - if path_components.peek().is_some() || follow_symlink { - if let FakeFsEntry::Symlink { target, .. } = entry { - let mut target = target.clone(); - target.extend(path_components); - path = target; - continue 'outer; - } + if (path_components.peek().is_some() || follow_symlink) + && let FakeFsEntry::Symlink { target, .. } = entry + { + let mut target = target.clone(); + target.extend(path_components); + path = target; + continue 'outer; } entry_stack.push(entry); canonical_path = canonical_path.join(name); @@ -1566,10 +1566,10 @@ impl FakeFs { pub fn insert_branches(&self, dot_git: &Path, branches: &[&str]) { self.with_git_state(dot_git, true, |state| { - if let Some(first) = branches.first() { - if state.current_branch_name.is_none() { - state.current_branch_name = Some(first.to_string()) - } + if let Some(first) = branches.first() + && state.current_branch_name.is_none() + { + state.current_branch_name = Some(first.to_string()) } state .branches diff --git a/crates/fs/src/mac_watcher.rs b/crates/fs/src/mac_watcher.rs index aa75ad31d9..7bd176639f 100644 --- a/crates/fs/src/mac_watcher.rs +++ b/crates/fs/src/mac_watcher.rs @@ -41,10 +41,9 @@ impl Watcher for MacWatcher { if let Some((watched_path, _)) = handles .range::<Path, _>((Bound::Unbounded, Bound::Included(path))) .next_back() + && path.starts_with(watched_path) { - if path.starts_with(watched_path) { - return Ok(()); - } + return Ok(()); } let (stream, handle) = EventStream::new(&[path], self.latency); diff --git a/crates/fsevent/src/fsevent.rs b/crates/fsevent/src/fsevent.rs index 81ca0a4114..c97ab5f35d 100644 --- a/crates/fsevent/src/fsevent.rs +++ b/crates/fsevent/src/fsevent.rs @@ -178,40 +178,39 @@ impl EventStream { flags.contains(StreamFlags::USER_DROPPED) || flags.contains(StreamFlags::KERNEL_DROPPED) }) + && let Some(last_valid_event_id) = state.last_valid_event_id.take() { - if let Some(last_valid_event_id) = state.last_valid_event_id.take() { - fs::FSEventStreamStop(state.stream); - fs::FSEventStreamInvalidate(state.stream); - fs::FSEventStreamRelease(state.stream); + fs::FSEventStreamStop(state.stream); + fs::FSEventStreamInvalidate(state.stream); + fs::FSEventStreamRelease(state.stream); - let stream_context = fs::FSEventStreamContext { - version: 0, - info, - retain: None, - release: None, - copy_description: None, - }; - let stream = fs::FSEventStreamCreate( - cf::kCFAllocatorDefault, - Self::trampoline, - &stream_context, - state.paths, - last_valid_event_id, - state.latency.as_secs_f64(), - fs::kFSEventStreamCreateFlagFileEvents - | fs::kFSEventStreamCreateFlagNoDefer - | fs::kFSEventStreamCreateFlagWatchRoot, - ); + let stream_context = fs::FSEventStreamContext { + version: 0, + info, + retain: None, + release: None, + copy_description: None, + }; + let stream = fs::FSEventStreamCreate( + cf::kCFAllocatorDefault, + Self::trampoline, + &stream_context, + state.paths, + last_valid_event_id, + state.latency.as_secs_f64(), + fs::kFSEventStreamCreateFlagFileEvents + | fs::kFSEventStreamCreateFlagNoDefer + | fs::kFSEventStreamCreateFlagWatchRoot, + ); - state.stream = stream; - fs::FSEventStreamScheduleWithRunLoop( - state.stream, - cf::CFRunLoopGetCurrent(), - cf::kCFRunLoopDefaultMode, - ); - fs::FSEventStreamStart(state.stream); - stream_restarted = true; - } + state.stream = stream; + fs::FSEventStreamScheduleWithRunLoop( + state.stream, + cf::CFRunLoopGetCurrent(), + cf::kCFRunLoopDefaultMode, + ); + fs::FSEventStreamStart(state.stream); + stream_restarted = true; } if !stream_restarted { diff --git a/crates/git/src/blame.rs b/crates/git/src/blame.rs index 6f12681ea0..24b2c44218 100644 --- a/crates/git/src/blame.rs +++ b/crates/git/src/blame.rs @@ -289,14 +289,12 @@ fn parse_git_blame(output: &str) -> Result<Vec<BlameEntry>> { } }; - if done { - if let Some(entry) = current_entry.take() { - index.insert(entry.sha, entries.len()); + if done && let Some(entry) = current_entry.take() { + index.insert(entry.sha, entries.len()); - // We only want annotations that have a commit. - if !entry.sha.is_zero() { - entries.push(entry); - } + // We only want annotations that have a commit. + if !entry.sha.is_zero() { + entries.push(entry); } } } diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index ae8c5f849c..c30b789d9f 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -1447,12 +1447,11 @@ impl GitRepository for RealGitRepository { let mut remote_branches = vec![]; let mut add_if_matching = async |remote_head: &str| { - if let Ok(merge_base) = git_cmd(&["merge-base", &head, remote_head]).await { - if merge_base.trim() == head { - if let Some(s) = remote_head.strip_prefix("refs/remotes/") { - remote_branches.push(s.to_owned().into()); - } - } + if let Ok(merge_base) = git_cmd(&["merge-base", &head, remote_head]).await + && merge_base.trim() == head + && let Some(s) = remote_head.strip_prefix("refs/remotes/") + { + remote_branches.push(s.to_owned().into()); } }; @@ -1574,10 +1573,9 @@ impl GitRepository for RealGitRepository { Err(error) => { if let Some(GitBinaryCommandError { status, .. }) = error.downcast_ref::<GitBinaryCommandError>() + && status.code() == Some(1) { - if status.code() == Some(1) { - return Ok(false); - } + return Ok(false); } Err(error) diff --git a/crates/git_hosting_providers/src/git_hosting_providers.rs b/crates/git_hosting_providers/src/git_hosting_providers.rs index d4b3a59375..1d88c47f2e 100644 --- a/crates/git_hosting_providers/src/git_hosting_providers.rs +++ b/crates/git_hosting_providers/src/git_hosting_providers.rs @@ -49,10 +49,10 @@ pub fn register_additional_providers( pub fn get_host_from_git_remote_url(remote_url: &str) -> Result<String> { maybe!({ - if let Some(remote_url) = remote_url.strip_prefix("git@") { - if let Some((host, _)) = remote_url.trim_start_matches("git@").split_once(':') { - return Some(host.to_string()); - } + if let Some(remote_url) = remote_url.strip_prefix("git@") + && let Some((host, _)) = remote_url.trim_start_matches("git@").split_once(':') + { + return Some(host.to_string()); } Url::parse(remote_url) diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index 5e7430ebc6..4303f53275 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -135,11 +135,10 @@ impl CommitModal { .as_ref() .and_then(|repo| repo.read(cx).head_commit.as_ref()) .is_some() + && !git_panel.amend_pending() { - if !git_panel.amend_pending() { - git_panel.set_amend_pending(true, cx); - git_panel.load_last_commit_message_if_empty(cx); - } + git_panel.set_amend_pending(true, cx); + git_panel.load_last_commit_message_if_empty(cx); } } ForceMode::Commit => { @@ -195,12 +194,12 @@ impl CommitModal { let commit_message = commit_editor.read(cx).text(cx); - if let Some(suggested_commit_message) = suggested_commit_message { - if commit_message.is_empty() { - commit_editor.update(cx, |editor, cx| { - editor.set_placeholder_text(suggested_commit_message, cx); - }); - } + if let Some(suggested_commit_message) = suggested_commit_message + && commit_message.is_empty() + { + commit_editor.update(cx, |editor, cx| { + editor.set_placeholder_text(suggested_commit_message, cx); + }); } let focus_handle = commit_editor.focus_handle(cx); diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index b1bdcdc3e0..82870b4e75 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -926,19 +926,17 @@ impl GitPanel { let workspace = self.workspace.upgrade()?; let git_repo = self.active_repository.as_ref()?; - if let Some(project_diff) = workspace.read(cx).active_item_as::<ProjectDiff>(cx) { - if let Some(project_path) = project_diff.read(cx).active_path(cx) { - if Some(&entry.repo_path) - == git_repo - .read(cx) - .project_path_to_repo_path(&project_path, cx) - .as_ref() - { - project_diff.focus_handle(cx).focus(window); - project_diff.update(cx, |project_diff, cx| project_diff.autoscroll(cx)); - return None; - } - } + if let Some(project_diff) = workspace.read(cx).active_item_as::<ProjectDiff>(cx) + && let Some(project_path) = project_diff.read(cx).active_path(cx) + && Some(&entry.repo_path) + == git_repo + .read(cx) + .project_path_to_repo_path(&project_path, cx) + .as_ref() + { + project_diff.focus_handle(cx).focus(window); + project_diff.update(cx, |project_diff, cx| project_diff.autoscroll(cx)); + return None; }; self.workspace @@ -2514,10 +2512,11 @@ impl GitPanel { new_co_authors.push((name.clone(), email.clone())) } } - if !project.is_local() && !project.is_read_only(cx) { - if let Some(local_committer) = self.local_committer(room, cx) { - new_co_authors.push(local_committer); - } + if !project.is_local() + && !project.is_read_only(cx) + && let Some(local_committer) = self.local_committer(room, cx) + { + new_co_authors.push(local_committer); } new_co_authors } @@ -2758,14 +2757,13 @@ impl GitPanel { pending_staged_count += pending.entries.len(); last_pending_staged = pending.entries.first().cloned(); } - if let Some(single_staged) = &single_staged_entry { - if pending + if let Some(single_staged) = &single_staged_entry + && pending .entries .iter() .any(|entry| entry.repo_path == single_staged.repo_path) - { - pending_status_for_single_staged = Some(pending.target_status); - } + { + pending_status_for_single_staged = Some(pending.target_status); } } diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 09c5ce1152..3c0898fabf 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -363,10 +363,10 @@ impl ProjectDiff { } _ => {} } - if editor.focus_handle(cx).contains_focused(window, cx) { - if self.multibuffer.read(cx).is_empty() { - self.focus_handle.focus(window) - } + if editor.focus_handle(cx).contains_focused(window, cx) + && self.multibuffer.read(cx).is_empty() + { + self.focus_handle.focus(window) } } diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index af92621378..9d918048fa 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -95,10 +95,8 @@ impl CursorPosition { .ok() .unwrap_or(true); - if !is_singleton { - if let Some(debounce) = debounce { - cx.background_executor().timer(debounce).await; - } + if !is_singleton && let Some(debounce) = debounce { + cx.background_executor().timer(debounce).await; } editor @@ -234,13 +232,11 @@ impl Render for CursorPosition { if let Some(editor) = workspace .active_item(cx) .and_then(|item| item.act_as::<Editor>(cx)) + && let Some((_, buffer, _)) = editor.read(cx).active_excerpt(cx) { - if let Some((_, buffer, _)) = editor.read(cx).active_excerpt(cx) - { - workspace.toggle_modal(window, cx, |window, cx| { - crate::GoToLine::new(editor, buffer, window, cx) - }) - } + workspace.toggle_modal(window, cx, |window, cx| { + crate::GoToLine::new(editor, buffer, window, cx) + }) } }); } diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 1ac933e316..908e61cac7 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -103,11 +103,11 @@ impl GoToLine { return; }; editor.update(cx, |editor, cx| { - if let Some(placeholder_text) = editor.placeholder_text() { - if editor.text(cx).is_empty() { - let placeholder_text = placeholder_text.to_string(); - editor.set_text(placeholder_text, window, cx); - } + if let Some(placeholder_text) = editor.placeholder_text() + && editor.text(cx).is_empty() + { + let placeholder_text = placeholder_text.to_string(); + editor.set_text(placeholder_text, window, cx); } }); } diff --git a/crates/google_ai/src/google_ai.rs b/crates/google_ai/src/google_ai.rs index dfa51d024c..95a6daa1d9 100644 --- a/crates/google_ai/src/google_ai.rs +++ b/crates/google_ai/src/google_ai.rs @@ -106,10 +106,9 @@ pub fn validate_generate_content_request(request: &GenerateContentRequest) -> Re .contents .iter() .find(|content| content.role == Role::User) + && user_content.parts.is_empty() { - if user_content.parts.is_empty() { - bail!("User content must contain at least one part"); - } + bail!("User content must contain at least one part"); } Ok(()) diff --git a/crates/gpui/build.rs b/crates/gpui/build.rs index 3a80ee12a0..0040046f90 100644 --- a/crates/gpui/build.rs +++ b/crates/gpui/build.rs @@ -327,10 +327,10 @@ mod windows { /// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler. fn find_fxc_compiler() -> String { // Check environment variable - if let Ok(path) = std::env::var("GPUI_FXC_PATH") { - if Path::new(&path).exists() { - return path; - } + if let Ok(path) = std::env::var("GPUI_FXC_PATH") + && Path::new(&path).exists() + { + return path; } // Try to find in PATH @@ -338,11 +338,10 @@ mod windows { if let Ok(output) = std::process::Command::new("where.exe") .arg("fxc.exe") .output() + && output.status.success() { - if output.status.success() { - let path = String::from_utf8_lossy(&output.stdout); - return path.trim().to_string(); - } + let path = String::from_utf8_lossy(&output.stdout); + return path.trim().to_string(); } // Check the default path diff --git a/crates/gpui/examples/input.rs b/crates/gpui/examples/input.rs index 170df3cad7..ae635c94b8 100644 --- a/crates/gpui/examples/input.rs +++ b/crates/gpui/examples/input.rs @@ -549,10 +549,10 @@ impl Element for TextElement { line.paint(bounds.origin, window.line_height(), window, cx) .unwrap(); - if focus_handle.is_focused(window) { - if let Some(cursor) = prepaint.cursor.take() { - window.paint_quad(cursor); - } + if focus_handle.is_focused(window) + && let Some(cursor) = prepaint.cursor.take() + { + window.paint_quad(cursor); } self.input.update(cx, |input, _cx| { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index ed1b935c58..c4499aff07 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1516,12 +1516,11 @@ impl App { /// the bindings in the element tree, and any global action listeners. pub fn is_action_available(&mut self, action: &dyn Action) -> bool { let mut action_available = false; - if let Some(window) = self.active_window() { - if let Ok(window_action_available) = + if let Some(window) = self.active_window() + && let Ok(window_action_available) = window.update(self, |_, window, cx| window.is_action_available(action, cx)) - { - action_available = window_action_available; - } + { + action_available = window_action_available; } action_available @@ -1606,27 +1605,26 @@ impl App { .insert(action.as_any().type_id(), global_listeners); } - if self.propagate_event { - if let Some(mut global_listeners) = self + if self.propagate_event + && let Some(mut global_listeners) = self .global_action_listeners .remove(&action.as_any().type_id()) - { - for listener in global_listeners.iter().rev() { - listener(action.as_any(), DispatchPhase::Bubble, self); - if !self.propagate_event { - break; - } + { + for listener in global_listeners.iter().rev() { + listener(action.as_any(), DispatchPhase::Bubble, self); + if !self.propagate_event { + break; } - - global_listeners.extend( - self.global_action_listeners - .remove(&action.as_any().type_id()) - .unwrap_or_default(), - ); - - self.global_action_listeners - .insert(action.as_any().type_id(), global_listeners); } + + global_listeners.extend( + self.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + self.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); } } diff --git a/crates/gpui/src/app/context.rs b/crates/gpui/src/app/context.rs index 68c41592b3..a6ab026770 100644 --- a/crates/gpui/src/app/context.rs +++ b/crates/gpui/src/app/context.rs @@ -610,16 +610,16 @@ impl<'a, T: 'static> Context<'a, T> { let (subscription, activate) = window.new_focus_listener(Box::new(move |event, window, cx| { view.update(cx, |view, cx| { - if let Some(blurred_id) = event.previous_focus_path.last().copied() { - if event.is_focus_out(focus_id) { - let event = FocusOutEvent { - blurred: WeakFocusHandle { - id: blurred_id, - handles: Arc::downgrade(&cx.focus_handles), - }, - }; - listener(view, event, window, cx) - } + if let Some(blurred_id) = event.previous_focus_path.last().copied() + && event.is_focus_out(focus_id) + { + let event = FocusOutEvent { + blurred: WeakFocusHandle { + id: blurred_id, + handles: Arc::downgrade(&cx.focus_handles), + }, + }; + listener(view, event, window, cx) } }) .is_ok() diff --git a/crates/gpui/src/element.rs b/crates/gpui/src/element.rs index e5f49c7be1..f537bc5ac8 100644 --- a/crates/gpui/src/element.rs +++ b/crates/gpui/src/element.rs @@ -603,10 +603,8 @@ impl AnyElement { self.0.prepaint(window, cx); - if !focus_assigned { - if let Some(focus_id) = window.next_frame.focus { - return FocusHandle::for_id(focus_id, &cx.focus_handles); - } + if !focus_assigned && let Some(focus_id) = window.next_frame.focus { + return FocusHandle::for_id(focus_id, &cx.focus_handles); } None diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index f553bf55f6..7b689ca0ad 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -286,21 +286,20 @@ impl Interactivity { { self.mouse_move_listeners .push(Box::new(move |event, phase, hitbox, window, cx| { - if phase == DispatchPhase::Capture { - if let Some(drag) = &cx.active_drag { - if drag.value.as_ref().type_id() == TypeId::of::<T>() { - (listener)( - &DragMoveEvent { - event: event.clone(), - bounds: hitbox.bounds, - drag: PhantomData, - dragged_item: Arc::clone(&drag.value), - }, - window, - cx, - ); - } - } + if phase == DispatchPhase::Capture + && let Some(drag) = &cx.active_drag + && drag.value.as_ref().type_id() == TypeId::of::<T>() + { + (listener)( + &DragMoveEvent { + event: event.clone(), + bounds: hitbox.bounds, + drag: PhantomData, + dragged_item: Arc::clone(&drag.value), + }, + window, + cx, + ); } })); } @@ -1514,15 +1513,14 @@ impl Interactivity { let mut element_state = element_state.map(|element_state| element_state.unwrap_or_default()); - if let Some(element_state) = element_state.as_ref() { - if cx.has_active_drag() { - if let Some(pending_mouse_down) = element_state.pending_mouse_down.as_ref() - { - *pending_mouse_down.borrow_mut() = None; - } - if let Some(clicked_state) = element_state.clicked_state.as_ref() { - *clicked_state.borrow_mut() = ElementClickedState::default(); - } + if let Some(element_state) = element_state.as_ref() + && cx.has_active_drag() + { + if let Some(pending_mouse_down) = element_state.pending_mouse_down.as_ref() { + *pending_mouse_down.borrow_mut() = None; + } + if let Some(clicked_state) = element_state.clicked_state.as_ref() { + *clicked_state.borrow_mut() = ElementClickedState::default(); } } @@ -1530,35 +1528,35 @@ impl Interactivity { // If there's an explicit focus handle we're tracking, use that. Otherwise // create a new handle and store it in the element state, which lives for as // as frames contain an element with this id. - if self.focusable && self.tracked_focus_handle.is_none() { - if let Some(element_state) = element_state.as_mut() { - let mut handle = element_state - .focus_handle - .get_or_insert_with(|| cx.focus_handle()) - .clone() - .tab_stop(false); + if self.focusable + && self.tracked_focus_handle.is_none() + && let Some(element_state) = element_state.as_mut() + { + let mut handle = element_state + .focus_handle + .get_or_insert_with(|| cx.focus_handle()) + .clone() + .tab_stop(false); - if let Some(index) = self.tab_index { - handle = handle.tab_index(index).tab_stop(true); - } - - self.tracked_focus_handle = Some(handle); + if let Some(index) = self.tab_index { + handle = handle.tab_index(index).tab_stop(true); } + + self.tracked_focus_handle = Some(handle); } if let Some(scroll_handle) = self.tracked_scroll_handle.as_ref() { self.scroll_offset = Some(scroll_handle.0.borrow().offset.clone()); - } else if self.base_style.overflow.x == Some(Overflow::Scroll) - || self.base_style.overflow.y == Some(Overflow::Scroll) + } else if (self.base_style.overflow.x == Some(Overflow::Scroll) + || self.base_style.overflow.y == Some(Overflow::Scroll)) + && let Some(element_state) = element_state.as_mut() { - if let Some(element_state) = element_state.as_mut() { - self.scroll_offset = Some( - element_state - .scroll_offset - .get_or_insert_with(Rc::default) - .clone(), - ); - } + self.scroll_offset = Some( + element_state + .scroll_offset + .get_or_insert_with(Rc::default) + .clone(), + ); } let style = self.compute_style_internal(None, element_state.as_mut(), window, cx); @@ -2031,26 +2029,27 @@ impl Interactivity { let hitbox = hitbox.clone(); window.on_mouse_event({ move |_: &MouseUpEvent, phase, window, cx| { - if let Some(drag) = &cx.active_drag { - if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { - let drag_state_type = drag.value.as_ref().type_id(); - for (drop_state_type, listener) in &drop_listeners { - if *drop_state_type == drag_state_type { - let drag = cx - .active_drag - .take() - .expect("checked for type drag state type above"); + if let Some(drag) = &cx.active_drag + && phase == DispatchPhase::Bubble + && hitbox.is_hovered(window) + { + let drag_state_type = drag.value.as_ref().type_id(); + for (drop_state_type, listener) in &drop_listeners { + if *drop_state_type == drag_state_type { + let drag = cx + .active_drag + .take() + .expect("checked for type drag state type above"); - let mut can_drop = true; - if let Some(predicate) = &can_drop_predicate { - can_drop = predicate(drag.value.as_ref(), window, cx); - } + let mut can_drop = true; + if let Some(predicate) = &can_drop_predicate { + can_drop = predicate(drag.value.as_ref(), window, cx); + } - if can_drop { - listener(drag.value.as_ref(), window, cx); - window.refresh(); - cx.stop_propagation(); - } + if can_drop { + listener(drag.value.as_ref(), window, cx); + window.refresh(); + cx.stop_propagation(); } } } @@ -2094,31 +2093,24 @@ impl Interactivity { } let mut pending_mouse_down = pending_mouse_down.borrow_mut(); - if let Some(mouse_down) = pending_mouse_down.clone() { - if !cx.has_active_drag() - && (event.position - mouse_down.position).magnitude() - > DRAG_THRESHOLD - { - if let Some((drag_value, drag_listener)) = drag_listener.take() { - *clicked_state.borrow_mut() = ElementClickedState::default(); - let cursor_offset = event.position - hitbox.origin; - let drag = (drag_listener)( - drag_value.as_ref(), - cursor_offset, - window, - cx, - ); - cx.active_drag = Some(AnyDrag { - view: drag, - value: drag_value, - cursor_offset, - cursor_style: drag_cursor_style, - }); - pending_mouse_down.take(); - window.refresh(); - cx.stop_propagation(); - } - } + if let Some(mouse_down) = pending_mouse_down.clone() + && !cx.has_active_drag() + && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD + && let Some((drag_value, drag_listener)) = drag_listener.take() + { + *clicked_state.borrow_mut() = ElementClickedState::default(); + let cursor_offset = event.position - hitbox.origin; + let drag = + (drag_listener)(drag_value.as_ref(), cursor_offset, window, cx); + cx.active_drag = Some(AnyDrag { + view: drag, + value: drag_value, + cursor_offset, + cursor_style: drag_cursor_style, + }); + pending_mouse_down.take(); + window.refresh(); + cx.stop_propagation(); } } }); @@ -2428,33 +2420,32 @@ impl Interactivity { style.refine(&self.base_style); if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { - if let Some(in_focus_style) = self.in_focus_style.as_ref() { - if focus_handle.within_focused(window, cx) { - style.refine(in_focus_style); - } + if let Some(in_focus_style) = self.in_focus_style.as_ref() + && focus_handle.within_focused(window, cx) + { + style.refine(in_focus_style); } - if let Some(focus_style) = self.focus_style.as_ref() { - if focus_handle.is_focused(window) { - style.refine(focus_style); - } + if let Some(focus_style) = self.focus_style.as_ref() + && focus_handle.is_focused(window) + { + style.refine(focus_style); } } if let Some(hitbox) = hitbox { if !cx.has_active_drag() { - if let Some(group_hover) = self.group_hover_style.as_ref() { - if let Some(group_hitbox_id) = GroupHitboxes::get(&group_hover.group, cx) { - if group_hitbox_id.is_hovered(window) { - style.refine(&group_hover.style); - } - } + if let Some(group_hover) = self.group_hover_style.as_ref() + && let Some(group_hitbox_id) = GroupHitboxes::get(&group_hover.group, cx) + && group_hitbox_id.is_hovered(window) + { + style.refine(&group_hover.style); } - if let Some(hover_style) = self.hover_style.as_ref() { - if hitbox.is_hovered(window) { - style.refine(hover_style); - } + if let Some(hover_style) = self.hover_style.as_ref() + && hitbox.is_hovered(window) + { + style.refine(hover_style); } } @@ -2468,12 +2459,10 @@ impl Interactivity { for (state_type, group_drag_style) in &self.group_drag_over_styles { if let Some(group_hitbox_id) = GroupHitboxes::get(&group_drag_style.group, cx) + && *state_type == drag.value.as_ref().type_id() + && group_hitbox_id.is_hovered(window) { - if *state_type == drag.value.as_ref().type_id() - && group_hitbox_id.is_hovered(window) - { - style.refine(&group_drag_style.style); - } + style.refine(&group_drag_style.style); } } @@ -2495,16 +2484,16 @@ impl Interactivity { .clicked_state .get_or_insert_with(Default::default) .borrow(); - if clicked_state.group { - if let Some(group) = self.group_active_style.as_ref() { - style.refine(&group.style) - } + if clicked_state.group + && let Some(group) = self.group_active_style.as_ref() + { + style.refine(&group.style) } - if let Some(active_style) = self.active_style.as_ref() { - if clicked_state.element { - style.refine(active_style) - } + if let Some(active_style) = self.active_style.as_ref() + && clicked_state.element + { + style.refine(active_style) } } diff --git a/crates/gpui/src/elements/image_cache.rs b/crates/gpui/src/elements/image_cache.rs index e7bdeaf9eb..263f0aafc2 100644 --- a/crates/gpui/src/elements/image_cache.rs +++ b/crates/gpui/src/elements/image_cache.rs @@ -297,10 +297,10 @@ impl RetainAllImageCache { /// Remove the image from the cache by the given source. pub fn remove(&mut self, source: &Resource, window: &mut Window, cx: &mut App) { let hash = hash(source); - if let Some(mut item) = self.0.remove(&hash) { - if let Some(Ok(image)) = item.get() { - cx.drop_image(image, Some(window)); - } + if let Some(mut item) = self.0.remove(&hash) + && let Some(Ok(image)) = item.get() + { + cx.drop_image(image, Some(window)); } } diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index 993b319b69..ae63819ca2 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -379,13 +379,12 @@ impl Element for Img { None => { if let Some(state) = &mut state { if let Some((started_loading, _)) = state.started_loading { - if started_loading.elapsed() > LOADING_DELAY { - if let Some(loading) = self.style.loading.as_ref() { - let mut element = loading(); - replacement_id = - Some(element.request_layout(window, cx)); - layout_state.replacement = Some(element); - } + if started_loading.elapsed() > LOADING_DELAY + && let Some(loading) = self.style.loading.as_ref() + { + let mut element = loading(); + replacement_id = Some(element.request_layout(window, cx)); + layout_state.replacement = Some(element); } } else { let current_view = window.current_view(); diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 39f38bdc69..98b63ef907 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -732,46 +732,44 @@ impl StateInner { item.element.prepaint_at(item_origin, window, cx); }); - if let Some(autoscroll_bounds) = window.take_autoscroll() { - if autoscroll { - if autoscroll_bounds.top() < bounds.top() { - return Err(ListOffset { - item_ix: item.index, - offset_in_item: autoscroll_bounds.top() - item_origin.y, - }); - } else if autoscroll_bounds.bottom() > bounds.bottom() { - let mut cursor = self.items.cursor::<Count>(&()); - cursor.seek(&Count(item.index), Bias::Right); - let mut height = bounds.size.height - padding.top - padding.bottom; - - // Account for the height of the element down until the autoscroll bottom. - height -= autoscroll_bounds.bottom() - item_origin.y; - - // Keep decreasing the scroll top until we fill all the available space. - while height > Pixels::ZERO { - cursor.prev(); - let Some(item) = cursor.item() else { break }; - - let size = item.size().unwrap_or_else(|| { - let mut item = render_item(cursor.start().0, window, cx); - let item_available_size = size( - bounds.size.width.into(), - AvailableSpace::MinContent, - ); - item.layout_as_root(item_available_size, window, cx) - }); - height -= size.height; - } - - return Err(ListOffset { - item_ix: cursor.start().0, - offset_in_item: if height < Pixels::ZERO { - -height - } else { - Pixels::ZERO - }, + if let Some(autoscroll_bounds) = window.take_autoscroll() + && autoscroll + { + if autoscroll_bounds.top() < bounds.top() { + return Err(ListOffset { + item_ix: item.index, + offset_in_item: autoscroll_bounds.top() - item_origin.y, + }); + } else if autoscroll_bounds.bottom() > bounds.bottom() { + let mut cursor = self.items.cursor::<Count>(&()); + cursor.seek(&Count(item.index), Bias::Right); + let mut height = bounds.size.height - padding.top - padding.bottom; + + // Account for the height of the element down until the autoscroll bottom. + height -= autoscroll_bounds.bottom() - item_origin.y; + + // Keep decreasing the scroll top until we fill all the available space. + while height > Pixels::ZERO { + cursor.prev(); + let Some(item) = cursor.item() else { break }; + + let size = item.size().unwrap_or_else(|| { + let mut item = render_item(cursor.start().0, window, cx); + let item_available_size = + size(bounds.size.width.into(), AvailableSpace::MinContent); + item.layout_as_root(item_available_size, window, cx) }); + height -= size.height; } + + return Err(ListOffset { + item_ix: cursor.start().0, + offset_in_item: if height < Pixels::ZERO { + -height + } else { + Pixels::ZERO + }, + }); } } diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 014f617e2c..c58f72267c 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -356,12 +356,11 @@ impl TextLayout { (None, "".into()) }; - if let Some(text_layout) = element_state.0.borrow().as_ref() { - if text_layout.size.is_some() - && (wrap_width.is_none() || wrap_width == text_layout.wrap_width) - { - return text_layout.size.unwrap(); - } + if let Some(text_layout) = element_state.0.borrow().as_ref() + && text_layout.size.is_some() + && (wrap_width.is_none() || wrap_width == text_layout.wrap_width) + { + return text_layout.size.unwrap(); } let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size); @@ -763,14 +762,13 @@ impl Element for InteractiveText { let mut interactive_state = interactive_state.unwrap_or_default(); if let Some(click_listener) = self.click_listener.take() { let mouse_position = window.mouse_position(); - if let Ok(ix) = text_layout.index_for_position(mouse_position) { - if self + if let Ok(ix) = text_layout.index_for_position(mouse_position) + && self .clickable_ranges .iter() .any(|range| range.contains(&ix)) - { - window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox) - } + { + window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox) } let text_layout = text_layout.clone(); @@ -803,13 +801,13 @@ impl Element for InteractiveText { } else { let hitbox = hitbox.clone(); window.on_mouse_event(move |event: &MouseDownEvent, phase, window, _| { - if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { - if let Ok(mouse_down_index) = + if phase == DispatchPhase::Bubble + && hitbox.is_hovered(window) + && let Ok(mouse_down_index) = text_layout.index_for_position(event.position) - { - mouse_down.set(Some(mouse_down_index)); - window.refresh(); - } + { + mouse_down.set(Some(mouse_down_index)); + window.refresh(); } }); } diff --git a/crates/gpui/src/keymap/binding.rs b/crates/gpui/src/keymap/binding.rs index 1d3f612c5b..6d36cbb4e0 100644 --- a/crates/gpui/src/keymap/binding.rs +++ b/crates/gpui/src/keymap/binding.rs @@ -53,10 +53,10 @@ impl KeyBinding { if let Some(equivalents) = key_equivalents { for keystroke in keystrokes.iter_mut() { - if keystroke.key.chars().count() == 1 { - if let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap()) { - keystroke.key = key.to_string(); - } + if keystroke.key.chars().count() == 1 + && let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap()) + { + keystroke.key = key.to_string(); } } } diff --git a/crates/gpui/src/platform/blade/blade_renderer.rs b/crates/gpui/src/platform/blade/blade_renderer.rs index 46d3c16c72..cc1df7748b 100644 --- a/crates/gpui/src/platform/blade/blade_renderer.rs +++ b/crates/gpui/src/platform/blade/blade_renderer.rs @@ -434,24 +434,24 @@ impl BladeRenderer { } fn wait_for_gpu(&mut self) { - if let Some(last_sp) = self.last_sync_point.take() { - if !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS) { - log::error!("GPU hung"); - #[cfg(target_os = "linux")] - if self.gpu.device_information().driver_name == "radv" { - log::error!( - "there's a known bug with amdgpu/radv, try setting ZED_PATH_SAMPLE_COUNT=0 as a workaround" - ); - log::error!( - "if that helps you're running into https://github.com/zed-industries/zed/issues/26143" - ); - } + if let Some(last_sp) = self.last_sync_point.take() + && !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS) + { + log::error!("GPU hung"); + #[cfg(target_os = "linux")] + if self.gpu.device_information().driver_name == "radv" { log::error!( - "your device information is: {:?}", - self.gpu.device_information() + "there's a known bug with amdgpu/radv, try setting ZED_PATH_SAMPLE_COUNT=0 as a workaround" + ); + log::error!( + "if that helps you're running into https://github.com/zed-industries/zed/issues/26143" ); - while !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS) {} } + log::error!( + "your device information is: {:?}", + self.gpu.device_information() + ); + while !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS) {} } } diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 0ab61fbf0c..d1aa590192 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -359,13 +359,13 @@ impl WaylandClientStatePtr { } changed }; - if changed { - if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() { - drop(state); - callback(); - state = client.borrow_mut(); - state.common.callbacks.keyboard_layout_change = Some(callback); - } + + if changed && let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() + { + drop(state); + callback(); + state = client.borrow_mut(); + state.common.callbacks.keyboard_layout_change = Some(callback); } } @@ -373,15 +373,15 @@ impl WaylandClientStatePtr { let mut client = self.get_client(); let mut state = client.borrow_mut(); let closed_window = state.windows.remove(surface_id).unwrap(); - if let Some(window) = state.mouse_focused_window.take() { - if !window.ptr_eq(&closed_window) { - state.mouse_focused_window = Some(window); - } + if let Some(window) = state.mouse_focused_window.take() + && !window.ptr_eq(&closed_window) + { + state.mouse_focused_window = Some(window); } - if let Some(window) = state.keyboard_focused_window.take() { - if !window.ptr_eq(&closed_window) { - state.keyboard_focused_window = Some(window); - } + if let Some(window) = state.keyboard_focused_window.take() + && !window.ptr_eq(&closed_window) + { + state.keyboard_focused_window = Some(window); } if state.windows.is_empty() { state.common.signal.stop(); @@ -1784,17 +1784,17 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr { drop(state); window.handle_input(input); } - } else if let Some(discrete) = discrete { - if let Some(window) = state.mouse_focused_window.clone() { - let input = PlatformInput::ScrollWheel(ScrollWheelEvent { - position: state.mouse_location.unwrap(), - delta: ScrollDelta::Lines(discrete), - modifiers: state.modifiers, - touch_phase: TouchPhase::Moved, - }); - drop(state); - window.handle_input(input); - } + } else if let Some(discrete) = discrete + && let Some(window) = state.mouse_focused_window.clone() + { + let input = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: state.mouse_location.unwrap(), + delta: ScrollDelta::Lines(discrete), + modifiers: state.modifiers, + touch_phase: TouchPhase::Moved, + }); + drop(state); + window.handle_input(input); } } } diff --git a/crates/gpui/src/platform/linux/wayland/cursor.rs b/crates/gpui/src/platform/linux/wayland/cursor.rs index bfbedf234d..a21263ccfe 100644 --- a/crates/gpui/src/platform/linux/wayland/cursor.rs +++ b/crates/gpui/src/platform/linux/wayland/cursor.rs @@ -45,10 +45,11 @@ impl Cursor { } fn set_theme_internal(&mut self, theme_name: Option<String>) { - if let Some(loaded_theme) = self.loaded_theme.as_ref() { - if loaded_theme.name == theme_name && loaded_theme.scaled_size == self.scaled_size { - return; - } + if let Some(loaded_theme) = self.loaded_theme.as_ref() + && loaded_theme.name == theme_name + && loaded_theme.scaled_size == self.scaled_size + { + return; } let result = if let Some(theme_name) = theme_name.as_ref() { CursorTheme::load_from_name( diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 2b2207e22c..7cf2d02d3b 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -713,21 +713,20 @@ impl WaylandWindowStatePtr { } pub fn handle_input(&self, input: PlatformInput) { - if let Some(ref mut fun) = self.callbacks.borrow_mut().input { - if !fun(input.clone()).propagate { - return; - } + if let Some(ref mut fun) = self.callbacks.borrow_mut().input + && !fun(input.clone()).propagate + { + return; } - if let PlatformInput::KeyDown(event) = input { - if event.keystroke.modifiers.is_subset_of(&Modifiers::shift()) { - if let Some(key_char) = &event.keystroke.key_char { - let mut state = self.state.borrow_mut(); - if let Some(mut input_handler) = state.input_handler.take() { - drop(state); - input_handler.replace_text_in_range(None, key_char); - self.state.borrow_mut().input_handler = Some(input_handler); - } - } + if let PlatformInput::KeyDown(event) = input + && event.keystroke.modifiers.is_subset_of(&Modifiers::shift()) + && let Some(key_char) = &event.keystroke.key_char + { + let mut state = self.state.borrow_mut(); + if let Some(mut input_handler) = state.input_handler.take() { + drop(state); + input_handler.replace_text_in_range(None, key_char); + self.state.borrow_mut().input_handler = Some(input_handler); } } } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index dd0cea3290..b4914c9dd2 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -565,10 +565,10 @@ impl X11Client { events.push(last_keymap_change_event); } - if let Some(last_press) = last_key_press.as_ref() { - if last_press.detail == key_press.detail { - continue; - } + if let Some(last_press) = last_key_press.as_ref() + && last_press.detail == key_press.detail + { + continue; } if let Some(Event::KeyRelease(key_release)) = @@ -2035,12 +2035,11 @@ fn xdnd_get_supported_atom( ), ) .log_with_level(Level::Warn) + && let Some(atoms) = reply.value32() { - if let Some(atoms) = reply.value32() { - for atom in atoms { - if xdnd_is_atom_supported(atom, supported_atoms) { - return atom; - } + for atom in atoms { + if xdnd_is_atom_supported(atom, supported_atoms) { + return atom; } } } @@ -2411,11 +2410,13 @@ fn legacy_get_randr_scale_factor(connection: &XCBConnection, root: u32) -> Optio let mut crtc_infos: HashMap<randr::Crtc, randr::GetCrtcInfoReply> = HashMap::default(); let mut valid_outputs: HashSet<randr::Output> = HashSet::new(); for (crtc, cookie) in crtc_cookies { - if let Ok(reply) = cookie.reply() { - if reply.width > 0 && reply.height > 0 && !reply.outputs.is_empty() { - crtc_infos.insert(crtc, reply.clone()); - valid_outputs.extend(&reply.outputs); - } + if let Ok(reply) = cookie.reply() + && reply.width > 0 + && reply.height > 0 + && !reply.outputs.is_empty() + { + crtc_infos.insert(crtc, reply.clone()); + valid_outputs.extend(&reply.outputs); } } diff --git a/crates/gpui/src/platform/linux/x11/clipboard.rs b/crates/gpui/src/platform/linux/x11/clipboard.rs index 5d42eadaaf..5b32f2c93e 100644 --- a/crates/gpui/src/platform/linux/x11/clipboard.rs +++ b/crates/gpui/src/platform/linux/x11/clipboard.rs @@ -1120,25 +1120,25 @@ impl Drop for Clipboard { log::error!("Failed to flush the clipboard window. Error: {}", e); return; } - if let Some(global_cb) = global_cb { - if let Err(e) = global_cb.server_handle.join() { - // Let's try extracting the error message - let message; - if let Some(msg) = e.downcast_ref::<&'static str>() { - message = Some((*msg).to_string()); - } else if let Some(msg) = e.downcast_ref::<String>() { - message = Some(msg.clone()); - } else { - message = None; - } - if let Some(message) = message { - log::error!( - "The clipboard server thread panicked. Panic message: '{}'", - message, - ); - } else { - log::error!("The clipboard server thread panicked."); - } + if let Some(global_cb) = global_cb + && let Err(e) = global_cb.server_handle.join() + { + // Let's try extracting the error message + let message; + if let Some(msg) = e.downcast_ref::<&'static str>() { + message = Some((*msg).to_string()); + } else if let Some(msg) = e.downcast_ref::<String>() { + message = Some(msg.clone()); + } else { + message = None; + } + if let Some(message) = message { + log::error!( + "The clipboard server thread panicked. Panic message: '{}'", + message, + ); + } else { + log::error!("The clipboard server thread panicked."); } } } diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 2bf58d6184..c33d6fa462 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -515,19 +515,19 @@ impl X11WindowState { xcb.configure_window(x_window, &xproto::ConfigureWindowAux::new().x(x).y(y)), )?; } - if let Some(titlebar) = params.titlebar { - if let Some(title) = titlebar.title { - check_reply( - || "X11 ChangeProperty8 on window title failed.", - xcb.change_property8( - xproto::PropMode::REPLACE, - x_window, - xproto::AtomEnum::WM_NAME, - xproto::AtomEnum::STRING, - title.as_bytes(), - ), - )?; - } + if let Some(titlebar) = params.titlebar + && let Some(title) = titlebar.title + { + check_reply( + || "X11 ChangeProperty8 on window title failed.", + xcb.change_property8( + xproto::PropMode::REPLACE, + x_window, + xproto::AtomEnum::WM_NAME, + xproto::AtomEnum::STRING, + title.as_bytes(), + ), + )?; } if params.kind == WindowKind::PopUp { check_reply( @@ -956,10 +956,10 @@ impl X11WindowStatePtr { } pub fn handle_input(&self, input: PlatformInput) { - if let Some(ref mut fun) = self.callbacks.borrow_mut().input { - if !fun(input.clone()).propagate { - return; - } + if let Some(ref mut fun) = self.callbacks.borrow_mut().input + && !fun(input.clone()).propagate + { + return; } if let PlatformInput::KeyDown(event) = input { // only allow shift modifier when inserting text @@ -1068,15 +1068,14 @@ impl X11WindowStatePtr { } let mut callbacks = self.callbacks.borrow_mut(); - if let Some((content_size, scale_factor)) = resize_args { - if let Some(ref mut fun) = callbacks.resize { - fun(content_size, scale_factor) - } + if let Some((content_size, scale_factor)) = resize_args + && let Some(ref mut fun) = callbacks.resize + { + fun(content_size, scale_factor) } - if !is_resize { - if let Some(ref mut fun) = callbacks.moved { - fun(); - } + + if !is_resize && let Some(ref mut fun) = callbacks.moved { + fun(); } Ok(()) diff --git a/crates/gpui/src/platform/mac/open_type.rs b/crates/gpui/src/platform/mac/open_type.rs index 2ae5e8f87a..37a29559fd 100644 --- a/crates/gpui/src/platform/mac/open_type.rs +++ b/crates/gpui/src/platform/mac/open_type.rs @@ -35,14 +35,14 @@ pub fn apply_features_and_fallbacks( unsafe { let mut keys = vec![kCTFontFeatureSettingsAttribute]; let mut values = vec![generate_feature_array(features)]; - if let Some(fallbacks) = fallbacks { - if !fallbacks.fallback_list().is_empty() { - keys.push(kCTFontCascadeListAttribute); - values.push(generate_fallback_array( - fallbacks, - font.native_font().as_concrete_TypeRef(), - )); - } + if let Some(fallbacks) = fallbacks + && !fallbacks.fallback_list().is_empty() + { + keys.push(kCTFontCascadeListAttribute); + values.push(generate_fallback_array( + fallbacks, + font.native_font().as_concrete_TypeRef(), + )); } let attrs = CFDictionaryCreate( kCFAllocatorDefault, diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index f094ed9f30..57dfa9c603 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -715,10 +715,10 @@ impl Platform for MacPlatform { let urls = panel.URLs(); for i in 0..urls.count() { let url = urls.objectAtIndex(i); - if url.isFileURL() == YES { - if let Ok(path) = ns_url_to_path(url) { - result.push(path) - } + if url.isFileURL() == YES + && let Ok(path) = ns_url_to_path(url) + { + result.push(path) } } Some(result) @@ -786,15 +786,16 @@ impl Platform for MacPlatform { // This is conditional on OS version because I'd like to get rid of it, so that // you can manually create a file called `a.sql.s`. That said it seems better // to break that use-case than breaking `a.sql`. - if chunks.len() == 3 && chunks[1].starts_with(chunks[2]) { - if Self::os_version() >= SemanticVersion::new(15, 0, 0) { - let new_filename = OsStr::from_bytes( - &filename.as_bytes() - [..chunks[0].len() + 1 + chunks[1].len()], - ) - .to_owned(); - result.set_file_name(&new_filename); - } + if chunks.len() == 3 + && chunks[1].starts_with(chunks[2]) + && Self::os_version() >= SemanticVersion::new(15, 0, 0) + { + let new_filename = OsStr::from_bytes( + &filename.as_bytes() + [..chunks[0].len() + 1 + chunks[1].len()], + ) + .to_owned(); + result.set_file_name(&new_filename); } return result; }) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 40a03b6c4a..b6f684a72c 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1478,18 +1478,18 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: return YES; } - if key_down_event.is_held { - if let Some(key_char) = key_down_event.keystroke.key_char.as_ref() { - let handled = with_input_handler(this, |input_handler| { - if !input_handler.apple_press_and_hold_enabled() { - input_handler.replace_text_in_range(None, key_char); - return YES; - } - NO - }); - if handled == Some(YES) { + if key_down_event.is_held + && let Some(key_char) = key_down_event.keystroke.key_char.as_ref() + { + let handled = with_input_handler(this, |input_handler| { + if !input_handler.apple_press_and_hold_enabled() { + input_handler.replace_text_in_range(None, key_char); return YES; } + NO + }); + if handled == Some(YES) { + return YES; } } @@ -1624,10 +1624,10 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { modifiers: prev_modifiers, capslock: prev_capslock, })) = &lock.previous_modifiers_changed_event + && prev_modifiers == modifiers + && prev_capslock == capslock { - if prev_modifiers == modifiers && prev_capslock == capslock { - return; - } + return; } lock.previous_modifiers_changed_event = Some(event.clone()); @@ -1995,10 +1995,10 @@ extern "C" fn attributed_substring_for_proposed_range( let mut adjusted: Option<Range<usize>> = None; let selected_text = input_handler.text_for_range(range.clone(), &mut adjusted)?; - if let Some(adjusted) = adjusted { - if adjusted != range { - unsafe { (actual_range as *mut NSRange).write(NSRange::from(adjusted)) }; - } + if let Some(adjusted) = adjusted + && adjusted != range + { + unsafe { (actual_range as *mut NSRange).write(NSRange::from(adjusted)) }; } unsafe { let string: id = msg_send![class!(NSAttributedString), alloc]; @@ -2073,11 +2073,10 @@ extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDr let paths = external_paths_from_event(dragging_info); if let Some(event) = paths.map(|paths| PlatformInput::FileDrop(FileDropEvent::Entered { position, paths })) + && send_new_event(&window_state, event) { - if send_new_event(&window_state, event) { - window_state.lock().external_files_dragged = true; - return NSDragOperationCopy; - } + window_state.lock().external_files_dragged = true; + return NSDragOperationCopy; } NSDragOperationNone } diff --git a/crates/gpui/src/platform/test/dispatcher.rs b/crates/gpui/src/platform/test/dispatcher.rs index 16edabfa4b..bdc7834931 100644 --- a/crates/gpui/src/platform/test/dispatcher.rs +++ b/crates/gpui/src/platform/test/dispatcher.rs @@ -78,11 +78,11 @@ impl TestDispatcher { let state = self.state.lock(); let next_due_time = state.delayed.first().map(|(time, _)| *time); drop(state); - if let Some(due_time) = next_due_time { - if due_time <= new_now { - self.state.lock().time = due_time; - continue; - } + if let Some(due_time) = next_due_time + && due_time <= new_now + { + self.state.lock().time = due_time; + continue; } break; } diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index 69371bc8c4..2b4914baed 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -201,10 +201,10 @@ impl TestPlatform { executor .spawn(async move { if let Some(previous_window) = previous_window { - if let Some(window) = window.as_ref() { - if Rc::ptr_eq(&previous_window.0, &window.0) { - return; - } + if let Some(window) = window.as_ref() + && Rc::ptr_eq(&previous_window.0, &window.0) + { + return; } previous_window.simulate_active_status_change(false); } diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 9b25ab360e..c3bb8bb22b 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -701,29 +701,28 @@ impl WindowsWindowInner { // Fix auto hide taskbar not showing. This solution is based on the approach // used by Chrome. However, it may result in one row of pixels being obscured // in our client area. But as Chrome says, "there seems to be no better solution." - if is_maximized { - if let Some(ref taskbar_position) = self + if is_maximized + && let Some(ref taskbar_position) = self .state .borrow() .system_settings .auto_hide_taskbar_position - { - // Fot the auto-hide taskbar, adjust in by 1 pixel on taskbar edge, - // so the window isn't treated as a "fullscreen app", which would cause - // the taskbar to disappear. - match taskbar_position { - AutoHideTaskbarPosition::Left => { - requested_client_rect[0].left += AUTO_HIDE_TASKBAR_THICKNESS_PX - } - AutoHideTaskbarPosition::Top => { - requested_client_rect[0].top += AUTO_HIDE_TASKBAR_THICKNESS_PX - } - AutoHideTaskbarPosition::Right => { - requested_client_rect[0].right -= AUTO_HIDE_TASKBAR_THICKNESS_PX - } - AutoHideTaskbarPosition::Bottom => { - requested_client_rect[0].bottom -= AUTO_HIDE_TASKBAR_THICKNESS_PX - } + { + // Fot the auto-hide taskbar, adjust in by 1 pixel on taskbar edge, + // so the window isn't treated as a "fullscreen app", which would cause + // the taskbar to disappear. + match taskbar_position { + AutoHideTaskbarPosition::Left => { + requested_client_rect[0].left += AUTO_HIDE_TASKBAR_THICKNESS_PX + } + AutoHideTaskbarPosition::Top => { + requested_client_rect[0].top += AUTO_HIDE_TASKBAR_THICKNESS_PX + } + AutoHideTaskbarPosition::Right => { + requested_client_rect[0].right -= AUTO_HIDE_TASKBAR_THICKNESS_PX + } + AutoHideTaskbarPosition::Bottom => { + requested_client_rect[0].bottom -= AUTO_HIDE_TASKBAR_THICKNESS_PX } } } @@ -1125,28 +1124,26 @@ impl WindowsWindowInner { // lParam is a pointer to a string that indicates the area containing the system parameter // that was changed. let parameter = PCWSTR::from_raw(lparam.0 as _); - if unsafe { !parameter.is_null() && !parameter.is_empty() } { - if let Some(parameter_string) = unsafe { parameter.to_string() }.log_err() { - log::info!("System settings changed: {}", parameter_string); - match parameter_string.as_str() { - "ImmersiveColorSet" => { - let new_appearance = system_appearance() - .context( - "unable to get system appearance when handling ImmersiveColorSet", - ) - .log_err()?; - let mut lock = self.state.borrow_mut(); - if new_appearance != lock.appearance { - lock.appearance = new_appearance; - let mut callback = lock.callbacks.appearance_changed.take()?; - drop(lock); - callback(); - self.state.borrow_mut().callbacks.appearance_changed = Some(callback); - configure_dwm_dark_mode(handle, new_appearance); - } + if unsafe { !parameter.is_null() && !parameter.is_empty() } + && let Some(parameter_string) = unsafe { parameter.to_string() }.log_err() + { + log::info!("System settings changed: {}", parameter_string); + match parameter_string.as_str() { + "ImmersiveColorSet" => { + let new_appearance = system_appearance() + .context("unable to get system appearance when handling ImmersiveColorSet") + .log_err()?; + let mut lock = self.state.borrow_mut(); + if new_appearance != lock.appearance { + lock.appearance = new_appearance; + let mut callback = lock.callbacks.appearance_changed.take()?; + drop(lock); + callback(); + self.state.borrow_mut().callbacks.appearance_changed = Some(callback); + configure_dwm_dark_mode(handle, new_appearance); } - _ => {} } + _ => {} } } Some(0) diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 856187fa57..b13b9915f1 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -821,14 +821,14 @@ fn file_save_dialog( window: Option<HWND>, ) -> Result<Option<PathBuf>> { let dialog: IFileSaveDialog = unsafe { CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)? }; - if !directory.to_string_lossy().is_empty() { - if let Some(full_path) = directory.canonicalize().log_err() { - let full_path = SanitizedPath::from(full_path); - let full_path_string = full_path.to_string(); - let path_item: IShellItem = - unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_string), None)? }; - unsafe { dialog.SetFolder(&path_item).log_err() }; - } + if !directory.to_string_lossy().is_empty() + && let Some(full_path) = directory.canonicalize().log_err() + { + let full_path = SanitizedPath::from(full_path); + let full_path_string = full_path.to_string(); + let path_item: IShellItem = + unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_string), None)? }; + unsafe { dialog.SetFolder(&path_item).log_err() }; } if let Some(suggested_name) = suggested_name { diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index b48c3a2935..29af900b66 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -366,15 +366,14 @@ impl WindowTextSystem { let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); for run in runs { - if let Some(last_run) = decoration_runs.last_mut() { - if last_run.color == run.color - && last_run.underline == run.underline - && last_run.strikethrough == run.strikethrough - && last_run.background_color == run.background_color - { - last_run.len += run.len as u32; - continue; - } + if let Some(last_run) = decoration_runs.last_mut() + && last_run.color == run.color + && last_run.underline == run.underline + && last_run.strikethrough == run.strikethrough + && last_run.background_color == run.background_color + { + last_run.len += run.len as u32; + continue; } decoration_runs.push(DecorationRun { len: run.len as u32, @@ -492,14 +491,14 @@ impl WindowTextSystem { let mut split_lines = text.split('\n'); let mut processed = false; - if let Some(first_line) = split_lines.next() { - if let Some(second_line) = split_lines.next() { - processed = true; - process_line(first_line.to_string().into()); - process_line(second_line.to_string().into()); - for line_text in split_lines { - process_line(line_text.to_string().into()); - } + if let Some(first_line) = split_lines.next() + && let Some(second_line) = split_lines.next() + { + processed = true; + process_line(first_line.to_string().into()); + process_line(second_line.to_string().into()); + for line_text in split_lines { + process_line(line_text.to_string().into()); } } @@ -534,11 +533,11 @@ impl WindowTextSystem { let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); for run in runs.iter() { let font_id = self.resolve_font(&run.font); - if let Some(last_run) = font_runs.last_mut() { - if last_run.font_id == font_id { - last_run.len += run.len; - continue; - } + if let Some(last_run) = font_runs.last_mut() + && last_run.font_id == font_id + { + last_run.len += run.len; + continue; } font_runs.push(FontRun { len: run.len, diff --git a/crates/gpui/src/text_system/line.rs b/crates/gpui/src/text_system/line.rs index 3813393d81..8d559f9815 100644 --- a/crates/gpui/src/text_system/line.rs +++ b/crates/gpui/src/text_system/line.rs @@ -292,10 +292,10 @@ fn paint_line( } if let Some(style_run) = style_run { - if let Some((_, underline_style)) = &mut current_underline { - if style_run.underline.as_ref() != Some(underline_style) { - finished_underline = current_underline.take(); - } + if let Some((_, underline_style)) = &mut current_underline + && style_run.underline.as_ref() != Some(underline_style) + { + finished_underline = current_underline.take(); } if let Some(run_underline) = style_run.underline.as_ref() { current_underline.get_or_insert(( @@ -310,10 +310,10 @@ fn paint_line( }, )); } - if let Some((_, strikethrough_style)) = &mut current_strikethrough { - if style_run.strikethrough.as_ref() != Some(strikethrough_style) { - finished_strikethrough = current_strikethrough.take(); - } + if let Some((_, strikethrough_style)) = &mut current_strikethrough + && style_run.strikethrough.as_ref() != Some(strikethrough_style) + { + finished_strikethrough = current_strikethrough.take(); } if let Some(run_strikethrough) = style_run.strikethrough.as_ref() { current_strikethrough.get_or_insert(( @@ -509,10 +509,10 @@ fn paint_line_background( } if let Some(style_run) = style_run { - if let Some((_, background_color)) = &mut current_background { - if style_run.background_color.as_ref() != Some(background_color) { - finished_background = current_background.take(); - } + if let Some((_, background_color)) = &mut current_background + && style_run.background_color.as_ref() != Some(background_color) + { + finished_background = current_background.take(); } if let Some(run_background) = style_run.background_color { current_background.get_or_insert(( diff --git a/crates/gpui/src/text_system/line_layout.rs b/crates/gpui/src/text_system/line_layout.rs index 9c2dd7f087..43694702a8 100644 --- a/crates/gpui/src/text_system/line_layout.rs +++ b/crates/gpui/src/text_system/line_layout.rs @@ -185,10 +185,10 @@ impl LineLayout { if width > wrap_width && boundary > last_boundary { // When used line_clamp, we should limit the number of lines. - if let Some(max_lines) = max_lines { - if boundaries.len() >= max_lines - 1 { - break; - } + if let Some(max_lines) = max_lines + && boundaries.len() >= max_lines - 1 + { + break; } if let Some(last_candidate_ix) = last_candidate_ix.take() { diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index f461e2f7d0..217971792e 100644 --- a/crates/gpui/src/view.rs +++ b/crates/gpui/src/view.rs @@ -205,22 +205,21 @@ impl Element for AnyView { let content_mask = window.content_mask(); let text_style = window.text_style(); - if let Some(mut element_state) = element_state { - if element_state.cache_key.bounds == bounds - && element_state.cache_key.content_mask == content_mask - && element_state.cache_key.text_style == text_style - && !window.dirty_views.contains(&self.entity_id()) - && !window.refreshing - { - let prepaint_start = window.prepaint_index(); - window.reuse_prepaint(element_state.prepaint_range.clone()); - cx.entities - .extend_accessed(&element_state.accessed_entities); - let prepaint_end = window.prepaint_index(); - element_state.prepaint_range = prepaint_start..prepaint_end; + if let Some(mut element_state) = element_state + && element_state.cache_key.bounds == bounds + && element_state.cache_key.content_mask == content_mask + && element_state.cache_key.text_style == text_style + && !window.dirty_views.contains(&self.entity_id()) + && !window.refreshing + { + let prepaint_start = window.prepaint_index(); + window.reuse_prepaint(element_state.prepaint_range.clone()); + cx.entities + .extend_accessed(&element_state.accessed_entities); + let prepaint_end = window.prepaint_index(); + element_state.prepaint_range = prepaint_start..prepaint_end; - return (None, element_state); - } + return (None, element_state); } let refreshing = mem::replace(&mut window.refreshing, true); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index c0ffd34a0d..62aeb0df11 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -3401,16 +3401,16 @@ impl Window { let focus_id = handle.id; let (subscription, activate) = self.new_focus_listener(Box::new(move |event, window, cx| { - if let Some(blurred_id) = event.previous_focus_path.last().copied() { - if event.is_focus_out(focus_id) { - let event = FocusOutEvent { - blurred: WeakFocusHandle { - id: blurred_id, - handles: Arc::downgrade(&cx.focus_handles), - }, - }; - listener(event, window, cx) - } + if let Some(blurred_id) = event.previous_focus_path.last().copied() + && event.is_focus_out(focus_id) + { + let event = FocusOutEvent { + blurred: WeakFocusHandle { + id: blurred_id, + handles: Arc::downgrade(&cx.focus_handles), + }, + }; + listener(event, window, cx) } true })); @@ -3444,12 +3444,12 @@ impl Window { return true; } - if let Some(input) = keystroke.key_char { - if let Some(mut input_handler) = self.platform_window.take_input_handler() { - input_handler.dispatch_input(&input, self, cx); - self.platform_window.set_input_handler(input_handler); - return true; - } + if let Some(input) = keystroke.key_char + && let Some(mut input_handler) = self.platform_window.take_input_handler() + { + input_handler.dispatch_input(&input, self, cx); + self.platform_window.set_input_handler(input_handler); + return true; } false @@ -3864,11 +3864,11 @@ impl Window { if !cx.propagate_event { continue 'replay; } - if let Some(input) = replay.keystroke.key_char.as_ref().cloned() { - if let Some(mut input_handler) = self.platform_window.take_input_handler() { - input_handler.dispatch_input(&input, self, cx); - self.platform_window.set_input_handler(input_handler) - } + if let Some(input) = replay.keystroke.key_char.as_ref().cloned() + && let Some(mut input_handler) = self.platform_window.take_input_handler() + { + input_handler.dispatch_input(&input, self, cx); + self.platform_window.set_input_handler(input_handler) } } } @@ -4309,15 +4309,15 @@ impl Window { cx: &mut App, f: impl FnOnce(&mut Option<T>, &mut Self) -> R, ) -> R { - if let Some(inspector_id) = _inspector_id { - if let Some(inspector) = &self.inspector { - let inspector = inspector.clone(); - let active_element_id = inspector.read(cx).active_element_id(); - if Some(inspector_id) == active_element_id { - return inspector.update(cx, |inspector, _cx| { - inspector.with_active_element_state(self, f) - }); - } + if let Some(inspector_id) = _inspector_id + && let Some(inspector) = &self.inspector + { + let inspector = inspector.clone(); + let active_element_id = inspector.read(cx).active_element_id(); + if Some(inspector_id) == active_element_id { + return inspector.update(cx, |inspector, _cx| { + inspector.with_active_element_state(self, f) + }); } } f(&mut None, self) @@ -4389,15 +4389,13 @@ impl Window { if let Some(inspector) = self.inspector.as_ref() { let inspector = inspector.read(cx); if let Some((hitbox_id, _)) = self.hovered_inspector_hitbox(inspector, &self.next_frame) - { - if let Some(hitbox) = self + && let Some(hitbox) = self .next_frame .hitboxes .iter() .find(|hitbox| hitbox.id == hitbox_id) - { - self.paint_quad(crate::fill(hitbox.bounds, crate::rgba(0x61afef4d))); - } + { + self.paint_quad(crate::fill(hitbox.bounds, crate::rgba(0x61afef4d))); } } } diff --git a/crates/gpui_macros/src/derive_inspector_reflection.rs b/crates/gpui_macros/src/derive_inspector_reflection.rs index fa22f95f9a..5415807ea0 100644 --- a/crates/gpui_macros/src/derive_inspector_reflection.rs +++ b/crates/gpui_macros/src/derive_inspector_reflection.rs @@ -160,16 +160,14 @@ fn extract_doc_comment(attrs: &[Attribute]) -> Option<String> { let mut doc_lines = Vec::new(); for attr in attrs { - if attr.path().is_ident("doc") { - if let Meta::NameValue(meta) = &attr.meta { - if let Expr::Lit(expr_lit) = &meta.value { - if let Lit::Str(lit_str) = &expr_lit.lit { - let line = lit_str.value(); - let line = line.strip_prefix(' ').unwrap_or(&line); - doc_lines.push(line.to_string()); - } - } - } + if attr.path().is_ident("doc") + && let Meta::NameValue(meta) = &attr.meta + && let Expr::Lit(expr_lit) = &meta.value + && let Lit::Str(lit_str) = &expr_lit.lit + { + let line = lit_str.value(); + let line = line.strip_prefix(' ').unwrap_or(&line); + doc_lines.push(line.to_string()); } } diff --git a/crates/gpui_macros/src/test.rs b/crates/gpui_macros/src/test.rs index 5a8b1cf7fc..0153c5889a 100644 --- a/crates/gpui_macros/src/test.rs +++ b/crates/gpui_macros/src/test.rs @@ -152,28 +152,28 @@ fn generate_test_function( } _ => {} } - } else if let Type::Reference(ty) = &*arg.ty { - if let Type::Path(ty) = &*ty.elem { - let last_segment = ty.path.segments.last(); - if let Some("TestAppContext") = - last_segment.map(|s| s.ident.to_string()).as_deref() - { - let cx_varname = format_ident!("cx_{}", ix); - cx_vars.extend(quote!( - let mut #cx_varname = gpui::TestAppContext::build( - dispatcher.clone(), - Some(stringify!(#outer_fn_name)), - ); - )); - cx_teardowns.extend(quote!( - dispatcher.run_until_parked(); - #cx_varname.executor().forbid_parking(); - #cx_varname.quit(); - dispatcher.run_until_parked(); - )); - inner_fn_args.extend(quote!(&mut #cx_varname,)); - continue; - } + } else if let Type::Reference(ty) = &*arg.ty + && let Type::Path(ty) = &*ty.elem + { + let last_segment = ty.path.segments.last(); + if let Some("TestAppContext") = + last_segment.map(|s| s.ident.to_string()).as_deref() + { + let cx_varname = format_ident!("cx_{}", ix); + cx_vars.extend(quote!( + let mut #cx_varname = gpui::TestAppContext::build( + dispatcher.clone(), + Some(stringify!(#outer_fn_name)), + ); + )); + cx_teardowns.extend(quote!( + dispatcher.run_until_parked(); + #cx_varname.executor().forbid_parking(); + #cx_varname.quit(); + dispatcher.run_until_parked(); + )); + inner_fn_args.extend(quote!(&mut #cx_varname,)); + continue; } } } @@ -215,48 +215,48 @@ fn generate_test_function( inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),)); continue; } - } else if let Type::Reference(ty) = &*arg.ty { - if let Type::Path(ty) = &*ty.elem { - let last_segment = ty.path.segments.last(); - match last_segment.map(|s| s.ident.to_string()).as_deref() { - Some("App") => { - let cx_varname = format_ident!("cx_{}", ix); - let cx_varname_lock = format_ident!("cx_{}_lock", ix); - cx_vars.extend(quote!( - let mut #cx_varname = gpui::TestAppContext::build( - dispatcher.clone(), - Some(stringify!(#outer_fn_name)) - ); - let mut #cx_varname_lock = #cx_varname.app.borrow_mut(); - )); - inner_fn_args.extend(quote!(&mut #cx_varname_lock,)); - cx_teardowns.extend(quote!( + } else if let Type::Reference(ty) = &*arg.ty + && let Type::Path(ty) = &*ty.elem + { + let last_segment = ty.path.segments.last(); + match last_segment.map(|s| s.ident.to_string()).as_deref() { + Some("App") => { + let cx_varname = format_ident!("cx_{}", ix); + let cx_varname_lock = format_ident!("cx_{}_lock", ix); + cx_vars.extend(quote!( + let mut #cx_varname = gpui::TestAppContext::build( + dispatcher.clone(), + Some(stringify!(#outer_fn_name)) + ); + let mut #cx_varname_lock = #cx_varname.app.borrow_mut(); + )); + inner_fn_args.extend(quote!(&mut #cx_varname_lock,)); + cx_teardowns.extend(quote!( drop(#cx_varname_lock); dispatcher.run_until_parked(); #cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); }); dispatcher.run_until_parked(); )); - continue; - } - Some("TestAppContext") => { - let cx_varname = format_ident!("cx_{}", ix); - cx_vars.extend(quote!( - let mut #cx_varname = gpui::TestAppContext::build( - dispatcher.clone(), - Some(stringify!(#outer_fn_name)) - ); - )); - cx_teardowns.extend(quote!( - dispatcher.run_until_parked(); - #cx_varname.executor().forbid_parking(); - #cx_varname.quit(); - dispatcher.run_until_parked(); - )); - inner_fn_args.extend(quote!(&mut #cx_varname,)); - continue; - } - _ => {} + continue; } + Some("TestAppContext") => { + let cx_varname = format_ident!("cx_{}", ix); + cx_vars.extend(quote!( + let mut #cx_varname = gpui::TestAppContext::build( + dispatcher.clone(), + Some(stringify!(#outer_fn_name)) + ); + )); + cx_teardowns.extend(quote!( + dispatcher.run_until_parked(); + #cx_varname.executor().forbid_parking(); + #cx_varname.quit(); + dispatcher.run_until_parked(); + )); + inner_fn_args.extend(quote!(&mut #cx_varname,)); + continue; + } + _ => {} } } } diff --git a/crates/html_to_markdown/src/markdown.rs b/crates/html_to_markdown/src/markdown.rs index b9ffbac79c..bb3b3563bc 100644 --- a/crates/html_to_markdown/src/markdown.rs +++ b/crates/html_to_markdown/src/markdown.rs @@ -34,15 +34,14 @@ impl HandleTag for ParagraphHandler { tag: &HtmlElement, writer: &mut MarkdownWriter, ) -> StartTagOutcome { - if tag.is_inline() && writer.is_inside("p") { - if let Some(parent) = writer.current_element_stack().iter().last() { - if !(parent.is_inline() - || writer.markdown.ends_with(' ') - || writer.markdown.ends_with('\n')) - { - writer.push_str(" "); - } - } + if tag.is_inline() + && writer.is_inside("p") + && let Some(parent) = writer.current_element_stack().iter().last() + && !(parent.is_inline() + || writer.markdown.ends_with(' ') + || writer.markdown.ends_with('\n')) + { + writer.push_str(" "); } if tag.tag() == "p" { diff --git a/crates/http_client/src/github.rs b/crates/http_client/src/github.rs index 89309ff344..32efed8e72 100644 --- a/crates/http_client/src/github.rs +++ b/crates/http_client/src/github.rs @@ -77,10 +77,10 @@ pub async fn latest_github_release( .find(|release| release.pre_release == pre_release) .context("finding a prerelease")?; release.assets.iter_mut().for_each(|asset| { - if let Some(digest) = &mut asset.digest { - if let Some(stripped) = digest.strip_prefix("sha256:") { - *digest = stripped.to_owned(); - } + if let Some(digest) = &mut asset.digest + && let Some(stripped) = digest.strip_prefix("sha256:") + { + *digest = stripped.to_owned(); } }); Ok(release) diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 0335a746cd..53887eb736 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -170,23 +170,23 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap .await }; - if let Some(Some(Ok(item))) = opened.first() { - if let Some(editor) = item.downcast::<Editor>().map(|editor| editor.downgrade()) { - editor.update_in(cx, |editor, window, cx| { - let len = editor.buffer().read(cx).len(cx); - editor.change_selections( - SelectionEffects::scroll(Autoscroll::center()), - window, - cx, - |s| s.select_ranges([len..len]), - ); - if len > 0 { - editor.insert("\n\n", window, cx); - } - editor.insert(&entry_heading, window, cx); + if let Some(Some(Ok(item))) = opened.first() + && let Some(editor) = item.downcast::<Editor>().map(|editor| editor.downgrade()) + { + editor.update_in(cx, |editor, window, cx| { + let len = editor.buffer().read(cx).len(cx); + editor.change_selections( + SelectionEffects::scroll(Autoscroll::center()), + window, + cx, + |s| s.select_ranges([len..len]), + ); + if len > 0 { editor.insert("\n\n", window, cx); - })?; - } + } + editor.insert(&entry_heading, window, cx); + editor.insert("\n\n", window, cx); + })?; } anyhow::Ok(()) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index abb8d3b151..9227d35a50 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1158,13 +1158,12 @@ impl Buffer { base_buffer.edit(edits, None, cx) }); - if let Some(operation) = operation { - if let Some(BufferBranchState { + if let Some(operation) = operation + && let Some(BufferBranchState { merged_operations, .. }) = &mut self.branch_state - { - merged_operations.push(operation); - } + { + merged_operations.push(operation); } } @@ -1185,11 +1184,11 @@ impl Buffer { }; let mut operation_to_undo = None; - if let Operation::Buffer(text::Operation::Edit(operation)) = &operation { - if let Ok(ix) = merged_operations.binary_search(&operation.timestamp) { - merged_operations.remove(ix); - operation_to_undo = Some(operation.timestamp); - } + if let Operation::Buffer(text::Operation::Edit(operation)) = &operation + && let Ok(ix) = merged_operations.binary_search(&operation.timestamp) + { + merged_operations.remove(ix); + operation_to_undo = Some(operation.timestamp); } self.apply_ops([operation.clone()], cx); @@ -1424,10 +1423,10 @@ impl Buffer { .map(|info| info.language.clone()) .collect(); - if languages.is_empty() { - if let Some(buffer_language) = self.language() { - languages.push(buffer_language.clone()); - } + if languages.is_empty() + && let Some(buffer_language) = self.language() + { + languages.push(buffer_language.clone()); } languages @@ -2589,10 +2588,10 @@ impl Buffer { line_mode, cursor_shape, } => { - if let Some(set) = self.remote_selections.get(&lamport_timestamp.replica_id) { - if set.lamport_timestamp > lamport_timestamp { - return; - } + if let Some(set) = self.remote_selections.get(&lamport_timestamp.replica_id) + && set.lamport_timestamp > lamport_timestamp + { + return; } self.remote_selections.insert( @@ -3365,8 +3364,8 @@ impl BufferSnapshot { } } - if let Some(range) = range { - if smallest_range_and_depth.as_ref().map_or( + if let Some(range) = range + && smallest_range_and_depth.as_ref().map_or( true, |(smallest_range, smallest_range_depth)| { if layer.depth > *smallest_range_depth { @@ -3377,13 +3376,13 @@ impl BufferSnapshot { false } }, - ) { - smallest_range_and_depth = Some((range, layer.depth)); - scope = Some(LanguageScope { - language: layer.language.clone(), - override_id: layer.override_id(offset, &self.text), - }); - } + ) + { + smallest_range_and_depth = Some((range, layer.depth)); + scope = Some(LanguageScope { + language: layer.language.clone(), + override_id: layer.override_id(offset, &self.text), + }); } } @@ -3499,17 +3498,17 @@ impl BufferSnapshot { // If there is a candidate node on both sides of the (empty) range, then // decide between the two by favoring a named node over an anonymous token. // If both nodes are the same in that regard, favor the right one. - if let Some(right_node) = right_node { - if right_node.is_named() || !left_node.is_named() { - layer_result = right_node; - } + if let Some(right_node) = right_node + && (right_node.is_named() || !left_node.is_named()) + { + layer_result = right_node; } } - if let Some(previous_result) = &result { - if previous_result.byte_range().len() < layer_result.byte_range().len() { - continue; - } + if let Some(previous_result) = &result + && previous_result.byte_range().len() < layer_result.byte_range().len() + { + continue; } result = Some(layer_result); } @@ -4081,10 +4080,10 @@ impl BufferSnapshot { let mut result: Option<(Range<usize>, Range<usize>)> = None; for pair in self.enclosing_bracket_ranges(range.clone()) { - if let Some(range_filter) = range_filter { - if !range_filter(pair.open_range.clone(), pair.close_range.clone()) { - continue; - } + if let Some(range_filter) = range_filter + && !range_filter(pair.open_range.clone(), pair.close_range.clone()) + { + continue; } let len = pair.close_range.end - pair.open_range.start; @@ -4474,27 +4473,26 @@ impl BufferSnapshot { current_word_start_ix = Some(ix); } - if let Some(query_chars) = &query_chars { - if query_ix < query_len { - if c.to_lowercase().eq(query_chars[query_ix].to_lowercase()) { - query_ix += 1; - } - } + if let Some(query_chars) = &query_chars + && query_ix < query_len + && c.to_lowercase().eq(query_chars[query_ix].to_lowercase()) + { + query_ix += 1; } continue; - } else if let Some(word_start) = current_word_start_ix.take() { - if query_ix == query_len { - let word_range = self.anchor_before(word_start)..self.anchor_after(ix); - let mut word_text = self.text_for_range(word_start..ix).peekable(); - let first_char = word_text - .peek() - .and_then(|first_chunk| first_chunk.chars().next()); - // Skip empty and "words" starting with digits as a heuristic to reduce useless completions - if !query.skip_digits - || first_char.map_or(true, |first_char| !first_char.is_digit(10)) - { - words.insert(word_text.collect(), word_range); - } + } else if let Some(word_start) = current_word_start_ix.take() + && query_ix == query_len + { + let word_range = self.anchor_before(word_start)..self.anchor_after(ix); + let mut word_text = self.text_for_range(word_start..ix).peekable(); + let first_char = word_text + .peek() + .and_then(|first_chunk| first_chunk.chars().next()); + // Skip empty and "words" starting with digits as a heuristic to reduce useless completions + if !query.skip_digits + || first_char.map_or(true, |first_char| !first_char.is_digit(10)) + { + words.insert(word_text.collect(), word_range); } } query_ix = 0; @@ -4607,17 +4605,17 @@ impl<'a> BufferChunks<'a> { highlights .stack .retain(|(end_offset, _)| *end_offset > range.start); - if let Some(capture) = &highlights.next_capture { - if range.start >= capture.node.start_byte() { - let next_capture_end = capture.node.end_byte(); - if range.start < next_capture_end { - highlights.stack.push(( - next_capture_end, - highlights.highlight_maps[capture.grammar_index].get(capture.index), - )); - } - highlights.next_capture.take(); + if let Some(capture) = &highlights.next_capture + && range.start >= capture.node.start_byte() + { + let next_capture_end = capture.node.end_byte(); + if range.start < next_capture_end { + highlights.stack.push(( + next_capture_end, + highlights.highlight_maps[capture.grammar_index].get(capture.index), + )); } + highlights.next_capture.take(); } } else if let Some(snapshot) = self.buffer_snapshot { let (captures, highlight_maps) = snapshot.get_highlights(self.range.clone()); @@ -4642,33 +4640,33 @@ impl<'a> BufferChunks<'a> { } fn initialize_diagnostic_endpoints(&mut self) { - if let Some(diagnostics) = self.diagnostic_endpoints.as_mut() { - if let Some(buffer) = self.buffer_snapshot { - let mut diagnostic_endpoints = Vec::new(); - for entry in buffer.diagnostics_in_range::<_, usize>(self.range.clone(), false) { - diagnostic_endpoints.push(DiagnosticEndpoint { - offset: entry.range.start, - is_start: true, - severity: entry.diagnostic.severity, - is_unnecessary: entry.diagnostic.is_unnecessary, - underline: entry.diagnostic.underline, - }); - diagnostic_endpoints.push(DiagnosticEndpoint { - offset: entry.range.end, - is_start: false, - severity: entry.diagnostic.severity, - is_unnecessary: entry.diagnostic.is_unnecessary, - underline: entry.diagnostic.underline, - }); - } - diagnostic_endpoints - .sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start)); - *diagnostics = diagnostic_endpoints.into_iter().peekable(); - self.hint_depth = 0; - self.error_depth = 0; - self.warning_depth = 0; - self.information_depth = 0; + if let Some(diagnostics) = self.diagnostic_endpoints.as_mut() + && let Some(buffer) = self.buffer_snapshot + { + let mut diagnostic_endpoints = Vec::new(); + for entry in buffer.diagnostics_in_range::<_, usize>(self.range.clone(), false) { + diagnostic_endpoints.push(DiagnosticEndpoint { + offset: entry.range.start, + is_start: true, + severity: entry.diagnostic.severity, + is_unnecessary: entry.diagnostic.is_unnecessary, + underline: entry.diagnostic.underline, + }); + diagnostic_endpoints.push(DiagnosticEndpoint { + offset: entry.range.end, + is_start: false, + severity: entry.diagnostic.severity, + is_unnecessary: entry.diagnostic.is_unnecessary, + underline: entry.diagnostic.underline, + }); } + diagnostic_endpoints + .sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start)); + *diagnostics = diagnostic_endpoints.into_iter().peekable(); + self.hint_depth = 0; + self.error_depth = 0; + self.warning_depth = 0; + self.information_depth = 0; } } @@ -4779,11 +4777,11 @@ impl<'a> Iterator for BufferChunks<'a> { .min(next_capture_start) .min(next_diagnostic_endpoint); let mut highlight_id = None; - if let Some(highlights) = self.highlights.as_ref() { - if let Some((parent_capture_end, parent_highlight_id)) = highlights.stack.last() { - chunk_end = chunk_end.min(*parent_capture_end); - highlight_id = Some(*parent_highlight_id); - } + if let Some(highlights) = self.highlights.as_ref() + && let Some((parent_capture_end, parent_highlight_id)) = highlights.stack.last() + { + chunk_end = chunk_end.min(*parent_capture_end); + highlight_id = Some(*parent_highlight_id); } let slice = @@ -4977,11 +4975,12 @@ pub(crate) fn contiguous_ranges( std::iter::from_fn(move || { loop { if let Some(value) = values.next() { - if let Some(range) = &mut current_range { - if value == range.end && range.len() < max_len { - range.end += 1; - continue; - } + if let Some(range) = &mut current_range + && value == range.end + && range.len() < max_len + { + range.end += 1; + continue; } let prev_range = current_range.clone(); @@ -5049,10 +5048,10 @@ impl CharClassifier { } else { scope.word_characters() }; - if let Some(characters) = characters { - if characters.contains(&c) { - return CharKind::Word; - } + if let Some(characters) = characters + && characters.contains(&c) + { + return CharKind::Word; } } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 3a41733191..68addc804e 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -329,8 +329,8 @@ pub trait LspAdapter: 'static + Send + Sync { // We only want to cache when we fall back to the global one, // because we don't want to download and overwrite our global one // for each worktree we might have open. - if binary_options.allow_path_lookup { - if let Some(binary) = self.check_if_user_installed(delegate.as_ref(), toolchains, cx).await { + if binary_options.allow_path_lookup + && let Some(binary) = self.check_if_user_installed(delegate.as_ref(), toolchains, cx).await { log::info!( "found user-installed language server for {}. path: {:?}, arguments: {:?}", self.name().0, @@ -339,7 +339,6 @@ pub trait LspAdapter: 'static + Send + Sync { ); return Ok(binary); } - } anyhow::ensure!(binary_options.allow_binary_download, "downloading language servers disabled"); @@ -1776,10 +1775,10 @@ impl Language { BufferChunks::new(text, range, Some((captures, highlight_maps)), false, None) { let end_offset = offset + chunk.text.len(); - if let Some(highlight_id) = chunk.syntax_highlight_id { - if !highlight_id.is_default() { - result.push((offset..end_offset, highlight_id)); - } + if let Some(highlight_id) = chunk.syntax_highlight_id + && !highlight_id.is_default() + { + result.push((offset..end_offset, highlight_id)); } offset = end_offset; } @@ -1796,11 +1795,11 @@ impl Language { } pub fn set_theme(&self, theme: &SyntaxTheme) { - if let Some(grammar) = self.grammar.as_ref() { - if let Some(highlights_query) = &grammar.highlights_query { - *grammar.highlight_map.lock() = - HighlightMap::new(highlights_query.capture_names(), theme); - } + if let Some(grammar) = self.grammar.as_ref() + && let Some(highlights_query) = &grammar.highlights_query + { + *grammar.highlight_map.lock() = + HighlightMap::new(highlights_query.capture_names(), theme); } } @@ -1920,11 +1919,11 @@ impl LanguageScope { .enumerate() .map(move |(ix, bracket)| { let mut is_enabled = true; - if let Some(next_disabled_ix) = disabled_ids.first() { - if ix == *next_disabled_ix as usize { - disabled_ids = &disabled_ids[1..]; - is_enabled = false; - } + if let Some(next_disabled_ix) = disabled_ids.first() + && ix == *next_disabled_ix as usize + { + disabled_ids = &disabled_ids[1..]; + is_enabled = false; } (bracket, is_enabled) }) diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 30bbc88f7e..1e1060c843 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -414,42 +414,42 @@ impl SyntaxSnapshot { .collect::<Vec<_>>(); self.reparse_with_ranges(text, root_language.clone(), edit_ranges, registry.as_ref()); - if let Some(registry) = registry { - if registry.version() != self.language_registry_version { - let mut resolved_injection_ranges = Vec::new(); - let mut cursor = self - .layers - .filter::<_, ()>(text, |summary| summary.contains_unknown_injections); + if let Some(registry) = registry + && registry.version() != self.language_registry_version + { + let mut resolved_injection_ranges = Vec::new(); + let mut cursor = self + .layers + .filter::<_, ()>(text, |summary| summary.contains_unknown_injections); + cursor.next(); + while let Some(layer) = cursor.item() { + let SyntaxLayerContent::Pending { language_name } = &layer.content else { + unreachable!() + }; + if registry + .language_for_name_or_extension(language_name) + .now_or_never() + .and_then(|language| language.ok()) + .is_some() + { + let range = layer.range.to_offset(text); + log::trace!("reparse range {range:?} for language {language_name:?}"); + resolved_injection_ranges.push(range); + } + cursor.next(); - while let Some(layer) = cursor.item() { - let SyntaxLayerContent::Pending { language_name } = &layer.content else { - unreachable!() - }; - if registry - .language_for_name_or_extension(language_name) - .now_or_never() - .and_then(|language| language.ok()) - .is_some() - { - let range = layer.range.to_offset(text); - log::trace!("reparse range {range:?} for language {language_name:?}"); - resolved_injection_ranges.push(range); - } - - cursor.next(); - } - drop(cursor); - - if !resolved_injection_ranges.is_empty() { - self.reparse_with_ranges( - text, - root_language, - resolved_injection_ranges, - Some(®istry), - ); - } - self.language_registry_version = registry.version(); } + drop(cursor); + + if !resolved_injection_ranges.is_empty() { + self.reparse_with_ranges( + text, + root_language, + resolved_injection_ranges, + Some(®istry), + ); + } + self.language_registry_version = registry.version(); } self.update_count += 1; @@ -1065,10 +1065,10 @@ impl<'a> SyntaxMapCaptures<'a> { pub fn set_byte_range(&mut self, range: Range<usize>) { for layer in &mut self.layers { layer.captures.set_byte_range(range.clone()); - if let Some(capture) = &layer.next_capture { - if capture.node.end_byte() > range.start { - continue; - } + if let Some(capture) = &layer.next_capture + && capture.node.end_byte() > range.start + { + continue; } layer.advance(); } @@ -1277,11 +1277,11 @@ fn join_ranges( (None, None) => break, }; - if let Some(last) = result.last_mut() { - if range.start <= last.end { - last.end = last.end.max(range.end); - continue; - } + if let Some(last) = result.last_mut() + && range.start <= last.end + { + last.end = last.end.max(range.end); + continue; } result.push(range); } @@ -1330,14 +1330,13 @@ fn get_injections( // if there currently no matches for that injection. combined_injection_ranges.clear(); for pattern in &config.patterns { - if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) { - if let Some(language) = language_registry + if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) + && let Some(language) = language_registry .language_for_name_or_extension(language_name) .now_or_never() .and_then(|language| language.ok()) - { - combined_injection_ranges.insert(language.id, (language, Vec::new())); - } + { + combined_injection_ranges.insert(language.id, (language, Vec::new())); } } @@ -1357,10 +1356,11 @@ fn get_injections( content_ranges.first().unwrap().start_byte..content_ranges.last().unwrap().end_byte; // Avoid duplicate matches if two changed ranges intersect the same injection. - if let Some((prev_pattern_ix, prev_range)) = &prev_match { - if mat.pattern_index == *prev_pattern_ix && content_range == *prev_range { - continue; - } + if let Some((prev_pattern_ix, prev_range)) = &prev_match + && mat.pattern_index == *prev_pattern_ix + && content_range == *prev_range + { + continue; } prev_match = Some((mat.pattern_index, content_range.clone())); diff --git a/crates/language/src/text_diff.rs b/crates/language/src/text_diff.rs index af8ce60881..1e3e12758d 100644 --- a/crates/language/src/text_diff.rs +++ b/crates/language/src/text_diff.rs @@ -189,11 +189,11 @@ fn tokenize(text: &str, language_scope: Option<LanguageScope>) -> impl Iterator< while let Some((ix, c)) = chars.next() { let mut token = None; let kind = classifier.kind(c); - if let Some((prev_char, prev_kind)) = prev { - if kind != prev_kind || (kind == CharKind::Punctuation && c != prev_char) { - token = Some(&text[start_ix..ix]); - start_ix = ix; - } + if let Some((prev_char, prev_kind)) = prev + && (kind != prev_kind || (kind == CharKind::Punctuation && c != prev_char)) + { + token = Some(&text[start_ix..ix]); + start_ix = ix; } prev = Some((c, kind)); if token.is_some() { diff --git a/crates/language_model/src/request.rs b/crates/language_model/src/request.rs index 8c2d169973..1182e0f7a8 100644 --- a/crates/language_model/src/request.rs +++ b/crates/language_model/src/request.rs @@ -221,36 +221,33 @@ impl<'de> Deserialize<'de> for LanguageModelToolResultContent { // Accept wrapped text format: { "type": "text", "text": "..." } if let (Some(type_value), Some(text_value)) = (get_field(obj, "type"), get_field(obj, "text")) + && let Some(type_str) = type_value.as_str() + && type_str.to_lowercase() == "text" + && let Some(text) = text_value.as_str() { - if let Some(type_str) = type_value.as_str() { - if type_str.to_lowercase() == "text" { - if let Some(text) = text_value.as_str() { - return Ok(Self::Text(Arc::from(text))); - } - } - } + return Ok(Self::Text(Arc::from(text))); } // Check for wrapped Text variant: { "text": "..." } - if let Some((_key, value)) = obj.iter().find(|(k, _)| k.to_lowercase() == "text") { - if obj.len() == 1 { - // Only one field, and it's "text" (case-insensitive) - if let Some(text) = value.as_str() { - return Ok(Self::Text(Arc::from(text))); - } + if let Some((_key, value)) = obj.iter().find(|(k, _)| k.to_lowercase() == "text") + && obj.len() == 1 + { + // Only one field, and it's "text" (case-insensitive) + if let Some(text) = value.as_str() { + return Ok(Self::Text(Arc::from(text))); } } // Check for wrapped Image variant: { "image": { "source": "...", "size": ... } } - if let Some((_key, value)) = obj.iter().find(|(k, _)| k.to_lowercase() == "image") { - if obj.len() == 1 { - // Only one field, and it's "image" (case-insensitive) - // Try to parse the nested image object - if let Some(image_obj) = value.as_object() { - if let Some(image) = LanguageModelImage::from_json(image_obj) { - return Ok(Self::Image(image)); - } - } + if let Some((_key, value)) = obj.iter().find(|(k, _)| k.to_lowercase() == "image") + && obj.len() == 1 + { + // Only one field, and it's "image" (case-insensitive) + // Try to parse the nested image object + if let Some(image_obj) = value.as_object() + && let Some(image) = LanguageModelImage::from_json(image_obj) + { + return Ok(Self::Image(image)); } } diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index 7ba56ec775..b16be36ea1 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -633,11 +633,11 @@ pub fn into_anthropic( Role::Assistant => anthropic::Role::Assistant, Role::System => unreachable!("System role should never occur here"), }; - if let Some(last_message) = new_messages.last_mut() { - if last_message.role == anthropic_role { - last_message.content.extend(anthropic_message_content); - continue; - } + if let Some(last_message) = new_messages.last_mut() + && last_message.role == anthropic_role + { + last_message.content.extend(anthropic_message_content); + continue; } // Mark the last segment of the message as cached diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index f33a00972d..193d218094 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -412,10 +412,10 @@ impl BedrockModel { .region(Region::new(region)) .timeout_config(TimeoutConfig::disabled()); - if let Some(endpoint_url) = endpoint { - if !endpoint_url.is_empty() { - config_builder = config_builder.endpoint_url(endpoint_url); - } + if let Some(endpoint_url) = endpoint + && !endpoint_url.is_empty() + { + config_builder = config_builder.endpoint_url(endpoint_url); } match auth_method { @@ -728,11 +728,11 @@ pub fn into_bedrock( Role::Assistant => bedrock::BedrockRole::Assistant, Role::System => unreachable!("System role should never occur here"), }; - if let Some(last_message) = new_messages.last_mut() { - if last_message.role == bedrock_role { - last_message.content.extend(bedrock_message_content); - continue; - } + if let Some(last_message) = new_messages.last_mut() + && last_message.role == bedrock_role + { + last_message.content.extend(bedrock_message_content); + continue; } new_messages.push( BedrockMessage::builder() diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index f226d0c6a8..e99dadc28d 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -597,15 +597,13 @@ impl CloudLanguageModel { .headers() .get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME) .and_then(|resource| resource.to_str().ok()) - { - if let Some(plan) = response + && let Some(plan) = response .headers() .get(CURRENT_PLAN_HEADER_NAME) .and_then(|plan| plan.to_str().ok()) .and_then(|plan| cloud_llm_client::Plan::from_str(plan).ok()) - { - return Err(anyhow!(ModelRequestLimitReachedError { plan })); - } + { + return Err(anyhow!(ModelRequestLimitReachedError { plan })); } } else if status == StatusCode::PAYMENT_REQUIRED { return Err(anyhow!(PaymentRequiredError)); @@ -662,29 +660,29 @@ where impl From<ApiError> for LanguageModelCompletionError { fn from(error: ApiError) -> Self { - if let Ok(cloud_error) = serde_json::from_str::<CloudApiError>(&error.body) { - if cloud_error.code.starts_with("upstream_http_") { - let status = if let Some(status) = cloud_error.upstream_status { - status - } else if cloud_error.code.ends_with("_error") { - error.status - } else { - // If there's a status code in the code string (e.g. "upstream_http_429") - // then use that; otherwise, see if the JSON contains a status code. - cloud_error - .code - .strip_prefix("upstream_http_") - .and_then(|code_str| code_str.parse::<u16>().ok()) - .and_then(|code| StatusCode::from_u16(code).ok()) - .unwrap_or(error.status) - }; + if let Ok(cloud_error) = serde_json::from_str::<CloudApiError>(&error.body) + && cloud_error.code.starts_with("upstream_http_") + { + let status = if let Some(status) = cloud_error.upstream_status { + status + } else if cloud_error.code.ends_with("_error") { + error.status + } else { + // If there's a status code in the code string (e.g. "upstream_http_429") + // then use that; otherwise, see if the JSON contains a status code. + cloud_error + .code + .strip_prefix("upstream_http_") + .and_then(|code_str| code_str.parse::<u16>().ok()) + .and_then(|code| StatusCode::from_u16(code).ok()) + .unwrap_or(error.status) + }; - return LanguageModelCompletionError::UpstreamProviderError { - message: cloud_error.message, - status, - retry_after: cloud_error.retry_after.map(Duration::from_secs_f64), - }; - } + return LanguageModelCompletionError::UpstreamProviderError { + message: cloud_error.message, + status, + retry_after: cloud_error.retry_after.map(Duration::from_secs_f64), + }; } let retry_after = None; diff --git a/crates/language_selector/src/active_buffer_language.rs b/crates/language_selector/src/active_buffer_language.rs index c5c5eceab5..56924c4cd2 100644 --- a/crates/language_selector/src/active_buffer_language.rs +++ b/crates/language_selector/src/active_buffer_language.rs @@ -28,10 +28,10 @@ impl ActiveBufferLanguage { self.active_language = Some(None); let editor = editor.read(cx); - if let Some((_, buffer, _)) = editor.active_excerpt(cx) { - if let Some(language) = buffer.read(cx).language() { - self.active_language = Some(Some(language.name())); - } + if let Some((_, buffer, _)) = editor.active_excerpt(cx) + && let Some(language) = buffer.read(cx).language() + { + self.active_language = Some(Some(language.name())); } cx.notify(); diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index c303a8c305..3285efaaef 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -254,35 +254,35 @@ impl LogStore { let copilot_subscription = Copilot::global(cx).map(|copilot| { let copilot = &copilot; cx.subscribe(copilot, |this, copilot, edit_prediction_event, cx| { - if let copilot::Event::CopilotLanguageServerStarted = edit_prediction_event { - if let Some(server) = copilot.read(cx).language_server() { - let server_id = server.server_id(); - let weak_this = cx.weak_entity(); - this.copilot_log_subscription = - Some(server.on_notification::<copilot::request::LogMessage, _>( - move |params, cx| { - weak_this - .update(cx, |this, cx| { - this.add_language_server_log( - server_id, - MessageType::LOG, - ¶ms.message, - cx, - ); - }) - .ok(); - }, - )); - let name = LanguageServerName::new_static("copilot"); - this.add_language_server( - LanguageServerKind::Global, - server.server_id(), - Some(name), - None, - Some(server.clone()), - cx, - ); - } + if let copilot::Event::CopilotLanguageServerStarted = edit_prediction_event + && let Some(server) = copilot.read(cx).language_server() + { + let server_id = server.server_id(); + let weak_this = cx.weak_entity(); + this.copilot_log_subscription = + Some(server.on_notification::<copilot::request::LogMessage, _>( + move |params, cx| { + weak_this + .update(cx, |this, cx| { + this.add_language_server_log( + server_id, + MessageType::LOG, + ¶ms.message, + cx, + ); + }) + .ok(); + }, + )); + let name = LanguageServerName::new_static("copilot"); + this.add_language_server( + LanguageServerKind::Global, + server.server_id(), + Some(name), + None, + Some(server.clone()), + cx, + ); } }) }); @@ -733,16 +733,14 @@ impl LspLogView { let first_server_id_for_project = store.read(cx).server_ids_for_project(&weak_project).next(); if let Some(current_lsp) = this.current_server_id { - if !store.read(cx).language_servers.contains_key(¤t_lsp) { - if let Some(server_id) = first_server_id_for_project { - match this.active_entry_kind { - LogKind::Rpc => { - this.show_rpc_trace_for_server(server_id, window, cx) - } - LogKind::Trace => this.show_trace_for_server(server_id, window, cx), - LogKind::Logs => this.show_logs_for_server(server_id, window, cx), - LogKind::ServerInfo => this.show_server_info(server_id, window, cx), - } + if !store.read(cx).language_servers.contains_key(¤t_lsp) + && let Some(server_id) = first_server_id_for_project + { + match this.active_entry_kind { + LogKind::Rpc => this.show_rpc_trace_for_server(server_id, window, cx), + LogKind::Trace => this.show_trace_for_server(server_id, window, cx), + LogKind::Logs => this.show_logs_for_server(server_id, window, cx), + LogKind::ServerInfo => this.show_server_info(server_id, window, cx), } } } else if let Some(server_id) = first_server_id_for_project { @@ -776,21 +774,17 @@ impl LspLogView { ], cx, ); - if text.len() > 1024 { - if let Some((fold_offset, _)) = + if text.len() > 1024 + && let Some((fold_offset, _)) = text.char_indices().dropping(1024).next() - { - if fold_offset < text.len() { - editor.fold_ranges( - vec![ - last_offset + fold_offset..last_offset + text.len(), - ], - false, - window, - cx, - ); - } - } + && fold_offset < text.len() + { + editor.fold_ranges( + vec![last_offset + fold_offset..last_offset + text.len()], + false, + window, + cx, + ); } if newest_cursor_is_at_end { @@ -1311,14 +1305,14 @@ impl ToolbarItemView for LspLogToolbarItemView { _: &mut Window, cx: &mut Context<Self>, ) -> workspace::ToolbarItemLocation { - if let Some(item) = active_pane_item { - if let Some(log_view) = item.downcast::<LspLogView>() { - self.log_view = Some(log_view.clone()); - self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| { - cx.notify(); - })); - return ToolbarItemLocation::PrimaryLeft; - } + if let Some(item) = active_pane_item + && let Some(log_view) = item.downcast::<LspLogView>() + { + self.log_view = Some(log_view.clone()); + self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| { + cx.notify(); + })); + return ToolbarItemLocation::PrimaryLeft; } self.log_view = None; self._log_view_subscription = None; diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 9946442ec8..4fe8e11f94 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -103,12 +103,11 @@ impl SyntaxTreeView { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(item) = active_item { - if item.item_id() != cx.entity_id() { - if let Some(editor) = item.act_as::<Editor>(cx) { - self.set_editor(editor, window, cx); - } - } + if let Some(item) = active_item + && item.item_id() != cx.entity_id() + && let Some(editor) = item.act_as::<Editor>(cx) + { + self.set_editor(editor, window, cx); } } @@ -537,12 +536,12 @@ impl ToolbarItemView for SyntaxTreeToolbarItemView { window: &mut Window, cx: &mut Context<Self>, ) -> ToolbarItemLocation { - if let Some(item) = active_pane_item { - if let Some(view) = item.downcast::<SyntaxTreeView>() { - self.tree_view = Some(view.clone()); - self.subscription = Some(cx.observe_in(&view, window, |_, _, _, cx| cx.notify())); - return ToolbarItemLocation::PrimaryLeft; - } + if let Some(item) = active_pane_item + && let Some(view) = item.downcast::<SyntaxTreeView>() + { + self.tree_view = Some(view.clone()); + self.subscription = Some(cx.observe_in(&view, window, |_, _, _, cx| cx.notify())); + return ToolbarItemLocation::PrimaryLeft; } self.tree_view = None; self.subscription = None; diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index f739c5c4c6..00e3cad436 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -131,19 +131,19 @@ impl super::LspAdapter for GoLspAdapter { if let Some(version) = *version { let binary_path = container_dir.join(format!("gopls_{version}_go_{go_version}")); - if let Ok(metadata) = fs::metadata(&binary_path).await { - if metadata.is_file() { - remove_matching(&container_dir, |entry| { - entry != binary_path && entry.file_name() != Some(OsStr::new("gobin")) - }) - .await; + if let Ok(metadata) = fs::metadata(&binary_path).await + && metadata.is_file() + { + remove_matching(&container_dir, |entry| { + entry != binary_path && entry.file_name() != Some(OsStr::new("gobin")) + }) + .await; - return Ok(LanguageServerBinary { - path: binary_path.to_path_buf(), - arguments: server_binary_arguments(), - env: None, - }); - } + return Ok(LanguageServerBinary { + path: binary_path.to_path_buf(), + arguments: server_binary_arguments(), + env: None, + }); } } else if let Some(path) = this .cached_server_binary(container_dir.clone(), delegate) diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index e446f22713..75289dd59d 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -244,11 +244,8 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) { cx.observe_flag::<BasedPyrightFeatureFlag, _>({ let languages = languages.clone(); move |enabled, _| { - if enabled { - if let Some(adapter) = basedpyright_lsp_adapter.take() { - languages - .register_available_lsp_adapter(adapter.name(), move || adapter.clone()); - } + if enabled && let Some(adapter) = basedpyright_lsp_adapter.take() { + languages.register_available_lsp_adapter(adapter.name(), move || adapter.clone()); } } }) diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 17d0d98fad..89a091797e 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -338,31 +338,31 @@ impl LspAdapter for PythonLspAdapter { let interpreter_path = toolchain.path.to_string(); // Detect if this is a virtual environment - if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() { - if let Some(venv_dir) = interpreter_dir.parent() { - // Check if this looks like a virtual environment - if venv_dir.join("pyvenv.cfg").exists() - || venv_dir.join("bin/activate").exists() - || venv_dir.join("Scripts/activate.bat").exists() - { - // Set venvPath and venv at the root level - // This matches the format of a pyrightconfig.json file - if let Some(parent) = venv_dir.parent() { - // Use relative path if the venv is inside the workspace - let venv_path = if parent == adapter.worktree_root_path() { - ".".to_string() - } else { - parent.to_string_lossy().into_owned() - }; - object.insert("venvPath".to_string(), Value::String(venv_path)); - } + if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() + && let Some(venv_dir) = interpreter_dir.parent() + { + // Check if this looks like a virtual environment + if venv_dir.join("pyvenv.cfg").exists() + || venv_dir.join("bin/activate").exists() + || venv_dir.join("Scripts/activate.bat").exists() + { + // Set venvPath and venv at the root level + // This matches the format of a pyrightconfig.json file + if let Some(parent) = venv_dir.parent() { + // Use relative path if the venv is inside the workspace + let venv_path = if parent == adapter.worktree_root_path() { + ".".to_string() + } else { + parent.to_string_lossy().into_owned() + }; + object.insert("venvPath".to_string(), Value::String(venv_path)); + } - if let Some(venv_name) = venv_dir.file_name() { - object.insert( - "venv".to_owned(), - Value::String(venv_name.to_string_lossy().into_owned()), - ); - } + if let Some(venv_name) = venv_dir.file_name() { + object.insert( + "venv".to_owned(), + Value::String(venv_name.to_string_lossy().into_owned()), + ); } } } @@ -1519,31 +1519,31 @@ impl LspAdapter for BasedPyrightLspAdapter { let interpreter_path = toolchain.path.to_string(); // Detect if this is a virtual environment - if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() { - if let Some(venv_dir) = interpreter_dir.parent() { - // Check if this looks like a virtual environment - if venv_dir.join("pyvenv.cfg").exists() - || venv_dir.join("bin/activate").exists() - || venv_dir.join("Scripts/activate.bat").exists() - { - // Set venvPath and venv at the root level - // This matches the format of a pyrightconfig.json file - if let Some(parent) = venv_dir.parent() { - // Use relative path if the venv is inside the workspace - let venv_path = if parent == adapter.worktree_root_path() { - ".".to_string() - } else { - parent.to_string_lossy().into_owned() - }; - object.insert("venvPath".to_string(), Value::String(venv_path)); - } + if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() + && let Some(venv_dir) = interpreter_dir.parent() + { + // Check if this looks like a virtual environment + if venv_dir.join("pyvenv.cfg").exists() + || venv_dir.join("bin/activate").exists() + || venv_dir.join("Scripts/activate.bat").exists() + { + // Set venvPath and venv at the root level + // This matches the format of a pyrightconfig.json file + if let Some(parent) = venv_dir.parent() { + // Use relative path if the venv is inside the workspace + let venv_path = if parent == adapter.worktree_root_path() { + ".".to_string() + } else { + parent.to_string_lossy().into_owned() + }; + object.insert("venvPath".to_string(), Value::String(venv_path)); + } - if let Some(venv_name) = venv_dir.file_name() { - object.insert( - "venv".to_owned(), - Value::String(venv_name.to_string_lossy().into_owned()), - ); - } + if let Some(venv_name) = venv_dir.file_name() { + object.insert( + "venv".to_owned(), + Value::String(venv_name.to_string_lossy().into_owned()), + ); } } } diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index bbdfcdb499..f9b23ed9f4 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -598,12 +598,10 @@ impl ContextProvider for RustContextProvider { if let Some(path) = local_abs_path .as_deref() .and_then(|local_abs_path| local_abs_path.parent()) - { - if let Some(package_name) = + && let Some(package_name) = human_readable_package_name(path, project_env.as_ref()).await - { - variables.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name); - } + { + variables.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name); } if let Some(path) = local_abs_path.as_ref() && let Some((target, manifest_path)) = diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index 7937adbc09..afc84c3aff 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -341,10 +341,10 @@ async fn detect_package_manager( fs: Arc<dyn Fs>, package_json_data: Option<PackageJsonData>, ) -> &'static str { - if let Some(package_json_data) = package_json_data { - if let Some(package_manager) = package_json_data.package_manager { - return package_manager; - } + if let Some(package_json_data) = package_json_data + && let Some(package_manager) = package_json_data.package_manager + { + return package_manager; } if fs.is_file(&worktree_root.join("pnpm-lock.yaml")).await { return "pnpm"; diff --git a/crates/livekit_client/src/test.rs b/crates/livekit_client/src/test.rs index e0058d1163..873e0222d0 100644 --- a/crates/livekit_client/src/test.rs +++ b/crates/livekit_client/src/test.rs @@ -736,14 +736,14 @@ impl Room { impl Drop for RoomState { fn drop(&mut self) { - if self.connection_state == ConnectionState::Connected { - if let Ok(server) = TestServer::get(&self.url) { - let executor = server.executor.clone(); - let token = self.token.clone(); - executor - .spawn(async move { server.leave_room(token).await.ok() }) - .detach(); - } + if self.connection_state == ConnectionState::Connected + && let Ok(server) = TestServer::get(&self.url) + { + let executor = server.executor.clone(); + let token = self.token.clone(); + executor + .spawn(async move { server.leave_room(token).await.ok() }) + .detach(); } } } diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index e5709bc07c..7939e97e48 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -340,27 +340,26 @@ impl Markdown { } for (range, event) in &events { - if let MarkdownEvent::Start(MarkdownTag::Image { dest_url, .. }) = event { - if let Some(data_url) = dest_url.strip_prefix("data:") { - let Some((mime_info, data)) = data_url.split_once(',') else { - continue; - }; - let Some((mime_type, encoding)) = mime_info.split_once(';') else { - continue; - }; - let Some(format) = ImageFormat::from_mime_type(mime_type) else { - continue; - }; - let is_base64 = encoding == "base64"; - if is_base64 { - if let Some(bytes) = base64::prelude::BASE64_STANDARD - .decode(data) - .log_with_level(Level::Debug) - { - let image = Arc::new(Image::from_bytes(format, bytes)); - images_by_source_offset.insert(range.start, image); - } - } + if let MarkdownEvent::Start(MarkdownTag::Image { dest_url, .. }) = event + && let Some(data_url) = dest_url.strip_prefix("data:") + { + let Some((mime_info, data)) = data_url.split_once(',') else { + continue; + }; + let Some((mime_type, encoding)) = mime_info.split_once(';') else { + continue; + }; + let Some(format) = ImageFormat::from_mime_type(mime_type) else { + continue; + }; + let is_base64 = encoding == "base64"; + if is_base64 + && let Some(bytes) = base64::prelude::BASE64_STANDARD + .decode(data) + .log_with_level(Level::Debug) + { + let image = Arc::new(Image::from_bytes(format, bytes)); + images_by_source_offset.insert(range.start, image); } } } @@ -659,13 +658,13 @@ impl MarkdownElement { let rendered_text = rendered_text.clone(); move |markdown, event: &MouseUpEvent, phase, window, cx| { if phase.bubble() { - if let Some(pressed_link) = markdown.pressed_link.take() { - if Some(&pressed_link) == rendered_text.link_for_position(event.position) { - if let Some(open_url) = on_open_url.as_ref() { - open_url(pressed_link.destination_url, window, cx); - } else { - cx.open_url(&pressed_link.destination_url); - } + if let Some(pressed_link) = markdown.pressed_link.take() + && Some(&pressed_link) == rendered_text.link_for_position(event.position) + { + if let Some(open_url) = on_open_url.as_ref() { + open_url(pressed_link.destination_url, window, cx); + } else { + cx.open_url(&pressed_link.destination_url); } } } else if markdown.selection.pending { @@ -758,10 +757,10 @@ impl Element for MarkdownElement { let mut current_img_block_range: Option<Range<usize>> = None; for (range, event) in parsed_markdown.events.iter() { // Skip alt text for images that rendered - if let Some(current_img_block_range) = ¤t_img_block_range { - if current_img_block_range.end > range.end { - continue; - } + if let Some(current_img_block_range) = ¤t_img_block_range + && current_img_block_range.end > range.end + { + continue; } match event { @@ -1696,10 +1695,10 @@ impl RenderedText { while let Some(line) = lines.next() { let line_bounds = line.layout.bounds(); if position.y > line_bounds.bottom() { - if let Some(next_line) = lines.peek() { - if position.y < next_line.layout.bounds().top() { - return Err(line.source_end); - } + if let Some(next_line) = lines.peek() + && position.y < next_line.layout.bounds().top() + { + return Err(line.source_end); } continue; diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index 27691f2ecf..890d564b7a 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -300,13 +300,12 @@ impl<'a> MarkdownParser<'a> { if style != MarkdownHighlightStyle::default() && last_run_len < text.len() { let mut new_highlight = true; - if let Some((last_range, last_style)) = highlights.last_mut() { - if last_range.end == last_run_len - && last_style == &MarkdownHighlight::Style(style.clone()) - { - last_range.end = text.len(); - new_highlight = false; - } + if let Some((last_range, last_style)) = highlights.last_mut() + && last_range.end == last_run_len + && last_style == &MarkdownHighlight::Style(style.clone()) + { + last_range.end = text.len(); + new_highlight = false; } if new_highlight { highlights.push(( @@ -579,10 +578,10 @@ impl<'a> MarkdownParser<'a> { } } else { let block = self.parse_block().await; - if let Some(block) = block { - if let Some(list_item) = items_stack.last_mut() { - list_item.content.extend(block); - } + if let Some(block) = block + && let Some(list_item) = items_stack.last_mut() + { + list_item.content.extend(block); } } } diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index a0c8819991..c2b98f69c8 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -151,10 +151,9 @@ impl MarkdownPreviewView { if let Some(editor) = workspace .active_item(cx) .and_then(|item| item.act_as::<Editor>(cx)) + && Self::is_markdown_file(&editor, cx) { - if Self::is_markdown_file(&editor, cx) { - return Some(editor); - } + return Some(editor); } None } @@ -243,32 +242,30 @@ impl MarkdownPreviewView { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(item) = active_item { - if item.item_id() != cx.entity_id() { - if let Some(editor) = item.act_as::<Editor>(cx) { - if Self::is_markdown_file(&editor, cx) { - self.set_editor(editor, window, cx); - } - } - } + if let Some(item) = active_item + && item.item_id() != cx.entity_id() + && let Some(editor) = item.act_as::<Editor>(cx) + && Self::is_markdown_file(&editor, cx) + { + self.set_editor(editor, window, cx); } } pub fn is_markdown_file<V>(editor: &Entity<Editor>, cx: &mut Context<V>) -> bool { let buffer = editor.read(cx).buffer().read(cx); - if let Some(buffer) = buffer.as_singleton() { - if let Some(language) = buffer.read(cx).language() { - return language.name() == "Markdown".into(); - } + if let Some(buffer) = buffer.as_singleton() + && let Some(language) = buffer.read(cx).language() + { + return language.name() == "Markdown".into(); } false } fn set_editor(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) { - if let Some(active) = &self.active_editor { - if active.editor == editor { - return; - } + if let Some(active) = &self.active_editor + && active.editor == editor + { + return; } let subscription = cx.subscribe_in( @@ -552,21 +549,20 @@ impl Render for MarkdownPreviewView { .group("markdown-block") .on_click(cx.listener( move |this, event: &ClickEvent, window, cx| { - if event.click_count() == 2 { - if let Some(source_range) = this + if event.click_count() == 2 + && let Some(source_range) = this .contents .as_ref() .and_then(|c| c.children.get(ix)) .and_then(|block: &ParsedMarkdownElement| { block.source_range() }) - { - this.move_cursor_to_block( - window, - cx, - source_range.start..source_range.start, - ); - } + { + this.move_cursor_to_block( + window, + cx, + source_range.start..source_range.start, + ); } }, )) diff --git a/crates/migrator/src/migrations/m_2025_06_16/settings.rs b/crates/migrator/src/migrations/m_2025_06_16/settings.rs index cce407e21b..cd79eae204 100644 --- a/crates/migrator/src/migrations/m_2025_06_16/settings.rs +++ b/crates/migrator/src/migrations/m_2025_06_16/settings.rs @@ -40,20 +40,20 @@ fn migrate_context_server_settings( // Parse the server settings to check what keys it contains let mut cursor = server_settings.walk(); for child in server_settings.children(&mut cursor) { - if child.kind() == "pair" { - if let Some(key_node) = child.child_by_field_name("key") { - if let (None, Some(quote_content)) = (column, key_node.child(0)) { - column = Some(quote_content.start_position().column); - } - if let Some(string_content) = key_node.child(1) { - let key = &contents[string_content.byte_range()]; - match key { - // If it already has a source key, don't modify it - "source" => return None, - "command" => has_command = true, - "settings" => has_settings = true, - _ => other_keys += 1, - } + if child.kind() == "pair" + && let Some(key_node) = child.child_by_field_name("key") + { + if let (None, Some(quote_content)) = (column, key_node.child(0)) { + column = Some(quote_content.start_position().column); + } + if let Some(string_content) = key_node.child(1) { + let key = &contents[string_content.byte_range()]; + match key { + // If it already has a source key, don't modify it + "source" => return None, + "command" => has_command = true, + "settings" => has_settings = true, + _ => other_keys += 1, } } } diff --git a/crates/migrator/src/migrations/m_2025_06_25/settings.rs b/crates/migrator/src/migrations/m_2025_06_25/settings.rs index 5dd6c3093a..2bf7658eeb 100644 --- a/crates/migrator/src/migrations/m_2025_06_25/settings.rs +++ b/crates/migrator/src/migrations/m_2025_06_25/settings.rs @@ -84,10 +84,10 @@ fn remove_pair_with_whitespace( } } else { // If no next sibling, check if there's a comma before - if let Some(prev_sibling) = pair_node.prev_sibling() { - if prev_sibling.kind() == "," { - range_to_remove.start = prev_sibling.start_byte(); - } + if let Some(prev_sibling) = pair_node.prev_sibling() + && prev_sibling.kind() == "," + { + range_to_remove.start = prev_sibling.start_byte(); } } @@ -123,10 +123,10 @@ fn remove_pair_with_whitespace( // Also check if we need to include trailing whitespace up to the next line let text_after = &contents[range_to_remove.end..]; - if let Some(newline_pos) = text_after.find('\n') { - if text_after[..newline_pos].chars().all(|c| c.is_whitespace()) { - range_to_remove.end += newline_pos + 1; - } + if let Some(newline_pos) = text_after.find('\n') + && text_after[..newline_pos].chars().all(|c| c.is_whitespace()) + { + range_to_remove.end += newline_pos + 1; } Some((range_to_remove, String::new())) diff --git a/crates/migrator/src/migrations/m_2025_06_27/settings.rs b/crates/migrator/src/migrations/m_2025_06_27/settings.rs index 6156308fce..e3e951b1a6 100644 --- a/crates/migrator/src/migrations/m_2025_06_27/settings.rs +++ b/crates/migrator/src/migrations/m_2025_06_27/settings.rs @@ -56,19 +56,18 @@ fn flatten_context_server_command( let mut cursor = command_object.walk(); for child in command_object.children(&mut cursor) { - if child.kind() == "pair" { - if let Some(key_node) = child.child_by_field_name("key") { - if let Some(string_content) = key_node.child(1) { - let key = &contents[string_content.byte_range()]; - if let Some(value_node) = child.child_by_field_name("value") { - let value_range = value_node.byte_range(); - match key { - "path" => path_value = Some(&contents[value_range]), - "args" => args_value = Some(&contents[value_range]), - "env" => env_value = Some(&contents[value_range]), - _ => {} - } - } + if child.kind() == "pair" + && let Some(key_node) = child.child_by_field_name("key") + && let Some(string_content) = key_node.child(1) + { + let key = &contents[string_content.byte_range()]; + if let Some(value_node) = child.child_by_field_name("value") { + let value_range = value_node.byte_range(); + match key { + "path" => path_value = Some(&contents[value_range]), + "args" => args_value = Some(&contents[value_range]), + "env" => env_value = Some(&contents[value_range]), + _ => {} } } } diff --git a/crates/multi_buffer/src/anchor.rs b/crates/multi_buffer/src/anchor.rs index 8584519d56..6bed0a4028 100644 --- a/crates/multi_buffer/src/anchor.rs +++ b/crates/multi_buffer/src/anchor.rs @@ -76,27 +76,26 @@ impl Anchor { if text_cmp.is_ne() { return text_cmp; } - if self.diff_base_anchor.is_some() || other.diff_base_anchor.is_some() { - if let Some(base_text) = snapshot + if (self.diff_base_anchor.is_some() || other.diff_base_anchor.is_some()) + && let Some(base_text) = snapshot .diffs .get(&excerpt.buffer_id) .map(|diff| diff.base_text()) - { - let self_anchor = self.diff_base_anchor.filter(|a| base_text.can_resolve(a)); - let other_anchor = other.diff_base_anchor.filter(|a| base_text.can_resolve(a)); - return match (self_anchor, other_anchor) { - (Some(a), Some(b)) => a.cmp(&b, base_text), - (Some(_), None) => match other.text_anchor.bias { - Bias::Left => Ordering::Greater, - Bias::Right => Ordering::Less, - }, - (None, Some(_)) => match self.text_anchor.bias { - Bias::Left => Ordering::Less, - Bias::Right => Ordering::Greater, - }, - (None, None) => Ordering::Equal, - }; - } + { + let self_anchor = self.diff_base_anchor.filter(|a| base_text.can_resolve(a)); + let other_anchor = other.diff_base_anchor.filter(|a| base_text.can_resolve(a)); + return match (self_anchor, other_anchor) { + (Some(a), Some(b)) => a.cmp(&b, base_text), + (Some(_), None) => match other.text_anchor.bias { + Bias::Left => Ordering::Greater, + Bias::Right => Ordering::Less, + }, + (None, Some(_)) => match self.text_anchor.bias { + Bias::Left => Ordering::Less, + Bias::Right => Ordering::Greater, + }, + (None, None) => Ordering::Equal, + }; } } Ordering::Equal @@ -107,51 +106,49 @@ impl Anchor { } pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor { - if self.text_anchor.bias != Bias::Left { - if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { - return Self { - buffer_id: self.buffer_id, - excerpt_id: self.excerpt_id, - text_anchor: self.text_anchor.bias_left(&excerpt.buffer), - diff_base_anchor: self.diff_base_anchor.map(|a| { - if let Some(base_text) = snapshot - .diffs - .get(&excerpt.buffer_id) - .map(|diff| diff.base_text()) - { - if a.buffer_id == Some(base_text.remote_id()) { - return a.bias_left(base_text); - } - } - a - }), - }; - } + if self.text_anchor.bias != Bias::Left + && let Some(excerpt) = snapshot.excerpt(self.excerpt_id) + { + return Self { + buffer_id: self.buffer_id, + excerpt_id: self.excerpt_id, + text_anchor: self.text_anchor.bias_left(&excerpt.buffer), + diff_base_anchor: self.diff_base_anchor.map(|a| { + if let Some(base_text) = snapshot + .diffs + .get(&excerpt.buffer_id) + .map(|diff| diff.base_text()) + && a.buffer_id == Some(base_text.remote_id()) + { + return a.bias_left(base_text); + } + a + }), + }; } *self } pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor { - if self.text_anchor.bias != Bias::Right { - if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { - return Self { - buffer_id: self.buffer_id, - excerpt_id: self.excerpt_id, - text_anchor: self.text_anchor.bias_right(&excerpt.buffer), - diff_base_anchor: self.diff_base_anchor.map(|a| { - if let Some(base_text) = snapshot - .diffs - .get(&excerpt.buffer_id) - .map(|diff| diff.base_text()) - { - if a.buffer_id == Some(base_text.remote_id()) { - return a.bias_right(base_text); - } - } - a - }), - }; - } + if self.text_anchor.bias != Bias::Right + && let Some(excerpt) = snapshot.excerpt(self.excerpt_id) + { + return Self { + buffer_id: self.buffer_id, + excerpt_id: self.excerpt_id, + text_anchor: self.text_anchor.bias_right(&excerpt.buffer), + diff_base_anchor: self.diff_base_anchor.map(|a| { + if let Some(base_text) = snapshot + .diffs + .get(&excerpt.buffer_id) + .map(|diff| diff.base_text()) + && a.buffer_id == Some(base_text.remote_id()) + { + return a.bias_right(base_text); + } + a + }), + }; } *self } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 59eaa9934d..ab5f148d6c 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1082,11 +1082,11 @@ impl MultiBuffer { let mut ranges: Vec<Range<usize>> = Vec::new(); for edit in edits { - if let Some(last_range) = ranges.last_mut() { - if edit.range.start <= last_range.end { - last_range.end = last_range.end.max(edit.range.end); - continue; - } + if let Some(last_range) = ranges.last_mut() + && edit.range.start <= last_range.end + { + last_range.end = last_range.end.max(edit.range.end); + continue; } ranges.push(edit.range); } @@ -1212,25 +1212,24 @@ impl MultiBuffer { for range in buffer.edited_ranges_for_transaction_id::<D>(*buffer_transaction) { for excerpt_id in &buffer_state.excerpts { cursor.seek(excerpt_id, Bias::Left); - if let Some(excerpt) = cursor.item() { - if excerpt.locator == *excerpt_id { - let excerpt_buffer_start = - excerpt.range.context.start.summary::<D>(buffer); - let excerpt_buffer_end = excerpt.range.context.end.summary::<D>(buffer); - let excerpt_range = excerpt_buffer_start..excerpt_buffer_end; - if excerpt_range.contains(&range.start) - && excerpt_range.contains(&range.end) - { - let excerpt_start = D::from_text_summary(&cursor.start().text); + if let Some(excerpt) = cursor.item() + && excerpt.locator == *excerpt_id + { + let excerpt_buffer_start = excerpt.range.context.start.summary::<D>(buffer); + let excerpt_buffer_end = excerpt.range.context.end.summary::<D>(buffer); + let excerpt_range = excerpt_buffer_start..excerpt_buffer_end; + if excerpt_range.contains(&range.start) + && excerpt_range.contains(&range.end) + { + let excerpt_start = D::from_text_summary(&cursor.start().text); - let mut start = excerpt_start; - start.add_assign(&(range.start - excerpt_buffer_start)); - let mut end = excerpt_start; - end.add_assign(&(range.end - excerpt_buffer_start)); + let mut start = excerpt_start; + start.add_assign(&(range.start - excerpt_buffer_start)); + let mut end = excerpt_start; + end.add_assign(&(range.end - excerpt_buffer_start)); - ranges.push(start..end); - break; - } + ranges.push(start..end); + break; } } } @@ -1251,25 +1250,25 @@ impl MultiBuffer { buffer.update(cx, |buffer, _| { buffer.merge_transactions(transaction, destination) }); - } else if let Some(transaction) = self.history.forget(transaction) { - if let Some(destination) = self.history.transaction_mut(destination) { - for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions { - if let Some(destination_buffer_transaction_id) = - destination.buffer_transactions.get(&buffer_id) - { - if let Some(state) = self.buffers.borrow().get(&buffer_id) { - state.buffer.update(cx, |buffer, _| { - buffer.merge_transactions( - buffer_transaction_id, - *destination_buffer_transaction_id, - ) - }); - } - } else { - destination - .buffer_transactions - .insert(buffer_id, buffer_transaction_id); + } else if let Some(transaction) = self.history.forget(transaction) + && let Some(destination) = self.history.transaction_mut(destination) + { + for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions { + if let Some(destination_buffer_transaction_id) = + destination.buffer_transactions.get(&buffer_id) + { + if let Some(state) = self.buffers.borrow().get(&buffer_id) { + state.buffer.update(cx, |buffer, _| { + buffer.merge_transactions( + buffer_transaction_id, + *destination_buffer_transaction_id, + ) + }); } + } else { + destination + .buffer_transactions + .insert(buffer_id, buffer_transaction_id); } } } @@ -1562,11 +1561,11 @@ impl MultiBuffer { }); let mut merged_ranges: Vec<ExcerptRange<Point>> = Vec::new(); for range in expanded_ranges { - if let Some(last_range) = merged_ranges.last_mut() { - if last_range.context.end >= range.context.start { - last_range.context.end = range.context.end; - continue; - } + if let Some(last_range) = merged_ranges.last_mut() + && last_range.context.end >= range.context.start + { + last_range.context.end = range.context.end; + continue; } merged_ranges.push(range) } @@ -1794,25 +1793,25 @@ impl MultiBuffer { }; if let Some((last_id, last)) = to_insert.last_mut() { - if let Some(new) = new { - if last.context.end >= new.context.start { - last.context.end = last.context.end.max(new.context.end); - excerpt_ids.push(*last_id); - new_iter.next(); - continue; - } + if let Some(new) = new + && last.context.end >= new.context.start + { + last.context.end = last.context.end.max(new.context.end); + excerpt_ids.push(*last_id); + new_iter.next(); + continue; } - if let Some((existing_id, existing_range)) = &existing { - if last.context.end >= existing_range.start { - last.context.end = last.context.end.max(existing_range.end); - to_remove.push(*existing_id); - self.snapshot - .borrow_mut() - .replaced_excerpts - .insert(*existing_id, *last_id); - existing_iter.next(); - continue; - } + if let Some((existing_id, existing_range)) = &existing + && last.context.end >= existing_range.start + { + last.context.end = last.context.end.max(existing_range.end); + to_remove.push(*existing_id); + self.snapshot + .borrow_mut() + .replaced_excerpts + .insert(*existing_id, *last_id); + existing_iter.next(); + continue; } } @@ -2105,10 +2104,10 @@ impl MultiBuffer { .flatten() { cursor.seek_forward(&Some(locator), Bias::Left); - if let Some(excerpt) = cursor.item() { - if excerpt.locator == *locator { - excerpts.push((excerpt.id, excerpt.range.clone())); - } + if let Some(excerpt) = cursor.item() + && excerpt.locator == *locator + { + excerpts.push((excerpt.id, excerpt.range.clone())); } } @@ -2132,22 +2131,21 @@ impl MultiBuffer { let mut result = Vec::new(); for locator in locators { excerpts.seek_forward(&Some(locator), Bias::Left); - if let Some(excerpt) = excerpts.item() { - if excerpt.locator == *locator { - let excerpt_start = excerpts.start().1.clone(); - let excerpt_end = - ExcerptDimension(excerpt_start.0 + excerpt.text_summary.lines); + if let Some(excerpt) = excerpts.item() + && excerpt.locator == *locator + { + let excerpt_start = excerpts.start().1.clone(); + let excerpt_end = ExcerptDimension(excerpt_start.0 + excerpt.text_summary.lines); - diff_transforms.seek_forward(&excerpt_start, Bias::Left); - let overshoot = excerpt_start.0 - diff_transforms.start().0.0; - let start = diff_transforms.start().1.0 + overshoot; + diff_transforms.seek_forward(&excerpt_start, Bias::Left); + let overshoot = excerpt_start.0 - diff_transforms.start().0.0; + let start = diff_transforms.start().1.0 + overshoot; - diff_transforms.seek_forward(&excerpt_end, Bias::Right); - let overshoot = excerpt_end.0 - diff_transforms.start().0.0; - let end = diff_transforms.start().1.0 + overshoot; + diff_transforms.seek_forward(&excerpt_end, Bias::Right); + let overshoot = excerpt_end.0 - diff_transforms.start().0.0; + let end = diff_transforms.start().1.0 + overshoot; - result.push(start..end) - } + result.push(start..end) } } result @@ -2316,12 +2314,12 @@ impl MultiBuffer { // Skip over any subsequent excerpts that are also removed. if let Some(&next_excerpt_id) = excerpt_ids.peek() { let next_locator = snapshot.excerpt_locator_for_id(next_excerpt_id); - if let Some(next_excerpt) = cursor.item() { - if next_excerpt.locator == *next_locator { - excerpt_ids.next(); - excerpt = next_excerpt; - continue 'remove_excerpts; - } + if let Some(next_excerpt) = cursor.item() + && next_excerpt.locator == *next_locator + { + excerpt_ids.next(); + excerpt = next_excerpt; + continue 'remove_excerpts; } } @@ -2494,33 +2492,33 @@ impl MultiBuffer { .excerpts .cursor::<Dimensions<Option<&Locator>, ExcerptOffset>>(&()); cursor.seek_forward(&Some(locator), Bias::Left); - if let Some(excerpt) = cursor.item() { - if excerpt.locator == *locator { - let excerpt_buffer_range = excerpt.range.context.to_offset(&excerpt.buffer); - if diff_change_range.end < excerpt_buffer_range.start - || diff_change_range.start > excerpt_buffer_range.end - { - continue; - } - let excerpt_start = cursor.start().1; - let excerpt_len = ExcerptOffset::new(excerpt.text_summary.len); - let diff_change_start_in_excerpt = ExcerptOffset::new( - diff_change_range - .start - .saturating_sub(excerpt_buffer_range.start), - ); - let diff_change_end_in_excerpt = ExcerptOffset::new( - diff_change_range - .end - .saturating_sub(excerpt_buffer_range.start), - ); - let edit_start = excerpt_start + diff_change_start_in_excerpt.min(excerpt_len); - let edit_end = excerpt_start + diff_change_end_in_excerpt.min(excerpt_len); - excerpt_edits.push(Edit { - old: edit_start..edit_end, - new: edit_start..edit_end, - }); + if let Some(excerpt) = cursor.item() + && excerpt.locator == *locator + { + let excerpt_buffer_range = excerpt.range.context.to_offset(&excerpt.buffer); + if diff_change_range.end < excerpt_buffer_range.start + || diff_change_range.start > excerpt_buffer_range.end + { + continue; } + let excerpt_start = cursor.start().1; + let excerpt_len = ExcerptOffset::new(excerpt.text_summary.len); + let diff_change_start_in_excerpt = ExcerptOffset::new( + diff_change_range + .start + .saturating_sub(excerpt_buffer_range.start), + ); + let diff_change_end_in_excerpt = ExcerptOffset::new( + diff_change_range + .end + .saturating_sub(excerpt_buffer_range.start), + ); + let edit_start = excerpt_start + diff_change_start_in_excerpt.min(excerpt_len); + let edit_end = excerpt_start + diff_change_end_in_excerpt.min(excerpt_len); + excerpt_edits.push(Edit { + old: edit_start..edit_end, + new: edit_start..edit_end, + }); } } @@ -3155,13 +3153,12 @@ impl MultiBuffer { at_transform_boundary = false; let transforms_before_edit = old_diff_transforms.slice(&edit.old.start, Bias::Left); self.append_diff_transforms(&mut new_diff_transforms, transforms_before_edit); - if let Some(transform) = old_diff_transforms.item() { - if old_diff_transforms.end().0 == edit.old.start - && old_diff_transforms.start().0 < edit.old.start - { - self.push_diff_transform(&mut new_diff_transforms, transform.clone()); - old_diff_transforms.next(); - } + if let Some(transform) = old_diff_transforms.item() + && old_diff_transforms.end().0 == edit.old.start + && old_diff_transforms.start().0 < edit.old.start + { + self.push_diff_transform(&mut new_diff_transforms, transform.clone()); + old_diff_transforms.next(); } } @@ -3431,18 +3428,17 @@ impl MultiBuffer { inserted_hunk_info, summary, }) = subtree.first() - { - if self.extend_last_buffer_content_transform( + && self.extend_last_buffer_content_transform( new_transforms, *inserted_hunk_info, *summary, - ) { - let mut cursor = subtree.cursor::<()>(&()); - cursor.next(); - cursor.next(); - new_transforms.append(cursor.suffix(), &()); - return; - } + ) + { + let mut cursor = subtree.cursor::<()>(&()); + cursor.next(); + cursor.next(); + new_transforms.append(cursor.suffix(), &()); + return; } new_transforms.append(subtree, &()); } @@ -3456,14 +3452,13 @@ impl MultiBuffer { inserted_hunk_info: inserted_hunk_anchor, summary, } = transform - { - if self.extend_last_buffer_content_transform( + && self.extend_last_buffer_content_transform( new_transforms, inserted_hunk_anchor, summary, - ) { - return; - } + ) + { + return; } new_transforms.push(transform, &()); } @@ -3518,11 +3513,10 @@ impl MultiBuffer { summary, inserted_hunk_info: inserted_hunk_anchor, } = last_transform + && *inserted_hunk_anchor == new_inserted_hunk_info { - if *inserted_hunk_anchor == new_inserted_hunk_info { - *summary += summary_to_add; - did_extend = true; - } + *summary += summary_to_add; + did_extend = true; } }, &(), @@ -4037,10 +4031,10 @@ impl MultiBufferSnapshot { cursor.seek(&query_range.start); - if let Some(region) = cursor.region().filter(|region| !region.is_main_buffer) { - if region.range.start > D::zero(&()) { - cursor.prev() - } + if let Some(region) = cursor.region().filter(|region| !region.is_main_buffer) + && region.range.start > D::zero(&()) + { + cursor.prev() } iter::from_fn(move || { @@ -4070,10 +4064,10 @@ impl MultiBufferSnapshot { buffer_start = cursor.main_buffer_position()?; }; let mut buffer_end = excerpt.range.context.end.summary::<D>(&excerpt.buffer); - if let Some((end_excerpt_id, end_buffer_offset)) = range_end { - if excerpt.id == end_excerpt_id { - buffer_end = buffer_end.min(end_buffer_offset); - } + if let Some((end_excerpt_id, end_buffer_offset)) = range_end + && excerpt.id == end_excerpt_id + { + buffer_end = buffer_end.min(end_buffer_offset); } if let Some(iterator) = @@ -4144,10 +4138,10 @@ impl MultiBufferSnapshot { // When there are no more metadata items for this excerpt, move to the next excerpt. else { current_excerpt_metadata.take(); - if let Some((end_excerpt_id, _)) = range_end { - if excerpt.id == end_excerpt_id { - return None; - } + if let Some((end_excerpt_id, _)) = range_end + && excerpt.id == end_excerpt_id + { + return None; } cursor.next_excerpt(); } @@ -4622,20 +4616,20 @@ impl MultiBufferSnapshot { pub fn indent_and_comment_for_line(&self, row: MultiBufferRow, cx: &App) -> String { let mut indent = self.indent_size_for_line(row).chars().collect::<String>(); - if self.language_settings(cx).extend_comment_on_newline { - if let Some(language_scope) = self.language_scope_at(Point::new(row.0, 0)) { - let delimiters = language_scope.line_comment_prefixes(); - for delimiter in delimiters { - if *self - .chars_at(Point::new(row.0, indent.len() as u32)) - .take(delimiter.chars().count()) - .collect::<String>() - .as_str() - == **delimiter - { - indent.push_str(delimiter); - break; - } + if self.language_settings(cx).extend_comment_on_newline + && let Some(language_scope) = self.language_scope_at(Point::new(row.0, 0)) + { + let delimiters = language_scope.line_comment_prefixes(); + for delimiter in delimiters { + if *self + .chars_at(Point::new(row.0, indent.len() as u32)) + .take(delimiter.chars().count()) + .collect::<String>() + .as_str() + == **delimiter + { + indent.push_str(delimiter); + break; } } } @@ -4893,25 +4887,22 @@ impl MultiBufferSnapshot { base_text_byte_range, .. }) => { - if let Some(diff_base_anchor) = &anchor.diff_base_anchor { - if let Some(base_text) = + if let Some(diff_base_anchor) = &anchor.diff_base_anchor + && let Some(base_text) = self.diffs.get(buffer_id).map(|diff| diff.base_text()) + && base_text.can_resolve(diff_base_anchor) + { + let base_text_offset = diff_base_anchor.to_offset(base_text); + if base_text_offset >= base_text_byte_range.start + && base_text_offset <= base_text_byte_range.end { - if base_text.can_resolve(diff_base_anchor) { - let base_text_offset = diff_base_anchor.to_offset(base_text); - if base_text_offset >= base_text_byte_range.start - && base_text_offset <= base_text_byte_range.end - { - let position_in_hunk = base_text - .text_summary_for_range::<D, _>( - base_text_byte_range.start..base_text_offset, - ); - position.add_assign(&position_in_hunk); - } else if at_transform_end { - diff_transforms.next(); - continue; - } - } + let position_in_hunk = base_text.text_summary_for_range::<D, _>( + base_text_byte_range.start..base_text_offset, + ); + position.add_assign(&position_in_hunk); + } else if at_transform_end { + diff_transforms.next(); + continue; } } } @@ -4941,20 +4932,19 @@ impl MultiBufferSnapshot { } let mut position = cursor.start().1; - if let Some(excerpt) = cursor.item() { - if excerpt.id == anchor.excerpt_id { - let excerpt_buffer_start = excerpt - .buffer - .offset_for_anchor(&excerpt.range.context.start); - let excerpt_buffer_end = - excerpt.buffer.offset_for_anchor(&excerpt.range.context.end); - let buffer_position = cmp::min( - excerpt_buffer_end, - excerpt.buffer.offset_for_anchor(&anchor.text_anchor), - ); - if buffer_position > excerpt_buffer_start { - position.value += buffer_position - excerpt_buffer_start; - } + if let Some(excerpt) = cursor.item() + && excerpt.id == anchor.excerpt_id + { + let excerpt_buffer_start = excerpt + .buffer + .offset_for_anchor(&excerpt.range.context.start); + let excerpt_buffer_end = excerpt.buffer.offset_for_anchor(&excerpt.range.context.end); + let buffer_position = cmp::min( + excerpt_buffer_end, + excerpt.buffer.offset_for_anchor(&anchor.text_anchor), + ); + if buffer_position > excerpt_buffer_start { + position.value += buffer_position - excerpt_buffer_start; } } position @@ -5211,14 +5201,15 @@ impl MultiBufferSnapshot { .cursor::<Dimensions<usize, ExcerptOffset>>(&()); diff_transforms.seek(&offset, Bias::Right); - if offset == diff_transforms.start().0 && bias == Bias::Left { - if let Some(prev_item) = diff_transforms.prev_item() { - match prev_item { - DiffTransform::DeletedHunk { .. } => { - diff_transforms.prev(); - } - _ => {} + if offset == diff_transforms.start().0 + && bias == Bias::Left + && let Some(prev_item) = diff_transforms.prev_item() + { + match prev_item { + DiffTransform::DeletedHunk { .. } => { + diff_transforms.prev(); } + _ => {} } } let offset_in_transform = offset - diff_transforms.start().0; @@ -5296,17 +5287,17 @@ impl MultiBufferSnapshot { let locator = self.excerpt_locator_for_id(excerpt_id); let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&()); cursor.seek(locator, Bias::Left); - if let Some(excerpt) = cursor.item() { - if excerpt.id == excerpt_id { - let text_anchor = excerpt.clip_anchor(text_anchor); - drop(cursor); - return Some(Anchor { - buffer_id: Some(excerpt.buffer_id), - excerpt_id, - text_anchor, - diff_base_anchor: None, - }); - } + if let Some(excerpt) = cursor.item() + && excerpt.id == excerpt_id + { + let text_anchor = excerpt.clip_anchor(text_anchor); + drop(cursor); + return Some(Anchor { + buffer_id: Some(excerpt.buffer_id), + excerpt_id, + text_anchor, + diff_base_anchor: None, + }); } None } @@ -5860,10 +5851,10 @@ impl MultiBufferSnapshot { let current_depth = indent_stack.len() as u32; // Avoid retrieving the language settings repeatedly for every buffer row. - if let Some((prev_buffer_id, _)) = &prev_settings { - if prev_buffer_id != &buffer.remote_id() { - prev_settings.take(); - } + if let Some((prev_buffer_id, _)) = &prev_settings + && prev_buffer_id != &buffer.remote_id() + { + prev_settings.take(); } let settings = &prev_settings .get_or_insert_with(|| { @@ -6192,10 +6183,10 @@ impl MultiBufferSnapshot { } else { let mut cursor = self.excerpt_ids.cursor::<ExcerptId>(&()); cursor.seek(&id, Bias::Left); - if let Some(entry) = cursor.item() { - if entry.id == id { - return &entry.locator; - } + if let Some(entry) = cursor.item() + && entry.id == id + { + return &entry.locator; } panic!("invalid excerpt id {id:?}") } @@ -6272,10 +6263,10 @@ impl MultiBufferSnapshot { pub fn buffer_range_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<Range<text::Anchor>> { let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&()); let locator = self.excerpt_locator_for_id(excerpt_id); - if cursor.seek(&Some(locator), Bias::Left) { - if let Some(excerpt) = cursor.item() { - return Some(excerpt.range.context.clone()); - } + if cursor.seek(&Some(locator), Bias::Left) + && let Some(excerpt) = cursor.item() + { + return Some(excerpt.range.context.clone()); } None } @@ -6284,10 +6275,10 @@ impl MultiBufferSnapshot { let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&()); let locator = self.excerpt_locator_for_id(excerpt_id); cursor.seek(&Some(locator), Bias::Left); - if let Some(excerpt) = cursor.item() { - if excerpt.id == excerpt_id { - return Some(excerpt); - } + if let Some(excerpt) = cursor.item() + && excerpt.id == excerpt_id + { + return Some(excerpt); } None } @@ -6446,13 +6437,12 @@ impl MultiBufferSnapshot { inserted_hunk_info: prev_inserted_hunk_info, .. }) = prev_transform + && *inserted_hunk_info == *prev_inserted_hunk_info { - if *inserted_hunk_info == *prev_inserted_hunk_info { - panic!( - "multiple adjacent buffer content transforms with is_inserted_hunk = {inserted_hunk_info:?}. transforms: {:+?}", - self.diff_transforms.items(&()) - ); - } + panic!( + "multiple adjacent buffer content transforms with is_inserted_hunk = {inserted_hunk_info:?}. transforms: {:+?}", + self.diff_transforms.items(&()) + ); } if summary.len == 0 && !self.is_empty() { panic!("empty buffer content transform"); @@ -6552,14 +6542,12 @@ where self.excerpts.next(); } else if let Some(DiffTransform::DeletedHunk { hunk_info, .. }) = self.diff_transforms.item() - { - if self + && self .excerpts .item() .map_or(false, |excerpt| excerpt.id != hunk_info.excerpt_id) - { - self.excerpts.next(); - } + { + self.excerpts.next(); } } } @@ -7855,10 +7843,11 @@ impl io::Read for ReversedMultiBufferBytes<'_> { if len > 0 { self.range.end -= len; self.chunk = &self.chunk[..self.chunk.len() - len]; - if !self.range.is_empty() && self.chunk.is_empty() { - if let Some(chunk) = self.chunks.next() { - self.chunk = chunk.as_bytes(); - } + if !self.range.is_empty() + && self.chunk.is_empty() + && let Some(chunk) = self.chunks.next() + { + self.chunk = chunk.as_bytes(); } } Ok(len) diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index fefeddb4da..598ee0f9cb 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -3592,24 +3592,20 @@ fn assert_position_translation(snapshot: &MultiBufferSnapshot) { for (anchors, bias) in [(&left_anchors, Bias::Left), (&right_anchors, Bias::Right)] { for (ix, (offset, anchor)) in offsets.iter().zip(anchors).enumerate() { - if ix > 0 { - if *offset == 252 { - if offset > &offsets[ix - 1] { - let prev_anchor = left_anchors[ix - 1]; - assert!( - anchor.cmp(&prev_anchor, snapshot).is_gt(), - "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_gt()", - offsets[ix], - offsets[ix - 1], - ); - assert!( - prev_anchor.cmp(anchor, snapshot).is_lt(), - "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_lt()", - offsets[ix - 1], - offsets[ix], - ); - } - } + if ix > 0 && *offset == 252 && offset > &offsets[ix - 1] { + let prev_anchor = left_anchors[ix - 1]; + assert!( + anchor.cmp(&prev_anchor, snapshot).is_gt(), + "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_gt()", + offsets[ix], + offsets[ix - 1], + ); + assert!( + prev_anchor.cmp(anchor, snapshot).is_lt(), + "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_lt()", + offsets[ix - 1], + offsets[ix], + ); } } } diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index 29653748e4..af2601bd18 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -138,10 +138,10 @@ impl NotificationStore { pub fn notification_for_id(&self, id: u64) -> Option<&NotificationEntry> { let mut cursor = self.notifications.cursor::<NotificationId>(&()); cursor.seek(&NotificationId(id), Bias::Left); - if let Some(item) = cursor.item() { - if item.id == id { - return Some(item); - } + if let Some(item) = cursor.item() + && item.id == id + { + return Some(item); } None } @@ -229,25 +229,24 @@ impl NotificationStore { mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |this, cx| { - if let Some(notification) = envelope.payload.notification { - if let Some(rpc::Notification::ChannelMessageMention { message_id, .. }) = + if let Some(notification) = envelope.payload.notification + && let Some(rpc::Notification::ChannelMessageMention { message_id, .. }) = Notification::from_proto(¬ification) - { - let fetch_message_task = this.channel_store.update(cx, |this, cx| { - this.fetch_channel_messages(vec![message_id], cx) - }); + { + let fetch_message_task = this.channel_store.update(cx, |this, cx| { + this.fetch_channel_messages(vec![message_id], cx) + }); - cx.spawn(async move |this, cx| { - let messages = fetch_message_task.await?; - this.update(cx, move |this, cx| { - for message in messages { - this.channel_messages.insert(message_id, message); - } - cx.notify(); - }) + cx.spawn(async move |this, cx| { + let messages = fetch_message_task.await?; + this.update(cx, move |this, cx| { + for message in messages { + this.channel_messages.insert(message_id, message); + } + cx.notify(); }) - .detach_and_log_err(cx) - } + }) + .detach_and_log_err(cx) } Ok(()) })? @@ -390,12 +389,12 @@ impl NotificationStore { }); } } - } else if let Some(new_notification) = &new_notification { - if is_new { - cx.emit(NotificationEvent::NewNotification { - entry: new_notification.clone(), - }); - } + } else if let Some(new_notification) = &new_notification + && is_new + { + cx.emit(NotificationEvent::NewNotification { + entry: new_notification.clone(), + }); } if let Some(notification) = new_notification { diff --git a/crates/open_router/src/open_router.rs b/crates/open_router/src/open_router.rs index 3e6e406d98..7c304bad64 100644 --- a/crates/open_router/src/open_router.rs +++ b/crates/open_router/src/open_router.rs @@ -240,10 +240,10 @@ impl MessageContent { impl From<Vec<MessagePart>> for MessageContent { fn from(parts: Vec<MessagePart>) -> Self { - if parts.len() == 1 { - if let MessagePart::Text { text } = &parts[0] { - return Self::Plain(text.clone()); - } + if parts.len() == 1 + && let MessagePart::Text { text } = &parts[0] + { + return Self::Plain(text.clone()); } Self::Multipart(parts) } diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 9514fd7e36..9b7ec473fd 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -1170,12 +1170,11 @@ impl OutlinePanel { }); } else { let mut offset = Point::default(); - if let Some(buffer_id) = scroll_to_buffer { - if multi_buffer_snapshot.as_singleton().is_none() - && !active_editor.read(cx).is_buffer_folded(buffer_id, cx) - { - offset.y = -(active_editor.read(cx).file_header_size() as f32); - } + if let Some(buffer_id) = scroll_to_buffer + && multi_buffer_snapshot.as_singleton().is_none() + && !active_editor.read(cx).is_buffer_folded(buffer_id, cx) + { + offset.y = -(active_editor.read(cx).file_header_size() as f32); } active_editor.update(cx, |editor, cx| { @@ -1606,16 +1605,14 @@ impl OutlinePanel { } PanelEntry::FoldedDirs(folded_dirs) => { let mut folded = false; - if let Some(dir_entry) = folded_dirs.entries.last() { - if self + if let Some(dir_entry) = folded_dirs.entries.last() + && self .collapsed_entries .insert(CollapsedEntry::Dir(folded_dirs.worktree_id, dir_entry.id)) - { - folded = true; - buffers_to_fold.extend( - self.buffers_inside_directory(folded_dirs.worktree_id, dir_entry), - ); - } + { + folded = true; + buffers_to_fold + .extend(self.buffers_inside_directory(folded_dirs.worktree_id, dir_entry)); } folded } @@ -2108,11 +2105,11 @@ impl OutlinePanel { dirs_to_expand.push(current_entry.id); } - if traversal.back_to_parent() { - if let Some(parent_entry) = traversal.entry() { - current_entry = parent_entry.clone(); - continue; - } + if traversal.back_to_parent() + && let Some(parent_entry) = traversal.entry() + { + current_entry = parent_entry.clone(); + continue; } break; } @@ -2475,17 +2472,17 @@ impl OutlinePanel { let search_data = match render_data.get() { Some(search_data) => search_data, None => { - if let ItemsDisplayMode::Search(search_state) = &mut self.mode { - if let Some(multi_buffer_snapshot) = multi_buffer_snapshot { - search_state - .highlight_search_match_tx - .try_send(HighlightArguments { - multi_buffer_snapshot: multi_buffer_snapshot.clone(), - match_range: match_range.clone(), - search_data: Arc::clone(render_data), - }) - .ok(); - } + if let ItemsDisplayMode::Search(search_state) = &mut self.mode + && let Some(multi_buffer_snapshot) = multi_buffer_snapshot + { + search_state + .highlight_search_match_tx + .try_send(HighlightArguments { + multi_buffer_snapshot: multi_buffer_snapshot.clone(), + match_range: match_range.clone(), + search_data: Arc::clone(render_data), + }) + .ok(); } return None; } @@ -2833,11 +2830,12 @@ impl OutlinePanel { let new_entry_added = entries_to_add .insert(current_entry.id, current_entry) .is_none(); - if new_entry_added && traversal.back_to_parent() { - if let Some(parent_entry) = traversal.entry() { - current_entry = parent_entry.to_owned(); - continue; - } + if new_entry_added + && traversal.back_to_parent() + && let Some(parent_entry) = traversal.entry() + { + current_entry = parent_entry.to_owned(); + continue; } break; } @@ -2878,18 +2876,17 @@ impl OutlinePanel { entries .into_iter() .filter_map(|entry| { - if auto_fold_dirs { - if let Some(parent) = entry.path.parent() { - let children = new_children_count - .entry(worktree_id) - .or_default() - .entry(Arc::from(parent)) - .or_default(); - if entry.is_dir() { - children.dirs += 1; - } else { - children.files += 1; - } + if auto_fold_dirs && let Some(parent) = entry.path.parent() + { + let children = new_children_count + .entry(worktree_id) + .or_default() + .entry(Arc::from(parent)) + .or_default(); + if entry.is_dir() { + children.dirs += 1; + } else { + children.files += 1; } } @@ -3409,30 +3406,29 @@ impl OutlinePanel { { excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines); - if let Some(default_depth) = pending_default_depth { - if let ExcerptOutlines::Outlines(outlines) = + if let Some(default_depth) = pending_default_depth + && let ExcerptOutlines::Outlines(outlines) = &excerpt.outlines - { - outlines - .iter() - .filter(|outline| { - (default_depth == 0 - || outline.depth >= default_depth) - && outlines_with_children.contains(&( - outline.range.clone(), - outline.depth, - )) - }) - .for_each(|outline| { - outline_panel.collapsed_entries.insert( - CollapsedEntry::Outline( - buffer_id, - excerpt_id, - outline.range.clone(), - ), - ); - }); - } + { + outlines + .iter() + .filter(|outline| { + (default_depth == 0 + || outline.depth >= default_depth) + && outlines_with_children.contains(&( + outline.range.clone(), + outline.depth, + )) + }) + .for_each(|outline| { + outline_panel.collapsed_entries.insert( + CollapsedEntry::Outline( + buffer_id, + excerpt_id, + outline.range.clone(), + ), + ); + }); } // Even if no outlines to check, we still need to update cached entries @@ -3611,10 +3607,9 @@ impl OutlinePanel { .update_in(cx, |outline_panel, window, cx| { outline_panel.cached_entries = new_cached_entries; outline_panel.max_width_item_index = max_width_item_index; - if outline_panel.selected_entry.is_invalidated() - || matches!(outline_panel.selected_entry, SelectedEntry::None) - { - if let Some(new_selected_entry) = + if (outline_panel.selected_entry.is_invalidated() + || matches!(outline_panel.selected_entry, SelectedEntry::None)) + && let Some(new_selected_entry) = outline_panel.active_editor().and_then(|active_editor| { outline_panel.location_for_editor_selection( &active_editor, @@ -3622,9 +3617,8 @@ impl OutlinePanel { cx, ) }) - { - outline_panel.select_entry(new_selected_entry, false, window, cx); - } + { + outline_panel.select_entry(new_selected_entry, false, window, cx); } outline_panel.autoscroll(cx); @@ -3921,19 +3915,19 @@ impl OutlinePanel { } else { None }; - if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider { - if !active_editor.read(cx).is_buffer_folded(buffer_id, cx) { - outline_panel.add_excerpt_entries( - &mut generation_state, - buffer_id, - entry_excerpts, - depth, - track_matches, - is_singleton, - query.as_deref(), - cx, - ); - } + if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider + && !active_editor.read(cx).is_buffer_folded(buffer_id, cx) + { + outline_panel.add_excerpt_entries( + &mut generation_state, + buffer_id, + entry_excerpts, + depth, + track_matches, + is_singleton, + query.as_deref(), + cx, + ); } } } @@ -4404,15 +4398,15 @@ impl OutlinePanel { }) .filter(|(match_range, _)| { let editor = active_editor.read(cx); - if let Some(buffer_id) = match_range.start.buffer_id { - if editor.is_buffer_folded(buffer_id, cx) { - return false; - } + if let Some(buffer_id) = match_range.start.buffer_id + && editor.is_buffer_folded(buffer_id, cx) + { + return false; } - if let Some(buffer_id) = match_range.start.buffer_id { - if editor.is_buffer_folded(buffer_id, cx) { - return false; - } + if let Some(buffer_id) = match_range.start.buffer_id + && editor.is_buffer_folded(buffer_id, cx) + { + return false; } true }); @@ -4456,16 +4450,14 @@ impl OutlinePanel { cx: &mut Context<Self>, ) { self.pinned = !self.pinned; - if !self.pinned { - if let Some((active_item, active_editor)) = self + if !self.pinned + && let Some((active_item, active_editor)) = self .workspace .upgrade() .and_then(|workspace| workspace_active_editor(workspace.read(cx), cx)) - { - if self.should_replace_active_item(active_item.as_ref()) { - self.replace_active_editor(active_item, active_editor, window, cx); - } - } + && self.should_replace_active_item(active_item.as_ref()) + { + self.replace_active_editor(active_item, active_editor, window, cx); } cx.notify(); @@ -5067,24 +5059,23 @@ impl Panel for OutlinePanel { let old_active = outline_panel.active; outline_panel.active = active; if old_active != active { - if active { - if let Some((active_item, active_editor)) = + if active + && let Some((active_item, active_editor)) = outline_panel.workspace.upgrade().and_then(|workspace| { workspace_active_editor(workspace.read(cx), cx) }) - { - if outline_panel.should_replace_active_item(active_item.as_ref()) { - outline_panel.replace_active_editor( - active_item, - active_editor, - window, - cx, - ); - } else { - outline_panel.update_fs_entries(active_editor, None, window, cx) - } - return; + { + if outline_panel.should_replace_active_item(active_item.as_ref()) { + outline_panel.replace_active_editor( + active_item, + active_editor, + window, + cx, + ); + } else { + outline_panel.update_fs_entries(active_editor, None, window, cx) } + return; } if !outline_panel.pinned { @@ -5319,8 +5310,8 @@ fn subscribe_for_editor_events( }) .copied(), ); - if !ignore_selections_change { - if let Some(entry_to_select) = latest_unfolded_buffer_id + if !ignore_selections_change + && let Some(entry_to_select) = latest_unfolded_buffer_id .or(latest_folded_buffer_id) .and_then(|toggled_buffer_id| { outline_panel.fs_entries.iter().find_map( @@ -5344,9 +5335,8 @@ fn subscribe_for_editor_events( ) }) .map(PanelEntry::Fs) - { - outline_panel.select_entry(entry_to_select, true, window, cx); - } + { + outline_panel.select_entry(entry_to_select, true, window, cx); } outline_panel.update_fs_entries(editor.clone(), debounce, window, cx); diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index 33320e6845..8e1485dc9a 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -185,11 +185,11 @@ impl Prettier { .metadata(&ignore_path) .await .with_context(|| format!("fetching metadata for {ignore_path:?}"))? + && !metadata.is_dir + && !metadata.is_symlink { - if !metadata.is_dir && !metadata.is_symlink { - log::info!("Found prettier ignore at {ignore_path:?}"); - return Ok(ControlFlow::Continue(Some(path_to_check))); - } + log::info!("Found prettier ignore at {ignore_path:?}"); + return Ok(ControlFlow::Continue(Some(path_to_check))); } match &closest_package_json_path { None => closest_package_json_path = Some(path_to_check.clone()), @@ -223,13 +223,13 @@ impl Prettier { }) { let workspace_ignore = path_to_check.join(".prettierignore"); - if let Some(metadata) = fs.metadata(&workspace_ignore).await? { - if !metadata.is_dir { - log::info!( - "Found prettier ignore at workspace root {workspace_ignore:?}" - ); - return Ok(ControlFlow::Continue(Some(path_to_check))); - } + if let Some(metadata) = fs.metadata(&workspace_ignore).await? + && !metadata.is_dir + { + log::info!( + "Found prettier ignore at workspace root {workspace_ignore:?}" + ); + return Ok(ControlFlow::Continue(Some(path_to_check))); } } } @@ -549,18 +549,16 @@ async fn read_package_json( .metadata(&possible_package_json) .await .with_context(|| format!("fetching metadata for package json {possible_package_json:?}"))? + && !package_json_metadata.is_dir + && !package_json_metadata.is_symlink { - if !package_json_metadata.is_dir && !package_json_metadata.is_symlink { - let package_json_contents = fs - .load(&possible_package_json) - .await - .with_context(|| format!("reading {possible_package_json:?} file contents"))?; - return serde_json::from_str::<HashMap<String, serde_json::Value>>( - &package_json_contents, - ) + let package_json_contents = fs + .load(&possible_package_json) + .await + .with_context(|| format!("reading {possible_package_json:?} file contents"))?; + return serde_json::from_str::<HashMap<String, serde_json::Value>>(&package_json_contents) .map(Some) .with_context(|| format!("parsing {possible_package_json:?} file contents")); - } } Ok(None) } diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index b8101e14f3..1522376e9a 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -1094,10 +1094,10 @@ impl BufferStore { .collect::<Vec<_>>() })?; for buffer_task in buffers { - if let Some(buffer) = buffer_task.await.log_err() { - if tx.send(buffer).await.is_err() { - return anyhow::Ok(()); - } + if let Some(buffer) = buffer_task.await.log_err() + && tx.send(buffer).await.is_err() + { + return anyhow::Ok(()); } } } @@ -1173,11 +1173,11 @@ impl BufferStore { buffer_id: BufferId, handle: OpenLspBufferHandle, ) { - if let Some(shared_buffers) = self.shared_buffers.get_mut(&peer_id) { - if let Some(buffer) = shared_buffers.get_mut(&buffer_id) { - buffer.lsp_handle = Some(handle); - return; - } + if let Some(shared_buffers) = self.shared_buffers.get_mut(&peer_id) + && let Some(buffer) = shared_buffers.get_mut(&buffer_id) + { + buffer.lsp_handle = Some(handle); + return; } debug_panic!("tried to register shared lsp handle, but buffer was not shared") } @@ -1388,14 +1388,14 @@ impl BufferStore { let peer_id = envelope.sender_id; let buffer_id = BufferId::new(envelope.payload.buffer_id)?; this.update(&mut cx, |this, cx| { - if let Some(shared) = this.shared_buffers.get_mut(&peer_id) { - if shared.remove(&buffer_id).is_some() { - cx.emit(BufferStoreEvent::SharedBufferClosed(peer_id, buffer_id)); - if shared.is_empty() { - this.shared_buffers.remove(&peer_id); - } - return; + if let Some(shared) = this.shared_buffers.get_mut(&peer_id) + && shared.remove(&buffer_id).is_some() + { + cx.emit(BufferStoreEvent::SharedBufferClosed(peer_id, buffer_id)); + if shared.is_empty() { + this.shared_buffers.remove(&peer_id); } + return; } debug_panic!( "peer_id {} closed buffer_id {} which was either not open or already closed", diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index 091189db7c..faa9948596 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -623,12 +623,11 @@ impl BreakpointStore { file_breakpoints.breakpoints.iter().filter_map({ let range = range.clone(); move |bp| { - if let Some(range) = &range { - if bp.position().cmp(&range.start, buffer_snapshot).is_lt() - || bp.position().cmp(&range.end, buffer_snapshot).is_gt() - { - return None; - } + if let Some(range) = &range + && (bp.position().cmp(&range.start, buffer_snapshot).is_lt() + || bp.position().cmp(&range.end, buffer_snapshot).is_gt()) + { + return None; } let session_state = active_session_id .and_then(|id| bp.session_state.get(&id)) diff --git a/crates/project/src/debugger/memory.rs b/crates/project/src/debugger/memory.rs index fec3c344c5..092435fda7 100644 --- a/crates/project/src/debugger/memory.rs +++ b/crates/project/src/debugger/memory.rs @@ -318,14 +318,13 @@ impl Iterator for MemoryIterator { return None; } if let Some((current_page_address, current_memory_chunk)) = self.current_known_page.as_mut() + && current_page_address.0 <= self.start { - if current_page_address.0 <= self.start { - if let Some(next_cell) = current_memory_chunk.next() { - self.start += 1; - return Some(next_cell); - } else { - self.current_known_page.take(); - } + if let Some(next_cell) = current_memory_chunk.next() { + self.start += 1; + return Some(next_cell); + } else { + self.current_known_page.take(); } } if !self.fetch_next_page() { diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 9539008530..ebc29a0a4b 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -570,23 +570,22 @@ impl GitStore { cx: &mut Context<Self>, ) -> Task<Result<Entity<BufferDiff>>> { let buffer_id = buffer.read(cx).remote_id(); - if let Some(diff_state) = self.diffs.get(&buffer_id) { - if let Some(unstaged_diff) = diff_state + if let Some(diff_state) = self.diffs.get(&buffer_id) + && let Some(unstaged_diff) = diff_state .read(cx) .unstaged_diff .as_ref() .and_then(|weak| weak.upgrade()) + { + if let Some(task) = + diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation()) { - if let Some(task) = - diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation()) - { - return cx.background_executor().spawn(async move { - task.await; - Ok(unstaged_diff) - }); - } - return Task::ready(Ok(unstaged_diff)); + return cx.background_executor().spawn(async move { + task.await; + Ok(unstaged_diff) + }); } + return Task::ready(Ok(unstaged_diff)); } let Some((repo, repo_path)) = @@ -627,23 +626,22 @@ impl GitStore { ) -> Task<Result<Entity<BufferDiff>>> { let buffer_id = buffer.read(cx).remote_id(); - if let Some(diff_state) = self.diffs.get(&buffer_id) { - if let Some(uncommitted_diff) = diff_state + if let Some(diff_state) = self.diffs.get(&buffer_id) + && let Some(uncommitted_diff) = diff_state .read(cx) .uncommitted_diff .as_ref() .and_then(|weak| weak.upgrade()) + { + if let Some(task) = + diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation()) { - if let Some(task) = - diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation()) - { - return cx.background_executor().spawn(async move { - task.await; - Ok(uncommitted_diff) - }); - } - return Task::ready(Ok(uncommitted_diff)); + return cx.background_executor().spawn(async move { + task.await; + Ok(uncommitted_diff) + }); } + return Task::ready(Ok(uncommitted_diff)); } let Some((repo, repo_path)) = @@ -764,22 +762,21 @@ impl GitStore { log::debug!("open conflict set"); let buffer_id = buffer.read(cx).remote_id(); - if let Some(git_state) = self.diffs.get(&buffer_id) { - if let Some(conflict_set) = git_state + if let Some(git_state) = self.diffs.get(&buffer_id) + && let Some(conflict_set) = git_state .read(cx) .conflict_set .as_ref() .and_then(|weak| weak.upgrade()) - { - let conflict_set = conflict_set.clone(); - let buffer_snapshot = buffer.read(cx).text_snapshot(); + { + let conflict_set = conflict_set.clone(); + let buffer_snapshot = buffer.read(cx).text_snapshot(); - git_state.update(cx, |state, cx| { - let _ = state.reparse_conflict_markers(buffer_snapshot, cx); - }); + git_state.update(cx, |state, cx| { + let _ = state.reparse_conflict_markers(buffer_snapshot, cx); + }); - return conflict_set; - } + return conflict_set; } let is_unmerged = self @@ -1151,29 +1148,26 @@ impl GitStore { for (buffer_id, diff) in self.diffs.iter() { if let Some((buffer_repo, repo_path)) = self.repository_and_path_for_buffer_id(*buffer_id, cx) + && buffer_repo == repo { - if buffer_repo == repo { - diff.update(cx, |diff, cx| { - if let Some(conflict_set) = &diff.conflict_set { - let conflict_status_changed = - conflict_set.update(cx, |conflict_set, cx| { - let has_conflict = repo_snapshot.has_conflict(&repo_path); - conflict_set.set_has_conflict(has_conflict, cx) - })?; - if conflict_status_changed { - let buffer_store = self.buffer_store.read(cx); - if let Some(buffer) = buffer_store.get(*buffer_id) { - let _ = diff.reparse_conflict_markers( - buffer.read(cx).text_snapshot(), - cx, - ); - } + diff.update(cx, |diff, cx| { + if let Some(conflict_set) = &diff.conflict_set { + let conflict_status_changed = + conflict_set.update(cx, |conflict_set, cx| { + let has_conflict = repo_snapshot.has_conflict(&repo_path); + conflict_set.set_has_conflict(has_conflict, cx) + })?; + if conflict_status_changed { + let buffer_store = self.buffer_store.read(cx); + if let Some(buffer) = buffer_store.get(*buffer_id) { + let _ = diff + .reparse_conflict_markers(buffer.read(cx).text_snapshot(), cx); } } - anyhow::Ok(()) - }) - .ok(); - } + } + anyhow::Ok(()) + }) + .ok(); } } cx.emit(GitStoreEvent::RepositoryUpdated( @@ -2231,13 +2225,13 @@ impl GitStore { ) -> Result<()> { let buffer_id = BufferId::new(request.payload.buffer_id)?; this.update(&mut cx, |this, cx| { - if let Some(diff_state) = this.diffs.get_mut(&buffer_id) { - if let Some(buffer) = this.buffer_store.read(cx).get(buffer_id) { - let buffer = buffer.read(cx).text_snapshot(); - diff_state.update(cx, |diff_state, cx| { - diff_state.handle_base_texts_updated(buffer, request.payload, cx); - }) - } + if let Some(diff_state) = this.diffs.get_mut(&buffer_id) + && let Some(buffer) = this.buffer_store.read(cx).get(buffer_id) + { + let buffer = buffer.read(cx).text_snapshot(); + diff_state.update(cx, |diff_state, cx| { + diff_state.handle_base_texts_updated(buffer, request.payload, cx); + }) } }) } @@ -3533,14 +3527,13 @@ impl Repository { let Some(project_path) = self.repo_path_to_project_path(path, cx) else { continue; }; - if let Some(buffer) = buffer_store.get_by_path(&project_path) { - if buffer + if let Some(buffer) = buffer_store.get_by_path(&project_path) + && buffer .read(cx) .file() .map_or(false, |file| file.disk_state().exists()) - { - save_futures.push(buffer_store.save_buffer(buffer, cx)); - } + { + save_futures.push(buffer_store.save_buffer(buffer, cx)); } } }) @@ -3600,14 +3593,13 @@ impl Repository { let Some(project_path) = self.repo_path_to_project_path(path, cx) else { continue; }; - if let Some(buffer) = buffer_store.get_by_path(&project_path) { - if buffer + if let Some(buffer) = buffer_store.get_by_path(&project_path) + && buffer .read(cx) .file() .map_or(false, |file| file.disk_state().exists()) - { - save_futures.push(buffer_store.save_buffer(buffer, cx)); - } + { + save_futures.push(buffer_store.save_buffer(buffer, cx)); } } }) @@ -4421,14 +4413,13 @@ impl Repository { } if let Some(job) = jobs.pop_front() { - if let Some(current_key) = &job.key { - if jobs + if let Some(current_key) = &job.key + && jobs .iter() .any(|other_job| other_job.key.as_ref() == Some(current_key)) { continue; } - } (job.job)(state.clone(), cx).await; } else if let Some(job) = job_rx.next().await { jobs.push_back(job); @@ -4459,13 +4450,12 @@ impl Repository { } if let Some(job) = jobs.pop_front() { - if let Some(current_key) = &job.key { - if jobs + if let Some(current_key) = &job.key + && jobs .iter() .any(|other_job| other_job.key.as_ref() == Some(current_key)) - { - continue; - } + { + continue; } (job.job)(state.clone(), cx).await; } else if let Some(job) = job_rx.next().await { @@ -4589,10 +4579,10 @@ impl Repository { for (repo_path, status) in &*statuses.entries { changed_paths.remove(repo_path); - if cursor.seek_forward(&PathTarget::Path(repo_path), Bias::Left) { - if cursor.item().is_some_and(|entry| entry.status == *status) { - continue; - } + if cursor.seek_forward(&PathTarget::Path(repo_path), Bias::Left) + && cursor.item().is_some_and(|entry| entry.status == *status) + { + continue; } changed_path_statuses.push(Edit::Insert(StatusEntry { diff --git a/crates/project/src/git_store/git_traversal.rs b/crates/project/src/git_store/git_traversal.rs index de5ff9b935..4594e8d140 100644 --- a/crates/project/src/git_store/git_traversal.rs +++ b/crates/project/src/git_store/git_traversal.rs @@ -182,11 +182,11 @@ impl<'a> Iterator for ChildEntriesGitIter<'a> { type Item = GitEntryRef<'a>; fn next(&mut self) -> Option<Self::Item> { - if let Some(item) = self.traversal.entry() { - if item.path.starts_with(self.parent_path) { - self.traversal.advance_to_sibling(); - return Some(item); - } + if let Some(item) = self.traversal.entry() + && item.path.starts_with(self.parent_path) + { + self.traversal.advance_to_sibling(); + return Some(item); } None } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index d5c3cc424f..2a1facd3c0 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2341,15 +2341,14 @@ impl LspCommand for GetCompletions { .zip(completion_edits) .map(|(mut lsp_completion, mut edit)| { LineEnding::normalize(&mut edit.new_text); - if lsp_completion.data.is_none() { - if let Some(default_data) = lsp_defaults + if lsp_completion.data.is_none() + && let Some(default_data) = lsp_defaults .as_ref() .and_then(|item_defaults| item_defaults.data.clone()) - { - // Servers (e.g. JDTLS) prefer unchanged completions, when resolving the items later, - // so we do not insert the defaults here, but `data` is needed for resolving, so this is an exception. - lsp_completion.data = Some(default_data); - } + { + // Servers (e.g. JDTLS) prefer unchanged completions, when resolving the items later, + // so we do not insert the defaults here, but `data` is needed for resolving, so this is an exception. + lsp_completion.data = Some(default_data); } CoreCompletion { replace_range: edit.replace_range, @@ -2623,10 +2622,10 @@ impl LspCommand for GetCodeActions { .filter_map(|entry| { let (lsp_action, resolved) = match entry { lsp::CodeActionOrCommand::CodeAction(lsp_action) => { - if let Some(command) = lsp_action.command.as_ref() { - if !available_commands.contains(&command.command) { - return None; - } + if let Some(command) = lsp_action.command.as_ref() + && !available_commands.contains(&command.command) + { + return None; } (LspAction::Action(Box::new(lsp_action)), false) } @@ -2641,10 +2640,9 @@ impl LspCommand for GetCodeActions { if let Some((requested_kinds, kind)) = requested_kinds_set.as_ref().zip(lsp_action.action_kind()) + && !requested_kinds.contains(&kind) { - if !requested_kinds.contains(&kind) { - return None; - } + return None; } Some(CodeAction { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 9410eea742..23061149bf 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -701,10 +701,9 @@ impl LocalLspStore { async move { this.update(&mut cx, |this, _| { if let Some(status) = this.language_server_statuses.get_mut(&server_id) + && let lsp::NumberOrString::String(token) = params.token { - if let lsp::NumberOrString::String(token) = params.token { - status.progress_tokens.insert(token); - } + status.progress_tokens.insert(token); } })?; @@ -1015,10 +1014,10 @@ impl LocalLspStore { } } LanguageServerState::Starting { startup, .. } => { - if let Some(server) = startup.await { - if let Some(shutdown) = server.shutdown() { - shutdown.await; - } + if let Some(server) = startup.await + && let Some(shutdown) = server.shutdown() + { + shutdown.await; } } } @@ -2384,15 +2383,15 @@ impl LocalLspStore { return None; } if !only_register_servers.is_empty() { - if let Some(server_id) = server_node.server_id() { - if !only_register_servers.contains(&LanguageServerSelector::Id(server_id)) { - return None; - } + if let Some(server_id) = server_node.server_id() + && !only_register_servers.contains(&LanguageServerSelector::Id(server_id)) + { + return None; } - if let Some(name) = server_node.name() { - if !only_register_servers.contains(&LanguageServerSelector::Name(name)) { - return None; - } + if let Some(name) = server_node.name() + && !only_register_servers.contains(&LanguageServerSelector::Name(name)) + { + return None; } } @@ -2410,11 +2409,11 @@ impl LocalLspStore { cx, ); - if let Some(state) = self.language_servers.get(&server_id) { - if let Ok(uri) = uri { - state.add_workspace_folder(uri); - }; - } + if let Some(state) = self.language_servers.get(&server_id) + && let Ok(uri) = uri + { + state.add_workspace_folder(uri); + }; server_id }; @@ -3844,13 +3843,13 @@ impl LspStore { } BufferStoreEvent::BufferChangedFilePath { buffer, old_file } => { let buffer_id = buffer.read(cx).remote_id(); - if let Some(local) = self.as_local_mut() { - if let Some(old_file) = File::from_dyn(old_file.as_ref()) { - local.reset_buffer(buffer, old_file, cx); + if let Some(local) = self.as_local_mut() + && let Some(old_file) = File::from_dyn(old_file.as_ref()) + { + local.reset_buffer(buffer, old_file, cx); - if local.registered_buffers.contains_key(&buffer_id) { - local.unregister_old_buffer_from_language_servers(buffer, old_file, cx); - } + if local.registered_buffers.contains_key(&buffer_id) { + local.unregister_old_buffer_from_language_servers(buffer, old_file, cx); } } @@ -4201,14 +4200,12 @@ impl LspStore { if local .registered_buffers .contains_key(&buffer.read(cx).remote_id()) - { - if let Some(file_url) = + && let Some(file_url) = file_path_to_lsp_url(&f.abs_path(cx)).log_err() - { - local.unregister_buffer_from_language_servers( - &buffer, &file_url, cx, - ); - } + { + local.unregister_buffer_from_language_servers( + &buffer, &file_url, cx, + ); } } } @@ -4306,20 +4303,13 @@ impl LspStore { let buffer = buffer_entity.read(cx); let buffer_file = buffer.file().cloned(); let buffer_id = buffer.remote_id(); - if let Some(local_store) = self.as_local_mut() { - if local_store.registered_buffers.contains_key(&buffer_id) { - if let Some(abs_path) = - File::from_dyn(buffer_file.as_ref()).map(|file| file.abs_path(cx)) - { - if let Some(file_url) = file_path_to_lsp_url(&abs_path).log_err() { - local_store.unregister_buffer_from_language_servers( - buffer_entity, - &file_url, - cx, - ); - } - } - } + if let Some(local_store) = self.as_local_mut() + && local_store.registered_buffers.contains_key(&buffer_id) + && let Some(abs_path) = + File::from_dyn(buffer_file.as_ref()).map(|file| file.abs_path(cx)) + && let Some(file_url) = file_path_to_lsp_url(&abs_path).log_err() + { + local_store.unregister_buffer_from_language_servers(buffer_entity, &file_url, cx); } buffer_entity.update(cx, |buffer, cx| { if buffer.language().map_or(true, |old_language| { @@ -4336,33 +4326,28 @@ impl LspStore { let worktree_id = if let Some(file) = buffer_file { let worktree = file.worktree.clone(); - if let Some(local) = self.as_local_mut() { - if local.registered_buffers.contains_key(&buffer_id) { - local.register_buffer_with_language_servers( - buffer_entity, - HashSet::default(), - cx, - ); - } + if let Some(local) = self.as_local_mut() + && local.registered_buffers.contains_key(&buffer_id) + { + local.register_buffer_with_language_servers(buffer_entity, HashSet::default(), cx); } Some(worktree.read(cx).id()) } else { None }; - if settings.prettier.allowed { - if let Some(prettier_plugins) = prettier_store::prettier_plugins_for_language(&settings) - { - let prettier_store = self.as_local().map(|s| s.prettier_store.clone()); - if let Some(prettier_store) = prettier_store { - prettier_store.update(cx, |prettier_store, cx| { - prettier_store.install_default_prettier( - worktree_id, - prettier_plugins.iter().map(|s| Arc::from(s.as_str())), - cx, - ) - }) - } + if settings.prettier.allowed + && let Some(prettier_plugins) = prettier_store::prettier_plugins_for_language(&settings) + { + let prettier_store = self.as_local().map(|s| s.prettier_store.clone()); + if let Some(prettier_store) = prettier_store { + prettier_store.update(cx, |prettier_store, cx| { + prettier_store.install_default_prettier( + worktree_id, + prettier_plugins.iter().map(|s| Arc::from(s.as_str())), + cx, + ) + }) } } @@ -4381,26 +4366,25 @@ impl LspStore { } pub(crate) fn send_diagnostic_summaries(&self, worktree: &mut Worktree) { - if let Some((client, downstream_project_id)) = self.downstream_client.clone() { - if let Some(diangostic_summaries) = self.diagnostic_summaries.get(&worktree.id()) { - let mut summaries = - diangostic_summaries + if let Some((client, downstream_project_id)) = self.downstream_client.clone() + && let Some(diangostic_summaries) = self.diagnostic_summaries.get(&worktree.id()) + { + let mut summaries = diangostic_summaries + .into_iter() + .flat_map(|(path, summaries)| { + summaries .into_iter() - .flat_map(|(path, summaries)| { - summaries - .into_iter() - .map(|(server_id, summary)| summary.to_proto(*server_id, path)) - }); - if let Some(summary) = summaries.next() { - client - .send(proto::UpdateDiagnosticSummary { - project_id: downstream_project_id, - worktree_id: worktree.id().to_proto(), - summary: Some(summary), - more_summaries: summaries.collect(), - }) - .log_err(); - } + .map(|(server_id, summary)| summary.to_proto(*server_id, path)) + }); + if let Some(summary) = summaries.next() { + client + .send(proto::UpdateDiagnosticSummary { + project_id: downstream_project_id, + worktree_id: worktree.id().to_proto(), + summary: Some(summary), + more_summaries: summaries.collect(), + }) + .log_err(); } } } @@ -4730,11 +4714,11 @@ impl LspStore { &language.name(), cx, ); - if let Some(state) = local.language_servers.get(&server_id) { - if let Ok(uri) = uri { - state.add_workspace_folder(uri); - }; - } + if let Some(state) = local.language_servers.get(&server_id) + && let Ok(uri) = uri + { + state.add_workspace_folder(uri); + }; server_id }); @@ -4805,8 +4789,8 @@ impl LspStore { LocalLspStore::try_resolve_code_action(&lang_server, &mut action) .await .context("resolving a code action")?; - if let Some(edit) = action.lsp_action.edit() { - if edit.changes.is_some() || edit.document_changes.is_some() { + if let Some(edit) = action.lsp_action.edit() + && (edit.changes.is_some() || edit.document_changes.is_some()) { return LocalLspStore::deserialize_workspace_edit( this.upgrade().context("no app present")?, edit.clone(), @@ -4817,7 +4801,6 @@ impl LspStore { ) .await; } - } if let Some(command) = action.lsp_action.command() { let server_capabilities = lang_server.capabilities(); @@ -5736,28 +5719,28 @@ impl LspStore { let version_queried_for = buffer.read(cx).version(); let buffer_id = buffer.read(cx).remote_id(); - if let Some(cached_data) = self.lsp_code_lens.get(&buffer_id) { - if !version_queried_for.changed_since(&cached_data.lens_for_version) { - let has_different_servers = self.as_local().is_some_and(|local| { - local - .buffers_opened_in_servers - .get(&buffer_id) - .cloned() - .unwrap_or_default() - != cached_data.lens.keys().copied().collect() - }); - if !has_different_servers { - return Task::ready(Ok(cached_data.lens.values().flatten().cloned().collect())) - .shared(); - } + if let Some(cached_data) = self.lsp_code_lens.get(&buffer_id) + && !version_queried_for.changed_since(&cached_data.lens_for_version) + { + let has_different_servers = self.as_local().is_some_and(|local| { + local + .buffers_opened_in_servers + .get(&buffer_id) + .cloned() + .unwrap_or_default() + != cached_data.lens.keys().copied().collect() + }); + if !has_different_servers { + return Task::ready(Ok(cached_data.lens.values().flatten().cloned().collect())) + .shared(); } } let lsp_data = self.lsp_code_lens.entry(buffer_id).or_default(); - if let Some((updating_for, running_update)) = &lsp_data.update { - if !version_queried_for.changed_since(updating_for) { - return running_update.clone(); - } + if let Some((updating_for, running_update)) = &lsp_data.update + && !version_queried_for.changed_since(updating_for) + { + return running_update.clone(); } let buffer = buffer.clone(); let query_version_queried_for = version_queried_for.clone(); @@ -6372,11 +6355,11 @@ impl LspStore { .old_replace_start .and_then(deserialize_anchor) .zip(response.old_replace_end.and_then(deserialize_anchor)); - if let Some((old_replace_start, old_replace_end)) = replace_range { - if !response.new_text.is_empty() { - completion.new_text = response.new_text; - completion.replace_range = old_replace_start..old_replace_end; - } + if let Some((old_replace_start, old_replace_end)) = replace_range + && !response.new_text.is_empty() + { + completion.new_text = response.new_text; + completion.replace_range = old_replace_start..old_replace_end; } Ok(()) @@ -6751,33 +6734,33 @@ impl LspStore { LspFetchStrategy::UseCache { known_cache_version, } => { - if let Some(cached_data) = self.lsp_document_colors.get(&buffer_id) { - if !version_queried_for.changed_since(&cached_data.colors_for_version) { - let has_different_servers = self.as_local().is_some_and(|local| { - local - .buffers_opened_in_servers - .get(&buffer_id) - .cloned() - .unwrap_or_default() - != cached_data.colors.keys().copied().collect() - }); - if !has_different_servers { - if Some(cached_data.cache_version) == known_cache_version { - return None; - } else { - return Some( - Task::ready(Ok(DocumentColors { - colors: cached_data - .colors - .values() - .flatten() - .cloned() - .collect(), - cache_version: Some(cached_data.cache_version), - })) - .shared(), - ); - } + if let Some(cached_data) = self.lsp_document_colors.get(&buffer_id) + && !version_queried_for.changed_since(&cached_data.colors_for_version) + { + let has_different_servers = self.as_local().is_some_and(|local| { + local + .buffers_opened_in_servers + .get(&buffer_id) + .cloned() + .unwrap_or_default() + != cached_data.colors.keys().copied().collect() + }); + if !has_different_servers { + if Some(cached_data.cache_version) == known_cache_version { + return None; + } else { + return Some( + Task::ready(Ok(DocumentColors { + colors: cached_data + .colors + .values() + .flatten() + .cloned() + .collect(), + cache_version: Some(cached_data.cache_version), + })) + .shared(), + ); } } } @@ -6785,10 +6768,10 @@ impl LspStore { } let lsp_data = self.lsp_document_colors.entry(buffer_id).or_default(); - if let Some((updating_for, running_update)) = &lsp_data.colors_update { - if !version_queried_for.changed_since(updating_for) { - return Some(running_update.clone()); - } + if let Some((updating_for, running_update)) = &lsp_data.colors_update + && !version_queried_for.changed_since(updating_for) + { + return Some(running_update.clone()); } let query_version_queried_for = version_queried_for.clone(); let new_task = cx @@ -8785,12 +8768,11 @@ impl LspStore { if summary.is_empty() { if let Some(worktree_summaries) = lsp_store.diagnostic_summaries.get_mut(&worktree_id) + && let Some(summaries) = worktree_summaries.get_mut(&path) { - if let Some(summaries) = worktree_summaries.get_mut(&path) { - summaries.remove(&server_id); - if summaries.is_empty() { - worktree_summaries.remove(&path); - } + summaries.remove(&server_id); + if summaries.is_empty() { + worktree_summaries.remove(&path); } } } else { @@ -9491,10 +9473,10 @@ impl LspStore { cx: &mut Context<Self>, ) { if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { - if let Some(work) = status.pending_work.remove(&token) { - if !work.is_disk_based_diagnostics_progress { - cx.emit(LspStoreEvent::RefreshInlayHints); - } + if let Some(work) = status.pending_work.remove(&token) + && !work.is_disk_based_diagnostics_progress + { + cx.emit(LspStoreEvent::RefreshInlayHints); } cx.notify(); } @@ -10288,10 +10270,10 @@ impl LspStore { None => None, }; - if let Some(server) = server { - if let Some(shutdown) = server.shutdown() { - shutdown.await; - } + if let Some(server) = server + && let Some(shutdown) = server.shutdown() + { + shutdown.await; } } @@ -10565,18 +10547,18 @@ impl LspStore { for buffer in buffers { buffer.update(cx, |buffer, cx| { language_servers_to_stop.extend(local.language_server_ids_for_buffer(buffer, cx)); - if let Some(worktree_id) = buffer.file().map(|f| f.worktree_id(cx)) { - if covered_worktrees.insert(worktree_id) { - language_server_names_to_stop.retain(|name| { - let old_ids_count = language_servers_to_stop.len(); - let all_language_servers_with_this_name = local - .language_server_ids - .iter() - .filter_map(|(seed, state)| seed.name.eq(name).then(|| state.id)); - language_servers_to_stop.extend(all_language_servers_with_this_name); - old_ids_count == language_servers_to_stop.len() - }); - } + if let Some(worktree_id) = buffer.file().map(|f| f.worktree_id(cx)) + && covered_worktrees.insert(worktree_id) + { + language_server_names_to_stop.retain(|name| { + let old_ids_count = language_servers_to_stop.len(); + let all_language_servers_with_this_name = local + .language_server_ids + .iter() + .filter_map(|(seed, state)| seed.name.eq(name).then(|| state.id)); + language_servers_to_stop.extend(all_language_servers_with_this_name); + old_ids_count == language_servers_to_stop.len() + }); } }); } @@ -11081,10 +11063,10 @@ impl LspStore { if let Some((LanguageServerState::Running { server, .. }, status)) = server.zip(status) { for (token, progress) in &status.pending_work { - if let Some(token_to_cancel) = token_to_cancel.as_ref() { - if token != token_to_cancel { - continue; - } + if let Some(token_to_cancel) = token_to_cancel.as_ref() + && token != token_to_cancel + { + continue; } if progress.is_cancellable { server @@ -11191,38 +11173,36 @@ impl LspStore { for server_id in &language_server_ids { if let Some(LanguageServerState::Running { server, .. }) = local.language_servers.get(server_id) - { - if let Some(watched_paths) = local + && let Some(watched_paths) = local .language_server_watched_paths .get(server_id) .and_then(|paths| paths.worktree_paths.get(&worktree_id)) - { - let params = lsp::DidChangeWatchedFilesParams { - changes: changes - .iter() - .filter_map(|(path, _, change)| { - if !watched_paths.is_match(path) { - return None; - } - let typ = match change { - PathChange::Loaded => return None, - PathChange::Added => lsp::FileChangeType::CREATED, - PathChange::Removed => lsp::FileChangeType::DELETED, - PathChange::Updated => lsp::FileChangeType::CHANGED, - PathChange::AddedOrUpdated => lsp::FileChangeType::CHANGED, - }; - Some(lsp::FileEvent { - uri: lsp::Url::from_file_path(abs_path.join(path)).unwrap(), - typ, - }) + { + let params = lsp::DidChangeWatchedFilesParams { + changes: changes + .iter() + .filter_map(|(path, _, change)| { + if !watched_paths.is_match(path) { + return None; + } + let typ = match change { + PathChange::Loaded => return None, + PathChange::Added => lsp::FileChangeType::CREATED, + PathChange::Removed => lsp::FileChangeType::DELETED, + PathChange::Updated => lsp::FileChangeType::CHANGED, + PathChange::AddedOrUpdated => lsp::FileChangeType::CHANGED, + }; + Some(lsp::FileEvent { + uri: lsp::Url::from_file_path(abs_path.join(path)).unwrap(), + typ, }) - .collect(), - }; - if !params.changes.is_empty() { - server - .notify::<lsp::notification::DidChangeWatchedFiles>(¶ms) - .ok(); - } + }) + .collect(), + }; + if !params.changes.is_empty() { + server + .notify::<lsp::notification::DidChangeWatchedFiles>(¶ms) + .ok(); } } } diff --git a/crates/project/src/manifest_tree/path_trie.rs b/crates/project/src/manifest_tree/path_trie.rs index 1a0736765a..16110463ac 100644 --- a/crates/project/src/manifest_tree/path_trie.rs +++ b/crates/project/src/manifest_tree/path_trie.rs @@ -84,11 +84,11 @@ impl<Label: Ord + Clone> RootPathTrie<Label> { ) { let mut current = self; for key in path.0.iter() { - if !current.labels.is_empty() { - if (callback)(¤t.worktree_relative_path, ¤t.labels).is_break() { - return; - }; - } + if !current.labels.is_empty() + && (callback)(¤t.worktree_relative_path, ¤t.labels).is_break() + { + return; + }; current = match current.children.get(key) { Some(child) => child, None => return, diff --git a/crates/project/src/prettier_store.rs b/crates/project/src/prettier_store.rs index 29997545cd..3ae5dc24ae 100644 --- a/crates/project/src/prettier_store.rs +++ b/crates/project/src/prettier_store.rs @@ -590,8 +590,8 @@ impl PrettierStore { new_plugins.clear(); } let mut needs_install = should_write_prettier_server_file(fs.as_ref()).await; - if let Some(previous_installation_task) = previous_installation_task { - if let Err(e) = previous_installation_task.await { + if let Some(previous_installation_task) = previous_installation_task + && let Err(e) = previous_installation_task.await { log::error!("Failed to install default prettier: {e:#}"); prettier_store.update(cx, |prettier_store, _| { if let PrettierInstallation::NotInstalled { attempts, not_installed_plugins, .. } = &mut prettier_store.default_prettier.prettier { @@ -601,8 +601,7 @@ impl PrettierStore { needs_install = true; }; })?; - } - }; + }; if installation_attempt > prettier::FAIL_THRESHOLD { prettier_store.update(cx, |prettier_store, _| { if let PrettierInstallation::NotInstalled { installation_task, .. } = &mut prettier_store.default_prettier.prettier { @@ -679,13 +678,13 @@ impl PrettierStore { ) { let mut prettier_plugins_by_worktree = HashMap::default(); for (worktree, language_settings) in language_formatters_to_check { - if language_settings.prettier.allowed { - if let Some(plugins) = prettier_plugins_for_language(&language_settings) { - prettier_plugins_by_worktree - .entry(worktree) - .or_insert_with(HashSet::default) - .extend(plugins.iter().cloned()); - } + if language_settings.prettier.allowed + && let Some(plugins) = prettier_plugins_for_language(&language_settings) + { + prettier_plugins_by_worktree + .entry(worktree) + .or_insert_with(HashSet::default) + .extend(plugins.iter().cloned()); } } for (worktree, prettier_plugins) in prettier_plugins_by_worktree { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 17997850b6..3906befee2 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -489,67 +489,63 @@ impl CompletionSource { .. } = self { - if apply_defaults { - if let Some(lsp_defaults) = lsp_defaults { - let mut completion_with_defaults = *lsp_completion.clone(); - let default_commit_characters = lsp_defaults.commit_characters.as_ref(); - let default_edit_range = lsp_defaults.edit_range.as_ref(); - let default_insert_text_format = lsp_defaults.insert_text_format.as_ref(); - let default_insert_text_mode = lsp_defaults.insert_text_mode.as_ref(); + if apply_defaults && let Some(lsp_defaults) = lsp_defaults { + let mut completion_with_defaults = *lsp_completion.clone(); + let default_commit_characters = lsp_defaults.commit_characters.as_ref(); + let default_edit_range = lsp_defaults.edit_range.as_ref(); + let default_insert_text_format = lsp_defaults.insert_text_format.as_ref(); + let default_insert_text_mode = lsp_defaults.insert_text_mode.as_ref(); - if default_commit_characters.is_some() - || default_edit_range.is_some() - || default_insert_text_format.is_some() - || default_insert_text_mode.is_some() + if default_commit_characters.is_some() + || default_edit_range.is_some() + || default_insert_text_format.is_some() + || default_insert_text_mode.is_some() + { + if completion_with_defaults.commit_characters.is_none() + && default_commit_characters.is_some() { - if completion_with_defaults.commit_characters.is_none() - && default_commit_characters.is_some() - { - completion_with_defaults.commit_characters = - default_commit_characters.cloned() - } - if completion_with_defaults.text_edit.is_none() { - match default_edit_range { - Some(lsp::CompletionListItemDefaultsEditRange::Range(range)) => { - completion_with_defaults.text_edit = - Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: *range, - new_text: completion_with_defaults.label.clone(), - })) - } - Some( - lsp::CompletionListItemDefaultsEditRange::InsertAndReplace { - insert, - replace, - }, - ) => { - completion_with_defaults.text_edit = - Some(lsp::CompletionTextEdit::InsertAndReplace( - lsp::InsertReplaceEdit { - new_text: completion_with_defaults.label.clone(), - insert: *insert, - replace: *replace, - }, - )) - } - None => {} + completion_with_defaults.commit_characters = + default_commit_characters.cloned() + } + if completion_with_defaults.text_edit.is_none() { + match default_edit_range { + Some(lsp::CompletionListItemDefaultsEditRange::Range(range)) => { + completion_with_defaults.text_edit = + Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: *range, + new_text: completion_with_defaults.label.clone(), + })) } - } - if completion_with_defaults.insert_text_format.is_none() - && default_insert_text_format.is_some() - { - completion_with_defaults.insert_text_format = - default_insert_text_format.cloned() - } - if completion_with_defaults.insert_text_mode.is_none() - && default_insert_text_mode.is_some() - { - completion_with_defaults.insert_text_mode = - default_insert_text_mode.cloned() + Some(lsp::CompletionListItemDefaultsEditRange::InsertAndReplace { + insert, + replace, + }) => { + completion_with_defaults.text_edit = + Some(lsp::CompletionTextEdit::InsertAndReplace( + lsp::InsertReplaceEdit { + new_text: completion_with_defaults.label.clone(), + insert: *insert, + replace: *replace, + }, + )) + } + None => {} } } - return Some(Cow::Owned(completion_with_defaults)); + if completion_with_defaults.insert_text_format.is_none() + && default_insert_text_format.is_some() + { + completion_with_defaults.insert_text_format = + default_insert_text_format.cloned() + } + if completion_with_defaults.insert_text_mode.is_none() + && default_insert_text_mode.is_some() + { + completion_with_defaults.insert_text_mode = + default_insert_text_mode.cloned() + } } + return Some(Cow::Owned(completion_with_defaults)); } Some(Cow::Borrowed(lsp_completion)) } else { @@ -2755,11 +2751,12 @@ impl Project { operations, })) })?; - if let Some(request) = request { - if request.await.is_err() && !is_local { - *needs_resync_with_host = true; - break; - } + if let Some(request) = request + && request.await.is_err() + && !is_local + { + *needs_resync_with_host = true; + break; } } Ok(()) @@ -3939,10 +3936,10 @@ impl Project { if let Some(entry) = b .entry_id(cx) .and_then(|entry_id| worktree_store.entry_for_id(entry_id, cx)) + && entry.is_ignored + && !search_query.include_ignored() { - if entry.is_ignored && !search_query.include_ignored() { - return false; - } + return false; } } true @@ -4151,11 +4148,11 @@ impl Project { ) -> Task<Option<ResolvedPath>> { let mut candidates = vec![path.clone()]; - if let Some(file) = buffer.read(cx).file() { - if let Some(dir) = file.path().parent() { - let joined = dir.to_path_buf().join(path); - candidates.push(joined); - } + if let Some(file) = buffer.read(cx).file() + && let Some(dir) = file.path().parent() + { + let joined = dir.to_path_buf().join(path); + candidates.push(joined); } let buffer_worktree_id = buffer.read(cx).file().map(|file| file.worktree_id(cx)); @@ -4168,16 +4165,14 @@ impl Project { .collect(); cx.spawn(async move |_, cx| { - if let Some(buffer_worktree_id) = buffer_worktree_id { - if let Some((worktree, _)) = worktrees_with_ids + if let Some(buffer_worktree_id) = buffer_worktree_id + && let Some((worktree, _)) = worktrees_with_ids .iter() .find(|(_, id)| *id == buffer_worktree_id) - { - for candidate in candidates.iter() { - if let Some(path) = Self::resolve_path_in_worktree(worktree, candidate, cx) - { - return Some(path); - } + { + for candidate in candidates.iter() { + if let Some(path) = Self::resolve_path_in_worktree(worktree, candidate, cx) { + return Some(path); } } } diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 4f024837c8..ee216a9976 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -155,16 +155,16 @@ impl SearchQuery { let initial_query = Arc::from(query.as_str()); if whole_word { let mut word_query = String::new(); - if let Some(first) = query.get(0..1) { - if WORD_MATCH_TEST.is_match(first).is_ok_and(|x| !x) { - word_query.push_str("\\b"); - } + if let Some(first) = query.get(0..1) + && WORD_MATCH_TEST.is_match(first).is_ok_and(|x| !x) + { + word_query.push_str("\\b"); } word_query.push_str(&query); - if let Some(last) = query.get(query.len() - 1..) { - if WORD_MATCH_TEST.is_match(last).is_ok_and(|x| !x) { - word_query.push_str("\\b"); - } + if let Some(last) = query.get(query.len() - 1..) + && WORD_MATCH_TEST.is_match(last).is_ok_and(|x| !x) + { + word_query.push_str("\\b"); } query = word_query } diff --git a/crates/project/src/search_history.rs b/crates/project/src/search_history.rs index 90b169bb0c..401f375094 100644 --- a/crates/project/src/search_history.rs +++ b/crates/project/src/search_history.rs @@ -45,20 +45,19 @@ impl SearchHistory { } pub fn add(&mut self, cursor: &mut SearchHistoryCursor, search_string: String) { - if self.insertion_behavior == QueryInsertionBehavior::ReplacePreviousIfContains { - if let Some(previously_searched) = self.history.back_mut() { - if search_string.contains(previously_searched.as_str()) { - *previously_searched = search_string; - cursor.selection = Some(self.history.len() - 1); - return; - } - } + if self.insertion_behavior == QueryInsertionBehavior::ReplacePreviousIfContains + && let Some(previously_searched) = self.history.back_mut() + && search_string.contains(previously_searched.as_str()) + { + *previously_searched = search_string; + cursor.selection = Some(self.history.len() - 1); + return; } - if let Some(max_history_len) = self.max_history_len { - if self.history.len() >= max_history_len { - self.history.pop_front(); - } + if let Some(max_history_len) = self.max_history_len + && self.history.len() >= max_history_len + { + self.history.pop_front(); } self.history.push_back(search_string); diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index f5d08990b5..5f98a10c75 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -119,13 +119,13 @@ impl Project { }; let mut settings_location = None; - if let Some(path) = path.as_ref() { - if let Some((worktree, _)) = self.find_worktree(path, cx) { - settings_location = Some(SettingsLocation { - worktree_id: worktree.read(cx).id(), - path, - }); - } + if let Some(path) = path.as_ref() + && let Some((worktree, _)) = self.find_worktree(path, cx) + { + settings_location = Some(SettingsLocation { + worktree_id: worktree.read(cx).id(), + path, + }); } let venv = TerminalSettings::get(settings_location, cx) .detect_venv @@ -151,13 +151,13 @@ impl Project { cx: &'a App, ) -> &'a TerminalSettings { let mut settings_location = None; - if let Some(path) = path.as_ref() { - if let Some((worktree, _)) = self.find_worktree(path, cx) { - settings_location = Some(SettingsLocation { - worktree_id: worktree.read(cx).id(), - path, - }); - } + if let Some(path) = path.as_ref() + && let Some((worktree, _)) = self.find_worktree(path, cx) + { + settings_location = Some(SettingsLocation { + worktree_id: worktree.read(cx).id(), + path, + }); } TerminalSettings::get(settings_location, cx) } @@ -239,13 +239,13 @@ impl Project { let is_ssh_terminal = ssh_details.is_some(); let mut settings_location = None; - if let Some(path) = path.as_ref() { - if let Some((worktree, _)) = this.find_worktree(path, cx) { - settings_location = Some(SettingsLocation { - worktree_id: worktree.read(cx).id(), - path, - }); - } + if let Some(path) = path.as_ref() + && let Some((worktree, _)) = this.find_worktree(path, cx) + { + settings_location = Some(SettingsLocation { + worktree_id: worktree.read(cx).id(), + path, + }); } let settings = TerminalSettings::get(settings_location, cx).clone(); @@ -665,11 +665,11 @@ pub fn wrap_for_ssh( env_changes.push_str(&format!("{}={} ", k, v)); } } - if let Some(venv_directory) = venv_directory { - if let Ok(str) = shlex::try_quote(venv_directory.to_string_lossy().as_ref()) { - let path = RemotePathBuf::new(PathBuf::from(str.to_string()), path_style).to_string(); - env_changes.push_str(&format!("PATH={}:$PATH ", path)); - } + if let Some(venv_directory) = venv_directory + && let Ok(str) = shlex::try_quote(venv_directory.to_string_lossy().as_ref()) + { + let path = RemotePathBuf::new(PathBuf::from(str.to_string()), path_style).to_string(); + env_changes.push_str(&format!("PATH={}:$PATH ", path)); } let commands = if let Some(path) = path { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 892847a380..dd6b081e98 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -652,8 +652,8 @@ impl ProjectPanel { focus_opened_item, allow_preview, } => { - if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { - if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { + if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) + && let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { let file_path = entry.path.clone(); let worktree_id = worktree.read(cx).id(); let entry_id = entry.id; @@ -703,11 +703,10 @@ impl ProjectPanel { } } } - } } &Event::SplitEntry { entry_id } => { - if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { - if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { + if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) + && let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { workspace .split_path( ProjectPath { @@ -718,7 +717,6 @@ impl ProjectPanel { ) .detach_and_log_err(cx); } - } } _ => {} @@ -1002,10 +1000,10 @@ impl ProjectPanel { if let Some(parent_path) = entry.path.parent() { let snapshot = worktree.snapshot(); let mut child_entries = snapshot.child_entries(parent_path); - if let Some(child) = child_entries.next() { - if child_entries.next().is_none() { - return child.kind.is_dir(); - } + if let Some(child) = child_entries.next() + && child_entries.next().is_none() + { + return child.kind.is_dir(); } }; false @@ -1016,10 +1014,10 @@ impl ProjectPanel { let snapshot = worktree.snapshot(); let mut child_entries = snapshot.child_entries(&entry.path); - if let Some(child) = child_entries.next() { - if child_entries.next().is_none() { - return child.kind.is_dir(); - } + if let Some(child) = child_entries.next() + && child_entries.next().is_none() + { + return child.kind.is_dir(); } } false @@ -1032,12 +1030,12 @@ impl ProjectPanel { cx: &mut Context<Self>, ) { if let Some((worktree, entry)) = self.selected_entry(cx) { - if let Some(folded_ancestors) = self.ancestors.get_mut(&entry.id) { - if folded_ancestors.current_ancestor_depth > 0 { - folded_ancestors.current_ancestor_depth -= 1; - cx.notify(); - return; - } + if let Some(folded_ancestors) = self.ancestors.get_mut(&entry.id) + && folded_ancestors.current_ancestor_depth > 0 + { + folded_ancestors.current_ancestor_depth -= 1; + cx.notify(); + return; } if entry.is_dir() { let worktree_id = worktree.id(); @@ -1079,12 +1077,12 @@ impl ProjectPanel { fn collapse_entry(&mut self, entry: Entry, worktree: Entity<Worktree>, cx: &mut Context<Self>) { let worktree = worktree.read(cx); - if let Some(folded_ancestors) = self.ancestors.get_mut(&entry.id) { - if folded_ancestors.current_ancestor_depth + 1 < folded_ancestors.max_ancestor_depth() { - folded_ancestors.current_ancestor_depth += 1; - cx.notify(); - return; - } + if let Some(folded_ancestors) = self.ancestors.get_mut(&entry.id) + && folded_ancestors.current_ancestor_depth + 1 < folded_ancestors.max_ancestor_depth() + { + folded_ancestors.current_ancestor_depth += 1; + cx.notify(); + return; } let worktree_id = worktree.id(); let expanded_dir_ids = @@ -1137,23 +1135,23 @@ impl ProjectPanel { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) { - if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { - self.project.update(cx, |project, cx| { - match expanded_dir_ids.binary_search(&entry_id) { - Ok(ix) => { - expanded_dir_ids.remove(ix); - } - Err(ix) => { - project.expand_entry(worktree_id, entry_id, cx); - expanded_dir_ids.insert(ix, entry_id); - } + if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) + && let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) + { + self.project.update(cx, |project, cx| { + match expanded_dir_ids.binary_search(&entry_id) { + Ok(ix) => { + expanded_dir_ids.remove(ix); } - }); - self.update_visible_entries(Some((worktree_id, entry_id)), cx); - window.focus(&self.focus_handle); - cx.notify(); - } + Err(ix) => { + project.expand_entry(worktree_id, entry_id, cx); + expanded_dir_ids.insert(ix, entry_id); + } + } + }); + self.update_visible_entries(Some((worktree_id, entry_id)), cx); + window.focus(&self.focus_handle); + cx.notify(); } } @@ -1163,20 +1161,20 @@ impl ProjectPanel { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) { - if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { - match expanded_dir_ids.binary_search(&entry_id) { - Ok(_ix) => { - self.collapse_all_for_entry(worktree_id, entry_id, cx); - } - Err(_ix) => { - self.expand_all_for_entry(worktree_id, entry_id, cx); - } + if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) + && let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) + { + match expanded_dir_ids.binary_search(&entry_id) { + Ok(_ix) => { + self.collapse_all_for_entry(worktree_id, entry_id, cx); + } + Err(_ix) => { + self.expand_all_for_entry(worktree_id, entry_id, cx); } - self.update_visible_entries(Some((worktree_id, entry_id)), cx); - window.focus(&self.focus_handle); - cx.notify(); } + self.update_visible_entries(Some((worktree_id, entry_id)), cx); + window.focus(&self.focus_handle); + cx.notify(); } } @@ -1251,20 +1249,20 @@ impl ProjectPanel { } fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) { - if let Some(edit_state) = &self.edit_state { - if edit_state.processing_filename.is_none() { - self.filename_editor.update(cx, |editor, cx| { - editor.move_to_beginning_of_line( - &editor::actions::MoveToBeginningOfLine { - stop_at_soft_wraps: false, - stop_at_indent: false, - }, - window, - cx, - ); - }); - return; - } + if let Some(edit_state) = &self.edit_state + && edit_state.processing_filename.is_none() + { + self.filename_editor.update(cx, |editor, cx| { + editor.move_to_beginning_of_line( + &editor::actions::MoveToBeginningOfLine { + stop_at_soft_wraps: false, + stop_at_indent: false, + }, + window, + cx, + ); + }); + return; } if let Some(selection) = self.selection { let (mut worktree_ix, mut entry_ix, _) = @@ -1341,39 +1339,37 @@ impl ProjectPanel { .project .read(cx) .worktree_for_id(edit_state.worktree_id, cx) + && let Some(entry) = worktree.read(cx).entry_for_id(edit_state.entry_id) { - if let Some(entry) = worktree.read(cx).entry_for_id(edit_state.entry_id) { - let mut already_exists = false; - if edit_state.is_new_entry() { - let new_path = entry.path.join(filename.trim_start_matches('/')); - if worktree - .read(cx) - .entry_for_path(new_path.as_path()) - .is_some() - { - already_exists = true; - } - } else { - let new_path = if let Some(parent) = entry.path.clone().parent() { - parent.join(&filename) - } else { - filename.clone().into() - }; - if let Some(existing) = worktree.read(cx).entry_for_path(new_path.as_path()) - { - if existing.id != entry.id { - already_exists = true; - } - } - }; - if already_exists { - edit_state.validation_state = ValidationState::Error(format!( - "File or directory '{}' already exists at location. Please choose a different name.", - filename - )); - cx.notify(); - return; + let mut already_exists = false; + if edit_state.is_new_entry() { + let new_path = entry.path.join(filename.trim_start_matches('/')); + if worktree + .read(cx) + .entry_for_path(new_path.as_path()) + .is_some() + { + already_exists = true; } + } else { + let new_path = if let Some(parent) = entry.path.clone().parent() { + parent.join(&filename) + } else { + filename.clone().into() + }; + if let Some(existing) = worktree.read(cx).entry_for_path(new_path.as_path()) + && existing.id != entry.id + { + already_exists = true; + } + }; + if already_exists { + edit_state.validation_state = ValidationState::Error(format!( + "File or directory '{}' already exists at location. Please choose a different name.", + filename + )); + cx.notify(); + return; } } let trimmed_filename = filename.trim(); @@ -1477,14 +1473,13 @@ impl ProjectPanel { } Ok(CreatedEntry::Included(new_entry)) => { project_panel.update( cx, |project_panel, cx| { - if let Some(selection) = &mut project_panel.selection { - if selection.entry_id == edited_entry_id { + if let Some(selection) = &mut project_panel.selection + && selection.entry_id == edited_entry_id { selection.worktree_id = worktree_id; selection.entry_id = new_entry.id; project_panel.marked_entries.clear(); project_panel.expand_to_selection(cx); } - } project_panel.update_visible_entries(None, cx); if is_new_entry && !is_dir { project_panel.open_entry(new_entry.id, true, false, cx); @@ -1617,11 +1612,11 @@ impl ProjectPanel { directory_id = entry.id; break; } else { - if let Some(parent_path) = entry.path.parent() { - if let Some(parent_entry) = worktree.entry_for_path(parent_path) { - entry = parent_entry; - continue; - } + if let Some(parent_path) = entry.path.parent() + && let Some(parent_entry) = worktree.entry_for_path(parent_path) + { + entry = parent_entry; + continue; } return; } @@ -1675,57 +1670,56 @@ impl ProjectPanel { worktree_id, entry_id, }) = self.selection + && let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) { - if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) { - let sub_entry_id = self.unflatten_entry_id(entry_id); - if let Some(entry) = worktree.read(cx).entry_for_id(sub_entry_id) { - #[cfg(target_os = "windows")] - if Some(entry) == worktree.read(cx).root_entry() { + let sub_entry_id = self.unflatten_entry_id(entry_id); + if let Some(entry) = worktree.read(cx).entry_for_id(sub_entry_id) { + #[cfg(target_os = "windows")] + if Some(entry) == worktree.read(cx).root_entry() { + return; + } + + if Some(entry) == worktree.read(cx).root_entry() { + let settings = ProjectPanelSettings::get_global(cx); + let visible_worktrees_count = + self.project.read(cx).visible_worktrees(cx).count(); + if settings.hide_root && visible_worktrees_count == 1 { return; } - - if Some(entry) == worktree.read(cx).root_entry() { - let settings = ProjectPanelSettings::get_global(cx); - let visible_worktrees_count = - self.project.read(cx).visible_worktrees(cx).count(); - if settings.hide_root && visible_worktrees_count == 1 { - return; - } - } - - self.edit_state = Some(EditState { - worktree_id, - entry_id: sub_entry_id, - leaf_entry_id: Some(entry_id), - is_dir: entry.is_dir(), - processing_filename: None, - previously_focused: None, - depth: 0, - validation_state: ValidationState::None, - }); - let file_name = entry - .path - .file_name() - .map(|s| s.to_string_lossy()) - .unwrap_or_default() - .to_string(); - let selection = selection.unwrap_or_else(|| { - let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy()); - let selection_end = - file_stem.map_or(file_name.len(), |file_stem| file_stem.len()); - 0..selection_end - }); - self.filename_editor.update(cx, |editor, cx| { - editor.set_text(file_name, window, cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges([selection]) - }); - window.focus(&editor.focus_handle(cx)); - }); - self.update_visible_entries(None, cx); - self.autoscroll(cx); - cx.notify(); } + + self.edit_state = Some(EditState { + worktree_id, + entry_id: sub_entry_id, + leaf_entry_id: Some(entry_id), + is_dir: entry.is_dir(), + processing_filename: None, + previously_focused: None, + depth: 0, + validation_state: ValidationState::None, + }); + let file_name = entry + .path + .file_name() + .map(|s| s.to_string_lossy()) + .unwrap_or_default() + .to_string(); + let selection = selection.unwrap_or_else(|| { + let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy()); + let selection_end = + file_stem.map_or(file_name.len(), |file_stem| file_stem.len()); + 0..selection_end + }); + self.filename_editor.update(cx, |editor, cx| { + editor.set_text(file_name, window, cx); + editor.change_selections(Default::default(), window, cx, |s| { + s.select_ranges([selection]) + }); + window.focus(&editor.focus_handle(cx)); + }); + self.update_visible_entries(None, cx); + self.autoscroll(cx); + cx.notify(); } } } @@ -1831,10 +1825,10 @@ impl ProjectPanel { }; let next_selection = self.find_next_selection_after_deletion(items_to_delete, cx); cx.spawn_in(window, async move |panel, cx| { - if let Some(answer) = answer { - if answer.await != Ok(0) { - return anyhow::Ok(()); - } + if let Some(answer) = answer + && answer.await != Ok(0) + { + return anyhow::Ok(()); } for (entry_id, _) in file_paths { panel @@ -1999,19 +1993,19 @@ impl ProjectPanel { } fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) { - if let Some(edit_state) = &self.edit_state { - if edit_state.processing_filename.is_none() { - self.filename_editor.update(cx, |editor, cx| { - editor.move_to_end_of_line( - &editor::actions::MoveToEndOfLine { - stop_at_soft_wraps: false, - }, - window, - cx, - ); - }); - return; - } + if let Some(edit_state) = &self.edit_state + && edit_state.processing_filename.is_none() + { + self.filename_editor.update(cx, |editor, cx| { + editor.move_to_end_of_line( + &editor::actions::MoveToEndOfLine { + stop_at_soft_wraps: false, + }, + window, + cx, + ); + }); + return; } if let Some(selection) = self.selection { let (mut worktree_ix, mut entry_ix, _) = @@ -2032,20 +2026,19 @@ impl ProjectPanel { entries, .. }) = self.visible_entries.get(worktree_ix) + && let Some(entry) = entries.get(entry_ix) { - if let Some(entry) = entries.get(entry_ix) { - let selection = SelectedEntry { - worktree_id: *worktree_id, - entry_id: entry.id, - }; - self.selection = Some(selection); - if window.modifiers().shift { - self.marked_entries.push(selection); - } - - self.autoscroll(cx); - cx.notify(); + let selection = SelectedEntry { + worktree_id: *worktree_id, + entry_id: entry.id, + }; + self.selection = Some(selection); + if window.modifiers().shift { + self.marked_entries.push(selection); } + + self.autoscroll(cx); + cx.notify(); } } else { self.select_first(&SelectFirst {}, window, cx); @@ -2274,19 +2267,18 @@ impl ProjectPanel { entries, .. }) = self.visible_entries.first() + && let Some(entry) = entries.first() { - if let Some(entry) = entries.first() { - let selection = SelectedEntry { - worktree_id: *worktree_id, - entry_id: entry.id, - }; - self.selection = Some(selection); - if window.modifiers().shift { - self.marked_entries.push(selection); - } - self.autoscroll(cx); - cx.notify(); + let selection = SelectedEntry { + worktree_id: *worktree_id, + entry_id: entry.id, + }; + self.selection = Some(selection); + if window.modifiers().shift { + self.marked_entries.push(selection); } + self.autoscroll(cx); + cx.notify(); } } @@ -2947,10 +2939,10 @@ impl ProjectPanel { let Some(entry) = worktree.entry_for_path(path) else { continue; }; - if entry.is_dir() { - if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) { - expanded_dir_ids.insert(idx, entry.id); - } + if entry.is_dir() + && let Err(idx) = expanded_dir_ids.binary_search(&entry.id) + { + expanded_dir_ids.insert(idx, entry.id); } } @@ -3024,15 +3016,16 @@ impl ProjectPanel { let mut new_entry_parent_id = None; let mut new_entry_kind = EntryKind::Dir; - if let Some(edit_state) = &self.edit_state { - if edit_state.worktree_id == worktree_id && edit_state.is_new_entry() { - new_entry_parent_id = Some(edit_state.entry_id); - new_entry_kind = if edit_state.is_dir { - EntryKind::Dir - } else { - EntryKind::File - }; - } + if let Some(edit_state) = &self.edit_state + && edit_state.worktree_id == worktree_id + && edit_state.is_new_entry() + { + new_entry_parent_id = Some(edit_state.entry_id); + new_entry_kind = if edit_state.is_dir { + EntryKind::Dir + } else { + EntryKind::File + }; } let mut visible_worktree_entries = Vec::new(); @@ -3054,19 +3047,18 @@ impl ProjectPanel { } if auto_collapse_dirs && entry.kind.is_dir() { auto_folded_ancestors.push(entry.id); - if !self.unfolded_dir_ids.contains(&entry.id) { - if let Some(root_path) = worktree_snapshot.root_entry() { - let mut child_entries = worktree_snapshot.child_entries(&entry.path); - if let Some(child) = child_entries.next() { - if entry.path != root_path.path - && child_entries.next().is_none() - && child.kind.is_dir() - { - entry_iter.advance(); + if !self.unfolded_dir_ids.contains(&entry.id) + && let Some(root_path) = worktree_snapshot.root_entry() + { + let mut child_entries = worktree_snapshot.child_entries(&entry.path); + if let Some(child) = child_entries.next() + && entry.path != root_path.path + && child_entries.next().is_none() + && child.kind.is_dir() + { + entry_iter.advance(); - continue; - } - } + continue; } } let depth = old_ancestors @@ -3074,10 +3066,10 @@ impl ProjectPanel { .map(|ancestor| ancestor.current_ancestor_depth) .unwrap_or_default() .min(auto_folded_ancestors.len()); - if let Some(edit_state) = &mut self.edit_state { - if edit_state.entry_id == entry.id { - edit_state.depth = depth; - } + if let Some(edit_state) = &mut self.edit_state + && edit_state.entry_id == entry.id + { + edit_state.depth = depth; } let mut ancestors = std::mem::take(&mut auto_folded_ancestors); if ancestors.len() > 1 { @@ -3314,11 +3306,10 @@ impl ProjectPanel { ) })?.await?; - if answer == 1 { - if let Some(item_idx) = paths.iter().position(|p| p == original_path) { + if answer == 1 + && let Some(item_idx) = paths.iter().position(|p| p == original_path) { paths.remove(item_idx); } - } } if paths.is_empty() { @@ -4309,8 +4300,8 @@ impl ProjectPanel { } } else if kind.is_dir() { project_panel.marked_entries.clear(); - if is_sticky { - if let Some((_, _, index)) = project_panel.index_for_entry(entry_id, worktree_id) { + if is_sticky + && let Some((_, _, index)) = project_panel.index_for_entry(entry_id, worktree_id) { project_panel.scroll_handle.scroll_to_item_with_offset(index, ScrollStrategy::Top, sticky_index.unwrap_or(0)); cx.notify(); // move down by 1px so that clicked item @@ -4325,7 +4316,6 @@ impl ProjectPanel { }); return; } - } if event.modifiers().alt { project_panel.toggle_expand_all(entry_id, window, cx); } else { @@ -4547,15 +4537,14 @@ impl ProjectPanel { }) }) .on_click(cx.listener(move |this, _, _, cx| { - if index != active_index { - if let Some(folds) = + if index != active_index + && let Some(folds) = this.ancestors.get_mut(&entry_id) { folds.current_ancestor_depth = components_len - 1 - index; cx.notify(); } - } })) .child( Label::new(component) @@ -5034,12 +5023,12 @@ impl ProjectPanel { 'outer: loop { if let Some(parent_path) = current_path.parent() { for ancestor_path in parent_path.ancestors() { - if paths.contains(ancestor_path) { - if let Some(parent_entry) = worktree.entry_for_path(ancestor_path) { - sticky_parents.push(parent_entry.clone()); - current_path = parent_entry.path.clone(); - continue 'outer; - } + if paths.contains(ancestor_path) + && let Some(parent_entry) = worktree.entry_for_path(ancestor_path) + { + sticky_parents.push(parent_entry.clone()); + current_path = parent_entry.path.clone(); + continue 'outer; } } } @@ -5291,25 +5280,25 @@ impl Render for ProjectPanel { .on_action(cx.listener(Self::paste)) .on_action(cx.listener(Self::duplicate)) .on_click(cx.listener(|this, event: &gpui::ClickEvent, window, cx| { - if event.click_count() > 1 { - if let Some(entry_id) = this.last_worktree_root_id { - let project = this.project.read(cx); + if event.click_count() > 1 + && let Some(entry_id) = this.last_worktree_root_id + { + let project = this.project.read(cx); - let worktree_id = if let Some(worktree) = - project.worktree_for_entry(entry_id, cx) - { - worktree.read(cx).id() - } else { - return; - }; + let worktree_id = if let Some(worktree) = + project.worktree_for_entry(entry_id, cx) + { + worktree.read(cx).id() + } else { + return; + }; - this.selection = Some(SelectedEntry { - worktree_id, - entry_id, - }); + this.selection = Some(SelectedEntry { + worktree_id, + entry_id, + }); - this.new_file(&NewFile, window, cx); - } + this.new_file(&NewFile, window, cx); } })) }) diff --git a/crates/prompt_store/src/prompts.rs b/crates/prompt_store/src/prompts.rs index 7eb63eec5e..526d2c6a34 100644 --- a/crates/prompt_store/src/prompts.rs +++ b/crates/prompt_store/src/prompts.rs @@ -261,13 +261,12 @@ impl PromptBuilder { // Initial scan of the prompt overrides directory if let Ok(mut entries) = params.fs.read_dir(&templates_dir).await { while let Some(Ok(file_path)) = entries.next().await { - if file_path.to_string_lossy().ends_with(".hbs") { - if let Ok(content) = params.fs.load(&file_path).await { + if file_path.to_string_lossy().ends_with(".hbs") + && let Ok(content) = params.fs.load(&file_path).await { let file_name = file_path.file_stem().unwrap().to_string_lossy(); log::debug!("Registering prompt template override: {}", file_name); handlebars.lock().register_template_string(&file_name, content).log_err(); } - } } } @@ -280,13 +279,12 @@ impl PromptBuilder { let mut combined_changes = futures::stream::select(changes, parent_changes); while let Some(changed_paths) = combined_changes.next().await { - if changed_paths.iter().any(|p| &p.path == &templates_dir) { - if !params.fs.is_dir(&templates_dir).await { + if changed_paths.iter().any(|p| &p.path == &templates_dir) + && !params.fs.is_dir(&templates_dir).await { log::info!("Prompt template overrides directory removed. Restoring built-in prompt templates."); Self::register_built_in_templates(&mut handlebars.lock()).log_err(); break; } - } for event in changed_paths { if event.path.starts_with(&templates_dir) && event.path.extension().map_or(false, |ext| ext == "hbs") { log::info!("Reloading prompt template override: {}", event.path.display()); @@ -311,12 +309,11 @@ impl PromptBuilder { .split('/') .next_back() .and_then(|s| s.strip_suffix(".hbs")) + && let Some(prompt) = Assets.load(path.as_ref()).log_err().flatten() { - if let Some(prompt) = Assets.load(path.as_ref()).log_err().flatten() { - log::debug!("Registering built-in prompt template: {}", id); - let prompt = String::from_utf8_lossy(prompt.as_ref()); - handlebars.register_template_string(id, LineEnding::normalize_cow(prompt))? - } + log::debug!("Registering built-in prompt template: {}", id); + let prompt = String::from_utf8_lossy(prompt.as_ref()); + handlebars.register_template_string(id, LineEnding::normalize_cow(prompt))? } } diff --git a/crates/proto/src/error.rs b/crates/proto/src/error.rs index 7ba08df57d..1724a70217 100644 --- a/crates/proto/src/error.rs +++ b/crates/proto/src/error.rs @@ -190,10 +190,10 @@ impl ErrorExt for RpcError { fn error_tag(&self, k: &str) -> Option<&str> { for tag in &self.tags { let mut parts = tag.split('='); - if let Some(key) = parts.next() { - if key == k { - return parts.next(); - } + if let Some(key) = parts.next() + && key == k + { + return parts.next(); } } None diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index bc837b1a1e..0fd6d5af8c 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -664,10 +664,10 @@ impl RemoteServerProjects { let text = Some(state.editor.read(cx).text(cx)).filter(|text| !text.is_empty()); let index = state.index; self.update_settings_file(cx, move |setting, _| { - if let Some(connections) = setting.ssh_connections.as_mut() { - if let Some(connection) = connections.get_mut(index) { - connection.nickname = text; - } + if let Some(connections) = setting.ssh_connections.as_mut() + && let Some(connection) = connections.get_mut(index) + { + connection.nickname = text; } }); self.mode = Mode::default_mode(&self.ssh_config_servers, cx); diff --git a/crates/refineable/derive_refineable/src/derive_refineable.rs b/crates/refineable/derive_refineable/src/derive_refineable.rs index 3f6b45cc12..ddf3855a4d 100644 --- a/crates/refineable/derive_refineable/src/derive_refineable.rs +++ b/crates/refineable/derive_refineable/src/derive_refineable.rs @@ -510,12 +510,12 @@ fn is_refineable_field(f: &Field) -> bool { } fn is_optional_field(f: &Field) -> bool { - if let Type::Path(typepath) = &f.ty { - if typepath.qself.is_none() { - let segments = &typepath.path.segments; - if segments.len() == 1 && segments.iter().any(|s| s.ident == "Option") { - return true; - } + if let Type::Path(typepath) = &f.ty + && typepath.qself.is_none() + { + let segments = &typepath.path.segments; + if segments.len() == 1 && segments.iter().any(|s| s.ident == "Option") { + return true; } } false diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 71e8f6e8e7..2180fbb5ee 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -1310,10 +1310,10 @@ impl ConnectionPool { return task.clone(); } Some(ConnectionPoolEntry::Connected(ssh)) => { - if let Some(ssh) = ssh.upgrade() { - if !ssh.has_been_killed() { - return Task::ready(Ok(ssh)).shared(); - } + if let Some(ssh) = ssh.upgrade() + && !ssh.has_been_killed() + { + return Task::ready(Ok(ssh)).shared(); } self.connections.remove(&opts); } @@ -1840,26 +1840,25 @@ impl SshRemoteConnection { )), self.ssh_path_style, ); - if !self.socket.connection_options.upload_binary_over_ssh { - if let Some((url, body)) = delegate + if !self.socket.connection_options.upload_binary_over_ssh + && let Some((url, body)) = delegate .get_download_params(self.ssh_platform, release_channel, wanted_version, cx) .await? + { + match self + .download_binary_on_server(&url, &body, &tmp_path_gz, delegate, cx) + .await { - match self - .download_binary_on_server(&url, &body, &tmp_path_gz, delegate, cx) - .await - { - Ok(_) => { - self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx) - .await?; - return Ok(dst_path); - } - Err(e) => { - log::error!( - "Failed to download binary on server, attempting to upload server: {}", - e - ) - } + Ok(_) => { + self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx) + .await?; + return Ok(dst_path); + } + Err(e) => { + log::error!( + "Failed to download binary on server, attempting to upload server: {}", + e + ) } } } diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 4daacb3eec..76e74b75bd 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -951,13 +951,13 @@ fn cleanup_old_binaries() -> Result<()> { for entry in std::fs::read_dir(server_dir)? { let path = entry?.path(); - if let Some(file_name) = path.file_name() { - if let Some(version) = file_name.to_string_lossy().strip_prefix(&prefix) { - if !is_new_version(version) && !is_file_in_use(file_name) { - log::info!("removing old remote server binary: {:?}", path); - std::fs::remove_file(&path)?; - } - } + if let Some(file_name) = path.file_name() + && let Some(version) = file_name.to_string_lossy().strip_prefix(&prefix) + && !is_new_version(version) + && !is_file_in_use(file_name) + { + log::info!("removing old remote server binary: {:?}", path); + std::fs::remove_file(&path)?; } } diff --git a/crates/repl/src/kernels/native_kernel.rs b/crates/repl/src/kernels/native_kernel.rs index aa6a812809..83271fae16 100644 --- a/crates/repl/src/kernels/native_kernel.rs +++ b/crates/repl/src/kernels/native_kernel.rs @@ -399,10 +399,10 @@ async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> Result<Vec<LocalKernelS while let Some(path) = kernelspec_dirs.next().await { match path { Ok(path) => { - if fs.is_dir(path.as_path()).await { - if let Ok(kernelspec) = read_kernelspec_at(path, fs).await { - valid_kernelspecs.push(kernelspec); - } + if fs.is_dir(path.as_path()).await + && let Ok(kernelspec) = read_kernelspec_at(path, fs).await + { + valid_kernelspecs.push(kernelspec); } } Err(err) => log::warn!("Error reading kernelspec directory: {err:?}"), @@ -429,14 +429,14 @@ pub async fn local_kernel_specifications(fs: Arc<dyn Fs>) -> Result<Vec<LocalKer .output() .await; - if let Ok(command) = command { - if command.status.success() { - let python_prefix = String::from_utf8(command.stdout); - if let Ok(python_prefix) = python_prefix { - let python_prefix = PathBuf::from(python_prefix.trim()); - let python_data_dir = python_prefix.join("share").join("jupyter"); - data_dirs.push(python_data_dir); - } + if let Ok(command) = command + && command.status.success() + { + let python_prefix = String::from_utf8(command.stdout); + if let Ok(python_prefix) = python_prefix { + let python_prefix = PathBuf::from(python_prefix.trim()); + let python_data_dir = python_prefix.join("share").join("jupyter"); + data_dirs.push(python_data_dir); } } diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs index ed252b239f..1508c2b531 100644 --- a/crates/repl/src/outputs.rs +++ b/crates/repl/src/outputs.rs @@ -412,10 +412,10 @@ impl ExecutionView { }; // Check for a clear output marker as the previous output, so we can clear it out - if let Some(output) = self.outputs.last() { - if let Output::ClearOutputWaitMarker = output { - self.outputs.clear(); - } + if let Some(output) = self.outputs.last() + && let Output::ClearOutputWaitMarker = output + { + self.outputs.clear(); } self.outputs.push(output); @@ -433,11 +433,11 @@ impl ExecutionView { let mut any = false; self.outputs.iter_mut().for_each(|output| { - if let Some(other_display_id) = output.display_id().as_ref() { - if other_display_id == display_id { - *output = Output::new(data, Some(display_id.to_owned()), window, cx); - any = true; - } + if let Some(other_display_id) = output.display_id().as_ref() + && other_display_id == display_id + { + *output = Output::new(data, Some(display_id.to_owned()), window, cx); + any = true; } }); @@ -452,19 +452,18 @@ impl ExecutionView { window: &mut Window, cx: &mut Context<Self>, ) -> Option<Output> { - if let Some(last_output) = self.outputs.last_mut() { - if let Output::Stream { + if let Some(last_output) = self.outputs.last_mut() + && let Output::Stream { content: last_stream, } = last_output - { - // Don't need to add a new output, we already have a terminal output - // and can just update the most recent terminal output - last_stream.update(cx, |last_stream, cx| { - last_stream.append_text(text, cx); - cx.notify(); - }); - return None; - } + { + // Don't need to add a new output, we already have a terminal output + // and can just update the most recent terminal output + last_stream.update(cx, |last_stream, cx| { + last_stream.append_text(text, cx); + cx.notify(); + }); + return None; } Some(Output::Stream { diff --git a/crates/repl/src/repl_editor.rs b/crates/repl/src/repl_editor.rs index 32b59d639d..f5dd659597 100644 --- a/crates/repl/src/repl_editor.rs +++ b/crates/repl/src/repl_editor.rs @@ -417,10 +417,10 @@ fn runnable_ranges( range: Range<Point>, cx: &mut App, ) -> (Vec<Range<Point>>, Option<Point>) { - if let Some(language) = buffer.language() { - if language.name() == "Markdown".into() { - return (markdown_code_blocks(buffer, range.clone(), cx), None); - } + if let Some(language) = buffer.language() + && language.name() == "Markdown".into() + { + return (markdown_code_blocks(buffer, range.clone(), cx), None); } let (jupytext_snippets, next_cursor) = jupytext_cells(buffer, range.clone()); diff --git a/crates/repl/src/repl_sessions_ui.rs b/crates/repl/src/repl_sessions_ui.rs index 2f4c1f86fc..f57dd64770 100644 --- a/crates/repl/src/repl_sessions_ui.rs +++ b/crates/repl/src/repl_sessions_ui.rs @@ -102,21 +102,16 @@ pub fn init(cx: &mut App) { let editor_handle = cx.entity().downgrade(); - if let Some(language) = language { - if language.name() == "Python".into() { - if let (Some(project_path), Some(project)) = (project_path, project) { - let store = ReplStore::global(cx); - store.update(cx, |store, cx| { - store - .refresh_python_kernelspecs( - project_path.worktree_id, - &project, - cx, - ) - .detach_and_log_err(cx); - }); - } - } + if let Some(language) = language + && language.name() == "Python".into() + && let (Some(project_path), Some(project)) = (project_path, project) + { + let store = ReplStore::global(cx); + store.update(cx, |store, cx| { + store + .refresh_python_kernelspecs(project_path.worktree_id, &project, cx) + .detach_and_log_err(cx); + }); } editor diff --git a/crates/repl/src/repl_store.rs b/crates/repl/src/repl_store.rs index 1a3c9fa49a..b9a36a18ae 100644 --- a/crates/repl/src/repl_store.rs +++ b/crates/repl/src/repl_store.rs @@ -171,10 +171,10 @@ impl ReplStore { .map(KernelSpecification::Jupyter) .collect::<Vec<_>>(); - if let Some(remote_task) = remote_kernel_specifications { - if let Ok(remote_specs) = remote_task.await { - all_specs.extend(remote_specs); - } + if let Some(remote_task) = remote_kernel_specifications + && let Ok(remote_specs) = remote_task.await + { + all_specs.extend(remote_specs); } anyhow::Ok(all_specs) diff --git a/crates/reqwest_client/src/reqwest_client.rs b/crates/reqwest_client/src/reqwest_client.rs index 6461a0ae17..9053f4e452 100644 --- a/crates/reqwest_client/src/reqwest_client.rs +++ b/crates/reqwest_client/src/reqwest_client.rs @@ -201,12 +201,11 @@ pub fn poll_read_buf( } fn redact_error(mut error: reqwest::Error) -> reqwest::Error { - if let Some(url) = error.url_mut() { - if let Some(query) = url.query() { - if let Cow::Owned(redacted) = REDACT_REGEX.replace_all(query, "key=REDACTED") { - url.set_query(Some(redacted.as_str())); - } - } + if let Some(url) = error.url_mut() + && let Some(query) = url.query() + && let Cow::Owned(redacted) = REDACT_REGEX.replace_all(query, "key=REDACTED") + { + url.set_query(Some(redacted.as_str())); } error } diff --git a/crates/rich_text/src/rich_text.rs b/crates/rich_text/src/rich_text.rs index 575e4318c3..2af9988f03 100644 --- a/crates/rich_text/src/rich_text.rs +++ b/crates/rich_text/src/rich_text.rs @@ -162,10 +162,10 @@ impl RichText { } } for range in &custom_tooltip_ranges { - if range.contains(&idx) { - if let Some(f) = &custom_tooltip_fn { - return f(idx, range.clone(), window, cx); - } + if range.contains(&idx) + && let Some(f) = &custom_tooltip_fn + { + return f(idx, range.clone(), window, cx); } } None @@ -281,13 +281,12 @@ pub fn render_markdown_mut( if style != HighlightStyle::default() && last_run_len < text.len() { let mut new_highlight = true; - if let Some((last_range, last_style)) = highlights.last_mut() { - if last_range.end == last_run_len - && last_style == &Highlight::Highlight(style) - { - last_range.end = text.len(); - new_highlight = false; - } + if let Some((last_range, last_style)) = highlights.last_mut() + && last_range.end == last_run_len + && last_style == &Highlight::Highlight(style) + { + last_range.end = text.len(); + new_highlight = false; } if new_highlight { highlights diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index d8ed3bfac8..78ce6f78a2 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -31,22 +31,21 @@ impl Rope { } pub fn append(&mut self, rope: Rope) { - if let Some(chunk) = rope.chunks.first() { - if self + if let Some(chunk) = rope.chunks.first() + && (self .chunks .last() .map_or(false, |c| c.text.len() < chunk::MIN_BASE) - || chunk.text.len() < chunk::MIN_BASE - { - self.push_chunk(chunk.as_slice()); + || chunk.text.len() < chunk::MIN_BASE) + { + self.push_chunk(chunk.as_slice()); - let mut chunks = rope.chunks.cursor::<()>(&()); - chunks.next(); - chunks.next(); - self.chunks.append(chunks.suffix(), &()); - self.check_invariants(); - return; - } + let mut chunks = rope.chunks.cursor::<()>(&()); + chunks.next(); + chunks.next(); + self.chunks.append(chunks.suffix(), &()); + self.check_invariants(); + return; } self.chunks.append(rope.chunks.clone(), &()); @@ -735,16 +734,16 @@ impl<'a> Chunks<'a> { self.chunks .search_backward(|summary| summary.text.lines.row > 0); self.offset = *self.chunks.start(); - if let Some(chunk) = self.chunks.item() { - if let Some(newline_ix) = chunk.text.rfind('\n') { - self.offset += newline_ix + 1; - if self.offset_is_valid() { - if self.offset == self.chunks.end() { - self.chunks.next(); - } - - return true; + if let Some(chunk) = self.chunks.item() + && let Some(newline_ix) = chunk.text.rfind('\n') + { + self.offset += newline_ix + 1; + if self.offset_is_valid() { + if self.offset == self.chunks.end() { + self.chunks.next(); } + + return true; } } diff --git a/crates/rpc/src/notification.rs b/crates/rpc/src/notification.rs index cb405b63ca..338ef33c8a 100644 --- a/crates/rpc/src/notification.rs +++ b/crates/rpc/src/notification.rs @@ -48,10 +48,10 @@ impl Notification { let Some(Value::String(kind)) = value.remove(KIND) else { unreachable!("kind is the enum tag") }; - if let map::Entry::Occupied(e) = value.entry(ENTITY_ID) { - if e.get().is_u64() { - entity_id = e.remove().as_u64(); - } + if let map::Entry::Occupied(e) = value.entry(ENTITY_ID) + && e.get().is_u64() + { + entity_id = e.remove().as_u64(); } proto::Notification { kind, diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 80a104641f..8b77788d22 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -520,10 +520,10 @@ impl Peer { &response.payload { // Remove the transmitting end of the response channel to end the stream. - if let Some(channels) = stream_response_channels.upgrade() { - if let Some(channels) = channels.lock().as_mut() { - channels.remove(&message_id); - } + if let Some(channels) = stream_response_channels.upgrade() + && let Some(channels) = channels.lock().as_mut() + { + channels.remove(&message_id); } None } else { diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index ebec96dd7b..ec83993e5f 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -456,11 +456,11 @@ impl RulesLibrary { pub fn new_rule(&mut self, window: &mut Window, cx: &mut Context<Self>) { // If we already have an untitled rule, use that instead // of creating a new one. - if let Some(metadata) = self.store.read(cx).first() { - if metadata.title.is_none() { - self.load_rule(metadata.id, true, window, cx); - return; - } + if let Some(metadata) = self.store.read(cx).first() + && metadata.title.is_none() + { + self.load_rule(metadata.id, true, window, cx); + return; } let prompt_id = PromptId::new(); @@ -706,15 +706,13 @@ impl RulesLibrary { .map_or(true, |old_selected_prompt| { old_selected_prompt.id != prompt_id }) - { - if let Some(ix) = picker + && let Some(ix) = picker .delegate .matches .iter() .position(|mat| mat.id == prompt_id) - { - picker.set_selected_index(ix, None, true, window, cx); - } + { + picker.set_selected_index(ix, None, true, window, cx); } } else { picker.focus(window, cx); @@ -869,10 +867,10 @@ impl RulesLibrary { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(rule_id) = self.active_rule_id { - if let Some(rule_editor) = self.rule_editors.get(&rule_id) { - window.focus(&rule_editor.body_editor.focus_handle(cx)); - } + if let Some(rule_id) = self.active_rule_id + && let Some(rule_editor) = self.rule_editors.get(&rule_id) + { + window.focus(&rule_editor.body_editor.focus_handle(cx)); } } @@ -882,10 +880,10 @@ impl RulesLibrary { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(rule_id) = self.active_rule_id { - if let Some(rule_editor) = self.rule_editors.get(&rule_id) { - window.focus(&rule_editor.title_editor.focus_handle(cx)); - } + if let Some(rule_id) = self.active_rule_id + && let Some(rule_editor) = self.rule_editors.get(&rule_id) + { + window.focus(&rule_editor.title_editor.focus_handle(cx)); } } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 189f48e6b6..78e4da7bc6 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -794,15 +794,13 @@ impl BufferSearchBar { } pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context<Self>) { - if let Some(match_ix) = self.active_match_index { - if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { - if let Some(matches) = self - .searchable_items_with_matches - .get(&active_searchable_item.downgrade()) - { - active_searchable_item.activate_match(match_ix, matches, window, cx) - } - } + if let Some(match_ix) = self.active_match_index + && let Some(active_searchable_item) = self.active_searchable_item.as_ref() + && let Some(matches) = self + .searchable_items_with_matches + .get(&active_searchable_item.downgrade()) + { + active_searchable_item.activate_match(match_ix, matches, window, cx) } } @@ -951,16 +949,15 @@ impl BufferSearchBar { window: &mut Window, cx: &mut Context<Self>, ) { - if !self.dismissed && self.active_match_index.is_some() { - if let Some(searchable_item) = self.active_searchable_item.as_ref() { - if let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - searchable_item.select_matches(matches, window, cx); - self.focus_editor(&FocusEditor, window, cx); - } - } + if !self.dismissed + && self.active_match_index.is_some() + && let Some(searchable_item) = self.active_searchable_item.as_ref() + && let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + searchable_item.select_matches(matches, window, cx); + self.focus_editor(&FocusEditor, window, cx); } } @@ -971,59 +968,55 @@ impl BufferSearchBar { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(index) = self.active_match_index { - if let Some(searchable_item) = self.active_searchable_item.as_ref() { - if let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - .filter(|matches| !matches.is_empty()) - { - // If 'wrapscan' is disabled, searches do not wrap around the end of the file. - if !EditorSettings::get_global(cx).search_wrap - && ((direction == Direction::Next && index + count >= matches.len()) - || (direction == Direction::Prev && index < count)) - { - crate::show_no_more_matches(window, cx); - return; - } - let new_match_index = searchable_item - .match_index_for_direction(matches, index, direction, count, window, cx); - - searchable_item.update_matches(matches, window, cx); - searchable_item.activate_match(new_match_index, matches, window, cx); - } + if let Some(index) = self.active_match_index + && let Some(searchable_item) = self.active_searchable_item.as_ref() + && let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + .filter(|matches| !matches.is_empty()) + { + // If 'wrapscan' is disabled, searches do not wrap around the end of the file. + if !EditorSettings::get_global(cx).search_wrap + && ((direction == Direction::Next && index + count >= matches.len()) + || (direction == Direction::Prev && index < count)) + { + crate::show_no_more_matches(window, cx); + return; } + let new_match_index = searchable_item + .match_index_for_direction(matches, index, direction, count, window, cx); + + searchable_item.update_matches(matches, window, cx); + searchable_item.activate_match(new_match_index, matches, window, cx); } } pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context<Self>) { - if let Some(searchable_item) = self.active_searchable_item.as_ref() { - if let Some(matches) = self + if let Some(searchable_item) = self.active_searchable_item.as_ref() + && let Some(matches) = self .searchable_items_with_matches .get(&searchable_item.downgrade()) - { - if matches.is_empty() { - return; - } - searchable_item.update_matches(matches, window, cx); - searchable_item.activate_match(0, matches, window, cx); + { + if matches.is_empty() { + return; } + searchable_item.update_matches(matches, window, cx); + searchable_item.activate_match(0, matches, window, cx); } } pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context<Self>) { - if let Some(searchable_item) = self.active_searchable_item.as_ref() { - if let Some(matches) = self + if let Some(searchable_item) = self.active_searchable_item.as_ref() + && let Some(matches) = self .searchable_items_with_matches .get(&searchable_item.downgrade()) - { - if matches.is_empty() { - return; - } - let new_match_index = matches.len() - 1; - searchable_item.update_matches(matches, window, cx); - searchable_item.activate_match(new_match_index, matches, window, cx); + { + if matches.is_empty() { + return; } + let new_match_index = matches.len() - 1; + searchable_item.update_matches(matches, window, cx); + searchable_item.activate_match(new_match_index, matches, window, cx); } } @@ -1344,15 +1337,14 @@ impl BufferSearchBar { window: &mut Window, cx: &mut Context<Self>, ) { - if self.query(cx).is_empty() { - if let Some(new_query) = self + if self.query(cx).is_empty() + && let Some(new_query) = self .search_history .current(&mut self.search_history_cursor) .map(str::to_string) - { - drop(self.search(&new_query, Some(self.search_options), window, cx)); - return; - } + { + drop(self.search(&new_query, Some(self.search_options), window, cx)); + return; } if let Some(new_query) = self @@ -1384,25 +1376,23 @@ impl BufferSearchBar { fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context<Self>) { let mut should_propagate = true; - if !self.dismissed && self.active_search.is_some() { - if let Some(searchable_item) = self.active_searchable_item.as_ref() { - if let Some(query) = self.active_search.as_ref() { - if let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - if let Some(active_index) = self.active_match_index { - let query = query - .as_ref() - .clone() - .with_replacement(self.replacement(cx)); - searchable_item.replace(matches.at(active_index), &query, window, cx); - self.select_next_match(&SelectNextMatch, window, cx); - } - should_propagate = false; - } - } + if !self.dismissed + && self.active_search.is_some() + && let Some(searchable_item) = self.active_searchable_item.as_ref() + && let Some(query) = self.active_search.as_ref() + && let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + if let Some(active_index) = self.active_match_index { + let query = query + .as_ref() + .clone() + .with_replacement(self.replacement(cx)); + searchable_item.replace(matches.at(active_index), &query, window, cx); + self.select_next_match(&SelectNextMatch, window, cx); } + should_propagate = false; } if !should_propagate { cx.stop_propagation(); @@ -1410,21 +1400,19 @@ impl BufferSearchBar { } pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context<Self>) { - if !self.dismissed && self.active_search.is_some() { - if let Some(searchable_item) = self.active_searchable_item.as_ref() { - if let Some(query) = self.active_search.as_ref() { - if let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - let query = query - .as_ref() - .clone() - .with_replacement(self.replacement(cx)); - searchable_item.replace_all(&mut matches.iter(), &query, window, cx); - } - } - } + if !self.dismissed + && self.active_search.is_some() + && let Some(searchable_item) = self.active_searchable_item.as_ref() + && let Some(query) = self.active_search.as_ref() + && let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + let query = query + .as_ref() + .clone() + .with_replacement(self.replacement(cx)); + searchable_item.replace_all(&mut matches.iter(), &query, window, cx); } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 443bbb0427..51cb1fdb26 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -775,15 +775,15 @@ impl ProjectSearchView { // Subscribe to query_editor in order to reraise editor events for workspace item activation purposes subscriptions.push( cx.subscribe(&query_editor, |this, _, event: &EditorEvent, cx| { - if let EditorEvent::Edited { .. } = event { - if EditorSettings::get_global(cx).use_smartcase_search { - let query = this.search_query_text(cx); - if !query.is_empty() - && this.search_options.contains(SearchOptions::CASE_SENSITIVE) - != contains_uppercase(&query) - { - this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); - } + if let EditorEvent::Edited { .. } = event + && EditorSettings::get_global(cx).use_smartcase_search + { + let query = this.search_query_text(cx); + if !query.is_empty() + && this.search_options.contains(SearchOptions::CASE_SENSITIVE) + != contains_uppercase(&query) + { + this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); } } cx.emit(ViewEvent::EditorEvent(event.clone())) @@ -947,14 +947,14 @@ impl ProjectSearchView { { let new_query = search_view.update(cx, |search_view, cx| { let new_query = search_view.build_search_query(cx); - if new_query.is_some() { - if let Some(old_query) = search_view.entity.read(cx).active_query.clone() { - search_view.query_editor.update(cx, |editor, cx| { - editor.set_text(old_query.as_str(), window, cx); - }); - search_view.search_options = SearchOptions::from_query(&old_query); - search_view.adjust_query_regex_language(cx); - } + if new_query.is_some() + && let Some(old_query) = search_view.entity.read(cx).active_query.clone() + { + search_view.query_editor.update(cx, |editor, cx| { + editor.set_text(old_query.as_str(), window, cx); + }); + search_view.search_options = SearchOptions::from_query(&old_query); + search_view.adjust_query_regex_language(cx); } new_query }); @@ -1844,8 +1844,8 @@ impl ProjectSearchBar { ), ] { if editor.focus_handle(cx).is_focused(window) { - if editor.read(cx).text(cx).is_empty() { - if let Some(new_query) = search_view + if editor.read(cx).text(cx).is_empty() + && let Some(new_query) = search_view .entity .read(cx) .project @@ -1853,10 +1853,9 @@ impl ProjectSearchBar { .search_history(kind) .current(search_view.entity.read(cx).cursor(kind)) .map(str::to_string) - { - search_view.set_search_editor(kind, &new_query, window, cx); - return; - } + { + search_view.set_search_editor(kind, &new_query, window, cx); + return; } if let Some(new_query) = search_view.entity.update(cx, |model, cx| { diff --git a/crates/semantic_index/src/embedding_index.rs b/crates/semantic_index/src/embedding_index.rs index d2d10ad0ad..eeb3c91fcd 100644 --- a/crates/semantic_index/src/embedding_index.rs +++ b/crates/semantic_index/src/embedding_index.rs @@ -194,11 +194,11 @@ impl EmbeddingIndex { project::PathChange::Added | project::PathChange::Updated | project::PathChange::AddedOrUpdated => { - if let Some(entry) = worktree.entry_for_id(*entry_id) { - if entry.is_file() { - let handle = entries_being_indexed.insert(entry.id); - updated_entries_tx.send((entry.clone(), handle)).await?; - } + if let Some(entry) = worktree.entry_for_id(*entry_id) + && entry.is_file() + { + let handle = entries_being_indexed.insert(entry.id); + updated_entries_tx.send((entry.clone(), handle)).await?; } } project::PathChange::Removed => { diff --git a/crates/semantic_index/src/project_index.rs b/crates/semantic_index/src/project_index.rs index 5e852327dd..60b2770dd3 100644 --- a/crates/semantic_index/src/project_index.rs +++ b/crates/semantic_index/src/project_index.rs @@ -384,10 +384,10 @@ impl ProjectIndex { cx: &App, ) -> Option<Entity<WorktreeIndex>> { for index in self.worktree_indices.values() { - if let WorktreeIndexHandle::Loaded { index, .. } = index { - if index.read(cx).worktree().read(cx).id() == worktree_id { - return Some(index.clone()); - } + if let WorktreeIndexHandle::Loaded { index, .. } = index + && index.read(cx).worktree().read(cx).id() == worktree_id + { + return Some(index.clone()); } } None diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index a9cc08382b..1dafeb072f 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -174,14 +174,13 @@ impl SemanticDb { file_content[start_line_byte_offset..end_line_byte_offset].to_string(); LineEnding::normalize(&mut excerpt_content); - if let Some(prev_result) = loaded_results.last_mut() { - if prev_result.full_path == full_path { - if *prev_result.row_range.end() + 1 == start_row { - prev_result.row_range = *prev_result.row_range.start()..=end_row; - prev_result.excerpt_content.push_str(&excerpt_content); - continue; - } - } + if let Some(prev_result) = loaded_results.last_mut() + && prev_result.full_path == full_path + && *prev_result.row_range.end() + 1 == start_row + { + prev_result.row_range = *prev_result.row_range.start()..=end_row; + prev_result.excerpt_content.push_str(&excerpt_content); + continue; } loaded_results.push(LoadedSearchResult { diff --git a/crates/semantic_index/src/summary_index.rs b/crates/semantic_index/src/summary_index.rs index 20858c8d3f..d1c9a3abac 100644 --- a/crates/semantic_index/src/summary_index.rs +++ b/crates/semantic_index/src/summary_index.rs @@ -379,18 +379,14 @@ impl SummaryIndex { | project::PathChange::Added | project::PathChange::Updated | project::PathChange::AddedOrUpdated => { - if let Some(entry) = worktree.entry_for_id(*entry_id) { - if entry.is_file() { - let needs_summary = Self::add_to_backlog( - Arc::clone(&backlog), - digest_db, - &txn, - entry, - ); + if let Some(entry) = worktree.entry_for_id(*entry_id) + && entry.is_file() + { + let needs_summary = + Self::add_to_backlog(Arc::clone(&backlog), digest_db, &txn, entry); - if !needs_summary.is_empty() { - tx.send(needs_summary).await?; - } + if !needs_summary.is_empty() { + tx.send(needs_summary).await?; } } } diff --git a/crates/session/src/session.rs b/crates/session/src/session.rs index f027df8762..438059fef7 100644 --- a/crates/session/src/session.rs +++ b/crates/session/src/session.rs @@ -70,11 +70,11 @@ impl AppSession { let _serialization_task = cx.spawn(async move |_, cx| { let mut current_window_stack = Vec::new(); loop { - if let Some(windows) = cx.update(|cx| window_stack(cx)).ok().flatten() { - if windows != current_window_stack { - store_window_stack(&windows).await; - current_window_stack = windows; - } + if let Some(windows) = cx.update(|cx| window_stack(cx)).ok().flatten() + && windows != current_window_stack + { + store_window_stack(&windows).await; + current_window_stack = windows; } cx.background_executor() diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index b0f7d2449e..e95617512d 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -543,27 +543,27 @@ impl KeymapFile { // // When a struct with no deserializable fields is added by deriving `Action`, an empty // object schema is produced. The action should be invoked without data in this case. - if let Some(schema) = action_schema { - if schema != empty_object { - let mut matches_action_name = json_schema!({ - "const": name - }); - if let Some(desc) = description.clone() { - add_description(&mut matches_action_name, desc); - } - if let Some(message) = deprecation_messages.get(name) { - add_deprecation(&mut matches_action_name, message.to_string()); - } else if let Some(new_name) = deprecation { - add_deprecation_preferred_name(&mut matches_action_name, new_name); - } - let action_with_input = json_schema!({ - "type": "array", - "items": [matches_action_name, schema], - "minItems": 2, - "maxItems": 2 - }); - keymap_action_alternatives.push(action_with_input); + if let Some(schema) = action_schema + && schema != empty_object + { + let mut matches_action_name = json_schema!({ + "const": name + }); + if let Some(desc) = description.clone() { + add_description(&mut matches_action_name, desc); } + if let Some(message) = deprecation_messages.get(name) { + add_deprecation(&mut matches_action_name, message.to_string()); + } else if let Some(new_name) = deprecation { + add_deprecation_preferred_name(&mut matches_action_name, new_name); + } + let action_with_input = json_schema!({ + "type": "array", + "items": [matches_action_name, schema], + "minItems": 2, + "maxItems": 2 + }); + keymap_action_alternatives.push(action_with_input); } } @@ -593,10 +593,10 @@ impl KeymapFile { match fs.load(paths::keymap_file()).await { result @ Ok(_) => result, Err(err) => { - if let Some(e) = err.downcast_ref::<std::io::Error>() { - if e.kind() == std::io::ErrorKind::NotFound { - return Ok(crate::initial_keymap_content().to_string()); - } + if let Some(e) = err.downcast_ref::<std::io::Error>() + && e.kind() == std::io::ErrorKind::NotFound + { + return Ok(crate::initial_keymap_content().to_string()); } Err(err) } diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index c43f3e79e8..d31dd82da4 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -67,10 +67,10 @@ pub fn watch_config_file( break; } - if let Ok(contents) = fs.load(&path).await { - if tx.unbounded_send(contents).is_err() { - break; - } + if let Ok(contents) = fs.load(&path).await + && tx.unbounded_send(contents).is_err() + { + break; } } }) @@ -88,12 +88,11 @@ pub fn watch_config_dir( executor .spawn(async move { for file_path in &config_paths { - if fs.metadata(file_path).await.is_ok_and(|v| v.is_some()) { - if let Ok(contents) = fs.load(file_path).await { - if tx.unbounded_send(contents).is_err() { - return; - } - } + if fs.metadata(file_path).await.is_ok_and(|v| v.is_some()) + && let Ok(contents) = fs.load(file_path).await + && tx.unbounded_send(contents).is_err() + { + return; } } @@ -110,10 +109,10 @@ pub fn watch_config_dir( } } Some(PathEventKind::Created) | Some(PathEventKind::Changed) => { - if let Ok(contents) = fs.load(&event.path).await { - if tx.unbounded_send(contents).is_err() { - return; - } + if let Ok(contents) = fs.load(&event.path).await + && tx.unbounded_send(contents).is_err() + { + return; } } _ => {} diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs index e6683857e7..8e7e11dc82 100644 --- a/crates/settings/src/settings_json.rs +++ b/crates/settings/src/settings_json.rs @@ -369,13 +369,12 @@ pub fn replace_top_level_array_value_in_json_text( if cursor.node().kind() == "," { remove_range.end = cursor.node().range().end_byte; } - if let Some(next_newline) = &text[remove_range.end + 1..].find('\n') { - if text[remove_range.end + 1..remove_range.end + next_newline] + if let Some(next_newline) = &text[remove_range.end + 1..].find('\n') + && text[remove_range.end + 1..remove_range.end + next_newline] .chars() .all(|c| c.is_ascii_whitespace()) - { - remove_range.end = remove_range.end + next_newline; - } + { + remove_range.end = remove_range.end + next_newline; } } else { while cursor.goto_previous_sibling() @@ -508,10 +507,10 @@ pub fn append_top_level_array_value_in_json_text( replace_value.insert(0, ','); } } else { - if let Some(prev_newline) = text[..replace_range.start].rfind('\n') { - if text[prev_newline..replace_range.start].trim().is_empty() { - replace_range.start = prev_newline; - } + if let Some(prev_newline) = text[..replace_range.start].rfind('\n') + && text[prev_newline..replace_range.start].trim().is_empty() + { + replace_range.start = prev_newline; } let indent = format!("\n{space:width$}", width = tab_size); replace_value = replace_value.replace('\n', &indent); diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index bfdafbffe8..23f495d850 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -346,14 +346,13 @@ impl SettingsStore { } let mut profile_value = None; - if let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() { - if let Some(profiles) = self.raw_user_settings.get("profiles") { - if let Some(profile_settings) = profiles.get(&active_profile.0) { - profile_value = setting_value - .deserialize_setting(profile_settings) - .log_err(); - } - } + if let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() + && let Some(profiles) = self.raw_user_settings.get("profiles") + && let Some(profile_settings) = profiles.get(&active_profile.0) + { + profile_value = setting_value + .deserialize_setting(profile_settings) + .log_err(); } let server_value = self @@ -482,10 +481,10 @@ impl SettingsStore { match fs.load(paths::settings_file()).await { result @ Ok(_) => result, Err(err) => { - if let Some(e) = err.downcast_ref::<std::io::Error>() { - if e.kind() == std::io::ErrorKind::NotFound { - return Ok(crate::initial_user_settings_content().to_string()); - } + if let Some(e) = err.downcast_ref::<std::io::Error>() + && e.kind() == std::io::ErrorKind::NotFound + { + return Ok(crate::initial_user_settings_content().to_string()); } Err(err) } @@ -496,10 +495,10 @@ impl SettingsStore { match fs.load(paths::global_settings_file()).await { result @ Ok(_) => result, Err(err) => { - if let Some(e) = err.downcast_ref::<std::io::Error>() { - if e.kind() == std::io::ErrorKind::NotFound { - return Ok("{}".to_string()); - } + if let Some(e) = err.downcast_ref::<std::io::Error>() + && e.kind() == std::io::ErrorKind::NotFound + { + return Ok("{}".to_string()); } Err(err) } @@ -955,13 +954,13 @@ impl SettingsStore { let mut setting_schema = setting_value.json_schema(&mut generator); if let Some(key) = setting_value.key() { - if let Some(properties) = combined_schema.get_mut("properties") { - if let Some(properties_obj) = properties.as_object_mut() { - if let Some(target) = properties_obj.get_mut(key) { - merge_schema(target, setting_schema.to_value()); - } else { - properties_obj.insert(key.to_string(), setting_schema.to_value()); - } + if let Some(properties) = combined_schema.get_mut("properties") + && let Some(properties_obj) = properties.as_object_mut() + { + if let Some(target) = properties_obj.get_mut(key) { + merge_schema(target, setting_schema.to_value()); + } else { + properties_obj.insert(key.to_string(), setting_schema.to_value()); } } } else { @@ -1038,16 +1037,15 @@ impl SettingsStore { | "additionalProperties" => { if let Some(old_value) = target_obj.insert(source_key.clone(), source_value.clone()) + && old_value != source_value { - if old_value != source_value { - log::error!( - "bug: while merging JSON schemas, \ + log::error!( + "bug: while merging JSON schemas, \ mismatch `\"{}\": {}` (before was `{}`)", - source_key, - old_value, - source_value - ); - } + source_key, + old_value, + source_value + ); } } _ => { @@ -1168,35 +1166,31 @@ impl SettingsStore { if let Some(release_settings) = &self .raw_user_settings .get(release_channel::RELEASE_CHANNEL.dev_name()) - { - if let Some(release_settings) = setting_value + && let Some(release_settings) = setting_value .deserialize_setting(release_settings) .log_err() - { - release_channel_settings = Some(release_settings); - } + { + release_channel_settings = Some(release_settings); } let mut os_settings = None; - if let Some(settings) = &self.raw_user_settings.get(env::consts::OS) { - if let Some(settings) = setting_value.deserialize_setting(settings).log_err() { - os_settings = Some(settings); - } + if let Some(settings) = &self.raw_user_settings.get(env::consts::OS) + && let Some(settings) = setting_value.deserialize_setting(settings).log_err() + { + os_settings = Some(settings); } let mut profile_settings = None; - if let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() { - if let Some(profiles) = self.raw_user_settings.get("profiles") { - if let Some(profile_json) = profiles.get(&active_profile.0) { - profile_settings = - setting_value.deserialize_setting(profile_json).log_err(); - } - } + if let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() + && let Some(profiles) = self.raw_user_settings.get("profiles") + && let Some(profile_json) = profiles.get(&active_profile.0) + { + profile_settings = setting_value.deserialize_setting(profile_json).log_err(); } // If the global settings file changed, reload the global value for the field. - if changed_local_path.is_none() { - if let Some(value) = setting_value + if changed_local_path.is_none() + && let Some(value) = setting_value .load_setting( SettingsSources { default: &default_settings, @@ -1212,9 +1206,8 @@ impl SettingsStore { cx, ) .log_err() - { - setting_value.set_global_value(value); - } + { + setting_value.set_global_value(value); } // Reload the local values for the setting. @@ -1223,12 +1216,12 @@ impl SettingsStore { for ((root_id, directory_path), local_settings) in &self.raw_local_settings { // Build a stack of all of the local values for that setting. while let Some(prev_entry) = paths_stack.last() { - if let Some((prev_root_id, prev_path)) = prev_entry { - if root_id != prev_root_id || !directory_path.starts_with(prev_path) { - paths_stack.pop(); - project_settings_stack.pop(); - continue; - } + if let Some((prev_root_id, prev_path)) = prev_entry + && (root_id != prev_root_id || !directory_path.starts_with(prev_path)) + { + paths_stack.pop(); + project_settings_stack.pop(); + continue; } break; } diff --git a/crates/snippets_ui/src/snippets_ui.rs b/crates/snippets_ui/src/snippets_ui.rs index bf0ef63bff..db76ab6f9a 100644 --- a/crates/snippets_ui/src/snippets_ui.rs +++ b/crates/snippets_ui/src/snippets_ui.rs @@ -163,13 +163,12 @@ impl ScopeSelectorDelegate { for entry in read_dir { if let Some(entry) = entry.log_err() { let path = entry.path(); - if let (Some(stem), Some(extension)) = (path.file_stem(), path.extension()) { - if extension.to_os_string().to_str() == Some("json") { - if let Ok(file_name) = stem.to_os_string().into_string() { - existing_scopes - .insert(ScopeName::from(ScopeFileName(Cow::Owned(file_name)))); - } - } + if let (Some(stem), Some(extension)) = (path.file_stem(), path.extension()) + && extension.to_os_string().to_str() == Some("json") + && let Ok(file_name) = stem.to_os_string().into_string() + { + existing_scopes + .insert(ScopeName::from(ScopeFileName(Cow::Owned(file_name)))); } } } diff --git a/crates/sqlez/src/connection.rs b/crates/sqlez/src/connection.rs index f56ae2427d..228bd4c6a2 100644 --- a/crates/sqlez/src/connection.rs +++ b/crates/sqlez/src/connection.rs @@ -213,38 +213,37 @@ impl Connection { fn parse_alter_table(remaining_sql_str: &str) -> Option<(String, String)> { let remaining_sql_str = remaining_sql_str.to_lowercase(); - if remaining_sql_str.starts_with("alter") { - if let Some(table_offset) = remaining_sql_str.find("table") { - let after_table_offset = table_offset + "table".len(); - let table_to_alter = remaining_sql_str - .chars() - .skip(after_table_offset) - .skip_while(|c| c.is_whitespace()) - .take_while(|c| !c.is_whitespace()) - .collect::<String>(); - if !table_to_alter.is_empty() { - let column_name = - if let Some(rename_offset) = remaining_sql_str.find("rename column") { - let after_rename_offset = rename_offset + "rename column".len(); - remaining_sql_str - .chars() - .skip(after_rename_offset) - .skip_while(|c| c.is_whitespace()) - .take_while(|c| !c.is_whitespace()) - .collect::<String>() - } else if let Some(drop_offset) = remaining_sql_str.find("drop column") { - let after_drop_offset = drop_offset + "drop column".len(); - remaining_sql_str - .chars() - .skip(after_drop_offset) - .skip_while(|c| c.is_whitespace()) - .take_while(|c| !c.is_whitespace()) - .collect::<String>() - } else { - "__place_holder_column_for_syntax_checking".to_string() - }; - return Some((table_to_alter, column_name)); - } + if remaining_sql_str.starts_with("alter") + && let Some(table_offset) = remaining_sql_str.find("table") + { + let after_table_offset = table_offset + "table".len(); + let table_to_alter = remaining_sql_str + .chars() + .skip(after_table_offset) + .skip_while(|c| c.is_whitespace()) + .take_while(|c| !c.is_whitespace()) + .collect::<String>(); + if !table_to_alter.is_empty() { + let column_name = if let Some(rename_offset) = remaining_sql_str.find("rename column") { + let after_rename_offset = rename_offset + "rename column".len(); + remaining_sql_str + .chars() + .skip(after_rename_offset) + .skip_while(|c| c.is_whitespace()) + .take_while(|c| !c.is_whitespace()) + .collect::<String>() + } else if let Some(drop_offset) = remaining_sql_str.find("drop column") { + let after_drop_offset = drop_offset + "drop column".len(); + remaining_sql_str + .chars() + .skip(after_drop_offset) + .skip_while(|c| c.is_whitespace()) + .take_while(|c| !c.is_whitespace()) + .collect::<String>() + } else { + "__place_holder_column_for_syntax_checking".to_string() + }; + return Some((table_to_alter, column_name)); } } None diff --git a/crates/sum_tree/src/cursor.rs b/crates/sum_tree/src/cursor.rs index 50a556a6d2..53458b65ec 100644 --- a/crates/sum_tree/src/cursor.rs +++ b/crates/sum_tree/src/cursor.rs @@ -530,10 +530,10 @@ where debug_assert!(self.stack.is_empty() || self.stack.last().unwrap().tree.0.is_leaf()); let mut end = self.position.clone(); - if bias == Bias::Left { - if let Some(summary) = self.item_summary() { - end.add_summary(summary, self.cx); - } + if bias == Bias::Left + && let Some(summary) = self.item_summary() + { + end.add_summary(summary, self.cx); } target.cmp(&end, self.cx) == Ordering::Equal diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index 3a12e3a681..f551bb32e6 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -674,11 +674,11 @@ impl<T: KeyedItem> SumTree<T> { *self = { let mut cursor = self.cursor::<T::Key>(cx); let mut new_tree = cursor.slice(&item.key(), Bias::Left); - if let Some(cursor_item) = cursor.item() { - if cursor_item.key() == item.key() { - replaced = Some(cursor_item.clone()); - cursor.next(); - } + if let Some(cursor_item) = cursor.item() + && cursor_item.key() == item.key() + { + replaced = Some(cursor_item.clone()); + cursor.next(); } new_tree.push(item, cx); new_tree.append(cursor.suffix(), cx); @@ -692,11 +692,11 @@ impl<T: KeyedItem> SumTree<T> { *self = { let mut cursor = self.cursor::<T::Key>(cx); let mut new_tree = cursor.slice(key, Bias::Left); - if let Some(item) = cursor.item() { - if item.key() == *key { - removed = Some(item.clone()); - cursor.next(); - } + if let Some(item) = cursor.item() + && item.key() == *key + { + removed = Some(item.clone()); + cursor.next(); } new_tree.append(cursor.suffix(), cx); new_tree @@ -736,11 +736,11 @@ impl<T: KeyedItem> SumTree<T> { old_item = cursor.item(); } - if let Some(old_item) = old_item { - if old_item.key() == new_key { - removed.push(old_item.clone()); - cursor.next(); - } + if let Some(old_item) = old_item + && old_item.key() == new_key + { + removed.push(old_item.clone()); + cursor.next(); } match edit { diff --git a/crates/svg_preview/src/svg_preview_view.rs b/crates/svg_preview/src/svg_preview_view.rs index 327856d749..4e4c83c8de 100644 --- a/crates/svg_preview/src/svg_preview_view.rs +++ b/crates/svg_preview/src/svg_preview_view.rs @@ -32,79 +32,74 @@ pub enum SvgPreviewMode { impl SvgPreviewView { pub fn register(workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context<Workspace>) { workspace.register_action(move |workspace, _: &OpenPreview, window, cx| { - if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) { - if Self::is_svg_file(&editor, cx) { - let view = Self::create_svg_view( - SvgPreviewMode::Default, - workspace, - editor.clone(), - window, - cx, - ); - workspace.active_pane().update(cx, |pane, cx| { - if let Some(existing_view_idx) = - Self::find_existing_preview_item_idx(pane, &editor, cx) - { - pane.activate_item(existing_view_idx, true, true, window, cx); - } else { - pane.add_item(Box::new(view), true, true, None, window, cx) - } - }); - cx.notify(); - } + if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) + && Self::is_svg_file(&editor, cx) + { + let view = Self::create_svg_view( + SvgPreviewMode::Default, + workspace, + editor.clone(), + window, + cx, + ); + workspace.active_pane().update(cx, |pane, cx| { + if let Some(existing_view_idx) = + Self::find_existing_preview_item_idx(pane, &editor, cx) + { + pane.activate_item(existing_view_idx, true, true, window, cx); + } else { + pane.add_item(Box::new(view), true, true, None, window, cx) + } + }); + cx.notify(); } }); workspace.register_action(move |workspace, _: &OpenPreviewToTheSide, window, cx| { - if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) { - if Self::is_svg_file(&editor, cx) { - let editor_clone = editor.clone(); - let view = Self::create_svg_view( - SvgPreviewMode::Default, - workspace, - editor_clone, - window, - cx, - ); - let pane = workspace - .find_pane_in_direction(workspace::SplitDirection::Right, cx) - .unwrap_or_else(|| { - workspace.split_pane( - workspace.active_pane().clone(), - workspace::SplitDirection::Right, - window, - cx, - ) - }); - pane.update(cx, |pane, cx| { - if let Some(existing_view_idx) = - Self::find_existing_preview_item_idx(pane, &editor, cx) - { - pane.activate_item(existing_view_idx, true, true, window, cx); - } else { - pane.add_item(Box::new(view), false, false, None, window, cx) - } + if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) + && Self::is_svg_file(&editor, cx) + { + let editor_clone = editor.clone(); + let view = Self::create_svg_view( + SvgPreviewMode::Default, + workspace, + editor_clone, + window, + cx, + ); + let pane = workspace + .find_pane_in_direction(workspace::SplitDirection::Right, cx) + .unwrap_or_else(|| { + workspace.split_pane( + workspace.active_pane().clone(), + workspace::SplitDirection::Right, + window, + cx, + ) }); - cx.notify(); - } + pane.update(cx, |pane, cx| { + if let Some(existing_view_idx) = + Self::find_existing_preview_item_idx(pane, &editor, cx) + { + pane.activate_item(existing_view_idx, true, true, window, cx); + } else { + pane.add_item(Box::new(view), false, false, None, window, cx) + } + }); + cx.notify(); } }); workspace.register_action(move |workspace, _: &OpenFollowingPreview, window, cx| { - if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) { - if Self::is_svg_file(&editor, cx) { - let view = Self::create_svg_view( - SvgPreviewMode::Follow, - workspace, - editor, - window, - cx, - ); - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item(Box::new(view), true, true, None, window, cx) - }); - cx.notify(); - } + if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) + && Self::is_svg_file(&editor, cx) + { + let view = + Self::create_svg_view(SvgPreviewMode::Follow, workspace, editor, window, cx); + workspace.active_pane().update(cx, |pane, cx| { + pane.add_item(Box::new(view), true, true, None, window, cx) + }); + cx.notify(); } }); } @@ -192,18 +187,15 @@ impl SvgPreviewView { match event { workspace::Event::ActiveItemChanged => { let workspace_read = workspace.read(cx); - if let Some(active_item) = workspace_read.active_item(cx) { - if let Some(editor_entity) = + if let Some(active_item) = workspace_read.active_item(cx) + && let Some(editor_entity) = active_item.downcast::<Editor>() - { - if Self::is_svg_file(&editor_entity, cx) { - let new_path = - Self::get_svg_path(&editor_entity, cx); - if this.svg_path != new_path { - this.svg_path = new_path; - cx.notify(); - } - } + && Self::is_svg_file(&editor_entity, cx) + { + let new_path = Self::get_svg_path(&editor_entity, cx); + if this.svg_path != new_path { + this.svg_path = new_path; + cx.notify(); } } } @@ -232,15 +224,15 @@ impl SvgPreviewView { { let app = cx.borrow(); let buffer = editor.read(app).buffer().read(app); - if let Some(buffer) = buffer.as_singleton() { - if let Some(file) = buffer.read(app).file() { - return file - .path() - .extension() - .and_then(|ext| ext.to_str()) - .map(|ext| ext.eq_ignore_ascii_case("svg")) - .unwrap_or(false); - } + if let Some(buffer) = buffer.as_singleton() + && let Some(file) = buffer.read(app).file() + { + return file + .path() + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.eq_ignore_ascii_case("svg")) + .unwrap_or(false); } false } diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index f20f55975e..38089670e2 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -299,13 +299,12 @@ impl DebugTaskFile { if let Some(properties) = template_object .get_mut("properties") .and_then(|value| value.as_object_mut()) + && properties.remove("label").is_none() { - if properties.remove("label").is_none() { - debug_panic!( - "Generated TaskTemplate json schema did not have expected 'label' field. \ + debug_panic!( + "Generated TaskTemplate json schema did not have expected 'label' field. \ Schema of 2nd alternative is: {template_object:?}" - ); - } + ); } if let Some(arr) = template_object diff --git a/crates/task/src/vscode_debug_format.rs b/crates/task/src/vscode_debug_format.rs index e688760a5e..5e21cd6530 100644 --- a/crates/task/src/vscode_debug_format.rs +++ b/crates/task/src/vscode_debug_format.rs @@ -30,12 +30,12 @@ impl VsCodeDebugTaskDefinition { let label = replacer.replace(&self.name); let mut config = replacer.replace_value(self.other_attributes); let adapter = task_type_to_adapter_name(&self.r#type); - if let Some(config) = config.as_object_mut() { - if adapter == "JavaScript" { - config.insert("type".to_owned(), self.r#type.clone().into()); - if let Some(port) = self.port.take() { - config.insert("port".to_owned(), port.into()); - } + if let Some(config) = config.as_object_mut() + && adapter == "JavaScript" + { + config.insert("type".to_owned(), self.r#type.clone().into()); + if let Some(port) = self.port.take() { + config.insert("port".to_owned(), port.into()); } } let definition = DebugScenario { diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index 0b3f70e6bc..90e6ea8878 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -227,10 +227,10 @@ where tasks.retain_mut(|(task_source_kind, target_task)| { if predicate((task_source_kind, target_task)) { - if let Some(overrides) = &overrides { - if let Some(target_override) = overrides.reveal_target { - target_task.reveal_target = target_override; - } + if let Some(overrides) = &overrides + && let Some(target_override) = overrides.reveal_target + { + target_task.reveal_target = target_override; } workspace.schedule_task( task_source_kind.clone(), @@ -343,11 +343,10 @@ pub fn task_contexts( task_contexts.lsp_task_sources = lsp_task_sources; task_contexts.latest_selection = latest_selection; - if let Some(editor_context_task) = editor_context_task { - if let Some(editor_context) = editor_context_task.await { - task_contexts.active_item_context = - Some((active_worktree, location, editor_context)); - } + if let Some(editor_context_task) = editor_context_task + && let Some(editor_context) = editor_context_task.await + { + task_contexts.active_item_context = Some((active_worktree, location, editor_context)); } if let Some(active_worktree) = active_worktree { diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 3dfde8a9af..42b3694789 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1051,15 +1051,16 @@ impl Terminal { navigation_target: MaybeNavigationTarget, cx: &mut Context<Self>, ) { - if let Some(prev_word) = prev_word { - if prev_word.word == word && prev_word.word_match == word_match { - self.last_content.last_hovered_word = Some(HoveredWord { - word, - word_match, - id: prev_word.id, - }); - return; - } + if let Some(prev_word) = prev_word + && prev_word.word == word + && prev_word.word_match == word_match + { + self.last_content.last_hovered_word = Some(HoveredWord { + word, + word_match, + id: prev_word.id, + }); + return; } self.last_content.last_hovered_word = Some(HoveredWord { @@ -1517,12 +1518,11 @@ impl Terminal { self.last_content.display_offset, ); - if self.mouse_changed(point, side) { - if let Some(bytes) = + if self.mouse_changed(point, side) + && let Some(bytes) = mouse_moved_report(point, e.pressed_button, e.modifiers, self.last_content.mode) - { - self.pty_tx.notify(bytes); - } + { + self.pty_tx.notify(bytes); } } else if e.modifiers.secondary() { self.word_from_position(e.position); @@ -1864,10 +1864,10 @@ impl Terminal { } pub fn kill_active_task(&mut self) { - if let Some(task) = self.task() { - if task.status == TaskStatus::Running { - self.pty_info.kill_current_process(); - } + if let Some(task) = self.task() + && task.status == TaskStatus::Running + { + self.pty_info.kill_current_process(); } } diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index 3f89afffab..635e3e2ca5 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -325,10 +325,10 @@ impl settings::Settings for TerminalSettings { .and_then(|v| v.as_object()) { for (k, v) in env { - if v.is_null() { - if let Some(zed_env) = current.env.as_mut() { - zed_env.remove(k); - } + if v.is_null() + && let Some(zed_env) = current.env.as_mut() + { + zed_env.remove(k); } let Some(v) = v.as_str() else { continue }; if let Some(zed_env) = current.env.as_mut() { diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 6c1be9d5e7..7575706db0 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -583,15 +583,15 @@ impl TerminalElement { strikethrough, }; - if let Some((style, range)) = hyperlink { - if range.contains(&indexed.point) { - if let Some(underline) = style.underline { - result.underline = Some(underline); - } + if let Some((style, range)) = hyperlink + && range.contains(&indexed.point) + { + if let Some(underline) = style.underline { + result.underline = Some(underline); + } - if let Some(color) = style.color { - result.color = color; - } + if let Some(color) = style.color { + result.color = color; } } @@ -1275,9 +1275,9 @@ impl Element for TerminalElement { } let text_paint_time = text_paint_start.elapsed(); - if let Some(text_to_mark) = &marked_text_cloned { - if !text_to_mark.is_empty() { - if let Some(cursor_layout) = &original_cursor { + if let Some(text_to_mark) = &marked_text_cloned + && !text_to_mark.is_empty() + && let Some(cursor_layout) = &original_cursor { let ime_position = cursor_layout.bounding_rect(origin).origin; let mut ime_style = layout.base_text_style.clone(); ime_style.underline = Some(UnderlineStyle { @@ -1303,14 +1303,11 @@ impl Element for TerminalElement { .paint(ime_position, layout.dimensions.line_height, window, cx) .log_err(); } - } - } - if self.cursor_visible && marked_text_cloned.is_none() { - if let Some(mut cursor) = original_cursor { + if self.cursor_visible && marked_text_cloned.is_none() + && let Some(mut cursor) = original_cursor { cursor.paint(origin, window, cx); } - } if let Some(mut element) = block_below_cursor_element { element.paint(window, cx); diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index cdf405b642..b161a8ea89 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -255,8 +255,7 @@ impl TerminalPanel { .transpose() .log_err() .flatten() - { - if let Ok(serialized) = workspace + && let Ok(serialized) = workspace .update_in(&mut cx, |workspace, window, cx| { deserialize_terminal_panel( workspace.weak_handle(), @@ -268,9 +267,8 @@ impl TerminalPanel { ) })? .await - { - terminal_panel = Some(serialized); - } + { + terminal_panel = Some(serialized); } } _ => {} @@ -1077,11 +1075,10 @@ pub fn new_terminal_pane( return ControlFlow::Break(()); } }; - } else if let Some(project_path) = item.project_path(cx) { - if let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx) - { - add_paths_to_terminal(pane, &[entry_path], window, cx); - } + } else if let Some(project_path) = item.project_path(cx) + && let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx) + { + add_paths_to_terminal(pane, &[entry_path], window, cx); } } } else if let Some(selection) = dropped_item.downcast_ref::<DraggedSelection>() { @@ -1103,10 +1100,8 @@ pub fn new_terminal_pane( { add_paths_to_terminal(pane, &[entry_path], window, cx); } - } else if is_local { - if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() { - add_paths_to_terminal(pane, paths.paths(), window, cx); - } + } else if is_local && let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() { + add_paths_to_terminal(pane, paths.paths(), window, cx); } ControlFlow::Break(()) diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 559faea42a..14b642bc12 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -308,10 +308,10 @@ impl TerminalView { } else { let mut displayed_lines = total_lines; - if !self.focus_handle.is_focused(window) { - if let Some(max_lines) = max_lines_when_unfocused { - displayed_lines = displayed_lines.min(*max_lines) - } + if !self.focus_handle.is_focused(window) + && let Some(max_lines) = max_lines_when_unfocused + { + displayed_lines = displayed_lines.min(*max_lines) } ContentMode::Inline { @@ -1156,26 +1156,26 @@ fn subscribe_for_terminal_events( if let Some(opened_item) = opened_items.first() { if open_target.is_file() { - if let Some(Ok(opened_item)) = opened_item { - if let Some(row) = path_to_open.row { - let col = path_to_open.column.unwrap_or(0); - if let Some(active_editor) = - opened_item.downcast::<Editor>() - { - active_editor - .downgrade() - .update_in(cx, |editor, window, cx| { - editor.go_to_singleton_buffer_point( - language::Point::new( - row.saturating_sub(1), - col.saturating_sub(1), - ), - window, - cx, - ) - }) - .log_err(); - } + if let Some(Ok(opened_item)) = opened_item + && let Some(row) = path_to_open.row + { + let col = path_to_open.column.unwrap_or(0); + if let Some(active_editor) = + opened_item.downcast::<Editor>() + { + active_editor + .downgrade() + .update_in(cx, |editor, window, cx| { + editor.go_to_singleton_buffer_point( + language::Point::new( + row.saturating_sub(1), + col.saturating_sub(1), + ), + window, + cx, + ) + }) + .log_err(); } } } else if open_target.is_dir() { @@ -1321,17 +1321,17 @@ fn possible_open_target( } }; - if path_to_check.path.is_relative() { - if let Some(entry) = worktree.read(cx).entry_for_path(&path_to_check.path) { - return Task::ready(Some(OpenTarget::Worktree( - PathWithPosition { - path: worktree_root.join(&entry.path), - row: path_to_check.row, - column: path_to_check.column, - }, - entry.clone(), - ))); - } + if path_to_check.path.is_relative() + && let Some(entry) = worktree.read(cx).entry_for_path(&path_to_check.path) + { + return Task::ready(Some(OpenTarget::Worktree( + PathWithPosition { + path: worktree_root.join(&entry.path), + row: path_to_check.row, + column: path_to_check.column, + }, + entry.clone(), + ))); } paths_to_check.push(path_to_check); @@ -1428,11 +1428,11 @@ fn possible_open_target( let fs = workspace.read(cx).project().read(cx).fs().clone(); cx.background_spawn(async move { for mut path_to_check in fs_paths_to_check { - if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok() { - if let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten() { - path_to_check.path = fs_path_to_check; - return Some(OpenTarget::File(path_to_check, metadata)); - } + if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok() + && let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten() + { + path_to_check.path = fs_path_to_check; + return Some(OpenTarget::File(path_to_check, metadata)); } } diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 9f7e49d24d..8e37567738 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -446,10 +446,10 @@ impl History { } fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) { - if let Some(transaction) = self.forget(transaction) { - if let Some(destination) = self.transaction_mut(destination) { - destination.edit_ids.extend(transaction.edit_ids); - } + if let Some(transaction) = self.forget(transaction) + && let Some(destination) = self.transaction_mut(destination) + { + destination.edit_ids.extend(transaction.edit_ids); } } @@ -1585,11 +1585,11 @@ impl Buffer { .map(Some) .chain([None]) .filter_map(move |range| { - if let Some((range, prev_range)) = range.as_ref().zip(prev_range.as_mut()) { - if prev_range.end == range.start { - prev_range.end = range.end; - return None; - } + if let Some((range, prev_range)) = range.as_ref().zip(prev_range.as_mut()) + && prev_range.end == range.start + { + prev_range.end = range.end; + return None; } let result = prev_range.clone(); prev_range = range; @@ -1685,10 +1685,10 @@ impl Buffer { rx = Some(channel.1); } async move { - if let Some(mut rx) = rx { - if rx.recv().await.is_none() { - anyhow::bail!("gave up waiting for version"); - } + if let Some(mut rx) = rx + && rx.recv().await.is_none() + { + anyhow::bail!("gave up waiting for version"); } Ok(()) } diff --git a/crates/theme/src/fallback_themes.rs b/crates/theme/src/fallback_themes.rs index e9e8e2d0db..13786aca57 100644 --- a/crates/theme/src/fallback_themes.rs +++ b/crates/theme/src/fallback_themes.rs @@ -34,10 +34,10 @@ pub(crate) fn apply_status_color_defaults(status: &mut StatusColorsRefinement) { (&status.error, &mut status.error_background), (&status.hidden, &mut status.hidden_background), ] { - if bg_color.is_none() { - if let Some(fg_color) = fg_color { - *bg_color = Some(fg_color.opacity(0.25)); - } + if bg_color.is_none() + && let Some(fg_color) = fg_color + { + *bg_color = Some(fg_color.opacity(0.25)); } } } diff --git a/crates/title_bar/src/application_menu.rs b/crates/title_bar/src/application_menu.rs index 98f0eeb6cc..d8b0b8dc6b 100644 --- a/crates/title_bar/src/application_menu.rs +++ b/crates/title_bar/src/application_menu.rs @@ -269,32 +269,31 @@ impl Render for ApplicationMenu { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let all_menus_shown = self.all_menus_shown(cx); - if let Some(pending_menu_open) = self.pending_menu_open.take() { - if let Some(entry) = self + if let Some(pending_menu_open) = self.pending_menu_open.take() + && let Some(entry) = self .entries .iter() .find(|entry| entry.menu.name == pending_menu_open && !entry.handle.is_deployed()) - { - let handle_to_show = entry.handle.clone(); - let handles_to_hide: Vec<_> = self - .entries - .iter() - .filter(|e| e.menu.name != pending_menu_open && e.handle.is_deployed()) - .map(|e| e.handle.clone()) - .collect(); + { + let handle_to_show = entry.handle.clone(); + let handles_to_hide: Vec<_> = self + .entries + .iter() + .filter(|e| e.menu.name != pending_menu_open && e.handle.is_deployed()) + .map(|e| e.handle.clone()) + .collect(); - if handles_to_hide.is_empty() { - // We need to wait for the next frame to show all menus first, - // before we can handle show/hide operations - window.on_next_frame(move |window, cx| { - handles_to_hide.iter().for_each(|handle| handle.hide(cx)); - window.defer(cx, move |window, cx| handle_to_show.show(window, cx)); - }); - } else { - // Since menus are already shown, we can directly handle show/hide operations + if handles_to_hide.is_empty() { + // We need to wait for the next frame to show all menus first, + // before we can handle show/hide operations + window.on_next_frame(move |window, cx| { handles_to_hide.iter().for_each(|handle| handle.hide(cx)); - cx.defer_in(window, move |_, window, cx| handle_to_show.show(window, cx)); - } + window.defer(cx, move |window, cx| handle_to_show.show(window, cx)); + }); + } else { + // Since menus are already shown, we can directly handle show/hide operations + handles_to_hide.iter().for_each(|handle| handle.hide(cx)); + cx.defer_in(window, move |_, window, cx| handle_to_show.show(window, cx)); } } diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 84622888f1..5bd6a17e4b 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -593,11 +593,11 @@ impl TitleBar { Button::new("connection-status", label) .label_size(LabelSize::Small) .on_click(|_, window, cx| { - if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) { - if auto_updater.read(cx).status().is_updated() { - workspace::reload(cx); - return; - } + if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) + && auto_updater.read(cx).status().is_updated() + { + workspace::reload(cx); + return; } auto_update::check(&Default::default(), window, cx); }) diff --git a/crates/toolchain_selector/src/active_toolchain.rs b/crates/toolchain_selector/src/active_toolchain.rs index 01bd7b0a9c..ea5dcc2a19 100644 --- a/crates/toolchain_selector/src/active_toolchain.rs +++ b/crates/toolchain_selector/src/active_toolchain.rs @@ -121,31 +121,30 @@ impl ActiveToolchain { cx: &mut Context<Self>, ) { let editor = editor.read(cx); - if let Some((_, buffer, _)) = editor.active_excerpt(cx) { - if let Some(worktree_id) = buffer.read(cx).file().map(|file| file.worktree_id(cx)) { - if self - .active_buffer - .as_ref() - .is_some_and(|(old_worktree_id, old_buffer, _)| { - (old_worktree_id, old_buffer.entity_id()) - == (&worktree_id, buffer.entity_id()) - }) - { - return; - } - - let subscription = cx.subscribe_in( - &buffer, - window, - |this, _, event: &BufferEvent, window, cx| { - if matches!(event, BufferEvent::LanguageChanged) { - this._update_toolchain_task = Self::spawn_tracker_task(window, cx); - } - }, - ); - self.active_buffer = Some((worktree_id, buffer.downgrade(), subscription)); - self._update_toolchain_task = Self::spawn_tracker_task(window, cx); + if let Some((_, buffer, _)) = editor.active_excerpt(cx) + && let Some(worktree_id) = buffer.read(cx).file().map(|file| file.worktree_id(cx)) + { + if self + .active_buffer + .as_ref() + .is_some_and(|(old_worktree_id, old_buffer, _)| { + (old_worktree_id, old_buffer.entity_id()) == (&worktree_id, buffer.entity_id()) + }) + { + return; } + + let subscription = cx.subscribe_in( + &buffer, + window, + |this, _, event: &BufferEvent, window, cx| { + if matches!(event, BufferEvent::LanguageChanged) { + this._update_toolchain_task = Self::spawn_tracker_task(window, cx); + } + }, + ); + self.active_buffer = Some((worktree_id, buffer.downgrade(), subscription)); + self._update_toolchain_task = Self::spawn_tracker_task(window, cx); } cx.notify(); diff --git a/crates/toolchain_selector/src/toolchain_selector.rs b/crates/toolchain_selector/src/toolchain_selector.rs index 21d95a66de..cdd3db99e0 100644 --- a/crates/toolchain_selector/src/toolchain_selector.rs +++ b/crates/toolchain_selector/src/toolchain_selector.rs @@ -211,16 +211,15 @@ impl ToolchainSelectorDelegate { let _ = this.update_in(cx, move |this, window, cx| { this.delegate.candidates = available_toolchains; - if let Some(active_toolchain) = active_toolchain { - if let Some(position) = this + if let Some(active_toolchain) = active_toolchain + && let Some(position) = this .delegate .candidates .toolchains .iter() .position(|toolchain| *toolchain == active_toolchain) - { - this.delegate.set_selected_index(position, window, cx); - } + { + this.delegate.set_selected_index(position, window, cx); } this.update_matches(this.query(cx), window, cx); }); diff --git a/crates/ui/src/components/label/highlighted_label.rs b/crates/ui/src/components/label/highlighted_label.rs index 72f54e08da..576d47eda5 100644 --- a/crates/ui/src/components/label/highlighted_label.rs +++ b/crates/ui/src/components/label/highlighted_label.rs @@ -98,12 +98,12 @@ pub fn highlight_ranges( loop { end_ix = end_ix + text[end_ix..].chars().next().unwrap().len_utf8(); - if let Some(&next_ix) = highlight_indices.peek() { - if next_ix == end_ix { - end_ix = next_ix; - highlight_indices.next(); - continue; - } + if let Some(&next_ix) = highlight_indices.peek() + && next_ix == end_ix + { + end_ix = next_ix; + highlight_indices.next(); + continue; } break; } diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index 55ce0218c7..f77eea4bdc 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -72,10 +72,10 @@ impl<M: ManagedView> PopoverMenuHandle<M> { } pub fn hide(&self, cx: &mut App) { - if let Some(state) = self.0.borrow().as_ref() { - if let Some(menu) = state.menu.borrow().as_ref() { - menu.update(cx, |_, cx| cx.emit(DismissEvent)); - } + if let Some(state) = self.0.borrow().as_ref() + && let Some(menu) = state.menu.borrow().as_ref() + { + menu.update(cx, |_, cx| cx.emit(DismissEvent)); } } @@ -278,10 +278,10 @@ fn show_menu<M: ManagedView>( window .subscribe(&new_menu, cx, move |modal, _: &DismissEvent, window, cx| { - if modal.focus_handle(cx).contains_focused(window, cx) { - if let Some(previous_focus_handle) = previous_focus_handle.as_ref() { - window.focus(previous_focus_handle); - } + if modal.focus_handle(cx).contains_focused(window, cx) + && let Some(previous_focus_handle) = previous_focus_handle.as_ref() + { + window.focus(previous_focus_handle); } *menu2.borrow_mut() = None; window.refresh(); @@ -373,14 +373,14 @@ impl<M: ManagedView> Element for PopoverMenu<M> { (child_builder)(element_state.menu.clone(), self.menu_builder.clone()) }); - if let Some(trigger_handle) = self.trigger_handle.take() { - if let Some(menu_builder) = self.menu_builder.clone() { - *trigger_handle.0.borrow_mut() = Some(PopoverMenuHandleState { - menu_builder, - menu: element_state.menu.clone(), - on_open: self.on_open.clone(), - }); - } + if let Some(trigger_handle) = self.trigger_handle.take() + && let Some(menu_builder) = self.menu_builder.clone() + { + *trigger_handle.0.borrow_mut() = Some(PopoverMenuHandleState { + menu_builder, + menu: element_state.menu.clone(), + on_open: self.on_open.clone(), + }); } let child_layout_id = child_element diff --git a/crates/ui/src/components/right_click_menu.rs b/crates/ui/src/components/right_click_menu.rs index 85ef549bc0..761189671b 100644 --- a/crates/ui/src/components/right_click_menu.rs +++ b/crates/ui/src/components/right_click_menu.rs @@ -250,12 +250,11 @@ impl<M: ManagedView> Element for RightClickMenu<M> { window .subscribe(&new_menu, cx, move |modal, _: &DismissEvent, window, cx| { - if modal.focus_handle(cx).contains_focused(window, cx) { - if let Some(previous_focus_handle) = + if modal.focus_handle(cx).contains_focused(window, cx) + && let Some(previous_focus_handle) = previous_focus_handle.as_ref() - { - window.focus(previous_focus_handle); - } + { + window.focus(previous_focus_handle); } *menu2.borrow_mut() = None; window.refresh(); diff --git a/crates/util/src/fs.rs b/crates/util/src/fs.rs index 3e96594f85..60aab4a2e7 100644 --- a/crates/util/src/fs.rs +++ b/crates/util/src/fs.rs @@ -13,13 +13,13 @@ where while let Some(entry) = entries.next().await { if let Some(entry) = entry.log_err() { let entry_path = entry.path(); - if predicate(entry_path.as_path()) { - if let Ok(metadata) = fs::metadata(&entry_path).await { - if metadata.is_file() { - fs::remove_file(&entry_path).await.log_err(); - } else { - fs::remove_dir_all(&entry_path).await.log_err(); - } + if predicate(entry_path.as_path()) + && let Ok(metadata) = fs::metadata(&entry_path).await + { + if metadata.is_file() { + fs::remove_file(&entry_path).await.log_err(); + } else { + fs::remove_dir_all(&entry_path).await.log_err(); } } } @@ -35,10 +35,10 @@ where if let Some(mut entries) = fs::read_dir(dir).await.log_err() { while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - if predicate(entry.path().as_path()) { - matching.push(entry.path()); - } + if let Some(entry) = entry.log_err() + && predicate(entry.path().as_path()) + { + matching.push(entry.path()); } } } @@ -58,10 +58,9 @@ where if let Some(file_name) = entry_path .file_name() .map(|file_name| file_name.to_string_lossy()) + && predicate(&file_name) { - if predicate(&file_name) { - return Some(entry_path); - } + return Some(entry_path); } } } diff --git a/crates/util/src/schemars.rs b/crates/util/src/schemars.rs index e162b41933..a59d24c325 100644 --- a/crates/util/src/schemars.rs +++ b/crates/util/src/schemars.rs @@ -44,13 +44,12 @@ pub struct DefaultDenyUnknownFields; impl schemars::transform::Transform for DefaultDenyUnknownFields { fn transform(&mut self, schema: &mut schemars::Schema) { - if let Some(object) = schema.as_object_mut() { - if object.contains_key("properties") - && !object.contains_key("additionalProperties") - && !object.contains_key("unevaluatedProperties") - { - object.insert("additionalProperties".to_string(), false.into()); - } + if let Some(object) = schema.as_object_mut() + && object.contains_key("properties") + && !object.contains_key("additionalProperties") + && !object.contains_key("unevaluatedProperties") + { + object.insert("additionalProperties".to_string(), false.into()); } transform_subschemas(self, schema); } diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index e1b25f4dba..187678f8af 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -128,11 +128,9 @@ pub fn truncate_lines_to_byte_limit(s: &str, max_bytes: usize) -> &str { } for i in (0..max_bytes).rev() { - if s.is_char_boundary(i) { - if s.as_bytes()[i] == b'\n' { - // Since the i-th character is \n, valid to slice at i + 1. - return &s[..i + 1]; - } + if s.is_char_boundary(i) && s.as_bytes()[i] == b'\n' { + // Since the i-th character is \n, valid to slice at i + 1. + return &s[..i + 1]; } } diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index fe1537684c..00d3bde750 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -510,17 +510,16 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) { vim.switch_mode(Mode::Normal, true, window, cx); } vim.update_editor(cx, |_, editor, cx| { - if let Some(first_sel) = initial_selections { - if let Some(tx_id) = editor + if let Some(first_sel) = initial_selections + && let Some(tx_id) = editor .buffer() .update(cx, |multi, cx| multi.last_transaction_id(cx)) - { - let last_sel = editor.selections.disjoint_anchors(); - editor.modify_transaction_selection_history(tx_id, |old| { - old.0 = first_sel; - old.1 = Some(last_sel); - }); - } + { + let last_sel = editor.selections.disjoint_anchors(); + editor.modify_transaction_selection_history(tx_id, |old| { + old.0 = first_sel; + old.1 = Some(last_sel); + }); } }); }) @@ -1713,14 +1712,12 @@ impl Vim { match c { '%' => { self.update_editor(cx, |_, editor, cx| { - if let Some((_, buffer, _)) = editor.active_excerpt(cx) { - if let Some(file) = buffer.read(cx).file() { - if let Some(local) = file.as_local() { - if let Some(str) = local.path().to_str() { - ret.push_str(str) - } - } - } + if let Some((_, buffer, _)) = editor.active_excerpt(cx) + && let Some(file) = buffer.read(cx).file() + && let Some(local) = file.as_local() + && let Some(str) = local.path().to_str() + { + ret.push_str(str) } }); } @@ -1954,19 +1951,19 @@ impl ShellExec { return; }; - if let Some(mut stdin) = running.stdin.take() { - if let Some(snapshot) = input_snapshot { - let range = range.clone(); - cx.background_spawn(async move { - for chunk in snapshot.text_for_range(range) { - if stdin.write_all(chunk.as_bytes()).log_err().is_none() { - return; - } + if let Some(mut stdin) = running.stdin.take() + && let Some(snapshot) = input_snapshot + { + let range = range.clone(); + cx.background_spawn(async move { + for chunk in snapshot.text_for_range(range) { + if stdin.write_all(chunk.as_bytes()).log_err().is_none() { + return; } - stdin.flush().log_err(); - }) - .detach(); - } + } + stdin.flush().log_err(); + }) + .detach(); }; let output = cx diff --git a/crates/vim/src/digraph.rs b/crates/vim/src/digraph.rs index c555b781b1..beb3bd54ba 100644 --- a/crates/vim/src/digraph.rs +++ b/crates/vim/src/digraph.rs @@ -63,15 +63,15 @@ impl Vim { } fn literal(&mut self, action: &Literal, window: &mut Window, cx: &mut Context<Self>) { - if let Some(Operator::Literal { prefix }) = self.active_operator() { - if let Some(prefix) = prefix { - if let Some(keystroke) = Keystroke::parse(&action.0).ok() { - window.defer(cx, |window, cx| { - window.dispatch_keystroke(keystroke, cx); - }); - } - return self.handle_literal_input(prefix, "", window, cx); + if let Some(Operator::Literal { prefix }) = self.active_operator() + && let Some(prefix) = prefix + { + if let Some(keystroke) = Keystroke::parse(&action.0).ok() { + window.defer(cx, |window, cx| { + window.dispatch_keystroke(keystroke, cx); + }); } + return self.handle_literal_input(prefix, "", window, cx); } self.insert_literal(Some(action.1), "", window, cx); diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 367b5130b6..e703b18117 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -1811,10 +1811,10 @@ fn previous_word_end( .ignore_punctuation(ignore_punctuation); let mut point = point.to_point(map); - if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) { - if let Some(ch) = map.buffer_snapshot.chars_at(point).next() { - point.column += ch.len_utf8() as u32; - } + if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) + && let Some(ch) = map.buffer_snapshot.chars_at(point).next() + { + point.column += ch.len_utf8() as u32; } for _ in 0..times { let new_point = movement::find_preceding_boundary_point( @@ -1986,10 +1986,10 @@ fn previous_subword_end( .ignore_punctuation(ignore_punctuation); let mut point = point.to_point(map); - if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) { - if let Some(ch) = map.buffer_snapshot.chars_at(point).next() { - point.column += ch.len_utf8() as u32; - } + if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) + && let Some(ch) = map.buffer_snapshot.chars_at(point).next() + { + point.column += ch.len_utf8() as u32; } for _ in 0..times { let new_point = movement::find_preceding_boundary_point( @@ -2054,10 +2054,10 @@ pub(crate) fn last_non_whitespace( let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map)); // NOTE: depending on clip_at_line_end we may already be one char back from the end. - if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() { - if classifier.kind(ch) != CharKind::Whitespace { - return end_of_line.to_display_point(map); - } + if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() + && classifier.kind(ch) != CharKind::Whitespace + { + return end_of_line.to_display_point(map); } for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) { diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index d7a6932baa..6f406d0c44 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -74,10 +74,10 @@ impl Vim { editor.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { let mut cursor = selection.head(); - if kind.linewise() { - if let Some(column) = original_columns.get(&selection.id) { - *cursor.column_mut() = *column - } + if kind.linewise() + && let Some(column) = original_columns.get(&selection.id) + { + *cursor.column_mut() = *column } cursor = map.clip_point(cursor, Bias::Left); selection.collapse_to(cursor, selection.goal) diff --git a/crates/vim/src/normal/mark.rs b/crates/vim/src/normal/mark.rs index 1d6264d593..80d94def05 100644 --- a/crates/vim/src/normal/mark.rs +++ b/crates/vim/src/normal/mark.rs @@ -256,10 +256,8 @@ impl Vim { } }); - if should_jump { - if let Some(anchor) = anchor { - self.motion(Motion::Jump { anchor, line }, window, cx) - } + if should_jump && let Some(anchor) = anchor { + self.motion(Motion::Jump { anchor, line }, window, cx) } } } diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index 5cc3762990..2d79274808 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -221,14 +221,14 @@ impl Vim { if actions.is_empty() { return None; } - if globals.replayer.is_none() { - if let Some(recording_register) = globals.recording_register { - globals - .recordings - .entry(recording_register) - .or_default() - .push(ReplayableAction::Action(Repeat.boxed_clone())); - } + if globals.replayer.is_none() + && let Some(recording_register) = globals.recording_register + { + globals + .recordings + .entry(recording_register) + .or_default() + .push(ReplayableAction::Action(Repeat.boxed_clone())); } let mut mode = None; @@ -320,10 +320,10 @@ impl Vim { // vim doesn't treat 3a1 as though you literally repeated a1 // 3 times, instead it inserts the content thrice at the insert position. if let Some(to_repeat) = repeatable_insert(&actions[0]) { - if let Some(ReplayableAction::Action(action)) = actions.last() { - if NormalBefore.partial_eq(&**action) { - actions.pop(); - } + if let Some(ReplayableAction::Action(action)) = actions.last() + && NormalBefore.partial_eq(&**action) + { + actions.pop(); } let mut new_actions = actions.clone(); diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index cff23c4bd4..c65da4f90b 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -100,10 +100,10 @@ fn cover_or_next<I: Iterator<Item = (Range<usize>, Range<usize>)>>( for (open_range, close_range) in ranges { let start_off = open_range.start; let end_off = close_range.end; - if let Some(range_filter) = range_filter { - if !range_filter(open_range.clone(), close_range.clone()) { - continue; - } + if let Some(range_filter) = range_filter + && !range_filter(open_range.clone(), close_range.clone()) + { + continue; } let candidate = CandidateWithRanges { candidate: CandidateRange { @@ -1060,11 +1060,11 @@ fn text_object( .filter_map(|(r, m)| if m == target { Some(r) } else { None }) .collect(); matches.sort_by_key(|r| r.start); - if let Some(buffer_range) = matches.first() { - if !buffer_range.is_empty() { - let range = excerpt.map_range_from_buffer(buffer_range.clone()); - return Some(range.start.to_display_point(map)..range.end.to_display_point(map)); - } + if let Some(buffer_range) = matches.first() + && !buffer_range.is_empty() + { + let range = excerpt.map_range_from_buffer(buffer_range.clone()); + return Some(range.start.to_display_point(map)..range.end.to_display_point(map)); } let buffer_range = excerpt.map_range_from_buffer(around_range.clone()); return Some(buffer_range.start.to_display_point(map)..buffer_range.end.to_display_point(map)); @@ -1529,25 +1529,25 @@ fn surrounding_markers( Some((ch, _)) => ch, _ => '\0', }; - if let Some((ch, range)) = movement::chars_after(map, point).next() { - if ch == open_marker && before_ch != '\\' { - if open_marker == close_marker { - let mut total = 0; - for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows() - { - if ch == '\n' { - break; - } - if ch == open_marker && before_ch != '\\' { - total += 1; - } + if let Some((ch, range)) = movement::chars_after(map, point).next() + && ch == open_marker + && before_ch != '\\' + { + if open_marker == close_marker { + let mut total = 0; + for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows() { + if ch == '\n' { + break; } - if total % 2 == 0 { - opening = Some(range) + if ch == open_marker && before_ch != '\\' { + total += 1; } - } else { + } + if total % 2 == 0 { opening = Some(range) } + } else { + opening = Some(range) } } @@ -1558,10 +1558,10 @@ fn surrounding_markers( break; } - if let Some((before_ch, _)) = chars_before.peek() { - if *before_ch == '\\' { - continue; - } + if let Some((before_ch, _)) = chars_before.peek() + && *before_ch == '\\' + { + continue; } if ch == open_marker { diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 2e8e2f76bd..db19562f02 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -412,20 +412,20 @@ impl MarksState { let mut to_write = HashMap::default(); for (key, value) in &new_points { - if self.is_global_mark(key) { - if self.global_marks.get(key) != Some(&MarkLocation::Path(path.clone())) { - if let Some(workspace_id) = self.workspace_id(cx) { - let path = path.clone(); - let key = key.clone(); - cx.background_spawn(async move { - DB.set_global_mark_path(workspace_id, key, path).await - }) - .detach_and_log_err(cx); - } - - self.global_marks - .insert(key.clone(), MarkLocation::Path(path.clone())); + if self.is_global_mark(key) + && self.global_marks.get(key) != Some(&MarkLocation::Path(path.clone())) + { + if let Some(workspace_id) = self.workspace_id(cx) { + let path = path.clone(); + let key = key.clone(); + cx.background_spawn(async move { + DB.set_global_mark_path(workspace_id, key, path).await + }) + .detach_and_log_err(cx); } + + self.global_marks + .insert(key.clone(), MarkLocation::Path(path.clone())); } if old_points.and_then(|o| o.get(key)) != Some(value) { to_write.insert(key.clone(), value.clone()); @@ -456,15 +456,15 @@ impl MarksState { buffer: &Entity<Buffer>, cx: &mut Context<Self>, ) { - if let MarkLocation::Buffer(entity_id) = old_path { - if let Some(old_marks) = self.multibuffer_marks.remove(&entity_id) { - let buffer_marks = old_marks - .into_iter() - .map(|(k, v)| (k, v.into_iter().map(|anchor| anchor.text_anchor).collect())) - .collect(); - self.buffer_marks - .insert(buffer.read(cx).remote_id(), buffer_marks); - } + if let MarkLocation::Buffer(entity_id) = old_path + && let Some(old_marks) = self.multibuffer_marks.remove(&entity_id) + { + let buffer_marks = old_marks + .into_iter() + .map(|(k, v)| (k, v.into_iter().map(|anchor| anchor.text_anchor).collect())) + .collect(); + self.buffer_marks + .insert(buffer.read(cx).remote_id(), buffer_marks); } self.watch_buffer(MarkLocation::Path(new_path.clone()), buffer, cx); self.serialize_buffer_marks(new_path, buffer, cx); @@ -512,10 +512,9 @@ impl MarksState { .watched_buffers .get(&buffer_id.clone()) .map(|(path, _, _)| path.clone()) + && let Some(new_path) = this.path_for_buffer(&buffer, cx) { - if let Some(new_path) = this.path_for_buffer(&buffer, cx) { - this.rename_buffer(old_path, new_path, &buffer, cx) - } + this.rename_buffer(old_path, new_path, &buffer, cx) } } _ => {} @@ -897,13 +896,13 @@ impl VimGlobals { self.stop_recording_after_next_action = false; } } - if self.replayer.is_none() { - if let Some(recording_register) = self.recording_register { - self.recordings - .entry(recording_register) - .or_default() - .push(ReplayableAction::Action(action)); - } + if self.replayer.is_none() + && let Some(recording_register) = self.recording_register + { + self.recordings + .entry(recording_register) + .or_default() + .push(ReplayableAction::Action(action)); } } @@ -1330,10 +1329,10 @@ impl MarksMatchInfo { let mut offset = 0; for chunk in chunks { line.push_str(chunk.text); - if let Some(highlight_style) = chunk.syntax_highlight_id { - if let Some(highlight) = highlight_style.style(cx.theme().syntax()) { - highlights.push((offset..offset + chunk.text.len(), highlight)) - } + if let Some(highlight_style) = chunk.syntax_highlight_id + && let Some(highlight) = highlight_style.style(cx.theme().syntax()) + { + highlights.push((offset..offset + chunk.text.len(), highlight)) } offset += chunk.text.len(); } diff --git a/crates/vim/src/surrounds.rs b/crates/vim/src/surrounds.rs index 63cd21e88c..ca65204fab 100644 --- a/crates/vim/src/surrounds.rs +++ b/crates/vim/src/surrounds.rs @@ -174,12 +174,11 @@ impl Vim { if ch.to_string() == pair.start { let start = offset; let mut end = start + 1; - if surround { - if let Some((next_ch, _)) = chars_and_offset.peek() { - if next_ch.eq(&' ') { - end += 1; - } - } + if surround + && let Some((next_ch, _)) = chars_and_offset.peek() + && next_ch.eq(&' ') + { + end += 1; } edits.push((start..end, "")); anchors.push(start..start); @@ -193,12 +192,11 @@ impl Vim { if ch.to_string() == pair.end { let mut start = offset; let end = start + 1; - if surround { - if let Some((next_ch, _)) = reverse_chars_and_offsets.peek() { - if next_ch.eq(&' ') { - start -= 1; - } - } + if surround + && let Some((next_ch, _)) = reverse_chars_and_offsets.peek() + && next_ch.eq(&' ') + { + start -= 1; } edits.push((start..end, "")); break; diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 45cef3a2b9..98dabb8316 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -217,10 +217,11 @@ impl NeovimConnection { .expect("Could not set nvim cursor position"); } - if let Some(NeovimData::Get { mode, state }) = self.data.back() { - if *mode == Mode::Normal && *state == marked_text { - return; - } + if let Some(NeovimData::Get { mode, state }) = self.data.back() + && *mode == Mode::Normal + && *state == marked_text + { + return; } self.data.push_back(NeovimData::Put { state: marked_text.to_string(), diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 15b0b443b5..81c1a6b0b3 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -788,10 +788,10 @@ impl Vim { editor.selections.line_mode = false; editor.unregister_addon::<VimAddon>(); editor.set_relative_line_number(None, cx); - if let Some(vim) = Vim::globals(cx).focused_vim() { - if vim.entity_id() == cx.entity().entity_id() { - Vim::globals(cx).focused_vim = None; - } + if let Some(vim) = Vim::globals(cx).focused_vim() + && vim.entity_id() == cx.entity().entity_id() + { + Vim::globals(cx).focused_vim = None; } } @@ -833,10 +833,10 @@ impl Vim { if self.exit_temporary_mode { self.exit_temporary_mode = false; // Don't switch to insert mode if the action is temporary_normal. - if let Some(action) = keystroke_event.action.as_ref() { - if action.as_any().downcast_ref::<TemporaryNormal>().is_some() { - return; - } + if let Some(action) = keystroke_event.action.as_ref() + && action.as_any().downcast_ref::<TemporaryNormal>().is_some() + { + return; } self.switch_mode(Mode::Insert, false, window, cx) } @@ -1006,10 +1006,10 @@ impl Vim { Some((point, goal)) }) } - if last_mode == Mode::Insert || last_mode == Mode::Replace { - if let Some(prior_tx) = prior_tx { - editor.group_until_transaction(prior_tx, cx) - } + if (last_mode == Mode::Insert || last_mode == Mode::Replace) + && let Some(prior_tx) = prior_tx + { + editor.group_until_transaction(prior_tx, cx) } editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { @@ -1031,14 +1031,16 @@ impl Vim { } let snapshot = s.display_map(); - if let Some(pending) = s.pending.as_mut() { - if pending.selection.reversed && mode.is_visual() && !last_mode.is_visual() { - let mut end = pending.selection.end.to_point(&snapshot.buffer_snapshot); - end = snapshot - .buffer_snapshot - .clip_point(end + Point::new(0, 1), Bias::Right); - pending.selection.end = snapshot.buffer_snapshot.anchor_before(end); - } + if let Some(pending) = s.pending.as_mut() + && pending.selection.reversed + && mode.is_visual() + && !last_mode.is_visual() + { + let mut end = pending.selection.end.to_point(&snapshot.buffer_snapshot); + end = snapshot + .buffer_snapshot + .clip_point(end + Point::new(0, 1), Bias::Right); + pending.selection.end = snapshot.buffer_snapshot.anchor_before(end); } s.move_with(|map, selection| { @@ -1536,12 +1538,12 @@ impl Vim { if self.mode == Mode::Insert && self.current_tx.is_some() { if self.current_anchor.is_none() { self.current_anchor = Some(newest); - } else if self.current_anchor.as_ref().unwrap() != &newest { - if let Some(tx_id) = self.current_tx.take() { - self.update_editor(cx, |_, editor, cx| { - editor.group_until_transaction(tx_id, cx) - }); - } + } else if self.current_anchor.as_ref().unwrap() != &newest + && let Some(tx_id) = self.current_tx.take() + { + self.update_editor(cx, |_, editor, cx| { + editor.group_until_transaction(tx_id, cx) + }); } } else if self.mode == Mode::Normal && newest.start != newest.end { if matches!(newest.goal, SelectionGoal::HorizontalRange { .. }) { diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index ae72df3971..079f66ae9d 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -305,15 +305,14 @@ impl Dock { .detach(); cx.observe_in(&dock, window, move |workspace, dock, window, cx| { - if dock.read(cx).is_open() { - if let Some(panel) = dock.read(cx).active_panel() { - if panel.is_zoomed(window, cx) { - workspace.zoomed = Some(panel.to_any().downgrade()); - workspace.zoomed_position = Some(position); - cx.emit(Event::ZoomChanged); - return; - } - } + if dock.read(cx).is_open() + && let Some(panel) = dock.read(cx).active_panel() + && panel.is_zoomed(window, cx) + { + workspace.zoomed = Some(panel.to_any().downgrade()); + workspace.zoomed_position = Some(position); + cx.emit(Event::ZoomChanged); + return; } if workspace.zoomed_position == Some(position) { workspace.zoomed = None; @@ -541,10 +540,10 @@ impl Dock { Ok(ix) => ix, Err(ix) => ix, }; - if let Some(active_index) = self.active_panel_index.as_mut() { - if *active_index >= index { - *active_index += 1; - } + if let Some(active_index) = self.active_panel_index.as_mut() + && *active_index >= index + { + *active_index += 1; } self.panel_entries.insert( index, @@ -566,16 +565,16 @@ impl Dock { pub fn restore_state(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool { if let Some(serialized) = self.serialized_dock.clone() { - if let Some(active_panel) = serialized.active_panel.filter(|_| serialized.visible) { - if let Some(idx) = self.panel_index_for_persistent_name(active_panel.as_str(), cx) { - self.activate_panel(idx, window, cx); - } + if let Some(active_panel) = serialized.active_panel.filter(|_| serialized.visible) + && let Some(idx) = self.panel_index_for_persistent_name(active_panel.as_str(), cx) + { + self.activate_panel(idx, window, cx); } - if serialized.zoom { - if let Some(panel) = self.active_panel() { - panel.set_zoomed(true, window, cx) - } + if serialized.zoom + && let Some(panel) = self.active_panel() + { + panel.set_zoomed(true, window, cx) } self.set_open(serialized.visible, window, cx); return true; diff --git a/crates/workspace/src/history_manager.rs b/crates/workspace/src/history_manager.rs index e63b1823ea..a8387369f4 100644 --- a/crates/workspace/src/history_manager.rs +++ b/crates/workspace/src/history_manager.rs @@ -101,11 +101,11 @@ impl HistoryManager { } let mut deleted_ids = Vec::new(); for idx in (0..self.history.len()).rev() { - if let Some(entry) = self.history.get(idx) { - if user_removed.contains(&entry.path) { - deleted_ids.push(entry.id); - self.history.remove(idx); - } + if let Some(entry) = self.history.get(idx) + && user_removed.contains(&entry.path) + { + deleted_ids.push(entry.id); + self.history.remove(idx); } } cx.spawn(async move |_| { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 0c5543650e..014af7b0bc 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -832,10 +832,10 @@ impl<T: Item> ItemHandle for Entity<T> { if let Some(item) = item.to_followable_item_handle(cx) { let leader_id = workspace.leader_for_pane(&pane); - if let Some(leader_id) = leader_id { - if let Some(FollowEvent::Unfollow) = item.to_follow_event(event) { - workspace.unfollow(leader_id, window, cx); - } + if let Some(leader_id) = leader_id + && let Some(FollowEvent::Unfollow) = item.to_follow_event(event) + { + workspace.unfollow(leader_id, window, cx); } if item.item_focus_handle(cx).contains_focused(window, cx) { @@ -863,10 +863,10 @@ impl<T: Item> ItemHandle for Entity<T> { } } - if let Some(item) = item.to_serializable_item_handle(cx) { - if item.should_serialize(event, cx) { - workspace.enqueue_item_serialization(item).ok(); - } + if let Some(item) = item.to_serializable_item_handle(cx) + && item.should_serialize(event, cx) + { + workspace.enqueue_item_serialization(item).ok(); } T::to_item_events(event, |event| match event { @@ -948,11 +948,11 @@ impl<T: Item> ItemHandle for Entity<T> { &self.read(cx).focus_handle(cx), window, move |workspace, window, cx| { - if let Some(item) = weak_item.upgrade() { - if item.workspace_settings(cx).autosave == AutosaveSetting::OnFocusChange { - Pane::autosave_item(&item, workspace.project.clone(), window, cx) - .detach_and_log_err(cx); - } + if let Some(item) = weak_item.upgrade() + && item.workspace_settings(cx).autosave == AutosaveSetting::OnFocusChange + { + Pane::autosave_item(&item, workspace.project.clone(), window, cx) + .detach_and_log_err(cx); } }, ) diff --git a/crates/workspace/src/modal_layer.rs b/crates/workspace/src/modal_layer.rs index 7e92c7b8e9..bcd7db3a82 100644 --- a/crates/workspace/src/modal_layer.rs +++ b/crates/workspace/src/modal_layer.rs @@ -141,10 +141,10 @@ impl ModalLayer { } if let Some(active_modal) = self.active_modal.take() { - if let Some(previous_focus) = active_modal.previous_focus_handle { - if active_modal.focus_handle.contains_focused(window, cx) { - previous_focus.focus(window); - } + if let Some(previous_focus) = active_modal.previous_focus_handle + && active_modal.focus_handle.contains_focused(window, cx) + { + previous_focus.focus(window); } cx.notify(); } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 0a40dbc12c..a1affc5362 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -580,19 +580,18 @@ impl Pane { // or focus the active item itself if let Some(weak_last_focus_handle) = self.last_focus_handle_by_item.get(&active_item.item_id()) + && let Some(focus_handle) = weak_last_focus_handle.upgrade() { - if let Some(focus_handle) = weak_last_focus_handle.upgrade() { - focus_handle.focus(window); - return; - } + focus_handle.focus(window); + return; } active_item.item_focus_handle(cx).focus(window); - } else if let Some(focused) = window.focused(cx) { - if !self.context_menu_focused(window, cx) { - self.last_focus_handle_by_item - .insert(active_item.item_id(), focused.downgrade()); - } + } else if let Some(focused) = window.focused(cx) + && !self.context_menu_focused(window, cx) + { + self.last_focus_handle_by_item + .insert(active_item.item_id(), focused.downgrade()); } } } @@ -858,10 +857,11 @@ impl Pane { } pub fn handle_item_edit(&mut self, item_id: EntityId, cx: &App) { - if let Some(preview_item) = self.preview_item() { - if preview_item.item_id() == item_id && !preview_item.preserve_preview(cx) { - self.set_preview_item_id(None, cx); - } + if let Some(preview_item) = self.preview_item() + && preview_item.item_id() == item_id + && !preview_item.preserve_preview(cx) + { + self.set_preview_item_id(None, cx); } } @@ -900,12 +900,12 @@ impl Pane { if let Some((index, existing_item)) = existing_item { // If the item is already open, and the item is a preview item // and we are not allowing items to open as preview, mark the item as persistent. - if let Some(preview_item_id) = self.preview_item_id { - if let Some(tab) = self.items.get(index) { - if tab.item_id() == preview_item_id && !allow_preview { - self.set_preview_item_id(None, cx); - } - } + if let Some(preview_item_id) = self.preview_item_id + && let Some(tab) = self.items.get(index) + && tab.item_id() == preview_item_id + && !allow_preview + { + self.set_preview_item_id(None, cx); } if activate { self.activate_item(index, focus_item, focus_item, window, cx); @@ -977,21 +977,21 @@ impl Pane { self.close_items_on_item_open(window, cx); } - if item.is_singleton(cx) { - if let Some(&entry_id) = item.project_entry_ids(cx).first() { - let Some(project) = self.project.upgrade() else { - return; - }; + if item.is_singleton(cx) + && let Some(&entry_id) = item.project_entry_ids(cx).first() + { + let Some(project) = self.project.upgrade() else { + return; + }; - let project = project.read(cx); - if let Some(project_path) = project.path_for_entry(entry_id, cx) { - let abs_path = project.absolute_path(&project_path, cx); - self.nav_history - .0 - .lock() - .paths_by_item - .insert(item.item_id(), (project_path, abs_path)); - } + let project = project.read(cx); + if let Some(project_path) = project.path_for_entry(entry_id, cx) { + let abs_path = project.absolute_path(&project_path, cx); + self.nav_history + .0 + .lock() + .paths_by_item + .insert(item.item_id(), (project_path, abs_path)); } } // If no destination index is specified, add or move the item after the @@ -1192,12 +1192,11 @@ impl Pane { use NavigationMode::{GoingBack, GoingForward}; if index < self.items.len() { let prev_active_item_ix = mem::replace(&mut self.active_item_index, index); - if prev_active_item_ix != self.active_item_index - || matches!(self.nav_history.mode(), GoingBack | GoingForward) + if (prev_active_item_ix != self.active_item_index + || matches!(self.nav_history.mode(), GoingBack | GoingForward)) + && let Some(prev_item) = self.items.get(prev_active_item_ix) { - if let Some(prev_item) = self.items.get(prev_active_item_ix) { - prev_item.deactivated(window, cx); - } + prev_item.deactivated(window, cx); } self.update_history(index); self.update_toolbar(window, cx); @@ -2462,10 +2461,11 @@ impl Pane { .on_mouse_down( MouseButton::Left, cx.listener(move |pane, event: &MouseDownEvent, _, cx| { - if let Some(id) = pane.preview_item_id { - if id == item_id && event.click_count > 1 { - pane.set_preview_item_id(None, cx); - } + if let Some(id) = pane.preview_item_id + && id == item_id + && event.click_count > 1 + { + pane.set_preview_item_id(None, cx); } }), ) @@ -3048,18 +3048,18 @@ impl Pane { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(custom_drop_handle) = self.custom_drop_handle.clone() { - if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) { - return; - } + if let Some(custom_drop_handle) = self.custom_drop_handle.clone() + && let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) + { + return; } let mut to_pane = cx.entity(); let split_direction = self.drag_split_direction; let item_id = dragged_tab.item.item_id(); - if let Some(preview_item_id) = self.preview_item_id { - if item_id == preview_item_id { - self.set_preview_item_id(None, cx); - } + if let Some(preview_item_id) = self.preview_item_id + && item_id == preview_item_id + { + self.set_preview_item_id(None, cx); } let is_clone = cfg!(target_os = "macos") && window.modifiers().alt @@ -3136,11 +3136,10 @@ impl Pane { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(custom_drop_handle) = self.custom_drop_handle.clone() { - if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx) - { - return; - } + if let Some(custom_drop_handle) = self.custom_drop_handle.clone() + && let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx) + { + return; } self.handle_project_entry_drop( &dragged_selection.active_selection.entry_id, @@ -3157,10 +3156,10 @@ impl Pane { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(custom_drop_handle) = self.custom_drop_handle.clone() { - if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) { - return; - } + if let Some(custom_drop_handle) = self.custom_drop_handle.clone() + && let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) + { + return; } let mut to_pane = cx.entity(); let split_direction = self.drag_split_direction; @@ -3233,10 +3232,10 @@ impl Pane { window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(custom_drop_handle) = self.custom_drop_handle.clone() { - if let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) { - return; - } + if let Some(custom_drop_handle) = self.custom_drop_handle.clone() + && let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) + { + return; } let mut to_pane = cx.entity(); let mut split_direction = self.drag_split_direction; @@ -3790,10 +3789,10 @@ impl NavHistory { borrowed_history.paths_by_item.get(&entry.item.id()) { f(entry, project_and_abs_path.clone()); - } else if let Some(item) = entry.item.upgrade() { - if let Some(path) = item.project_path(cx) { - f(entry, (path, None)); - } + } else if let Some(item) = entry.item.upgrade() + && let Some(path) = item.project_path(cx) + { + f(entry, (path, None)); } }) } diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 5c87206e9e..bd2aafb7f4 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -619,15 +619,15 @@ impl PaneAxis { let mut found_axis_index: Option<usize> = None; if !found_pane { for (i, pa) in self.members.iter_mut().enumerate() { - if let Member::Axis(pa) = pa { - if let Some(done) = pa.resize(pane, axis, amount, bounds) { - if done { - return Some(true); // pane found and operations already done - } else if self.axis != axis { - return Some(false); // pane found but this is not the correct axis direction - } else { - found_axis_index = Some(i); // pane found and this is correct direction - } + if let Member::Axis(pa) = pa + && let Some(done) = pa.resize(pane, axis, amount, bounds) + { + if done { + return Some(true); // pane found and operations already done + } else if self.axis != axis { + return Some(false); // pane found but this is not the correct axis direction + } else { + found_axis_index = Some(i); // pane found and this is correct direction } } } @@ -743,13 +743,13 @@ impl PaneAxis { let bounding_boxes = self.bounding_boxes.lock(); for (idx, member) in self.members.iter().enumerate() { - if let Some(coordinates) = bounding_boxes[idx] { - if coordinates.contains(&coordinate) { - return match member { - Member::Pane(found) => Some(found), - Member::Axis(axis) => axis.pane_at_pixel_position(coordinate), - }; - } + if let Some(coordinates) = bounding_boxes[idx] + && coordinates.contains(&coordinate) + { + return match member { + Member::Pane(found) => Some(found), + Member::Axis(axis) => axis.pane_at_pixel_position(coordinate), + }; } } None @@ -1273,17 +1273,18 @@ mod element { window.paint_quad(gpui::fill(overlay_bounds, overlay_background)); } - if let Some(border) = overlay_border { - if self.active_pane_ix == Some(ix) && child.is_leaf_pane { - window.paint_quad(gpui::quad( - overlay_bounds, - 0., - gpui::transparent_black(), - border, - cx.theme().colors().border_selected, - BorderStyle::Solid, - )); - } + if let Some(border) = overlay_border + && self.active_pane_ix == Some(ix) + && child.is_leaf_pane + { + window.paint_quad(gpui::quad( + overlay_bounds, + 0., + gpui::transparent_black(), + border, + cx.theme().colors().border_selected, + BorderStyle::Solid, + )); } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index babf2ac1d5..4a22107c42 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1345,18 +1345,18 @@ impl Workspace { .timer(Duration::from_millis(100)) .await; this.update_in(cx, |this, window, cx| { - if let Some(display) = window.display(cx) { - if let Ok(display_uuid) = display.uuid() { - let window_bounds = window.inner_window_bounds(); - if let Some(database_id) = workspace_id { - cx.background_executor() - .spawn(DB.set_window_open_status( - database_id, - SerializedWindowBounds(window_bounds), - display_uuid, - )) - .detach_and_log_err(cx); - } + if let Some(display) = window.display(cx) + && let Ok(display_uuid) = display.uuid() + { + let window_bounds = window.inner_window_bounds(); + if let Some(database_id) = workspace_id { + cx.background_executor() + .spawn(DB.set_window_open_status( + database_id, + SerializedWindowBounds(window_bounds), + display_uuid, + )) + .detach_and_log_err(cx); } } this.bounds_save_task_queued.take(); @@ -1729,13 +1729,12 @@ impl Workspace { let item_map: HashMap<EntityId, &Box<dyn ItemHandle>> = pane.items().map(|item| (item.item_id(), item)).collect(); for entry in pane.activation_history() { - if entry.timestamp > recent_timestamp { - if let Some(&item) = item_map.get(&entry.entity_id) { - if let Some(typed_item) = item.act_as::<T>(cx) { - recent_timestamp = entry.timestamp; - recent_item = Some(typed_item); - } - } + if entry.timestamp > recent_timestamp + && let Some(&item) = item_map.get(&entry.entity_id) + && let Some(typed_item) = item.act_as::<T>(cx) + { + recent_timestamp = entry.timestamp; + recent_item = Some(typed_item); } } } @@ -1774,19 +1773,19 @@ impl Workspace { } }); - if let Some(item) = pane.active_item() { - if let Some(project_path) = item.project_path(cx) { - let fs_path = self.project.read(cx).absolute_path(&project_path, cx); + if let Some(item) = pane.active_item() + && let Some(project_path) = item.project_path(cx) + { + let fs_path = self.project.read(cx).absolute_path(&project_path, cx); - if let Some(fs_path) = &fs_path { - abs_paths_opened - .entry(fs_path.clone()) - .or_default() - .insert(project_path.clone()); - } - - history.insert(project_path, (fs_path, std::usize::MAX)); + if let Some(fs_path) = &fs_path { + abs_paths_opened + .entry(fs_path.clone()) + .or_default() + .insert(project_path.clone()); } + + history.insert(project_path, (fs_path, std::usize::MAX)); } } @@ -2250,29 +2249,28 @@ impl Workspace { .count() })?; - if let Some(active_call) = active_call { - if close_intent != CloseIntent::Quit - && workspace_count == 1 - && active_call.read_with(cx, |call, _| call.room().is_some())? - { - let answer = cx.update(|window, cx| { - window.prompt( - PromptLevel::Warning, - "Do you want to leave the current call?", - None, - &["Close window and hang up", "Cancel"], - cx, - ) - })?; + if let Some(active_call) = active_call + && close_intent != CloseIntent::Quit + && workspace_count == 1 + && active_call.read_with(cx, |call, _| call.room().is_some())? + { + let answer = cx.update(|window, cx| { + window.prompt( + PromptLevel::Warning, + "Do you want to leave the current call?", + None, + &["Close window and hang up", "Cancel"], + cx, + ) + })?; - if answer.await.log_err() == Some(1) { - return anyhow::Ok(false); - } else { - active_call - .update(cx, |call, cx| call.hang_up(cx))? - .await - .log_err(); - } + if answer.await.log_err() == Some(1) { + return anyhow::Ok(false); + } else { + active_call + .update(cx, |call, cx| call.hang_up(cx))? + .await + .log_err(); } } @@ -2448,10 +2446,10 @@ impl Workspace { for (pane, item) in dirty_items { let (singleton, project_entry_ids) = cx.update(|_, cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?; - if singleton || !project_entry_ids.is_empty() { - if !Pane::save_item(project.clone(), &pane, &*item, save_intent, cx).await? { - return Ok(false); - } + if (singleton || !project_entry_ids.is_empty()) + && !Pane::save_item(project.clone(), &pane, &*item, save_intent, cx).await? + { + return Ok(false); } } Ok(true) @@ -3080,14 +3078,12 @@ impl Workspace { let mut focus_center = false; for dock in self.all_docks() { dock.update(cx, |dock, cx| { - if Some(dock.position()) != dock_to_reveal { - if let Some(panel) = dock.active_panel() { - if panel.is_zoomed(window, cx) { - focus_center |= - panel.panel_focus_handle(cx).contains_focused(window, cx); - dock.set_open(false, window, cx); - } - } + if Some(dock.position()) != dock_to_reveal + && let Some(panel) = dock.active_panel() + && panel.is_zoomed(window, cx) + { + focus_center |= panel.panel_focus_handle(cx).contains_focused(window, cx); + dock.set_open(false, window, cx); } }); } @@ -3328,10 +3324,10 @@ impl Workspace { .downgrade() }); - if let Member::Pane(center_pane) = &self.center.root { - if center_pane.read(cx).items_len() == 0 { - return self.open_path(path, Some(pane), true, window, cx); - } + if let Member::Pane(center_pane) = &self.center.root + && center_pane.read(cx).items_len() == 0 + { + return self.open_path(path, Some(pane), true, window, cx); } let project_path = path.into(); @@ -3393,10 +3389,10 @@ impl Workspace { if let Some(entry_id) = entry_id { item = pane.read(cx).item_for_entry(entry_id, cx); } - if item.is_none() { - if let Some(project_path) = project_path { - item = pane.read(cx).item_for_path(project_path, cx); - } + if item.is_none() + && let Some(project_path) = project_path + { + item = pane.read(cx).item_for_path(project_path, cx); } item.and_then(|item| item.downcast::<T>()) @@ -3440,12 +3436,11 @@ impl Workspace { let item_id = item.item_id(); let mut destination_index = None; pane.update(cx, |pane, cx| { - if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation { - if let Some(preview_item_id) = pane.preview_item_id() { - if preview_item_id != item_id { - destination_index = pane.close_current_preview_item(window, cx); - } - } + if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation + && let Some(preview_item_id) = pane.preview_item_id() + && preview_item_id != item_id + { + destination_index = pane.close_current_preview_item(window, cx); } pane.set_preview_item_id(Some(item.item_id()), cx) }); @@ -3912,10 +3907,10 @@ impl Workspace { pane::Event::RemovedItem { item } => { cx.emit(Event::ActiveItemChanged); self.update_window_edited(window, cx); - if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id()) { - if entry.get().entity_id() == pane.entity_id() { - entry.remove(); - } + if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id()) + && entry.get().entity_id() == pane.entity_id() + { + entry.remove(); } } pane::Event::Focus => { @@ -4105,14 +4100,13 @@ impl Workspace { pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity<Pane> { for dock in self.all_docks() { - if dock.focus_handle(cx).contains_focused(window, cx) { - if let Some(pane) = dock + if dock.focus_handle(cx).contains_focused(window, cx) + && let Some(pane) = dock .read(cx) .active_panel() .and_then(|panel| panel.pane(cx)) - { - return pane; - } + { + return pane; } } self.active_pane().clone() @@ -4393,10 +4387,10 @@ impl Workspace { title.push_str(" ↗"); } - if let Some(last_title) = self.last_window_title.as_ref() { - if &title == last_title { - return; - } + if let Some(last_title) = self.last_window_title.as_ref() + && &title == last_title + { + return; } window.set_window_title(&title); self.last_window_title = Some(title); @@ -4575,10 +4569,8 @@ impl Workspace { } })??; - if should_add_view { - if let Some(view) = update_active_view.view { - Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await? - } + if should_add_view && let Some(view) = update_active_view.view { + Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await? } } proto::update_followers::Variant::UpdateView(update_view) => { @@ -4774,40 +4766,40 @@ impl Workspace { if window.is_window_active() { let (active_item, panel_id) = self.active_item_for_followers(window, cx); - if let Some(item) = active_item { - if item.item_focus_handle(cx).contains_focused(window, cx) { - let leader_id = self - .pane_for(&*item) - .and_then(|pane| self.leader_for_pane(&pane)); - let leader_peer_id = match leader_id { - Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id), - Some(CollaboratorId::Agent) | None => None, - }; + if let Some(item) = active_item + && item.item_focus_handle(cx).contains_focused(window, cx) + { + let leader_id = self + .pane_for(&*item) + .and_then(|pane| self.leader_for_pane(&pane)); + let leader_peer_id = match leader_id { + Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id), + Some(CollaboratorId::Agent) | None => None, + }; - if let Some(item) = item.to_followable_item_handle(cx) { - let id = item - .remote_id(&self.app_state.client, window, cx) - .map(|id| id.to_proto()); + if let Some(item) = item.to_followable_item_handle(cx) { + let id = item + .remote_id(&self.app_state.client, window, cx) + .map(|id| id.to_proto()); - if let Some(id) = id { - if let Some(variant) = item.to_state_proto(window, cx) { - let view = Some(proto::View { - id: id.clone(), - leader_id: leader_peer_id, - variant: Some(variant), - panel_id: panel_id.map(|id| id as i32), - }); + if let Some(id) = id + && let Some(variant) = item.to_state_proto(window, cx) + { + let view = Some(proto::View { + id: id.clone(), + leader_id: leader_peer_id, + variant: Some(variant), + panel_id: panel_id.map(|id| id as i32), + }); - is_project_item = item.is_project_item(window, cx); - update = proto::UpdateActiveView { - view, - // TODO: Remove after version 0.145.x stabilizes. - id, - leader_id: leader_peer_id, - }; - } + is_project_item = item.is_project_item(window, cx); + update = proto::UpdateActiveView { + view, + // TODO: Remove after version 0.145.x stabilizes. + id, + leader_id: leader_peer_id, }; - } + }; } } } @@ -4832,16 +4824,14 @@ impl Workspace { let mut active_item = None; let mut panel_id = None; for dock in self.all_docks() { - if dock.focus_handle(cx).contains_focused(window, cx) { - if let Some(panel) = dock.read(cx).active_panel() { - if let Some(pane) = panel.pane(cx) { - if let Some(item) = pane.read(cx).active_item() { - active_item = Some(item); - panel_id = panel.remote_id(); - break; - } - } - } + if dock.focus_handle(cx).contains_focused(window, cx) + && let Some(panel) = dock.read(cx).active_panel() + && let Some(pane) = panel.pane(cx) + && let Some(item) = pane.read(cx).active_item() + { + active_item = Some(item); + panel_id = panel.remote_id(); + break; } } @@ -4969,10 +4959,10 @@ impl Workspace { let state = self.follower_states.get(&peer_id.into())?; let mut item_to_activate = None; if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) { - if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) { - if leader_in_this_project || !item.view.is_project_item(window, cx) { - item_to_activate = Some((item.location, item.view.boxed_clone())); - } + if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) + && (leader_in_this_project || !item.view.is_project_item(window, cx)) + { + item_to_activate = Some((item.location, item.view.boxed_clone())); } } else if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx) @@ -6079,10 +6069,10 @@ fn open_items( project_paths_to_open .iter_mut() .for_each(|(_, project_path)| { - if let Some(project_path_to_open) = project_path { - if restored_project_paths.contains(project_path_to_open) { - *project_path = None; - } + if let Some(project_path_to_open) = project_path + && restored_project_paths.contains(project_path_to_open) + { + *project_path = None; } }); } else { @@ -6109,24 +6099,24 @@ fn open_items( // We only want to open file paths here. If one of the items // here is a directory, it was already opened further above // with a `find_or_create_worktree`. - if let Ok(task) = abs_path_task { - if task.await.map_or(true, |p| p.is_file()) { - return Some(( - ix, - workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_path( - file_project_path, - None, - true, - window, - cx, - ) - }) - .log_err()? - .await, - )); - } + if let Ok(task) = abs_path_task + && task.await.map_or(true, |p| p.is_file()) + { + return Some(( + ix, + workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_path( + file_project_path, + None, + true, + window, + cx, + ) + }) + .log_err()? + .await, + )); } None }) @@ -6728,10 +6718,10 @@ impl WorkspaceStore { .update(cx, |workspace, window, cx| { let handler_response = workspace.handle_follow(follower.project_id, window, cx); - if let Some(active_view) = handler_response.active_view.clone() { - if workspace.project.read(cx).remote_id() == follower.project_id { - response.active_view = Some(active_view) - } + if let Some(active_view) = handler_response.active_view.clone() + && workspace.project.read(cx).remote_id() == follower.project_id + { + response.active_view = Some(active_view) } }) .is_ok() @@ -6965,34 +6955,35 @@ async fn join_channel_internal( } // If you are the first to join a channel, see if you should share your project. - if room.remote_participants().is_empty() && !room.local_participant_is_guest() { - if let Some(workspace) = requesting_window { - let project = workspace.update(cx, |workspace, _, cx| { - let project = workspace.project.read(cx); + if room.remote_participants().is_empty() + && !room.local_participant_is_guest() + && let Some(workspace) = requesting_window + { + let project = workspace.update(cx, |workspace, _, cx| { + let project = workspace.project.read(cx); - if !CallSettings::get_global(cx).share_on_join { - return None; - } - - if (project.is_local() || project.is_via_ssh()) - && project.visible_worktrees(cx).any(|tree| { - tree.read(cx) - .root_entry() - .map_or(false, |entry| entry.is_dir()) - }) - { - Some(workspace.project.clone()) - } else { - None - } - }); - if let Ok(Some(project)) = project { - return Some(cx.spawn(async move |room, cx| { - room.update(cx, |room, cx| room.share_project(project, cx))? - .await?; - Ok(()) - })); + if !CallSettings::get_global(cx).share_on_join { + return None; } + + if (project.is_local() || project.is_via_ssh()) + && project.visible_worktrees(cx).any(|tree| { + tree.read(cx) + .root_entry() + .map_or(false, |entry| entry.is_dir()) + }) + { + Some(workspace.project.clone()) + } else { + None + } + }); + if let Ok(Some(project)) = project { + return Some(cx.spawn(async move |room, cx| { + room.update(cx, |room, cx| room.share_project(project, cx))? + .await?; + Ok(()) + })); } } @@ -7189,35 +7180,35 @@ pub fn open_paths( } })?; - if open_options.open_new_workspace.is_none() && existing.is_none() { - if all_metadatas.iter().all(|file| !file.is_dir) { - cx.update(|cx| { - if let Some(window) = cx - .active_window() - .and_then(|window| window.downcast::<Workspace>()) - { - if let Ok(workspace) = window.read(cx) { - let project = workspace.project().read(cx); - if project.is_local() && !project.is_via_collab() { - existing = Some(window); - open_visible = OpenVisible::None; - return; - } - } + if open_options.open_new_workspace.is_none() + && existing.is_none() + && all_metadatas.iter().all(|file| !file.is_dir) + { + cx.update(|cx| { + if let Some(window) = cx + .active_window() + .and_then(|window| window.downcast::<Workspace>()) + && let Ok(workspace) = window.read(cx) + { + let project = workspace.project().read(cx); + if project.is_local() && !project.is_via_collab() { + existing = Some(window); + open_visible = OpenVisible::None; + return; } - for window in local_workspace_windows(cx) { - if let Ok(workspace) = window.read(cx) { - let project = workspace.project().read(cx); - if project.is_via_collab() { - continue; - } - existing = Some(window); - open_visible = OpenVisible::None; - break; + } + for window in local_workspace_windows(cx) { + if let Ok(workspace) = window.read(cx) { + let project = workspace.project().read(cx); + if project.is_via_collab() { + continue; } + existing = Some(window); + open_visible = OpenVisible::None; + break; } - })?; - } + } + })?; } } @@ -7651,10 +7642,9 @@ pub fn reload(cx: &mut App) { for window in workspace_windows { if let Ok(should_close) = window.update(cx, |workspace, window, cx| { workspace.prepare_to_close(CloseIntent::Quit, window, cx) - }) { - if !should_close.await? { - return Ok(()); - } + }) && !should_close.await? + { + return Ok(()); } } cx.update(|cx| cx.restart()) diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 4a8c9d4666..5635347514 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -282,19 +282,17 @@ impl Settings for WorkspaceSettings { if vscode .read_bool("accessibility.dimUnfocused.enabled") .unwrap_or_default() - { - if let Some(opacity) = vscode + && let Some(opacity) = vscode .read_value("accessibility.dimUnfocused.opacity") .and_then(|v| v.as_f64()) - { - if let Some(settings) = current.active_pane_modifiers.as_mut() { - settings.inactive_opacity = Some(opacity as f32) - } else { - current.active_pane_modifiers = Some(ActivePanelModifiers { - inactive_opacity: Some(opacity as f32), - ..Default::default() - }) - } + { + if let Some(settings) = current.active_pane_modifiers.as_mut() { + settings.inactive_opacity = Some(opacity as f32) + } else { + current.active_pane_modifiers = Some(ActivePanelModifiers { + inactive_opacity: Some(opacity as f32), + ..Default::default() + }) } } @@ -345,13 +343,11 @@ impl Settings for WorkspaceSettings { .read_value("workbench.editor.limit.value") .and_then(|v| v.as_u64()) .and_then(|n| NonZeroUsize::new(n as usize)) - { - if vscode + && vscode .read_bool("workbench.editor.limit.enabled") .unwrap_or_default() - { - current.max_tabs = Some(n) - } + { + current.max_tabs = Some(n) } // some combination of "window.restoreWindows" and "workbench.startupEditor" might diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index f110726afd..9e1832721f 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -1522,10 +1522,10 @@ impl LocalWorktree { // reasonable limit { const FILE_SIZE_MAX: u64 = 6 * 1024 * 1024 * 1024; // 6GB - if let Ok(Some(metadata)) = fs.metadata(&abs_path).await { - if metadata.len >= FILE_SIZE_MAX { - anyhow::bail!("File is too large to load"); - } + if let Ok(Some(metadata)) = fs.metadata(&abs_path).await + && metadata.len >= FILE_SIZE_MAX + { + anyhow::bail!("File is too large to load"); } } let text = fs.load(&abs_path).await?; @@ -2503,10 +2503,10 @@ impl Snapshot { if let Some(PathEntry { path, .. }) = self.entries_by_id.get(&entry.id, &()) { entries_by_path_edits.push(Edit::Remove(PathKey(path.clone()))); } - if let Some(old_entry) = self.entries_by_path.get(&PathKey(entry.path.clone()), &()) { - if old_entry.id != entry.id { - entries_by_id_edits.push(Edit::Remove(old_entry.id)); - } + if let Some(old_entry) = self.entries_by_path.get(&PathKey(entry.path.clone()), &()) + && old_entry.id != entry.id + { + entries_by_id_edits.push(Edit::Remove(old_entry.id)); } entries_by_id_edits.push(Edit::Insert(PathEntry { id: entry.id, @@ -2747,20 +2747,19 @@ impl LocalSnapshot { } } - if entry.kind == EntryKind::PendingDir { - if let Some(existing_entry) = + if entry.kind == EntryKind::PendingDir + && let Some(existing_entry) = self.entries_by_path.get(&PathKey(entry.path.clone()), &()) - { - entry.kind = existing_entry.kind; - } + { + entry.kind = existing_entry.kind; } let scan_id = self.scan_id; let removed = self.entries_by_path.insert_or_replace(entry.clone(), &()); - if let Some(removed) = removed { - if removed.id != entry.id { - self.entries_by_id.remove(&removed.id, &()); - } + if let Some(removed) = removed + && removed.id != entry.id + { + self.entries_by_id.remove(&removed.id, &()); } self.entries_by_id.insert_or_replace( PathEntry { @@ -4138,13 +4137,13 @@ impl BackgroundScanner { let root_path = state.snapshot.abs_path.clone(); for path in paths { for ancestor in path.ancestors() { - if let Some(entry) = state.snapshot.entry_for_path(ancestor) { - if entry.kind == EntryKind::UnloadedDir { - let abs_path = root_path.as_path().join(ancestor); - state.enqueue_scan_dir(abs_path.into(), entry, &scan_job_tx); - state.paths_to_scan.insert(path.clone()); - break; - } + if let Some(entry) = state.snapshot.entry_for_path(ancestor) + && entry.kind == EntryKind::UnloadedDir + { + let abs_path = root_path.as_path().join(ancestor); + state.enqueue_scan_dir(abs_path.into(), entry, &scan_job_tx); + state.paths_to_scan.insert(path.clone()); + break; } } } @@ -4214,11 +4213,10 @@ impl BackgroundScanner { // Recursively load directories from the file system. job = scan_jobs_rx.recv().fuse() => { let Ok(job) = job else { break }; - if let Err(err) = self.scan_dir(&job).await { - if job.path.as_ref() != Path::new("") { + if let Err(err) = self.scan_dir(&job).await + && job.path.as_ref() != Path::new("") { log::error!("error scanning directory {:?}: {}", job.abs_path, err); } - } } } } @@ -4554,18 +4552,18 @@ impl BackgroundScanner { state.insert_entry(fs_entry.clone(), self.fs.as_ref(), self.watcher.as_ref()); - if path.as_ref() == Path::new("") { - if let Some((ignores, repo)) = new_ancestor_repo.take() { - log::trace!("updating ancestor git repository"); - state.snapshot.ignores_by_parent_abs_path.extend(ignores); - if let Some((ancestor_dot_git, work_directory)) = repo { - state.insert_git_repository_for_path( - work_directory, - ancestor_dot_git.as_path().into(), - self.fs.as_ref(), - self.watcher.as_ref(), - ); - } + if path.as_ref() == Path::new("") + && let Some((ignores, repo)) = new_ancestor_repo.take() + { + log::trace!("updating ancestor git repository"); + state.snapshot.ignores_by_parent_abs_path.extend(ignores); + if let Some((ancestor_dot_git, work_directory)) = repo { + state.insert_git_repository_for_path( + work_directory, + ancestor_dot_git.as_path().into(), + self.fs.as_ref(), + self.watcher.as_ref(), + ); } } } @@ -4590,13 +4588,12 @@ impl BackgroundScanner { if !path .components() .any(|component| component.as_os_str() == *DOT_GIT) + && let Some(local_repo) = snapshot.local_repo_for_work_directory_path(path) { - if let Some(local_repo) = snapshot.local_repo_for_work_directory_path(path) { - let id = local_repo.work_directory_id; - log::debug!("remove repo path: {:?}", path); - snapshot.git_repositories.remove(&id); - return Some(()); - } + let id = local_repo.work_directory_id; + log::debug!("remove repo path: {:?}", path); + snapshot.git_repositories.remove(&id); + return Some(()); } Some(()) @@ -4738,10 +4735,10 @@ impl BackgroundScanner { let state = &mut self.state.lock(); for edit in &entries_by_path_edits { - if let Edit::Insert(entry) = edit { - if let Err(ix) = state.changed_paths.binary_search(&entry.path) { - state.changed_paths.insert(ix, entry.path.clone()); - } + if let Edit::Insert(entry) = edit + && let Err(ix) = state.changed_paths.binary_search(&entry.path) + { + state.changed_paths.insert(ix, entry.path.clone()); } } @@ -5287,13 +5284,12 @@ impl<'a> Traversal<'a> { while let Some(entry) = self.cursor.item() { self.cursor .seek_forward(&TraversalTarget::successor(&entry.path), Bias::Left); - if let Some(entry) = self.cursor.item() { - if (self.include_files || !entry.is_file()) - && (self.include_dirs || !entry.is_dir()) - && (self.include_ignored || !entry.is_ignored || entry.is_always_included) - { - return true; - } + if let Some(entry) = self.cursor.item() + && (self.include_files || !entry.is_file()) + && (self.include_dirs || !entry.is_dir()) + && (self.include_ignored || !entry.is_ignored || entry.is_always_included) + { + return true; } } false @@ -5437,11 +5433,11 @@ impl<'a> Iterator for ChildEntriesIter<'a> { type Item = &'a Entry; fn next(&mut self) -> Option<Self::Item> { - if let Some(item) = self.traversal.entry() { - if item.path.starts_with(self.parent_path) { - self.traversal.advance_to_sibling(); - return Some(item); - } + if let Some(item) = self.traversal.entry() + && item.path.starts_with(self.parent_path) + { + self.traversal.advance_to_sibling(); + return Some(item); } None } @@ -5564,12 +5560,10 @@ fn discover_git_paths(dot_git_abs_path: &Arc<Path>, fs: &dyn Fs) -> (Arc<Path>, repository_dir_abs_path = Path::new(&path).into(); common_dir_abs_path = repository_dir_abs_path.clone(); if let Some(commondir_contents) = smol::block_on(fs.load(&path.join("commondir"))).ok() - { - if let Some(commondir_path) = + && let Some(commondir_path) = smol::block_on(fs.canonicalize(&path.join(commondir_contents.trim()))).log_err() - { - common_dir_abs_path = commondir_path.as_path().into(); - } + { + common_dir_abs_path = commondir_path.as_path().into(); } } }; diff --git a/crates/zed/build.rs b/crates/zed/build.rs index eb18617add..c6d943a459 100644 --- a/crates/zed/build.rs +++ b/crates/zed/build.rs @@ -23,22 +23,20 @@ fn main() { "cargo:rustc-env=TARGET={}", std::env::var("TARGET").unwrap() ); - if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() { - if output.status.success() { - let git_sha = String::from_utf8_lossy(&output.stdout); - let git_sha = git_sha.trim(); + if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() + && output.status.success() + { + let git_sha = String::from_utf8_lossy(&output.stdout); + let git_sha = git_sha.trim(); - println!("cargo:rustc-env=ZED_COMMIT_SHA={git_sha}"); + println!("cargo:rustc-env=ZED_COMMIT_SHA={git_sha}"); - if let Ok(build_profile) = std::env::var("PROFILE") { - if build_profile == "release" { - // This is currently the best way to make `cargo build ...`'s build script - // to print something to stdout without extra verbosity. - println!( - "cargo:warning=Info: using '{git_sha}' hash for ZED_COMMIT_SHA env var" - ); - } - } + if let Ok(build_profile) = std::env::var("PROFILE") + && build_profile == "release" + { + // This is currently the best way to make `cargo build ...`'s build script + // to print something to stdout without extra verbosity. + println!("cargo:warning=Info: using '{git_sha}' hash for ZED_COMMIT_SHA env var"); } } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index a66b30c44a..df30d4dd7b 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -1026,18 +1026,18 @@ async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: &mut AsyncApp // Try to find an active workspace to show the toast let toast_shown = cx .update(|cx| { - if let Some(window) = cx.active_window() { - if let Some(workspace) = window.downcast::<Workspace>() { - workspace - .update(cx, |workspace, _, cx| { - workspace.show_toast( - Toast::new(NotificationId::unique::<()>(), message), - cx, - ) - }) - .ok(); - return true; - } + if let Some(window) = cx.active_window() + && let Some(workspace) = window.downcast::<Workspace>() + { + workspace + .update(cx, |workspace, _, cx| { + workspace.show_toast( + Toast::new(NotificationId::unique::<()>(), message), + cx, + ) + }) + .ok(); + return true; } false }) @@ -1117,10 +1117,8 @@ pub(crate) async fn restorable_workspace_locations( // Since last_session_window_order returns the windows ordered front-to-back // we need to open the window that was frontmost last. - if ordered { - if let Some(locations) = locations.as_mut() { - locations.reverse(); - } + if ordered && let Some(locations) = locations.as_mut() { + locations.reverse(); } locations @@ -1290,21 +1288,21 @@ fn eager_load_active_theme_and_icon_theme(fs: Arc<dyn Fs>, cx: &App) { if let Some(theme_selection) = theme_settings.theme_selection.as_ref() { let theme_name = theme_selection.theme(appearance); - if matches!(theme_registry.get(theme_name), Err(ThemeNotFoundError(_))) { - if let Some(theme_path) = extension_store.read(cx).path_to_extension_theme(theme_name) { - cx.spawn({ - let theme_registry = theme_registry.clone(); - let fs = fs.clone(); - async move |cx| { - theme_registry.load_user_theme(&theme_path, fs).await?; + if matches!(theme_registry.get(theme_name), Err(ThemeNotFoundError(_))) + && let Some(theme_path) = extension_store.read(cx).path_to_extension_theme(theme_name) + { + cx.spawn({ + let theme_registry = theme_registry.clone(); + let fs = fs.clone(); + async move |cx| { + theme_registry.load_user_theme(&theme_path, fs).await?; - cx.update(|cx| { - ThemeSettings::reload_current_theme(cx); - }) - } - }) - .detach_and_log_err(cx); - } + cx.update(|cx| { + ThemeSettings::reload_current_theme(cx); + }) + } + }) + .detach_and_log_err(cx); } } @@ -1313,26 +1311,24 @@ fn eager_load_active_theme_and_icon_theme(fs: Arc<dyn Fs>, cx: &App) { if matches!( theme_registry.get_icon_theme(icon_theme_name), Err(IconThemeNotFoundError(_)) - ) { - if let Some((icon_theme_path, icons_root_path)) = extension_store - .read(cx) - .path_to_extension_icon_theme(icon_theme_name) - { - cx.spawn({ - let theme_registry = theme_registry.clone(); - let fs = fs.clone(); - async move |cx| { - theme_registry - .load_icon_theme(&icon_theme_path, &icons_root_path, fs) - .await?; + ) && let Some((icon_theme_path, icons_root_path)) = extension_store + .read(cx) + .path_to_extension_icon_theme(icon_theme_name) + { + cx.spawn({ + let theme_registry = theme_registry.clone(); + let fs = fs.clone(); + async move |cx| { + theme_registry + .load_icon_theme(&icon_theme_path, &icons_root_path, fs) + .await?; - cx.update(|cx| { - ThemeSettings::reload_current_icon_theme(cx); - }) - } - }) - .detach_and_log_err(cx); - } + cx.update(|cx| { + ThemeSettings::reload_current_icon_theme(cx); + }) + } + }) + .detach_and_log_err(cx); } } } @@ -1381,18 +1377,15 @@ fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut App) { while let Some(paths) = events.next().await { for event in paths { - if fs.metadata(&event.path).await.ok().flatten().is_some() { - if let Some(theme_registry) = + if fs.metadata(&event.path).await.ok().flatten().is_some() + && let Some(theme_registry) = cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err() - { - if let Some(()) = theme_registry - .load_user_theme(&event.path, fs.clone()) - .await - .log_err() - { - cx.update(ThemeSettings::reload_current_theme).log_err(); - } - } + && let Some(()) = theme_registry + .load_user_theme(&event.path, fs.clone()) + .await + .log_err() + { + cx.update(ThemeSettings::reload_current_theme).log_err(); } } } diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index f2e65b4f53..cbd31c2e26 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -146,19 +146,17 @@ pub fn init_panic_hook( } zlog::flush(); - if !is_pty { - if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() { - let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string(); - let panic_file_path = paths::logs_dir().join(format!("zed-{timestamp}.panic")); - let panic_file = fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&panic_file_path) - .log_err(); - if let Some(mut panic_file) = panic_file { - writeln!(&mut panic_file, "{panic_data_json}").log_err(); - panic_file.flush().log_err(); - } + if !is_pty && let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() { + let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string(); + let panic_file_path = paths::logs_dir().join(format!("zed-{timestamp}.panic")); + let panic_file = fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&panic_file_path) + .log_err(); + if let Some(mut panic_file) = panic_file { + writeln!(&mut panic_file, "{panic_data_json}").log_err(); + panic_file.flush().log_err(); } } @@ -459,10 +457,10 @@ pub fn monitor_main_thread_hangs( continue; }; - if let Some(response) = http_client.send(request).await.log_err() { - if response.status() != 200 { - log::error!("Failed to send hang report: HTTP {:?}", response.status()); - } + if let Some(response) = http_client.send(request).await.log_err() + && response.status() != 200 + { + log::error!("Failed to send hang report: HTTP {:?}", response.status()); } } } @@ -563,8 +561,8 @@ pub async fn upload_previous_minidumps(http: Arc<HttpClientWithUrl>) -> anyhow:: } let mut json_path = child_path.clone(); json_path.set_extension("json"); - if let Ok(metadata) = serde_json::from_slice(&smol::fs::read(&json_path).await?) { - if upload_minidump( + if let Ok(metadata) = serde_json::from_slice(&smol::fs::read(&json_path).await?) + && upload_minidump( http.clone(), minidump_endpoint, smol::fs::read(&child_path) @@ -575,10 +573,9 @@ pub async fn upload_previous_minidumps(http: Arc<HttpClientWithUrl>) -> anyhow:: .await .log_err() .is_some() - { - fs::remove_file(child_path).ok(); - fs::remove_file(json_path).ok(); - } + { + fs::remove_file(child_path).ok(); + fs::remove_file(json_path).ok(); } } Ok(()) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 535cb12e1a..93a62afc6f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1054,27 +1054,25 @@ fn quit(_: &Quit, cx: &mut App) { }) .log_err(); - if should_confirm { - if let Some(workspace) = workspace_windows.first() { - let answer = workspace - .update(cx, |_, window, cx| { - window.prompt( - PromptLevel::Info, - "Are you sure you want to quit?", - None, - &["Quit", "Cancel"], - cx, - ) - }) - .log_err(); + if should_confirm && let Some(workspace) = workspace_windows.first() { + let answer = workspace + .update(cx, |_, window, cx| { + window.prompt( + PromptLevel::Info, + "Are you sure you want to quit?", + None, + &["Quit", "Cancel"], + cx, + ) + }) + .log_err(); - if let Some(answer) = answer { - WAITING_QUIT_CONFIRMATION.store(true, atomic::Ordering::Release); - let answer = answer.await.ok(); - WAITING_QUIT_CONFIRMATION.store(false, atomic::Ordering::Release); - if answer != Some(0) { - return Ok(()); - } + if let Some(answer) = answer { + WAITING_QUIT_CONFIRMATION.store(true, atomic::Ordering::Release); + let answer = answer.await.ok(); + WAITING_QUIT_CONFIRMATION.store(false, atomic::Ordering::Release); + if answer != Some(0) { + return Ok(()); } } } @@ -1086,10 +1084,9 @@ fn quit(_: &Quit, cx: &mut App) { workspace.prepare_to_close(CloseIntent::Quit, window, cx) }) .log_err() + && !should_close.await? { - if !should_close.await? { - return Ok(()); - } + return Ok(()); } } cx.update(|cx| cx.quit())?; @@ -1633,15 +1630,15 @@ fn open_local_file( }; if !file_exists { - if let Some(dir_path) = settings_relative_path.parent() { - if worktree.read_with(cx, |tree, _| tree.entry_for_path(dir_path).is_none())? { - project - .update(cx, |project, cx| { - project.create_entry((tree_id, dir_path), true, cx) - })? - .await - .context("worktree was removed")?; - } + if let Some(dir_path) = settings_relative_path.parent() + && worktree.read_with(cx, |tree, _| tree.entry_for_path(dir_path).is_none())? + { + project + .update(cx, |project, cx| { + project.create_entry((tree_id, dir_path), true, cx) + })? + .await + .context("worktree was removed")?; } if worktree.read_with(cx, |tree, _| { @@ -1667,12 +1664,12 @@ fn open_local_file( editor .downgrade() .update(cx, |editor, cx| { - if let Some(buffer) = editor.buffer().read(cx).as_singleton() { - if buffer.read(cx).is_empty() { - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, initial_contents)], None, cx) - }); - } + if let Some(buffer) = editor.buffer().read(cx).as_singleton() + && buffer.read(cx).is_empty() + { + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, initial_contents)], None, cx) + }); } }) .ok(); diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index 915c40030a..d855fc3af7 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/crates/zed/src/zed/component_preview.rs @@ -318,25 +318,25 @@ impl ComponentPreview { let lowercase_scope = scope_name.to_lowercase(); let lowercase_desc = description.to_lowercase(); - if lowercase_scopeless.contains(&lowercase_filter) { - if let Some(index) = lowercase_scopeless.find(&lowercase_filter) { - let end = index + lowercase_filter.len(); + if lowercase_scopeless.contains(&lowercase_filter) + && let Some(index) = lowercase_scopeless.find(&lowercase_filter) + { + let end = index + lowercase_filter.len(); - if end <= scopeless_name.len() { - let mut positions = Vec::new(); - for i in index..end { - if scopeless_name.is_char_boundary(i) { - positions.push(i); - } + if end <= scopeless_name.len() { + let mut positions = Vec::new(); + for i in index..end { + if scopeless_name.is_char_boundary(i) { + positions.push(i); } + } - if !positions.is_empty() { - scope_groups - .entry(component.scope()) - .or_insert_with(Vec::new) - .push((component.clone(), Some(positions))); - continue; - } + if !positions.is_empty() { + scope_groups + .entry(component.scope()) + .or_insert_with(Vec::new) + .push((component.clone(), Some(positions))); + continue; } } } @@ -372,32 +372,32 @@ impl ComponentPreview { scopes.sort_by_key(|s| s.to_string()); for scope in scopes { - if let Some(components) = scope_groups.remove(&scope) { - if !components.is_empty() { - entries.push(PreviewEntry::Separator); - entries.push(PreviewEntry::SectionHeader(scope.to_string().into())); + if let Some(components) = scope_groups.remove(&scope) + && !components.is_empty() + { + entries.push(PreviewEntry::Separator); + entries.push(PreviewEntry::SectionHeader(scope.to_string().into())); - let mut sorted_components = components; - sorted_components.sort_by_key(|(component, _)| component.sort_name()); + let mut sorted_components = components; + sorted_components.sort_by_key(|(component, _)| component.sort_name()); - for (component, positions) in sorted_components { - entries.push(PreviewEntry::Component(component, positions)); - } + for (component, positions) in sorted_components { + entries.push(PreviewEntry::Component(component, positions)); } } } // Add uncategorized components last - if let Some(components) = scope_groups.get(&ComponentScope::None) { - if !components.is_empty() { - entries.push(PreviewEntry::Separator); - entries.push(PreviewEntry::SectionHeader("Uncategorized".into())); - let mut sorted_components = components.clone(); - sorted_components.sort_by_key(|(c, _)| c.sort_name()); + if let Some(components) = scope_groups.get(&ComponentScope::None) + && !components.is_empty() + { + entries.push(PreviewEntry::Separator); + entries.push(PreviewEntry::SectionHeader("Uncategorized".into())); + let mut sorted_components = components.clone(); + sorted_components.sort_by_key(|(c, _)| c.sort_name()); - for (component, positions) in sorted_components { - entries.push(PreviewEntry::Component(component, positions)); - } + for (component, positions) in sorted_components { + entries.push(PreviewEntry::Component(component, positions)); } } @@ -415,19 +415,20 @@ impl ComponentPreview { let filtered_components = self.filtered_components(); - if !self.filter_text.is_empty() && !matches!(self.active_page, PreviewPage::AllComponents) { - if let PreviewPage::Component(ref component_id) = self.active_page { - let component_still_visible = filtered_components - .iter() - .any(|component| component.id() == *component_id); + if !self.filter_text.is_empty() + && !matches!(self.active_page, PreviewPage::AllComponents) + && let PreviewPage::Component(ref component_id) = self.active_page + { + let component_still_visible = filtered_components + .iter() + .any(|component| component.id() == *component_id); - if !component_still_visible { - if !filtered_components.is_empty() { - let first_component = &filtered_components[0]; - self.set_active_page(PreviewPage::Component(first_component.id()), cx); - } else { - self.set_active_page(PreviewPage::AllComponents, cx); - } + if !component_still_visible { + if !filtered_components.is_empty() { + let first_component = &filtered_components[0]; + self.set_active_page(PreviewPage::Component(first_component.id()), cx); + } else { + self.set_active_page(PreviewPage::AllComponents, cx); } } } diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index 587786fe8f..8d12a5bfad 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -204,12 +204,12 @@ fn assign_edit_prediction_provider( } EditPredictionProvider::Copilot => { if let Some(copilot) = Copilot::global(cx) { - if let Some(buffer) = singleton_buffer { - if buffer.read(cx).file().is_some() { - copilot.update(cx, |copilot, cx| { - copilot.register_buffer(&buffer, cx); - }); - } + if let Some(buffer) = singleton_buffer + && buffer.read(cx).file().is_some() + { + copilot.update(cx, |copilot, cx| { + copilot.register_buffer(&buffer, cx); + }); } let provider = cx.new(|_| CopilotCompletionProvider::new(copilot)); editor.set_edit_prediction_provider(Some(provider), window, cx); @@ -225,15 +225,15 @@ fn assign_edit_prediction_provider( if user_store.read(cx).current_user().is_some() { let mut worktree = None; - if let Some(buffer) = &singleton_buffer { - if let Some(file) = buffer.read(cx).file() { - let id = file.worktree_id(cx); - if let Some(inner_worktree) = editor - .project() - .and_then(|project| project.read(cx).worktree_for_id(id, cx)) - { - worktree = Some(inner_worktree); - } + if let Some(buffer) = &singleton_buffer + && let Some(file) = buffer.read(cx).file() + { + let id = file.worktree_id(cx); + if let Some(inner_worktree) = editor + .project() + .and_then(|project| project.read(cx).worktree_for_id(id, cx)) + { + worktree = Some(inner_worktree); } } @@ -245,12 +245,12 @@ fn assign_edit_prediction_provider( let zeta = zeta::Zeta::register(workspace, worktree, client.clone(), user_store, cx); - if let Some(buffer) = &singleton_buffer { - if buffer.read(cx).file().is_some() { - zeta.update(cx, |zeta, cx| { - zeta.register_buffer(buffer, cx); - }); - } + if let Some(buffer) = &singleton_buffer + && buffer.read(cx).file().is_some() + { + zeta.update(cx, |zeta, cx| { + zeta.register_buffer(buffer, cx); + }); } let data_collection = diff --git a/crates/zed/src/zed/mac_only_instance.rs b/crates/zed/src/zed/mac_only_instance.rs index 716c2224e3..cb9641e9df 100644 --- a/crates/zed/src/zed/mac_only_instance.rs +++ b/crates/zed/src/zed/mac_only_instance.rs @@ -37,20 +37,19 @@ fn address() -> SocketAddr { let mut user_port = port; let mut sys = System::new_all(); sys.refresh_all(); - if let Ok(current_pid) = sysinfo::get_current_pid() { - if let Some(uid) = sys + if let Ok(current_pid) = sysinfo::get_current_pid() + && let Some(uid) = sys .process(current_pid) .and_then(|process| process.user_id()) - { - let uid_u32 = get_uid_as_u32(uid); - // Ensure that the user ID is not too large to avoid overflow when - // calculating the port number. This seems unlikely but it doesn't - // hurt to be safe. - let max_port = 65535; - let max_uid: u32 = max_port - port as u32; - let wrapped_uid: u16 = (uid_u32 % max_uid) as u16; - user_port += wrapped_uid; - } + { + let uid_u32 = get_uid_as_u32(uid); + // Ensure that the user ID is not too large to avoid overflow when + // calculating the port number. This seems unlikely but it doesn't + // hurt to be safe. + let max_port = 65535; + let max_uid: u32 = max_port - port as u32; + let wrapped_uid: u16 = (uid_u32 % max_uid) as u16; + user_port += wrapped_uid; } SocketAddr::V4(SocketAddrV4::new(LOCALHOST, user_port)) diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index f282860e2c..5baf76b64c 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -123,26 +123,24 @@ impl OpenRequest { fn parse_request_path(&mut self, request_path: &str) -> Result<()> { let mut parts = request_path.split('/'); - if parts.next() == Some("channel") { - if let Some(slug) = parts.next() { - if let Some(id_str) = slug.split('-').next_back() { - if let Ok(channel_id) = id_str.parse::<u64>() { - let Some(next) = parts.next() else { - self.join_channel = Some(channel_id); - return Ok(()); - }; + if parts.next() == Some("channel") + && let Some(slug) = parts.next() + && let Some(id_str) = slug.split('-').next_back() + && let Ok(channel_id) = id_str.parse::<u64>() + { + let Some(next) = parts.next() else { + self.join_channel = Some(channel_id); + return Ok(()); + }; - if let Some(heading) = next.strip_prefix("notes#") { - self.open_channel_notes - .push((channel_id, Some(heading.to_string()))); - return Ok(()); - } - if next == "notes" { - self.open_channel_notes.push((channel_id, None)); - return Ok(()); - } - } - } + if let Some(heading) = next.strip_prefix("notes#") { + self.open_channel_notes + .push((channel_id, Some(heading.to_string()))); + return Ok(()); + } + if next == "notes" { + self.open_channel_notes.push((channel_id, None)); + return Ok(()); } } anyhow::bail!("invalid zed url: {request_path}") @@ -181,10 +179,10 @@ pub fn listen_for_cli_connections(opener: OpenListener) -> Result<()> { let sock_path = paths::data_dir().join(format!("zed-{}.sock", *RELEASE_CHANNEL_NAME)); // remove the socket if the process listening on it has died - if let Err(e) = UnixDatagram::unbound()?.connect(&sock_path) { - if e.kind() == std::io::ErrorKind::ConnectionRefused { - std::fs::remove_file(&sock_path)?; - } + if let Err(e) = UnixDatagram::unbound()?.connect(&sock_path) + && e.kind() == std::io::ErrorKind::ConnectionRefused + { + std::fs::remove_file(&sock_path)?; } let listener = UnixDatagram::bind(&sock_path)?; thread::spawn(move || { @@ -244,12 +242,12 @@ pub async fn open_paths_with_positions( .iter() .map(|path_with_position| { let path = path_with_position.path.clone(); - if let Some(row) = path_with_position.row { - if path.is_file() { - let row = row.saturating_sub(1); - let col = path_with_position.column.unwrap_or(0).saturating_sub(1); - caret_positions.insert(path.clone(), Point::new(row, col)); - } + if let Some(row) = path_with_position.row + && path.is_file() + { + let row = row.saturating_sub(1); + let col = path_with_position.column.unwrap_or(0).saturating_sub(1); + caret_positions.insert(path.clone(), Point::new(row, col)); } path }) @@ -264,10 +262,9 @@ pub async fn open_paths_with_positions( let new_path = Path::new(&diff_pair[1]).canonicalize()?; if let Ok(diff_view) = workspace.update(cx, |workspace, window, cx| { FileDiffView::open(old_path, new_path, workspace, window, cx) - }) { - if let Some(diff_view) = diff_view.await.log_err() { - items.push(Some(Ok(Box::new(diff_view)))) - } + }) && let Some(diff_view) = diff_view.await.log_err() + { + items.push(Some(Ok(Box::new(diff_view)))) } } diff --git a/crates/zeta/src/rate_completion_modal.rs b/crates/zeta/src/rate_completion_modal.rs index ac7fcade91..313e4c3779 100644 --- a/crates/zeta/src/rate_completion_modal.rs +++ b/crates/zeta/src/rate_completion_modal.rs @@ -267,13 +267,13 @@ impl RateCompletionModal { .unwrap_or(self.selected_index); cx.notify(); - if let Some(prev_completion) = self.active_completion.as_ref() { - if completion.id == prev_completion.completion.id { - if focus { - window.focus(&prev_completion.feedback_editor.focus_handle(cx)); - } - return; + if let Some(prev_completion) = self.active_completion.as_ref() + && completion.id == prev_completion.completion.id + { + if focus { + window.focus(&prev_completion.feedback_editor.focus_handle(cx)); } + return; } } diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 956e416fe9..2a121c407c 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -836,12 +836,11 @@ and then another .headers() .get(MINIMUM_REQUIRED_VERSION_HEADER_NAME) .and_then(|version| SemanticVersion::from_str(version.to_str().ok()?).ok()) + && app_version < minimum_required_version { - if app_version < minimum_required_version { - return Err(anyhow!(ZedUpdateRequiredError { - minimum_version: minimum_required_version - })); - } + return Err(anyhow!(ZedUpdateRequiredError { + minimum_version: minimum_required_version + })); } if response.status().is_success() { diff --git a/crates/zlog/src/sink.rs b/crates/zlog/src/sink.rs index 17aa08026e..3ac85d4bbf 100644 --- a/crates/zlog/src/sink.rs +++ b/crates/zlog/src/sink.rs @@ -194,10 +194,10 @@ pub fn flush() { ENABLED_SINKS_FILE.clear_poison(); handle.into_inner() }); - if let Some(file) = file.as_mut() { - if let Err(err) = file.flush() { - eprintln!("Failed to flush log file: {}", err); - } + if let Some(file) = file.as_mut() + && let Err(err) = file.flush() + { + eprintln!("Failed to flush log file: {}", err); } } diff --git a/crates/zlog/src/zlog.rs b/crates/zlog/src/zlog.rs index 5b40278f3f..df3a210231 100644 --- a/crates/zlog/src/zlog.rs +++ b/crates/zlog/src/zlog.rs @@ -28,10 +28,8 @@ pub fn try_init() -> anyhow::Result<()> { } pub fn init_test() { - if get_env_config().is_some() { - if try_init().is_ok() { - init_output_stdout(); - } + if get_env_config().is_some() && try_init().is_ok() { + init_output_stdout(); } } @@ -344,18 +342,18 @@ impl Timer { return; } let elapsed = self.start_time.elapsed(); - if let Some(warn_limit) = self.warn_if_longer_than { - if elapsed > warn_limit { - crate::warn!( - self.logger => - "Timer '{}' took {:?}. Which was longer than the expected limit of {:?}", - self.name, - elapsed, - warn_limit - ); - self.done = true; - return; - } + if let Some(warn_limit) = self.warn_if_longer_than + && elapsed > warn_limit + { + crate::warn!( + self.logger => + "Timer '{}' took {:?}. Which was longer than the expected limit of {:?}", + self.name, + elapsed, + warn_limit + ); + self.done = true; + return; } crate::trace!( self.logger => diff --git a/extensions/glsl/src/glsl.rs b/extensions/glsl/src/glsl.rs index a42403ebef..ba506d2b11 100644 --- a/extensions/glsl/src/glsl.rs +++ b/extensions/glsl/src/glsl.rs @@ -16,10 +16,10 @@ impl GlslExtension { return Ok(path); } - if let Some(path) = &self.cached_binary_path { - if fs::metadata(path).map_or(false, |stat| stat.is_file()) { - return Ok(path.clone()); - } + if let Some(path) = &self.cached_binary_path + && fs::metadata(path).map_or(false, |stat| stat.is_file()) + { + return Ok(path.clone()); } zed::set_language_server_installation_status( diff --git a/extensions/ruff/src/ruff.rs b/extensions/ruff/src/ruff.rs index da9b6c0bf1..7b811db212 100644 --- a/extensions/ruff/src/ruff.rs +++ b/extensions/ruff/src/ruff.rs @@ -38,13 +38,13 @@ impl RuffExtension { }); } - if let Some(path) = &self.cached_binary_path { - if fs::metadata(path).map_or(false, |stat| stat.is_file()) { - return Ok(RuffBinary { - path: path.clone(), - args: binary_args, - }); - } + if let Some(path) = &self.cached_binary_path + && fs::metadata(path).map_or(false, |stat| stat.is_file()) + { + return Ok(RuffBinary { + path: path.clone(), + args: binary_args, + }); } zed::set_language_server_installation_status( diff --git a/extensions/snippets/src/snippets.rs b/extensions/snippets/src/snippets.rs index 46ba746930..682709a28a 100644 --- a/extensions/snippets/src/snippets.rs +++ b/extensions/snippets/src/snippets.rs @@ -17,10 +17,10 @@ impl SnippetExtension { return Ok(path); } - if let Some(path) = &self.cached_binary_path { - if fs::metadata(path).map_or(false, |stat| stat.is_file()) { - return Ok(path.clone()); - } + if let Some(path) = &self.cached_binary_path + && fs::metadata(path).map_or(false, |stat| stat.is_file()) + { + return Ok(path.clone()); } zed::set_language_server_installation_status( diff --git a/extensions/test-extension/src/test_extension.rs b/extensions/test-extension/src/test_extension.rs index 5b6a3f920a..0ef522bd51 100644 --- a/extensions/test-extension/src/test_extension.rs +++ b/extensions/test-extension/src/test_extension.rs @@ -18,10 +18,10 @@ impl TestExtension { println!("{}", String::from_utf8_lossy(&echo_output.stdout)); - if let Some(path) = &self.cached_binary_path { - if fs::metadata(path).map_or(false, |stat| stat.is_file()) { - return Ok(path.clone()); - } + if let Some(path) = &self.cached_binary_path + && fs::metadata(path).map_or(false, |stat| stat.is_file()) + { + return Ok(path.clone()); } zed::set_language_server_installation_status( diff --git a/extensions/toml/src/toml.rs b/extensions/toml/src/toml.rs index 20f27b6d97..30a2cd6ce3 100644 --- a/extensions/toml/src/toml.rs +++ b/extensions/toml/src/toml.rs @@ -39,13 +39,13 @@ impl TomlExtension { }); } - if let Some(path) = &self.cached_binary_path { - if fs::metadata(path).map_or(false, |stat| stat.is_file()) { - return Ok(TaploBinary { - path: path.clone(), - args: binary_args, - }); - } + if let Some(path) = &self.cached_binary_path + && fs::metadata(path).map_or(false, |stat| stat.is_file()) + { + return Ok(TaploBinary { + path: path.clone(), + args: binary_args, + }); } zed::set_language_server_installation_status( From e3b593efbdfab2609a44ce3dee14be143d341155 Mon Sep 17 00:00:00 2001 From: Smit Barmase <heysmitbarmase@gmail.com> Date: Tue, 19 Aug 2025 19:04:48 +0530 Subject: [PATCH 480/693] project: Take 2 on Handle textDocument/didSave and textDocument/didChange (un)registration and usage correctly (#36485) Relands https://github.com/zed-industries/zed/pull/36441 with a deserialization fix. Previously, deserializing `"includeText"` into `lsp::TextDocumentSyncSaveOptions` resulted in a `Supported(false)` type instead of `SaveOptions(SaveOptions { include_text: Option<bool> })`. ```rs impl From<bool> for TextDocumentSyncSaveOptions { fn from(from: bool) -> Self { Self::Supported(from) } } ``` Looks like, while dynamic registartion we only get `SaveOptions` type and never `Supported` type. (https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentSaveRegistrationOptions) Release Notes: - N/A --------- Co-authored-by: Lukas Wirth <lukas@zed.dev> --- crates/project/src/lsp_store.rs | 88 ++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 18 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 23061149bf..75609b3187 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -74,8 +74,8 @@ use lsp::{ FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, LanguageServerName, LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, - OneOf, RenameFilesParams, SymbolKind, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams, - WorkspaceFolder, notification::DidRenameFiles, + OneOf, RenameFilesParams, SymbolKind, TextDocumentSyncSaveOptions, TextEdit, WillRenameFiles, + WorkDoneProgressCancelParams, WorkspaceFolder, notification::DidRenameFiles, }; use node_runtime::read_package_installed_version; use parking_lot::Mutex; @@ -11800,8 +11800,40 @@ impl LspStore { .transpose()? { server.update_capabilities(|capabilities| { + let mut sync_options = + Self::take_text_document_sync_options(capabilities); + sync_options.change = Some(sync_kind); capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)); + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + }); + notify_server_capabilities_updated(&server, cx); + } + } + "textDocument/didSave" => { + if let Some(include_text) = reg + .register_options + .map(|opts| { + let transpose = opts + .get("includeText") + .cloned() + .map(serde_json::from_value::<Option<bool>>) + .transpose(); + match transpose { + Ok(value) => Ok(value.flatten()), + Err(e) => Err(e), + } + }) + .transpose()? + { + server.update_capabilities(|capabilities| { + let mut sync_options = + Self::take_text_document_sync_options(capabilities); + sync_options.save = + Some(TextDocumentSyncSaveOptions::SaveOptions(lsp::SaveOptions { + include_text, + })); + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); }); notify_server_capabilities_updated(&server, cx); } @@ -11953,7 +11985,19 @@ impl LspStore { } "textDocument/didChange" => { server.update_capabilities(|capabilities| { - capabilities.text_document_sync = None; + let mut sync_options = Self::take_text_document_sync_options(capabilities); + sync_options.change = None; + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/didSave" => { + server.update_capabilities(|capabilities| { + let mut sync_options = Self::take_text_document_sync_options(capabilities); + sync_options.save = None; + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); }); notify_server_capabilities_updated(&server, cx); } @@ -11981,6 +12025,20 @@ impl LspStore { Ok(()) } + + fn take_text_document_sync_options( + capabilities: &mut lsp::ServerCapabilities, + ) -> lsp::TextDocumentSyncOptions { + match capabilities.text_document_sync.take() { + Some(lsp::TextDocumentSyncCapability::Options(sync_options)) => sync_options, + Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)) => { + let mut sync_options = lsp::TextDocumentSyncOptions::default(); + sync_options.change = Some(sync_kind); + sync_options + } + None => lsp::TextDocumentSyncOptions::default(), + } + } } // Registration with empty capabilities should be ignored. @@ -13083,24 +13141,18 @@ async fn populate_labels_for_symbols( fn include_text(server: &lsp::LanguageServer) -> Option<bool> { match server.capabilities().text_document_sync.as_ref()? { - lsp::TextDocumentSyncCapability::Kind(kind) => match *kind { - lsp::TextDocumentSyncKind::NONE => None, - lsp::TextDocumentSyncKind::FULL => Some(true), - lsp::TextDocumentSyncKind::INCREMENTAL => Some(false), - _ => None, - }, - lsp::TextDocumentSyncCapability::Options(options) => match options.save.as_ref()? { - lsp::TextDocumentSyncSaveOptions::Supported(supported) => { - if *supported { - Some(true) - } else { - None - } - } + lsp::TextDocumentSyncCapability::Options(opts) => match opts.save.as_ref()? { + // Server wants didSave but didn't specify includeText. + lsp::TextDocumentSyncSaveOptions::Supported(true) => Some(false), + // Server doesn't want didSave at all. + lsp::TextDocumentSyncSaveOptions::Supported(false) => None, + // Server provided SaveOptions. lsp::TextDocumentSyncSaveOptions::SaveOptions(save_options) => { Some(save_options.include_text.unwrap_or(false)) } }, + // We do not have any save info. Kind affects didChange only. + lsp::TextDocumentSyncCapability::Kind(_) => None, } } From c4083b9b63efddd093126a2b613e44ceb9e7e505 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 19 Aug 2025 16:20:01 +0200 Subject: [PATCH 481/693] Fix unnecessary-mut-passed lint (#36490) Release Notes: - N/A --- Cargo.toml | 1 + crates/agent_ui/src/acp/message_editor.rs | 2 +- crates/assistant_context/src/context_store.rs | 2 +- crates/auto_update/src/auto_update.rs | 2 +- crates/call/src/call_impl/mod.rs | 2 +- crates/channel/src/channel_buffer.rs | 4 +- crates/channel/src/channel_chat.rs | 4 +- crates/channel/src/channel_store.rs | 4 +- crates/client/src/client.rs | 10 ++-- crates/client/src/user.rs | 4 +- crates/editor/src/hover_links.rs | 8 ++-- crates/gpui/src/app.rs | 6 +-- crates/gpui/src/app/context.rs | 2 +- crates/gpui/src/app/test_context.rs | 6 +-- crates/project/src/buffer_store.rs | 6 +-- .../project/src/debugger/breakpoint_store.rs | 2 +- crates/project/src/lsp_command.rs | 46 +++++++++---------- crates/project/src/lsp_store.rs | 24 +++++----- .../project/src/lsp_store/lsp_ext_command.rs | 12 ++--- crates/project/src/project.rs | 32 ++++++------- crates/project/src/search_history.rs | 2 +- crates/project/src/task_store.rs | 2 +- crates/remote_server/src/headless_project.rs | 8 ++-- crates/remote_server/src/unix.rs | 2 +- crates/search/src/buffer_search.rs | 2 +- crates/search/src/project_search.rs | 2 +- .../src/ui_components/keystroke_input.rs | 4 +- crates/snippet_provider/src/lib.rs | 6 +-- 28 files changed, 103 insertions(+), 104 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 89aadbcba0..603897084c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -832,6 +832,7 @@ redundant_closure = { level = "deny" } declare_interior_mutable_const = { level = "deny" } collapsible_if = { level = "warn"} needless_borrow = { level = "warn"} +unnecessary_mut_passed = {level = "warn"} # Individual rules that have violations in the codebase: type_complexity = "allow" # We often return trait objects from `new` functions. diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index a32d0ce6ce..00368d6087 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -2173,7 +2173,7 @@ mod tests { cx.run_until_parked(); - editor.read_with(&mut cx, |editor, cx| { + editor.read_with(&cx, |editor, cx| { assert_eq!( editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) [@MySymbol](file:///dir/a/one.txt?symbol=MySymbol#L1:1) " diff --git a/crates/assistant_context/src/context_store.rs b/crates/assistant_context/src/context_store.rs index af43b912e9..a2b3adc686 100644 --- a/crates/assistant_context/src/context_store.rs +++ b/crates/assistant_context/src/context_store.rs @@ -320,7 +320,7 @@ impl ContextStore { .client .subscribe_to_entity(remote_id) .log_err() - .map(|subscription| subscription.set_entity(&cx.entity(), &mut cx.to_async())); + .map(|subscription| subscription.set_entity(&cx.entity(), &cx.to_async())); self.advertise_contexts(cx); } else { self.client_subscription = None; diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 4d0d2d5984..2150873cad 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -543,7 +543,7 @@ impl AutoUpdater { async fn update(this: Entity<Self>, mut cx: AsyncApp) -> Result<()> { let (client, installed_version, previous_status, release_channel) = - this.read_with(&mut cx, |this, cx| { + this.read_with(&cx, |this, cx| { ( this.http_client.clone(), this.current_version, diff --git a/crates/call/src/call_impl/mod.rs b/crates/call/src/call_impl/mod.rs index 71c3149324..6cc94a5dd5 100644 --- a/crates/call/src/call_impl/mod.rs +++ b/crates/call/src/call_impl/mod.rs @@ -116,7 +116,7 @@ impl ActiveCall { envelope: TypedEnvelope<proto::IncomingCall>, mut cx: AsyncApp, ) -> Result<proto::Ack> { - let user_store = this.read_with(&mut cx, |this, _| this.user_store.clone())?; + let user_store = this.read_with(&cx, |this, _| this.user_store.clone())?; let call = IncomingCall { room_id: envelope.payload.room_id, participants: user_store diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index a367ffbf09..943e819ad6 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -82,7 +82,7 @@ impl ChannelBuffer { collaborators: Default::default(), acknowledge_task: None, channel_id: channel.id, - subscription: Some(subscription.set_entity(&cx.entity(), &mut cx.to_async())), + subscription: Some(subscription.set_entity(&cx.entity(), &cx.to_async())), user_store, channel_store, }; @@ -110,7 +110,7 @@ impl ChannelBuffer { let Ok(subscription) = self.client.subscribe_to_entity(self.channel_id.0) else { return; }; - self.subscription = Some(subscription.set_entity(&cx.entity(), &mut cx.to_async())); + self.subscription = Some(subscription.set_entity(&cx.entity(), &cx.to_async())); cx.emit(ChannelBufferEvent::Connected); } } diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index 02b5ccec68..86f307717c 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -532,7 +532,7 @@ impl ChannelChat { message: TypedEnvelope<proto::ChannelMessageSent>, mut cx: AsyncApp, ) -> Result<()> { - let user_store = this.read_with(&mut cx, |this, _| this.user_store.clone())?; + let user_store = this.read_with(&cx, |this, _| this.user_store.clone())?; let message = message.payload.message.context("empty message")?; let message_id = message.id; @@ -564,7 +564,7 @@ impl ChannelChat { message: TypedEnvelope<proto::ChannelMessageUpdate>, mut cx: AsyncApp, ) -> Result<()> { - let user_store = this.read_with(&mut cx, |this, _| this.user_store.clone())?; + let user_store = this.read_with(&cx, |this, _| this.user_store.clone())?; let message = message.payload.message.context("empty message")?; let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?; diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 6d1716a7ea..42a1851408 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -908,9 +908,9 @@ impl ChannelStore { async fn handle_update_channels( this: Entity<Self>, message: TypedEnvelope<proto::UpdateChannels>, - mut cx: AsyncApp, + cx: AsyncApp, ) -> Result<()> { - this.read_with(&mut cx, |this, _| { + this.read_with(&cx, |this, _| { this.update_channels_tx .unbounded_send(message.payload) .unwrap(); diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index d7d8b60211..218cf2b079 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -2073,8 +2073,8 @@ mod tests { let (done_tx1, done_rx1) = smol::channel::unbounded(); let (done_tx2, done_rx2) = smol::channel::unbounded(); AnyProtoClient::from(client.clone()).add_entity_message_handler( - move |entity: Entity<TestEntity>, _: TypedEnvelope<proto::JoinProject>, mut cx| { - match entity.read_with(&mut cx, |entity, _| entity.id).unwrap() { + move |entity: Entity<TestEntity>, _: TypedEnvelope<proto::JoinProject>, cx| { + match entity.read_with(&cx, |entity, _| entity.id).unwrap() { 1 => done_tx1.try_send(()).unwrap(), 2 => done_tx2.try_send(()).unwrap(), _ => unreachable!(), @@ -2098,17 +2098,17 @@ mod tests { let _subscription1 = client .subscribe_to_entity(1) .unwrap() - .set_entity(&entity1, &mut cx.to_async()); + .set_entity(&entity1, &cx.to_async()); let _subscription2 = client .subscribe_to_entity(2) .unwrap() - .set_entity(&entity2, &mut cx.to_async()); + .set_entity(&entity2, &cx.to_async()); // Ensure dropping a subscription for the same entity type still allows receiving of // messages for other entity IDs of the same type. let subscription3 = client .subscribe_to_entity(3) .unwrap() - .set_entity(&entity3, &mut cx.to_async()); + .set_entity(&entity3, &cx.to_async()); drop(subscription3); server.send(proto::JoinProject { diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 3509a8c57f..722d6861ff 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -332,9 +332,9 @@ impl UserStore { async fn handle_update_contacts( this: Entity<Self>, message: TypedEnvelope<proto::UpdateContacts>, - mut cx: AsyncApp, + cx: AsyncApp, ) -> Result<()> { - this.read_with(&mut cx, |this, _| { + this.read_with(&cx, |this, _| { this.update_contacts_tx .unbounded_send(UpdateContacts::Update(message.payload)) .unwrap(); diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index b431834d35..358d8683fe 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -655,11 +655,11 @@ pub fn show_link_definition( pub(crate) fn find_url( buffer: &Entity<language::Buffer>, position: text::Anchor, - mut cx: AsyncWindowContext, + cx: AsyncWindowContext, ) -> Option<(Range<text::Anchor>, String)> { const LIMIT: usize = 2048; - let Ok(snapshot) = buffer.read_with(&mut cx, |buffer, _| buffer.snapshot()) else { + let Ok(snapshot) = buffer.read_with(&cx, |buffer, _| buffer.snapshot()) else { return None; }; @@ -717,11 +717,11 @@ pub(crate) fn find_url( pub(crate) fn find_url_from_range( buffer: &Entity<language::Buffer>, range: Range<text::Anchor>, - mut cx: AsyncWindowContext, + cx: AsyncWindowContext, ) -> Option<String> { const LIMIT: usize = 2048; - let Ok(snapshot) = buffer.read_with(&mut cx, |buffer, _| buffer.snapshot()) else { + let Ok(snapshot) = buffer.read_with(&cx, |buffer, _| buffer.snapshot()) else { return None; }; diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index c4499aff07..2be1a34e49 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -368,7 +368,7 @@ impl App { }), }); - init_app_menus(platform.as_ref(), &mut app.borrow_mut()); + init_app_menus(platform.as_ref(), &app.borrow()); platform.on_keyboard_layout_change(Box::new({ let app = Rc::downgrade(&app); @@ -1332,7 +1332,7 @@ impl App { } inner( - &mut self.keystroke_observers, + &self.keystroke_observers, Box::new(move |event, window, cx| { f(event, window, cx); true @@ -1358,7 +1358,7 @@ impl App { } inner( - &mut self.keystroke_interceptors, + &self.keystroke_interceptors, Box::new(move |event, window, cx| { f(event, window, cx); true diff --git a/crates/gpui/src/app/context.rs b/crates/gpui/src/app/context.rs index a6ab026770..1112878a66 100644 --- a/crates/gpui/src/app/context.rs +++ b/crates/gpui/src/app/context.rs @@ -472,7 +472,7 @@ impl<'a, T: 'static> Context<'a, T> { let view = self.weak_entity(); inner( - &mut self.keystroke_observers, + &self.keystroke_observers, Box::new(move |event, window, cx| { if let Some(view) = view.upgrade() { view.update(cx, |view, cx| f(view, event, window, cx)); diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index a96c24432a..43adacf7dd 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -219,7 +219,7 @@ impl TestAppContext { let mut cx = self.app.borrow_mut(); // Some tests rely on the window size matching the bounds of the test display - let bounds = Bounds::maximized(None, &mut cx); + let bounds = Bounds::maximized(None, &cx); cx.open_window( WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), @@ -233,7 +233,7 @@ impl TestAppContext { /// Adds a new window with no content. pub fn add_empty_window(&mut self) -> &mut VisualTestContext { let mut cx = self.app.borrow_mut(); - let bounds = Bounds::maximized(None, &mut cx); + let bounds = Bounds::maximized(None, &cx); let window = cx .open_window( WindowOptions { @@ -261,7 +261,7 @@ impl TestAppContext { V: 'static + Render, { let mut cx = self.app.borrow_mut(); - let bounds = Bounds::maximized(None, &mut cx); + let bounds = Bounds::maximized(None, &cx); let window = cx .open_window( WindowOptions { diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 1522376e9a..96e87b1fe0 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -1345,7 +1345,7 @@ impl BufferStore { mut cx: AsyncApp, ) -> Result<proto::BufferSaved> { let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let (buffer, project_id) = this.read_with(&mut cx, |this, _| { + let (buffer, project_id) = this.read_with(&cx, |this, _| { anyhow::Ok(( this.get_existing(buffer_id)?, this.downstream_client @@ -1359,7 +1359,7 @@ impl BufferStore { buffer.wait_for_version(deserialize_version(&envelope.payload.version)) })? .await?; - let buffer_id = buffer.read_with(&mut cx, |buffer, _| buffer.remote_id())?; + let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id())?; if let Some(new_path) = envelope.payload.new_path { let new_path = ProjectPath::from_proto(new_path); @@ -1372,7 +1372,7 @@ impl BufferStore { .await?; } - buffer.read_with(&mut cx, |buffer, _| proto::BufferSaved { + buffer.read_with(&cx, |buffer, _| proto::BufferSaved { project_id, buffer_id: buffer_id.into(), version: serialize_version(buffer.saved_version()), diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index faa9948596..38d8b4cfc6 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -267,7 +267,7 @@ impl BreakpointStore { message: TypedEnvelope<proto::ToggleBreakpoint>, mut cx: AsyncApp, ) -> Result<proto::Ack> { - let breakpoints = this.read_with(&mut cx, |this, _| this.breakpoint_store())?; + let breakpoints = this.read_with(&cx, |this, _| this.breakpoint_store())?; let path = this .update(&mut cx, |this, cx| { this.project_path_for_absolute_path(message.payload.path.as_ref(), cx) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 2a1facd3c0..64414c6545 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -332,9 +332,9 @@ impl LspCommand for PrepareRename { _: Entity<LspStore>, buffer: Entity<Buffer>, _: LanguageServerId, - mut cx: AsyncApp, + cx: AsyncApp, ) -> Result<PrepareRenameResponse> { - buffer.read_with(&mut cx, |buffer, _| match message { + buffer.read_with(&cx, |buffer, _| match message { Some(lsp::PrepareRenameResponse::Range(range)) | Some(lsp::PrepareRenameResponse::RangeWithPlaceholder { range, .. }) => { let Range { start, end } = range_from_lsp(range); @@ -386,7 +386,7 @@ impl LspCommand for PrepareRename { .await?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -543,7 +543,7 @@ impl LspCommand for PerformRename { })? .await?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, new_name: message.new_name, push_to_history: false, }) @@ -658,7 +658,7 @@ impl LspCommand for GetDefinitions { })? .await?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -761,7 +761,7 @@ impl LspCommand for GetDeclarations { })? .await?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -863,7 +863,7 @@ impl LspCommand for GetImplementations { })? .await?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -962,7 +962,7 @@ impl LspCommand for GetTypeDefinitions { })? .await?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -1330,7 +1330,7 @@ impl LspCommand for GetReferences { target_buffer_handle .clone() - .read_with(&mut cx, |target_buffer, _| { + .read_with(&cx, |target_buffer, _| { let target_start = target_buffer .clip_point_utf16(point_from_lsp(lsp_location.range.start), Bias::Left); let target_end = target_buffer @@ -1374,7 +1374,7 @@ impl LspCommand for GetReferences { })? .await?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -1484,9 +1484,9 @@ impl LspCommand for GetDocumentHighlights { _: Entity<LspStore>, buffer: Entity<Buffer>, _: LanguageServerId, - mut cx: AsyncApp, + cx: AsyncApp, ) -> Result<Vec<DocumentHighlight>> { - buffer.read_with(&mut cx, |buffer, _| { + buffer.read_with(&cx, |buffer, _| { let mut lsp_highlights = lsp_highlights.unwrap_or_default(); lsp_highlights.sort_unstable_by_key(|h| (h.range.start, Reverse(h.range.end))); lsp_highlights @@ -1534,7 +1534,7 @@ impl LspCommand for GetDocumentHighlights { })? .await?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -1865,7 +1865,7 @@ impl LspCommand for GetSignatureHelp { })? .await .with_context(|| format!("waiting for version for buffer {}", buffer.entity_id()))?; - let buffer_snapshot = buffer.read_with(&mut cx, |buffer, _| buffer.snapshot())?; + let buffer_snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot())?; Ok(Self { position: payload .position @@ -1947,13 +1947,13 @@ impl LspCommand for GetHover { _: Entity<LspStore>, buffer: Entity<Buffer>, _: LanguageServerId, - mut cx: AsyncApp, + cx: AsyncApp, ) -> Result<Self::Response> { let Some(hover) = message else { return Ok(None); }; - let (language, range) = buffer.read_with(&mut cx, |buffer, _| { + let (language, range) = buffer.read_with(&cx, |buffer, _| { ( buffer.language().cloned(), hover.range.map(|range| { @@ -2039,7 +2039,7 @@ impl LspCommand for GetHover { })? .await?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -2113,7 +2113,7 @@ impl LspCommand for GetHover { return Ok(None); } - let language = buffer.read_with(&mut cx, |buffer, _| buffer.language().cloned())?; + let language = buffer.read_with(&cx, |buffer, _| buffer.language().cloned())?; let range = if let (Some(start), Some(end)) = (message.start, message.end) { language::proto::deserialize_anchor(start) .and_then(|start| language::proto::deserialize_anchor(end).map(|end| start..end)) @@ -2208,7 +2208,7 @@ impl LspCommand for GetCompletions { let unfiltered_completions_count = completions.len(); let language_server_adapter = lsp_store - .read_with(&mut cx, |lsp_store, _| { + .read_with(&cx, |lsp_store, _| { lsp_store.language_server_adapter_for_id(server_id) })? .with_context(|| format!("no language server with id {server_id}"))?; @@ -2394,7 +2394,7 @@ impl LspCommand for GetCompletions { .position .and_then(language::proto::deserialize_anchor) .map(|p| { - buffer.read_with(&mut cx, |buffer, _| { + buffer.read_with(&cx, |buffer, _| { buffer.clip_point_utf16(Unclipped(p.to_point_utf16(buffer)), Bias::Left) }) }) @@ -2862,7 +2862,7 @@ impl LspCommand for OnTypeFormatting { })?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, trigger: message.trigger.clone(), options, push_to_history: false, @@ -3474,9 +3474,9 @@ impl LspCommand for GetCodeLens { lsp_store: Entity<LspStore>, buffer: Entity<Buffer>, server_id: LanguageServerId, - mut cx: AsyncApp, + cx: AsyncApp, ) -> anyhow::Result<Vec<CodeAction>> { - let snapshot = buffer.read_with(&mut cx, |buffer, _| buffer.snapshot())?; + let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot())?; let language_server = cx.update(|cx| { lsp_store .read(cx) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 75609b3187..e93e859dcf 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -669,10 +669,10 @@ impl LocalLspStore { let this = this.clone(); move |_, cx| { let this = this.clone(); - let mut cx = cx.clone(); + let cx = cx.clone(); async move { - let Some(server) = this - .read_with(&mut cx, |this, _| this.language_server_for_id(server_id))? + let Some(server) = + this.read_with(&cx, |this, _| this.language_server_for_id(server_id))? else { return Ok(None); }; @@ -8154,7 +8154,7 @@ impl LspStore { envelope: TypedEnvelope<proto::MultiLspQuery>, mut cx: AsyncApp, ) -> Result<proto::MultiLspQueryResponse> { - let response_from_ssh = lsp_store.read_with(&mut cx, |this, _| { + let response_from_ssh = lsp_store.read_with(&cx, |this, _| { let (upstream_client, project_id) = this.upstream_client()?; let mut payload = envelope.payload.clone(); payload.project_id = project_id; @@ -8176,7 +8176,7 @@ impl LspStore { buffer.wait_for_version(version.clone()) })? .await?; - let buffer_version = buffer.read_with(&mut cx, |buffer, _| buffer.version())?; + let buffer_version = buffer.read_with(&cx, |buffer, _| buffer.version())?; match envelope .payload .strategy @@ -8717,7 +8717,7 @@ impl LspStore { })? .context("worktree not found")?; let (old_abs_path, new_abs_path) = { - let root_path = worktree.read_with(&mut cx, |this, _| this.abs_path())?; + let root_path = worktree.read_with(&cx, |this, _| this.abs_path())?; let new_path = PathBuf::from_proto(envelope.payload.new_path.clone()); (root_path.join(&old_path), root_path.join(&new_path)) }; @@ -8732,7 +8732,7 @@ impl LspStore { ) .await; let response = Worktree::handle_rename_entry(worktree, envelope.payload, cx.clone()).await; - this.read_with(&mut cx, |this, _| { + this.read_with(&cx, |this, _| { this.did_rename_entry(worktree_id, &old_abs_path, &new_abs_path, is_dir); }) .ok(); @@ -8966,10 +8966,10 @@ impl LspStore { async fn handle_lsp_ext_cancel_flycheck( lsp_store: Entity<Self>, envelope: TypedEnvelope<proto::LspExtCancelFlycheck>, - mut cx: AsyncApp, + cx: AsyncApp, ) -> Result<proto::Ack> { let server_id = LanguageServerId(envelope.payload.language_server_id as usize); - lsp_store.read_with(&mut cx, |lsp_store, _| { + lsp_store.read_with(&cx, |lsp_store, _| { if let Some(server) = lsp_store.language_server_for_id(server_id) { server .notify::<lsp_store::lsp_ext_command::LspExtCancelFlycheck>(&()) @@ -9018,10 +9018,10 @@ impl LspStore { async fn handle_lsp_ext_clear_flycheck( lsp_store: Entity<Self>, envelope: TypedEnvelope<proto::LspExtClearFlycheck>, - mut cx: AsyncApp, + cx: AsyncApp, ) -> Result<proto::Ack> { let server_id = LanguageServerId(envelope.payload.language_server_id as usize); - lsp_store.read_with(&mut cx, |lsp_store, _| { + lsp_store.read_with(&cx, |lsp_store, _| { if let Some(server) = lsp_store.language_server_for_id(server_id) { server .notify::<lsp_store::lsp_ext_command::LspExtClearFlycheck>(&()) @@ -9789,7 +9789,7 @@ impl LspStore { let peer_id = envelope.original_sender_id().unwrap_or_default(); let symbol = envelope.payload.symbol.context("invalid symbol")?; let symbol = Self::deserialize_symbol(symbol)?; - let symbol = this.read_with(&mut cx, |this, _| { + let symbol = this.read_with(&cx, |this, _| { let signature = this.symbol_signature(&symbol.path); anyhow::ensure!(signature == symbol.signature, "invalid symbol signature"); Ok(symbol) diff --git a/crates/project/src/lsp_store/lsp_ext_command.rs b/crates/project/src/lsp_store/lsp_ext_command.rs index cb13fa5efc..1c969f8114 100644 --- a/crates/project/src/lsp_store/lsp_ext_command.rs +++ b/crates/project/src/lsp_store/lsp_ext_command.rs @@ -115,14 +115,14 @@ impl LspCommand for ExpandMacro { message: Self::ProtoRequest, _: Entity<LspStore>, buffer: Entity<Buffer>, - mut cx: AsyncApp, + cx: AsyncApp, ) -> anyhow::Result<Self> { let position = message .position .and_then(deserialize_anchor) .context("invalid position")?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -249,14 +249,14 @@ impl LspCommand for OpenDocs { message: Self::ProtoRequest, _: Entity<LspStore>, buffer: Entity<Buffer>, - mut cx: AsyncApp, + cx: AsyncApp, ) -> anyhow::Result<Self> { let position = message .position .and_then(deserialize_anchor) .context("invalid position")?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -462,14 +462,14 @@ impl LspCommand for GoToParentModule { request: Self::ProtoRequest, _: Entity<LspStore>, buffer: Entity<Buffer>, - mut cx: AsyncApp, + cx: AsyncApp, ) -> anyhow::Result<Self> { let position = request .position .and_then(deserialize_anchor) .context("bad request with bad position")?; Ok(Self { - position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, }) } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 3906befee2..f825cd8c47 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1613,25 +1613,23 @@ impl Project { .into_iter() .map(|s| match s { EntitySubscription::BufferStore(subscription) => { - subscription.set_entity(&buffer_store, &mut cx) + subscription.set_entity(&buffer_store, &cx) } EntitySubscription::WorktreeStore(subscription) => { - subscription.set_entity(&worktree_store, &mut cx) + subscription.set_entity(&worktree_store, &cx) } EntitySubscription::GitStore(subscription) => { - subscription.set_entity(&git_store, &mut cx) + subscription.set_entity(&git_store, &cx) } EntitySubscription::SettingsObserver(subscription) => { - subscription.set_entity(&settings_observer, &mut cx) - } - EntitySubscription::Project(subscription) => { - subscription.set_entity(&this, &mut cx) + subscription.set_entity(&settings_observer, &cx) } + EntitySubscription::Project(subscription) => subscription.set_entity(&this, &cx), EntitySubscription::LspStore(subscription) => { - subscription.set_entity(&lsp_store, &mut cx) + subscription.set_entity(&lsp_store, &cx) } EntitySubscription::DapStore(subscription) => { - subscription.set_entity(&dap_store, &mut cx) + subscription.set_entity(&dap_store, &cx) } }) .collect::<Vec<_>>(); @@ -2226,28 +2224,28 @@ impl Project { self.client_subscriptions.extend([ self.client .subscribe_to_entity(project_id)? - .set_entity(&cx.entity(), &mut cx.to_async()), + .set_entity(&cx.entity(), &cx.to_async()), self.client .subscribe_to_entity(project_id)? - .set_entity(&self.worktree_store, &mut cx.to_async()), + .set_entity(&self.worktree_store, &cx.to_async()), self.client .subscribe_to_entity(project_id)? - .set_entity(&self.buffer_store, &mut cx.to_async()), + .set_entity(&self.buffer_store, &cx.to_async()), self.client .subscribe_to_entity(project_id)? - .set_entity(&self.lsp_store, &mut cx.to_async()), + .set_entity(&self.lsp_store, &cx.to_async()), self.client .subscribe_to_entity(project_id)? - .set_entity(&self.settings_observer, &mut cx.to_async()), + .set_entity(&self.settings_observer, &cx.to_async()), self.client .subscribe_to_entity(project_id)? - .set_entity(&self.dap_store, &mut cx.to_async()), + .set_entity(&self.dap_store, &cx.to_async()), self.client .subscribe_to_entity(project_id)? - .set_entity(&self.breakpoint_store, &mut cx.to_async()), + .set_entity(&self.breakpoint_store, &cx.to_async()), self.client .subscribe_to_entity(project_id)? - .set_entity(&self.git_store, &mut cx.to_async()), + .set_entity(&self.git_store, &cx.to_async()), ]); self.buffer_store.update(cx, |buffer_store, cx| { diff --git a/crates/project/src/search_history.rs b/crates/project/src/search_history.rs index 401f375094..4b2a7a065b 100644 --- a/crates/project/src/search_history.rs +++ b/crates/project/src/search_history.rs @@ -202,7 +202,7 @@ mod tests { assert_eq!(search_history.current(&cursor), Some("TypeScript")); cursor.reset(); - assert_eq!(search_history.current(&mut cursor), None); + assert_eq!(search_history.current(&cursor), None); assert_eq!( search_history.previous(&mut cursor), Some("TypeScript"), diff --git a/crates/project/src/task_store.rs b/crates/project/src/task_store.rs index f6718a3f3c..ae49ce5b4d 100644 --- a/crates/project/src/task_store.rs +++ b/crates/project/src/task_store.rs @@ -71,7 +71,7 @@ impl TaskStore { .payload .location .context("no location given for task context handling")?; - let (buffer_store, is_remote) = store.read_with(&mut cx, |store, _| { + let (buffer_store, is_remote) = store.read_with(&cx, |store, _| { Ok(match store { TaskStore::Functional(state) => ( state.buffer_store.clone(), diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 6b0cc2219f..85150f629e 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -372,7 +372,7 @@ impl HeadlessProject { mut cx: AsyncApp, ) -> Result<proto::AddWorktreeResponse> { use client::ErrorCodeExt; - let fs = this.read_with(&mut cx, |this, _| this.fs.clone())?; + let fs = this.read_with(&cx, |this, _| this.fs.clone())?; let path = PathBuf::from_proto(shellexpand::tilde(&message.payload.path).to_string()); let canonicalized = match fs.canonicalize(&path).await { @@ -396,7 +396,7 @@ impl HeadlessProject { }; let worktree = this - .read_with(&mut cx.clone(), |this, _| { + .read_with(&cx.clone(), |this, _| { Worktree::local( Arc::from(canonicalized.as_path()), message.payload.visible, @@ -407,7 +407,7 @@ impl HeadlessProject { })? .await?; - let response = this.read_with(&mut cx, |_, cx| { + let response = this.read_with(&cx, |_, cx| { let worktree = worktree.read(cx); proto::AddWorktreeResponse { worktree_id: worktree.id().to_proto(), @@ -586,7 +586,7 @@ impl HeadlessProject { let buffer_store = this.read_with(&cx, |this, _| this.buffer_store.clone())?; while let Ok(buffer) = results.recv().await { - let buffer_id = buffer.read_with(&mut cx, |this, _| this.remote_id())?; + let buffer_id = buffer.read_with(&cx, |this, _| this.remote_id())?; response.buffer_ids.push(buffer_id.to_proto()); buffer_store .update(&mut cx, |buffer_store, cx| { diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 76e74b75bd..9315536e6b 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -622,7 +622,7 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> { Err(anyhow!(error))?; } n => { - stderr.write_all(&mut stderr_buffer[..n]).await?; + stderr.write_all(&stderr_buffer[..n]).await?; stderr.flush().await?; } } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 78e4da7bc6..75042f184f 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1340,7 +1340,7 @@ impl BufferSearchBar { if self.query(cx).is_empty() && let Some(new_query) = self .search_history - .current(&mut self.search_history_cursor) + .current(&self.search_history_cursor) .map(str::to_string) { drop(self.search(&new_query, Some(self.search_options), window, cx)); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 51cb1fdb26..b6836324db 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -4111,7 +4111,7 @@ pub mod tests { }); cx.run_until_parked(); let project_search_view = pane - .read_with(&mut cx, |pane, _| { + .read_with(&cx, |pane, _| { pane.active_item() .and_then(|item| item.downcast::<ProjectSearchView>()) }) diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/settings_ui/src/ui_components/keystroke_input.rs index f23d80931c..de133d406b 100644 --- a/crates/settings_ui/src/ui_components/keystroke_input.rs +++ b/crates/settings_ui/src/ui_components/keystroke_input.rs @@ -811,7 +811,7 @@ mod tests { pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self { let actual = self .input - .read_with(&mut self.cx, |input, _| input.keystrokes.clone()); + .read_with(&self.cx, |input, _| input.keystrokes.clone()); Self::expect_keystrokes_equal(&actual, expected); self } @@ -820,7 +820,7 @@ mod tests { pub fn expect_close_keystrokes(&mut self, expected: &[&str]) -> &mut Self { let actual = self .input - .read_with(&mut self.cx, |input, _| input.close_keystrokes.clone()) + .read_with(&self.cx, |input, _| input.close_keystrokes.clone()) .unwrap_or_default(); Self::expect_keystrokes_equal(&actual, expected); self diff --git a/crates/snippet_provider/src/lib.rs b/crates/snippet_provider/src/lib.rs index d1112a8d00..c8d2555df2 100644 --- a/crates/snippet_provider/src/lib.rs +++ b/crates/snippet_provider/src/lib.rs @@ -69,7 +69,7 @@ async fn process_updates( entries: Vec<PathBuf>, mut cx: AsyncApp, ) -> Result<()> { - let fs = this.read_with(&mut cx, |this, _| this.fs.clone())?; + let fs = this.read_with(&cx, |this, _| this.fs.clone())?; for entry_path in entries { if !entry_path .extension() @@ -118,9 +118,9 @@ async fn process_updates( async fn initial_scan( this: WeakEntity<SnippetProvider>, path: Arc<Path>, - mut cx: AsyncApp, + cx: AsyncApp, ) -> Result<()> { - let fs = this.read_with(&mut cx, |this, _| this.fs.clone())?; + let fs = this.read_with(&cx, |this, _| this.fs.clone())?; let entries = fs.read_dir(&path).await; if let Ok(entries) = entries { let entries = entries From 6c255c19736389916e8862f339464ae319dc9019 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra <me@as-cii.com> Date: Tue, 19 Aug 2025 16:24:23 +0200 Subject: [PATCH 482/693] Lay the groundwork to support history in agent2 (#36483) This pull request introduces title generation and history replaying. We still need to wire up the rest of the history but this gets us very close. I extracted a lot of this code from `agent2-history` because that branch was starting to get long-lived and there were lots of changes since we started. Release Notes: - N/A --- Cargo.lock | 3 + crates/acp_thread/src/acp_thread.rs | 39 +- crates/acp_thread/src/connection.rs | 16 +- crates/acp_thread/src/diff.rs | 13 +- crates/acp_thread/src/mention.rs | 3 +- crates/agent2/Cargo.toml | 4 + crates/agent2/src/agent.rs | 104 +-- crates/agent2/src/tests/mod.rs | 94 ++- crates/agent2/src/thread.rs | 667 ++++++++++++++---- .../src/tools/context_server_registry.rs | 10 + crates/agent2/src/tools/edit_file_tool.rs | 137 ++-- crates/agent2/src/tools/terminal_tool.rs | 4 +- crates/agent2/src/tools/web_search_tool.rs | 67 +- crates/agent_servers/Cargo.toml | 1 + crates/agent_servers/src/acp/v0.rs | 4 +- crates/agent_servers/src/acp/v1.rs | 7 +- crates/agent_servers/src/claude.rs | 12 +- crates/agent_ui/src/acp/thread_view.rs | 31 +- crates/agent_ui/src/agent_diff.rs | 41 +- 19 files changed, 929 insertions(+), 328 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d7edc54257..dc9d074f01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,6 +191,7 @@ version = "0.1.0" dependencies = [ "acp_thread", "action_log", + "agent", "agent-client-protocol", "agent_servers", "agent_settings", @@ -208,6 +209,7 @@ dependencies = [ "env_logger 0.11.8", "fs", "futures 0.3.31", + "git", "gpui", "gpui_tokio", "handlebars 4.5.0", @@ -256,6 +258,7 @@ name = "agent_servers" version = "0.1.0" dependencies = [ "acp_thread", + "action_log", "agent-client-protocol", "agent_settings", "agentic-coding-protocol", diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 227ca984d4..7d70727252 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -537,9 +537,15 @@ impl ToolCallContent { acp::ToolCallContent::Content { content } => { Self::ContentBlock(ContentBlock::new(content, &language_registry, cx)) } - acp::ToolCallContent::Diff { diff } => { - Self::Diff(cx.new(|cx| Diff::from_acp(diff, language_registry, cx))) - } + acp::ToolCallContent::Diff { diff } => Self::Diff(cx.new(|cx| { + Diff::finalized( + diff.path, + diff.old_text, + diff.new_text, + language_registry, + cx, + ) + })), } } @@ -682,6 +688,7 @@ pub struct AcpThread { #[derive(Debug)] pub enum AcpThreadEvent { NewEntry, + TitleUpdated, EntryUpdated(usize), EntriesRemoved(Range<usize>), ToolAuthorizationRequired, @@ -728,11 +735,9 @@ impl AcpThread { title: impl Into<SharedString>, connection: Rc<dyn AgentConnection>, project: Entity<Project>, + action_log: Entity<ActionLog>, session_id: acp::SessionId, - cx: &mut Context<Self>, ) -> Self { - let action_log = cx.new(|_| ActionLog::new(project.clone())); - Self { action_log, shared_buffers: Default::default(), @@ -926,6 +931,12 @@ impl AcpThread { cx.emit(AcpThreadEvent::NewEntry); } + pub fn update_title(&mut self, title: SharedString, cx: &mut Context<Self>) -> Result<()> { + self.title = title; + cx.emit(AcpThreadEvent::TitleUpdated); + Ok(()) + } + pub fn update_retry_status(&mut self, status: RetryStatus, cx: &mut Context<Self>) { cx.emit(AcpThreadEvent::Retry(status)); } @@ -1657,7 +1668,7 @@ mod tests { use super::*; use anyhow::anyhow; use futures::{channel::mpsc, future::LocalBoxFuture, select}; - use gpui::{AsyncApp, TestAppContext, WeakEntity}; + use gpui::{App, AsyncApp, TestAppContext, WeakEntity}; use indoc::indoc; use project::{FakeFs, Fs}; use rand::Rng as _; @@ -2327,7 +2338,7 @@ mod tests { self: Rc<Self>, project: Entity<Project>, _cwd: &Path, - cx: &mut gpui::App, + cx: &mut App, ) -> Task<gpui::Result<Entity<AcpThread>>> { let session_id = acp::SessionId( rand::thread_rng() @@ -2337,8 +2348,16 @@ mod tests { .collect::<String>() .into(), ); - let thread = - cx.new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx)); + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let thread = cx.new(|_cx| { + AcpThread::new( + "Test", + self.clone(), + project, + action_log, + session_id.clone(), + ) + }); self.sessions.lock().insert(session_id, thread.downgrade()); Task::ready(Ok(thread)) } diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 0d4116321d..b09f383029 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -5,11 +5,12 @@ use collections::IndexMap; use gpui::{Entity, SharedString, Task}; use language_model::LanguageModelProviderId; use project::Project; +use serde::{Deserialize, Serialize}; use std::{any::Any, error::Error, fmt, path::Path, rc::Rc, sync::Arc}; use ui::{App, IconName}; use uuid::Uuid; -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct UserMessageId(Arc<str>); impl UserMessageId { @@ -208,6 +209,7 @@ impl AgentModelList { mod test_support { use std::sync::Arc; + use action_log::ActionLog; use collections::HashMap; use futures::{channel::oneshot, future::try_join_all}; use gpui::{AppContext as _, WeakEntity}; @@ -295,8 +297,16 @@ mod test_support { cx: &mut gpui::App, ) -> Task<gpui::Result<Entity<AcpThread>>> { let session_id = acp::SessionId(self.sessions.lock().len().to_string().into()); - let thread = - cx.new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx)); + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let thread = cx.new(|_cx| { + AcpThread::new( + "Test", + self.clone(), + project, + action_log, + session_id.clone(), + ) + }); self.sessions.lock().insert( session_id, Session { diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs index e5f71d2109..4b779931c5 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -1,4 +1,3 @@ -use agent_client_protocol as acp; use anyhow::Result; use buffer_diff::{BufferDiff, BufferDiffSnapshot}; use editor::{MultiBuffer, PathKey}; @@ -21,17 +20,13 @@ pub enum Diff { } impl Diff { - pub fn from_acp( - diff: acp::Diff, + pub fn finalized( + path: PathBuf, + old_text: Option<String>, + new_text: String, language_registry: Arc<LanguageRegistry>, cx: &mut Context<Self>, ) -> Self { - let acp::Diff { - path, - old_text, - new_text, - } = diff; - let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly)); let new_buffer = cx.new(|cx| Buffer::local(new_text, cx)); diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 25e64acbee..350785ec1e 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -2,6 +2,7 @@ use agent::ThreadId; use anyhow::{Context as _, Result, bail}; use file_icons::FileIcons; use prompt_store::{PromptId, UserPromptId}; +use serde::{Deserialize, Serialize}; use std::{ fmt, ops::Range, @@ -11,7 +12,7 @@ use std::{ use ui::{App, IconName, SharedString}; use url::Url; -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum MentionUri { File { abs_path: PathBuf, diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index ac1840e5e5..8129341545 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -14,6 +14,7 @@ workspace = true [dependencies] acp_thread.workspace = true action_log.workspace = true +agent.workspace = true agent-client-protocol.workspace = true agent_servers.workspace = true agent_settings.workspace = true @@ -26,6 +27,7 @@ collections.workspace = true context_server.workspace = true fs.workspace = true futures.workspace = true +git.workspace = true gpui.workspace = true handlebars = { workspace = true, features = ["rust-embed"] } html_to_markdown.workspace = true @@ -59,6 +61,7 @@ which.workspace = true workspace-hack.workspace = true [dev-dependencies] +agent = { workspace = true, "features" = ["test-support"] } ctor.workspace = true client = { workspace = true, "features" = ["test-support"] } clock = { workspace = true, "features" = ["test-support"] } @@ -66,6 +69,7 @@ context_server = { workspace = true, "features" = ["test-support"] } editor = { workspace = true, "features" = ["test-support"] } env_logger.workspace = true fs = { workspace = true, "features" = ["test-support"] } +git = { workspace = true, "features" = ["test-support"] } gpui = { workspace = true, "features" = ["test-support"] } gpui_tokio.workspace = true language = { workspace = true, "features" = ["test-support"] } diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 480b2baa95..9cf0c3b603 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,10 +1,11 @@ use crate::{ - AgentResponseEvent, ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DeletePathTool, - DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool, ListDirectoryTool, - MovePathTool, NowTool, OpenTool, ReadFileTool, TerminalTool, ThinkingTool, Thread, - ToolCallAuthorization, UserMessageContent, WebSearchTool, templates::Templates, + ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DeletePathTool, DiagnosticsTool, + EditFileTool, FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MovePathTool, NowTool, + OpenTool, ReadFileTool, TerminalTool, ThinkingTool, Thread, ThreadEvent, ToolCallAuthorization, + UserMessageContent, WebSearchTool, templates::Templates, }; use acp_thread::AgentModelSelector; +use action_log::ActionLog; use agent_client_protocol as acp; use agent_settings::AgentSettings; use anyhow::{Context as _, Result, anyhow}; @@ -427,18 +428,19 @@ impl NativeAgent { ) { self.models.refresh_list(cx); - let default_model = LanguageModelRegistry::read_global(cx) - .default_model() - .map(|m| m.model.clone()); + let registry = LanguageModelRegistry::read_global(cx); + let default_model = registry.default_model().map(|m| m.model.clone()); + let summarization_model = registry.thread_summary_model().map(|m| m.model.clone()); for session in self.sessions.values_mut() { session.thread.update(cx, |thread, cx| { if thread.model().is_none() && let Some(model) = default_model.clone() { - thread.set_model(model); + thread.set_model(model, cx); cx.notify(); } + thread.set_summarization_model(summarization_model.clone(), cx); }); } } @@ -462,10 +464,7 @@ impl NativeAgentConnection { session_id: acp::SessionId, cx: &mut App, f: impl 'static - + FnOnce( - Entity<Thread>, - &mut App, - ) -> Result<mpsc::UnboundedReceiver<Result<AgentResponseEvent>>>, + + FnOnce(Entity<Thread>, &mut App) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>>, ) -> Task<Result<acp::PromptResponse>> { let Some((thread, acp_thread)) = self.0.update(cx, |agent, _cx| { agent @@ -489,7 +488,18 @@ impl NativeAgentConnection { log::trace!("Received completion event: {:?}", event); match event { - AgentResponseEvent::Text(text) => { + ThreadEvent::UserMessage(message) => { + acp_thread.update(cx, |thread, cx| { + for content in message.content { + thread.push_user_content_block( + Some(message.id.clone()), + content.into(), + cx, + ); + } + })?; + } + ThreadEvent::AgentText(text) => { acp_thread.update(cx, |thread, cx| { thread.push_assistant_content_block( acp::ContentBlock::Text(acp::TextContent { @@ -501,7 +511,7 @@ impl NativeAgentConnection { ) })?; } - AgentResponseEvent::Thinking(text) => { + ThreadEvent::AgentThinking(text) => { acp_thread.update(cx, |thread, cx| { thread.push_assistant_content_block( acp::ContentBlock::Text(acp::TextContent { @@ -513,7 +523,7 @@ impl NativeAgentConnection { ) })?; } - AgentResponseEvent::ToolCallAuthorization(ToolCallAuthorization { + ThreadEvent::ToolCallAuthorization(ToolCallAuthorization { tool_call, options, response, @@ -536,22 +546,26 @@ impl NativeAgentConnection { }) .detach(); } - AgentResponseEvent::ToolCall(tool_call) => { + ThreadEvent::ToolCall(tool_call) => { acp_thread.update(cx, |thread, cx| { thread.upsert_tool_call(tool_call, cx) })??; } - AgentResponseEvent::ToolCallUpdate(update) => { + ThreadEvent::ToolCallUpdate(update) => { acp_thread.update(cx, |thread, cx| { thread.update_tool_call(update, cx) })??; } - AgentResponseEvent::Retry(status) => { + ThreadEvent::TitleUpdate(title) => { + acp_thread + .update(cx, |thread, cx| thread.update_title(title, cx))??; + } + ThreadEvent::Retry(status) => { acp_thread.update(cx, |thread, cx| { thread.update_retry_status(status, cx) })?; } - AgentResponseEvent::Stop(stop_reason) => { + ThreadEvent::Stop(stop_reason) => { log::debug!("Assistant message complete: {:?}", stop_reason); return Ok(acp::PromptResponse { stop_reason }); } @@ -604,8 +618,8 @@ impl AgentModelSelector for NativeAgentConnection { return Task::ready(Err(anyhow!("Invalid model ID {}", model_id))); }; - thread.update(cx, |thread, _cx| { - thread.set_model(model.clone()); + thread.update(cx, |thread, cx| { + thread.set_model(model.clone(), cx); }); update_settings_file::<AgentSettings>( @@ -665,30 +679,14 @@ impl acp_thread::AgentConnection for NativeAgentConnection { cx.spawn(async move |cx| { log::debug!("Starting thread creation in async context"); - // Generate session ID - let session_id = acp::SessionId(uuid::Uuid::new_v4().to_string().into()); - log::info!("Created session with ID: {}", session_id); - - // Create AcpThread - let acp_thread = cx.update(|cx| { - cx.new(|cx| { - acp_thread::AcpThread::new( - "agent2", - self.clone(), - project.clone(), - session_id.clone(), - cx, - ) - }) - })?; - let action_log = cx.update(|cx| acp_thread.read(cx).action_log().clone())?; - + let action_log = cx.new(|_cx| ActionLog::new(project.clone()))?; // Create Thread let thread = agent.update( cx, |agent, cx: &mut gpui::Context<NativeAgent>| -> Result<_> { // Fetch default model from registry settings let registry = LanguageModelRegistry::read_global(cx); + let language_registry = project.read(cx).languages().clone(); // Log available models for debugging let available_count = registry.available_models(cx).count(); @@ -699,6 +697,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { .models .model_from_id(&LanguageModels::model_id(&default_model.model)) }); + let summarization_model = registry.thread_summary_model().map(|c| c.model); let thread = cx.new(|cx| { let mut thread = Thread::new( @@ -708,13 +707,14 @@ impl acp_thread::AgentConnection for NativeAgentConnection { action_log.clone(), agent.templates.clone(), default_model, + summarization_model, cx, ); thread.add_tool(CopyPathTool::new(project.clone())); thread.add_tool(CreateDirectoryTool::new(project.clone())); thread.add_tool(DeletePathTool::new(project.clone(), action_log.clone())); thread.add_tool(DiagnosticsTool::new(project.clone())); - thread.add_tool(EditFileTool::new(cx.entity())); + thread.add_tool(EditFileTool::new(cx.weak_entity(), language_registry)); thread.add_tool(FetchTool::new(project.read(cx).client().http_client())); thread.add_tool(FindPathTool::new(project.clone())); thread.add_tool(GrepTool::new(project.clone())); @@ -722,7 +722,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { thread.add_tool(MovePathTool::new(project.clone())); thread.add_tool(NowTool); thread.add_tool(OpenTool::new(project.clone())); - thread.add_tool(ReadFileTool::new(project.clone(), action_log)); + thread.add_tool(ReadFileTool::new(project.clone(), action_log.clone())); thread.add_tool(TerminalTool::new(project.clone(), cx)); thread.add_tool(ThinkingTool); thread.add_tool(WebSearchTool); // TODO: Enable this only if it's a zed model. @@ -733,6 +733,21 @@ impl acp_thread::AgentConnection for NativeAgentConnection { }, )??; + let session_id = thread.read_with(cx, |thread, _| thread.id().clone())?; + log::info!("Created session with ID: {}", session_id); + // Create AcpThread + let acp_thread = cx.update(|cx| { + cx.new(|_cx| { + acp_thread::AcpThread::new( + "agent2", + self.clone(), + project.clone(), + action_log.clone(), + session_id.clone(), + ) + }) + })?; + // Store the session agent.update(cx, |agent, cx| { agent.sessions.insert( @@ -803,7 +818,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { log::info!("Cancelling on session: {}", session_id); self.0.update(cx, |agent, cx| { if let Some(agent) = agent.sessions.get(session_id) { - agent.thread.update(cx, |thread, _cx| thread.cancel()); + agent.thread.update(cx, |thread, cx| thread.cancel(cx)); } }); } @@ -830,7 +845,10 @@ struct NativeAgentSessionEditor(Entity<Thread>); impl acp_thread::AgentSessionEditor for NativeAgentSessionEditor { fn truncate(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task<Result<()>> { - Task::ready(self.0.update(cx, |thread, _cx| thread.truncate(message_id))) + Task::ready( + self.0 + .update(cx, |thread, cx| thread.truncate(message_id, cx)), + ) } } diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index c83479f2cf..33706b05de 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -345,7 +345,7 @@ async fn test_streaming_tool_calls(cx: &mut TestAppContext) { let mut saw_partial_tool_use = false; while let Some(event) = events.next().await { - if let Ok(AgentResponseEvent::ToolCall(tool_call)) = event { + if let Ok(ThreadEvent::ToolCall(tool_call)) = event { thread.update(cx, |thread, _cx| { // Look for a tool use in the thread's last message let message = thread.last_message().unwrap(); @@ -735,16 +735,14 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) { ); } -async fn expect_tool_call( - events: &mut UnboundedReceiver<Result<AgentResponseEvent>>, -) -> acp::ToolCall { +async fn expect_tool_call(events: &mut UnboundedReceiver<Result<ThreadEvent>>) -> acp::ToolCall { let event = events .next() .await .expect("no tool call authorization event received") .unwrap(); match event { - AgentResponseEvent::ToolCall(tool_call) => return tool_call, + ThreadEvent::ToolCall(tool_call) => return tool_call, event => { panic!("Unexpected event {event:?}"); } @@ -752,7 +750,7 @@ async fn expect_tool_call( } async fn expect_tool_call_update_fields( - events: &mut UnboundedReceiver<Result<AgentResponseEvent>>, + events: &mut UnboundedReceiver<Result<ThreadEvent>>, ) -> acp::ToolCallUpdate { let event = events .next() @@ -760,7 +758,7 @@ async fn expect_tool_call_update_fields( .expect("no tool call authorization event received") .unwrap(); match event { - AgentResponseEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => { + ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => { return update; } event => { @@ -770,7 +768,7 @@ async fn expect_tool_call_update_fields( } async fn next_tool_call_authorization( - events: &mut UnboundedReceiver<Result<AgentResponseEvent>>, + events: &mut UnboundedReceiver<Result<ThreadEvent>>, ) -> ToolCallAuthorization { loop { let event = events @@ -778,7 +776,7 @@ async fn next_tool_call_authorization( .await .expect("no tool call authorization event received") .unwrap(); - if let AgentResponseEvent::ToolCallAuthorization(tool_call_authorization) = event { + if let ThreadEvent::ToolCallAuthorization(tool_call_authorization) = event { let permission_kinds = tool_call_authorization .options .iter() @@ -945,13 +943,13 @@ async fn test_cancellation(cx: &mut TestAppContext) { let mut echo_completed = false; while let Some(event) = events.next().await { match event.unwrap() { - AgentResponseEvent::ToolCall(tool_call) => { + ThreadEvent::ToolCall(tool_call) => { assert_eq!(tool_call.title, expected_tools.remove(0)); if tool_call.title == "Echo" { echo_id = Some(tool_call.id); } } - AgentResponseEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields( + ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields( acp::ToolCallUpdate { id, fields: @@ -973,13 +971,13 @@ async fn test_cancellation(cx: &mut TestAppContext) { // Cancel the current send and ensure that the event stream is closed, even // if one of the tools is still running. - thread.update(cx, |thread, _cx| thread.cancel()); + thread.update(cx, |thread, cx| thread.cancel(cx)); let events = events.collect::<Vec<_>>().await; let last_event = events.last(); assert!( matches!( last_event, - Some(Ok(AgentResponseEvent::Stop(acp::StopReason::Canceled))) + Some(Ok(ThreadEvent::Stop(acp::StopReason::Canceled))) ), "unexpected event {last_event:?}" ); @@ -1161,7 +1159,7 @@ async fn test_truncate(cx: &mut TestAppContext) { }); thread - .update(cx, |thread, _cx| thread.truncate(message_id)) + .update(cx, |thread, cx| thread.truncate(message_id, cx)) .unwrap(); cx.run_until_parked(); thread.read_with(cx, |thread, _| { @@ -1203,6 +1201,51 @@ async fn test_truncate(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_title_generation(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let summary_model = Arc::new(FakeLanguageModel::default()); + thread.update(cx, |thread, cx| { + thread.set_summarization_model(Some(summary_model.clone()), cx) + }); + + let send = thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello"], cx) + }) + .unwrap(); + cx.run_until_parked(); + + fake_model.send_last_completion_stream_text_chunk("Hey!"); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "New Thread")); + + // Ensure the summary model has been invoked to generate a title. + summary_model.send_last_completion_stream_text_chunk("Hello "); + summary_model.send_last_completion_stream_text_chunk("world\nG"); + summary_model.send_last_completion_stream_text_chunk("oodnight Moon"); + summary_model.end_last_completion_stream(); + send.collect::<Vec<_>>().await; + thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world")); + + // Send another message, ensuring no title is generated this time. + let send = thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hello again"], cx) + }) + .unwrap(); + cx.run_until_parked(); + fake_model.send_last_completion_stream_text_chunk("Hey again!"); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + assert_eq!(summary_model.pending_completions(), Vec::new()); + send.collect::<Vec<_>>().await; + thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world")); +} + #[gpui::test] async fn test_agent_connection(cx: &mut TestAppContext) { cx.update(settings::init); @@ -1442,7 +1485,7 @@ async fn test_send_no_retry_on_success(cx: &mut TestAppContext) { let mut events = thread .update(cx, |thread, cx| { - thread.set_completion_mode(agent_settings::CompletionMode::Burn); + thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx); thread.send(UserMessageId::new(), ["Hello!"], cx) }) .unwrap(); @@ -1454,10 +1497,10 @@ async fn test_send_no_retry_on_success(cx: &mut TestAppContext) { let mut retry_events = Vec::new(); while let Some(Ok(event)) = events.next().await { match event { - AgentResponseEvent::Retry(retry_status) => { + ThreadEvent::Retry(retry_status) => { retry_events.push(retry_status); } - AgentResponseEvent::Stop(..) => break, + ThreadEvent::Stop(..) => break, _ => {} } } @@ -1486,7 +1529,7 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) { let mut events = thread .update(cx, |thread, cx| { - thread.set_completion_mode(agent_settings::CompletionMode::Burn); + thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx); thread.send(UserMessageId::new(), ["Hello!"], cx) }) .unwrap(); @@ -1507,10 +1550,10 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) { let mut retry_events = Vec::new(); while let Some(Ok(event)) = events.next().await { match event { - AgentResponseEvent::Retry(retry_status) => { + ThreadEvent::Retry(retry_status) => { retry_events.push(retry_status); } - AgentResponseEvent::Stop(..) => break, + ThreadEvent::Stop(..) => break, _ => {} } } @@ -1543,7 +1586,7 @@ async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) { let mut events = thread .update(cx, |thread, cx| { - thread.set_completion_mode(agent_settings::CompletionMode::Burn); + thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx); thread.send(UserMessageId::new(), ["Hello!"], cx) }) .unwrap(); @@ -1565,10 +1608,10 @@ async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) { let mut retry_events = Vec::new(); while let Some(event) = events.next().await { match event { - Ok(AgentResponseEvent::Retry(retry_status)) => { + Ok(ThreadEvent::Retry(retry_status)) => { retry_events.push(retry_status); } - Ok(AgentResponseEvent::Stop(..)) => break, + Ok(ThreadEvent::Stop(..)) => break, Err(error) => errors.push(error), _ => {} } @@ -1592,11 +1635,11 @@ async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) { } /// Filters out the stop events for asserting against in tests -fn stop_events(result_events: Vec<Result<AgentResponseEvent>>) -> Vec<acp::StopReason> { +fn stop_events(result_events: Vec<Result<ThreadEvent>>) -> Vec<acp::StopReason> { result_events .into_iter() .filter_map(|event| match event.unwrap() { - AgentResponseEvent::Stop(stop_reason) => Some(stop_reason), + ThreadEvent::Stop(stop_reason) => Some(stop_reason), _ => None, }) .collect() @@ -1713,6 +1756,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { action_log, templates, Some(model.clone()), + None, cx, ) }); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 856e70ce59..aeb600e232 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1,25 +1,34 @@ use crate::{ContextServerRegistry, SystemPromptTemplate, Template, Templates}; use acp_thread::{MentionUri, UserMessageId}; use action_log::ActionLog; +use agent::thread::{DetailedSummaryState, GitState, ProjectSnapshot, WorktreeSnapshot}; use agent_client_protocol as acp; -use agent_settings::{AgentProfileId, AgentSettings, CompletionMode}; +use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_PROMPT}; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::adapt_schema_to_format; +use chrono::{DateTime, Utc}; use cloud_llm_client::{CompletionIntent, CompletionRequestStatus}; use collections::IndexMap; use fs::Fs; use futures::{ + FutureExt, channel::{mpsc, oneshot}, + future::Shared, stream::FuturesUnordered, }; +use git::repository::DiffType; use gpui::{App, AsyncApp, Context, Entity, SharedString, Task, WeakEntity}; use language_model::{ LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage, LanguageModelProviderId, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason, + TokenUsage, +}; +use project::{ + Project, + git_store::{GitStore, RepositoryState}, }; -use project::Project; use prompt_store::ProjectContext; use schemars::{JsonSchema, Schema}; use serde::{Deserialize, Serialize}; @@ -35,28 +44,7 @@ use std::{fmt::Write, ops::Range}; use util::{ResultExt, markdown::MarkdownCodeBlock}; use uuid::Uuid; -#[derive( - Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize, JsonSchema, -)] -pub struct ThreadId(Arc<str>); - -impl ThreadId { - pub fn new() -> Self { - Self(Uuid::new_v4().to_string().into()) - } -} - -impl std::fmt::Display for ThreadId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From<&str> for ThreadId { - fn from(value: &str) -> Self { - Self(value.into()) - } -} +const TOOL_CANCELED_MESSAGE: &str = "Tool canceled by user"; /// The ID of the user prompt that initiated a request. /// @@ -91,7 +79,7 @@ enum RetryStrategy { }, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Message { User(UserMessage), Agent(AgentMessage), @@ -106,6 +94,18 @@ impl Message { } } + pub fn to_request(&self) -> Vec<LanguageModelRequestMessage> { + match self { + Message::User(message) => vec![message.to_request()], + Message::Agent(message) => message.to_request(), + Message::Resume => vec![LanguageModelRequestMessage { + role: Role::User, + content: vec!["Continue where you left off".into()], + cache: false, + }], + } + } + pub fn to_markdown(&self) -> String { match self { Message::User(message) => message.to_markdown(), @@ -113,15 +113,22 @@ impl Message { Message::Resume => "[resumed after tool use limit was reached]".into(), } } + + pub fn role(&self) -> Role { + match self { + Message::User(_) | Message::Resume => Role::User, + Message::Agent(_) => Role::Assistant, + } + } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct UserMessage { pub id: UserMessageId, pub content: Vec<UserMessageContent>, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum UserMessageContent { Text(String), Mention { uri: MentionUri, content: String }, @@ -345,9 +352,6 @@ impl AgentMessage { AgentMessageContent::RedactedThinking(_) => { markdown.push_str("<redacted_thinking />\n") } - AgentMessageContent::Image(_) => { - markdown.push_str("<image />\n"); - } AgentMessageContent::ToolUse(tool_use) => { markdown.push_str(&format!( "**Tool Use**: {} (ID: {})\n", @@ -418,9 +422,6 @@ impl AgentMessage { AgentMessageContent::ToolUse(value) => { language_model::MessageContent::ToolUse(value.clone()) } - AgentMessageContent::Image(value) => { - language_model::MessageContent::Image(value.clone()) - } }; assistant_message.content.push(chunk); } @@ -450,13 +451,13 @@ impl AgentMessage { } } -#[derive(Default, Debug, Clone, PartialEq, Eq)] +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct AgentMessage { pub content: Vec<AgentMessageContent>, pub tool_results: IndexMap<LanguageModelToolUseId, LanguageModelToolResult>, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum AgentMessageContent { Text(String), Thinking { @@ -464,17 +465,18 @@ pub enum AgentMessageContent { signature: Option<String>, }, RedactedThinking(String), - Image(LanguageModelImage), ToolUse(LanguageModelToolUse), } #[derive(Debug)] -pub enum AgentResponseEvent { - Text(String), - Thinking(String), +pub enum ThreadEvent { + UserMessage(UserMessage), + AgentText(String), + AgentThinking(String), ToolCall(acp::ToolCall), ToolCallUpdate(acp_thread::ToolCallUpdate), ToolCallAuthorization(ToolCallAuthorization), + TitleUpdate(SharedString), Retry(acp_thread::RetryStatus), Stop(acp::StopReason), } @@ -487,8 +489,12 @@ pub struct ToolCallAuthorization { } pub struct Thread { - id: ThreadId, + id: acp::SessionId, prompt_id: PromptId, + updated_at: DateTime<Utc>, + title: Option<SharedString>, + #[allow(unused)] + summary: DetailedSummaryState, messages: Vec<Message>, completion_mode: CompletionMode, /// Holds the task that handles agent interaction until the end of the turn. @@ -498,11 +504,18 @@ pub struct Thread { pending_message: Option<AgentMessage>, tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>, tool_use_limit_reached: bool, + #[allow(unused)] + request_token_usage: Vec<TokenUsage>, + #[allow(unused)] + cumulative_token_usage: TokenUsage, + #[allow(unused)] + initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>, context_server_registry: Entity<ContextServerRegistry>, profile_id: AgentProfileId, project_context: Entity<ProjectContext>, templates: Arc<Templates>, model: Option<Arc<dyn LanguageModel>>, + summarization_model: Option<Arc<dyn LanguageModel>>, project: Entity<Project>, action_log: Entity<ActionLog>, } @@ -515,36 +528,254 @@ impl Thread { action_log: Entity<ActionLog>, templates: Arc<Templates>, model: Option<Arc<dyn LanguageModel>>, + summarization_model: Option<Arc<dyn LanguageModel>>, cx: &mut Context<Self>, ) -> Self { let profile_id = AgentSettings::get_global(cx).default_profile.clone(); Self { - id: ThreadId::new(), + id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()), prompt_id: PromptId::new(), + updated_at: Utc::now(), + title: None, + summary: DetailedSummaryState::default(), messages: Vec::new(), completion_mode: AgentSettings::get_global(cx).preferred_completion_mode, running_turn: None, pending_message: None, tools: BTreeMap::default(), tool_use_limit_reached: false, + request_token_usage: Vec::new(), + cumulative_token_usage: TokenUsage::default(), + initial_project_snapshot: { + let project_snapshot = Self::project_snapshot(project.clone(), cx); + cx.foreground_executor() + .spawn(async move { Some(project_snapshot.await) }) + .shared() + }, context_server_registry, profile_id, project_context, templates, model, + summarization_model, project, action_log, } } - pub fn project(&self) -> &Entity<Project> { - &self.project + pub fn id(&self) -> &acp::SessionId { + &self.id + } + + pub fn replay( + &mut self, + cx: &mut Context<Self>, + ) -> mpsc::UnboundedReceiver<Result<ThreadEvent>> { + let (tx, rx) = mpsc::unbounded(); + let stream = ThreadEventStream(tx); + for message in &self.messages { + match message { + Message::User(user_message) => stream.send_user_message(user_message), + Message::Agent(assistant_message) => { + for content in &assistant_message.content { + match content { + AgentMessageContent::Text(text) => stream.send_text(text), + AgentMessageContent::Thinking { text, .. } => { + stream.send_thinking(text) + } + AgentMessageContent::RedactedThinking(_) => {} + AgentMessageContent::ToolUse(tool_use) => { + self.replay_tool_call( + tool_use, + assistant_message.tool_results.get(&tool_use.id), + &stream, + cx, + ); + } + } + } + } + Message::Resume => {} + } + } + rx + } + + fn replay_tool_call( + &self, + tool_use: &LanguageModelToolUse, + tool_result: Option<&LanguageModelToolResult>, + stream: &ThreadEventStream, + cx: &mut Context<Self>, + ) { + let Some(tool) = self.tools.get(tool_use.name.as_ref()) else { + stream + .0 + .unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall { + id: acp::ToolCallId(tool_use.id.to_string().into()), + title: tool_use.name.to_string(), + kind: acp::ToolKind::Other, + status: acp::ToolCallStatus::Failed, + content: Vec::new(), + locations: Vec::new(), + raw_input: Some(tool_use.input.clone()), + raw_output: None, + }))) + .ok(); + return; + }; + + let title = tool.initial_title(tool_use.input.clone()); + let kind = tool.kind(); + stream.send_tool_call(&tool_use.id, title, kind, tool_use.input.clone()); + + let output = tool_result + .as_ref() + .and_then(|result| result.output.clone()); + if let Some(output) = output.clone() { + let tool_event_stream = ToolCallEventStream::new( + tool_use.id.clone(), + stream.clone(), + Some(self.project.read(cx).fs().clone()), + ); + tool.replay(tool_use.input.clone(), output, tool_event_stream, cx) + .log_err(); + } + + stream.update_tool_call_fields( + &tool_use.id, + acp::ToolCallUpdateFields { + status: Some(acp::ToolCallStatus::Completed), + raw_output: output, + ..Default::default() + }, + ); + } + + /// Create a snapshot of the current project state including git information and unsaved buffers. + fn project_snapshot( + project: Entity<Project>, + cx: &mut Context<Self>, + ) -> Task<Arc<agent::thread::ProjectSnapshot>> { + let git_store = project.read(cx).git_store().clone(); + let worktree_snapshots: Vec<_> = project + .read(cx) + .visible_worktrees(cx) + .map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx)) + .collect(); + + cx.spawn(async move |_, cx| { + let worktree_snapshots = futures::future::join_all(worktree_snapshots).await; + + let mut unsaved_buffers = Vec::new(); + cx.update(|app_cx| { + let buffer_store = project.read(app_cx).buffer_store(); + for buffer_handle in buffer_store.read(app_cx).buffers() { + let buffer = buffer_handle.read(app_cx); + if buffer.is_dirty() + && let Some(file) = buffer.file() + { + let path = file.path().to_string_lossy().to_string(); + unsaved_buffers.push(path); + } + } + }) + .ok(); + + Arc::new(ProjectSnapshot { + worktree_snapshots, + unsaved_buffer_paths: unsaved_buffers, + timestamp: Utc::now(), + }) + }) + } + + fn worktree_snapshot( + worktree: Entity<project::Worktree>, + git_store: Entity<GitStore>, + cx: &App, + ) -> Task<agent::thread::WorktreeSnapshot> { + cx.spawn(async move |cx| { + // Get worktree path and snapshot + let worktree_info = cx.update(|app_cx| { + let worktree = worktree.read(app_cx); + let path = worktree.abs_path().to_string_lossy().to_string(); + let snapshot = worktree.snapshot(); + (path, snapshot) + }); + + let Ok((worktree_path, _snapshot)) = worktree_info else { + return WorktreeSnapshot { + worktree_path: String::new(), + git_state: None, + }; + }; + + let git_state = git_store + .update(cx, |git_store, cx| { + git_store + .repositories() + .values() + .find(|repo| { + repo.read(cx) + .abs_path_to_repo_path(&worktree.read(cx).abs_path()) + .is_some() + }) + .cloned() + }) + .ok() + .flatten() + .map(|repo| { + repo.update(cx, |repo, _| { + let current_branch = + repo.branch.as_ref().map(|branch| branch.name().to_owned()); + repo.send_job(None, |state, _| async move { + let RepositoryState::Local { backend, .. } = state else { + return GitState { + remote_url: None, + head_sha: None, + current_branch, + diff: None, + }; + }; + + let remote_url = backend.remote_url("origin"); + let head_sha = backend.head_sha().await; + let diff = backend.diff(DiffType::HeadToWorktree).await.ok(); + + GitState { + remote_url, + head_sha, + current_branch, + diff, + } + }) + }) + }); + + let git_state = match git_state { + Some(git_state) => match git_state.ok() { + Some(git_state) => git_state.await.ok(), + None => None, + }, + None => None, + }; + + WorktreeSnapshot { + worktree_path, + git_state, + } + }) } pub fn project_context(&self) -> &Entity<ProjectContext> { &self.project_context } + pub fn project(&self) -> &Entity<Project> { + &self.project + } + pub fn action_log(&self) -> &Entity<ActionLog> { &self.action_log } @@ -553,16 +784,27 @@ impl Thread { self.model.as_ref() } - pub fn set_model(&mut self, model: Arc<dyn LanguageModel>) { + pub fn set_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) { self.model = Some(model); + cx.notify() + } + + pub fn set_summarization_model( + &mut self, + model: Option<Arc<dyn LanguageModel>>, + cx: &mut Context<Self>, + ) { + self.summarization_model = model; + cx.notify() } pub fn completion_mode(&self) -> CompletionMode { self.completion_mode } - pub fn set_completion_mode(&mut self, mode: CompletionMode) { + pub fn set_completion_mode(&mut self, mode: CompletionMode, cx: &mut Context<Self>) { self.completion_mode = mode; + cx.notify() } #[cfg(any(test, feature = "test-support"))] @@ -590,29 +832,29 @@ impl Thread { self.profile_id = profile_id; } - pub fn cancel(&mut self) { + pub fn cancel(&mut self, cx: &mut Context<Self>) { if let Some(running_turn) = self.running_turn.take() { running_turn.cancel(); } - self.flush_pending_message(); + self.flush_pending_message(cx); } - pub fn truncate(&mut self, message_id: UserMessageId) -> Result<()> { - self.cancel(); + pub fn truncate(&mut self, message_id: UserMessageId, cx: &mut Context<Self>) -> Result<()> { + self.cancel(cx); let Some(position) = self.messages.iter().position( |msg| matches!(msg, Message::User(UserMessage { id, .. }) if id == &message_id), ) else { return Err(anyhow!("Message not found")); }; self.messages.truncate(position); + cx.notify(); Ok(()) } pub fn resume( &mut self, cx: &mut Context<Self>, - ) -> Result<mpsc::UnboundedReceiver<Result<AgentResponseEvent>>> { - anyhow::ensure!(self.model.is_some(), "Model not set"); + ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> { anyhow::ensure!( self.tool_use_limit_reached, "can only resume after tool use limit is reached" @@ -633,7 +875,7 @@ impl Thread { id: UserMessageId, content: impl IntoIterator<Item = T>, cx: &mut Context<Self>, - ) -> Result<mpsc::UnboundedReceiver<Result<AgentResponseEvent>>> + ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> where T: Into<UserMessageContent>, { @@ -656,22 +898,19 @@ impl Thread { fn run_turn( &mut self, cx: &mut Context<Self>, - ) -> Result<mpsc::UnboundedReceiver<Result<AgentResponseEvent>>> { - self.cancel(); + ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> { + self.cancel(cx); - let model = self - .model() - .cloned() - .context("No language model configured")?; - let (events_tx, events_rx) = mpsc::unbounded::<Result<AgentResponseEvent>>(); - let event_stream = AgentResponseEventStream(events_tx); + let model = self.model.clone().context("No language model configured")?; + let (events_tx, events_rx) = mpsc::unbounded::<Result<ThreadEvent>>(); + let event_stream = ThreadEventStream(events_tx); let message_ix = self.messages.len().saturating_sub(1); self.tool_use_limit_reached = false; self.running_turn = Some(RunningTurn { event_stream: event_stream.clone(), _task: cx.spawn(async move |this, cx| { log::info!("Starting agent turn execution"); - let turn_result: Result<()> = async { + let turn_result: Result<StopReason> = async { let mut completion_intent = CompletionIntent::UserPrompt; loop { log::debug!( @@ -685,18 +924,27 @@ impl Thread { log::info!("Calling model.stream_completion"); let mut tool_use_limit_reached = false; + let mut refused = false; + let mut reached_max_tokens = false; let mut tool_uses = Self::stream_completion_with_retries( this.clone(), model.clone(), request, - message_ix, &event_stream, &mut tool_use_limit_reached, + &mut refused, + &mut reached_max_tokens, cx, ) .await?; - let used_tools = tool_uses.is_empty(); + if refused { + return Ok(StopReason::Refusal); + } else if reached_max_tokens { + return Ok(StopReason::MaxTokens); + } + + let end_turn = tool_uses.is_empty(); while let Some(tool_result) = tool_uses.next().await { log::info!("Tool finished {:?}", tool_result); @@ -724,29 +972,42 @@ impl Thread { log::info!("Tool use limit reached, completing turn"); this.update(cx, |this, _cx| this.tool_use_limit_reached = true)?; return Err(language_model::ToolUseLimitReachedError.into()); - } else if used_tools { + } else if end_turn { log::info!("No tool uses found, completing turn"); - return Ok(()); + return Ok(StopReason::EndTurn); } else { - this.update(cx, |this, _| this.flush_pending_message())?; + this.update(cx, |this, cx| this.flush_pending_message(cx))?; completion_intent = CompletionIntent::ToolResults; } } } .await; + _ = this.update(cx, |this, cx| this.flush_pending_message(cx)); - if let Err(error) = turn_result { - log::error!("Turn execution failed: {:?}", error); - event_stream.send_error(error); - } else { - log::info!("Turn execution completed successfully"); + match turn_result { + Ok(reason) => { + log::info!("Turn execution completed: {:?}", reason); + + let update_title = this + .update(cx, |this, cx| this.update_title(&event_stream, cx)) + .ok() + .flatten(); + if let Some(update_title) = update_title { + update_title.await.context("update title failed").log_err(); + } + + event_stream.send_stop(reason); + if reason == StopReason::Refusal { + _ = this.update(cx, |this, _| this.messages.truncate(message_ix)); + } + } + Err(error) => { + log::error!("Turn execution failed: {:?}", error); + event_stream.send_error(error); + } } - this.update(cx, |this, _| { - this.flush_pending_message(); - this.running_turn.take(); - }) - .ok(); + _ = this.update(cx, |this, _| this.running_turn.take()); }), }); Ok(events_rx) @@ -756,9 +1017,10 @@ impl Thread { this: WeakEntity<Self>, model: Arc<dyn LanguageModel>, request: LanguageModelRequest, - message_ix: usize, - event_stream: &AgentResponseEventStream, + event_stream: &ThreadEventStream, tool_use_limit_reached: &mut bool, + refusal: &mut bool, + max_tokens_reached: &mut bool, cx: &mut AsyncApp, ) -> Result<FuturesUnordered<Task<LanguageModelToolResult>>> { log::debug!("Stream completion started successfully"); @@ -774,16 +1036,17 @@ impl Thread { )) => { *tool_use_limit_reached = true; } - Ok(LanguageModelCompletionEvent::Stop(reason)) => { - event_stream.send_stop(reason); - if reason == StopReason::Refusal { - this.update(cx, |this, _cx| { - this.flush_pending_message(); - this.messages.truncate(message_ix); - })?; - return Ok(tool_uses); - } + Ok(LanguageModelCompletionEvent::Stop(StopReason::Refusal)) => { + *refusal = true; + return Ok(FuturesUnordered::default()); } + Ok(LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)) => { + *max_tokens_reached = true; + return Ok(FuturesUnordered::default()); + } + Ok(LanguageModelCompletionEvent::Stop( + StopReason::ToolUse | StopReason::EndTurn, + )) => break, Ok(event) => { log::trace!("Received completion event: {:?}", event); this.update(cx, |this, cx| { @@ -843,6 +1106,7 @@ impl Thread { } } } + return Ok(tool_uses); } } @@ -870,7 +1134,7 @@ impl Thread { fn handle_streamed_completion_event( &mut self, event: LanguageModelCompletionEvent, - event_stream: &AgentResponseEventStream, + event_stream: &ThreadEventStream, cx: &mut Context<Self>, ) -> Option<Task<LanguageModelToolResult>> { log::trace!("Handling streamed completion event: {:?}", event); @@ -878,7 +1142,7 @@ impl Thread { match event { StartMessage { .. } => { - self.flush_pending_message(); + self.flush_pending_message(cx); self.pending_message = Some(AgentMessage::default()); } Text(new_text) => self.handle_text_event(new_text, event_stream, cx), @@ -912,7 +1176,7 @@ impl Thread { fn handle_text_event( &mut self, new_text: String, - event_stream: &AgentResponseEventStream, + event_stream: &ThreadEventStream, cx: &mut Context<Self>, ) { event_stream.send_text(&new_text); @@ -933,7 +1197,7 @@ impl Thread { &mut self, new_text: String, new_signature: Option<String>, - event_stream: &AgentResponseEventStream, + event_stream: &ThreadEventStream, cx: &mut Context<Self>, ) { event_stream.send_thinking(&new_text); @@ -965,7 +1229,7 @@ impl Thread { fn handle_tool_use_event( &mut self, tool_use: LanguageModelToolUse, - event_stream: &AgentResponseEventStream, + event_stream: &ThreadEventStream, cx: &mut Context<Self>, ) -> Option<Task<LanguageModelToolResult>> { cx.notify(); @@ -1083,11 +1347,85 @@ impl Thread { } } + pub fn title(&self) -> SharedString { + self.title.clone().unwrap_or("New Thread".into()) + } + + fn update_title( + &mut self, + event_stream: &ThreadEventStream, + cx: &mut Context<Self>, + ) -> Option<Task<Result<()>>> { + if self.title.is_some() { + log::debug!("Skipping title generation because we already have one."); + return None; + } + + log::info!( + "Generating title with model: {:?}", + self.summarization_model.as_ref().map(|model| model.name()) + ); + let model = self.summarization_model.clone()?; + let event_stream = event_stream.clone(); + let mut request = LanguageModelRequest { + intent: Some(CompletionIntent::ThreadSummarization), + temperature: AgentSettings::temperature_for_model(&model, cx), + ..Default::default() + }; + + for message in &self.messages { + request.messages.extend(message.to_request()); + } + + request.messages.push(LanguageModelRequestMessage { + role: Role::User, + content: vec![SUMMARIZE_THREAD_PROMPT.into()], + cache: false, + }); + Some(cx.spawn(async move |this, cx| { + let mut title = String::new(); + let mut messages = model.stream_completion(request, cx).await?; + while let Some(event) = messages.next().await { + let event = event?; + let text = match event { + LanguageModelCompletionEvent::Text(text) => text, + LanguageModelCompletionEvent::StatusUpdate( + CompletionRequestStatus::UsageUpdated { .. }, + ) => { + // this.update(cx, |thread, cx| { + // thread.update_model_request_usage(amount as u32, limit, cx); + // })?; + // TODO: handle usage update + continue; + } + _ => continue, + }; + + let mut lines = text.lines(); + title.extend(lines.next()); + + // Stop if the LLM generated multiple lines. + if lines.next().is_some() { + break; + } + } + + log::info!("Setting title: {}", title); + + this.update(cx, |this, cx| { + let title = SharedString::from(title); + event_stream.send_title_update(title.clone()); + this.title = Some(title); + cx.notify(); + }) + })) + } + fn pending_message(&mut self) -> &mut AgentMessage { self.pending_message.get_or_insert_default() } - fn flush_pending_message(&mut self) { + fn flush_pending_message(&mut self, cx: &mut Context<Self>) { let Some(mut message) = self.pending_message.take() else { return; }; @@ -1104,9 +1442,7 @@ impl Thread { tool_use_id: tool_use.id.clone(), tool_name: tool_use.name.clone(), is_error: true, - content: LanguageModelToolResultContent::Text( - "Tool canceled by user".into(), - ), + content: LanguageModelToolResultContent::Text(TOOL_CANCELED_MESSAGE.into()), output: None, }, ); @@ -1114,6 +1450,8 @@ impl Thread { } self.messages.push(Message::Agent(message)); + self.updated_at = Utc::now(); + cx.notify() } pub(crate) fn build_completion_request( @@ -1205,15 +1543,7 @@ impl Thread { ); let mut messages = vec![self.build_system_message(cx)]; for message in &self.messages { - match message { - Message::User(message) => messages.push(message.to_request()), - Message::Agent(message) => messages.extend(message.to_request()), - Message::Resume => messages.push(LanguageModelRequestMessage { - role: Role::User, - content: vec!["Continue where you left off".into()], - cache: false, - }), - } + messages.extend(message.to_request()); } if let Some(message) = self.pending_message.as_ref() { @@ -1367,7 +1697,7 @@ struct RunningTurn { _task: Task<()>, /// The current event stream for the running turn. Used to report a final /// cancellation event if we cancel the turn. - event_stream: AgentResponseEventStream, + event_stream: ThreadEventStream, } impl RunningTurn { @@ -1420,6 +1750,17 @@ where cx: &mut App, ) -> Task<Result<Self::Output>>; + /// Emits events for a previous execution of the tool. + fn replay( + &self, + _input: Self::Input, + _output: Self::Output, + _event_stream: ToolCallEventStream, + _cx: &mut App, + ) -> Result<()> { + Ok(()) + } + fn erase(self) -> Arc<dyn AnyAgentTool> { Arc::new(Erased(Arc::new(self))) } @@ -1447,6 +1788,13 @@ pub trait AnyAgentTool { event_stream: ToolCallEventStream, cx: &mut App, ) -> Task<Result<AgentToolOutput>>; + fn replay( + &self, + input: serde_json::Value, + output: serde_json::Value, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Result<()>; } impl<T> AnyAgentTool for Erased<Arc<T>> @@ -1498,21 +1846,45 @@ where }) }) } + + fn replay( + &self, + input: serde_json::Value, + output: serde_json::Value, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Result<()> { + let input = serde_json::from_value(input)?; + let output = serde_json::from_value(output)?; + self.0.replay(input, output, event_stream, cx) + } } #[derive(Clone)] -struct AgentResponseEventStream(mpsc::UnboundedSender<Result<AgentResponseEvent>>); +struct ThreadEventStream(mpsc::UnboundedSender<Result<ThreadEvent>>); + +impl ThreadEventStream { + fn send_title_update(&self, text: SharedString) { + self.0 + .unbounded_send(Ok(ThreadEvent::TitleUpdate(text))) + .ok(); + } + + fn send_user_message(&self, message: &UserMessage) { + self.0 + .unbounded_send(Ok(ThreadEvent::UserMessage(message.clone()))) + .ok(); + } -impl AgentResponseEventStream { fn send_text(&self, text: &str) { self.0 - .unbounded_send(Ok(AgentResponseEvent::Text(text.to_string()))) + .unbounded_send(Ok(ThreadEvent::AgentText(text.to_string()))) .ok(); } fn send_thinking(&self, text: &str) { self.0 - .unbounded_send(Ok(AgentResponseEvent::Thinking(text.to_string()))) + .unbounded_send(Ok(ThreadEvent::AgentThinking(text.to_string()))) .ok(); } @@ -1524,7 +1896,7 @@ impl AgentResponseEventStream { input: serde_json::Value, ) { self.0 - .unbounded_send(Ok(AgentResponseEvent::ToolCall(Self::initial_tool_call( + .unbounded_send(Ok(ThreadEvent::ToolCall(Self::initial_tool_call( id, title.to_string(), kind, @@ -1557,7 +1929,7 @@ impl AgentResponseEventStream { fields: acp::ToolCallUpdateFields, ) { self.0 - .unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate( + .unbounded_send(Ok(ThreadEvent::ToolCallUpdate( acp::ToolCallUpdate { id: acp::ToolCallId(tool_use_id.to_string().into()), fields, @@ -1568,26 +1940,24 @@ impl AgentResponseEventStream { } fn send_retry(&self, status: acp_thread::RetryStatus) { - self.0 - .unbounded_send(Ok(AgentResponseEvent::Retry(status))) - .ok(); + self.0.unbounded_send(Ok(ThreadEvent::Retry(status))).ok(); } fn send_stop(&self, reason: StopReason) { match reason { StopReason::EndTurn => { self.0 - .unbounded_send(Ok(AgentResponseEvent::Stop(acp::StopReason::EndTurn))) + .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::EndTurn))) .ok(); } StopReason::MaxTokens => { self.0 - .unbounded_send(Ok(AgentResponseEvent::Stop(acp::StopReason::MaxTokens))) + .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::MaxTokens))) .ok(); } StopReason::Refusal => { self.0 - .unbounded_send(Ok(AgentResponseEvent::Stop(acp::StopReason::Refusal))) + .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::Refusal))) .ok(); } StopReason::ToolUse => {} @@ -1596,7 +1966,7 @@ impl AgentResponseEventStream { fn send_canceled(&self) { self.0 - .unbounded_send(Ok(AgentResponseEvent::Stop(acp::StopReason::Canceled))) + .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::Canceled))) .ok(); } @@ -1608,24 +1978,23 @@ impl AgentResponseEventStream { #[derive(Clone)] pub struct ToolCallEventStream { tool_use_id: LanguageModelToolUseId, - stream: AgentResponseEventStream, + stream: ThreadEventStream, fs: Option<Arc<dyn Fs>>, } impl ToolCallEventStream { #[cfg(test)] pub fn test() -> (Self, ToolCallEventStreamReceiver) { - let (events_tx, events_rx) = mpsc::unbounded::<Result<AgentResponseEvent>>(); + let (events_tx, events_rx) = mpsc::unbounded::<Result<ThreadEvent>>(); - let stream = - ToolCallEventStream::new("test_id".into(), AgentResponseEventStream(events_tx), None); + let stream = ToolCallEventStream::new("test_id".into(), ThreadEventStream(events_tx), None); (stream, ToolCallEventStreamReceiver(events_rx)) } fn new( tool_use_id: LanguageModelToolUseId, - stream: AgentResponseEventStream, + stream: ThreadEventStream, fs: Option<Arc<dyn Fs>>, ) -> Self { Self { @@ -1643,7 +2012,7 @@ impl ToolCallEventStream { pub fn update_diff(&self, diff: Entity<acp_thread::Diff>) { self.stream .0 - .unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate( + .unbounded_send(Ok(ThreadEvent::ToolCallUpdate( acp_thread::ToolCallUpdateDiff { id: acp::ToolCallId(self.tool_use_id.to_string().into()), diff, @@ -1656,7 +2025,7 @@ impl ToolCallEventStream { pub fn update_terminal(&self, terminal: Entity<acp_thread::Terminal>) { self.stream .0 - .unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate( + .unbounded_send(Ok(ThreadEvent::ToolCallUpdate( acp_thread::ToolCallUpdateTerminal { id: acp::ToolCallId(self.tool_use_id.to_string().into()), terminal, @@ -1674,7 +2043,7 @@ impl ToolCallEventStream { let (response_tx, response_rx) = oneshot::channel(); self.stream .0 - .unbounded_send(Ok(AgentResponseEvent::ToolCallAuthorization( + .unbounded_send(Ok(ThreadEvent::ToolCallAuthorization( ToolCallAuthorization { tool_call: acp::ToolCallUpdate { id: acp::ToolCallId(self.tool_use_id.to_string().into()), @@ -1724,13 +2093,13 @@ impl ToolCallEventStream { } #[cfg(test)] -pub struct ToolCallEventStreamReceiver(mpsc::UnboundedReceiver<Result<AgentResponseEvent>>); +pub struct ToolCallEventStreamReceiver(mpsc::UnboundedReceiver<Result<ThreadEvent>>); #[cfg(test)] impl ToolCallEventStreamReceiver { pub async fn expect_authorization(&mut self) -> ToolCallAuthorization { let event = self.0.next().await; - if let Some(Ok(AgentResponseEvent::ToolCallAuthorization(auth))) = event { + if let Some(Ok(ThreadEvent::ToolCallAuthorization(auth))) = event { auth } else { panic!("Expected ToolCallAuthorization but got: {:?}", event); @@ -1739,9 +2108,9 @@ impl ToolCallEventStreamReceiver { pub async fn expect_terminal(&mut self) -> Entity<acp_thread::Terminal> { let event = self.0.next().await; - if let Some(Ok(AgentResponseEvent::ToolCallUpdate( - acp_thread::ToolCallUpdate::UpdateTerminal(update), - ))) = event + if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal( + update, + )))) = event { update.terminal } else { @@ -1752,7 +2121,7 @@ impl ToolCallEventStreamReceiver { #[cfg(test)] impl std::ops::Deref for ToolCallEventStreamReceiver { - type Target = mpsc::UnboundedReceiver<Result<AgentResponseEvent>>; + type Target = mpsc::UnboundedReceiver<Result<ThreadEvent>>; fn deref(&self) -> &Self::Target { &self.0 @@ -1821,6 +2190,38 @@ impl From<acp::ContentBlock> for UserMessageContent { } } +impl From<UserMessageContent> for acp::ContentBlock { + fn from(content: UserMessageContent) -> Self { + match content { + UserMessageContent::Text(text) => acp::ContentBlock::Text(acp::TextContent { + text, + annotations: None, + }), + UserMessageContent::Image(image) => acp::ContentBlock::Image(acp::ImageContent { + data: image.source.to_string(), + mime_type: "image/png".to_string(), + annotations: None, + uri: None, + }), + UserMessageContent::Mention { uri, content } => { + acp::ContentBlock::ResourceLink(acp::ResourceLink { + uri: uri.to_uri().to_string(), + name: uri.name(), + annotations: None, + description: if content.is_empty() { + None + } else { + Some(content) + }, + mime_type: None, + size: None, + title: None, + }) + } + } + } +} + fn convert_image(image_content: acp::ImageContent) -> LanguageModelImage { LanguageModelImage { source: image_content.data.into(), diff --git a/crates/agent2/src/tools/context_server_registry.rs b/crates/agent2/src/tools/context_server_registry.rs index ddeb08a046..69c4221a81 100644 --- a/crates/agent2/src/tools/context_server_registry.rs +++ b/crates/agent2/src/tools/context_server_registry.rs @@ -228,4 +228,14 @@ impl AnyAgentTool for ContextServerTool { }) }) } + + fn replay( + &self, + _input: serde_json::Value, + _output: serde_json::Value, + _event_stream: ToolCallEventStream, + _cx: &mut App, + ) -> Result<()> { + Ok(()) + } } diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 7687d68702..b3b1a428bf 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -5,10 +5,10 @@ use anyhow::{Context as _, Result, anyhow}; use assistant_tools::edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat}; use cloud_llm_client::CompletionIntent; use collections::HashSet; -use gpui::{App, AppContext, AsyncApp, Entity, Task}; +use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; use indoc::formatdoc; -use language::ToPoint; use language::language_settings::{self, FormatOnSave}; +use language::{LanguageRegistry, ToPoint}; use language_model::LanguageModelToolResultContent; use paths; use project::lsp_store::{FormatTrigger, LspFormatTarget}; @@ -98,11 +98,13 @@ pub enum EditFileMode { #[derive(Debug, Serialize, Deserialize)] pub struct EditFileToolOutput { + #[serde(alias = "original_path")] input_path: PathBuf, - project_path: PathBuf, new_text: String, old_text: Arc<String>, + #[serde(default)] diff: String, + #[serde(alias = "raw_output")] edit_agent_output: EditAgentOutput, } @@ -122,12 +124,16 @@ impl From<EditFileToolOutput> for LanguageModelToolResultContent { } pub struct EditFileTool { - thread: Entity<Thread>, + thread: WeakEntity<Thread>, + language_registry: Arc<LanguageRegistry>, } impl EditFileTool { - pub fn new(thread: Entity<Thread>) -> Self { - Self { thread } + pub fn new(thread: WeakEntity<Thread>, language_registry: Arc<LanguageRegistry>) -> Self { + Self { + thread, + language_registry, + } } fn authorize( @@ -167,8 +173,11 @@ impl EditFileTool { // Check if path is inside the global config directory // First check if it's already inside project - if not, try to canonicalize - let thread = self.thread.read(cx); - let project_path = thread.project().read(cx).find_project_path(&input.path, cx); + let Ok(project_path) = self.thread.read_with(cx, |thread, cx| { + thread.project().read(cx).find_project_path(&input.path, cx) + }) else { + return Task::ready(Err(anyhow!("thread was dropped"))); + }; // If the path is inside the project, and it's not one of the above edge cases, // then no confirmation is necessary. Otherwise, confirmation is necessary. @@ -221,7 +230,12 @@ impl AgentTool for EditFileTool { event_stream: ToolCallEventStream, cx: &mut App, ) -> Task<Result<Self::Output>> { - let project = self.thread.read(cx).project().clone(); + let Ok(project) = self + .thread + .read_with(cx, |thread, _cx| thread.project().clone()) + else { + return Task::ready(Err(anyhow!("thread was dropped"))); + }; let project_path = match resolve_path(&input, project.clone(), cx) { Ok(path) => path, Err(err) => return Task::ready(Err(anyhow!(err))), @@ -237,23 +251,17 @@ impl AgentTool for EditFileTool { }); } - let Some(request) = self.thread.update(cx, |thread, cx| { - thread - .build_completion_request(CompletionIntent::ToolResults, cx) - .ok() - }) else { - return Task::ready(Err(anyhow!("Failed to build completion request"))); - }; - let thread = self.thread.read(cx); - let Some(model) = thread.model().cloned() else { - return Task::ready(Err(anyhow!("No language model configured"))); - }; - let action_log = thread.action_log().clone(); - let authorize = self.authorize(&input, &event_stream, cx); cx.spawn(async move |cx: &mut AsyncApp| { authorize.await?; + let (request, model, action_log) = self.thread.update(cx, |thread, cx| { + let request = thread.build_completion_request(CompletionIntent::ToolResults, cx); + (request, thread.model().cloned(), thread.action_log().clone()) + })?; + let request = request?; + let model = model.context("No language model configured")?; + let edit_format = EditFormat::from_model(model.clone())?; let edit_agent = EditAgent::new( model, @@ -419,7 +427,6 @@ impl AgentTool for EditFileTool { Ok(EditFileToolOutput { input_path: input.path, - project_path: project_path.path.to_path_buf(), new_text: new_text.clone(), old_text, diff: unified_diff, @@ -427,6 +434,25 @@ impl AgentTool for EditFileTool { }) }) } + + fn replay( + &self, + _input: Self::Input, + output: Self::Output, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Result<()> { + event_stream.update_diff(cx.new(|cx| { + Diff::finalized( + output.input_path, + Some(output.old_text.to_string()), + output.new_text, + self.language_registry.clone(), + cx, + ) + })); + Ok(()) + } } /// Validate that the file path is valid, meaning: @@ -515,6 +541,7 @@ mod tests { let fs = project::FakeFs::new(cx.executor()); fs.insert_tree("/root", json!({})).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); @@ -527,6 +554,7 @@ mod tests { action_log, Templates::new(), Some(model), + None, cx, ) }); @@ -537,7 +565,11 @@ mod tests { path: "root/nonexistent_file.txt".into(), mode: EditFileMode::Edit, }; - Arc::new(EditFileTool { thread }).run(input, ToolCallEventStream::test().0, cx) + Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run( + input, + ToolCallEventStream::test().0, + cx, + ) }) .await; assert_eq!( @@ -724,6 +756,7 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), + None, cx, ) }); @@ -750,9 +783,10 @@ mod tests { path: "root/src/main.rs".into(), mode: EditFileMode::Overwrite, }; - Arc::new(EditFileTool { - thread: thread.clone(), - }) + Arc::new(EditFileTool::new( + thread.downgrade(), + language_registry.clone(), + )) .run(input, ToolCallEventStream::test().0, cx) }); @@ -806,7 +840,11 @@ mod tests { path: "root/src/main.rs".into(), mode: EditFileMode::Overwrite, }; - Arc::new(EditFileTool { thread }).run(input, ToolCallEventStream::test().0, cx) + Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run( + input, + ToolCallEventStream::test().0, + cx, + ) }); // Stream the unformatted content @@ -850,6 +888,7 @@ mod tests { let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); + let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let action_log = cx.new(|_| ActionLog::new(project.clone())); let model = Arc::new(FakeLanguageModel::default()); let thread = cx.new(|cx| { @@ -860,6 +899,7 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), + None, cx, ) }); @@ -887,9 +927,10 @@ mod tests { path: "root/src/main.rs".into(), mode: EditFileMode::Overwrite, }; - Arc::new(EditFileTool { - thread: thread.clone(), - }) + Arc::new(EditFileTool::new( + thread.downgrade(), + language_registry.clone(), + )) .run(input, ToolCallEventStream::test().0, cx) }); @@ -938,10 +979,11 @@ mod tests { path: "root/src/main.rs".into(), mode: EditFileMode::Overwrite, }; - Arc::new(EditFileTool { - thread: thread.clone(), - }) - .run(input, ToolCallEventStream::test().0, cx) + Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run( + input, + ToolCallEventStream::test().0, + cx, + ) }); // Stream the content with trailing whitespace @@ -976,6 +1018,7 @@ mod tests { let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); + let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let action_log = cx.new(|_| ActionLog::new(project.clone())); let model = Arc::new(FakeLanguageModel::default()); let thread = cx.new(|cx| { @@ -986,10 +1029,11 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), + None, cx, ) }); - let tool = Arc::new(EditFileTool { thread }); + let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); fs.insert_tree("/root", json!({})).await; // Test 1: Path with .zed component should require confirmation @@ -1111,6 +1155,7 @@ mod tests { let fs = project::FakeFs::new(cx.executor()); fs.insert_tree("/project", json!({})).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let action_log = cx.new(|_| ActionLog::new(project.clone())); @@ -1123,10 +1168,11 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), + None, cx, ) }); - let tool = Arc::new(EditFileTool { thread }); + let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); // Test global config paths - these should require confirmation if they exist and are outside the project let test_cases = vec![ @@ -1220,7 +1266,7 @@ mod tests { cx, ) .await; - + let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); @@ -1233,10 +1279,11 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), + None, cx, ) }); - let tool = Arc::new(EditFileTool { thread }); + let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); // Test files in different worktrees let test_cases = vec![ @@ -1302,6 +1349,7 @@ mod tests { ) .await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); @@ -1314,10 +1362,11 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), + None, cx, ) }); - let tool = Arc::new(EditFileTool { thread }); + let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); // Test edge cases let test_cases = vec![ @@ -1386,6 +1435,7 @@ mod tests { ) .await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); @@ -1398,10 +1448,11 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), + None, cx, ) }); - let tool = Arc::new(EditFileTool { thread }); + let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); // Test different EditFileMode values let modes = vec![ @@ -1467,6 +1518,7 @@ mod tests { init_test(cx); let fs = project::FakeFs::new(cx.executor()); let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); @@ -1479,10 +1531,11 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), + None, cx, ) }); - let tool = Arc::new(EditFileTool { thread }); + let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); assert_eq!( tool.initial_title(Err(json!({ diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs index ac79874c36..1804d0ab30 100644 --- a/crates/agent2/src/tools/terminal_tool.rs +++ b/crates/agent2/src/tools/terminal_tool.rs @@ -319,7 +319,7 @@ mod tests { use theme::ThemeSettings; use util::test::TempTree; - use crate::AgentResponseEvent; + use crate::ThreadEvent; use super::*; @@ -396,7 +396,7 @@ mod tests { }); cx.run_until_parked(); let event = stream_rx.try_next(); - if let Ok(Some(Ok(AgentResponseEvent::ToolCallAuthorization(auth)))) = event { + if let Ok(Some(Ok(ThreadEvent::ToolCallAuthorization(auth)))) = event { auth.response.send(auth.options[0].id.clone()).unwrap(); } diff --git a/crates/agent2/src/tools/web_search_tool.rs b/crates/agent2/src/tools/web_search_tool.rs index c1c0970742..d71a128bfe 100644 --- a/crates/agent2/src/tools/web_search_tool.rs +++ b/crates/agent2/src/tools/web_search_tool.rs @@ -80,33 +80,48 @@ impl AgentTool for WebSearchTool { } }; - let result_text = if response.results.len() == 1 { - "1 result".to_string() - } else { - format!("{} results", response.results.len()) - }; - event_stream.update_fields(acp::ToolCallUpdateFields { - title: Some(format!("Searched the web: {result_text}")), - content: Some( - response - .results - .iter() - .map(|result| acp::ToolCallContent::Content { - content: acp::ContentBlock::ResourceLink(acp::ResourceLink { - name: result.title.clone(), - uri: result.url.clone(), - title: Some(result.title.clone()), - description: Some(result.text.clone()), - mime_type: None, - annotations: None, - size: None, - }), - }) - .collect(), - ), - ..Default::default() - }); + emit_update(&response, &event_stream); Ok(WebSearchToolOutput(response)) }) } + + fn replay( + &self, + _input: Self::Input, + output: Self::Output, + event_stream: ToolCallEventStream, + _cx: &mut App, + ) -> Result<()> { + emit_update(&output.0, &event_stream); + Ok(()) + } +} + +fn emit_update(response: &WebSearchResponse, event_stream: &ToolCallEventStream) { + let result_text = if response.results.len() == 1 { + "1 result".to_string() + } else { + format!("{} results", response.results.len()) + }; + event_stream.update_fields(acp::ToolCallUpdateFields { + title: Some(format!("Searched the web: {result_text}")), + content: Some( + response + .results + .iter() + .map(|result| acp::ToolCallContent::Content { + content: acp::ContentBlock::ResourceLink(acp::ResourceLink { + name: result.title.clone(), + uri: result.url.clone(), + title: Some(result.title.clone()), + description: Some(result.text.clone()), + mime_type: None, + annotations: None, + size: None, + }), + }) + .collect(), + ), + ..Default::default() + }); } diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index 886f650470..cbc874057a 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -18,6 +18,7 @@ doctest = false [dependencies] acp_thread.workspace = true +action_log.workspace = true agent-client-protocol.workspace = true agent_settings.workspace = true agentic-coding-protocol.workspace = true diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index 551e9fa01a..aa80f01c15 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -1,4 +1,5 @@ // Translates old acp agents into the new schema +use action_log::ActionLog; use agent_client_protocol as acp; use agentic_coding_protocol::{self as acp_old, AgentRequest as _}; use anyhow::{Context as _, Result, anyhow}; @@ -443,7 +444,8 @@ impl AgentConnection for AcpConnection { cx.update(|cx| { let thread = cx.new(|cx| { let session_id = acp::SessionId("acp-old-no-id".into()); - AcpThread::new(self.name, self.clone(), project, session_id, cx) + let action_log = cx.new(|_| ActionLog::new(project.clone())); + AcpThread::new(self.name, self.clone(), project, action_log, session_id) }); current_thread.replace(thread.downgrade()); thread diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index 93a5ae757a..d749537c4c 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -1,3 +1,4 @@ +use action_log::ActionLog; use agent_client_protocol::{self as acp, Agent as _}; use anyhow::anyhow; use collections::HashMap; @@ -153,14 +154,14 @@ impl AgentConnection for AcpConnection { })?; let session_id = response.session_id; - - let thread = cx.new(|cx| { + let action_log = cx.new(|_| ActionLog::new(project.clone()))?; + let thread = cx.new(|_cx| { AcpThread::new( self.server_name, self.clone(), project, + action_log, session_id.clone(), - cx, ) })?; diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 34d55f39dc..f27c973ad6 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -1,6 +1,7 @@ mod mcp_server; pub mod tools; +use action_log::ActionLog; use collections::HashMap; use context_server::listener::McpServerTool; use language_models::provider::anthropic::AnthropicLanguageModelProvider; @@ -215,8 +216,15 @@ impl AgentConnection for ClaudeAgentConnection { } }); - let thread = cx.new(|cx| { - AcpThread::new("Claude Code", self.clone(), project, session_id.clone(), cx) + let action_log = cx.new(|_| ActionLog::new(project.clone()))?; + let thread = cx.new(|_cx| { + AcpThread::new( + "Claude Code", + self.clone(), + project, + action_log, + session_id.clone(), + ) })?; thread_tx.send(thread.downgrade())?; diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index ad0920bc4a..150f1ea73b 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -303,8 +303,13 @@ impl AcpThreadView { let action_log_subscription = cx.observe(&action_log, |_, _, cx| cx.notify()); - this.list_state - .splice(0..0, thread.read(cx).entries().len()); + let count = thread.read(cx).entries().len(); + this.list_state.splice(0..0, count); + this.entry_view_state.update(cx, |view_state, cx| { + for ix in 0..count { + view_state.sync_entry(ix, &thread, window, cx); + } + }); AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx); @@ -808,6 +813,7 @@ impl AcpThreadView { self.thread_retry_status.take(); self.thread_state = ThreadState::ServerExited { status: *status }; } + AcpThreadEvent::TitleUpdated => {} } cx.notify(); } @@ -2816,12 +2822,15 @@ impl AcpThreadView { return; }; - thread.update(cx, |thread, _cx| { + thread.update(cx, |thread, cx| { let current_mode = thread.completion_mode(); - thread.set_completion_mode(match current_mode { - CompletionMode::Burn => CompletionMode::Normal, - CompletionMode::Normal => CompletionMode::Burn, - }); + thread.set_completion_mode( + match current_mode { + CompletionMode::Burn => CompletionMode::Normal, + CompletionMode::Normal => CompletionMode::Burn, + }, + cx, + ); }); } @@ -3572,8 +3581,9 @@ impl AcpThreadView { )) .on_click({ cx.listener(move |this, _, _window, cx| { - thread.update(cx, |thread, _cx| { - thread.set_completion_mode(CompletionMode::Burn); + thread.update(cx, |thread, cx| { + thread + .set_completion_mode(CompletionMode::Burn, cx); }); this.resume_chat(cx); }) @@ -4156,12 +4166,13 @@ pub(crate) mod tests { cx: &mut gpui::App, ) -> Task<gpui::Result<Entity<AcpThread>>> { Task::ready(Ok(cx.new(|cx| { + let action_log = cx.new(|_| ActionLog::new(project.clone())); AcpThread::new( "SaboteurAgentConnection", self, project, + action_log, SessionId("test".into()), - cx, ) }))) } diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index f474fdf3ae..5b4f1038e2 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -199,24 +199,21 @@ impl AgentDiffPane { let action_log = thread.action_log(cx).clone(); let mut this = Self { - _subscriptions: [ - Some( - cx.observe_in(&action_log, window, |this, _action_log, window, cx| { - this.update_excerpts(window, cx) - }), - ), + _subscriptions: vec![ + cx.observe_in(&action_log, window, |this, _action_log, window, cx| { + this.update_excerpts(window, cx) + }), match &thread { - AgentDiffThread::Native(thread) => { - Some(cx.subscribe(thread, |this, _thread, event, cx| { - this.handle_thread_event(event, cx) - })) - } - AgentDiffThread::AcpThread(_) => None, + AgentDiffThread::Native(thread) => cx + .subscribe(thread, |this, _thread, event, cx| { + this.handle_native_thread_event(event, cx) + }), + AgentDiffThread::AcpThread(thread) => cx + .subscribe(thread, |this, _thread, event, cx| { + this.handle_acp_thread_event(event, cx) + }), }, - ] - .into_iter() - .flatten() - .collect(), + ], title: SharedString::default(), multibuffer, editor, @@ -324,13 +321,20 @@ impl AgentDiffPane { } } - fn handle_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context<Self>) { + fn handle_native_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context<Self>) { match event { ThreadEvent::SummaryGenerated => self.update_title(cx), _ => {} } } + fn handle_acp_thread_event(&mut self, event: &AcpThreadEvent, cx: &mut Context<Self>) { + match event { + AcpThreadEvent::TitleUpdated => self.update_title(cx), + _ => {} + } + } + pub fn move_to_path(&self, path_key: PathKey, window: &mut Window, cx: &mut App) { if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) { self.editor.update(cx, |editor, cx| { @@ -1523,7 +1527,8 @@ impl AgentDiff { AcpThreadEvent::Stopped | AcpThreadEvent::Error | AcpThreadEvent::ServerExited(_) => { self.update_reviewing_editors(workspace, window, cx); } - AcpThreadEvent::EntriesRemoved(_) + AcpThreadEvent::TitleUpdated + | AcpThreadEvent::EntriesRemoved(_) | AcpThreadEvent::ToolAuthorizationRequired | AcpThreadEvent::Retry(_) => {} } From 1444cd9839dcd04f60bb3ba2284be2183cae567d Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Tue, 19 Aug 2025 10:53:10 -0400 Subject: [PATCH 483/693] Fix Windows test failures not being detected in CI (#36446) Bug introduced in #35926 Release Notes: - N/A --- .github/actions/run_tests_windows/action.yml | 1 - crates/acp_thread/src/acp_thread.rs | 14 ++- crates/acp_thread/src/mention.rs | 83 ++++++++--------- crates/agent_ui/src/acp/message_editor.rs | 93 +++++++------------- crates/fs/src/fake_git_repo.rs | 6 +- crates/fs/src/fs.rs | 4 +- 6 files changed, 87 insertions(+), 114 deletions(-) diff --git a/.github/actions/run_tests_windows/action.yml b/.github/actions/run_tests_windows/action.yml index e3e3b7142e..0a550c7d32 100644 --- a/.github/actions/run_tests_windows/action.yml +++ b/.github/actions/run_tests_windows/action.yml @@ -56,7 +56,6 @@ runs: $env:COMPlus_CreateDumpDiagnostics = "1" cargo nextest run --workspace --no-fail-fast - continue-on-error: true - name: Analyze crash dumps if: always() diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 7d70727252..1de8110f07 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -2155,7 +2155,7 @@ mod tests { "} ); }); - assert_eq!(fs.files(), vec![Path::new("/test/file-0")]); + assert_eq!(fs.files(), vec![Path::new(path!("/test/file-0"))]); cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["ipsum".into()], cx))) .await @@ -2185,7 +2185,10 @@ mod tests { }); assert_eq!( fs.files(), - vec![Path::new("/test/file-0"), Path::new("/test/file-1")] + vec![ + Path::new(path!("/test/file-0")), + Path::new(path!("/test/file-1")) + ] ); // Checkpoint isn't stored when there are no changes. @@ -2226,7 +2229,10 @@ mod tests { }); assert_eq!( fs.files(), - vec![Path::new("/test/file-0"), Path::new("/test/file-1")] + vec![ + Path::new(path!("/test/file-0")), + Path::new(path!("/test/file-1")) + ] ); // Rewinding the conversation truncates the history and restores the checkpoint. @@ -2254,7 +2260,7 @@ mod tests { "} ); }); - assert_eq!(fs.files(), vec![Path::new("/test/file-0")]); + assert_eq!(fs.files(), vec![Path::new(path!("/test/file-0"))]); } async fn run_until_first_tool_call( diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 350785ec1e..fcf50b0fd7 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -52,6 +52,7 @@ impl MentionUri { let path = url.path(); match url.scheme() { "file" => { + let path = url.to_file_path().ok().context("Extracting file path")?; if let Some(fragment) = url.fragment() { let range = fragment .strip_prefix("L") @@ -72,23 +73,17 @@ impl MentionUri { if let Some(name) = single_query_param(&url, "symbol")? { Ok(Self::Symbol { name, - path: path.into(), + path, line_range, }) } else { - Ok(Self::Selection { - path: path.into(), - line_range, - }) + Ok(Self::Selection { path, line_range }) } } else { - let abs_path = - PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path)); - if input.ends_with("/") { - Ok(Self::Directory { abs_path }) + Ok(Self::Directory { abs_path: path }) } else { - Ok(Self::File { abs_path }) + Ok(Self::File { abs_path: path }) } } } @@ -162,27 +157,17 @@ impl MentionUri { pub fn to_uri(&self) -> Url { match self { MentionUri::File { abs_path } => { - let mut url = Url::parse("file:///").unwrap(); - let path = abs_path.to_string_lossy(); - url.set_path(&path); - url + Url::from_file_path(abs_path).expect("mention path should be absolute") } MentionUri::Directory { abs_path } => { - let mut url = Url::parse("file:///").unwrap(); - let mut path = abs_path.to_string_lossy().to_string(); - if !path.ends_with("/") { - path.push_str("/"); - } - url.set_path(&path); - url + Url::from_directory_path(abs_path).expect("mention path should be absolute") } MentionUri::Symbol { path, name, line_range, } => { - let mut url = Url::parse("file:///").unwrap(); - url.set_path(&path.to_string_lossy()); + let mut url = Url::from_file_path(path).expect("mention path should be absolute"); url.query_pairs_mut().append_pair("symbol", name); url.set_fragment(Some(&format!( "L{}:{}", @@ -192,8 +177,7 @@ impl MentionUri { url } MentionUri::Selection { path, line_range } => { - let mut url = Url::parse("file:///").unwrap(); - url.set_path(&path.to_string_lossy()); + let mut url = Url::from_file_path(path).expect("mention path should be absolute"); url.set_fragment(Some(&format!( "L{}:{}", line_range.start + 1, @@ -266,15 +250,17 @@ pub fn selection_name(path: &Path, line_range: &Range<u32>) -> String { #[cfg(test)] mod tests { + use util::{path, uri}; + use super::*; #[test] fn test_parse_file_uri() { - let file_uri = "file:///path/to/file.rs"; + let file_uri = uri!("file:///path/to/file.rs"); let parsed = MentionUri::parse(file_uri).unwrap(); match &parsed { MentionUri::File { abs_path } => { - assert_eq!(abs_path.to_str().unwrap(), "/path/to/file.rs"); + assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/file.rs")); } _ => panic!("Expected File variant"), } @@ -283,11 +269,11 @@ mod tests { #[test] fn test_parse_directory_uri() { - let file_uri = "file:///path/to/dir/"; + let file_uri = uri!("file:///path/to/dir/"); let parsed = MentionUri::parse(file_uri).unwrap(); match &parsed { MentionUri::Directory { abs_path } => { - assert_eq!(abs_path.to_str().unwrap(), "/path/to/dir/"); + assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/dir/")); } _ => panic!("Expected Directory variant"), } @@ -297,22 +283,24 @@ mod tests { #[test] fn test_to_directory_uri_with_slash() { let uri = MentionUri::Directory { - abs_path: PathBuf::from("/path/to/dir/"), + abs_path: PathBuf::from(path!("/path/to/dir/")), }; - assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/"); + let expected = uri!("file:///path/to/dir/"); + assert_eq!(uri.to_uri().to_string(), expected); } #[test] fn test_to_directory_uri_without_slash() { let uri = MentionUri::Directory { - abs_path: PathBuf::from("/path/to/dir"), + abs_path: PathBuf::from(path!("/path/to/dir")), }; - assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/"); + let expected = uri!("file:///path/to/dir/"); + assert_eq!(uri.to_uri().to_string(), expected); } #[test] fn test_parse_symbol_uri() { - let symbol_uri = "file:///path/to/file.rs?symbol=MySymbol#L10:20"; + let symbol_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20"); let parsed = MentionUri::parse(symbol_uri).unwrap(); match &parsed { MentionUri::Symbol { @@ -320,7 +308,7 @@ mod tests { name, line_range, } => { - assert_eq!(path.to_str().unwrap(), "/path/to/file.rs"); + assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs")); assert_eq!(name, "MySymbol"); assert_eq!(line_range.start, 9); assert_eq!(line_range.end, 19); @@ -332,11 +320,11 @@ mod tests { #[test] fn test_parse_selection_uri() { - let selection_uri = "file:///path/to/file.rs#L5:15"; + let selection_uri = uri!("file:///path/to/file.rs#L5:15"); let parsed = MentionUri::parse(selection_uri).unwrap(); match &parsed { MentionUri::Selection { path, line_range } => { - assert_eq!(path.to_str().unwrap(), "/path/to/file.rs"); + assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs")); assert_eq!(line_range.start, 4); assert_eq!(line_range.end, 14); } @@ -418,32 +406,35 @@ mod tests { #[test] fn test_invalid_line_range_format() { // Missing L prefix - assert!(MentionUri::parse("file:///path/to/file.rs#10:20").is_err()); + assert!(MentionUri::parse(uri!("file:///path/to/file.rs#10:20")).is_err()); // Missing colon separator - assert!(MentionUri::parse("file:///path/to/file.rs#L1020").is_err()); + assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L1020")).is_err()); // Invalid numbers - assert!(MentionUri::parse("file:///path/to/file.rs#L10:abc").is_err()); - assert!(MentionUri::parse("file:///path/to/file.rs#Labc:20").is_err()); + assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L10:abc")).is_err()); + assert!(MentionUri::parse(uri!("file:///path/to/file.rs#Labc:20")).is_err()); } #[test] fn test_invalid_query_parameters() { // Invalid query parameter name - assert!(MentionUri::parse("file:///path/to/file.rs#L10:20?invalid=test").is_err()); + assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L10:20?invalid=test")).is_err()); // Too many query parameters assert!( - MentionUri::parse("file:///path/to/file.rs#L10:20?symbol=test&another=param").is_err() + MentionUri::parse(uri!( + "file:///path/to/file.rs#L10:20?symbol=test&another=param" + )) + .is_err() ); } #[test] fn test_zero_based_line_numbers() { // Test that 0-based line numbers are rejected (should be 1-based) - assert!(MentionUri::parse("file:///path/to/file.rs#L0:10").is_err()); - assert!(MentionUri::parse("file:///path/to/file.rs#L1:0").is_err()); - assert!(MentionUri::parse("file:///path/to/file.rs#L0:0").is_err()); + assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L0:10")).is_err()); + assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L1:0")).is_err()); + assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L0:0")).is_err()); } } diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 00368d6087..afb1512e5d 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1640,7 +1640,7 @@ mod tests { use serde_json::json; use text::Point; use ui::{App, Context, IntoElement, Render, SharedString, Window}; - use util::path; + use util::{path, uri}; use workspace::{AppState, Item, Workspace}; use crate::acp::{ @@ -1950,13 +1950,12 @@ mod tests { editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); }); + let url_one = uri!("file:///dir/a/one.txt"); editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) "); + let text = editor.text(cx); + assert_eq!(text, format!("Lorem [@one.txt]({url_one}) ")); assert!(!editor.has_visible_completions_menu()); - assert_eq!( - fold_ranges(editor, cx), - vec![Point::new(0, 6)..Point::new(0, 39)] - ); + assert_eq!(fold_ranges(editor, cx).len(), 1); }); let contents = message_editor @@ -1977,47 +1976,35 @@ mod tests { contents, [Mention::Text { content: "1".into(), - uri: "file:///dir/a/one.txt".parse().unwrap() + uri: url_one.parse().unwrap() }] ); cx.simulate_input(" "); editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) "); + let text = editor.text(cx); + assert_eq!(text, format!("Lorem [@one.txt]({url_one}) ")); assert!(!editor.has_visible_completions_menu()); - assert_eq!( - fold_ranges(editor, cx), - vec![Point::new(0, 6)..Point::new(0, 39)] - ); + assert_eq!(fold_ranges(editor, cx).len(), 1); }); cx.simulate_input("Ipsum "); editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum ", - ); + let text = editor.text(cx); + assert_eq!(text, format!("Lorem [@one.txt]({url_one}) Ipsum "),); assert!(!editor.has_visible_completions_menu()); - assert_eq!( - fold_ranges(editor, cx), - vec![Point::new(0, 6)..Point::new(0, 39)] - ); + assert_eq!(fold_ranges(editor, cx).len(), 1); }); cx.simulate_input("@file "); editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum @file ", - ); + let text = editor.text(cx); + assert_eq!(text, format!("Lorem [@one.txt]({url_one}) Ipsum @file "),); assert!(editor.has_visible_completions_menu()); - assert_eq!( - fold_ranges(editor, cx), - vec![Point::new(0, 6)..Point::new(0, 39)] - ); + assert_eq!(fold_ranges(editor, cx).len(), 1); }); editor.update_in(&mut cx, |editor, window, cx| { @@ -2041,28 +2028,23 @@ mod tests { .collect::<Vec<_>>(); assert_eq!(contents.len(), 2); + let url_eight = uri!("file:///dir/b/eight.txt"); pretty_assertions::assert_eq!( contents[1], Mention::Text { content: "8".to_string(), - uri: "file:///dir/b/eight.txt".parse().unwrap(), + uri: url_eight.parse().unwrap(), } ); editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) " - ); - assert!(!editor.has_visible_completions_menu()); - assert_eq!( - fold_ranges(editor, cx), - vec![ - Point::new(0, 6)..Point::new(0, 39), - Point::new(0, 47)..Point::new(0, 84) - ] - ); - }); + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) ") + ); + assert!(!editor.has_visible_completions_menu()); + assert_eq!(fold_ranges(editor, cx).len(), 2); + }); let plain_text_language = Arc::new(language::Language::new( language::LanguageConfig { @@ -2108,7 +2090,7 @@ mod tests { let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>( - |_, _| async move { + move |_, _| async move { Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![ #[allow(deprecated)] lsp::SymbolInformation { @@ -2132,18 +2114,13 @@ mod tests { cx.simulate_input("@symbol "); editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) @symbol " - ); - assert!(editor.has_visible_completions_menu()); - assert_eq!( - current_completion_labels(editor), - &[ - "MySymbol", - ] - ); - }); + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) @symbol ") + ); + assert!(editor.has_visible_completions_menu()); + assert_eq!(current_completion_labels(editor), &["MySymbol"]); + }); editor.update_in(&mut cx, |editor, window, cx| { editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); @@ -2165,9 +2142,7 @@ mod tests { contents[2], Mention::Text { content: "1".into(), - uri: "file:///dir/a/one.txt?symbol=MySymbol#L1:1" - .parse() - .unwrap(), + uri: format!("{url_one}?symbol=MySymbol#L1:1").parse().unwrap(), } ); @@ -2176,7 +2151,7 @@ mod tests { editor.read_with(&cx, |editor, cx| { assert_eq!( editor.text(cx), - "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) [@MySymbol](file:///dir/a/one.txt?symbol=MySymbol#L1:1) " + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") ); }); } diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index f0936d400a..5b093ac6a0 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -590,9 +590,9 @@ mod tests { assert_eq!( fs.files_with_contents(Path::new("")), [ - (Path::new("/bar/baz").into(), b"qux".into()), - (Path::new("/foo/a").into(), b"lorem".into()), - (Path::new("/foo/b").into(), b"ipsum".into()) + (Path::new(path!("/bar/baz")).into(), b"qux".into()), + (Path::new(path!("/foo/a")).into(), b"lorem".into()), + (Path::new(path!("/foo/b")).into(), b"ipsum".into()) ] ); } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 847e98d6c4..399c0f3e32 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -1101,7 +1101,9 @@ impl FakeFsState { ) -> Option<(&mut FakeFsEntry, PathBuf)> { let canonical_path = self.canonicalize(target, follow_symlink)?; - let mut components = canonical_path.components(); + let mut components = canonical_path + .components() + .skip_while(|component| matches!(component, Component::Prefix(_))); let Some(Component::RootDir) = components.next() else { panic!( "the path {:?} was not canonicalized properly {:?}", From 43b4363b34ceb5070ab80343cecd83c55be1e942 Mon Sep 17 00:00:00 2001 From: Smit Barmase <heysmitbarmase@gmail.com> Date: Tue, 19 Aug 2025 20:30:25 +0530 Subject: [PATCH 484/693] lsp: Enable dynamic registration for TextDocumentSyncClientCapabilities post revert (#36494) Follow up: https://github.com/zed-industries/zed/pull/36485 Release Notes: - N/A --- crates/lsp/src/lsp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 366005a4ab..ce9e2fe229 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -827,7 +827,7 @@ impl LanguageServer { }), synchronization: Some(TextDocumentSyncClientCapabilities { did_save: Some(true), - dynamic_registration: Some(false), + dynamic_registration: Some(true), ..TextDocumentSyncClientCapabilities::default() }), code_lens: Some(CodeLensClientCapabilities { From 013eaaeadd9952a8bf3b546a271b7d8e08368e1b Mon Sep 17 00:00:00 2001 From: Lukas Wirth <lukas@zed.dev> Date: Tue, 19 Aug 2025 18:43:42 +0200 Subject: [PATCH 485/693] editor: Render dirty and conflict markers in multibuffer headers (#36489) Release Notes: - Added rendering of status indicators for multi buffer headers --- crates/editor/src/element.rs | 19 +++++++++++++++---- crates/inspector_ui/src/div_inspector.rs | 12 ++++++------ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 915a3cdc38..0922752e44 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -82,7 +82,7 @@ use sum_tree::Bias; use text::{BufferId, SelectionGoal}; use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor}; use ui::{ - ButtonLike, ContextMenu, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*, + ButtonLike, ContextMenu, Indicator, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*, right_click_menu, }; use unicode_segmentation::UnicodeSegmentation; @@ -3563,9 +3563,8 @@ impl EditorElement { cx: &mut App, ) -> impl IntoElement { let editor = self.editor.read(cx); - let file_status = editor - .buffer - .read(cx) + let multi_buffer = editor.buffer.read(cx); + let file_status = multi_buffer .all_diff_hunks_expanded() .then(|| { editor @@ -3575,6 +3574,17 @@ impl EditorElement { .status_for_buffer_id(for_excerpt.buffer_id, cx) }) .flatten(); + let indicator = multi_buffer + .buffer(for_excerpt.buffer_id) + .and_then(|buffer| { + let buffer = buffer.read(cx); + let indicator_color = match (buffer.has_conflict(), buffer.is_dirty()) { + (true, _) => Some(Color::Warning), + (_, true) => Some(Color::Accent), + (false, false) => None, + }; + indicator_color.map(|indicator_color| Indicator::dot().color(indicator_color)) + }); let include_root = editor .project @@ -3683,6 +3693,7 @@ impl EditorElement { }) .take(1), ) + .children(indicator) .child( h_flex() .cursor_pointer() diff --git a/crates/inspector_ui/src/div_inspector.rs b/crates/inspector_ui/src/div_inspector.rs index bd395aa01b..e9460cc9cc 100644 --- a/crates/inspector_ui/src/div_inspector.rs +++ b/crates/inspector_ui/src/div_inspector.rs @@ -395,11 +395,11 @@ impl DivInspector { .zip(self.rust_completion_replace_range.as_ref()) { let before_text = snapshot - .text_for_range(0..completion_range.start.to_offset(&snapshot)) + .text_for_range(0..completion_range.start.to_offset(snapshot)) .collect::<String>(); let after_text = snapshot .text_for_range( - completion_range.end.to_offset(&snapshot) + completion_range.end.to_offset(snapshot) ..snapshot.clip_offset(usize::MAX, Bias::Left), ) .collect::<String>(); @@ -702,10 +702,10 @@ impl CompletionProvider for RustStyleCompletionProvider { } fn completion_replace_range(snapshot: &BufferSnapshot, anchor: &Anchor) -> Option<Range<Anchor>> { - let point = anchor.to_point(&snapshot); - let offset = point.to_offset(&snapshot); - let line_start = Point::new(point.row, 0).to_offset(&snapshot); - let line_end = Point::new(point.row, snapshot.line_len(point.row)).to_offset(&snapshot); + let point = anchor.to_point(snapshot); + let offset = point.to_offset(snapshot); + let line_start = Point::new(point.row, 0).to_offset(snapshot); + let line_end = Point::new(point.row, snapshot.line_len(point.row)).to_offset(snapshot); let mut lines = snapshot.text_for_range(line_start..line_end).lines(); let line = lines.next()?; From d1cabef2bfbe37bea8415d6b32835be9ed108249 Mon Sep 17 00:00:00 2001 From: Lukas Wirth <lukas@zed.dev> Date: Tue, 19 Aug 2025 18:53:45 +0200 Subject: [PATCH 486/693] editor: Fix inline diagnostics min column inaccuracy (#36501) Closes https://github.com/zed-industries/zed/issues/33346 Release Notes: - Fixed `diagnostic.inline.min_column` being inaccurate --- crates/editor/src/element.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 0922752e44..d8fe3ccf15 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2173,11 +2173,13 @@ impl EditorElement { }; let padding = ProjectSettings::get_global(cx).diagnostics.inline.padding as f32 * em_width; - let min_x = ProjectSettings::get_global(cx) - .diagnostics - .inline - .min_column as f32 - * em_width; + let min_x = self.column_pixels( + ProjectSettings::get_global(cx) + .diagnostics + .inline + .min_column as usize, + window, + ); let mut elements = HashMap::default(); for (row, mut diagnostics) in diagnostics_by_rows { From e092aed253a7814f3fb04b4b700e9b65c80ec993 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Tue, 19 Aug 2025 14:25:25 -0300 Subject: [PATCH 487/693] Split external agent flags (#36499) Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 132 +++++++++++++--------- crates/feature_flags/src/feature_flags.rs | 6 + 2 files changed, 82 insertions(+), 56 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 55d07ed495..995bf771e2 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -4,7 +4,6 @@ use std::rc::Rc; use std::sync::Arc; use std::time::Duration; -use agent_servers::AgentServer; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; @@ -44,7 +43,7 @@ use assistant_tool::ToolWorkingSet; use client::{UserStore, zed_urls}; use cloud_llm_client::{CompletionIntent, Plan, UsageLimit}; use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer}; -use feature_flags::{self, FeatureFlagAppExt}; +use feature_flags::{self, AcpFeatureFlag, ClaudeCodeFeatureFlag, FeatureFlagAppExt}; use fs::Fs; use gpui::{ Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem, @@ -971,7 +970,7 @@ impl AgentPanel { let text_thread_store = self.context_store.clone(); cx.spawn_in(window, async move |this, cx| { - let server: Rc<dyn AgentServer> = match agent_choice { + let ext_agent = match agent_choice { Some(agent) => { cx.background_spawn(async move { if let Some(serialized) = @@ -985,10 +984,10 @@ impl AgentPanel { }) .detach(); - agent.server(fs) + agent } - None => cx - .background_spawn(async move { + None => { + cx.background_spawn(async move { KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY) }) .await @@ -999,10 +998,25 @@ impl AgentPanel { }) .unwrap_or_default() .agent - .server(fs), + } }; + let server = ext_agent.server(fs); + this.update_in(cx, |this, window, cx| { + match ext_agent { + crate::ExternalAgent::Gemini | crate::ExternalAgent::NativeAgent => { + if !cx.has_flag::<AcpFeatureFlag>() { + return; + } + } + crate::ExternalAgent::ClaudeCode => { + if !cx.has_flag::<ClaudeCodeFeatureFlag>() { + return; + } + } + } + let thread_view = cx.new(|cx| { crate::acp::AcpThreadView::new( server, @@ -2320,56 +2334,60 @@ impl AgentPanel { ) .separator() .header("External Agents") - .item( - ContextMenuEntry::new("New Gemini Thread") - .icon(IconName::AiGemini) - .icon_color(Color::Muted) - .handler({ - let workspace = workspace.clone(); - move |window, cx| { - if let Some(workspace) = workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = - workspace.panel::<AgentPanel>(cx) - { - panel.update(cx, |panel, cx| { - panel.set_selected_agent( - AgentType::Gemini, - window, - cx, - ); - }); - } - }); + .when(cx.has_flag::<AcpFeatureFlag>(), |menu| { + menu.item( + ContextMenuEntry::new("New Gemini Thread") + .icon(IconName::AiGemini) + .icon_color(Color::Muted) + .handler({ + let workspace = workspace.clone(); + move |window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = + workspace.panel::<AgentPanel>(cx) + { + panel.update(cx, |panel, cx| { + panel.set_selected_agent( + AgentType::Gemini, + window, + cx, + ); + }); + } + }); + } } - } - }), - ) - .item( - ContextMenuEntry::new("New Claude Code Thread") - .icon(IconName::AiClaude) - .icon_color(Color::Muted) - .handler({ - let workspace = workspace.clone(); - move |window, cx| { - if let Some(workspace) = workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = - workspace.panel::<AgentPanel>(cx) - { - panel.update(cx, |panel, cx| { - panel.set_selected_agent( - AgentType::ClaudeCode, - window, - cx, - ); - }); - } - }); + }), + ) + }) + .when(cx.has_flag::<ClaudeCodeFeatureFlag>(), |menu| { + menu.item( + ContextMenuEntry::new("New Claude Code Thread") + .icon(IconName::AiClaude) + .icon_color(Color::Muted) + .handler({ + let workspace = workspace.clone(); + move |window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = + workspace.panel::<AgentPanel>(cx) + { + panel.update(cx, |panel, cx| { + panel.set_selected_agent( + AgentType::ClaudeCode, + window, + cx, + ); + }); + } + }); + } } - } - }), - ); + }), + ) + }); menu })) } @@ -2439,7 +2457,9 @@ impl AgentPanel { } fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { - if cx.has_flag::<feature_flags::AcpFeatureFlag>() { + if cx.has_flag::<feature_flags::AcpFeatureFlag>() + || cx.has_flag::<feature_flags::ClaudeCodeFeatureFlag>() + { self.render_toolbar_new(window, cx).into_any_element() } else { self.render_toolbar_old(window, cx).into_any_element() diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index f87932bfaf..7c12571f24 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -95,6 +95,12 @@ impl FeatureFlag for AcpFeatureFlag { const NAME: &'static str = "acp"; } +pub struct ClaudeCodeFeatureFlag; + +impl FeatureFlag for ClaudeCodeFeatureFlag { + const NAME: &'static str = "claude-code"; +} + pub trait FeatureFlagViewExt<V: 'static> { fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription where From 1af47a563fd11ac83d676dee07f87e2b46fe3649 Mon Sep 17 00:00:00 2001 From: fantacell <ghub@giggo.de> Date: Tue, 19 Aug 2025 19:52:29 +0200 Subject: [PATCH 488/693] helix: Uncomment one test (#36328) There are two tests commented out in the helix file, but one of them works again. I don't know if this is too little a change to be merged, but I wanted to suggest it. The other test might be more complicated though, so I didn't touch it. Release Notes: - N/A --- crates/vim/src/helix.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 0c8c06d8ab..3cc9772d42 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -547,27 +547,27 @@ mod test { ); } - // #[gpui::test] - // async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) { - // let mut cx = VimTestContext::new(cx, true).await; + #[gpui::test] + async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; - // cx.set_state( - // indoc! {" - // The quick brownˇ - // fox jumps over - // the lazy dog."}, - // Mode::HelixNormal, - // ); + cx.set_state( + indoc! {" + The quick brownˇ + fox jumps over + the lazy dog."}, + Mode::HelixNormal, + ); - // cx.simulate_keystrokes("d"); + cx.simulate_keystrokes("d"); - // cx.assert_state( - // indoc! {" - // The quick brownˇfox jumps over - // the lazy dog."}, - // Mode::HelixNormal, - // ); - // } + cx.assert_state( + indoc! {" + The quick brownˇfox jumps over + the lazy dog."}, + Mode::HelixNormal, + ); + } // #[gpui::test] // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) { From 6b6eb116438f055cb6344d510e37138d8b998ccb Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Tue, 19 Aug 2025 20:06:09 +0200 Subject: [PATCH 489/693] agent2: Fix tool schemas for Gemini (#36507) Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga <agus@zed.dev> --- crates/agent2/src/agent2.rs | 1 + crates/agent2/src/thread.rs | 6 ++--- crates/agent2/src/tool_schema.rs | 43 +++++++++++++++++++++++++++++++ crates/google_ai/src/google_ai.rs | 2 +- 4 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 crates/agent2/src/tool_schema.rs diff --git a/crates/agent2/src/agent2.rs b/crates/agent2/src/agent2.rs index f13cd1bd67..8d18da7fe1 100644 --- a/crates/agent2/src/agent2.rs +++ b/crates/agent2/src/agent2.rs @@ -2,6 +2,7 @@ mod agent; mod native_agent_server; mod templates; mod thread; +mod tool_schema; mod tools; #[cfg(test)] diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index aeb600e232..d90d0bd4f8 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1732,8 +1732,8 @@ where fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString; /// Returns the JSON schema that describes the tool's input. - fn input_schema(&self) -> Schema { - schemars::schema_for!(Self::Input) + fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Schema { + crate::tool_schema::root_schema_for::<Self::Input>(format) } /// Some tools rely on a provider for the underlying billing or other reasons. @@ -1819,7 +1819,7 @@ where } fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> { - let mut json = serde_json::to_value(self.0.input_schema())?; + let mut json = serde_json::to_value(self.0.input_schema(format))?; adapt_schema_to_format(&mut json, format)?; Ok(json) } diff --git a/crates/agent2/src/tool_schema.rs b/crates/agent2/src/tool_schema.rs new file mode 100644 index 0000000000..f608336b41 --- /dev/null +++ b/crates/agent2/src/tool_schema.rs @@ -0,0 +1,43 @@ +use language_model::LanguageModelToolSchemaFormat; +use schemars::{ + JsonSchema, Schema, + generate::SchemaSettings, + transform::{Transform, transform_subschemas}, +}; + +pub(crate) fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema { + let mut generator = match format { + LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(), + LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3() + .with(|settings| { + settings.meta_schema = None; + settings.inline_subschemas = true; + }) + .with_transform(ToJsonSchemaSubsetTransform) + .into_generator(), + }; + generator.root_schema_for::<T>() +} + +#[derive(Debug, Clone)] +struct ToJsonSchemaSubsetTransform; + +impl Transform for ToJsonSchemaSubsetTransform { + fn transform(&mut self, schema: &mut Schema) { + // Ensure that the type field is not an array, this happens when we use + // Option<T>, the type will be [T, "null"]. + if let Some(type_field) = schema.get_mut("type") + && let Some(types) = type_field.as_array() + && let Some(first_type) = types.first() + { + *type_field = first_type.clone(); + } + + // oneOf is not supported, use anyOf instead + if let Some(one_of) = schema.remove("oneOf") { + schema.insert("anyOf".to_string(), one_of); + } + + transform_subschemas(self, schema); + } +} diff --git a/crates/google_ai/src/google_ai.rs b/crates/google_ai/src/google_ai.rs index 95a6daa1d9..a1b5ca3a03 100644 --- a/crates/google_ai/src/google_ai.rs +++ b/crates/google_ai/src/google_ai.rs @@ -266,7 +266,7 @@ pub struct CitationMetadata { pub struct PromptFeedback { #[serde(skip_serializing_if = "Option::is_none")] pub block_reason: Option<String>, - pub safety_ratings: Vec<SafetyRating>, + pub safety_ratings: Option<Vec<SafetyRating>>, #[serde(skip_serializing_if = "Option::is_none")] pub block_reason_message: Option<String>, } From 6ba52a3a42cbbb9dc4daa3d3e283ca1f98e11d30 Mon Sep 17 00:00:00 2001 From: Conrad Irwin <conrad.irwin@gmail.com> Date: Tue, 19 Aug 2025 12:08:11 -0600 Subject: [PATCH 490/693] Re-add history entries for native agent threads (#36500) Closes #ISSUE Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra <me@as-cii.com> --- Cargo.lock | 4 + crates/agent/src/thread_store.rs | 2 +- crates/agent2/Cargo.toml | 4 + crates/agent2/src/agent.rs | 200 ++++-- crates/agent2/src/agent2.rs | 4 + crates/agent2/src/db.rs | 470 +++++++++++++ crates/agent2/src/history_store.rs | 314 +++++++++ crates/agent2/src/native_agent_server.rs | 11 +- crates/agent2/src/tests/mod.rs | 4 +- crates/agent2/src/thread.rs | 132 +++- crates/agent2/src/tools/edit_file_tool.rs | 9 - crates/agent_ui/src/acp.rs | 2 + crates/agent_ui/src/acp/thread_history.rs | 766 ++++++++++++++++++++++ crates/agent_ui/src/acp/thread_view.rs | 42 +- crates/agent_ui/src/agent_panel.rs | 154 ++++- crates/agent_ui/src/agent_ui.rs | 8 +- 16 files changed, 2007 insertions(+), 119 deletions(-) create mode 100644 crates/agent2/src/db.rs create mode 100644 crates/agent2/src/history_store.rs create mode 100644 crates/agent_ui/src/acp/thread_history.rs diff --git a/Cargo.lock b/Cargo.lock index dc9d074f01..4a5dec4734 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -196,6 +196,7 @@ dependencies = [ "agent_servers", "agent_settings", "anyhow", + "assistant_context", "assistant_tool", "assistant_tools", "chrono", @@ -223,6 +224,7 @@ dependencies = [ "log", "lsp", "open", + "parking_lot", "paths", "portable-pty", "pretty_assertions", @@ -235,6 +237,7 @@ dependencies = [ "serde_json", "settings", "smol", + "sqlez", "task", "tempfile", "terminal", @@ -251,6 +254,7 @@ dependencies = [ "workspace-hack", "worktree", "zlog", + "zstd", ] [[package]] diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index 96bf639306..ed1605aacf 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -893,7 +893,7 @@ impl ThreadsDatabase { let needs_migration_from_heed = mdb_path.exists(); - let connection = if *ZED_STATELESS { + let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) { Connection::open_memory(Some("THREAD_FALLBACK_DB")) } else { Connection::open_file(&sqlite_path.to_string_lossy()) diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 8129341545..890f7e774b 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -19,6 +19,7 @@ agent-client-protocol.workspace = true agent_servers.workspace = true agent_settings.workspace = true anyhow.workspace = true +assistant_context.workspace = true assistant_tool.workspace = true assistant_tools.workspace = true chrono.workspace = true @@ -39,6 +40,7 @@ language_model.workspace = true language_models.workspace = true log.workspace = true open.workspace = true +parking_lot.workspace = true paths.workspace = true portable-pty.workspace = true project.workspace = true @@ -49,6 +51,7 @@ serde.workspace = true serde_json.workspace = true settings.workspace = true smol.workspace = true +sqlez.workspace = true task.workspace = true terminal.workspace = true text.workspace = true @@ -59,6 +62,7 @@ watch.workspace = true web_search.workspace = true which.workspace = true workspace-hack.workspace = true +zstd.workspace = true [dev-dependencies] agent = { workspace = true, "features" = ["test-support"] } diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 9cf0c3b603..bc46ad1657 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,10 +1,9 @@ use crate::{ - ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DeletePathTool, DiagnosticsTool, - EditFileTool, FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MovePathTool, NowTool, - OpenTool, ReadFileTool, TerminalTool, ThinkingTool, Thread, ThreadEvent, ToolCallAuthorization, - UserMessageContent, WebSearchTool, templates::Templates, + ContextServerRegistry, Thread, ThreadEvent, ToolCallAuthorization, UserMessageContent, + templates::Templates, }; -use acp_thread::AgentModelSelector; +use crate::{HistoryStore, ThreadsDatabase}; +use acp_thread::{AcpThread, AgentModelSelector}; use action_log::ActionLog; use agent_client_protocol as acp; use agent_settings::AgentSettings; @@ -51,7 +50,8 @@ struct Session { thread: Entity<Thread>, /// The ACP thread that handles protocol communication acp_thread: WeakEntity<acp_thread::AcpThread>, - _subscription: Subscription, + pending_save: Task<()>, + _subscriptions: Vec<Subscription>, } pub struct LanguageModels { @@ -155,6 +155,7 @@ impl LanguageModels { pub struct NativeAgent { /// Session ID -> Session mapping sessions: HashMap<acp::SessionId, Session>, + history: Entity<HistoryStore>, /// Shared project context for all threads project_context: Entity<ProjectContext>, project_context_needs_refresh: watch::Sender<()>, @@ -173,6 +174,7 @@ pub struct NativeAgent { impl NativeAgent { pub async fn new( project: Entity<Project>, + history: Entity<HistoryStore>, templates: Arc<Templates>, prompt_store: Option<Entity<PromptStore>>, fs: Arc<dyn Fs>, @@ -200,6 +202,7 @@ impl NativeAgent { watch::channel(()); Self { sessions: HashMap::new(), + history, project_context: cx.new(|_| project_context), project_context_needs_refresh: project_context_needs_refresh_tx, _maintain_project_context: cx.spawn(async move |this, cx| { @@ -218,6 +221,55 @@ impl NativeAgent { }) } + fn register_session( + &mut self, + thread_handle: Entity<Thread>, + cx: &mut Context<Self>, + ) -> Entity<AcpThread> { + let connection = Rc::new(NativeAgentConnection(cx.entity())); + let registry = LanguageModelRegistry::read_global(cx); + let summarization_model = registry.thread_summary_model().map(|c| c.model); + + thread_handle.update(cx, |thread, cx| { + thread.set_summarization_model(summarization_model, cx); + thread.add_default_tools(cx) + }); + + let thread = thread_handle.read(cx); + let session_id = thread.id().clone(); + let title = thread.title(); + let project = thread.project.clone(); + let action_log = thread.action_log.clone(); + let acp_thread = cx.new(|_cx| { + acp_thread::AcpThread::new( + title, + connection, + project.clone(), + action_log.clone(), + session_id.clone(), + ) + }); + let subscriptions = vec![ + cx.observe_release(&acp_thread, |this, acp_thread, _cx| { + this.sessions.remove(acp_thread.session_id()); + }), + cx.observe(&thread_handle, move |this, thread, cx| { + this.save_thread(thread.clone(), cx) + }), + ]; + + self.sessions.insert( + session_id, + Session { + thread: thread_handle, + acp_thread: acp_thread.downgrade(), + _subscriptions: subscriptions, + pending_save: Task::ready(()), + }, + ); + acp_thread + } + pub fn models(&self) -> &LanguageModels { &self.models } @@ -444,6 +496,63 @@ impl NativeAgent { }); } } + + pub fn open_thread( + &mut self, + id: acp::SessionId, + cx: &mut Context<Self>, + ) -> Task<Result<Entity<AcpThread>>> { + let database_future = ThreadsDatabase::connect(cx); + cx.spawn(async move |this, cx| { + let database = database_future.await.map_err(|err| anyhow!(err))?; + let db_thread = database + .load_thread(id.clone()) + .await? + .with_context(|| format!("no thread found with ID: {id:?}"))?; + + let thread = this.update(cx, |this, cx| { + let action_log = cx.new(|_cx| ActionLog::new(this.project.clone())); + cx.new(|cx| { + Thread::from_db( + id.clone(), + db_thread, + this.project.clone(), + this.project_context.clone(), + this.context_server_registry.clone(), + action_log.clone(), + this.templates.clone(), + cx, + ) + }) + })?; + let acp_thread = + this.update(cx, |this, cx| this.register_session(thread.clone(), cx))?; + let events = thread.update(cx, |thread, cx| thread.replay(cx))?; + cx.update(|cx| { + NativeAgentConnection::handle_thread_events(events, acp_thread.downgrade(), cx) + })? + .await?; + Ok(acp_thread) + }) + } + + fn save_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) { + let database_future = ThreadsDatabase::connect(cx); + let (id, db_thread) = + thread.update(cx, |thread, cx| (thread.id().clone(), thread.to_db(cx))); + let Some(session) = self.sessions.get_mut(&id) else { + return; + }; + let history = self.history.clone(); + session.pending_save = cx.spawn(async move |_, cx| { + let Some(database) = database_future.await.map_err(|err| anyhow!(err)).log_err() else { + return; + }; + let db_thread = db_thread.await; + database.save_thread(id, db_thread).await.log_err(); + history.update(cx, |history, cx| history.reload(cx)).ok(); + }); + } } /// Wrapper struct that implements the AgentConnection trait @@ -476,13 +585,21 @@ impl NativeAgentConnection { }; log::debug!("Found session for: {}", session_id); - let mut response_stream = match f(thread, cx) { + let response_stream = match f(thread, cx) { Ok(stream) => stream, Err(err) => return Task::ready(Err(err)), }; + Self::handle_thread_events(response_stream, acp_thread, cx) + } + + fn handle_thread_events( + mut events: mpsc::UnboundedReceiver<Result<ThreadEvent>>, + acp_thread: WeakEntity<AcpThread>, + cx: &App, + ) -> Task<Result<acp::PromptResponse>> { cx.spawn(async move |cx| { // Handle response stream and forward to session.acp_thread - while let Some(result) = response_stream.next().await { + while let Some(result) = events.next().await { match result { Ok(event) => { log::trace!("Received completion event: {:?}", event); @@ -686,8 +803,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection { |agent, cx: &mut gpui::Context<NativeAgent>| -> Result<_> { // Fetch default model from registry settings let registry = LanguageModelRegistry::read_global(cx); - let language_registry = project.read(cx).languages().clone(); - // Log available models for debugging let available_count = registry.available_models(cx).count(); log::debug!("Total available models: {}", available_count); @@ -697,72 +812,23 @@ impl acp_thread::AgentConnection for NativeAgentConnection { .models .model_from_id(&LanguageModels::model_id(&default_model.model)) }); - let summarization_model = registry.thread_summary_model().map(|c| c.model); let thread = cx.new(|cx| { - let mut thread = Thread::new( + Thread::new( project.clone(), agent.project_context.clone(), agent.context_server_registry.clone(), action_log.clone(), agent.templates.clone(), default_model, - summarization_model, cx, - ); - thread.add_tool(CopyPathTool::new(project.clone())); - thread.add_tool(CreateDirectoryTool::new(project.clone())); - thread.add_tool(DeletePathTool::new(project.clone(), action_log.clone())); - thread.add_tool(DiagnosticsTool::new(project.clone())); - thread.add_tool(EditFileTool::new(cx.weak_entity(), language_registry)); - thread.add_tool(FetchTool::new(project.read(cx).client().http_client())); - thread.add_tool(FindPathTool::new(project.clone())); - thread.add_tool(GrepTool::new(project.clone())); - thread.add_tool(ListDirectoryTool::new(project.clone())); - thread.add_tool(MovePathTool::new(project.clone())); - thread.add_tool(NowTool); - thread.add_tool(OpenTool::new(project.clone())); - thread.add_tool(ReadFileTool::new(project.clone(), action_log.clone())); - thread.add_tool(TerminalTool::new(project.clone(), cx)); - thread.add_tool(ThinkingTool); - thread.add_tool(WebSearchTool); // TODO: Enable this only if it's a zed model. - thread + ) }); Ok(thread) }, )??; - - let session_id = thread.read_with(cx, |thread, _| thread.id().clone())?; - log::info!("Created session with ID: {}", session_id); - // Create AcpThread - let acp_thread = cx.update(|cx| { - cx.new(|_cx| { - acp_thread::AcpThread::new( - "agent2", - self.clone(), - project.clone(), - action_log.clone(), - session_id.clone(), - ) - }) - })?; - - // Store the session - agent.update(cx, |agent, cx| { - agent.sessions.insert( - session_id, - Session { - thread, - acp_thread: acp_thread.downgrade(), - _subscription: cx.observe_release(&acp_thread, |this, acp_thread, _cx| { - this.sessions.remove(acp_thread.session_id()); - }), - }, - ); - })?; - - Ok(acp_thread) + agent.update(cx, |agent, cx| agent.register_session(thread, cx)) }) } @@ -887,8 +953,11 @@ mod tests { ) .await; let project = Project::test(fs.clone(), [], cx).await; + let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, [], cx)); let agent = NativeAgent::new( project.clone(), + history_store, Templates::new(), None, fs.clone(), @@ -942,9 +1011,12 @@ mod tests { let fs = FakeFs::new(cx.executor()); fs.insert_tree("/", json!({ "a": {} })).await; let project = Project::test(fs.clone(), [], cx).await; + let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, [], cx)); let connection = NativeAgentConnection( NativeAgent::new( project.clone(), + history_store, Templates::new(), None, fs.clone(), @@ -995,9 +1067,13 @@ mod tests { .await; let project = Project::test(fs.clone(), [], cx).await; + let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, [], cx)); + // Create the agent and connection let agent = NativeAgent::new( project.clone(), + history_store, Templates::new(), None, fs.clone(), diff --git a/crates/agent2/src/agent2.rs b/crates/agent2/src/agent2.rs index 8d18da7fe1..1fc9c1cb95 100644 --- a/crates/agent2/src/agent2.rs +++ b/crates/agent2/src/agent2.rs @@ -1,4 +1,6 @@ mod agent; +mod db; +mod history_store; mod native_agent_server; mod templates; mod thread; @@ -9,6 +11,8 @@ mod tools; mod tests; pub use agent::*; +pub use db::*; +pub use history_store::*; pub use native_agent_server::NativeAgentServer; pub use templates::*; pub use thread::*; diff --git a/crates/agent2/src/db.rs b/crates/agent2/src/db.rs new file mode 100644 index 0000000000..c3e6352ef6 --- /dev/null +++ b/crates/agent2/src/db.rs @@ -0,0 +1,470 @@ +use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent}; +use agent::thread_store; +use agent_client_protocol as acp; +use agent_settings::{AgentProfileId, CompletionMode}; +use anyhow::{Result, anyhow}; +use chrono::{DateTime, Utc}; +use collections::{HashMap, IndexMap}; +use futures::{FutureExt, future::Shared}; +use gpui::{BackgroundExecutor, Global, Task}; +use indoc::indoc; +use parking_lot::Mutex; +use serde::{Deserialize, Serialize}; +use sqlez::{ + bindable::{Bind, Column}, + connection::Connection, + statement::Statement, +}; +use std::sync::Arc; +use ui::{App, SharedString}; + +pub type DbMessage = crate::Message; +pub type DbSummary = agent::thread::DetailedSummaryState; +pub type DbLanguageModel = thread_store::SerializedLanguageModel; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DbThreadMetadata { + pub id: acp::SessionId, + #[serde(alias = "summary")] + pub title: SharedString, + pub updated_at: DateTime<Utc>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DbThread { + pub title: SharedString, + pub messages: Vec<DbMessage>, + pub updated_at: DateTime<Utc>, + #[serde(default)] + pub summary: DbSummary, + #[serde(default)] + pub initial_project_snapshot: Option<Arc<agent::thread::ProjectSnapshot>>, + #[serde(default)] + pub cumulative_token_usage: language_model::TokenUsage, + #[serde(default)] + pub request_token_usage: Vec<language_model::TokenUsage>, + #[serde(default)] + pub model: Option<DbLanguageModel>, + #[serde(default)] + pub completion_mode: Option<CompletionMode>, + #[serde(default)] + pub profile: Option<AgentProfileId>, +} + +impl DbThread { + pub const VERSION: &'static str = "0.3.0"; + + pub fn from_json(json: &[u8]) -> Result<Self> { + let saved_thread_json = serde_json::from_slice::<serde_json::Value>(json)?; + match saved_thread_json.get("version") { + Some(serde_json::Value::String(version)) => match version.as_str() { + Self::VERSION => Ok(serde_json::from_value(saved_thread_json)?), + _ => Self::upgrade_from_agent_1(agent::SerializedThread::from_json(json)?), + }, + _ => Self::upgrade_from_agent_1(agent::SerializedThread::from_json(json)?), + } + } + + fn upgrade_from_agent_1(thread: agent::SerializedThread) -> Result<Self> { + let mut messages = Vec::new(); + for msg in thread.messages { + let message = match msg.role { + language_model::Role::User => { + let mut content = Vec::new(); + + // Convert segments to content + for segment in msg.segments { + match segment { + thread_store::SerializedMessageSegment::Text { text } => { + content.push(UserMessageContent::Text(text)); + } + thread_store::SerializedMessageSegment::Thinking { text, .. } => { + // User messages don't have thinking segments, but handle gracefully + content.push(UserMessageContent::Text(text)); + } + thread_store::SerializedMessageSegment::RedactedThinking { .. } => { + // User messages don't have redacted thinking, skip. + } + } + } + + // If no content was added, add context as text if available + if content.is_empty() && !msg.context.is_empty() { + content.push(UserMessageContent::Text(msg.context)); + } + + crate::Message::User(UserMessage { + // MessageId from old format can't be meaningfully converted, so generate a new one + id: acp_thread::UserMessageId::new(), + content, + }) + } + language_model::Role::Assistant => { + let mut content = Vec::new(); + + // Convert segments to content + for segment in msg.segments { + match segment { + thread_store::SerializedMessageSegment::Text { text } => { + content.push(AgentMessageContent::Text(text)); + } + thread_store::SerializedMessageSegment::Thinking { + text, + signature, + } => { + content.push(AgentMessageContent::Thinking { text, signature }); + } + thread_store::SerializedMessageSegment::RedactedThinking { data } => { + content.push(AgentMessageContent::RedactedThinking(data)); + } + } + } + + // Convert tool uses + let mut tool_names_by_id = HashMap::default(); + for tool_use in msg.tool_uses { + tool_names_by_id.insert(tool_use.id.clone(), tool_use.name.clone()); + content.push(AgentMessageContent::ToolUse( + language_model::LanguageModelToolUse { + id: tool_use.id, + name: tool_use.name.into(), + raw_input: serde_json::to_string(&tool_use.input) + .unwrap_or_default(), + input: tool_use.input, + is_input_complete: true, + }, + )); + } + + // Convert tool results + let mut tool_results = IndexMap::default(); + for tool_result in msg.tool_results { + let name = tool_names_by_id + .remove(&tool_result.tool_use_id) + .unwrap_or_else(|| SharedString::from("unknown")); + tool_results.insert( + tool_result.tool_use_id.clone(), + language_model::LanguageModelToolResult { + tool_use_id: tool_result.tool_use_id, + tool_name: name.into(), + is_error: tool_result.is_error, + content: tool_result.content, + output: tool_result.output, + }, + ); + } + + crate::Message::Agent(AgentMessage { + content, + tool_results, + }) + } + language_model::Role::System => { + // Skip system messages as they're not supported in the new format + continue; + } + }; + + messages.push(message); + } + + Ok(Self { + title: thread.summary, + messages, + updated_at: thread.updated_at, + summary: thread.detailed_summary_state, + initial_project_snapshot: thread.initial_project_snapshot, + cumulative_token_usage: thread.cumulative_token_usage, + request_token_usage: thread.request_token_usage, + model: thread.model, + completion_mode: thread.completion_mode, + profile: thread.profile, + }) + } +} + +pub static ZED_STATELESS: std::sync::LazyLock<bool> = + std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty())); + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum DataType { + #[serde(rename = "json")] + Json, + #[serde(rename = "zstd")] + Zstd, +} + +impl Bind for DataType { + fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> { + let value = match self { + DataType::Json => "json", + DataType::Zstd => "zstd", + }; + value.bind(statement, start_index) + } +} + +impl Column for DataType { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let (value, next_index) = String::column(statement, start_index)?; + let data_type = match value.as_str() { + "json" => DataType::Json, + "zstd" => DataType::Zstd, + _ => anyhow::bail!("Unknown data type: {}", value), + }; + Ok((data_type, next_index)) + } +} + +pub(crate) struct ThreadsDatabase { + executor: BackgroundExecutor, + connection: Arc<Mutex<Connection>>, +} + +struct GlobalThreadsDatabase(Shared<Task<Result<Arc<ThreadsDatabase>, Arc<anyhow::Error>>>>); + +impl Global for GlobalThreadsDatabase {} + +impl ThreadsDatabase { + pub fn connect(cx: &mut App) -> Shared<Task<Result<Arc<ThreadsDatabase>, Arc<anyhow::Error>>>> { + if cx.has_global::<GlobalThreadsDatabase>() { + return cx.global::<GlobalThreadsDatabase>().0.clone(); + } + let executor = cx.background_executor().clone(); + let task = executor + .spawn({ + let executor = executor.clone(); + async move { + match ThreadsDatabase::new(executor) { + Ok(db) => Ok(Arc::new(db)), + Err(err) => Err(Arc::new(err)), + } + } + }) + .shared(); + + cx.set_global(GlobalThreadsDatabase(task.clone())); + task + } + + pub fn new(executor: BackgroundExecutor) -> Result<Self> { + let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) { + Connection::open_memory(Some("THREAD_FALLBACK_DB")) + } else { + let threads_dir = paths::data_dir().join("threads"); + std::fs::create_dir_all(&threads_dir)?; + let sqlite_path = threads_dir.join("threads.db"); + Connection::open_file(&sqlite_path.to_string_lossy()) + }; + + connection.exec(indoc! {" + CREATE TABLE IF NOT EXISTS threads ( + id TEXT PRIMARY KEY, + summary TEXT NOT NULL, + updated_at TEXT NOT NULL, + data_type TEXT NOT NULL, + data BLOB NOT NULL + ) + "})?() + .map_err(|e| anyhow!("Failed to create threads table: {}", e))?; + + let db = Self { + executor: executor.clone(), + connection: Arc::new(Mutex::new(connection)), + }; + + Ok(db) + } + + fn save_thread_sync( + connection: &Arc<Mutex<Connection>>, + id: acp::SessionId, + thread: DbThread, + ) -> Result<()> { + const COMPRESSION_LEVEL: i32 = 3; + + #[derive(Serialize)] + struct SerializedThread { + #[serde(flatten)] + thread: DbThread, + version: &'static str, + } + + let title = thread.title.to_string(); + let updated_at = thread.updated_at.to_rfc3339(); + let json_data = serde_json::to_string(&SerializedThread { + thread, + version: DbThread::VERSION, + })?; + + let connection = connection.lock(); + + let compressed = zstd::encode_all(json_data.as_bytes(), COMPRESSION_LEVEL)?; + let data_type = DataType::Zstd; + let data = compressed; + + let mut insert = connection.exec_bound::<(Arc<str>, String, String, DataType, Vec<u8>)>(indoc! {" + INSERT OR REPLACE INTO threads (id, summary, updated_at, data_type, data) VALUES (?, ?, ?, ?, ?) + "})?; + + insert((id.0.clone(), title, updated_at, data_type, data))?; + + Ok(()) + } + + pub fn list_threads(&self) -> Task<Result<Vec<DbThreadMetadata>>> { + let connection = self.connection.clone(); + + self.executor.spawn(async move { + let connection = connection.lock(); + + let mut select = + connection.select_bound::<(), (Arc<str>, String, String)>(indoc! {" + SELECT id, summary, updated_at FROM threads ORDER BY updated_at DESC + "})?; + + let rows = select(())?; + let mut threads = Vec::new(); + + for (id, summary, updated_at) in rows { + threads.push(DbThreadMetadata { + id: acp::SessionId(id), + title: summary.into(), + updated_at: DateTime::parse_from_rfc3339(&updated_at)?.with_timezone(&Utc), + }); + } + + Ok(threads) + }) + } + + pub fn load_thread(&self, id: acp::SessionId) -> Task<Result<Option<DbThread>>> { + let connection = self.connection.clone(); + + self.executor.spawn(async move { + let connection = connection.lock(); + let mut select = connection.select_bound::<Arc<str>, (DataType, Vec<u8>)>(indoc! {" + SELECT data_type, data FROM threads WHERE id = ? LIMIT 1 + "})?; + + let rows = select(id.0)?; + if let Some((data_type, data)) = rows.into_iter().next() { + let json_data = match data_type { + DataType::Zstd => { + let decompressed = zstd::decode_all(&data[..])?; + String::from_utf8(decompressed)? + } + DataType::Json => String::from_utf8(data)?, + }; + let thread = DbThread::from_json(json_data.as_bytes())?; + Ok(Some(thread)) + } else { + Ok(None) + } + }) + } + + pub fn save_thread(&self, id: acp::SessionId, thread: DbThread) -> Task<Result<()>> { + let connection = self.connection.clone(); + + self.executor + .spawn(async move { Self::save_thread_sync(&connection, id, thread) }) + } + + pub fn delete_thread(&self, id: acp::SessionId) -> Task<Result<()>> { + let connection = self.connection.clone(); + + self.executor.spawn(async move { + let connection = connection.lock(); + + let mut delete = connection.exec_bound::<Arc<str>>(indoc! {" + DELETE FROM threads WHERE id = ? + "})?; + + delete(id.0)?; + + Ok(()) + }) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use agent::MessageSegment; + use agent::context::LoadedContext; + use client::Client; + use fs::FakeFs; + use gpui::AppContext; + use gpui::TestAppContext; + use http_client::FakeHttpClient; + use language_model::Role; + use project::Project; + use settings::SettingsStore; + + fn init_test(cx: &mut TestAppContext) { + env_logger::try_init().ok(); + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + Project::init_settings(cx); + language::init(cx); + + let http_client = FakeHttpClient::with_404_response(); + let clock = Arc::new(clock::FakeSystemClock::new()); + let client = Client::new(clock, http_client, cx); + agent::init(cx); + agent_settings::init(cx); + language_model::init(client.clone(), cx); + }); + } + + #[gpui::test] + async fn test_retrieving_old_thread(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [], cx).await; + + // Save a thread using the old agent. + let thread_store = cx.new(|cx| agent::ThreadStore::fake(project, cx)); + let thread = thread_store.update(cx, |thread_store, cx| thread_store.create_thread(cx)); + thread.update(cx, |thread, cx| { + thread.insert_message( + Role::User, + vec![MessageSegment::Text("Hey!".into())], + LoadedContext::default(), + vec![], + false, + cx, + ); + thread.insert_message( + Role::Assistant, + vec![MessageSegment::Text("How're you doing?".into())], + LoadedContext::default(), + vec![], + false, + cx, + ) + }); + thread_store + .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx)) + .await + .unwrap(); + + // Open that same thread using the new agent. + let db = cx.update(ThreadsDatabase::connect).await.unwrap(); + let threads = db.list_threads().await.unwrap(); + assert_eq!(threads.len(), 1); + let thread = db + .load_thread(threads[0].id.clone()) + .await + .unwrap() + .unwrap(); + assert_eq!(thread.messages[0].to_markdown(), "## User\n\nHey!\n"); + assert_eq!( + thread.messages[1].to_markdown(), + "## Assistant\n\nHow're you doing?\n" + ); + } +} diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs new file mode 100644 index 0000000000..34a5e7b4ef --- /dev/null +++ b/crates/agent2/src/history_store.rs @@ -0,0 +1,314 @@ +use crate::{DbThreadMetadata, ThreadsDatabase}; +use agent_client_protocol as acp; +use anyhow::{Context as _, Result, anyhow}; +use assistant_context::SavedContextMetadata; +use chrono::{DateTime, Utc}; +use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*}; +use itertools::Itertools; +use paths::contexts_dir; +use serde::{Deserialize, Serialize}; +use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration}; +use util::ResultExt as _; + +const MAX_RECENTLY_OPENED_ENTRIES: usize = 6; +const NAVIGATION_HISTORY_PATH: &str = "agent-navigation-history.json"; +const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50); + +const DEFAULT_TITLE: &SharedString = &SharedString::new_static("New Thread"); + +#[derive(Clone, Debug)] +pub enum HistoryEntry { + AcpThread(DbThreadMetadata), + TextThread(SavedContextMetadata), +} + +impl HistoryEntry { + pub fn updated_at(&self) -> DateTime<Utc> { + match self { + HistoryEntry::AcpThread(thread) => thread.updated_at, + HistoryEntry::TextThread(context) => context.mtime.to_utc(), + } + } + + pub fn id(&self) -> HistoryEntryId { + match self { + HistoryEntry::AcpThread(thread) => HistoryEntryId::AcpThread(thread.id.clone()), + HistoryEntry::TextThread(context) => HistoryEntryId::TextThread(context.path.clone()), + } + } + + pub fn title(&self) -> &SharedString { + match self { + HistoryEntry::AcpThread(thread) if thread.title.is_empty() => DEFAULT_TITLE, + HistoryEntry::AcpThread(thread) => &thread.title, + HistoryEntry::TextThread(context) => &context.title, + } + } +} + +/// Generic identifier for a history entry. +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum HistoryEntryId { + AcpThread(acp::SessionId), + TextThread(Arc<Path>), +} + +#[derive(Serialize, Deserialize)] +enum SerializedRecentOpen { + Thread(String), + ContextName(String), + /// Old format which stores the full path + Context(String), +} + +pub struct HistoryStore { + threads: Vec<DbThreadMetadata>, + context_store: Entity<assistant_context::ContextStore>, + recently_opened_entries: VecDeque<HistoryEntryId>, + _subscriptions: Vec<gpui::Subscription>, + _save_recently_opened_entries_task: Task<()>, +} + +impl HistoryStore { + pub fn new( + context_store: Entity<assistant_context::ContextStore>, + initial_recent_entries: impl IntoIterator<Item = HistoryEntryId>, + cx: &mut Context<Self>, + ) -> Self { + let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())]; + + cx.spawn(async move |this, cx| { + let entries = Self::load_recently_opened_entries(cx).await.log_err()?; + this.update(cx, |this, _| { + this.recently_opened_entries + .extend( + entries.into_iter().take( + MAX_RECENTLY_OPENED_ENTRIES + .saturating_sub(this.recently_opened_entries.len()), + ), + ); + }) + .ok() + }) + .detach(); + + Self { + context_store, + recently_opened_entries: initial_recent_entries.into_iter().collect(), + threads: Vec::default(), + _subscriptions: subscriptions, + _save_recently_opened_entries_task: Task::ready(()), + } + } + + pub fn delete_thread( + &mut self, + id: acp::SessionId, + cx: &mut Context<Self>, + ) -> Task<Result<()>> { + let database_future = ThreadsDatabase::connect(cx); + cx.spawn(async move |this, cx| { + let database = database_future.await.map_err(|err| anyhow!(err))?; + database.delete_thread(id.clone()).await?; + this.update(cx, |this, cx| this.reload(cx)) + }) + } + + pub fn delete_text_thread( + &mut self, + path: Arc<Path>, + cx: &mut Context<Self>, + ) -> Task<Result<()>> { + self.context_store.update(cx, |context_store, cx| { + context_store.delete_local_context(path, cx) + }) + } + + pub fn reload(&self, cx: &mut Context<Self>) { + let database_future = ThreadsDatabase::connect(cx); + cx.spawn(async move |this, cx| { + let threads = database_future + .await + .map_err(|err| anyhow!(err))? + .list_threads() + .await?; + + this.update(cx, |this, cx| { + this.threads = threads; + cx.notify(); + }) + }) + .detach_and_log_err(cx); + } + + pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> { + let mut history_entries = Vec::new(); + + #[cfg(debug_assertions)] + if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() { + return history_entries; + } + + history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread)); + history_entries.extend( + self.context_store + .read(cx) + .unordered_contexts() + .cloned() + .map(HistoryEntry::TextThread), + ); + + history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at())); + history_entries + } + + pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> { + self.entries(cx).into_iter().take(limit).collect() + } + + pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> { + #[cfg(debug_assertions)] + if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() { + return Vec::new(); + } + + let thread_entries = self.threads.iter().flat_map(|thread| { + self.recently_opened_entries + .iter() + .enumerate() + .flat_map(|(index, entry)| match entry { + HistoryEntryId::AcpThread(id) if &thread.id == id => { + Some((index, HistoryEntry::AcpThread(thread.clone()))) + } + _ => None, + }) + }); + + let context_entries = + self.context_store + .read(cx) + .unordered_contexts() + .flat_map(|context| { + self.recently_opened_entries + .iter() + .enumerate() + .flat_map(|(index, entry)| match entry { + HistoryEntryId::TextThread(path) if &context.path == path => { + Some((index, HistoryEntry::TextThread(context.clone()))) + } + _ => None, + }) + }); + + thread_entries + .chain(context_entries) + // optimization to halt iteration early + .take(self.recently_opened_entries.len()) + .sorted_unstable_by_key(|(index, _)| *index) + .map(|(_, entry)| entry) + .collect() + } + + fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) { + let serialized_entries = self + .recently_opened_entries + .iter() + .filter_map(|entry| match entry { + HistoryEntryId::TextThread(path) => path.file_name().map(|file| { + SerializedRecentOpen::ContextName(file.to_string_lossy().to_string()) + }), + HistoryEntryId::AcpThread(id) => Some(SerializedRecentOpen::Thread(id.to_string())), + }) + .collect::<Vec<_>>(); + + self._save_recently_opened_entries_task = cx.spawn(async move |_, cx| { + cx.background_executor() + .timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE) + .await; + cx.background_spawn(async move { + let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH); + let content = serde_json::to_string(&serialized_entries)?; + std::fs::write(path, content)?; + anyhow::Ok(()) + }) + .await + .log_err(); + }); + } + + fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<Vec<HistoryEntryId>>> { + cx.background_spawn(async move { + let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH); + let contents = match smol::fs::read_to_string(path).await { + Ok(it) => it, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + return Ok(Vec::new()); + } + Err(e) => { + return Err(e) + .context("deserializing persisted agent panel navigation history"); + } + }; + let entries = serde_json::from_str::<Vec<SerializedRecentOpen>>(&contents) + .context("deserializing persisted agent panel navigation history")? + .into_iter() + .take(MAX_RECENTLY_OPENED_ENTRIES) + .flat_map(|entry| match entry { + SerializedRecentOpen::Thread(id) => Some(HistoryEntryId::AcpThread( + acp::SessionId(id.as_str().into()), + )), + SerializedRecentOpen::ContextName(file_name) => Some( + HistoryEntryId::TextThread(contexts_dir().join(file_name).into()), + ), + SerializedRecentOpen::Context(path) => { + Path::new(&path).file_name().map(|file_name| { + HistoryEntryId::TextThread(contexts_dir().join(file_name).into()) + }) + } + }) + .collect::<Vec<_>>(); + Ok(entries) + }) + } + + pub fn push_recently_opened_entry(&mut self, entry: HistoryEntryId, cx: &mut Context<Self>) { + self.recently_opened_entries + .retain(|old_entry| old_entry != &entry); + self.recently_opened_entries.push_front(entry); + self.recently_opened_entries + .truncate(MAX_RECENTLY_OPENED_ENTRIES); + self.save_recently_opened_entries(cx); + } + + pub fn remove_recently_opened_thread(&mut self, id: acp::SessionId, cx: &mut Context<Self>) { + self.recently_opened_entries.retain(|entry| match entry { + HistoryEntryId::AcpThread(thread_id) if thread_id == &id => false, + _ => true, + }); + self.save_recently_opened_entries(cx); + } + + pub fn replace_recently_opened_text_thread( + &mut self, + old_path: &Path, + new_path: &Arc<Path>, + cx: &mut Context<Self>, + ) { + for entry in &mut self.recently_opened_entries { + match entry { + HistoryEntryId::TextThread(path) if path.as_ref() == old_path => { + *entry = HistoryEntryId::TextThread(new_path.clone()); + break; + } + _ => {} + } + } + self.save_recently_opened_entries(cx); + } + + pub fn remove_recently_opened_entry(&mut self, entry: &HistoryEntryId, cx: &mut Context<Self>) { + self.recently_opened_entries + .retain(|old_entry| old_entry != entry); + self.save_recently_opened_entries(cx); + } +} diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index 6f09ee1175..f8cf3dd602 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -7,16 +7,17 @@ use gpui::{App, Entity, Task}; use project::Project; use prompt_store::PromptStore; -use crate::{NativeAgent, NativeAgentConnection, templates::Templates}; +use crate::{HistoryStore, NativeAgent, NativeAgentConnection, templates::Templates}; #[derive(Clone)] pub struct NativeAgentServer { fs: Arc<dyn Fs>, + history: Entity<HistoryStore>, } impl NativeAgentServer { - pub fn new(fs: Arc<dyn Fs>) -> Self { - Self { fs } + pub fn new(fs: Arc<dyn Fs>, history: Entity<HistoryStore>) -> Self { + Self { fs, history } } } @@ -50,6 +51,7 @@ impl AgentServer for NativeAgentServer { ); let project = project.clone(); let fs = self.fs.clone(); + let history = self.history.clone(); let prompt_store = PromptStore::global(cx); cx.spawn(async move |cx| { log::debug!("Creating templates for native agent"); @@ -57,7 +59,8 @@ impl AgentServer for NativeAgentServer { let prompt_store = prompt_store.await?; log::debug!("Creating native agent entity"); - let agent = NativeAgent::new(project, templates, Some(prompt_store), fs, cx).await?; + let agent = + NativeAgent::new(project, history, templates, Some(prompt_store), fs, cx).await?; // Create the connection wrapper let connection = NativeAgentConnection(agent); diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 33706b05de..f01873cfc1 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1273,10 +1273,13 @@ async fn test_agent_connection(cx: &mut TestAppContext) { fake_fs.insert_tree(path!("/test"), json!({})).await; let project = Project::test(fake_fs.clone(), [Path::new("/test")], cx).await; let cwd = Path::new("/test"); + let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, [], cx)); // Create agent and connection let agent = NativeAgent::new( project.clone(), + history_store, templates.clone(), None, fake_fs.clone(), @@ -1756,7 +1759,6 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { action_log, templates, Some(model.clone()), - None, cx, ) }); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index d90d0bd4f8..66b4485f72 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1,4 +1,9 @@ -use crate::{ContextServerRegistry, SystemPromptTemplate, Template, Templates}; +use crate::{ + ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DbLanguageModel, DbThread, + DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool, + ListDirectoryTool, MovePathTool, NowTool, OpenTool, ReadFileTool, SystemPromptTemplate, + Template, Templates, TerminalTool, ThinkingTool, WebSearchTool, +}; use acp_thread::{MentionUri, UserMessageId}; use action_log::ActionLog; use agent::thread::{DetailedSummaryState, GitState, ProjectSnapshot, WorktreeSnapshot}; @@ -17,13 +22,13 @@ use futures::{ stream::FuturesUnordered, }; use git::repository::DiffType; -use gpui::{App, AsyncApp, Context, Entity, SharedString, Task, WeakEntity}; +use gpui::{App, AppContext, AsyncApp, Context, Entity, SharedString, Task, WeakEntity}; use language_model::{ LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage, - LanguageModelProviderId, LanguageModelRequest, LanguageModelRequestMessage, - LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent, - LanguageModelToolSchemaFormat, LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason, - TokenUsage, + LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest, + LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult, + LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse, + LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, }; use project::{ Project, @@ -516,8 +521,8 @@ pub struct Thread { templates: Arc<Templates>, model: Option<Arc<dyn LanguageModel>>, summarization_model: Option<Arc<dyn LanguageModel>>, - project: Entity<Project>, - action_log: Entity<ActionLog>, + pub(crate) project: Entity<Project>, + pub(crate) action_log: Entity<ActionLog>, } impl Thread { @@ -528,7 +533,6 @@ impl Thread { action_log: Entity<ActionLog>, templates: Arc<Templates>, model: Option<Arc<dyn LanguageModel>>, - summarization_model: Option<Arc<dyn LanguageModel>>, cx: &mut Context<Self>, ) -> Self { let profile_id = AgentSettings::get_global(cx).default_profile.clone(); @@ -557,7 +561,7 @@ impl Thread { project_context, templates, model, - summarization_model, + summarization_model: None, project, action_log, } @@ -652,6 +656,88 @@ impl Thread { ); } + pub fn from_db( + id: acp::SessionId, + db_thread: DbThread, + project: Entity<Project>, + project_context: Entity<ProjectContext>, + context_server_registry: Entity<ContextServerRegistry>, + action_log: Entity<ActionLog>, + templates: Arc<Templates>, + cx: &mut Context<Self>, + ) -> Self { + let profile_id = db_thread + .profile + .unwrap_or_else(|| AgentSettings::get_global(cx).default_profile.clone()); + let model = LanguageModelRegistry::global(cx).update(cx, |registry, cx| { + db_thread + .model + .and_then(|model| { + let model = SelectedModel { + provider: model.provider.clone().into(), + model: model.model.clone().into(), + }; + registry.select_model(&model, cx) + }) + .or_else(|| registry.default_model()) + .map(|model| model.model) + }); + + Self { + id, + prompt_id: PromptId::new(), + title: if db_thread.title.is_empty() { + None + } else { + Some(db_thread.title.clone()) + }, + summary: db_thread.summary, + messages: db_thread.messages, + completion_mode: db_thread.completion_mode.unwrap_or_default(), + running_turn: None, + pending_message: None, + tools: BTreeMap::default(), + tool_use_limit_reached: false, + request_token_usage: db_thread.request_token_usage.clone(), + cumulative_token_usage: db_thread.cumulative_token_usage, + initial_project_snapshot: Task::ready(db_thread.initial_project_snapshot).shared(), + context_server_registry, + profile_id, + project_context, + templates, + model, + summarization_model: None, + project, + action_log, + updated_at: db_thread.updated_at, + } + } + + pub fn to_db(&self, cx: &App) -> Task<DbThread> { + let initial_project_snapshot = self.initial_project_snapshot.clone(); + let mut thread = DbThread { + title: self.title.clone().unwrap_or_default(), + messages: self.messages.clone(), + updated_at: self.updated_at, + summary: self.summary.clone(), + initial_project_snapshot: None, + cumulative_token_usage: self.cumulative_token_usage, + request_token_usage: self.request_token_usage.clone(), + model: self.model.as_ref().map(|model| DbLanguageModel { + provider: model.provider_id().to_string(), + model: model.name().0.to_string(), + }), + completion_mode: Some(self.completion_mode), + profile: Some(self.profile_id.clone()), + }; + + cx.background_spawn(async move { + let initial_project_snapshot = initial_project_snapshot.await; + thread.initial_project_snapshot = initial_project_snapshot; + thread + }) + } + /// Create a snapshot of the current project state including git information and unsaved buffers. fn project_snapshot( project: Entity<Project>, @@ -816,6 +902,32 @@ impl Thread { } } + pub fn add_default_tools(&mut self, cx: &mut Context<Self>) { + let language_registry = self.project.read(cx).languages().clone(); + self.add_tool(CopyPathTool::new(self.project.clone())); + self.add_tool(CreateDirectoryTool::new(self.project.clone())); + self.add_tool(DeletePathTool::new( + self.project.clone(), + self.action_log.clone(), + )); + self.add_tool(DiagnosticsTool::new(self.project.clone())); + self.add_tool(EditFileTool::new(cx.weak_entity(), language_registry)); + self.add_tool(FetchTool::new(self.project.read(cx).client().http_client())); + self.add_tool(FindPathTool::new(self.project.clone())); + self.add_tool(GrepTool::new(self.project.clone())); + self.add_tool(ListDirectoryTool::new(self.project.clone())); + self.add_tool(MovePathTool::new(self.project.clone())); + self.add_tool(NowTool); + self.add_tool(OpenTool::new(self.project.clone())); + self.add_tool(ReadFileTool::new( + self.project.clone(), + self.action_log.clone(), + )); + self.add_tool(TerminalTool::new(self.project.clone(), cx)); + self.add_tool(ThinkingTool); + self.add_tool(WebSearchTool); // TODO: Enable this only if it's a zed model. + } + pub fn add_tool(&mut self, tool: impl AgentTool) { self.tools.insert(tool.name(), tool.erase()); } diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index b3b1a428bf..21eb282110 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -554,7 +554,6 @@ mod tests { action_log, Templates::new(), Some(model), - None, cx, ) }); @@ -756,7 +755,6 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), - None, cx, ) }); @@ -899,7 +897,6 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), - None, cx, ) }); @@ -1029,7 +1026,6 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), - None, cx, ) }); @@ -1168,7 +1164,6 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), - None, cx, ) }); @@ -1279,7 +1274,6 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), - None, cx, ) }); @@ -1362,7 +1356,6 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), - None, cx, ) }); @@ -1448,7 +1441,6 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), - None, cx, ) }); @@ -1531,7 +1523,6 @@ mod tests { action_log.clone(), Templates::new(), Some(model.clone()), - None, cx, ) }); diff --git a/crates/agent_ui/src/acp.rs b/crates/agent_ui/src/acp.rs index 831d296eeb..6f228b91d6 100644 --- a/crates/agent_ui/src/acp.rs +++ b/crates/agent_ui/src/acp.rs @@ -3,8 +3,10 @@ mod entry_view_state; mod message_editor; mod model_selector; mod model_selector_popover; +mod thread_history; mod thread_view; pub use model_selector::AcpModelSelector; pub use model_selector_popover::AcpModelSelectorPopover; +pub use thread_history::*; pub use thread_view::AcpThreadView; diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs new file mode 100644 index 0000000000..8a05801139 --- /dev/null +++ b/crates/agent_ui/src/acp/thread_history.rs @@ -0,0 +1,766 @@ +use crate::RemoveSelectedThread; +use agent2::{HistoryEntry, HistoryStore}; +use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; +use editor::{Editor, EditorEvent}; +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{ + App, Empty, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, + UniformListScrollHandle, Window, uniform_list, +}; +use std::{fmt::Display, ops::Range, sync::Arc}; +use time::{OffsetDateTime, UtcOffset}; +use ui::{ + HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, + Tooltip, prelude::*, +}; +use util::ResultExt; + +pub struct AcpThreadHistory { + pub(crate) history_store: Entity<HistoryStore>, + scroll_handle: UniformListScrollHandle, + selected_index: usize, + hovered_index: Option<usize>, + search_editor: Entity<Editor>, + all_entries: Arc<Vec<HistoryEntry>>, + // When the search is empty, we display date separators between history entries + // This vector contains an enum of either a separator or an actual entry + separated_items: Vec<ListItemType>, + // Maps entry indexes to list item indexes + separated_item_indexes: Vec<u32>, + _separated_items_task: Option<Task<()>>, + search_state: SearchState, + scrollbar_visibility: bool, + scrollbar_state: ScrollbarState, + local_timezone: UtcOffset, + _subscriptions: Vec<gpui::Subscription>, +} + +enum SearchState { + Empty, + Searching { + query: SharedString, + _task: Task<()>, + }, + Searched { + query: SharedString, + matches: Vec<StringMatch>, + }, +} + +enum ListItemType { + BucketSeparator(TimeBucket), + Entry { + index: usize, + format: EntryTimeFormat, + }, +} + +pub enum ThreadHistoryEvent { + Open(HistoryEntry), +} + +impl EventEmitter<ThreadHistoryEvent> for AcpThreadHistory {} + +impl AcpThreadHistory { + pub(crate) fn new( + history_store: Entity<agent2::HistoryStore>, + window: &mut Window, + cx: &mut Context<Self>, + ) -> Self { + let search_editor = cx.new(|cx| { + let mut editor = Editor::single_line(window, cx); + editor.set_placeholder_text("Search threads...", cx); + editor + }); + + let search_editor_subscription = + cx.subscribe(&search_editor, |this, search_editor, event, cx| { + if let EditorEvent::BufferEdited = event { + let query = search_editor.read(cx).text(cx); + this.search(query.into(), cx); + } + }); + + let history_store_subscription = cx.observe(&history_store, |this, _, cx| { + this.update_all_entries(cx); + }); + + let scroll_handle = UniformListScrollHandle::default(); + let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); + + let mut this = Self { + history_store, + scroll_handle, + selected_index: 0, + hovered_index: None, + search_state: SearchState::Empty, + all_entries: Default::default(), + separated_items: Default::default(), + separated_item_indexes: Default::default(), + search_editor, + scrollbar_visibility: true, + scrollbar_state, + local_timezone: UtcOffset::from_whole_seconds( + chrono::Local::now().offset().local_minus_utc(), + ) + .unwrap(), + _subscriptions: vec![search_editor_subscription, history_store_subscription], + _separated_items_task: None, + }; + this.update_all_entries(cx); + this + } + + fn update_all_entries(&mut self, cx: &mut Context<Self>) { + let new_entries: Arc<Vec<HistoryEntry>> = self + .history_store + .update(cx, |store, cx| store.entries(cx)) + .into(); + + self._separated_items_task.take(); + + let mut items = Vec::with_capacity(new_entries.len() + 1); + let mut indexes = Vec::with_capacity(new_entries.len() + 1); + + let bg_task = cx.background_spawn(async move { + let mut bucket = None; + let today = Local::now().naive_local().date(); + + for (index, entry) in new_entries.iter().enumerate() { + let entry_date = entry + .updated_at() + .with_timezone(&Local) + .naive_local() + .date(); + let entry_bucket = TimeBucket::from_dates(today, entry_date); + + if Some(entry_bucket) != bucket { + bucket = Some(entry_bucket); + items.push(ListItemType::BucketSeparator(entry_bucket)); + } + + indexes.push(items.len() as u32); + items.push(ListItemType::Entry { + index, + format: entry_bucket.into(), + }); + } + (new_entries, items, indexes) + }); + + let task = cx.spawn(async move |this, cx| { + let (new_entries, items, indexes) = bg_task.await; + this.update(cx, |this, cx| { + let previously_selected_entry = + this.all_entries.get(this.selected_index).map(|e| e.id()); + + this.all_entries = new_entries; + this.separated_items = items; + this.separated_item_indexes = indexes; + + match &this.search_state { + SearchState::Empty => { + if this.selected_index >= this.all_entries.len() { + this.set_selected_entry_index( + this.all_entries.len().saturating_sub(1), + cx, + ); + } else if let Some(prev_id) = previously_selected_entry + && let Some(new_ix) = this + .all_entries + .iter() + .position(|probe| probe.id() == prev_id) + { + this.set_selected_entry_index(new_ix, cx); + } + } + SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => { + this.search(query.clone(), cx); + } + } + + cx.notify(); + }) + .log_err(); + }); + self._separated_items_task = Some(task); + } + + fn search(&mut self, query: SharedString, cx: &mut Context<Self>) { + if query.is_empty() { + self.search_state = SearchState::Empty; + cx.notify(); + return; + } + + let all_entries = self.all_entries.clone(); + + let fuzzy_search_task = cx.background_spawn({ + let query = query.clone(); + let executor = cx.background_executor().clone(); + async move { + let mut candidates = Vec::with_capacity(all_entries.len()); + + for (idx, entry) in all_entries.iter().enumerate() { + candidates.push(StringMatchCandidate::new(idx, entry.title())); + } + + const MAX_MATCHES: usize = 100; + + fuzzy::match_strings( + &candidates, + &query, + false, + true, + MAX_MATCHES, + &Default::default(), + executor, + ) + .await + } + }); + + let task = cx.spawn({ + let query = query.clone(); + async move |this, cx| { + let matches = fuzzy_search_task.await; + + this.update(cx, |this, cx| { + let SearchState::Searching { + query: current_query, + _task, + } = &this.search_state + else { + return; + }; + + if &query == current_query { + this.search_state = SearchState::Searched { + query: query.clone(), + matches, + }; + + this.set_selected_entry_index(0, cx); + cx.notify(); + }; + }) + .log_err(); + } + }); + + self.search_state = SearchState::Searching { query, _task: task }; + cx.notify(); + } + + fn matched_count(&self) -> usize { + match &self.search_state { + SearchState::Empty => self.all_entries.len(), + SearchState::Searching { .. } => 0, + SearchState::Searched { matches, .. } => matches.len(), + } + } + + fn list_item_count(&self) -> usize { + match &self.search_state { + SearchState::Empty => self.separated_items.len(), + SearchState::Searching { .. } => 0, + SearchState::Searched { matches, .. } => matches.len(), + } + } + + fn search_produced_no_matches(&self) -> bool { + match &self.search_state { + SearchState::Empty => false, + SearchState::Searching { .. } => false, + SearchState::Searched { matches, .. } => matches.is_empty(), + } + } + + fn get_match(&self, ix: usize) -> Option<&HistoryEntry> { + match &self.search_state { + SearchState::Empty => self.all_entries.get(ix), + SearchState::Searching { .. } => None, + SearchState::Searched { matches, .. } => matches + .get(ix) + .and_then(|m| self.all_entries.get(m.candidate_id)), + } + } + + pub fn select_previous( + &mut self, + _: &menu::SelectPrevious, + _window: &mut Window, + cx: &mut Context<Self>, + ) { + let count = self.matched_count(); + if count > 0 { + if self.selected_index == 0 { + self.set_selected_entry_index(count - 1, cx); + } else { + self.set_selected_entry_index(self.selected_index - 1, cx); + } + } + } + + pub fn select_next( + &mut self, + _: &menu::SelectNext, + _window: &mut Window, + cx: &mut Context<Self>, + ) { + let count = self.matched_count(); + if count > 0 { + if self.selected_index == count - 1 { + self.set_selected_entry_index(0, cx); + } else { + self.set_selected_entry_index(self.selected_index + 1, cx); + } + } + } + + fn select_first( + &mut self, + _: &menu::SelectFirst, + _window: &mut Window, + cx: &mut Context<Self>, + ) { + let count = self.matched_count(); + if count > 0 { + self.set_selected_entry_index(0, cx); + } + } + + fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) { + let count = self.matched_count(); + if count > 0 { + self.set_selected_entry_index(count - 1, cx); + } + } + + fn set_selected_entry_index(&mut self, entry_index: usize, cx: &mut Context<Self>) { + self.selected_index = entry_index; + + let scroll_ix = match self.search_state { + SearchState::Empty | SearchState::Searching { .. } => self + .separated_item_indexes + .get(entry_index) + .map(|ix| *ix as usize) + .unwrap_or(entry_index + 1), + SearchState::Searched { .. } => entry_index, + }; + + self.scroll_handle + .scroll_to_item(scroll_ix, ScrollStrategy::Top); + + cx.notify(); + } + + fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> { + if !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) { + return None; + } + + Some( + div() + .occlude() + .id("thread-history-scroll") + .h_full() + .bg(cx.theme().colors().panel_background.opacity(0.8)) + .border_l_1() + .border_color(cx.theme().colors().border_variant) + .absolute() + .right_1() + .top_0() + .bottom_0() + .w_4() + .pl_1() + .cursor_default() + .on_mouse_move(cx.listener(|_, _, _window, cx| { + cx.notify(); + cx.stop_propagation() + })) + .on_hover(|_, _window, cx| { + cx.stop_propagation(); + }) + .on_any_mouse_down(|_, _window, cx| { + cx.stop_propagation(); + }) + .on_scroll_wheel(cx.listener(|_, _, _window, cx| { + cx.notify(); + })) + .children(Scrollbar::vertical(self.scrollbar_state.clone())), + ) + } + + fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) { + self.confirm_entry(self.selected_index, cx); + } + + fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) { + let Some(entry) = self.get_match(ix) else { + return; + }; + cx.emit(ThreadHistoryEvent::Open(entry.clone())); + } + + fn remove_selected_thread( + &mut self, + _: &RemoveSelectedThread, + _window: &mut Window, + cx: &mut Context<Self>, + ) { + self.remove_thread(self.selected_index, cx) + } + + fn remove_thread(&mut self, ix: usize, cx: &mut Context<Self>) { + let Some(entry) = self.get_match(ix) else { + return; + }; + + let task = match entry { + HistoryEntry::AcpThread(thread) => self + .history_store + .update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)), + HistoryEntry::TextThread(context) => self.history_store.update(cx, |this, cx| { + this.delete_text_thread(context.path.clone(), cx) + }), + }; + task.detach_and_log_err(cx); + } + + fn list_items( + &mut self, + range: Range<usize>, + _window: &mut Window, + cx: &mut Context<Self>, + ) -> Vec<AnyElement> { + match &self.search_state { + SearchState::Empty => self + .separated_items + .get(range) + .iter() + .flat_map(|items| { + items + .iter() + .map(|item| self.render_list_item(item, vec![], cx)) + }) + .collect(), + SearchState::Searched { matches, .. } => matches[range] + .iter() + .filter_map(|m| { + let entry = self.all_entries.get(m.candidate_id)?; + Some(self.render_history_entry( + entry, + EntryTimeFormat::DateAndTime, + m.candidate_id, + m.positions.clone(), + cx, + )) + }) + .collect(), + SearchState::Searching { .. } => { + vec![] + } + } + } + + fn render_list_item( + &self, + item: &ListItemType, + highlight_positions: Vec<usize>, + cx: &Context<Self>, + ) -> AnyElement { + match item { + ListItemType::Entry { index, format } => match self.all_entries.get(*index) { + Some(entry) => self + .render_history_entry(entry, *format, *index, highlight_positions, cx) + .into_any(), + None => Empty.into_any_element(), + }, + ListItemType::BucketSeparator(bucket) => div() + .px(DynamicSpacing::Base06.rems(cx)) + .pt_2() + .pb_1() + .child( + Label::new(bucket.to_string()) + .size(LabelSize::XSmall) + .color(Color::Muted), + ) + .into_any_element(), + } + } + + fn render_history_entry( + &self, + entry: &HistoryEntry, + format: EntryTimeFormat, + list_entry_ix: usize, + highlight_positions: Vec<usize>, + cx: &Context<Self>, + ) -> AnyElement { + let selected = list_entry_ix == self.selected_index; + let hovered = Some(list_entry_ix) == self.hovered_index; + let timestamp = entry.updated_at().timestamp(); + let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone); + + h_flex() + .w_full() + .pb_1() + .child( + ListItem::new(list_entry_ix) + .rounded() + .toggle_state(selected) + .spacing(ListItemSpacing::Sparse) + .start_slot( + h_flex() + .w_full() + .gap_2() + .justify_between() + .child( + HighlightedLabel::new(entry.title(), highlight_positions) + .size(LabelSize::Small) + .truncate(), + ) + .child( + Label::new(thread_timestamp) + .color(Color::Muted) + .size(LabelSize::XSmall), + ), + ) + .on_hover(cx.listener(move |this, is_hovered, _window, cx| { + if *is_hovered { + this.hovered_index = Some(list_entry_ix); + } else if this.hovered_index == Some(list_entry_ix) { + this.hovered_index = None; + } + + cx.notify(); + })) + .end_slot::<IconButton>(if hovered || selected { + Some( + IconButton::new("delete", IconName::Trash) + .shape(IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .tooltip(move |window, cx| { + Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx) + }) + .on_click(cx.listener(move |this, _, _, cx| { + this.remove_thread(list_entry_ix, cx) + })), + ) + } else { + None + }) + .on_click( + cx.listener(move |this, _, _, cx| this.confirm_entry(list_entry_ix, cx)), + ), + ) + .into_any_element() + } +} + +impl Focusable for AcpThreadHistory { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.search_editor.focus_handle(cx) + } +} + +impl Render for AcpThreadHistory { + fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { + v_flex() + .key_context("ThreadHistory") + .size_full() + .on_action(cx.listener(Self::select_previous)) + .on_action(cx.listener(Self::select_next)) + .on_action(cx.listener(Self::select_first)) + .on_action(cx.listener(Self::select_last)) + .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::remove_selected_thread)) + .when(!self.all_entries.is_empty(), |parent| { + parent.child( + h_flex() + .h(px(41.)) // Match the toolbar perfectly + .w_full() + .py_1() + .px_2() + .gap_2() + .justify_between() + .border_b_1() + .border_color(cx.theme().colors().border) + .child( + Icon::new(IconName::MagnifyingGlass) + .color(Color::Muted) + .size(IconSize::Small), + ) + .child(self.search_editor.clone()), + ) + }) + .child({ + let view = v_flex() + .id("list-container") + .relative() + .overflow_hidden() + .flex_grow(); + + if self.all_entries.is_empty() { + view.justify_center() + .child( + h_flex().w_full().justify_center().child( + Label::new("You don't have any past threads yet.") + .size(LabelSize::Small), + ), + ) + } else if self.search_produced_no_matches() { + view.justify_center().child( + h_flex().w_full().justify_center().child( + Label::new("No threads match your search.").size(LabelSize::Small), + ), + ) + } else { + view.pr_5() + .child( + uniform_list( + "thread-history", + self.list_item_count(), + cx.processor(|this, range: Range<usize>, window, cx| { + this.list_items(range, window, cx) + }), + ) + .p_1() + .track_scroll(self.scroll_handle.clone()) + .flex_grow(), + ) + .when_some(self.render_scrollbar(cx), |div, scrollbar| { + div.child(scrollbar) + }) + } + }) + } +} + +#[derive(Clone, Copy)] +pub enum EntryTimeFormat { + DateAndTime, + TimeOnly, +} + +impl EntryTimeFormat { + fn format_timestamp(&self, timestamp: i64, timezone: UtcOffset) -> String { + let timestamp = OffsetDateTime::from_unix_timestamp(timestamp).unwrap(); + + match self { + EntryTimeFormat::DateAndTime => time_format::format_localized_timestamp( + timestamp, + OffsetDateTime::now_utc(), + timezone, + time_format::TimestampFormat::EnhancedAbsolute, + ), + EntryTimeFormat::TimeOnly => time_format::format_time(timestamp), + } + } +} + +impl From<TimeBucket> for EntryTimeFormat { + fn from(bucket: TimeBucket) -> Self { + match bucket { + TimeBucket::Today => EntryTimeFormat::TimeOnly, + TimeBucket::Yesterday => EntryTimeFormat::TimeOnly, + TimeBucket::ThisWeek => EntryTimeFormat::DateAndTime, + TimeBucket::PastWeek => EntryTimeFormat::DateAndTime, + TimeBucket::All => EntryTimeFormat::DateAndTime, + } + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +enum TimeBucket { + Today, + Yesterday, + ThisWeek, + PastWeek, + All, +} + +impl TimeBucket { + fn from_dates(reference: NaiveDate, date: NaiveDate) -> Self { + if date == reference { + return TimeBucket::Today; + } + + if date == reference - TimeDelta::days(1) { + return TimeBucket::Yesterday; + } + + let week = date.iso_week(); + + if reference.iso_week() == week { + return TimeBucket::ThisWeek; + } + + let last_week = (reference - TimeDelta::days(7)).iso_week(); + + if week == last_week { + return TimeBucket::PastWeek; + } + + TimeBucket::All + } +} + +impl Display for TimeBucket { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TimeBucket::Today => write!(f, "Today"), + TimeBucket::Yesterday => write!(f, "Yesterday"), + TimeBucket::ThisWeek => write!(f, "This Week"), + TimeBucket::PastWeek => write!(f, "Past Week"), + TimeBucket::All => write!(f, "All"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::NaiveDate; + + #[test] + fn test_time_bucket_from_dates() { + let today = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap(); + + let date = today; + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Today); + + let date = NaiveDate::from_ymd_opt(2023, 1, 14).unwrap(); + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Yesterday); + + let date = NaiveDate::from_ymd_opt(2023, 1, 13).unwrap(); + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek); + + let date = NaiveDate::from_ymd_opt(2023, 1, 11).unwrap(); + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek); + + let date = NaiveDate::from_ymd_opt(2023, 1, 8).unwrap(); + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek); + + let date = NaiveDate::from_ymd_opt(2023, 1, 5).unwrap(); + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek); + + // All: not in this week or last week + let date = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(); + assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::All); + + // Test year boundary cases + let new_year = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(); + + let date = NaiveDate::from_ymd_opt(2022, 12, 31).unwrap(); + assert_eq!( + TimeBucket::from_dates(new_year, date), + TimeBucket::Yesterday + ); + + let date = NaiveDate::from_ymd_opt(2022, 12, 28).unwrap(); + assert_eq!(TimeBucket::from_dates(new_year, date), TimeBucket::ThisWeek); + } +} diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 150f1ea73b..bf5b8efbc8 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -9,6 +9,7 @@ use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::{self as acp}; use agent_servers::{AgentServer, ClaudeCode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; +use agent2::DbThreadMetadata; use anyhow::bail; use audio::{Audio, Sound}; use buffer_diff::BufferDiff; @@ -155,6 +156,7 @@ enum ThreadState { impl AcpThreadView { pub fn new( agent: Rc<dyn AgentServer>, + resume_thread: Option<DbThreadMetadata>, workspace: WeakEntity<Workspace>, project: Entity<Project>, thread_store: Entity<ThreadStore>, @@ -203,7 +205,7 @@ impl AcpThreadView { workspace: workspace.clone(), project: project.clone(), entry_view_state, - thread_state: Self::initial_state(agent, workspace, project, window, cx), + thread_state: Self::initial_state(agent, resume_thread, workspace, project, window, cx), message_editor, model_selector: None, profile_selector: None, @@ -228,6 +230,7 @@ impl AcpThreadView { fn initial_state( agent: Rc<dyn AgentServer>, + resume_thread: Option<DbThreadMetadata>, workspace: WeakEntity<Workspace>, project: Entity<Project>, window: &mut Window, @@ -254,28 +257,27 @@ impl AcpThreadView { } }; - // this.update_in(cx, |_this, _window, cx| { - // let status = connection.exit_status(cx); - // cx.spawn(async move |this, cx| { - // let status = status.await.ok(); - // this.update(cx, |this, cx| { - // this.thread_state = ThreadState::ServerExited { status }; - // cx.notify(); - // }) - // .ok(); - // }) - // .detach(); - // }) - // .ok(); - - let Some(result) = cx - .update(|_, cx| { + let result = if let Some(native_agent) = connection + .clone() + .downcast::<agent2::NativeAgentConnection>() + && let Some(resume) = resume_thread + { + cx.update(|_, cx| { + native_agent + .0 + .update(cx, |agent, cx| agent.open_thread(resume.id, cx)) + }) + .log_err() + } else { + cx.update(|_, cx| { connection .clone() .new_thread(project.clone(), &root_dir, cx) }) .log_err() - else { + }; + + let Some(result) = result else { return; }; @@ -382,6 +384,7 @@ impl AcpThreadView { this.update(cx, |this, cx| { this.thread_state = Self::initial_state( agent.clone(), + None, this.workspace.clone(), this.project.clone(), window, @@ -842,6 +845,7 @@ impl AcpThreadView { } else { this.thread_state = Self::initial_state( agent, + None, this.workspace.clone(), project.clone(), window, @@ -4044,6 +4048,7 @@ pub(crate) mod tests { cx.new(|cx| { AcpThreadView::new( Rc::new(agent), + None, workspace.downgrade(), project, thread_store.clone(), @@ -4248,6 +4253,7 @@ pub(crate) mod tests { cx.new(|cx| { AcpThreadView::new( Rc::new(StubAgentServer::new(connection.as_ref().clone())), + None, workspace.downgrade(), project.clone(), thread_store.clone(), diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 995bf771e2..f9aea84376 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -4,10 +4,11 @@ use std::rc::Rc; use std::sync::Arc; use std::time::Duration; +use agent2::{DbThreadMetadata, HistoryEntry}; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; -use crate::NewExternalAgentThread; +use crate::acp::{AcpThreadHistory, ThreadHistoryEvent}; use crate::agent_diff::AgentDiffThread; use crate::{ AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode, @@ -28,6 +29,7 @@ use crate::{ thread_history::{HistoryEntryElement, ThreadHistory}, ui::{AgentOnboardingModal, EndTrialUpsell}, }; +use crate::{ExternalAgent, NewExternalAgentThread}; use agent::{ Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio, context_store::ContextStore, @@ -117,7 +119,7 @@ pub fn init(cx: &mut App) { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { workspace.focus_panel::<AgentPanel>(window, cx); panel.update(cx, |panel, cx| { - panel.new_external_thread(action.agent, window, cx) + panel.external_thread(action.agent, None, window, cx) }); } }) @@ -360,6 +362,7 @@ impl ActiveView { pub fn prompt_editor( context_editor: Entity<TextThreadEditor>, history_store: Entity<HistoryStore>, + acp_history_store: Entity<agent2::HistoryStore>, language_registry: Arc<LanguageRegistry>, window: &mut Window, cx: &mut App, @@ -437,6 +440,18 @@ impl ActiveView { ); } }); + + acp_history_store.update(cx, |history_store, cx| { + if let Some(old_path) = old_path { + history_store + .replace_recently_opened_text_thread(old_path, new_path, cx); + } else { + history_store.push_recently_opened_entry( + agent2::HistoryEntryId::TextThread(new_path.clone()), + cx, + ); + } + }); } _ => {} } @@ -465,6 +480,8 @@ pub struct AgentPanel { fs: Arc<dyn Fs>, language_registry: Arc<LanguageRegistry>, thread_store: Entity<ThreadStore>, + acp_history: Entity<AcpThreadHistory>, + acp_history_store: Entity<agent2::HistoryStore>, _default_model_subscription: Subscription, context_store: Entity<TextThreadStore>, prompt_store: Option<Entity<PromptStore>>, @@ -631,6 +648,29 @@ impl AgentPanel { ) }); + let acp_history_store = + cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), [], cx)); + let acp_history = cx.new(|cx| AcpThreadHistory::new(acp_history_store.clone(), window, cx)); + cx.subscribe_in( + &acp_history, + window, + |this, _, event, window, cx| match event { + ThreadHistoryEvent::Open(HistoryEntry::AcpThread(thread)) => { + this.external_thread( + Some(crate::ExternalAgent::NativeAgent), + Some(thread.clone()), + window, + cx, + ); + } + ThreadHistoryEvent::Open(HistoryEntry::TextThread(thread)) => { + this.open_saved_prompt_editor(thread.path.clone(), window, cx) + .detach_and_log_err(cx); + } + }, + ) + .detach(); + cx.observe(&history_store, |_, _, cx| cx.notify()).detach(); let active_thread = cx.new(|cx| { @@ -669,6 +709,7 @@ impl AgentPanel { ActiveView::prompt_editor( context_editor, history_store.clone(), + acp_history_store.clone(), language_registry.clone(), window, cx, @@ -685,7 +726,11 @@ impl AgentPanel { let assistant_navigation_menu = ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| { if let Some(panel) = panel.upgrade() { - menu = Self::populate_recently_opened_menu_section(menu, panel, cx); + if cx.has_flag::<AcpFeatureFlag>() { + menu = Self::populate_recently_opened_menu_section_new(menu, panel, cx); + } else { + menu = Self::populate_recently_opened_menu_section_old(menu, panel, cx); + } } menu.action("View All", Box::new(OpenHistory)) .end_slot_action(DeleteRecentlyOpenThread.boxed_clone()) @@ -773,6 +818,8 @@ impl AgentPanel { zoomed: false, pending_serialization: None, onboarding, + acp_history, + acp_history_store, selected_agent: AgentType::default(), } } @@ -939,6 +986,7 @@ impl AgentPanel { ActiveView::prompt_editor( context_editor.clone(), self.history_store.clone(), + self.acp_history_store.clone(), self.language_registry.clone(), window, cx, @@ -949,9 +997,10 @@ impl AgentPanel { context_editor.focus_handle(cx).focus(window); } - fn new_external_thread( + fn external_thread( &mut self, agent_choice: Option<crate::ExternalAgent>, + resume_thread: Option<DbThreadMetadata>, window: &mut Window, cx: &mut Context<Self>, ) { @@ -968,6 +1017,7 @@ impl AgentPanel { let thread_store = self.thread_store.clone(); let text_thread_store = self.context_store.clone(); + let history = self.acp_history_store.clone(); cx.spawn_in(window, async move |this, cx| { let ext_agent = match agent_choice { @@ -1001,7 +1051,7 @@ impl AgentPanel { } }; - let server = ext_agent.server(fs); + let server = ext_agent.server(fs, history); this.update_in(cx, |this, window, cx| { match ext_agent { @@ -1020,6 +1070,7 @@ impl AgentPanel { let thread_view = cx.new(|cx| { crate::acp::AcpThreadView::new( server, + resume_thread, workspace.clone(), project, thread_store.clone(), @@ -1114,6 +1165,7 @@ impl AgentPanel { ActiveView::prompt_editor( editor.clone(), self.history_store.clone(), + self.acp_history_store.clone(), self.language_registry.clone(), window, cx, @@ -1580,7 +1632,7 @@ impl AgentPanel { self.focus_handle(cx).focus(window); } - fn populate_recently_opened_menu_section( + fn populate_recently_opened_menu_section_old( mut menu: ContextMenu, panel: Entity<Self>, cx: &mut Context<ContextMenu>, @@ -1644,6 +1696,72 @@ impl AgentPanel { menu } + fn populate_recently_opened_menu_section_new( + mut menu: ContextMenu, + panel: Entity<Self>, + cx: &mut Context<ContextMenu>, + ) -> ContextMenu { + let entries = panel + .read(cx) + .acp_history_store + .read(cx) + .recently_opened_entries(cx); + + if entries.is_empty() { + return menu; + } + + menu = menu.header("Recently Opened"); + + for entry in entries { + let title = entry.title().clone(); + + menu = menu.entry_with_end_slot_on_hover( + title, + None, + { + let panel = panel.downgrade(); + let entry = entry.clone(); + move |window, cx| { + let entry = entry.clone(); + panel + .update(cx, move |this, cx| match &entry { + agent2::HistoryEntry::AcpThread(entry) => this.external_thread( + Some(ExternalAgent::NativeAgent), + Some(entry.clone()), + window, + cx, + ), + agent2::HistoryEntry::TextThread(entry) => this + .open_saved_prompt_editor(entry.path.clone(), window, cx) + .detach_and_log_err(cx), + }) + .ok(); + } + }, + IconName::Close, + "Close Entry".into(), + { + let panel = panel.downgrade(); + let id = entry.id(); + move |_window, cx| { + panel + .update(cx, |this, cx| { + this.acp_history_store.update(cx, |history_store, cx| { + history_store.remove_recently_opened_entry(&id, cx); + }); + }) + .ok(); + } + }, + ); + } + + menu = menu.separator(); + + menu + } + pub fn set_selected_agent( &mut self, agent: AgentType, @@ -1653,8 +1771,8 @@ impl AgentPanel { if self.selected_agent != agent { self.selected_agent = agent; self.serialize(cx); - self.new_agent_thread(agent, window, cx); } + self.new_agent_thread(agent, window, cx); } pub fn selected_agent(&self) -> AgentType { @@ -1681,13 +1799,13 @@ impl AgentPanel { window.dispatch_action(NewTextThread.boxed_clone(), cx); } AgentType::NativeAgent => { - self.new_external_thread(Some(crate::ExternalAgent::NativeAgent), window, cx) + self.external_thread(Some(crate::ExternalAgent::NativeAgent), None, window, cx) } AgentType::Gemini => { - self.new_external_thread(Some(crate::ExternalAgent::Gemini), window, cx) + self.external_thread(Some(crate::ExternalAgent::Gemini), None, window, cx) } AgentType::ClaudeCode => { - self.new_external_thread(Some(crate::ExternalAgent::ClaudeCode), window, cx) + self.external_thread(Some(crate::ExternalAgent::ClaudeCode), None, window, cx) } } } @@ -1698,7 +1816,13 @@ impl Focusable for AgentPanel { match &self.active_view { ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx), ActiveView::ExternalAgentThread { thread_view, .. } => thread_view.focus_handle(cx), - ActiveView::History => self.history.focus_handle(cx), + ActiveView::History => { + if cx.has_flag::<feature_flags::AcpFeatureFlag>() { + self.acp_history.focus_handle(cx) + } else { + self.history.focus_handle(cx) + } + } ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx), ActiveView::Configuration => { if let Some(configuration) = self.configuration.as_ref() { @@ -3534,7 +3658,13 @@ impl Render for AgentPanel { ActiveView::ExternalAgentThread { thread_view, .. } => parent .child(thread_view.clone()) .child(self.render_drag_target(cx)), - ActiveView::History => parent.child(self.history.clone()), + ActiveView::History => { + if cx.has_flag::<feature_flags::AcpFeatureFlag>() { + parent.child(self.acp_history.clone()) + } else { + parent.child(self.history.clone()) + } + } ActiveView::TextThread { context_editor, buffer_search_bar, diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 8525d7f9e5..a1dbc77084 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -156,11 +156,15 @@ enum ExternalAgent { } impl ExternalAgent { - pub fn server(&self, fs: Arc<dyn fs::Fs>) -> Rc<dyn agent_servers::AgentServer> { + pub fn server( + &self, + fs: Arc<dyn fs::Fs>, + history: Entity<agent2::HistoryStore>, + ) -> Rc<dyn agent_servers::AgentServer> { match self { ExternalAgent::Gemini => Rc::new(agent_servers::Gemini), ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode), - ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs)), + ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)), } } } From a91acb5f4126fe650efc758a4159d0758e34b02e Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Wed, 20 Aug 2025 00:13:54 +0530 Subject: [PATCH 491/693] onboarding: Fix theme selection in system mode (#36484) Previously, selecting the "System" theme during onboarding would hardcode the theme based on the device's current mode (e.g., Light or Dark). This change ensures the "System" setting is saved correctly, allowing the app to dynamically follow the OS theme by inserting the correct theme in the config for both light and dark mode. Release Notes: - N/A Signed-off-by: Umesh Yadav <git@umesh.dev> --- crates/onboarding/src/basics_page.rs | 32 +++++++++++++++++++--------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 86ddc22a86..8d89c6662e 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -16,6 +16,23 @@ use vim_mode_setting::VimModeSetting; use crate::theme_preview::{ThemePreviewStyle, ThemePreviewTile}; +const LIGHT_THEMES: [&'static str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"]; +const DARK_THEMES: [&'static str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"]; +const FAMILY_NAMES: [SharedString; 3] = [ + SharedString::new_static("One"), + SharedString::new_static("Ayu"), + SharedString::new_static("Gruvbox"), +]; + +fn get_theme_family_themes(theme_name: &str) -> Option<(&'static str, &'static str)> { + for i in 0..LIGHT_THEMES.len() { + if LIGHT_THEMES[i] == theme_name || DARK_THEMES[i] == theme_name { + return Some((LIGHT_THEMES[i], DARK_THEMES[i])); + } + } + None +} + fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement { let theme_selection = ThemeSettings::get_global(cx).theme_selection.clone(); let system_appearance = theme::SystemAppearance::global(cx); @@ -90,14 +107,6 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement }; let current_theme_name = theme_selection.theme(appearance); - const LIGHT_THEMES: [&'static str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"]; - const DARK_THEMES: [&'static str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"]; - const FAMILY_NAMES: [SharedString; 3] = [ - SharedString::new_static("One"), - SharedString::new_static("Ayu"), - SharedString::new_static("Gruvbox"), - ]; - let theme_names = match appearance { Appearance::Light => LIGHT_THEMES, Appearance::Dark => DARK_THEMES, @@ -184,10 +193,13 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement let theme = theme.into(); update_settings_file::<ThemeSettings>(fs, cx, move |settings, cx| { if theme_mode == ThemeMode::System { + let (light_theme, dark_theme) = + get_theme_family_themes(&theme).unwrap_or((theme.as_ref(), theme.as_ref())); + settings.theme = Some(ThemeSelection::Dynamic { mode: ThemeMode::System, - light: ThemeName(theme.clone()), - dark: ThemeName(theme.clone()), + light: ThemeName(light_theme.into()), + dark: ThemeName(dark_theme.into()), }); } else { let appearance = *SystemAppearance::global(cx); From df9c2aefb1e4f589753980dbdf6622a1f2dcf52a Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Tue, 19 Aug 2025 15:14:43 -0400 Subject: [PATCH 492/693] thread_view: Fix issues with images (#36509) - Clean up failed load tasks for mentions that require async processing - When dragging and dropping files, hold onto added worktrees until any async processing has completed; this fixes a bug when dragging items from outside the project Release Notes: - N/A --- .../agent_ui/src/acp/completion_provider.rs | 18 +-- crates/agent_ui/src/acp/message_editor.rs | 125 +++++++++++------- crates/agent_ui/src/acp/thread_view.rs | 3 +- 3 files changed, 85 insertions(+), 61 deletions(-) diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index d2af2a880d..a8a690190a 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -763,14 +763,16 @@ fn confirm_completion_callback( message_editor .clone() .update(cx, |message_editor, cx| { - message_editor.confirm_completion( - crease_text, - start, - content_len, - mention_uri, - window, - cx, - ) + message_editor + .confirm_completion( + crease_text, + start, + content_len, + mention_uri, + window, + cx, + ) + .detach(); }) .ok(); }); diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index afb1512e5d..3ed202f66a 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -26,7 +26,7 @@ use gpui::{ }; use language::{Buffer, Language}; use language_model::LanguageModelImage; -use project::{CompletionIntent, Project, ProjectPath, Worktree}; +use project::{Project, ProjectPath, Worktree}; use rope::Point; use settings::Settings; use std::{ @@ -202,18 +202,18 @@ impl MessageEditor { mention_uri: MentionUri, window: &mut Window, cx: &mut Context<Self>, - ) { + ) -> Task<()> { let snapshot = self .editor .update(cx, |editor, cx| editor.snapshot(window, cx)); let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else { - return; + return Task::ready(()); }; let Some(anchor) = snapshot .buffer_snapshot .anchor_in_excerpt(*excerpt_id, start) else { - return; + return Task::ready(()); }; if let MentionUri::File { abs_path, .. } = &mention_uri { @@ -228,7 +228,7 @@ impl MessageEditor { .read(cx) .project_path_for_absolute_path(abs_path, cx) else { - return; + return Task::ready(()); }; let image = cx .spawn(async move |_, cx| { @@ -252,9 +252,9 @@ impl MessageEditor { window, cx, ) else { - return; + return Task::ready(()); }; - self.confirm_mention_for_image( + return self.confirm_mention_for_image( crease_id, anchor, Some(abs_path.clone()), @@ -262,7 +262,6 @@ impl MessageEditor { window, cx, ); - return; } } @@ -276,27 +275,28 @@ impl MessageEditor { window, cx, ) else { - return; + return Task::ready(()); }; match mention_uri { MentionUri::Fetch { url } => { - self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx); + self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx) } MentionUri::Directory { abs_path } => { - self.confirm_mention_for_directory(crease_id, anchor, abs_path, window, cx); + self.confirm_mention_for_directory(crease_id, anchor, abs_path, window, cx) } MentionUri::Thread { id, name } => { - self.confirm_mention_for_thread(crease_id, anchor, id, name, window, cx); + self.confirm_mention_for_thread(crease_id, anchor, id, name, window, cx) } MentionUri::TextThread { path, name } => { - self.confirm_mention_for_text_thread(crease_id, anchor, path, name, window, cx); + self.confirm_mention_for_text_thread(crease_id, anchor, path, name, window, cx) } MentionUri::File { .. } | MentionUri::Symbol { .. } | MentionUri::Rule { .. } | MentionUri::Selection { .. } => { self.mention_set.insert_uri(crease_id, mention_uri.clone()); + Task::ready(()) } } } @@ -308,7 +308,7 @@ impl MessageEditor { abs_path: PathBuf, window: &mut Window, cx: &mut Context<Self>, - ) { + ) -> Task<()> { fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc<Path>, PathBuf)> { let mut files = Vec::new(); @@ -331,13 +331,13 @@ impl MessageEditor { .read(cx) .project_path_for_absolute_path(&abs_path, cx) else { - return; + return Task::ready(()); }; let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else { - return; + return Task::ready(()); }; let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else { - return; + return Task::ready(()); }; let project = self.project.clone(); let task = cx.spawn(async move |_, cx| { @@ -396,7 +396,9 @@ impl MessageEditor { }) .shared(); - self.mention_set.directories.insert(abs_path, task.clone()); + self.mention_set + .directories + .insert(abs_path.clone(), task.clone()); let editor = self.editor.clone(); cx.spawn_in(window, async move |this, cx| { @@ -414,9 +416,12 @@ impl MessageEditor { editor.remove_creases([crease_id], cx); }) .ok(); + this.update(cx, |this, _cx| { + this.mention_set.directories.remove(&abs_path); + }) + .ok(); } }) - .detach(); } fn confirm_mention_for_fetch( @@ -426,13 +431,13 @@ impl MessageEditor { url: url::Url, window: &mut Window, cx: &mut Context<Self>, - ) { + ) -> Task<()> { let Some(http_client) = self .workspace .update(cx, |workspace, _cx| workspace.client().http_client()) .ok() else { - return; + return Task::ready(()); }; let url_string = url.to_string(); @@ -450,9 +455,9 @@ impl MessageEditor { cx.spawn_in(window, async move |this, cx| { let fetch = fetch.await.notify_async_err(cx); this.update(cx, |this, cx| { - let mention_uri = MentionUri::Fetch { url }; if fetch.is_some() { - this.mention_set.insert_uri(crease_id, mention_uri.clone()); + this.mention_set + .insert_uri(crease_id, MentionUri::Fetch { url }); } else { // Remove crease if we failed to fetch this.editor.update(cx, |editor, cx| { @@ -461,11 +466,11 @@ impl MessageEditor { }); editor.remove_creases([crease_id], cx); }); + this.mention_set.fetch_results.remove(&url); } }) .ok(); }) - .detach(); } pub fn confirm_mention_for_selection( @@ -528,7 +533,7 @@ impl MessageEditor { name: String, window: &mut Window, cx: &mut Context<Self>, - ) { + ) -> Task<()> { let uri = MentionUri::Thread { id: id.clone(), name, @@ -546,7 +551,7 @@ impl MessageEditor { }) .shared(); - self.mention_set.insert_thread(id, task.clone()); + self.mention_set.insert_thread(id.clone(), task.clone()); let editor = self.editor.clone(); cx.spawn_in(window, async move |this, cx| { @@ -564,9 +569,12 @@ impl MessageEditor { editor.remove_creases([crease_id], cx); }) .ok(); + this.update(cx, |this, _| { + this.mention_set.thread_summaries.remove(&id); + }) + .ok(); } }) - .detach(); } fn confirm_mention_for_text_thread( @@ -577,7 +585,7 @@ impl MessageEditor { name: String, window: &mut Window, cx: &mut Context<Self>, - ) { + ) -> Task<()> { let uri = MentionUri::TextThread { path: path.clone(), name, @@ -595,7 +603,8 @@ impl MessageEditor { }) .shared(); - self.mention_set.insert_text_thread(path, task.clone()); + self.mention_set + .insert_text_thread(path.clone(), task.clone()); let editor = self.editor.clone(); cx.spawn_in(window, async move |this, cx| { @@ -613,9 +622,12 @@ impl MessageEditor { editor.remove_creases([crease_id], cx); }) .ok(); + this.update(cx, |this, _| { + this.mention_set.text_thread_summaries.remove(&path); + }) + .ok(); } }) - .detach(); } pub fn contents( @@ -784,13 +796,15 @@ impl MessageEditor { ) else { return; }; - self.confirm_mention_for_image(crease_id, anchor, None, task, window, cx); + self.confirm_mention_for_image(crease_id, anchor, None, task, window, cx) + .detach(); } } pub fn insert_dragged_files( - &self, + &mut self, paths: Vec<project::ProjectPath>, + added_worktrees: Vec<Entity<Worktree>>, window: &mut Window, cx: &mut Context<Self>, ) { @@ -798,6 +812,7 @@ impl MessageEditor { let Some(buffer) = buffer.read(cx).as_singleton() else { return; }; + let mut tasks = Vec::new(); for path in paths { let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else { continue; @@ -805,39 +820,44 @@ impl MessageEditor { let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else { continue; }; - - let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len())); let path_prefix = abs_path .file_name() .unwrap_or(path.path.as_os_str()) .display() .to_string(); - let Some(completion) = ContextPickerCompletionProvider::completion_for_path( - path, - &path_prefix, - false, - entry.is_dir(), - anchor..anchor, - cx.weak_entity(), - self.project.clone(), - cx, - ) else { - continue; + let (file_name, _) = + crate::context_picker::file_context_picker::extract_file_name_and_directory( + &path.path, + &path_prefix, + ); + + let uri = if entry.is_dir() { + MentionUri::Directory { abs_path } + } else { + MentionUri::File { abs_path } }; + let new_text = format!("{} ", uri.as_link()); + let content_len = new_text.len() - 1; + + let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len())); + self.editor.update(cx, |message_editor, cx| { message_editor.edit( [( multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), - completion.new_text, + new_text, )], cx, ); }); - if let Some(confirm) = completion.confirm.clone() { - confirm(CompletionIntent::Complete, window, cx); - } + tasks.push(self.confirm_completion(file_name, anchor, content_len, uri, window, cx)); } + cx.spawn(async move |_, _| { + join_all(tasks).await; + drop(added_worktrees); + }) + .detach(); } pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) { @@ -855,7 +875,7 @@ impl MessageEditor { image: Shared<Task<Result<Arc<Image>, String>>>, window: &mut Window, cx: &mut Context<Self>, - ) { + ) -> Task<()> { let editor = self.editor.clone(); let task = cx .spawn_in(window, { @@ -900,9 +920,12 @@ impl MessageEditor { editor.remove_creases([crease_id], cx); }) .ok(); + this.update(cx, |this, _cx| { + this.mention_set.images.remove(&crease_id); + }) + .ok(); } }) - .detach(); } pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index bf5b8efbc8..f1d3870d6d 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -3429,8 +3429,7 @@ impl AcpThreadView { cx: &mut Context<Self>, ) { self.message_editor.update(cx, |message_editor, cx| { - message_editor.insert_dragged_files(paths, window, cx); - drop(added_worktrees); + message_editor.insert_dragged_files(paths, added_worktrees, window, cx); }) } From 05fc0c432c024596e68ac5223c556d2a642ff135 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 19 Aug 2025 21:26:17 +0200 Subject: [PATCH 493/693] Fix a bunch of other low-hanging style lints (#36498) - **Fix a bunch of low hanging style lints like unnecessary-return** - **Fix single worktree violation** - **And the rest** Release Notes: - N/A --- Cargo.toml | 6 + crates/acp_thread/src/acp_thread.rs | 2 +- crates/acp_thread/src/mention.rs | 8 +- crates/action_log/src/action_log.rs | 10 +- crates/agent/src/agent_profile.rs | 2 +- crates/agent/src/thread_store.rs | 2 +- crates/agent/src/tool_use.rs | 2 +- crates/agent2/src/db.rs | 2 +- crates/agent2/src/tests/mod.rs | 6 +- crates/agent2/src/thread.rs | 4 +- crates/agent2/src/tools/read_file_tool.rs | 2 +- crates/agent2/src/tools/terminal_tool.rs | 8 +- crates/agent_servers/src/agent_servers.rs | 4 +- crates/agent_servers/src/e2e_tests.rs | 2 +- crates/agent_settings/src/agent_profile.rs | 2 +- .../agent_ui/src/acp/completion_provider.rs | 2 +- crates/agent_ui/src/acp/message_editor.rs | 4 +- crates/agent_ui/src/acp/thread_view.rs | 32 ++-- crates/agent_ui/src/active_thread.rs | 4 +- crates/agent_ui/src/agent_configuration.rs | 2 +- crates/agent_ui/src/agent_diff.rs | 12 +- crates/agent_ui/src/agent_panel.rs | 14 +- crates/agent_ui/src/context_picker.rs | 6 +- .../src/context_picker/completion_provider.rs | 2 +- .../context_picker/fetch_context_picker.rs | 7 +- .../src/context_picker/file_context_picker.rs | 4 +- .../context_picker/rules_context_picker.rs | 2 +- .../context_picker/symbol_context_picker.rs | 2 +- .../context_picker/thread_context_picker.rs | 10 +- crates/agent_ui/src/inline_assistant.rs | 2 +- crates/agent_ui/src/inline_prompt_editor.rs | 2 +- crates/agent_ui/src/message_editor.rs | 6 +- crates/agent_ui/src/slash_command_picker.rs | 10 +- crates/agent_ui/src/text_thread_editor.rs | 4 +- crates/askpass/src/askpass.rs | 6 +- .../src/assistant_context.rs | 21 ++- .../src/assistant_context_tests.rs | 8 +- crates/assistant_context/src/context_store.rs | 2 +- .../src/context_server_command.rs | 7 +- .../src/diagnostics_command.rs | 2 +- .../src/file_command.rs | 4 +- crates/assistant_tools/src/assistant_tools.rs | 2 +- .../assistant_tools/src/edit_agent/evals.rs | 2 +- crates/assistant_tools/src/read_file_tool.rs | 2 +- crates/assistant_tools/src/terminal_tool.rs | 8 +- crates/bedrock/src/bedrock.rs | 6 +- crates/call/src/call_impl/mod.rs | 2 +- crates/call/src/call_impl/participant.rs | 2 +- crates/call/src/call_impl/room.rs | 23 ++- crates/channel/src/channel_chat.rs | 6 +- crates/channel/src/channel_store.rs | 6 +- crates/cli/src/main.rs | 8 +- crates/client/src/client.rs | 2 +- crates/client/src/user.rs | 2 +- .../cloud_api_client/src/cloud_api_client.rs | 6 +- .../random_project_collaboration_tests.rs | 2 +- crates/collab_ui/src/chat_panel.rs | 4 +- crates/collab_ui/src/collab_panel.rs | 6 +- crates/collab_ui/src/notification_panel.rs | 2 +- .../src/credentials_provider.rs | 2 +- crates/dap_adapters/src/codelldb.rs | 2 +- crates/db/src/db.rs | 2 +- crates/db/src/kvp.rs | 2 +- crates/debugger_tools/src/dap_log.rs | 2 +- crates/debugger_ui/src/session/running.rs | 5 +- .../src/session/running/breakpoint_list.rs | 2 +- crates/diagnostics/src/diagnostic_renderer.rs | 16 +- crates/diagnostics/src/diagnostics.rs | 10 +- crates/docs_preprocessor/src/main.rs | 2 +- .../src/edit_prediction_button.rs | 6 +- crates/editor/src/clangd_ext.rs | 2 +- crates/editor/src/code_context_menus.rs | 6 +- crates/editor/src/display_map.rs | 2 +- crates/editor/src/display_map/block_map.rs | 27 ++- crates/editor/src/display_map/fold_map.rs | 22 +-- crates/editor/src/display_map/inlay_map.rs | 2 +- crates/editor/src/display_map/wrap_map.rs | 20 +-- crates/editor/src/editor.rs | 111 ++++++------- crates/editor/src/editor_settings.rs | 6 +- crates/editor/src/element.rs | 120 ++++++------- crates/editor/src/git/blame.rs | 2 +- crates/editor/src/hover_links.rs | 4 +- crates/editor/src/items.rs | 4 +- crates/editor/src/jsx_tag_auto_close.rs | 24 ++- crates/editor/src/mouse_context_menu.rs | 4 +- crates/editor/src/tasks.rs | 2 +- crates/eval/src/assertions.rs | 2 +- crates/eval/src/eval.rs | 2 +- .../src/examples/add_arg_to_trait_method.rs | 6 +- crates/extension/src/extension_builder.rs | 2 +- crates/extension/src/extension_events.rs | 5 +- crates/extension_host/src/extension_host.rs | 12 +- crates/feature_flags/src/feature_flags.rs | 2 +- crates/feedback/src/system_specs.rs | 6 +- crates/file_finder/src/file_finder.rs | 4 +- crates/file_finder/src/open_path_prompt.rs | 2 +- crates/file_icons/src/file_icons.rs | 2 +- crates/fs/src/fs.rs | 4 +- crates/git/src/repository.rs | 8 +- crates/git_ui/src/commit_view.rs | 3 +- crates/git_ui/src/git_panel.rs | 45 ++--- crates/git_ui/src/project_diff.rs | 4 +- crates/go_to_line/src/cursor_position.rs | 2 +- crates/google_ai/src/google_ai.rs | 4 +- crates/gpui/src/app/entity_map.rs | 2 +- crates/gpui/src/elements/div.rs | 6 +- crates/gpui/src/elements/image_cache.rs | 2 +- crates/gpui/src/elements/list.rs | 7 +- crates/gpui/src/key_dispatch.rs | 7 +- crates/gpui/src/keymap/context.rs | 4 +- crates/gpui/src/platform.rs | 2 +- .../gpui/src/platform/blade/blade_context.rs | 2 +- crates/gpui/src/platform/linux/platform.rs | 2 +- .../gpui/src/platform/linux/wayland/client.rs | 6 +- .../gpui/src/platform/linux/wayland/window.rs | 4 +- crates/gpui/src/platform/linux/x11/client.rs | 16 +- .../gpui/src/platform/linux/x11/clipboard.rs | 4 +- crates/gpui/src/platform/linux/x11/event.rs | 2 +- crates/gpui/src/platform/mac/events.rs | 5 +- crates/gpui/src/platform/mac/platform.rs | 2 +- crates/gpui/src/platform/mac/text_system.rs | 2 +- crates/gpui/src/platform/mac/window.rs | 4 +- crates/gpui/src/platform/test/dispatcher.rs | 4 +- crates/gpui/src/style.rs | 6 +- crates/gpui/src/text_system.rs | 2 +- crates/gpui/src/window.rs | 10 +- .../src/derive_inspector_reflection.rs | 2 +- crates/language/src/buffer.rs | 37 ++--- crates/language/src/language_registry.rs | 2 +- crates/language/src/language_settings.rs | 2 +- crates/language/src/syntax_map.rs | 6 +- .../language_models/src/provider/anthropic.rs | 4 +- crates/language_models/src/provider/cloud.rs | 2 +- crates/language_models/src/provider/google.rs | 2 +- crates/language_tools/src/key_context_view.rs | 8 +- crates/language_tools/src/lsp_tool.rs | 2 - crates/languages/src/go.rs | 2 +- crates/languages/src/json.rs | 2 +- crates/languages/src/python.rs | 2 +- crates/languages/src/rust.rs | 4 +- crates/livekit_client/examples/test_app.rs | 4 +- .../markdown_preview/src/markdown_parser.rs | 8 +- .../markdown_preview/src/markdown_renderer.rs | 5 +- .../src/migrations/m_2025_05_05/settings.rs | 4 +- crates/multi_buffer/src/multi_buffer.rs | 157 +++++++++--------- crates/node_runtime/src/node_runtime.rs | 2 +- crates/onboarding/src/theme_preview.rs | 8 +- crates/open_ai/src/open_ai.rs | 4 +- crates/open_router/src/open_router.rs | 2 +- crates/outline_panel/src/outline_panel.rs | 35 ++-- crates/prettier/src/prettier.rs | 4 +- crates/project/src/buffer_store.rs | 7 +- crates/project/src/color_extractor.rs | 2 +- crates/project/src/debugger/locators/cargo.rs | 2 +- .../project/src/debugger/locators/python.rs | 4 +- crates/project/src/debugger/memory.rs | 2 +- crates/project/src/debugger/session.rs | 8 +- crates/project/src/git_store.rs | 14 +- crates/project/src/lsp_command.rs | 4 +- crates/project/src/lsp_store.rs | 45 +++-- crates/project/src/manifest_tree.rs | 6 +- .../project/src/manifest_tree/server_tree.rs | 2 +- crates/project/src/project.rs | 19 ++- crates/project/src/project_settings.rs | 6 +- crates/project/src/project_tests.rs | 7 +- crates/project/src/terminals.rs | 4 +- crates/project_panel/src/project_panel.rs | 42 +++-- crates/project_symbols/src/project_symbols.rs | 2 +- crates/prompt_store/src/prompt_store.rs | 4 +- crates/prompt_store/src/prompts.rs | 2 +- .../src/disconnected_overlay.rs | 2 +- crates/recent_projects/src/remote_servers.rs | 2 +- crates/recent_projects/src/ssh_connections.rs | 2 +- crates/remote/src/ssh_session.rs | 4 +- crates/repl/src/components/kernel_options.rs | 2 +- crates/repl/src/repl_editor.rs | 2 +- crates/rope/src/rope.rs | 4 +- crates/rules_library/src/rules_library.rs | 4 +- crates/settings/src/keymap_file.rs | 8 +- crates/settings/src/settings_json.rs | 8 +- crates/settings/src/settings_store.rs | 3 +- .../src/settings_profile_selector.rs | 2 +- crates/settings_ui/src/keybindings.rs | 35 ++-- .../src/ui_components/keystroke_input.rs | 16 +- crates/settings_ui/src/ui_components/table.rs | 2 +- crates/snippet/src/snippet.rs | 2 +- crates/snippet_provider/src/lib.rs | 6 +- crates/sum_tree/src/sum_tree.rs | 6 +- crates/supermaven/src/supermaven.rs | 2 +- crates/supermaven_api/src/supermaven_api.rs | 4 +- crates/tasks_ui/src/tasks_ui.rs | 4 +- crates/telemetry/src/telemetry.rs | 1 - crates/terminal/src/pty_info.rs | 2 +- crates/terminal/src/terminal.rs | 8 +- crates/terminal/src/terminal_hyperlinks.rs | 4 +- crates/terminal_view/src/color_contrast.rs | 8 +- crates/terminal_view/src/terminal_element.rs | 2 +- crates/terminal_view/src/terminal_panel.rs | 42 +++-- crates/terminal_view/src/terminal_view.rs | 4 +- crates/text/src/anchor.rs | 2 +- crates/text/src/patch.rs | 4 +- crates/text/src/text.rs | 6 +- crates/title_bar/src/collab.rs | 2 +- crates/title_bar/src/onboarding_banner.rs | 2 +- crates/ui/src/components/popover_menu.rs | 6 +- crates/ui/src/components/sticky_items.rs | 1 - crates/util/src/paths.rs | 4 +- crates/util/src/size.rs | 12 +- crates/util/src/util.rs | 4 +- crates/vim/src/command.rs | 3 +- crates/vim/src/digraph.rs | 1 - crates/vim/src/helix.rs | 1 - crates/vim/src/motion.rs | 4 +- crates/vim/src/normal/mark.rs | 1 - crates/vim/src/normal/search.rs | 2 +- crates/vim/src/state.rs | 4 +- .../src/web_search_providers.rs | 2 +- crates/workspace/src/dock.rs | 4 +- crates/workspace/src/item.rs | 2 +- crates/workspace/src/pane.rs | 16 +- crates/workspace/src/workspace.rs | 28 ++-- crates/worktree/src/worktree.rs | 26 ++- crates/worktree/src/worktree_settings.rs | 2 +- crates/zed/src/zed.rs | 2 +- crates/zed/src/zed/migrate.rs | 2 +- crates/zed/src/zed/quick_action_bar.rs | 6 +- crates/zeta/src/init.rs | 8 +- crates/zeta/src/onboarding_modal.rs | 2 +- crates/zeta/src/rate_completion_modal.rs | 2 +- crates/zeta/src/zeta.rs | 4 +- crates/zlog/src/filter.rs | 28 ++-- crates/zlog/src/zlog.rs | 10 +- extensions/glsl/src/glsl.rs | 4 +- extensions/html/src/html.rs | 2 +- extensions/ruff/src/ruff.rs | 4 +- extensions/snippets/src/snippets.rs | 4 +- .../test-extension/src/test_extension.rs | 4 +- extensions/toml/src/toml.rs | 4 +- tooling/xtask/src/tasks/package_conformity.rs | 6 +- 239 files changed, 854 insertions(+), 1015 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 603897084c..46c5646c90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -821,6 +821,7 @@ single_range_in_vec_init = "allow" style = { level = "allow", priority = -1 } # Temporary list of style lints that we've fixed so far. +comparison_to_empty = "warn" iter_cloned_collect = "warn" iter_next_slice = "warn" iter_nth = "warn" @@ -831,8 +832,13 @@ question_mark = { level = "deny" } redundant_closure = { level = "deny" } declare_interior_mutable_const = { level = "deny" } collapsible_if = { level = "warn"} +collapsible_else_if = { level = "warn" } needless_borrow = { level = "warn"} +needless_return = { level = "warn" } unnecessary_mut_passed = {level = "warn"} +unnecessary_map_or = { level = "warn" } +unused_unit = "warn" + # Individual rules that have violations in the codebase: type_complexity = "allow" # We often return trait objects from `new` functions. diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 1de8110f07..d4d73e1edd 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -49,7 +49,7 @@ impl UserMessage { if self .checkpoint .as_ref() - .map_or(false, |checkpoint| checkpoint.show) + .is_some_and(|checkpoint| checkpoint.show) { writeln!(markdown, "## User (checkpoint)").unwrap(); } else { diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index fcf50b0fd7..4615e9a551 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -79,12 +79,10 @@ impl MentionUri { } else { Ok(Self::Selection { path, line_range }) } + } else if input.ends_with("/") { + Ok(Self::Directory { abs_path: path }) } else { - if input.ends_with("/") { - Ok(Self::Directory { abs_path: path }) - } else { - Ok(Self::File { abs_path: path }) - } + Ok(Self::File { abs_path: path }) } } "zed" => { diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs index ceced1bcdd..602357ed2b 100644 --- a/crates/action_log/src/action_log.rs +++ b/crates/action_log/src/action_log.rs @@ -116,7 +116,7 @@ impl ActionLog { } else if buffer .read(cx) .file() - .map_or(false, |file| file.disk_state().exists()) + .is_some_and(|file| file.disk_state().exists()) { TrackedBufferStatus::Created { existing_file_content: Some(buffer.read(cx).as_rope().clone()), @@ -215,7 +215,7 @@ impl ActionLog { if buffer .read(cx) .file() - .map_or(false, |file| file.disk_state() == DiskState::Deleted) + .is_some_and(|file| file.disk_state() == DiskState::Deleted) { // If the buffer had been edited by a tool, but it got // deleted externally, we want to stop tracking it. @@ -227,7 +227,7 @@ impl ActionLog { if buffer .read(cx) .file() - .map_or(false, |file| file.disk_state() != DiskState::Deleted) + .is_some_and(|file| file.disk_state() != DiskState::Deleted) { // If the buffer had been deleted by a tool, but it got // resurrected externally, we want to clear the edits we @@ -811,7 +811,7 @@ impl ActionLog { tracked.version != buffer.version && buffer .file() - .map_or(false, |file| file.disk_state() != DiskState::Deleted) + .is_some_and(|file| file.disk_state() != DiskState::Deleted) }) .map(|(buffer, _)| buffer) } @@ -847,7 +847,7 @@ fn apply_non_conflicting_edits( conflict = true; if new_edits .peek() - .map_or(false, |next_edit| next_edit.old.overlaps(&old_edit.new)) + .is_some_and(|next_edit| next_edit.old.overlaps(&old_edit.new)) { new_edit = new_edits.next().unwrap(); } else { diff --git a/crates/agent/src/agent_profile.rs b/crates/agent/src/agent_profile.rs index 38e697dd9b..1636508df6 100644 --- a/crates/agent/src/agent_profile.rs +++ b/crates/agent/src/agent_profile.rs @@ -90,7 +90,7 @@ impl AgentProfile { return false; }; - return Self::is_enabled(settings, source, tool_name); + Self::is_enabled(settings, source, tool_name) } fn is_enabled(settings: &AgentProfileSettings, source: ToolSource, name: String) -> bool { diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index ed1605aacf..63d0f72e00 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -42,7 +42,7 @@ use std::{ use util::ResultExt as _; pub static ZED_STATELESS: std::sync::LazyLock<bool> = - std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty())); + std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty())); #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum DataType { diff --git a/crates/agent/src/tool_use.rs b/crates/agent/src/tool_use.rs index d109891bf2..962dca591f 100644 --- a/crates/agent/src/tool_use.rs +++ b/crates/agent/src/tool_use.rs @@ -275,7 +275,7 @@ impl ToolUseState { pub fn message_has_tool_results(&self, assistant_message_id: MessageId) -> bool { self.tool_uses_by_assistant_message .get(&assistant_message_id) - .map_or(false, |results| !results.is_empty()) + .is_some_and(|results| !results.is_empty()) } pub fn tool_result( diff --git a/crates/agent2/src/db.rs b/crates/agent2/src/db.rs index c3e6352ef6..27a109c573 100644 --- a/crates/agent2/src/db.rs +++ b/crates/agent2/src/db.rs @@ -184,7 +184,7 @@ impl DbThread { } pub static ZED_STATELESS: std::sync::LazyLock<bool> = - std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty())); + std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty())); #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum DataType { diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index f01873cfc1..7fa12e5711 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -742,7 +742,7 @@ async fn expect_tool_call(events: &mut UnboundedReceiver<Result<ThreadEvent>>) - .expect("no tool call authorization event received") .unwrap(); match event { - ThreadEvent::ToolCall(tool_call) => return tool_call, + ThreadEvent::ToolCall(tool_call) => tool_call, event => { panic!("Unexpected event {event:?}"); } @@ -758,9 +758,7 @@ async fn expect_tool_call_update_fields( .expect("no tool call authorization event received") .unwrap(); match event { - ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => { - return update; - } + ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => update, event => { panic!("Unexpected event {event:?}"); } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 66b4485f72..ba5cd1f477 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1356,7 +1356,7 @@ impl Thread { // Ensure the last message ends in the current tool use let last_message = self.pending_message(); - let push_new_tool_use = last_message.content.last_mut().map_or(true, |content| { + let push_new_tool_use = last_message.content.last_mut().is_none_or(|content| { if let AgentMessageContent::ToolUse(last_tool_use) = content { if last_tool_use.id == tool_use.id { *last_tool_use = tool_use.clone(); @@ -1408,7 +1408,7 @@ impl Thread { status: Some(acp::ToolCallStatus::InProgress), ..Default::default() }); - let supports_images = self.model().map_or(false, |model| model.supports_images()); + let supports_images = self.model().is_some_and(|model| model.supports_images()); let tool_result = tool.run(tool_use.input, tool_event_stream, cx); log::info!("Running tool {}", tool_use.name); Some(cx.foreground_executor().spawn(async move { diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index f21643cbbb..f37dff4f47 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -175,7 +175,7 @@ impl AgentTool for ReadFileTool { buffer .file() .as_ref() - .map_or(true, |file| !file.disk_state().exists()) + .is_none_or(|file| !file.disk_state().exists()) })? { anyhow::bail!("{file_path} not found"); } diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs index 1804d0ab30..d8f0282f4b 100644 --- a/crates/agent2/src/tools/terminal_tool.rs +++ b/crates/agent2/src/tools/terminal_tool.rs @@ -271,7 +271,7 @@ fn working_dir( let project = project.read(cx); let cd = &input.cd; - if cd == "." || cd == "" { + if cd == "." || cd.is_empty() { // Accept "." or "" as meaning "the one worktree" if we only have one worktree. let mut worktrees = project.worktrees(cx); @@ -296,10 +296,8 @@ fn working_dir( { return Ok(Some(input_path.into())); } - } else { - if let Some(worktree) = project.worktree_for_root_name(cd, cx) { - return Ok(Some(worktree.read(cx).abs_path().to_path_buf())); - } + } else if let Some(worktree) = project.worktree_for_root_name(cd, cx) { + return Ok(Some(worktree.read(cx).abs_path().to_path_buf())); } anyhow::bail!("`cd` directory {cd:?} was not in any of the project's worktrees."); diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 8f8aa1d788..cebf82cddb 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -104,7 +104,7 @@ impl AgentServerCommand { cx: &mut AsyncApp, ) -> Option<Self> { if let Some(agent_settings) = settings { - return Some(Self { + Some(Self { path: agent_settings.command.path, args: agent_settings .command @@ -113,7 +113,7 @@ impl AgentServerCommand { .chain(extra_args.iter().map(|arg| arg.to_string())) .collect(), env: agent_settings.command.env, - }); + }) } else { match find_bin_in_path(path_bin_name, project, cx).await { Some(path) => Some(Self { diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 2b32edcd4f..fef80b4d42 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -471,7 +471,7 @@ pub fn get_zed_path() -> PathBuf { while zed_path .file_name() - .map_or(true, |name| name.to_string_lossy() != "debug") + .is_none_or(|name| name.to_string_lossy() != "debug") { if !zed_path.pop() { panic!("Could not find target directory"); diff --git a/crates/agent_settings/src/agent_profile.rs b/crates/agent_settings/src/agent_profile.rs index 402cf81678..04fdd4a753 100644 --- a/crates/agent_settings/src/agent_profile.rs +++ b/crates/agent_settings/src/agent_profile.rs @@ -58,7 +58,7 @@ impl AgentProfileSettings { || self .context_servers .get(server_id) - .map_or(false, |preset| preset.tools.get(tool_name) == Some(&true)) + .is_some_and(|preset| preset.tools.get(tool_name) == Some(&true)) } } diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index a8a690190a..1a5e9c7d81 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -797,7 +797,7 @@ impl MentionCompletion { && line .chars() .nth(last_mention_start - 1) - .map_or(false, |c| !c.is_whitespace()) + .is_some_and(|c| !c.is_whitespace()) { return None; } diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 3ed202f66a..e7f0d4f88f 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1552,14 +1552,14 @@ impl SemanticsProvider for SlashCommandSemanticsProvider { return None; } let range = snapshot.anchor_after(start)..snapshot.anchor_after(end); - return Some(Task::ready(vec![project::Hover { + Some(Task::ready(vec![project::Hover { contents: vec![project::HoverBlock { text: "Slash commands are not supported".into(), kind: project::HoverBlockKind::PlainText, }], range: Some(range), language: None, - }])); + }])) } fn inline_values( diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index f1d3870d6d..7b38ba9301 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -102,7 +102,7 @@ impl ProfileProvider for Entity<agent2::Thread> { fn profiles_supported(&self, cx: &App) -> bool { self.read(cx) .model() - .map_or(false, |model| model.supports_tools()) + .is_some_and(|model| model.supports_tools()) } } @@ -2843,7 +2843,7 @@ impl AcpThreadView { if thread .model() - .map_or(true, |model| !model.supports_burn_mode()) + .is_none_or(|model| !model.supports_burn_mode()) { return None; } @@ -2875,9 +2875,9 @@ impl AcpThreadView { fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement { let is_editor_empty = self.message_editor.read(cx).is_empty(cx); - let is_generating = self.thread().map_or(false, |thread| { - thread.read(cx).status() != ThreadStatus::Idle - }); + let is_generating = self + .thread() + .is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle); if is_generating && is_editor_empty { IconButton::new("stop-generation", IconName::Stop) @@ -3455,18 +3455,16 @@ impl AcpThreadView { } else { format!("Retrying. Next attempt in {next_attempt_in_secs} seconds.") } + } else if next_attempt_in_secs == 1 { + format!( + "Retrying. Next attempt in 1 second (Attempt {} of {}).", + state.attempt, state.max_attempts, + ) } else { - if next_attempt_in_secs == 1 { - format!( - "Retrying. Next attempt in 1 second (Attempt {} of {}).", - state.attempt, state.max_attempts, - ) - } else { - format!( - "Retrying. Next attempt in {next_attempt_in_secs} seconds (Attempt {} of {}).", - state.attempt, state.max_attempts, - ) - } + format!( + "Retrying. Next attempt in {next_attempt_in_secs} seconds (Attempt {} of {}).", + state.attempt, state.max_attempts, + ) }; Some( @@ -3552,7 +3550,7 @@ impl AcpThreadView { let supports_burn_mode = thread .read(cx) .model() - .map_or(false, |model| model.supports_burn_mode()); + .is_some_and(|model| model.supports_burn_mode()); let focus_handle = self.focus_handle(cx); diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 3defa42d17..a1e51f883a 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -2246,9 +2246,7 @@ impl ActiveThread { let after_editing_message = self .editing_message .as_ref() - .map_or(false, |(editing_message_id, _)| { - message_id > *editing_message_id - }); + .is_some_and(|(editing_message_id, _)| message_id > *editing_message_id); let backdrop = div() .id(("backdrop", ix)) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index a0584f9e2e..b032201d8c 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -96,7 +96,7 @@ impl AgentConfiguration { let mut expanded_provider_configurations = HashMap::default(); if LanguageModelRegistry::read_global(cx) .provider(&ZED_CLOUD_PROVIDER_ID) - .map_or(false, |cloud_provider| cloud_provider.must_accept_terms(cx)) + .is_some_and(|cloud_provider| cloud_provider.must_accept_terms(cx)) { expanded_provider_configurations.insert(ZED_CLOUD_PROVIDER_ID, true); } diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 5b4f1038e2..e80cd20846 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -285,7 +285,7 @@ impl AgentDiffPane { && buffer .read(cx) .file() - .map_or(false, |file| file.disk_state() == DiskState::Deleted) + .is_some_and(|file| file.disk_state() == DiskState::Deleted) { editor.fold_buffer(snapshot.text.remote_id(), cx) } @@ -1063,7 +1063,7 @@ impl ToolbarItemView for AgentDiffToolbar { } self.active_item = None; - return self.location(cx); + self.location(cx) } fn pane_focus_update( @@ -1509,7 +1509,7 @@ impl AgentDiff { .read(cx) .entries() .last() - .map_or(false, |entry| entry.diffs().next().is_some()) + .is_some_and(|entry| entry.diffs().next().is_some()) { self.update_reviewing_editors(workspace, window, cx); } @@ -1519,7 +1519,7 @@ impl AgentDiff { .read(cx) .entries() .get(*ix) - .map_or(false, |entry| entry.diffs().next().is_some()) + .is_some_and(|entry| entry.diffs().next().is_some()) { self.update_reviewing_editors(workspace, window, cx); } @@ -1709,7 +1709,7 @@ impl AgentDiff { .read_with(cx, |editor, _cx| editor.workspace()) .ok() .flatten() - .map_or(false, |editor_workspace| { + .is_some_and(|editor_workspace| { editor_workspace.entity_id() == workspace.entity_id() }); @@ -1868,7 +1868,7 @@ impl AgentDiff { } } - return Some(Task::ready(Ok(()))); + Some(Task::ready(Ok(()))) } } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index f9aea84376..c79349e3a9 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1463,7 +1463,7 @@ impl AgentPanel { AssistantConfigurationEvent::NewThread(provider) => { if LanguageModelRegistry::read_global(cx) .default_model() - .map_or(true, |model| model.provider.id() != provider.id()) + .is_none_or(|model| model.provider.id() != provider.id()) && let Some(model) = provider.default_model(cx) { update_settings_file::<AgentSettings>( @@ -2708,9 +2708,7 @@ impl AgentPanel { } ActiveView::ExternalAgentThread { .. } | ActiveView::History - | ActiveView::Configuration => { - return None; - } + | ActiveView::Configuration => None, } } @@ -2726,7 +2724,7 @@ impl AgentPanel { .thread() .read(cx) .configured_model() - .map_or(false, |model| { + .is_some_and(|model| { model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID }) { @@ -2737,7 +2735,7 @@ impl AgentPanel { if LanguageModelRegistry::global(cx) .read(cx) .default_model() - .map_or(false, |model| { + .is_some_and(|model| { model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID }) { @@ -3051,9 +3049,7 @@ impl AgentPanel { let zed_provider_configured = AgentSettings::get_global(cx) .default_model .as_ref() - .map_or(false, |selection| { - selection.provider.0.as_str() == "zed.dev" - }); + .is_some_and(|selection| selection.provider.0.as_str() == "zed.dev"); let callout = if zed_provider_configured { Callout::new() diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index 131023d249..697f704991 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -610,9 +610,7 @@ pub(crate) fn available_context_picker_entries( .read(cx) .active_item(cx) .and_then(|item| item.downcast::<Editor>()) - .map_or(false, |editor| { - editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx)) - }); + .is_some_and(|editor| editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))); if has_selection { entries.push(ContextPickerEntry::Action( ContextPickerAction::AddSelections, @@ -680,7 +678,7 @@ pub(crate) fn recent_context_picker_entries( .filter(|(_, abs_path)| { abs_path .as_ref() - .map_or(true, |path| !exclude_paths.contains(path.as_path())) + .is_none_or(|path| !exclude_paths.contains(path.as_path())) }) .take(4) .filter_map(|(project_path, _)| { diff --git a/crates/agent_ui/src/context_picker/completion_provider.rs b/crates/agent_ui/src/context_picker/completion_provider.rs index 79e56acacf..747ec46e0a 100644 --- a/crates/agent_ui/src/context_picker/completion_provider.rs +++ b/crates/agent_ui/src/context_picker/completion_provider.rs @@ -1020,7 +1020,7 @@ impl MentionCompletion { && line .chars() .nth(last_mention_start - 1) - .map_or(false, |c| !c.is_whitespace()) + .is_some_and(|c| !c.is_whitespace()) { return None; } diff --git a/crates/agent_ui/src/context_picker/fetch_context_picker.rs b/crates/agent_ui/src/context_picker/fetch_context_picker.rs index 8ff68a8365..dd558b2a1c 100644 --- a/crates/agent_ui/src/context_picker/fetch_context_picker.rs +++ b/crates/agent_ui/src/context_picker/fetch_context_picker.rs @@ -226,9 +226,10 @@ impl PickerDelegate for FetchContextPickerDelegate { _window: &mut Window, cx: &mut Context<Picker<Self>>, ) -> Option<Self::ListItem> { - let added = self.context_store.upgrade().map_or(false, |context_store| { - context_store.read(cx).includes_url(&self.url) - }); + let added = self + .context_store + .upgrade() + .is_some_and(|context_store| context_store.read(cx).includes_url(&self.url)); Some( ListItem::new(ix) diff --git a/crates/agent_ui/src/context_picker/file_context_picker.rs b/crates/agent_ui/src/context_picker/file_context_picker.rs index 4f74e2cea4..6c224caf4c 100644 --- a/crates/agent_ui/src/context_picker/file_context_picker.rs +++ b/crates/agent_ui/src/context_picker/file_context_picker.rs @@ -239,9 +239,7 @@ pub(crate) fn search_files( PathMatchCandidateSet { snapshot: worktree.snapshot(), - include_ignored: worktree - .root_entry() - .map_or(false, |entry| entry.is_ignored), + include_ignored: worktree.root_entry().is_some_and(|entry| entry.is_ignored), include_root_name: true, candidates: project::Candidates::Entries, } diff --git a/crates/agent_ui/src/context_picker/rules_context_picker.rs b/crates/agent_ui/src/context_picker/rules_context_picker.rs index 8ce821cfaa..f3982f61cb 100644 --- a/crates/agent_ui/src/context_picker/rules_context_picker.rs +++ b/crates/agent_ui/src/context_picker/rules_context_picker.rs @@ -159,7 +159,7 @@ pub fn render_thread_context_entry( context_store: WeakEntity<ContextStore>, cx: &mut App, ) -> Div { - let added = context_store.upgrade().map_or(false, |context_store| { + let added = context_store.upgrade().is_some_and(|context_store| { context_store .read(cx) .includes_user_rules(user_rules.prompt_id) diff --git a/crates/agent_ui/src/context_picker/symbol_context_picker.rs b/crates/agent_ui/src/context_picker/symbol_context_picker.rs index 805c10c965..b00d4e3693 100644 --- a/crates/agent_ui/src/context_picker/symbol_context_picker.rs +++ b/crates/agent_ui/src/context_picker/symbol_context_picker.rs @@ -294,7 +294,7 @@ pub(crate) fn search_symbols( .partition(|candidate| { project .entry_for_path(&symbols[candidate.id].path, cx) - .map_or(false, |e| !e.is_ignored) + .is_some_and(|e| !e.is_ignored) }) }) .log_err() diff --git a/crates/agent_ui/src/context_picker/thread_context_picker.rs b/crates/agent_ui/src/context_picker/thread_context_picker.rs index e660e64ae3..66654f3d8c 100644 --- a/crates/agent_ui/src/context_picker/thread_context_picker.rs +++ b/crates/agent_ui/src/context_picker/thread_context_picker.rs @@ -236,12 +236,10 @@ pub fn render_thread_context_entry( let is_added = match entry { ThreadContextEntry::Thread { id, .. } => context_store .upgrade() - .map_or(false, |ctx_store| ctx_store.read(cx).includes_thread(id)), - ThreadContextEntry::Context { path, .. } => { - context_store.upgrade().map_or(false, |ctx_store| { - ctx_store.read(cx).includes_text_thread(path) - }) - } + .is_some_and(|ctx_store| ctx_store.read(cx).includes_thread(id)), + ThreadContextEntry::Context { path, .. } => context_store + .upgrade() + .is_some_and(|ctx_store| ctx_store.read(cx).includes_text_thread(path)), }; h_flex() diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 101eb899b2..90302236fb 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -1120,7 +1120,7 @@ impl InlineAssistant { if editor_assists .scroll_lock .as_ref() - .map_or(false, |lock| lock.assist_id == assist_id) + .is_some_and(|lock| lock.assist_id == assist_id) { editor_assists.scroll_lock = None; } diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index 6f12050f88..5608143464 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -345,7 +345,7 @@ impl<T: 'static> PromptEditor<T> { let prompt = self.editor.read(cx).text(cx); if self .prompt_history_ix - .map_or(true, |ix| self.prompt_history[ix] != prompt) + .is_none_or(|ix| self.prompt_history[ix] != prompt) { self.prompt_history_ix.take(); self.pending_prompt = prompt; diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 64c9a873f5..6e4d2638c1 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -156,7 +156,7 @@ impl ProfileProvider for Entity<Thread> { fn profiles_supported(&self, cx: &App) -> bool { self.read(cx) .configured_model() - .map_or(false, |model| model.model.supports_tools()) + .is_some_and(|model| model.model.supports_tools()) } fn profile_id(&self, cx: &App) -> AgentProfileId { @@ -1289,7 +1289,7 @@ impl MessageEditor { self.thread .read(cx) .configured_model() - .map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID) + .is_some_and(|model| model.provider.id() == ZED_CLOUD_PROVIDER_ID) } fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> { @@ -1442,7 +1442,7 @@ impl MessageEditor { let message_text = editor.read(cx).text(cx); if message_text.is_empty() - && loaded_context.map_or(true, |loaded_context| loaded_context.is_empty()) + && loaded_context.is_none_or(|loaded_context| loaded_context.is_empty()) { return None; } diff --git a/crates/agent_ui/src/slash_command_picker.rs b/crates/agent_ui/src/slash_command_picker.rs index bab2364679..03f2c97887 100644 --- a/crates/agent_ui/src/slash_command_picker.rs +++ b/crates/agent_ui/src/slash_command_picker.rs @@ -140,12 +140,10 @@ impl PickerDelegate for SlashCommandDelegate { ); ret.push(index - 1); } - } else { - if let SlashCommandEntry::Advert { .. } = command { - previous_is_advert = true; - if index != 0 { - ret.push(index - 1); - } + } else if let SlashCommandEntry::Advert { .. } = command { + previous_is_advert = true; + if index != 0 { + ret.push(index - 1); } } } diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 3b5f2e5069..b7e5d83d6d 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -373,7 +373,7 @@ impl TextThreadEditor { .map(|default| default.provider); if provider .as_ref() - .map_or(false, |provider| provider.must_accept_terms(cx)) + .is_some_and(|provider| provider.must_accept_terms(cx)) { self.show_accept_terms = true; cx.notify(); @@ -457,7 +457,7 @@ impl TextThreadEditor { || snapshot .chars_at(newest_cursor) .next() - .map_or(false, |ch| ch != '\n') + .is_some_and(|ch| ch != '\n') { editor.move_to_end_of_line( &MoveToEndOfLine { diff --git a/crates/askpass/src/askpass.rs b/crates/askpass/src/askpass.rs index f085a2be72..9e84a9fed0 100644 --- a/crates/askpass/src/askpass.rs +++ b/crates/askpass/src/askpass.rs @@ -177,11 +177,11 @@ impl AskPassSession { _ = askpass_opened_rx.fuse() => { // Note: this await can only resolve after we are dropped. askpass_kill_master_rx.await.ok(); - return AskPassResult::CancelledByUser + AskPassResult::CancelledByUser } _ = futures::FutureExt::fuse(smol::Timer::after(connection_timeout)) => { - return AskPassResult::Timedout + AskPassResult::Timedout } } } @@ -215,7 +215,7 @@ pub fn main(socket: &str) { } #[cfg(target_os = "windows")] - while buffer.last().map_or(false, |&b| b == b'\n' || b == b'\r') { + while buffer.last().is_some_and(|&b| b == b'\n' || b == b'\r') { buffer.pop(); } if buffer.last() != Some(&b'\0') { diff --git a/crates/assistant_context/src/assistant_context.rs b/crates/assistant_context/src/assistant_context.rs index 151586564f..2d71a1c08a 100644 --- a/crates/assistant_context/src/assistant_context.rs +++ b/crates/assistant_context/src/assistant_context.rs @@ -1023,9 +1023,11 @@ impl AssistantContext { summary: new_summary, .. } => { - if self.summary.timestamp().map_or(true, |current_timestamp| { - new_summary.timestamp > current_timestamp - }) { + if self + .summary + .timestamp() + .is_none_or(|current_timestamp| new_summary.timestamp > current_timestamp) + { self.summary = ContextSummary::Content(new_summary); summary_generated = true; } @@ -1339,7 +1341,7 @@ impl AssistantContext { let is_invalid = self .messages_metadata .get(&message_id) - .map_or(true, |metadata| { + .is_none_or(|metadata| { !metadata.is_cache_valid(&buffer, &message.offset_range) || *encountered_invalid }); @@ -1860,7 +1862,7 @@ impl AssistantContext { { let newline_offset = insert_position.saturating_sub(1); if buffer.contains_str_at(newline_offset, "\n") - && last_section_range.map_or(true, |last_section_range| { + && last_section_range.is_none_or(|last_section_range| { !last_section_range .to_offset(buffer) .contains(&newline_offset) @@ -2313,10 +2315,7 @@ impl AssistantContext { let mut request_message = LanguageModelRequestMessage { role: message.role, content: Vec::new(), - cache: message - .cache - .as_ref() - .map_or(false, |cache| cache.is_anchor), + cache: message.cache.as_ref().is_some_and(|cache| cache.is_anchor), }; while let Some(content) = contents.peek() { @@ -2797,7 +2796,7 @@ impl AssistantContext { let mut current_message = messages.next(); while let Some(offset) = offsets.next() { // Locate the message that contains the offset. - while current_message.as_ref().map_or(false, |message| { + while current_message.as_ref().is_some_and(|message| { !message.offset_range.contains(&offset) && messages.peek().is_some() }) { current_message = messages.next(); @@ -2807,7 +2806,7 @@ impl AssistantContext { }; // Skip offsets that are in the same message. - while offsets.peek().map_or(false, |offset| { + while offsets.peek().is_some_and(|offset| { message.offset_range.contains(offset) || messages.peek().is_none() }) { offsets.next(); diff --git a/crates/assistant_context/src/assistant_context_tests.rs b/crates/assistant_context/src/assistant_context_tests.rs index eae7741358..28cc8ef8f0 100644 --- a/crates/assistant_context/src/assistant_context_tests.rs +++ b/crates/assistant_context/src/assistant_context_tests.rs @@ -1055,7 +1055,7 @@ fn test_mark_cache_anchors(cx: &mut App) { assert_eq!( messages_cache(&context, cx) .iter() - .filter(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor)) + .filter(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor)) .count(), 0, "Empty messages should not have any cache anchors." @@ -1083,7 +1083,7 @@ fn test_mark_cache_anchors(cx: &mut App) { assert_eq!( messages_cache(&context, cx) .iter() - .filter(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor)) + .filter(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor)) .count(), 0, "Messages should not be marked for cache before going over the token minimum." @@ -1098,7 +1098,7 @@ fn test_mark_cache_anchors(cx: &mut App) { assert_eq!( messages_cache(&context, cx) .iter() - .map(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor)) + .map(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor)) .collect::<Vec<bool>>(), vec![true, true, false], "Last message should not be an anchor on speculative request." @@ -1116,7 +1116,7 @@ fn test_mark_cache_anchors(cx: &mut App) { assert_eq!( messages_cache(&context, cx) .iter() - .map(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor)) + .map(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor)) .collect::<Vec<bool>>(), vec![false, true, true, false], "Most recent message should also be cached if not a speculative request." diff --git a/crates/assistant_context/src/context_store.rs b/crates/assistant_context/src/context_store.rs index a2b3adc686..6d13531a57 100644 --- a/crates/assistant_context/src/context_store.rs +++ b/crates/assistant_context/src/context_store.rs @@ -789,7 +789,7 @@ impl ContextStore { let fs = self.fs.clone(); cx.spawn(async move |this, cx| { pub static ZED_STATELESS: LazyLock<bool> = - LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty())); + LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty())); if *ZED_STATELESS { return Ok(()); } diff --git a/crates/assistant_slash_commands/src/context_server_command.rs b/crates/assistant_slash_commands/src/context_server_command.rs index 219c3b30bc..6caa1beb3b 100644 --- a/crates/assistant_slash_commands/src/context_server_command.rs +++ b/crates/assistant_slash_commands/src/context_server_command.rs @@ -62,9 +62,10 @@ impl SlashCommand for ContextServerSlashCommand { } fn requires_argument(&self) -> bool { - self.prompt.arguments.as_ref().map_or(false, |args| { - args.iter().any(|arg| arg.required == Some(true)) - }) + self.prompt + .arguments + .as_ref() + .is_some_and(|args| args.iter().any(|arg| arg.required == Some(true))) } fn complete_argument( diff --git a/crates/assistant_slash_commands/src/diagnostics_command.rs b/crates/assistant_slash_commands/src/diagnostics_command.rs index 45c976c826..536fe9f0ef 100644 --- a/crates/assistant_slash_commands/src/diagnostics_command.rs +++ b/crates/assistant_slash_commands/src/diagnostics_command.rs @@ -61,7 +61,7 @@ impl DiagnosticsSlashCommand { snapshot: worktree.snapshot(), include_ignored: worktree .root_entry() - .map_or(false, |entry| entry.is_ignored), + .is_some_and(|entry| entry.is_ignored), include_root_name: true, candidates: project::Candidates::Entries, } diff --git a/crates/assistant_slash_commands/src/file_command.rs b/crates/assistant_slash_commands/src/file_command.rs index c913ccc0f1..6875189927 100644 --- a/crates/assistant_slash_commands/src/file_command.rs +++ b/crates/assistant_slash_commands/src/file_command.rs @@ -92,7 +92,7 @@ impl FileSlashCommand { snapshot: worktree.snapshot(), include_ignored: worktree .root_entry() - .map_or(false, |entry| entry.is_ignored), + .is_some_and(|entry| entry.is_ignored), include_root_name: true, candidates: project::Candidates::Entries, } @@ -536,7 +536,7 @@ mod custom_path_matcher { let path_str = path.to_string_lossy(); let separator = std::path::MAIN_SEPARATOR_STR; if path_str.ends_with(separator) { - return false; + false } else { self.glob.is_match(path_str.to_string() + separator) } diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs index bf668e6918..f381103c27 100644 --- a/crates/assistant_tools/src/assistant_tools.rs +++ b/crates/assistant_tools/src/assistant_tools.rs @@ -86,7 +86,7 @@ fn register_web_search_tool(registry: &Entity<LanguageModelRegistry>, cx: &mut A let using_zed_provider = registry .read(cx) .default_model() - .map_or(false, |default| default.is_provided_by_zed()); + .is_some_and(|default| default.is_provided_by_zed()); if using_zed_provider { ToolRegistry::global(cx).register_tool(WebSearchTool); } else { diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/assistant_tools/src/edit_agent/evals.rs index 0d529a5573..ea2fa02663 100644 --- a/crates/assistant_tools/src/edit_agent/evals.rs +++ b/crates/assistant_tools/src/edit_agent/evals.rs @@ -1586,7 +1586,7 @@ impl EditAgentTest { let has_system_prompt = eval .conversation .first() - .map_or(false, |msg| msg.role == Role::System); + .is_some_and(|msg| msg.role == Role::System); let messages = if has_system_prompt { eval.conversation } else { diff --git a/crates/assistant_tools/src/read_file_tool.rs b/crates/assistant_tools/src/read_file_tool.rs index 68b870e40f..766ee3b161 100644 --- a/crates/assistant_tools/src/read_file_tool.rs +++ b/crates/assistant_tools/src/read_file_tool.rs @@ -201,7 +201,7 @@ impl Tool for ReadFileTool { buffer .file() .as_ref() - .map_or(true, |file| !file.disk_state().exists()) + .is_none_or(|file| !file.disk_state().exists()) })? { anyhow::bail!("{file_path} not found"); } diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index 3de22ad28d..dd0a0c8e4c 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -387,7 +387,7 @@ fn working_dir( let project = project.read(cx); let cd = &input.cd; - if cd == "." || cd == "" { + if cd == "." || cd.is_empty() { // Accept "." or "" as meaning "the one worktree" if we only have one worktree. let mut worktrees = project.worktrees(cx); @@ -412,10 +412,8 @@ fn working_dir( { return Ok(Some(input_path.into())); } - } else { - if let Some(worktree) = project.worktree_for_root_name(cd, cx) { - return Ok(Some(worktree.read(cx).abs_path().to_path_buf())); - } + } else if let Some(worktree) = project.worktree_for_root_name(cd, cx) { + return Ok(Some(worktree.read(cx).abs_path().to_path_buf())); } anyhow::bail!("`cd` directory {cd:?} was not in any of the project's worktrees."); diff --git a/crates/bedrock/src/bedrock.rs b/crates/bedrock/src/bedrock.rs index 1c6a9bd0a1..c8315d4201 100644 --- a/crates/bedrock/src/bedrock.rs +++ b/crates/bedrock/src/bedrock.rs @@ -54,11 +54,7 @@ pub async fn stream_completion( )]))); } - if request - .tools - .as_ref() - .map_or(false, |t| !t.tools.is_empty()) - { + if request.tools.as_ref().is_some_and(|t| !t.tools.is_empty()) { response = response.set_tool_config(request.tools); } diff --git a/crates/call/src/call_impl/mod.rs b/crates/call/src/call_impl/mod.rs index 6cc94a5dd5..156a80faba 100644 --- a/crates/call/src/call_impl/mod.rs +++ b/crates/call/src/call_impl/mod.rs @@ -147,7 +147,7 @@ impl ActiveCall { let mut incoming_call = this.incoming_call.0.borrow_mut(); if incoming_call .as_ref() - .map_or(false, |call| call.room_id == envelope.payload.room_id) + .is_some_and(|call| call.room_id == envelope.payload.room_id) { incoming_call.take(); } diff --git a/crates/call/src/call_impl/participant.rs b/crates/call/src/call_impl/participant.rs index 8e1e264a23..6fb6a2eb79 100644 --- a/crates/call/src/call_impl/participant.rs +++ b/crates/call/src/call_impl/participant.rs @@ -64,7 +64,7 @@ pub struct RemoteParticipant { impl RemoteParticipant { pub fn has_video_tracks(&self) -> bool { - return !self.video_tracks.is_empty(); + !self.video_tracks.is_empty() } pub fn can_write(&self) -> bool { diff --git a/crates/call/src/call_impl/room.rs b/crates/call/src/call_impl/room.rs index bab99cd3f3..ffe4c6c251 100644 --- a/crates/call/src/call_impl/room.rs +++ b/crates/call/src/call_impl/room.rs @@ -939,8 +939,7 @@ impl Room { self.client.user_id() ) })?; - if self.live_kit.as_ref().map_or(true, |kit| kit.deafened) && publication.is_audio() - { + if self.live_kit.as_ref().is_none_or(|kit| kit.deafened) && publication.is_audio() { publication.set_enabled(false, cx); } match track { @@ -1174,7 +1173,7 @@ impl Room { this.update(cx, |this, cx| { this.shared_projects.insert(project.downgrade()); let active_project = this.local_participant.active_project.as_ref(); - if active_project.map_or(false, |location| *location == project) { + if active_project.is_some_and(|location| *location == project) { this.set_location(Some(&project), cx) } else { Task::ready(Ok(())) @@ -1247,9 +1246,9 @@ impl Room { } pub fn is_sharing_screen(&self) -> bool { - self.live_kit.as_ref().map_or(false, |live_kit| { - !matches!(live_kit.screen_track, LocalTrack::None) - }) + self.live_kit + .as_ref() + .is_some_and(|live_kit| !matches!(live_kit.screen_track, LocalTrack::None)) } pub fn shared_screen_id(&self) -> Option<u64> { @@ -1262,13 +1261,13 @@ impl Room { } pub fn is_sharing_mic(&self) -> bool { - self.live_kit.as_ref().map_or(false, |live_kit| { - !matches!(live_kit.microphone_track, LocalTrack::None) - }) + self.live_kit + .as_ref() + .is_some_and(|live_kit| !matches!(live_kit.microphone_track, LocalTrack::None)) } pub fn is_muted(&self) -> bool { - self.live_kit.as_ref().map_or(false, |live_kit| { + self.live_kit.as_ref().is_some_and(|live_kit| { matches!(live_kit.microphone_track, LocalTrack::None) || live_kit.muted_by_user || live_kit.deafened @@ -1278,13 +1277,13 @@ impl Room { pub fn muted_by_user(&self) -> bool { self.live_kit .as_ref() - .map_or(false, |live_kit| live_kit.muted_by_user) + .is_some_and(|live_kit| live_kit.muted_by_user) } pub fn is_speaking(&self) -> bool { self.live_kit .as_ref() - .map_or(false, |live_kit| live_kit.speaking) + .is_some_and(|live_kit| live_kit.speaking) } pub fn is_deafened(&self) -> Option<bool> { diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index 86f307717c..baf23ac39f 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -340,7 +340,7 @@ impl ChannelChat { return ControlFlow::Break( if cursor .item() - .map_or(false, |message| message.id == message_id) + .is_some_and(|message| message.id == message_id) { Some(cursor.start().1.0) } else { @@ -362,7 +362,7 @@ impl ChannelChat { if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id && self .last_acknowledged_id - .map_or(true, |acknowledged_id| acknowledged_id < latest_message_id) + .is_none_or(|acknowledged_id| acknowledged_id < latest_message_id) { self.rpc .send(proto::AckChannelMessage { @@ -612,7 +612,7 @@ impl ChannelChat { while let Some(message) = old_cursor.item() { let message_ix = old_cursor.start().1.0; if nonces.contains(&message.nonce) { - if ranges.last().map_or(false, |r| r.end == message_ix) { + if ranges.last().is_some_and(|r| r.end == message_ix) { ranges.last_mut().unwrap().end += 1; } else { ranges.push(message_ix..message_ix + 1); diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 42a1851408..850a494613 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -568,16 +568,14 @@ impl ChannelStore { self.channel_index .by_id() .get(&channel_id) - .map_or(false, |channel| channel.is_root_channel()) + .is_some_and(|channel| channel.is_root_channel()) } pub fn is_public_channel(&self, channel_id: ChannelId) -> bool { self.channel_index .by_id() .get(&channel_id) - .map_or(false, |channel| { - channel.visibility == ChannelVisibility::Public - }) + .is_some_and(|channel| channel.visibility == ChannelVisibility::Public) } pub fn channel_capability(&self, channel_id: ChannelId) -> Capability { diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index d8b46dabb6..57890628f2 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -363,7 +363,7 @@ fn anonymous_fd(path: &str) -> Option<fs::File> { let fd: fd::RawFd = fd_str.parse().ok()?; let file = unsafe { fs::File::from_raw_fd(fd) }; - return Some(file); + Some(file) } #[cfg(any(target_os = "macos", target_os = "freebsd"))] { @@ -381,13 +381,13 @@ fn anonymous_fd(path: &str) -> Option<fs::File> { } let fd: fd::RawFd = fd_str.parse().ok()?; let file = unsafe { fs::File::from_raw_fd(fd) }; - return Some(file); + Some(file) } #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))] { _ = path; // not implemented for bsd, windows. Could be, but isn't yet - return None; + None } } @@ -586,7 +586,7 @@ mod flatpak { pub fn set_bin_if_no_escape(mut args: super::Args) -> super::Args { if env::var(NO_ESCAPE_ENV_NAME).is_ok() - && env::var("FLATPAK_ID").map_or(false, |id| id.starts_with("dev.zed.Zed")) + && env::var("FLATPAK_ID").is_ok_and(|id| id.starts_with("dev.zed.Zed")) && args.zed.is_none() { args.zed = Some("/app/libexec/zed-editor".into()); diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 218cf2b079..058a12417a 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -76,7 +76,7 @@ pub static ZED_APP_PATH: LazyLock<Option<PathBuf>> = LazyLock::new(|| std::env::var("ZED_APP_PATH").ok().map(PathBuf::from)); pub static ZED_ALWAYS_ACTIVE: LazyLock<bool> = - LazyLock::new(|| std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| !e.is_empty())); + LazyLock::new(|| std::env::var("ZED_ALWAYS_ACTIVE").is_ok_and(|e| !e.is_empty())); pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(500); pub const MAX_RECONNECTION_DELAY: Duration = Duration::from_secs(30); diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 722d6861ff..2599be9b16 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -848,7 +848,7 @@ impl UserStore { pub fn has_accepted_terms_of_service(&self) -> bool { self.accepted_tos_at - .map_or(false, |accepted_tos_at| accepted_tos_at.is_some()) + .is_some_and(|accepted_tos_at| accepted_tos_at.is_some()) } pub fn accept_terms_of_service(&self, cx: &Context<Self>) -> Task<Result<()>> { diff --git a/crates/cloud_api_client/src/cloud_api_client.rs b/crates/cloud_api_client/src/cloud_api_client.rs index ef9a1a9a55..92417d8319 100644 --- a/crates/cloud_api_client/src/cloud_api_client.rs +++ b/crates/cloud_api_client/src/cloud_api_client.rs @@ -205,12 +205,12 @@ impl CloudApiClient { let mut body = String::new(); response.body_mut().read_to_string(&mut body).await?; if response.status() == StatusCode::UNAUTHORIZED { - return Ok(false); + Ok(false) } else { - return Err(anyhow!( + Err(anyhow!( "Failed to get authenticated user.\nStatus: {:?}\nBody: {body}", response.status() - )); + )) } } } diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index ca8a42d54d..cd4cf69f60 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -304,7 +304,7 @@ impl RandomizedTest for ProjectCollaborationTest { let worktree = worktree.read(cx); worktree.is_visible() && worktree.entries(false, 0).any(|e| e.is_file()) - && worktree.root_entry().map_or(false, |e| e.is_dir()) + && worktree.root_entry().is_some_and(|e| e.is_dir()) }) .choose(rng) }); diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 77ce74d581..5ed3907f6c 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -890,7 +890,7 @@ impl ChatPanel { this.highlighted_message = Some((highlight_message_id, task)); } - if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) { + if this.active_chat.as_ref().is_some_and(|(c, _)| *c == chat) { this.message_list.scroll_to(ListOffset { item_ix, offset_in_item: px(0.0), @@ -1186,7 +1186,7 @@ impl Panel for ChatPanel { let is_in_call = ActiveCall::global(cx) .read(cx) .room() - .map_or(false, |room| room.read(cx).contains_guests()); + .is_some_and(|room| room.read(cx).contains_guests()); self.active || is_in_call } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 526aacf066..0f785c1f90 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -664,9 +664,7 @@ impl CollabPanel { let has_children = channel_store .channel_at_index(mat.candidate_id + 1) - .map_or(false, |next_channel| { - next_channel.parent_path.ends_with(&[channel.id]) - }); + .is_some_and(|next_channel| next_channel.parent_path.ends_with(&[channel.id])); match &self.channel_editing_state { Some(ChannelEditingState::Create { @@ -1125,7 +1123,7 @@ impl CollabPanel { } fn has_subchannels(&self, ix: usize) -> bool { - self.entries.get(ix).map_or(false, |entry| { + self.entries.get(ix).is_some_and(|entry| { if let ListEntry::Channel { has_children, .. } = entry { *has_children } else { diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 00c3bbf623..a900d585f8 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -497,7 +497,7 @@ impl NotificationPanel { panel.is_scrolled_to_bottom() && panel .active_chat() - .map_or(false, |chat| chat.read(cx).channel_id.0 == *channel_id) + .is_some_and(|chat| chat.read(cx).channel_id.0 == *channel_id) } else { false }; diff --git a/crates/credentials_provider/src/credentials_provider.rs b/crates/credentials_provider/src/credentials_provider.rs index f72fd6c39b..2c8dd6fc81 100644 --- a/crates/credentials_provider/src/credentials_provider.rs +++ b/crates/credentials_provider/src/credentials_provider.rs @@ -19,7 +19,7 @@ use release_channel::ReleaseChannel; /// Only works in development. Setting this environment variable in other /// release channels is a no-op. static ZED_DEVELOPMENT_USE_KEYCHAIN: LazyLock<bool> = LazyLock::new(|| { - std::env::var("ZED_DEVELOPMENT_USE_KEYCHAIN").map_or(false, |value| !value.is_empty()) + std::env::var("ZED_DEVELOPMENT_USE_KEYCHAIN").is_ok_and(|value| !value.is_empty()) }); /// A provider for credentials. diff --git a/crates/dap_adapters/src/codelldb.rs b/crates/dap_adapters/src/codelldb.rs index 842bb264a8..25dc875740 100644 --- a/crates/dap_adapters/src/codelldb.rs +++ b/crates/dap_adapters/src/codelldb.rs @@ -385,7 +385,7 @@ impl DebugAdapter for CodeLldbDebugAdapter { && let Some(source_languages) = config.get("sourceLanguages").filter(|value| { value .as_array() - .map_or(false, |array| array.iter().all(Value::is_string)) + .is_some_and(|array| array.iter().all(Value::is_string)) }) { let ret = vec![ diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 7fed761f5a..37e347282d 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -37,7 +37,7 @@ const FALLBACK_DB_NAME: &str = "FALLBACK_MEMORY_DB"; const DB_FILE_NAME: &str = "db.sqlite"; pub static ZED_STATELESS: LazyLock<bool> = - LazyLock::new(|| env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty())); + LazyLock::new(|| env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty())); pub static ALL_FILE_DB_FAILED: LazyLock<AtomicBool> = LazyLock::new(|| AtomicBool::new(false)); diff --git a/crates/db/src/kvp.rs b/crates/db/src/kvp.rs index daf0b136fd..256b789c9b 100644 --- a/crates/db/src/kvp.rs +++ b/crates/db/src/kvp.rs @@ -20,7 +20,7 @@ pub trait Dismissable { KEY_VALUE_STORE .read_kvp(Self::KEY) .log_err() - .map_or(false, |s| s.is_some()) + .is_some_and(|s| s.is_some()) } fn set_dismissed(is_dismissed: bool, cx: &mut App) { diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index e60c08cd0f..131272da6b 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -392,7 +392,7 @@ impl LogStore { session.label(), session .adapter_client() - .map_or(false, |client| client.has_adapter_logs()), + .is_some_and(|client| client.has_adapter_logs()), ) }); diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 3c1d35cdd3..449deb4ddb 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -414,7 +414,7 @@ pub(crate) fn new_debugger_pane( .and_then(|item| item.downcast::<SubView>()); let is_hovered = as_subview .as_ref() - .map_or(false, |item| item.read(cx).hovered); + .is_some_and(|item| item.read(cx).hovered); h_flex() .track_focus(&focus_handle) @@ -427,7 +427,6 @@ pub(crate) fn new_debugger_pane( .bg(cx.theme().colors().tab_bar_background) .on_action(|_: &menu::Cancel, window, cx| { if cx.stop_active_drag(window) { - return; } else { cx.propagate(); } @@ -449,7 +448,7 @@ pub(crate) fn new_debugger_pane( .children(pane.items().enumerate().map(|(ix, item)| { let selected = active_pane_item .as_ref() - .map_or(false, |active| active.item_id() == item.item_id()); + .is_some_and(|active| active.item_id() == item.item_id()); let deemphasized = !pane.has_focus(window, cx); let item_ = item.boxed_clone(); div() diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index 095b069fa3..26a26c7bef 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -528,7 +528,7 @@ impl BreakpointList { cx.background_executor() .spawn(async move { KEY_VALUE_STORE.write_kvp(key, value?).await }) } else { - return Task::ready(Result::Ok(())); + Task::ready(Result::Ok(())) } } diff --git a/crates/diagnostics/src/diagnostic_renderer.rs b/crates/diagnostics/src/diagnostic_renderer.rs index cb1c052925..e9731f84ce 100644 --- a/crates/diagnostics/src/diagnostic_renderer.rs +++ b/crates/diagnostics/src/diagnostic_renderer.rs @@ -287,15 +287,13 @@ impl DiagnosticBlock { } } } - } else { - if let Some(diagnostic) = editor - .snapshot(window, cx) - .buffer_snapshot - .diagnostic_group(buffer_id, group_id) - .nth(ix) - { - Self::jump_to(editor, diagnostic.range, window, cx) - } + } else if let Some(diagnostic) = editor + .snapshot(window, cx) + .buffer_snapshot + .diagnostic_group(buffer_id, group_id) + .nth(ix) + { + Self::jump_to(editor, diagnostic.range, window, cx) }; } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index c15c0f2493..2e20118381 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -383,12 +383,10 @@ impl ProjectDiagnosticsEditor { } else { self.update_all_diagnostics(false, window, cx); } + } else if self.update_excerpts_task.is_some() { + self.update_excerpts_task = None; } else { - if self.update_excerpts_task.is_some() { - self.update_excerpts_task = None; - } else { - self.update_all_diagnostics(false, window, cx); - } + self.update_all_diagnostics(false, window, cx); } cx.notify(); } @@ -542,7 +540,7 @@ impl ProjectDiagnosticsEditor { return true; } this.diagnostics.insert(buffer_id, diagnostics.clone()); - return false; + false })?; if unchanged { return Ok(()); diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index 29011352fb..6ac0f49fad 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -295,7 +295,7 @@ fn dump_all_gpui_actions() -> Vec<ActionDef> { actions.sort_by_key(|a| a.name); - return actions; + actions } fn handle_postprocessing() -> Result<()> { diff --git a/crates/edit_prediction_button/src/edit_prediction_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs index 4632a03daf..21c934fefa 100644 --- a/crates/edit_prediction_button/src/edit_prediction_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -185,7 +185,7 @@ impl Render for EditPredictionButton { let this = cx.entity(); let fs = self.fs.clone(); - return div().child( + div().child( PopoverMenu::new("supermaven") .menu(move |window, cx| match &status { SupermavenButtonStatus::NeedsActivation(activate_url) => { @@ -230,7 +230,7 @@ impl Render for EditPredictionButton { }, ) .with_handle(self.popover_menu_handle.clone()), - ); + ) } EditPredictionProvider::Zed => { @@ -343,7 +343,7 @@ impl Render for EditPredictionButton { let is_refreshing = self .edit_prediction_provider .as_ref() - .map_or(false, |provider| provider.is_refreshing(cx)); + .is_some_and(|provider| provider.is_refreshing(cx)); if is_refreshing { popover_menu = popover_menu.trigger( diff --git a/crates/editor/src/clangd_ext.rs b/crates/editor/src/clangd_ext.rs index 07be9ea9e9..c78d4c83c0 100644 --- a/crates/editor/src/clangd_ext.rs +++ b/crates/editor/src/clangd_ext.rs @@ -13,7 +13,7 @@ use crate::{Editor, SwitchSourceHeader, element::register_action}; use project::lsp_store::clangd_ext::CLANGD_SERVER_NAME; fn is_c_language(language: &Language) -> bool { - return language.name() == "C++".into() || language.name() == "C".into(); + language.name() == "C++".into() || language.name() == "C".into() } pub fn switch_source_header( diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 24d2cfddcb..4847bc2565 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -1111,10 +1111,8 @@ impl CompletionsMenu { let query_start_doesnt_match_split_words = query_start_lower .map(|query_char| { !split_words(&string_match.string).any(|word| { - word.chars() - .next() - .and_then(|c| c.to_lowercase().next()) - .map_or(false, |word_char| word_char == query_char) + word.chars().next().and_then(|c| c.to_lowercase().next()) + == Some(query_char) }) }) .unwrap_or(false); diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index cc1cc2c440..c16e4a6ddb 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -991,7 +991,7 @@ impl DisplaySnapshot { if let Some(severity) = chunk.diagnostic_severity.filter(|severity| { self.diagnostics_max_severity .into_lsp() - .map_or(false, |max_severity| severity <= &max_severity) + .is_some_and(|max_severity| severity <= &max_severity) }) { if chunk.is_unnecessary { diagnostic_highlight.fade_out = Some(editor_style.unnecessary_code_fade); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 5ae37d20fa..5d5c9500eb 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -528,10 +528,7 @@ impl BlockMap { if let Some(transform) = cursor.item() && transform.summary.input_rows > 0 && cursor.end() == old_start - && transform - .block - .as_ref() - .map_or(true, |b| !b.is_replacement()) + && transform.block.as_ref().is_none_or(|b| !b.is_replacement()) { // Preserve the transform (push and next) new_transforms.push(transform.clone(), &()); @@ -539,7 +536,7 @@ impl BlockMap { // Preserve below blocks at end of edit while let Some(transform) = cursor.item() { - if transform.block.as_ref().map_or(false, |b| b.place_below()) { + if transform.block.as_ref().is_some_and(|b| b.place_below()) { new_transforms.push(transform.clone(), &()); cursor.next(); } else { @@ -606,7 +603,7 @@ impl BlockMap { // Discard below blocks at the end of the edit. They'll be reconstructed. while let Some(transform) = cursor.item() { - if transform.block.as_ref().map_or(false, |b| b.place_below()) { + if transform.block.as_ref().is_some_and(|b| b.place_below()) { cursor.next(); } else { break; @@ -1328,7 +1325,7 @@ impl BlockSnapshot { let Dimensions(output_start, input_start, _) = cursor.start(); let overshoot = if cursor .item() - .map_or(false, |transform| transform.block.is_none()) + .is_some_and(|transform| transform.block.is_none()) { start_row.0 - output_start.0 } else { @@ -1358,7 +1355,7 @@ impl BlockSnapshot { && transform .block .as_ref() - .map_or(false, |block| block.height() > 0)) + .is_some_and(|block| block.height() > 0)) { break; } @@ -1511,7 +1508,7 @@ impl BlockSnapshot { pub(super) fn is_block_line(&self, row: BlockRow) -> bool { let mut cursor = self.transforms.cursor::<Dimensions<BlockRow, WrapRow>>(&()); cursor.seek(&row, Bias::Right); - cursor.item().map_or(false, |t| t.block.is_some()) + cursor.item().is_some_and(|t| t.block.is_some()) } pub(super) fn is_folded_buffer_header(&self, row: BlockRow) -> bool { @@ -1529,11 +1526,11 @@ impl BlockSnapshot { .make_wrap_point(Point::new(row.0, 0), Bias::Left); let mut cursor = self.transforms.cursor::<Dimensions<WrapRow, BlockRow>>(&()); cursor.seek(&WrapRow(wrap_point.row()), Bias::Right); - cursor.item().map_or(false, |transform| { + cursor.item().is_some_and(|transform| { transform .block .as_ref() - .map_or(false, |block| block.is_replacement()) + .is_some_and(|block| block.is_replacement()) }) } @@ -1653,7 +1650,7 @@ impl BlockChunks<'_> { if transform .block .as_ref() - .map_or(false, |block| block.height() == 0) + .is_some_and(|block| block.height() == 0) { self.transforms.next(); } else { @@ -1664,7 +1661,7 @@ impl BlockChunks<'_> { if self .transforms .item() - .map_or(false, |transform| transform.block.is_none()) + .is_some_and(|transform| transform.block.is_none()) { let start_input_row = self.transforms.start().1.0; let start_output_row = self.transforms.start().0.0; @@ -1774,7 +1771,7 @@ impl Iterator for BlockRows<'_> { if transform .block .as_ref() - .map_or(false, |block| block.height() == 0) + .is_some_and(|block| block.height() == 0) { self.transforms.next(); } else { @@ -1786,7 +1783,7 @@ impl Iterator for BlockRows<'_> { if transform .block .as_ref() - .map_or(true, |block| block.is_replacement()) + .is_none_or(|block| block.is_replacement()) { self.input_rows.seek(self.transforms.start().1.0); } diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 3509bcbba8..3dcd172c3c 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -491,14 +491,14 @@ impl FoldMap { while folds .peek() - .map_or(false, |(_, fold_range)| fold_range.start < edit.new.end) + .is_some_and(|(_, fold_range)| fold_range.start < edit.new.end) { let (fold, mut fold_range) = folds.next().unwrap(); let sum = new_transforms.summary(); assert!(fold_range.start.0 >= sum.input.len); - while folds.peek().map_or(false, |(next_fold, next_fold_range)| { + while folds.peek().is_some_and(|(next_fold, next_fold_range)| { next_fold_range.start < fold_range.end || (next_fold_range.start == fold_range.end && fold.placeholder.merge_adjacent @@ -575,14 +575,14 @@ impl FoldMap { for mut edit in inlay_edits { old_transforms.seek(&edit.old.start, Bias::Left); - if old_transforms.item().map_or(false, |t| t.is_fold()) { + if old_transforms.item().is_some_and(|t| t.is_fold()) { edit.old.start = old_transforms.start().0; } let old_start = old_transforms.start().1.0 + (edit.old.start - old_transforms.start().0).0; old_transforms.seek_forward(&edit.old.end, Bias::Right); - if old_transforms.item().map_or(false, |t| t.is_fold()) { + if old_transforms.item().is_some_and(|t| t.is_fold()) { old_transforms.next(); edit.old.end = old_transforms.start().0; } @@ -590,14 +590,14 @@ impl FoldMap { old_transforms.start().1.0 + (edit.old.end - old_transforms.start().0).0; new_transforms.seek(&edit.new.start, Bias::Left); - if new_transforms.item().map_or(false, |t| t.is_fold()) { + if new_transforms.item().is_some_and(|t| t.is_fold()) { edit.new.start = new_transforms.start().0; } let new_start = new_transforms.start().1.0 + (edit.new.start - new_transforms.start().0).0; new_transforms.seek_forward(&edit.new.end, Bias::Right); - if new_transforms.item().map_or(false, |t| t.is_fold()) { + if new_transforms.item().is_some_and(|t| t.is_fold()) { new_transforms.next(); edit.new.end = new_transforms.start().0; } @@ -709,7 +709,7 @@ impl FoldSnapshot { .transforms .cursor::<Dimensions<InlayPoint, FoldPoint>>(&()); cursor.seek(&point, Bias::Right); - if cursor.item().map_or(false, |t| t.is_fold()) { + if cursor.item().is_some_and(|t| t.is_fold()) { if bias == Bias::Left || point == cursor.start().0 { cursor.start().1 } else { @@ -788,7 +788,7 @@ impl FoldSnapshot { let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset); let mut cursor = self.transforms.cursor::<InlayOffset>(&()); cursor.seek(&inlay_offset, Bias::Right); - cursor.item().map_or(false, |t| t.placeholder.is_some()) + cursor.item().is_some_and(|t| t.placeholder.is_some()) } pub fn is_line_folded(&self, buffer_row: MultiBufferRow) -> bool { @@ -839,7 +839,7 @@ impl FoldSnapshot { let inlay_end = if transform_cursor .item() - .map_or(true, |transform| transform.is_fold()) + .is_none_or(|transform| transform.is_fold()) { inlay_start } else if range.end < transform_end.0 { @@ -1348,7 +1348,7 @@ impl FoldChunks<'_> { let inlay_end = if self .transform_cursor .item() - .map_or(true, |transform| transform.is_fold()) + .is_none_or(|transform| transform.is_fold()) { inlay_start } else if range.end < transform_end.0 { @@ -1463,7 +1463,7 @@ impl FoldOffset { .transforms .cursor::<Dimensions<FoldOffset, TransformSummary>>(&()); cursor.seek(&self, Bias::Right); - let overshoot = if cursor.item().map_or(true, |t| t.is_fold()) { + let overshoot = if cursor.item().is_none_or(|t| t.is_fold()) { Point::new(0, (self.0 - cursor.start().0.0) as u32) } else { let inlay_offset = cursor.start().1.input.len + self.0 - cursor.start().0.0; diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 626dbf5cba..3db9d10fdc 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -625,7 +625,7 @@ impl InlayMap { // we can push its remainder. if buffer_edits_iter .peek() - .map_or(true, |edit| edit.old.start >= cursor.end().0) + .is_none_or(|edit| edit.old.start >= cursor.end().0) { let transform_start = new_transforms.summary().input.len; let transform_end = diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 7aa252a7f3..500ec3a0bb 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -74,10 +74,10 @@ impl WrapRows<'_> { self.transforms .seek(&WrapPoint::new(start_row, 0), Bias::Left); let mut input_row = self.transforms.start().1.row(); - if self.transforms.item().map_or(false, |t| t.is_isomorphic()) { + if self.transforms.item().is_some_and(|t| t.is_isomorphic()) { input_row += start_row - self.transforms.start().0.row(); } - self.soft_wrapped = self.transforms.item().map_or(false, |t| !t.is_isomorphic()); + self.soft_wrapped = self.transforms.item().is_some_and(|t| !t.is_isomorphic()); self.input_buffer_rows.seek(input_row); self.input_buffer_row = self.input_buffer_rows.next().unwrap(); self.output_row = start_row; @@ -603,7 +603,7 @@ impl WrapSnapshot { .cursor::<Dimensions<WrapPoint, TabPoint>>(&()); transforms.seek(&output_start, Bias::Right); let mut input_start = TabPoint(transforms.start().1.0); - if transforms.item().map_or(false, |t| t.is_isomorphic()) { + if transforms.item().is_some_and(|t| t.is_isomorphic()) { input_start.0 += output_start.0 - transforms.start().0.0; } let input_end = self @@ -634,7 +634,7 @@ impl WrapSnapshot { cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left); if cursor .item() - .map_or(false, |transform| transform.is_isomorphic()) + .is_some_and(|transform| transform.is_isomorphic()) { let overshoot = row - cursor.start().0.row(); let tab_row = cursor.start().1.row() + overshoot; @@ -732,10 +732,10 @@ impl WrapSnapshot { .cursor::<Dimensions<WrapPoint, TabPoint>>(&()); transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left); let mut input_row = transforms.start().1.row(); - if transforms.item().map_or(false, |t| t.is_isomorphic()) { + if transforms.item().is_some_and(|t| t.is_isomorphic()) { input_row += start_row - transforms.start().0.row(); } - let soft_wrapped = transforms.item().map_or(false, |t| !t.is_isomorphic()); + let soft_wrapped = transforms.item().is_some_and(|t| !t.is_isomorphic()); let mut input_buffer_rows = self.tab_snapshot.rows(input_row); let input_buffer_row = input_buffer_rows.next().unwrap(); WrapRows { @@ -754,7 +754,7 @@ impl WrapSnapshot { .cursor::<Dimensions<WrapPoint, TabPoint>>(&()); cursor.seek(&point, Bias::Right); let mut tab_point = cursor.start().1.0; - if cursor.item().map_or(false, |t| t.is_isomorphic()) { + if cursor.item().is_some_and(|t| t.is_isomorphic()) { tab_point += point.0 - cursor.start().0.0; } TabPoint(tab_point) @@ -780,7 +780,7 @@ impl WrapSnapshot { if bias == Bias::Left { let mut cursor = self.transforms.cursor::<WrapPoint>(&()); cursor.seek(&point, Bias::Right); - if cursor.item().map_or(false, |t| !t.is_isomorphic()) { + if cursor.item().is_some_and(|t| !t.is_isomorphic()) { point = *cursor.start(); *point.column_mut() -= 1; } @@ -901,7 +901,7 @@ impl WrapChunks<'_> { let output_end = WrapPoint::new(rows.end, 0); self.transforms.seek(&output_start, Bias::Right); let mut input_start = TabPoint(self.transforms.start().1.0); - if self.transforms.item().map_or(false, |t| t.is_isomorphic()) { + if self.transforms.item().is_some_and(|t| t.is_isomorphic()) { input_start.0 += output_start.0 - self.transforms.start().0.0; } let input_end = self @@ -993,7 +993,7 @@ impl Iterator for WrapRows<'_> { self.output_row += 1; self.transforms .seek_forward(&WrapPoint::new(self.output_row, 0), Bias::Left); - if self.transforms.item().map_or(false, |t| t.is_isomorphic()) { + if self.transforms.item().is_some_and(|t| t.is_isomorphic()) { self.input_buffer_row = self.input_buffer_rows.next().unwrap(); self.soft_wrapped = false; } else { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ca1f1f8828..7c36a41046 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1429,7 +1429,7 @@ impl SelectionHistory { if self .undo_stack .back() - .map_or(true, |e| e.selections != entry.selections) + .is_none_or(|e| e.selections != entry.selections) { self.undo_stack.push_back(entry); if self.undo_stack.len() > MAX_SELECTION_HISTORY_LEN { @@ -1442,7 +1442,7 @@ impl SelectionHistory { if self .redo_stack .back() - .map_or(true, |e| e.selections != entry.selections) + .is_none_or(|e| e.selections != entry.selections) { self.redo_stack.push_back(entry); if self.redo_stack.len() > MAX_SELECTION_HISTORY_LEN { @@ -2512,9 +2512,7 @@ impl Editor { .context_menu .borrow() .as_ref() - .map_or(false, |context| { - matches!(context, CodeContextMenu::Completions(_)) - }); + .is_some_and(|context| matches!(context, CodeContextMenu::Completions(_))); showing_completions || self.edit_prediction_requires_modifier() @@ -2545,7 +2543,7 @@ impl Editor { || binding .keystrokes() .first() - .map_or(false, |keystroke| keystroke.modifiers.modified()) + .is_some_and(|keystroke| keystroke.modifiers.modified()) })) } @@ -2941,7 +2939,7 @@ impl Editor { return false; }; - scope.override_name().map_or(false, |scope_name| { + scope.override_name().is_some_and(|scope_name| { settings .edit_predictions_disabled_in .iter() @@ -4033,18 +4031,18 @@ impl Editor { let following_text_allows_autoclose = snapshot .chars_at(selection.start) .next() - .map_or(true, |c| scope.should_autoclose_before(c)); + .is_none_or(|c| scope.should_autoclose_before(c)); let preceding_text_allows_autoclose = selection.start.column == 0 - || snapshot.reversed_chars_at(selection.start).next().map_or( - true, - |c| { + || snapshot + .reversed_chars_at(selection.start) + .next() + .is_none_or(|c| { bracket_pair.start != bracket_pair.end || !snapshot .char_classifier_at(selection.start) .is_word(c) - }, - ); + }); let is_closing_quote = if bracket_pair.end == bracket_pair.start && bracket_pair.start.len() == 1 @@ -4185,7 +4183,7 @@ impl Editor { if !self.linked_edit_ranges.is_empty() { let start_anchor = snapshot.anchor_before(selection.start); - let is_word_char = text.chars().next().map_or(true, |char| { + let is_word_char = text.chars().next().is_none_or(|char| { let classifier = snapshot .char_classifier_at(start_anchor.to_offset(&snapshot)) .ignore_punctuation(true); @@ -5427,11 +5425,11 @@ impl Editor { let sort_completions = provider .as_ref() - .map_or(false, |provider| provider.sort_completions()); + .is_some_and(|provider| provider.sort_completions()); let filter_completions = provider .as_ref() - .map_or(true, |provider| provider.filter_completions()); + .is_none_or(|provider| provider.filter_completions()); let trigger_kind = match trigger { Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => { @@ -5537,7 +5535,7 @@ impl Editor { let skip_digits = query .as_ref() - .map_or(true, |query| !query.chars().any(|c| c.is_digit(10))); + .is_none_or(|query| !query.chars().any(|c| c.is_digit(10))); let (mut words, provider_responses) = match &provider { Some(provider) => { @@ -5971,7 +5969,7 @@ impl Editor { let show_new_completions_on_confirm = completion .confirm .as_ref() - .map_or(false, |confirm| confirm(intent, window, cx)); + .is_some_and(|confirm| confirm(intent, window, cx)); if show_new_completions_on_confirm { self.show_completions(&ShowCompletions { trigger: None }, window, cx); } @@ -6103,10 +6101,10 @@ impl Editor { let spawn_straight_away = quick_launch && resolved_tasks .as_ref() - .map_or(false, |tasks| tasks.templates.len() == 1) + .is_some_and(|tasks| tasks.templates.len() == 1) && code_actions .as_ref() - .map_or(true, |actions| actions.is_empty()) + .is_none_or(|actions| actions.is_empty()) && debug_scenarios.is_empty(); editor.update_in(cx, |editor, window, cx| { @@ -6720,9 +6718,9 @@ impl Editor { let buffer_id = cursor_position.buffer_id; let buffer = this.buffer.read(cx); - if !buffer + if buffer .text_anchor_for_position(cursor_position, cx) - .map_or(false, |(buffer, _)| buffer == cursor_buffer) + .is_none_or(|(buffer, _)| buffer != cursor_buffer) { return; } @@ -6972,9 +6970,7 @@ impl Editor { || self .quick_selection_highlight_task .as_ref() - .map_or(true, |(prev_anchor_range, _)| { - prev_anchor_range != &query_range - }) + .is_none_or(|(prev_anchor_range, _)| prev_anchor_range != &query_range) { let multi_buffer_visible_start = self .scroll_manager @@ -7003,9 +6999,7 @@ impl Editor { || self .debounced_selection_highlight_task .as_ref() - .map_or(true, |(prev_anchor_range, _)| { - prev_anchor_range != &query_range - }) + .is_none_or(|(prev_anchor_range, _)| prev_anchor_range != &query_range) { let multi_buffer_start = multi_buffer_snapshot .anchor_before(0) @@ -7140,9 +7134,7 @@ impl Editor { && self .edit_prediction_provider .as_ref() - .map_or(false, |provider| { - provider.provider.show_completions_in_menu() - }); + .is_some_and(|provider| provider.provider.show_completions_in_menu()); let preview_requires_modifier = all_language_settings(file, cx).edit_predictions_mode() == EditPredictionsMode::Subtle; @@ -7726,7 +7718,7 @@ impl Editor { || self .active_edit_prediction .as_ref() - .map_or(false, |completion| { + .is_some_and(|completion| { let invalidation_range = completion.invalidation_range.to_offset(&multibuffer); let invalidation_range = invalidation_range.start..=invalidation_range.end; !invalidation_range.contains(&offset_selection.head()) @@ -8427,7 +8419,7 @@ impl Editor { .context_menu .borrow() .as_ref() - .map_or(false, |menu| menu.visible()) + .is_some_and(|menu| menu.visible()) } pub fn context_menu_origin(&self) -> Option<ContextMenuOrigin> { @@ -8973,9 +8965,8 @@ impl Editor { let end_row = start_row + line_count as u32; visible_row_range.contains(&start_row) && visible_row_range.contains(&end_row) - && cursor_row.map_or(true, |cursor_row| { - !((start_row..end_row).contains(&cursor_row)) - }) + && cursor_row + .is_none_or(|cursor_row| !((start_row..end_row).contains(&cursor_row))) })?; content_origin @@ -9585,7 +9576,7 @@ impl Editor { .tabstops .iter() .map(|tabstop| { - let is_end_tabstop = tabstop.ranges.first().map_or(false, |tabstop| { + let is_end_tabstop = tabstop.ranges.first().is_some_and(|tabstop| { tabstop.is_empty() && tabstop.start == snippet.text.len() as isize }); let mut tabstop_ranges = tabstop @@ -11716,7 +11707,7 @@ impl Editor { let transpose_start = display_map .buffer_snapshot .clip_offset(transpose_offset.saturating_sub(1), Bias::Left); - if edits.last().map_or(true, |e| e.0.end <= transpose_start) { + if edits.last().is_none_or(|e| e.0.end <= transpose_start) { let transpose_end = display_map .buffer_snapshot .clip_offset(transpose_offset + 1, Bias::Right); @@ -16229,23 +16220,21 @@ impl Editor { if split { workspace.split_item(SplitDirection::Right, item.clone(), window, cx); - } else { - if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation { - let (preview_item_id, preview_item_idx) = - workspace.active_pane().read_with(cx, |pane, _| { - (pane.preview_item_id(), pane.preview_item_idx()) - }); + } else if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation { + let (preview_item_id, preview_item_idx) = + workspace.active_pane().read_with(cx, |pane, _| { + (pane.preview_item_id(), pane.preview_item_idx()) + }); - workspace.add_item_to_active_pane(item.clone(), preview_item_idx, true, window, cx); + workspace.add_item_to_active_pane(item.clone(), preview_item_idx, true, window, cx); - if let Some(preview_item_id) = preview_item_id { - workspace.active_pane().update(cx, |pane, cx| { - pane.remove_item(preview_item_id, false, false, window, cx); - }); - } - } else { - workspace.add_item_to_active_pane(item.clone(), None, true, window, cx); + if let Some(preview_item_id) = preview_item_id { + workspace.active_pane().update(cx, |pane, cx| { + pane.remove_item(preview_item_id, false, false, window, cx); + }); } + } else { + workspace.add_item_to_active_pane(item.clone(), None, true, window, cx); } workspace.active_pane().update(cx, |pane, cx| { pane.set_preview_item_id(Some(item_id), cx); @@ -19010,7 +18999,7 @@ impl Editor { fn has_blame_entries(&self, cx: &App) -> bool { self.blame() - .map_or(false, |blame| blame.read(cx).has_generated_entries()) + .is_some_and(|blame| blame.read(cx).has_generated_entries()) } fn newest_selection_head_on_empty_line(&self, cx: &App) -> bool { @@ -19660,7 +19649,7 @@ impl Editor { pub fn has_background_highlights<T: 'static>(&self) -> bool { self.background_highlights .get(&HighlightKey::Type(TypeId::of::<T>())) - .map_or(false, |(_, highlights)| !highlights.is_empty()) + .is_some_and(|(_, highlights)| !highlights.is_empty()) } pub fn background_highlights_in_range( @@ -20582,7 +20571,7 @@ impl Editor { // For now, don't allow opening excerpts in buffers that aren't backed by // regular project files. fn can_open_excerpts_in_file(file: Option<&Arc<dyn language::File>>) -> bool { - file.map_or(true, |file| project::File::from_dyn(Some(file)).is_some()) + file.is_none_or(|file| project::File::from_dyn(Some(file)).is_some()) } fn marked_text_ranges(&self, cx: &App) -> Option<Vec<Range<OffsetUtf16>>> { @@ -21125,7 +21114,7 @@ impl Editor { pub fn has_visible_completions_menu(&self) -> bool { !self.edit_prediction_preview_is_active() - && self.context_menu.borrow().as_ref().map_or(false, |menu| { + && self.context_menu.borrow().as_ref().is_some_and(|menu| { menu.visible() && matches!(menu, CodeContextMenu::Completions(_)) }) } @@ -21548,9 +21537,9 @@ fn is_grapheme_whitespace(text: &str) -> bool { } fn should_stay_with_preceding_ideograph(text: &str) -> bool { - text.chars().next().map_or(false, |ch| { - matches!(ch, '。' | '、' | ',' | '?' | '!' | ':' | ';' | '…') - }) + text.chars() + .next() + .is_some_and(|ch| matches!(ch, '。' | '、' | ',' | '?' | '!' | ':' | ';' | '…')) } #[derive(PartialEq, Eq, Debug, Clone, Copy)] @@ -21589,11 +21578,11 @@ impl<'a> Iterator for WordBreakingTokenizer<'a> { } else { let mut words = self.input[offset..].split_word_bound_indices().peekable(); let mut next_word_bound = words.peek().copied(); - if next_word_bound.map_or(false, |(i, _)| i == 0) { + if next_word_bound.is_some_and(|(i, _)| i == 0) { next_word_bound = words.next(); } while let Some(grapheme) = iter.peek().copied() { - if next_word_bound.map_or(false, |(i, _)| i == offset) { + if next_word_bound.is_some_and(|(i, _)| i == offset) { break; }; if is_grapheme_whitespace(grapheme) != is_whitespace diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index d3a21c7642..1d7e04cae0 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -810,10 +810,8 @@ impl Settings for EditorSettings { if gutter.line_numbers.is_some() { old_gutter.line_numbers = gutter.line_numbers } - } else { - if gutter != GutterContent::default() { - current.gutter = Some(gutter) - } + } else if gutter != GutterContent::default() { + current.gutter = Some(gutter) } if let Some(b) = vscode.read_bool("editor.scrollBeyondLastLine") { current.scroll_beyond_last_line = Some(if b { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index d8fe3ccf15..c14e49fc1d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -919,6 +919,7 @@ impl EditorElement { { #[allow( clippy::collapsible_if, + clippy::needless_return, reason = "The cfg-block below makes this a false positive" )] if !text_hitbox.is_hovered(window) || editor.read_only(cx) { @@ -1126,26 +1127,24 @@ impl EditorElement { let hovered_diff_hunk_row = if let Some(control_row) = hovered_diff_control { Some(control_row) - } else { - if text_hovered { - let current_row = valid_point.row(); - position_map.display_hunks.iter().find_map(|(hunk, _)| { - if let DisplayDiffHunk::Unfolded { - display_row_range, .. - } = hunk - { - if display_row_range.contains(¤t_row) { - Some(display_row_range.start) - } else { - None - } + } else if text_hovered { + let current_row = valid_point.row(); + position_map.display_hunks.iter().find_map(|(hunk, _)| { + if let DisplayDiffHunk::Unfolded { + display_row_range, .. + } = hunk + { + if display_row_range.contains(¤t_row) { + Some(display_row_range.start) } else { None } - }) - } else { - None - } + } else { + None + } + }) + } else { + None }; if hovered_diff_hunk_row != editor.hovered_diff_hunk_row { @@ -1159,11 +1158,11 @@ impl EditorElement { .inline_blame_popover .as_ref() .and_then(|state| state.popover_bounds) - .map_or(false, |bounds| bounds.contains(&event.position)); + .is_some_and(|bounds| bounds.contains(&event.position)); let keyboard_grace = editor .inline_blame_popover .as_ref() - .map_or(false, |state| state.keyboard_grace); + .is_some_and(|state| state.keyboard_grace); if mouse_over_inline_blame || mouse_over_popover { editor.show_blame_popover(blame_entry, event.position, false, cx); @@ -1190,10 +1189,10 @@ impl EditorElement { let is_visible = editor .gutter_breakpoint_indicator .0 - .map_or(false, |indicator| indicator.is_active); + .is_some_and(|indicator| indicator.is_active); let has_existing_breakpoint = - editor.breakpoint_store.as_ref().map_or(false, |store| { + editor.breakpoint_store.as_ref().is_some_and(|store| { let Some(project) = &editor.project else { return false; }; @@ -2220,12 +2219,11 @@ impl EditorElement { cmp::max(padded_line, min_start) }; - let behind_edit_prediction_popover = edit_prediction_popover_origin.as_ref().map_or( - false, - |edit_prediction_popover_origin| { + let behind_edit_prediction_popover = edit_prediction_popover_origin + .as_ref() + .is_some_and(|edit_prediction_popover_origin| { (pos_y..pos_y + line_height).contains(&edit_prediction_popover_origin.y) - }, - ); + }); let opacity = if behind_edit_prediction_popover { 0.5 } else { @@ -2291,9 +2289,7 @@ impl EditorElement { None } }) - .map_or(false, |source| { - matches!(source, CodeActionSource::Indicator(..)) - }); + .is_some_and(|source| matches!(source, CodeActionSource::Indicator(..))); Some(editor.render_inline_code_actions(icon_size, display_point.row(), active, cx)) })?; @@ -2909,7 +2905,7 @@ impl EditorElement { if multibuffer_row .0 .checked_sub(1) - .map_or(false, |previous_row| { + .is_some_and(|previous_row| { snapshot.is_line_folded(MultiBufferRow(previous_row)) }) { @@ -3900,7 +3896,7 @@ impl EditorElement { for (row, block) in fixed_blocks { let block_id = block.id(); - if focused_block.as_ref().map_or(false, |b| b.id == block_id) { + if focused_block.as_ref().is_some_and(|b| b.id == block_id) { focused_block = None; } @@ -3957,7 +3953,7 @@ impl EditorElement { }; let block_id = block.id(); - if focused_block.as_ref().map_or(false, |b| b.id == block_id) { + if focused_block.as_ref().is_some_and(|b| b.id == block_id) { focused_block = None; } @@ -4736,7 +4732,7 @@ impl EditorElement { } }; - let source_included = source_display_point.map_or(true, |source_display_point| { + let source_included = source_display_point.is_none_or(|source_display_point| { visible_range .to_inclusive() .contains(&source_display_point.row()) @@ -4916,7 +4912,7 @@ impl EditorElement { let intersects_menu = |bounds: Bounds<Pixels>| -> bool { context_menu_layout .as_ref() - .map_or(false, |menu| bounds.intersects(&menu.bounds)) + .is_some_and(|menu| bounds.intersects(&menu.bounds)) }; let can_place_above = { @@ -5101,7 +5097,7 @@ impl EditorElement { if active_positions .iter() - .any(|p| p.map_or(false, |p| display_row_range.contains(&p.row()))) + .any(|p| p.is_some_and(|p| display_row_range.contains(&p.row()))) { let y = display_row_range.start.as_f32() * line_height + text_hitbox.bounds.top() @@ -5214,7 +5210,7 @@ impl EditorElement { let intersects_menu = |bounds: Bounds<Pixels>| -> bool { context_menu_layout .as_ref() - .map_or(false, |menu| bounds.intersects(&menu.bounds)) + .is_some_and(|menu| bounds.intersects(&menu.bounds)) }; let final_origin = if popover_bounds_above.is_contained_within(hitbox) @@ -5299,7 +5295,7 @@ impl EditorElement { let mut end_row = start_row.0; while active_rows .peek() - .map_or(false, |(active_row, has_selection)| { + .is_some_and(|(active_row, has_selection)| { active_row.0 == end_row + 1 && has_selection.selection == contains_non_empty_selection.selection }) @@ -6687,25 +6683,23 @@ impl EditorElement { editor.set_scroll_position(position, window, cx); } cx.stop_propagation(); - } else { - if minimap_hitbox.is_hovered(window) { - editor.scroll_manager.set_is_hovering_minimap_thumb( - !event.dragging() - && layout - .thumb_layout - .thumb_bounds - .is_some_and(|bounds| bounds.contains(&event.position)), - cx, - ); + } else if minimap_hitbox.is_hovered(window) { + editor.scroll_manager.set_is_hovering_minimap_thumb( + !event.dragging() + && layout + .thumb_layout + .thumb_bounds + .is_some_and(|bounds| bounds.contains(&event.position)), + cx, + ); - // Stop hover events from propagating to the - // underlying editor if the minimap hitbox is hovered - if !event.dragging() { - cx.stop_propagation(); - } - } else { - editor.scroll_manager.hide_minimap_thumb(cx); + // Stop hover events from propagating to the + // underlying editor if the minimap hitbox is hovered + if !event.dragging() { + cx.stop_propagation(); } + } else { + editor.scroll_manager.hide_minimap_thumb(cx); } mouse_position = event.position; }); @@ -7084,9 +7078,7 @@ impl EditorElement { let unstaged_hollow = ProjectSettings::get_global(cx) .git .hunk_style - .map_or(false, |style| { - matches!(style, GitHunkStyleSetting::UnstagedHollow) - }); + .is_some_and(|style| matches!(style, GitHunkStyleSetting::UnstagedHollow)); unstaged == unstaged_hollow } @@ -8183,7 +8175,7 @@ impl Element for EditorElement { let is_row_soft_wrapped = |row: usize| { row_infos .get(row) - .map_or(true, |info| info.buffer_row.is_none()) + .is_none_or(|info| info.buffer_row.is_none()) }; let start_anchor = if start_row == Default::default() { @@ -9718,14 +9710,12 @@ impl PointForPosition { false } else if start_row == end_row { candidate_col >= start_col && candidate_col < end_col + } else if candidate_row == start_row { + candidate_col >= start_col + } else if candidate_row == end_row { + candidate_col < end_col } else { - if candidate_row == start_row { - candidate_col >= start_col - } else if candidate_row == end_row { - candidate_col < end_col - } else { - true - } + true } } } diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index 712325f339..2f6106c86c 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -415,7 +415,7 @@ impl GitBlame { let old_end = cursor.end(); if row_edits .peek() - .map_or(true, |next_edit| next_edit.old.start >= old_end) + .is_none_or(|next_edit| next_edit.old.start >= old_end) && let Some(entry) = cursor.item() { if old_end > edit.old.end { diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 358d8683fe..04e66a234c 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -271,7 +271,7 @@ impl Editor { Task::ready(Ok(Navigated::No)) }; self.select(SelectPhase::End, window, cx); - return navigate_task; + navigate_task } } @@ -871,7 +871,7 @@ fn surrounding_filename( .peekable(); while let Some(ch) = forwards.next() { // Skip escaped whitespace - if ch == '\\' && forwards.peek().map_or(false, |ch| ch.is_whitespace()) { + if ch == '\\' && forwards.peek().is_some_and(|ch| ch.is_whitespace()) { token_end += ch.len_utf8(); let whitespace = forwards.next().unwrap(); token_end += whitespace.len_utf8(); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 136b0b314d..e3d2f92c55 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -201,7 +201,7 @@ impl FollowableItem for Editor { if buffer .as_singleton() .and_then(|buffer| buffer.read(cx).file()) - .map_or(false, |file| file.is_private()) + .is_some_and(|file| file.is_private()) { return None; } @@ -715,7 +715,7 @@ impl Item for Editor { .read(cx) .as_singleton() .and_then(|buffer| buffer.read(cx).file()) - .map_or(false, |file| file.disk_state() == DiskState::Deleted); + .is_some_and(|file| file.disk_state() == DiskState::Deleted); h_flex() .gap_2() diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index cae4b565b4..13e5d0a8c7 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -86,9 +86,9 @@ pub(crate) fn should_auto_close( }); } if to_auto_edit.is_empty() { - return None; + None } else { - return Some(to_auto_edit); + Some(to_auto_edit) } } @@ -186,7 +186,7 @@ pub(crate) fn generate_auto_close_edits( let range = node_name.byte_range(); return buffer.text_for_range(range).equals_str(name); } - return is_empty; + is_empty }; let tree_root_node = { @@ -227,7 +227,7 @@ pub(crate) fn generate_auto_close_edits( let has_open_tag_with_same_tag_name = ancestor .named_child(0) .filter(|n| n.kind() == config.open_tag_node_name) - .map_or(false, |element_open_tag_node| { + .is_some_and(|element_open_tag_node| { tag_node_name_equals(&element_open_tag_node, &tag_name) }); if has_open_tag_with_same_tag_name { @@ -263,8 +263,7 @@ pub(crate) fn generate_auto_close_edits( } let is_after_open_tag = |node: &Node| { - return node.start_byte() < open_tag.start_byte() - && node.end_byte() < open_tag.start_byte(); + node.start_byte() < open_tag.start_byte() && node.end_byte() < open_tag.start_byte() }; // perf: use cursor for more efficient traversal @@ -301,7 +300,7 @@ pub(crate) fn generate_auto_close_edits( let edit_range = edit_anchor..edit_anchor; edits.push((edit_range, format!("</{}>", tag_name))); } - return Ok(edits); + Ok(edits) } pub(crate) fn refresh_enabled_in_any_buffer( @@ -367,7 +366,7 @@ pub(crate) fn construct_initial_buffer_versions_map< initial_buffer_versions.insert(buffer_id, buffer_version); } } - return initial_buffer_versions; + initial_buffer_versions } pub(crate) fn handle_from( @@ -455,12 +454,9 @@ pub(crate) fn handle_from( let ensure_no_edits_since_start = || -> Option<()> { let has_edits_since_start = this .read_with(cx, |this, cx| { - this.buffer - .read(cx) - .buffer(buffer_id) - .map_or(true, |buffer| { - buffer.read(cx).has_edits_since(&buffer_version_initial) - }) + this.buffer.read(cx).buffer(buffer_id).is_none_or(|buffer| { + buffer.read(cx).has_edits_since(&buffer_version_initial) + }) }) .ok()?; diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 7f9eb374e8..5cf22de537 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -61,13 +61,13 @@ impl MouseContextMenu { source, offset: position - (source_position + content_origin), }; - return Some(MouseContextMenu::new( + Some(MouseContextMenu::new( editor, menu_position, context_menu, window, cx, - )); + )) } pub(crate) fn new( diff --git a/crates/editor/src/tasks.rs b/crates/editor/src/tasks.rs index 0d497e4cac..8be2a3a2e1 100644 --- a/crates/editor/src/tasks.rs +++ b/crates/editor/src/tasks.rs @@ -89,7 +89,7 @@ impl Editor { .lsp_task_source()?; if lsp_settings .get(&lsp_tasks_source) - .map_or(true, |s| s.enable_lsp_tasks) + .is_none_or(|s| s.enable_lsp_tasks) { let buffer_id = buffer.read(cx).remote_id(); Some((lsp_tasks_source, buffer_id)) diff --git a/crates/eval/src/assertions.rs b/crates/eval/src/assertions.rs index 489e4aa22e..01fac186d3 100644 --- a/crates/eval/src/assertions.rs +++ b/crates/eval/src/assertions.rs @@ -54,7 +54,7 @@ impl AssertionsReport { pub fn passed_count(&self) -> usize { self.ran .iter() - .filter(|a| a.result.as_ref().map_or(false, |result| result.passed)) + .filter(|a| a.result.as_ref().is_ok_and(|result| result.passed)) .count() } diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index 809b530ed7..1d2bece5cc 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -112,7 +112,7 @@ fn main() { let telemetry = app_state.client.telemetry(); telemetry.start(system_id, installation_id, session_id, cx); - let enable_telemetry = env::var("ZED_EVAL_TELEMETRY").map_or(false, |value| value == "1") + let enable_telemetry = env::var("ZED_EVAL_TELEMETRY").is_ok_and(|value| value == "1") && telemetry.has_checksum_seed(); if enable_telemetry { println!("Telemetry enabled"); diff --git a/crates/eval/src/examples/add_arg_to_trait_method.rs b/crates/eval/src/examples/add_arg_to_trait_method.rs index 9c538f9260..084f12bc62 100644 --- a/crates/eval/src/examples/add_arg_to_trait_method.rs +++ b/crates/eval/src/examples/add_arg_to_trait_method.rs @@ -70,10 +70,10 @@ impl Example for AddArgToTraitMethod { let path_str = format!("crates/assistant_tools/src/{}.rs", tool_name); let edits = edits.get(Path::new(&path_str)); - let ignored = edits.map_or(false, |edits| { + let ignored = edits.is_some_and(|edits| { edits.has_added_line(" _window: Option<gpui::AnyWindowHandle>,\n") }); - let uningored = edits.map_or(false, |edits| { + let uningored = edits.is_some_and(|edits| { edits.has_added_line(" window: Option<gpui::AnyWindowHandle>,\n") }); @@ -89,7 +89,7 @@ impl Example for AddArgToTraitMethod { let batch_tool_edits = edits.get(Path::new("crates/assistant_tools/src/batch_tool.rs")); cx.assert( - batch_tool_edits.map_or(false, |edits| { + batch_tool_edits.is_some_and(|edits| { edits.has_added_line(" window: Option<gpui::AnyWindowHandle>,\n") }), "Argument: batch_tool", diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs index b80525798b..432adaf4bc 100644 --- a/crates/extension/src/extension_builder.rs +++ b/crates/extension/src/extension_builder.rs @@ -401,7 +401,7 @@ impl ExtensionBuilder { let mut clang_path = wasi_sdk_dir.clone(); clang_path.extend(["bin", &format!("clang{}", env::consts::EXE_SUFFIX)]); - if fs::metadata(&clang_path).map_or(false, |metadata| metadata.is_file()) { + if fs::metadata(&clang_path).is_ok_and(|metadata| metadata.is_file()) { return Ok(clang_path); } diff --git a/crates/extension/src/extension_events.rs b/crates/extension/src/extension_events.rs index b151b3f412..94f3277b05 100644 --- a/crates/extension/src/extension_events.rs +++ b/crates/extension/src/extension_events.rs @@ -19,9 +19,8 @@ pub struct ExtensionEvents; impl ExtensionEvents { /// Returns the global [`ExtensionEvents`]. pub fn try_global(cx: &App) -> Option<Entity<Self>> { - return cx - .try_global::<GlobalExtensionEvents>() - .map(|g| g.0.clone()); + cx.try_global::<GlobalExtensionEvents>() + .map(|g| g.0.clone()) } fn new(_cx: &mut Context<Self>) -> Self { diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 01edb5c033..1a05dbc570 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -562,12 +562,12 @@ impl ExtensionStore { extensions .into_iter() .filter(|extension| { - this.extension_index.extensions.get(&extension.id).map_or( - true, - |installed_extension| { + this.extension_index + .extensions + .get(&extension.id) + .is_none_or(|installed_extension| { installed_extension.manifest.version != extension.manifest.version - }, - ) + }) }) .collect() }) @@ -1451,7 +1451,7 @@ impl ExtensionStore { if extension_dir .file_name() - .map_or(false, |file_name| file_name == ".DS_Store") + .is_some_and(|file_name| file_name == ".DS_Store") { continue; } diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index 7c12571f24..49ccfcc85c 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -14,7 +14,7 @@ struct FeatureFlags { } pub static ZED_DISABLE_STAFF: LazyLock<bool> = LazyLock::new(|| { - std::env::var("ZED_DISABLE_STAFF").map_or(false, |value| !value.is_empty() && value != "0") + std::env::var("ZED_DISABLE_STAFF").is_ok_and(|value| !value.is_empty() && value != "0") }); impl FeatureFlags { diff --git a/crates/feedback/src/system_specs.rs b/crates/feedback/src/system_specs.rs index 7c002d90e9..b5ccaca689 100644 --- a/crates/feedback/src/system_specs.rs +++ b/crates/feedback/src/system_specs.rs @@ -135,7 +135,7 @@ impl Display for SystemSpecs { fn try_determine_available_gpus() -> Option<String> { #[cfg(any(target_os = "linux", target_os = "freebsd"))] { - return std::process::Command::new("vulkaninfo") + std::process::Command::new("vulkaninfo") .args(&["--summary"]) .output() .ok() @@ -150,11 +150,11 @@ fn try_determine_available_gpus() -> Option<String> { ] .join("\n") }) - .or(Some("Failed to run `vulkaninfo --summary`".to_string())); + .or(Some("Failed to run `vulkaninfo --summary`".to_string())) } #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] { - return None; + None } } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index aebc262af0..3a08ec08e0 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -878,9 +878,7 @@ impl FileFinderDelegate { PathMatchCandidateSet { snapshot: worktree.snapshot(), include_ignored: self.include_ignored.unwrap_or_else(|| { - worktree - .root_entry() - .map_or(false, |entry| entry.is_ignored) + worktree.root_entry().is_some_and(|entry| entry.is_ignored) }), include_root_name, candidates: project::Candidates::Files, diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index 3a99afc8cb..77acdf8097 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -728,7 +728,7 @@ impl PickerDelegate for OpenPathDelegate { .child(LabelLike::new().child(label_with_highlights)), ) } - DirectoryState::None { .. } => return None, + DirectoryState::None { .. } => None, } } diff --git a/crates/file_icons/src/file_icons.rs b/crates/file_icons/src/file_icons.rs index 82a8e05d85..42c00fb12d 100644 --- a/crates/file_icons/src/file_icons.rs +++ b/crates/file_icons/src/file_icons.rs @@ -72,7 +72,7 @@ impl FileIcons { return maybe_path; } } - return this.get_icon_for_type("default", cx); + this.get_icon_for_type("default", cx) } fn default_icon_theme(cx: &App) -> Option<Arc<IconTheme>> { diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 399c0f3e32..d17cbdcf51 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -625,13 +625,13 @@ impl Fs for RealFs { async fn is_file(&self, path: &Path) -> bool { smol::fs::metadata(path) .await - .map_or(false, |metadata| metadata.is_file()) + .is_ok_and(|metadata| metadata.is_file()) } async fn is_dir(&self, path: &Path) -> bool { smol::fs::metadata(path) .await - .map_or(false, |metadata| metadata.is_dir()) + .is_ok_and(|metadata| metadata.is_dir()) } async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> { diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index c30b789d9f..edcad514bb 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -269,10 +269,8 @@ impl GitExcludeOverride { pub async fn restore_original(&mut self) -> Result<()> { if let Some(ref original) = self.original_excludes { smol::fs::write(&self.git_exclude_path, original).await?; - } else { - if self.git_exclude_path.exists() { - smol::fs::remove_file(&self.git_exclude_path).await?; - } + } else if self.git_exclude_path.exists() { + smol::fs::remove_file(&self.git_exclude_path).await?; } self.added_excludes = None; @@ -2052,7 +2050,7 @@ fn parse_branch_input(input: &str) -> Result<Vec<Branch>> { } fn parse_upstream_track(upstream_track: &str) -> Result<UpstreamTracking> { - if upstream_track == "" { + if upstream_track.is_empty() { return Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus { ahead: 0, behind: 0, diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index 07896b0c01..d428ccbb05 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -88,11 +88,10 @@ impl CommitView { let ix = pane.items().position(|item| { let commit_view = item.downcast::<CommitView>(); commit_view - .map_or(false, |view| view.read(cx).commit.sha == commit.sha) + .is_some_and(|view| view.read(cx).commit.sha == commit.sha) }); if let Some(ix) = ix { pane.activate_item(ix, true, true, window, cx); - return; } else { pane.add_item(Box::new(commit_view), true, true, None, window, cx); } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 82870b4e75..ace3a8eb15 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -775,7 +775,7 @@ impl GitPanel { if window .focused(cx) - .map_or(false, |focused| self.focus_handle == focused) + .is_some_and(|focused| self.focus_handle == focused) { dispatch_context.add("menu"); dispatch_context.add("ChangesList"); @@ -894,9 +894,7 @@ impl GitPanel { let have_entries = self .active_repository .as_ref() - .map_or(false, |active_repository| { - active_repository.read(cx).status_summary().count > 0 - }); + .is_some_and(|active_repository| active_repository.read(cx).status_summary().count > 0); if have_entries && self.selected_entry.is_none() { self.selected_entry = Some(1); self.scroll_to_selected_entry(cx); @@ -1207,9 +1205,7 @@ impl GitPanel { }) .ok(); } - _ => { - return; - } + _ => {} }) .detach(); } @@ -1640,13 +1636,12 @@ impl GitPanel { fn has_commit_message(&self, cx: &mut Context<Self>) -> bool { let text = self.commit_editor.read(cx).text(cx); if !text.trim().is_empty() { - return true; + true } else if text.is_empty() { - return self - .suggest_commit_message(cx) - .is_some_and(|text| !text.trim().is_empty()); + self.suggest_commit_message(cx) + .is_some_and(|text| !text.trim().is_empty()) } else { - return false; + false } } @@ -2938,8 +2933,7 @@ impl GitPanel { .matches(git::repository::REMOTE_CANCELLED_BY_USER) .next() .is_some() - { - return; // Hide the cancelled by user message + { // Hide the cancelled by user message } else { workspace.update(cx, |workspace, cx| { let workspace_weak = cx.weak_entity(); @@ -3272,12 +3266,10 @@ impl GitPanel { } else { "Amend Tracked" } + } else if self.has_staged_changes() { + "Commit" } else { - if self.has_staged_changes() { - "Commit" - } else { - "Commit Tracked" - } + "Commit Tracked" } } @@ -4498,7 +4490,7 @@ impl Render for GitPanel { let has_write_access = self.has_write_access(cx); - let has_co_authors = room.map_or(false, |room| { + let has_co_authors = room.is_some_and(|room| { self.load_local_committer(cx); let room = room.read(cx); room.remote_participants() @@ -4814,12 +4806,10 @@ impl RenderOnce for PanelRepoFooter { // ideally, show the whole branch and repo names but // when we can't, use a budget to allocate space between the two - let (repo_display_len, branch_display_len) = if branch_actual_len + repo_actual_len - <= LABEL_CHARACTER_BUDGET - { - (repo_actual_len, branch_actual_len) - } else { - if branch_actual_len <= MAX_BRANCH_LEN { + let (repo_display_len, branch_display_len) = + if branch_actual_len + repo_actual_len <= LABEL_CHARACTER_BUDGET { + (repo_actual_len, branch_actual_len) + } else if branch_actual_len <= MAX_BRANCH_LEN { let repo_space = (LABEL_CHARACTER_BUDGET - branch_actual_len).min(MAX_REPO_LEN); (repo_space, branch_actual_len) } else if repo_actual_len <= MAX_REPO_LEN { @@ -4827,8 +4817,7 @@ impl RenderOnce for PanelRepoFooter { (repo_actual_len, branch_space) } else { (MAX_REPO_LEN, MAX_BRANCH_LEN) - } - }; + }; let truncated_repo_name = if repo_actual_len <= repo_display_len { active_repo_name.to_string() diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 3c0898fabf..c12ef58ce2 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -329,14 +329,14 @@ impl ProjectDiff { }) .ok(); - return ButtonStates { + ButtonStates { stage: has_unstaged_hunks, unstage: has_staged_hunks, prev_next, selection, stage_all, unstage_all, - }; + } } fn handle_editor_event( diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index 9d918048fa..23729be062 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -129,7 +129,7 @@ impl CursorPosition { cursor_position.selected_count.lines += 1; } } - if last_selection.as_ref().map_or(true, |last_selection| { + if last_selection.as_ref().is_none_or(|last_selection| { selection.id > last_selection.id }) { last_selection = Some(selection); diff --git a/crates/google_ai/src/google_ai.rs b/crates/google_ai/src/google_ai.rs index a1b5ca3a03..ca0aa309b1 100644 --- a/crates/google_ai/src/google_ai.rs +++ b/crates/google_ai/src/google_ai.rs @@ -477,10 +477,10 @@ impl<'de> Deserialize<'de> for ModelName { model_id: id.to_string(), }) } else { - return Err(serde::de::Error::custom(format!( + Err(serde::de::Error::custom(format!( "Expected model name to begin with {}, got: {}", MODEL_NAME_PREFIX, string - ))); + ))) } } } diff --git a/crates/gpui/src/app/entity_map.rs b/crates/gpui/src/app/entity_map.rs index 48b2bcaf98..6099ee5857 100644 --- a/crates/gpui/src/app/entity_map.rs +++ b/crates/gpui/src/app/entity_map.rs @@ -786,7 +786,7 @@ impl<T: 'static> PartialOrd for WeakEntity<T> { #[cfg(any(test, feature = "leak-detection"))] static LEAK_BACKTRACE: std::sync::LazyLock<bool> = - std::sync::LazyLock::new(|| std::env::var("LEAK_BACKTRACE").map_or(false, |b| !b.is_empty())); + std::sync::LazyLock::new(|| std::env::var("LEAK_BACKTRACE").is_ok_and(|b| !b.is_empty())); #[cfg(any(test, feature = "leak-detection"))] #[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 7b689ca0ad..c9826b704e 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -2274,7 +2274,7 @@ impl Interactivity { window.on_mouse_event(move |_: &MouseDownEvent, phase, window, _cx| { if phase == DispatchPhase::Bubble && !window.default_prevented() { let group_hovered = active_group_hitbox - .map_or(false, |group_hitbox_id| group_hitbox_id.is_hovered(window)); + .is_some_and(|group_hitbox_id| group_hitbox_id.is_hovered(window)); let element_hovered = hitbox.is_hovered(window); if group_hovered || element_hovered { *active_state.borrow_mut() = ElementClickedState { @@ -2614,7 +2614,7 @@ pub(crate) fn register_tooltip_mouse_handlers( window.on_mouse_event({ let active_tooltip = active_tooltip.clone(); move |_: &MouseDownEvent, _phase, window: &mut Window, _cx| { - if !tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(window)) { + if !tooltip_id.is_some_and(|tooltip_id| tooltip_id.is_hovered(window)) { clear_active_tooltip_if_not_hoverable(&active_tooltip, window); } } @@ -2623,7 +2623,7 @@ pub(crate) fn register_tooltip_mouse_handlers( window.on_mouse_event({ let active_tooltip = active_tooltip.clone(); move |_: &ScrollWheelEvent, _phase, window: &mut Window, _cx| { - if !tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(window)) { + if !tooltip_id.is_some_and(|tooltip_id| tooltip_id.is_hovered(window)) { clear_active_tooltip_if_not_hoverable(&active_tooltip, window); } } diff --git a/crates/gpui/src/elements/image_cache.rs b/crates/gpui/src/elements/image_cache.rs index 263f0aafc2..ee1436134a 100644 --- a/crates/gpui/src/elements/image_cache.rs +++ b/crates/gpui/src/elements/image_cache.rs @@ -64,7 +64,7 @@ mod any_image_cache { cx: &mut App, ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> { let image_cache = image_cache.clone().downcast::<I>().unwrap(); - return image_cache.update(cx, |image_cache, cx| image_cache.load(resource, window, cx)); + image_cache.update(cx, |image_cache, cx| image_cache.load(resource, window, cx)) } } diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 98b63ef907..6758f4eee1 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -938,9 +938,10 @@ impl Element for List { let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); // If the width of the list has changed, invalidate all cached item heights - if state.last_layout_bounds.map_or(true, |last_bounds| { - last_bounds.size.width != bounds.size.width - }) { + if state + .last_layout_bounds + .is_none_or(|last_bounds| last_bounds.size.width != bounds.size.width) + { let new_items = SumTree::from_iter( state.items.iter().map(|item| ListItem::Unmeasured { focus_handle: item.focus_handle(), diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index f682b78c41..95374e579f 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -458,7 +458,7 @@ impl DispatchTree { .keymap .borrow() .bindings_for_input(input, &context_stack); - return (bindings, partial, context_stack); + (bindings, partial, context_stack) } /// dispatch_key processes the keystroke @@ -639,10 +639,7 @@ mod tests { } fn partial_eq(&self, action: &dyn Action) -> bool { - action - .as_any() - .downcast_ref::<Self>() - .map_or(false, |a| self == a) + action.as_any().downcast_ref::<Self>() == Some(self) } fn boxed_clone(&self) -> std::boxed::Box<dyn Action> { diff --git a/crates/gpui/src/keymap/context.rs b/crates/gpui/src/keymap/context.rs index 281035fe97..976f99c26e 100644 --- a/crates/gpui/src/keymap/context.rs +++ b/crates/gpui/src/keymap/context.rs @@ -287,7 +287,7 @@ impl KeyBindingContextPredicate { return false; } } - return true; + true } // Workspace > Pane > Editor // @@ -305,7 +305,7 @@ impl KeyBindingContextPredicate { return true; } } - return false; + false } Self::And(left, right) => { left.eval_inner(contexts, all_contexts) && right.eval_inner(contexts, all_contexts) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 3e002309e4..1df8a608f4 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -592,7 +592,7 @@ impl PlatformTextSystem for NoopTextSystem { } fn font_id(&self, _descriptor: &Font) -> Result<FontId> { - return Ok(FontId(1)); + Ok(FontId(1)) } fn font_metrics(&self, _font_id: FontId) -> FontMetrics { diff --git a/crates/gpui/src/platform/blade/blade_context.rs b/crates/gpui/src/platform/blade/blade_context.rs index 48872f1619..12c68a1e70 100644 --- a/crates/gpui/src/platform/blade/blade_context.rs +++ b/crates/gpui/src/platform/blade/blade_context.rs @@ -49,7 +49,7 @@ fn parse_pci_id(id: &str) -> anyhow::Result<u32> { "Expected a 4 digit PCI ID in hexadecimal format" ); - return u32::from_str_radix(id, 16).context("parsing PCI ID as hex"); + u32::from_str_radix(id, 16).context("parsing PCI ID as hex") } #[cfg(test)] diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index a1da088b75..ed824744a9 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -441,7 +441,7 @@ impl<P: LinuxClient + 'static> Platform for P { fn app_path(&self) -> Result<PathBuf> { // get the path of the executable of the current process let app_path = env::current_exe()?; - return Ok(app_path); + Ok(app_path) } fn set_menus(&self, menus: Vec<Menu>, _keymap: &Keymap) { diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index d1aa590192..3278dfbe38 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -710,9 +710,7 @@ impl LinuxClient for WaylandClient { fn set_cursor_style(&self, style: CursorStyle) { let mut state = self.0.borrow_mut(); - let need_update = state - .cursor_style - .map_or(true, |current_style| current_style != style); + let need_update = state.cursor_style != Some(style); if need_update { let serial = state.serial_tracker.get(SerialKind::MouseEnter); @@ -1577,7 +1575,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr { if state .keyboard_focused_window .as_ref() - .map_or(false, |keyboard_window| window.ptr_eq(keyboard_window)) + .is_some_and(|keyboard_window| window.ptr_eq(keyboard_window)) { state.enter_token = None; } diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 7cf2d02d3b..1d1166a56c 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -669,8 +669,8 @@ impl WaylandWindowStatePtr { pub fn set_size_and_scale(&self, size: Option<Size<Pixels>>, scale: Option<f32>) { let (size, scale) = { let mut state = self.state.borrow_mut(); - if size.map_or(true, |size| size == state.bounds.size) - && scale.map_or(true, |scale| scale == state.scale) + if size.is_none_or(|size| size == state.bounds.size) + && scale.is_none_or(|scale| scale == state.scale) { return; } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index b4914c9dd2..e422af961f 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1586,11 +1586,11 @@ impl LinuxClient for X11Client { fn read_from_primary(&self) -> Option<crate::ClipboardItem> { let state = self.0.borrow_mut(); - return state + state .clipboard .get_any(clipboard::ClipboardKind::Primary) .context("X11: Failed to read from clipboard (primary)") - .log_with_level(log::Level::Debug); + .log_with_level(log::Level::Debug) } fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> { @@ -1603,11 +1603,11 @@ impl LinuxClient for X11Client { { return state.clipboard_item.clone(); } - return state + state .clipboard .get_any(clipboard::ClipboardKind::Clipboard) .context("X11: Failed to read from clipboard (clipboard)") - .log_with_level(log::Level::Debug); + .log_with_level(log::Level::Debug) } fn run(&self) { @@ -2010,12 +2010,12 @@ fn check_gtk_frame_extents_supported( } fn xdnd_is_atom_supported(atom: u32, atoms: &XcbAtoms) -> bool { - return atom == atoms.TEXT + atom == atoms.TEXT || atom == atoms.STRING || atom == atoms.UTF8_STRING || atom == atoms.TEXT_PLAIN || atom == atoms.TEXT_PLAIN_UTF8 - || atom == atoms.TextUriList; + || atom == atoms.TextUriList } fn xdnd_get_supported_atom( @@ -2043,7 +2043,7 @@ fn xdnd_get_supported_atom( } } } - return 0; + 0 } fn xdnd_send_finished( @@ -2144,7 +2144,7 @@ fn current_pointer_device_states( if pointer_device_states.is_empty() { log::error!("Found no xinput mouse pointers."); } - return Some(pointer_device_states); + Some(pointer_device_states) } /// Returns true if the device is a pointer device. Does not include pointer device groups. diff --git a/crates/gpui/src/platform/linux/x11/clipboard.rs b/crates/gpui/src/platform/linux/x11/clipboard.rs index 5b32f2c93e..a6f96d38c4 100644 --- a/crates/gpui/src/platform/linux/x11/clipboard.rs +++ b/crates/gpui/src/platform/linux/x11/clipboard.rs @@ -1078,11 +1078,11 @@ impl Clipboard { } else { String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)? }; - return Ok(ClipboardItem::new_string(text)); + Ok(ClipboardItem::new_string(text)) } pub fn is_owner(&self, selection: ClipboardKind) -> bool { - return self.inner.is_owner(selection).unwrap_or(false); + self.inner.is_owner(selection).unwrap_or(false) } } diff --git a/crates/gpui/src/platform/linux/x11/event.rs b/crates/gpui/src/platform/linux/x11/event.rs index a566762c54..17bcc908d3 100644 --- a/crates/gpui/src/platform/linux/x11/event.rs +++ b/crates/gpui/src/platform/linux/x11/event.rs @@ -104,7 +104,7 @@ fn bit_is_set_in_vec(bit_vec: &Vec<u32>, bit_index: u16) -> bool { let array_index = bit_index as usize / 32; bit_vec .get(array_index) - .map_or(false, |bits| bit_is_set(*bits, bit_index % 32)) + .is_some_and(|bits| bit_is_set(*bits, bit_index % 32)) } fn bit_is_set(bits: u32, bit_index: u16) -> bool { diff --git a/crates/gpui/src/platform/mac/events.rs b/crates/gpui/src/platform/mac/events.rs index 0dc361b9dc..50a516cb38 100644 --- a/crates/gpui/src/platform/mac/events.rs +++ b/crates/gpui/src/platform/mac/events.rs @@ -311,9 +311,8 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask); let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask); let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask) - && first_char.map_or(true, |ch| { - !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch) - }); + && first_char + .is_none_or(|ch| !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch)); #[allow(non_upper_case_globals)] let key = match first_char { diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 57dfa9c603..832550dc46 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -797,7 +797,7 @@ impl Platform for MacPlatform { .to_owned(); result.set_file_name(&new_filename); } - return result; + result }) } } diff --git a/crates/gpui/src/platform/mac/text_system.rs b/crates/gpui/src/platform/mac/text_system.rs index 849925c727..72a0f2e565 100644 --- a/crates/gpui/src/platform/mac/text_system.rs +++ b/crates/gpui/src/platform/mac/text_system.rs @@ -319,7 +319,7 @@ impl MacTextSystemState { fn is_emoji(&self, font_id: FontId) -> bool { self.postscript_names_by_font_id .get(&font_id) - .map_or(false, |postscript_name| { + .is_some_and(|postscript_name| { postscript_name == "AppleColorEmoji" || postscript_name == ".AppleColorEmojiUI" }) } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index b6f684a72c..bc60e13a59 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -653,7 +653,7 @@ impl MacWindow { .and_then(|titlebar| titlebar.traffic_light_position), transparent_titlebar: titlebar .as_ref() - .map_or(true, |titlebar| titlebar.appears_transparent), + .is_none_or(|titlebar| titlebar.appears_transparent), previous_modifiers_changed_event: None, keystroke_for_do_command: None, do_command_handled: None, @@ -688,7 +688,7 @@ impl MacWindow { }); } - if titlebar.map_or(true, |titlebar| titlebar.appears_transparent) { + if titlebar.is_none_or(|titlebar| titlebar.appears_transparent) { native_window.setTitlebarAppearsTransparent_(YES); native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden); } diff --git a/crates/gpui/src/platform/test/dispatcher.rs b/crates/gpui/src/platform/test/dispatcher.rs index bdc7834931..4ce62c4bdc 100644 --- a/crates/gpui/src/platform/test/dispatcher.rs +++ b/crates/gpui/src/platform/test/dispatcher.rs @@ -270,9 +270,7 @@ impl PlatformDispatcher for TestDispatcher { fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>) { { let mut state = self.state.lock(); - if label.map_or(false, |label| { - state.deprioritized_task_labels.contains(&label) - }) { + if label.is_some_and(|label| state.deprioritized_task_labels.contains(&label)) { state.deprioritized_background.push(runnable); } else { state.background.push(runnable); diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 09985722ef..5b69ce7fa6 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -573,7 +573,7 @@ impl Style { if self .border_color - .map_or(false, |color| !color.is_transparent()) + .is_some_and(|color| !color.is_transparent()) { min.x += self.border_widths.left.to_pixels(rem_size); max.x -= self.border_widths.right.to_pixels(rem_size); @@ -633,7 +633,7 @@ impl Style { window.paint_shadows(bounds, corner_radii, &self.box_shadow); let background_color = self.background.as_ref().and_then(Fill::color); - if background_color.map_or(false, |color| !color.is_transparent()) { + if background_color.is_some_and(|color| !color.is_transparent()) { let mut border_color = match background_color { Some(color) => match color.tag { BackgroundTag::Solid => color.solid, @@ -729,7 +729,7 @@ impl Style { fn is_border_visible(&self) -> bool { self.border_color - .map_or(false, |color| !color.is_transparent()) + .is_some_and(|color| !color.is_transparent()) && self.border_widths.any(|length| !length.is_zero()) } } diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index 29af900b66..53991089da 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -435,7 +435,7 @@ impl WindowTextSystem { }); } - if decoration_runs.last().map_or(false, |last_run| { + if decoration_runs.last().is_some_and(|last_run| { last_run.color == run.color && last_run.underline == run.underline && last_run.strikethrough == run.strikethrough diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 62aeb0df11..89c1595a3f 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -243,14 +243,14 @@ impl FocusId { pub fn contains_focused(&self, window: &Window, cx: &App) -> bool { window .focused(cx) - .map_or(false, |focused| self.contains(focused.id, window)) + .is_some_and(|focused| self.contains(focused.id, window)) } /// Obtains whether the element associated with this handle is contained within the /// focused element or is itself focused. pub fn within_focused(&self, window: &Window, cx: &App) -> bool { let focused = window.focused(cx); - focused.map_or(false, |focused| focused.id.contains(*self, window)) + focused.is_some_and(|focused| focused.id.contains(*self, window)) } /// Obtains whether this handle contains the given handle in the most recently rendered frame. @@ -504,7 +504,7 @@ impl HitboxId { return true; } } - return false; + false } /// Checks if the hitbox with this ID contains the mouse and should handle scroll events. @@ -634,7 +634,7 @@ impl TooltipId { window .tooltip_bounds .as_ref() - .map_or(false, |tooltip_bounds| { + .is_some_and(|tooltip_bounds| { tooltip_bounds.id == *self && tooltip_bounds.bounds.contains(&window.mouse_position()) }) @@ -4466,7 +4466,7 @@ impl Window { } } } - return None; + None } } diff --git a/crates/gpui_macros/src/derive_inspector_reflection.rs b/crates/gpui_macros/src/derive_inspector_reflection.rs index 5415807ea0..9c1cb503a8 100644 --- a/crates/gpui_macros/src/derive_inspector_reflection.rs +++ b/crates/gpui_macros/src/derive_inspector_reflection.rs @@ -189,7 +189,7 @@ fn extract_cfg_attributes(attrs: &[Attribute]) -> Vec<Attribute> { fn is_called_from_gpui_crate(_span: Span) -> bool { // Check if we're being called from within the gpui crate by examining the call site // This is a heuristic approach - we check if the current crate name is "gpui" - std::env::var("CARGO_PKG_NAME").map_or(false, |name| name == "gpui") + std::env::var("CARGO_PKG_NAME").is_ok_and(|name| name == "gpui") } struct MacroExpander; diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 9227d35a50..972a90ddab 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1406,7 +1406,7 @@ impl Buffer { }) .unwrap_or(true); let result = any_sub_ranges_contain_range; - return result; + result }) .last() .map(|info| info.language.clone()) @@ -1520,12 +1520,12 @@ impl Buffer { let new_syntax_map = parse_task.await; this.update(cx, move |this, cx| { let grammar_changed = - this.language.as_ref().map_or(true, |current_language| { + this.language.as_ref().is_none_or(|current_language| { !Arc::ptr_eq(&language, current_language) }); let language_registry_changed = new_syntax_map .contains_unknown_injections() - && language_registry.map_or(false, |registry| { + && language_registry.is_some_and(|registry| { registry.version() != new_syntax_map.language_registry_version() }); let parse_again = language_registry_changed @@ -1719,8 +1719,7 @@ impl Buffer { }) .with_delta(suggestion.delta, language_indent_size); - if old_suggestions.get(&new_row).map_or( - true, + if old_suggestions.get(&new_row).is_none_or( |(old_indentation, was_within_error)| { suggested_indent != *old_indentation && (!suggestion.within_error || *was_within_error) @@ -2014,7 +2013,7 @@ impl Buffer { fn was_changed(&mut self) { self.change_bits.retain(|change_bit| { - change_bit.upgrade().map_or(false, |bit| { + change_bit.upgrade().is_some_and(|bit| { bit.replace(true); true }) @@ -2191,7 +2190,7 @@ impl Buffer { if self .remote_selections .get(&self.text.replica_id()) - .map_or(true, |set| !set.selections.is_empty()) + .is_none_or(|set| !set.selections.is_empty()) { self.set_active_selections(Arc::default(), false, Default::default(), cx); } @@ -2839,7 +2838,7 @@ impl Buffer { let mut edits: Vec<(Range<usize>, String)> = Vec::new(); let mut last_end = None; for _ in 0..old_range_count { - if last_end.map_or(false, |last_end| last_end >= self.len()) { + if last_end.is_some_and(|last_end| last_end >= self.len()) { break; } @@ -3059,14 +3058,14 @@ impl BufferSnapshot { if config .decrease_indent_pattern .as_ref() - .map_or(false, |regex| regex.is_match(line)) + .is_some_and(|regex| regex.is_match(line)) { indent_change_rows.push((row, Ordering::Less)); } if config .increase_indent_pattern .as_ref() - .map_or(false, |regex| regex.is_match(line)) + .is_some_and(|regex| regex.is_match(line)) { indent_change_rows.push((row + 1, Ordering::Greater)); } @@ -3082,7 +3081,7 @@ impl BufferSnapshot { } } for rule in &config.decrease_indent_patterns { - if rule.pattern.as_ref().map_or(false, |r| r.is_match(line)) { + if rule.pattern.as_ref().is_some_and(|r| r.is_match(line)) { let row_start_column = self.indent_size_for_line(row).len; let basis_row = rule .valid_after @@ -3295,8 +3294,7 @@ impl BufferSnapshot { range: Range<D>, ) -> Option<SyntaxLayer<'_>> { let range = range.to_offset(self); - return self - .syntax + self.syntax .layers_for_range(range, &self.text, false) .max_by(|a, b| { if a.depth != b.depth { @@ -3306,7 +3304,7 @@ impl BufferSnapshot { } else { a.node().end_byte().cmp(&b.node().end_byte()).reverse() } - }); + }) } /// Returns the main [`Language`]. @@ -3365,8 +3363,7 @@ impl BufferSnapshot { } if let Some(range) = range - && smallest_range_and_depth.as_ref().map_or( - true, + && smallest_range_and_depth.as_ref().is_none_or( |(smallest_range, smallest_range_depth)| { if layer.depth > *smallest_range_depth { true @@ -3543,7 +3540,7 @@ impl BufferSnapshot { } } - return Some(cursor.node()); + Some(cursor.node()) } /// Returns the outline for the buffer. @@ -3572,7 +3569,7 @@ impl BufferSnapshot { )?; let mut prev_depth = None; items.retain(|item| { - let result = prev_depth.map_or(true, |prev_depth| item.depth > prev_depth); + let result = prev_depth.is_none_or(|prev_depth| item.depth > prev_depth); prev_depth = Some(item.depth); result }); @@ -4449,7 +4446,7 @@ impl BufferSnapshot { pub fn words_in_range(&self, query: WordsQuery) -> BTreeMap<String, Range<Anchor>> { let query_str = query.fuzzy_contents; - if query_str.map_or(false, |query| query.is_empty()) { + if query_str.is_some_and(|query| query.is_empty()) { return BTreeMap::default(); } @@ -4490,7 +4487,7 @@ impl BufferSnapshot { .and_then(|first_chunk| first_chunk.chars().next()); // Skip empty and "words" starting with digits as a heuristic to reduce useless completions if !query.skip_digits - || first_char.map_or(true, |first_char| !first_char.is_digit(10)) + || first_char.is_none_or(|first_char| !first_char.is_digit(10)) { words.insert(word_text.collect(), word_range); } diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 83c16f4558..589fc68e99 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -773,7 +773,7 @@ impl LanguageRegistry { }; let content_matches = || { - config.first_line_pattern.as_ref().map_or(false, |pattern| { + config.first_line_pattern.as_ref().is_some_and(|pattern| { content .as_ref() .is_some_and(|content| pattern.is_match(content)) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 62fe75b6a8..fbb67a9818 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -253,7 +253,7 @@ impl EditPredictionSettings { !self.disabled_globs.iter().any(|glob| { if glob.is_absolute { file.as_local() - .map_or(false, |local| glob.matcher.is_match(local.abs_path(cx))) + .is_some_and(|local| glob.matcher.is_match(local.abs_path(cx))) } else { glob.matcher.is_match(file.path()) } diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 1e1060c843..f10056af13 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -1630,10 +1630,8 @@ impl<'a> SyntaxLayer<'a> { if offset < range.start || offset > range.end { continue; } - } else { - if offset <= range.start || offset >= range.end { - continue; - } + } else if offset <= range.start || offset >= range.end { + continue; } if let Some((_, smallest_range)) = &smallest_match { diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index b16be36ea1..0d061c0587 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -554,7 +554,7 @@ pub fn into_anthropic( .into_iter() .filter_map(|content| match content { MessageContent::Text(text) => { - let text = if text.chars().last().map_or(false, |c| c.is_whitespace()) { + let text = if text.chars().last().is_some_and(|c| c.is_whitespace()) { text.trim_end().to_string() } else { text @@ -813,7 +813,7 @@ impl AnthropicEventMapper { ))]; } } - return vec![]; + vec![] } }, Event::ContentBlockStop { index } => { diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index e99dadc28d..d3fee7b63b 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -270,7 +270,7 @@ impl State { if response.status().is_success() { let mut body = String::new(); response.body_mut().read_to_string(&mut body).await?; - return Ok(serde_json::from_str(&body)?); + Ok(serde_json::from_str(&body)?) } else { let mut body = String::new(); response.body_mut().read_to_string(&mut body).await?; diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index 1bb9f3fa00..a36ce949b1 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -530,7 +530,7 @@ pub fn into_google( let system_instructions = if request .messages .first() - .map_or(false, |msg| matches!(msg.role, Role::System)) + .is_some_and(|msg| matches!(msg.role, Role::System)) { let message = request.messages.remove(0); Some(SystemInstruction { diff --git a/crates/language_tools/src/key_context_view.rs b/crates/language_tools/src/key_context_view.rs index 320668cfc2..057259d114 100644 --- a/crates/language_tools/src/key_context_view.rs +++ b/crates/language_tools/src/key_context_view.rs @@ -71,12 +71,10 @@ impl KeyContextView { } else { None } + } else if this.action_matches(&e.action, binding.action()) { + Some(true) } else { - if this.action_matches(&e.action, binding.action()) { - Some(true) - } else { - Some(false) - } + Some(false) }; let predicate = if let Some(predicate) = binding.predicate() { format!("{}", predicate) diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_tool.rs index 3244350a34..dd3e80212f 100644 --- a/crates/language_tools/src/lsp_tool.rs +++ b/crates/language_tools/src/lsp_tool.rs @@ -349,7 +349,6 @@ impl LanguageServerState { .detach(); } else { cx.propagate(); - return; } } }, @@ -523,7 +522,6 @@ impl LspTool { if ProjectSettings::get_global(cx).global_lsp_settings.button { if lsp_tool.lsp_menu.is_none() { lsp_tool.refresh_lsp_menu(true, window, cx); - return; } } else if lsp_tool.lsp_menu.take().is_some() { cx.notify(); diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 00e3cad436..d6f9538ee4 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -452,7 +452,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ && entry .file_name() .to_str() - .map_or(false, |name| name.starts_with("gopls_")) + .is_some_and(|name| name.starts_with("gopls_")) { last_binary_path = Some(entry.path()); } diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 6f57ace488..2c490b45cf 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -280,7 +280,7 @@ impl JsonLspAdapter { ) })?; writer.replace(config.clone()); - return Ok(config); + Ok(config) } } diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 89a091797e..906e45bb3a 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -828,7 +828,7 @@ impl ToolchainLister for PythonToolchainProvider { .get_env_var("CONDA_PREFIX".to_string()) .map(|conda_prefix| { let is_match = |exe: &Option<PathBuf>| { - exe.as_ref().map_or(false, |e| e.starts_with(&conda_prefix)) + exe.as_ref().is_some_and(|e| e.starts_with(&conda_prefix)) }; match (is_match(&lhs.executable), is_match(&rhs.executable)) { (true, false) => Ordering::Less, diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index f9b23ed9f4..eb5e0cee7c 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -403,7 +403,7 @@ impl LspAdapter for RustLspAdapter { } else if completion .detail .as_ref() - .map_or(false, |detail| detail.starts_with("macro_rules! ")) + .is_some_and(|detail| detail.starts_with("macro_rules! ")) { let text = completion.label.clone(); let len = text.len(); @@ -496,7 +496,7 @@ impl LspAdapter for RustLspAdapter { let enable_lsp_tasks = ProjectSettings::get_global(cx) .lsp .get(&SERVER_NAME) - .map_or(false, |s| s.enable_lsp_tasks); + .is_some_and(|s| s.enable_lsp_tasks); if enable_lsp_tasks { let experimental = json!({ "runnables": { diff --git a/crates/livekit_client/examples/test_app.rs b/crates/livekit_client/examples/test_app.rs index e1d01df534..51f335c2db 100644 --- a/crates/livekit_client/examples/test_app.rs +++ b/crates/livekit_client/examples/test_app.rs @@ -159,14 +159,14 @@ impl LivekitWindow { if output .audio_output_stream .as_ref() - .map_or(false, |(track, _)| track.sid() == unpublish_sid) + .is_some_and(|(track, _)| track.sid() == unpublish_sid) { output.audio_output_stream.take(); } if output .screen_share_output_view .as_ref() - .map_or(false, |(track, _)| track.sid() == unpublish_sid) + .is_some_and(|(track, _)| track.sid() == unpublish_sid) { output.screen_share_output_view.take(); } diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index 890d564b7a..8c8d9e177f 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -76,22 +76,22 @@ impl<'a> MarkdownParser<'a> { if self.eof() || (steps + self.cursor) >= self.tokens.len() { return self.tokens.last(); } - return self.tokens.get(self.cursor + steps); + self.tokens.get(self.cursor + steps) } fn previous(&self) -> Option<&(Event<'_>, Range<usize>)> { if self.cursor == 0 || self.cursor > self.tokens.len() { return None; } - return self.tokens.get(self.cursor - 1); + self.tokens.get(self.cursor - 1) } fn current(&self) -> Option<&(Event<'_>, Range<usize>)> { - return self.peek(0); + self.peek(0) } fn current_event(&self) -> Option<&Event<'_>> { - return self.current().map(|(event, _)| event); + self.current().map(|(event, _)| event) } fn is_text_like(event: &Event) -> bool { diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 3acc4b5600..b0b10e927c 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -111,11 +111,10 @@ impl RenderContext { /// buffer font size changes. The callees of this function should be reimplemented to use real /// relative sizing once that is implemented in GPUI pub fn scaled_rems(&self, rems: f32) -> Rems { - return self - .buffer_text_style + self.buffer_text_style .font_size .to_rems(self.window_rem_size) - .mul(rems); + .mul(rems) } /// This ensures that children inside of block quotes diff --git a/crates/migrator/src/migrations/m_2025_05_05/settings.rs b/crates/migrator/src/migrations/m_2025_05_05/settings.rs index 88c6c338d1..77da1b9a07 100644 --- a/crates/migrator/src/migrations/m_2025_05_05/settings.rs +++ b/crates/migrator/src/migrations/m_2025_05_05/settings.rs @@ -24,7 +24,7 @@ fn rename_assistant( .nodes_for_capture_index(key_capture_ix) .next()? .byte_range(); - return Some((key_range, "agent".to_string())); + Some((key_range, "agent".to_string())) } fn rename_edit_prediction_assistant( @@ -37,5 +37,5 @@ fn rename_edit_prediction_assistant( .nodes_for_capture_index(key_capture_ix) .next()? .byte_range(); - return Some((key_range, "enabled_in_text_threads".to_string())); + Some((key_range, "enabled_in_text_threads".to_string())) } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index ab5f148d6c..7f65ccf5ea 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1146,13 +1146,13 @@ impl MultiBuffer { pub fn last_transaction_id(&self, cx: &App) -> Option<TransactionId> { if let Some(buffer) = self.as_singleton() { - return buffer + buffer .read(cx) .peek_undo_stack() - .map(|history_entry| history_entry.transaction_id()); + .map(|history_entry| history_entry.transaction_id()) } else { let last_transaction = self.history.undo_stack.last()?; - return Some(last_transaction.id); + Some(last_transaction.id) } } @@ -1725,7 +1725,7 @@ impl MultiBuffer { merged_ranges.push(range.clone()); counts.push(1); } - return (merged_ranges, counts); + (merged_ranges, counts) } fn update_path_excerpts( @@ -2482,7 +2482,7 @@ impl MultiBuffer { let base_text_changed = snapshot .diffs .get(&buffer_id) - .map_or(true, |old_diff| !new_diff.base_texts_eq(old_diff)); + .is_none_or(|old_diff| !new_diff.base_texts_eq(old_diff)); snapshot.diffs.insert(buffer_id, new_diff); @@ -2776,7 +2776,7 @@ impl MultiBuffer { if diff_hunk.excerpt_id.cmp(&end_excerpt_id, &snapshot).is_gt() { continue; } - if last_hunk_row.map_or(false, |row| row >= diff_hunk.row_range.start) { + if last_hunk_row.is_some_and(|row| row >= diff_hunk.row_range.start) { continue; } let start = Anchor::in_buffer( @@ -3040,7 +3040,7 @@ impl MultiBuffer { is_dirty |= buffer.is_dirty(); has_deleted_file |= buffer .file() - .map_or(false, |file| file.disk_state() == DiskState::Deleted); + .is_some_and(|file| file.disk_state() == DiskState::Deleted); has_conflict |= buffer.has_conflict(); } if edited { @@ -3198,9 +3198,10 @@ impl MultiBuffer { // If this is the last edit that intersects the current diff transform, // then recreate the content up to the end of this transform, to prepare // for reusing additional slices of the old transforms. - if excerpt_edits.peek().map_or(true, |next_edit| { - next_edit.old.start >= old_diff_transforms.end().0 - }) { + if excerpt_edits + .peek() + .is_none_or(|next_edit| next_edit.old.start >= old_diff_transforms.end().0) + { let keep_next_old_transform = (old_diff_transforms.start().0 >= edit.old.end) && match old_diff_transforms.item() { Some(DiffTransform::BufferContent { @@ -3595,7 +3596,7 @@ impl MultiBuffer { let mut edits: Vec<(Range<usize>, Arc<str>)> = Vec::new(); let mut last_end = None; for _ in 0..edit_count { - if last_end.map_or(false, |last_end| last_end >= snapshot.len()) { + if last_end.is_some_and(|last_end| last_end >= snapshot.len()) { break; } @@ -4649,7 +4650,7 @@ impl MultiBufferSnapshot { return true; } } - return true; + true } pub fn prev_non_blank_row(&self, mut row: MultiBufferRow) -> Option<MultiBufferRow> { @@ -4954,7 +4955,7 @@ impl MultiBufferSnapshot { while let Some(replacement) = self.replaced_excerpts.get(&excerpt_id) { excerpt_id = *replacement; } - return excerpt_id; + excerpt_id } pub fn summaries_for_anchors<'a, D, I>(&'a self, anchors: I) -> Vec<D> @@ -5072,9 +5073,9 @@ impl MultiBufferSnapshot { if point == region.range.end.key && region.has_trailing_newline { position.add_assign(&D::from_text_summary(&TextSummary::newline())); } - return Some(position); + Some(position) } else { - return Some(D::from_text_summary(&self.text_summary())); + Some(D::from_text_summary(&self.text_summary())) } }) } @@ -5114,7 +5115,7 @@ impl MultiBufferSnapshot { // Leave min and max anchors unchanged if invalid or // if the old excerpt still exists at this location let mut kept_position = next_excerpt - .map_or(false, |e| e.id == old_excerpt_id && e.contains(&anchor)) + .is_some_and(|e| e.id == old_excerpt_id && e.contains(&anchor)) || old_excerpt_id == ExcerptId::max() || old_excerpt_id == ExcerptId::min(); @@ -5482,7 +5483,7 @@ impl MultiBufferSnapshot { let range_filter = |open: Range<usize>, close: Range<usize>| -> bool { excerpt_buffer_range.contains(&open.start) && excerpt_buffer_range.contains(&close.end) - && range_filter.map_or(true, |filter| filter(buffer, open, close)) + && range_filter.is_none_or(|filter| filter(buffer, open, close)) }; let (open, close) = excerpt.buffer().innermost_enclosing_bracket_ranges( @@ -5642,10 +5643,10 @@ impl MultiBufferSnapshot { .buffer .line_indents_in_row_range(buffer_start_row..buffer_end_row); cursor.next(); - return Some(line_indents.map(move |(buffer_row, indent)| { + Some(line_indents.map(move |(buffer_row, indent)| { let row = region.range.start.row + (buffer_row - region.buffer_range.start.row); (MultiBufferRow(row), indent, ®ion.excerpt.buffer) - })); + })) }) .flatten() } @@ -5682,10 +5683,10 @@ impl MultiBufferSnapshot { .buffer .reversed_line_indents_in_row_range(buffer_start_row..buffer_end_row); cursor.prev(); - return Some(line_indents.map(move |(buffer_row, indent)| { + Some(line_indents.map(move |(buffer_row, indent)| { let row = region.range.start.row + (buffer_row - region.buffer_range.start.row); (MultiBufferRow(row), indent, ®ion.excerpt.buffer) - })); + })) }) .flatten() } @@ -6545,7 +6546,7 @@ where && self .excerpts .item() - .map_or(false, |excerpt| excerpt.id != hunk_info.excerpt_id) + .is_some_and(|excerpt| excerpt.id != hunk_info.excerpt_id) { self.excerpts.next(); } @@ -6592,7 +6593,7 @@ where let prev_transform = self.diff_transforms.item(); self.diff_transforms.next(); - prev_transform.map_or(true, |next_transform| { + prev_transform.is_none_or(|next_transform| { matches!(next_transform, DiffTransform::BufferContent { .. }) }) } @@ -6607,12 +6608,12 @@ where } let next_transform = self.diff_transforms.next_item(); - next_transform.map_or(true, |next_transform| match next_transform { + next_transform.is_none_or(|next_transform| match next_transform { DiffTransform::BufferContent { .. } => true, DiffTransform::DeletedHunk { hunk_info, .. } => self .excerpts .item() - .map_or(false, |excerpt| excerpt.id != hunk_info.excerpt_id), + .is_some_and(|excerpt| excerpt.id != hunk_info.excerpt_id), }) } @@ -6645,7 +6646,7 @@ where buffer_end.add_assign(&buffer_range_len); let start = self.diff_transforms.start().output_dimension.0; let end = self.diff_transforms.end().output_dimension.0; - return Some(MultiBufferRegion { + Some(MultiBufferRegion { buffer, excerpt, has_trailing_newline: *has_trailing_newline, @@ -6655,7 +6656,7 @@ where )), buffer_range: buffer_start..buffer_end, range: start..end, - }); + }) } DiffTransform::BufferContent { inserted_hunk_info, .. @@ -7493,61 +7494,59 @@ impl Iterator for MultiBufferRows<'_> { self.cursor.next(); if let Some(next_region) = self.cursor.region() { region = next_region; - } else { - if self.point == self.cursor.diff_transforms.end().output_dimension.0 { - let multibuffer_row = MultiBufferRow(self.point.row); - let last_excerpt = self - .cursor - .excerpts - .item() - .or(self.cursor.excerpts.prev_item())?; - let last_row = last_excerpt - .range - .context - .end - .to_point(&last_excerpt.buffer) - .row; + } else if self.point == self.cursor.diff_transforms.end().output_dimension.0 { + let multibuffer_row = MultiBufferRow(self.point.row); + let last_excerpt = self + .cursor + .excerpts + .item() + .or(self.cursor.excerpts.prev_item())?; + let last_row = last_excerpt + .range + .context + .end + .to_point(&last_excerpt.buffer) + .row; - let first_row = last_excerpt - .range - .context - .start - .to_point(&last_excerpt.buffer) - .row; + let first_row = last_excerpt + .range + .context + .start + .to_point(&last_excerpt.buffer) + .row; - let expand_info = if self.is_singleton { - None - } else { - let needs_expand_up = first_row == last_row - && last_row > 0 - && !region.diff_hunk_status.is_some_and(|d| d.is_deleted()); - let needs_expand_down = last_row < last_excerpt.buffer.max_point().row; - - if needs_expand_up && needs_expand_down { - Some(ExpandExcerptDirection::UpAndDown) - } else if needs_expand_up { - Some(ExpandExcerptDirection::Up) - } else if needs_expand_down { - Some(ExpandExcerptDirection::Down) - } else { - None - } - .map(|direction| ExpandInfo { - direction, - excerpt_id: last_excerpt.id, - }) - }; - self.point += Point::new(1, 0); - return Some(RowInfo { - buffer_id: Some(last_excerpt.buffer_id), - buffer_row: Some(last_row), - multibuffer_row: Some(multibuffer_row), - diff_status: None, - expand_info, - }); + let expand_info = if self.is_singleton { + None } else { - return None; - } + let needs_expand_up = first_row == last_row + && last_row > 0 + && !region.diff_hunk_status.is_some_and(|d| d.is_deleted()); + let needs_expand_down = last_row < last_excerpt.buffer.max_point().row; + + if needs_expand_up && needs_expand_down { + Some(ExpandExcerptDirection::UpAndDown) + } else if needs_expand_up { + Some(ExpandExcerptDirection::Up) + } else if needs_expand_down { + Some(ExpandExcerptDirection::Down) + } else { + None + } + .map(|direction| ExpandInfo { + direction, + excerpt_id: last_excerpt.id, + }) + }; + self.point += Point::new(1, 0); + return Some(RowInfo { + buffer_id: Some(last_excerpt.buffer_id), + buffer_row: Some(last_row), + multibuffer_row: Some(multibuffer_row), + diff_status: None, + expand_info, + }); + } else { + return None; }; } diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index f92c122e71..871c72ea0b 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -197,7 +197,7 @@ impl NodeRuntime { state.instance = Some(instance.boxed_clone()); state.last_options = Some(options); - return instance; + instance } pub async fn binary_path(&self) -> Result<PathBuf> { diff --git a/crates/onboarding/src/theme_preview.rs b/crates/onboarding/src/theme_preview.rs index 9f299eb6ea..6a072b00e9 100644 --- a/crates/onboarding/src/theme_preview.rs +++ b/crates/onboarding/src/theme_preview.rs @@ -210,12 +210,12 @@ impl ThemePreviewTile { } fn render_borderless(seed: f32, theme: Arc<Theme>) -> impl IntoElement { - return Self::render_editor( + Self::render_editor( seed, theme, Self::SIDEBAR_WIDTH_DEFAULT, Self::SKELETON_HEIGHT_DEFAULT, - ); + ) } fn render_border(seed: f32, theme: Arc<Theme>) -> impl IntoElement { @@ -246,7 +246,7 @@ impl ThemePreviewTile { ) -> impl IntoElement { let sidebar_width = relative(0.20); - return div() + div() .size_full() .p(Self::ROOT_PADDING) .rounded(Self::ROOT_RADIUS) @@ -278,7 +278,7 @@ impl ThemePreviewTile { )), ), ) - .into_any_element(); + .into_any_element() } } diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 1fb9a1342c..acf6ec434a 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -9,7 +9,7 @@ use strum::EnumIter; pub const OPEN_AI_API_URL: &str = "https://api.openai.com/v1"; fn is_none_or_empty<T: AsRef<[U]>, U>(opt: &Option<T>) -> bool { - opt.as_ref().map_or(true, |v| v.as_ref().is_empty()) + opt.as_ref().is_none_or(|v| v.as_ref().is_empty()) } #[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] @@ -241,7 +241,7 @@ impl Model { /// /// If the model does not support the parameter, do not pass it up. pub fn supports_prompt_cache_key(&self) -> bool { - return true; + true } } diff --git a/crates/open_router/src/open_router.rs b/crates/open_router/src/open_router.rs index 7c304bad64..b7e6d69d8f 100644 --- a/crates/open_router/src/open_router.rs +++ b/crates/open_router/src/open_router.rs @@ -8,7 +8,7 @@ use std::convert::TryFrom; pub const OPEN_ROUTER_API_URL: &str = "https://openrouter.ai/api/v1"; fn is_none_or_empty<T: AsRef<[U]>, U>(opt: &Option<T>) -> bool { - opt.as_ref().map_or(true, |v| v.as_ref().is_empty()) + opt.as_ref().is_none_or(|v| v.as_ref().is_empty()) } #[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 9b7ec473fd..78f512f7f3 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -503,16 +503,16 @@ impl SearchData { && multi_buffer_snapshot .chars_at(extended_context_left_border) .last() - .map_or(false, |c| !c.is_whitespace()); + .is_some_and(|c| !c.is_whitespace()); let truncated_right = entire_context_text .chars() .last() - .map_or(true, |c| !c.is_whitespace()) + .is_none_or(|c| !c.is_whitespace()) && extended_context_right_border > context_right_border && multi_buffer_snapshot .chars_at(extended_context_right_border) .next() - .map_or(false, |c| !c.is_whitespace()); + .is_some_and(|c| !c.is_whitespace()); search_match_indices.iter_mut().for_each(|range| { range.start = multi_buffer_snapshot.clip_offset( range.start.saturating_sub(left_whitespaces_offset), @@ -1259,7 +1259,7 @@ impl OutlinePanel { dirs_worktree_id == worktree_id && dirs .last() - .map_or(false, |dir| dir.path.as_ref() == parent_path) + .is_some_and(|dir| dir.path.as_ref() == parent_path) } _ => false, }) @@ -1453,9 +1453,7 @@ impl OutlinePanel { if self .unfolded_dirs .get(&directory_worktree) - .map_or(true, |unfolded_dirs| { - !unfolded_dirs.contains(&directory_entry.id) - }) + .is_none_or(|unfolded_dirs| !unfolded_dirs.contains(&directory_entry.id)) { return false; } @@ -2156,7 +2154,7 @@ impl OutlinePanel { ExcerptOutlines::Invalidated(outlines) => Some(outlines), ExcerptOutlines::NotFetched => None, }) - .map_or(false, |outlines| !outlines.is_empty()); + .is_some_and(|outlines| !outlines.is_empty()); let is_expanded = !self .collapsed_entries .contains(&CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)); @@ -2953,7 +2951,7 @@ impl OutlinePanel { .map(|(parent_dir_id, _)| { new_unfolded_dirs .get(&directory.worktree_id) - .map_or(true, |unfolded_dirs| { + .is_none_or(|unfolded_dirs| { unfolded_dirs .contains(parent_dir_id) }) @@ -3444,9 +3442,8 @@ impl OutlinePanel { } fn is_singleton_active(&self, cx: &App) -> bool { - self.active_editor().map_or(false, |active_editor| { - active_editor.read(cx).buffer().read(cx).is_singleton() - }) + self.active_editor() + .is_some_and(|active_editor| active_editor.read(cx).buffer().read(cx).is_singleton()) } fn invalidate_outlines(&mut self, ids: &[ExcerptId]) { @@ -3664,7 +3661,7 @@ impl OutlinePanel { let is_root = project .read(cx) .worktree_for_id(directory_entry.worktree_id, cx) - .map_or(false, |worktree| { + .is_some_and(|worktree| { worktree.read(cx).root_entry() == Some(&directory_entry.entry) }); let folded = auto_fold_dirs @@ -3672,7 +3669,7 @@ impl OutlinePanel { && outline_panel .unfolded_dirs .get(&directory_entry.worktree_id) - .map_or(true, |unfolded_dirs| { + .is_none_or(|unfolded_dirs| { !unfolded_dirs.contains(&directory_entry.entry.id) }); let fs_depth = outline_panel @@ -3752,7 +3749,7 @@ impl OutlinePanel { .iter() .rev() .nth(folded_dirs.entries.len() + 1) - .map_or(true, |parent| parent.expanded); + .is_none_or(|parent| parent.expanded); if start_of_collapsed_dir_sequence || parent_expanded || query.is_some() @@ -3812,7 +3809,7 @@ impl OutlinePanel { .iter() .all(|entry| entry.path != parent.path) }) - .map_or(true, |parent| parent.expanded); + .is_none_or(|parent| parent.expanded); if !is_singleton && (parent_expanded || query.is_some()) { outline_panel.push_entry( &mut generation_state, @@ -3837,7 +3834,7 @@ impl OutlinePanel { .iter() .all(|entry| entry.path != parent.path) }) - .map_or(true, |parent| parent.expanded); + .is_none_or(|parent| parent.expanded); if !is_singleton && (parent_expanded || query.is_some()) { outline_panel.push_entry( &mut generation_state, @@ -3958,7 +3955,7 @@ impl OutlinePanel { .iter() .all(|entry| entry.path != parent.path) }) - .map_or(true, |parent| parent.expanded); + .is_none_or(|parent| parent.expanded); if parent_expanded || query.is_some() { outline_panel.push_entry( &mut generation_state, @@ -4438,7 +4435,7 @@ impl OutlinePanel { } fn should_replace_active_item(&self, new_active_item: &dyn ItemHandle) -> bool { - self.active_item().map_or(true, |active_item| { + self.active_item().is_none_or(|active_item| { !self.pinned && active_item.item_id() != new_active_item.item_id() }) } diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index 8e1485dc9a..32e39d466f 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -119,7 +119,7 @@ impl Prettier { None } }).any(|workspace_definition| { - workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition]).ok().map_or(false, |path_matcher| path_matcher.is_match(subproject_path)) + workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition]).ok().is_some_and(|path_matcher| path_matcher.is_match(subproject_path)) }) { anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?, "Path {path_to_check:?} is the workspace root for project in {closest_package_json_path:?}, but it has no prettier installed"); log::info!("Found prettier path {path_to_check:?} in the workspace root for project in {closest_package_json_path:?}"); @@ -217,7 +217,7 @@ impl Prettier { workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition]) .ok() - .map_or(false, |path_matcher| { + .is_some_and(|path_matcher| { path_matcher.is_match(subproject_path) }) }) diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 96e87b1fe0..296749c14e 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -234,7 +234,7 @@ impl RemoteBufferStore { } } } - return Ok(None); + Ok(None) } pub fn incomplete_buffer_ids(&self) -> Vec<BufferId> { @@ -1313,10 +1313,7 @@ impl BufferStore { let new_path = file.path.clone(); buffer.file_updated(Arc::new(file), cx); - if old_file - .as_ref() - .map_or(true, |old| *old.path() != new_path) - { + if old_file.as_ref().is_none_or(|old| *old.path() != new_path) { Some(old_file) } else { None diff --git a/crates/project/src/color_extractor.rs b/crates/project/src/color_extractor.rs index 5473da88af..dbbd3d7b99 100644 --- a/crates/project/src/color_extractor.rs +++ b/crates/project/src/color_extractor.rs @@ -102,7 +102,7 @@ fn parse(str: &str, mode: ParseMode) -> Option<Hsla> { }; } - return None; + None } fn parse_component(value: &str, max: f32) -> Option<f32> { diff --git a/crates/project/src/debugger/locators/cargo.rs b/crates/project/src/debugger/locators/cargo.rs index 9a36584e71..3e28fac8af 100644 --- a/crates/project/src/debugger/locators/cargo.rs +++ b/crates/project/src/debugger/locators/cargo.rs @@ -146,7 +146,7 @@ impl DapLocator for CargoLocator { let is_test = build_config .args .first() - .map_or(false, |arg| arg == "test" || arg == "t"); + .is_some_and(|arg| arg == "test" || arg == "t"); let executables = output .lines() diff --git a/crates/project/src/debugger/locators/python.rs b/crates/project/src/debugger/locators/python.rs index 3de1281aed..71efbb75b9 100644 --- a/crates/project/src/debugger/locators/python.rs +++ b/crates/project/src/debugger/locators/python.rs @@ -28,9 +28,7 @@ impl DapLocator for PythonLocator { let valid_program = build_config.command.starts_with("$ZED_") || Path::new(&build_config.command) .file_name() - .map_or(false, |name| { - name.to_str().is_some_and(|path| path.starts_with("python")) - }); + .is_some_and(|name| name.to_str().is_some_and(|path| path.starts_with("python"))); if !valid_program || build_config.args.iter().any(|arg| arg == "-c") { // We cannot debug selections. return None; diff --git a/crates/project/src/debugger/memory.rs b/crates/project/src/debugger/memory.rs index 092435fda7..a8729a8ff4 100644 --- a/crates/project/src/debugger/memory.rs +++ b/crates/project/src/debugger/memory.rs @@ -329,7 +329,7 @@ impl Iterator for MemoryIterator { } if !self.fetch_next_page() { self.start += 1; - return Some(MemoryCell(None)); + Some(MemoryCell(None)) } else { self.next() } diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index b5ae714841..ee5baf1d3b 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -431,7 +431,7 @@ impl RunningMode { let should_send_exception_breakpoints = capabilities .exception_breakpoint_filters .as_ref() - .map_or(false, |filters| !filters.is_empty()) + .is_some_and(|filters| !filters.is_empty()) || !configuration_done_supported; let supports_exception_filters = capabilities .supports_exception_filter_options @@ -710,9 +710,7 @@ where T: LocalDapCommand + PartialEq + Eq + Hash, { fn dyn_eq(&self, rhs: &dyn CacheableCommand) -> bool { - (rhs as &dyn Any) - .downcast_ref::<Self>() - .map_or(false, |rhs| self == rhs) + (rhs as &dyn Any).downcast_ref::<Self>() == Some(self) } fn dyn_hash(&self, mut hasher: &mut dyn Hasher) { @@ -1085,7 +1083,7 @@ impl Session { }) .detach(); - return tx; + tx } pub fn is_started(&self) -> bool { diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index ebc29a0a4b..edc6b00a7b 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -781,9 +781,7 @@ impl GitStore { let is_unmerged = self .repository_and_path_for_buffer_id(buffer_id, cx) - .map_or(false, |(repo, path)| { - repo.read(cx).snapshot.has_conflict(&path) - }); + .is_some_and(|(repo, path)| repo.read(cx).snapshot.has_conflict(&path)); let git_store = cx.weak_entity(); let buffer_git_state = self .diffs @@ -2501,14 +2499,14 @@ impl BufferGitState { pub fn wait_for_recalculation(&mut self) -> Option<impl Future<Output = ()> + use<>> { if *self.recalculating_tx.borrow() { let mut rx = self.recalculating_tx.subscribe(); - return Some(async move { + Some(async move { loop { let is_recalculating = rx.recv().await; if is_recalculating != Some(true) { break; } } - }); + }) } else { None } @@ -2879,7 +2877,7 @@ impl RepositorySnapshot { self.merge.conflicted_paths.contains(repo_path); let has_conflict_currently = self .status_for_path(repo_path) - .map_or(false, |entry| entry.status.is_conflicted()); + .is_some_and(|entry| entry.status.is_conflicted()); had_conflict_on_last_merge_head_change || has_conflict_currently } @@ -3531,7 +3529,7 @@ impl Repository { && buffer .read(cx) .file() - .map_or(false, |file| file.disk_state().exists()) + .is_some_and(|file| file.disk_state().exists()) { save_futures.push(buffer_store.save_buffer(buffer, cx)); } @@ -3597,7 +3595,7 @@ impl Repository { && buffer .read(cx) .file() - .map_or(false, |file| file.disk_state().exists()) + .is_some_and(|file| file.disk_state().exists()) { save_futures.push(buffer_store.save_buffer(buffer, cx)); } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 64414c6545..217e00ee96 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -3447,9 +3447,7 @@ impl LspCommand for GetCodeLens { .server_capabilities .code_lens_provider .as_ref() - .map_or(false, |code_lens_options| { - code_lens_options.resolve_provider.unwrap_or(false) - }) + .is_some_and(|code_lens_options| code_lens_options.resolve_provider.unwrap_or(false)) } fn to_lsp( diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index e93e859dcf..e6ea01ff9a 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -1038,7 +1038,7 @@ impl LocalLspStore { if let Some(LanguageServerState::Running { server, .. }) = self.language_servers.get(&state.id) { - return Some(server); + Some(server) } else { None } @@ -1879,7 +1879,7 @@ impl LocalLspStore { ) -> Result<Vec<(Range<Anchor>, Arc<str>)>> { let capabilities = &language_server.capabilities(); let range_formatting_provider = capabilities.document_range_formatting_provider.as_ref(); - if range_formatting_provider.map_or(false, |provider| provider == &OneOf::Left(false)) { + if range_formatting_provider == Some(&OneOf::Left(false)) { anyhow::bail!( "{} language server does not support range formatting", language_server.name() @@ -2642,7 +2642,7 @@ impl LocalLspStore { this.request_lsp(buffer.clone(), server, request, cx) })? .await?; - return Ok(actions); + Ok(actions) } pub async fn execute_code_actions_on_server( @@ -2718,7 +2718,7 @@ impl LocalLspStore { } } } - return Ok(()); + Ok(()) } pub async fn deserialize_text_edits( @@ -2957,11 +2957,11 @@ impl LocalLspStore { .update(cx, |this, cx| { let path = buffer_to_edit.read(cx).project_path(cx); let active_entry = this.active_entry; - let is_active_entry = path.clone().map_or(false, |project_path| { + let is_active_entry = path.clone().is_some_and(|project_path| { this.worktree_store .read(cx) .entry_for_path(&project_path, cx) - .map_or(false, |entry| Some(entry.id) == active_entry) + .is_some_and(|entry| Some(entry.id) == active_entry) }); let local = this.as_local_mut().unwrap(); @@ -4038,7 +4038,7 @@ impl LspStore { servers.push((json_adapter, json_server, json_delegate)); } - return Some(servers); + Some(servers) }) .ok() .flatten(); @@ -4050,7 +4050,7 @@ impl LspStore { let Ok(Some((fs, _))) = this.read_with(cx, |this, _| { let local = this.as_local()?; let toolchain_store = local.toolchain_store().clone(); - return Some((local.fs.clone(), toolchain_store)); + Some((local.fs.clone(), toolchain_store)) }) else { return; }; @@ -4312,9 +4312,10 @@ impl LspStore { local_store.unregister_buffer_from_language_servers(buffer_entity, &file_url, cx); } buffer_entity.update(cx, |buffer, cx| { - if buffer.language().map_or(true, |old_language| { - !Arc::ptr_eq(old_language, &new_language) - }) { + if buffer + .language() + .is_none_or(|old_language| !Arc::ptr_eq(old_language, &new_language)) + { buffer.set_language(Some(new_language.clone()), cx); } }); @@ -4514,7 +4515,7 @@ impl LspStore { if !request.check_capabilities(language_server.adapter_server_capabilities()) { return Task::ready(Ok(Default::default())); } - return cx.spawn(async move |this, cx| { + cx.spawn(async move |this, cx| { let lsp_request = language_server.request::<R::LspRequest>(lsp_params); let id = lsp_request.id(); @@ -4573,7 +4574,7 @@ impl LspStore { ) .await; response - }); + }) } fn on_settings_changed(&mut self, cx: &mut Context<Self>) { @@ -7297,7 +7298,7 @@ impl LspStore { include_ignored || worktree .entry_for_path(path.as_ref()) - .map_or(false, |entry| !entry.is_ignored) + .is_some_and(|entry| !entry.is_ignored) }) .flat_map(move |(path, summaries)| { summaries.iter().map(move |(server_id, summary)| { @@ -9341,9 +9342,7 @@ impl LspStore { let is_disk_based_diagnostics_progress = disk_based_diagnostics_progress_token .as_ref() - .map_or(false, |disk_based_token| { - token.starts_with(disk_based_token) - }); + .is_some_and(|disk_based_token| token.starts_with(disk_based_token)); match progress { lsp::WorkDoneProgress::Begin(report) => { @@ -10676,7 +10675,7 @@ impl LspStore { let is_supporting = diagnostic .related_information .as_ref() - .map_or(false, |infos| { + .is_some_and(|infos| { infos.iter().any(|info| { primary_diagnostic_group_ids.contains_key(&( source, @@ -10689,11 +10688,11 @@ impl LspStore { let is_unnecessary = diagnostic .tags .as_ref() - .map_or(false, |tags| tags.contains(&DiagnosticTag::UNNECESSARY)); + .is_some_and(|tags| tags.contains(&DiagnosticTag::UNNECESSARY)); let underline = self .language_server_adapter_for_id(server_id) - .map_or(true, |adapter| adapter.underline_diagnostic(diagnostic)); + .is_none_or(|adapter| adapter.underline_diagnostic(diagnostic)); if is_supporting { supporting_diagnostics.insert( @@ -10703,7 +10702,7 @@ impl LspStore { } else { let group_id = post_inc(&mut self.as_local_mut().unwrap().next_diagnostic_group_id); let is_disk_based = - source.map_or(false, |source| disk_based_sources.contains(source)); + source.is_some_and(|source| disk_based_sources.contains(source)); sources_by_group_id.insert(group_id, source); primary_diagnostic_group_ids @@ -12409,7 +12408,7 @@ impl TryFrom<&FileOperationFilter> for RenameActionPredicate { ops.pattern .options .as_ref() - .map_or(false, |ops| ops.ignore_case.unwrap_or(false)), + .is_some_and(|ops| ops.ignore_case.unwrap_or(false)), ) .build()? .compile_matcher(), @@ -12424,7 +12423,7 @@ struct RenameActionPredicate { impl RenameActionPredicate { // Returns true if language server should be notified fn eval(&self, path: &str, is_dir: bool) -> bool { - self.kind.as_ref().map_or(true, |kind| { + self.kind.as_ref().is_none_or(|kind| { let expected_kind = if is_dir { FileOperationPatternKind::Folder } else { diff --git a/crates/project/src/manifest_tree.rs b/crates/project/src/manifest_tree.rs index f68905d14c..750815c477 100644 --- a/crates/project/src/manifest_tree.rs +++ b/crates/project/src/manifest_tree.rs @@ -218,10 +218,8 @@ impl ManifestQueryDelegate { impl ManifestDelegate for ManifestQueryDelegate { fn exists(&self, path: &Path, is_dir: Option<bool>) -> bool { - self.worktree.entry_for_path(path).map_or(false, |entry| { - is_dir.map_or(true, |is_required_to_be_dir| { - is_required_to_be_dir == entry.is_dir() - }) + self.worktree.entry_for_path(path).is_some_and(|entry| { + is_dir.is_none_or(|is_required_to_be_dir| is_required_to_be_dir == entry.is_dir()) }) } diff --git a/crates/project/src/manifest_tree/server_tree.rs b/crates/project/src/manifest_tree/server_tree.rs index 7da43feeef..f5fd481324 100644 --- a/crates/project/src/manifest_tree/server_tree.rs +++ b/crates/project/src/manifest_tree/server_tree.rs @@ -314,7 +314,7 @@ impl LanguageServerTree { pub(crate) fn remove_nodes(&mut self, ids: &BTreeSet<LanguageServerId>) { for (_, servers) in &mut self.instances { for (_, nodes) in &mut servers.roots { - nodes.retain(|_, (node, _)| node.id.get().map_or(true, |id| !ids.contains(id))); + nodes.retain(|_, (node, _)| node.id.get().is_none_or(|id| !ids.contains(id))); } } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f825cd8c47..f9ad7b96d3 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1897,7 +1897,7 @@ impl Project { return true; } - return false; + false } pub fn ssh_connection_string(&self, cx: &App) -> Option<SharedString> { @@ -1905,7 +1905,7 @@ impl Project { return Some(ssh_state.read(cx).connection_string().into()); } - return None; + None } pub fn ssh_connection_state(&self, cx: &App) -> Option<remote::ConnectionState> { @@ -4134,7 +4134,7 @@ impl Project { } }) } else { - return Task::ready(None); + Task::ready(None) } } @@ -5187,7 +5187,7 @@ impl<'a> fuzzy::PathMatchCandidateSet<'a> for PathMatchCandidateSet { } fn prefix(&self) -> Arc<str> { - if self.snapshot.root_entry().map_or(false, |e| e.is_file()) { + if self.snapshot.root_entry().is_some_and(|e| e.is_file()) { self.snapshot.root_name().into() } else if self.include_root_name { format!("{}{}", self.snapshot.root_name(), std::path::MAIN_SEPARATOR).into() @@ -5397,7 +5397,7 @@ impl Completion { self.source // `lsp::CompletionListItemDefaults` has `insert_text_format` field .lsp_completion(true) - .map_or(false, |lsp_completion| { + .is_some_and(|lsp_completion| { lsp_completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET) }) } @@ -5453,9 +5453,10 @@ fn provide_inline_values( .collect::<String>(); let point = snapshot.offset_to_point(capture_range.end); - while scopes.last().map_or(false, |scope: &Range<_>| { - !scope.contains(&capture_range.start) - }) { + while scopes + .last() + .is_some_and(|scope: &Range<_>| !scope.contains(&capture_range.start)) + { scopes.pop(); } @@ -5465,7 +5466,7 @@ fn provide_inline_values( let scope = if scopes .last() - .map_or(true, |scope| !scope.contains(&active_debug_line_offset)) + .is_none_or(|scope| !scope.contains(&active_debug_line_offset)) { VariableScope::Global } else { diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 050ca60e7a..a6fea4059c 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -188,9 +188,9 @@ pub struct DiagnosticsSettings { impl DiagnosticsSettings { pub fn fetch_cargo_diagnostics(&self) -> bool { - self.cargo.as_ref().map_or(false, |cargo_diagnostics| { - cargo_diagnostics.fetch_cargo_diagnostics - }) + self.cargo + .as_ref() + .is_some_and(|cargo_diagnostics| cargo_diagnostics.fetch_cargo_diagnostics) } } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 5b3827b42b..5137d64fab 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -2947,9 +2947,10 @@ fn chunks_with_diagnostics<T: ToOffset + ToPoint>( ) -> Vec<(String, Option<DiagnosticSeverity>)> { let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new(); for chunk in buffer.snapshot().chunks(range, true) { - if chunks.last().map_or(false, |prev_chunk| { - prev_chunk.1 == chunk.diagnostic_severity - }) { + if chunks + .last() + .is_some_and(|prev_chunk| prev_chunk.1 == chunk.diagnostic_severity) + { chunks.last_mut().unwrap().0.push_str(chunk.text); } else { chunks.push((chunk.text.to_string(), chunk.diagnostic_severity)); diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 5f98a10c75..212d2dd2d9 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -99,7 +99,7 @@ impl Project { } } - return None; + None } pub fn create_terminal( @@ -518,7 +518,7 @@ impl Project { smol::block_on(fs.metadata(&bin_path)) .ok() .flatten() - .map_or(false, |meta| meta.is_dir) + .is_some_and(|meta| meta.is_dir) }) } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index dd6b081e98..9a87874ed8 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -563,7 +563,7 @@ impl ProjectPanel { if project_panel .edit_state .as_ref() - .map_or(false, |state| state.processing_filename.is_none()) + .is_some_and(|state| state.processing_filename.is_none()) { project_panel.edit_state = None; project_panel.update_visible_entries(None, cx); @@ -3091,7 +3091,7 @@ impl ProjectPanel { entry.id == new_entry_id || { self.ancestors .get(&entry.id) - .map_or(false, |entries| entries.ancestors.contains(&new_entry_id)) + .is_some_and(|entries| entries.ancestors.contains(&new_entry_id)) } } else { false @@ -3974,7 +3974,7 @@ impl ProjectPanel { let is_marked = self.marked_entries.contains(&selection); let is_active = self .selection - .map_or(false, |selection| selection.entry_id == entry_id); + .is_some_and(|selection| selection.entry_id == entry_id); let file_name = details.filename.clone(); @@ -4181,7 +4181,7 @@ impl ProjectPanel { || this .expanded_dir_ids .get(&details.worktree_id) - .map_or(false, |ids| ids.binary_search(&entry_id).is_ok()) + .is_some_and(|ids| ids.binary_search(&entry_id).is_ok()) { return; } @@ -4401,19 +4401,17 @@ impl ProjectPanel { } else { h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted)) } + } else if let Some((icon_name, color)) = + entry_diagnostic_aware_icon_name_and_color(diagnostic_severity) + { + h_flex() + .size(IconSize::default().rems()) + .child(Icon::new(icon_name).color(color).size(IconSize::Small)) } else { - if let Some((icon_name, color)) = - entry_diagnostic_aware_icon_name_and_color(diagnostic_severity) - { - h_flex() - .size(IconSize::default().rems()) - .child(Icon::new(icon_name).color(color).size(IconSize::Small)) - } else { - h_flex() - .size(IconSize::default().rems()) - .invisible() - .flex_none() - } + h_flex() + .size(IconSize::default().rems()) + .invisible() + .flex_none() }) .child( if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) { @@ -4465,7 +4463,7 @@ impl ProjectPanel { ); } else { let is_current_target = this.folded_directory_drag_target - .map_or(false, |target| + .is_some_and(|target| target.entry_id == entry_id && target.index == delimiter_target_index && target.is_delimiter_target @@ -4509,7 +4507,7 @@ impl ProjectPanel { } else { let is_current_target = this.folded_directory_drag_target .as_ref() - .map_or(false, |target| + .is_some_and(|target| target.entry_id == entry_id && target.index == index && !target.is_delimiter_target @@ -4528,7 +4526,7 @@ impl ProjectPanel { this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx); } })) - .when(folded_directory_drag_target.map_or(false, |target| + .when(folded_directory_drag_target.is_some_and(|target| target.entry_id == entry_id && target.index == index ), |this| { @@ -4694,7 +4692,7 @@ impl ProjectPanel { let is_cut = self .clipboard .as_ref() - .map_or(false, |e| e.is_cut() && e.items().contains(&selection)); + .is_some_and(|e| e.is_cut() && e.items().contains(&selection)); EntryDetails { filename, @@ -4892,7 +4890,7 @@ impl ProjectPanel { if skip_ignored && worktree .entry_for_id(entry_id) - .map_or(true, |entry| entry.is_ignored && !entry.is_always_included) + .is_none_or(|entry| entry.is_ignored && !entry.is_always_included) { anyhow::bail!("can't reveal an ignored entry in the project panel"); } @@ -5687,7 +5685,7 @@ impl Panel for ProjectPanel { project.visible_worktrees(cx).any(|tree| { tree.read(cx) .root_entry() - .map_or(false, |entry| entry.is_dir()) + .is_some_and(|entry| entry.is_dir()) }) } diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 9fffbde5f7..9d0f54bc01 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -196,7 +196,7 @@ impl PickerDelegate for ProjectSymbolsDelegate { .partition(|candidate| { project .entry_for_path(&symbols[candidate.id].path, cx) - .map_or(false, |e| !e.is_ignored) + .is_some_and(|e| !e.is_ignored) }); delegate.visible_match_candidates = visible_match_candidates; diff --git a/crates/prompt_store/src/prompt_store.rs b/crates/prompt_store/src/prompt_store.rs index 06a65b97cd..fb087ce34d 100644 --- a/crates/prompt_store/src/prompt_store.rs +++ b/crates/prompt_store/src/prompt_store.rs @@ -247,9 +247,7 @@ impl PromptStore { if metadata_db .get(&txn, &prompt_id_v2)? - .map_or(true, |metadata_v2| { - metadata_v1.saved_at > metadata_v2.saved_at - }) + .is_none_or(|metadata_v2| metadata_v1.saved_at > metadata_v2.saved_at) { metadata_db.put( &mut txn, diff --git a/crates/prompt_store/src/prompts.rs b/crates/prompt_store/src/prompts.rs index 526d2c6a34..cd34bafb20 100644 --- a/crates/prompt_store/src/prompts.rs +++ b/crates/prompt_store/src/prompts.rs @@ -286,7 +286,7 @@ impl PromptBuilder { break; } for event in changed_paths { - if event.path.starts_with(&templates_dir) && event.path.extension().map_or(false, |ext| ext == "hbs") { + if event.path.starts_with(&templates_dir) && event.path.extension().is_some_and(|ext| ext == "hbs") { log::info!("Reloading prompt template override: {}", event.path.display()); if let Some(content) = params.fs.load(&event.path).await.log_err() { let file_name = event.path.file_stem().unwrap().to_string_lossy(); diff --git a/crates/recent_projects/src/disconnected_overlay.rs b/crates/recent_projects/src/disconnected_overlay.rs index a6cd26355c..9b79d3ce9c 100644 --- a/crates/recent_projects/src/disconnected_overlay.rs +++ b/crates/recent_projects/src/disconnected_overlay.rs @@ -37,7 +37,7 @@ impl ModalView for DisconnectedOverlay { _window: &mut Window, _: &mut Context<Self>, ) -> workspace::DismissDecision { - return workspace::DismissDecision::Dismiss(self.finished); + workspace::DismissDecision::Dismiss(self.finished) } fn fade_out_background(&self) -> bool { true diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 0fd6d5af8c..0f43d83d86 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -1410,7 +1410,7 @@ impl RemoteServerProjects { if ssh_settings .ssh_connections .as_ref() - .map_or(false, |connections| { + .is_some_and(|connections| { state .servers .iter() diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index 7b58792178..670fcb4800 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -436,7 +436,7 @@ impl ModalView for SshConnectionModal { _window: &mut Window, _: &mut Context<Self>, ) -> workspace::DismissDecision { - return workspace::DismissDecision::Dismiss(self.finished); + workspace::DismissDecision::Dismiss(self.finished) } fn fade_out_background(&self) -> bool { diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 2180fbb5ee..abde2d7568 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -1119,7 +1119,7 @@ impl SshRemoteClient { } fn state_is(&self, check: impl FnOnce(&State) -> bool) -> bool { - self.state.lock().as_ref().map_or(false, check) + self.state.lock().as_ref().is_some_and(check) } fn try_set_state(&self, cx: &mut Context<Self>, map: impl FnOnce(&State) -> Option<State>) { @@ -1870,7 +1870,7 @@ impl SshRemoteConnection { .await?; self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx) .await?; - return Ok(dst_path); + Ok(dst_path) } async fn download_binary_on_server( diff --git a/crates/repl/src/components/kernel_options.rs b/crates/repl/src/components/kernel_options.rs index cd73783b4c..b8fd2e57f2 100644 --- a/crates/repl/src/components/kernel_options.rs +++ b/crates/repl/src/components/kernel_options.rs @@ -126,7 +126,7 @@ impl PickerDelegate for KernelPickerDelegate { .collect() }; - return Task::ready(()); + Task::ready(()) } fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) { diff --git a/crates/repl/src/repl_editor.rs b/crates/repl/src/repl_editor.rs index f5dd659597..e97223ceb9 100644 --- a/crates/repl/src/repl_editor.rs +++ b/crates/repl/src/repl_editor.rs @@ -434,7 +434,7 @@ fn runnable_ranges( if start_language .zip(end_language) - .map_or(false, |(start, end)| start == end) + .is_some_and(|(start, end)| start == end) { (vec![snippet_range], None) } else { diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 78ce6f78a2..0d3f5abbde 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -35,7 +35,7 @@ impl Rope { && (self .chunks .last() - .map_or(false, |c| c.text.len() < chunk::MIN_BASE) + .is_some_and(|c| c.text.len() < chunk::MIN_BASE) || chunk.text.len() < chunk::MIN_BASE) { self.push_chunk(chunk.as_slice()); @@ -816,7 +816,7 @@ impl<'a> Chunks<'a> { } } - return true; + true } } diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index ec83993e5f..355deb5d20 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -703,9 +703,7 @@ impl RulesLibrary { .delegate .matches .get(picker.delegate.selected_index()) - .map_or(true, |old_selected_prompt| { - old_selected_prompt.id != prompt_id - }) + .is_none_or(|old_selected_prompt| old_selected_prompt.id != prompt_id) && let Some(ix) = picker .delegate .matches diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index e95617512d..ae3f42853a 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -653,7 +653,7 @@ impl KeymapFile { let is_only_binding = keymap.0[index] .bindings .as_ref() - .map_or(true, |bindings| bindings.len() == 1); + .is_none_or(|bindings| bindings.len() == 1); let key_path: &[&str] = if is_only_binding { &[] } else { @@ -703,7 +703,7 @@ impl KeymapFile { } else if keymap.0[index] .bindings .as_ref() - .map_or(true, |bindings| bindings.len() == 1) + .is_none_or(|bindings| bindings.len() == 1) { // if we are replacing the only binding in the section, // just update the section in place, updating the context @@ -1056,10 +1056,10 @@ mod tests { #[track_caller] fn parse_keystrokes(keystrokes: &str) -> Vec<Keystroke> { - return keystrokes + keystrokes .split(' ') .map(|s| Keystroke::parse(s).expect("Keystrokes valid")) - .collect(); + .collect() } #[test] diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs index 8e7e11dc82..c102b303c1 100644 --- a/crates/settings/src/settings_json.rs +++ b/crates/settings/src/settings_json.rs @@ -72,7 +72,7 @@ pub fn update_value_in_json_text<'a>( } } else if key_path .last() - .map_or(false, |key| preserved_keys.contains(key)) + .is_some_and(|key| preserved_keys.contains(key)) || old_value != new_value { let mut new_value = new_value.clone(); @@ -384,7 +384,7 @@ pub fn replace_top_level_array_value_in_json_text( remove_range.start = cursor.node().range().start_byte; } } - return Ok((remove_range, String::new())); + Ok((remove_range, String::new())) } else { let (mut replace_range, mut replace_value) = replace_value_in_json_text(value_str, key_path, tab_size, new_value, replace_key); @@ -405,7 +405,7 @@ pub fn replace_top_level_array_value_in_json_text( } } - return Ok((replace_range, replace_value)); + Ok((replace_range, replace_value)) } } @@ -527,7 +527,7 @@ pub fn append_top_level_array_value_in_json_text( let descendant_index = cursor.descendant_index(); let res = cursor.goto_first_child() && cursor.node().kind() == kind; cursor.goto_descendant(descendant_index); - return res; + res } } diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 23f495d850..211db46c6c 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -1233,8 +1233,7 @@ impl SettingsStore { // If a local settings file changed, then avoid recomputing local // settings for any path outside of that directory. - if changed_local_path.map_or( - false, + if changed_local_path.is_some_and( |(changed_root_id, changed_local_path)| { *root_id != changed_root_id || !directory_path.starts_with(changed_local_path) diff --git a/crates/settings_profile_selector/src/settings_profile_selector.rs b/crates/settings_profile_selector/src/settings_profile_selector.rs index 8a34c12051..25be67bfd7 100644 --- a/crates/settings_profile_selector/src/settings_profile_selector.rs +++ b/crates/settings_profile_selector/src/settings_profile_selector.rs @@ -126,7 +126,7 @@ impl SettingsProfileSelectorDelegate { ) -> Option<String> { let mat = self.matches.get(self.selected_index)?; let profile_name = self.profile_names.get(mat.candidate_id)?; - return Self::update_active_profile_name_global(profile_name.clone(), cx); + Self::update_active_profile_name_global(profile_name.clone(), cx) } fn update_active_profile_name_global( diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index b8c52602a6..457d58e5a7 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -553,7 +553,7 @@ impl KeymapEditor { if exact_match { keystrokes_match_exactly(&keystroke_query, keystrokes) } else if keystroke_query.len() > keystrokes.len() { - return false; + false } else { for keystroke_offset in 0..keystrokes.len() { let mut found_count = 0; @@ -568,12 +568,9 @@ impl KeymapEditor { query.modifiers.is_subset_of(&keystroke.modifiers) && ((query.key.is_empty() || query.key == keystroke.key) - && query - .key_char - .as_ref() - .map_or(true, |q_kc| { - q_kc == &keystroke.key - })); + && query.key_char.as_ref().is_none_or( + |q_kc| q_kc == &keystroke.key, + )); if matches { found_count += 1; query_cursor += 1; @@ -585,7 +582,7 @@ impl KeymapEditor { return true; } } - return false; + false } }) }); @@ -2715,7 +2712,7 @@ impl ActionArgumentsEditor { }) .ok(); } - return result; + result }) .detach_and_log_err(cx); Self { @@ -2818,7 +2815,7 @@ impl Render for ActionArgumentsEditor { self.editor .update(cx, |editor, _| editor.set_text_style_refinement(text_style)); - return v_flex().w_full().child( + v_flex().w_full().child( h_flex() .min_h_8() .min_w_48() @@ -2831,7 +2828,7 @@ impl Render for ActionArgumentsEditor { .border_color(border_color) .track_focus(&self.focus_handle) .child(self.editor.clone()), - ); + ) } } @@ -2889,9 +2886,9 @@ impl CompletionProvider for KeyContextCompletionProvider { _menu_is_open: bool, _cx: &mut Context<Editor>, ) -> bool { - text.chars().last().map_or(false, |last_char| { - last_char.is_ascii_alphanumeric() || last_char == '_' - }) + text.chars() + .last() + .is_some_and(|last_char| last_char.is_ascii_alphanumeric() || last_char == '_') } } @@ -2910,7 +2907,7 @@ async fn load_json_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) Some(task) => task.await.context("Failed to load JSON language").log_err(), None => None, }; - return json_language.unwrap_or_else(|| { + json_language.unwrap_or_else(|| { Arc::new(Language::new( LanguageConfig { name: "JSON".into(), @@ -2918,7 +2915,7 @@ async fn load_json_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) }, Some(tree_sitter_json::LANGUAGE.into()), )) - }); + }) } async fn load_keybind_context_language( @@ -2942,7 +2939,7 @@ async fn load_keybind_context_language( .log_err(), None => None, }; - return language.unwrap_or_else(|| { + language.unwrap_or_else(|| { Arc::new(Language::new( LanguageConfig { name: "Zed Keybind Context".into(), @@ -2950,7 +2947,7 @@ async fn load_keybind_context_language( }, Some(tree_sitter_rust::LANGUAGE.into()), )) - }); + }) } async fn save_keybinding_update( @@ -3130,7 +3127,7 @@ fn collect_contexts_from_assets() -> Vec<SharedString> { let mut contexts = contexts.into_iter().collect::<Vec<_>>(); contexts.sort(); - return contexts; + contexts } impl SerializableItem for KeymapEditor { diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/settings_ui/src/ui_components/keystroke_input.rs index de133d406b..66593524a3 100644 --- a/crates/settings_ui/src/ui_components/keystroke_input.rs +++ b/crates/settings_ui/src/ui_components/keystroke_input.rs @@ -116,19 +116,19 @@ impl KeystrokeInput { && self .keystrokes .last() - .map_or(false, |last| last.key.is_empty()) + .is_some_and(|last| last.key.is_empty()) { return &self.keystrokes[..self.keystrokes.len() - 1]; } - return &self.keystrokes; + &self.keystrokes } fn dummy(modifiers: Modifiers) -> Keystroke { - return Keystroke { + Keystroke { modifiers, key: "".to_string(), key_char: None, - }; + } } fn keystrokes_changed(&self, cx: &mut Context<Self>) { @@ -182,7 +182,7 @@ impl KeystrokeInput { fn end_close_keystrokes_capture(&mut self) -> Option<usize> { self.close_keystrokes.take(); self.clear_close_keystrokes_timer.take(); - return self.close_keystrokes_start.take(); + self.close_keystrokes_start.take() } fn handle_possible_close_keystroke( @@ -233,7 +233,7 @@ impl KeystrokeInput { return CloseKeystrokeResult::Partial; } self.end_close_keystrokes_capture(); - return CloseKeystrokeResult::None; + CloseKeystrokeResult::None } fn on_modifiers_changed( @@ -437,7 +437,7 @@ impl KeystrokeInput { // is a much more reliable check, as the intercept keystroke handlers are installed // on focus of the inner focus handle, thereby ensuring our recording state does // not get de-synced - return self.inner_focus_handle.is_focused(window); + self.inner_focus_handle.is_focused(window) } } @@ -934,7 +934,7 @@ mod tests { let change_tracker = KeystrokeUpdateTracker::new(self.input.clone(), &mut self.cx); let result = self.input.update_in(&mut self.cx, cb); KeystrokeUpdateTracker::finish(change_tracker, &self.cx); - return result; + result } } diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index 66dd636d21..a91d497572 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/src/ui_components/table.rs @@ -731,7 +731,7 @@ impl<const COLS: usize> ColumnWidths<COLS> { } widths[col_idx] = widths[col_idx] + (diff - diff_remaining); - return diff_remaining; + diff_remaining } } diff --git a/crates/snippet/src/snippet.rs b/crates/snippet/src/snippet.rs index 6a673fe08b..4be4281d9a 100644 --- a/crates/snippet/src/snippet.rs +++ b/crates/snippet/src/snippet.rs @@ -33,7 +33,7 @@ impl Snippet { choices: None, }; - if !tabstops.last().map_or(false, |t| *t == end_tabstop) { + if !tabstops.last().is_some_and(|t| *t == end_tabstop) { tabstops.push(end_tabstop); } } diff --git a/crates/snippet_provider/src/lib.rs b/crates/snippet_provider/src/lib.rs index c8d2555df2..eac06924a7 100644 --- a/crates/snippet_provider/src/lib.rs +++ b/crates/snippet_provider/src/lib.rs @@ -71,16 +71,16 @@ async fn process_updates( ) -> Result<()> { let fs = this.read_with(&cx, |this, _| this.fs.clone())?; for entry_path in entries { - if !entry_path + if entry_path .extension() - .map_or(false, |extension| extension == "json") + .is_none_or(|extension| extension != "json") { continue; } let entry_metadata = fs.metadata(&entry_path).await; // Entry could have been removed, in which case we should no longer show completions for it. let entry_exists = entry_metadata.is_ok(); - if entry_metadata.map_or(false, |entry| entry.map_or(false, |e| e.is_dir)) { + if entry_metadata.is_ok_and(|entry| entry.is_some_and(|e| e.is_dir)) { // Don't process dirs. continue; } diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index f551bb32e6..710fdd4fbf 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -94,9 +94,7 @@ impl<'a, S: Summary, D: Dimension<'a, S> + Ord> SeekTarget<'a, S, D> for D { } impl<'a, T: Summary> Dimension<'a, T> for () { - fn zero(_: &T::Context) -> Self { - () - } + fn zero(_: &T::Context) -> Self {} fn add_summary(&mut self, _: &'a T, _: &T::Context) {} } @@ -728,7 +726,7 @@ impl<T: KeyedItem> SumTree<T> { if old_item .as_ref() - .map_or(false, |old_item| old_item.key() < new_key) + .is_some_and(|old_item| old_item.key() < new_key) { new_tree.extend(buffered_items.drain(..), cx); let slice = cursor.slice(&new_key, Bias::Left); diff --git a/crates/supermaven/src/supermaven.rs b/crates/supermaven/src/supermaven.rs index a31b96d882..743c0d4c7d 100644 --- a/crates/supermaven/src/supermaven.rs +++ b/crates/supermaven/src/supermaven.rs @@ -243,7 +243,7 @@ fn find_relevant_completion<'a>( None => continue 'completions, }; - if best_completion.map_or(false, |best| best.len() > trimmed_completion.len()) { + if best_completion.is_some_and(|best| best.len() > trimmed_completion.len()) { continue; } diff --git a/crates/supermaven_api/src/supermaven_api.rs b/crates/supermaven_api/src/supermaven_api.rs index 61d14d5dc7..c4b1409d64 100644 --- a/crates/supermaven_api/src/supermaven_api.rs +++ b/crates/supermaven_api/src/supermaven_api.rs @@ -221,9 +221,7 @@ pub fn version_path(version: u64) -> PathBuf { } pub async fn has_version(version_path: &Path) -> bool { - fs::metadata(version_path) - .await - .map_or(false, |m| m.is_file()) + fs::metadata(version_path).await.is_ok_and(|m| m.is_file()) } pub async fn get_supermaven_agent_path(client: Arc<dyn HttpClient>) -> Result<PathBuf> { diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index 90e6ea8878..dae366a979 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -283,7 +283,7 @@ pub fn task_contexts( .project() .read(cx) .worktree_for_id(*worktree_id, cx) - .map_or(false, |worktree| is_visible_directory(&worktree, cx)) + .is_some_and(|worktree| is_visible_directory(&worktree, cx)) }) .or_else(|| { workspace @@ -372,7 +372,7 @@ pub fn task_contexts( fn is_visible_directory(worktree: &Entity<Worktree>, cx: &App) -> bool { let worktree = worktree.read(cx); - worktree.is_visible() && worktree.root_entry().map_or(false, |entry| entry.is_dir()) + worktree.is_visible() && worktree.root_entry().is_some_and(|entry| entry.is_dir()) } fn worktree_context(worktree_abs_path: &Path) -> TaskContext { diff --git a/crates/telemetry/src/telemetry.rs b/crates/telemetry/src/telemetry.rs index f8f7d5851e..ac43457c33 100644 --- a/crates/telemetry/src/telemetry.rs +++ b/crates/telemetry/src/telemetry.rs @@ -55,7 +55,6 @@ macro_rules! serialize_property { pub fn send_event(event: Event) { if let Some(queue) = TELEMETRY_QUEUE.get() { queue.unbounded_send(event).ok(); - return; } } diff --git a/crates/terminal/src/pty_info.rs b/crates/terminal/src/pty_info.rs index 802470493c..a1a559051a 100644 --- a/crates/terminal/src/pty_info.rs +++ b/crates/terminal/src/pty_info.rs @@ -122,7 +122,7 @@ impl PtyProcessInfo { } pub(crate) fn kill_current_process(&mut self) -> bool { - self.refresh().map_or(false, |process| process.kill()) + self.refresh().is_some_and(|process| process.kill()) } fn load(&mut self) -> Option<ProcessInfo> { diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 42b3694789..16c1efabba 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1299,23 +1299,19 @@ impl Terminal { let selection = Selection::new(selection_type, point, side); self.events .push_back(InternalEvent::SetSelection(Some((selection, point)))); - return; } "escape" => { self.events.push_back(InternalEvent::SetSelection(None)); - return; } "y" => { self.copy(Some(false)); - return; } "i" => { self.scroll_to_bottom(); self.toggle_vi_mode(); - return; } _ => {} } @@ -1891,11 +1887,11 @@ impl Terminal { let e: Option<ExitStatus> = error_code.map(|code| { #[cfg(unix)] { - return std::os::unix::process::ExitStatusExt::from_raw(code); + std::os::unix::process::ExitStatusExt::from_raw(code) } #[cfg(windows)] { - return std::os::windows::process::ExitStatusExt::from_raw(code as u32); + std::os::windows::process::ExitStatusExt::from_raw(code as u32) } }); diff --git a/crates/terminal/src/terminal_hyperlinks.rs b/crates/terminal/src/terminal_hyperlinks.rs index e318ae21bd..9f565bd306 100644 --- a/crates/terminal/src/terminal_hyperlinks.rs +++ b/crates/terminal/src/terminal_hyperlinks.rs @@ -124,12 +124,12 @@ pub(super) fn find_from_grid_point<T: EventListener>( && file_path .chars() .nth(last_index - 1) - .map_or(false, |c| c.is_ascii_digit()); + .is_some_and(|c| c.is_ascii_digit()); let next_is_digit = last_index < file_path.len() - 1 && file_path .chars() .nth(last_index + 1) - .map_or(true, |c| c.is_ascii_digit()); + .is_none_or(|c| c.is_ascii_digit()); if prev_is_digit && !next_is_digit { let stripped_len = file_path.len() - last_index; word_match = Match::new( diff --git a/crates/terminal_view/src/color_contrast.rs b/crates/terminal_view/src/color_contrast.rs index fe4a881cea..522dca3e91 100644 --- a/crates/terminal_view/src/color_contrast.rs +++ b/crates/terminal_view/src/color_contrast.rs @@ -235,12 +235,10 @@ fn adjust_lightness_for_contrast( } else { high = mid; } + } else if should_go_darker { + high = mid; } else { - if should_go_darker { - high = mid; - } else { - low = mid; - } + low = mid; } // If we're close enough to the target, stop diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 7575706db0..1c38dbc877 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1478,7 +1478,7 @@ pub fn is_blank(cell: &IndexedCell) -> bool { return false; } - return true; + true } fn to_highlighted_range_lines( diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index b161a8ea89..c50e2bd3a7 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -350,12 +350,10 @@ impl TerminalPanel { pane.set_zoomed(false, cx); }); cx.emit(PanelEvent::Close); - } else { - if let Some(focus_on_pane) = - focus_on_pane.as_ref().or_else(|| self.center.panes().pop()) - { - focus_on_pane.focus_handle(cx).focus(window); - } + } else if let Some(focus_on_pane) = + focus_on_pane.as_ref().or_else(|| self.center.panes().pop()) + { + focus_on_pane.focus_handle(cx).focus(window); } } pane::Event::ZoomIn => { @@ -896,9 +894,9 @@ impl TerminalPanel { } fn is_enabled(&self, cx: &App) -> bool { - self.workspace.upgrade().map_or(false, |workspace| { - is_enabled_in_workspace(workspace.read(cx), cx) - }) + self.workspace + .upgrade() + .is_some_and(|workspace| is_enabled_in_workspace(workspace.read(cx), cx)) } fn activate_pane_in_direction( @@ -1242,20 +1240,18 @@ impl Render for TerminalPanel { let panes = terminal_panel.center.panes(); if let Some(&pane) = panes.get(action.0) { window.focus(&pane.read(cx).focus_handle(cx)); - } else { - if let Some(new_pane) = - terminal_panel.new_pane_with_cloned_active_terminal(window, cx) - { - terminal_panel - .center - .split( - &terminal_panel.active_pane, - &new_pane, - SplitDirection::Right, - ) - .log_err(); - window.focus(&new_pane.focus_handle(cx)); - } + } else if let Some(new_pane) = + terminal_panel.new_pane_with_cloned_active_terminal(window, cx) + { + terminal_panel + .center + .split( + &terminal_panel.active_pane, + &new_pane, + SplitDirection::Right, + ) + .log_err(); + window.focus(&new_pane.focus_handle(cx)); } }), ) diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 14b642bc12..f434e46159 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -385,9 +385,7 @@ impl TerminalView { .workspace .upgrade() .and_then(|workspace| workspace.read(cx).panel::<TerminalPanel>(cx)) - .map_or(false, |terminal_panel| { - terminal_panel.read(cx).assistant_enabled() - }); + .is_some_and(|terminal_panel| terminal_panel.read(cx).assistant_enabled()); let context_menu = ContextMenu::build(window, cx, |menu, _, _| { menu.context(self.focus_handle.clone()) .action("New Terminal", Box::new(NewTerminal)) diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index c4778216e0..becc5d9c0a 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -108,7 +108,7 @@ impl Anchor { fragment_cursor.seek(&Some(fragment_id), Bias::Left); fragment_cursor .item() - .map_or(false, |fragment| fragment.visible) + .is_some_and(|fragment| fragment.visible) } } } diff --git a/crates/text/src/patch.rs b/crates/text/src/patch.rs index 96fed17571..dcb35e9a92 100644 --- a/crates/text/src/patch.rs +++ b/crates/text/src/patch.rs @@ -57,7 +57,7 @@ where // Push the old edit if its new end is before the new edit's old start. if let Some(old_edit) = old_edit.as_ref() { let new_edit = new_edit.as_ref(); - if new_edit.map_or(true, |new_edit| old_edit.new.end < new_edit.old.start) { + if new_edit.is_none_or(|new_edit| old_edit.new.end < new_edit.old.start) { let catchup = old_edit.old.start - old_start; old_start += catchup; new_start += catchup; @@ -78,7 +78,7 @@ where // Push the new edit if its old end is before the old edit's new start. if let Some(new_edit) = new_edit.as_ref() { let old_edit = old_edit.as_ref(); - if old_edit.map_or(true, |old_edit| new_edit.old.end < old_edit.new.start) { + if old_edit.is_none_or(|old_edit| new_edit.old.end < old_edit.new.start) { let catchup = new_edit.new.start - new_start; old_start += catchup; new_start += catchup; diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 8e37567738..705d3f1788 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1149,7 +1149,7 @@ impl Buffer { // Insert the new text before any existing fragments within the range. if !new_text.is_empty() { let mut old_start = old_fragments.start().1; - if old_fragments.item().map_or(false, |f| f.visible) { + if old_fragments.item().is_some_and(|f| f.visible) { old_start += fragment_start.0 - old_fragments.start().0.full_offset().0; } let new_start = new_fragments.summary().text.visible; @@ -1834,7 +1834,7 @@ impl Buffer { let mut edits: Vec<(Range<usize>, Arc<str>)> = Vec::new(); let mut last_end = None; for _ in 0..edit_count { - if last_end.map_or(false, |last_end| last_end >= self.len()) { + if last_end.is_some_and(|last_end| last_end >= self.len()) { break; } let new_start = last_end.map_or(0, |last_end| last_end + 1); @@ -2671,7 +2671,7 @@ impl<D: TextDimension + Ord, F: FnMut(&FragmentSummary) -> bool> Iterator for Ed if pending_edit .as_ref() - .map_or(false, |(change, _)| change.new.end < self.new_end) + .is_some_and(|(change, _)| change.new.end < self.new_end) { break; } diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index c2171d3899..275f47912a 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -189,7 +189,7 @@ impl TitleBar { .as_ref()? .read(cx) .is_being_followed(collaborator.peer_id); - let is_present = project_id.map_or(false, |project_id| { + let is_present = project_id.is_some_and(|project_id| { collaborator.location == ParticipantLocation::SharedProject { project_id } }); diff --git a/crates/title_bar/src/onboarding_banner.rs b/crates/title_bar/src/onboarding_banner.rs index e7cf0cd2d9..ed43c5277a 100644 --- a/crates/title_bar/src/onboarding_banner.rs +++ b/crates/title_bar/src/onboarding_banner.rs @@ -73,7 +73,7 @@ fn get_dismissed(source: &str) -> bool { db::kvp::KEY_VALUE_STORE .read_kvp(&dismissed_at) .log_err() - .map_or(false, |dismissed| dismissed.is_some()) + .is_some_and(|dismissed| dismissed.is_some()) } fn persist_dismissed(source: &str, cx: &mut App) { diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index f77eea4bdc..439b53f038 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -93,16 +93,16 @@ impl<M: ManagedView> PopoverMenuHandle<M> { self.0 .borrow() .as_ref() - .map_or(false, |state| state.menu.borrow().as_ref().is_some()) + .is_some_and(|state| state.menu.borrow().as_ref().is_some()) } pub fn is_focused(&self, window: &Window, cx: &App) -> bool { - self.0.borrow().as_ref().map_or(false, |state| { + self.0.borrow().as_ref().is_some_and(|state| { state .menu .borrow() .as_ref() - .map_or(false, |model| model.focus_handle(cx).is_focused(window)) + .is_some_and(|model| model.focus_handle(cx).is_focused(window)) }) } diff --git a/crates/ui/src/components/sticky_items.rs b/crates/ui/src/components/sticky_items.rs index ca8b336a5a..c3e0886404 100644 --- a/crates/ui/src/components/sticky_items.rs +++ b/crates/ui/src/components/sticky_items.rs @@ -105,7 +105,6 @@ impl Element for StickyItemsElement { _window: &mut Window, _cx: &mut App, ) -> Self::PrepaintState { - () } fn paint( diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 211831125d..292ec4874c 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -1215,11 +1215,11 @@ mod tests { // Verify iterators advanced correctly assert!( - !a_iter.next().map_or(false, |c| c.is_ascii_digit()), + !a_iter.next().is_some_and(|c| c.is_ascii_digit()), "Iterator a should have consumed all digits" ); assert!( - !b_iter.next().map_or(false, |c| c.is_ascii_digit()), + !b_iter.next().is_some_and(|c| c.is_ascii_digit()), "Iterator b should have consumed all digits" ); diff --git a/crates/util/src/size.rs b/crates/util/src/size.rs index 084a0e5a56..c6ecebd548 100644 --- a/crates/util/src/size.rs +++ b/crates/util/src/size.rs @@ -7,14 +7,12 @@ pub fn format_file_size(size: u64, use_decimal: bool) -> String { } else { format!("{:.1}MB", size as f64 / (1000.0 * 1000.0)) } + } else if size < 1024 { + format!("{size}B") + } else if size < 1024 * 1024 { + format!("{:.1}KiB", size as f64 / 1024.0) } else { - if size < 1024 { - format!("{size}B") - } else if size < 1024 * 1024 { - format!("{:.1}KiB", size as f64 / 1024.0) - } else { - format!("{:.1}MiB", size as f64 / (1024.0 * 1024.0)) - } + format!("{:.1}MiB", size as f64 / (1024.0 * 1024.0)) } } diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 187678f8af..69a2c88706 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -301,7 +301,7 @@ pub fn get_shell_safe_zed_path() -> anyhow::Result<String> { let zed_path_escaped = shlex::try_quote(&zed_path).context("Failed to shell-escape Zed executable path.")?; - return Ok(zed_path_escaped.to_string()); + Ok(zed_path_escaped.to_string()) } #[cfg(unix)] @@ -825,7 +825,7 @@ mod rng { pub fn new(rng: T) -> Self { Self { rng, - simple_text: std::env::var("SIMPLE_TEXT").map_or(false, |v| !v.is_empty()), + simple_text: std::env::var("SIMPLE_TEXT").is_ok_and(|v| !v.is_empty()), } } diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 00d3bde750..7269fc8bec 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -566,7 +566,6 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) { workspace.update(cx, |workspace, cx| { e.notify_err(workspace, cx); }); - return; } }); @@ -1444,7 +1443,7 @@ pub fn command_interceptor(mut input: &str, cx: &App) -> Vec<CommandInterceptRes }]; } } - return Vec::default(); + Vec::default() } fn generate_positions(string: &str, query: &str) -> Vec<usize> { diff --git a/crates/vim/src/digraph.rs b/crates/vim/src/digraph.rs index beb3bd54ba..248047bb55 100644 --- a/crates/vim/src/digraph.rs +++ b/crates/vim/src/digraph.rs @@ -103,7 +103,6 @@ impl Vim { window.dispatch_keystroke(keystroke, cx); }); } - return; } pub fn handle_literal_input( diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 3cc9772d42..e2ce54b994 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -47,7 +47,6 @@ impl Vim { } self.stop_recording_immediately(action.boxed_clone(), cx); self.switch_mode(Mode::HelixNormal, false, window, cx); - return; } pub fn helix_normal_motion( diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index e703b18117..92e3c97265 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -2375,7 +2375,7 @@ fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoin } } - return None; + None } fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint { @@ -2517,7 +2517,7 @@ fn unmatched_forward( } display_point = new_point; } - return display_point; + display_point } fn unmatched_backward( diff --git a/crates/vim/src/normal/mark.rs b/crates/vim/src/normal/mark.rs index 80d94def05..619769d41a 100644 --- a/crates/vim/src/normal/mark.rs +++ b/crates/vim/src/normal/mark.rs @@ -120,7 +120,6 @@ impl Vim { }); }) }); - return; } fn open_path_mark( diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 4054c552ae..4fbeec7236 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -224,7 +224,7 @@ impl Vim { .search .prior_selections .last() - .map_or(true, |range| range.start != new_head); + .is_none_or(|range| range.start != new_head); if is_different_head { count = count.saturating_sub(1) diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index db19562f02..81efcef17a 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -606,11 +606,11 @@ impl MarksState { match target? { MarkLocation::Buffer(entity_id) => { let anchors = self.multibuffer_marks.get(entity_id)?; - return Some(Mark::Buffer(*entity_id, anchors.get(name)?.clone())); + Some(Mark::Buffer(*entity_id, anchors.get(name)?.clone())) } MarkLocation::Path(path) => { let points = self.serialized_marks.get(path)?; - return Some(Mark::Path(path.clone(), points.get(name)?.clone())); + Some(Mark::Path(path.clone(), points.get(name)?.clone())) } } } diff --git a/crates/web_search_providers/src/web_search_providers.rs b/crates/web_search_providers/src/web_search_providers.rs index 2248cb7eb3..7f8a5f3fa4 100644 --- a/crates/web_search_providers/src/web_search_providers.rs +++ b/crates/web_search_providers/src/web_search_providers.rs @@ -46,7 +46,7 @@ fn register_zed_web_search_provider( let using_zed_provider = language_model_registry .read(cx) .default_model() - .map_or(false, |default| default.is_provided_by_zed()); + .is_some_and(|default| default.is_provided_by_zed()); if using_zed_provider { registry.register_provider(cloud::CloudWebSearchProvider::new(client, cx), cx) } else { diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 079f66ae9d..1d9170684e 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -460,7 +460,7 @@ impl Dock { }; let was_visible = this.is_open() - && this.visible_panel().map_or(false, |active_panel| { + && this.visible_panel().is_some_and(|active_panel| { active_panel.panel_id() == Entity::entity_id(&panel) }); @@ -523,7 +523,7 @@ impl Dock { PanelEvent::Close => { if this .visible_panel() - .map_or(false, |p| p.panel_id() == Entity::entity_id(panel)) + .is_some_and(|p| p.panel_id() == Entity::entity_id(panel)) { this.set_open(false, window, cx); } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 014af7b0bc..5a497398f9 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -489,7 +489,7 @@ where fn should_serialize(&self, event: &dyn Any, cx: &App) -> bool { event .downcast_ref::<T::Event>() - .map_or(false, |event| self.read(cx).should_serialize(event)) + .is_some_and(|event| self.read(cx).should_serialize(event)) } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index a1affc5362..d42b59f08e 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -552,9 +552,9 @@ impl Pane { // to the item, and `focus_handle.contains_focus` returns false because the `active_item` // is not hooked up to us in the dispatch tree. self.focus_handle.contains_focused(window, cx) - || self.active_item().map_or(false, |item| { - item.item_focus_handle(cx).contains_focused(window, cx) - }) + || self + .active_item() + .is_some_and(|item| item.item_focus_handle(cx).contains_focused(window, cx)) } fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) { @@ -1021,7 +1021,7 @@ impl Pane { existing_item .project_entry_ids(cx) .first() - .map_or(false, |existing_entry_id| { + .is_some_and(|existing_entry_id| { Some(existing_entry_id) == project_entry_id.as_ref() }) } else { @@ -1558,7 +1558,7 @@ impl Pane { let other_project_item_ids = open_item.project_item_model_ids(cx); dirty_project_item_ids.retain(|id| !other_project_item_ids.contains(id)); } - return dirty_project_item_ids.is_empty(); + dirty_project_item_ids.is_empty() } pub(super) fn file_names_for_prompt( @@ -2745,7 +2745,7 @@ impl Pane { worktree .read(cx) .root_entry() - .map_or(false, |entry| entry.is_dir()) + .is_some_and(|entry| entry.is_dir()) }); let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx); @@ -3210,8 +3210,7 @@ impl Pane { return; }; - if target.map_or(false, |target| this.is_tab_pinned(target)) - { + if target.is_some_and(|target| this.is_tab_pinned(target)) { this.pin_tab_at(index, window, cx); } }) @@ -3615,7 +3614,6 @@ impl Render for Pane { ) .on_action(cx.listener(|_, _: &menu::Cancel, window, cx| { if cx.stop_active_drag(window) { - return; } else { cx.propagate(); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 4a22107c42..9dac340b5c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1804,7 +1804,7 @@ impl Workspace { .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id)) }); - latest_project_path_opened.map_or(true, |path| path == history_path) + latest_project_path_opened.is_none_or(|path| path == history_path) }) } @@ -2284,7 +2284,7 @@ impl Workspace { // the current session. if close_intent != CloseIntent::Quit && !save_last_workspace - && save_result.as_ref().map_or(false, |&res| res) + && save_result.as_ref().is_ok_and(|&res| res) { this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx))? .await; @@ -5133,13 +5133,11 @@ impl Workspace { self.panes.retain(|p| p != pane); if let Some(focus_on) = focus_on { focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))); - } else { - if self.active_pane() == pane { - self.panes - .last() - .unwrap() - .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))); - } + } else if self.active_pane() == pane { + self.panes + .last() + .unwrap() + .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))); } if self.last_active_center_pane == Some(pane.downgrade()) { self.last_active_center_pane = None; @@ -5893,7 +5891,6 @@ impl Workspace { pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) { if cx.stop_active_drag(window) { - return; } else if let Some((notification_id, _)) = self.notifications.pop() { dismiss_app_notification(¬ification_id, cx); } else { @@ -6100,7 +6097,7 @@ fn open_items( // here is a directory, it was already opened further above // with a `find_or_create_worktree`. if let Ok(task) = abs_path_task - && task.await.map_or(true, |p| p.is_file()) + && task.await.is_none_or(|p| p.is_file()) { return Some(( ix, @@ -6970,7 +6967,7 @@ async fn join_channel_internal( && project.visible_worktrees(cx).any(|tree| { tree.read(cx) .root_entry() - .map_or(false, |entry| entry.is_dir()) + .is_some_and(|entry| entry.is_dir()) }) { Some(workspace.project.clone()) @@ -7900,7 +7897,6 @@ fn join_pane_into_active( cx: &mut App, ) { if pane == active_pane { - return; } else if pane.read(cx).items_len() == 0 { pane.update(cx, |_, cx| { cx.emit(pane::Event::Remove { @@ -9149,11 +9145,11 @@ mod tests { workspace.update_in(cx, |workspace, window, cx| { workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx); }); - return item; + item } fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> { - return workspace.update_in(cx, |workspace, window, cx| { + workspace.update_in(cx, |workspace, window, cx| { let new_pane = workspace.split_pane( workspace.active_pane().clone(), SplitDirection::Right, @@ -9161,7 +9157,7 @@ mod tests { cx, ); new_pane - }); + }) } #[gpui::test] diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 9e1832721f..d38f3cac3d 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3393,12 +3393,10 @@ impl File { let disk_state = if proto.is_deleted { DiskState::Deleted + } else if let Some(mtime) = proto.mtime.map(&Into::into) { + DiskState::Present { mtime } } else { - if let Some(mtime) = proto.mtime.map(&Into::into) { - DiskState::Present { mtime } - } else { - DiskState::New - } + DiskState::New }; Ok(Self { @@ -4074,10 +4072,10 @@ impl BackgroundScanner { } } - let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { + let parent_dir_is_loaded = relative_path.parent().is_none_or(|parent| { snapshot .entry_for_path(parent) - .map_or(false, |entry| entry.kind == EntryKind::Dir) + .is_some_and(|entry| entry.kind == EntryKind::Dir) }); if !parent_dir_is_loaded { log::debug!("ignoring event {relative_path:?} within unloaded directory"); @@ -4630,7 +4628,7 @@ impl BackgroundScanner { while let Some(parent_abs_path) = ignores_to_update.next() { while ignores_to_update .peek() - .map_or(false, |p| p.starts_with(&parent_abs_path)) + .is_some_and(|p| p.starts_with(&parent_abs_path)) { ignores_to_update.next().unwrap(); } @@ -4797,9 +4795,7 @@ impl BackgroundScanner { for (&work_directory_id, entry) in snapshot.git_repositories.iter() { let exists_in_snapshot = snapshot .entry_for_id(work_directory_id) - .map_or(false, |entry| { - snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some() - }); + .is_some_and(|entry| snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some()); if exists_in_snapshot || matches!( @@ -4924,10 +4920,10 @@ fn build_diff( new_paths.next(); for path in event_paths { let path = PathKey(path.clone()); - if old_paths.item().map_or(false, |e| e.path < path.0) { + if old_paths.item().is_some_and(|e| e.path < path.0) { old_paths.seek_forward(&path, Bias::Left); } - if new_paths.item().map_or(false, |e| e.path < path.0) { + if new_paths.item().is_some_and(|e| e.path < path.0) { new_paths.seek_forward(&path, Bias::Left); } loop { @@ -4977,7 +4973,7 @@ fn build_diff( let is_newly_loaded = phase == InitialScan || last_newly_loaded_dir_path .as_ref() - .map_or(false, |dir| new_entry.path.starts_with(dir)); + .is_some_and(|dir| new_entry.path.starts_with(dir)); changes.push(( new_entry.path.clone(), new_entry.id, @@ -4995,7 +4991,7 @@ fn build_diff( let is_newly_loaded = phase == InitialScan || last_newly_loaded_dir_path .as_ref() - .map_or(false, |dir| new_entry.path.starts_with(dir)); + .is_some_and(|dir| new_entry.path.starts_with(dir)); changes.push(( new_entry.path.clone(), new_entry.id, diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs index 26cf16e8f6..b18d3509be 100644 --- a/crates/worktree/src/worktree_settings.rs +++ b/crates/worktree/src/worktree_settings.rs @@ -82,7 +82,7 @@ impl Settings for WorktreeSettings { .ancestors() .map(|a| a.to_string_lossy().into()) }) - .filter(|p| p != "") + .filter(|p: &String| !p.is_empty()) .collect(); file_scan_exclusions.sort(); private_files.sort(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 93a62afc6f..d3a503f172 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1625,7 +1625,7 @@ fn open_local_file( .await .ok() .flatten() - .map_or(false, |metadata| !metadata.is_dir && !metadata.is_fifo); + .is_some_and(|metadata| !metadata.is_dir && !metadata.is_fifo); file_exists }; diff --git a/crates/zed/src/zed/migrate.rs b/crates/zed/src/zed/migrate.rs index 48bffb4114..2452f17d04 100644 --- a/crates/zed/src/zed/migrate.rs +++ b/crates/zed/src/zed/migrate.rs @@ -177,7 +177,7 @@ impl ToolbarItemView for MigrationBanner { })); } - return ToolbarItemLocation::Hidden; + ToolbarItemLocation::Hidden } } diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index d65053c05f..10d60fcd9d 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -175,9 +175,9 @@ impl Render for QuickActionBar { let code_action_menu = menu_ref .as_ref() .filter(|menu| matches!(menu, CodeContextMenu::CodeActions(..))); - code_action_menu.as_ref().map_or(false, |menu| { - matches!(menu.origin(), ContextMenuOrigin::QuickActionBar) - }) + code_action_menu + .as_ref() + .is_some_and(|menu| matches!(menu.origin(), ContextMenuOrigin::QuickActionBar)) }; let code_action_element = if is_deployed { editor.update(cx, |editor, cx| { diff --git a/crates/zeta/src/init.rs b/crates/zeta/src/init.rs index a01e3a89a2..6e5b31f99a 100644 --- a/crates/zeta/src/init.rs +++ b/crates/zeta/src/init.rs @@ -85,12 +85,10 @@ fn feature_gate_predict_edits_actions(cx: &mut App) { CommandPaletteFilter::update_global(cx, |filter, _cx| { if is_ai_disabled { filter.hide_action_types(&zeta_all_action_types); + } else if has_feature_flag { + filter.show_action_types(rate_completion_action_types.iter()); } else { - if has_feature_flag { - filter.show_action_types(rate_completion_action_types.iter()); - } else { - filter.hide_action_types(&rate_completion_action_types); - } + filter.hide_action_types(&rate_completion_action_types); } }); }) diff --git a/crates/zeta/src/onboarding_modal.rs b/crates/zeta/src/onboarding_modal.rs index c2886f2864..3a58c8c7b8 100644 --- a/crates/zeta/src/onboarding_modal.rs +++ b/crates/zeta/src/onboarding_modal.rs @@ -46,7 +46,7 @@ impl ZedPredictModal { user_store.clone(), client.clone(), copilot::Copilot::global(cx) - .map_or(false, |copilot| copilot.read(cx).status().is_configured()), + .is_some_and(|copilot| copilot.read(cx).status().is_configured()), Arc::new({ let this = weak_entity.clone(); move |_window, cx| { diff --git a/crates/zeta/src/rate_completion_modal.rs b/crates/zeta/src/rate_completion_modal.rs index 313e4c3779..0cd814388a 100644 --- a/crates/zeta/src/rate_completion_modal.rs +++ b/crates/zeta/src/rate_completion_modal.rs @@ -607,7 +607,7 @@ impl Render for RateCompletionModal { .children(self.zeta.read(cx).shown_completions().cloned().enumerate().map( |(index, completion)| { let selected = - self.active_completion.as_ref().map_or(false, |selected| { + self.active_completion.as_ref().is_some_and(|selected| { selected.completion.id == completion.id }); let rated = diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 2a121c407c..640f408dd3 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -106,7 +106,7 @@ impl Dismissable for ZedPredictUpsell { if KEY_VALUE_STORE .read_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE) .log_err() - .map_or(false, |s| s.is_some()) + .is_some_and(|s| s.is_some()) { return true; } @@ -114,7 +114,7 @@ impl Dismissable for ZedPredictUpsell { KEY_VALUE_STORE .read_kvp(Self::KEY) .log_err() - .map_or(false, |s| s.is_some()) + .is_some_and(|s| s.is_some()) } } diff --git a/crates/zlog/src/filter.rs b/crates/zlog/src/filter.rs index cf1604bd9f..27a5314e28 100644 --- a/crates/zlog/src/filter.rs +++ b/crates/zlog/src/filter.rs @@ -55,7 +55,7 @@ pub fn init_env_filter(filter: env_config::EnvFilter) { } pub fn is_possibly_enabled_level(level: log::Level) -> bool { - return level as u8 <= LEVEL_ENABLED_MAX_CONFIG.load(Ordering::Relaxed); + level as u8 <= LEVEL_ENABLED_MAX_CONFIG.load(Ordering::Relaxed) } pub fn is_scope_enabled(scope: &Scope, module_path: Option<&str>, level: log::Level) -> bool { @@ -70,7 +70,7 @@ pub fn is_scope_enabled(scope: &Scope, module_path: Option<&str>, level: log::Le let is_enabled_by_default = level <= unsafe { LEVEL_ENABLED_MAX_STATIC }; let global_scope_map = SCOPE_MAP.read().unwrap_or_else(|err| { SCOPE_MAP.clear_poison(); - return err.into_inner(); + err.into_inner() }); let Some(map) = global_scope_map.as_ref() else { @@ -83,11 +83,11 @@ pub fn is_scope_enabled(scope: &Scope, module_path: Option<&str>, level: log::Le return is_enabled_by_default; } let enabled_status = map.is_enabled(scope, module_path, level); - return match enabled_status { + match enabled_status { EnabledStatus::NotConfigured => is_enabled_by_default, EnabledStatus::Enabled => true, EnabledStatus::Disabled => false, - }; + } } pub fn refresh_from_settings(settings: &HashMap<String, String>) { @@ -132,7 +132,7 @@ fn level_filter_from_str(level_str: &str) -> Option<log::LevelFilter> { return None; } }; - return Some(level); + Some(level) } fn scope_alloc_from_scope_str(scope_str: &str) -> Option<ScopeAlloc> { @@ -143,7 +143,7 @@ fn scope_alloc_from_scope_str(scope_str: &str) -> Option<ScopeAlloc> { let Some(scope) = scope_iter.next() else { break; }; - if scope == "" { + if scope.is_empty() { continue; } scope_buf[index] = scope; @@ -159,7 +159,7 @@ fn scope_alloc_from_scope_str(scope_str: &str) -> Option<ScopeAlloc> { return None; } let scope = scope_buf.map(|s| s.to_string()); - return Some(scope); + Some(scope) } #[derive(Debug, PartialEq, Eq)] @@ -280,7 +280,7 @@ impl ScopeMap { cursor += 1; } let sub_items_end = cursor; - if scope_name == "" { + if scope_name.is_empty() { assert_eq!(sub_items_start + 1, sub_items_end); assert_ne!(depth, 0); assert_ne!(parent_index, usize::MAX); @@ -288,7 +288,7 @@ impl ScopeMap { this.entries[parent_index].enabled = Some(items[sub_items_start].1); continue; } - let is_valid_scope = scope_name != ""; + let is_valid_scope = !scope_name.is_empty(); let is_last = depth + 1 == SCOPE_DEPTH_MAX || !is_valid_scope; let mut enabled = None; if is_last { @@ -321,7 +321,7 @@ impl ScopeMap { } } - return this; + this } pub fn is_empty(&self) -> bool { @@ -358,7 +358,7 @@ impl ScopeMap { } break 'search; } - return enabled; + enabled } let mut enabled = search(self, scope); @@ -394,7 +394,7 @@ impl ScopeMap { } return EnabledStatus::Disabled; } - return EnabledStatus::NotConfigured; + EnabledStatus::NotConfigured } } @@ -456,7 +456,7 @@ mod tests { let Some(scope) = scope_iter.next() else { break; }; - if scope == "" { + if scope.is_empty() { continue; } scope_buf[index] = scope; @@ -464,7 +464,7 @@ mod tests { } assert_ne!(index, 0); assert!(scope_iter.next().is_none()); - return scope_buf; + scope_buf } #[test] diff --git a/crates/zlog/src/zlog.rs b/crates/zlog/src/zlog.rs index df3a210231..d1c6cd4747 100644 --- a/crates/zlog/src/zlog.rs +++ b/crates/zlog/src/zlog.rs @@ -240,7 +240,7 @@ pub mod private { let Some((crate_name, _)) = module_path.split_at_checked(index) else { return module_path; }; - return crate_name; + crate_name } pub const fn scope_new(scopes: &[&'static str]) -> Scope { @@ -262,7 +262,7 @@ pub mod private { } pub fn scope_to_alloc(scope: &Scope) -> ScopeAlloc { - return scope.map(|s| s.to_string()); + scope.map(|s| s.to_string()) } } @@ -319,18 +319,18 @@ impl Drop for Timer { impl Timer { #[must_use = "Timer will stop when dropped, the result of this function should be saved in a variable prefixed with `_` if it should stop when dropped"] pub fn new(logger: Logger, name: &'static str) -> Self { - return Self { + Self { logger, name, start_time: std::time::Instant::now(), warn_if_longer_than: None, done: false, - }; + } } pub fn warn_if_gt(mut self, warn_limit: std::time::Duration) -> Self { self.warn_if_longer_than = Some(warn_limit); - return self; + self } pub fn end(mut self) { diff --git a/extensions/glsl/src/glsl.rs b/extensions/glsl/src/glsl.rs index ba506d2b11..695fd7a053 100644 --- a/extensions/glsl/src/glsl.rs +++ b/extensions/glsl/src/glsl.rs @@ -17,7 +17,7 @@ impl GlslExtension { } if let Some(path) = &self.cached_binary_path - && fs::metadata(path).map_or(false, |stat| stat.is_file()) + && fs::metadata(path).is_ok_and(|stat| stat.is_file()) { return Ok(path.clone()); } @@ -60,7 +60,7 @@ impl GlslExtension { .map_err(|err| format!("failed to create directory '{version_dir}': {err}"))?; let binary_path = format!("{version_dir}/bin/glsl_analyzer"); - if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { zed::set_language_server_installation_status( language_server_id, &zed::LanguageServerInstallationStatus::Downloading, diff --git a/extensions/html/src/html.rs b/extensions/html/src/html.rs index 44ec4fe4b9..07d4642ff4 100644 --- a/extensions/html/src/html.rs +++ b/extensions/html/src/html.rs @@ -13,7 +13,7 @@ struct HtmlExtension { impl HtmlExtension { fn server_exists(&self) -> bool { - fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file()) + fs::metadata(SERVER_PATH).is_ok_and(|stat| stat.is_file()) } fn server_script_path(&mut self, language_server_id: &LanguageServerId) -> Result<String> { diff --git a/extensions/ruff/src/ruff.rs b/extensions/ruff/src/ruff.rs index 7b811db212..b918c52686 100644 --- a/extensions/ruff/src/ruff.rs +++ b/extensions/ruff/src/ruff.rs @@ -39,7 +39,7 @@ impl RuffExtension { } if let Some(path) = &self.cached_binary_path - && fs::metadata(path).map_or(false, |stat| stat.is_file()) + && fs::metadata(path).is_ok_and(|stat| stat.is_file()) { return Ok(RuffBinary { path: path.clone(), @@ -94,7 +94,7 @@ impl RuffExtension { _ => format!("{version_dir}/{asset_stem}/ruff"), }; - if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { zed::set_language_server_installation_status( language_server_id, &zed::LanguageServerInstallationStatus::Downloading, diff --git a/extensions/snippets/src/snippets.rs b/extensions/snippets/src/snippets.rs index 682709a28a..b2d68b6e1a 100644 --- a/extensions/snippets/src/snippets.rs +++ b/extensions/snippets/src/snippets.rs @@ -18,7 +18,7 @@ impl SnippetExtension { } if let Some(path) = &self.cached_binary_path - && fs::metadata(path).map_or(false, |stat| stat.is_file()) + && fs::metadata(path).is_ok_and(|stat| stat.is_file()) { return Ok(path.clone()); } @@ -59,7 +59,7 @@ impl SnippetExtension { let version_dir = format!("simple-completion-language-server-{}", release.version); let binary_path = format!("{version_dir}/simple-completion-language-server"); - if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { zed::set_language_server_installation_status( language_server_id, &zed::LanguageServerInstallationStatus::Downloading, diff --git a/extensions/test-extension/src/test_extension.rs b/extensions/test-extension/src/test_extension.rs index 0ef522bd51..ee0b1b36a1 100644 --- a/extensions/test-extension/src/test_extension.rs +++ b/extensions/test-extension/src/test_extension.rs @@ -19,7 +19,7 @@ impl TestExtension { println!("{}", String::from_utf8_lossy(&echo_output.stdout)); if let Some(path) = &self.cached_binary_path - && fs::metadata(path).map_or(false, |stat| stat.is_file()) + && fs::metadata(path).is_ok_and(|stat| stat.is_file()) { return Ok(path.clone()); } @@ -61,7 +61,7 @@ impl TestExtension { let version_dir = format!("gleam-{}", release.version); let binary_path = format!("{version_dir}/gleam"); - if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { zed::set_language_server_installation_status( language_server_id, &zed::LanguageServerInstallationStatus::Downloading, diff --git a/extensions/toml/src/toml.rs b/extensions/toml/src/toml.rs index 30a2cd6ce3..c9b96aecac 100644 --- a/extensions/toml/src/toml.rs +++ b/extensions/toml/src/toml.rs @@ -40,7 +40,7 @@ impl TomlExtension { } if let Some(path) = &self.cached_binary_path - && fs::metadata(path).map_or(false, |stat| stat.is_file()) + && fs::metadata(path).is_ok_and(|stat| stat.is_file()) { return Ok(TaploBinary { path: path.clone(), @@ -93,7 +93,7 @@ impl TomlExtension { } ); - if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { zed::set_language_server_installation_status( language_server_id, &zed::LanguageServerInstallationStatus::Downloading, diff --git a/tooling/xtask/src/tasks/package_conformity.rs b/tooling/xtask/src/tasks/package_conformity.rs index c82b9cdf84..c8bed4bb35 100644 --- a/tooling/xtask/src/tasks/package_conformity.rs +++ b/tooling/xtask/src/tasks/package_conformity.rs @@ -21,13 +21,11 @@ pub fn run_package_conformity(_args: PackageConformityArgs) -> Result<()> { .manifest_path .parent() .and_then(|parent| parent.parent()) - .map_or(false, |grandparent_dir| { - grandparent_dir.ends_with("extensions") - }); + .is_some_and(|grandparent_dir| grandparent_dir.ends_with("extensions")); let cargo_toml = read_cargo_toml(&package.manifest_path)?; - let is_using_workspace_lints = cargo_toml.lints.map_or(false, |lints| lints.workspace); + let is_using_workspace_lints = cargo_toml.lints.is_some_and(|lints| lints.workspace); if !is_using_workspace_lints { eprintln!( "{package:?} is not using workspace lints", From 69b1c6d6f56e8ebc4c6b0ce6aaed06986521a47d Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Tue, 19 Aug 2025 15:26:40 -0400 Subject: [PATCH 494/693] Fix `workspace::SendKeystrokes` example in docs (#36515) Closes: https://github.com/zed-industries/zed/issues/25683 Remove two bad examples from the key binding docs. `cmd-shift-p` (command palette) and `cmd-p` (file finder) are async operations and thus do not work properly with `workspace::SendKeystrokes`. Originally reported in https://github.com/zed-industries/zed/issues/25683#issuecomment-3145830534 Release Notes: - N/A --- docs/src/key-bindings.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/src/key-bindings.md b/docs/src/key-bindings.md index 9fc94840b7..838dceaa86 100644 --- a/docs/src/key-bindings.md +++ b/docs/src/key-bindings.md @@ -225,12 +225,14 @@ A common request is to be able to map from a single keystroke to a sequence. You [ { "bindings": { + // Move down four times "alt-down": ["workspace::SendKeystrokes", "down down down down"], + // Expand the selection (editor::SelectLargerSyntaxNode); + // copy to the clipboard; and then undo the selection expansion. "cmd-alt-c": [ "workspace::SendKeystrokes", - "cmd-shift-p copy relative path enter" - ], - "cmd-alt-r": ["workspace::SendKeystrokes", "cmd-p README enter"] + "ctrl-shift-right ctrl-shift-right ctrl-shift-right cmd-c ctrl-shift-left ctrl-shift-left ctrl-shift-left" + ] } }, { From 68257155037fa0f5c2093964eddb9a4c288741d9 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 19 Aug 2025 22:33:44 +0200 Subject: [PATCH 495/693] Another batch of lint fixes (#36521) - **Enable a bunch of extra lints** - **First batch of fixes** - **More fixes** Release Notes: - N/A --- Cargo.toml | 10 ++ crates/action_log/src/action_log.rs | 5 +- .../src/activity_indicator.rs | 29 ++-- crates/agent/src/thread.rs | 6 +- crates/agent/src/thread_store.rs | 2 +- crates/agent2/src/agent.rs | 2 +- crates/agent2/src/tools/edit_file_tool.rs | 3 +- crates/agent_servers/src/acp/v0.rs | 4 +- crates/agent_servers/src/claude/mcp_server.rs | 6 +- crates/agent_servers/src/e2e_tests.rs | 7 +- crates/agent_ui/src/acp/message_editor.rs | 5 +- crates/agent_ui/src/acp/thread_view.rs | 4 +- crates/agent_ui/src/active_thread.rs | 12 +- crates/agent_ui/src/agent_configuration.rs | 2 +- .../configure_context_server_modal.rs | 2 +- crates/agent_ui/src/agent_diff.rs | 24 +-- crates/agent_ui/src/agent_panel.rs | 55 +++--- crates/agent_ui/src/context_picker.rs | 9 +- crates/agent_ui/src/message_editor.rs | 17 +- crates/agent_ui/src/slash_command_picker.rs | 4 +- crates/agent_ui/src/text_thread_editor.rs | 15 +- crates/agent_ui/src/tool_compatibility.rs | 12 +- .../src/assistant_context.rs | 20 +-- .../src/file_command.rs | 6 +- crates/assistant_tool/src/tool_working_set.rs | 4 +- crates/assistant_tools/src/assistant_tools.rs | 5 +- crates/assistant_tools/src/edit_file_tool.rs | 3 +- crates/assistant_tools/src/terminal_tool.rs | 6 +- crates/cli/src/main.rs | 8 +- crates/collab/src/tests/integration_tests.rs | 2 +- .../src/chat_panel/message_editor.rs | 5 +- crates/context_server/src/listener.rs | 2 +- crates/dap_adapters/src/python.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 2 +- crates/debugger_ui/src/session.rs | 6 +- crates/debugger_ui/src/session/running.rs | 6 +- .../src/session/running/breakpoint_list.rs | 36 ++-- .../src/session/running/variable_list.rs | 2 +- crates/debugger_ui/src/tests/variable_list.rs | 7 +- crates/docs_preprocessor/src/main.rs | 19 +- crates/editor/src/display_map/invisibles.rs | 10 +- crates/editor/src/editor.rs | 162 ++++++++---------- crates/editor/src/editor_tests.rs | 4 +- crates/editor/src/element.rs | 29 ++-- crates/editor/src/git/blame.rs | 7 +- crates/editor/src/hover_popover.rs | 21 +-- crates/editor/src/items.rs | 16 +- crates/editor/src/jsx_tag_auto_close.rs | 5 +- crates/editor/src/proposed_changes_editor.rs | 25 +-- crates/editor/src/selections_collection.rs | 22 +-- crates/eval/src/instance.rs | 10 +- crates/extension/src/extension_builder.rs | 12 +- crates/extension_host/src/extension_host.rs | 5 +- crates/extension_host/src/wasm_host.rs | 2 +- crates/extensions_ui/src/extensions_ui.rs | 6 +- crates/file_finder/src/open_path_prompt.rs | 2 +- crates/fs/src/fs.rs | 5 +- crates/git/src/repository.rs | 2 +- crates/git_ui/src/file_diff_view.rs | 2 +- crates/git_ui/src/git_panel.rs | 11 +- crates/git_ui/src/project_diff.rs | 31 ++-- crates/git_ui/src/text_diff_view.rs | 2 +- crates/gpui/src/app.rs | 4 +- crates/gpui/src/elements/text.rs | 6 +- .../gpui/src/platform/linux/wayland/client.rs | 51 +++--- .../gpui/src/platform/linux/wayland/window.rs | 147 ++++++++-------- crates/gpui/src/platform/linux/x11/client.rs | 44 ++--- crates/gpui/src/platform/mac/events.rs | 6 +- crates/gpui/src/platform/mac/window.rs | 4 +- .../gpui/src/platform/scap_screen_capture.rs | 2 +- crates/gpui/src/platform/windows/events.rs | 29 ++-- crates/gpui/src/taffy.rs | 18 +- crates/gpui/src/text_system/line_wrapper.rs | 2 +- crates/gpui/src/util.rs | 8 +- crates/gpui_macros/src/test.rs | 2 +- crates/http_client/src/http_client.rs | 3 +- crates/jj/src/jj_repository.rs | 7 +- crates/journal/src/journal.rs | 6 +- crates/language/src/buffer.rs | 13 +- crates/language/src/language.rs | 5 +- crates/language_model/src/language_model.rs | 2 +- .../language_models/src/provider/open_ai.rs | 2 +- crates/languages/src/json.rs | 2 +- crates/languages/src/python.rs | 2 +- crates/multi_buffer/src/multi_buffer.rs | 8 +- crates/node_runtime/src/node_runtime.rs | 5 +- crates/onboarding/src/ai_setup_page.rs | 4 +- crates/onboarding/src/basics_page.rs | 10 +- crates/onboarding/src/editing_page.rs | 2 +- crates/outline_panel/src/outline_panel.rs | 20 +-- crates/project/src/buffer_store.rs | 18 +- crates/project/src/color_extractor.rs | 6 +- crates/project/src/context_server_store.rs | 16 +- crates/project/src/debugger/dap_store.rs | 3 +- crates/project/src/debugger/locators/go.rs | 6 +- crates/project/src/debugger/session.rs | 13 +- crates/project/src/image_store.rs | 26 ++- crates/project/src/lsp_command.rs | 4 +- crates/project/src/lsp_store.rs | 46 ++--- crates/project/src/manifest_tree.rs | 16 +- crates/project/src/project.rs | 26 ++- crates/project/src/project_tests.rs | 6 +- crates/project/src/task_inventory.rs | 2 +- crates/project/src/terminals.rs | 6 +- crates/project_panel/src/project_panel.rs | 6 +- .../src/disconnected_overlay.rs | 7 +- crates/remote/src/protocol.rs | 4 +- crates/remote_server/src/headless_project.rs | 37 ++-- crates/remote_server/src/unix.rs | 8 +- crates/repl/src/components/kernel_options.rs | 5 +- crates/repl/src/notebook/cell.rs | 2 +- crates/repl/src/notebook/notebook_ui.rs | 4 +- crates/repl/src/outputs/plain.rs | 6 +- crates/rope/src/chunk.rs | 2 +- crates/rules_library/src/rules_library.rs | 2 +- crates/search/src/project_search.rs | 4 +- crates/settings/src/key_equivalents.rs | 2 +- crates/settings/src/settings_json.rs | 6 +- crates/settings_ui/src/keybindings.rs | 26 ++- .../src/ui_components/keystroke_input.rs | 2 +- crates/settings_ui/src/ui_components/table.rs | 4 +- crates/storybook/src/story_selector.rs | 6 +- crates/svg_preview/src/svg_preview_view.rs | 43 ++--- crates/task/src/shell_builder.rs | 12 +- crates/tasks_ui/src/modal.rs | 5 +- crates/terminal_view/src/terminal_panel.rs | 53 +++--- crates/terminal_view/src/terminal_view.rs | 7 +- crates/theme/src/icon_theme.rs | 2 +- crates/title_bar/src/collab.rs | 6 +- crates/ui/src/components/facepile.rs | 2 +- crates/ui/src/components/toggle.rs | 2 +- crates/ui/src/components/tooltip.rs | 2 +- crates/vim/src/helix.rs | 20 +-- crates/vim/src/normal/change.rs | 7 +- crates/vim/src/state.rs | 56 +++--- crates/vim/src/test/neovim_connection.rs | 2 +- crates/web_search_providers/src/cloud.rs | 2 +- .../src/web_search_providers.rs | 5 +- crates/workspace/src/shared_screen.rs | 11 +- crates/workspace/src/workspace.rs | 13 +- crates/zed/src/main.rs | 9 +- crates/zed/src/zed.rs | 7 +- .../zed/src/zed/edit_prediction_registry.rs | 5 +- crates/zeta/src/zeta.rs | 12 +- crates/zeta_cli/src/main.rs | 15 +- crates/zlog/src/filter.rs | 13 +- crates/zlog/src/zlog.rs | 11 +- 147 files changed, 788 insertions(+), 1042 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 46c5646c90..ad45def2d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -822,14 +822,20 @@ style = { level = "allow", priority = -1 } # Temporary list of style lints that we've fixed so far. comparison_to_empty = "warn" +into_iter_on_ref = "warn" iter_cloned_collect = "warn" iter_next_slice = "warn" iter_nth = "warn" iter_nth_zero = "warn" iter_skip_next = "warn" +let_and_return = "warn" module_inception = { level = "deny" } question_mark = { level = "deny" } +single_match = "warn" redundant_closure = { level = "deny" } +redundant_static_lifetimes = { level = "warn" } +redundant_pattern_matching = "warn" +redundant_field_names = "warn" declare_interior_mutable_const = { level = "deny" } collapsible_if = { level = "warn"} collapsible_else_if = { level = "warn" } @@ -857,6 +863,10 @@ too_many_arguments = "allow" # We often have large enum variants yet we rarely actually bother with splitting them up. large_enum_variant = "allow" +# `enum_variant_names` fires for all enums, even when they derive serde traits. +# Adhering to this lint would be a breaking change. +enum_variant_names = "allow" + [workspace.metadata.cargo-machete] ignored = [ "bindgen", diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs index 602357ed2b..1c3cad386d 100644 --- a/crates/action_log/src/action_log.rs +++ b/crates/action_log/src/action_log.rs @@ -264,15 +264,14 @@ impl ActionLog { if let Some((git_diff, (buffer_repo, _))) = git_diff.as_ref().zip(buffer_repo) { cx.update(|cx| { let mut old_head = buffer_repo.read(cx).head_commit.clone(); - Some(cx.subscribe(git_diff, move |_, event, cx| match event { - buffer_diff::BufferDiffEvent::DiffChanged { .. } => { + Some(cx.subscribe(git_diff, move |_, event, cx| { + if let buffer_diff::BufferDiffEvent::DiffChanged { .. } = event { let new_head = buffer_repo.read(cx).head_commit.clone(); if new_head != old_head { old_head = new_head; git_diff_updates_tx.send(()).ok(); } } - _ => {} })) })? } else { diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 8faf74736a..324480f5b4 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -103,26 +103,21 @@ impl ActivityIndicator { cx.subscribe_in( &workspace_handle, window, - |activity_indicator, _, event, window, cx| match event { - workspace::Event::ClearActivityIndicator { .. } => { - if activity_indicator.statuses.pop().is_some() { - activity_indicator.dismiss_error_message( - &DismissErrorMessage, - window, - cx, - ); - cx.notify(); - } + |activity_indicator, _, event, window, cx| { + if let workspace::Event::ClearActivityIndicator { .. } = event + && activity_indicator.statuses.pop().is_some() + { + activity_indicator.dismiss_error_message(&DismissErrorMessage, window, cx); + cx.notify(); } - _ => {} }, ) .detach(); cx.subscribe( &project.read(cx).lsp_store(), - |activity_indicator, _, event, cx| match event { - LspStoreEvent::LanguageServerUpdate { name, message, .. } => { + |activity_indicator, _, event, cx| { + if let LspStoreEvent::LanguageServerUpdate { name, message, .. } = event { if let proto::update_language_server::Variant::StatusUpdate(status_update) = message { @@ -191,7 +186,6 @@ impl ActivityIndicator { } cx.notify() } - _ => {} }, ) .detach(); @@ -206,9 +200,10 @@ impl ActivityIndicator { cx.subscribe( &project.read(cx).git_store().clone(), - |_, _, event: &GitStoreEvent, cx| match event { - project::git_store::GitStoreEvent::JobsUpdated => cx.notify(), - _ => {} + |_, _, event: &GitStoreEvent, cx| { + if let project::git_store::GitStoreEvent::JobsUpdated = event { + cx.notify() + } }, ) .detach(); diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 5c4b2b8ebf..80ed277f10 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -1645,15 +1645,13 @@ impl Thread { self.tool_use .request_tool_use(tool_message_id, tool_use, tool_use_metadata.clone(), cx); - let pending_tool_use = self.tool_use.insert_tool_output( + self.tool_use.insert_tool_output( tool_use_id.clone(), tool_name, tool_output, self.configured_model.as_ref(), self.completion_mode, - ); - - pending_tool_use + ) } pub fn stream_completion( diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index 63d0f72e00..45e551dbdf 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -74,7 +74,7 @@ impl Column for DataType { } } -const RULES_FILE_NAMES: [&'static str; 9] = [ +const RULES_FILE_NAMES: [&str; 9] = [ ".rules", ".cursorrules", ".windsurfrules", diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index bc46ad1657..48f46a52fc 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -28,7 +28,7 @@ use std::rc::Rc; use std::sync::Arc; use util::ResultExt; -const RULES_FILE_NAMES: [&'static str; 9] = [ +const RULES_FILE_NAMES: [&str; 9] = [ ".rules", ".cursorrules", ".windsurfrules", diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 21eb282110..a87699bd12 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -655,8 +655,7 @@ mod tests { mode: mode.clone(), }; - let result = cx.update(|cx| resolve_path(&input, project, cx)); - result + cx.update(|cx| resolve_path(&input, project, cx)) } fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) { diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index aa80f01c15..30643dd005 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -149,7 +149,7 @@ impl acp_old::Client for OldAcpClientDelegate { Ok(acp_old::RequestToolCallConfirmationResponse { id: acp_old::ToolCallId(old_acp_id), - outcome: outcome, + outcome, }) } @@ -266,7 +266,7 @@ impl acp_old::Client for OldAcpClientDelegate { fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall { acp::ToolCall { - id: id, + id, title: request.label, kind: acp_kind_from_old_icon(request.icon), status: acp::ToolCallStatus::InProgress, diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/claude/mcp_server.rs index 38587574db..3086752850 100644 --- a/crates/agent_servers/src/claude/mcp_server.rs +++ b/crates/agent_servers/src/claude/mcp_server.rs @@ -175,9 +175,9 @@ impl McpServerTool for PermissionTool { let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone()); let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into()); - const ALWAYS_ALLOW: &'static str = "always_allow"; - const ALLOW: &'static str = "allow"; - const REJECT: &'static str = "reject"; + const ALWAYS_ALLOW: &str = "always_allow"; + const ALLOW: &str = "allow"; + const REJECT: &str = "reject"; let chosen_option = thread .update(cx, |thread, cx| { diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index fef80b4d42..8b2703575d 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -428,12 +428,9 @@ pub async fn new_test_thread( .await .unwrap(); - let thread = cx - .update(|cx| connection.new_thread(project.clone(), current_dir.as_ref(), cx)) + cx.update(|cx| connection.new_thread(project.clone(), current_dir.as_ref(), cx)) .await - .unwrap(); - - thread + .unwrap() } pub async fn run_until_first_tool_call( diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index e7f0d4f88f..311fe258de 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -134,8 +134,8 @@ impl MessageEditor { if prevent_slash_commands { subscriptions.push(cx.subscribe_in(&editor, window, { let semantics_provider = semantics_provider.clone(); - move |this, editor, event, window, cx| match event { - EditorEvent::Edited { .. } => { + move |this, editor, event, window, cx| { + if let EditorEvent::Edited { .. } = event { this.highlight_slash_command( semantics_provider.clone(), editor.clone(), @@ -143,7 +143,6 @@ impl MessageEditor { cx, ); } - _ => {} } })); } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 7b38ba9301..9f1e8d857f 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2124,7 +2124,7 @@ impl AcpThreadView { .map(|view| div().px_4().w_full().max_w_128().child(view)), ) .child(h_flex().mt_1p5().justify_center().children( - connection.auth_methods().into_iter().map(|method| { + connection.auth_methods().iter().map(|method| { Button::new(SharedString::from(method.id.0.clone()), method.name.clone()) .on_click({ let method_id = method.id.clone(); @@ -2574,7 +2574,7 @@ impl AcpThreadView { ) -> Div { let editor_bg_color = cx.theme().colors().editor_background; - v_flex().children(changed_buffers.into_iter().enumerate().flat_map( + v_flex().children(changed_buffers.iter().enumerate().flat_map( |(index, (buffer, _diff))| { let file = buffer.read(cx).file()?; let path = file.path(); diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index a1e51f883a..e595b94ebb 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -1373,12 +1373,12 @@ impl ActiveThread { editor.focus_handle(cx).focus(window); editor.move_to_end(&editor::actions::MoveToEnd, window, cx); }); - let buffer_edited_subscription = cx.subscribe(&editor, |this, _, event, cx| match event { - EditorEvent::BufferEdited => { - this.update_editing_message_token_count(true, cx); - } - _ => {} - }); + let buffer_edited_subscription = + cx.subscribe(&editor, |this, _, event: &EditorEvent, cx| { + if event == &EditorEvent::BufferEdited { + this.update_editing_message_token_count(true, cx); + } + }); let context_picker_menu_handle = PopoverMenuHandle::default(); let context_strip = cx.new(|cx| { diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index b032201d8c..ecb0bca4a1 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -958,7 +958,7 @@ impl AgentConfiguration { } parent.child(v_flex().py_1p5().px_1().gap_1().children( - tools.into_iter().enumerate().map(|(ix, tool)| { + tools.iter().enumerate().map(|(ix, tool)| { h_flex() .id(("tool-item", ix)) .px_1() diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs index 311f75af3b..6159b9be80 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs @@ -487,7 +487,7 @@ impl ConfigureContextServerModal { } fn render_modal_description(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement { - const MODAL_DESCRIPTION: &'static str = "Visit the MCP server configuration docs to find all necessary arguments and environment variables."; + const MODAL_DESCRIPTION: &str = "Visit the MCP server configuration docs to find all necessary arguments and environment variables."; if let ConfigurationSource::Extension { installation_instructions: Some(installation_instructions), diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index e80cd20846..9d2ee0bf89 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -322,16 +322,14 @@ impl AgentDiffPane { } fn handle_native_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context<Self>) { - match event { - ThreadEvent::SummaryGenerated => self.update_title(cx), - _ => {} + if let ThreadEvent::SummaryGenerated = event { + self.update_title(cx) } } fn handle_acp_thread_event(&mut self, event: &AcpThreadEvent, cx: &mut Context<Self>) { - match event { - AcpThreadEvent::TitleUpdated => self.update_title(cx), - _ => {} + if let AcpThreadEvent::TitleUpdated = event { + self.update_title(cx) } } @@ -1541,15 +1539,11 @@ impl AgentDiff { window: &mut Window, cx: &mut Context<Self>, ) { - match event { - workspace::Event::ItemAdded { item } => { - if let Some(editor) = item.downcast::<Editor>() - && let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx) - { - self.register_editor(workspace.downgrade(), buffer.clone(), editor, window, cx); - } - } - _ => {} + if let workspace::Event::ItemAdded { item } = event + && let Some(editor) = item.downcast::<Editor>() + && let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx) + { + self.register_editor(workspace.downgrade(), buffer.clone(), editor, window, cx); } } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index c79349e3a9..c5cab34030 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -354,7 +354,7 @@ impl ActiveView { Self::Thread { change_title_editor: editor, thread: active_thread, - message_editor: message_editor, + message_editor, _subscriptions: subscriptions, } } @@ -756,25 +756,25 @@ impl AgentPanel { .ok(); }); - let _default_model_subscription = cx.subscribe( - &LanguageModelRegistry::global(cx), - |this, _, event: &language_model::Event, cx| match event { - language_model::Event::DefaultModelChanged => match &this.active_view { - ActiveView::Thread { thread, .. } => { - thread - .read(cx) - .thread() - .clone() - .update(cx, |thread, cx| thread.get_or_init_configured_model(cx)); + let _default_model_subscription = + cx.subscribe( + &LanguageModelRegistry::global(cx), + |this, _, event: &language_model::Event, cx| { + if let language_model::Event::DefaultModelChanged = event { + match &this.active_view { + ActiveView::Thread { thread, .. } => { + thread.read(cx).thread().clone().update(cx, |thread, cx| { + thread.get_or_init_configured_model(cx) + }); + } + ActiveView::ExternalAgentThread { .. } + | ActiveView::TextThread { .. } + | ActiveView::History + | ActiveView::Configuration => {} + } } - ActiveView::ExternalAgentThread { .. } - | ActiveView::TextThread { .. } - | ActiveView::History - | ActiveView::Configuration => {} }, - _ => {} - }, - ); + ); let onboarding = cx.new(|cx| { AgentPanelOnboarding::new( @@ -1589,17 +1589,14 @@ impl AgentPanel { let current_is_special = current_is_history || current_is_config; let new_is_special = new_is_history || new_is_config; - match &self.active_view { - ActiveView::Thread { thread, .. } => { - let thread = thread.read(cx); - if thread.is_empty() { - let id = thread.thread().read(cx).id().clone(); - self.history_store.update(cx, |store, cx| { - store.remove_recently_opened_thread(id, cx); - }); - } + if let ActiveView::Thread { thread, .. } = &self.active_view { + let thread = thread.read(cx); + if thread.is_empty() { + let id = thread.thread().read(cx).id().clone(); + self.history_store.update(cx, |store, cx| { + store.remove_recently_opened_thread(id, cx); + }); } - _ => {} } match &new_view { @@ -3465,7 +3462,7 @@ impl AgentPanel { .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| { let tasks = paths .paths() - .into_iter() + .iter() .map(|path| { Workspace::project_path_for_path(this.project.clone(), path, false, cx) }) diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index 697f704991..0b4568dc87 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -385,12 +385,11 @@ impl ContextPicker { } pub fn select_first(&mut self, window: &mut Window, cx: &mut Context<Self>) { - match &self.mode { - ContextPickerState::Default(entity) => entity.update(cx, |entity, cx| { + // Other variants already select their first entry on open automatically + if let ContextPickerState::Default(entity) = &self.mode { + entity.update(cx, |entity, cx| { entity.select_first(&Default::default(), window, cx) - }), - // Other variants already select their first entry on open automatically - _ => {} + }) } } diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 6e4d2638c1..f70d10c1ae 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -117,7 +117,7 @@ pub(crate) fn create_editor( let mut editor = Editor::new( editor::EditorMode::AutoHeight { min_lines, - max_lines: max_lines, + max_lines, }, buffer, None, @@ -215,9 +215,10 @@ impl MessageEditor { let subscriptions = vec![ cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event), - cx.subscribe(&editor, |this, _, event, cx| match event { - EditorEvent::BufferEdited => this.handle_message_changed(cx), - _ => {} + cx.subscribe(&editor, |this, _, event: &EditorEvent, cx| { + if event == &EditorEvent::BufferEdited { + this.handle_message_changed(cx) + } }), cx.observe(&context_store, |this, _, cx| { // When context changes, reload it for token counting. @@ -1132,7 +1133,7 @@ impl MessageEditor { ) .when(is_edit_changes_expanded, |parent| { parent.child( - v_flex().children(changed_buffers.into_iter().enumerate().flat_map( + v_flex().children(changed_buffers.iter().enumerate().flat_map( |(index, (buffer, _diff))| { let file = buffer.read(cx).file()?; let path = file.path(); @@ -1605,7 +1606,8 @@ pub fn extract_message_creases( .collect::<HashMap<_, _>>(); // Filter the addon's list of creases based on what the editor reports, // since the addon might have removed creases in it. - let creases = editor.display_map.update(cx, |display_map, cx| { + + editor.display_map.update(cx, |display_map, cx| { display_map .snapshot(cx) .crease_snapshot @@ -1629,8 +1631,7 @@ pub fn extract_message_creases( } }) .collect() - }); - creases + }) } impl EventEmitter<MessageEditorEvent> for MessageEditor {} diff --git a/crates/agent_ui/src/slash_command_picker.rs b/crates/agent_ui/src/slash_command_picker.rs index 03f2c97887..a6bb61510c 100644 --- a/crates/agent_ui/src/slash_command_picker.rs +++ b/crates/agent_ui/src/slash_command_picker.rs @@ -327,9 +327,7 @@ where }; let picker_view = cx.new(|cx| { - let picker = - Picker::uniform_list(delegate, window, cx).max_height(Some(rems(20.).into())); - picker + Picker::uniform_list(delegate, window, cx).max_height(Some(rems(20.).into())) }); let handle = self diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index b7e5d83d6d..b3f55ffc43 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -540,7 +540,7 @@ impl TextThreadEditor { let context = self.context.read(cx); let sections = context .slash_command_output_sections() - .into_iter() + .iter() .filter(|section| section.is_valid(context.buffer().read(cx))) .cloned() .collect::<Vec<_>>(); @@ -1237,7 +1237,7 @@ impl TextThreadEditor { let mut new_blocks = vec![]; let mut block_index_to_message = vec![]; for message in self.context.read(cx).messages(cx) { - if let Some(_) = blocks_to_remove.remove(&message.id) { + if blocks_to_remove.remove(&message.id).is_some() { // This is an old message that we might modify. let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else { debug_assert!( @@ -1275,7 +1275,7 @@ impl TextThreadEditor { context_editor_view: &Entity<TextThreadEditor>, cx: &mut Context<Workspace>, ) -> Option<(String, bool)> { - const CODE_FENCE_DELIMITER: &'static str = "```"; + const CODE_FENCE_DELIMITER: &str = "```"; let context_editor = context_editor_view.read(cx).editor.clone(); context_editor.update(cx, |context_editor, cx| { @@ -2161,8 +2161,8 @@ impl TextThreadEditor { /// Returns the contents of the *outermost* fenced code block that contains the given offset. fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Option<Range<usize>> { - const CODE_BLOCK_NODE: &'static str = "fenced_code_block"; - const CODE_BLOCK_CONTENT: &'static str = "code_fence_content"; + const CODE_BLOCK_NODE: &str = "fenced_code_block"; + const CODE_BLOCK_CONTENT: &str = "code_fence_content"; let layer = snapshot.syntax_layers().next()?; @@ -3129,7 +3129,7 @@ mod tests { let context_editor = window .update(&mut cx, |_, window, cx| { cx.new(|cx| { - let editor = TextThreadEditor::for_context( + TextThreadEditor::for_context( context.clone(), fs, workspace.downgrade(), @@ -3137,8 +3137,7 @@ mod tests { None, window, cx, - ); - editor + ) }) }) .unwrap(); diff --git a/crates/agent_ui/src/tool_compatibility.rs b/crates/agent_ui/src/tool_compatibility.rs index d4e1da5bb0..046c0a4abc 100644 --- a/crates/agent_ui/src/tool_compatibility.rs +++ b/crates/agent_ui/src/tool_compatibility.rs @@ -14,13 +14,11 @@ pub struct IncompatibleToolsState { impl IncompatibleToolsState { pub fn new(thread: Entity<Thread>, cx: &mut Context<Self>) -> Self { - let _tool_working_set_subscription = - cx.subscribe(&thread, |this, _, event, _| match event { - ThreadEvent::ProfileChanged => { - this.cache.clear(); - } - _ => {} - }); + let _tool_working_set_subscription = cx.subscribe(&thread, |this, _, event, _| { + if let ThreadEvent::ProfileChanged = event { + this.cache.clear(); + } + }); Self { cache: HashMap::default(), diff --git a/crates/assistant_context/src/assistant_context.rs b/crates/assistant_context/src/assistant_context.rs index 2d71a1c08a..4d0bfae444 100644 --- a/crates/assistant_context/src/assistant_context.rs +++ b/crates/assistant_context/src/assistant_context.rs @@ -590,7 +590,7 @@ impl From<&Message> for MessageMetadata { impl MessageMetadata { pub fn is_cache_valid(&self, buffer: &BufferSnapshot, range: &Range<usize>) -> bool { - let result = match &self.cache { + match &self.cache { Some(MessageCacheMetadata { cached_at, .. }) => !buffer.has_edits_since_in_range( cached_at, Range { @@ -599,8 +599,7 @@ impl MessageMetadata { }, ), _ => false, - }; - result + } } } @@ -2081,15 +2080,12 @@ impl AssistantContext { match event { LanguageModelCompletionEvent::StatusUpdate(status_update) => { - match status_update { - CompletionRequestStatus::UsageUpdated { amount, limit } => { - this.update_model_request_usage( - amount as u32, - limit, - cx, - ); - } - _ => {} + if let CompletionRequestStatus::UsageUpdated { amount, limit } = status_update { + this.update_model_request_usage( + amount as u32, + limit, + cx, + ); } } LanguageModelCompletionEvent::StartMessage { .. } => {} diff --git a/crates/assistant_slash_commands/src/file_command.rs b/crates/assistant_slash_commands/src/file_command.rs index 6875189927..894aa94a27 100644 --- a/crates/assistant_slash_commands/src/file_command.rs +++ b/crates/assistant_slash_commands/src/file_command.rs @@ -223,7 +223,7 @@ fn collect_files( cx: &mut App, ) -> impl Stream<Item = Result<SlashCommandEvent>> + use<> { let Ok(matchers) = glob_inputs - .into_iter() + .iter() .map(|glob_input| { custom_path_matcher::PathMatcher::new(&[glob_input.to_owned()]) .with_context(|| format!("invalid path {glob_input}")) @@ -379,7 +379,7 @@ fn collect_files( } } - while let Some(_) = directory_stack.pop() { + while directory_stack.pop().is_some() { events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection))?; } } @@ -491,7 +491,7 @@ mod custom_path_matcher { impl PathMatcher { pub fn new(globs: &[String]) -> Result<Self, globset::Error> { let globs = globs - .into_iter() + .iter() .map(|glob| Glob::new(&SanitizedPath::from(glob).to_glob_string())) .collect::<Result<Vec<_>, _>>()?; let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect(); diff --git a/crates/assistant_tool/src/tool_working_set.rs b/crates/assistant_tool/src/tool_working_set.rs index c0a358917b..61f57affc7 100644 --- a/crates/assistant_tool/src/tool_working_set.rs +++ b/crates/assistant_tool/src/tool_working_set.rs @@ -156,13 +156,13 @@ fn resolve_context_server_tool_name_conflicts( if duplicated_tool_names.is_empty() { return context_server_tools - .into_iter() + .iter() .map(|tool| (resolve_tool_name(tool).into(), tool.clone())) .collect(); } context_server_tools - .into_iter() + .iter() .filter_map(|tool| { let mut tool_name = resolve_tool_name(tool); if !duplicated_tool_names.contains(&tool_name) { diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs index f381103c27..ce3b639cb2 100644 --- a/crates/assistant_tools/src/assistant_tools.rs +++ b/crates/assistant_tools/src/assistant_tools.rs @@ -72,11 +72,10 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) { register_web_search_tool(&LanguageModelRegistry::global(cx), cx); cx.subscribe( &LanguageModelRegistry::global(cx), - move |registry, event, cx| match event { - language_model::Event::DefaultModelChanged => { + move |registry, event, cx| { + if let language_model::Event::DefaultModelChanged = event { register_web_search_tool(®istry, cx); } - _ => {} }, ) .detach(); diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 2d6b5ce924..33d08b4f88 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -1356,8 +1356,7 @@ mod tests { mode: mode.clone(), }; - let result = cx.update(|cx| resolve_path(&input, project, cx)); - result + cx.update(|cx| resolve_path(&input, project, cx)) } fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) { diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index dd0a0c8e4c..14bbcef8b4 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -216,7 +216,8 @@ impl Tool for TerminalTool { async move |cx| { let program = program.await; let env = env.await; - let terminal = project + + project .update(cx, |project, cx| { project.create_terminal( TerminalKind::Task(task::SpawnInTerminal { @@ -229,8 +230,7 @@ impl Tool for TerminalTool { cx, ) })? - .await; - terminal + .await } }); diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 57890628f2..925d5ddefb 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -494,11 +494,11 @@ mod linux { Ok(Fork::Parent(_)) => Ok(()), Ok(Fork::Child) => { unsafe { std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "") }; - if let Err(_) = fork::setsid() { + if fork::setsid().is_err() { eprintln!("failed to setsid: {}", std::io::Error::last_os_error()); process::exit(1); } - if let Err(_) = fork::close_fd() { + if fork::close_fd().is_err() { eprintln!("failed to close_fd: {}", std::io::Error::last_os_error()); } let error = @@ -534,8 +534,8 @@ mod flatpak { use std::process::Command; use std::{env, process}; - const EXTRA_LIB_ENV_NAME: &'static str = "ZED_FLATPAK_LIB_PATH"; - const NO_ESCAPE_ENV_NAME: &'static str = "ZED_FLATPAK_NO_ESCAPE"; + const EXTRA_LIB_ENV_NAME: &str = "ZED_FLATPAK_LIB_PATH"; + const NO_ESCAPE_ENV_NAME: &str = "ZED_FLATPAK_NO_ESCAPE"; /// Adds bundled libraries to LD_LIBRARY_PATH if running under flatpak pub fn ld_extra_libs() { diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 5a2c40b890..930e635dd8 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -4970,7 +4970,7 @@ async fn test_references( "Rust", FakeLspAdapter { name: "my-fake-lsp-adapter", - capabilities: capabilities, + capabilities, ..FakeLspAdapter::default() }, ); diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index 57f6341297..5fead5bcf1 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -397,11 +397,10 @@ impl MessageEditor { ) -> Option<(Anchor, String, &'static [StringMatchCandidate])> { static EMOJI_FUZZY_MATCH_CANDIDATES: LazyLock<Vec<StringMatchCandidate>> = LazyLock::new(|| { - let emojis = emojis::iter() + emojis::iter() .flat_map(|s| s.shortcodes()) .map(|emoji| StringMatchCandidate::new(0, emoji)) - .collect::<Vec<_>>(); - emojis + .collect::<Vec<_>>() }); let end_offset = end_anchor.to_offset(buffer.read(cx)); diff --git a/crates/context_server/src/listener.rs b/crates/context_server/src/listener.rs index f3c199a14e..6f4b5c1369 100644 --- a/crates/context_server/src/listener.rs +++ b/crates/context_server/src/listener.rs @@ -77,7 +77,7 @@ impl McpServer { socket_path, _server_task: server_task, tools, - handlers: handlers, + handlers, }) }) } diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 6e80ec484c..614cd0e05d 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -238,7 +238,7 @@ impl PythonDebugAdapter { return Err("Failed to create base virtual environment".into()); } - const DIR: &'static str = if cfg!(target_os = "windows") { + const DIR: &str = if cfg!(target_os = "windows") { "Scripts" } else { "bin" diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 4e1b0d19e2..6c70a935e0 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -257,7 +257,7 @@ impl DebugPanel { .as_ref() .map(|entity| entity.downgrade()), task_context: task_context.clone(), - worktree_id: worktree_id, + worktree_id, }); }; running.resolve_scenario( diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index 73cfef78cc..0fc003a14d 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -87,7 +87,7 @@ impl DebugSession { self.stack_trace_view.get_or_init(|| { let stackframe_list = running_state.read(cx).stack_frame_list().clone(); - let stack_frame_view = cx.new(|cx| { + cx.new(|cx| { StackTraceView::new( workspace.clone(), project.clone(), @@ -95,9 +95,7 @@ impl DebugSession { window, cx, ) - }); - - stack_frame_view + }) }) } diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 449deb4ddb..e3682ac991 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -358,7 +358,7 @@ pub(crate) fn new_debugger_pane( } }; - let ret = cx.new(move |cx| { + cx.new(move |cx| { let mut pane = Pane::new( workspace.clone(), project.clone(), @@ -562,9 +562,7 @@ pub(crate) fn new_debugger_pane( } }); pane - }); - - ret + }) } pub struct DebugTerminal { diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index 26a26c7bef..c17fffc42c 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -329,8 +329,8 @@ impl BreakpointList { let text = self.input.read(cx).text(cx); match mode { - ActiveBreakpointStripMode::Log => match &entry.kind { - BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { + ActiveBreakpointStripMode::Log => { + if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &entry.kind { Self::edit_line_breakpoint_inner( &self.breakpoint_store, line_breakpoint.breakpoint.path.clone(), @@ -339,10 +339,9 @@ impl BreakpointList { cx, ); } - _ => {} - }, - ActiveBreakpointStripMode::Condition => match &entry.kind { - BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { + } + ActiveBreakpointStripMode::Condition => { + if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &entry.kind { Self::edit_line_breakpoint_inner( &self.breakpoint_store, line_breakpoint.breakpoint.path.clone(), @@ -351,10 +350,9 @@ impl BreakpointList { cx, ); } - _ => {} - }, - ActiveBreakpointStripMode::HitCondition => match &entry.kind { - BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { + } + ActiveBreakpointStripMode::HitCondition => { + if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &entry.kind { Self::edit_line_breakpoint_inner( &self.breakpoint_store, line_breakpoint.breakpoint.path.clone(), @@ -363,8 +361,7 @@ impl BreakpointList { cx, ); } - _ => {} - }, + } } self.focus_handle.focus(window); } else { @@ -426,13 +423,10 @@ impl BreakpointList { return; }; - match &mut entry.kind { - BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { - let path = line_breakpoint.breakpoint.path.clone(); - let row = line_breakpoint.breakpoint.row; - self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx); - } - _ => {} + if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &mut entry.kind { + let path = line_breakpoint.breakpoint.path.clone(); + let row = line_breakpoint.breakpoint.row; + self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx); } cx.notify(); } @@ -967,7 +961,7 @@ impl LineBreakpoint { props, breakpoint: BreakpointEntry { kind: BreakpointEntryKind::LineBreakpoint(self.clone()), - weak: weak, + weak, }, is_selected, focus_handle, @@ -1179,7 +1173,7 @@ impl ExceptionBreakpoint { props, breakpoint: BreakpointEntry { kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()), - weak: weak, + weak, }, is_selected, focus_handle, diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index 3cc5fbc272..7461bffdf9 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -947,7 +947,7 @@ impl VariableList { #[track_caller] #[cfg(test)] pub(crate) fn assert_visual_entries(&self, expected: Vec<&str>) { - const INDENT: &'static str = " "; + const INDENT: &str = " "; let entries = &self.entries; let mut visual_entries = Vec::with_capacity(entries.len()); diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index fbbd529641..4cfdae093f 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -1445,11 +1445,8 @@ async fn test_variable_list_only_sends_requests_when_rendering( cx.run_until_parked(); - let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, _| { - let state = item.running_state().clone(); - - state - }); + let running_state = active_debug_session_panel(workspace, cx) + .update_in(cx, |item, _, _| item.running_state().clone()); client .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index 6ac0f49fad..99e588ada9 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -21,7 +21,7 @@ static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| { static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions); -const FRONT_MATTER_COMMENT: &'static str = "<!-- ZED_META {} -->"; +const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->"; fn main() -> Result<()> { zlog::init(); @@ -105,8 +105,8 @@ fn handle_preprocessing() -> Result<()> { template_and_validate_actions(&mut book, &mut errors); if !errors.is_empty() { - const ANSI_RED: &'static str = "\x1b[31m"; - const ANSI_RESET: &'static str = "\x1b[0m"; + const ANSI_RED: &str = "\x1b[31m"; + const ANSI_RESET: &str = "\x1b[0m"; for error in &errors { eprintln!("{ANSI_RED}ERROR{ANSI_RESET}: {}", error); } @@ -143,11 +143,8 @@ fn handle_frontmatter(book: &mut Book, errors: &mut HashSet<PreprocessorError>) &serde_json::to_string(&metadata).expect("Failed to serialize metadata"), ) }); - match new_content { - Cow::Owned(content) => { - chapter.content = content; - } - Cow::Borrowed(_) => {} + if let Cow::Owned(content) = new_content { + chapter.content = content; } }); } @@ -409,13 +406,13 @@ fn handle_postprocessing() -> Result<()> { .captures(contents) .with_context(|| format!("Failed to find title in {:?}", pretty_path)) .expect("Page has <title> element")[1]; - let title = title_tag_contents + + title_tag_contents .trim() .strip_suffix("- Zed") .unwrap_or(title_tag_contents) .trim() - .to_string(); - title + .to_string() } } diff --git a/crates/editor/src/display_map/invisibles.rs b/crates/editor/src/display_map/invisibles.rs index 19e4c2b42a..0712ddf9e2 100644 --- a/crates/editor/src/display_map/invisibles.rs +++ b/crates/editor/src/display_map/invisibles.rs @@ -61,14 +61,14 @@ pub fn replacement(c: char) -> Option<&'static str> { // but could if we tracked state in the classifier. const IDEOGRAPHIC_SPACE: char = '\u{3000}'; -const C0_SYMBOLS: &'static [&'static str] = &[ +const C0_SYMBOLS: &[&str] = &[ "␀", "␁", "␂", "␃", "␄", "␅", "␆", "␇", "␈", "␉", "␊", "␋", "␌", "␍", "␎", "␏", "␐", "␑", "␒", "␓", "␔", "␕", "␖", "␗", "␘", "␙", "␚", "␛", "␜", "␝", "␞", "␟", ]; -const DEL: &'static str = "␡"; +const DEL: &str = "␡"; // generated using ucd-generate: ucd-generate general-category --include Format --chars ucd-16.0.0 -pub const FORMAT: &'static [(char, char)] = &[ +pub const FORMAT: &[(char, char)] = &[ ('\u{ad}', '\u{ad}'), ('\u{600}', '\u{605}'), ('\u{61c}', '\u{61c}'), @@ -93,7 +93,7 @@ pub const FORMAT: &'static [(char, char)] = &[ ]; // hand-made base on https://invisible-characters.com (Excluding Cf) -pub const OTHER: &'static [(char, char)] = &[ +pub const OTHER: &[(char, char)] = &[ ('\u{034f}', '\u{034f}'), ('\u{115F}', '\u{1160}'), ('\u{17b4}', '\u{17b5}'), @@ -107,7 +107,7 @@ pub const OTHER: &'static [(char, char)] = &[ ]; // a subset of FORMAT/OTHER that may appear within glyphs -const PRESERVE: &'static [(char, char)] = &[ +const PRESERVE: &[(char, char)] = &[ ('\u{034f}', '\u{034f}'), ('\u{200d}', '\u{200d}'), ('\u{17b4}', '\u{17b5}'), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7c36a41046..3805904243 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1943,26 +1943,24 @@ impl Editor { let git_store = project.read(cx).git_store().clone(); let project = project.clone(); project_subscriptions.push(cx.subscribe(&git_store, move |this, _, event, cx| { - match event { - GitStoreEvent::RepositoryUpdated( - _, - RepositoryEvent::Updated { - new_instance: true, .. - }, - _, - ) => { - this.load_diff_task = Some( - update_uncommitted_diff_for_buffer( - cx.entity(), - &project, - this.buffer.read(cx).all_buffers(), - this.buffer.clone(), - cx, - ) - .shared(), - ); - } - _ => {} + if let GitStoreEvent::RepositoryUpdated( + _, + RepositoryEvent::Updated { + new_instance: true, .. + }, + _, + ) = event + { + this.load_diff_task = Some( + update_uncommitted_diff_for_buffer( + cx.entity(), + &project, + this.buffer.read(cx).all_buffers(), + this.buffer.clone(), + cx, + ) + .shared(), + ); } })); } @@ -3221,35 +3219,31 @@ impl Editor { selections.select_anchors(other_selections); }); - let other_subscription = - cx.subscribe(&other, |this, other, other_evt, cx| match other_evt { - EditorEvent::SelectionsChanged { local: true } => { - let other_selections = other.read(cx).selections.disjoint.to_vec(); - if other_selections.is_empty() { - return; - } - this.selections.change_with(cx, |selections| { - selections.select_anchors(other_selections); - }); + let other_subscription = cx.subscribe(&other, |this, other, other_evt, cx| { + if let EditorEvent::SelectionsChanged { local: true } = other_evt { + let other_selections = other.read(cx).selections.disjoint.to_vec(); + if other_selections.is_empty() { + return; } - _ => {} - }); + this.selections.change_with(cx, |selections| { + selections.select_anchors(other_selections); + }); + } + }); - let this_subscription = - cx.subscribe_self::<EditorEvent>(move |this, this_evt, cx| match this_evt { - EditorEvent::SelectionsChanged { local: true } => { - let these_selections = this.selections.disjoint.to_vec(); - if these_selections.is_empty() { - return; - } - other.update(cx, |other_editor, cx| { - other_editor.selections.change_with(cx, |selections| { - selections.select_anchors(these_selections); - }) - }); + let this_subscription = cx.subscribe_self::<EditorEvent>(move |this, this_evt, cx| { + if let EditorEvent::SelectionsChanged { local: true } = this_evt { + let these_selections = this.selections.disjoint.to_vec(); + if these_selections.is_empty() { + return; } - _ => {} - }); + other.update(cx, |other_editor, cx| { + other_editor.selections.change_with(cx, |selections| { + selections.select_anchors(these_selections); + }) + }); + } + }); Subscription::join(other_subscription, this_subscription) } @@ -5661,34 +5655,31 @@ impl Editor { let Ok(()) = editor.update_in(cx, |editor, window, cx| { // Newer menu already set, so exit. - match editor.context_menu.borrow().as_ref() { - Some(CodeContextMenu::Completions(prev_menu)) => { - if prev_menu.id > id { - return; - } - } - _ => {} + if let Some(CodeContextMenu::Completions(prev_menu)) = + editor.context_menu.borrow().as_ref() + && prev_menu.id > id + { + return; }; // Only valid to take prev_menu because it the new menu is immediately set // below, or the menu is hidden. - match editor.context_menu.borrow_mut().take() { - Some(CodeContextMenu::Completions(prev_menu)) => { - let position_matches = - if prev_menu.initial_position == menu.initial_position { - true - } else { - let snapshot = editor.buffer.read(cx).read(cx); - prev_menu.initial_position.to_offset(&snapshot) - == menu.initial_position.to_offset(&snapshot) - }; - if position_matches { - // Preserve markdown cache before `set_filter_results` because it will - // try to populate the documentation cache. - menu.preserve_markdown_cache(prev_menu); - } + if let Some(CodeContextMenu::Completions(prev_menu)) = + editor.context_menu.borrow_mut().take() + { + let position_matches = + if prev_menu.initial_position == menu.initial_position { + true + } else { + let snapshot = editor.buffer.read(cx).read(cx); + prev_menu.initial_position.to_offset(&snapshot) + == menu.initial_position.to_offset(&snapshot) + }; + if position_matches { + // Preserve markdown cache before `set_filter_results` because it will + // try to populate the documentation cache. + menu.preserve_markdown_cache(prev_menu); } - _ => {} }; menu.set_filter_results(matches, provider, window, cx); @@ -6179,12 +6170,11 @@ impl Editor { } }); Some(cx.background_spawn(async move { - let scenarios = futures::future::join_all(scenarios) + futures::future::join_all(scenarios) .await .into_iter() .flatten() - .collect::<Vec<_>>(); - scenarios + .collect::<Vec<_>>() })) }) .unwrap_or_else(|| Task::ready(vec![])) @@ -7740,12 +7730,9 @@ impl Editor { self.edit_prediction_settings = self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx); - match self.edit_prediction_settings { - EditPredictionSettings::Disabled => { - self.discard_edit_prediction(false, cx); - return None; - } - _ => {} + if let EditPredictionSettings::Disabled = self.edit_prediction_settings { + self.discard_edit_prediction(false, cx); + return None; }; self.edit_prediction_indent_conflict = multibuffer.is_line_whitespace_upto(cursor); @@ -10638,8 +10625,7 @@ impl Editor { .buffer_snapshot .anchor_after(Point::new(row, line_len)); - let bp = self - .breakpoint_store + self.breakpoint_store .as_ref()? .read_with(cx, |breakpoint_store, cx| { breakpoint_store @@ -10664,8 +10650,7 @@ impl Editor { None } }) - }); - bp + }) } pub fn edit_log_breakpoint( @@ -10701,7 +10686,7 @@ impl Editor { let cursors = self .selections .disjoint_anchors() - .into_iter() + .iter() .map(|selection| { let cursor_position: Point = selection.head().to_point(&snapshot.buffer_snapshot); @@ -14878,7 +14863,7 @@ impl Editor { let start = parent.start - offset; offset += parent.len() - text.len(); selections.push(Selection { - id: id, + id, start, end: start + text.len(), reversed: false, @@ -19202,7 +19187,7 @@ impl Editor { let locations = self .selections .all_anchors(cx) - .into_iter() + .iter() .map(|selection| Location { buffer: buffer.clone(), range: selection.start.text_anchor..selection.end.text_anchor, @@ -19914,11 +19899,8 @@ impl Editor { event: &SessionEvent, cx: &mut Context<Self>, ) { - match event { - SessionEvent::InvalidateInlineValue => { - self.refresh_inline_values(cx); - } - _ => {} + if let SessionEvent::InvalidateInlineValue = event { + self.refresh_inline_values(cx); } } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 685cc47cdb..1f1239ba0a 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -21037,7 +21037,7 @@ fn assert_breakpoint( let mut breakpoint = breakpoints .get(path) .unwrap() - .into_iter() + .iter() .map(|breakpoint| { ( breakpoint.row, @@ -23622,7 +23622,7 @@ pub fn handle_completion_request( complete_from_position ); Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList { - is_incomplete: is_incomplete, + is_incomplete, item_defaults: None, items: completions .iter() diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c14e49fc1d..f1ebd2c3df 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -724,7 +724,7 @@ impl EditorElement { ColumnarMode::FromMouse => true, ColumnarMode::FromSelection => false, }, - mode: mode, + mode, goal_column: point_for_position.exact_unclipped.column(), }, window, @@ -2437,14 +2437,13 @@ impl EditorElement { .unwrap_or_default() .padding as f32; - if let Some(edit_prediction) = editor.active_edit_prediction.as_ref() { - match &edit_prediction.completion { - EditPrediction::Edit { - display_mode: EditDisplayMode::TabAccept, - .. - } => padding += INLINE_ACCEPT_SUGGESTION_EM_WIDTHS, - _ => {} - } + if let Some(edit_prediction) = editor.active_edit_prediction.as_ref() + && let EditPrediction::Edit { + display_mode: EditDisplayMode::TabAccept, + .. + } = &edit_prediction.completion + { + padding += INLINE_ACCEPT_SUGGESTION_EM_WIDTHS } padding * em_width @@ -2978,8 +2977,8 @@ impl EditorElement { .ilog10() + 1; - let elements = buffer_rows - .into_iter() + buffer_rows + .iter() .enumerate() .map(|(ix, row_info)| { let ExpandInfo { @@ -3034,9 +3033,7 @@ impl EditorElement { Some((toggle, origin)) }) - .collect(); - - elements + .collect() } fn calculate_relative_line_numbers( @@ -3136,7 +3133,7 @@ impl EditorElement { let relative_rows = self.calculate_relative_line_numbers(snapshot, &rows, relative_to); let mut line_number = String::new(); let line_numbers = buffer_rows - .into_iter() + .iter() .enumerate() .flat_map(|(ix, row_info)| { let display_row = DisplayRow(rows.start.0 + ix as u32); @@ -3213,7 +3210,7 @@ impl EditorElement { && self.editor.read(cx).is_singleton(cx); if include_fold_statuses { row_infos - .into_iter() + .iter() .enumerate() .map(|(ix, info)| { if info.expand_info.is_some() { diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index 2f6106c86c..b11617ccec 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -213,8 +213,8 @@ impl GitBlame { let project_subscription = cx.subscribe(&project, { let buffer = buffer.clone(); - move |this, _, event, cx| match event { - project::Event::WorktreeUpdatedEntries(_, updated) => { + move |this, _, event, cx| { + if let project::Event::WorktreeUpdatedEntries(_, updated) = event { let project_entry_id = buffer.read(cx).entry_id(cx); if updated .iter() @@ -224,7 +224,6 @@ impl GitBlame { this.generate(cx); } } - _ => {} } }); @@ -292,7 +291,7 @@ impl GitBlame { let buffer_id = self.buffer_snapshot.remote_id(); let mut cursor = self.entries.cursor::<u32>(&()); - rows.into_iter().map(move |info| { + rows.iter().map(move |info| { let row = info .buffer_row .filter(|_| info.buffer_id == Some(buffer_id))?; diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index a8cdfa99df..bb3fd2830d 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -603,18 +603,15 @@ async fn parse_blocks( }) .join("\n\n"); - let rendered_block = cx - .new_window_entity(|_window, cx| { - Markdown::new( - combined_text.into(), - language_registry.cloned(), - language.map(|language| language.name()), - cx, - ) - }) - .ok(); - - rendered_block + cx.new_window_entity(|_window, cx| { + Markdown::new( + combined_text.into(), + language_registry.cloned(), + language.map(|language| language.name()), + cx, + ) + }) + .ok() } pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index e3d2f92c55..8957e0e99c 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1009,16 +1009,12 @@ impl Item for Editor { ) { self.workspace = Some((workspace.weak_handle(), workspace.database_id())); if let Some(workspace) = &workspace.weak_handle().upgrade() { - cx.subscribe( - workspace, - |editor, _, event: &workspace::Event, _cx| match event { - workspace::Event::ModalOpened => { - editor.mouse_context_menu.take(); - editor.inline_blame_popover.take(); - } - _ => {} - }, - ) + cx.subscribe(workspace, |editor, _, event: &workspace::Event, _cx| { + if let workspace::Event::ModalOpened = event { + editor.mouse_context_menu.take(); + editor.inline_blame_popover.take(); + } + }) .detach(); } } diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index 13e5d0a8c7..a3fc41228f 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -808,10 +808,7 @@ mod jsx_tag_autoclose_tests { ); buf }); - let buffer_c = cx.new(|cx| { - let buf = language::Buffer::local("<span", cx); - buf - }); + let buffer_c = cx.new(|cx| language::Buffer::local("<span", cx)); let buffer = cx.new(|cx| { let mut buf = MultiBuffer::new(language::Capability::ReadWrite); buf.push_excerpts( diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index e549f64758..c79feccb4b 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -241,24 +241,13 @@ impl ProposedChangesEditor { event: &BufferEvent, _cx: &mut Context<Self>, ) { - match event { - BufferEvent::Operation { .. } => { - self.recalculate_diffs_tx - .unbounded_send(RecalculateDiff { - buffer, - debounce: true, - }) - .ok(); - } - // BufferEvent::DiffBaseChanged => { - // self.recalculate_diffs_tx - // .unbounded_send(RecalculateDiff { - // buffer, - // debounce: false, - // }) - // .ok(); - // } - _ => (), + if let BufferEvent::Operation { .. } = event { + self.recalculate_diffs_tx + .unbounded_send(RecalculateDiff { + buffer, + debounce: true, + }) + .ok(); } } } diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 73c5f1c076..0a02390b64 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -119,8 +119,8 @@ impl SelectionsCollection { cx: &mut App, ) -> Option<Selection<D>> { let map = self.display_map(cx); - let selection = resolve_selections(self.pending_anchor().as_ref(), &map).next(); - selection + + resolve_selections(self.pending_anchor().as_ref(), &map).next() } pub(crate) fn pending_mode(&self) -> Option<SelectMode> { @@ -276,18 +276,18 @@ impl SelectionsCollection { cx: &mut App, ) -> Selection<D> { let map = self.display_map(cx); - let selection = resolve_selections([self.newest_anchor()], &map) + + resolve_selections([self.newest_anchor()], &map) .next() - .unwrap(); - selection + .unwrap() } pub fn newest_display(&self, cx: &mut App) -> Selection<DisplayPoint> { let map = self.display_map(cx); - let selection = resolve_selections_display([self.newest_anchor()], &map) + + resolve_selections_display([self.newest_anchor()], &map) .next() - .unwrap(); - selection + .unwrap() } pub fn oldest_anchor(&self) -> &Selection<Anchor> { @@ -303,10 +303,10 @@ impl SelectionsCollection { cx: &mut App, ) -> Selection<D> { let map = self.display_map(cx); - let selection = resolve_selections([self.oldest_anchor()], &map) + + resolve_selections([self.oldest_anchor()], &map) .next() - .unwrap(); - selection + .unwrap() } pub fn first_anchor(&self) -> Selection<Anchor> { diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs index dd9b4f8bba..bbbe54b43f 100644 --- a/crates/eval/src/instance.rs +++ b/crates/eval/src/instance.rs @@ -678,8 +678,8 @@ pub fn wait_for_lang_server( [ cx.subscribe(&lsp_store, { let log_prefix = log_prefix.clone(); - move |_, event, _| match event { - project::LspStoreEvent::LanguageServerUpdate { + move |_, event, _| { + if let project::LspStoreEvent::LanguageServerUpdate { message: client::proto::update_language_server::Variant::WorkProgress( LspWorkProgress { @@ -688,8 +688,10 @@ pub fn wait_for_lang_server( }, ), .. - } => println!("{}⟲ {message}", log_prefix), - _ => {} + } = event + { + println!("{}⟲ {message}", log_prefix) + } } }), cx.subscribe(project, { diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs index 432adaf4bc..3a3026f19c 100644 --- a/crates/extension/src/extension_builder.rs +++ b/crates/extension/src/extension_builder.rs @@ -484,14 +484,10 @@ impl ExtensionBuilder { _ => {} } - match &payload { - CustomSection(c) => { - if strip_custom_section(c.name()) { - continue; - } - } - - _ => {} + if let CustomSection(c) = &payload + && strip_custom_section(c.name()) + { + continue; } if let Some((id, range)) = payload.as_section() { RawSection { diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 1a05dbc570..4c3ab8d242 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1675,9 +1675,8 @@ impl ExtensionStore { let schema_path = &extension::build_debug_adapter_schema_path(adapter_name, meta); if fs.is_file(&src_dir.join(schema_path)).await { - match schema_path.parent() { - Some(parent) => fs.create_dir(&tmp_dir.join(parent)).await?, - None => {} + if let Some(parent) = schema_path.parent() { + fs.create_dir(&tmp_dir.join(parent)).await? } fs.copy_file( &src_dir.join(schema_path), diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index 4fe27aedc9..c5bc21fc1c 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -532,7 +532,7 @@ fn wasm_engine(executor: &BackgroundExecutor) -> wasmtime::Engine { // `Future::poll`. const EPOCH_INTERVAL: Duration = Duration::from_millis(100); let mut timer = Timer::interval(EPOCH_INTERVAL); - while let Some(_) = timer.next().await { + while (timer.next().await).is_some() { // Exit the loop and thread once the engine is dropped. let Some(engine) = engine_ref.upgrade() else { break; diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 7f0e8171f6..a6ee84eb60 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -863,7 +863,7 @@ impl ExtensionsPage { window: &mut Window, cx: &mut App, ) -> Entity<ContextMenu> { - let context_menu = ContextMenu::build(window, cx, |context_menu, window, _| { + ContextMenu::build(window, cx, |context_menu, window, _| { context_menu .entry( "Install Another Version...", @@ -887,9 +887,7 @@ impl ExtensionsPage { cx.write_to_clipboard(ClipboardItem::new_string(authors.join(", "))); } }) - }); - - context_menu + }) } fn show_extension_version_list( diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index 77acdf8097..ffe3d42a27 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -112,7 +112,7 @@ impl OpenPathDelegate { entries, .. } => user_input - .into_iter() + .iter() .filter(|user_input| !user_input.exists || !user_input.is_dir) .map(|user_input| user_input.file.string.clone()) .chain(self.string_matches.iter().filter_map(|string_match| { diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index d17cbdcf51..11177512c3 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -2419,12 +2419,11 @@ impl Fs for FakeFs { let watcher = watcher.clone(); move |events| { let result = events.iter().any(|evt_path| { - let result = watcher + watcher .prefixes .lock() .iter() - .any(|prefix| evt_path.path.starts_with(prefix)); - result + .any(|prefix| evt_path.path.starts_with(prefix)) }); let executor = executor.clone(); async move { diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index edcad514bb..9c125d2c47 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -2028,7 +2028,7 @@ fn parse_branch_input(input: &str) -> Result<Vec<Branch>> { branches.push(Branch { is_head: is_current_branch, - ref_name: ref_name, + ref_name, most_recent_commit: Some(CommitSummary { sha: head_sha, subject, diff --git a/crates/git_ui/src/file_diff_view.rs b/crates/git_ui/src/file_diff_view.rs index f7d29cdfa7..a320888b3b 100644 --- a/crates/git_ui/src/file_diff_view.rs +++ b/crates/git_ui/src/file_diff_view.rs @@ -123,7 +123,7 @@ impl FileDiffView { old_buffer, new_buffer, _recalculate_diff_task: cx.spawn(async move |this, cx| { - while let Ok(_) = buffer_changes_rx.recv().await { + while buffer_changes_rx.recv().await.is_ok() { loop { let mut timer = cx .background_executor() diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index ace3a8eb15..3eae1acb04 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -426,7 +426,7 @@ impl GitPanel { let git_store = project.read(cx).git_store().clone(); let active_repository = project.read(cx).active_repository(cx); - let git_panel = cx.new(|cx| { + cx.new(|cx| { let focus_handle = cx.focus_handle(); cx.on_focus(&focus_handle, window, Self::focus_in).detach(); cx.on_focus_out(&focus_handle, window, |this, _, window, cx| { @@ -563,9 +563,7 @@ impl GitPanel { this.schedule_update(false, window, cx); this - }); - - git_panel + }) } fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) { @@ -1198,14 +1196,13 @@ impl GitPanel { window, cx, ); - cx.spawn(async move |this, cx| match prompt.await { - Ok(RestoreCancel::RestoreTrackedFiles) => { + cx.spawn(async move |this, cx| { + if let Ok(RestoreCancel::RestoreTrackedFiles) = prompt.await { this.update(cx, |this, cx| { this.perform_checkout(entries, cx); }) .ok(); } - _ => {} }) .detach(); } diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index c12ef58ce2..cc1535b7c3 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -346,22 +346,19 @@ impl ProjectDiff { window: &mut Window, cx: &mut Context<Self>, ) { - match event { - EditorEvent::SelectionsChanged { local: true } => { - let Some(project_path) = self.active_path(cx) else { - return; - }; - self.workspace - .update(cx, |workspace, cx| { - if let Some(git_panel) = workspace.panel::<GitPanel>(cx) { - git_panel.update(cx, |git_panel, cx| { - git_panel.select_entry_by_path(project_path, window, cx) - }) - } - }) - .ok(); - } - _ => {} + if let EditorEvent::SelectionsChanged { local: true } = event { + let Some(project_path) = self.active_path(cx) else { + return; + }; + self.workspace + .update(cx, |workspace, cx| { + if let Some(git_panel) = workspace.panel::<GitPanel>(cx) { + git_panel.update(cx, |git_panel, cx| { + git_panel.select_entry_by_path(project_path, window, cx) + }) + } + }) + .ok(); } if editor.focus_handle(cx).contains_focused(window, cx) && self.multibuffer.read(cx).is_empty() @@ -513,7 +510,7 @@ impl ProjectDiff { mut recv: postage::watch::Receiver<()>, cx: &mut AsyncWindowContext, ) -> Result<()> { - while let Some(_) = recv.next().await { + while (recv.next().await).is_some() { let buffers_to_load = this.update(cx, |this, cx| this.load_buffers(cx))?; for buffer_to_load in buffers_to_load { if let Some(buffer) = buffer_to_load.await.log_err() { diff --git a/crates/git_ui/src/text_diff_view.rs b/crates/git_ui/src/text_diff_view.rs index d07868c3e1..e38e3698d5 100644 --- a/crates/git_ui/src/text_diff_view.rs +++ b/crates/git_ui/src/text_diff_view.rs @@ -207,7 +207,7 @@ impl TextDiffView { path: Some(format!("Clipboard ↔ {selection_location_path}").into()), buffer_changes_tx, _recalculate_diff_task: cx.spawn(async move |_, cx| { - while let Ok(_) = buffer_changes_rx.recv().await { + while buffer_changes_rx.recv().await.is_ok() { loop { let mut timer = cx .background_executor() diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 2be1a34e49..bbd59fa7bc 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1707,8 +1707,8 @@ impl App { .unwrap_or_else(|| { is_first = true; let future = A::load(source.clone(), self); - let task = self.background_executor().spawn(future).shared(); - task + + self.background_executor().spawn(future).shared() }); self.loading_assets.insert(asset_id, Box::new(task.clone())); diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index c58f72267c..b5e0712796 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -326,7 +326,7 @@ impl TextLayout { vec![text_style.to_run(text.len())] }; - let layout_id = window.request_measured_layout(Default::default(), { + window.request_measured_layout(Default::default(), { let element_state = self.clone(); move |known_dimensions, available_space, window, cx| { @@ -416,9 +416,7 @@ impl TextLayout { size } - }); - - layout_id + }) } fn prepaint(&self, bounds: Bounds<Pixels>, text: &str) { diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 3278dfbe38..4d31428094 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -949,11 +949,8 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr { }; drop(state); - match event { - wl_callback::Event::Done { .. } => { - window.frame(); - } - _ => {} + if let wl_callback::Event::Done { .. } = event { + window.frame(); } } } @@ -2014,25 +2011,22 @@ impl Dispatch<wl_data_offer::WlDataOffer, ()> for WaylandClientStatePtr { let client = this.get_client(); let mut state = client.borrow_mut(); - match event { - wl_data_offer::Event::Offer { mime_type } => { - // Drag and drop - if mime_type == FILE_LIST_MIME_TYPE { - let serial = state.serial_tracker.get(SerialKind::DataDevice); - let mime_type = mime_type.clone(); - data_offer.accept(serial, Some(mime_type)); - } - - // Clipboard - if let Some(offer) = state - .data_offers - .iter_mut() - .find(|wrapper| wrapper.inner.id() == data_offer.id()) - { - offer.add_mime_type(mime_type); - } + if let wl_data_offer::Event::Offer { mime_type } = event { + // Drag and drop + if mime_type == FILE_LIST_MIME_TYPE { + let serial = state.serial_tracker.get(SerialKind::DataDevice); + let mime_type = mime_type.clone(); + data_offer.accept(serial, Some(mime_type)); + } + + // Clipboard + if let Some(offer) = state + .data_offers + .iter_mut() + .find(|wrapper| wrapper.inner.id() == data_offer.id()) + { + offer.add_mime_type(mime_type); } - _ => {} } } } @@ -2113,13 +2107,10 @@ impl Dispatch<zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1, ()> let client = this.get_client(); let mut state = client.borrow_mut(); - match event { - zwp_primary_selection_offer_v1::Event::Offer { mime_type } => { - if let Some(offer) = state.primary_data_offer.as_mut() { - offer.add_mime_type(mime_type); - } - } - _ => {} + if let zwp_primary_selection_offer_v1::Event::Offer { mime_type } = event + && let Some(offer) = state.primary_data_offer.as_mut() + { + offer.add_mime_type(mime_type); } } } diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 1d1166a56c..ce1468335d 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -355,85 +355,82 @@ impl WaylandWindowStatePtr { } pub fn handle_xdg_surface_event(&self, event: xdg_surface::Event) { - match event { - xdg_surface::Event::Configure { serial } => { - { - let mut state = self.state.borrow_mut(); - if let Some(window_controls) = state.in_progress_window_controls.take() { - state.window_controls = window_controls; - - drop(state); - let mut callbacks = self.callbacks.borrow_mut(); - if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() { - appearance_changed(); - } - } - } - { - let mut state = self.state.borrow_mut(); - - if let Some(mut configure) = state.in_progress_configure.take() { - let got_unmaximized = state.maximized && !configure.maximized; - state.fullscreen = configure.fullscreen; - state.maximized = configure.maximized; - state.tiling = configure.tiling; - // Limit interactive resizes to once per vblank - if configure.resizing && state.resize_throttle { - return; - } else if configure.resizing { - state.resize_throttle = true; - } - if !configure.fullscreen && !configure.maximized { - configure.size = if got_unmaximized { - Some(state.window_bounds.size) - } else { - compute_outer_size(state.inset(), configure.size, state.tiling) - }; - if let Some(size) = configure.size { - state.window_bounds = Bounds { - origin: Point::default(), - size, - }; - } - } - drop(state); - if let Some(size) = configure.size { - self.resize(size); - } - } - } + if let xdg_surface::Event::Configure { serial } = event { + { let mut state = self.state.borrow_mut(); - state.xdg_surface.ack_configure(serial); + if let Some(window_controls) = state.in_progress_window_controls.take() { + state.window_controls = window_controls; - let window_geometry = inset_by_tiling( - state.bounds.map_origin(|_| px(0.0)), - state.inset(), - state.tiling, - ) - .map(|v| v.0 as i32) - .map_size(|v| if v <= 0 { 1 } else { v }); - - state.xdg_surface.set_window_geometry( - window_geometry.origin.x, - window_geometry.origin.y, - window_geometry.size.width, - window_geometry.size.height, - ); - - let request_frame_callback = !state.acknowledged_first_configure; - if request_frame_callback { - state.acknowledged_first_configure = true; drop(state); - self.frame(); + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() { + appearance_changed(); + } } } - _ => {} + { + let mut state = self.state.borrow_mut(); + + if let Some(mut configure) = state.in_progress_configure.take() { + let got_unmaximized = state.maximized && !configure.maximized; + state.fullscreen = configure.fullscreen; + state.maximized = configure.maximized; + state.tiling = configure.tiling; + // Limit interactive resizes to once per vblank + if configure.resizing && state.resize_throttle { + return; + } else if configure.resizing { + state.resize_throttle = true; + } + if !configure.fullscreen && !configure.maximized { + configure.size = if got_unmaximized { + Some(state.window_bounds.size) + } else { + compute_outer_size(state.inset(), configure.size, state.tiling) + }; + if let Some(size) = configure.size { + state.window_bounds = Bounds { + origin: Point::default(), + size, + }; + } + } + drop(state); + if let Some(size) = configure.size { + self.resize(size); + } + } + } + let mut state = self.state.borrow_mut(); + state.xdg_surface.ack_configure(serial); + + let window_geometry = inset_by_tiling( + state.bounds.map_origin(|_| px(0.0)), + state.inset(), + state.tiling, + ) + .map(|v| v.0 as i32) + .map_size(|v| if v <= 0 { 1 } else { v }); + + state.xdg_surface.set_window_geometry( + window_geometry.origin.x, + window_geometry.origin.y, + window_geometry.size.width, + window_geometry.size.height, + ); + + let request_frame_callback = !state.acknowledged_first_configure; + if request_frame_callback { + state.acknowledged_first_configure = true; + drop(state); + self.frame(); + } } } pub fn handle_toplevel_decoration_event(&self, event: zxdg_toplevel_decoration_v1::Event) { - match event { - zxdg_toplevel_decoration_v1::Event::Configure { mode } => match mode { + if let zxdg_toplevel_decoration_v1::Event::Configure { mode } = event { + match mode { WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => { self.state.borrow_mut().decorations = WindowDecorations::Server; if let Some(mut appearance_changed) = @@ -457,17 +454,13 @@ impl WaylandWindowStatePtr { WEnum::Unknown(v) => { log::warn!("Unknown decoration mode: {}", v); } - }, - _ => {} + } } } pub fn handle_fractional_scale_event(&self, event: wp_fractional_scale_v1::Event) { - match event { - wp_fractional_scale_v1::Event::PreferredScale { scale } => { - self.rescale(scale as f32 / 120.0); - } - _ => {} + if let wp_fractional_scale_v1::Event::PreferredScale { scale } = event { + self.rescale(scale as f32 / 120.0); } } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index e422af961f..346ba8718b 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -232,15 +232,12 @@ impl X11ClientStatePtr { }; let mut state = client.0.borrow_mut(); - if let Some(window_ref) = state.windows.remove(&x_window) { - match window_ref.refresh_state { - Some(RefreshState::PeriodicRefresh { - event_loop_token, .. - }) => { - state.loop_handle.remove(event_loop_token); - } - _ => {} - } + if let Some(window_ref) = state.windows.remove(&x_window) + && let Some(RefreshState::PeriodicRefresh { + event_loop_token, .. + }) = window_ref.refresh_state + { + state.loop_handle.remove(event_loop_token); } if state.mouse_focused_window == Some(x_window) { state.mouse_focused_window = None; @@ -876,22 +873,19 @@ impl X11Client { let Some(reply) = reply else { return Some(()); }; - match str::from_utf8(&reply.value) { - Ok(file_list) => { - let paths: SmallVec<[_; 2]> = file_list - .lines() - .filter_map(|path| Url::parse(path).log_err()) - .filter_map(|url| url.to_file_path().log_err()) - .collect(); - let input = PlatformInput::FileDrop(FileDropEvent::Entered { - position: state.xdnd_state.position, - paths: crate::ExternalPaths(paths), - }); - drop(state); - window.handle_input(input); - self.0.borrow_mut().xdnd_state.retrieved = true; - } - Err(_) => {} + if let Ok(file_list) = str::from_utf8(&reply.value) { + let paths: SmallVec<[_; 2]> = file_list + .lines() + .filter_map(|path| Url::parse(path).log_err()) + .filter_map(|url| url.to_file_path().log_err()) + .collect(); + let input = PlatformInput::FileDrop(FileDropEvent::Entered { + position: state.xdnd_state.position, + paths: crate::ExternalPaths(paths), + }); + drop(state); + window.handle_input(input); + self.0.borrow_mut().xdnd_state.retrieved = true; } } Event::ConfigureNotify(event) => { diff --git a/crates/gpui/src/platform/mac/events.rs b/crates/gpui/src/platform/mac/events.rs index 50a516cb38..938db4b762 100644 --- a/crates/gpui/src/platform/mac/events.rs +++ b/crates/gpui/src/platform/mac/events.rs @@ -426,7 +426,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { key_char = Some(chars_for_modified_key(native_event.keyCode(), mods)); } - let mut key = if shift + if shift && chars_ignoring_modifiers .chars() .all(|c| c.is_ascii_lowercase()) @@ -437,9 +437,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { chars_with_shift } else { chars_ignoring_modifiers - }; - - key + } } }; diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index bc60e13a59..cd923a1859 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -2063,8 +2063,8 @@ fn screen_point_to_gpui_point(this: &Object, position: NSPoint) -> Point<Pixels> let frame = get_frame(this); let window_x = position.x - frame.origin.x; let window_y = frame.size.height - (position.y - frame.origin.y); - let position = point(px(window_x as f32), px(window_y as f32)); - position + + point(px(window_x as f32), px(window_y as f32)) } extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation { diff --git a/crates/gpui/src/platform/scap_screen_capture.rs b/crates/gpui/src/platform/scap_screen_capture.rs index 32041b655f..d6d19cd810 100644 --- a/crates/gpui/src/platform/scap_screen_capture.rs +++ b/crates/gpui/src/platform/scap_screen_capture.rs @@ -228,7 +228,7 @@ fn run_capture( display, size, })); - if let Err(_) = stream_send_result { + if stream_send_result.is_err() { return; } while !cancel_stream.load(std::sync::atomic::Ordering::SeqCst) { diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index c3bb8bb22b..607163b577 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -1128,22 +1128,19 @@ impl WindowsWindowInner { && let Some(parameter_string) = unsafe { parameter.to_string() }.log_err() { log::info!("System settings changed: {}", parameter_string); - match parameter_string.as_str() { - "ImmersiveColorSet" => { - let new_appearance = system_appearance() - .context("unable to get system appearance when handling ImmersiveColorSet") - .log_err()?; - let mut lock = self.state.borrow_mut(); - if new_appearance != lock.appearance { - lock.appearance = new_appearance; - let mut callback = lock.callbacks.appearance_changed.take()?; - drop(lock); - callback(); - self.state.borrow_mut().callbacks.appearance_changed = Some(callback); - configure_dwm_dark_mode(handle, new_appearance); - } + if parameter_string.as_str() == "ImmersiveColorSet" { + let new_appearance = system_appearance() + .context("unable to get system appearance when handling ImmersiveColorSet") + .log_err()?; + let mut lock = self.state.borrow_mut(); + if new_appearance != lock.appearance { + lock.appearance = new_appearance; + let mut callback = lock.callbacks.appearance_changed.take()?; + drop(lock); + callback(); + self.state.borrow_mut().callbacks.appearance_changed = Some(callback); + configure_dwm_dark_mode(handle, new_appearance); } - _ => {} } } Some(0) @@ -1469,7 +1466,7 @@ pub(crate) fn current_modifiers() -> Modifiers { #[inline] pub(crate) fn current_capslock() -> Capslock { let on = unsafe { GetKeyState(VK_CAPITAL.0 as i32) & 1 } > 0; - Capslock { on: on } + Capslock { on } } fn get_client_area_insets( diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index f78d6b30c7..f198bb7718 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -58,23 +58,21 @@ impl TaffyLayoutEngine { children: &[LayoutId], ) -> LayoutId { let taffy_style = style.to_taffy(rem_size); - let layout_id = if children.is_empty() { + + if children.is_empty() { self.taffy .new_leaf(taffy_style) .expect(EXPECT_MESSAGE) .into() } else { - let parent_id = self - .taffy + self.taffy // This is safe because LayoutId is repr(transparent) to taffy::tree::NodeId. .new_with_children(taffy_style, unsafe { std::mem::transmute::<&[LayoutId], &[taffy::NodeId]>(children) }) .expect(EXPECT_MESSAGE) - .into(); - parent_id - }; - layout_id + .into() + } } pub fn request_measured_layout( @@ -91,8 +89,7 @@ impl TaffyLayoutEngine { ) -> LayoutId { let taffy_style = style.to_taffy(rem_size); - let layout_id = self - .taffy + self.taffy .new_leaf_with_context( taffy_style, NodeContext { @@ -100,8 +97,7 @@ impl TaffyLayoutEngine { }, ) .expect(EXPECT_MESSAGE) - .into(); - layout_id + .into() } // Used to understand performance diff --git a/crates/gpui/src/text_system/line_wrapper.rs b/crates/gpui/src/text_system/line_wrapper.rs index 648d714c89..93ec6c854c 100644 --- a/crates/gpui/src/text_system/line_wrapper.rs +++ b/crates/gpui/src/text_system/line_wrapper.rs @@ -44,7 +44,7 @@ impl LineWrapper { let mut prev_c = '\0'; let mut index = 0; let mut candidates = fragments - .into_iter() + .iter() .flat_map(move |fragment| fragment.wrap_boundary_candidates()) .peekable(); iter::from_fn(move || { diff --git a/crates/gpui/src/util.rs b/crates/gpui/src/util.rs index f357034fbf..3d7fa06e6c 100644 --- a/crates/gpui/src/util.rs +++ b/crates/gpui/src/util.rs @@ -58,13 +58,7 @@ pub trait FluentBuilder { where Self: Sized, { - self.map(|this| { - if let Some(_) = option { - this - } else { - then(this) - } - }) + self.map(|this| if option.is_some() { this } else { then(this) }) } } diff --git a/crates/gpui_macros/src/test.rs b/crates/gpui_macros/src/test.rs index 0153c5889a..648d3499ed 100644 --- a/crates/gpui_macros/src/test.rs +++ b/crates/gpui_macros/src/test.rs @@ -86,7 +86,7 @@ impl Parse for Args { Ok(Args { seeds, max_retries, - max_iterations: max_iterations, + max_iterations, on_failure_fn_name, }) } diff --git a/crates/http_client/src/http_client.rs b/crates/http_client/src/http_client.rs index a7f75b0962..62468573ed 100644 --- a/crates/http_client/src/http_client.rs +++ b/crates/http_client/src/http_client.rs @@ -435,8 +435,7 @@ impl HttpClient for FakeHttpClient { &self, req: Request<AsyncBody>, ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> { - let future = (self.handler.lock().as_ref().unwrap())(req); - future + ((self.handler.lock().as_ref().unwrap())(req)) as _ } fn user_agent(&self) -> Option<&HeaderValue> { diff --git a/crates/jj/src/jj_repository.rs b/crates/jj/src/jj_repository.rs index 93ae79eb90..afbe54c99d 100644 --- a/crates/jj/src/jj_repository.rs +++ b/crates/jj/src/jj_repository.rs @@ -50,16 +50,13 @@ impl RealJujutsuRepository { impl JujutsuRepository for RealJujutsuRepository { fn list_bookmarks(&self) -> Vec<Bookmark> { - let bookmarks = self - .repository + self.repository .view() .bookmarks() .map(|(ref_name, _target)| Bookmark { ref_name: ref_name.as_str().to_string().into(), }) - .collect(); - - bookmarks + .collect() } } diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 53887eb736..81dc36093b 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -195,11 +195,9 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap } fn journal_dir(path: &str) -> Option<PathBuf> { - let expanded_journal_dir = shellexpand::full(path) //TODO handle this better + shellexpand::full(path) //TODO handle this better .ok() - .map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal")); - - expanded_journal_dir + .map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal")) } fn heading_entry(now: NaiveTime, hour_format: &Option<HourFormat>) -> String { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 972a90ddab..cc96022e63 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1128,7 +1128,7 @@ impl Buffer { } else { ranges.as_slice() } - .into_iter() + .iter() .peekable(); let mut edits = Vec::new(); @@ -1395,7 +1395,8 @@ impl Buffer { is_first = false; return true; } - let any_sub_ranges_contain_range = layer + + layer .included_sub_ranges .map(|sub_ranges| { sub_ranges.iter().any(|sub_range| { @@ -1404,9 +1405,7 @@ impl Buffer { !is_before_start && !is_after_end }) }) - .unwrap_or(true); - let result = any_sub_ranges_contain_range; - result + .unwrap_or(true) }) .last() .map(|info| info.language.clone()) @@ -2616,7 +2615,7 @@ impl Buffer { self.completion_triggers = self .completion_triggers_per_language_server .values() - .flat_map(|triggers| triggers.into_iter().cloned()) + .flat_map(|triggers| triggers.iter().cloned()) .collect(); } else { self.completion_triggers_per_language_server @@ -2776,7 +2775,7 @@ impl Buffer { self.completion_triggers = self .completion_triggers_per_language_server .values() - .flat_map(|triggers| triggers.into_iter().cloned()) + .flat_map(|triggers| triggers.iter().cloned()) .collect(); } else { self.completion_triggers_per_language_server diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 68addc804e..b70e466246 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -1513,9 +1513,8 @@ impl Language { .map(|ix| { let mut config = BracketsPatternConfig::default(); for setting in query.property_settings(ix) { - match setting.key.as_ref() { - "newline.only" => config.newline_only = true, - _ => {} + if setting.key.as_ref() == "newline.only" { + config.newline_only = true } } config diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index 70e42cb02d..b10529c3d9 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -300,7 +300,7 @@ impl From<AnthropicError> for LanguageModelCompletionError { }, AnthropicError::ServerOverloaded { retry_after } => Self::ServerOverloaded { provider, - retry_after: retry_after, + retry_after, }, AnthropicError::ApiError(api_error) => api_error.into(), } diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 1a5c09cdc4..4348fd4211 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -404,7 +404,7 @@ pub fn into_open_ai( match content { MessageContent::Text(text) | MessageContent::Thinking { text, .. } => { add_message_content_part( - open_ai::MessagePart::Text { text: text }, + open_ai::MessagePart::Text { text }, message.role, &mut messages, ) diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 2c490b45cf..ac653d5b2e 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -234,7 +234,7 @@ impl JsonLspAdapter { schemas .as_array_mut() .unwrap() - .extend(cx.all_action_names().into_iter().map(|&name| { + .extend(cx.all_action_names().iter().map(|&name| { project::lsp_store::json_language_server_ext::url_schema_for_action(name) })); diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 906e45bb3a..6c92d78525 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -711,7 +711,7 @@ impl Default for PythonToolchainProvider { } } -static ENV_PRIORITY_LIST: &'static [PythonEnvironmentKind] = &[ +static ENV_PRIORITY_LIST: &[PythonEnvironmentKind] = &[ // Prioritize non-Conda environments. PythonEnvironmentKind::Poetry, PythonEnvironmentKind::Pipenv, diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 7f65ccf5ea..162e3bea78 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -5205,13 +5205,9 @@ impl MultiBufferSnapshot { if offset == diff_transforms.start().0 && bias == Bias::Left && let Some(prev_item) = diff_transforms.prev_item() + && let DiffTransform::DeletedHunk { .. } = prev_item { - match prev_item { - DiffTransform::DeletedHunk { .. } => { - diff_transforms.prev(); - } - _ => {} - } + diff_transforms.prev(); } let offset_in_transform = offset - diff_transforms.start().0; let mut excerpt_offset = diff_transforms.start().1; diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 871c72ea0b..9d41eb1562 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -76,9 +76,8 @@ impl NodeRuntime { let mut state = self.0.lock().await; let options = loop { - match state.options.borrow().as_ref() { - Some(options) => break options.clone(), - None => {} + if let Some(options) = state.options.borrow().as_ref() { + break options.clone(); } match state.options.changed().await { Ok(()) => {} diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index d700fa08bd..672bcf1cd9 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -19,7 +19,7 @@ use util::ResultExt; use workspace::{ModalView, Workspace}; use zed_actions::agent::OpenSettings; -const FEATURED_PROVIDERS: [&'static str; 4] = ["anthropic", "google", "openai", "ollama"]; +const FEATURED_PROVIDERS: [&str; 4] = ["anthropic", "google", "openai", "ollama"]; fn render_llm_provider_section( tab_index: &mut isize, @@ -410,7 +410,7 @@ impl AiPrivacyTooltip { impl Render for AiPrivacyTooltip { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { - const DESCRIPTION: &'static str = "We believe in opt-in data sharing as the default for building AI products, rather than opt-out. We'll only use or store your data if you affirmatively send it to us. "; + const DESCRIPTION: &str = "We believe in opt-in data sharing as the default for building AI products, rather than opt-out. We'll only use or store your data if you affirmatively send it to us. "; tooltip_container(window, cx, move |this, _, _| { this.child( diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 8d89c6662e..77a70dfc8d 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -16,8 +16,8 @@ use vim_mode_setting::VimModeSetting; use crate::theme_preview::{ThemePreviewStyle, ThemePreviewTile}; -const LIGHT_THEMES: [&'static str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"]; -const DARK_THEMES: [&'static str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"]; +const LIGHT_THEMES: [&str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"]; +const DARK_THEMES: [&str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"]; const FAMILY_NAMES: [SharedString; 3] = [ SharedString::new_static("One"), SharedString::new_static("Ayu"), @@ -114,7 +114,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement let themes = theme_names.map(|theme| theme_registry.get(theme).unwrap()); - let theme_previews = [0, 1, 2].map(|index| { + [0, 1, 2].map(|index| { let theme = &themes[index]; let is_selected = theme.name == current_theme_name; let name = theme.name.clone(); @@ -176,9 +176,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement .color(Color::Muted) .size(LabelSize::Small), ) - }); - - theme_previews + }) } fn write_mode_change(mode: ThemeMode, cx: &mut App) { diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index d941a0315a..60a9856abe 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -605,7 +605,7 @@ fn render_popular_settings_section( window: &mut Window, cx: &mut App, ) -> impl IntoElement { - const LIGATURE_TOOLTIP: &'static str = + const LIGATURE_TOOLTIP: &str = "Font ligatures combine two characters into one. For example, turning =/= into ≠."; v_flex() diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 78f512f7f3..891ae1595d 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -733,7 +733,8 @@ impl OutlinePanel { ) -> Entity<Self> { let project = workspace.project().clone(); let workspace_handle = cx.entity().downgrade(); - let outline_panel = cx.new(|cx| { + + cx.new(|cx| { let filter_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); editor.set_placeholder_text("Filter...", cx); @@ -912,9 +913,7 @@ impl OutlinePanel { outline_panel.replace_active_editor(item, editor, window, cx); } outline_panel - }); - - outline_panel + }) } fn serialization_key(workspace: &Workspace) -> Option<String> { @@ -2624,7 +2623,7 @@ impl OutlinePanel { } fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &App) -> String { - let name = match self.project.read(cx).worktree_for_id(*worktree_id, cx) { + match self.project.read(cx).worktree_for_id(*worktree_id, cx) { Some(worktree) => { let worktree = worktree.read(cx); match worktree.snapshot().root_entry() { @@ -2645,8 +2644,7 @@ impl OutlinePanel { } } None => file_name(entry.path.as_ref()), - }; - name + } } fn update_fs_entries( @@ -2681,7 +2679,8 @@ impl OutlinePanel { new_collapsed_entries = outline_panel.collapsed_entries.clone(); new_unfolded_dirs = outline_panel.unfolded_dirs.clone(); let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx); - let buffer_excerpts = multi_buffer_snapshot.excerpts().fold( + + multi_buffer_snapshot.excerpts().fold( HashMap::default(), |mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| { let buffer_id = buffer_snapshot.remote_id(); @@ -2728,8 +2727,7 @@ impl OutlinePanel { ); buffer_excerpts }, - ); - buffer_excerpts + ) }) else { return; }; @@ -4807,7 +4805,7 @@ impl OutlinePanel { .with_compute_indents_fn(cx.entity(), |outline_panel, range, _, _| { let entries = outline_panel.cached_entries.get(range); if let Some(entries) = entries { - entries.into_iter().map(|item| item.depth).collect() + entries.iter().map(|item| item.depth).collect() } else { smallvec::SmallVec::new() } diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 296749c14e..d365089377 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -413,13 +413,10 @@ impl LocalBufferStore { cx: &mut Context<BufferStore>, ) { cx.subscribe(worktree, |this, worktree, event, cx| { - if worktree.read(cx).is_local() { - match event { - worktree::Event::UpdatedEntries(changes) => { - Self::local_worktree_entries_changed(this, &worktree, changes, cx); - } - _ => {} - } + if worktree.read(cx).is_local() + && let worktree::Event::UpdatedEntries(changes) = event + { + Self::local_worktree_entries_changed(this, &worktree, changes, cx); } }) .detach(); @@ -947,10 +944,9 @@ impl BufferStore { } pub fn get_by_path(&self, path: &ProjectPath) -> Option<Entity<Buffer>> { - self.path_to_buffer_id.get(path).and_then(|buffer_id| { - let buffer = self.get(*buffer_id); - buffer - }) + self.path_to_buffer_id + .get(path) + .and_then(|buffer_id| self.get(*buffer_id)) } pub fn get(&self, buffer_id: BufferId) -> Option<Entity<Buffer>> { diff --git a/crates/project/src/color_extractor.rs b/crates/project/src/color_extractor.rs index dbbd3d7b99..6e9907e30b 100644 --- a/crates/project/src/color_extractor.rs +++ b/crates/project/src/color_extractor.rs @@ -4,8 +4,8 @@ use gpui::{Hsla, Rgba}; use lsp::{CompletionItem, Documentation}; use regex::{Regex, RegexBuilder}; -const HEX: &'static str = r#"(#(?:[\da-fA-F]{3}){1,2})"#; -const RGB_OR_HSL: &'static str = r#"(rgba?|hsla?)\(\s*(\d{1,3}%?)\s*,\s*(\d{1,3}%?)\s*,\s*(\d{1,3}%?)\s*(?:,\s*(1|0?\.\d+))?\s*\)"#; +const HEX: &str = r#"(#(?:[\da-fA-F]{3}){1,2})"#; +const RGB_OR_HSL: &str = r#"(rgba?|hsla?)\(\s*(\d{1,3}%?)\s*,\s*(\d{1,3}%?)\s*,\s*(\d{1,3}%?)\s*(?:,\s*(1|0?\.\d+))?\s*\)"#; static RELAXED_HEX_REGEX: LazyLock<Regex> = LazyLock::new(|| { RegexBuilder::new(HEX) @@ -141,7 +141,7 @@ mod tests { use gpui::rgba; use lsp::{CompletionItem, CompletionItemKind}; - pub const COLOR_TABLE: &[(&'static str, Option<u32>)] = &[ + pub const COLOR_TABLE: &[(&str, Option<u32>)] = &[ // -- Invalid -- // Invalid hex ("f0f", None), diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index f80f24bb71..16625caeb4 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -642,8 +642,8 @@ mod tests { #[gpui::test] async fn test_context_server_status(cx: &mut TestAppContext) { - const SERVER_1_ID: &'static str = "mcp-1"; - const SERVER_2_ID: &'static str = "mcp-2"; + const SERVER_1_ID: &str = "mcp-1"; + const SERVER_2_ID: &str = "mcp-2"; let (_fs, project) = setup_context_server_test( cx, @@ -722,8 +722,8 @@ mod tests { #[gpui::test] async fn test_context_server_status_events(cx: &mut TestAppContext) { - const SERVER_1_ID: &'static str = "mcp-1"; - const SERVER_2_ID: &'static str = "mcp-2"; + const SERVER_1_ID: &str = "mcp-1"; + const SERVER_2_ID: &str = "mcp-2"; let (_fs, project) = setup_context_server_test( cx, @@ -784,7 +784,7 @@ mod tests { #[gpui::test(iterations = 25)] async fn test_context_server_concurrent_starts(cx: &mut TestAppContext) { - const SERVER_1_ID: &'static str = "mcp-1"; + const SERVER_1_ID: &str = "mcp-1"; let (_fs, project) = setup_context_server_test( cx, @@ -845,8 +845,8 @@ mod tests { #[gpui::test] async fn test_context_server_maintain_servers_loop(cx: &mut TestAppContext) { - const SERVER_1_ID: &'static str = "mcp-1"; - const SERVER_2_ID: &'static str = "mcp-2"; + const SERVER_1_ID: &str = "mcp-1"; + const SERVER_2_ID: &str = "mcp-2"; let server_1_id = ContextServerId(SERVER_1_ID.into()); let server_2_id = ContextServerId(SERVER_2_ID.into()); @@ -1084,7 +1084,7 @@ mod tests { #[gpui::test] async fn test_context_server_enabled_disabled(cx: &mut TestAppContext) { - const SERVER_1_ID: &'static str = "mcp-1"; + const SERVER_1_ID: &str = "mcp-1"; let server_1_id = ContextServerId(SERVER_1_ID.into()); diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index ccda64fba8..382e83587a 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -470,9 +470,8 @@ impl DapStore { session_id: impl Borrow<SessionId>, ) -> Option<Entity<session::Session>> { let session_id = session_id.borrow(); - let client = self.sessions.get(session_id).cloned(); - client + self.sessions.get(session_id).cloned() } pub fn sessions(&self) -> impl Iterator<Item = &Entity<Session>> { self.sessions.values() diff --git a/crates/project/src/debugger/locators/go.rs b/crates/project/src/debugger/locators/go.rs index 61436fce8f..eec06084ec 100644 --- a/crates/project/src/debugger/locators/go.rs +++ b/crates/project/src/debugger/locators/go.rs @@ -174,7 +174,7 @@ impl DapLocator for GoLocator { request: "launch".to_string(), mode: "test".to_string(), program, - args: args, + args, build_flags, cwd: build_config.cwd.clone(), env: build_config.env.clone(), @@ -185,7 +185,7 @@ impl DapLocator for GoLocator { label: resolved_label.to_string().into(), adapter: adapter.0.clone(), build: None, - config: config, + config, tcp_connection: None, }) } @@ -220,7 +220,7 @@ impl DapLocator for GoLocator { request: "launch".to_string(), mode: "debug".to_string(), program, - args: args, + args, build_flags, }) .unwrap(); diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index ee5baf1d3b..cd792877b6 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -226,7 +226,7 @@ impl RunningMode { fn unset_breakpoints_from_paths(&self, paths: &Vec<Arc<Path>>, cx: &mut App) -> Task<()> { let tasks: Vec<_> = paths - .into_iter() + .iter() .map(|path| { self.request(dap_command::SetBreakpoints { source: client_source(path), @@ -508,13 +508,12 @@ impl RunningMode { .ok(); } - let ret = if configuration_done_supported { + if configuration_done_supported { this.request(ConfigurationDone {}) } else { Task::ready(Ok(())) } - .await; - ret + .await } }); @@ -839,7 +838,7 @@ impl Session { }) .detach(); - let this = Self { + Self { mode: SessionState::Booting(None), id: session_id, child_session_ids: HashSet::default(), @@ -868,9 +867,7 @@ impl Session { task_context, memory: memory::Memory::new(), quirks, - }; - - this + } }) } diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs index 54d87d230c..c5a198954e 100644 --- a/crates/project/src/image_store.rs +++ b/crates/project/src/image_store.rs @@ -446,15 +446,12 @@ impl ImageStore { event: &ImageItemEvent, cx: &mut Context<Self>, ) { - match event { - ImageItemEvent::FileHandleChanged => { - if let Some(local) = self.state.as_local() { - local.update(cx, |local, cx| { - local.image_changed_file(image, cx); - }) - } - } - _ => {} + if let ImageItemEvent::FileHandleChanged = event + && let Some(local) = self.state.as_local() + { + local.update(cx, |local, cx| { + local.image_changed_file(image, cx); + }) } } } @@ -531,13 +528,10 @@ impl ImageStoreImpl for Entity<LocalImageStore> { impl LocalImageStore { fn subscribe_to_worktree(&mut self, worktree: &Entity<Worktree>, cx: &mut Context<Self>) { cx.subscribe(worktree, |this, worktree, event, cx| { - if worktree.read(cx).is_local() { - match event { - worktree::Event::UpdatedEntries(changes) => { - this.local_worktree_entries_changed(&worktree, changes, cx); - } - _ => {} - } + if worktree.read(cx).is_local() + && let worktree::Event::UpdatedEntries(changes) = event + { + this.local_worktree_entries_changed(&worktree, changes, cx); } }) .detach(); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 217e00ee96..de6848701f 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2501,8 +2501,8 @@ pub(crate) fn parse_completion_text_edit( }; Some(ParsedCompletionEdit { - insert_range: insert_range, - replace_range: replace_range, + insert_range, + replace_range, new_text: new_text.clone(), }) } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index e6ea01ff9a..a8c6ffd878 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -550,7 +550,7 @@ impl LocalLspStore { if let Some(settings) = settings.binary.as_ref() { if let Some(arguments) = &settings.arguments { - binary.arguments = arguments.into_iter().map(Into::into).collect(); + binary.arguments = arguments.iter().map(Into::into).collect(); } if let Some(env) = &settings.env { shell_env.extend(env.iter().map(|(k, v)| (k.clone(), v.clone()))); @@ -1060,8 +1060,8 @@ impl LocalLspStore { }; let delegate: Arc<dyn ManifestDelegate> = Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); - let root = self - .lsp_tree + + self.lsp_tree .get( project_path, language.name(), @@ -1069,9 +1069,7 @@ impl LocalLspStore { &delegate, cx, ) - .collect::<Vec<_>>(); - - root + .collect::<Vec<_>>() } fn language_server_ids_for_buffer( @@ -2397,7 +2395,8 @@ impl LocalLspStore { let server_id = server_node.server_id_or_init(|disposition| { let path = &disposition.path; - let server_id = { + + { let uri = Url::from_file_path(worktree.read(cx).abs_path().join(&path.path)); @@ -2415,9 +2414,7 @@ impl LocalLspStore { state.add_workspace_folder(uri); }; server_id - }; - - server_id + } })?; let server_state = self.language_servers.get(&server_id)?; if let LanguageServerState::Running { @@ -3047,16 +3044,14 @@ impl LocalLspStore { buffer.edit([(range, text)], None, cx); } - let transaction = buffer.end_transaction(cx).and_then(|transaction_id| { + buffer.end_transaction(cx).and_then(|transaction_id| { if push_to_history { buffer.finalize_last_transaction(); buffer.get_transaction(transaction_id).cloned() } else { buffer.forget_transaction(transaction_id) } - }); - - transaction + }) })?; if let Some(transaction) = transaction { project_transaction.0.insert(buffer_to_edit, transaction); @@ -4370,13 +4365,11 @@ impl LspStore { if let Some((client, downstream_project_id)) = self.downstream_client.clone() && let Some(diangostic_summaries) = self.diagnostic_summaries.get(&worktree.id()) { - let mut summaries = diangostic_summaries - .into_iter() - .flat_map(|(path, summaries)| { - summaries - .into_iter() - .map(|(server_id, summary)| summary.to_proto(*server_id, path)) - }); + let mut summaries = diangostic_summaries.iter().flat_map(|(path, summaries)| { + summaries + .iter() + .map(|(server_id, summary)| summary.to_proto(*server_id, path)) + }); if let Some(summary) = summaries.next() { client .send(proto::UpdateDiagnosticSummary { @@ -4564,7 +4557,7 @@ impl LspStore { anyhow::anyhow!(message) })?; - let response = request + request .response_from_lsp( response, this.upgrade().context("no app context")?, @@ -4572,8 +4565,7 @@ impl LspStore { language_server.server_id(), cx.clone(), ) - .await; - response + .await }) } @@ -4853,7 +4845,7 @@ impl LspStore { push_to_history: bool, cx: &mut Context<Self>, ) -> Task<anyhow::Result<ProjectTransaction>> { - if let Some(_) = self.as_local() { + if self.as_local().is_some() { cx.spawn(async move |lsp_store, cx| { let buffers = buffers.into_iter().collect::<Vec<_>>(); let result = LocalLspStore::execute_code_action_kind_locally( @@ -7804,7 +7796,7 @@ impl LspStore { } None => { diagnostics_summary = Some(proto::UpdateDiagnosticSummary { - project_id: project_id, + project_id, worktree_id: worktree_id.to_proto(), summary: Some(proto::DiagnosticSummary { path: project_path.path.as_ref().to_proto(), @@ -10054,7 +10046,7 @@ impl LspStore { cx: &mut Context<Self>, ) -> Task<anyhow::Result<ProjectTransaction>> { let logger = zlog::scoped!("format"); - if let Some(_) = self.as_local() { + if self.as_local().is_some() { zlog::trace!(logger => "Formatting locally"); let logger = zlog::scoped!(logger => "local"); let buffers = buffers diff --git a/crates/project/src/manifest_tree.rs b/crates/project/src/manifest_tree.rs index 750815c477..ced9b34d93 100644 --- a/crates/project/src/manifest_tree.rs +++ b/crates/project/src/manifest_tree.rs @@ -43,12 +43,9 @@ impl WorktreeRoots { match event { WorktreeEvent::UpdatedEntries(changes) => { for (path, _, kind) in changes.iter() { - match kind { - worktree::PathChange::Removed => { - let path = TriePath::from(path.as_ref()); - this.roots.remove(&path); - } - _ => {} + if kind == &worktree::PathChange::Removed { + let path = TriePath::from(path.as_ref()); + this.roots.remove(&path); } } } @@ -197,11 +194,8 @@ impl ManifestTree { evt: &WorktreeStoreEvent, _: &mut Context<Self>, ) { - match evt { - WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => { - self.root_points.remove(worktree_id); - } - _ => {} + if let WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) = evt { + self.root_points.remove(worktree_id); } } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f9ad7b96d3..6712b3fab0 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2885,14 +2885,11 @@ impl Project { event: &DapStoreEvent, cx: &mut Context<Self>, ) { - match event { - DapStoreEvent::Notification(message) => { - cx.emit(Event::Toast { - notification_id: "dap".into(), - message: message.clone(), - }); - } - _ => {} + if let DapStoreEvent::Notification(message) = event { + cx.emit(Event::Toast { + notification_id: "dap".into(), + message: message.clone(), + }); } } @@ -3179,14 +3176,11 @@ impl Project { event: &ImageItemEvent, cx: &mut Context<Self>, ) -> Option<()> { - match event { - ImageItemEvent::ReloadNeeded => { - if !self.is_via_collab() { - self.reload_images([image.clone()].into_iter().collect(), cx) - .detach_and_log_err(cx); - } - } - _ => {} + if let ImageItemEvent::ReloadNeeded = event + && !self.is_via_collab() + { + self.reload_images([image.clone()].into_iter().collect(), cx) + .detach_and_log_err(cx); } None diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 5137d64fab..eb1e3828e9 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -695,7 +695,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { assert_eq!( buffer .completion_triggers() - .into_iter() + .iter() .cloned() .collect::<Vec<_>>(), &[".".to_string(), "::".to_string()] @@ -747,7 +747,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { assert_eq!( buffer .completion_triggers() - .into_iter() + .iter() .cloned() .collect::<Vec<_>>(), &[":".to_string()] @@ -766,7 +766,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { assert_eq!( buffer .completion_triggers() - .into_iter() + .iter() .cloned() .collect::<Vec<_>>(), &[".".to_string(), "::".to_string()] diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index 8d8a1bd008..e51f8e0b3b 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -110,7 +110,7 @@ impl<T: InventoryContents> InventoryFor<T> { fn global_scenarios(&self) -> impl '_ + Iterator<Item = (TaskSourceKind, T)> { self.global.iter().flat_map(|(file_path, templates)| { - templates.into_iter().map(|template| { + templates.iter().map(|template| { ( TaskSourceKind::AbsPath { id_base: Cow::Owned(format!("global {}", T::GLOBAL_SOURCE_FILE)), diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 212d2dd2d9..b2556d7584 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -67,13 +67,11 @@ pub struct SshDetails { impl Project { pub fn active_project_directory(&self, cx: &App) -> Option<Arc<Path>> { - let worktree = self - .active_entry() + self.active_entry() .and_then(|entry_id| self.worktree_for_entry(entry_id, cx)) .into_iter() .chain(self.worktrees(cx)) - .find_map(|tree| tree.read(cx).root_dir()); - worktree + .find_map(|tree| tree.read(cx).root_dir()) } pub fn first_project_directory(&self, cx: &App) -> Option<PathBuf> { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 9a87874ed8..dc92ee8c70 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -3589,7 +3589,7 @@ impl ProjectPanel { previous_components.next(); } - if let Some(_) = suffix_components { + if suffix_components.is_some() { new_path.push(previous_components); } if let Some(str) = new_path.to_str() { @@ -4422,9 +4422,7 @@ impl ProjectPanel { let components = Path::new(&file_name) .components() .map(|comp| { - let comp_str = - comp.as_os_str().to_string_lossy().into_owned(); - comp_str + comp.as_os_str().to_string_lossy().into_owned() }) .collect::<Vec<_>>(); diff --git a/crates/recent_projects/src/disconnected_overlay.rs b/crates/recent_projects/src/disconnected_overlay.rs index 9b79d3ce9c..dd4d788cfd 100644 --- a/crates/recent_projects/src/disconnected_overlay.rs +++ b/crates/recent_projects/src/disconnected_overlay.rs @@ -88,11 +88,8 @@ impl DisconnectedOverlay { self.finished = true; cx.emit(DismissEvent); - match &self.host { - Host::SshRemoteProject(ssh_connection_options) => { - self.reconnect_to_ssh_remote(ssh_connection_options.clone(), window, cx); - } - _ => {} + if let Host::SshRemoteProject(ssh_connection_options) = &self.host { + self.reconnect_to_ssh_remote(ssh_connection_options.clone(), window, cx); } } diff --git a/crates/remote/src/protocol.rs b/crates/remote/src/protocol.rs index 787094781d..e5a9c5b7a5 100644 --- a/crates/remote/src/protocol.rs +++ b/crates/remote/src/protocol.rs @@ -31,8 +31,8 @@ pub async fn read_message<S: AsyncRead + Unpin>( stream.read_exact(buffer).await?; let len = message_len_from_buffer(buffer); - let result = read_message_with_len(stream, buffer, len).await; - result + + read_message_with_len(stream, buffer, len).await } pub async fn write_message<S: AsyncWrite + Unpin>( diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 85150f629e..6fc327ac1c 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -194,15 +194,11 @@ impl HeadlessProject { languages.clone(), ); - cx.subscribe( - &buffer_store, - |_this, _buffer_store, event, cx| match event { - BufferStoreEvent::BufferAdded(buffer) => { - cx.subscribe(buffer, Self::on_buffer_event).detach(); - } - _ => {} - }, - ) + cx.subscribe(&buffer_store, |_this, _buffer_store, event, cx| { + if let BufferStoreEvent::BufferAdded(buffer) = event { + cx.subscribe(buffer, Self::on_buffer_event).detach(); + } + }) .detach(); let extensions = HeadlessExtensionStore::new( @@ -285,18 +281,17 @@ impl HeadlessProject { event: &BufferEvent, cx: &mut Context<Self>, ) { - match event { - BufferEvent::Operation { - operation, - is_local: true, - } => cx - .background_spawn(self.session.request(proto::UpdateBuffer { - project_id: SSH_PROJECT_ID, - buffer_id: buffer.read(cx).remote_id().to_proto(), - operations: vec![serialize_operation(operation)], - })) - .detach(), - _ => {} + if let BufferEvent::Operation { + operation, + is_local: true, + } = event + { + cx.background_spawn(self.session.request(proto::UpdateBuffer { + project_id: SSH_PROJECT_ID, + buffer_id: buffer.read(cx).remote_id().to_proto(), + operations: vec![serialize_operation(operation)], + })) + .detach() } } diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 9315536e6b..15a465a880 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -334,7 +334,7 @@ fn start_server( let (mut stdin_msg_tx, mut stdin_msg_rx) = mpsc::unbounded::<Envelope>(); cx.background_spawn(async move { while let Ok(msg) = read_message(&mut stdin_stream, &mut input_buffer).await { - if let Err(_) = stdin_msg_tx.send(msg).await { + if (stdin_msg_tx.send(msg).await).is_err() { break; } } @@ -891,7 +891,8 @@ pub fn handle_settings_file_changes( fn read_proxy_settings(cx: &mut Context<HeadlessProject>) -> Option<Url> { let proxy_str = ProxySettings::get_global(cx).proxy.to_owned(); - let proxy_url = proxy_str + + proxy_str .as_ref() .and_then(|input: &String| { input @@ -899,8 +900,7 @@ fn read_proxy_settings(cx: &mut Context<HeadlessProject>) -> Option<Url> { .inspect_err(|e| log::error!("Error parsing proxy settings: {}", e)) .ok() }) - .or_else(read_proxy_from_env); - proxy_url + .or_else(read_proxy_from_env) } fn daemonize() -> Result<ControlFlow<()>> { diff --git a/crates/repl/src/components/kernel_options.rs b/crates/repl/src/components/kernel_options.rs index b8fd2e57f2..714cb3aed3 100644 --- a/crates/repl/src/components/kernel_options.rs +++ b/crates/repl/src/components/kernel_options.rs @@ -269,10 +269,9 @@ where }; let picker_view = cx.new(|cx| { - let picker = Picker::uniform_list(delegate, window, cx) + Picker::uniform_list(delegate, window, cx) .width(rems(30.)) - .max_height(Some(rems(20.).into())); - picker + .max_height(Some(rems(20.).into())) }); PopoverMenu::new("kernel-switcher") diff --git a/crates/repl/src/notebook/cell.rs b/crates/repl/src/notebook/cell.rs index 15179a632c..87b8e1d55a 100644 --- a/crates/repl/src/notebook/cell.rs +++ b/crates/repl/src/notebook/cell.rs @@ -91,7 +91,7 @@ fn convert_outputs( cx: &mut App, ) -> Vec<Output> { outputs - .into_iter() + .iter() .map(|output| match output { nbformat::v4::Output::Stream { text, .. } => Output::Stream { content: cx.new(|cx| TerminalOutput::from(&text.0, window, cx)), diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index a84f147dd2..325d262d9e 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -584,8 +584,8 @@ impl project::ProjectItem for NotebookItem { Ok(nbformat::Notebook::Legacy(legacy_notebook)) => { // TODO: Decide if we want to mutate the notebook by including Cell IDs // and any other conversions - let notebook = nbformat::upgrade_legacy_notebook(legacy_notebook)?; - notebook + + nbformat::upgrade_legacy_notebook(legacy_notebook)? } // Bad notebooks and notebooks v4.0 and below are not supported Err(e) => { diff --git a/crates/repl/src/outputs/plain.rs b/crates/repl/src/outputs/plain.rs index 74c7bfa3c3..ae3c728c8a 100644 --- a/crates/repl/src/outputs/plain.rs +++ b/crates/repl/src/outputs/plain.rs @@ -68,7 +68,7 @@ pub fn text_style(window: &mut Window, cx: &mut App) -> TextStyle { let theme = cx.theme(); - let text_style = TextStyle { + TextStyle { font_family, font_features, font_weight, @@ -81,9 +81,7 @@ pub fn text_style(window: &mut Window, cx: &mut App) -> TextStyle { // These are going to be overridden per-cell color: theme.colors().terminal_foreground, ..Default::default() - }; - - text_style + } } /// Returns the default terminal size for the terminal output. diff --git a/crates/rope/src/chunk.rs b/crates/rope/src/chunk.rs index 96f7d1db11..e3c7d6f750 100644 --- a/crates/rope/src/chunk.rs +++ b/crates/rope/src/chunk.rs @@ -543,7 +543,7 @@ impl Iterator for Tabs { // Since tabs are 1 byte the tab offset is the same as the byte offset let position = TabPosition { byte_offset: tab_offset, - char_offset: char_offset, + char_offset, }; // Remove the tab we've just seen self.tabs ^= 1 << tab_offset; diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index 355deb5d20..bebe4315e4 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -49,7 +49,7 @@ actions!( ] ); -const BUILT_IN_TOOLTIP_TEXT: &'static str = concat!( +const BUILT_IN_TOOLTIP_TEXT: &str = concat!( "This rule supports special functionality.\n", "It's read-only, but you can remove it from your default rules." ); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index b6836324db..0886654d62 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1113,8 +1113,8 @@ impl ProjectSearchView { .await .log_err(); } - let should_search = result != 2; - should_search + + result != 2 } else { true }; diff --git a/crates/settings/src/key_equivalents.rs b/crates/settings/src/key_equivalents.rs index bf08de97ae..6580137535 100644 --- a/crates/settings/src/key_equivalents.rs +++ b/crates/settings/src/key_equivalents.rs @@ -1415,7 +1415,7 @@ pub fn get_key_equivalents(layout: &str) -> Option<HashMap<char, char>> { _ => return None, }; - Some(HashMap::from_iter(mappings.into_iter().cloned())) + Some(HashMap::from_iter(mappings.iter().cloned())) } #[cfg(not(target_os = "macos"))] diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs index c102b303c1..a472c50e6c 100644 --- a/crates/settings/src/settings_json.rs +++ b/crates/settings/src/settings_json.rs @@ -295,9 +295,9 @@ fn replace_value_in_json_text( } } -const TS_DOCUMENT_KIND: &'static str = "document"; -const TS_ARRAY_KIND: &'static str = "array"; -const TS_COMMENT_KIND: &'static str = "comment"; +const TS_DOCUMENT_KIND: &str = "document"; +const TS_ARRAY_KIND: &str = "array"; +const TS_COMMENT_KIND: &str = "comment"; pub fn replace_top_level_array_value_in_json_text( text: &str, diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 457d58e5a7..12e3c0c274 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -621,8 +621,7 @@ impl KeymapEditor { let key_bindings_ptr = cx.key_bindings(); let lock = key_bindings_ptr.borrow(); let key_bindings = lock.bindings(); - let mut unmapped_action_names = - HashSet::from_iter(cx.all_action_names().into_iter().copied()); + let mut unmapped_action_names = HashSet::from_iter(cx.all_action_names().iter().copied()); let action_documentation = cx.action_documentation(); let mut generator = KeymapFile::action_schema_generator(); let actions_with_schemas = HashSet::from_iter( @@ -1289,7 +1288,7 @@ struct HumanizedActionNameCache { impl HumanizedActionNameCache { fn new(cx: &App) -> Self { - let cache = HashMap::from_iter(cx.all_action_names().into_iter().map(|&action_name| { + let cache = HashMap::from_iter(cx.all_action_names().iter().map(|&action_name| { ( action_name, command_palette::humanize_action_name(action_name).into(), @@ -1857,18 +1856,15 @@ impl Render for KeymapEditor { mouse_down_event: &gpui::MouseDownEvent, window, cx| { - match mouse_down_event.button { - MouseButton::Right => { - this.select_index( - row_index, None, window, cx, - ); - this.create_context_menu( - mouse_down_event.position, - window, - cx, - ); - } - _ => {} + if mouse_down_event.button == MouseButton::Right { + this.select_index( + row_index, None, window, cx, + ); + this.create_context_menu( + mouse_down_event.position, + window, + cx, + ); } }, )) diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/settings_ui/src/ui_components/keystroke_input.rs index 66593524a3..1b8010853e 100644 --- a/crates/settings_ui/src/ui_components/keystroke_input.rs +++ b/crates/settings_ui/src/ui_components/keystroke_input.rs @@ -19,7 +19,7 @@ actions!( ] ); -const KEY_CONTEXT_VALUE: &'static str = "KeystrokeInput"; +const KEY_CONTEXT_VALUE: &str = "KeystrokeInput"; const CLOSE_KEYSTROKE_CAPTURE_END_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(300); diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index a91d497572..9d7bb07360 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/src/ui_components/table.rs @@ -213,7 +213,7 @@ impl TableInteractionState { let mut column_ix = 0; let resizable_columns_slice = *resizable_columns; - let mut resizable_columns = resizable_columns.into_iter(); + let mut resizable_columns = resizable_columns.iter(); let dividers = intersperse_with(spacers, || { window.with_id(column_ix, |window| { @@ -801,7 +801,7 @@ impl<const COLS: usize> Table<COLS> { ) -> Self { self.rows = TableContents::UniformList(UniformListData { element_id: id.into(), - row_count: row_count, + row_count, render_item_fn: Box::new(render_item_fn), }); self diff --git a/crates/storybook/src/story_selector.rs b/crates/storybook/src/story_selector.rs index fd0be97ff6..aad3875410 100644 --- a/crates/storybook/src/story_selector.rs +++ b/crates/storybook/src/story_selector.rs @@ -109,15 +109,13 @@ static ALL_STORY_SELECTORS: OnceLock<Vec<StorySelector>> = OnceLock::new(); impl ValueEnum for StorySelector { fn value_variants<'a>() -> &'a [Self] { - let stories = ALL_STORY_SELECTORS.get_or_init(|| { + (ALL_STORY_SELECTORS.get_or_init(|| { let component_stories = ComponentStory::iter().map(StorySelector::Component); component_stories .chain(std::iter::once(StorySelector::KitchenSink)) .collect::<Vec<_>>() - }); - - stories + })) as _ } fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> { diff --git a/crates/svg_preview/src/svg_preview_view.rs b/crates/svg_preview/src/svg_preview_view.rs index 4e4c83c8de..12dd97f0c8 100644 --- a/crates/svg_preview/src/svg_preview_view.rs +++ b/crates/svg_preview/src/svg_preview_view.rs @@ -157,18 +157,15 @@ impl SvgPreviewView { &active_editor, window, |this: &mut SvgPreviewView, _editor, event: &EditorEvent, window, cx| { - match event { - EditorEvent::Saved => { - // Remove cached image to force reload - if let Some(svg_path) = &this.svg_path { - let resource = Resource::Path(svg_path.clone().into()); - this.image_cache.update(cx, |cache, cx| { - cache.remove(&resource, window, cx); - }); - } - cx.notify(); + if event == &EditorEvent::Saved { + // Remove cached image to force reload + if let Some(svg_path) = &this.svg_path { + let resource = Resource::Path(svg_path.clone().into()); + this.image_cache.update(cx, |cache, cx| { + cache.remove(&resource, window, cx); + }); } - _ => {} + cx.notify(); } }, ); @@ -184,22 +181,18 @@ impl SvgPreviewView { event: &workspace::Event, _window, cx| { - match event { - workspace::Event::ActiveItemChanged => { - let workspace_read = workspace.read(cx); - if let Some(active_item) = workspace_read.active_item(cx) - && let Some(editor_entity) = - active_item.downcast::<Editor>() - && Self::is_svg_file(&editor_entity, cx) - { - let new_path = Self::get_svg_path(&editor_entity, cx); - if this.svg_path != new_path { - this.svg_path = new_path; - cx.notify(); - } + if let workspace::Event::ActiveItemChanged = event { + let workspace_read = workspace.read(cx); + if let Some(active_item) = workspace_read.active_item(cx) + && let Some(editor_entity) = active_item.downcast::<Editor>() + && Self::is_svg_file(&editor_entity, cx) + { + let new_path = Self::get_svg_path(&editor_entity, cx); + if this.svg_path != new_path { + this.svg_path = new_path; + cx.notify(); } } - _ => {} } }, ) diff --git a/crates/task/src/shell_builder.rs b/crates/task/src/shell_builder.rs index b8c49d4230..5ed29fd733 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -237,13 +237,11 @@ impl ShellBuilder { task_args: &Vec<String>, ) -> (String, Vec<String>) { if let Some(task_command) = task_command { - let combined_command = task_args - .into_iter() - .fold(task_command, |mut command, arg| { - command.push(' '); - command.push_str(&self.kind.to_shell_variable(arg)); - command - }); + let combined_command = task_args.iter().fold(task_command, |mut command, arg| { + command.push(' '); + command.push_str(&self.kind.to_shell_variable(arg)); + command + }); self.args .extend(self.kind.args_for_shell(self.interactive, combined_command)); diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index c4b0931c35..9fbdc152f3 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -550,7 +550,7 @@ impl PickerDelegate for TasksModalDelegate { list_item.tooltip(move |_, _| item_label.clone()) }) .map(|item| { - let item = if matches!(source_kind, TaskSourceKind::UserInput) + if matches!(source_kind, TaskSourceKind::UserInput) || Some(ix) <= self.divider_index { let task_index = hit.candidate_id; @@ -579,8 +579,7 @@ impl PickerDelegate for TasksModalDelegate { item.end_hover_slot(delete_button) } else { item - }; - item + } }) .toggle_state(selected) .child(highlighted_location.render(window, cx)), diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index c50e2bd3a7..1d76f70152 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -236,7 +236,7 @@ impl TerminalPanel { ) -> Result<Entity<Self>> { let mut terminal_panel = None; - match workspace + if let Some((database_id, serialization_key)) = workspace .read_with(&cx, |workspace, _| { workspace .database_id() @@ -244,34 +244,29 @@ impl TerminalPanel { }) .ok() .flatten() + && let Some(serialized_panel) = cx + .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) }) + .await + .log_err() + .flatten() + .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel)) + .transpose() + .log_err() + .flatten() + && let Ok(serialized) = workspace + .update_in(&mut cx, |workspace, window, cx| { + deserialize_terminal_panel( + workspace.weak_handle(), + workspace.project().clone(), + database_id, + serialized_panel, + window, + cx, + ) + })? + .await { - Some((database_id, serialization_key)) => { - if let Some(serialized_panel) = cx - .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) }) - .await - .log_err() - .flatten() - .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel)) - .transpose() - .log_err() - .flatten() - && let Ok(serialized) = workspace - .update_in(&mut cx, |workspace, window, cx| { - deserialize_terminal_panel( - workspace.weak_handle(), - workspace.project().clone(), - database_id, - serialized_panel, - window, - cx, - ) - })? - .await - { - terminal_panel = Some(serialized); - } - } - _ => {} + terminal_panel = Some(serialized); } let terminal_panel = if let Some(panel) = terminal_panel { @@ -629,7 +624,7 @@ impl TerminalPanel { workspace .read(cx) .panes() - .into_iter() + .iter() .cloned() .flat_map(pane_terminal_views), ) diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index f434e46159..956bcebfd0 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1937,7 +1937,8 @@ impl SearchableItem for TerminalView { // Selection head might have a value if there's a selection that isn't // associated with a match. Therefore, if there are no matches, we should // report None, no matter the state of the terminal - let res = if !matches.is_empty() { + + if !matches.is_empty() { if let Some(selection_head) = self.terminal().read(cx).selection_head { // If selection head is contained in a match. Return that match match direction { @@ -1977,9 +1978,7 @@ impl SearchableItem for TerminalView { } } else { None - }; - - res + } } fn replace( &mut self, diff --git a/crates/theme/src/icon_theme.rs b/crates/theme/src/icon_theme.rs index 5bd69c1733..c21709559a 100644 --- a/crates/theme/src/icon_theme.rs +++ b/crates/theme/src/icon_theme.rs @@ -398,7 +398,7 @@ static DEFAULT_ICON_THEME: LazyLock<Arc<IconTheme>> = LazyLock::new(|| { }, file_stems: icon_keys_by_association(FILE_STEMS_BY_ICON_KEY), file_suffixes: icon_keys_by_association(FILE_SUFFIXES_BY_ICON_KEY), - file_icons: HashMap::from_iter(FILE_ICONS.into_iter().map(|(ty, path)| { + file_icons: HashMap::from_iter(FILE_ICONS.iter().map(|(ty, path)| { ( ty.to_string(), IconDefinition { diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index 275f47912a..5be68afeb4 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -41,7 +41,8 @@ fn toggle_screen_sharing( let Some(room) = call.room().cloned() else { return; }; - let toggle_screen_sharing = room.update(cx, |room, cx| { + + room.update(cx, |room, cx| { let clicked_on_currently_shared_screen = room.shared_screen_id().is_some_and(|screen_id| { Some(screen_id) @@ -78,8 +79,7 @@ fn toggle_screen_sharing( } else { Task::ready(Ok(())) } - }); - toggle_screen_sharing + }) } Err(e) => Task::ready(Err(e)), }; diff --git a/crates/ui/src/components/facepile.rs b/crates/ui/src/components/facepile.rs index 879bfce041..83e99df7c2 100644 --- a/crates/ui/src/components/facepile.rs +++ b/crates/ui/src/components/facepile.rs @@ -78,7 +78,7 @@ impl RenderOnce for Facepile { } } -pub const EXAMPLE_FACES: [&'static str; 6] = [ +pub const EXAMPLE_FACES: [&str; 6] = [ "https://avatars.githubusercontent.com/u/326587?s=60&v=4", "https://avatars.githubusercontent.com/u/2280405?s=60&v=4", "https://avatars.githubusercontent.com/u/1789?s=60&v=4", diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index e5f28e3b25..2ca635c05b 100644 --- a/crates/ui/src/components/toggle.rs +++ b/crates/ui/src/components/toggle.rs @@ -616,7 +616,7 @@ impl SwitchField { Self { id: id.into(), label: label.into(), - description: description, + description, toggle_state: toggle_state.into(), on_click: Arc::new(on_click), disabled: false, diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs index ed0fdd0114..65ed2f2b68 100644 --- a/crates/ui/src/components/tooltip.rs +++ b/crates/ui/src/components/tooltip.rs @@ -175,7 +175,7 @@ impl Tooltip { move |_, cx| { let title = title.clone(); cx.new(|_| Self { - title: title, + title, meta: None, key_binding: None, }) diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index e2ce54b994..2bc531268d 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -201,10 +201,7 @@ impl Vim { let right_kind = classifier.kind_with(right, ignore_punctuation); let at_newline = (left == '\n') ^ (right == '\n'); - let found = (left_kind != right_kind && right_kind != CharKind::Whitespace) - || at_newline; - - found + (left_kind != right_kind && right_kind != CharKind::Whitespace) || at_newline }) } Motion::NextWordEnd { ignore_punctuation } => { @@ -213,10 +210,7 @@ impl Vim { let right_kind = classifier.kind_with(right, ignore_punctuation); let at_newline = (left == '\n') ^ (right == '\n'); - let found = (left_kind != right_kind && left_kind != CharKind::Whitespace) - || at_newline; - - found + (left_kind != right_kind && left_kind != CharKind::Whitespace) || at_newline }) } Motion::PreviousWordStart { ignore_punctuation } => { @@ -225,10 +219,7 @@ impl Vim { let right_kind = classifier.kind_with(right, ignore_punctuation); let at_newline = (left == '\n') ^ (right == '\n'); - let found = (left_kind != right_kind && left_kind != CharKind::Whitespace) - || at_newline; - - found + (left_kind != right_kind && left_kind != CharKind::Whitespace) || at_newline }) } Motion::PreviousWordEnd { ignore_punctuation } => { @@ -237,10 +228,7 @@ impl Vim { let right_kind = classifier.kind_with(right, ignore_punctuation); let at_newline = (left == '\n') ^ (right == '\n'); - let found = (left_kind != right_kind && right_kind != CharKind::Whitespace) - || at_newline; - - found + (left_kind != right_kind && right_kind != CharKind::Whitespace) || at_newline }) } Motion::FindForward { diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index fcd36dd7ee..2af22bf050 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -155,12 +155,11 @@ fn expand_changed_word_selection( let classifier = map .buffer_snapshot .char_classifier_at(selection.start.to_point(map)); - let in_word = map - .buffer_chars_at(selection.head().to_offset(map, Bias::Left)) + + map.buffer_chars_at(selection.head().to_offset(map, Bias::Left)) .next() .map(|(c, _)| !classifier.is_whitespace(c)) - .unwrap_or_default(); - in_word + .unwrap_or_default() }; if (times.is_none() || times.unwrap() == 1) && is_in_word() { let next_char = map diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 81efcef17a..23efd39139 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -255,16 +255,11 @@ impl MarksState { pub fn new(workspace: &Workspace, cx: &mut App) -> Entity<MarksState> { cx.new(|cx| { let buffer_store = workspace.project().read(cx).buffer_store().clone(); - let subscription = - cx.subscribe( - &buffer_store, - move |this: &mut Self, _, event, cx| match event { - project::buffer_store::BufferStoreEvent::BufferAdded(buffer) => { - this.on_buffer_loaded(buffer, cx); - } - _ => {} - }, - ); + let subscription = cx.subscribe(&buffer_store, move |this: &mut Self, _, event, cx| { + if let project::buffer_store::BufferStoreEvent::BufferAdded(buffer) = event { + this.on_buffer_loaded(buffer, cx); + } + }); let mut this = Self { workspace: workspace.weak_handle(), @@ -596,7 +591,7 @@ impl MarksState { if let Some(anchors) = self.buffer_marks.get(&buffer_id) { let text_anchors = anchors.get(name)?; let anchors = text_anchors - .into_iter() + .iter() .map(|anchor| Anchor::in_buffer(excerpt_id, buffer_id, *anchor)) .collect(); return Some(Mark::Local(anchors)); @@ -1710,26 +1705,25 @@ impl VimDb { marks: HashMap<String, Vec<Point>>, ) -> Result<()> { log::debug!("Setting path {path:?} for {} marks", marks.len()); - let result = self - .write(move |conn| { - let mut query = conn.exec_bound(sql!( - INSERT OR REPLACE INTO vim_marks - (workspace_id, mark_name, path, value) - VALUES - (?, ?, ?, ?) - ))?; - for (mark_name, value) in marks { - let pairs: Vec<(u32, u32)> = value - .into_iter() - .map(|point| (point.row, point.column)) - .collect(); - let serialized = serde_json::to_string(&pairs)?; - query((workspace_id, mark_name, path.clone(), serialized))?; - } - Ok(()) - }) - .await; - result + + self.write(move |conn| { + let mut query = conn.exec_bound(sql!( + INSERT OR REPLACE INTO vim_marks + (workspace_id, mark_name, path, value) + VALUES + (?, ?, ?, ?) + ))?; + for (mark_name, value) in marks { + let pairs: Vec<(u32, u32)> = value + .into_iter() + .map(|point| (point.row, point.column)) + .collect(); + let serialized = serde_json::to_string(&pairs)?; + query((workspace_id, mark_name, path.clone(), serialized))?; + } + Ok(()) + }) + .await } fn get_marks(&self, workspace_id: WorkspaceId) -> Result<Vec<SerializedMark>> { diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 98dabb8316..c2f7414f44 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -590,7 +590,7 @@ fn parse_state(marked_text: &str) -> (String, Vec<Range<Point>>) { #[cfg(feature = "neovim")] fn encode_ranges(text: &str, point_ranges: &Vec<Range<Point>>) -> String { let byte_ranges = point_ranges - .into_iter() + .iter() .map(|range| { let mut byte_range = 0..0; let mut ix = 0; diff --git a/crates/web_search_providers/src/cloud.rs b/crates/web_search_providers/src/cloud.rs index 52ee0da0d4..75ffb1da63 100644 --- a/crates/web_search_providers/src/cloud.rs +++ b/crates/web_search_providers/src/cloud.rs @@ -50,7 +50,7 @@ impl State { } } -pub const ZED_WEB_SEARCH_PROVIDER_ID: &'static str = "zed.dev"; +pub const ZED_WEB_SEARCH_PROVIDER_ID: &str = "zed.dev"; impl WebSearchProvider for CloudWebSearchProvider { fn id(&self) -> WebSearchProviderId { diff --git a/crates/web_search_providers/src/web_search_providers.rs b/crates/web_search_providers/src/web_search_providers.rs index 7f8a5f3fa4..8ab0aee47a 100644 --- a/crates/web_search_providers/src/web_search_providers.rs +++ b/crates/web_search_providers/src/web_search_providers.rs @@ -27,11 +27,10 @@ fn register_web_search_providers( cx.subscribe( &LanguageModelRegistry::global(cx), - move |this, registry, event, cx| match event { - language_model::Event::DefaultModelChanged => { + move |this, registry, event, cx| { + if let language_model::Event::DefaultModelChanged = event { register_zed_web_search_provider(this, client.clone(), ®istry, cx) } - _ => {} }, ) .detach(); diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index febb83d683..d77be8ed76 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -33,13 +33,12 @@ impl SharedScreen { cx: &mut Context<Self>, ) -> Self { let my_sid = track.sid(); - cx.subscribe(&room, move |_, _, ev, cx| match ev { - call::room::Event::RemoteVideoTrackUnsubscribed { sid } => { - if sid == &my_sid { - cx.emit(Event::Close) - } + cx.subscribe(&room, move |_, _, ev, cx| { + if let call::room::Event::RemoteVideoTrackUnsubscribed { sid } = ev + && sid == &my_sid + { + cx.emit(Event::Close) } - _ => {} }) .detach(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 9dac340b5c..8c1be61abf 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3283,7 +3283,8 @@ impl Workspace { let task = self.load_path(project_path.clone(), window, cx); window.spawn(cx, async move |cx| { let (project_entry_id, build_item) = task.await?; - let result = pane.update_in(cx, |pane, window, cx| { + + pane.update_in(cx, |pane, window, cx| { pane.open_item( project_entry_id, project_path, @@ -3295,8 +3296,7 @@ impl Workspace { cx, build_item, ) - }); - result + }) }) } @@ -9150,13 +9150,12 @@ mod tests { fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> { workspace.update_in(cx, |workspace, window, cx| { - let new_pane = workspace.split_pane( + workspace.split_pane( workspace.active_pane().clone(), SplitDirection::Right, window, cx, - ); - new_pane + ) }) } @@ -9413,7 +9412,7 @@ mod tests { let workspace = workspace.clone(); move |cx: &mut VisualTestContext| { workspace.update_in(cx, |workspace, window, cx| { - if let Some(_) = workspace.active_modal::<TestModal>(cx) { + if workspace.active_modal::<TestModal>(cx).is_some() { workspace.toggle_modal(window, cx, TestModal::new); workspace.toggle_modal(window, cx, TestModal::new); } else { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index df30d4dd7b..851c4e79f1 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -80,12 +80,9 @@ fn files_not_created_on_launch(errors: HashMap<io::ErrorKind, Vec<&Path>>) { #[cfg(unix)] { - match kind { - io::ErrorKind::PermissionDenied => { - error_kind_details.push_str("\n\nConsider using chown and chmod tools for altering the directories permissions if your user has corresponding rights.\ - \nFor example, `sudo chown $(whoami):staff ~/.config` and `chmod +uwrx ~/.config`"); - } - _ => {} + if kind == io::ErrorKind::PermissionDenied { + error_kind_details.push_str("\n\nConsider using chown and chmod tools for altering the directories permissions if your user has corresponding rights.\ + \nFor example, `sudo chown $(whoami):staff ~/.config` and `chmod +uwrx ~/.config`"); } } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d3a503f172..232dfc42a3 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1620,13 +1620,12 @@ fn open_local_file( .read_with(cx, |tree, _| tree.abs_path().join(settings_relative_path))?; let fs = project.read_with(cx, |project, _| project.fs().clone())?; - let file_exists = fs - .metadata(&full_path) + + fs.metadata(&full_path) .await .ok() .flatten() - .is_some_and(|metadata| !metadata.is_dir && !metadata.is_fifo); - file_exists + .is_some_and(|metadata| !metadata.is_dir && !metadata.is_fifo) }; if !file_exists { diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index 8d12a5bfad..1123e53ddd 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -60,8 +60,8 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) { cx.subscribe(&user_store, { let editors = editors.clone(); let client = client.clone(); - move |user_store, event, cx| match event { - client::user::Event::PrivateUserInfoUpdated => { + move |user_store, event, cx| { + if let client::user::Event::PrivateUserInfoUpdated = event { assign_edit_prediction_providers( &editors, provider, @@ -70,7 +70,6 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) { cx, ); } - _ => {} } }) .detach(); diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 640f408dd3..916699d29b 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -55,10 +55,10 @@ use workspace::Workspace; use workspace::notifications::{ErrorMessagePrompt, NotificationId}; use worktree::Worktree; -const CURSOR_MARKER: &'static str = "<|user_cursor_is_here|>"; -const START_OF_FILE_MARKER: &'static str = "<|start_of_file|>"; -const EDITABLE_REGION_START_MARKER: &'static str = "<|editable_region_start|>"; -const EDITABLE_REGION_END_MARKER: &'static str = "<|editable_region_end|>"; +const CURSOR_MARKER: &str = "<|user_cursor_is_here|>"; +const START_OF_FILE_MARKER: &str = "<|start_of_file|>"; +const EDITABLE_REGION_START_MARKER: &str = "<|editable_region_start|>"; +const EDITABLE_REGION_END_MARKER: &str = "<|editable_region_end|>"; const BUFFER_CHANGE_GROUPING_INTERVAL: Duration = Duration::from_secs(1); const ZED_PREDICT_DATA_COLLECTION_CHOICE: &str = "zed_predict_data_collection_choice"; @@ -166,7 +166,7 @@ fn interpolate( ) -> Option<Vec<(Range<Anchor>, String)>> { let mut edits = Vec::new(); - let mut model_edits = current_edits.into_iter().peekable(); + let mut model_edits = current_edits.iter().peekable(); for user_edit in new_snapshot.edits_since::<usize>(&old_snapshot.version) { while let Some((model_old_range, _)) = model_edits.peek() { let model_old_range = model_old_range.to_offset(old_snapshot); @@ -2123,7 +2123,7 @@ mod tests { let completion = completion_task.await.unwrap().unwrap(); completion .edits - .into_iter() + .iter() .map(|(old_range, new_text)| (old_range.to_point(&snapshot), new_text.clone())) .collect::<Vec<_>>() } diff --git a/crates/zeta_cli/src/main.rs b/crates/zeta_cli/src/main.rs index ba854b8732..5b2d4cf615 100644 --- a/crates/zeta_cli/src/main.rs +++ b/crates/zeta_cli/src/main.rs @@ -190,9 +190,8 @@ async fn get_context( .await; // Disable data collection for these requests, as this is currently just used for evals - match gather_context_output.as_mut() { - Ok(gather_context_output) => gather_context_output.body.can_collect_data = false, - Err(_) => {} + if let Ok(gather_context_output) = gather_context_output.as_mut() { + gather_context_output.body.can_collect_data = false } gather_context_output @@ -277,8 +276,8 @@ pub fn wait_for_lang_server( let subscriptions = [ cx.subscribe(&lsp_store, { let log_prefix = log_prefix.clone(); - move |_, event, _| match event { - project::LspStoreEvent::LanguageServerUpdate { + move |_, event, _| { + if let project::LspStoreEvent::LanguageServerUpdate { message: client::proto::update_language_server::Variant::WorkProgress( client::proto::LspWorkProgress { @@ -287,8 +286,10 @@ pub fn wait_for_lang_server( }, ), .. - } => println!("{}⟲ {message}", log_prefix), - _ => {} + } = event + { + println!("{}⟲ {message}", log_prefix) + } } }), cx.subscribe(project, { diff --git a/crates/zlog/src/filter.rs b/crates/zlog/src/filter.rs index 27a5314e28..36a77e37bd 100644 --- a/crates/zlog/src/filter.rs +++ b/crates/zlog/src/filter.rs @@ -4,7 +4,6 @@ use std::{ OnceLock, RwLock, atomic::{AtomicU8, Ordering}, }, - usize, }; use crate::{SCOPE_DEPTH_MAX, SCOPE_STRING_SEP_STR, Scope, ScopeAlloc, env_config, private}; @@ -152,7 +151,7 @@ fn scope_alloc_from_scope_str(scope_str: &str) -> Option<ScopeAlloc> { if index == 0 { return None; } - if let Some(_) = scope_iter.next() { + if scope_iter.next().is_some() { crate::warn!( "Invalid scope key, too many nested scopes: '{scope_str}'. Max depth is {SCOPE_DEPTH_MAX}", ); @@ -204,12 +203,10 @@ impl ScopeMap { .map(|(scope_str, level_filter)| (scope_str.as_str(), *level_filter)) }); - let new_filters = items_input_map - .into_iter() - .filter_map(|(scope_str, level_str)| { - let level_filter = level_filter_from_str(level_str)?; - Some((scope_str.as_str(), level_filter)) - }); + let new_filters = items_input_map.iter().filter_map(|(scope_str, level_str)| { + let level_filter = level_filter_from_str(level_str)?; + Some((scope_str.as_str(), level_filter)) + }); let all_filters = default_filters .iter() diff --git a/crates/zlog/src/zlog.rs b/crates/zlog/src/zlog.rs index d1c6cd4747..d0e8958df5 100644 --- a/crates/zlog/src/zlog.rs +++ b/crates/zlog/src/zlog.rs @@ -10,12 +10,9 @@ pub use sink::{flush, init_output_file, init_output_stderr, init_output_stdout}; pub const SCOPE_DEPTH_MAX: usize = 4; pub fn init() { - match try_init() { - Err(err) => { - log::error!("{err}"); - eprintln!("{err}"); - } - Ok(()) => {} + if let Err(err) = try_init() { + log::error!("{err}"); + eprintln!("{err}"); } } @@ -268,7 +265,7 @@ pub mod private { pub type Scope = [&'static str; SCOPE_DEPTH_MAX]; pub type ScopeAlloc = [String; SCOPE_DEPTH_MAX]; -const SCOPE_STRING_SEP_STR: &'static str = "."; +const SCOPE_STRING_SEP_STR: &str = "."; const SCOPE_STRING_SEP_CHAR: char = '.'; #[derive(Clone, Copy, Debug, PartialEq, Eq)] From 5fb68cb8bef6a18c48a21ca7357dc7b049d3021f Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Tue, 19 Aug 2025 22:40:31 +0200 Subject: [PATCH 496/693] agent2: Token count (#36496) Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga <agus@zed.dev> --- crates/acp_thread/src/acp_thread.rs | 19 ++++ crates/acp_thread/src/connection.rs | 2 +- crates/agent2/Cargo.toml | 1 + crates/agent2/src/agent.rs | 44 +++++--- crates/agent2/src/db.rs | 21 +++- crates/agent2/src/tests/mod.rs | 144 ++++++++++++++++++++++++- crates/agent2/src/thread.rs | 74 +++++++++++-- crates/agent_ui/src/acp/thread_view.rs | 41 ++++++- crates/agent_ui/src/agent_diff.rs | 1 + 9 files changed, 321 insertions(+), 26 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index d4d73e1edd..793ef35be2 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -6,6 +6,7 @@ mod terminal; pub use connection::*; pub use diff::*; pub use mention::*; +use serde::{Deserialize, Serialize}; pub use terminal::*; use action_log::ActionLog; @@ -664,6 +665,12 @@ impl PlanEntry { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct TokenUsage { + pub max_tokens: u64, + pub used_tokens: u64, +} + #[derive(Debug, Clone)] pub struct RetryStatus { pub last_error: SharedString, @@ -683,12 +690,14 @@ pub struct AcpThread { send_task: Option<Task<()>>, connection: Rc<dyn AgentConnection>, session_id: acp::SessionId, + token_usage: Option<TokenUsage>, } #[derive(Debug)] pub enum AcpThreadEvent { NewEntry, TitleUpdated, + TokenUsageUpdated, EntryUpdated(usize), EntriesRemoved(Range<usize>), ToolAuthorizationRequired, @@ -748,6 +757,7 @@ impl AcpThread { send_task: None, connection, session_id, + token_usage: None, } } @@ -787,6 +797,10 @@ impl AcpThread { } } + pub fn token_usage(&self) -> Option<&TokenUsage> { + self.token_usage.as_ref() + } + pub fn has_pending_edit_tool_calls(&self) -> bool { for entry in self.entries.iter().rev() { match entry { @@ -937,6 +951,11 @@ impl AcpThread { Ok(()) } + pub fn update_token_usage(&mut self, usage: Option<TokenUsage>, cx: &mut Context<Self>) { + self.token_usage = usage; + cx.emit(AcpThreadEvent::TokenUsageUpdated); + } + pub fn update_retry_status(&mut self, status: RetryStatus, cx: &mut Context<Self>) { cx.emit(AcpThreadEvent::Retry(status)); } diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index b09f383029..8cae975ce5 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -10,7 +10,7 @@ use std::{any::Any, error::Error, fmt, path::Path, rc::Rc, sync::Arc}; use ui::{App, IconName}; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] pub struct UserMessageId(Arc<str>); impl UserMessageId { diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 890f7e774b..d18773ff7b 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -66,6 +66,7 @@ zstd.workspace = true [dev-dependencies] agent = { workspace = true, "features" = ["test-support"] } +assistant_context = { workspace = true, "features" = ["test-support"] } ctor.workspace = true client = { workspace = true, "features" = ["test-support"] } clock = { workspace = true, "features" = ["test-support"] } diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 48f46a52fc..6303144d96 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,8 +1,8 @@ +use crate::HistoryStore; use crate::{ - ContextServerRegistry, Thread, ThreadEvent, ToolCallAuthorization, UserMessageContent, - templates::Templates, + ContextServerRegistry, Thread, ThreadEvent, ThreadsDatabase, ToolCallAuthorization, + UserMessageContent, templates::Templates, }; -use crate::{HistoryStore, ThreadsDatabase}; use acp_thread::{AcpThread, AgentModelSelector}; use action_log::ActionLog; use agent_client_protocol as acp; @@ -673,6 +673,11 @@ impl NativeAgentConnection { thread.update_tool_call(update, cx) })??; } + ThreadEvent::TokenUsageUpdate(usage) => { + acp_thread.update(cx, |thread, cx| { + thread.update_token_usage(Some(usage), cx) + })?; + } ThreadEvent::TitleUpdate(title) => { acp_thread .update(cx, |thread, cx| thread.update_title(title, cx))??; @@ -895,10 +900,12 @@ impl acp_thread::AgentConnection for NativeAgentConnection { cx: &mut App, ) -> Option<Rc<dyn acp_thread::AgentSessionEditor>> { self.0.update(cx, |agent, _cx| { - agent - .sessions - .get(session_id) - .map(|session| Rc::new(NativeAgentSessionEditor(session.thread.clone())) as _) + agent.sessions.get(session_id).map(|session| { + Rc::new(NativeAgentSessionEditor { + thread: session.thread.clone(), + acp_thread: session.acp_thread.clone(), + }) as _ + }) }) } @@ -907,14 +914,27 @@ impl acp_thread::AgentConnection for NativeAgentConnection { } } -struct NativeAgentSessionEditor(Entity<Thread>); +struct NativeAgentSessionEditor { + thread: Entity<Thread>, + acp_thread: WeakEntity<AcpThread>, +} impl acp_thread::AgentSessionEditor for NativeAgentSessionEditor { fn truncate(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task<Result<()>> { - Task::ready( - self.0 - .update(cx, |thread, cx| thread.truncate(message_id, cx)), - ) + match self.thread.update(cx, |thread, cx| { + thread.truncate(message_id.clone(), cx)?; + Ok(thread.latest_token_usage()) + }) { + Ok(usage) => { + self.acp_thread + .update(cx, |thread, cx| { + thread.update_token_usage(usage, cx); + }) + .ok(); + Task::ready(Ok(())) + } + Err(error) => Task::ready(Err(error)), + } } } diff --git a/crates/agent2/src/db.rs b/crates/agent2/src/db.rs index 27a109c573..610a2575c4 100644 --- a/crates/agent2/src/db.rs +++ b/crates/agent2/src/db.rs @@ -1,4 +1,5 @@ use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent}; +use acp_thread::UserMessageId; use agent::thread_store; use agent_client_protocol as acp; use agent_settings::{AgentProfileId, CompletionMode}; @@ -42,7 +43,7 @@ pub struct DbThread { #[serde(default)] pub cumulative_token_usage: language_model::TokenUsage, #[serde(default)] - pub request_token_usage: Vec<language_model::TokenUsage>, + pub request_token_usage: HashMap<acp_thread::UserMessageId, language_model::TokenUsage>, #[serde(default)] pub model: Option<DbLanguageModel>, #[serde(default)] @@ -67,7 +68,10 @@ impl DbThread { fn upgrade_from_agent_1(thread: agent::SerializedThread) -> Result<Self> { let mut messages = Vec::new(); - for msg in thread.messages { + let mut request_token_usage = HashMap::default(); + + let mut last_user_message_id = None; + for (ix, msg) in thread.messages.into_iter().enumerate() { let message = match msg.role { language_model::Role::User => { let mut content = Vec::new(); @@ -93,9 +97,12 @@ impl DbThread { content.push(UserMessageContent::Text(msg.context)); } + let id = UserMessageId::new(); + last_user_message_id = Some(id.clone()); + crate::Message::User(UserMessage { // MessageId from old format can't be meaningfully converted, so generate a new one - id: acp_thread::UserMessageId::new(), + id, content, }) } @@ -154,6 +161,12 @@ impl DbThread { ); } + if let Some(last_user_message_id) = &last_user_message_id + && let Some(token_usage) = thread.request_token_usage.get(ix).copied() + { + request_token_usage.insert(last_user_message_id.clone(), token_usage); + } + crate::Message::Agent(AgentMessage { content, tool_results, @@ -175,7 +188,7 @@ impl DbThread { summary: thread.detailed_summary_state, initial_project_snapshot: thread.initial_project_snapshot, cumulative_token_usage: thread.cumulative_token_usage, - request_token_usage: thread.request_token_usage, + request_token_usage, model: thread.model, completion_mode: thread.completion_mode, profile: thread.profile, diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 7fa12e5711..d07ca42d3b 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1117,7 +1117,7 @@ async fn test_refusal(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_truncate(cx: &mut TestAppContext) { +async fn test_truncate_first_message(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); @@ -1137,9 +1137,18 @@ async fn test_truncate(cx: &mut TestAppContext) { Hello "} ); + assert_eq!(thread.latest_token_usage(), None); }); fake_model.send_last_completion_stream_text_chunk("Hey!"); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate( + language_model::TokenUsage { + input_tokens: 32_000, + output_tokens: 16_000, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + }, + )); cx.run_until_parked(); thread.read_with(cx, |thread, _| { assert_eq!( @@ -1154,6 +1163,13 @@ async fn test_truncate(cx: &mut TestAppContext) { Hey! "} ); + assert_eq!( + thread.latest_token_usage(), + Some(acp_thread::TokenUsage { + used_tokens: 32_000 + 16_000, + max_tokens: 1_000_000, + }) + ); }); thread @@ -1162,6 +1178,7 @@ async fn test_truncate(cx: &mut TestAppContext) { cx.run_until_parked(); thread.read_with(cx, |thread, _| { assert_eq!(thread.to_markdown(), ""); + assert_eq!(thread.latest_token_usage(), None); }); // Ensure we can still send a new message after truncation. @@ -1182,6 +1199,14 @@ async fn test_truncate(cx: &mut TestAppContext) { }); cx.run_until_parked(); fake_model.send_last_completion_stream_text_chunk("Ahoy!"); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate( + language_model::TokenUsage { + input_tokens: 40_000, + output_tokens: 20_000, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + }, + )); cx.run_until_parked(); thread.read_with(cx, |thread, _| { assert_eq!( @@ -1196,9 +1221,126 @@ async fn test_truncate(cx: &mut TestAppContext) { Ahoy! "} ); + + assert_eq!( + thread.latest_token_usage(), + Some(acp_thread::TokenUsage { + used_tokens: 40_000 + 20_000, + max_tokens: 1_000_000, + }) + ); }); } +#[gpui::test] +async fn test_truncate_second_message(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Message 1"], cx) + }) + .unwrap(); + cx.run_until_parked(); + fake_model.send_last_completion_stream_text_chunk("Message 1 response"); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate( + language_model::TokenUsage { + input_tokens: 32_000, + output_tokens: 16_000, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + }, + )); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + let assert_first_message_state = |cx: &mut TestAppContext| { + thread.clone().read_with(cx, |thread, _| { + assert_eq!( + thread.to_markdown(), + indoc! {" + ## User + + Message 1 + + ## Assistant + + Message 1 response + "} + ); + + assert_eq!( + thread.latest_token_usage(), + Some(acp_thread::TokenUsage { + used_tokens: 32_000 + 16_000, + max_tokens: 1_000_000, + }) + ); + }); + }; + + assert_first_message_state(cx); + + let second_message_id = UserMessageId::new(); + thread + .update(cx, |thread, cx| { + thread.send(second_message_id.clone(), ["Message 2"], cx) + }) + .unwrap(); + cx.run_until_parked(); + + fake_model.send_last_completion_stream_text_chunk("Message 2 response"); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate( + language_model::TokenUsage { + input_tokens: 40_000, + output_tokens: 20_000, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + }, + )); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + thread.read_with(cx, |thread, _| { + assert_eq!( + thread.to_markdown(), + indoc! {" + ## User + + Message 1 + + ## Assistant + + Message 1 response + + ## User + + Message 2 + + ## Assistant + + Message 2 response + "} + ); + + assert_eq!( + thread.latest_token_usage(), + Some(acp_thread::TokenUsage { + used_tokens: 40_000 + 20_000, + max_tokens: 1_000_000, + }) + ); + }); + + thread + .update(cx, |thread, cx| thread.truncate(second_message_id, cx)) + .unwrap(); + cx.run_until_parked(); + + assert_first_message_state(cx); +} + #[gpui::test] async fn test_title_generation(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index ba5cd1f477..4bc45f1544 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -13,7 +13,7 @@ use anyhow::{Context as _, Result, anyhow}; use assistant_tool::adapt_schema_to_format; use chrono::{DateTime, Utc}; use cloud_llm_client::{CompletionIntent, CompletionRequestStatus}; -use collections::IndexMap; +use collections::{HashMap, IndexMap}; use fs::Fs; use futures::{ FutureExt, @@ -24,8 +24,8 @@ use futures::{ use git::repository::DiffType; use gpui::{App, AppContext, AsyncApp, Context, Entity, SharedString, Task, WeakEntity}; use language_model::{ - LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage, - LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest, + LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelExt, + LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse, LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, @@ -481,6 +481,7 @@ pub enum ThreadEvent { ToolCall(acp::ToolCall), ToolCallUpdate(acp_thread::ToolCallUpdate), ToolCallAuthorization(ToolCallAuthorization), + TokenUsageUpdate(acp_thread::TokenUsage), TitleUpdate(SharedString), Retry(acp_thread::RetryStatus), Stop(acp::StopReason), @@ -509,8 +510,7 @@ pub struct Thread { pending_message: Option<AgentMessage>, tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>, tool_use_limit_reached: bool, - #[allow(unused)] - request_token_usage: Vec<TokenUsage>, + request_token_usage: HashMap<UserMessageId, language_model::TokenUsage>, #[allow(unused)] cumulative_token_usage: TokenUsage, #[allow(unused)] @@ -548,7 +548,7 @@ impl Thread { pending_message: None, tools: BTreeMap::default(), tool_use_limit_reached: false, - request_token_usage: Vec::new(), + request_token_usage: HashMap::default(), cumulative_token_usage: TokenUsage::default(), initial_project_snapshot: { let project_snapshot = Self::project_snapshot(project.clone(), cx); @@ -951,6 +951,15 @@ impl Thread { self.flush_pending_message(cx); } + pub fn update_token_usage(&mut self, update: language_model::TokenUsage) { + let Some(last_user_message) = self.last_user_message() else { + return; + }; + + self.request_token_usage + .insert(last_user_message.id.clone(), update); + } + pub fn truncate(&mut self, message_id: UserMessageId, cx: &mut Context<Self>) -> Result<()> { self.cancel(cx); let Some(position) = self.messages.iter().position( @@ -958,11 +967,31 @@ impl Thread { ) else { return Err(anyhow!("Message not found")); }; - self.messages.truncate(position); + + for message in self.messages.drain(position..) { + match message { + Message::User(message) => { + self.request_token_usage.remove(&message.id); + } + Message::Agent(_) | Message::Resume => {} + } + } + cx.notify(); Ok(()) } + pub fn latest_token_usage(&self) -> Option<acp_thread::TokenUsage> { + let last_user_message = self.last_user_message()?; + let tokens = self.request_token_usage.get(&last_user_message.id)?; + let model = self.model.clone()?; + + Some(acp_thread::TokenUsage { + max_tokens: model.max_token_count_for_mode(self.completion_mode.into()), + used_tokens: tokens.total_tokens(), + }) + } + pub fn resume( &mut self, cx: &mut Context<Self>, @@ -1148,6 +1177,21 @@ impl Thread { )) => { *tool_use_limit_reached = true; } + Ok(LanguageModelCompletionEvent::UsageUpdate(token_usage)) => { + let usage = acp_thread::TokenUsage { + max_tokens: model.max_token_count_for_mode( + request + .mode + .unwrap_or(cloud_llm_client::CompletionMode::Normal), + ), + used_tokens: token_usage.total_tokens(), + }; + + this.update(cx, |this, _cx| this.update_token_usage(token_usage)) + .ok(); + + event_stream.send_token_usage_update(usage); + } Ok(LanguageModelCompletionEvent::Stop(StopReason::Refusal)) => { *refusal = true; return Ok(FuturesUnordered::default()); @@ -1532,6 +1576,16 @@ impl Thread { }) })) } + fn last_user_message(&self) -> Option<&UserMessage> { + self.messages + .iter() + .rev() + .find_map(|message| match message { + Message::User(user_message) => Some(user_message), + Message::Agent(_) => None, + Message::Resume => None, + }) + } fn pending_message(&mut self) -> &mut AgentMessage { self.pending_message.get_or_insert_default() @@ -2051,6 +2105,12 @@ impl ThreadEventStream { .ok(); } + fn send_token_usage_update(&self, usage: acp_thread::TokenUsage) { + self.0 + .unbounded_send(Ok(ThreadEvent::TokenUsageUpdate(usage))) + .ok(); + } + fn send_retry(&self, status: acp_thread::RetryStatus) { self.0.unbounded_send(Ok(ThreadEvent::Retry(status))).ok(); } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 9f1e8d857f..878891c6f1 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -816,7 +816,7 @@ impl AcpThreadView { self.thread_retry_status.take(); self.thread_state = ThreadState::ServerExited { status: *status }; } - AcpThreadEvent::TitleUpdated => {} + AcpThreadEvent::TitleUpdated | AcpThreadEvent::TokenUsageUpdated => {} } cx.notify(); } @@ -2794,6 +2794,7 @@ impl AcpThreadView { .child( h_flex() .gap_1() + .children(self.render_token_usage(cx)) .children(self.profile_selector.clone()) .children(self.model_selector.clone()) .child(self.render_send_button(cx)), @@ -2816,6 +2817,44 @@ impl AcpThreadView { .thread(acp_thread.session_id(), cx) } + fn render_token_usage(&self, cx: &mut Context<Self>) -> Option<Div> { + let thread = self.thread()?.read(cx); + let usage = thread.token_usage()?; + let is_generating = thread.status() != ThreadStatus::Idle; + + let used = crate::text_thread_editor::humanize_token_count(usage.used_tokens); + let max = crate::text_thread_editor::humanize_token_count(usage.max_tokens); + + Some( + h_flex() + .flex_shrink_0() + .gap_0p5() + .mr_1() + .child( + Label::new(used) + .size(LabelSize::Small) + .color(Color::Muted) + .map(|label| { + if is_generating { + label + .with_animation( + "used-tokens-label", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.6, 1.)), + |label, delta| label.alpha(delta), + ) + .into_any() + } else { + label.into_any_element() + } + }), + ) + .child(Label::new("/").size(LabelSize::Small).color(Color::Muted)) + .child(Label::new(max).size(LabelSize::Small).color(Color::Muted)), + ) + } + fn toggle_burn_mode( &mut self, _: &ToggleBurnMode, diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 9d2ee0bf89..a695136562 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1526,6 +1526,7 @@ impl AgentDiff { self.update_reviewing_editors(workspace, window, cx); } AcpThreadEvent::TitleUpdated + | AcpThreadEvent::TokenUsageUpdated | AcpThreadEvent::EntriesRemoved(_) | AcpThreadEvent::ToolAuthorizationRequired | AcpThreadEvent::Retry(_) => {} From 88c4a5ca49799637b7cc790771de65bd9b4b5253 Mon Sep 17 00:00:00 2001 From: Julia Ryan <juliaryan3.14@gmail.com> Date: Tue, 19 Aug 2025 16:31:13 -0500 Subject: [PATCH 497/693] Suspend macOS threads during crashes (#36520) This should improve our detection of which thread crashed since they wont be able to resume while the minidump is being generated. Release Notes: - N/A --- Cargo.lock | 20 +++++++++++++++----- Cargo.toml | 1 + crates/crashes/Cargo.toml | 3 +++ crates/crashes/src/crashes.rs | 20 ++++++++++++++++++++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a5dec4734..d1f4b22e9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3872,7 +3872,7 @@ dependencies = [ "jni", "js-sys", "libc", - "mach2", + "mach2 0.4.2", "ndk", "ndk-context", "num-derive", @@ -4022,7 +4022,7 @@ checksum = "031ed29858d90cfdf27fe49fae28028a1f20466db97962fa2f4ea34809aeebf3" dependencies = [ "cfg-if", "libc", - "mach2", + "mach2 0.4.2", ] [[package]] @@ -4034,7 +4034,7 @@ dependencies = [ "cfg-if", "crash-context", "libc", - "mach2", + "mach2 0.4.2", "parking_lot", ] @@ -4044,6 +4044,7 @@ version = "0.1.0" dependencies = [ "crash-handler", "log", + "mach2 0.5.0", "minidumper", "paths", "release_channel", @@ -9866,6 +9867,15 @@ dependencies = [ "libc", ] +[[package]] +name = "mach2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -10202,7 +10212,7 @@ dependencies = [ "goblin", "libc", "log", - "mach2", + "mach2 0.4.2", "memmap2", "memoffset", "minidump-common", @@ -18292,7 +18302,7 @@ dependencies = [ "indexmap", "libc", "log", - "mach2", + "mach2 0.4.2", "memfd", "object", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index ad45def2d4..dc14c8ebd9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -515,6 +515,7 @@ libsqlite3-sys = { version = "0.30.1", features = ["bundled"] } linkify = "0.10.0" log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] } lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "39f629bdd03d59abd786ed9fc27e8bca02c0c0ec" } +mach2 = "0.5" markup5ever_rcdom = "0.3.0" metal = "0.29" minidumper = "0.8" diff --git a/crates/crashes/Cargo.toml b/crates/crashes/Cargo.toml index 2420b499f8..f12913d1cb 100644 --- a/crates/crashes/Cargo.toml +++ b/crates/crashes/Cargo.toml @@ -16,6 +16,9 @@ serde.workspace = true serde_json.workspace = true workspace-hack.workspace = true +[target.'cfg(target_os = "macos")'.dependencies] +mach2.workspace = true + [lints] workspace = true diff --git a/crates/crashes/src/crashes.rs b/crates/crashes/src/crashes.rs index ddf6468be8..12997f51a3 100644 --- a/crates/crashes/src/crashes.rs +++ b/crates/crashes/src/crashes.rs @@ -74,6 +74,9 @@ pub async fn init(crash_init: InitCrashHandler) { .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) .is_ok() { + #[cfg(target_os = "macos")] + suspend_all_other_threads(); + client.ping().unwrap(); client.request_dump(crash_context).is_ok() } else { @@ -98,6 +101,23 @@ pub async fn init(crash_init: InitCrashHandler) { } } +#[cfg(target_os = "macos")] +unsafe fn suspend_all_other_threads() { + let task = unsafe { mach2::traps::current_task() }; + let mut threads: mach2::mach_types::thread_act_array_t = std::ptr::null_mut(); + let mut count = 0; + unsafe { + mach2::task::task_threads(task, &raw mut threads, &raw mut count); + } + let current = unsafe { mach2::mach_init::mach_thread_self() }; + for i in 0..count { + let t = unsafe { *threads.add(i as usize) }; + if t != current { + unsafe { mach2::thread_act::thread_suspend(t) }; + } + } +} + pub struct CrashServer { initialization_params: OnceLock<InitCrashHandler>, panic_info: OnceLock<CrashPanic>, From 88754a70f7f2a566daf26980ed177d8c0e3b3240 Mon Sep 17 00:00:00 2001 From: Conrad Irwin <conrad.irwin@gmail.com> Date: Tue, 19 Aug 2025 16:26:30 -0600 Subject: [PATCH 498/693] Rebuild recently opened threads for ACP (#36531) Closes #ISSUE Release Notes: - N/A --- Cargo.lock | 1 + crates/agent2/Cargo.toml | 1 + crates/agent2/src/agent.rs | 6 +- crates/agent2/src/history_store.rs | 102 +++++++++++++------------ crates/agent2/src/tests/mod.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 39 ++++++++-- crates/agent_ui/src/agent_panel.rs | 21 +++-- 7 files changed, 109 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d1f4b22e9d..34a8ceac49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -206,6 +206,7 @@ dependencies = [ "collections", "context_server", "ctor", + "db", "editor", "env_logger 0.11.8", "fs", diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index d18773ff7b..849ea041e9 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -26,6 +26,7 @@ chrono.workspace = true cloud_llm_client.workspace = true collections.workspace = true context_server.workspace = true +db.workspace = true fs.workspace = true futures.workspace = true git.workspace = true diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 6303144d96..212460d690 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -974,7 +974,7 @@ mod tests { .await; let project = Project::test(fs.clone(), [], cx).await; let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, [], cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); let agent = NativeAgent::new( project.clone(), history_store, @@ -1032,7 +1032,7 @@ mod tests { fs.insert_tree("/", json!({ "a": {} })).await; let project = Project::test(fs.clone(), [], cx).await; let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, [], cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); let connection = NativeAgentConnection( NativeAgent::new( project.clone(), @@ -1088,7 +1088,7 @@ mod tests { let project = Project::test(fs.clone(), [], cx).await; let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, [], cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); // Create the agent and connection let agent = NativeAgent::new( diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs index 34a5e7b4ef..4ce304ae5f 100644 --- a/crates/agent2/src/history_store.rs +++ b/crates/agent2/src/history_store.rs @@ -3,6 +3,7 @@ use agent_client_protocol as acp; use anyhow::{Context as _, Result, anyhow}; use assistant_context::SavedContextMetadata; use chrono::{DateTime, Utc}; +use db::kvp::KEY_VALUE_STORE; use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*}; use itertools::Itertools; use paths::contexts_dir; @@ -11,7 +12,7 @@ use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration}; use util::ResultExt as _; const MAX_RECENTLY_OPENED_ENTRIES: usize = 6; -const NAVIGATION_HISTORY_PATH: &str = "agent-navigation-history.json"; +const RECENTLY_OPENED_THREADS_KEY: &str = "recent-agent-threads"; const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50); const DEFAULT_TITLE: &SharedString = &SharedString::new_static("New Thread"); @@ -53,12 +54,10 @@ pub enum HistoryEntryId { TextThread(Arc<Path>), } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] enum SerializedRecentOpen { - Thread(String), - ContextName(String), - /// Old format which stores the full path - Context(String), + AcpThread(String), + TextThread(String), } pub struct HistoryStore { @@ -72,29 +71,26 @@ pub struct HistoryStore { impl HistoryStore { pub fn new( context_store: Entity<assistant_context::ContextStore>, - initial_recent_entries: impl IntoIterator<Item = HistoryEntryId>, cx: &mut Context<Self>, ) -> Self { let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())]; cx.spawn(async move |this, cx| { - let entries = Self::load_recently_opened_entries(cx).await.log_err()?; - this.update(cx, |this, _| { - this.recently_opened_entries - .extend( - entries.into_iter().take( - MAX_RECENTLY_OPENED_ENTRIES - .saturating_sub(this.recently_opened_entries.len()), - ), - ); + let entries = Self::load_recently_opened_entries(cx).await; + this.update(cx, |this, cx| { + if let Some(entries) = entries.log_err() { + this.recently_opened_entries = entries; + } + + this.reload(cx); }) - .ok() + .ok(); }) .detach(); Self { context_store, - recently_opened_entries: initial_recent_entries.into_iter().collect(), + recently_opened_entries: VecDeque::default(), threads: Vec::default(), _subscriptions: subscriptions, _save_recently_opened_entries_task: Task::ready(()), @@ -134,6 +130,18 @@ impl HistoryStore { .await?; this.update(cx, |this, cx| { + if this.recently_opened_entries.len() < MAX_RECENTLY_OPENED_ENTRIES { + for thread in threads + .iter() + .take(MAX_RECENTLY_OPENED_ENTRIES - this.recently_opened_entries.len()) + .rev() + { + this.push_recently_opened_entry( + HistoryEntryId::AcpThread(thread.id.clone()), + cx, + ) + } + } this.threads = threads; cx.notify(); }) @@ -162,6 +170,16 @@ impl HistoryStore { history_entries } + pub fn is_empty(&self, cx: &App) -> bool { + self.threads.is_empty() + && self + .context_store + .read(cx) + .unordered_contexts() + .next() + .is_none() + } + pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> { self.entries(cx).into_iter().take(limit).collect() } @@ -215,58 +233,44 @@ impl HistoryStore { .iter() .filter_map(|entry| match entry { HistoryEntryId::TextThread(path) => path.file_name().map(|file| { - SerializedRecentOpen::ContextName(file.to_string_lossy().to_string()) + SerializedRecentOpen::TextThread(file.to_string_lossy().to_string()) }), - HistoryEntryId::AcpThread(id) => Some(SerializedRecentOpen::Thread(id.to_string())), + HistoryEntryId::AcpThread(id) => { + Some(SerializedRecentOpen::AcpThread(id.to_string())) + } }) .collect::<Vec<_>>(); self._save_recently_opened_entries_task = cx.spawn(async move |_, cx| { + let content = serde_json::to_string(&serialized_entries).unwrap(); cx.background_executor() .timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE) .await; - cx.background_spawn(async move { - let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH); - let content = serde_json::to_string(&serialized_entries)?; - std::fs::write(path, content)?; - anyhow::Ok(()) - }) - .await - .log_err(); + KEY_VALUE_STORE + .write_kvp(RECENTLY_OPENED_THREADS_KEY.to_owned(), content) + .await + .log_err(); }); } - fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<Vec<HistoryEntryId>>> { + fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<VecDeque<HistoryEntryId>>> { cx.background_spawn(async move { - let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH); - let contents = match smol::fs::read_to_string(path).await { - Ok(it) => it, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - return Ok(Vec::new()); - } - Err(e) => { - return Err(e) - .context("deserializing persisted agent panel navigation history"); - } - }; - let entries = serde_json::from_str::<Vec<SerializedRecentOpen>>(&contents) + let json = KEY_VALUE_STORE + .read_kvp(RECENTLY_OPENED_THREADS_KEY)? + .unwrap_or("[]".to_string()); + let entries = serde_json::from_str::<Vec<SerializedRecentOpen>>(&json) .context("deserializing persisted agent panel navigation history")? .into_iter() .take(MAX_RECENTLY_OPENED_ENTRIES) .flat_map(|entry| match entry { - SerializedRecentOpen::Thread(id) => Some(HistoryEntryId::AcpThread( + SerializedRecentOpen::AcpThread(id) => Some(HistoryEntryId::AcpThread( acp::SessionId(id.as_str().into()), )), - SerializedRecentOpen::ContextName(file_name) => Some( + SerializedRecentOpen::TextThread(file_name) => Some( HistoryEntryId::TextThread(contexts_dir().join(file_name).into()), ), - SerializedRecentOpen::Context(path) => { - Path::new(&path).file_name().map(|file_name| { - HistoryEntryId::TextThread(contexts_dir().join(file_name).into()) - }) - } }) - .collect::<Vec<_>>(); + .collect(); Ok(entries) }) } diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index d07ca42d3b..55bfa6f0b5 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1414,7 +1414,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) { let project = Project::test(fake_fs.clone(), [Path::new("/test")], cx).await; let cwd = Path::new("/test"); let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, [], cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); // Create agent and connection let agent = NativeAgent::new( diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 878891c6f1..5e5d4bb83c 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -9,7 +9,7 @@ use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::{self as acp}; use agent_servers::{AgentServer, ClaudeCode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; -use agent2::DbThreadMetadata; +use agent2::{DbThreadMetadata, HistoryEntryId, HistoryStore}; use anyhow::bail; use audio::{Audio, Sound}; use buffer_diff::BufferDiff; @@ -111,6 +111,7 @@ pub struct AcpThreadView { workspace: WeakEntity<Workspace>, project: Entity<Project>, thread_state: ThreadState, + history_store: Entity<HistoryStore>, entry_view_state: Entity<EntryViewState>, message_editor: Entity<MessageEditor>, model_selector: Option<Entity<AcpModelSelectorPopover>>, @@ -159,6 +160,7 @@ impl AcpThreadView { resume_thread: Option<DbThreadMetadata>, workspace: WeakEntity<Workspace>, project: Entity<Project>, + history_store: Entity<HistoryStore>, thread_store: Entity<ThreadStore>, text_thread_store: Entity<TextThreadStore>, window: &mut Window, @@ -223,6 +225,7 @@ impl AcpThreadView { plan_expanded: false, editor_expanded: false, terminal_expanded: true, + history_store, _subscriptions: subscriptions, _cancel_task: None, } @@ -260,7 +263,7 @@ impl AcpThreadView { let result = if let Some(native_agent) = connection .clone() .downcast::<agent2::NativeAgentConnection>() - && let Some(resume) = resume_thread + && let Some(resume) = resume_thread.clone() { cx.update(|_, cx| { native_agent @@ -313,6 +316,15 @@ impl AcpThreadView { } }); + if let Some(resume) = resume_thread { + this.history_store.update(cx, |history, cx| { + history.push_recently_opened_entry( + HistoryEntryId::AcpThread(resume.id), + cx, + ); + }); + } + AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx); this.model_selector = @@ -555,9 +567,15 @@ impl AcpThreadView { } fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) { - if let Some(thread) = self.thread() - && thread.read(cx).status() != ThreadStatus::Idle - { + let Some(thread) = self.thread() else { return }; + self.history_store.update(cx, |history, cx| { + history.push_recently_opened_entry( + HistoryEntryId::AcpThread(thread.read(cx).session_id().clone()), + cx, + ); + }); + + if thread.read(cx).status() != ThreadStatus::Idle { self.stop_current_and_send_new_message(window, cx); return; } @@ -3942,6 +3960,7 @@ pub(crate) mod tests { use acp_thread::StubAgentConnection; use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::SessionId; + use assistant_context::ContextStore; use editor::EditorSettings; use fs::FakeFs; use gpui::{EventEmitter, SemanticVersion, TestAppContext, VisualTestContext}; @@ -4079,6 +4098,10 @@ pub(crate) mod tests { cx.update(|_window, cx| cx.new(|cx| ThreadStore::fake(project.clone(), cx))); let text_thread_store = cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx))); + let context_store = + cx.update(|_window, cx| cx.new(|cx| ContextStore::fake(project.clone(), cx))); + let history_store = + cx.update(|_window, cx| cx.new(|cx| HistoryStore::new(context_store, cx))); let thread_view = cx.update(|window, cx| { cx.new(|cx| { @@ -4087,6 +4110,7 @@ pub(crate) mod tests { None, workspace.downgrade(), project, + history_store, thread_store.clone(), text_thread_store.clone(), window, @@ -4283,6 +4307,10 @@ pub(crate) mod tests { cx.update(|_window, cx| cx.new(|cx| ThreadStore::fake(project.clone(), cx))); let text_thread_store = cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx))); + let context_store = + cx.update(|_window, cx| cx.new(|cx| ContextStore::fake(project.clone(), cx))); + let history_store = + cx.update(|_window, cx| cx.new(|cx| HistoryStore::new(context_store, cx))); let connection = Rc::new(StubAgentConnection::new()); let thread_view = cx.update(|window, cx| { @@ -4292,6 +4320,7 @@ pub(crate) mod tests { None, workspace.downgrade(), project.clone(), + history_store.clone(), thread_store.clone(), text_thread_store.clone(), window, diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index c5cab34030..0310ae7c80 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -648,8 +648,7 @@ impl AgentPanel { ) }); - let acp_history_store = - cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), [], cx)); + let acp_history_store = cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), cx)); let acp_history = cx.new(|cx| AcpThreadHistory::new(acp_history_store.clone(), window, cx)); cx.subscribe_in( &acp_history, @@ -1073,6 +1072,7 @@ impl AgentPanel { resume_thread, workspace.clone(), project, + this.acp_history_store.clone(), thread_store.clone(), text_thread_store.clone(), window, @@ -1609,6 +1609,14 @@ impl AgentPanel { if let Some(path) = context_editor.read(cx).context().read(cx).path() { store.push_recently_opened_entry(HistoryEntryId::Context(path.clone()), cx) } + }); + self.acp_history_store.update(cx, |store, cx| { + if let Some(path) = context_editor.read(cx).context().read(cx).path() { + store.push_recently_opened_entry( + agent2::HistoryEntryId::TextThread(path.clone()), + cx, + ) + } }) } ActiveView::ExternalAgentThread { .. } => {} @@ -2763,9 +2771,12 @@ impl AgentPanel { false } _ => { - let history_is_empty = self - .history_store - .update(cx, |store, cx| store.recent_entries(1, cx).is_empty()); + let history_is_empty = if cx.has_flag::<AcpFeatureFlag>() { + self.acp_history_store.read(cx).is_empty(cx) + } else { + self.history_store + .update(cx, |store, cx| store.recent_entries(1, cx).is_empty()) + }; let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx) .providers() From ecee6746ecada543ae89d37ff3882a38dd555cae Mon Sep 17 00:00:00 2001 From: Julia Ryan <juliaryan3.14@gmail.com> Date: Tue, 19 Aug 2025 17:37:39 -0500 Subject: [PATCH 499/693] Attach minidump errors to uploaded crash events (#36527) We see a bunch of crash events with truncated minidumps where they have a valid header but no events. We think this is due to an issue generating them, so we're attaching the relevant result to the uploaded tags. Release Notes: - N/A Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com> --- crates/crashes/src/crashes.rs | 12 ++++++------ crates/zed/src/reliability.rs | 3 +++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/crashes/src/crashes.rs b/crates/crashes/src/crashes.rs index 12997f51a3..4e4b69f639 100644 --- a/crates/crashes/src/crashes.rs +++ b/crates/crashes/src/crashes.rs @@ -128,6 +128,7 @@ pub struct CrashServer { pub struct CrashInfo { pub init: InitCrashHandler, pub panic: Option<CrashPanic>, + pub minidump_error: Option<String>, } #[derive(Debug, Deserialize, Serialize, Clone)] @@ -162,16 +163,14 @@ impl minidumper::ServerHandler for CrashServer { } fn on_minidump_created(&self, result: Result<MinidumpBinary, minidumper::Error>) -> LoopAction { - match result { + let minidump_error = match result { Ok(mut md_bin) => { use io::Write; let _ = md_bin.file.flush(); - info!("wrote minidump to disk {:?}", md_bin.path); + None } - Err(e) => { - info!("failed to write minidump: {:#}", e); - } - } + Err(e) => Some(format!("{e:?}")), + }; let crash_info = CrashInfo { init: self @@ -180,6 +179,7 @@ impl minidumper::ServerHandler for CrashServer { .expect("not initialized") .clone(), panic: self.panic_info.get().cloned(), + minidump_error, }; let crash_data_path = paths::logs_dir() diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index cbd31c2e26..f55468280c 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -607,6 +607,9 @@ async fn upload_minidump( // TODO: add gpu-context, feature-flag-context, and more of device-context like gpu // name, screen resolution, available ram, device model, etc } + if let Some(minidump_error) = metadata.minidump_error.clone() { + form = form.text("minidump_error", minidump_error); + } let mut response_text = String::new(); let mut response = http.send_multipart_form(endpoint, form).await?; From 757b37fd41b459988c0741f2d51b1e77d02b9d3f Mon Sep 17 00:00:00 2001 From: Conrad Irwin <conrad.irwin@gmail.com> Date: Tue, 19 Aug 2025 16:42:52 -0600 Subject: [PATCH 500/693] Hide old Agent UI when ACP flag set (#36533) - **Use key value store instead of JSON** - **Default NewThread to the native agent when flagged** Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/agent_ui/src/agent_panel.rs | 34 ++++++------------------------ 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 0310ae7c80..93e9f619af 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -881,6 +881,9 @@ impl AgentPanel { } fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) { + if cx.has_flag::<AcpFeatureFlag>() { + return self.new_agent_thread(AgentType::NativeAgent, window, cx); + } // Preserve chat box text when using creating new thread let preserved_text = self .active_message_editor() @@ -2386,9 +2389,9 @@ impl AgentPanel { }) .item( ContextMenuEntry::new("New Thread") - .icon(IconName::Thread) - .icon_color(Color::Muted) .action(NewThread::default().boxed_clone()) + .icon(IconName::ZedAssistant) + .icon_color(Color::Muted) .handler({ let workspace = workspace.clone(); move |window, cx| { @@ -2399,7 +2402,7 @@ impl AgentPanel { { panel.update(cx, |panel, cx| { panel.set_selected_agent( - AgentType::Zed, + AgentType::NativeAgent, window, cx, ); @@ -2436,31 +2439,6 @@ impl AgentPanel { } }), ) - .item( - ContextMenuEntry::new("New Native Agent Thread") - .icon(IconName::ZedAssistant) - .icon_color(Color::Muted) - .handler({ - let workspace = workspace.clone(); - move |window, cx| { - if let Some(workspace) = workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = - workspace.panel::<AgentPanel>(cx) - { - panel.update(cx, |panel, cx| { - panel.set_selected_agent( - AgentType::NativeAgent, - window, - cx, - ); - }); - } - }); - } - } - }), - ) .separator() .header("External Agents") .when(cx.has_flag::<AcpFeatureFlag>(), |menu| { From 82ac8a8aaaac089a9e2d1333108686cf2f11636f Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Tue, 19 Aug 2025 20:25:07 -0400 Subject: [PATCH 501/693] collab: Make `stripe_subscription_id` and `stripe_subscription_status` nullable on `billing_subscriptions` (#36536) This PR makes the `stripe_subscription_id` and `stripe_subscription_status` columns nullable on the `billing_subscriptions` table. Release Notes: - N/A --- ...916_make_stripe_fields_optional_on_billing_subscription.sql | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 crates/collab/migrations/20250819225916_make_stripe_fields_optional_on_billing_subscription.sql diff --git a/crates/collab/migrations/20250819225916_make_stripe_fields_optional_on_billing_subscription.sql b/crates/collab/migrations/20250819225916_make_stripe_fields_optional_on_billing_subscription.sql new file mode 100644 index 0000000000..cf3b79da60 --- /dev/null +++ b/crates/collab/migrations/20250819225916_make_stripe_fields_optional_on_billing_subscription.sql @@ -0,0 +1,3 @@ +alter table billing_subscriptions + alter column stripe_subscription_id drop not null, + alter column stripe_subscription_status drop not null; From ce216432be5a967feb0d30ee9878d0cf4fb07cb7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld <maxbrunsfeld@gmail.com> Date: Tue, 19 Aug 2025 17:33:56 -0700 Subject: [PATCH 502/693] Refactor ssh remoting - make ChannelClient type private (#36514) This PR is one step in a series of refactors to prepare for having "remote" projects that do not use SSH. The main use cases for this are WSL and dev containers. Release Notes: - N/A --- crates/editor/src/editor.rs | 5 +- crates/project/src/project.rs | 23 +-- crates/remote/src/ssh_session.rs | 146 +++++++++---------- crates/remote_server/src/headless_project.rs | 67 ++++----- crates/remote_server/src/unix.rs | 13 +- crates/rpc/src/proto_client.rs | 19 +++ crates/tasks_ui/src/tasks_ui.rs | 6 +- 7 files changed, 133 insertions(+), 146 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3805904243..f943e64923 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -14895,10 +14895,7 @@ impl Editor { }; let hide_runnables = project - .update(cx, |project, cx| { - // Do not display any test indicators in non-dev server remote projects. - project.is_via_collab() && project.ssh_connection_string(cx).is_none() - }) + .update(cx, |project, _| project.is_via_collab()) .unwrap_or(true); if hide_runnables { return; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 6712b3fab0..f07ee13866 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1346,14 +1346,13 @@ impl Project { }; // ssh -> local machine handlers - let ssh = ssh.read(cx); - ssh.subscribe_to_entity(SSH_PROJECT_ID, &cx.entity()); - ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.buffer_store); - ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.worktree_store); - ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.lsp_store); - ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.dap_store); - ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.settings_observer); - ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.git_store); + ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &cx.entity()); + ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.buffer_store); + ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.worktree_store); + ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.lsp_store); + ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.dap_store); + ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.settings_observer); + ssh_proto.subscribe_to_entity(SSH_PROJECT_ID, &this.git_store); ssh_proto.add_entity_message_handler(Self::handle_create_buffer_for_peer); ssh_proto.add_entity_message_handler(Self::handle_update_worktree); @@ -1900,14 +1899,6 @@ impl Project { false } - pub fn ssh_connection_string(&self, cx: &App) -> Option<SharedString> { - if let Some(ssh_state) = &self.ssh_client { - return Some(ssh_state.read(cx).connection_string().into()); - } - - None - } - pub fn ssh_connection_state(&self, cx: &App) -> Option<remote::ConnectionState> { self.ssh_client .as_ref() diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index abde2d7568..ffd0cac310 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -26,8 +26,7 @@ use parking_lot::Mutex; use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use rpc::{ - AnyProtoClient, EntityMessageSubscriber, ErrorExt, ProtoClient, ProtoMessageHandlerSet, - RpcError, + AnyProtoClient, ErrorExt, ProtoClient, ProtoMessageHandlerSet, RpcError, proto::{self, Envelope, EnvelopedMessage, PeerId, RequestMessage, build_typed_envelope}, }; use schemars::JsonSchema; @@ -37,7 +36,6 @@ use smol::{ process::{self, Child, Stdio}, }; use std::{ - any::TypeId, collections::VecDeque, fmt, iter, ops::ControlFlow, @@ -664,6 +662,7 @@ impl ConnectionIdentifier { pub fn setup() -> Self { Self::Setup(NEXT_ID.fetch_add(1, SeqCst)) } + // This string gets used in a socket name, and so must be relatively short. // The total length of: // /home/{username}/.local/share/zed/server_state/{name}/stdout.sock @@ -760,6 +759,15 @@ impl SshRemoteClient { }) } + pub fn proto_client_from_channels( + incoming_rx: mpsc::UnboundedReceiver<Envelope>, + outgoing_tx: mpsc::UnboundedSender<Envelope>, + cx: &App, + name: &'static str, + ) -> AnyProtoClient { + ChannelClient::new(incoming_rx, outgoing_tx, cx, name).into() + } + pub fn shutdown_processes<T: RequestMessage>( &self, shutdown_request: Option<T>, @@ -990,64 +998,63 @@ impl SshRemoteClient { }; cx.spawn(async move |cx| { - let mut missed_heartbeats = 0; + let mut missed_heartbeats = 0; - let keepalive_timer = cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse(); - futures::pin_mut!(keepalive_timer); + let keepalive_timer = cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse(); + futures::pin_mut!(keepalive_timer); - loop { - select_biased! { - result = connection_activity_rx.next().fuse() => { - if result.is_none() { - log::warn!("ssh heartbeat: connection activity channel has been dropped. stopping."); - return Ok(()); - } - - if missed_heartbeats != 0 { - missed_heartbeats = 0; - let _ =this.update(cx, |this, cx| { - this.handle_heartbeat_result(missed_heartbeats, cx) - })?; - } + loop { + select_biased! { + result = connection_activity_rx.next().fuse() => { + if result.is_none() { + log::warn!("ssh heartbeat: connection activity channel has been dropped. stopping."); + return Ok(()); } - _ = keepalive_timer => { - log::debug!("Sending heartbeat to server..."); - let result = select_biased! { - _ = connection_activity_rx.next().fuse() => { - Ok(()) - } - ping_result = client.ping(HEARTBEAT_TIMEOUT).fuse() => { - ping_result - } - }; - - if result.is_err() { - missed_heartbeats += 1; - log::warn!( - "No heartbeat from server after {:?}. Missed heartbeat {} out of {}.", - HEARTBEAT_TIMEOUT, - missed_heartbeats, - MAX_MISSED_HEARTBEATS - ); - } else if missed_heartbeats != 0 { - missed_heartbeats = 0; - } else { - continue; - } - - let result = this.update(cx, |this, cx| { + if missed_heartbeats != 0 { + missed_heartbeats = 0; + let _ =this.update(cx, |this, cx| { this.handle_heartbeat_result(missed_heartbeats, cx) })?; - if result.is_break() { - return Ok(()); - } } } + _ = keepalive_timer => { + log::debug!("Sending heartbeat to server..."); - keepalive_timer.set(cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse()); + let result = select_biased! { + _ = connection_activity_rx.next().fuse() => { + Ok(()) + } + ping_result = client.ping(HEARTBEAT_TIMEOUT).fuse() => { + ping_result + } + }; + + if result.is_err() { + missed_heartbeats += 1; + log::warn!( + "No heartbeat from server after {:?}. Missed heartbeat {} out of {}.", + HEARTBEAT_TIMEOUT, + missed_heartbeats, + MAX_MISSED_HEARTBEATS + ); + } else if missed_heartbeats != 0 { + missed_heartbeats = 0; + } else { + continue; + } + + let result = this.update(cx, |this, cx| { + this.handle_heartbeat_result(missed_heartbeats, cx) + })?; + if result.is_break() { + return Ok(()); + } + } } + keepalive_timer.set(cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse()); + } }) } @@ -1145,10 +1152,6 @@ impl SshRemoteClient { cx.notify(); } - pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Entity<E>) { - self.client.subscribe_to_entity(remote_id, entity); - } - pub fn ssh_info(&self) -> Option<(SshArgs, PathStyle)> { self.state .lock() @@ -1222,7 +1225,7 @@ impl SshRemoteClient { pub fn fake_server( client_cx: &mut gpui::TestAppContext, server_cx: &mut gpui::TestAppContext, - ) -> (SshConnectionOptions, Arc<ChannelClient>) { + ) -> (SshConnectionOptions, AnyProtoClient) { let port = client_cx .update(|cx| cx.default_global::<ConnectionPool>().connections.len() as u16 + 1); let opts = SshConnectionOptions { @@ -1255,7 +1258,7 @@ impl SshRemoteClient { }) }); - (opts, server_client) + (opts, server_client.into()) } #[cfg(any(test, feature = "test-support"))] @@ -2269,7 +2272,7 @@ impl SshRemoteConnection { type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, oneshot::Sender<()>)>>>; -pub struct ChannelClient { +struct ChannelClient { next_message_id: AtomicU32, outgoing_tx: Mutex<mpsc::UnboundedSender<Envelope>>, buffer: Mutex<VecDeque<Envelope>>, @@ -2281,7 +2284,7 @@ pub struct ChannelClient { } impl ChannelClient { - pub fn new( + fn new( incoming_rx: mpsc::UnboundedReceiver<Envelope>, outgoing_tx: mpsc::UnboundedSender<Envelope>, cx: &App, @@ -2402,7 +2405,7 @@ impl ChannelClient { }) } - pub fn reconnect( + fn reconnect( self: &Arc<Self>, incoming_rx: UnboundedReceiver<Envelope>, outgoing_tx: UnboundedSender<Envelope>, @@ -2412,26 +2415,7 @@ impl ChannelClient { *self.task.lock() = Self::start_handling_messages(Arc::downgrade(self), incoming_rx, cx); } - pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Entity<E>) { - let id = (TypeId::of::<E>(), remote_id); - - let mut message_handlers = self.message_handlers.lock(); - if message_handlers - .entities_by_type_and_remote_id - .contains_key(&id) - { - panic!("already subscribed to entity"); - } - - message_handlers.entities_by_type_and_remote_id.insert( - id, - EntityMessageSubscriber::Entity { - handle: entity.downgrade().into(), - }, - ); - } - - pub fn request<T: RequestMessage>( + fn request<T: RequestMessage>( &self, payload: T, ) -> impl 'static + Future<Output = Result<T::Response>> { @@ -2453,7 +2437,7 @@ impl ChannelClient { } } - pub async fn resync(&self, timeout: Duration) -> Result<()> { + async fn resync(&self, timeout: Duration) -> Result<()> { smol::future::or( async { self.request_internal(proto::FlushBufferedMessages {}, false) @@ -2475,7 +2459,7 @@ impl ChannelClient { .await } - pub async fn ping(&self, timeout: Duration) -> Result<()> { + async fn ping(&self, timeout: Duration) -> Result<()> { smol::future::or( async { self.request(proto::Ping {}).await?; diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 6fc327ac1c..3bcdcbd73c 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -19,7 +19,6 @@ use project::{ task_store::TaskStore, worktree_store::WorktreeStore, }; -use remote::ssh_session::ChannelClient; use rpc::{ AnyProtoClient, TypedEnvelope, proto::{self, SSH_PEER_ID, SSH_PROJECT_ID}, @@ -50,7 +49,7 @@ pub struct HeadlessProject { } pub struct HeadlessAppState { - pub session: Arc<ChannelClient>, + pub session: AnyProtoClient, pub fs: Arc<dyn Fs>, pub http_client: Arc<dyn HttpClient>, pub node_runtime: NodeRuntime, @@ -81,7 +80,7 @@ impl HeadlessProject { let worktree_store = cx.new(|cx| { let mut store = WorktreeStore::local(true, fs.clone()); - store.shared(SSH_PROJECT_ID, session.clone().into(), cx); + store.shared(SSH_PROJECT_ID, session.clone(), cx); store }); @@ -99,7 +98,7 @@ impl HeadlessProject { let buffer_store = cx.new(|cx| { let mut buffer_store = BufferStore::local(worktree_store.clone(), cx); - buffer_store.shared(SSH_PROJECT_ID, session.clone().into(), cx); + buffer_store.shared(SSH_PROJECT_ID, session.clone(), cx); buffer_store }); @@ -117,7 +116,7 @@ impl HeadlessProject { breakpoint_store.clone(), cx, ); - dap_store.shared(SSH_PROJECT_ID, session.clone().into(), cx); + dap_store.shared(SSH_PROJECT_ID, session.clone(), cx); dap_store }); @@ -129,7 +128,7 @@ impl HeadlessProject { fs.clone(), cx, ); - store.shared(SSH_PROJECT_ID, session.clone().into(), cx); + store.shared(SSH_PROJECT_ID, session.clone(), cx); store }); @@ -152,7 +151,7 @@ impl HeadlessProject { environment.clone(), cx, ); - task_store.shared(SSH_PROJECT_ID, session.clone().into(), cx); + task_store.shared(SSH_PROJECT_ID, session.clone(), cx); task_store }); let settings_observer = cx.new(|cx| { @@ -162,7 +161,7 @@ impl HeadlessProject { task_store.clone(), cx, ); - observer.shared(SSH_PROJECT_ID, session.clone().into(), cx); + observer.shared(SSH_PROJECT_ID, session.clone(), cx); observer }); @@ -183,7 +182,7 @@ impl HeadlessProject { fs.clone(), cx, ); - lsp_store.shared(SSH_PROJECT_ID, session.clone().into(), cx); + lsp_store.shared(SSH_PROJECT_ID, session.clone(), cx); lsp_store }); @@ -210,8 +209,6 @@ impl HeadlessProject { cx, ); - let client: AnyProtoClient = session.clone().into(); - // local_machine -> ssh handlers session.subscribe_to_entity(SSH_PROJECT_ID, &worktree_store); session.subscribe_to_entity(SSH_PROJECT_ID, &buffer_store); @@ -223,44 +220,45 @@ impl HeadlessProject { session.subscribe_to_entity(SSH_PROJECT_ID, &settings_observer); session.subscribe_to_entity(SSH_PROJECT_ID, &git_store); - client.add_request_handler(cx.weak_entity(), Self::handle_list_remote_directory); - client.add_request_handler(cx.weak_entity(), Self::handle_get_path_metadata); - client.add_request_handler(cx.weak_entity(), Self::handle_shutdown_remote_server); - client.add_request_handler(cx.weak_entity(), Self::handle_ping); + session.add_request_handler(cx.weak_entity(), Self::handle_list_remote_directory); + session.add_request_handler(cx.weak_entity(), Self::handle_get_path_metadata); + session.add_request_handler(cx.weak_entity(), Self::handle_shutdown_remote_server); + session.add_request_handler(cx.weak_entity(), Self::handle_ping); - client.add_entity_request_handler(Self::handle_add_worktree); - client.add_request_handler(cx.weak_entity(), Self::handle_remove_worktree); + session.add_entity_request_handler(Self::handle_add_worktree); + session.add_request_handler(cx.weak_entity(), Self::handle_remove_worktree); - client.add_entity_request_handler(Self::handle_open_buffer_by_path); - client.add_entity_request_handler(Self::handle_open_new_buffer); - client.add_entity_request_handler(Self::handle_find_search_candidates); - client.add_entity_request_handler(Self::handle_open_server_settings); + session.add_entity_request_handler(Self::handle_open_buffer_by_path); + session.add_entity_request_handler(Self::handle_open_new_buffer); + session.add_entity_request_handler(Self::handle_find_search_candidates); + session.add_entity_request_handler(Self::handle_open_server_settings); - client.add_entity_request_handler(BufferStore::handle_update_buffer); - client.add_entity_message_handler(BufferStore::handle_close_buffer); + session.add_entity_request_handler(BufferStore::handle_update_buffer); + session.add_entity_message_handler(BufferStore::handle_close_buffer); - client.add_request_handler( + session.add_request_handler( extensions.clone().downgrade(), HeadlessExtensionStore::handle_sync_extensions, ); - client.add_request_handler( + session.add_request_handler( extensions.clone().downgrade(), HeadlessExtensionStore::handle_install_extension, ); - BufferStore::init(&client); - WorktreeStore::init(&client); - SettingsObserver::init(&client); - LspStore::init(&client); - TaskStore::init(Some(&client)); - ToolchainStore::init(&client); - DapStore::init(&client, cx); + BufferStore::init(&session); + WorktreeStore::init(&session); + SettingsObserver::init(&session); + LspStore::init(&session); + TaskStore::init(Some(&session)); + ToolchainStore::init(&session); + DapStore::init(&session, cx); // todo(debugger): Re init breakpoint store when we set it up for collab // BreakpointStore::init(&client); - GitStore::init(&client); + GitStore::init(&session); HeadlessProject { - session: client, + next_entry_id: Default::default(), + session, settings_observer, fs, worktree_store, @@ -268,7 +266,6 @@ impl HeadlessProject { lsp_store, task_store, dap_store, - next_entry_id: Default::default(), languages, extensions, git_store, diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 15a465a880..3352b317cb 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -19,11 +19,11 @@ use project::project_settings::ProjectSettings; use proto::CrashReport; use release_channel::{AppVersion, RELEASE_CHANNEL, ReleaseChannel}; -use remote::proxy::ProxyLaunchError; -use remote::ssh_session::ChannelClient; +use remote::SshRemoteClient; use remote::{ json_log::LogRecord, protocol::{read_message, write_message}, + proxy::ProxyLaunchError, }; use reqwest_client::ReqwestClient; use rpc::proto::{self, Envelope, SSH_PROJECT_ID}; @@ -199,8 +199,7 @@ fn init_panic_hook(session_id: String) { })); } -fn handle_crash_files_requests(project: &Entity<HeadlessProject>, client: &Arc<ChannelClient>) { - let client: AnyProtoClient = client.clone().into(); +fn handle_crash_files_requests(project: &Entity<HeadlessProject>, client: &AnyProtoClient) { client.add_request_handler( project.downgrade(), |_, _: TypedEnvelope<proto::GetCrashFiles>, _cx| async move { @@ -276,7 +275,7 @@ fn start_server( listeners: ServerListeners, log_rx: Receiver<Vec<u8>>, cx: &mut App, -) -> Arc<ChannelClient> { +) -> AnyProtoClient { // This is the server idle timeout. If no connection comes in this timeout, the server will shut down. const IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10 * 60); @@ -395,7 +394,7 @@ fn start_server( }) .detach(); - ChannelClient::new(incoming_rx, outgoing_tx, cx, "server") + SshRemoteClient::proto_client_from_channels(incoming_rx, outgoing_tx, cx, "server") } fn init_paths() -> anyhow::Result<()> { @@ -792,7 +791,7 @@ async fn write_size_prefixed_buffer<S: AsyncWrite + Unpin>( } fn initialize_settings( - session: Arc<ChannelClient>, + session: AnyProtoClient, fs: Arc<dyn Fs>, cx: &mut App, ) -> watch::Receiver<Option<NodeBinaryOptions>> { diff --git a/crates/rpc/src/proto_client.rs b/crates/rpc/src/proto_client.rs index eb570b96a3..05b6bd1439 100644 --- a/crates/rpc/src/proto_client.rs +++ b/crates/rpc/src/proto_client.rs @@ -315,4 +315,23 @@ impl AnyProtoClient { }), ); } + + pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Entity<E>) { + let id = (TypeId::of::<E>(), remote_id); + + let mut message_handlers = self.0.message_handler_set().lock(); + if message_handlers + .entities_by_type_and_remote_id + .contains_key(&id) + { + panic!("already subscribed to entity"); + } + + message_handlers.entities_by_type_and_remote_id.insert( + id, + EntityMessageSubscriber::Entity { + handle: entity.downgrade().into(), + }, + ); + } } diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index dae366a979..a4fdc24e17 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -148,9 +148,9 @@ pub fn toggle_modal( ) -> Task<()> { let task_store = workspace.project().read(cx).task_store().clone(); let workspace_handle = workspace.weak_handle(); - let can_open_modal = workspace.project().update(cx, |project, cx| { - project.is_local() || project.ssh_connection_string(cx).is_some() || project.is_via_ssh() - }); + let can_open_modal = workspace + .project() + .read_with(cx, |project, _| !project.is_via_collab()); if can_open_modal { let task_contexts = task_contexts(workspace, window, cx); cx.spawn_in(window, async move |workspace, cx| { From 714c36fa7b196c398c03c536a973811a8cb5851d Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Tue, 19 Aug 2025 22:30:26 -0300 Subject: [PATCH 503/693] claude: Include all mentions and images in user message (#36539) User messages sent to Claude Code will now include the content of all mentions, and any images included. Release Notes: - N/A --- crates/agent_servers/src/claude.rs | 242 ++++++++++++++++++++++++++--- 1 file changed, 218 insertions(+), 24 deletions(-) diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index f27c973ad6..e214ee875c 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -32,7 +32,7 @@ use util::{ResultExt, debug_panic}; use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig}; use crate::claude::tools::ClaudeTool; use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings}; -use acp_thread::{AcpThread, AgentConnection, AuthRequired}; +use acp_thread::{AcpThread, AgentConnection, AuthRequired, MentionUri}; #[derive(Clone)] pub struct ClaudeCode; @@ -267,27 +267,12 @@ impl AgentConnection for ClaudeAgentConnection { let (end_tx, end_rx) = oneshot::channel(); session.turn_state.replace(TurnState::InProgress { end_tx }); - let mut content = String::new(); - for chunk in params.prompt { - match chunk { - acp::ContentBlock::Text(text_content) => { - content.push_str(&text_content.text); - } - acp::ContentBlock::ResourceLink(resource_link) => { - content.push_str(&format!("@{}", resource_link.uri)); - } - acp::ContentBlock::Audio(_) - | acp::ContentBlock::Image(_) - | acp::ContentBlock::Resource(_) => { - // TODO - } - } - } + let content = acp_content_to_claude(params.prompt); if let Err(err) = session.outgoing_tx.unbounded_send(SdkMessage::User { message: Message { role: Role::User, - content: Content::UntaggedText(content), + content: Content::Chunks(content), id: None, model: None, stop_reason: None, @@ -513,10 +498,17 @@ impl ClaudeAgentSession { chunk ); } + ContentChunk::Image { source } => { + if !turn_state.borrow().is_canceled() { + thread + .update(cx, |thread, cx| { + thread.push_user_content_block(None, source.into(), cx) + }) + .log_err(); + } + } - ContentChunk::Image - | ContentChunk::Document - | ContentChunk::WebSearchToolResult => { + ContentChunk::Document | ContentChunk::WebSearchToolResult => { thread .update(cx, |thread, cx| { thread.push_assistant_content_block( @@ -602,7 +594,14 @@ impl ClaudeAgentSession { "Should not get tool results with role: assistant. should we handle this?" ); } - ContentChunk::Image | ContentChunk::Document => { + ContentChunk::Image { source } => { + thread + .update(cx, |thread, cx| { + thread.push_assistant_content_block(source.into(), false, cx) + }) + .log_err(); + } + ContentChunk::Document => { thread .update(cx, |thread, cx| { thread.push_assistant_content_block( @@ -768,14 +767,44 @@ enum ContentChunk { thinking: String, }, RedactedThinking, + Image { + source: ImageSource, + }, // TODO - Image, Document, WebSearchToolResult, #[serde(untagged)] UntaggedText(String), } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +enum ImageSource { + Base64 { data: String, media_type: String }, + Url { url: String }, +} + +impl Into<acp::ContentBlock> for ImageSource { + fn into(self) -> acp::ContentBlock { + match self { + ImageSource::Base64 { data, media_type } => { + acp::ContentBlock::Image(acp::ImageContent { + annotations: None, + data, + mime_type: media_type, + uri: None, + }) + } + ImageSource::Url { url } => acp::ContentBlock::Image(acp::ImageContent { + annotations: None, + data: "".to_string(), + mime_type: "".to_string(), + uri: Some(url), + }), + } + } +} + impl Display for ContentChunk { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -784,7 +813,7 @@ impl Display for ContentChunk { ContentChunk::RedactedThinking => write!(f, "Thinking: [REDACTED]"), ContentChunk::UntaggedText(text) => write!(f, "{}", text), ContentChunk::ToolResult { content, .. } => write!(f, "{}", content), - ContentChunk::Image + ContentChunk::Image { .. } | ContentChunk::Document | ContentChunk::ToolUse { .. } | ContentChunk::WebSearchToolResult => { @@ -896,6 +925,75 @@ impl Display for ResultErrorType { } } +fn acp_content_to_claude(prompt: Vec<acp::ContentBlock>) -> Vec<ContentChunk> { + let mut content = Vec::with_capacity(prompt.len()); + let mut context = Vec::with_capacity(prompt.len()); + + for chunk in prompt { + match chunk { + acp::ContentBlock::Text(text_content) => { + content.push(ContentChunk::Text { + text: text_content.text, + }); + } + acp::ContentBlock::ResourceLink(resource_link) => { + match MentionUri::parse(&resource_link.uri) { + Ok(uri) => { + content.push(ContentChunk::Text { + text: format!("{}", uri.as_link()), + }); + } + Err(_) => { + content.push(ContentChunk::Text { + text: resource_link.uri, + }); + } + } + } + acp::ContentBlock::Resource(resource) => match resource.resource { + acp::EmbeddedResourceResource::TextResourceContents(resource) => { + match MentionUri::parse(&resource.uri) { + Ok(uri) => { + content.push(ContentChunk::Text { + text: format!("{}", uri.as_link()), + }); + } + Err(_) => { + content.push(ContentChunk::Text { + text: resource.uri.clone(), + }); + } + } + + context.push(ContentChunk::Text { + text: format!( + "\n<context ref=\"{}\">\n{}\n</context>", + resource.uri, resource.text + ), + }); + } + acp::EmbeddedResourceResource::BlobResourceContents(_) => { + // Unsupported by SDK + } + }, + acp::ContentBlock::Image(acp::ImageContent { + data, mime_type, .. + }) => content.push(ContentChunk::Image { + source: ImageSource::Base64 { + data, + media_type: mime_type, + }, + }), + acp::ContentBlock::Audio(_) => { + // Unsupported by SDK + } + } + } + + content.extend(context); + content +} + fn new_request_id() -> String { use rand::Rng; // In the Claude Code TS SDK they just generate a random 12 character string, @@ -1112,4 +1210,100 @@ pub(crate) mod tests { _ => panic!("Expected ToolResult variant"), } } + + #[test] + fn test_acp_content_to_claude() { + let acp_content = vec![ + acp::ContentBlock::Text(acp::TextContent { + text: "Hello world".to_string(), + annotations: None, + }), + acp::ContentBlock::Image(acp::ImageContent { + data: "base64data".to_string(), + mime_type: "image/png".to_string(), + annotations: None, + uri: None, + }), + acp::ContentBlock::ResourceLink(acp::ResourceLink { + uri: "file:///path/to/example.rs".to_string(), + name: "example.rs".to_string(), + annotations: None, + description: None, + mime_type: None, + size: None, + title: None, + }), + acp::ContentBlock::Resource(acp::EmbeddedResource { + annotations: None, + resource: acp::EmbeddedResourceResource::TextResourceContents( + acp::TextResourceContents { + mime_type: None, + text: "fn main() { println!(\"Hello!\"); }".to_string(), + uri: "file:///path/to/code.rs".to_string(), + }, + ), + }), + acp::ContentBlock::ResourceLink(acp::ResourceLink { + uri: "invalid_uri_format".to_string(), + name: "invalid.txt".to_string(), + annotations: None, + description: None, + mime_type: None, + size: None, + title: None, + }), + ]; + + let claude_content = acp_content_to_claude(acp_content); + + assert_eq!(claude_content.len(), 6); + + match &claude_content[0] { + ContentChunk::Text { text } => assert_eq!(text, "Hello world"), + _ => panic!("Expected Text chunk"), + } + + match &claude_content[1] { + ContentChunk::Image { source } => match source { + ImageSource::Base64 { data, media_type } => { + assert_eq!(data, "base64data"); + assert_eq!(media_type, "image/png"); + } + _ => panic!("Expected Base64 image source"), + }, + _ => panic!("Expected Image chunk"), + } + + match &claude_content[2] { + ContentChunk::Text { text } => { + assert!(text.contains("example.rs")); + assert!(text.contains("file:///path/to/example.rs")); + } + _ => panic!("Expected Text chunk for ResourceLink"), + } + + match &claude_content[3] { + ContentChunk::Text { text } => { + assert!(text.contains("code.rs")); + assert!(text.contains("file:///path/to/code.rs")); + } + _ => panic!("Expected Text chunk for Resource"), + } + + match &claude_content[4] { + ContentChunk::Text { text } => { + assert_eq!(text, "invalid_uri_format"); + } + _ => panic!("Expected Text chunk for invalid URI"), + } + + match &claude_content[5] { + ContentChunk::Text { text } => { + assert!(text.contains("<context ref=\"file:///path/to/code.rs\">")); + assert!(text.contains("fn main() { println!(\"Hello!\"); }")); + assert!(text.contains("</context>")); + } + _ => panic!("Expected Text chunk for context"), + } + } } From 7c7043947b1551470a55063ad13e0ea3b6745171 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Tue, 19 Aug 2025 22:42:11 -0300 Subject: [PATCH 504/693] Improve claude tools (#36538) - Return unified diff from `Edit` tool so model can see the final state - Format on save if enabled - Provide `Write` tool - Disable `MultiEdit` tool - Better prompting Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 76 ++++- crates/agent_servers/Cargo.toml | 1 + crates/agent_servers/src/claude.rs | 25 +- crates/agent_servers/src/claude/edit_tool.rs | 178 +++++++++++ crates/agent_servers/src/claude/mcp_server.rs | 279 +----------------- .../src/claude/permission_tool.rs | 158 ++++++++++ crates/agent_servers/src/claude/read_tool.rs | 59 ++++ crates/agent_servers/src/claude/tools.rs | 39 ++- crates/agent_servers/src/claude/write_tool.rs | 59 ++++ crates/context_server/src/listener.rs | 24 +- crates/context_server/src/types.rs | 10 + 11 files changed, 606 insertions(+), 302 deletions(-) create mode 100644 crates/agent_servers/src/claude/edit_tool.rs create mode 100644 crates/agent_servers/src/claude/permission_tool.rs create mode 100644 crates/agent_servers/src/claude/read_tool.rs create mode 100644 crates/agent_servers/src/claude/write_tool.rs diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 793ef35be2..2be7ea7a12 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -3,9 +3,12 @@ mod diff; mod mention; mod terminal; +use collections::HashSet; pub use connection::*; pub use diff::*; +use language::language_settings::FormatOnSave; pub use mention::*; +use project::lsp_store::{FormatTrigger, LspFormatTarget}; use serde::{Deserialize, Serialize}; pub use terminal::*; @@ -1051,6 +1054,22 @@ impl AcpThread { }) } + pub fn tool_call(&mut self, id: &acp::ToolCallId) -> Option<(usize, &ToolCall)> { + self.entries + .iter() + .enumerate() + .rev() + .find_map(|(index, tool_call)| { + if let AgentThreadEntry::ToolCall(tool_call) = tool_call + && &tool_call.id == id + { + Some((index, tool_call)) + } else { + None + } + }) + } + pub fn resolve_locations(&mut self, id: acp::ToolCallId, cx: &mut Context<Self>) { let project = self.project.clone(); let Some((_, tool_call)) = self.tool_call_mut(&id) else { @@ -1601,30 +1620,59 @@ impl AcpThread { .collect::<Vec<_>>() }) .await; - cx.update(|cx| { - project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: edits - .last() - .map(|(range, _)| range.end) - .unwrap_or(Anchor::MIN), - }), - cx, - ); - }); + project.update(cx, |project, cx| { + project.set_agent_location( + Some(AgentLocation { + buffer: buffer.downgrade(), + position: edits + .last() + .map(|(range, _)| range.end) + .unwrap_or(Anchor::MIN), + }), + cx, + ); + })?; + + let format_on_save = cx.update(|cx| { action_log.update(cx, |action_log, cx| { action_log.buffer_read(buffer.clone(), cx); }); - buffer.update(cx, |buffer, cx| { + + let format_on_save = buffer.update(cx, |buffer, cx| { buffer.edit(edits, None, cx); + + let settings = language::language_settings::language_settings( + buffer.language().map(|l| l.name()), + buffer.file(), + cx, + ); + + settings.format_on_save != FormatOnSave::Off }); action_log.update(cx, |action_log, cx| { action_log.buffer_edited(buffer.clone(), cx); }); + format_on_save })?; + + if format_on_save { + let format_task = project.update(cx, |project, cx| { + project.format( + HashSet::from_iter([buffer.clone()]), + LspFormatTarget::Buffers, + false, + FormatTrigger::Save, + cx, + ) + })?; + format_task.await.log_err(); + + action_log.update(cx, |action_log, cx| { + action_log.buffer_edited(buffer.clone(), cx); + })?; + } + project .update(cx, |project, cx| project.save_buffer(buffer, cx))? .await diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index cbc874057a..8cd6980ae1 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -29,6 +29,7 @@ futures.workspace = true gpui.workspace = true indoc.workspace = true itertools.workspace = true +language.workspace = true language_model.workspace = true language_models.workspace = true log.workspace = true diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index e214ee875c..a53c81d4c4 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -1,5 +1,9 @@ +mod edit_tool; mod mcp_server; +mod permission_tool; +mod read_tool; pub mod tools; +mod write_tool; use action_log::ActionLog; use collections::HashMap; @@ -351,18 +355,16 @@ fn spawn_claude( &format!( "mcp__{}__{}", mcp_server::SERVER_NAME, - mcp_server::PermissionTool::NAME, + permission_tool::PermissionTool::NAME, ), "--allowedTools", &format!( - "mcp__{}__{},mcp__{}__{}", + "mcp__{}__{}", mcp_server::SERVER_NAME, - mcp_server::EditTool::NAME, - mcp_server::SERVER_NAME, - mcp_server::ReadTool::NAME + read_tool::ReadTool::NAME ), "--disallowedTools", - "Read,Edit", + "Read,Write,Edit,MultiEdit", ]) .args(match mode { ClaudeSessionMode::Start => ["--session-id".to_string(), session_id.to_string()], @@ -470,9 +472,16 @@ impl ClaudeAgentSession { let content = content.to_string(); thread .update(cx, |thread, cx| { + let id = acp::ToolCallId(tool_use_id.into()); + let set_new_content = !content.is_empty() + && thread.tool_call(&id).is_none_or(|(_, tool_call)| { + // preserve rich diff if we have one + tool_call.diffs().next().is_none() + }); + thread.update_tool_call( acp::ToolCallUpdate { - id: acp::ToolCallId(tool_use_id.into()), + id, fields: acp::ToolCallUpdateFields { status: if turn_state.borrow().is_canceled() { // Do not set to completed if turn was canceled @@ -480,7 +489,7 @@ impl ClaudeAgentSession { } else { Some(acp::ToolCallStatus::Completed) }, - content: (!content.is_empty()) + content: set_new_content .then(|| vec![content.into()]), ..Default::default() }, diff --git a/crates/agent_servers/src/claude/edit_tool.rs b/crates/agent_servers/src/claude/edit_tool.rs new file mode 100644 index 0000000000..a8d93c3f3d --- /dev/null +++ b/crates/agent_servers/src/claude/edit_tool.rs @@ -0,0 +1,178 @@ +use acp_thread::AcpThread; +use anyhow::Result; +use context_server::{ + listener::{McpServerTool, ToolResponse}, + types::{ToolAnnotations, ToolResponseContent}, +}; +use gpui::{AsyncApp, WeakEntity}; +use language::unified_diff; +use util::markdown::MarkdownCodeBlock; + +use crate::tools::EditToolParams; + +#[derive(Clone)] +pub struct EditTool { + thread_rx: watch::Receiver<WeakEntity<AcpThread>>, +} + +impl EditTool { + pub fn new(thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self { + Self { thread_rx } + } +} + +impl McpServerTool for EditTool { + type Input = EditToolParams; + type Output = (); + + const NAME: &'static str = "Edit"; + + fn annotations(&self) -> ToolAnnotations { + ToolAnnotations { + title: Some("Edit file".to_string()), + read_only_hint: Some(false), + destructive_hint: Some(false), + open_world_hint: Some(false), + idempotent_hint: Some(false), + } + } + + async fn run( + &self, + input: Self::Input, + cx: &mut AsyncApp, + ) -> Result<ToolResponse<Self::Output>> { + let mut thread_rx = self.thread_rx.clone(); + let Some(thread) = thread_rx.recv().await?.upgrade() else { + anyhow::bail!("Thread closed"); + }; + + let content = thread + .update(cx, |thread, cx| { + thread.read_text_file(input.abs_path.clone(), None, None, true, cx) + })? + .await?; + + let (new_content, diff) = cx + .background_executor() + .spawn(async move { + let new_content = content.replace(&input.old_text, &input.new_text); + if new_content == content { + return Err(anyhow::anyhow!("Failed to find `old_text`",)); + } + let diff = unified_diff(&content, &new_content); + + Ok((new_content, diff)) + }) + .await?; + + thread + .update(cx, |thread, cx| { + thread.write_text_file(input.abs_path, new_content, cx) + })? + .await?; + + Ok(ToolResponse { + content: vec![ToolResponseContent::Text { + text: MarkdownCodeBlock { + tag: "diff", + text: diff.as_str().trim_end_matches('\n'), + } + .to_string(), + }], + structured_content: (), + }) + } +} + +#[cfg(test)] +mod tests { + use std::rc::Rc; + + use acp_thread::{AgentConnection, StubAgentConnection}; + use gpui::{Entity, TestAppContext}; + use indoc::indoc; + use project::{FakeFs, Project}; + use serde_json::json; + use settings::SettingsStore; + use util::path; + + use super::*; + + #[gpui::test] + async fn old_text_not_found(cx: &mut TestAppContext) { + let (_thread, tool) = init_test(cx).await; + + let result = tool + .run( + EditToolParams { + abs_path: path!("/root/file.txt").into(), + old_text: "hi".into(), + new_text: "bye".into(), + }, + &mut cx.to_async(), + ) + .await; + + assert_eq!(result.unwrap_err().to_string(), "Failed to find `old_text`"); + } + + #[gpui::test] + async fn found_and_replaced(cx: &mut TestAppContext) { + let (_thread, tool) = init_test(cx).await; + + let result = tool + .run( + EditToolParams { + abs_path: path!("/root/file.txt").into(), + old_text: "hello".into(), + new_text: "hi".into(), + }, + &mut cx.to_async(), + ) + .await; + + assert_eq!( + result.unwrap().content[0].text().unwrap(), + indoc! { + r" + ```diff + @@ -1,1 +1,1 @@ + -hello + +hi + ``` + " + } + ); + } + + async fn init_test(cx: &mut TestAppContext) -> (Entity<AcpThread>, EditTool) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + Project::init_settings(cx); + }); + + let connection = Rc::new(StubAgentConnection::new()); + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/root"), + json!({ + "file.txt": "hello" + }), + ) + .await; + let project = Project::test(fs, [path!("/root").as_ref()], cx).await; + let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid()); + + let thread = cx + .update(|cx| connection.new_thread(project, path!("/test").as_ref(), cx)) + .await + .unwrap(); + + thread_tx.send(thread.downgrade()).unwrap(); + + (thread, EditTool::new(thread_rx)) + } +} diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/claude/mcp_server.rs index 3086752850..6442c784b5 100644 --- a/crates/agent_servers/src/claude/mcp_server.rs +++ b/crates/agent_servers/src/claude/mcp_server.rs @@ -1,23 +1,22 @@ use std::path::PathBuf; use std::sync::Arc; -use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams}; +use crate::claude::edit_tool::EditTool; +use crate::claude::permission_tool::PermissionTool; +use crate::claude::read_tool::ReadTool; +use crate::claude::write_tool::WriteTool; use acp_thread::AcpThread; -use agent_client_protocol as acp; -use agent_settings::AgentSettings; -use anyhow::{Context, Result}; +#[cfg(not(test))] +use anyhow::Context as _; +use anyhow::Result; use collections::HashMap; -use context_server::listener::{McpServerTool, ToolResponse}; use context_server::types::{ Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities, - ToolAnnotations, ToolResponseContent, ToolsCapabilities, requests, + ToolsCapabilities, requests, }; use gpui::{App, AsyncApp, Task, WeakEntity}; use project::Fs; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings as _, update_settings_file}; -use util::debug_panic; +use serde::Serialize; pub struct ClaudeZedMcpServer { server: context_server::listener::McpServer, @@ -34,16 +33,10 @@ impl ClaudeZedMcpServer { let mut mcp_server = context_server::listener::McpServer::new(cx).await?; mcp_server.handle_request::<requests::Initialize>(Self::handle_initialize); - mcp_server.add_tool(PermissionTool { - thread_rx: thread_rx.clone(), - fs: fs.clone(), - }); - mcp_server.add_tool(ReadTool { - thread_rx: thread_rx.clone(), - }); - mcp_server.add_tool(EditTool { - thread_rx: thread_rx.clone(), - }); + mcp_server.add_tool(PermissionTool::new(fs.clone(), thread_rx.clone())); + mcp_server.add_tool(ReadTool::new(thread_rx.clone())); + mcp_server.add_tool(EditTool::new(thread_rx.clone())); + mcp_server.add_tool(WriteTool::new(thread_rx.clone())); Ok(Self { server: mcp_server }) } @@ -104,249 +97,3 @@ pub struct McpServerConfig { #[serde(skip_serializing_if = "Option::is_none")] pub env: Option<HashMap<String, String>>, } - -// Tools - -#[derive(Clone)] -pub struct PermissionTool { - fs: Arc<dyn Fs>, - thread_rx: watch::Receiver<WeakEntity<AcpThread>>, -} - -#[derive(Deserialize, JsonSchema, Debug)] -pub struct PermissionToolParams { - tool_name: String, - input: serde_json::Value, - tool_use_id: Option<String>, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct PermissionToolResponse { - behavior: PermissionToolBehavior, - updated_input: serde_json::Value, -} - -#[derive(Serialize)] -#[serde(rename_all = "snake_case")] -enum PermissionToolBehavior { - Allow, - Deny, -} - -impl McpServerTool for PermissionTool { - type Input = PermissionToolParams; - type Output = (); - - const NAME: &'static str = "Confirmation"; - - fn description(&self) -> &'static str { - "Request permission for tool calls" - } - - async fn run( - &self, - input: Self::Input, - cx: &mut AsyncApp, - ) -> Result<ToolResponse<Self::Output>> { - if agent_settings::AgentSettings::try_read_global(cx, |settings| { - settings.always_allow_tool_actions - }) - .unwrap_or(false) - { - let response = PermissionToolResponse { - behavior: PermissionToolBehavior::Allow, - updated_input: input.input, - }; - - return Ok(ToolResponse { - content: vec![ToolResponseContent::Text { - text: serde_json::to_string(&response)?, - }], - structured_content: (), - }); - } - - let mut thread_rx = self.thread_rx.clone(); - let Some(thread) = thread_rx.recv().await?.upgrade() else { - anyhow::bail!("Thread closed"); - }; - - let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone()); - let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into()); - - const ALWAYS_ALLOW: &str = "always_allow"; - const ALLOW: &str = "allow"; - const REJECT: &str = "reject"; - - let chosen_option = thread - .update(cx, |thread, cx| { - thread.request_tool_call_authorization( - claude_tool.as_acp(tool_call_id).into(), - vec![ - acp::PermissionOption { - id: acp::PermissionOptionId(ALWAYS_ALLOW.into()), - name: "Always Allow".into(), - kind: acp::PermissionOptionKind::AllowAlways, - }, - acp::PermissionOption { - id: acp::PermissionOptionId(ALLOW.into()), - name: "Allow".into(), - kind: acp::PermissionOptionKind::AllowOnce, - }, - acp::PermissionOption { - id: acp::PermissionOptionId(REJECT.into()), - name: "Reject".into(), - kind: acp::PermissionOptionKind::RejectOnce, - }, - ], - cx, - ) - })?? - .await?; - - let response = match chosen_option.0.as_ref() { - ALWAYS_ALLOW => { - cx.update(|cx| { - update_settings_file::<AgentSettings>(self.fs.clone(), cx, |settings, _| { - settings.set_always_allow_tool_actions(true); - }); - })?; - - PermissionToolResponse { - behavior: PermissionToolBehavior::Allow, - updated_input: input.input, - } - } - ALLOW => PermissionToolResponse { - behavior: PermissionToolBehavior::Allow, - updated_input: input.input, - }, - REJECT => PermissionToolResponse { - behavior: PermissionToolBehavior::Deny, - updated_input: input.input, - }, - opt => { - debug_panic!("Unexpected option: {}", opt); - PermissionToolResponse { - behavior: PermissionToolBehavior::Deny, - updated_input: input.input, - } - } - }; - - Ok(ToolResponse { - content: vec![ToolResponseContent::Text { - text: serde_json::to_string(&response)?, - }], - structured_content: (), - }) - } -} - -#[derive(Clone)] -pub struct ReadTool { - thread_rx: watch::Receiver<WeakEntity<AcpThread>>, -} - -impl McpServerTool for ReadTool { - type Input = ReadToolParams; - type Output = (); - - const NAME: &'static str = "Read"; - - fn description(&self) -> &'static str { - "Read the contents of a file. In sessions with mcp__zed__Read always use it instead of Read as it contains the most up-to-date contents." - } - - fn annotations(&self) -> ToolAnnotations { - ToolAnnotations { - title: Some("Read file".to_string()), - read_only_hint: Some(true), - destructive_hint: Some(false), - open_world_hint: Some(false), - idempotent_hint: None, - } - } - - async fn run( - &self, - input: Self::Input, - cx: &mut AsyncApp, - ) -> Result<ToolResponse<Self::Output>> { - let mut thread_rx = self.thread_rx.clone(); - let Some(thread) = thread_rx.recv().await?.upgrade() else { - anyhow::bail!("Thread closed"); - }; - - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(input.abs_path, input.offset, input.limit, false, cx) - })? - .await?; - - Ok(ToolResponse { - content: vec![ToolResponseContent::Text { text: content }], - structured_content: (), - }) - } -} - -#[derive(Clone)] -pub struct EditTool { - thread_rx: watch::Receiver<WeakEntity<AcpThread>>, -} - -impl McpServerTool for EditTool { - type Input = EditToolParams; - type Output = (); - - const NAME: &'static str = "Edit"; - - fn description(&self) -> &'static str { - "Edits a file. In sessions with mcp__zed__Edit always use it instead of Edit as it will show the diff to the user better." - } - - fn annotations(&self) -> ToolAnnotations { - ToolAnnotations { - title: Some("Edit file".to_string()), - read_only_hint: Some(false), - destructive_hint: Some(false), - open_world_hint: Some(false), - idempotent_hint: Some(false), - } - } - - async fn run( - &self, - input: Self::Input, - cx: &mut AsyncApp, - ) -> Result<ToolResponse<Self::Output>> { - let mut thread_rx = self.thread_rx.clone(); - let Some(thread) = thread_rx.recv().await?.upgrade() else { - anyhow::bail!("Thread closed"); - }; - - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(input.abs_path.clone(), None, None, true, cx) - })? - .await?; - - let new_content = content.replace(&input.old_text, &input.new_text); - if new_content == content { - return Err(anyhow::anyhow!("The old_text was not found in the content")); - } - - thread - .update(cx, |thread, cx| { - thread.write_text_file(input.abs_path, new_content, cx) - })? - .await?; - - Ok(ToolResponse { - content: vec![], - structured_content: (), - }) - } -} diff --git a/crates/agent_servers/src/claude/permission_tool.rs b/crates/agent_servers/src/claude/permission_tool.rs new file mode 100644 index 0000000000..96a24105e8 --- /dev/null +++ b/crates/agent_servers/src/claude/permission_tool.rs @@ -0,0 +1,158 @@ +use std::sync::Arc; + +use acp_thread::AcpThread; +use agent_client_protocol as acp; +use agent_settings::AgentSettings; +use anyhow::{Context as _, Result}; +use context_server::{ + listener::{McpServerTool, ToolResponse}, + types::ToolResponseContent, +}; +use gpui::{AsyncApp, WeakEntity}; +use project::Fs; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::{Settings as _, update_settings_file}; +use util::debug_panic; + +use crate::tools::ClaudeTool; + +#[derive(Clone)] +pub struct PermissionTool { + fs: Arc<dyn Fs>, + thread_rx: watch::Receiver<WeakEntity<AcpThread>>, +} + +/// Request permission for tool calls +#[derive(Deserialize, JsonSchema, Debug)] +pub struct PermissionToolParams { + tool_name: String, + input: serde_json::Value, + tool_use_id: Option<String>, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PermissionToolResponse { + behavior: PermissionToolBehavior, + updated_input: serde_json::Value, +} + +#[derive(Serialize)] +#[serde(rename_all = "snake_case")] +enum PermissionToolBehavior { + Allow, + Deny, +} + +impl PermissionTool { + pub fn new(fs: Arc<dyn Fs>, thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self { + Self { fs, thread_rx } + } +} + +impl McpServerTool for PermissionTool { + type Input = PermissionToolParams; + type Output = (); + + const NAME: &'static str = "Confirmation"; + + async fn run( + &self, + input: Self::Input, + cx: &mut AsyncApp, + ) -> Result<ToolResponse<Self::Output>> { + if agent_settings::AgentSettings::try_read_global(cx, |settings| { + settings.always_allow_tool_actions + }) + .unwrap_or(false) + { + let response = PermissionToolResponse { + behavior: PermissionToolBehavior::Allow, + updated_input: input.input, + }; + + return Ok(ToolResponse { + content: vec![ToolResponseContent::Text { + text: serde_json::to_string(&response)?, + }], + structured_content: (), + }); + } + + let mut thread_rx = self.thread_rx.clone(); + let Some(thread) = thread_rx.recv().await?.upgrade() else { + anyhow::bail!("Thread closed"); + }; + + let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone()); + let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into()); + + const ALWAYS_ALLOW: &str = "always_allow"; + const ALLOW: &str = "allow"; + const REJECT: &str = "reject"; + + let chosen_option = thread + .update(cx, |thread, cx| { + thread.request_tool_call_authorization( + claude_tool.as_acp(tool_call_id).into(), + vec![ + acp::PermissionOption { + id: acp::PermissionOptionId(ALWAYS_ALLOW.into()), + name: "Always Allow".into(), + kind: acp::PermissionOptionKind::AllowAlways, + }, + acp::PermissionOption { + id: acp::PermissionOptionId(ALLOW.into()), + name: "Allow".into(), + kind: acp::PermissionOptionKind::AllowOnce, + }, + acp::PermissionOption { + id: acp::PermissionOptionId(REJECT.into()), + name: "Reject".into(), + kind: acp::PermissionOptionKind::RejectOnce, + }, + ], + cx, + ) + })?? + .await?; + + let response = match chosen_option.0.as_ref() { + ALWAYS_ALLOW => { + cx.update(|cx| { + update_settings_file::<AgentSettings>(self.fs.clone(), cx, |settings, _| { + settings.set_always_allow_tool_actions(true); + }); + })?; + + PermissionToolResponse { + behavior: PermissionToolBehavior::Allow, + updated_input: input.input, + } + } + ALLOW => PermissionToolResponse { + behavior: PermissionToolBehavior::Allow, + updated_input: input.input, + }, + REJECT => PermissionToolResponse { + behavior: PermissionToolBehavior::Deny, + updated_input: input.input, + }, + opt => { + debug_panic!("Unexpected option: {}", opt); + PermissionToolResponse { + behavior: PermissionToolBehavior::Deny, + updated_input: input.input, + } + } + }; + + Ok(ToolResponse { + content: vec![ToolResponseContent::Text { + text: serde_json::to_string(&response)?, + }], + structured_content: (), + }) + } +} diff --git a/crates/agent_servers/src/claude/read_tool.rs b/crates/agent_servers/src/claude/read_tool.rs new file mode 100644 index 0000000000..cbe25876b3 --- /dev/null +++ b/crates/agent_servers/src/claude/read_tool.rs @@ -0,0 +1,59 @@ +use acp_thread::AcpThread; +use anyhow::Result; +use context_server::{ + listener::{McpServerTool, ToolResponse}, + types::{ToolAnnotations, ToolResponseContent}, +}; +use gpui::{AsyncApp, WeakEntity}; + +use crate::tools::ReadToolParams; + +#[derive(Clone)] +pub struct ReadTool { + thread_rx: watch::Receiver<WeakEntity<AcpThread>>, +} + +impl ReadTool { + pub fn new(thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self { + Self { thread_rx } + } +} + +impl McpServerTool for ReadTool { + type Input = ReadToolParams; + type Output = (); + + const NAME: &'static str = "Read"; + + fn annotations(&self) -> ToolAnnotations { + ToolAnnotations { + title: Some("Read file".to_string()), + read_only_hint: Some(true), + destructive_hint: Some(false), + open_world_hint: Some(false), + idempotent_hint: None, + } + } + + async fn run( + &self, + input: Self::Input, + cx: &mut AsyncApp, + ) -> Result<ToolResponse<Self::Output>> { + let mut thread_rx = self.thread_rx.clone(); + let Some(thread) = thread_rx.recv().await?.upgrade() else { + anyhow::bail!("Thread closed"); + }; + + let content = thread + .update(cx, |thread, cx| { + thread.read_text_file(input.abs_path, input.offset, input.limit, false, cx) + })? + .await?; + + Ok(ToolResponse { + content: vec![ToolResponseContent::Text { text: content }], + structured_content: (), + }) + } +} diff --git a/crates/agent_servers/src/claude/tools.rs b/crates/agent_servers/src/claude/tools.rs index 7ca150c0bd..3be10ed94c 100644 --- a/crates/agent_servers/src/claude/tools.rs +++ b/crates/agent_servers/src/claude/tools.rs @@ -34,6 +34,7 @@ impl ClaudeTool { // Known tools "mcp__zed__Read" => Self::ReadFile(serde_json::from_value(input).log_err()), "mcp__zed__Edit" => Self::Edit(serde_json::from_value(input).log_err()), + "mcp__zed__Write" => Self::Write(serde_json::from_value(input).log_err()), "MultiEdit" => Self::MultiEdit(serde_json::from_value(input).log_err()), "Write" => Self::Write(serde_json::from_value(input).log_err()), "LS" => Self::Ls(serde_json::from_value(input).log_err()), @@ -93,7 +94,7 @@ impl ClaudeTool { } Self::MultiEdit(None) => "Multi Edit".into(), Self::Write(Some(params)) => { - format!("Write {}", params.file_path.display()) + format!("Write {}", params.abs_path.display()) } Self::Write(None) => "Write".into(), Self::Glob(Some(params)) => { @@ -153,7 +154,7 @@ impl ClaudeTool { }], Self::Write(Some(params)) => vec![acp::ToolCallContent::Diff { diff: acp::Diff { - path: params.file_path.clone(), + path: params.abs_path.clone(), old_text: None, new_text: params.content.clone(), }, @@ -229,7 +230,10 @@ impl ClaudeTool { line: None, }] } - Self::Write(Some(WriteToolParams { file_path, .. })) => { + Self::Write(Some(WriteToolParams { + abs_path: file_path, + .. + })) => { vec![acp::ToolCallLocation { path: file_path.clone(), line: None, @@ -302,6 +306,20 @@ impl ClaudeTool { } } +/// Edit a file. +/// +/// In sessions with mcp__zed__Edit always use it instead of Edit as it will +/// allow the user to conveniently review changes. +/// +/// File editing instructions: +/// - The `old_text` param must match existing file content, including indentation. +/// - The `old_text` param must come from the actual file, not an outline. +/// - The `old_text` section must not be empty. +/// - Be minimal with replacements: +/// - For unique lines, include only those lines. +/// - For non-unique lines, include enough context to identify them. +/// - Do not escape quotes, newlines, or other characters. +/// - Only edit the specified file. #[derive(Deserialize, JsonSchema, Debug)] pub struct EditToolParams { /// The absolute path to the file to read. @@ -312,6 +330,11 @@ pub struct EditToolParams { pub new_text: String, } +/// Reads the content of the given file in the project. +/// +/// Never attempt to read a path that hasn't been previously mentioned. +/// +/// In sessions with mcp__zed__Read always use it instead of Read as it contains the most up-to-date contents. #[derive(Deserialize, JsonSchema, Debug)] pub struct ReadToolParams { /// The absolute path to the file to read. @@ -324,11 +347,15 @@ pub struct ReadToolParams { pub limit: Option<u32>, } +/// Writes content to the specified file in the project. +/// +/// In sessions with mcp__zed__Write always use it instead of Write as it will +/// allow the user to conveniently review changes. #[derive(Deserialize, JsonSchema, Debug)] pub struct WriteToolParams { - /// Absolute path for new file - pub file_path: PathBuf, - /// File content + /// The absolute path of the file to write. + pub abs_path: PathBuf, + /// The full content to write. pub content: String, } diff --git a/crates/agent_servers/src/claude/write_tool.rs b/crates/agent_servers/src/claude/write_tool.rs new file mode 100644 index 0000000000..39479a9c38 --- /dev/null +++ b/crates/agent_servers/src/claude/write_tool.rs @@ -0,0 +1,59 @@ +use acp_thread::AcpThread; +use anyhow::Result; +use context_server::{ + listener::{McpServerTool, ToolResponse}, + types::ToolAnnotations, +}; +use gpui::{AsyncApp, WeakEntity}; + +use crate::tools::WriteToolParams; + +#[derive(Clone)] +pub struct WriteTool { + thread_rx: watch::Receiver<WeakEntity<AcpThread>>, +} + +impl WriteTool { + pub fn new(thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self { + Self { thread_rx } + } +} + +impl McpServerTool for WriteTool { + type Input = WriteToolParams; + type Output = (); + + const NAME: &'static str = "Write"; + + fn annotations(&self) -> ToolAnnotations { + ToolAnnotations { + title: Some("Write file".to_string()), + read_only_hint: Some(false), + destructive_hint: Some(false), + open_world_hint: Some(false), + idempotent_hint: Some(false), + } + } + + async fn run( + &self, + input: Self::Input, + cx: &mut AsyncApp, + ) -> Result<ToolResponse<Self::Output>> { + let mut thread_rx = self.thread_rx.clone(); + let Some(thread) = thread_rx.recv().await?.upgrade() else { + anyhow::bail!("Thread closed"); + }; + + thread + .update(cx, |thread, cx| { + thread.write_text_file(input.abs_path, input.content, cx) + })? + .await?; + + Ok(ToolResponse { + content: vec![], + structured_content: (), + }) + } +} diff --git a/crates/context_server/src/listener.rs b/crates/context_server/src/listener.rs index 6f4b5c1369..1b44cefbd2 100644 --- a/crates/context_server/src/listener.rs +++ b/crates/context_server/src/listener.rs @@ -14,6 +14,7 @@ use serde::de::DeserializeOwned; use serde_json::{json, value::RawValue}; use smol::stream::StreamExt; use std::{ + any::TypeId, cell::RefCell, path::{Path, PathBuf}, rc::Rc, @@ -87,18 +88,26 @@ impl McpServer { settings.inline_subschemas = true; let mut generator = settings.into_generator(); - let output_schema = generator.root_schema_for::<T::Output>(); - let unit_schema = generator.root_schema_for::<T::Output>(); + let input_schema = generator.root_schema_for::<T::Input>(); + + let description = input_schema + .get("description") + .and_then(|desc| desc.as_str()) + .map(|desc| desc.to_string()); + debug_assert!( + description.is_some(), + "Input schema struct must include a doc comment for the tool description" + ); let registered_tool = RegisteredTool { tool: Tool { name: T::NAME.into(), - description: Some(tool.description().into()), - input_schema: generator.root_schema_for::<T::Input>().into(), - output_schema: if output_schema == unit_schema { + description, + input_schema: input_schema.into(), + output_schema: if TypeId::of::<T::Output>() == TypeId::of::<()>() { None } else { - Some(output_schema.into()) + Some(generator.root_schema_for::<T::Output>().into()) }, annotations: Some(tool.annotations()), }, @@ -399,8 +408,6 @@ pub trait McpServerTool { const NAME: &'static str; - fn description(&self) -> &'static str; - fn annotations(&self) -> ToolAnnotations { ToolAnnotations { title: None, @@ -418,6 +425,7 @@ pub trait McpServerTool { ) -> impl Future<Output = Result<ToolResponse<Self::Output>>>; } +#[derive(Debug)] pub struct ToolResponse<T> { pub content: Vec<ToolResponseContent>, pub structured_content: T, diff --git a/crates/context_server/src/types.rs b/crates/context_server/src/types.rs index e92a18c763..03aca4f3ca 100644 --- a/crates/context_server/src/types.rs +++ b/crates/context_server/src/types.rs @@ -711,6 +711,16 @@ pub enum ToolResponseContent { Resource { resource: ResourceContents }, } +impl ToolResponseContent { + pub fn text(&self) -> Option<&str> { + if let ToolResponseContent::Text { text } = self { + Some(text) + } else { + None + } + } +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ListToolsResponse { From 3996587c0b05ec30c54491f6911edb24c01996a5 Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Tue, 19 Aug 2025 21:59:14 -0400 Subject: [PATCH 505/693] Add version detection for CC (#36502) - Render a helpful message when the installed CC version is too old - Show the full path for agent binaries when the version is not recent enough (helps in cases where multiple binaries are installed in different places) - Add UI for the case where a server binary is not installed at all - Refresh thread view after installing/updating server binary Release Notes: - N/A --- Cargo.lock | 1 + crates/acp_thread/src/acp_thread.rs | 22 ++- crates/agent_servers/Cargo.toml | 1 + crates/agent_servers/src/acp/v1.rs | 6 +- crates/agent_servers/src/claude.rs | 57 +++++++- crates/agent_servers/src/gemini.rs | 11 +- crates/agent_ui/src/acp/thread_view.rs | 180 +++++++++++++++---------- crates/agent_ui/src/agent_diff.rs | 2 +- 8 files changed, 195 insertions(+), 85 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34a8ceac49..5dced73fb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -285,6 +285,7 @@ dependencies = [ "project", "rand 0.8.5", "schemars", + "semver", "serde", "serde_json", "settings", diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 2be7ea7a12..5d3b35d018 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -707,7 +707,7 @@ pub enum AcpThreadEvent { Retry(RetryStatus), Stopped, Error, - ServerExited(ExitStatus), + LoadError(LoadError), } impl EventEmitter<AcpThreadEvent> for AcpThread {} @@ -721,20 +721,30 @@ pub enum ThreadStatus { #[derive(Debug, Clone)] pub enum LoadError { + NotInstalled { + error_message: SharedString, + install_message: SharedString, + install_command: String, + }, Unsupported { error_message: SharedString, upgrade_message: SharedString, upgrade_command: String, }, - Exited(i32), + Exited { + status: ExitStatus, + }, Other(SharedString), } impl Display for LoadError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - LoadError::Unsupported { error_message, .. } => write!(f, "{}", error_message), - LoadError::Exited(status) => write!(f, "Server exited with status {}", status), + LoadError::NotInstalled { error_message, .. } + | LoadError::Unsupported { error_message, .. } => { + write!(f, "{error_message}") + } + LoadError::Exited { status } => write!(f, "Server exited with status {status}"), LoadError::Other(msg) => write!(f, "{}", msg), } } @@ -1683,8 +1693,8 @@ impl AcpThread { self.entries.iter().map(|e| e.to_markdown(cx)).collect() } - pub fn emit_server_exited(&mut self, status: ExitStatus, cx: &mut Context<Self>) { - cx.emit(AcpThreadEvent::ServerExited(status)); + pub fn emit_load_error(&mut self, error: LoadError, cx: &mut Context<Self>) { + cx.emit(AcpThreadEvent::LoadError(error)); } } diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index 8cd6980ae1..b654486cb6 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -37,6 +37,7 @@ paths.workspace = true project.workspace = true rand.workspace = true schemars.workspace = true +semver.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index d749537c4c..e0e92f29ba 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -14,7 +14,7 @@ use anyhow::{Context as _, Result}; use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity}; use crate::{AgentServerCommand, acp::UnsupportedVersion}; -use acp_thread::{AcpThread, AgentConnection, AuthRequired}; +use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError}; pub struct AcpConnection { server_name: &'static str, @@ -87,7 +87,9 @@ impl AcpConnection { for session in sessions.borrow().values() { session .thread - .update(cx, |thread, cx| thread.emit_server_exited(status, cx)) + .update(cx, |thread, cx| { + thread.emit_load_error(LoadError::Exited { status }, cx) + }) .ok(); } diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index a53c81d4c4..3008edebeb 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -15,8 +15,9 @@ use smol::process::Child; use std::any::Any; use std::cell::RefCell; use std::fmt::Display; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::rc::Rc; +use util::command::new_smol_command; use uuid::Uuid; use agent_client_protocol as acp; @@ -36,7 +37,7 @@ use util::{ResultExt, debug_panic}; use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig}; use crate::claude::tools::ClaudeTool; use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings}; -use acp_thread::{AcpThread, AgentConnection, AuthRequired, MentionUri}; +use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri}; #[derive(Clone)] pub struct ClaudeCode; @@ -103,7 +104,11 @@ impl AgentConnection for ClaudeAgentConnection { ) .await else { - anyhow::bail!("Failed to find claude binary"); + return Err(LoadError::NotInstalled { + error_message: "Failed to find Claude Code binary".into(), + install_message: "Install Claude Code".into(), + install_command: "npm install -g @anthropic-ai/claude-code@latest".into(), + }.into()); }; let api_key = @@ -211,9 +216,32 @@ impl AgentConnection for ClaudeAgentConnection { if let Some(status) = child.status().await.log_err() && let Some(thread) = thread_rx.recv().await.ok() { + let version = claude_version(command.path.clone(), cx).await.log_err(); + let help = claude_help(command.path.clone(), cx).await.log_err(); thread .update(cx, |thread, cx| { - thread.emit_server_exited(status, cx); + let error = if let Some(version) = version + && let Some(help) = help + && (!help.contains("--input-format") + || !help.contains("--session-id")) + { + LoadError::Unsupported { + error_message: format!( + "Your installed version of Claude Code ({}, version {}) does not have required features for use with Zed.", + command.path.to_string_lossy(), + version, + ) + .into(), + upgrade_message: "Upgrade Claude Code to latest".into(), + upgrade_command: format!( + "{} update", + command.path.to_string_lossy() + ), + } + } else { + LoadError::Exited { status } + }; + thread.emit_load_error(error, cx); }) .ok(); } @@ -383,6 +411,27 @@ fn spawn_claude( Ok(child) } +fn claude_version(path: PathBuf, cx: &mut AsyncApp) -> Task<Result<semver::Version>> { + cx.background_spawn(async move { + let output = new_smol_command(path).arg("--version").output().await?; + let output = String::from_utf8(output.stdout)?; + let version = output + .trim() + .strip_suffix(" (Claude Code)") + .context("parsing Claude version")?; + let version = semver::Version::parse(version)?; + anyhow::Ok(version) + }) +} + +fn claude_help(path: PathBuf, cx: &mut AsyncApp) -> Task<Result<String>> { + cx.background_spawn(async move { + let output = new_smol_command(path).arg("--help").output().await?; + let output = String::from_utf8(output.stdout)?; + anyhow::Ok(output) + }) +} + struct ClaudeAgentSession { outgoing_tx: UnboundedSender<SdkMessage>, turn_state: Rc<RefCell<TurnState>>, diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 167e632d79..e1ecaf0bb5 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -50,7 +50,11 @@ impl AgentServer for Gemini { let Some(command) = AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await else { - anyhow::bail!("Failed to find gemini binary"); + return Err(LoadError::NotInstalled { + error_message: "Failed to find Gemini CLI binary".into(), + install_message: "Install Gemini CLI".into(), + install_command: "npm install -g @google/gemini-cli@latest".into() + }.into()); }; let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await; @@ -75,10 +79,11 @@ impl AgentServer for Gemini { if !supported { return Err(LoadError::Unsupported { error_message: format!( - "Your installed version of Gemini {} doesn't support the Agentic Coding Protocol (ACP).", + "Your installed version of Gemini CLI ({}, version {}) doesn't support the Agentic Coding Protocol (ACP).", + command.path.to_string_lossy(), current_version ).into(), - upgrade_message: "Upgrade Gemini to Latest".into(), + upgrade_message: "Upgrade Gemini CLI to latest".into(), upgrade_command: "npm install -g @google/gemini-cli@latest".into(), }.into()) } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 5e5d4bb83c..4a9001b9f4 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -37,7 +37,7 @@ use rope::Point; use settings::{Settings as _, SettingsStore}; use std::sync::Arc; use std::time::Instant; -use std::{collections::BTreeMap, process::ExitStatus, rc::Rc, time::Duration}; +use std::{collections::BTreeMap, rc::Rc, time::Duration}; use text::Anchor; use theme::ThemeSettings; use ui::{ @@ -149,9 +149,6 @@ enum ThreadState { configuration_view: Option<AnyView>, _subscription: Option<Subscription>, }, - ServerExited { - status: ExitStatus, - }, } impl AcpThreadView { @@ -451,8 +448,7 @@ impl AcpThreadView { ThreadState::Ready { thread, .. } => Some(thread), ThreadState::Unauthenticated { .. } | ThreadState::Loading { .. } - | ThreadState::LoadError(..) - | ThreadState::ServerExited { .. } => None, + | ThreadState::LoadError { .. } => None, } } @@ -462,7 +458,6 @@ impl AcpThreadView { ThreadState::Loading { .. } => "Loading…".into(), ThreadState::LoadError(_) => "Failed to load".into(), ThreadState::Unauthenticated { .. } => "Authentication Required".into(), - ThreadState::ServerExited { .. } => "Server exited unexpectedly".into(), } } @@ -830,9 +825,9 @@ impl AcpThreadView { cx, ); } - AcpThreadEvent::ServerExited(status) => { + AcpThreadEvent::LoadError(error) => { self.thread_retry_status.take(); - self.thread_state = ThreadState::ServerExited { status: *status }; + self.thread_state = ThreadState::LoadError(error.clone()); } AcpThreadEvent::TitleUpdated | AcpThreadEvent::TokenUsageUpdated => {} } @@ -2154,28 +2149,6 @@ impl AcpThreadView { )) } - fn render_server_exited(&self, status: ExitStatus, _cx: &Context<Self>) -> AnyElement { - v_flex() - .items_center() - .justify_center() - .child(self.render_error_agent_logo()) - .child( - v_flex() - .mt_4() - .mb_2() - .gap_0p5() - .text_center() - .items_center() - .child(Headline::new("Server exited unexpectedly").size(HeadlineSize::Medium)) - .child( - Label::new(format!("Exit status: {}", status.code().unwrap_or(-127))) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - .into_any_element() - } - fn render_load_error(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement { let mut container = v_flex() .items_center() @@ -2204,39 +2177,102 @@ impl AcpThreadView { { let upgrade_message = upgrade_message.clone(); let upgrade_command = upgrade_command.clone(); - container = container.child(Button::new("upgrade", upgrade_message).on_click( - cx.listener(move |this, _, window, cx| { - this.workspace - .update(cx, |workspace, cx| { - let project = workspace.project().read(cx); - let cwd = project.first_project_directory(cx); - let shell = project.terminal_settings(&cwd, cx).shell.clone(); - let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId("install".to_string()), - full_label: upgrade_command.clone(), - label: upgrade_command.clone(), - command: Some(upgrade_command.clone()), - args: Vec::new(), - command_label: upgrade_command.clone(), - cwd, - env: Default::default(), - use_new_terminal: true, - allow_concurrent_runs: true, - reveal: Default::default(), - reveal_target: Default::default(), - hide: Default::default(), - shell, - show_summary: true, - show_command: true, - show_rerun: false, - }; - workspace - .spawn_in_terminal(spawn_in_terminal, window, cx) - .detach(); + container = container.child( + Button::new("upgrade", upgrade_message) + .tooltip(Tooltip::text(upgrade_command.clone())) + .on_click(cx.listener(move |this, _, window, cx| { + let task = this + .workspace + .update(cx, |workspace, cx| { + let project = workspace.project().read(cx); + let cwd = project.first_project_directory(cx); + let shell = project.terminal_settings(&cwd, cx).shell.clone(); + let spawn_in_terminal = task::SpawnInTerminal { + id: task::TaskId("upgrade".to_string()), + full_label: upgrade_command.clone(), + label: upgrade_command.clone(), + command: Some(upgrade_command.clone()), + args: Vec::new(), + command_label: upgrade_command.clone(), + cwd, + env: Default::default(), + use_new_terminal: true, + allow_concurrent_runs: true, + reveal: Default::default(), + reveal_target: Default::default(), + hide: Default::default(), + shell, + show_summary: true, + show_command: true, + show_rerun: false, + }; + workspace.spawn_in_terminal(spawn_in_terminal, window, cx) + }) + .ok(); + let Some(task) = task else { return }; + cx.spawn_in(window, async move |this, cx| { + if let Some(Ok(_)) = task.await { + this.update_in(cx, |this, window, cx| { + this.reset(window, cx); + }) + .ok(); + } }) - .ok(); - }), - )); + .detach() + })), + ); + } else if let LoadError::NotInstalled { + install_message, + install_command, + .. + } = e + { + let install_message = install_message.clone(); + let install_command = install_command.clone(); + container = container.child( + Button::new("install", install_message) + .tooltip(Tooltip::text(install_command.clone())) + .on_click(cx.listener(move |this, _, window, cx| { + let task = this + .workspace + .update(cx, |workspace, cx| { + let project = workspace.project().read(cx); + let cwd = project.first_project_directory(cx); + let shell = project.terminal_settings(&cwd, cx).shell.clone(); + let spawn_in_terminal = task::SpawnInTerminal { + id: task::TaskId("install".to_string()), + full_label: install_command.clone(), + label: install_command.clone(), + command: Some(install_command.clone()), + args: Vec::new(), + command_label: install_command.clone(), + cwd, + env: Default::default(), + use_new_terminal: true, + allow_concurrent_runs: true, + reveal: Default::default(), + reveal_target: Default::default(), + hide: Default::default(), + shell, + show_summary: true, + show_command: true, + show_rerun: false, + }; + workspace.spawn_in_terminal(spawn_in_terminal, window, cx) + }) + .ok(); + let Some(task) = task else { return }; + cx.spawn_in(window, async move |this, cx| { + if let Some(Ok(_)) = task.await { + this.update_in(cx, |this, window, cx| { + this.reset(window, cx); + }) + .ok(); + } + }) + .detach() + })), + ); } container.into_any() @@ -3705,6 +3741,18 @@ impl AcpThreadView { } })) } + + fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) { + self.thread_state = Self::initial_state( + self.agent.clone(), + None, + self.workspace.clone(), + self.project.clone(), + window, + cx, + ); + cx.notify(); + } } impl Focusable for AcpThreadView { @@ -3743,12 +3791,6 @@ impl Render for AcpThreadView { .items_center() .justify_center() .child(self.render_load_error(e, cx)), - ThreadState::ServerExited { status } => v_flex() - .p_2() - .flex_1() - .items_center() - .justify_center() - .child(self.render_server_exited(*status, cx)), ThreadState::Ready { thread, .. } => { let thread_clone = thread.clone(); diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index a695136562..b20b126d9b 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1522,7 +1522,7 @@ impl AgentDiff { self.update_reviewing_editors(workspace, window, cx); } } - AcpThreadEvent::Stopped | AcpThreadEvent::Error | AcpThreadEvent::ServerExited(_) => { + AcpThreadEvent::Stopped | AcpThreadEvent::Error | AcpThreadEvent::LoadError(_) => { self.update_reviewing_editors(workspace, window, cx); } AcpThreadEvent::TitleUpdated From b12d862236d9872a31868746c4ed7423535137d6 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Tue, 19 Aug 2025 23:11:17 -0300 Subject: [PATCH 506/693] Rename acp flag (#36541) Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 18 +++++++++--------- crates/feature_flags/src/feature_flags.rs | 11 ++++++++--- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 93e9f619af..297bb5f3e8 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -45,7 +45,7 @@ use assistant_tool::ToolWorkingSet; use client::{UserStore, zed_urls}; use cloud_llm_client::{CompletionIntent, Plan, UsageLimit}; use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer}; -use feature_flags::{self, AcpFeatureFlag, ClaudeCodeFeatureFlag, FeatureFlagAppExt}; +use feature_flags::{self, ClaudeCodeFeatureFlag, FeatureFlagAppExt, GeminiAndNativeFeatureFlag}; use fs::Fs; use gpui::{ Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem, @@ -725,7 +725,7 @@ impl AgentPanel { let assistant_navigation_menu = ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| { if let Some(panel) = panel.upgrade() { - if cx.has_flag::<AcpFeatureFlag>() { + if cx.has_flag::<GeminiAndNativeFeatureFlag>() { menu = Self::populate_recently_opened_menu_section_new(menu, panel, cx); } else { menu = Self::populate_recently_opened_menu_section_old(menu, panel, cx); @@ -881,7 +881,7 @@ impl AgentPanel { } fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) { - if cx.has_flag::<AcpFeatureFlag>() { + if cx.has_flag::<GeminiAndNativeFeatureFlag>() { return self.new_agent_thread(AgentType::NativeAgent, window, cx); } // Preserve chat box text when using creating new thread @@ -1058,7 +1058,7 @@ impl AgentPanel { this.update_in(cx, |this, window, cx| { match ext_agent { crate::ExternalAgent::Gemini | crate::ExternalAgent::NativeAgent => { - if !cx.has_flag::<AcpFeatureFlag>() { + if !cx.has_flag::<GeminiAndNativeFeatureFlag>() { return; } } @@ -1825,7 +1825,7 @@ impl Focusable for AgentPanel { ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx), ActiveView::ExternalAgentThread { thread_view, .. } => thread_view.focus_handle(cx), ActiveView::History => { - if cx.has_flag::<feature_flags::AcpFeatureFlag>() { + if cx.has_flag::<feature_flags::GeminiAndNativeFeatureFlag>() { self.acp_history.focus_handle(cx) } else { self.history.focus_handle(cx) @@ -2441,7 +2441,7 @@ impl AgentPanel { ) .separator() .header("External Agents") - .when(cx.has_flag::<AcpFeatureFlag>(), |menu| { + .when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| { menu.item( ContextMenuEntry::new("New Gemini Thread") .icon(IconName::AiGemini) @@ -2564,7 +2564,7 @@ impl AgentPanel { } fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { - if cx.has_flag::<feature_flags::AcpFeatureFlag>() + if cx.has_flag::<feature_flags::GeminiAndNativeFeatureFlag>() || cx.has_flag::<feature_flags::ClaudeCodeFeatureFlag>() { self.render_toolbar_new(window, cx).into_any_element() @@ -2749,7 +2749,7 @@ impl AgentPanel { false } _ => { - let history_is_empty = if cx.has_flag::<AcpFeatureFlag>() { + let history_is_empty = if cx.has_flag::<GeminiAndNativeFeatureFlag>() { self.acp_history_store.read(cx).is_empty(cx) } else { self.history_store @@ -3641,7 +3641,7 @@ impl Render for AgentPanel { .child(thread_view.clone()) .child(self.render_drag_target(cx)), ActiveView::History => { - if cx.has_flag::<feature_flags::AcpFeatureFlag>() { + if cx.has_flag::<feature_flags::GeminiAndNativeFeatureFlag>() { parent.child(self.acp_history.clone()) } else { parent.child(self.history.clone()) diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index 49ccfcc85c..422979c429 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -89,10 +89,15 @@ impl FeatureFlag for JjUiFeatureFlag { const NAME: &'static str = "jj-ui"; } -pub struct AcpFeatureFlag; +pub struct GeminiAndNativeFeatureFlag; -impl FeatureFlag for AcpFeatureFlag { - const NAME: &'static str = "acp"; +impl FeatureFlag for GeminiAndNativeFeatureFlag { + // This was previously called "acp". + // + // We renamed it because existing builds used it to enable the Claude Code + // integration too, and we'd like to turn Gemini/Native on in new builds + // without enabling Claude Code in old builds. + const NAME: &'static str = "gemini-and-native"; } pub struct ClaudeCodeFeatureFlag; From cac80e2ebde41fb66de4e47cc9f8e9a8398cb7a6 Mon Sep 17 00:00:00 2001 From: Conrad Irwin <conrad.irwin@gmail.com> Date: Tue, 19 Aug 2025 20:26:56 -0600 Subject: [PATCH 507/693] Silence a bucketload of logs (#36534) Closes #ISSUE Release Notes: - Silenced a bunch of logs that were on by default --- crates/agent2/src/tools/terminal_tool.rs | 5 +---- crates/assistant_context/src/context_store.rs | 2 +- crates/assistant_tools/src/terminal_tool.rs | 5 +---- crates/context_server/src/client.rs | 4 ++-- crates/context_server/src/context_server.rs | 2 +- crates/db/src/db.rs | 2 +- crates/editor/src/editor.rs | 5 +---- crates/gpui/src/arena.rs | 2 +- crates/language/src/language.rs | 6 +++--- crates/project/src/context_server_store.rs | 1 - crates/project/src/context_server_store/extension.rs | 2 +- crates/project/src/debugger/breakpoint_store.rs | 1 - crates/project/src/lsp_store.rs | 4 ++-- crates/prompt_store/src/prompts.rs | 8 ++++---- crates/rpc/src/peer.rs | 2 -- crates/workspace/src/workspace.rs | 2 -- crates/zeta/src/license_detection.rs | 8 ++++---- 17 files changed, 23 insertions(+), 38 deletions(-) diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs index d8f0282f4b..17e671fba3 100644 --- a/crates/agent2/src/tools/terminal_tool.rs +++ b/crates/agent2/src/tools/terminal_tool.rs @@ -47,12 +47,9 @@ impl TerminalTool { } if which::which("bash").is_ok() { - log::info!("agent selected bash for terminal tool"); "bash".into() } else { - let shell = get_system_shell(); - log::info!("agent selected {shell} for terminal tool"); - shell + get_system_shell() } }); Self { diff --git a/crates/assistant_context/src/context_store.rs b/crates/assistant_context/src/context_store.rs index 6d13531a57..c5b5e99a52 100644 --- a/crates/assistant_context/src/context_store.rs +++ b/crates/assistant_context/src/context_store.rs @@ -905,7 +905,7 @@ impl ContextStore { .into_iter() .filter(assistant_slash_commands::acceptable_prompt) .map(|prompt| { - log::info!("registering context server command: {:?}", prompt.name); + log::debug!("registering context server command: {:?}", prompt.name); slash_command_working_set.insert(Arc::new( assistant_slash_commands::ContextServerSlashCommand::new( context_server_store.clone(), diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index 14bbcef8b4..358d62ee1a 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -59,12 +59,9 @@ impl TerminalTool { } if which::which("bash").is_ok() { - log::info!("agent selected bash for terminal tool"); "bash".into() } else { - let shell = get_system_shell(); - log::info!("agent selected {shell} for terminal tool"); - shell + get_system_shell() } }); Self { diff --git a/crates/context_server/src/client.rs b/crates/context_server/src/client.rs index 609d2c43e3..ccf7622d82 100644 --- a/crates/context_server/src/client.rs +++ b/crates/context_server/src/client.rs @@ -161,7 +161,7 @@ impl Client { working_directory: &Option<PathBuf>, cx: AsyncApp, ) -> Result<Self> { - log::info!( + log::debug!( "starting context server (executable={:?}, args={:?})", binary.executable, &binary.args @@ -295,7 +295,7 @@ impl Client { /// Continuously reads and logs any error messages from the server. async fn handle_err(transport: Arc<dyn Transport>) -> anyhow::Result<()> { while let Some(err) = transport.receive_err().next().await { - log::warn!("context server stderr: {}", err.trim()); + log::debug!("context server stderr: {}", err.trim()); } Ok(()) diff --git a/crates/context_server/src/context_server.rs b/crates/context_server/src/context_server.rs index 34fa29678d..9ca78138db 100644 --- a/crates/context_server/src/context_server.rs +++ b/crates/context_server/src/context_server.rs @@ -137,7 +137,7 @@ impl ContextServer { } async fn initialize(&self, client: Client) -> Result<()> { - log::info!("starting context server {}", self.id); + log::debug!("starting context server {}", self.id); let protocol = crate::protocol::ModelContextProtocol::new(client); let client_info = types::Implementation { name: "Zed".to_string(), diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 37e347282d..8b790cbec8 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -74,7 +74,7 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, scope: &str) -> Threa } async fn open_main_db<M: Migrator>(db_path: &Path) -> Option<ThreadSafeConnection> { - log::info!("Opening database {}", db_path.display()); + log::trace!("Opening database {}", db_path.display()); ThreadSafeConnection::builder::<M>(db_path.to_string_lossy().as_ref(), true) .with_db_initialization_query(DB_INITIALIZE_QUERY) .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f943e64923..575631b517 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -16606,10 +16606,7 @@ impl Editor { .transaction(transaction_id_prev) .map(|t| t.0.clone()) }) - .unwrap_or_else(|| { - log::info!("Failed to determine selections from before format. Falling back to selections when format was initiated"); - self.selections.disjoint_anchors() - }); + .unwrap_or_else(|| self.selections.disjoint_anchors()); let mut timeout = cx.background_executor().timer(FORMAT_TIMEOUT).fuse(); let format = project.update(cx, |project, cx| { diff --git a/crates/gpui/src/arena.rs b/crates/gpui/src/arena.rs index ee72d0e964..0983bd2345 100644 --- a/crates/gpui/src/arena.rs +++ b/crates/gpui/src/arena.rs @@ -142,7 +142,7 @@ impl Arena { if self.current_chunk_index >= self.chunks.len() { self.chunks.push(Chunk::new(self.chunk_size)); assert_eq!(self.current_chunk_index, self.chunks.len() - 1); - log::info!( + log::trace!( "increased element arena capacity to {}kb", self.capacity() / 1024, ); diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index b70e466246..87fc846a53 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -331,7 +331,7 @@ pub trait LspAdapter: 'static + Send + Sync { // for each worktree we might have open. if binary_options.allow_path_lookup && let Some(binary) = self.check_if_user_installed(delegate.as_ref(), toolchains, cx).await { - log::info!( + log::debug!( "found user-installed language server for {}. path: {:?}, arguments: {:?}", self.name().0, binary.path, @@ -601,7 +601,7 @@ async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized> } let name = adapter.name(); - log::info!("fetching latest version of language server {:?}", name.0); + log::debug!("fetching latest version of language server {:?}", name.0); delegate.update_status(name.clone(), BinaryStatus::CheckingForUpdate); let latest_version = adapter @@ -612,7 +612,7 @@ async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized> .check_if_version_installed(latest_version.as_ref(), &container_dir, delegate.as_ref()) .await { - log::info!("language server {:?} is already installed", name.0); + log::debug!("language server {:?} is already installed", name.0); delegate.update_status(name.clone(), BinaryStatus::None); Ok(binary) } else { diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index 16625caeb4..e826f44b7b 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -399,7 +399,6 @@ impl ContextServerStore { async move |this, cx| { match server.clone().start(cx).await { Ok(_) => { - log::info!("Started {} context server", id); debug_assert!(server.client().is_some()); this.update(cx, |this, cx| { diff --git a/crates/project/src/context_server_store/extension.rs b/crates/project/src/context_server_store/extension.rs index 1eb0fe7da1..2a3a0c2e4b 100644 --- a/crates/project/src/context_server_store/extension.rs +++ b/crates/project/src/context_server_store/extension.rs @@ -63,7 +63,7 @@ impl registry::ContextServerDescriptor for ContextServerDescriptor { .await?; command.command = extension.path_from_extension(&command.command); - log::info!("loaded command for context server {id}: {command:?}"); + log::debug!("loaded command for context server {id}: {command:?}"); Ok(ContextServerCommand { path: command.command, diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index 38d8b4cfc6..00fcc7e69f 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -831,7 +831,6 @@ impl BreakpointStore { new_breakpoints.insert(path, breakpoints_for_file); } this.update(cx, |this, cx| { - log::info!("Finish deserializing breakpoints & initializing breakpoint store"); for (path, count) in new_breakpoints.iter().map(|(path, bp_in_file)| { (path.to_string_lossy(), bp_in_file.breakpoints.len()) }) { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index a8c6ffd878..d2fb12ee37 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -296,7 +296,7 @@ impl LocalLspStore { let stderr_capture = Arc::new(Mutex::new(Some(String::new()))); let server_id = self.languages.next_language_server_id(); - log::info!( + log::trace!( "attempting to start language server {:?}, path: {root_path:?}, id: {server_id}", adapter.name.0 ); @@ -7529,7 +7529,7 @@ impl LspStore { .ok() .flatten()?; - log::info!("Refreshing workspace configurations for servers {refreshed_servers:?}"); + log::debug!("Refreshing workspace configurations for servers {refreshed_servers:?}"); // TODO this asynchronous job runs concurrently with extension (de)registration and may take enough time for a certain extension // to stop and unregister its language server wrapper. // This is racy : an extension might have already removed all `local.language_servers` state, but here we `.clone()` and hold onto it anyway. diff --git a/crates/prompt_store/src/prompts.rs b/crates/prompt_store/src/prompts.rs index cd34bafb20..4ab867ab64 100644 --- a/crates/prompt_store/src/prompts.rs +++ b/crates/prompt_store/src/prompts.rs @@ -229,12 +229,12 @@ impl PromptBuilder { log_message.push_str(" -> "); log_message.push_str(&target.display().to_string()); } - log::info!("{}.", log_message); + log::trace!("{}.", log_message); } else { if !found_dir_once { - log::info!("No prompt template overrides directory found at {}. Using built-in prompts.", templates_dir.display()); + log::trace!("No prompt template overrides directory found at {}. Using built-in prompts.", templates_dir.display()); if let Some(target) = symlink_status { - log::info!("Symlink found pointing to {}, but target is invalid.", target.display()); + log::trace!("Symlink found pointing to {}, but target is invalid.", target.display()); } } @@ -247,7 +247,7 @@ impl PromptBuilder { log_message.push_str(" -> "); log_message.push_str(&target.display().to_string()); } - log::info!("{}.", log_message); + log::trace!("{}.", log_message); break; } } diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 8b77788d22..98f5fa40e9 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -26,7 +26,6 @@ use std::{ time::Duration, time::Instant, }; -use tracing::instrument; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize)] pub struct ConnectionId { @@ -109,7 +108,6 @@ impl Peer { self.epoch.load(SeqCst) } - #[instrument(skip_all)] pub fn add_connection<F, Fut, Out>( self: &Arc<Self>, connection: Connection, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8c1be61abf..d64a4472a0 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2503,8 +2503,6 @@ impl Workspace { window: &mut Window, cx: &mut Context<Self>, ) -> Task<Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>> { - log::info!("open paths {abs_paths:?}"); - let fs = self.app_state.fs.clone(); // Sort the paths to ensure we add worktrees for parents before their children. diff --git a/crates/zeta/src/license_detection.rs b/crates/zeta/src/license_detection.rs index 3dd025c1e1..022b2d19de 100644 --- a/crates/zeta/src/license_detection.rs +++ b/crates/zeta/src/license_detection.rs @@ -143,10 +143,10 @@ impl LicenseDetectionWatcher { } async fn is_path_eligible(fs: &Arc<dyn Fs>, abs_path: PathBuf) -> Option<bool> { - log::info!("checking if `{abs_path:?}` is an open source license"); + log::debug!("checking if `{abs_path:?}` is an open source license"); // Resolve symlinks so that the file size from metadata is correct. let Some(abs_path) = fs.canonicalize(&abs_path).await.ok() else { - log::info!( + log::debug!( "`{abs_path:?}` license file probably deleted (error canonicalizing the path)" ); return None; @@ -159,11 +159,11 @@ impl LicenseDetectionWatcher { let text = fs.load(&abs_path).await.log_err()?; let is_eligible = is_license_eligible_for_data_collection(&text); if is_eligible { - log::info!( + log::debug!( "`{abs_path:?}` matches a license that is eligible for data collection (if enabled)" ); } else { - log::info!( + log::debug!( "`{abs_path:?}` does not match a license that is eligible for data collection" ); } From ceec258bf32cf86ef6a3948d60385aeb8a639390 Mon Sep 17 00:00:00 2001 From: Ben Brandt <benjamin.j.brandt@gmail.com> Date: Wed, 20 Aug 2025 05:40:39 +0200 Subject: [PATCH 508/693] Some clippy fixes (#36544) These showed up today, so just applied the simplifications, which were mostly switching matches to if let Release Notes: - N/A --- crates/inspector_ui/src/div_inspector.rs | 42 +++-- crates/remote/src/ssh_session.rs | 193 +++++++++++------------ crates/vim/src/test/neovim_connection.rs | 8 +- 3 files changed, 117 insertions(+), 126 deletions(-) diff --git a/crates/inspector_ui/src/div_inspector.rs b/crates/inspector_ui/src/div_inspector.rs index e9460cc9cc..0c2b16b9f4 100644 --- a/crates/inspector_ui/src/div_inspector.rs +++ b/crates/inspector_ui/src/div_inspector.rs @@ -93,8 +93,8 @@ impl DivInspector { Ok((json_style_buffer, rust_style_buffer)) => { this.update_in(cx, |this, window, cx| { this.state = State::BuffersLoaded { - json_style_buffer: json_style_buffer, - rust_style_buffer: rust_style_buffer, + json_style_buffer, + rust_style_buffer, }; // Initialize editors immediately instead of waiting for @@ -200,8 +200,8 @@ impl DivInspector { cx.subscribe_in(&json_style_editor, window, { let id = id.clone(); let rust_style_buffer = rust_style_buffer.clone(); - move |this, editor, event: &EditorEvent, window, cx| match event { - EditorEvent::BufferEdited => { + move |this, editor, event: &EditorEvent, window, cx| { + if event == &EditorEvent::BufferEdited { let style_json = editor.read(cx).text(cx); match serde_json_lenient::from_str_lenient::<StyleRefinement>(&style_json) { Ok(new_style) => { @@ -243,7 +243,6 @@ impl DivInspector { Err(err) => this.json_style_error = Some(err.to_string().into()), } } - _ => {} } }) .detach(); @@ -251,11 +250,10 @@ impl DivInspector { cx.subscribe(&rust_style_editor, { let json_style_buffer = json_style_buffer.clone(); let rust_style_buffer = rust_style_buffer.clone(); - move |this, _editor, event: &EditorEvent, cx| match event { - EditorEvent::BufferEdited => { + move |this, _editor, event: &EditorEvent, cx| { + if let EditorEvent::BufferEdited = event { this.update_json_style_from_rust(&json_style_buffer, &rust_style_buffer, cx); } - _ => {} } }) .detach(); @@ -271,23 +269,19 @@ impl DivInspector { } fn reset_style(&mut self, cx: &mut App) { - match &self.state { - State::Ready { - rust_style_buffer, - json_style_buffer, - .. - } => { - if let Err(err) = self.reset_style_editors( - &rust_style_buffer.clone(), - &json_style_buffer.clone(), - cx, - ) { - self.json_style_error = Some(format!("{err}").into()); - } else { - self.json_style_error = None; - } + if let State::Ready { + rust_style_buffer, + json_style_buffer, + .. + } = &self.state + { + if let Err(err) = + self.reset_style_editors(&rust_style_buffer.clone(), &json_style_buffer.clone(), cx) + { + self.json_style_error = Some(format!("{err}").into()); + } else { + self.json_style_error = None; } - _ => {} } } diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index ffd0cac310..7173bc9b3b 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -2125,109 +2125,106 @@ impl SshRemoteConnection { .env("RUSTFLAGS", &rust_flags), ) .await?; + } else if build_remote_server.contains("cross") { + #[cfg(target_os = "windows")] + use util::paths::SanitizedPath; + + delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx); + log::info!("installing cross"); + run_cmd(Command::new("cargo").args([ + "install", + "cross", + "--git", + "https://github.com/cross-rs/cross", + ])) + .await?; + + delegate.set_status( + Some(&format!( + "Building remote server binary from source for {} with Docker", + &triple + )), + cx, + ); + log::info!("building remote server binary from source for {}", &triple); + + // On Windows, the binding needs to be set to the canonical path + #[cfg(target_os = "windows")] + let src = + SanitizedPath::from(smol::fs::canonicalize("./target").await?).to_glob_string(); + #[cfg(not(target_os = "windows"))] + let src = "./target"; + run_cmd( + Command::new("cross") + .args([ + "build", + "--package", + "remote_server", + "--features", + "debug-embed", + "--target-dir", + "target/remote_server", + "--target", + &triple, + ]) + .env( + "CROSS_CONTAINER_OPTS", + format!("--mount type=bind,src={src},dst=/app/target"), + ) + .env("RUSTFLAGS", &rust_flags), + ) + .await?; } else { - if build_remote_server.contains("cross") { - #[cfg(target_os = "windows")] - use util::paths::SanitizedPath; + let which = cx + .background_spawn(async move { which::which("zig") }) + .await; - delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx); - log::info!("installing cross"); - run_cmd(Command::new("cargo").args([ - "install", - "cross", - "--git", - "https://github.com/cross-rs/cross", - ])) - .await?; - - delegate.set_status( - Some(&format!( - "Building remote server binary from source for {} with Docker", - &triple - )), - cx, - ); - log::info!("building remote server binary from source for {}", &triple); - - // On Windows, the binding needs to be set to the canonical path - #[cfg(target_os = "windows")] - let src = - SanitizedPath::from(smol::fs::canonicalize("./target").await?).to_glob_string(); + if which.is_err() { #[cfg(not(target_os = "windows"))] - let src = "./target"; - run_cmd( - Command::new("cross") - .args([ - "build", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - "--target", - &triple, - ]) - .env( - "CROSS_CONTAINER_OPTS", - format!("--mount type=bind,src={src},dst=/app/target"), - ) - .env("RUSTFLAGS", &rust_flags), - ) - .await?; - } else { - let which = cx - .background_spawn(async move { which::which("zig") }) - .await; - - if which.is_err() { - #[cfg(not(target_os = "windows"))] - { - anyhow::bail!( - "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" - ) - } - #[cfg(target_os = "windows")] - { - anyhow::bail!( - "zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" - ) - } + { + anyhow::bail!( + "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" + ) + } + #[cfg(target_os = "windows")] + { + anyhow::bail!( + "zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" + ) } - - delegate.set_status(Some("Adding rustup target for cross-compilation"), cx); - log::info!("adding rustup target"); - run_cmd(Command::new("rustup").args(["target", "add"]).arg(&triple)).await?; - - delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx); - log::info!("installing cargo-zigbuild"); - run_cmd(Command::new("cargo").args(["install", "--locked", "cargo-zigbuild"])) - .await?; - - delegate.set_status( - Some(&format!( - "Building remote binary from source for {triple} with Zig" - )), - cx, - ); - log::info!("building remote binary from source for {triple} with Zig"); - run_cmd( - Command::new("cargo") - .args([ - "zigbuild", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - "--target", - &triple, - ]) - .env("RUSTFLAGS", &rust_flags), - ) - .await?; } + + delegate.set_status(Some("Adding rustup target for cross-compilation"), cx); + log::info!("adding rustup target"); + run_cmd(Command::new("rustup").args(["target", "add"]).arg(&triple)).await?; + + delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx); + log::info!("installing cargo-zigbuild"); + run_cmd(Command::new("cargo").args(["install", "--locked", "cargo-zigbuild"])).await?; + + delegate.set_status( + Some(&format!( + "Building remote binary from source for {triple} with Zig" + )), + cx, + ); + log::info!("building remote binary from source for {triple} with Zig"); + run_cmd( + Command::new("cargo") + .args([ + "zigbuild", + "--package", + "remote_server", + "--features", + "debug-embed", + "--target-dir", + "target/remote_server", + "--target", + &triple, + ]) + .env("RUSTFLAGS", &rust_flags), + ) + .await?; }; let bin_path = Path::new("target") .join("remote_server") diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index c2f7414f44..f87ccc283f 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -299,10 +299,10 @@ impl NeovimConnection { if let Some(NeovimData::Get { .. }) = self.data.front() { self.data.pop_front(); }; - if let Some(NeovimData::ReadRegister { name, value }) = self.data.pop_front() { - if name == register { - return value; - } + if let Some(NeovimData::ReadRegister { name, value }) = self.data.pop_front() + && name == register + { + return value; } panic!("operation does not match recorded script. re-record with --features=neovim") From d273aca1c1f3abc5159457b8af977fd40fbedb7c Mon Sep 17 00:00:00 2001 From: Ben Brandt <benjamin.j.brandt@gmail.com> Date: Wed, 20 Aug 2025 06:06:24 +0200 Subject: [PATCH 509/693] agent_ui: Add check to prevent sending empty messages in MessageEditor (#36545) Release Notes: - N/A --- crates/agent_ui/src/acp/message_editor.rs | 5 ++++- crates/agent_ui/src/acp/thread_view.rs | 26 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 311fe258de..cb20740f3c 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -66,7 +66,7 @@ pub struct MessageEditor { _parse_slash_command_task: Task<()>, } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub enum MessageEditorEvent { Send, Cancel, @@ -728,6 +728,9 @@ impl MessageEditor { } fn send(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) { + if self.is_empty(cx) { + return; + } cx.emit(MessageEditorEvent::Send) } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 4a9001b9f4..05f626d48e 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -4564,6 +4564,32 @@ pub(crate) mod tests { }); } + #[gpui::test] + async fn test_message_doesnt_send_if_empty(cx: &mut TestAppContext) { + init_test(cx); + + let connection = StubAgentConnection::new(); + + let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; + add_to_workspace(thread_view.clone(), cx); + + let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); + let mut events = cx.events(&message_editor); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("", window, cx); + }); + + message_editor.update_in(cx, |_editor, window, cx| { + window.dispatch_action(Box::new(Chat), cx); + }); + cx.run_until_parked(); + // We shouldn't have received any messages + assert!(matches!( + events.try_next(), + Err(futures::channel::mpsc::TryRecvError { .. }) + )); + } + #[gpui::test] async fn test_message_editing_regenerate(cx: &mut TestAppContext) { init_test(cx); From fbba6addfd1f1539408af582e65b356a308ba2f7 Mon Sep 17 00:00:00 2001 From: zumbalogy <zumbalogy@users.noreply.github.com> Date: Wed, 20 Aug 2025 06:39:51 +0200 Subject: [PATCH 510/693] docs: Document `global_lsp_settings.button` and remove duplicate docs for `lsp_highlight_debounce` (#36547) Follow up to this discussion: https://github.com/zed-industries/zed/pull/36337 Release Notes: - N/A This will (gracefully) break links to https://zed.dev/docs/configuring-zed#lsp-highlight-debounce-1 I don't see anything show up for that on google or github search and I don't think its load bearing. --------- Co-authored-by: zumbalogy <3770982+zumbalogy@users.noreply.github.com> --- docs/src/configuring-zed.md | 18 ++++++++++++------ docs/src/visual-customization.md | 6 ++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 9d56130256..39d172ea5f 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -539,12 +539,6 @@ List of `string` values - Setting: `selection_highlight` - Default: `true` -## LSP Highlight Debounce - -- Description: The debounce delay before querying highlights from the language server based on the current cursor location. -- Setting: `lsp_highlight_debounce` -- Default: `75` - ## Cursor Blink - Description: Whether or not the cursor blinks. @@ -1339,6 +1333,18 @@ While other options may be changed at a runtime and should be placed under `sett - Setting: `lsp_highlight_debounce` - Default: `75` +## Global LSP Settings + +- Description: Common language server settings. +- Setting: `global_lsp_settings` +- Default: + +```json +"global_lsp_settings": { + "button": true +} +``` + **Options** `integer` values representing milliseconds diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 6e598f4436..3ad1e381d9 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -321,6 +321,12 @@ TBD: Centered layout related settings // Defaults to true. "cursor_position_button": true, }, + "global_lsp_settings": { + // Show/hide the LSP button in the status bar. + // Activity from the LSP is still shown. + // Button is not shown if "enable_language_server" if false. + "button": true + }, ``` ### Multibuffer From 60960409f7a22ff0f66db53d74b0b45f574881d6 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 20 Aug 2025 01:47:28 -0300 Subject: [PATCH 511/693] thread view: Refine the UI a bit (#36504) Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga <agus@zed.dev> --- assets/icons/menu_alt.svg | 4 +- assets/icons/menu_alt_temp.svg | 3 + assets/icons/x_circle_filled.svg | 3 + assets/icons/zed_agent.svg | 27 ++ crates/agent2/src/native_agent_server.rs | 7 +- crates/agent_servers/src/gemini.rs | 2 +- crates/agent_ui/src/acp/entry_view_state.rs | 1 + crates/agent_ui/src/acp/thread_view.rs | 261 +++++++++++++------- crates/agent_ui/src/agent_panel.rs | 88 +++---- crates/icons/src/icons.rs | 3 + crates/markdown/src/markdown.rs | 8 +- 11 files changed, 262 insertions(+), 145 deletions(-) create mode 100644 assets/icons/menu_alt_temp.svg create mode 100644 assets/icons/x_circle_filled.svg create mode 100644 assets/icons/zed_agent.svg diff --git a/assets/icons/menu_alt.svg b/assets/icons/menu_alt.svg index f73102e286..87add13216 100644 --- a/assets/icons/menu_alt.svg +++ b/assets/icons/menu_alt.svg @@ -1 +1,3 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.667 8h8M2.667 4h10.666M2.667 12H8"/></svg> +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M13.333 10H8M13.333 6H2.66701" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/icons/menu_alt_temp.svg b/assets/icons/menu_alt_temp.svg new file mode 100644 index 0000000000..87add13216 --- /dev/null +++ b/assets/icons/menu_alt_temp.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M13.333 10H8M13.333 6H2.66701" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/icons/x_circle_filled.svg b/assets/icons/x_circle_filled.svg new file mode 100644 index 0000000000..52215acda8 --- /dev/null +++ b/assets/icons/x_circle_filled.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M8 2C11.3137 2 14 4.68629 14 8C14 11.3137 11.3137 14 8 14C4.68629 14 2 11.3137 2 8C2 4.68629 4.68629 2 8 2ZM10.4238 5.57617C10.1895 5.34187 9.81049 5.3419 9.57617 5.57617L8 7.15234L6.42383 5.57617C6.18953 5.34187 5.81049 5.3419 5.57617 5.57617C5.34186 5.81049 5.34186 6.18951 5.57617 6.42383L7.15234 8L5.57617 9.57617C5.34186 9.81049 5.34186 10.1895 5.57617 10.4238C5.81049 10.6581 6.18954 10.6581 6.42383 10.4238L8 8.84766L9.57617 10.4238C9.81049 10.6581 10.1895 10.6581 10.4238 10.4238C10.6581 10.1895 10.658 9.81048 10.4238 9.57617L8.84766 8L10.4238 6.42383C10.6581 6.18954 10.658 5.81048 10.4238 5.57617Z" fill="black"/> +</svg> diff --git a/assets/icons/zed_agent.svg b/assets/icons/zed_agent.svg new file mode 100644 index 0000000000..b6e120a0b6 --- /dev/null +++ b/assets/icons/zed_agent.svg @@ -0,0 +1,27 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M11 8.75V10.5C8.93097 10.5 8.06903 10.5 6 10.5V10L11 6V5.5H6V7.25" stroke="black" stroke-width="1.2"/> +<path d="M2 8.5C2.27614 8.5 2.5 8.27614 2.5 8C2.5 7.72386 2.27614 7.5 2 7.5C1.72386 7.5 1.5 7.72386 1.5 8C1.5 8.27614 1.72386 8.5 2 8.5Z" fill="black"/> +<path d="M2.99976 6.33002C3.2759 6.33002 3.49976 6.10616 3.49976 5.83002C3.49976 5.55387 3.2759 5.33002 2.99976 5.33002C2.72361 5.33002 2.49976 5.55387 2.49976 5.83002C2.49976 6.10616 2.72361 6.33002 2.99976 6.33002Z" fill="black"/> +<path d="M2.99976 10.66C3.2759 10.66 3.49976 10.4361 3.49976 10.16C3.49976 9.88383 3.2759 9.65997 2.99976 9.65997C2.72361 9.65997 2.49976 9.88383 2.49976 10.16C2.49976 10.4361 2.72361 10.66 2.99976 10.66Z" fill="black"/> +<path d="M15 8.5C15.2761 8.5 15.5 8.27614 15.5 8C15.5 7.72386 15.2761 7.5 15 7.5C14.7239 7.5 14.5 7.72386 14.5 8C14.5 8.27614 14.7239 8.5 15 8.5Z" fill="black"/> +<path d="M14 6.33002C14.2761 6.33002 14.5 6.10616 14.5 5.83002C14.5 5.55387 14.2761 5.33002 14 5.33002C13.7239 5.33002 13.5 5.55387 13.5 5.83002C13.5 6.10616 13.7239 6.33002 14 6.33002Z" fill="black"/> +<path d="M14 10.66C14.2761 10.66 14.5 10.4361 14.5 10.16C14.5 9.88383 14.2761 9.65997 14 9.65997C13.7239 9.65997 13.5 9.88383 13.5 10.16C13.5 10.4361 13.7239 10.66 14 10.66Z" fill="black"/> +<path d="M8.49219 2C8.76833 2 8.99219 1.77614 8.99219 1.5C8.99219 1.22386 8.76833 1 8.49219 1C8.21605 1 7.99219 1.22386 7.99219 1.5C7.99219 1.77614 8.21605 2 8.49219 2Z" fill="black"/> +<path d="M6 3C6.27614 3 6.5 2.77614 6.5 2.5C6.5 2.22386 6.27614 2 6 2C5.72386 2 5.5 2.22386 5.5 2.5C5.5 2.77614 5.72386 3 6 3Z" fill="black"/> +<path d="M4 4C4.27614 4 4.5 3.77614 4.5 3.5C4.5 3.22386 4.27614 3 4 3C3.72386 3 3.5 3.22386 3.5 3.5C3.5 3.77614 3.72386 4 4 4Z" fill="black"/> +<path d="M3.99976 13C4.2759 13 4.49976 12.7761 4.49976 12.5C4.49976 12.2239 4.2759 12 3.99976 12C3.72361 12 3.49976 12.2239 3.49976 12.5C3.49976 12.7761 3.72361 13 3.99976 13Z" fill="black"/> +<path d="M2 12.5C2.27614 12.5 2.5 12.2761 2.5 12C2.5 11.7239 2.27614 11.5 2 11.5C1.72386 11.5 1.5 11.7239 1.5 12C1.5 12.2761 1.72386 12.5 2 12.5Z" fill="black"/> +<path d="M2 4.5C2.27614 4.5 2.5 4.27614 2.5 4C2.5 3.72386 2.27614 3.5 2 3.5C1.72386 3.5 1.5 3.72386 1.5 4C1.5 4.27614 1.72386 4.5 2 4.5Z" fill="black"/> +<path d="M15 12.5C15.2761 12.5 15.5 12.2761 15.5 12C15.5 11.7239 15.2761 11.5 15 11.5C14.7239 11.5 14.5 11.7239 14.5 12C14.5 12.2761 14.7239 12.5 15 12.5Z" fill="black"/> +<path d="M15 4.5C15.2761 4.5 15.5 4.27614 15.5 4C15.5 3.72386 15.2761 3.5 15 3.5C14.7239 3.5 14.5 3.72386 14.5 4C14.5 4.27614 14.7239 4.5 15 4.5Z" fill="black"/> +<path d="M3.99976 15C4.2759 15 4.49976 14.7761 4.49976 14.5C4.49976 14.2239 4.2759 14 3.99976 14C3.72361 14 3.49976 14.2239 3.49976 14.5C3.49976 14.7761 3.72361 15 3.99976 15Z" fill="black"/> +<path d="M4 2C4.27614 2 4.5 1.77614 4.5 1.5C4.5 1.22386 4.27614 1 4 1C3.72386 1 3.5 1.22386 3.5 1.5C3.5 1.77614 3.72386 2 4 2Z" fill="black"/> +<path d="M13 15C13.2761 15 13.5 14.7761 13.5 14.5C13.5 14.2239 13.2761 14 13 14C12.7239 14 12.5 14.2239 12.5 14.5C12.5 14.7761 12.7239 15 13 15Z" fill="black"/> +<path d="M13 2C13.2761 2 13.5 1.77614 13.5 1.5C13.5 1.22386 13.2761 1 13 1C12.7239 1 12.5 1.22386 12.5 1.5C12.5 1.77614 12.7239 2 13 2Z" fill="black"/> +<path d="M13 4C13.2761 4 13.5 3.77614 13.5 3.5C13.5 3.22386 13.2761 3 13 3C12.7239 3 12.5 3.22386 12.5 3.5C12.5 3.77614 12.7239 4 13 4Z" fill="black"/> +<path d="M13 13C13.2761 13 13.5 12.7761 13.5 12.5C13.5 12.2239 13.2761 12 13 12C12.7239 12 12.5 12.2239 12.5 12.5C12.5 12.7761 12.7239 13 13 13Z" fill="black"/> +<path d="M11 3C11.2761 3 11.5 2.77614 11.5 2.5C11.5 2.22386 11.2761 2 11 2C10.7239 2 10.5 2.22386 10.5 2.5C10.5 2.77614 10.7239 3 11 3Z" fill="black"/> +<path d="M8.5 15C8.77614 15 9 14.7761 9 14.5C9 14.2239 8.77614 14 8.5 14C8.22386 14 8 14.2239 8 14.5C8 14.7761 8.22386 15 8.5 15Z" fill="black"/> +<path d="M6 14C6.27614 14 6.5 13.7761 6.5 13.5C6.5 13.2239 6.27614 13 6 13C5.72386 13 5.5 13.2239 5.5 13.5C5.5 13.7761 5.72386 14 6 14Z" fill="black"/> +<path d="M11 14C11.2761 14 11.5 13.7761 11.5 13.5C11.5 13.2239 11.2761 13 11 13C10.7239 13 10.5 13.2239 10.5 13.5C10.5 13.7761 10.7239 14 11 14Z" fill="black"/> +</svg> diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index f8cf3dd602..74d24efb13 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -27,16 +27,15 @@ impl AgentServer for NativeAgentServer { } fn empty_state_headline(&self) -> &'static str { - "Native Agent" + "" } fn empty_state_message(&self) -> &'static str { - "How can I help you today?" + "" } fn logo(&self) -> ui::IconName { - // Using the ZedAssistant icon as it's the native built-in agent - ui::IconName::ZedAssistant + ui::IconName::ZedAgent } fn connect( diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index e1ecaf0bb5..813f8b1fe0 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -26,7 +26,7 @@ impl AgentServer for Gemini { } fn empty_state_message(&self) -> &'static str { - "Ask questions, edit files, run commands.\nBe specific for the best results." + "Ask questions, edit files, run commands" } fn logo(&self) -> ui::IconName { diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index 0b0b8471a7..98af9bf838 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -189,6 +189,7 @@ pub enum ViewEvent { MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent), } +#[derive(Debug)] pub enum Entry { UserMessage(Entity<MessageEditor>), Content(HashMap<EntityId, AnyEntity>), diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 05f626d48e..4862bb0aa6 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -21,11 +21,11 @@ use file_icons::FileIcons; use fs::Fs; use gpui::{ Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, - EdgesRefinement, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, - MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, Task, - TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, - WindowHandle, div, linear_color_stop, linear_gradient, list, percentage, point, prelude::*, - pulsating_between, + EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, + ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, + Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, + WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, point, + prelude::*, pulsating_between, }; use language::Buffer; @@ -170,7 +170,7 @@ impl AcpThreadView { project.clone(), thread_store.clone(), text_thread_store.clone(), - "Message the agent - @ to include context", + "Message the agent — @ to include context", prevent_slash_commands, editor::EditorMode::AutoHeight { min_lines: MIN_EDITOR_LINES, @@ -928,29 +928,41 @@ impl AcpThreadView { None }; - div() + v_flex() .id(("user_message", entry_ix)) - .py_4() + .pt_2() + .pb_4() .px_2() + .gap_1p5() + .w_full() + .children(rules_item) .children(message.id.clone().and_then(|message_id| { message.checkpoint.as_ref()?.show.then(|| { - Button::new("restore-checkpoint", "Restore Checkpoint") - .icon(IconName::Undo) - .icon_size(IconSize::XSmall) - .icon_position(IconPosition::Start) - .label_size(LabelSize::XSmall) - .on_click(cx.listener(move |this, _, _window, cx| { - this.rewind(&message_id, cx); - })) + h_flex() + .gap_2() + .child(Divider::horizontal()) + .child( + Button::new("restore-checkpoint", "Restore Checkpoint") + .icon(IconName::Undo) + .icon_size(IconSize::XSmall) + .icon_position(IconPosition::Start) + .label_size(LabelSize::XSmall) + .icon_color(Color::Muted) + .color(Color::Muted) + .on_click(cx.listener(move |this, _, _window, cx| { + this.rewind(&message_id, cx); + })) + ) + .child(Divider::horizontal()) }) })) - .children(rules_item) .child( div() .relative() .child( div() - .p_3() + .py_3() + .px_2() .rounded_lg() .shadow_md() .bg(cx.theme().colors().editor_background) @@ -1080,12 +1092,20 @@ impl AcpThreadView { if let Some(editing_index) = self.editing_message.as_ref() && *editing_index < entry_ix { - div() - .child(primary) - .opacity(0.2) + let backdrop = div() + .id(("backdrop", entry_ix)) + .size_full() + .absolute() + .inset_0() + .bg(cx.theme().colors().panel_background) + .opacity(0.8) .block_mouse_except_scroll() - .id("overlay") - .on_click(cx.listener(Self::cancel_editing)) + .on_click(cx.listener(Self::cancel_editing)); + + div() + .relative() + .child(primary) + .child(backdrop) .into_any_element() } else { primary @@ -1100,7 +1120,7 @@ impl AcpThreadView { } fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla { - cx.theme().colors().border.opacity(0.6) + cx.theme().colors().border.opacity(0.8) } fn tool_name_font_size(&self) -> Rems { @@ -1299,23 +1319,14 @@ impl AcpThreadView { tool_call.status, ToolCallStatus::WaitingForConfirmation { .. } ); - let is_edit = matches!(tool_call.kind, acp::ToolKind::Edit); - let has_diff = tool_call - .content - .iter() - .any(|content| matches!(content, ToolCallContent::Diff { .. })); - let has_nonempty_diff = tool_call.content.iter().any(|content| match content { - ToolCallContent::Diff(diff) => diff.read(cx).has_revealed_range(cx), - _ => false, - }); - let use_card_layout = needs_confirmation || is_edit || has_diff; + let is_edit = + matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some(); + let use_card_layout = needs_confirmation || is_edit; let is_collapsible = !tool_call.content.is_empty() && !use_card_layout; - let is_open = tool_call.content.is_empty() - || needs_confirmation - || has_nonempty_diff - || self.expanded_tool_calls.contains(&tool_call.id); + let is_open = + needs_confirmation || is_edit || self.expanded_tool_calls.contains(&tool_call.id); let gradient_overlay = |color: Hsla| { div() @@ -1336,41 +1347,49 @@ impl AcpThreadView { cx.theme().colors().panel_background }; - let tool_output_display = match &tool_call.status { - ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex() - .w_full() - .children(tool_call.content.iter().map(|content| { - div() - .child( - self.render_tool_call_content(entry_ix, content, tool_call, window, cx), - ) - .into_any_element() - })) - .child(self.render_permission_buttons( - options, - entry_ix, - tool_call.id.clone(), - tool_call.content.is_empty(), - cx, - )), - ToolCallStatus::Pending - | ToolCallStatus::InProgress - | ToolCallStatus::Completed - | ToolCallStatus::Failed - | ToolCallStatus::Canceled => { - v_flex() + let tool_output_display = if is_open { + match &tool_call.status { + ToolCallStatus::WaitingForConfirmation { options, .. } => { + v_flex() + .w_full() + .children(tool_call.content.iter().map(|content| { + div() + .child(self.render_tool_call_content( + entry_ix, content, tool_call, window, cx, + )) + .into_any_element() + })) + .child(self.render_permission_buttons( + options, + entry_ix, + tool_call.id.clone(), + tool_call.content.is_empty(), + cx, + )) + .into_any() + } + ToolCallStatus::Pending | ToolCallStatus::InProgress + if is_edit && tool_call.content.is_empty() => + { + self.render_diff_loading(cx).into_any() + } + ToolCallStatus::Pending + | ToolCallStatus::InProgress + | ToolCallStatus::Completed + | ToolCallStatus::Failed + | ToolCallStatus::Canceled => v_flex() .w_full() .children(tool_call.content.iter().map(|content| { - div() - .child( - self.render_tool_call_content( - entry_ix, content, tool_call, window, cx, - ), - ) - .into_any_element() + div().child( + self.render_tool_call_content(entry_ix, content, tool_call, window, cx), + ) })) + .into_any(), + ToolCallStatus::Rejected => Empty.into_any(), } - ToolCallStatus::Rejected => v_flex().size_0(), + .into() + } else { + None }; v_flex() @@ -1390,9 +1409,13 @@ impl AcpThreadView { .map(|this| { if use_card_layout { this.pl_2() - .pr_1() + .pr_1p5() .py_1() .rounded_t_md() + .when(is_open, |this| { + this.border_b_1() + .border_color(self.tool_card_border_color(cx)) + }) .bg(self.tool_card_header_bg(cx)) } else { this.opacity(0.8).hover(|style| style.opacity(1.)) @@ -1403,6 +1426,7 @@ impl AcpThreadView { .group(&card_header_id) .relative() .w_full() + .min_h_6() .text_size(self.tool_name_font_size()) .child(self.render_tool_call_icon( card_header_id, @@ -1456,11 +1480,7 @@ impl AcpThreadView { .overflow_x_scroll() .child(self.render_markdown( tool_call.label.clone(), - default_markdown_style( - needs_confirmation || is_edit || has_diff, - window, - cx, - ), + default_markdown_style(false, window, cx), )), ) .child(gradient_overlay(gradient_color)) @@ -1480,7 +1500,7 @@ impl AcpThreadView { ) .children(status_icon), ) - .when(is_open, |this| this.child(tool_output_display)) + .children(tool_output_display) } fn render_tool_call_content( @@ -1501,7 +1521,7 @@ impl AcpThreadView { Empty.into_any_element() } } - ToolCallContent::Diff(diff) => self.render_diff_editor(entry_ix, diff, cx), + ToolCallContent::Diff(diff) => self.render_diff_editor(entry_ix, diff, tool_call, cx), ToolCallContent::Terminal(terminal) => { self.render_terminal_tool_call(entry_ix, terminal, tool_call, window, cx) } @@ -1645,21 +1665,69 @@ impl AcpThreadView { }))) } + fn render_diff_loading(&self, cx: &Context<Self>) -> AnyElement { + let bar = |n: u64, width_class: &str| { + let bg_color = cx.theme().colors().element_active; + let base = h_flex().h_1().rounded_full(); + + let modified = match width_class { + "w_4_5" => base.w_3_4(), + "w_1_4" => base.w_1_4(), + "w_2_4" => base.w_2_4(), + "w_3_5" => base.w_3_5(), + "w_2_5" => base.w_2_5(), + _ => base.w_1_2(), + }; + + modified.with_animation( + ElementId::Integer(n), + Animation::new(Duration::from_secs(2)).repeat(), + move |tab, delta| { + let delta = (delta - 0.15 * n as f32) / 0.7; + let delta = 1.0 - (0.5 - delta).abs() * 2.; + let delta = ease_in_out(delta.clamp(0., 1.)); + let delta = 0.1 + 0.9 * delta; + + tab.bg(bg_color.opacity(delta)) + }, + ) + }; + + v_flex() + .p_3() + .gap_1() + .rounded_b_md() + .bg(cx.theme().colors().editor_background) + .child(bar(0, "w_4_5")) + .child(bar(1, "w_1_4")) + .child(bar(2, "w_2_4")) + .child(bar(3, "w_3_5")) + .child(bar(4, "w_2_5")) + .into_any_element() + } + fn render_diff_editor( &self, entry_ix: usize, diff: &Entity<acp_thread::Diff>, + tool_call: &ToolCall, cx: &Context<Self>, ) -> AnyElement { + let tool_progress = matches!( + &tool_call.status, + ToolCallStatus::InProgress | ToolCallStatus::Pending + ); + v_flex() .h_full() - .border_t_1() - .border_color(self.tool_card_border_color(cx)) .child( if let Some(entry) = self.entry_view_state.read(cx).entry(entry_ix) && let Some(editor) = entry.editor_for_diff(diff) + && diff.read(cx).has_revealed_range(cx) { editor.clone().into_any_element() + } else if tool_progress { + self.render_diff_loading(cx) } else { Empty.into_any() }, @@ -1924,11 +1992,11 @@ impl AcpThreadView { .justify_center() .child(div().opacity(0.3).child(logo)) .child( - h_flex().absolute().right_1().bottom_0().child( - Icon::new(IconName::XCircle) - .color(Color::Error) - .size(IconSize::Small), - ), + h_flex() + .absolute() + .right_1() + .bottom_0() + .child(Icon::new(IconName::XCircleFilled).color(Color::Error)), ) .into_any_element() } @@ -1982,12 +2050,12 @@ impl AcpThreadView { Some( v_flex() - .pt_2() .px_2p5() .gap_1() .when_some(user_rules_text, |parent, user_rules_text| { parent.child( h_flex() + .group("user-rules") .w_full() .child( Icon::new(IconName::Reader) @@ -2008,6 +2076,7 @@ impl AcpThreadView { .shape(ui::IconButtonShape::Square) .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) + .visible_on_hover("user-rules") // TODO: Figure out a way to pass focus handle here so we can display the `OpenRulesLibrary` keybinding .tooltip(Tooltip::text("View User Rules")) .on_click(move |_event, window, cx| { @@ -2024,6 +2093,7 @@ impl AcpThreadView { .when_some(rules_file_text, |parent, rules_file_text| { parent.child( h_flex() + .group("project-rules") .w_full() .child( Icon::new(IconName::File) @@ -2044,7 +2114,8 @@ impl AcpThreadView { .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) .on_click(cx.listener(Self::handle_open_rules)) - .tooltip(Tooltip::text("View Rules")), + .visible_on_hover("project-rules") + .tooltip(Tooltip::text("View Project Rules")), ), ) }) @@ -2119,11 +2190,9 @@ impl AcpThreadView { .items_center() .justify_center() .child(self.render_error_agent_logo()) - .child( - h_flex().mt_4().mb_1().justify_center().child( - Headline::new("Authentication Required").size(HeadlineSize::Medium), - ), - ) + .child(h_flex().mt_4().mb_1().justify_center().child( + Headline::new(self.agent.empty_state_headline()).size(HeadlineSize::Medium), + )) .into_any(), ) .children(description.map(|desc| { @@ -2838,10 +2907,10 @@ impl AcpThreadView { .child( h_flex() .flex_none() + .flex_wrap() .justify_between() .child( h_flex() - .gap_1() .child(self.render_follow_toggle(cx)) .children(self.render_burn_mode_toggle(cx)), ) @@ -2883,7 +2952,7 @@ impl AcpThreadView { h_flex() .flex_shrink_0() .gap_0p5() - .mr_1() + .mr_1p5() .child( Label::new(used) .size(LabelSize::Small) @@ -2904,7 +2973,11 @@ impl AcpThreadView { } }), ) - .child(Label::new("/").size(LabelSize::Small).color(Color::Muted)) + .child( + Label::new("/") + .size(LabelSize::Small) + .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.5))), + ) .child(Label::new(max).size(LabelSize::Small).color(Color::Muted)), ) } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 297bb5f3e8..c89dc56795 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -65,8 +65,8 @@ use theme::ThemeSettings; use time::UtcOffset; use ui::utils::WithRemSize; use ui::{ - Banner, Callout, ContextMenu, ContextMenuEntry, Divider, ElevationIndex, KeyBinding, - PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*, + Banner, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding, PopoverMenu, + PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*, }; use util::ResultExt as _; use workspace::{ @@ -245,17 +245,16 @@ impl AgentType { match self { Self::Zed | Self::TextThread => "Zed Agent", Self::NativeAgent => "Agent 2", - Self::Gemini => "Google Gemini", + Self::Gemini => "Gemini CLI", Self::ClaudeCode => "Claude Code", } } - fn icon(self) -> IconName { + fn icon(self) -> Option<IconName> { match self { - Self::Zed | Self::TextThread => IconName::AiZed, - Self::NativeAgent => IconName::ZedAssistant, - Self::Gemini => IconName::AiGemini, - Self::ClaudeCode => IconName::AiClaude, + Self::Zed | Self::NativeAgent | Self::TextThread => None, + Self::Gemini => Some(IconName::AiGemini), + Self::ClaudeCode => Some(IconName::AiClaude), } } } @@ -2158,12 +2157,17 @@ impl AgentPanel { }) } - fn render_recent_entries_menu(&self, cx: &mut Context<Self>) -> impl IntoElement { + fn render_recent_entries_menu( + &self, + icon: IconName, + corner: Corner, + cx: &mut Context<Self>, + ) -> impl IntoElement { let focus_handle = self.focus_handle(cx); PopoverMenu::new("agent-nav-menu") .trigger_with_tooltip( - IconButton::new("agent-nav-menu", IconName::MenuAlt).icon_size(IconSize::Small), + IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small), { let focus_handle = focus_handle.clone(); move |window, cx| { @@ -2177,7 +2181,7 @@ impl AgentPanel { } }, ) - .anchor(Corner::TopLeft) + .anchor(corner) .with_handle(self.assistant_navigation_menu_handle.clone()) .menu({ let menu = self.assistant_navigation_menu.clone(); @@ -2304,7 +2308,9 @@ impl AgentPanel { .pl(DynamicSpacing::Base04.rems(cx)) .child(self.render_toolbar_back_button(cx)) .into_any_element(), - _ => self.render_recent_entries_menu(cx).into_any_element(), + _ => self + .render_recent_entries_menu(IconName::MenuAlt, Corner::TopLeft, cx) + .into_any_element(), }) .child(self.render_title_view(window, cx)), ) @@ -2390,7 +2396,7 @@ impl AgentPanel { .item( ContextMenuEntry::new("New Thread") .action(NewThread::default().boxed_clone()) - .icon(IconName::ZedAssistant) + .icon(IconName::Thread) .icon_color(Color::Muted) .handler({ let workspace = workspace.clone(); @@ -2443,7 +2449,7 @@ impl AgentPanel { .header("External Agents") .when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| { menu.item( - ContextMenuEntry::new("New Gemini Thread") + ContextMenuEntry::new("New Gemini CLI Thread") .icon(IconName::AiGemini) .icon_color(Color::Muted) .handler({ @@ -2503,16 +2509,18 @@ impl AgentPanel { let selected_agent_label = self.selected_agent.label().into(); let selected_agent = div() .id("selected_agent_icon") - .px(DynamicSpacing::Base02.rems(cx)) - .child(Icon::new(self.selected_agent.icon()).color(Color::Muted)) - .tooltip(move |window, cx| { - Tooltip::with_meta( - selected_agent_label.clone(), - None, - "Selected Agent", - window, - cx, - ) + .when_some(self.selected_agent.icon(), |this, icon| { + this.px(DynamicSpacing::Base02.rems(cx)) + .child(Icon::new(icon).color(Color::Muted)) + .tooltip(move |window, cx| { + Tooltip::with_meta( + selected_agent_label.clone(), + None, + "Selected Agent", + window, + cx, + ) + }) }) .into_any_element(); @@ -2535,31 +2543,23 @@ impl AgentPanel { ActiveView::History | ActiveView::Configuration => { self.render_toolbar_back_button(cx).into_any_element() } - _ => h_flex() - .gap_1() - .child(self.render_recent_entries_menu(cx)) - .child(Divider::vertical()) - .child(selected_agent) - .into_any_element(), + _ => selected_agent.into_any_element(), }) .child(self.render_title_view(window, cx)), ) .child( h_flex() - .h_full() - .gap_2() - .children(self.render_token_count(cx)) - .child( - h_flex() - .h_full() - .gap(DynamicSpacing::Base02.rems(cx)) - .pl(DynamicSpacing::Base04.rems(cx)) - .pr(DynamicSpacing::Base06.rems(cx)) - .border_l_1() - .border_color(cx.theme().colors().border) - .child(new_thread_menu) - .child(self.render_panel_options_menu(window, cx)), - ), + .flex_none() + .gap(DynamicSpacing::Base02.rems(cx)) + .pl(DynamicSpacing::Base04.rems(cx)) + .pr(DynamicSpacing::Base06.rems(cx)) + .child(new_thread_menu) + .child(self.render_recent_entries_menu( + IconName::MenuAltTemp, + Corner::TopRight, + cx, + )) + .child(self.render_panel_options_menu(window, cx)), ) } diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 8bd76cbecf..38f02c2206 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -155,6 +155,7 @@ pub enum IconName { Maximize, Menu, MenuAlt, + MenuAltTemp, Mic, MicMute, Minimize, @@ -245,6 +246,8 @@ pub enum IconName { Warning, WholeWord, XCircle, + XCircleFilled, + ZedAgent, ZedAssistant, ZedBurnMode, ZedBurnModeOn, diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 7939e97e48..a161ddd074 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -1084,7 +1084,13 @@ impl Element for MarkdownElement { cx, ); el.child( - div().absolute().top_1().right_0p5().w_5().child(codeblock), + h_flex() + .w_5() + .absolute() + .top_1() + .right_1() + .justify_center() + .child(codeblock), ) }); } From 1e1110ee8c8616cf2a35bbf021b127f6f08392ae Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 20 Aug 2025 02:20:58 -0300 Subject: [PATCH 512/693] thread_view: Increase click area of the user rules links (#36549) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 4862bb0aa6..ee033bf1f6 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2056,6 +2056,7 @@ impl AcpThreadView { parent.child( h_flex() .group("user-rules") + .id("user-rules") .w_full() .child( Icon::new(IconName::Reader) @@ -2078,25 +2079,26 @@ impl AcpThreadView { .icon_color(Color::Ignored) .visible_on_hover("user-rules") // TODO: Figure out a way to pass focus handle here so we can display the `OpenRulesLibrary` keybinding - .tooltip(Tooltip::text("View User Rules")) - .on_click(move |_event, window, cx| { - window.dispatch_action( - Box::new(OpenRulesLibrary { - prompt_to_select: first_user_rules_id, - }), - cx, - ) + .tooltip(Tooltip::text("View User Rules")), + ) + .on_click(move |_event, window, cx| { + window.dispatch_action( + Box::new(OpenRulesLibrary { + prompt_to_select: first_user_rules_id, }), - ), + cx, + ) + }), ) }) .when_some(rules_file_text, |parent, rules_file_text| { parent.child( h_flex() .group("project-rules") + .id("project-rules") .w_full() .child( - Icon::new(IconName::File) + Icon::new(IconName::Reader) .size(IconSize::XSmall) .color(Color::Disabled), ) @@ -2113,10 +2115,10 @@ impl AcpThreadView { .shape(ui::IconButtonShape::Square) .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) - .on_click(cx.listener(Self::handle_open_rules)) .visible_on_hover("project-rules") .tooltip(Tooltip::text("View Project Rules")), - ), + ) + .on_click(cx.listener(Self::handle_open_rules)), ) }) .into_any(), From 159b5e9fb5a74840fda1f2810af4c423522bcc96 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 20 Aug 2025 02:30:43 -0300 Subject: [PATCH 513/693] agent2: Port `user_modifier_to_send` setting (#36550) Release Notes: - N/A --- assets/keymaps/default-linux.json | 12 ++++++++- assets/keymaps/default-macos.json | 12 ++++++++- crates/agent_ui/src/acp/message_editor.rs | 32 ++++++++++++++++++++--- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 01c0b4e969..b4efa70572 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -327,7 +327,7 @@ } }, { - "context": "AcpThread > Editor", + "context": "AcpThread > Editor && !use_modifier_to_send", "use_key_equivalents": true, "bindings": { "enter": "agent::Chat", @@ -336,6 +336,16 @@ "ctrl-shift-n": "agent::RejectAll" } }, + { + "context": "AcpThread > Editor && use_modifier_to_send", + "use_key_equivalents": true, + "bindings": { + "ctrl-enter": "agent::Chat", + "shift-ctrl-r": "agent::OpenAgentDiff", + "ctrl-shift-y": "agent::KeepAll", + "ctrl-shift-n": "agent::RejectAll" + } + }, { "context": "ThreadHistory", "bindings": { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index e5b7fff9e1..ad2ab2ba89 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -379,7 +379,7 @@ } }, { - "context": "AcpThread > Editor", + "context": "AcpThread > Editor && !use_modifier_to_send", "use_key_equivalents": true, "bindings": { "enter": "agent::Chat", @@ -388,6 +388,16 @@ "cmd-shift-n": "agent::RejectAll" } }, + { + "context": "AcpThread > Editor && use_modifier_to_send", + "use_key_equivalents": true, + "bindings": { + "cmd-enter": "agent::Chat", + "shift-ctrl-r": "agent::OpenAgentDiff", + "cmd-shift-y": "agent::KeepAll", + "cmd-shift-n": "agent::RejectAll" + } + }, { "context": "ThreadHistory", "bindings": { diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index cb20740f3c..01a81c8cce 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -9,7 +9,7 @@ use anyhow::{Context as _, Result, anyhow}; use assistant_slash_commands::codeblock_fence_for_path; use collections::{HashMap, HashSet}; use editor::{ - Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, + Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, SemanticsProvider, ToOffset, actions::Paste, @@ -21,8 +21,8 @@ use futures::{ }; use gpui::{ AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, - HighlightStyle, Image, ImageFormat, Img, Subscription, Task, TextStyle, UnderlineStyle, - WeakEntity, + HighlightStyle, Image, ImageFormat, Img, KeyContext, Subscription, Task, TextStyle, + UnderlineStyle, WeakEntity, }; use language::{Buffer, Language}; use language_model::LanguageModelImage; @@ -122,6 +122,7 @@ impl MessageEditor { if prevent_slash_commands { editor.set_semantics_provider(Some(semantics_provider.clone())); } + editor.register_addon(MessageEditorAddon::new()); editor }); @@ -1648,6 +1649,31 @@ fn parse_slash_command(text: &str) -> Option<(usize, usize)> { None } +pub struct MessageEditorAddon {} + +impl MessageEditorAddon { + pub fn new() -> Self { + Self {} + } +} + +impl Addon for MessageEditorAddon { + fn to_any(&self) -> &dyn std::any::Any { + self + } + + fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> { + Some(self) + } + + fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) { + let settings = agent_settings::AgentSettings::get_global(cx); + if settings.use_modifier_to_send { + key_context.add("use_modifier_to_send"); + } + } +} + #[cfg(test)] mod tests { use std::{ops::Range, path::Path, sync::Arc}; From 5d2bb2466e4dc6d98063737a012b638c9deb2284 Mon Sep 17 00:00:00 2001 From: Conrad Irwin <conrad.irwin@gmail.com> Date: Wed, 20 Aug 2025 00:25:07 -0600 Subject: [PATCH 514/693] ACP history mentions (#36551) - **TEMP** - **Update @-mentions to use new history** Closes #ISSUE Release Notes: - N/A --- Cargo.lock | 1 - crates/acp_thread/Cargo.toml | 1 - crates/acp_thread/src/mention.rs | 8 +- crates/agent/src/thread.rs | 11 +- crates/agent2/Cargo.toml | 4 + crates/agent2/src/agent.rs | 22 + crates/agent2/src/db.rs | 13 +- crates/agent2/src/history_store.rs | 41 +- crates/agent2/src/thread.rs | 77 ++- crates/agent_settings/src/agent_settings.rs | 2 + crates/agent_ui/Cargo.toml | 2 + .../agent_ui/src/acp/completion_provider.rs | 559 ++++++++++-------- crates/agent_ui/src/acp/entry_view_state.rs | 30 +- crates/agent_ui/src/acp/message_editor.rs | 133 ++--- crates/agent_ui/src/acp/thread_view.rs | 44 +- crates/agent_ui/src/agent_panel.rs | 23 +- crates/assistant_context/src/context_store.rs | 2 +- 17 files changed, 581 insertions(+), 392 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5dced73fb9..fdc858ef50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,7 +7,6 @@ name = "acp_thread" version = "0.1.0" dependencies = [ "action_log", - "agent", "agent-client-protocol", "anyhow", "buffer_diff", diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index 173f4c4208..eab756db51 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -18,7 +18,6 @@ test-support = ["gpui/test-support", "project/test-support", "dep:parking_lot"] [dependencies] action_log.workspace = true agent-client-protocol.workspace = true -agent.workspace = true anyhow.workspace = true buffer_diff.workspace = true collections.workspace = true diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 4615e9a551..a1e713cffa 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -1,4 +1,4 @@ -use agent::ThreadId; +use agent_client_protocol as acp; use anyhow::{Context as _, Result, bail}; use file_icons::FileIcons; use prompt_store::{PromptId, UserPromptId}; @@ -12,7 +12,7 @@ use std::{ use ui::{App, IconName, SharedString}; use url::Url; -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)] pub enum MentionUri { File { abs_path: PathBuf, @@ -26,7 +26,7 @@ pub enum MentionUri { line_range: Range<u32>, }, Thread { - id: ThreadId, + id: acp::SessionId, name: String, }, TextThread { @@ -89,7 +89,7 @@ impl MentionUri { if let Some(thread_id) = path.strip_prefix("/agent/thread/") { let name = single_query_param(&url, "name")?.context("Missing thread name")?; Ok(Self::Thread { - id: thread_id.into(), + id: acp::SessionId(thread_id.into()), name, }) } else if let Some(path) = path.strip_prefix("/agent/text-thread/") { diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 80ed277f10..fc91e1bb62 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -9,7 +9,10 @@ use crate::{ tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState}, }; use action_log::ActionLog; -use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_PROMPT}; +use agent_settings::{ + AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_DETAILED_PROMPT, + SUMMARIZE_THREAD_PROMPT, +}; use anyhow::{Result, anyhow}; use assistant_tool::{AnyToolCard, Tool, ToolWorkingSet}; use chrono::{DateTime, Utc}; @@ -107,7 +110,7 @@ impl std::fmt::Display for PromptId { } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)] -pub struct MessageId(pub(crate) usize); +pub struct MessageId(pub usize); impl MessageId { fn post_inc(&mut self) -> Self { @@ -2425,12 +2428,10 @@ impl Thread { return; } - let added_user_message = include_str!("./prompts/summarize_thread_detailed_prompt.txt"); - let request = self.to_summarize_request( &model, CompletionIntent::ThreadContextSummarization, - added_user_message.into(), + SUMMARIZE_THREAD_DETAILED_PROMPT.into(), cx, ); diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 849ea041e9..2a39440af8 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -8,6 +8,9 @@ license = "GPL-3.0-or-later" [lib] path = "src/agent2.rs" +[features] +test-support = ["db/test-support"] + [lints] workspace = true @@ -72,6 +75,7 @@ ctor.workspace = true client = { workspace = true, "features" = ["test-support"] } clock = { workspace = true, "features" = ["test-support"] } context_server = { workspace = true, "features" = ["test-support"] } +db = { workspace = true, "features" = ["test-support"] } editor = { workspace = true, "features" = ["test-support"] } env_logger.workspace = true fs = { workspace = true, "features" = ["test-support"] } diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 212460d690..3c605de803 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -536,6 +536,28 @@ impl NativeAgent { }) } + pub fn thread_summary( + &mut self, + id: acp::SessionId, + cx: &mut Context<Self>, + ) -> Task<Result<SharedString>> { + let thread = self.open_thread(id.clone(), cx); + cx.spawn(async move |this, cx| { + let acp_thread = thread.await?; + let result = this + .update(cx, |this, cx| { + this.sessions + .get(&id) + .unwrap() + .thread + .update(cx, |thread, cx| thread.summary(cx)) + })? + .await?; + drop(acp_thread); + Ok(result) + }) + } + fn save_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) { let database_future = ThreadsDatabase::connect(cx); let (id, db_thread) = diff --git a/crates/agent2/src/db.rs b/crates/agent2/src/db.rs index 610a2575c4..c6a6c38201 100644 --- a/crates/agent2/src/db.rs +++ b/crates/agent2/src/db.rs @@ -1,6 +1,6 @@ use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent}; use acp_thread::UserMessageId; -use agent::thread_store; +use agent::{thread::DetailedSummaryState, thread_store}; use agent_client_protocol as acp; use agent_settings::{AgentProfileId, CompletionMode}; use anyhow::{Result, anyhow}; @@ -20,7 +20,7 @@ use std::sync::Arc; use ui::{App, SharedString}; pub type DbMessage = crate::Message; -pub type DbSummary = agent::thread::DetailedSummaryState; +pub type DbSummary = DetailedSummaryState; pub type DbLanguageModel = thread_store::SerializedLanguageModel; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -37,7 +37,7 @@ pub struct DbThread { pub messages: Vec<DbMessage>, pub updated_at: DateTime<Utc>, #[serde(default)] - pub summary: DbSummary, + pub detailed_summary: Option<SharedString>, #[serde(default)] pub initial_project_snapshot: Option<Arc<agent::thread::ProjectSnapshot>>, #[serde(default)] @@ -185,7 +185,12 @@ impl DbThread { title: thread.summary, messages, updated_at: thread.updated_at, - summary: thread.detailed_summary_state, + detailed_summary: match thread.detailed_summary_state { + DetailedSummaryState::NotGenerated | DetailedSummaryState::Generating { .. } => { + None + } + DetailedSummaryState::Generated { text, .. } => Some(text), + }, initial_project_snapshot: thread.initial_project_snapshot, cumulative_token_usage: thread.cumulative_token_usage, request_token_usage, diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs index 4ce304ae5f..7eb7da94ba 100644 --- a/crates/agent2/src/history_store.rs +++ b/crates/agent2/src/history_store.rs @@ -1,7 +1,8 @@ use crate::{DbThreadMetadata, ThreadsDatabase}; +use acp_thread::MentionUri; use agent_client_protocol as acp; use anyhow::{Context as _, Result, anyhow}; -use assistant_context::SavedContextMetadata; +use assistant_context::{AssistantContext, SavedContextMetadata}; use chrono::{DateTime, Utc}; use db::kvp::KEY_VALUE_STORE; use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*}; @@ -38,6 +39,19 @@ impl HistoryEntry { } } + pub fn mention_uri(&self) -> MentionUri { + match self { + HistoryEntry::AcpThread(thread) => MentionUri::Thread { + id: thread.id.clone(), + name: thread.title.to_string(), + }, + HistoryEntry::TextThread(context) => MentionUri::TextThread { + path: context.path.as_ref().to_owned(), + name: context.title.to_string(), + }, + } + } + pub fn title(&self) -> &SharedString { match self { HistoryEntry::AcpThread(thread) if thread.title.is_empty() => DEFAULT_TITLE, @@ -48,7 +62,7 @@ impl HistoryEntry { } /// Generic identifier for a history entry. -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug, Hash)] pub enum HistoryEntryId { AcpThread(acp::SessionId), TextThread(Arc<Path>), @@ -120,6 +134,16 @@ impl HistoryStore { }) } + pub fn load_text_thread( + &self, + path: Arc<Path>, + cx: &mut Context<Self>, + ) -> Task<Result<Entity<AssistantContext>>> { + self.context_store.update(cx, |context_store, cx| { + context_store.open_local_context(path, cx) + }) + } + pub fn reload(&self, cx: &mut Context<Self>) { let database_future = ThreadsDatabase::connect(cx); cx.spawn(async move |this, cx| { @@ -149,7 +173,7 @@ impl HistoryStore { .detach_and_log_err(cx); } - pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> { + pub fn entries(&self, cx: &App) -> Vec<HistoryEntry> { let mut history_entries = Vec::new(); #[cfg(debug_assertions)] @@ -180,10 +204,6 @@ impl HistoryStore { .is_none() } - pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> { - self.entries(cx).into_iter().take(limit).collect() - } - pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() { @@ -246,6 +266,10 @@ impl HistoryStore { cx.background_executor() .timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE) .await; + + if cfg!(any(feature = "test-support", test)) { + return; + } KEY_VALUE_STORE .write_kvp(RECENTLY_OPENED_THREADS_KEY.to_owned(), content) .await @@ -255,6 +279,9 @@ impl HistoryStore { fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<VecDeque<HistoryEntryId>>> { cx.background_spawn(async move { + if cfg!(any(feature = "test-support", test)) { + anyhow::bail!("history store does not persist in tests"); + } let json = KEY_VALUE_STORE .read_kvp(RECENTLY_OPENED_THREADS_KEY)? .unwrap_or("[]".to_string()); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 4bc45f1544..c1778bf38b 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -6,9 +6,12 @@ use crate::{ }; use acp_thread::{MentionUri, UserMessageId}; use action_log::ActionLog; -use agent::thread::{DetailedSummaryState, GitState, ProjectSnapshot, WorktreeSnapshot}; +use agent::thread::{GitState, ProjectSnapshot, WorktreeSnapshot}; use agent_client_protocol as acp; -use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_PROMPT}; +use agent_settings::{ + AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_DETAILED_PROMPT, + SUMMARIZE_THREAD_PROMPT, +}; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::adapt_schema_to_format; use chrono::{DateTime, Utc}; @@ -499,8 +502,7 @@ pub struct Thread { prompt_id: PromptId, updated_at: DateTime<Utc>, title: Option<SharedString>, - #[allow(unused)] - summary: DetailedSummaryState, + summary: Option<SharedString>, messages: Vec<Message>, completion_mode: CompletionMode, /// Holds the task that handles agent interaction until the end of the turn. @@ -541,7 +543,7 @@ impl Thread { prompt_id: PromptId::new(), updated_at: Utc::now(), title: None, - summary: DetailedSummaryState::default(), + summary: None, messages: Vec::new(), completion_mode: AgentSettings::get_global(cx).preferred_completion_mode, running_turn: None, @@ -691,7 +693,7 @@ impl Thread { } else { Some(db_thread.title.clone()) }, - summary: db_thread.summary, + summary: db_thread.detailed_summary, messages: db_thread.messages, completion_mode: db_thread.completion_mode.unwrap_or_default(), running_turn: None, @@ -719,7 +721,7 @@ impl Thread { title: self.title.clone().unwrap_or_default(), messages: self.messages.clone(), updated_at: self.updated_at, - summary: self.summary.clone(), + detailed_summary: self.summary.clone(), initial_project_snapshot: None, cumulative_token_usage: self.cumulative_token_usage, request_token_usage: self.request_token_usage.clone(), @@ -976,7 +978,7 @@ impl Thread { Message::Agent(_) | Message::Resume => {} } } - + self.summary = None; cx.notify(); Ok(()) } @@ -1047,6 +1049,7 @@ impl Thread { let event_stream = ThreadEventStream(events_tx); let message_ix = self.messages.len().saturating_sub(1); self.tool_use_limit_reached = false; + self.summary = None; self.running_turn = Some(RunningTurn { event_stream: event_stream.clone(), _task: cx.spawn(async move |this, cx| { @@ -1507,6 +1510,63 @@ impl Thread { self.title.clone().unwrap_or("New Thread".into()) } + pub fn summary(&mut self, cx: &mut Context<Self>) -> Task<Result<SharedString>> { + if let Some(summary) = self.summary.as_ref() { + return Task::ready(Ok(summary.clone())); + } + let Some(model) = self.summarization_model.clone() else { + return Task::ready(Err(anyhow!("No summarization model available"))); + }; + let mut request = LanguageModelRequest { + intent: Some(CompletionIntent::ThreadSummarization), + temperature: AgentSettings::temperature_for_model(&model, cx), + ..Default::default() + }; + + for message in &self.messages { + request.messages.extend(message.to_request()); + } + + request.messages.push(LanguageModelRequestMessage { + role: Role::User, + content: vec![SUMMARIZE_THREAD_DETAILED_PROMPT.into()], + cache: false, + }); + cx.spawn(async move |this, cx| { + let mut summary = String::new(); + let mut messages = model.stream_completion(request, cx).await?; + while let Some(event) = messages.next().await { + let event = event?; + let text = match event { + LanguageModelCompletionEvent::Text(text) => text, + LanguageModelCompletionEvent::StatusUpdate( + CompletionRequestStatus::UsageUpdated { .. }, + ) => { + // this.update(cx, |thread, cx| { + // thread.update_model_request_usage(amount as u32, limit, cx); + // })?; + // TODO: handle usage update + continue; + } + _ => continue, + }; + + let mut lines = text.lines(); + summary.extend(lines.next()); + } + + log::info!("Setting summary: {}", summary); + let summary = SharedString::from(summary); + + this.update(cx, |this, cx| { + this.summary = Some(summary.clone()); + cx.notify() + })?; + + Ok(summary) + }) + } + fn update_title( &mut self, event_stream: &ThreadEventStream, @@ -1617,6 +1677,7 @@ impl Thread { self.messages.push(Message::Agent(message)); self.updated_at = Utc::now(); + self.summary = None; cx.notify() } diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index afc834cdd8..1fe41d002c 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -15,6 +15,8 @@ pub use crate::agent_profile::*; pub const SUMMARIZE_THREAD_PROMPT: &str = include_str!("../../agent/src/prompts/summarize_thread_prompt.txt"); +pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str = + include_str!("../../agent/src/prompts/summarize_thread_detailed_prompt.txt"); pub fn init(cx: &mut App) { AgentSettings::register(cx); diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index fbf8590e68..43e3b25124 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -104,9 +104,11 @@ zed_actions.workspace = true [dev-dependencies] acp_thread = { workspace = true, features = ["test-support"] } agent = { workspace = true, features = ["test-support"] } +agent2 = { workspace = true, features = ["test-support"] } assistant_context = { workspace = true, features = ["test-support"] } assistant_tools.workspace = true buffer_diff = { workspace = true, features = ["test-support"] } +db = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] } gpui = { workspace = true, "features" = ["test-support"] } indoc.workspace = true diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 1a5e9c7d81..999e469d30 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use std::sync::atomic::AtomicBool; use acp_thread::MentionUri; +use agent2::{HistoryEntry, HistoryStore}; use anyhow::Result; use editor::{CompletionProvider, Editor, ExcerptId}; use fuzzy::{StringMatch, StringMatchCandidate}; @@ -18,25 +19,21 @@ use text::{Anchor, ToPoint as _}; use ui::prelude::*; use workspace::Workspace; -use agent::thread_store::{TextThreadStore, ThreadStore}; - +use crate::AgentPanel; use crate::acp::message_editor::MessageEditor; use crate::context_picker::file_context_picker::{FileMatch, search_files}; use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules}; use crate::context_picker::symbol_context_picker::SymbolMatch; use crate::context_picker::symbol_context_picker::search_symbols; -use crate::context_picker::thread_context_picker::{ - ThreadContextEntry, ThreadMatch, search_threads, -}; use crate::context_picker::{ - ContextPickerAction, ContextPickerEntry, ContextPickerMode, RecentEntry, - available_context_picker_entries, recent_context_picker_entries, selection_ranges, + ContextPickerAction, ContextPickerEntry, ContextPickerMode, selection_ranges, }; pub(crate) enum Match { File(FileMatch), Symbol(SymbolMatch), - Thread(ThreadMatch), + Thread(HistoryEntry), + RecentThread(HistoryEntry), Fetch(SharedString), Rules(RulesContextEntry), Entry(EntryMatch), @@ -53,6 +50,7 @@ impl Match { Match::File(file) => file.mat.score, Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.), Match::Thread(_) => 1., + Match::RecentThread(_) => 1., Match::Symbol(_) => 1., Match::Rules(_) => 1., Match::Fetch(_) => 1., @@ -60,209 +58,25 @@ impl Match { } } -fn search( - mode: Option<ContextPickerMode>, - query: String, - cancellation_flag: Arc<AtomicBool>, - recent_entries: Vec<RecentEntry>, - prompt_store: Option<Entity<PromptStore>>, - thread_store: WeakEntity<ThreadStore>, - text_thread_context_store: WeakEntity<assistant_context::ContextStore>, - workspace: Entity<Workspace>, - cx: &mut App, -) -> Task<Vec<Match>> { - match mode { - Some(ContextPickerMode::File) => { - let search_files_task = - search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); - cx.background_spawn(async move { - search_files_task - .await - .into_iter() - .map(Match::File) - .collect() - }) - } - - Some(ContextPickerMode::Symbol) => { - let search_symbols_task = - search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx); - cx.background_spawn(async move { - search_symbols_task - .await - .into_iter() - .map(Match::Symbol) - .collect() - }) - } - - Some(ContextPickerMode::Thread) => { - if let Some((thread_store, context_store)) = thread_store - .upgrade() - .zip(text_thread_context_store.upgrade()) - { - let search_threads_task = search_threads( - query.clone(), - cancellation_flag.clone(), - thread_store, - context_store, - cx, - ); - cx.background_spawn(async move { - search_threads_task - .await - .into_iter() - .map(Match::Thread) - .collect() - }) - } else { - Task::ready(Vec::new()) - } - } - - Some(ContextPickerMode::Fetch) => { - if !query.is_empty() { - Task::ready(vec![Match::Fetch(query.into())]) - } else { - Task::ready(Vec::new()) - } - } - - Some(ContextPickerMode::Rules) => { - if let Some(prompt_store) = prompt_store.as_ref() { - let search_rules_task = - search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx); - cx.background_spawn(async move { - search_rules_task - .await - .into_iter() - .map(Match::Rules) - .collect::<Vec<_>>() - }) - } else { - Task::ready(Vec::new()) - } - } - - None => { - if query.is_empty() { - let mut matches = recent_entries - .into_iter() - .map(|entry| match entry { - RecentEntry::File { - project_path, - path_prefix, - } => Match::File(FileMatch { - mat: fuzzy::PathMatch { - score: 1., - positions: Vec::new(), - worktree_id: project_path.worktree_id.to_usize(), - path: project_path.path, - path_prefix, - is_dir: false, - distance_to_relative_ancestor: 0, - }, - is_recent: true, - }), - RecentEntry::Thread(thread_context_entry) => Match::Thread(ThreadMatch { - thread: thread_context_entry, - is_recent: true, - }), - }) - .collect::<Vec<_>>(); - - matches.extend( - available_context_picker_entries( - &prompt_store, - &Some(thread_store.clone()), - &workspace, - cx, - ) - .into_iter() - .map(|mode| { - Match::Entry(EntryMatch { - entry: mode, - mat: None, - }) - }), - ); - - Task::ready(matches) - } else { - let executor = cx.background_executor().clone(); - - let search_files_task = - search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); - - let entries = available_context_picker_entries( - &prompt_store, - &Some(thread_store.clone()), - &workspace, - cx, - ); - let entry_candidates = entries - .iter() - .enumerate() - .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword())) - .collect::<Vec<_>>(); - - cx.background_spawn(async move { - let mut matches = search_files_task - .await - .into_iter() - .map(Match::File) - .collect::<Vec<_>>(); - - let entry_matches = fuzzy::match_strings( - &entry_candidates, - &query, - false, - true, - 100, - &Arc::new(AtomicBool::default()), - executor, - ) - .await; - - matches.extend(entry_matches.into_iter().map(|mat| { - Match::Entry(EntryMatch { - entry: entries[mat.candidate_id], - mat: Some(mat), - }) - })); - - matches.sort_by(|a, b| { - b.score() - .partial_cmp(&a.score()) - .unwrap_or(std::cmp::Ordering::Equal) - }); - - matches - }) - } - } - } -} - pub struct ContextPickerCompletionProvider { - workspace: WeakEntity<Workspace>, - thread_store: WeakEntity<ThreadStore>, - text_thread_store: WeakEntity<TextThreadStore>, message_editor: WeakEntity<MessageEditor>, + workspace: WeakEntity<Workspace>, + history_store: Entity<HistoryStore>, + prompt_store: Option<Entity<PromptStore>>, } impl ContextPickerCompletionProvider { pub fn new( - workspace: WeakEntity<Workspace>, - thread_store: WeakEntity<ThreadStore>, - text_thread_store: WeakEntity<TextThreadStore>, message_editor: WeakEntity<MessageEditor>, + workspace: WeakEntity<Workspace>, + history_store: Entity<HistoryStore>, + prompt_store: Option<Entity<PromptStore>>, ) -> Self { Self { - workspace, - thread_store, - text_thread_store, message_editor, + workspace, + history_store, + prompt_store, } } @@ -349,22 +163,13 @@ impl ContextPickerCompletionProvider { } fn completion_for_thread( - thread_entry: ThreadContextEntry, + thread_entry: HistoryEntry, source_range: Range<Anchor>, recent: bool, editor: WeakEntity<MessageEditor>, cx: &mut App, ) -> Completion { - let uri = match &thread_entry { - ThreadContextEntry::Thread { id, title } => MentionUri::Thread { - id: id.clone(), - name: title.to_string(), - }, - ThreadContextEntry::Context { path, title } => MentionUri::TextThread { - path: path.to_path_buf(), - name: title.to_string(), - }, - }; + let uri = thread_entry.mention_uri(); let icon_for_completion = if recent { IconName::HistoryRerun.path().into() @@ -547,6 +352,251 @@ impl ContextPickerCompletionProvider { )), }) } + + fn search( + &self, + mode: Option<ContextPickerMode>, + query: String, + cancellation_flag: Arc<AtomicBool>, + cx: &mut App, + ) -> Task<Vec<Match>> { + let Some(workspace) = self.workspace.upgrade() else { + return Task::ready(Vec::default()); + }; + match mode { + Some(ContextPickerMode::File) => { + let search_files_task = + search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); + cx.background_spawn(async move { + search_files_task + .await + .into_iter() + .map(Match::File) + .collect() + }) + } + + Some(ContextPickerMode::Symbol) => { + let search_symbols_task = + search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx); + cx.background_spawn(async move { + search_symbols_task + .await + .into_iter() + .map(Match::Symbol) + .collect() + }) + } + + Some(ContextPickerMode::Thread) => { + let search_threads_task = search_threads( + query.clone(), + cancellation_flag.clone(), + &self.history_store, + cx, + ); + cx.background_spawn(async move { + search_threads_task + .await + .into_iter() + .map(Match::Thread) + .collect() + }) + } + + Some(ContextPickerMode::Fetch) => { + if !query.is_empty() { + Task::ready(vec![Match::Fetch(query.into())]) + } else { + Task::ready(Vec::new()) + } + } + + Some(ContextPickerMode::Rules) => { + if let Some(prompt_store) = self.prompt_store.as_ref() { + let search_rules_task = + search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx); + cx.background_spawn(async move { + search_rules_task + .await + .into_iter() + .map(Match::Rules) + .collect::<Vec<_>>() + }) + } else { + Task::ready(Vec::new()) + } + } + + None if query.is_empty() => { + let mut matches = self.recent_context_picker_entries(&workspace, cx); + + matches.extend( + self.available_context_picker_entries(&workspace, cx) + .into_iter() + .map(|mode| { + Match::Entry(EntryMatch { + entry: mode, + mat: None, + }) + }), + ); + + Task::ready(matches) + } + None => { + let executor = cx.background_executor().clone(); + + let search_files_task = + search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); + + let entries = self.available_context_picker_entries(&workspace, cx); + let entry_candidates = entries + .iter() + .enumerate() + .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword())) + .collect::<Vec<_>>(); + + cx.background_spawn(async move { + let mut matches = search_files_task + .await + .into_iter() + .map(Match::File) + .collect::<Vec<_>>(); + + let entry_matches = fuzzy::match_strings( + &entry_candidates, + &query, + false, + true, + 100, + &Arc::new(AtomicBool::default()), + executor, + ) + .await; + + matches.extend(entry_matches.into_iter().map(|mat| { + Match::Entry(EntryMatch { + entry: entries[mat.candidate_id], + mat: Some(mat), + }) + })); + + matches.sort_by(|a, b| { + b.score() + .partial_cmp(&a.score()) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + matches + }) + } + } + } + + fn recent_context_picker_entries( + &self, + workspace: &Entity<Workspace>, + cx: &mut App, + ) -> Vec<Match> { + let mut recent = Vec::with_capacity(6); + + let mut mentions = self + .message_editor + .read_with(cx, |message_editor, _cx| message_editor.mentions()) + .unwrap_or_default(); + let workspace = workspace.read(cx); + let project = workspace.project().read(cx); + + if let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) + && let Some(thread) = agent_panel.read(cx).active_agent_thread(cx) + { + let thread = thread.read(cx); + mentions.insert(MentionUri::Thread { + id: thread.session_id().clone(), + name: thread.title().into(), + }); + } + + recent.extend( + workspace + .recent_navigation_history_iter(cx) + .filter(|(_, abs_path)| { + abs_path.as_ref().is_none_or(|path| { + !mentions.contains(&MentionUri::File { + abs_path: path.clone(), + }) + }) + }) + .take(4) + .filter_map(|(project_path, _)| { + project + .worktree_for_id(project_path.worktree_id, cx) + .map(|worktree| { + let path_prefix = worktree.read(cx).root_name().into(); + Match::File(FileMatch { + mat: fuzzy::PathMatch { + score: 1., + positions: Vec::new(), + worktree_id: project_path.worktree_id.to_usize(), + path: project_path.path, + path_prefix, + is_dir: false, + distance_to_relative_ancestor: 0, + }, + is_recent: true, + }) + }) + }), + ); + + const RECENT_COUNT: usize = 2; + let threads = self + .history_store + .read(cx) + .recently_opened_entries(cx) + .into_iter() + .filter(|thread| !mentions.contains(&thread.mention_uri())) + .take(RECENT_COUNT) + .collect::<Vec<_>>(); + + recent.extend(threads.into_iter().map(Match::RecentThread)); + + recent + } + + fn available_context_picker_entries( + &self, + workspace: &Entity<Workspace>, + cx: &mut App, + ) -> Vec<ContextPickerEntry> { + let mut entries = vec![ + ContextPickerEntry::Mode(ContextPickerMode::File), + ContextPickerEntry::Mode(ContextPickerMode::Symbol), + ContextPickerEntry::Mode(ContextPickerMode::Thread), + ]; + + let has_selection = workspace + .read(cx) + .active_item(cx) + .and_then(|item| item.downcast::<Editor>()) + .is_some_and(|editor| { + editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx)) + }); + if has_selection { + entries.push(ContextPickerEntry::Action( + ContextPickerAction::AddSelections, + )); + } + + if self.prompt_store.is_some() { + entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules)); + } + + entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch)); + + entries + } } fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel { @@ -596,45 +646,12 @@ impl CompletionProvider for ContextPickerCompletionProvider { let source_range = snapshot.anchor_before(state.source_range.start) ..snapshot.anchor_after(state.source_range.end); - let thread_store = self.thread_store.clone(); - let text_thread_store = self.text_thread_store.clone(); let editor = self.message_editor.clone(); - let Ok((exclude_paths, exclude_threads)) = - self.message_editor.update(cx, |message_editor, _cx| { - message_editor.mentioned_path_and_threads() - }) - else { - return Task::ready(Ok(Vec::new())); - }; let MentionCompletion { mode, argument, .. } = state; let query = argument.unwrap_or_else(|| "".to_string()); - let recent_entries = recent_context_picker_entries( - Some(thread_store.clone()), - Some(text_thread_store.clone()), - workspace.clone(), - &exclude_paths, - &exclude_threads, - cx, - ); - - let prompt_store = thread_store - .read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone()) - .ok() - .flatten(); - - let search_task = search( - mode, - query, - Arc::<AtomicBool>::default(), - recent_entries, - prompt_store, - thread_store.clone(), - text_thread_store.clone(), - workspace.clone(), - cx, - ); + let search_task = self.search(mode, query, Arc::<AtomicBool>::default(), cx); cx.spawn(async move |_, cx| { let matches = search_task.await; @@ -669,12 +686,18 @@ impl CompletionProvider for ContextPickerCompletionProvider { cx, ), - Match::Thread(ThreadMatch { - thread, is_recent, .. - }) => Some(Self::completion_for_thread( + Match::Thread(thread) => Some(Self::completion_for_thread( thread, source_range.clone(), - is_recent, + false, + editor.clone(), + cx, + )), + + Match::RecentThread(thread) => Some(Self::completion_for_thread( + thread, + source_range.clone(), + true, editor.clone(), cx, )), @@ -748,6 +771,42 @@ impl CompletionProvider for ContextPickerCompletionProvider { } } +pub(crate) fn search_threads( + query: String, + cancellation_flag: Arc<AtomicBool>, + history_store: &Entity<HistoryStore>, + cx: &mut App, +) -> Task<Vec<HistoryEntry>> { + let threads = history_store.read(cx).entries(cx); + if query.is_empty() { + return Task::ready(threads); + } + + let executor = cx.background_executor().clone(); + cx.background_spawn(async move { + let candidates = threads + .iter() + .enumerate() + .map(|(id, thread)| StringMatchCandidate::new(id, thread.title())) + .collect::<Vec<_>>(); + let matches = fuzzy::match_strings( + &candidates, + &query, + false, + true, + 100, + &cancellation_flag, + executor, + ) + .await; + + matches + .into_iter() + .map(|mat| threads[mat.candidate_id].clone()) + .collect() + }) +} + fn confirm_completion_callback( crease_text: SharedString, start: Anchor, diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index 98af9bf838..67acbb8b5b 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -1,7 +1,7 @@ use std::ops::Range; use acp_thread::{AcpThread, AgentThreadEntry}; -use agent::{TextThreadStore, ThreadStore}; +use agent2::HistoryStore; use collections::HashMap; use editor::{Editor, EditorMode, MinimapVisibility}; use gpui::{ @@ -10,6 +10,7 @@ use gpui::{ }; use language::language_settings::SoftWrap; use project::Project; +use prompt_store::PromptStore; use settings::Settings as _; use terminal_view::TerminalView; use theme::ThemeSettings; @@ -21,8 +22,8 @@ use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; pub struct EntryViewState { workspace: WeakEntity<Workspace>, project: Entity<Project>, - thread_store: Entity<ThreadStore>, - text_thread_store: Entity<TextThreadStore>, + history_store: Entity<HistoryStore>, + prompt_store: Option<Entity<PromptStore>>, entries: Vec<Entry>, prevent_slash_commands: bool, } @@ -31,15 +32,15 @@ impl EntryViewState { pub fn new( workspace: WeakEntity<Workspace>, project: Entity<Project>, - thread_store: Entity<ThreadStore>, - text_thread_store: Entity<TextThreadStore>, + history_store: Entity<HistoryStore>, + prompt_store: Option<Entity<PromptStore>>, prevent_slash_commands: bool, ) -> Self { Self { workspace, project, - thread_store, - text_thread_store, + history_store, + prompt_store, entries: Vec::new(), prevent_slash_commands, } @@ -77,8 +78,8 @@ impl EntryViewState { let mut editor = MessageEditor::new( self.workspace.clone(), self.project.clone(), - self.thread_store.clone(), - self.text_thread_store.clone(), + self.history_store.clone(), + self.prompt_store.clone(), "Edit message - @ to include context", self.prevent_slash_commands, editor::EditorMode::AutoHeight { @@ -313,9 +314,10 @@ mod tests { use std::{path::Path, rc::Rc}; use acp_thread::{AgentConnection, StubAgentConnection}; - use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol as acp; use agent_settings::AgentSettings; + use agent2::HistoryStore; + use assistant_context::ContextStore; use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; use editor::{EditorSettings, RowInfo}; use fs::FakeFs; @@ -378,15 +380,15 @@ mod tests { connection.send_update(session_id, acp::SessionUpdate::ToolCall(tool_call), cx) }); - let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx)); - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); let view_state = cx.new(|_cx| { EntryViewState::new( workspace.downgrade(), project.clone(), - thread_store, - text_thread_store, + history_store, + None, false, ) }); diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 01a81c8cce..c87c824015 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -3,8 +3,9 @@ use crate::{ context_picker::fetch_context_picker::fetch_url_content, }; use acp_thread::{MentionUri, selection_name}; -use agent::{TextThreadStore, ThreadId, ThreadStore}; use agent_client_protocol as acp; +use agent_servers::AgentServer; +use agent2::HistoryStore; use anyhow::{Context as _, Result, anyhow}; use assistant_slash_commands::codeblock_fence_for_path; use collections::{HashMap, HashSet}; @@ -27,6 +28,7 @@ use gpui::{ use language::{Buffer, Language}; use language_model::LanguageModelImage; use project::{Project, ProjectPath, Worktree}; +use prompt_store::PromptStore; use rope::Point; use settings::Settings; use std::{ @@ -59,8 +61,8 @@ pub struct MessageEditor { editor: Entity<Editor>, project: Entity<Project>, workspace: WeakEntity<Workspace>, - thread_store: Entity<ThreadStore>, - text_thread_store: Entity<TextThreadStore>, + history_store: Entity<HistoryStore>, + prompt_store: Option<Entity<PromptStore>>, prevent_slash_commands: bool, _subscriptions: Vec<Subscription>, _parse_slash_command_task: Task<()>, @@ -79,8 +81,8 @@ impl MessageEditor { pub fn new( workspace: WeakEntity<Workspace>, project: Entity<Project>, - thread_store: Entity<ThreadStore>, - text_thread_store: Entity<TextThreadStore>, + history_store: Entity<HistoryStore>, + prompt_store: Option<Entity<PromptStore>>, placeholder: impl Into<Arc<str>>, prevent_slash_commands: bool, mode: EditorMode, @@ -95,10 +97,10 @@ impl MessageEditor { None, ); let completion_provider = ContextPickerCompletionProvider::new( - workspace.clone(), - thread_store.downgrade(), - text_thread_store.downgrade(), cx.weak_entity(), + workspace.clone(), + history_store.clone(), + prompt_store.clone(), ); let semantics_provider = Rc::new(SlashCommandSemanticsProvider { range: Cell::new(None), @@ -152,9 +154,9 @@ impl MessageEditor { editor, project, mention_set, - thread_store, - text_thread_store, workspace, + history_store, + prompt_store, prevent_slash_commands, _subscriptions: subscriptions, _parse_slash_command_task: Task::ready(()), @@ -175,23 +177,12 @@ impl MessageEditor { self.editor.read(cx).is_empty(cx) } - pub fn mentioned_path_and_threads(&self) -> (HashSet<PathBuf>, HashSet<ThreadId>) { - let mut excluded_paths = HashSet::default(); - let mut excluded_threads = HashSet::default(); - - for uri in self.mention_set.uri_by_crease_id.values() { - match uri { - MentionUri::File { abs_path, .. } => { - excluded_paths.insert(abs_path.clone()); - } - MentionUri::Thread { id, .. } => { - excluded_threads.insert(id.clone()); - } - _ => {} - } - } - - (excluded_paths, excluded_threads) + pub fn mentions(&self) -> HashSet<MentionUri> { + self.mention_set + .uri_by_crease_id + .values() + .cloned() + .collect() } pub fn confirm_completion( @@ -529,7 +520,7 @@ impl MessageEditor { &mut self, crease_id: CreaseId, anchor: Anchor, - id: ThreadId, + id: acp::SessionId, name: String, window: &mut Window, cx: &mut Context<Self>, @@ -538,17 +529,25 @@ impl MessageEditor { id: id.clone(), name, }; - let open_task = self.thread_store.update(cx, |thread_store, cx| { - thread_store.open_thread(&id, window, cx) + let server = Rc::new(agent2::NativeAgentServer::new( + self.project.read(cx).fs().clone(), + self.history_store.clone(), + )); + let connection = server.connect(Path::new(""), &self.project, cx); + let load_summary = cx.spawn({ + let id = id.clone(); + async move |_, cx| { + let agent = connection.await?; + let agent = agent.downcast::<agent2::NativeAgentConnection>().unwrap(); + let summary = agent + .0 + .update(cx, |agent, cx| agent.thread_summary(id, cx))? + .await?; + anyhow::Ok(summary) + } }); let task = cx - .spawn(async move |_, cx| { - let thread = open_task.await.map_err(|e| e.to_string())?; - let content = thread - .read_with(cx, |thread, _cx| thread.latest_detailed_summary_or_text()) - .map_err(|e| e.to_string())?; - Ok(content) - }) + .spawn(async move |_, _| load_summary.await.map_err(|e| format!("{e}"))) .shared(); self.mention_set.insert_thread(id.clone(), task.clone()); @@ -590,8 +589,8 @@ impl MessageEditor { path: path.clone(), name, }; - let context = self.text_thread_store.update(cx, |text_thread_store, cx| { - text_thread_store.open_local_context(path.as_path().into(), cx) + let context = self.history_store.update(cx, |text_thread_store, cx| { + text_thread_store.load_text_thread(path.as_path().into(), cx) }); let task = cx .spawn(async move |_, cx| { @@ -637,7 +636,7 @@ impl MessageEditor { ) -> Task<Result<Vec<acp::ContentBlock>>> { let contents = self.mention_set - .contents(self.project.clone(), self.thread_store.clone(), window, cx); + .contents(&self.project, self.prompt_store.as_ref(), window, cx); let editor = self.editor.clone(); let prevent_slash_commands = self.prevent_slash_commands; @@ -1316,7 +1315,7 @@ pub struct MentionSet { uri_by_crease_id: HashMap<CreaseId, MentionUri>, fetch_results: HashMap<Url, Shared<Task<Result<String, String>>>>, images: HashMap<CreaseId, Shared<Task<Result<MentionImage, String>>>>, - thread_summaries: HashMap<ThreadId, Shared<Task<Result<SharedString, String>>>>, + thread_summaries: HashMap<acp::SessionId, Shared<Task<Result<SharedString, String>>>>, text_thread_summaries: HashMap<PathBuf, Shared<Task<Result<String, String>>>>, directories: HashMap<PathBuf, Shared<Task<Result<String, String>>>>, } @@ -1338,7 +1337,11 @@ impl MentionSet { self.images.insert(crease_id, task); } - fn insert_thread(&mut self, id: ThreadId, task: Shared<Task<Result<SharedString, String>>>) { + fn insert_thread( + &mut self, + id: acp::SessionId, + task: Shared<Task<Result<SharedString, String>>>, + ) { self.thread_summaries.insert(id, task); } @@ -1358,8 +1361,8 @@ impl MentionSet { pub fn contents( &self, - project: Entity<Project>, - thread_store: Entity<ThreadStore>, + project: &Entity<Project>, + prompt_store: Option<&Entity<PromptStore>>, _window: &mut Window, cx: &mut App, ) -> Task<Result<HashMap<CreaseId, Mention>>> { @@ -1484,8 +1487,7 @@ impl MentionSet { }) } MentionUri::Rule { id: prompt_id, .. } => { - let Some(prompt_store) = thread_store.read(cx).prompt_store().clone() - else { + let Some(prompt_store) = prompt_store else { return Task::ready(Err(anyhow!("missing prompt store"))); }; let text_task = prompt_store.read(cx).load(*prompt_id, cx); @@ -1678,8 +1680,9 @@ impl Addon for MessageEditorAddon { mod tests { use std::{ops::Range, path::Path, sync::Arc}; - use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol as acp; + use agent2::HistoryStore; + use assistant_context::ContextStore; use editor::{AnchorRangeExt as _, Editor, EditorMode}; use fs::FakeFs; use futures::StreamExt as _; @@ -1710,16 +1713,16 @@ mod tests { let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx)); - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); let message_editor = cx.update(|window, cx| { cx.new(|cx| { MessageEditor::new( workspace.downgrade(), project.clone(), - thread_store.clone(), - text_thread_store.clone(), + history_store.clone(), + None, "Test", false, EditorMode::AutoHeight { @@ -1908,8 +1911,8 @@ mod tests { opened_editors.push(buffer); } - let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx)); - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| { let workspace_handle = cx.weak_entity(); @@ -1917,8 +1920,8 @@ mod tests { MessageEditor::new( workspace_handle, project.clone(), - thread_store.clone(), - text_thread_store.clone(), + history_store.clone(), + None, "Test", false, EditorMode::AutoHeight { @@ -2011,12 +2014,9 @@ mod tests { let contents = message_editor .update_in(&mut cx, |message_editor, window, cx| { - message_editor.mention_set().contents( - project.clone(), - thread_store.clone(), - window, - cx, - ) + message_editor + .mention_set() + .contents(&project, None, window, cx) }) .await .unwrap() @@ -2066,12 +2066,9 @@ mod tests { let contents = message_editor .update_in(&mut cx, |message_editor, window, cx| { - message_editor.mention_set().contents( - project.clone(), - thread_store.clone(), - window, - cx, - ) + message_editor + .mention_set() + .contents(&project, None, window, cx) }) .await .unwrap() @@ -2181,7 +2178,7 @@ mod tests { .update_in(&mut cx, |message_editor, window, cx| { message_editor .mention_set() - .contents(project.clone(), thread_store, window, cx) + .contents(&project, None, window, cx) }) .await .unwrap() diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index ee033bf1f6..3be88ee3c3 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -5,7 +5,6 @@ use acp_thread::{ }; use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; -use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::{self as acp}; use agent_servers::{AgentServer, ClaudeCode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; @@ -32,7 +31,7 @@ use language::Buffer; use language_model::LanguageModelRegistry; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; use project::{Project, ProjectEntryId}; -use prompt_store::PromptId; +use prompt_store::{PromptId, PromptStore}; use rope::Point; use settings::{Settings as _, SettingsStore}; use std::sync::Arc; @@ -158,8 +157,7 @@ impl AcpThreadView { workspace: WeakEntity<Workspace>, project: Entity<Project>, history_store: Entity<HistoryStore>, - thread_store: Entity<ThreadStore>, - text_thread_store: Entity<TextThreadStore>, + prompt_store: Option<Entity<PromptStore>>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { @@ -168,8 +166,8 @@ impl AcpThreadView { MessageEditor::new( workspace.clone(), project.clone(), - thread_store.clone(), - text_thread_store.clone(), + history_store.clone(), + prompt_store.clone(), "Message the agent — @ to include context", prevent_slash_commands, editor::EditorMode::AutoHeight { @@ -187,8 +185,8 @@ impl AcpThreadView { EntryViewState::new( workspace.clone(), project.clone(), - thread_store.clone(), - text_thread_store.clone(), + history_store.clone(), + prompt_store.clone(), prevent_slash_commands, ) }); @@ -3201,12 +3199,18 @@ impl AcpThreadView { }) .detach_and_log_err(cx); } - MentionUri::Thread { id, .. } => { + MentionUri::Thread { id, name } => { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { panel.update(cx, |panel, cx| { - panel - .open_thread_by_id(&id, window, cx) - .detach_and_log_err(cx) + panel.load_agent_thread( + DbThreadMetadata { + id, + title: name.into(), + updated_at: Default::default(), + }, + window, + cx, + ) }); } } @@ -4075,7 +4079,6 @@ fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { #[cfg(test)] pub(crate) mod tests { use acp_thread::StubAgentConnection; - use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::SessionId; use assistant_context::ContextStore; use editor::EditorSettings; @@ -4211,10 +4214,6 @@ pub(crate) mod tests { let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let thread_store = - cx.update(|_window, cx| cx.new(|cx| ThreadStore::fake(project.clone(), cx))); - let text_thread_store = - cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx))); let context_store = cx.update(|_window, cx| cx.new(|cx| ContextStore::fake(project.clone(), cx))); let history_store = @@ -4228,8 +4227,7 @@ pub(crate) mod tests { workspace.downgrade(), project, history_store, - thread_store.clone(), - text_thread_store.clone(), + None, window, cx, ) @@ -4400,6 +4398,7 @@ pub(crate) mod tests { ThemeSettings::register(cx); release_channel::init(SemanticVersion::default(), cx); EditorSettings::register(cx); + prompt_store::init(cx) }); } @@ -4420,10 +4419,6 @@ pub(crate) mod tests { let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let thread_store = - cx.update(|_window, cx| cx.new(|cx| ThreadStore::fake(project.clone(), cx))); - let text_thread_store = - cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx))); let context_store = cx.update(|_window, cx| cx.new(|cx| ContextStore::fake(project.clone(), cx))); let history_store = @@ -4438,8 +4433,7 @@ pub(crate) mod tests { workspace.downgrade(), project.clone(), history_store.clone(), - thread_store.clone(), - text_thread_store.clone(), + None, window, cx, ) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index c89dc56795..b857052d69 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -4,6 +4,7 @@ use std::rc::Rc; use std::sync::Arc; use std::time::Duration; +use acp_thread::AcpThread; use agent2::{DbThreadMetadata, HistoryEntry}; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; @@ -1016,8 +1017,6 @@ impl AgentPanel { agent: crate::ExternalAgent, } - let thread_store = self.thread_store.clone(); - let text_thread_store = self.context_store.clone(); let history = self.acp_history_store.clone(); cx.spawn_in(window, async move |this, cx| { @@ -1075,8 +1074,7 @@ impl AgentPanel { workspace.clone(), project, this.acp_history_store.clone(), - thread_store.clone(), - text_thread_store.clone(), + this.prompt_store.clone(), window, cx, ) @@ -1499,6 +1497,14 @@ impl AgentPanel { _ => None, } } + pub(crate) fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> { + match &self.active_view { + ActiveView::ExternalAgentThread { thread_view, .. } => { + thread_view.read(cx).thread().cloned() + } + _ => None, + } + } pub(crate) fn delete_thread( &mut self, @@ -1816,6 +1822,15 @@ impl AgentPanel { } } } + + pub fn load_agent_thread( + &mut self, + thread: DbThreadMetadata, + window: &mut Window, + cx: &mut Context<Self>, + ) { + self.external_thread(Some(ExternalAgent::NativeAgent), Some(thread), window, cx); + } } impl Focusable for AgentPanel { diff --git a/crates/assistant_context/src/context_store.rs b/crates/assistant_context/src/context_store.rs index c5b5e99a52..6d13531a57 100644 --- a/crates/assistant_context/src/context_store.rs +++ b/crates/assistant_context/src/context_store.rs @@ -905,7 +905,7 @@ impl ContextStore { .into_iter() .filter(assistant_slash_commands::acceptable_prompt) .map(|prompt| { - log::debug!("registering context server command: {:?}", prompt.name); + log::info!("registering context server command: {:?}", prompt.name); slash_command_working_set.insert(Arc::new( assistant_slash_commands::ContextServerSlashCommand::new( context_server_store.clone(), From 4c85a0dc71c8f48ebd8acc090d0c8025b465cc14 Mon Sep 17 00:00:00 2001 From: Smit Barmase <heysmitbarmase@gmail.com> Date: Wed, 20 Aug 2025 12:20:09 +0530 Subject: [PATCH 515/693] project: Register dynamic capabilities even when registerOptions doesn't exist (#36554) Closes #36482 Looks like we accidentally referenced [common/formatting.ts#L67-L70](https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/formatting.ts#L67-L70) instead of [common/client.ts#L2133](https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133). Release Notes: - Fixed code not formatting on save in language servers like Biome. (Preview Only) --- crates/project/src/lsp_store.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index d2fb12ee37..12505a6a03 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -12032,16 +12032,15 @@ impl LspStore { } } -// Registration with empty capabilities should be ignored. -// https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/formatting.ts#L67-L70 +// Registration with registerOptions as null, should fallback to true. +// https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133 fn parse_register_capabilities<T: serde::de::DeserializeOwned>( reg: lsp::Registration, ) -> anyhow::Result<Option<OneOf<bool, T>>> { - Ok(reg - .register_options - .map(|options| serde_json::from_value::<T>(options)) - .transpose()? - .map(OneOf::Right)) + Ok(match reg.register_options { + Some(options) => Some(OneOf::Right(serde_json::from_value::<T>(options)?)), + None => Some(OneOf::Left(true)), + }) } fn subscribe_to_binary_statuses( From d4d049d7b91b3e8c846a13a35eedaa070e73a303 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Wed, 20 Aug 2025 10:45:03 +0200 Subject: [PATCH 516/693] agent2: Port more Zed AI features (#36559) Release Notes: - N/A --------- Co-authored-by: Danilo Leal <daniloleal09@gmail.com> --- crates/acp_thread/src/acp_thread.rs | 31 ++++++++ crates/agent_ui/src/acp/thread_view.rs | 101 +++++++++++++++++++++++++ crates/ui/src/components/callout.rs | 3 +- 3 files changed, 134 insertions(+), 1 deletion(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 5d3b35d018..e58f0a291f 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -674,6 +674,37 @@ pub struct TokenUsage { pub used_tokens: u64, } +impl TokenUsage { + pub fn ratio(&self) -> TokenUsageRatio { + #[cfg(debug_assertions)] + let warning_threshold: f32 = std::env::var("ZED_THREAD_WARNING_THRESHOLD") + .unwrap_or("0.8".to_string()) + .parse() + .unwrap(); + #[cfg(not(debug_assertions))] + let warning_threshold: f32 = 0.8; + + // When the maximum is unknown because there is no selected model, + // avoid showing the token limit warning. + if self.max_tokens == 0 { + TokenUsageRatio::Normal + } else if self.used_tokens >= self.max_tokens { + TokenUsageRatio::Exceeded + } else if self.used_tokens as f32 / self.max_tokens as f32 >= warning_threshold { + TokenUsageRatio::Warning + } else { + TokenUsageRatio::Normal + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TokenUsageRatio { + Normal, + Warning, + Exceeded, +} + #[derive(Debug, Clone)] pub struct RetryStatus { pub last_error: SharedString, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 3be88ee3c3..b93df3a5db 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -54,6 +54,7 @@ use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent}; use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; use crate::agent_diff::AgentDiff; use crate::profile_selector::{ProfileProvider, ProfileSelector}; +use crate::ui::preview::UsageCallout; use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip}; use crate::{ AgentDiffPane, AgentPanel, ContinueThread, ContinueWithBurnMode, ExpandMessageEditor, Follow, @@ -2940,6 +2941,12 @@ impl AcpThreadView { .thread(acp_thread.session_id(), cx) } + fn is_using_zed_ai_models(&self, cx: &App) -> bool { + self.as_native_thread(cx) + .and_then(|thread| thread.read(cx).model()) + .is_some_and(|model| model.provider_id() == language_model::ZED_CLOUD_PROVIDER_ID) + } + fn render_token_usage(&self, cx: &mut Context<Self>) -> Option<Div> { let thread = self.thread()?.read(cx); let usage = thread.token_usage()?; @@ -3587,6 +3594,88 @@ impl AcpThreadView { .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx))) } + fn render_token_limit_callout( + &self, + line_height: Pixels, + cx: &mut Context<Self>, + ) -> Option<Callout> { + let token_usage = self.thread()?.read(cx).token_usage()?; + let ratio = token_usage.ratio(); + + let (severity, title) = match ratio { + acp_thread::TokenUsageRatio::Normal => return None, + acp_thread::TokenUsageRatio::Warning => { + (Severity::Warning, "Thread reaching the token limit soon") + } + acp_thread::TokenUsageRatio::Exceeded => { + (Severity::Error, "Thread reached the token limit") + } + }; + + let burn_mode_available = self.as_native_thread(cx).is_some_and(|thread| { + thread.read(cx).completion_mode() == CompletionMode::Normal + && thread + .read(cx) + .model() + .is_some_and(|model| model.supports_burn_mode()) + }); + + let description = if burn_mode_available { + "To continue, start a new thread from a summary or turn Burn Mode on." + } else { + "To continue, start a new thread from a summary." + }; + + Some( + Callout::new() + .severity(severity) + .line_height(line_height) + .title(title) + .description(description) + .actions_slot( + h_flex() + .gap_0p5() + .child( + Button::new("start-new-thread", "Start New Thread") + .label_size(LabelSize::Small) + .on_click(cx.listener(|_this, _, _window, _cx| { + // todo: Once thread summarization is implemented, start a new thread from a summary. + })), + ) + .when(burn_mode_available, |this| { + this.child( + IconButton::new("burn-mode-callout", IconName::ZedBurnMode) + .icon_size(IconSize::XSmall) + .on_click(cx.listener(|this, _event, window, cx| { + this.toggle_burn_mode(&ToggleBurnMode, window, cx); + })), + ) + }), + ), + ) + } + + fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> { + if !self.is_using_zed_ai_models(cx) { + return None; + } + + let user_store = self.project.read(cx).user_store().read(cx); + if user_store.is_usage_based_billing_enabled() { + return None; + } + + let plan = user_store.plan().unwrap_or(cloud_llm_client::Plan::ZedFree); + + let usage = user_store.model_request_usage()?; + + Some( + div() + .child(UsageCallout::new(plan, usage)) + .line_height(line_height), + ) + } + fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) { self.entry_view_state.update(cx, |entry_view_state, cx| { entry_view_state.settings_changed(cx); @@ -3843,6 +3932,7 @@ impl Focusable for AcpThreadView { impl Render for AcpThreadView { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let has_messages = self.list_state.item_count() > 0; + let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5; v_flex() .size_full() @@ -3921,6 +4011,17 @@ impl Render for AcpThreadView { }) .children(self.render_thread_retry_status_callout(window, cx)) .children(self.render_thread_error(window, cx)) + .children( + if let Some(usage_callout) = self.render_usage_callout(line_height, cx) { + Some(usage_callout.into_any_element()) + } else if let Some(token_limit_callout) = + self.render_token_limit_callout(line_height, cx) + { + Some(token_limit_callout.into_any_element()) + } else { + None + }, + ) .child(self.render_message_editor(window, cx)) } } diff --git a/crates/ui/src/components/callout.rs b/crates/ui/src/components/callout.rs index 22ba0468cd..7ffeda881c 100644 --- a/crates/ui/src/components/callout.rs +++ b/crates/ui/src/components/callout.rs @@ -81,7 +81,8 @@ impl Callout { self } - /// Sets an optional tertiary call-to-action button. + /// Sets an optional dismiss button, which is usually an icon button with a close icon. + /// This button is always rendered as the last one to the far right. pub fn dismiss_action(mut self, action: impl IntoElement) -> Self { self.dismiss_action = Some(action.into_any_element()); self From 44941b5dfe5a6ce4fbb45fb3aaba8dcecee481b6 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Wed, 20 Aug 2025 12:22:19 +0300 Subject: [PATCH 517/693] Fix `clippy::for_kv_map` lint violations (#36493) Release Notes: - N/A --- Cargo.toml | 1 + crates/agent_ui/src/agent_diff.rs | 2 +- crates/channel/src/channel_buffer.rs | 2 +- crates/channel/src/channel_store.rs | 2 +- crates/extension_host/src/extension_host.rs | 10 +++++----- crates/gpui/src/platform/linux/wayland/client.rs | 2 +- crates/gpui/src/platform/linux/x11/client.rs | 2 +- crates/outline_panel/src/outline_panel.rs | 2 +- crates/project/src/lsp_store.rs | 2 +- crates/project/src/manifest_tree.rs | 2 +- crates/project/src/manifest_tree/server_tree.rs | 4 ++-- 11 files changed, 16 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dc14c8ebd9..1ed8edf836 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -823,6 +823,7 @@ style = { level = "allow", priority = -1 } # Temporary list of style lints that we've fixed so far. comparison_to_empty = "warn" +for_kv_map = "warn" into_iter_on_ref = "warn" iter_cloned_collect = "warn" iter_next_slice = "warn" diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index b20b126d9b..61a3ddd906 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1643,7 +1643,7 @@ impl AgentDiff { continue; }; - for (weak_editor, _) in buffer_editors { + for weak_editor in buffer_editors.keys() { let Some(editor) = weak_editor.upgrade() else { continue; }; diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 943e819ad6..828248b330 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -135,7 +135,7 @@ impl ChannelBuffer { } } - for (_, old_collaborator) in &self.collaborators { + for old_collaborator in self.collaborators.values() { if !new_collaborators.contains_key(&old_collaborator.peer_id) { self.buffer.update(cx, |buffer, cx| { buffer.remove_peer(old_collaborator.replica_id, cx) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 850a494613..daa8a91c7c 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -1073,7 +1073,7 @@ impl ChannelStore { if let Some(this) = this.upgrade() { this.update(cx, |this, cx| { - for (_, buffer) in &this.opened_buffers { + for buffer in this.opened_buffers.values() { if let OpenEntityHandle::Open(buffer) = &buffer && let Some(buffer) = buffer.upgrade() { diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 4c3ab8d242..fde0aeac94 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1175,16 +1175,16 @@ impl ExtensionStore { } } - for (server_id, _) in &extension.manifest.context_servers { + for server_id in extension.manifest.context_servers.keys() { self.proxy.unregister_context_server(server_id.clone(), cx); } - for (adapter, _) in &extension.manifest.debug_adapters { + for adapter in extension.manifest.debug_adapters.keys() { self.proxy.unregister_debug_adapter(adapter.clone()); } - for (locator, _) in &extension.manifest.debug_locators { + for locator in extension.manifest.debug_locators.keys() { self.proxy.unregister_debug_locator(locator.clone()); } - for (command_name, _) in &extension.manifest.slash_commands { + for command_name in extension.manifest.slash_commands.keys() { self.proxy.unregister_slash_command(command_name.clone()); } } @@ -1386,7 +1386,7 @@ impl ExtensionStore { ); } - for (id, _context_server_entry) in &manifest.context_servers { + for id in manifest.context_servers.keys() { this.proxy .register_context_server(extension.clone(), id.clone(), cx); } diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 4d31428094..2fe1da067b 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -528,7 +528,7 @@ impl WaylandClient { client.common.appearance = appearance; - for (_, window) in &mut client.windows { + for window in client.windows.values_mut() { window.set_appearance(appearance); } } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 346ba8718b..68198a285f 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -456,7 +456,7 @@ impl X11Client { move |event, _, client| match event { XDPEvent::WindowAppearance(appearance) => { client.with_common(|common| common.appearance = appearance); - for (_, window) in &mut client.0.borrow_mut().windows { + for window in client.0.borrow_mut().windows.values_mut() { window.window.set_appearance(appearance); } } diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 891ae1595d..832b7f09d1 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -5338,7 +5338,7 @@ fn subscribe_for_editor_events( } EditorEvent::Reparsed(buffer_id) => { if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) { - for (_, excerpt) in excerpts { + for excerpt in excerpts.values_mut() { excerpt.invalidate_outlines(); } } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 12505a6a03..04b14ae06e 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -10883,7 +10883,7 @@ impl LspStore { // Find all worktrees that have this server in their language server tree for (worktree_id, servers) in &local.lsp_tree.instances { if *worktree_id != key.worktree_id { - for (_, server_map) in &servers.roots { + for server_map in servers.roots.values() { if server_map.contains_key(&key.name) { worktrees_using_server.push(*worktree_id); } diff --git a/crates/project/src/manifest_tree.rs b/crates/project/src/manifest_tree.rs index ced9b34d93..5a3c7bd40f 100644 --- a/crates/project/src/manifest_tree.rs +++ b/crates/project/src/manifest_tree.rs @@ -77,7 +77,7 @@ impl ManifestTree { _subscriptions: [ cx.subscribe(&worktree_store, Self::on_worktree_store_event), cx.observe_global::<SettingsStore>(|this, cx| { - for (_, roots) in &mut this.root_points { + for roots in this.root_points.values_mut() { roots.update(cx, |worktree_roots, _| { worktree_roots.roots = RootPathTrie::new(); }) diff --git a/crates/project/src/manifest_tree/server_tree.rs b/crates/project/src/manifest_tree/server_tree.rs index f5fd481324..5e5f4bab49 100644 --- a/crates/project/src/manifest_tree/server_tree.rs +++ b/crates/project/src/manifest_tree/server_tree.rs @@ -312,8 +312,8 @@ impl LanguageServerTree { /// Remove nodes with a given ID from the tree. pub(crate) fn remove_nodes(&mut self, ids: &BTreeSet<LanguageServerId>) { - for (_, servers) in &mut self.instances { - for (_, nodes) in &mut servers.roots { + for servers in self.instances.values_mut() { + for nodes in &mut servers.roots.values_mut() { nodes.retain(|_, (node, _)| node.id.get().is_none_or(|id| !ids.contains(id))); } } From 4290f043cdb33c1f9ae5e296f95bd0509bb88b5b Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Wed, 20 Aug 2025 11:29:05 +0200 Subject: [PATCH 518/693] agent2: Fix token count not updating when changing model/toggling burn mode (#36562) Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra <me@as-cii.com> --- crates/agent2/Cargo.toml | 1 + crates/agent2/src/agent.rs | 25 +++++++++--- crates/agent2/src/thread.rs | 76 ++++++++++++++++++++++++------------- 3 files changed, 69 insertions(+), 33 deletions(-) diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 2a39440af8..bc32a79622 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -26,6 +26,7 @@ assistant_context.workspace = true assistant_tool.workspace = true assistant_tools.workspace = true chrono.workspace = true +client.workspace = true cloud_llm_client.workspace = true collections.workspace = true context_server.workspace = true diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 3c605de803..ab5716d8ad 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,8 +1,8 @@ -use crate::HistoryStore; use crate::{ ContextServerRegistry, Thread, ThreadEvent, ThreadsDatabase, ToolCallAuthorization, UserMessageContent, templates::Templates, }; +use crate::{HistoryStore, TokenUsageUpdated}; use acp_thread::{AcpThread, AgentModelSelector}; use action_log::ActionLog; use agent_client_protocol as acp; @@ -253,6 +253,7 @@ impl NativeAgent { cx.observe_release(&acp_thread, |this, acp_thread, _cx| { this.sessions.remove(acp_thread.session_id()); }), + cx.subscribe(&thread_handle, Self::handle_thread_token_usage_updated), cx.observe(&thread_handle, move |this, thread, cx| { this.save_thread(thread.clone(), cx) }), @@ -440,6 +441,23 @@ impl NativeAgent { }) } + fn handle_thread_token_usage_updated( + &mut self, + thread: Entity<Thread>, + usage: &TokenUsageUpdated, + cx: &mut Context<Self>, + ) { + let Some(session) = self.sessions.get(thread.read(cx).id()) else { + return; + }; + session + .acp_thread + .update(cx, |acp_thread, cx| { + acp_thread.update_token_usage(usage.0.clone(), cx); + }) + .ok(); + } + fn handle_project_event( &mut self, _project: Entity<Project>, @@ -695,11 +713,6 @@ impl NativeAgentConnection { thread.update_tool_call(update, cx) })??; } - ThreadEvent::TokenUsageUpdate(usage) => { - acp_thread.update(cx, |thread, cx| { - thread.update_token_usage(Some(usage), cx) - })?; - } ThreadEvent::TitleUpdate(title) => { acp_thread .update(cx, |thread, cx| thread.update_title(title, cx))??; diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index c1778bf38b..b6405dbcbd 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -15,7 +15,8 @@ use agent_settings::{ use anyhow::{Context as _, Result, anyhow}; use assistant_tool::adapt_schema_to_format; use chrono::{DateTime, Utc}; -use cloud_llm_client::{CompletionIntent, CompletionRequestStatus}; +use client::{ModelRequestUsage, RequestUsage}; +use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit}; use collections::{HashMap, IndexMap}; use fs::Fs; use futures::{ @@ -25,7 +26,9 @@ use futures::{ stream::FuturesUnordered, }; use git::repository::DiffType; -use gpui::{App, AppContext, AsyncApp, Context, Entity, SharedString, Task, WeakEntity}; +use gpui::{ + App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity, +}; use language_model::{ LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelExt, LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest, @@ -484,7 +487,6 @@ pub enum ThreadEvent { ToolCall(acp::ToolCall), ToolCallUpdate(acp_thread::ToolCallUpdate), ToolCallAuthorization(ToolCallAuthorization), - TokenUsageUpdate(acp_thread::TokenUsage), TitleUpdate(SharedString), Retry(acp_thread::RetryStatus), Stop(acp::StopReason), @@ -873,7 +875,12 @@ impl Thread { } pub fn set_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) { + let old_usage = self.latest_token_usage(); self.model = Some(model); + let new_usage = self.latest_token_usage(); + if old_usage != new_usage { + cx.emit(TokenUsageUpdated(new_usage)); + } cx.notify() } @@ -891,7 +898,12 @@ impl Thread { } pub fn set_completion_mode(&mut self, mode: CompletionMode, cx: &mut Context<Self>) { + let old_usage = self.latest_token_usage(); self.completion_mode = mode; + let new_usage = self.latest_token_usage(); + if old_usage != new_usage { + cx.emit(TokenUsageUpdated(new_usage)); + } cx.notify() } @@ -953,13 +965,15 @@ impl Thread { self.flush_pending_message(cx); } - pub fn update_token_usage(&mut self, update: language_model::TokenUsage) { + fn update_token_usage(&mut self, update: language_model::TokenUsage, cx: &mut Context<Self>) { let Some(last_user_message) = self.last_user_message() else { return; }; self.request_token_usage .insert(last_user_message.id.clone(), update); + cx.emit(TokenUsageUpdated(self.latest_token_usage())); + cx.notify(); } pub fn truncate(&mut self, message_id: UserMessageId, cx: &mut Context<Self>) -> Result<()> { @@ -1180,20 +1194,15 @@ impl Thread { )) => { *tool_use_limit_reached = true; } + Ok(LanguageModelCompletionEvent::StatusUpdate( + CompletionRequestStatus::UsageUpdated { amount, limit }, + )) => { + this.update(cx, |this, cx| { + this.update_model_request_usage(amount, limit, cx) + })?; + } Ok(LanguageModelCompletionEvent::UsageUpdate(token_usage)) => { - let usage = acp_thread::TokenUsage { - max_tokens: model.max_token_count_for_mode( - request - .mode - .unwrap_or(cloud_llm_client::CompletionMode::Normal), - ), - used_tokens: token_usage.total_tokens(), - }; - - this.update(cx, |this, _cx| this.update_token_usage(token_usage)) - .ok(); - - event_stream.send_token_usage_update(usage); + this.update(cx, |this, cx| this.update_token_usage(token_usage, cx))?; } Ok(LanguageModelCompletionEvent::Stop(StopReason::Refusal)) => { *refusal = true; @@ -1214,8 +1223,7 @@ impl Thread { event_stream, cx, )); - }) - .ok(); + })?; } Err(error) => { let completion_mode = @@ -1325,8 +1333,8 @@ impl Thread { json_parse_error, ))); } - UsageUpdate(_) | StatusUpdate(_) => {} - Stop(_) => unreachable!(), + StatusUpdate(_) => {} + UsageUpdate(_) | Stop(_) => unreachable!(), } None @@ -1506,6 +1514,21 @@ impl Thread { } } + fn update_model_request_usage(&self, amount: usize, limit: UsageLimit, cx: &mut Context<Self>) { + self.project + .read(cx) + .user_store() + .update(cx, |user_store, cx| { + user_store.update_model_request_usage( + ModelRequestUsage(RequestUsage { + amount: amount as i32, + limit, + }), + cx, + ) + }); + } + pub fn title(&self) -> SharedString { self.title.clone().unwrap_or("New Thread".into()) } @@ -1636,6 +1659,7 @@ impl Thread { }) })) } + fn last_user_message(&self) -> Option<&UserMessage> { self.messages .iter() @@ -1934,6 +1958,10 @@ impl RunningTurn { } } +pub struct TokenUsageUpdated(pub Option<acp_thread::TokenUsage>); + +impl EventEmitter<TokenUsageUpdated> for Thread {} + pub trait AgentTool where Self: 'static + Sized, @@ -2166,12 +2194,6 @@ impl ThreadEventStream { .ok(); } - fn send_token_usage_update(&self, usage: acp_thread::TokenUsage) { - self.0 - .unbounded_send(Ok(ThreadEvent::TokenUsageUpdate(usage))) - .ok(); - } - fn send_retry(&self, status: acp_thread::RetryStatus) { self.0.unbounded_send(Ok(ThreadEvent::Retry(status))).ok(); } From 83d361ba694fac74a131fde835ecae26b043100f Mon Sep 17 00:00:00 2001 From: Finn Evers <finn@zed.dev> Date: Wed, 20 Aug 2025 11:29:53 +0200 Subject: [PATCH 519/693] Add more string and comment overrides (#36566) Follow-up to #36469 Part of the issue was that we hadn't defined comment and string overrides for some languages. Hence, even after the fix edit predictions would show up in comments for me in e.g. JSONC files. This PR adds some more overrides where possible for this repo to ensure this happens less frequently. Release Notes: - N/A --- crates/languages/src/bash/overrides.scm | 2 ++ crates/languages/src/jsonc/overrides.scm | 1 + crates/languages/src/yaml/overrides.scm | 5 +++++ 3 files changed, 8 insertions(+) create mode 100644 crates/languages/src/bash/overrides.scm create mode 100644 crates/languages/src/yaml/overrides.scm diff --git a/crates/languages/src/bash/overrides.scm b/crates/languages/src/bash/overrides.scm new file mode 100644 index 0000000000..81fec9a5f5 --- /dev/null +++ b/crates/languages/src/bash/overrides.scm @@ -0,0 +1,2 @@ +(comment) @comment.inclusive +(string) @string diff --git a/crates/languages/src/jsonc/overrides.scm b/crates/languages/src/jsonc/overrides.scm index cc966ad4c1..81fec9a5f5 100644 --- a/crates/languages/src/jsonc/overrides.scm +++ b/crates/languages/src/jsonc/overrides.scm @@ -1 +1,2 @@ +(comment) @comment.inclusive (string) @string diff --git a/crates/languages/src/yaml/overrides.scm b/crates/languages/src/yaml/overrides.scm new file mode 100644 index 0000000000..9503051a62 --- /dev/null +++ b/crates/languages/src/yaml/overrides.scm @@ -0,0 +1,5 @@ +(comment) @comment.inclusive +[ + (single_quote_scalar) + (double_quote_scalar) +] @string From 0a80209c5e4f268f9ccdda0460ede2cd874f3c7b Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Wed, 20 Aug 2025 11:54:26 +0200 Subject: [PATCH 520/693] agent2: Fix remaining update_model_request_usage todos (#36570) Release Notes: - N/A --- crates/agent2/src/thread.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index b6405dbcbd..73a86d53ea 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1563,12 +1563,11 @@ impl Thread { let text = match event { LanguageModelCompletionEvent::Text(text) => text, LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::UsageUpdated { .. }, + CompletionRequestStatus::UsageUpdated { amount, limit }, ) => { - // this.update(cx, |thread, cx| { - // thread.update_model_request_usage(amount as u32, limit, cx); - // })?; - // TODO: handle usage update + this.update(cx, |thread, cx| { + thread.update_model_request_usage(amount, limit, cx); + })?; continue; } _ => continue, @@ -1629,12 +1628,11 @@ impl Thread { let text = match event { LanguageModelCompletionEvent::Text(text) => text, LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::UsageUpdated { .. }, + CompletionRequestStatus::UsageUpdated { amount, limit }, ) => { - // this.update(cx, |thread, cx| { - // thread.update_model_request_usage(amount as u32, limit, cx); - // })?; - // TODO: handle usage update + this.update(cx, |thread, cx| { + thread.update_model_request_usage(amount, limit, cx); + })?; continue; } _ => continue, From a32a264508cf1142c8cb943c68615771474c7183 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Wed, 20 Aug 2025 12:03:35 +0200 Subject: [PATCH 521/693] agent2: Use correct completion intent when generating summary (#36573) Release Notes: - N/A --- crates/agent2/src/thread.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 73a86d53ea..0e1287a920 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1541,7 +1541,7 @@ impl Thread { return Task::ready(Err(anyhow!("No summarization model available"))); }; let mut request = LanguageModelRequest { - intent: Some(CompletionIntent::ThreadSummarization), + intent: Some(CompletionIntent::ThreadContextSummarization), temperature: AgentSettings::temperature_for_model(&model, cx), ..Default::default() }; From cf7c64d77f1806cdd34b3812bbf27681fb3cb905 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 20 Aug 2025 12:05:58 +0200 Subject: [PATCH 522/693] lints: A bunch of extra style lint fixes (#36568) - **lints: Fix 'doc_lazy_continuation'** - **lints: Fix 'doc_overindented_list_items'** - **inherent_to_string and io_other_error** - **Some more lint fixes** - **lints: enable bool_assert_comparison, match_like_matches_macro and wrong_self_convention** Release Notes: - N/A --- Cargo.toml | 7 ++++ crates/agent/src/history_store.rs | 7 ++-- crates/agent/src/thread.rs | 4 +- crates/agent2/src/history_store.rs | 7 ++-- crates/agent_settings/src/agent_settings.rs | 5 +-- crates/agent_ui/src/active_thread.rs | 2 +- .../add_llm_provider_modal.rs | 24 ++++++------ .../src/assistant_context_tests.rs | 2 +- .../src/assistant_slash_command.rs | 14 +++---- .../src/extension_slash_command.rs | 2 +- .../src/cargo_workspace_command.rs | 2 +- .../src/context_server_command.rs | 2 +- .../src/default_command.rs | 2 +- .../src/delta_command.rs | 2 +- .../src/diagnostics_command.rs | 2 +- .../src/fetch_command.rs | 2 +- .../src/file_command.rs | 2 +- .../src/now_command.rs | 2 +- .../src/prompt_command.rs | 2 +- .../src/symbols_command.rs | 2 +- .../src/tab_command.rs | 2 +- .../src/auto_update_helper.rs | 19 ++++----- crates/auto_update_helper/src/updater.rs | 12 +----- crates/client/src/user.rs | 2 +- crates/collab/src/db.rs | 2 +- crates/collab/src/tests/integration_tests.rs | 12 +++--- crates/collab/src/tests/test_server.rs | 2 +- crates/collab_ui/src/collab_panel.rs | 8 ++-- crates/context_server/src/client.rs | 6 +-- crates/dap/src/client.rs | 2 +- crates/editor/src/display_map/block_map.rs | 7 +--- crates/editor/src/editor.rs | 5 +-- crates/editor/src/items.rs | 2 +- crates/editor/src/scroll/scroll_amount.rs | 5 +-- .../src/test/editor_lsp_test_context.rs | 2 + crates/eval/src/example.rs | 2 +- crates/eval/src/instance.rs | 4 +- crates/extension_api/src/extension_api.rs | 6 +-- crates/git/src/status.rs | 39 ++++++------------- crates/git_ui/src/project_diff.rs | 7 +--- crates/gpui/src/action.rs | 12 +++--- crates/gpui/src/app/test_context.rs | 7 ++-- crates/gpui/src/color.rs | 8 ++-- crates/gpui/src/geometry.rs | 28 ++++++------- crates/gpui/src/gpui.rs | 4 ++ crates/gpui/src/keymap.rs | 28 ++++++------- crates/gpui/src/keymap/binding.rs | 7 +--- crates/gpui/src/platform.rs | 2 +- crates/gpui/src/platform/linux/platform.rs | 31 +++++++-------- crates/gpui/src/platform/linux/wayland.rs | 2 +- .../gpui/src/platform/linux/wayland/window.rs | 4 +- crates/gpui/src/platform/linux/x11/window.rs | 2 +- crates/gpui/src/platform/mac/window.rs | 2 +- crates/gpui/src/tab_stop.rs | 21 +++------- crates/gpui_macros/src/gpui_macros.rs | 2 +- crates/language/src/language_registry.rs | 2 +- crates/language_model/src/role.rs | 2 +- crates/migrator/src/migrator.rs | 2 +- crates/multi_buffer/src/multi_buffer.rs | 2 +- crates/paths/src/paths.rs | 2 +- crates/project/src/debugger.rs | 4 +- .../project/src/debugger/breakpoint_store.rs | 2 +- crates/project/src/debugger/memory.rs | 3 +- crates/project/src/git_store/conflict_set.rs | 2 +- crates/project/src/git_store/git_traversal.rs | 2 +- crates/project/src/lsp_store.rs | 4 +- crates/project/src/manifest_tree/path_trie.rs | 6 +-- crates/project/src/project.rs | 30 ++++++-------- crates/project/src/project_tests.rs | 4 +- crates/project_panel/src/project_panel.rs | 4 +- crates/remote/src/ssh_session.rs | 10 ++--- .../remote_server/src/remote_editing_tests.rs | 2 +- crates/remote_server/src/unix.rs | 2 +- crates/repl/src/kernels/mod.rs | 5 +-- crates/reqwest_client/src/reqwest_client.rs | 2 +- crates/rope/src/chunk.rs | 2 +- crates/rpc/src/conn.rs | 4 +- crates/search/src/search.rs | 4 +- crates/settings/src/settings.rs | 4 +- crates/task/src/shell_builder.rs | 2 +- .../src/terminal_slash_command.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 2 +- .../ui/src/components/button/button_like.rs | 10 ++--- crates/ui/src/utils/format_distance.rs | 4 +- crates/util/src/archive.rs | 2 +- crates/util/src/markdown.rs | 2 +- crates/util/src/paths.rs | 25 +++++++----- crates/vim/src/motion.rs | 39 +++++++++---------- crates/vim/src/vim.rs | 5 +-- crates/worktree/src/worktree.rs | 6 +-- crates/worktree/src/worktree_tests.rs | 12 +++--- crates/x_ai/src/x_ai.rs | 5 +-- 92 files changed, 277 insertions(+), 345 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1ed8edf836..3610808984 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -822,15 +822,21 @@ single_range_in_vec_init = "allow" style = { level = "allow", priority = -1 } # Temporary list of style lints that we've fixed so far. +bool_assert_comparison = "warn" comparison_to_empty = "warn" +doc_lazy_continuation = "warn" +doc_overindented_list_items = "warn" +inherent_to_string = "warn" for_kv_map = "warn" into_iter_on_ref = "warn" +io_other_error = "warn" iter_cloned_collect = "warn" iter_next_slice = "warn" iter_nth = "warn" iter_nth_zero = "warn" iter_skip_next = "warn" let_and_return = "warn" +match_like_matches_macro = "warn" module_inception = { level = "deny" } question_mark = { level = "deny" } single_match = "warn" @@ -846,6 +852,7 @@ needless_return = { level = "warn" } unnecessary_mut_passed = {level = "warn"} unnecessary_map_or = { level = "warn" } unused_unit = "warn" +wrong_self_convention = "warn" # Individual rules that have violations in the codebase: type_complexity = "allow" diff --git a/crates/agent/src/history_store.rs b/crates/agent/src/history_store.rs index eb39c3e454..8f4c1a1e2e 100644 --- a/crates/agent/src/history_store.rs +++ b/crates/agent/src/history_store.rs @@ -254,10 +254,9 @@ impl HistoryStore { } pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) { - self.recently_opened_entries.retain(|entry| match entry { - HistoryEntryId::Thread(thread_id) if thread_id == &id => false, - _ => true, - }); + self.recently_opened_entries.retain( + |entry| !matches!(entry, HistoryEntryId::Thread(thread_id) if thread_id == &id), + ); self.save_recently_opened_entries(cx); } diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index fc91e1bb62..88f82701a4 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -181,7 +181,7 @@ impl Message { } } - pub fn to_string(&self) -> String { + pub fn to_message_content(&self) -> String { let mut result = String::new(); if !self.loaded_context.text.is_empty() { @@ -2823,7 +2823,7 @@ impl Thread { let message_content = self .message(message_id) - .map(|msg| msg.to_string()) + .map(|msg| msg.to_message_content()) .unwrap_or_default(); cx.background_spawn(async move { diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs index 7eb7da94ba..3df4eddde4 100644 --- a/crates/agent2/src/history_store.rs +++ b/crates/agent2/src/history_store.rs @@ -312,10 +312,9 @@ impl HistoryStore { } pub fn remove_recently_opened_thread(&mut self, id: acp::SessionId, cx: &mut Context<Self>) { - self.recently_opened_entries.retain(|entry| match entry { - HistoryEntryId::AcpThread(thread_id) if thread_id == &id => false, - _ => true, - }); + self.recently_opened_entries.retain( + |entry| !matches!(entry, HistoryEntryId::AcpThread(thread_id) if thread_id == &id), + ); self.save_recently_opened_entries(cx); } diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 1fe41d002c..ed1ed2b898 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -505,9 +505,8 @@ impl Settings for AgentSettings { } } - debug_assert_eq!( - sources.default.always_allow_tool_actions.unwrap_or(false), - false, + debug_assert!( + !sources.default.always_allow_tool_actions.unwrap_or(false), "For security, agent.always_allow_tool_actions should always be false in default.json. If it's true, that is a bug that should be fixed!" ); diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index e595b94ebb..92588cf213 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -1765,7 +1765,7 @@ impl ActiveThread { .thread .read(cx) .message(message_id) - .map(|msg| msg.to_string()) + .map(|msg| msg.to_message_content()) .unwrap_or_default(); telemetry::event!( diff --git a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs index 998641bf01..182831f488 100644 --- a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs +++ b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs @@ -668,10 +668,10 @@ mod tests { ); let parsed_model = model_input.parse(cx).unwrap(); - assert_eq!(parsed_model.capabilities.tools, true); - assert_eq!(parsed_model.capabilities.images, false); - assert_eq!(parsed_model.capabilities.parallel_tool_calls, false); - assert_eq!(parsed_model.capabilities.prompt_cache_key, false); + assert!(parsed_model.capabilities.tools); + assert!(!parsed_model.capabilities.images); + assert!(!parsed_model.capabilities.parallel_tool_calls); + assert!(!parsed_model.capabilities.prompt_cache_key); }); } @@ -693,10 +693,10 @@ mod tests { model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected; let parsed_model = model_input.parse(cx).unwrap(); - assert_eq!(parsed_model.capabilities.tools, false); - assert_eq!(parsed_model.capabilities.images, false); - assert_eq!(parsed_model.capabilities.parallel_tool_calls, false); - assert_eq!(parsed_model.capabilities.prompt_cache_key, false); + assert!(!parsed_model.capabilities.tools); + assert!(!parsed_model.capabilities.images); + assert!(!parsed_model.capabilities.parallel_tool_calls); + assert!(!parsed_model.capabilities.prompt_cache_key); }); } @@ -719,10 +719,10 @@ mod tests { let parsed_model = model_input.parse(cx).unwrap(); assert_eq!(parsed_model.name, "somemodel"); - assert_eq!(parsed_model.capabilities.tools, true); - assert_eq!(parsed_model.capabilities.images, false); - assert_eq!(parsed_model.capabilities.parallel_tool_calls, true); - assert_eq!(parsed_model.capabilities.prompt_cache_key, false); + assert!(parsed_model.capabilities.tools); + assert!(!parsed_model.capabilities.images); + assert!(parsed_model.capabilities.parallel_tool_calls); + assert!(!parsed_model.capabilities.prompt_cache_key); }); } diff --git a/crates/assistant_context/src/assistant_context_tests.rs b/crates/assistant_context/src/assistant_context_tests.rs index 28cc8ef8f0..3db4a33b19 100644 --- a/crates/assistant_context/src/assistant_context_tests.rs +++ b/crates/assistant_context/src/assistant_context_tests.rs @@ -1436,6 +1436,6 @@ impl SlashCommand for FakeSlashCommand { sections: vec![], run_commands_in_text: false, } - .to_event_stream())) + .into_event_stream())) } } diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 828f115bf5..4b85fa2edf 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -161,7 +161,7 @@ impl SlashCommandOutput { } /// Returns this [`SlashCommandOutput`] as a stream of [`SlashCommandEvent`]s. - pub fn to_event_stream(mut self) -> BoxStream<'static, Result<SlashCommandEvent>> { + pub fn into_event_stream(mut self) -> BoxStream<'static, Result<SlashCommandEvent>> { self.ensure_valid_section_ranges(); let mut events = Vec::new(); @@ -363,7 +363,7 @@ mod tests { run_commands_in_text: false, }; - let events = output.clone().to_event_stream().collect::<Vec<_>>().await; + let events = output.clone().into_event_stream().collect::<Vec<_>>().await; let events = events .into_iter() .filter_map(|event| event.ok()) @@ -386,7 +386,7 @@ mod tests { ); let new_output = - SlashCommandOutput::from_event_stream(output.clone().to_event_stream()) + SlashCommandOutput::from_event_stream(output.clone().into_event_stream()) .await .unwrap(); @@ -415,7 +415,7 @@ mod tests { run_commands_in_text: false, }; - let events = output.clone().to_event_stream().collect::<Vec<_>>().await; + let events = output.clone().into_event_stream().collect::<Vec<_>>().await; let events = events .into_iter() .filter_map(|event| event.ok()) @@ -452,7 +452,7 @@ mod tests { ); let new_output = - SlashCommandOutput::from_event_stream(output.clone().to_event_stream()) + SlashCommandOutput::from_event_stream(output.clone().into_event_stream()) .await .unwrap(); @@ -493,7 +493,7 @@ mod tests { run_commands_in_text: false, }; - let events = output.clone().to_event_stream().collect::<Vec<_>>().await; + let events = output.clone().into_event_stream().collect::<Vec<_>>().await; let events = events .into_iter() .filter_map(|event| event.ok()) @@ -562,7 +562,7 @@ mod tests { ); let new_output = - SlashCommandOutput::from_event_stream(output.clone().to_event_stream()) + SlashCommandOutput::from_event_stream(output.clone().into_event_stream()) .await .unwrap(); diff --git a/crates/assistant_slash_command/src/extension_slash_command.rs b/crates/assistant_slash_command/src/extension_slash_command.rs index 74c46ffb5f..e47ae52c98 100644 --- a/crates/assistant_slash_command/src/extension_slash_command.rs +++ b/crates/assistant_slash_command/src/extension_slash_command.rs @@ -166,7 +166,7 @@ impl SlashCommand for ExtensionSlashCommand { .collect(), run_commands_in_text: false, } - .to_event_stream()) + .into_event_stream()) }) } } diff --git a/crates/assistant_slash_commands/src/cargo_workspace_command.rs b/crates/assistant_slash_commands/src/cargo_workspace_command.rs index 8b088ea012..d58b2edc4c 100644 --- a/crates/assistant_slash_commands/src/cargo_workspace_command.rs +++ b/crates/assistant_slash_commands/src/cargo_workspace_command.rs @@ -150,7 +150,7 @@ impl SlashCommand for CargoWorkspaceSlashCommand { }], run_commands_in_text: false, } - .to_event_stream()) + .into_event_stream()) }) }); output.unwrap_or_else(|error| Task::ready(Err(error))) diff --git a/crates/assistant_slash_commands/src/context_server_command.rs b/crates/assistant_slash_commands/src/context_server_command.rs index 6caa1beb3b..ee0cbf54c2 100644 --- a/crates/assistant_slash_commands/src/context_server_command.rs +++ b/crates/assistant_slash_commands/src/context_server_command.rs @@ -191,7 +191,7 @@ impl SlashCommand for ContextServerSlashCommand { text: prompt, run_commands_in_text: false, } - .to_event_stream()) + .into_event_stream()) }) } else { Task::ready(Err(anyhow!("Context server not found"))) diff --git a/crates/assistant_slash_commands/src/default_command.rs b/crates/assistant_slash_commands/src/default_command.rs index 6fce7f07a4..01eff881cf 100644 --- a/crates/assistant_slash_commands/src/default_command.rs +++ b/crates/assistant_slash_commands/src/default_command.rs @@ -85,7 +85,7 @@ impl SlashCommand for DefaultSlashCommand { text, run_commands_in_text: true, } - .to_event_stream()) + .into_event_stream()) }) } } diff --git a/crates/assistant_slash_commands/src/delta_command.rs b/crates/assistant_slash_commands/src/delta_command.rs index 2cc4591386..ea05fca588 100644 --- a/crates/assistant_slash_commands/src/delta_command.rs +++ b/crates/assistant_slash_commands/src/delta_command.rs @@ -118,7 +118,7 @@ impl SlashCommand for DeltaSlashCommand { } anyhow::ensure!(changes_detected, "no new changes detected"); - Ok(output.to_event_stream()) + Ok(output.into_event_stream()) }) } } diff --git a/crates/assistant_slash_commands/src/diagnostics_command.rs b/crates/assistant_slash_commands/src/diagnostics_command.rs index 536fe9f0ef..10f950c866 100644 --- a/crates/assistant_slash_commands/src/diagnostics_command.rs +++ b/crates/assistant_slash_commands/src/diagnostics_command.rs @@ -189,7 +189,7 @@ impl SlashCommand for DiagnosticsSlashCommand { window.spawn(cx, async move |_| { task.await? - .map(|output| output.to_event_stream()) + .map(|output| output.into_event_stream()) .context("No diagnostics found") }) } diff --git a/crates/assistant_slash_commands/src/fetch_command.rs b/crates/assistant_slash_commands/src/fetch_command.rs index 4e0bb3d05a..6d3f66c9a2 100644 --- a/crates/assistant_slash_commands/src/fetch_command.rs +++ b/crates/assistant_slash_commands/src/fetch_command.rs @@ -177,7 +177,7 @@ impl SlashCommand for FetchSlashCommand { }], run_commands_in_text: false, } - .to_event_stream()) + .into_event_stream()) }) } } diff --git a/crates/assistant_slash_commands/src/file_command.rs b/crates/assistant_slash_commands/src/file_command.rs index 894aa94a27..a973d653e4 100644 --- a/crates/assistant_slash_commands/src/file_command.rs +++ b/crates/assistant_slash_commands/src/file_command.rs @@ -371,7 +371,7 @@ fn collect_files( &mut output, ) .log_err(); - let mut buffer_events = output.to_event_stream(); + let mut buffer_events = output.into_event_stream(); while let Some(event) = buffer_events.next().await { events_tx.unbounded_send(event)?; } diff --git a/crates/assistant_slash_commands/src/now_command.rs b/crates/assistant_slash_commands/src/now_command.rs index e4abef2a7c..aec21e7173 100644 --- a/crates/assistant_slash_commands/src/now_command.rs +++ b/crates/assistant_slash_commands/src/now_command.rs @@ -66,6 +66,6 @@ impl SlashCommand for NowSlashCommand { }], run_commands_in_text: false, } - .to_event_stream())) + .into_event_stream())) } } diff --git a/crates/assistant_slash_commands/src/prompt_command.rs b/crates/assistant_slash_commands/src/prompt_command.rs index c177f9f359..27029ac156 100644 --- a/crates/assistant_slash_commands/src/prompt_command.rs +++ b/crates/assistant_slash_commands/src/prompt_command.rs @@ -117,7 +117,7 @@ impl SlashCommand for PromptSlashCommand { }], run_commands_in_text: true, } - .to_event_stream()) + .into_event_stream()) }) } } diff --git a/crates/assistant_slash_commands/src/symbols_command.rs b/crates/assistant_slash_commands/src/symbols_command.rs index ef93146431..3028709144 100644 --- a/crates/assistant_slash_commands/src/symbols_command.rs +++ b/crates/assistant_slash_commands/src/symbols_command.rs @@ -92,7 +92,7 @@ impl SlashCommand for OutlineSlashCommand { text: outline_text, run_commands_in_text: false, } - .to_event_stream()) + .into_event_stream()) }) }); diff --git a/crates/assistant_slash_commands/src/tab_command.rs b/crates/assistant_slash_commands/src/tab_command.rs index e4ae391a9c..a124beed63 100644 --- a/crates/assistant_slash_commands/src/tab_command.rs +++ b/crates/assistant_slash_commands/src/tab_command.rs @@ -157,7 +157,7 @@ impl SlashCommand for TabSlashCommand { for (full_path, buffer, _) in tab_items_search.await? { append_buffer_to_output(&buffer, full_path.as_deref(), &mut output).log_err(); } - Ok(output.to_event_stream()) + Ok(output.into_event_stream()) }) } } diff --git a/crates/auto_update_helper/src/auto_update_helper.rs b/crates/auto_update_helper/src/auto_update_helper.rs index 3aa57094d3..21ead701b2 100644 --- a/crates/auto_update_helper/src/auto_update_helper.rs +++ b/crates/auto_update_helper/src/auto_update_helper.rs @@ -128,23 +128,20 @@ mod windows_impl { #[test] fn test_parse_args() { // launch can be specified via two separate arguments - assert_eq!(parse_args(["--launch".into(), "true".into()]).launch, true); - assert_eq!( - parse_args(["--launch".into(), "false".into()]).launch, - false - ); + assert!(parse_args(["--launch".into(), "true".into()]).launch); + assert!(!parse_args(["--launch".into(), "false".into()]).launch); // launch can be specified via one single argument - assert_eq!(parse_args(["--launch=true".into()]).launch, true); - assert_eq!(parse_args(["--launch=false".into()]).launch, false); + assert!(parse_args(["--launch=true".into()]).launch); + assert!(!parse_args(["--launch=false".into()]).launch); // launch defaults to true on no arguments - assert_eq!(parse_args([]).launch, true); + assert!(parse_args([]).launch); // launch defaults to true on invalid arguments - assert_eq!(parse_args(["--launch".into()]).launch, true); - assert_eq!(parse_args(["--launch=".into()]).launch, true); - assert_eq!(parse_args(["--launch=invalid".into()]).launch, true); + assert!(parse_args(["--launch".into()]).launch); + assert!(parse_args(["--launch=".into()]).launch); + assert!(parse_args(["--launch=invalid".into()]).launch); } } } diff --git a/crates/auto_update_helper/src/updater.rs b/crates/auto_update_helper/src/updater.rs index 920f8d5fcf..7627716176 100644 --- a/crates/auto_update_helper/src/updater.rs +++ b/crates/auto_update_helper/src/updater.rs @@ -90,11 +90,7 @@ pub(crate) const JOBS: [Job; 2] = [ std::thread::sleep(Duration::from_millis(1000)); if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") { match config.as_str() { - "err" => Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Simulated error", - )) - .context("Anyhow!"), + "err" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"), _ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config), } } else { @@ -105,11 +101,7 @@ pub(crate) const JOBS: [Job; 2] = [ std::thread::sleep(Duration::from_millis(1000)); if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") { match config.as_str() { - "err" => Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Simulated error", - )) - .context("Anyhow!"), + "err" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"), _ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config), } } else { diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 2599be9b16..20f99e3944 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -41,7 +41,7 @@ impl std::fmt::Display for ChannelId { pub struct ProjectId(pub u64); impl ProjectId { - pub fn to_proto(&self) -> u64 { + pub fn to_proto(self) -> u64 { self.0 } } diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 774eec5d2c..95a485305c 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -685,7 +685,7 @@ impl LocalSettingsKind { } } - pub fn to_proto(&self) -> proto::LocalSettingsKind { + pub fn to_proto(self) -> proto::LocalSettingsKind { match self { Self::Settings => proto::LocalSettingsKind::Settings, Self::Tasks => proto::LocalSettingsKind::Tasks, diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 930e635dd8..e01736f0ef 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -3208,7 +3208,7 @@ async fn test_fs_operations( }) .await .unwrap() - .to_included() + .into_included() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { @@ -3237,7 +3237,7 @@ async fn test_fs_operations( }) .await .unwrap() - .to_included() + .into_included() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { @@ -3266,7 +3266,7 @@ async fn test_fs_operations( }) .await .unwrap() - .to_included() + .into_included() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { @@ -3295,7 +3295,7 @@ async fn test_fs_operations( }) .await .unwrap() - .to_included() + .into_included() .unwrap(); project_b @@ -3304,7 +3304,7 @@ async fn test_fs_operations( }) .await .unwrap() - .to_included() + .into_included() .unwrap(); project_b @@ -3313,7 +3313,7 @@ async fn test_fs_operations( }) .await .unwrap() - .to_included() + .into_included() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 07ea1efc9d..f1c0b2d182 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -897,7 +897,7 @@ impl TestClient { let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap()); let entity = window.root(cx).unwrap(); - let cx = VisualTestContext::from_window(*window.deref(), cx).as_mut(); + let cx = VisualTestContext::from_window(*window.deref(), cx).into_mut(); // it might be nice to try and cleanup these at the end of each test. (entity, cx) } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 0f785c1f90..b756984a09 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1821,10 +1821,10 @@ impl CollabPanel { } fn select_channel_editor(&mut self) { - self.selection = self.entries.iter().position(|entry| match entry { - ListEntry::ChannelEditor { .. } => true, - _ => false, - }); + self.selection = self + .entries + .iter() + .position(|entry| matches!(entry, ListEntry::ChannelEditor { .. })); } fn new_subchannel( diff --git a/crates/context_server/src/client.rs b/crates/context_server/src/client.rs index ccf7622d82..03cf047ac5 100644 --- a/crates/context_server/src/client.rs +++ b/crates/context_server/src/client.rs @@ -67,11 +67,7 @@ pub(crate) struct Client { pub(crate) struct ContextServerId(pub Arc<str>); fn is_null_value<T: Serialize>(value: &T) -> bool { - if let Ok(Value::Null) = serde_json::to_value(value) { - true - } else { - false - } + matches!(serde_json::to_value(value), Ok(Value::Null)) } #[derive(Serialize, Deserialize)] diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 7b791450ec..2590bf5c8b 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -23,7 +23,7 @@ impl SessionId { Self(client_id as u32) } - pub fn to_proto(&self) -> u64 { + pub fn to_proto(self) -> u64 { self.0 as u64 } } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 5d5c9500eb..0d31398a54 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -2868,12 +2868,7 @@ mod tests { 1, blocks .iter() - .filter(|(_, block)| { - match block { - Block::FoldedBuffer { .. } => true, - _ => false, - } - }) + .filter(|(_, block)| { matches!(block, Block::FoldedBuffer { .. }) }) .count(), "Should have one folded block, producing a header of the second buffer" ); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 575631b517..2f3ced65dc 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -782,10 +782,7 @@ impl MinimapVisibility { } fn disabled(&self) -> bool { - match *self { - Self::Disabled => true, - _ => false, - } + matches!(*self, Self::Disabled) } fn settings_visibility(&self) -> bool { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 8957e0e99c..62889c638f 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -293,7 +293,7 @@ impl FollowableItem for Editor { EditorEvent::ExcerptsRemoved { ids, .. } => { update .deleted_excerpts - .extend(ids.iter().map(ExcerptId::to_proto)); + .extend(ids.iter().copied().map(ExcerptId::to_proto)); true } EditorEvent::ScrollPositionChanged { autoscroll, .. } if !autoscroll => { diff --git a/crates/editor/src/scroll/scroll_amount.rs b/crates/editor/src/scroll/scroll_amount.rs index b2af4f8e4f..5992c9023c 100644 --- a/crates/editor/src/scroll/scroll_amount.rs +++ b/crates/editor/src/scroll/scroll_amount.rs @@ -67,10 +67,7 @@ impl ScrollAmount { } pub fn is_full_page(&self) -> bool { - match self { - ScrollAmount::Page(count) if count.abs() == 1.0 => true, - _ => false, - } + matches!(self, ScrollAmount::Page(count) if count.abs() == 1.0) } pub fn direction(&self) -> ScrollDirection { diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index c59786b1eb..3f78fa2f3e 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -300,6 +300,7 @@ impl EditorLspTestContext { self.to_lsp_range(ranges[0].clone()) } + #[expect(clippy::wrong_self_convention, reason = "This is test code")] pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range { let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx)); let start_point = range.start.to_point(&snapshot.buffer_snapshot); @@ -326,6 +327,7 @@ impl EditorLspTestContext { }) } + #[expect(clippy::wrong_self_convention, reason = "This is test code")] pub fn to_lsp(&mut self, offset: usize) -> lsp::Position { let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx)); let point = offset.to_point(&snapshot.buffer_snapshot); diff --git a/crates/eval/src/example.rs b/crates/eval/src/example.rs index 82e95728a1..457b62e98c 100644 --- a/crates/eval/src/example.rs +++ b/crates/eval/src/example.rs @@ -335,7 +335,7 @@ impl ExampleContext { for message in thread.messages().skip(message_count_before) { messages.push(Message { _role: message.role, - text: message.to_string(), + text: message.to_message_content(), tool_use: thread .tool_uses_for_message(message.id, cx) .into_iter() diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs index bbbe54b43f..53ce6088c0 100644 --- a/crates/eval/src/instance.rs +++ b/crates/eval/src/instance.rs @@ -1192,7 +1192,7 @@ mod test { output.analysis, Some("The model did a good job but there were still compilations errors.".into()) ); - assert_eq!(output.passed, true); + assert!(output.passed); let response = r#" Text around ignored @@ -1212,6 +1212,6 @@ mod test { output.analysis, Some("Failed to compile:\n- Error 1\n- Error 2".into()) ); - assert_eq!(output.passed, false); + assert!(!output.passed); } } diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs index aacc5d8795..72327179ee 100644 --- a/crates/extension_api/src/extension_api.rs +++ b/crates/extension_api/src/extension_api.rs @@ -232,10 +232,10 @@ pub trait Extension: Send + Sync { /// /// To work through a real-world example, take a `cargo run` task and a hypothetical `cargo` locator: /// 1. We may need to modify the task; in this case, it is problematic that `cargo run` spawns a binary. We should turn `cargo run` into a debug scenario with - /// `cargo build` task. This is the decision we make at `dap_locator_create_scenario` scope. + /// `cargo build` task. This is the decision we make at `dap_locator_create_scenario` scope. /// 2. Then, after the build task finishes, we will run `run_dap_locator` of the locator that produced the build task to find the program to be debugged. This function - /// should give us a debugger-agnostic configuration for launching a debug target (that we end up resolving with [`Extension::dap_config_to_scenario`]). It's almost as if the user - /// found the artifact path by themselves. + /// should give us a debugger-agnostic configuration for launching a debug target (that we end up resolving with [`Extension::dap_config_to_scenario`]). It's almost as if the user + /// found the artifact path by themselves. /// /// Note that you're not obliged to use build tasks with locators. Specifically, it is sufficient to provide a debug configuration directly in the return value of /// `dap_locator_create_scenario` if you're able to do that. Make sure to not fill out `build` field in that case, as that will prevent Zed from running second phase of resolution in such case. diff --git a/crates/git/src/status.rs b/crates/git/src/status.rs index 92836042f2..71ca14c5b2 100644 --- a/crates/git/src/status.rs +++ b/crates/git/src/status.rs @@ -153,17 +153,11 @@ impl FileStatus { } pub fn is_conflicted(self) -> bool { - match self { - FileStatus::Unmerged { .. } => true, - _ => false, - } + matches!(self, FileStatus::Unmerged { .. }) } pub fn is_ignored(self) -> bool { - match self { - FileStatus::Ignored => true, - _ => false, - } + matches!(self, FileStatus::Ignored) } pub fn has_changes(&self) -> bool { @@ -176,40 +170,31 @@ impl FileStatus { pub fn is_modified(self) -> bool { match self { - FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) { - (StatusCode::Modified, _) | (_, StatusCode::Modified) => true, - _ => false, - }, + FileStatus::Tracked(tracked) => matches!( + (tracked.index_status, tracked.worktree_status), + (StatusCode::Modified, _) | (_, StatusCode::Modified) + ), _ => false, } } pub fn is_created(self) -> bool { match self { - FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) { - (StatusCode::Added, _) | (_, StatusCode::Added) => true, - _ => false, - }, + FileStatus::Tracked(tracked) => matches!( + (tracked.index_status, tracked.worktree_status), + (StatusCode::Added, _) | (_, StatusCode::Added) + ), FileStatus::Untracked => true, _ => false, } } pub fn is_deleted(self) -> bool { - match self { - FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) { - (StatusCode::Deleted, _) | (_, StatusCode::Deleted) => true, - _ => false, - }, - _ => false, - } + matches!(self, FileStatus::Tracked(tracked) if matches!((tracked.index_status, tracked.worktree_status), (StatusCode::Deleted, _) | (_, StatusCode::Deleted))) } pub fn is_untracked(self) -> bool { - match self { - FileStatus::Untracked => true, - _ => false, - } + matches!(self, FileStatus::Untracked) } pub fn summary(self) -> GitSummary { diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index cc1535b7c3..c1521004a2 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -1070,8 +1070,7 @@ pub struct ProjectDiffEmptyState { impl RenderOnce for ProjectDiffEmptyState { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let status_against_remote = |ahead_by: usize, behind_by: usize| -> bool { - match self.current_branch { - Some(Branch { + matches!(self.current_branch, Some(Branch { upstream: Some(Upstream { tracking: @@ -1081,9 +1080,7 @@ impl RenderOnce for ProjectDiffEmptyState { .. }), .. - }) if (ahead > 0) == (ahead_by > 0) && (behind > 0) == (behind_by > 0) => true, - _ => false, - } + }) if (ahead > 0) == (ahead_by > 0) && (behind > 0) == (behind_by > 0)) }; let change_count = |current_branch: &Branch| -> (usize, usize) { diff --git a/crates/gpui/src/action.rs b/crates/gpui/src/action.rs index b179076cd5..0b824fec34 100644 --- a/crates/gpui/src/action.rs +++ b/crates/gpui/src/action.rs @@ -73,18 +73,18 @@ macro_rules! actions { /// - `name = "ActionName"` overrides the action's name. This must not contain `::`. /// /// - `no_json` causes the `build` method to always error and `action_json_schema` to return `None`, -/// and allows actions not implement `serde::Serialize` and `schemars::JsonSchema`. +/// and allows actions not implement `serde::Serialize` and `schemars::JsonSchema`. /// /// - `no_register` skips registering the action. This is useful for implementing the `Action` trait -/// while not supporting invocation by name or JSON deserialization. +/// while not supporting invocation by name or JSON deserialization. /// /// - `deprecated_aliases = ["editor::SomeAction"]` specifies deprecated old names for the action. -/// These action names should *not* correspond to any actions that are registered. These old names -/// can then still be used to refer to invoke this action. In Zed, the keymap JSON schema will -/// accept these old names and provide warnings. +/// These action names should *not* correspond to any actions that are registered. These old names +/// can then still be used to refer to invoke this action. In Zed, the keymap JSON schema will +/// accept these old names and provide warnings. /// /// - `deprecated = "Message about why this action is deprecation"` specifies a deprecation message. -/// In Zed, the keymap JSON schema will cause this to be displayed as a warning. +/// In Zed, the keymap JSON schema will cause this to be displayed as a warning. /// /// # Manual Implementation /// diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 43adacf7dd..a69d9d1e26 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -192,6 +192,7 @@ impl TestAppContext { &self.foreground_executor } + #[expect(clippy::wrong_self_convention)] fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> { let mut cx = self.app.borrow_mut(); cx.new(build_entity) @@ -244,7 +245,7 @@ impl TestAppContext { ) .unwrap(); drop(cx); - let cx = VisualTestContext::from_window(*window.deref(), self).as_mut(); + let cx = VisualTestContext::from_window(*window.deref(), self).into_mut(); cx.run_until_parked(); cx } @@ -273,7 +274,7 @@ impl TestAppContext { .unwrap(); drop(cx); let view = window.root(self).unwrap(); - let cx = VisualTestContext::from_window(*window.deref(), self).as_mut(); + let cx = VisualTestContext::from_window(*window.deref(), self).into_mut(); cx.run_until_parked(); // it might be nice to try and cleanup these at the end of each test. @@ -882,7 +883,7 @@ impl VisualTestContext { /// Get an &mut VisualTestContext (which is mostly what you need to pass to other methods). /// This method internally retains the VisualTestContext until the end of the test. - pub fn as_mut(self) -> &'static mut Self { + pub fn into_mut(self) -> &'static mut Self { let ptr = Box::into_raw(Box::new(self)); // safety: on_quit will be called after the test has finished. // the executor will ensure that all tasks related to the test have stopped. diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 639c84c101..cb7329c03f 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -905,9 +905,9 @@ mod tests { assert_eq!(background.solid, color); assert_eq!(background.opacity(0.5).solid, color.opacity(0.5)); - assert_eq!(background.is_transparent(), false); + assert!(!background.is_transparent()); background.solid = hsla(0.0, 0.0, 0.0, 0.0); - assert_eq!(background.is_transparent(), true); + assert!(background.is_transparent()); } #[test] @@ -921,7 +921,7 @@ mod tests { assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5)); assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5)); - assert_eq!(background.is_transparent(), false); - assert_eq!(background.opacity(0.0).is_transparent(), true); + assert!(!background.is_transparent()); + assert!(background.opacity(0.0).is_transparent()); } } diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index 2de3e23ff7..ef446a073e 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -1641,7 +1641,7 @@ impl Bounds<Pixels> { } /// Convert the bounds from logical pixels to physical pixels - pub fn to_device_pixels(&self, factor: f32) -> Bounds<DevicePixels> { + pub fn to_device_pixels(self, factor: f32) -> Bounds<DevicePixels> { Bounds { origin: point( DevicePixels((self.origin.x.0 * factor).round() as i32), @@ -1957,7 +1957,7 @@ impl Edges<DefiniteLength> { /// assert_eq!(edges_in_pixels.bottom, px(32.0)); // 2 rems /// assert_eq!(edges_in_pixels.left, px(50.0)); // 25% of parent width /// ``` - pub fn to_pixels(&self, parent_size: Size<AbsoluteLength>, rem_size: Pixels) -> Edges<Pixels> { + pub fn to_pixels(self, parent_size: Size<AbsoluteLength>, rem_size: Pixels) -> Edges<Pixels> { Edges { top: self.top.to_pixels(parent_size.height, rem_size), right: self.right.to_pixels(parent_size.width, rem_size), @@ -2027,7 +2027,7 @@ impl Edges<AbsoluteLength> { /// assert_eq!(edges_in_pixels.bottom, px(20.0)); // Already in pixels /// assert_eq!(edges_in_pixels.left, px(32.0)); // 2 rems converted to pixels /// ``` - pub fn to_pixels(&self, rem_size: Pixels) -> Edges<Pixels> { + pub fn to_pixels(self, rem_size: Pixels) -> Edges<Pixels> { Edges { top: self.top.to_pixels(rem_size), right: self.right.to_pixels(rem_size), @@ -2272,7 +2272,7 @@ impl Corners<AbsoluteLength> { /// assert_eq!(corners_in_pixels.bottom_right, Pixels(30.0)); /// assert_eq!(corners_in_pixels.bottom_left, Pixels(32.0)); // 2 rems converted to pixels /// ``` - pub fn to_pixels(&self, rem_size: Pixels) -> Corners<Pixels> { + pub fn to_pixels(self, rem_size: Pixels) -> Corners<Pixels> { Corners { top_left: self.top_left.to_pixels(rem_size), top_right: self.top_right.to_pixels(rem_size), @@ -2858,7 +2858,7 @@ impl DevicePixels { /// let total_bytes = pixels.to_bytes(bytes_per_pixel); /// assert_eq!(total_bytes, 40); // 10 pixels * 4 bytes/pixel = 40 bytes /// ``` - pub fn to_bytes(&self, bytes_per_pixel: u8) -> u32 { + pub fn to_bytes(self, bytes_per_pixel: u8) -> u32 { self.0 as u32 * bytes_per_pixel as u32 } } @@ -3073,8 +3073,8 @@ pub struct Rems(pub f32); impl Rems { /// Convert this Rem value to pixels. - pub fn to_pixels(&self, rem_size: Pixels) -> Pixels { - *self * rem_size + pub fn to_pixels(self, rem_size: Pixels) -> Pixels { + self * rem_size } } @@ -3168,9 +3168,9 @@ impl AbsoluteLength { /// assert_eq!(length_in_pixels.to_pixels(rem_size), Pixels(42.0)); /// assert_eq!(length_in_rems.to_pixels(rem_size), Pixels(32.0)); /// ``` - pub fn to_pixels(&self, rem_size: Pixels) -> Pixels { + pub fn to_pixels(self, rem_size: Pixels) -> Pixels { match self { - AbsoluteLength::Pixels(pixels) => *pixels, + AbsoluteLength::Pixels(pixels) => pixels, AbsoluteLength::Rems(rems) => rems.to_pixels(rem_size), } } @@ -3184,10 +3184,10 @@ impl AbsoluteLength { /// # Returns /// /// Returns the `AbsoluteLength` as `Pixels`. - pub fn to_rems(&self, rem_size: Pixels) -> Rems { + pub fn to_rems(self, rem_size: Pixels) -> Rems { match self { AbsoluteLength::Pixels(pixels) => Rems(pixels.0 / rem_size.0), - AbsoluteLength::Rems(rems) => *rems, + AbsoluteLength::Rems(rems) => rems, } } } @@ -3315,12 +3315,12 @@ impl DefiniteLength { /// assert_eq!(length_in_rems.to_pixels(base_size, rem_size), Pixels(32.0)); /// assert_eq!(length_as_fraction.to_pixels(base_size, rem_size), Pixels(50.0)); /// ``` - pub fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Pixels { + pub fn to_pixels(self, base_size: AbsoluteLength, rem_size: Pixels) -> Pixels { match self { DefiniteLength::Absolute(size) => size.to_pixels(rem_size), DefiniteLength::Fraction(fraction) => match base_size { - AbsoluteLength::Pixels(px) => px * *fraction, - AbsoluteLength::Rems(rems) => rems * rem_size * *fraction, + AbsoluteLength::Pixels(px) => px * fraction, + AbsoluteLength::Rems(rems) => rems * rem_size * fraction, }, } } diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index f0ce04a915..5e4b5fe6e9 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -172,6 +172,10 @@ pub trait AppContext { type Result<T>; /// Create a new entity in the app context. + #[expect( + clippy::wrong_self_convention, + reason = "`App::new` is an ubiquitous function for creating entities" + )] fn new<T: 'static>( &mut self, build_entity: impl FnOnce(&mut Context<T>) -> T, diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index 66f191ca5d..d007876590 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -364,29 +364,29 @@ mod tests { // Ensure `space` results in pending input on the workspace, but not editor let space_workspace = keymap.bindings_for_input(&[space()], &workspace_context()); assert!(space_workspace.0.is_empty()); - assert_eq!(space_workspace.1, true); + assert!(space_workspace.1); let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); - assert_eq!(space_editor.1, false); + assert!(!space_editor.1); // Ensure `space w` results in pending input on the workspace, but not editor let space_w_workspace = keymap.bindings_for_input(&space_w, &workspace_context()); assert!(space_w_workspace.0.is_empty()); - assert_eq!(space_w_workspace.1, true); + assert!(space_w_workspace.1); let space_w_editor = keymap.bindings_for_input(&space_w, &editor_workspace_context()); assert!(space_w_editor.0.is_empty()); - assert_eq!(space_w_editor.1, false); + assert!(!space_w_editor.1); // Ensure `space w w` results in the binding in the workspace, but not in the editor let space_w_w_workspace = keymap.bindings_for_input(&space_w_w, &workspace_context()); assert!(!space_w_w_workspace.0.is_empty()); - assert_eq!(space_w_w_workspace.1, false); + assert!(!space_w_w_workspace.1); let space_w_w_editor = keymap.bindings_for_input(&space_w_w, &editor_workspace_context()); assert!(space_w_w_editor.0.is_empty()); - assert_eq!(space_w_w_editor.1, false); + assert!(!space_w_w_editor.1); // Now test what happens if we have another binding defined AFTER the NoAction // that should result in pending @@ -400,7 +400,7 @@ mod tests { let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); - assert_eq!(space_editor.1, true); + assert!(space_editor.1); // Now test what happens if we have another binding defined BEFORE the NoAction // that should result in pending @@ -414,7 +414,7 @@ mod tests { let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); - assert_eq!(space_editor.1, true); + assert!(space_editor.1); // Now test what happens if we have another binding defined at a higher context // that should result in pending @@ -428,7 +428,7 @@ mod tests { let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); - assert_eq!(space_editor.1, true); + assert!(space_editor.1); } #[test] @@ -447,7 +447,7 @@ mod tests { &[KeyContext::parse("editor").unwrap()], ); assert!(result.is_empty()); - assert_eq!(pending, true); + assert!(pending); let bindings = [ KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")), @@ -463,7 +463,7 @@ mod tests { &[KeyContext::parse("editor").unwrap()], ); assert_eq!(result.len(), 1); - assert_eq!(pending, false); + assert!(!pending); } #[test] @@ -482,7 +482,7 @@ mod tests { &[KeyContext::parse("editor").unwrap()], ); assert!(result.is_empty()); - assert_eq!(pending, false); + assert!(!pending); } #[test] @@ -505,7 +505,7 @@ mod tests { ], ); assert_eq!(result.len(), 1); - assert_eq!(pending, false); + assert!(!pending); } #[test] @@ -527,7 +527,7 @@ mod tests { ], ); assert_eq!(result.len(), 0); - assert_eq!(pending, false); + assert!(!pending); } #[test] diff --git a/crates/gpui/src/keymap/binding.rs b/crates/gpui/src/keymap/binding.rs index 6d36cbb4e0..729498d153 100644 --- a/crates/gpui/src/keymap/binding.rs +++ b/crates/gpui/src/keymap/binding.rs @@ -30,11 +30,8 @@ impl Clone for KeyBinding { impl KeyBinding { /// Construct a new keybinding from the given data. Panics on parse error. pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self { - let context_predicate = if let Some(context) = context { - Some(KeyBindingContextPredicate::parse(context).unwrap().into()) - } else { - None - }; + let context_predicate = + context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into()); Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap() } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 1df8a608f4..4d2feeaf1d 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -673,7 +673,7 @@ impl PlatformTextSystem for NoopTextSystem { } } let mut runs = Vec::default(); - if glyphs.len() > 0 { + if !glyphs.is_empty() { runs.push(ShapedRun { font_id: FontId(0), glyphs, diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index ed824744a9..399411843b 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -667,7 +667,7 @@ pub(super) const DEFAULT_CURSOR_ICON_NAME: &str = "left_ptr"; impl CursorStyle { #[cfg(any(feature = "wayland", feature = "x11"))] - pub(super) fn to_icon_names(&self) -> &'static [&'static str] { + pub(super) fn to_icon_names(self) -> &'static [&'static str] { // Based on cursor names from chromium: // https://github.com/chromium/chromium/blob/d3069cf9c973dc3627fa75f64085c6a86c8f41bf/ui/base/cursor/cursor_factory.cc#L113 match self { @@ -990,21 +990,18 @@ mod tests { #[test] fn test_is_within_click_distance() { let zero = Point::new(px(0.0), px(0.0)); - assert_eq!( - is_within_click_distance(zero, Point::new(px(5.0), px(5.0))), - true - ); - assert_eq!( - is_within_click_distance(zero, Point::new(px(-4.9), px(5.0))), - true - ); - assert_eq!( - is_within_click_distance(Point::new(px(3.0), px(2.0)), Point::new(px(-2.0), px(-2.0))), - true - ); - assert_eq!( - is_within_click_distance(zero, Point::new(px(5.0), px(5.1))), - false - ); + assert!(is_within_click_distance(zero, Point::new(px(5.0), px(5.0)))); + assert!(is_within_click_distance( + zero, + Point::new(px(-4.9), px(5.0)) + )); + assert!(is_within_click_distance( + Point::new(px(3.0), px(2.0)), + Point::new(px(-2.0), px(-2.0)) + )); + assert!(!is_within_click_distance( + zero, + Point::new(px(5.0), px(5.1)) + ),); } } diff --git a/crates/gpui/src/platform/linux/wayland.rs b/crates/gpui/src/platform/linux/wayland.rs index cf73832b11..487bc9f38c 100644 --- a/crates/gpui/src/platform/linux/wayland.rs +++ b/crates/gpui/src/platform/linux/wayland.rs @@ -12,7 +12,7 @@ use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1:: use crate::CursorStyle; impl CursorStyle { - pub(super) fn to_shape(&self) -> Shape { + pub(super) fn to_shape(self) -> Shape { match self { CursorStyle::Arrow => Shape::Default, CursorStyle::IBeam => Shape::Text, diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index ce1468335d..7570c58c09 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -1139,7 +1139,7 @@ fn update_window(mut state: RefMut<WaylandWindowState>) { } impl WindowDecorations { - fn to_xdg(&self) -> zxdg_toplevel_decoration_v1::Mode { + fn to_xdg(self) -> zxdg_toplevel_decoration_v1::Mode { match self { WindowDecorations::Client => zxdg_toplevel_decoration_v1::Mode::ClientSide, WindowDecorations::Server => zxdg_toplevel_decoration_v1::Mode::ServerSide, @@ -1148,7 +1148,7 @@ impl WindowDecorations { } impl ResizeEdge { - fn to_xdg(&self) -> xdg_toplevel::ResizeEdge { + fn to_xdg(self) -> xdg_toplevel::ResizeEdge { match self { ResizeEdge::Top => xdg_toplevel::ResizeEdge::Top, ResizeEdge::TopRight => xdg_toplevel::ResizeEdge::TopRight, diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index c33d6fa462..6af943b317 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -95,7 +95,7 @@ fn query_render_extent( } impl ResizeEdge { - fn to_moveresize(&self) -> u32 { + fn to_moveresize(self) -> u32 { match self { ResizeEdge::TopLeft => 0, ResizeEdge::Top => 1, diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index cd923a1859..4425d4fe24 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1090,7 +1090,7 @@ impl PlatformWindow for MacWindow { NSView::removeFromSuperview(blur_view); this.blurred_view = None; } - } else if this.blurred_view == None { + } else if this.blurred_view.is_none() { let content_view = this.native_window.contentView(); let frame = NSView::bounds(content_view); let mut blur_view: id = msg_send![BLURRED_VIEW_CLASS, alloc]; diff --git a/crates/gpui/src/tab_stop.rs b/crates/gpui/src/tab_stop.rs index 30d24e85e7..c4d2fda6e9 100644 --- a/crates/gpui/src/tab_stop.rs +++ b/crates/gpui/src/tab_stop.rs @@ -45,27 +45,18 @@ impl TabHandles { }) .unwrap_or_default(); - if let Some(next_handle) = self.handles.get(next_ix) { - Some(next_handle.clone()) - } else { - None - } + self.handles.get(next_ix).cloned() } pub(crate) fn prev(&self, focused_id: Option<&FocusId>) -> Option<FocusHandle> { let ix = self.current_index(focused_id).unwrap_or_default(); - let prev_ix; - if ix == 0 { - prev_ix = self.handles.len().saturating_sub(1); + let prev_ix = if ix == 0 { + self.handles.len().saturating_sub(1) } else { - prev_ix = ix.saturating_sub(1); - } + ix.saturating_sub(1) + }; - if let Some(prev_handle) = self.handles.get(prev_ix) { - Some(prev_handle.clone()) - } else { - None - } + self.handles.get(prev_ix).cloned() } } diff --git a/crates/gpui_macros/src/gpui_macros.rs b/crates/gpui_macros/src/gpui_macros.rs index 3a58af6705..0f1365be77 100644 --- a/crates/gpui_macros/src/gpui_macros.rs +++ b/crates/gpui_macros/src/gpui_macros.rs @@ -172,7 +172,7 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream { /// - `#[gpui::test(iterations = 5)]` runs five times, providing as seed the values in the range `0..5`. /// - `#[gpui::test(retries = 3)]` runs up to four times if it fails to try and make it pass. /// - `#[gpui::test(on_failure = "crate::test::report_failure")]` will call the specified function after the -/// tests fail so that you can write out more detail about the failure. +/// tests fail so that you can write out more detail about the failure. /// /// You can combine `iterations = ...` with `seeds(...)`: /// - `#[gpui::test(iterations = 5, seed = 10)]` is equivalent to `#[gpui::test(seeds(0, 1, 2, 3, 4, 10))]`. diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 589fc68e99..be68dc1e9f 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -49,7 +49,7 @@ impl LanguageName { pub fn from_proto(s: String) -> Self { Self(SharedString::from(s)) } - pub fn to_proto(self) -> String { + pub fn to_proto(&self) -> String { self.0.to_string() } pub fn lsp_id(&self) -> String { diff --git a/crates/language_model/src/role.rs b/crates/language_model/src/role.rs index 953dfa6fdf..4b47ef36dd 100644 --- a/crates/language_model/src/role.rs +++ b/crates/language_model/src/role.rs @@ -19,7 +19,7 @@ impl Role { } } - pub fn to_proto(&self) -> proto::LanguageModelRole { + pub fn to_proto(self) -> proto::LanguageModelRole { match self { Role::User => proto::LanguageModelRole::LanguageModelUser, Role::Assistant => proto::LanguageModelRole::LanguageModelAssistant, diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index 88e3e12f02..2180a049d0 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -28,7 +28,7 @@ fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Opt let mut parser = tree_sitter::Parser::new(); parser.set_language(&tree_sitter_json::LANGUAGE.into())?; let syntax_tree = parser - .parse(&text, None) + .parse(text, None) .context("failed to parse settings")?; let mut cursor = tree_sitter::QueryCursor::new(); diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 162e3bea78..0cc2f654ea 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -7149,7 +7149,7 @@ impl ExcerptId { Self(usize::MAX) } - pub fn to_proto(&self) -> u64 { + pub fn to_proto(self) -> u64 { self.0 as _ } diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index 47a0f12c06..aab0354c96 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -41,7 +41,7 @@ pub fn remote_server_dir_relative() -> &'static Path { /// # Arguments /// /// * `dir` - The path to use as the custom data directory. This will be used as the base -/// directory for all user data, including databases, extensions, and logs. +/// directory for all user data, including databases, extensions, and logs. /// /// # Returns /// diff --git a/crates/project/src/debugger.rs b/crates/project/src/debugger.rs index 6c22468040..0bf6a0d61b 100644 --- a/crates/project/src/debugger.rs +++ b/crates/project/src/debugger.rs @@ -6,9 +6,9 @@ //! //! There are few reasons for this divide: //! - Breakpoints persist across debug sessions and they're not really specific to any particular session. Sure, we have to send protocol messages for them -//! (so they're a "thing" in the protocol), but we also want to set them before any session starts up. +//! (so they're a "thing" in the protocol), but we also want to set them before any session starts up. //! - Debug clients are doing the heavy lifting, and this is where UI grabs all of it's data from. They also rely on breakpoint store during initialization to obtain -//! current set of breakpoints. +//! current set of breakpoints. //! - Since DAP store knows about all of the available debug sessions, it is responsible for routing RPC requests to sessions. It also knows how to find adapters for particular kind of session. pub mod breakpoint_store; diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index 00fcc7e69f..343ee83ccb 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -904,7 +904,7 @@ impl BreakpointState { } #[inline] - pub fn to_int(&self) -> i32 { + pub fn to_int(self) -> i32 { match self { BreakpointState::Enabled => 0, BreakpointState::Disabled => 1, diff --git a/crates/project/src/debugger/memory.rs b/crates/project/src/debugger/memory.rs index a8729a8ff4..42ad64e688 100644 --- a/crates/project/src/debugger/memory.rs +++ b/crates/project/src/debugger/memory.rs @@ -3,6 +3,7 @@ //! Each byte in memory can either be mapped or unmapped. We try to mimic that twofold: //! - We assume that the memory is divided into pages of a fixed size. //! - We assume that each page can be either mapped or unmapped. +//! //! These two assumptions drive the shape of the memory representation. //! In particular, we want the unmapped pages to be represented without allocating any memory, as *most* //! of the memory in a program space is usually unmapped. @@ -165,8 +166,8 @@ impl Memory { /// - If it succeeds/fails wholesale, cool; we have no unknown memory regions in this page. /// - If it succeeds partially, we know # of mapped bytes. /// We might also know the # of unmapped bytes. -/// However, we're still unsure about what's *after* the unreadable region. /// +/// However, we're still unsure about what's *after* the unreadable region. /// This is where this builder comes in. It lets us track the state of figuring out contents of a single page. pub(super) struct MemoryPageBuilder { chunks: MappedPageContents, diff --git a/crates/project/src/git_store/conflict_set.rs b/crates/project/src/git_store/conflict_set.rs index 27b191f65f..9d7bd26a92 100644 --- a/crates/project/src/git_store/conflict_set.rs +++ b/crates/project/src/git_store/conflict_set.rs @@ -653,7 +653,7 @@ mod tests { cx.run_until_parked(); conflict_set.update(cx, |conflict_set, _| { - assert_eq!(conflict_set.has_conflict, false); + assert!(!conflict_set.has_conflict); assert_eq!(conflict_set.snapshot.conflicts.len(), 0); }); diff --git a/crates/project/src/git_store/git_traversal.rs b/crates/project/src/git_store/git_traversal.rs index 4594e8d140..9eadaeac82 100644 --- a/crates/project/src/git_store/git_traversal.rs +++ b/crates/project/src/git_store/git_traversal.rs @@ -199,7 +199,7 @@ pub struct GitEntryRef<'a> { } impl GitEntryRef<'_> { - pub fn to_owned(&self) -> GitEntry { + pub fn to_owned(self) -> GitEntry { GitEntry { entry: self.entry.clone(), git_summary: self.git_summary, diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 04b14ae06e..aa2398e29b 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -5,7 +5,7 @@ //! This module is split up into three distinct parts: //! - [`LocalLspStore`], which is ran on the host machine (either project host or SSH host), that manages the lifecycle of language servers. //! - [`RemoteLspStore`], which is ran on the remote machine (project guests) which is mostly about passing through the requests via RPC. -//! The remote stores don't really care about which language server they're running against - they don't usually get to decide which language server is going to responsible for handling their request. +//! The remote stores don't really care about which language server they're running against - they don't usually get to decide which language server is going to responsible for handling their request. //! - [`LspStore`], which unifies the two under one consistent interface for interacting with language servers. //! //! Most of the interesting work happens at the local layer, as bulk of the complexity is with managing the lifecycle of language servers. The actual implementation of the LSP protocol is handled by [`lsp`] crate. @@ -12691,7 +12691,7 @@ impl DiagnosticSummary { } pub fn to_proto( - &self, + self, language_server_id: LanguageServerId, path: &Path, ) -> proto::DiagnosticSummary { diff --git a/crates/project/src/manifest_tree/path_trie.rs b/crates/project/src/manifest_tree/path_trie.rs index 16110463ac..9cebfda25c 100644 --- a/crates/project/src/manifest_tree/path_trie.rs +++ b/crates/project/src/manifest_tree/path_trie.rs @@ -22,9 +22,9 @@ pub(super) struct RootPathTrie<Label> { /// Label presence is a marker that allows to optimize searches within [RootPathTrie]; node label can be: /// - Present; we know there's definitely a project root at this node. /// - Known Absent - we know there's definitely no project root at this node and none of it's ancestors are Present (descendants can be present though!). -/// The distinction is there to optimize searching; when we encounter a node with unknown status, we don't need to look at it's full path -/// to the root of the worktree; it's sufficient to explore only the path between last node with a KnownAbsent state and the directory of a path, since we run searches -/// from the leaf up to the root of the worktree. +/// The distinction is there to optimize searching; when we encounter a node with unknown status, we don't need to look at it's full path +/// to the root of the worktree; it's sufficient to explore only the path between last node with a KnownAbsent state and the directory of a path, since we run searches +/// from the leaf up to the root of the worktree. /// /// In practical terms, it means that by storing label presence we don't need to do a project discovery on a given folder more than once /// (unless the node is invalidated, which can happen when FS entries are renamed/removed). diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f07ee13866..9cd83647ac 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4329,7 +4329,7 @@ impl Project { /// # Arguments /// /// * `path` - A full path that starts with a worktree root name, or alternatively a - /// relative path within a visible worktree. + /// relative path within a visible worktree. /// * `cx` - A reference to the `AppContext`. /// /// # Returns @@ -5508,7 +5508,7 @@ mod disable_ai_settings_tests { project: &[], }; let settings = DisableAiSettings::load(sources, cx).unwrap(); - assert_eq!(settings.disable_ai, false, "Default should allow AI"); + assert!(!settings.disable_ai, "Default should allow AI"); // Test 2: Global true, local false -> still disabled (local cannot re-enable) let global_true = Some(true); @@ -5525,8 +5525,8 @@ mod disable_ai_settings_tests { project: &[&local_false], }; let settings = DisableAiSettings::load(sources, cx).unwrap(); - assert_eq!( - settings.disable_ai, true, + assert!( + settings.disable_ai, "Local false cannot override global true" ); @@ -5545,10 +5545,7 @@ mod disable_ai_settings_tests { project: &[&local_true], }; let settings = DisableAiSettings::load(sources, cx).unwrap(); - assert_eq!( - settings.disable_ai, true, - "Local true can override global false" - ); + assert!(settings.disable_ai, "Local true can override global false"); // Test 4: Server can only make more restrictive (set to true) let user_false = Some(false); @@ -5565,8 +5562,8 @@ mod disable_ai_settings_tests { project: &[], }; let settings = DisableAiSettings::load(sources, cx).unwrap(); - assert_eq!( - settings.disable_ai, true, + assert!( + settings.disable_ai, "Server can set to true even if user is false" ); @@ -5585,8 +5582,8 @@ mod disable_ai_settings_tests { project: &[], }; let settings = DisableAiSettings::load(sources, cx).unwrap(); - assert_eq!( - settings.disable_ai, true, + assert!( + settings.disable_ai, "Server false cannot override user true" ); @@ -5607,10 +5604,7 @@ mod disable_ai_settings_tests { project: &[&local_false3, &local_true2, &local_false4], }; let settings = DisableAiSettings::load(sources, cx).unwrap(); - assert_eq!( - settings.disable_ai, true, - "Any local true should disable AI" - ); + assert!(settings.disable_ai, "Any local true should disable AI"); // Test 7: All three sources can independently disable AI let user_false2 = Some(false); @@ -5628,8 +5622,8 @@ mod disable_ai_settings_tests { project: &[&local_true3], }; let settings = DisableAiSettings::load(sources, cx).unwrap(); - assert_eq!( - settings.disable_ai, true, + assert!( + settings.disable_ai, "Local can disable even if user and server are false" ); }); diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index eb1e3828e9..70eb6d34f8 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -4123,7 +4123,7 @@ async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) { }) .unwrap() .await - .to_included() + .into_included() .unwrap(); cx.executor().run_until_parked(); @@ -5918,7 +5918,7 @@ async fn test_create_entry(cx: &mut gpui::TestAppContext) { }) .await .unwrap() - .to_included() + .into_included() .unwrap(); // Can't create paths outside the project diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index dc92ee8c70..bb612ac475 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -2515,7 +2515,7 @@ impl ProjectPanel { if clip_is_cut { // Convert the clipboard cut entry to a copy entry after the first paste. - self.clipboard = self.clipboard.take().map(ClipboardEntry::to_copy_entry); + self.clipboard = self.clipboard.take().map(ClipboardEntry::into_copy_entry); } self.expand_entry(worktree_id, entry.id, cx); @@ -5709,7 +5709,7 @@ impl ClipboardEntry { } } - fn to_copy_entry(self) -> Self { + fn into_copy_entry(self) -> Self { match self { ClipboardEntry::Copied(_) => self, ClipboardEntry::Cut(entries) => ClipboardEntry::Copied(entries), diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 7173bc9b3b..fddf47660d 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -1452,7 +1452,7 @@ impl RemoteConnection for SshRemoteConnection { .arg(format!( "{}:{}", self.socket.connection_options.scp_url(), - dest_path.to_string() + dest_path )) .output(); @@ -1836,11 +1836,7 @@ impl SshRemoteConnection { })??; let tmp_path_gz = RemotePathBuf::new( - PathBuf::from(format!( - "{}-download-{}.gz", - dst_path.to_string(), - std::process::id() - )), + PathBuf::from(format!("{}-download-{}.gz", dst_path, std::process::id())), self.ssh_path_style, ); if !self.socket.connection_options.upload_binary_over_ssh @@ -2036,7 +2032,7 @@ impl SshRemoteConnection { .arg(format!( "{}:{}", self.socket.connection_options.scp_url(), - dest_path.to_string() + dest_path )) .output() .await?; diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 514e5ce4c0..69fae7f399 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -1207,7 +1207,7 @@ async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestA }) .await .unwrap() - .to_included() + .into_included() .unwrap(); cx.run_until_parked(); diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 3352b317cb..4ce133cbb1 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -84,7 +84,7 @@ fn init_logging_server(log_file_path: PathBuf) -> Result<Receiver<Vec<u8>>> { fn flush(&mut self) -> std::io::Result<()> { self.channel .send_blocking(self.buffer.clone()) - .map_err(|error| std::io::Error::new(std::io::ErrorKind::Other, error))?; + .map_err(std::io::Error::other)?; self.buffer.clear(); self.file.flush() } diff --git a/crates/repl/src/kernels/mod.rs b/crates/repl/src/kernels/mod.rs index 3c3b766612..52188a39c4 100644 --- a/crates/repl/src/kernels/mod.rs +++ b/crates/repl/src/kernels/mod.rs @@ -169,10 +169,7 @@ pub enum KernelStatus { impl KernelStatus { pub fn is_connected(&self) -> bool { - match self { - KernelStatus::Idle | KernelStatus::Busy => true, - _ => false, - } + matches!(self, KernelStatus::Idle | KernelStatus::Busy) } } diff --git a/crates/reqwest_client/src/reqwest_client.rs b/crates/reqwest_client/src/reqwest_client.rs index 9053f4e452..d0d25bdf25 100644 --- a/crates/reqwest_client/src/reqwest_client.rs +++ b/crates/reqwest_client/src/reqwest_client.rs @@ -264,7 +264,7 @@ impl http_client::HttpClient for ReqwestClient { let bytes = response .bytes_stream() - .map_err(|e| futures::io::Error::new(futures::io::ErrorKind::Other, e)) + .map_err(futures::io::Error::other) .into_async_read(); let body = http_client::AsyncBody::from_reader(bytes); diff --git a/crates/rope/src/chunk.rs b/crates/rope/src/chunk.rs index e3c7d6f750..379daa4224 100644 --- a/crates/rope/src/chunk.rs +++ b/crates/rope/src/chunk.rs @@ -92,7 +92,7 @@ impl Into<Chunk> for ChunkSlice<'_> { impl<'a> ChunkSlice<'a> { #[inline(always)] - pub fn is_empty(self) -> bool { + pub fn is_empty(&self) -> bool { self.text.is_empty() } diff --git a/crates/rpc/src/conn.rs b/crates/rpc/src/conn.rs index 0a41570fcc..78db80e398 100644 --- a/crates/rpc/src/conn.rs +++ b/crates/rpc/src/conn.rs @@ -56,7 +56,7 @@ impl Connection { ) { use anyhow::anyhow; use futures::channel::mpsc; - use std::io::{Error, ErrorKind}; + use std::io::Error; let (tx, rx) = mpsc::unbounded::<WebSocketMessage>(); @@ -71,7 +71,7 @@ impl Connection { // Writes to a half-open TCP connection will error. if killed.load(SeqCst) { - std::io::Result::Err(Error::new(ErrorKind::Other, "connection lost"))?; + std::io::Result::Err(Error::other("connection lost"))?; } Ok(msg) diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 1afbc2c23b..65e59fd5de 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -116,8 +116,8 @@ impl SearchOption { } } - pub fn to_toggle_action(&self) -> &'static dyn Action { - match *self { + pub fn to_toggle_action(self) -> &'static dyn Action { + match self { SearchOption::WholeWord => &ToggleWholeWord, SearchOption::CaseSensitive => &ToggleCaseSensitive, SearchOption::IncludeIgnored => &ToggleIncludeIgnored, diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index afd4ea0890..b73ab9ae95 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -50,11 +50,11 @@ impl WorktreeId { Self(id as usize) } - pub fn to_proto(&self) -> u64 { + pub fn to_proto(self) -> u64 { self.0 as u64 } - pub fn to_usize(&self) -> usize { + pub fn to_usize(self) -> usize { self.0 } } diff --git a/crates/task/src/shell_builder.rs b/crates/task/src/shell_builder.rs index 5ed29fd733..770312bafc 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -28,7 +28,7 @@ impl ShellKind { } } - fn to_shell_variable(&self, input: &str) -> String { + fn to_shell_variable(self, input: &str) -> String { match self { Self::Powershell => Self::to_powershell_variable(input), Self::Cmd => Self::to_cmd_variable(input), diff --git a/crates/terminal_view/src/terminal_slash_command.rs b/crates/terminal_view/src/terminal_slash_command.rs index ac86eef2bc..13c2cef48c 100644 --- a/crates/terminal_view/src/terminal_slash_command.rs +++ b/crates/terminal_view/src/terminal_slash_command.rs @@ -104,7 +104,7 @@ impl SlashCommand for TerminalSlashCommand { }], run_commands_in_text: false, } - .to_event_stream())) + .into_event_stream())) } } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 956bcebfd0..0c16e3fb9d 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -2192,7 +2192,7 @@ mod tests { }) .await .unwrap() - .to_included() + .into_included() .unwrap(); (wt, entry) diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index 31bf76e843..477fc57b22 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -582,13 +582,9 @@ impl RenderOnce for ButtonLike { .when_some(self.width, |this, width| { this.w(width).justify_center().text_center() }) - .when( - match self.style { - ButtonStyle::Outlined => true, - _ => false, - }, - |this| this.border_1(), - ) + .when(matches!(self.style, ButtonStyle::Outlined), |this| { + this.border_1() + }) .when_some(self.rounding, |this, rounding| match rounding { ButtonLikeRounding::All => this.rounded_sm(), ButtonLikeRounding::Left => this.rounded_l_sm(), diff --git a/crates/ui/src/utils/format_distance.rs b/crates/ui/src/utils/format_distance.rs index 213d9c8b4c..a8f27f01da 100644 --- a/crates/ui/src/utils/format_distance.rs +++ b/crates/ui/src/utils/format_distance.rs @@ -13,9 +13,9 @@ impl DateTimeType { /// /// If the [`DateTimeType`] is already a [`NaiveDateTime`], it will be returned as is. /// If the [`DateTimeType`] is a [`DateTime<Local>`], it will be converted to a [`NaiveDateTime`]. - pub fn to_naive(&self) -> NaiveDateTime { + pub fn to_naive(self) -> NaiveDateTime { match self { - DateTimeType::Naive(naive) => *naive, + DateTimeType::Naive(naive) => naive, DateTimeType::Local(local) => local.naive_local(), } } diff --git a/crates/util/src/archive.rs b/crates/util/src/archive.rs index 3e4d281c29..9b58b16bed 100644 --- a/crates/util/src/archive.rs +++ b/crates/util/src/archive.rs @@ -154,7 +154,7 @@ mod tests { let mut builder = ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate); use std::os::unix::fs::PermissionsExt; - let metadata = std::fs::metadata(&path)?; + let metadata = std::fs::metadata(path)?; let perms = metadata.permissions().mode() as u16; builder = builder.unix_permissions(perms); writer.write_entry_whole(builder, &data).await?; diff --git a/crates/util/src/markdown.rs b/crates/util/src/markdown.rs index 7e66ed7bae..303dbe0cf5 100644 --- a/crates/util/src/markdown.rs +++ b/crates/util/src/markdown.rs @@ -23,7 +23,7 @@ impl Display for MarkdownString { /// the other characters involved are escaped: /// /// * `!`, `]`, `(`, and `)` are used in link syntax, but `[` is escaped so these are parsed as -/// plaintext. +/// plaintext. /// /// * `;` is used in HTML entity syntax, but `&` is escaped, so they are parsed as plaintext. /// diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 292ec4874c..b430120314 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -2,6 +2,7 @@ use globset::{Glob, GlobSet, GlobSetBuilder}; use regex::Regex; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; +use std::fmt::{Display, Formatter}; use std::path::StripPrefixError; use std::sync::{Arc, OnceLock}; use std::{ @@ -113,10 +114,6 @@ impl SanitizedPath { &self.0 } - pub fn to_string(&self) -> String { - self.0.to_string_lossy().to_string() - } - pub fn to_glob_string(&self) -> String { #[cfg(target_os = "windows")] { @@ -137,6 +134,12 @@ impl SanitizedPath { } } +impl Display for SanitizedPath { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.display()) + } +} + impl From<SanitizedPath> for Arc<Path> { fn from(sanitized_path: SanitizedPath) -> Self { sanitized_path.0 @@ -220,12 +223,8 @@ impl RemotePathBuf { Self::new(path_buf, style) } - pub fn to_string(&self) -> String { - self.string.clone() - } - #[cfg(target_os = "windows")] - pub fn to_proto(self) -> String { + pub fn to_proto(&self) -> String { match self.path_style() { PathStyle::Posix => self.to_string(), PathStyle::Windows => self.inner.to_string_lossy().replace('\\', "/"), @@ -233,7 +232,7 @@ impl RemotePathBuf { } #[cfg(not(target_os = "windows"))] - pub fn to_proto(self) -> String { + pub fn to_proto(&self) -> String { match self.path_style() { PathStyle::Posix => self.inner.to_string_lossy().to_string(), PathStyle::Windows => self.to_string(), @@ -255,6 +254,12 @@ impl RemotePathBuf { } } +impl Display for RemotePathBuf { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.string) + } +} + /// A delimiter to use in `path_query:row_number:column_number` strings parsing. pub const FILE_ROW_COLUMN_DELIMITER: char = ':'; diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 92e3c97265..350ffd666b 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -816,10 +816,7 @@ impl Motion { } fn skip_exclusive_special_case(&self) -> bool { - match self { - Motion::WrappingLeft | Motion::WrappingRight => true, - _ => false, - } + matches!(self, Motion::WrappingLeft | Motion::WrappingRight) } pub(crate) fn push_to_jump_list(&self) -> bool { @@ -4099,7 +4096,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" ˇhe quick brown fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" the quick bˇrown fox @@ -4109,7 +4106,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" ˇown fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" the quick brown foˇx @@ -4119,7 +4116,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" ˇ jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); } #[gpui::test] @@ -4134,7 +4131,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" ˇbrown fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" the quick bˇrown fox @@ -4144,7 +4141,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" the quickˇown fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" the quick brown foˇx @@ -4154,7 +4151,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" the quicˇk jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" ˇthe quick brown fox @@ -4164,7 +4161,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" ˇ fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" ˇthe quick brown fox @@ -4174,7 +4171,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" ˇuick brown fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); } #[gpui::test] @@ -4189,7 +4186,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" the quick brown foˇx jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" ˇthe quick brown fox @@ -4199,7 +4196,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" ˇx jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); } #[gpui::test] @@ -4215,7 +4212,7 @@ mod test { the quick brown fox ˇthe quick brown fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" the quick bˇrown fox @@ -4226,7 +4223,7 @@ mod test { the quick brˇrown fox jumped overown fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" the quick brown foˇx @@ -4237,7 +4234,7 @@ mod test { the quick brown foxˇx jumped over the la jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" the quick brown fox @@ -4248,7 +4245,7 @@ mod test { thˇhe quick brown fox je quick brown fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); } #[gpui::test] @@ -4263,7 +4260,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" ˇe quick brown fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" the quick bˇrown fox @@ -4273,7 +4270,7 @@ mod test { cx.shared_state().await.assert_eq(indoc! {" the quick bˇn fox jumped over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); cx.set_shared_state(indoc! {" the quick brown foˇx @@ -4282,6 +4279,6 @@ mod test { cx.simulate_shared_keystrokes("d v e").await; cx.shared_state().await.assert_eq(indoc! {" the quick brown foˇd over the lazy dog"}); - assert_eq!(cx.cx.forced_motion(), false); + assert!(!cx.cx.forced_motion()); } } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 81c1a6b0b3..11d6d89bac 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1734,10 +1734,7 @@ impl Vim { editor.set_autoindent(vim.should_autoindent()); editor.selections.line_mode = matches!(vim.mode, Mode::VisualLine); - let hide_edit_predictions = match vim.mode { - Mode::Insert | Mode::Replace => false, - _ => true, - }; + let hide_edit_predictions = !matches!(vim.mode, Mode::Insert | Mode::Replace); editor.set_edit_predictions_hidden_for_vim_mode(hide_edit_predictions, window, cx); }); cx.notify() diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index d38f3cac3d..b12fd13767 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -5509,7 +5509,7 @@ impl ProjectEntryId { Self(id as usize) } - pub fn to_proto(&self) -> u64 { + pub fn to_proto(self) -> u64 { self.0 as u64 } @@ -5517,14 +5517,14 @@ impl ProjectEntryId { ProjectEntryId(id) } - pub fn to_usize(&self) -> usize { + pub fn to_usize(self) -> usize { self.0 } } #[cfg(any(test, feature = "test-support"))] impl CreatedEntry { - pub fn to_included(self) -> Option<Entry> { + pub fn into_included(self) -> Option<Entry> { match self { CreatedEntry::Included(entry) => Some(entry), CreatedEntry::Excluded { .. } => None, diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index d4c309e5bc..ca9debb647 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -1274,7 +1274,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { }) .await .unwrap() - .to_included() + .into_included() .unwrap(); assert!(entry.is_dir()); @@ -1323,7 +1323,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { }) .await .unwrap() - .to_included() + .into_included() .unwrap(); assert!(entry.is_file()); @@ -1357,7 +1357,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { }) .await .unwrap() - .to_included() + .into_included() .unwrap(); assert!(entry.is_file()); @@ -1377,7 +1377,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { }) .await .unwrap() - .to_included() + .into_included() .unwrap(); assert!(entry.is_file()); @@ -1395,7 +1395,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { }) .await .unwrap() - .to_included() + .into_included() .unwrap(); assert!(entry.is_file()); @@ -1726,7 +1726,7 @@ fn randomly_mutate_worktree( ); let task = worktree.rename_entry(entry.id, new_path, cx); cx.background_spawn(async move { - task.await?.to_included().unwrap(); + task.await?.into_included().unwrap(); Ok(()) }) } diff --git a/crates/x_ai/src/x_ai.rs b/crates/x_ai/src/x_ai.rs index 23cd5b9320..569503784c 100644 --- a/crates/x_ai/src/x_ai.rs +++ b/crates/x_ai/src/x_ai.rs @@ -122,9 +122,6 @@ impl Model { } pub fn supports_images(&self) -> bool { - match self { - Self::Grok2Vision => true, - _ => false, - } + matches!(self, Self::Grok2Vision) } } From 7bdc99abc15327db75baab28957cba7e7e9fa122 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Wed, 20 Aug 2025 13:20:13 +0300 Subject: [PATCH 523/693] Fix `clippy::redundant_clone` lint violations (#36558) This removes around 900 unnecessary clones, ranging from cloning a few ints all the way to large data structures and images. A lot of these were fixed using `cargo clippy --fix --workspace --all-targets`, however it often breaks other lints and needs to be run again. This was then followed up with some manual fixing. I understand this is a large diff, but all the changes are pretty trivial. Rust is doing some heavy lifting here for us. Once I get it up to speed with main, I'd appreciate this getting merged rather sooner than later. Release Notes: - N/A --- Cargo.toml | 1 + crates/acp_thread/src/acp_thread.rs | 4 +- crates/acp_thread/src/diff.rs | 3 +- crates/action_log/src/action_log.rs | 10 +-- crates/agent/src/agent_profile.rs | 6 +- crates/agent/src/context_server_tool.rs | 6 +- crates/agent/src/thread.rs | 16 ++-- crates/agent2/src/agent.rs | 6 +- crates/agent2/src/db.rs | 6 +- crates/agent2/src/tests/mod.rs | 4 +- crates/agent2/src/thread.rs | 2 +- .../src/tools/context_server_registry.rs | 6 +- crates/agent2/src/tools/edit_file_tool.rs | 2 +- crates/agent2/src/tools/grep_tool.rs | 8 +- crates/agent2/src/tools/terminal_tool.rs | 2 +- crates/agent_servers/src/claude.rs | 2 +- crates/agent_servers/src/claude/tools.rs | 2 +- .../agent_ui/src/acp/completion_provider.rs | 38 ++++---- crates/agent_ui/src/acp/message_editor.rs | 19 ++-- crates/agent_ui/src/acp/thread_view.rs | 12 ++- crates/agent_ui/src/active_thread.rs | 12 ++- crates/agent_ui/src/agent_configuration.rs | 10 +-- .../configure_context_server_modal.rs | 1 - .../manage_profiles_modal.rs | 2 +- crates/agent_ui/src/agent_diff.rs | 10 +-- crates/agent_ui/src/agent_model_selector.rs | 6 +- crates/agent_ui/src/agent_panel.rs | 24 ++--- crates/agent_ui/src/agent_ui.rs | 8 +- crates/agent_ui/src/buffer_codegen.rs | 12 +-- crates/agent_ui/src/context_picker.rs | 9 +- .../src/context_picker/completion_provider.rs | 41 ++++----- crates/agent_ui/src/inline_assistant.rs | 2 +- .../agent_ui/src/language_model_selector.rs | 4 +- crates/agent_ui/src/message_editor.rs | 5 +- crates/agent_ui/src/profile_selector.rs | 4 +- crates/agent_ui/src/slash_command.rs | 4 +- .../agent_ui/src/terminal_inline_assistant.rs | 2 +- crates/agent_ui/src/text_thread_editor.rs | 8 +- crates/agent_ui/src/ui/context_pill.rs | 4 +- .../src/agent_api_keys_onboarding.rs | 2 +- .../src/agent_panel_onboarding_content.rs | 2 +- .../src/assistant_context.rs | 2 +- .../src/assistant_context_tests.rs | 4 +- crates/assistant_context/src/context_store.rs | 2 +- .../src/diagnostics_command.rs | 2 +- .../src/prompt_command.rs | 2 +- .../assistant_tools/src/edit_agent/evals.rs | 7 +- .../src/edit_agent/streaming_fuzzy_matcher.rs | 16 ++-- crates/assistant_tools/src/edit_file_tool.rs | 6 +- crates/assistant_tools/src/grep_tool.rs | 6 +- crates/assistant_tools/src/terminal_tool.rs | 2 +- .../src/ui/tool_call_card_header.rs | 9 +- crates/assistant_tools/src/web_search_tool.rs | 5 +- crates/auto_update_ui/src/auto_update_ui.rs | 2 +- crates/buffer_diff/src/buffer_diff.rs | 12 +-- crates/channel/src/channel_store_tests.rs | 2 +- crates/cli/src/main.rs | 2 +- crates/client/src/client.rs | 9 +- crates/client/src/telemetry.rs | 8 +- crates/collab/src/api/events.rs | 2 +- crates/collab/src/auth.rs | 2 +- crates/collab/src/db/tests/embedding_tests.rs | 4 +- crates/collab/src/rpc.rs | 10 +-- crates/collab/src/tests/editor_tests.rs | 18 +--- .../src/tests/random_channel_buffer_tests.rs | 2 +- .../random_project_collaboration_tests.rs | 2 +- crates/collab/src/tests/test_server.rs | 4 +- crates/collab_ui/src/channel_view.rs | 2 +- crates/collab_ui/src/chat_panel.rs | 2 +- crates/collab_ui/src/collab_panel.rs | 6 +- crates/collab_ui/src/notification_panel.rs | 2 +- crates/command_palette/src/command_palette.rs | 2 +- crates/component/src/component_layout.rs | 2 +- crates/context_server/src/listener.rs | 1 - crates/copilot/src/copilot.rs | 5 +- .../src/copilot_completion_provider.rs | 2 +- crates/debugger_tools/src/dap_log.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 10 +-- crates/debugger_ui/src/debugger_ui.rs | 6 +- crates/debugger_ui/src/dropdown_menus.rs | 3 +- crates/debugger_ui/src/new_process_modal.rs | 2 +- crates/debugger_ui/src/persistence.rs | 2 +- crates/debugger_ui/src/session/running.rs | 8 +- .../src/session/running/breakpoint_list.rs | 2 - .../src/session/running/console.rs | 2 +- .../src/session/running/loaded_source_list.rs | 2 +- .../src/session/running/memory_view.rs | 2 +- .../src/session/running/module_list.rs | 2 +- .../src/session/running/stack_frame_list.rs | 6 +- .../src/session/running/variable_list.rs | 5 +- .../debugger_ui/src/tests/debugger_panel.rs | 5 +- .../src/tests/new_process_modal.rs | 4 +- crates/docs_preprocessor/src/main.rs | 6 +- .../src/edit_prediction_button.rs | 2 +- crates/editor/src/code_context_menus.rs | 2 +- crates/editor/src/display_map/block_map.rs | 22 +++-- crates/editor/src/display_map/fold_map.rs | 12 +-- crates/editor/src/display_map/tab_map.rs | 14 +-- crates/editor/src/editor.rs | 29 +++--- crates/editor/src/editor_settings_controls.rs | 2 +- crates/editor/src/editor_tests.rs | 81 +++++++---------- crates/editor/src/element.rs | 6 +- crates/editor/src/hover_popover.rs | 4 +- crates/editor/src/jsx_tag_auto_close.rs | 2 +- crates/editor/src/test/editor_test_context.rs | 8 +- crates/eval/src/eval.rs | 10 +-- crates/eval/src/instance.rs | 9 +- .../extension_host/src/capability_granter.rs | 2 +- .../src/components/feature_upsell.rs | 1 - crates/extensions_ui/src/extensions_ui.rs | 4 +- crates/feedback/src/system_specs.rs | 2 +- crates/file_finder/src/file_finder.rs | 2 +- crates/file_finder/src/open_path_prompt.rs | 6 +- crates/fs/src/fake_git_repo.rs | 2 +- crates/fs/src/fs.rs | 6 +- crates/fs/src/fs_watcher.rs | 2 +- crates/git_ui/src/blame_ui.rs | 6 +- crates/git_ui/src/branch_picker.rs | 4 +- crates/git_ui/src/commit_modal.rs | 8 +- crates/git_ui/src/commit_tooltip.rs | 2 +- crates/git_ui/src/conflict_view.rs | 5 +- crates/git_ui/src/git_panel.rs | 36 ++++---- crates/git_ui/src/git_ui.rs | 17 ++-- crates/git_ui/src/project_diff.rs | 8 +- crates/git_ui/src/text_diff_view.rs | 4 +- crates/go_to_line/src/go_to_line.rs | 2 +- crates/gpui/examples/input.rs | 4 +- crates/gpui/examples/text.rs | 2 +- crates/gpui/src/app/async_context.rs | 2 +- crates/gpui/src/app/entity_map.rs | 3 +- crates/gpui/src/app/test_context.rs | 8 +- crates/gpui/src/elements/img.rs | 2 +- crates/gpui/src/geometry.rs | 14 +-- crates/gpui/src/keymap.rs | 32 +++---- crates/gpui/src/keymap/context.rs | 10 +-- crates/gpui/src/platform/linux/platform.rs | 4 +- .../gpui/src/platform/linux/wayland/client.rs | 1 - .../gpui/src/platform/linux/wayland/cursor.rs | 2 +- crates/gpui/src/platform/linux/x11/client.rs | 2 +- .../gpui/src/platform/mac/metal_renderer.rs | 2 +- crates/gpui/src/platform/test/platform.rs | 4 +- crates/gpui/src/platform/windows/events.rs | 2 +- crates/gpui/src/shared_string.rs | 2 +- crates/gpui/src/window.rs | 10 +-- .../tests/derive_inspector_reflection.rs | 4 +- crates/http_client/src/async_body.rs | 2 +- crates/journal/src/journal.rs | 2 +- crates/language/src/buffer.rs | 17 ++-- crates/language/src/buffer_tests.rs | 10 +-- crates/language/src/language.rs | 2 +- crates/language/src/language_registry.rs | 7 +- crates/language/src/language_settings.rs | 4 +- crates/language/src/syntax_map.rs | 2 +- .../src/syntax_map/syntax_map_tests.rs | 15 ++-- crates/language/src/text_diff.rs | 10 +-- crates/language_model/src/language_model.rs | 4 +- .../language_model/src/model/cloud_model.rs | 2 +- crates/language_models/src/language_models.rs | 2 +- .../language_models/src/provider/bedrock.rs | 15 +--- crates/language_models/src/provider/cloud.rs | 6 +- crates/language_models/src/provider/google.rs | 2 +- .../language_models/src/provider/lmstudio.rs | 2 +- crates/language_models/src/provider/ollama.rs | 2 +- .../src/ui/instruction_list_item.rs | 2 +- crates/language_tools/src/lsp_log.rs | 11 +-- crates/language_tools/src/syntax_tree_view.rs | 2 +- crates/languages/src/c.rs | 10 +-- crates/languages/src/go.rs | 8 +- crates/languages/src/json.rs | 2 +- crates/languages/src/lib.rs | 24 ++--- crates/languages/src/python.rs | 6 +- crates/languages/src/rust.rs | 8 +- crates/languages/src/tailwind.rs | 2 +- crates/languages/src/typescript.rs | 4 +- crates/languages/src/vtsls.rs | 2 +- crates/languages/src/yaml.rs | 2 +- crates/livekit_client/examples/test_app.rs | 2 +- .../src/livekit_client/playback.rs | 5 +- crates/markdown/examples/markdown_as_child.rs | 2 +- crates/markdown/src/markdown.rs | 3 +- .../markdown_preview/src/markdown_parser.rs | 5 +- .../src/markdown_preview_view.rs | 3 +- .../src/migrations/m_2025_01_02/settings.rs | 4 +- .../src/migrations/m_2025_01_29/keymap.rs | 2 +- .../src/migrations/m_2025_01_29/settings.rs | 2 +- .../src/migrations/m_2025_01_30/settings.rs | 6 +- .../src/migrations/m_2025_03_29/settings.rs | 2 +- .../src/migrations/m_2025_05_29/settings.rs | 4 +- crates/multi_buffer/src/multi_buffer.rs | 6 +- crates/onboarding/src/basics_page.rs | 4 +- crates/onboarding/src/editing_page.rs | 8 +- crates/onboarding/src/theme_preview.rs | 12 +-- crates/outline_panel/src/outline_panel.rs | 2 +- crates/panel/src/panel.rs | 2 +- crates/picker/src/popover_menu.rs | 2 +- crates/project/src/buffer_store.rs | 8 +- crates/project/src/context_server_store.rs | 2 +- .../project/src/debugger/breakpoint_store.rs | 16 ++-- crates/project/src/debugger/dap_command.rs | 2 +- crates/project/src/debugger/dap_store.rs | 2 +- crates/project/src/debugger/session.rs | 9 +- crates/project/src/git_store.rs | 19 ++-- crates/project/src/git_store/conflict_set.rs | 4 +- crates/project/src/image_store.rs | 3 +- crates/project/src/lsp_command.rs | 9 +- crates/project/src/lsp_store.rs | 13 ++- crates/project/src/lsp_store/clangd_ext.rs | 2 +- .../src/lsp_store/rust_analyzer_ext.rs | 1 - crates/project/src/project.rs | 6 +- crates/project/src/project_tests.rs | 10 +-- crates/project/src/task_inventory.rs | 4 +- crates/project/src/terminals.rs | 2 +- crates/project/src/worktree_store.rs | 2 +- crates/project_panel/src/project_panel.rs | 17 ++-- .../project_panel/src/project_panel_tests.rs | 90 +++++++++---------- crates/project_symbols/src/project_symbols.rs | 8 +- crates/prompt_store/src/prompts.rs | 2 +- crates/recent_projects/src/remote_servers.rs | 10 +-- crates/recent_projects/src/ssh_connections.rs | 2 +- crates/remote/src/ssh_session.rs | 6 +- crates/remote_server/src/headless_project.rs | 4 +- crates/remote_server/src/unix.rs | 3 +- crates/repl/src/components/kernel_options.rs | 2 +- crates/repl/src/kernels/remote_kernels.rs | 4 +- crates/repl/src/outputs.rs | 19 ++-- crates/repl/src/outputs/markdown.rs | 2 +- crates/repl/src/repl_editor.rs | 12 +-- crates/repl/src/repl_sessions_ui.rs | 1 - crates/repl/src/session.rs | 5 +- crates/rope/src/chunk.rs | 2 +- crates/rpc/src/conn.rs | 1 - crates/rpc/src/peer.rs | 1 - crates/rules_library/src/rules_library.rs | 4 +- crates/search/src/buffer_search.rs | 4 +- crates/search/src/buffer_search/registrar.rs | 1 - crates/search/src/project_search.rs | 3 +- crates/semantic_index/examples/index.rs | 4 +- crates/semantic_index/src/embedding_index.rs | 7 +- crates/semantic_index/src/semantic_index.rs | 2 +- crates/semantic_index/src/summary_index.rs | 2 +- crates/settings/src/settings_json.rs | 4 +- .../src/settings_profile_selector.rs | 2 +- .../src/appearance_settings_controls.rs | 4 +- crates/settings_ui/src/keybindings.rs | 12 +-- crates/story/src/story.rs | 2 +- crates/supermaven/src/supermaven.rs | 4 +- .../src/supermaven_completion_provider.rs | 4 +- crates/task/src/static_source.rs | 1 - crates/task/src/task.rs | 2 +- crates/task/src/task_template.rs | 12 ++- crates/tasks_ui/src/tasks_ui.rs | 2 +- crates/terminal/src/terminal.rs | 4 +- crates/terminal_view/src/terminal_element.rs | 5 +- crates/terminal_view/src/terminal_panel.rs | 1 - crates/theme/src/theme.rs | 6 +- crates/theme_importer/src/vscode/converter.rs | 12 ++- crates/title_bar/src/application_menu.rs | 2 +- crates/title_bar/src/collab.rs | 2 +- crates/title_bar/src/title_bar.rs | 7 +- .../src/toolchain_selector.rs | 1 - crates/ui/src/components/dropdown_menu.rs | 4 +- crates/ui/src/components/indent_guides.rs | 3 +- crates/ui/src/components/keybinding.rs | 2 +- crates/ui/src/components/keybinding_hint.rs | 2 +- .../components/notification/alert_modal.rs | 2 +- crates/ui/src/components/sticky_items.rs | 2 +- crates/ui/src/utils/format_distance.rs | 12 +-- crates/ui_input/src/ui_input.rs | 4 +- crates/vim/src/command.rs | 2 +- crates/vim/src/mode_indicator.rs | 6 +- crates/vim/src/motion.rs | 20 ++--- crates/vim/src/normal/paste.rs | 6 +- crates/vim/src/object.rs | 4 +- crates/vim/src/state.rs | 14 +-- .../src/test/neovim_backed_test_context.rs | 7 +- crates/vim/src/test/neovim_connection.rs | 2 +- crates/vim/src/test/vim_test_context.rs | 2 +- crates/vim/src/vim.rs | 2 +- crates/vim/src/visual.rs | 2 +- crates/watch/src/watch.rs | 2 +- crates/web_search/src/web_search.rs | 2 +- crates/workspace/src/dock.rs | 4 +- crates/workspace/src/notifications.rs | 1 - crates/workspace/src/pane.rs | 4 +- crates/workspace/src/pane_group.rs | 2 +- crates/workspace/src/persistence/model.rs | 2 +- crates/workspace/src/searchable.rs | 4 +- crates/workspace/src/status_bar.rs | 4 +- crates/workspace/src/theme_preview.rs | 1 - crates/workspace/src/workspace.rs | 6 +- crates/worktree/src/worktree.rs | 4 +- crates/worktree/src/worktree_tests.rs | 2 +- crates/zed/src/main.rs | 18 ++-- crates/zed/src/zed.rs | 8 +- crates/zed/src/zed/component_preview.rs | 18 ++-- .../zed/src/zed/edit_prediction_registry.rs | 11 +-- crates/zed/src/zed/open_listener.rs | 7 +- crates/zed/src/zed/quick_action_bar.rs | 4 +- .../zed/src/zed/quick_action_bar/repl_menu.rs | 5 +- crates/zeta/src/input_excerpt.rs | 2 +- crates/zeta_cli/src/headless.rs | 6 +- crates/zlog/src/filter.rs | 2 +- extensions/glsl/src/glsl.rs | 2 +- extensions/html/src/html.rs | 2 +- extensions/ruff/src/ruff.rs | 4 +- extensions/snippets/src/snippets.rs | 2 +- 306 files changed, 805 insertions(+), 1102 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3610808984..c3c7091279 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -840,6 +840,7 @@ match_like_matches_macro = "warn" module_inception = { level = "deny" } question_mark = { level = "deny" } single_match = "warn" +redundant_clone = "warn" redundant_closure = { level = "deny" } redundant_static_lifetimes = { level = "warn" } redundant_pattern_matching = "warn" diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index e58f0a291f..4f20dbd587 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -471,7 +471,7 @@ impl ContentBlock { fn block_string_contents(&self, block: acp::ContentBlock) -> String { match block { - acp::ContentBlock::Text(text_content) => text_content.text.clone(), + acp::ContentBlock::Text(text_content) => text_content.text, acp::ContentBlock::ResourceLink(resource_link) => { Self::resource_link_md(&resource_link.uri) } @@ -1020,7 +1020,7 @@ impl AcpThread { let location_updated = update.fields.locations.is_some(); current_call.update_fields(update.fields, languages, cx); if location_updated { - self.resolve_locations(update.id.clone(), cx); + self.resolve_locations(update.id, cx); } } ToolCallUpdate::UpdateDiff(update) => { diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs index 4b779931c5..70367e340a 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -222,7 +222,7 @@ impl PendingDiff { fn finalize(&self, cx: &mut Context<Diff>) -> FinalizedDiff { let ranges = self.excerpt_ranges(cx); let base_text = self.base_text.clone(); - let language_registry = self.buffer.read(cx).language_registry().clone(); + let language_registry = self.buffer.read(cx).language_registry(); let path = self .buffer @@ -248,7 +248,6 @@ impl PendingDiff { let buffer_diff = cx.spawn({ let buffer = buffer.clone(); - let language_registry = language_registry.clone(); async move |_this, cx| { build_buffer_diff(base_text, &buffer, language_registry, cx).await } diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs index 1c3cad386d..a1f332fc7c 100644 --- a/crates/action_log/src/action_log.rs +++ b/crates/action_log/src/action_log.rs @@ -161,7 +161,7 @@ impl ActionLog { diff_base, last_seen_base, unreviewed_edits, - snapshot: text_snapshot.clone(), + snapshot: text_snapshot, status, version: buffer.read(cx).version(), diff, @@ -461,7 +461,7 @@ impl ActionLog { anyhow::Ok(( tracked_buffer.diff.clone(), buffer.read(cx).language().cloned(), - buffer.read(cx).language_registry().clone(), + buffer.read(cx).language_registry(), )) })??; let diff_snapshot = BufferDiff::update_diff( @@ -529,12 +529,12 @@ impl ActionLog { /// Mark a buffer as created by agent, so we can refresh it in the context pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) { - self.track_buffer_internal(buffer.clone(), true, cx); + self.track_buffer_internal(buffer, true, cx); } /// Mark a buffer as edited by agent, so we can refresh it in the context pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) { - let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx); + let tracked_buffer = self.track_buffer_internal(buffer, false, cx); if let TrackedBufferStatus::Deleted = tracked_buffer.status { tracked_buffer.status = TrackedBufferStatus::Modified; } @@ -2425,7 +2425,7 @@ mod tests { assert_eq!( unreviewed_hunks(&action_log, cx), vec![( - buffer.clone(), + buffer, vec![ HunkStatus { range: Point::new(6, 0)..Point::new(7, 0), diff --git a/crates/agent/src/agent_profile.rs b/crates/agent/src/agent_profile.rs index 1636508df6..c9e73372f6 100644 --- a/crates/agent/src/agent_profile.rs +++ b/crates/agent/src/agent_profile.rs @@ -132,7 +132,7 @@ mod tests { }); let tool_set = default_tool_set(cx); - let profile = AgentProfile::new(id.clone(), tool_set); + let profile = AgentProfile::new(id, tool_set); let mut enabled_tools = cx .read(|cx| profile.enabled_tools(cx)) @@ -169,7 +169,7 @@ mod tests { }); let tool_set = default_tool_set(cx); - let profile = AgentProfile::new(id.clone(), tool_set); + let profile = AgentProfile::new(id, tool_set); let mut enabled_tools = cx .read(|cx| profile.enabled_tools(cx)) @@ -202,7 +202,7 @@ mod tests { }); let tool_set = default_tool_set(cx); - let profile = AgentProfile::new(id.clone(), tool_set); + let profile = AgentProfile::new(id, tool_set); let mut enabled_tools = cx .read(|cx| profile.enabled_tools(cx)) diff --git a/crates/agent/src/context_server_tool.rs b/crates/agent/src/context_server_tool.rs index 22d1a72bf5..696c569356 100644 --- a/crates/agent/src/context_server_tool.rs +++ b/crates/agent/src/context_server_tool.rs @@ -86,15 +86,13 @@ impl Tool for ContextServerTool { ) -> ToolResult { if let Some(server) = self.store.read(cx).get_running_server(&self.server_id) { let tool_name = self.tool.name.clone(); - let server_clone = server.clone(); - let input_clone = input.clone(); cx.spawn(async move |_cx| { - let Some(protocol) = server_clone.client() else { + let Some(protocol) = server.client() else { bail!("Context server not initialized"); }; - let arguments = if let serde_json::Value::Object(map) = input_clone { + let arguments = if let serde_json::Value::Object(map) = input { Some(map.into_iter().collect()) } else { None diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 88f82701a4..a584fba881 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -494,7 +494,7 @@ impl Thread { last_received_chunk_at: None, request_callback: None, remaining_turns: u32::MAX, - configured_model: configured_model.clone(), + configured_model, profile: AgentProfile::new(profile_id, tools), } } @@ -532,7 +532,7 @@ impl Thread { .and_then(|model| { let model = SelectedModel { provider: model.provider.clone().into(), - model: model.model.clone().into(), + model: model.model.into(), }; registry.select_model(&model, cx) }) @@ -1646,10 +1646,10 @@ impl Thread { }; self.tool_use - .request_tool_use(tool_message_id, tool_use, tool_use_metadata.clone(), cx); + .request_tool_use(tool_message_id, tool_use, tool_use_metadata, cx); self.tool_use.insert_tool_output( - tool_use_id.clone(), + tool_use_id, tool_name, tool_output, self.configured_model.as_ref(), @@ -3241,7 +3241,7 @@ impl Thread { self.configured_model.as_ref(), self.completion_mode, ); - self.tool_finished(tool_use_id.clone(), None, true, window, cx); + self.tool_finished(tool_use_id, None, true, window, cx); } } @@ -3873,7 +3873,7 @@ fn main() {{ AgentSettings { model_parameters: vec![LanguageModelParameters { provider: Some(model.provider_id().0.to_string().into()), - model: Some(model.id().0.clone()), + model: Some(model.id().0), temperature: Some(0.66), }], ..AgentSettings::get_global(cx).clone() @@ -3893,7 +3893,7 @@ fn main() {{ AgentSettings { model_parameters: vec![LanguageModelParameters { provider: None, - model: Some(model.id().0.clone()), + model: Some(model.id().0), temperature: Some(0.66), }], ..AgentSettings::get_global(cx).clone() @@ -3933,7 +3933,7 @@ fn main() {{ AgentSettings { model_parameters: vec![LanguageModelParameters { provider: Some("anthropic".into()), - model: Some(model.id().0.clone()), + model: Some(model.id().0), temperature: Some(0.66), }], ..AgentSettings::get_global(cx).clone() diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index ab5716d8ad..5496ecea7b 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -255,7 +255,7 @@ impl NativeAgent { }), cx.subscribe(&thread_handle, Self::handle_thread_token_usage_updated), cx.observe(&thread_handle, move |this, thread, cx| { - this.save_thread(thread.clone(), cx) + this.save_thread(thread, cx) }), ]; @@ -499,8 +499,8 @@ impl NativeAgent { self.models.refresh_list(cx); let registry = LanguageModelRegistry::read_global(cx); - let default_model = registry.default_model().map(|m| m.model.clone()); - let summarization_model = registry.thread_summary_model().map(|m| m.model.clone()); + let default_model = registry.default_model().map(|m| m.model); + let summarization_model = registry.thread_summary_model().map(|m| m.model); for session in self.sessions.values_mut() { session.thread.update(cx, |thread, cx| { diff --git a/crates/agent2/src/db.rs b/crates/agent2/src/db.rs index c6a6c38201..1b88955a24 100644 --- a/crates/agent2/src/db.rs +++ b/crates/agent2/src/db.rs @@ -287,7 +287,7 @@ impl ThreadsDatabase { .map_err(|e| anyhow!("Failed to create threads table: {}", e))?; let db = Self { - executor: executor.clone(), + executor, connection: Arc::new(Mutex::new(connection)), }; @@ -325,7 +325,7 @@ impl ThreadsDatabase { INSERT OR REPLACE INTO threads (id, summary, updated_at, data_type, data) VALUES (?, ?, ?, ?, ?) "})?; - insert((id.0.clone(), title, updated_at, data_type, data))?; + insert((id.0, title, updated_at, data_type, data))?; Ok(()) } @@ -434,7 +434,7 @@ mod tests { let client = Client::new(clock, http_client, cx); agent::init(cx); agent_settings::init(cx); - language_model::init(client.clone(), cx); + language_model::init(client, cx); }); } diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 55bfa6f0b5..478604b14a 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1401,7 +1401,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) { let client = Client::new(clock, http_client, cx); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); language_model::init(client.clone(), cx); - language_models::init(user_store.clone(), client.clone(), cx); + language_models::init(user_store, client.clone(), cx); Project::init_settings(cx); LanguageModelRegistry::test(cx); agent_settings::init(cx); @@ -1854,7 +1854,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { let client = Client::production(cx); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); language_model::init(client.clone(), cx); - language_models::init(user_store.clone(), client.clone(), cx); + language_models::init(user_store, client.clone(), cx); watch_settings(fs.clone(), cx); }); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 0e1287a920..cd97fa2060 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -679,7 +679,7 @@ impl Thread { .and_then(|model| { let model = SelectedModel { provider: model.provider.clone().into(), - model: model.model.clone().into(), + model: model.model.into(), }; registry.select_model(&model, cx) }) diff --git a/crates/agent2/src/tools/context_server_registry.rs b/crates/agent2/src/tools/context_server_registry.rs index 69c4221a81..c7963fa6e6 100644 --- a/crates/agent2/src/tools/context_server_registry.rs +++ b/crates/agent2/src/tools/context_server_registry.rs @@ -176,15 +176,13 @@ impl AnyAgentTool for ContextServerTool { return Task::ready(Err(anyhow!("Context server not found"))); }; let tool_name = self.tool.name.clone(); - let server_clone = server.clone(); - let input_clone = input.clone(); cx.spawn(async move |_cx| { - let Some(protocol) = server_clone.client() else { + let Some(protocol) = server.client() else { bail!("Context server not initialized"); }; - let arguments = if let serde_json::Value::Object(map) = input_clone { + let arguments = if let serde_json::Value::Object(map) = input { Some(map.into_iter().collect()) } else { None diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index a87699bd12..24fedda4eb 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -427,7 +427,7 @@ impl AgentTool for EditFileTool { Ok(EditFileToolOutput { input_path: input.path, - new_text: new_text.clone(), + new_text, old_text, diff: unified_diff, edit_agent_output, diff --git a/crates/agent2/src/tools/grep_tool.rs b/crates/agent2/src/tools/grep_tool.rs index 6d7c05d211..265c26926d 100644 --- a/crates/agent2/src/tools/grep_tool.rs +++ b/crates/agent2/src/tools/grep_tool.rs @@ -318,7 +318,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), serde_json::json!({ @@ -403,7 +403,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), serde_json::json!({ @@ -478,7 +478,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); // Create test file with syntax structures fs.insert_tree( @@ -763,7 +763,7 @@ mod tests { if cfg!(windows) { result.replace("root\\", "root/") } else { - result.to_string() + result } } Err(e) => panic!("Failed to run grep tool: {}", e), diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs index 17e671fba3..3d4faf2e03 100644 --- a/crates/agent2/src/tools/terminal_tool.rs +++ b/crates/agent2/src/tools/terminal_tool.rs @@ -234,7 +234,7 @@ fn process_content( if is_empty { "Command executed successfully.".to_string() } else { - content.to_string() + content } } Some(exit_status) => { diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 3008edebeb..df2a24e698 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -787,7 +787,7 @@ impl Content { pub fn chunks(self) -> impl Iterator<Item = ContentChunk> { match self { Self::Chunks(chunks) => chunks.into_iter(), - Self::UntaggedText(text) => vec![ContentChunk::Text { text: text.clone() }].into_iter(), + Self::UntaggedText(text) => vec![ContentChunk::Text { text }].into_iter(), } } } diff --git a/crates/agent_servers/src/claude/tools.rs b/crates/agent_servers/src/claude/tools.rs index 3be10ed94c..3231903001 100644 --- a/crates/agent_servers/src/claude/tools.rs +++ b/crates/agent_servers/src/claude/tools.rs @@ -58,7 +58,7 @@ impl ClaudeTool { Self::Terminal(None) } else { Self::Other { - name: tool_name.to_string(), + name: tool_name, input, } } diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 999e469d30..d90520d26a 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -89,7 +89,7 @@ impl ContextPickerCompletionProvider { ) -> Option<Completion> { match entry { ContextPickerEntry::Mode(mode) => Some(Completion { - replace_range: source_range.clone(), + replace_range: source_range, new_text: format!("@{} ", mode.keyword()), label: CodeLabel::plain(mode.label().to_string(), None), icon_path: Some(mode.icon().path().into()), @@ -146,7 +146,7 @@ impl ContextPickerCompletionProvider { }; Some(Completion { - replace_range: source_range.clone(), + replace_range: source_range, new_text, label: CodeLabel::plain(action.label().to_string(), None), icon_path: Some(action.icon().path().into()), @@ -187,7 +187,7 @@ impl ContextPickerCompletionProvider { documentation: None, insert_text_mode: None, source: project::CompletionSource::Custom, - icon_path: Some(icon_for_completion.clone()), + icon_path: Some(icon_for_completion), confirm: Some(confirm_completion_callback( thread_entry.title().clone(), source_range.start, @@ -218,9 +218,9 @@ impl ContextPickerCompletionProvider { documentation: None, insert_text_mode: None, source: project::CompletionSource::Custom, - icon_path: Some(icon_path.clone()), + icon_path: Some(icon_path), confirm: Some(confirm_completion_callback( - rule.title.clone(), + rule.title, source_range.start, new_text_len - 1, editor, @@ -260,7 +260,7 @@ impl ContextPickerCompletionProvider { let completion_icon_path = if is_recent { IconName::HistoryRerun.path().into() } else { - crease_icon_path.clone() + crease_icon_path }; let new_text = format!("{} ", uri.as_link()); @@ -309,10 +309,10 @@ impl ContextPickerCompletionProvider { label, documentation: None, source: project::CompletionSource::Custom, - icon_path: Some(icon_path.clone()), + icon_path: Some(icon_path), insert_text_mode: None, confirm: Some(confirm_completion_callback( - symbol.name.clone().into(), + symbol.name.into(), source_range.start, new_text_len - 1, message_editor, @@ -327,7 +327,7 @@ impl ContextPickerCompletionProvider { message_editor: WeakEntity<MessageEditor>, cx: &mut App, ) -> Option<Completion> { - let new_text = format!("@fetch {} ", url_to_fetch.clone()); + let new_text = format!("@fetch {} ", url_to_fetch); let url_to_fetch = url::Url::parse(url_to_fetch.as_ref()) .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}"))) .ok()?; @@ -341,7 +341,7 @@ impl ContextPickerCompletionProvider { label: CodeLabel::plain(url_to_fetch.to_string(), None), documentation: None, source: project::CompletionSource::Custom, - icon_path: Some(icon_path.clone()), + icon_path: Some(icon_path), insert_text_mode: None, confirm: Some(confirm_completion_callback( url_to_fetch.to_string().into(), @@ -365,8 +365,7 @@ impl ContextPickerCompletionProvider { }; match mode { Some(ContextPickerMode::File) => { - let search_files_task = - search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); + let search_files_task = search_files(query, cancellation_flag, &workspace, cx); cx.background_spawn(async move { search_files_task .await @@ -377,8 +376,7 @@ impl ContextPickerCompletionProvider { } Some(ContextPickerMode::Symbol) => { - let search_symbols_task = - search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx); + let search_symbols_task = search_symbols(query, cancellation_flag, &workspace, cx); cx.background_spawn(async move { search_symbols_task .await @@ -389,12 +387,8 @@ impl ContextPickerCompletionProvider { } Some(ContextPickerMode::Thread) => { - let search_threads_task = search_threads( - query.clone(), - cancellation_flag.clone(), - &self.history_store, - cx, - ); + let search_threads_task = + search_threads(query, cancellation_flag, &self.history_store, cx); cx.background_spawn(async move { search_threads_task .await @@ -415,7 +409,7 @@ impl ContextPickerCompletionProvider { Some(ContextPickerMode::Rules) => { if let Some(prompt_store) = self.prompt_store.as_ref() { let search_rules_task = - search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx); + search_rules(query, cancellation_flag, prompt_store, cx); cx.background_spawn(async move { search_rules_task .await @@ -448,7 +442,7 @@ impl ContextPickerCompletionProvider { let executor = cx.background_executor().clone(); let search_files_task = - search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); + search_files(query.clone(), cancellation_flag, &workspace, cx); let entries = self.available_context_picker_entries(&workspace, cx); let entry_candidates = entries diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index c87c824015..b5282bf891 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -260,7 +260,7 @@ impl MessageEditor { *excerpt_id, start, content_len, - crease_text.clone(), + crease_text, mention_uri.icon_path(cx), self.editor.clone(), window, @@ -883,7 +883,7 @@ impl MessageEditor { .spawn_in(window, { let abs_path = abs_path.clone(); async move |_, cx| { - let image = image.await.map_err(|e| e.to_string())?; + let image = image.await?; let format = image.format; let image = cx .update(|_, cx| LanguageModelImage::from_image(image, cx)) @@ -1231,7 +1231,6 @@ fn render_image_fold_icon_button( editor: WeakEntity<Editor>, ) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> { Arc::new({ - let image_task = image_task.clone(); move |fold_id, fold_range, cx| { let is_in_text_selection = editor .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx)) @@ -1408,10 +1407,7 @@ impl MentionSet { crease_id, Mention::Text { uri, - content: content - .await - .map_err(|e| anyhow::anyhow!("{e}"))? - .to_string(), + content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, }, )) }) @@ -1478,10 +1474,7 @@ impl MentionSet { crease_id, Mention::Text { uri, - content: content - .await - .map_err(|e| anyhow::anyhow!("{e}"))? - .to_string(), + content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, }, )) }) @@ -1821,7 +1814,7 @@ mod tests { impl Focusable for MessageEditorItem { fn focus_handle(&self, cx: &App) -> FocusHandle { - self.0.read(cx).focus_handle(cx).clone() + self.0.read(cx).focus_handle(cx) } } @@ -2219,7 +2212,7 @@ mod tests { let completions = editor.current_completions().expect("Missing completions"); completions .into_iter() - .map(|completion| completion.label.text.to_string()) + .map(|completion| completion.label.text) .collect::<Vec<_>>() } } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index b93df3a5db..b527775850 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1534,7 +1534,7 @@ impl AcpThreadView { window: &Window, cx: &Context<Self>, ) -> AnyElement { - let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id.clone())); + let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id)); v_flex() .mt_1p5() @@ -1555,9 +1555,8 @@ impl AcpThreadView { .icon_color(Color::Muted) .icon_position(IconPosition::Start) .on_click(cx.listener({ - let id = tool_call_id.clone(); move |this: &mut Self, _, _, cx: &mut Context<Self>| { - this.expanded_tool_calls.remove(&id); + this.expanded_tool_calls.remove(&tool_call_id); cx.notify(); } })), @@ -1578,7 +1577,7 @@ impl AcpThreadView { uri.clone() }; - let button_id = SharedString::from(format!("item-{}", uri.clone())); + let button_id = SharedString::from(format!("item-{}", uri)); div() .ml(px(7.)) @@ -1724,7 +1723,7 @@ impl AcpThreadView { && let Some(editor) = entry.editor_for_diff(diff) && diff.read(cx).has_revealed_range(cx) { - editor.clone().into_any_element() + editor.into_any_element() } else if tool_progress { self.render_diff_loading(cx) } else { @@ -2888,7 +2887,6 @@ impl AcpThreadView { .icon_size(IconSize::Small) .icon_color(Color::Muted) .tooltip({ - let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( expand_tooltip, @@ -4372,7 +4370,7 @@ pub(crate) mod tests { impl Focusable for ThreadViewItem { fn focus_handle(&self, cx: &App) -> FocusHandle { - self.0.read(cx).focus_handle(cx).clone() + self.0.read(cx).focus_handle(cx) } } diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 92588cf213..bb5b47f0d6 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -491,7 +491,7 @@ fn render_markdown_code_block( .on_click({ let active_thread = active_thread.clone(); let parsed_markdown = parsed_markdown.clone(); - let code_block_range = metadata.content_range.clone(); + let code_block_range = metadata.content_range; move |_event, _window, cx| { active_thread.update(cx, |this, cx| { this.copied_code_block_ids.insert((message_id, ix)); @@ -532,7 +532,6 @@ fn render_markdown_code_block( "Expand Code" })) .on_click({ - let active_thread = active_thread.clone(); move |_event, _window, cx| { active_thread.update(cx, |this, cx| { this.toggle_codeblock_expanded(message_id, ix); @@ -916,7 +915,7 @@ impl ActiveThread { ) { let rendered = self .rendered_tool_uses - .entry(tool_use_id.clone()) + .entry(tool_use_id) .or_insert_with(|| RenderedToolUse { label: cx.new(|cx| { Markdown::new("".into(), Some(self.language_registry.clone()), None, cx) @@ -1218,7 +1217,7 @@ impl ActiveThread { match AgentSettings::get_global(cx).notify_when_agent_waiting { NotifyWhenAgentWaiting::PrimaryScreen => { if let Some(primary) = cx.primary_display() { - self.pop_up(icon, caption.into(), title.clone(), window, primary, cx); + self.pop_up(icon, caption.into(), title, window, primary, cx); } } NotifyWhenAgentWaiting::AllScreens => { @@ -2112,7 +2111,7 @@ impl ActiveThread { .gap_1() .children(message_content) .when_some(editing_message_state, |this, state| { - let focus_handle = state.editor.focus_handle(cx).clone(); + let focus_handle = state.editor.focus_handle(cx); this.child( h_flex() @@ -2173,7 +2172,6 @@ impl ActiveThread { .icon_color(Color::Muted) .icon_size(IconSize::Small) .tooltip({ - let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( "Regenerate", @@ -2312,7 +2310,7 @@ impl ActiveThread { .into_any_element() } else if let Some(error) = error { restore_checkpoint_button - .tooltip(Tooltip::text(error.to_string())) + .tooltip(Tooltip::text(error)) .into_any_element() } else { restore_checkpoint_button.into_any_element() diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index ecb0bca4a1..6da84758ee 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -165,8 +165,8 @@ impl AgentConfiguration { provider: &Arc<dyn LanguageModelProvider>, cx: &mut Context<Self>, ) -> impl IntoElement + use<> { - let provider_id = provider.id().0.clone(); - let provider_name = provider.name().0.clone(); + let provider_id = provider.id().0; + let provider_name = provider.name().0; let provider_id_string = SharedString::from(format!("provider-disclosure-{provider_id}")); let configuration_view = self @@ -269,7 +269,7 @@ impl AgentConfiguration { .closed_icon(IconName::ChevronDown), ) .on_click(cx.listener({ - let provider_id = provider.id().clone(); + let provider_id = provider.id(); move |this, _event, _window, _cx| { let is_expanded = this .expanded_provider_configurations @@ -665,7 +665,7 @@ impl AgentConfiguration { .size(IconSize::XSmall) .color(Color::Accent) .with_animation( - SharedString::from(format!("{}-starting", context_server_id.0.clone(),)), + SharedString::from(format!("{}-starting", context_server_id.0,)), Animation::new(Duration::from_secs(3)).repeat(), |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), ) @@ -865,7 +865,6 @@ impl AgentConfiguration { .on_click({ let context_server_manager = self.context_server_store.clone(); - let context_server_id = context_server_id.clone(); let fs = self.fs.clone(); move |state, _window, cx| { @@ -1075,7 +1074,6 @@ fn show_unable_to_uninstall_extension_with_context_server( cx, move |this, _cx| { let workspace_handle = workspace_handle.clone(); - let context_server_id = context_server_id.clone(); this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning)) .dismiss_button(true) diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs index 6159b9be80..c898a5acb5 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs @@ -261,7 +261,6 @@ impl ConfigureContextServerModal { _cx: &mut Context<Workspace>, ) { workspace.register_action({ - let language_registry = language_registry.clone(); move |_workspace, _: &AddContextServer, window, cx| { let workspace_handle = cx.weak_entity(); let language_registry = language_registry.clone(); diff --git a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs index 09ad013d1c..7fcf76d1cb 100644 --- a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs +++ b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs @@ -464,7 +464,7 @@ impl ManageProfilesModal { }, )) .child(ListSeparator) - .child(h_flex().p_2().child(mode.name_editor.clone())) + .child(h_flex().p_2().child(mode.name_editor)) } fn render_view_profile( diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 61a3ddd906..e07424987c 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -185,7 +185,7 @@ impl AgentDiffPane { let focus_handle = cx.focus_handle(); let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - let project = thread.project(cx).clone(); + let project = thread.project(cx); let editor = cx.new(|cx| { let mut editor = Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx); @@ -196,7 +196,7 @@ impl AgentDiffPane { editor }); - let action_log = thread.action_log(cx).clone(); + let action_log = thread.action_log(cx); let mut this = Self { _subscriptions: vec![ @@ -1312,7 +1312,7 @@ impl AgentDiff { let entity = cx.new(|_cx| Self::default()); let global = AgentDiffGlobal(entity.clone()); cx.set_global(global); - entity.clone() + entity }) } @@ -1334,7 +1334,7 @@ impl AgentDiff { window: &mut Window, cx: &mut Context<Self>, ) { - let action_log = thread.action_log(cx).clone(); + let action_log = thread.action_log(cx); let action_log_subscription = cx.observe_in(&action_log, window, { let workspace = workspace.clone(); @@ -1544,7 +1544,7 @@ impl AgentDiff { && let Some(editor) = item.downcast::<Editor>() && let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx) { - self.register_editor(workspace.downgrade(), buffer.clone(), editor, window, cx); + self.register_editor(workspace.downgrade(), buffer, editor, window, cx); } } diff --git a/crates/agent_ui/src/agent_model_selector.rs b/crates/agent_ui/src/agent_model_selector.rs index b989e7bf1e..3de1027d91 100644 --- a/crates/agent_ui/src/agent_model_selector.rs +++ b/crates/agent_ui/src/agent_model_selector.rs @@ -66,10 +66,8 @@ impl AgentModelSelector { fs.clone(), cx, move |settings, _cx| { - settings.set_inline_assistant_model( - provider.clone(), - model_id.clone(), - ); + settings + .set_inline_assistant_model(provider.clone(), model_id); }, ); } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index b857052d69..3c4c403a77 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -956,7 +956,7 @@ impl AgentPanel { message_editor.focus_handle(cx).focus(window); - let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx); + let thread_view = ActiveView::thread(active_thread, message_editor, window, cx); self.set_active_view(thread_view, window, cx); AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx); @@ -1163,7 +1163,7 @@ impl AgentPanel { }); self.set_active_view( ActiveView::prompt_editor( - editor.clone(), + editor, self.history_store.clone(), self.acp_history_store.clone(), self.language_registry.clone(), @@ -1236,7 +1236,7 @@ impl AgentPanel { }); message_editor.focus_handle(cx).focus(window); - let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx); + let thread_view = ActiveView::thread(active_thread, message_editor, window, cx); self.set_active_view(thread_view, window, cx); AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx); } @@ -1525,7 +1525,7 @@ impl AgentPanel { return; } - let model = thread_state.configured_model().map(|cm| cm.model.clone()); + let model = thread_state.configured_model().map(|cm| cm.model); if let Some(model) = model { thread.update(cx, |active_thread, cx| { active_thread.thread().update(cx, |thread, cx| { @@ -1680,7 +1680,7 @@ impl AgentPanel { .open_thread_by_id(&id, window, cx) .detach_and_log_err(cx), HistoryEntryId::Context(path) => this - .open_saved_prompt_editor(path.clone(), window, cx) + .open_saved_prompt_editor(path, window, cx) .detach_and_log_err(cx), }) .ok(); @@ -1966,7 +1966,7 @@ impl AgentPanel { }; match state { - ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT.clone()) + ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT) .truncate() .into_any_element(), ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER) @@ -2106,7 +2106,6 @@ impl AgentPanel { .anchor(Corner::TopRight) .with_handle(self.agent_panel_menu_handle.clone()) .menu({ - let focus_handle = focus_handle.clone(); move |window, cx| { Some(ContextMenu::build(window, cx, |mut menu, _window, _| { menu = menu.context(focus_handle.clone()); @@ -2184,7 +2183,6 @@ impl AgentPanel { .trigger_with_tooltip( IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small), { - let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( "Toggle Recent Threads", @@ -2222,8 +2220,6 @@ impl AgentPanel { this.go_back(&workspace::GoBack, window, cx); })) .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, window, cx) } @@ -2249,7 +2245,6 @@ impl AgentPanel { .anchor(Corner::TopRight) .with_handle(self.new_thread_menu_handle.clone()) .menu({ - let focus_handle = focus_handle.clone(); move |window, cx| { let active_thread = active_thread.clone(); Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { @@ -2377,7 +2372,6 @@ impl AgentPanel { .anchor(Corner::TopLeft) .with_handle(self.new_thread_menu_handle.clone()) .menu({ - let focus_handle = focus_handle.clone(); let workspace = self.workspace.clone(); move |window, cx| { @@ -3015,7 +3009,7 @@ impl AgentPanel { // TODO: Add keyboard navigation. let is_hovered = self.hovered_recent_history_item == Some(index); - HistoryEntryElement::new(entry.clone(), cx.entity().downgrade()) + HistoryEntryElement::new(entry, cx.entity().downgrade()) .hovered(is_hovered) .on_hover(cx.listener( move |this, is_hovered, _window, cx| { @@ -3339,7 +3333,7 @@ impl AgentPanel { .severity(Severity::Error) .icon(IconName::XCircle) .title(header) - .description(message.clone()) + .description(message) .actions_slot( h_flex() .gap_0p5() @@ -3359,7 +3353,7 @@ impl AgentPanel { Callout::new() .severity(Severity::Error) .title("Error") - .description(message.clone()) + .description(message) .actions_slot( h_flex() .gap_0p5() diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index a1dbc77084..01a248994d 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -240,12 +240,7 @@ pub fn init( client.telemetry().clone(), cx, ); - terminal_inline_assistant::init( - fs.clone(), - prompt_builder.clone(), - client.telemetry().clone(), - cx, - ); + terminal_inline_assistant::init(fs.clone(), prompt_builder, client.telemetry().clone(), cx); cx.observe_new(move |workspace, window, cx| { ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx) }) @@ -391,7 +386,6 @@ fn register_slash_commands(cx: &mut App) { slash_command_registry.register_command(assistant_slash_commands::FetchSlashCommand, true); cx.observe_flag::<assistant_slash_commands::StreamingExampleSlashCommandFeatureFlag, _>({ - let slash_command_registry = slash_command_registry.clone(); move |is_enabled, _cx| { if is_enabled { slash_command_registry.register_command( diff --git a/crates/agent_ui/src/buffer_codegen.rs b/crates/agent_ui/src/buffer_codegen.rs index ff5e9362dd..04eb41793f 100644 --- a/crates/agent_ui/src/buffer_codegen.rs +++ b/crates/agent_ui/src/buffer_codegen.rs @@ -1129,7 +1129,7 @@ mod tests { ) }); - let chunks_tx = simulate_response_stream(codegen.clone(), cx); + let chunks_tx = simulate_response_stream(&codegen, cx); let mut new_text = concat!( " let mut x = 0;\n", @@ -1196,7 +1196,7 @@ mod tests { ) }); - let chunks_tx = simulate_response_stream(codegen.clone(), cx); + let chunks_tx = simulate_response_stream(&codegen, cx); cx.background_executor.run_until_parked(); @@ -1265,7 +1265,7 @@ mod tests { ) }); - let chunks_tx = simulate_response_stream(codegen.clone(), cx); + let chunks_tx = simulate_response_stream(&codegen, cx); cx.background_executor.run_until_parked(); @@ -1334,7 +1334,7 @@ mod tests { ) }); - let chunks_tx = simulate_response_stream(codegen.clone(), cx); + let chunks_tx = simulate_response_stream(&codegen, cx); let new_text = concat!( "func main() {\n", "\tx := 0\n", @@ -1391,7 +1391,7 @@ mod tests { ) }); - let chunks_tx = simulate_response_stream(codegen.clone(), cx); + let chunks_tx = simulate_response_stream(&codegen, cx); chunks_tx .unbounded_send("let mut x = 0;\nx += 1;".to_string()) .unwrap(); @@ -1473,7 +1473,7 @@ mod tests { } fn simulate_response_stream( - codegen: Entity<CodegenAlternative>, + codegen: &Entity<CodegenAlternative>, cx: &mut TestAppContext, ) -> mpsc::UnboundedSender<String> { let (chunks_tx, chunks_rx) = mpsc::unbounded(); diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index 0b4568dc87..405b5ed90b 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -818,13 +818,8 @@ pub fn crease_for_mention( let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any(); - Crease::inline( - range, - placeholder.clone(), - fold_toggle("mention"), - render_trailer, - ) - .with_metadata(CreaseMetadata { icon_path, label }) + Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer) + .with_metadata(CreaseMetadata { icon_path, label }) } fn render_fold_icon_button( diff --git a/crates/agent_ui/src/context_picker/completion_provider.rs b/crates/agent_ui/src/context_picker/completion_provider.rs index 747ec46e0a..020d799c79 100644 --- a/crates/agent_ui/src/context_picker/completion_provider.rs +++ b/crates/agent_ui/src/context_picker/completion_provider.rs @@ -79,8 +79,7 @@ fn search( ) -> Task<Vec<Match>> { match mode { Some(ContextPickerMode::File) => { - let search_files_task = - search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); + let search_files_task = search_files(query, cancellation_flag, &workspace, cx); cx.background_spawn(async move { search_files_task .await @@ -91,8 +90,7 @@ fn search( } Some(ContextPickerMode::Symbol) => { - let search_symbols_task = - search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx); + let search_symbols_task = search_symbols(query, cancellation_flag, &workspace, cx); cx.background_spawn(async move { search_symbols_task .await @@ -108,13 +106,8 @@ fn search( .and_then(|t| t.upgrade()) .zip(text_thread_context_store.as_ref().and_then(|t| t.upgrade())) { - let search_threads_task = search_threads( - query.clone(), - cancellation_flag.clone(), - thread_store, - context_store, - cx, - ); + let search_threads_task = + search_threads(query, cancellation_flag, thread_store, context_store, cx); cx.background_spawn(async move { search_threads_task .await @@ -137,8 +130,7 @@ fn search( Some(ContextPickerMode::Rules) => { if let Some(prompt_store) = prompt_store.as_ref() { - let search_rules_task = - search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx); + let search_rules_task = search_rules(query, cancellation_flag, prompt_store, cx); cx.background_spawn(async move { search_rules_task .await @@ -196,7 +188,7 @@ fn search( let executor = cx.background_executor().clone(); let search_files_task = - search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); + search_files(query.clone(), cancellation_flag, &workspace, cx); let entries = available_context_picker_entries(&prompt_store, &thread_store, &workspace, cx); @@ -283,7 +275,7 @@ impl ContextPickerCompletionProvider { ) -> Option<Completion> { match entry { ContextPickerEntry::Mode(mode) => Some(Completion { - replace_range: source_range.clone(), + replace_range: source_range, new_text: format!("@{} ", mode.keyword()), label: CodeLabel::plain(mode.label().to_string(), None), icon_path: Some(mode.icon().path().into()), @@ -330,9 +322,6 @@ impl ContextPickerCompletionProvider { ); let callback = Arc::new({ - let context_store = context_store.clone(); - let selections = selections.clone(); - let selection_infos = selection_infos.clone(); move |_, window: &mut Window, cx: &mut App| { context_store.update(cx, |context_store, cx| { for (buffer, range) in &selections { @@ -441,7 +430,7 @@ impl ContextPickerCompletionProvider { excerpt_id, source_range.start, new_text_len - 1, - editor.clone(), + editor, context_store.clone(), move |window, cx| match &thread_entry { ThreadContextEntry::Thread { id, .. } => { @@ -510,7 +499,7 @@ impl ContextPickerCompletionProvider { excerpt_id, source_range.start, new_text_len - 1, - editor.clone(), + editor, context_store.clone(), move |_, cx| { let user_prompt_id = rules.prompt_id; @@ -547,7 +536,7 @@ impl ContextPickerCompletionProvider { excerpt_id, source_range.start, new_text_len - 1, - editor.clone(), + editor, context_store.clone(), move |_, cx| { let context_store = context_store.clone(); @@ -704,16 +693,16 @@ impl ContextPickerCompletionProvider { excerpt_id, source_range.start, new_text_len - 1, - editor.clone(), + editor, context_store.clone(), move |_, cx| { let symbol = symbol.clone(); let context_store = context_store.clone(); let workspace = workspace.clone(); let result = super::symbol_context_picker::add_symbol( - symbol.clone(), + symbol, false, - workspace.clone(), + workspace, context_store.downgrade(), cx, ); @@ -1162,7 +1151,7 @@ mod tests { impl Focusable for AtMentionEditor { fn focus_handle(&self, cx: &App) -> FocusHandle { - self.0.read(cx).focus_handle(cx).clone() + self.0.read(cx).focus_handle(cx) } } @@ -1480,7 +1469,7 @@ mod tests { let completions = editor.current_completions().expect("Missing completions"); completions .into_iter() - .map(|completion| completion.label.text.to_string()) + .map(|completion| completion.label.text) .collect::<Vec<_>>() } diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 90302236fb..2111553340 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -1693,7 +1693,7 @@ impl InlineAssist { }), range, codegen: codegen.clone(), - workspace: workspace.clone(), + workspace, _subscriptions: vec![ window.on_focus_in(&prompt_editor_focus_handle, cx, move |_, cx| { InlineAssistant::update_global(cx, |this, cx| { diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index 845540979a..3633e533da 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -93,7 +93,7 @@ impl LanguageModelPickerDelegate { let entries = models.entries(); Self { - on_model_changed: on_model_changed.clone(), + on_model_changed, all_models: Arc::new(models), selected_index: Self::get_active_model_index(&entries, get_active_model(cx)), filtered_entries: entries, @@ -514,7 +514,7 @@ impl PickerDelegate for LanguageModelPickerDelegate { .pl_0p5() .gap_1p5() .w(px(240.)) - .child(Label::new(model_info.model.name().0.clone()).truncate()), + .child(Label::new(model_info.model.name().0).truncate()), ) .end_slot(div().pr_3().when(is_selected, |this| { this.child( diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index f70d10c1ae..fdbce14415 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -248,7 +248,7 @@ impl MessageEditor { editor: editor.clone(), project: thread.read(cx).project().clone(), thread, - incompatible_tools_state: incompatible_tools.clone(), + incompatible_tools_state: incompatible_tools, workspace, context_store, prompt_store, @@ -839,7 +839,6 @@ impl MessageEditor { .child(self.profile_selector.clone()) .child(self.model_selector.clone()) .map({ - let focus_handle = focus_handle.clone(); move |parent| { if is_generating { parent @@ -1801,7 +1800,7 @@ impl AgentPreview for MessageEditor { .bg(cx.theme().colors().panel_background) .border_1() .border_color(cx.theme().colors().border) - .child(default_message_editor.clone()) + .child(default_message_editor) .into_any_element(), )]) .into_any_element(), diff --git a/crates/agent_ui/src/profile_selector.rs b/crates/agent_ui/src/profile_selector.rs index ce25f531e2..f0f53b96b2 100644 --- a/crates/agent_ui/src/profile_selector.rs +++ b/crates/agent_ui/src/profile_selector.rs @@ -137,12 +137,11 @@ impl ProfileSelector { entry.handler({ let fs = self.fs.clone(); let provider = self.provider.clone(); - let profile_id = profile_id.clone(); move |_window, cx| { update_settings_file::<AgentSettings>(fs.clone(), cx, { let profile_id = profile_id.clone(); move |settings, _cx| { - settings.set_profile(profile_id.clone()); + settings.set_profile(profile_id); } }); @@ -175,7 +174,6 @@ impl Render for ProfileSelector { PopoverMenu::new("profile-selector") .trigger_with_tooltip(trigger_button, { - let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( "Toggle Profile Menu", diff --git a/crates/agent_ui/src/slash_command.rs b/crates/agent_ui/src/slash_command.rs index 6b37c5a2d7..87e5d45fe8 100644 --- a/crates/agent_ui/src/slash_command.rs +++ b/crates/agent_ui/src/slash_command.rs @@ -88,8 +88,6 @@ impl SlashCommandCompletionProvider { .map(|(editor, workspace)| { let command_name = mat.string.clone(); let command_range = command_range.clone(); - let editor = editor.clone(); - let workspace = workspace.clone(); Arc::new( move |intent: CompletionIntent, window: &mut Window, @@ -158,7 +156,7 @@ impl SlashCommandCompletionProvider { if let Some(command) = self.slash_commands.command(command_name, cx) { let completions = command.complete_argument( arguments, - new_cancel_flag.clone(), + new_cancel_flag, self.workspace.clone(), window, cx, diff --git a/crates/agent_ui/src/terminal_inline_assistant.rs b/crates/agent_ui/src/terminal_inline_assistant.rs index 3859863ebe..e7070c0d7f 100644 --- a/crates/agent_ui/src/terminal_inline_assistant.rs +++ b/crates/agent_ui/src/terminal_inline_assistant.rs @@ -432,7 +432,7 @@ impl TerminalInlineAssist { terminal: terminal.downgrade(), prompt_editor: Some(prompt_editor.clone()), codegen: codegen.clone(), - workspace: workspace.clone(), + workspace, context_store, prompt_store, _subscriptions: vec![ diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index b3f55ffc43..a928f7af54 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -1739,7 +1739,7 @@ impl TextThreadEditor { render_slash_command_output_toggle, |_, _, _, _| Empty.into_any(), ) - .with_metadata(metadata.crease.clone()) + .with_metadata(metadata.crease) }), cx, ); @@ -1810,7 +1810,7 @@ impl TextThreadEditor { .filter_map(|(anchor, render_image)| { const MAX_HEIGHT_IN_LINES: u32 = 8; let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap(); - let image = render_image.clone(); + let image = render_image; anchor.is_valid(&buffer).then(|| BlockProperties { placement: BlockPlacement::Above(anchor), height: Some(MAX_HEIGHT_IN_LINES), @@ -1873,7 +1873,7 @@ impl TextThreadEditor { } fn render_send_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { - let focus_handle = self.focus_handle(cx).clone(); + let focus_handle = self.focus_handle(cx); let (style, tooltip) = match token_state(&self.context, cx) { Some(TokenState::NoTokensLeft { .. }) => ( @@ -2015,7 +2015,7 @@ impl TextThreadEditor { None => IconName::Ai, }; - let focus_handle = self.editor().focus_handle(cx).clone(); + let focus_handle = self.editor().focus_handle(cx); PickerPopoverMenu::new( self.language_model_selector.clone(), diff --git a/crates/agent_ui/src/ui/context_pill.rs b/crates/agent_ui/src/ui/context_pill.rs index 4e33e151cd..7c7fbd27f0 100644 --- a/crates/agent_ui/src/ui/context_pill.rs +++ b/crates/agent_ui/src/ui/context_pill.rs @@ -499,7 +499,7 @@ impl AddedContext { let thread = handle.thread.clone(); Some(Rc::new(move |_, cx| { let text = thread.read(cx).latest_detailed_summary_or_text(); - ContextPillHover::new_text(text.clone(), cx).into() + ContextPillHover::new_text(text, cx).into() })) }, handle: AgentContextHandle::Thread(handle), @@ -574,7 +574,7 @@ impl AddedContext { .unwrap_or_else(|| "Unnamed Rule".into()); Some(AddedContext { kind: ContextKind::Rules, - name: title.clone(), + name: title, parent: None, tooltip: None, icon_path: None, diff --git a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs index 0a34a29068..fadc4222ae 100644 --- a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs +++ b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs @@ -33,7 +33,7 @@ impl ApiKeysWithProviders { .filter(|provider| { provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID }) - .map(|provider| (provider.icon(), provider.name().0.clone())) + .map(|provider| (provider.icon(), provider.name().0)) .collect() } } diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs index 23810b74f3..1a44fa3c17 100644 --- a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs +++ b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs @@ -50,7 +50,7 @@ impl AgentPanelOnboarding { .filter(|provider| { provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID }) - .map(|provider| (provider.icon(), provider.name().0.clone())) + .map(|provider| (provider.icon(), provider.name().0)) .collect() } } diff --git a/crates/assistant_context/src/assistant_context.rs b/crates/assistant_context/src/assistant_context.rs index 4d0bfae444..12eda0954a 100644 --- a/crates/assistant_context/src/assistant_context.rs +++ b/crates/assistant_context/src/assistant_context.rs @@ -2282,7 +2282,7 @@ impl AssistantContext { let mut contents = self.contents(cx).peekable(); fn collect_text_content(buffer: &Buffer, range: Range<usize>) -> Option<String> { - let text: String = buffer.text_for_range(range.clone()).collect(); + let text: String = buffer.text_for_range(range).collect(); if text.trim().is_empty() { None } else { diff --git a/crates/assistant_context/src/assistant_context_tests.rs b/crates/assistant_context/src/assistant_context_tests.rs index 3db4a33b19..61d748cbdd 100644 --- a/crates/assistant_context/src/assistant_context_tests.rs +++ b/crates/assistant_context/src/assistant_context_tests.rs @@ -1321,7 +1321,7 @@ fn test_summarize_error( fn setup_context_editor_with_fake_model( cx: &mut TestAppContext, ) -> (Entity<AssistantContext>, Arc<FakeLanguageModel>) { - let registry = Arc::new(LanguageRegistry::test(cx.executor().clone())); + let registry = Arc::new(LanguageRegistry::test(cx.executor())); let fake_provider = Arc::new(FakeLanguageModelProvider::default()); let fake_model = Arc::new(fake_provider.test_model()); @@ -1376,7 +1376,7 @@ fn messages_cache( context .read(cx) .messages(cx) - .map(|message| (message.id, message.cache.clone())) + .map(|message| (message.id, message.cache)) .collect() } diff --git a/crates/assistant_context/src/context_store.rs b/crates/assistant_context/src/context_store.rs index 6d13531a57..6960d9db79 100644 --- a/crates/assistant_context/src/context_store.rs +++ b/crates/assistant_context/src/context_store.rs @@ -862,7 +862,7 @@ impl ContextStore { ContextServerStatus::Running => { self.load_context_server_slash_commands( server_id.clone(), - context_server_store.clone(), + context_server_store, cx, ); } diff --git a/crates/assistant_slash_commands/src/diagnostics_command.rs b/crates/assistant_slash_commands/src/diagnostics_command.rs index 10f950c866..8b1dbd515c 100644 --- a/crates/assistant_slash_commands/src/diagnostics_command.rs +++ b/crates/assistant_slash_commands/src/diagnostics_command.rs @@ -44,7 +44,7 @@ impl DiagnosticsSlashCommand { score: 0., positions: Vec::new(), worktree_id: entry.worktree_id.to_usize(), - path: entry.path.clone(), + path: entry.path, path_prefix: path_prefix.clone(), is_dir: false, // Diagnostics can't be produced for directories distance_to_relative_ancestor: 0, diff --git a/crates/assistant_slash_commands/src/prompt_command.rs b/crates/assistant_slash_commands/src/prompt_command.rs index 27029ac156..bbd6d3e3ad 100644 --- a/crates/assistant_slash_commands/src/prompt_command.rs +++ b/crates/assistant_slash_commands/src/prompt_command.rs @@ -80,7 +80,7 @@ impl SlashCommand for PromptSlashCommand { }; let store = PromptStore::global(cx); - let title = SharedString::from(title.clone()); + let title = SharedString::from(title); let prompt = cx.spawn({ let title = title.clone(); async move |cx| { diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/assistant_tools/src/edit_agent/evals.rs index ea2fa02663..4f182b3148 100644 --- a/crates/assistant_tools/src/edit_agent/evals.rs +++ b/crates/assistant_tools/src/edit_agent/evals.rs @@ -1153,8 +1153,7 @@ impl EvalInput { .expect("Conversation must end with an edit_file tool use") .clone(); - let edit_file_input: EditFileToolInput = - serde_json::from_value(tool_use.input.clone()).unwrap(); + let edit_file_input: EditFileToolInput = serde_json::from_value(tool_use.input).unwrap(); EvalInput { conversation, @@ -1460,7 +1459,7 @@ impl EditAgentTest { async fn new(cx: &mut TestAppContext) -> Self { cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); cx.update(|cx| { settings::init(cx); gpui_tokio::init(cx); @@ -1475,7 +1474,7 @@ impl EditAgentTest { Project::init_settings(cx); language::init(cx); language_model::init(client.clone(), cx); - language_models::init(user_store.clone(), client.clone(), cx); + language_models::init(user_store, client.clone(), cx); crate::init(client.http_client(), cx); }); diff --git a/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs b/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs index 092bdce8b3..2dba8a2b6d 100644 --- a/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs +++ b/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs @@ -319,7 +319,7 @@ mod tests { ); let snapshot = buffer.snapshot(); - let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut finder = StreamingFuzzyMatcher::new(snapshot); assert_eq!(push(&mut finder, ""), None); assert_eq!(finish(finder), None); } @@ -333,7 +333,7 @@ mod tests { ); let snapshot = buffer.snapshot(); - let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut finder = StreamingFuzzyMatcher::new(snapshot); // Push partial query assert_eq!(push(&mut finder, "This"), None); @@ -365,7 +365,7 @@ mod tests { ); let snapshot = buffer.snapshot(); - let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut finder = StreamingFuzzyMatcher::new(snapshot); // Push a fuzzy query that should match the first function assert_eq!( @@ -391,7 +391,7 @@ mod tests { ); let snapshot = buffer.snapshot(); - let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut finder = StreamingFuzzyMatcher::new(snapshot); // No match initially assert_eq!(push(&mut finder, "Lin"), None); @@ -420,7 +420,7 @@ mod tests { ); let snapshot = buffer.snapshot(); - let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut finder = StreamingFuzzyMatcher::new(snapshot); // Push text in small chunks across line boundaries assert_eq!(push(&mut finder, "jumps "), None); // No newline yet @@ -458,7 +458,7 @@ mod tests { ); let snapshot = buffer.snapshot(); - let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut finder = StreamingFuzzyMatcher::new(snapshot); assert_eq!( push(&mut finder, "impl Debug for User {\n"), @@ -711,7 +711,7 @@ mod tests { "Expected to match `second_function` based on the line hint" ); - let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut matcher = StreamingFuzzyMatcher::new(snapshot); matcher.push(query, None); matcher.finish(); let best_match = matcher.select_best_match(); @@ -727,7 +727,7 @@ mod tests { let buffer = TextBuffer::new(0, BufferId::new(1).unwrap(), text.clone()); let snapshot = buffer.snapshot(); - let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone()); + let mut matcher = StreamingFuzzyMatcher::new(snapshot); // Split query into random chunks let chunks = to_random_chunks(rng, query); diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 33d08b4f88..95b01c40eb 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -376,7 +376,7 @@ impl Tool for EditFileTool { let output = EditFileToolOutput { original_path: project_path.path.to_path_buf(), - new_text: new_text.clone(), + new_text, old_text, raw_output: Some(agent_output), }; @@ -643,7 +643,7 @@ impl EditFileToolCard { diff }); - self.buffer = Some(buffer.clone()); + self.buffer = Some(buffer); self.base_text = Some(base_text.into()); self.buffer_diff = Some(buffer_diff.clone()); @@ -776,7 +776,6 @@ impl EditFileToolCard { let buffer_diff = cx.spawn({ let buffer = buffer.clone(); - let language_registry = language_registry.clone(); async move |_this, cx| { build_buffer_diff(base_text, &buffer, &language_registry, cx).await } @@ -863,7 +862,6 @@ impl ToolCard for EditFileToolCard { ) .on_click({ let path = self.path.clone(); - let workspace = workspace.clone(); move |_, window, cx| { workspace .update(cx, { diff --git a/crates/assistant_tools/src/grep_tool.rs b/crates/assistant_tools/src/grep_tool.rs index 1dd74b99e7..41dde5bbfe 100644 --- a/crates/assistant_tools/src/grep_tool.rs +++ b/crates/assistant_tools/src/grep_tool.rs @@ -327,7 +327,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), serde_json::json!({ @@ -415,7 +415,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), serde_json::json!({ @@ -494,7 +494,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); // Create test file with syntax structures fs.insert_tree( diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index 358d62ee1a..b28e55e78a 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -350,7 +350,7 @@ fn process_content( if is_empty { "Command executed successfully.".to_string() } else { - content.to_string() + content } } Some(exit_status) => { diff --git a/crates/assistant_tools/src/ui/tool_call_card_header.rs b/crates/assistant_tools/src/ui/tool_call_card_header.rs index b71453373f..b41f19432f 100644 --- a/crates/assistant_tools/src/ui/tool_call_card_header.rs +++ b/crates/assistant_tools/src/ui/tool_call_card_header.rs @@ -101,14 +101,11 @@ impl RenderOnce for ToolCallCardHeader { }) .when_some(secondary_text, |this, secondary_text| { this.child(bullet_divider()) - .child(div().text_size(font_size).child(secondary_text.clone())) + .child(div().text_size(font_size).child(secondary_text)) }) .when_some(code_path, |this, code_path| { - this.child(bullet_divider()).child( - Label::new(code_path.clone()) - .size(LabelSize::Small) - .inline_code(cx), - ) + this.child(bullet_divider()) + .child(Label::new(code_path).size(LabelSize::Small).inline_code(cx)) }) .with_animation( "loading-label", diff --git a/crates/assistant_tools/src/web_search_tool.rs b/crates/assistant_tools/src/web_search_tool.rs index 47a6958b7a..dbcca0a1f6 100644 --- a/crates/assistant_tools/src/web_search_tool.rs +++ b/crates/assistant_tools/src/web_search_tool.rs @@ -193,10 +193,7 @@ impl ToolCard for WebSearchToolCard { ) } }) - .on_click({ - let url = url.clone(); - move |_, _, cx| cx.open_url(&url) - }) + .on_click(move |_, _, cx| cx.open_url(&url)) })) .into_any(), ), diff --git a/crates/auto_update_ui/src/auto_update_ui.rs b/crates/auto_update_ui/src/auto_update_ui.rs index 63baef1f7d..7063dffd6d 100644 --- a/crates/auto_update_ui/src/auto_update_ui.rs +++ b/crates/auto_update_ui/src/auto_update_ui.rs @@ -114,7 +114,7 @@ fn view_release_notes_locally( cx, ); workspace.add_item_to_active_pane( - Box::new(markdown_preview.clone()), + Box::new(markdown_preview), None, true, window, diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 6b38fe5576..6a9ca026e7 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -175,12 +175,8 @@ impl BufferDiffSnapshot { if let Some(text) = &base_text { let base_text_rope = Rope::from(text.as_str()); base_text_pair = Some((text.clone(), base_text_rope.clone())); - let snapshot = language::Buffer::build_snapshot( - base_text_rope, - language.clone(), - language_registry.clone(), - cx, - ); + let snapshot = + language::Buffer::build_snapshot(base_text_rope, language, language_registry, cx); base_text_snapshot = cx.background_spawn(snapshot); base_text_exists = true; } else { @@ -957,7 +953,7 @@ impl BufferDiff { .buffer_range .start; let end = self - .hunks_intersecting_range_rev(range.clone(), buffer) + .hunks_intersecting_range_rev(range, buffer) .next()? .buffer_range .end; @@ -1441,7 +1437,7 @@ mod tests { .unindent(); let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text); - let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text.clone(), cx); + let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx); let mut uncommitted_diff = BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx); uncommitted_diff.secondary_diff = Some(Box::new(unstaged_diff)); diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index c92226eeeb..2a91433084 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -438,7 +438,7 @@ fn init_test(cx: &mut App) -> Entity<ChannelStore> { let clock = Arc::new(FakeSystemClock::new()); let http = FakeHttpClient::with_404_response(); - let client = Client::new(clock, http.clone(), cx); + let client = Client::new(clock, http, cx); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); client::init(&client, cx); diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 925d5ddefb..b84e7a9f7a 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -926,7 +926,7 @@ mod mac_os { fn path(&self) -> PathBuf { match self { - Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed").clone(), + Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"), Bundle::LocalPath { executable, .. } => executable.clone(), } } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 058a12417a..b6ce9d24e9 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -181,7 +181,7 @@ pub fn init(client: &Arc<Client>, cx: &mut App) { }); cx.on_action({ - let client = client.clone(); + let client = client; move |_: &Reconnect, cx| { if let Some(client) = client.upgrade() { cx.spawn(async move |cx| { @@ -791,7 +791,7 @@ impl Client { Arc::new(move |subscriber, envelope, client, cx| { let subscriber = subscriber.downcast::<E>().unwrap(); let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap(); - handler(subscriber, *envelope, client.clone(), cx).boxed_local() + handler(subscriber, *envelope, client, cx).boxed_local() }), ); if prev_handler.is_some() { @@ -2048,10 +2048,7 @@ mod tests { assert_eq!(*auth_count.lock(), 1); assert_eq!(*dropped_auth_count.lock(), 0); - let _authenticate = cx.spawn({ - let client = client.clone(); - |cx| async move { client.connect(false, &cx).await } - }); + let _authenticate = cx.spawn(|cx| async move { client.connect(false, &cx).await }); executor.run_until_parked(); assert_eq!(*auth_count.lock(), 2); assert_eq!(*dropped_auth_count.lock(), 1); diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 54b3d3f801..f3142a0af6 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -739,7 +739,7 @@ mod tests { ); // Third scan of worktree does not double report, as we already reported - test_project_discovery_helper(telemetry.clone(), vec!["package.json"], None, worktree_id); + test_project_discovery_helper(telemetry, vec!["package.json"], None, worktree_id); } #[gpui::test] @@ -751,7 +751,7 @@ mod tests { let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx)); test_project_discovery_helper( - telemetry.clone(), + telemetry, vec!["package.json", "pnpm-lock.yaml"], Some(vec!["node", "pnpm"]), 1, @@ -767,7 +767,7 @@ mod tests { let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx)); test_project_discovery_helper( - telemetry.clone(), + telemetry, vec!["package.json", "yarn.lock"], Some(vec!["node", "yarn"]), 1, @@ -786,7 +786,7 @@ mod tests { // project type for the same worktree multiple times test_project_discovery_helper( - telemetry.clone().clone(), + telemetry.clone(), vec!["global.json"], Some(vec!["dotnet"]), 1, diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index c500872fd7..da78a98069 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -280,7 +280,7 @@ pub async fn post_hang( service = "client", version = %report.app_version.unwrap_or_default().to_string(), os_name = %report.os_name, - os_version = report.os_version.unwrap_or_default().to_string(), + os_version = report.os_version.unwrap_or_default(), incident_id = %incident_id, installation_id = %report.installation_id.unwrap_or_default(), backtrace = %backtrace, diff --git a/crates/collab/src/auth.rs b/crates/collab/src/auth.rs index 5a2a1329bb..e484d6b510 100644 --- a/crates/collab/src/auth.rs +++ b/crates/collab/src/auth.rs @@ -236,7 +236,7 @@ mod test { #[gpui::test] async fn test_verify_access_token(cx: &mut gpui::TestAppContext) { - let test_db = crate::db::TestDb::sqlite(cx.executor().clone()); + let test_db = crate::db::TestDb::sqlite(cx.executor()); let db = test_db.db(); let user = db diff --git a/crates/collab/src/db/tests/embedding_tests.rs b/crates/collab/src/db/tests/embedding_tests.rs index 367e89f87b..5d8d69c030 100644 --- a/crates/collab/src/db/tests/embedding_tests.rs +++ b/crates/collab/src/db/tests/embedding_tests.rs @@ -8,7 +8,7 @@ use time::{Duration, OffsetDateTime, PrimitiveDateTime}; // SQLite does not support array arguments, so we only test this against a real postgres instance #[gpui::test] async fn test_get_embeddings_postgres(cx: &mut gpui::TestAppContext) { - let test_db = TestDb::postgres(cx.executor().clone()); + let test_db = TestDb::postgres(cx.executor()); let db = test_db.db(); let provider = "test_model"; @@ -38,7 +38,7 @@ async fn test_get_embeddings_postgres(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_purge_old_embeddings(cx: &mut gpui::TestAppContext) { - let test_db = TestDb::postgres(cx.executor().clone()); + let test_db = TestDb::postgres(cx.executor()); let db = test_db.db(); let model = "test_model"; diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 01f553edf2..06eb68610f 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -310,7 +310,7 @@ impl Server { let mut server = Self { id: parking_lot::Mutex::new(id), peer: Peer::new(id.0 as u32), - app_state: app_state.clone(), + app_state, connection_pool: Default::default(), handlers: Default::default(), teardown: watch::channel(false).0, @@ -1386,9 +1386,7 @@ async fn create_room( let live_kit = live_kit?; let user_id = session.user_id().to_string(); - let token = live_kit - .room_token(&livekit_room, &user_id.to_string()) - .trace_err()?; + let token = live_kit.room_token(&livekit_room, &user_id).trace_err()?; Some(proto::LiveKitConnectionInfo { server_url: live_kit.url().into(), @@ -2015,9 +2013,9 @@ async fn join_project( .unzip(); response.send(proto::JoinProjectResponse { project_id: project.id.0 as u64, - worktrees: worktrees.clone(), + worktrees, replica_id: replica_id.0 as u32, - collaborators: collaborators.clone(), + collaborators, language_servers, language_server_capabilities, role: project.role.into(), diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 7b95fdd458..4e7996ce3b 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -3593,7 +3593,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let abs_path = project_a.read_with(cx_a, |project, cx| { project .absolute_path(&project_path, cx) - .map(|path_buf| Arc::from(path_buf.to_owned())) + .map(Arc::from) .unwrap() }); @@ -3647,20 +3647,16 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let breakpoints_a = editor_a.update(cx_a, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); let breakpoints_b = editor_b.update(cx_b, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints_a.len()); @@ -3680,20 +3676,16 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let breakpoints_a = editor_a.update(cx_a, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); let breakpoints_b = editor_b.update(cx_b, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints_a.len()); @@ -3713,20 +3705,16 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let breakpoints_a = editor_a.update(cx_a, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); let breakpoints_b = editor_b.update(cx_b, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints_a.len()); @@ -3746,20 +3734,16 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let breakpoints_a = editor_a.update(cx_a, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); let breakpoints_b = editor_b.update(cx_b, |editor, cx| { editor .breakpoint_store() - .clone() .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(0, breakpoints_a.len()); diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index c283a9fcd1..6fcd6d75cd 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -266,7 +266,7 @@ impl RandomizedTest for RandomChannelBufferTest { "client {user_id} has different text than client {prev_user_id} for channel {channel_name}", ); } else { - prev_text = Some((user_id, text.clone())); + prev_text = Some((user_id, text)); } // Assert that all clients and the server agree about who is present in the diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index cd4cf69f60..ac5c4c54ca 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -643,7 +643,7 @@ impl RandomizedTest for ProjectCollaborationTest { ); let project = project.await?; - client.dev_server_projects_mut().push(project.clone()); + client.dev_server_projects_mut().push(project); } ClientOperation::CreateWorktreeEntry { diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index f1c0b2d182..fd5e3eefc1 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -370,8 +370,8 @@ impl TestServer { let client = TestClient { app_state, username: name.to_string(), - channel_store: cx.read(ChannelStore::global).clone(), - notification_store: cx.read(NotificationStore::global).clone(), + channel_store: cx.read(ChannelStore::global), + notification_store: cx.read(NotificationStore::global), state: Default::default(), }; client.wait_for_current_user(cx).await; diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 9993c0841c..61b3e05e48 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -66,7 +66,7 @@ impl ChannelView { channel_id, link_position, pane.clone(), - workspace.clone(), + workspace, window, cx, ); diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 5ed3907f6c..8aaf6c0aa2 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -1038,7 +1038,7 @@ impl Render for ChatPanel { .cloned(); el.when_some(reply_message, |el, reply_message| { - let user_being_replied_to = reply_message.sender.clone(); + let user_being_replied_to = reply_message.sender; el.child( h_flex() diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index b756984a09..cd37549783 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2507,7 +2507,7 @@ impl CollabPanel { let button = match section { Section::ActiveCall => channel_link.map(|channel_link| { - let channel_link_copy = channel_link.clone(); + let channel_link_copy = channel_link; IconButton::new("channel-link", IconName::Copy) .icon_size(IconSize::Small) .size(ButtonSize::None) @@ -2691,7 +2691,7 @@ impl CollabPanel { h_flex() .w_full() .justify_between() - .child(Label::new(github_login.clone())) + .child(Label::new(github_login)) .child(h_flex().children(controls)), ) .start_slot(Avatar::new(user.avatar_uri.clone())) @@ -3125,7 +3125,7 @@ impl Panel for CollabPanel { impl Focusable for CollabPanel { fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.filter_editor.focus_handle(cx).clone() + self.filter_editor.focus_handle(cx) } } diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index a900d585f8..bf6fc3b224 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -289,7 +289,7 @@ impl NotificationPanel { .gap_1() .size_full() .overflow_hidden() - .child(Label::new(text.clone())) + .child(Label::new(text)) .child( h_flex() .child( diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index b8800ff912..227d246f04 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -206,7 +206,7 @@ impl CommandPaletteDelegate { if parse_zed_link(&query, cx).is_some() { intercept_results = vec![CommandInterceptResult { action: OpenZedUrl { url: query.clone() }.boxed_clone(), - string: query.clone(), + string: query, positions: vec![], }] } diff --git a/crates/component/src/component_layout.rs b/crates/component/src/component_layout.rs index 58bf1d8f0c..a840d520a6 100644 --- a/crates/component/src/component_layout.rs +++ b/crates/component/src/component_layout.rs @@ -42,7 +42,7 @@ impl RenderOnce for ComponentExample { div() .text_size(rems(0.875)) .text_color(cx.theme().colors().text_muted) - .child(description.clone()), + .child(description), ) }), ) diff --git a/crates/context_server/src/listener.rs b/crates/context_server/src/listener.rs index 1b44cefbd2..4e5da2566e 100644 --- a/crates/context_server/src/listener.rs +++ b/crates/context_server/src/listener.rs @@ -112,7 +112,6 @@ impl McpServer { annotations: Some(tool.annotations()), }, handler: Box::new({ - let tool = tool.clone(); move |input_value, cx| { let input = match input_value { Some(input) => serde_json::from_value(input), diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 1916853a69..33455f5e52 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -81,10 +81,7 @@ pub fn init( }; copilot_chat::init(fs.clone(), http.clone(), configuration, cx); - let copilot = cx.new({ - let node_runtime = node_runtime.clone(); - move |cx| Copilot::start(new_server_id, fs, node_runtime, cx) - }); + let copilot = cx.new(move |cx| Copilot::start(new_server_id, fs, node_runtime, cx)); Copilot::set_global(copilot.clone(), cx); cx.observe(&copilot, |copilot, cx| { copilot.update(cx, |copilot, cx| copilot.update_action_visibilities(cx)); diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 2fd6df27b9..9308500ed4 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -1083,7 +1083,7 @@ mod tests { let replace_range_marker: TextRangeMarker = ('<', '>').into(); let (_, mut marked_ranges) = marked_text_ranges_by( marked_string, - vec![complete_from_marker.clone(), replace_range_marker.clone()], + vec![complete_from_marker, replace_range_marker.clone()], ); let replace_range = diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 131272da6b..c4338c6d00 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -664,7 +664,7 @@ impl ToolbarItemView for DapLogToolbarItemView { if let Some(item) = active_pane_item && let Some(log_view) = item.downcast::<DapLogView>() { - self.log_view = Some(log_view.clone()); + self.log_view = Some(log_view); return workspace::ToolbarItemLocation::PrimaryLeft; } self.log_view = None; diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 6c70a935e0..f81c1fff89 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -386,10 +386,10 @@ impl DebugPanel { return; }; - let dap_store_handle = self.project.read(cx).dap_store().clone(); + let dap_store_handle = self.project.read(cx).dap_store(); let label = curr_session.read(cx).label(); let quirks = curr_session.read(cx).quirks(); - let adapter = curr_session.read(cx).adapter().clone(); + let adapter = curr_session.read(cx).adapter(); let binary = curr_session.read(cx).binary().cloned().unwrap(); let task_context = curr_session.read(cx).task_context().clone(); @@ -447,9 +447,9 @@ impl DebugPanel { return; }; - let dap_store_handle = self.project.read(cx).dap_store().clone(); + let dap_store_handle = self.project.read(cx).dap_store(); let label = self.label_for_child_session(&parent_session, request, cx); - let adapter = parent_session.read(cx).adapter().clone(); + let adapter = parent_session.read(cx).adapter(); let quirks = parent_session.read(cx).quirks(); let Some(mut binary) = parent_session.read(cx).binary().cloned() else { log::error!("Attempted to start a child-session without a binary"); @@ -932,7 +932,6 @@ impl DebugPanel { .cloned(), |this, running_state| { this.children({ - let running_state = running_state.clone(); let threads = running_state.update(cx, |running_state, cx| { let session = running_state.session(); @@ -1645,7 +1644,6 @@ impl Render for DebugPanel { } }) .on_action({ - let this = this.clone(); move |_: &ToggleSessionPicker, window, cx| { this.update(cx, |this, cx| { this.toggle_session_picker(window, cx); diff --git a/crates/debugger_ui/src/debugger_ui.rs b/crates/debugger_ui/src/debugger_ui.rs index 5f5dfd1a1e..581cc16ff4 100644 --- a/crates/debugger_ui/src/debugger_ui.rs +++ b/crates/debugger_ui/src/debugger_ui.rs @@ -272,7 +272,6 @@ pub fn init(cx: &mut App) { } }) .on_action({ - let active_item = active_item.clone(); move |_: &ToggleIgnoreBreakpoints, _, cx| { active_item .update(cx, |item, cx| item.toggle_ignore_breakpoints(cx)) @@ -293,9 +292,8 @@ pub fn init(cx: &mut App) { let Some(debug_panel) = workspace.read(cx).panel::<DebugPanel>(cx) else { return; }; - let Some(active_session) = debug_panel - .clone() - .update(cx, |panel, _| panel.active_session()) + let Some(active_session) = + debug_panel.update(cx, |panel, _| panel.active_session()) else { return; }; diff --git a/crates/debugger_ui/src/dropdown_menus.rs b/crates/debugger_ui/src/dropdown_menus.rs index dca15eb052..c5399f6f69 100644 --- a/crates/debugger_ui/src/dropdown_menus.rs +++ b/crates/debugger_ui/src/dropdown_menus.rs @@ -272,10 +272,9 @@ impl DebugPanel { .child(session_entry.label_element(self_depth, cx)) .child( IconButton::new("close-debug-session", IconName::Close) - .visible_on_hover(id.clone()) + .visible_on_hover(id) .icon_size(IconSize::Small) .on_click({ - let weak = weak.clone(); move |_, window, cx| { weak.update(cx, |panel, cx| { panel.close_session(session_entity_id, window, cx); diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index eb0ad92dcc..b30e3995ff 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -785,7 +785,7 @@ impl RenderOnce for AttachMode { v_flex() .w_full() .track_focus(&self.attach_picker.focus_handle(cx)) - .child(self.attach_picker.clone()) + .child(self.attach_picker) } } diff --git a/crates/debugger_ui/src/persistence.rs b/crates/debugger_ui/src/persistence.rs index f0d7fd6fdd..cff2ba8335 100644 --- a/crates/debugger_ui/src/persistence.rs +++ b/crates/debugger_ui/src/persistence.rs @@ -256,7 +256,7 @@ pub(crate) fn deserialize_pane_layout( Some(Member::Axis(PaneAxis::load( if should_invert { axis.invert() } else { axis }, members, - flexes.clone(), + flexes, ))) } SerializedPaneLayout::Pane(serialized_pane) => { diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index e3682ac991..4306104877 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -180,7 +180,7 @@ impl SubView { let weak_list = list.downgrade(); let focus_handle = list.focus_handle(cx); let this = Self::new( - focus_handle.clone(), + focus_handle, list.into(), DebuggerPaneItem::BreakpointList, cx, @@ -1167,9 +1167,9 @@ impl RunningState { id: task::TaskId("debug".to_string()), full_label: title.clone(), label: title.clone(), - command: command.clone(), + command, args, - command_label: title.clone(), + command_label: title, cwd, env: envs, use_new_terminal: true, @@ -1756,7 +1756,7 @@ impl RunningState { this.activate_item(0, false, false, window, cx); }); - let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx); + let rightmost_pane = new_debugger_pane(workspace.clone(), project, window, cx); rightmost_pane.update(cx, |this, cx| { this.add_item( Box::new(SubView::new( diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index c17fffc42c..d04443e201 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -685,7 +685,6 @@ impl BreakpointList { selection_kind.map(|kind| kind.0) != Some(SelectedBreakpointKind::Source), ) .on_click({ - let focus_handle = focus_handle.clone(); move |_, window, cx| { focus_handle.focus(window); window.dispatch_action(UnsetBreakpoint.boxed_clone(), cx) @@ -1139,7 +1138,6 @@ impl ExceptionBreakpoint { } }) .on_click({ - let list = list.clone(); move |_, _, cx| { list.update(cx, |this, cx| { this.toggle_exception_breakpoint(&id, cx); diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index 05d2231da4..a801cedd26 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -365,7 +365,7 @@ impl Console { Some(ContextMenu::build(window, cx, |context_menu, _, _| { context_menu .when_some(keybinding_target.clone(), |el, keybinding_target| { - el.context(keybinding_target.clone()) + el.context(keybinding_target) }) .action("Watch Expression", WatchExpression.boxed_clone()) })) diff --git a/crates/debugger_ui/src/session/running/loaded_source_list.rs b/crates/debugger_ui/src/session/running/loaded_source_list.rs index 6b376bb892..921ebd8b5f 100644 --- a/crates/debugger_ui/src/session/running/loaded_source_list.rs +++ b/crates/debugger_ui/src/session/running/loaded_source_list.rs @@ -57,7 +57,7 @@ impl LoadedSourceList { h_flex() .text_ui_xs(cx) .text_color(cx.theme().colors().text_muted) - .when_some(source.path.clone(), |this, path| this.child(path)), + .when_some(source.path, |this, path| this.child(path)), ) .into_any() } diff --git a/crates/debugger_ui/src/session/running/memory_view.rs b/crates/debugger_ui/src/session/running/memory_view.rs index a09df6e728..e7b7963d3f 100644 --- a/crates/debugger_ui/src/session/running/memory_view.rs +++ b/crates/debugger_ui/src/session/running/memory_view.rs @@ -461,7 +461,7 @@ impl MemoryView { let data_breakpoint_info = this.data_breakpoint_info(context.clone(), None, cx); cx.spawn(async move |this, cx| { if let Some(info) = data_breakpoint_info.await { - let Some(data_id) = info.data_id.clone() else { + let Some(data_id) = info.data_id else { return; }; _ = this.update(cx, |this, cx| { diff --git a/crates/debugger_ui/src/session/running/module_list.rs b/crates/debugger_ui/src/session/running/module_list.rs index 74a9fb457a..1c1e0f3efc 100644 --- a/crates/debugger_ui/src/session/running/module_list.rs +++ b/crates/debugger_ui/src/session/running/module_list.rs @@ -157,7 +157,7 @@ impl ModuleList { h_flex() .text_ui_xs(cx) .text_color(cx.theme().colors().text_muted) - .when_some(module.path.clone(), |this, path| this.child(path)), + .when_some(module.path, |this, path| this.child(path)), ) .into_any() } diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index 8b44c231c3..f9b5ed5e3f 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -126,7 +126,7 @@ impl StackFrameList { self.stack_frames(cx) .unwrap_or_default() .into_iter() - .map(|stack_frame| stack_frame.dap.clone()) + .map(|stack_frame| stack_frame.dap) .collect() } @@ -224,7 +224,7 @@ impl StackFrameList { let collapsed_entries = std::mem::take(&mut collapsed_entries); if !collapsed_entries.is_empty() { - entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone())); + entries.push(StackFrameEntry::Collapsed(collapsed_entries)); } self.entries = entries; @@ -418,7 +418,7 @@ impl StackFrameList { let source = stack_frame.source.clone(); let is_selected_frame = Some(ix) == self.selected_ix; - let path = source.clone().and_then(|s| s.path.or(s.name)); + let path = source.and_then(|s| s.path.or(s.name)); let formatted_path = path.map(|path| format!("{}:{}", path, stack_frame.line,)); let formatted_path = formatted_path.map(|path| { Label::new(path) diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index 7461bffdf9..18f574389e 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -313,7 +313,7 @@ impl VariableList { watcher.variables_reference, watcher.variables_reference, EntryPath::for_watcher(watcher.expression.clone()), - DapEntry::Watcher(watcher.clone()), + DapEntry::Watcher(watcher), ) }) .collect::<Vec<_>>(), @@ -1301,8 +1301,6 @@ impl VariableList { IconName::Close, ) .on_click({ - let weak = weak.clone(); - let path = path.clone(); move |_, window, cx| { weak.update(cx, |variable_list, cx| { variable_list.selection = Some(path.clone()); @@ -1470,7 +1468,6 @@ impl VariableList { })) }) .on_secondary_mouse_down(cx.listener({ - let path = path.clone(); let entry = variable.clone(); move |this, event: &MouseDownEvent, window, cx| { this.selection = Some(path.clone()); diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 6180831ea9..ab6d5cb960 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -1330,7 +1330,6 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action( let called_set_breakpoints = Arc::new(AtomicBool::new(false)); client.on_request::<SetBreakpoints, _>({ - let called_set_breakpoints = called_set_breakpoints.clone(); move |_, args| { assert!( args.breakpoints.is_none_or(|bps| bps.is_empty()), @@ -1445,7 +1444,6 @@ async fn test_we_send_arguments_from_user_config( let launch_handler_called = Arc::new(AtomicBool::new(false)); start_debug_session_with(&workspace, cx, debug_definition.clone(), { - let debug_definition = debug_definition.clone(); let launch_handler_called = launch_handler_called.clone(); move |client| { @@ -1783,9 +1781,8 @@ async fn test_debug_adapters_shutdown_on_app_quit( let disconnect_request_received = Arc::new(AtomicBool::new(false)); let disconnect_clone = disconnect_request_received.clone(); - let disconnect_clone_for_handler = disconnect_clone.clone(); client.on_request::<Disconnect, _>(move |_, _| { - disconnect_clone_for_handler.store(true, Ordering::SeqCst); + disconnect_clone.store(true, Ordering::SeqCst); Ok(()) }); diff --git a/crates/debugger_ui/src/tests/new_process_modal.rs b/crates/debugger_ui/src/tests/new_process_modal.rs index 5ac6af389d..bfc445cf67 100644 --- a/crates/debugger_ui/src/tests/new_process_modal.rs +++ b/crates/debugger_ui/src/tests/new_process_modal.rs @@ -106,9 +106,7 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths( ); let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") { - input_path - .replace("$ZED_WORKTREE_ROOT", path!("/test/worktree/path")) - .to_owned() + input_path.replace("$ZED_WORKTREE_ROOT", path!("/test/worktree/path")) } else { input_path.to_string() }; diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index 99e588ada9..33158577c4 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -61,15 +61,13 @@ impl PreprocessorError { for alias in action.deprecated_aliases { if alias == &action_name { return PreprocessorError::DeprecatedActionUsed { - used: action_name.clone(), + used: action_name, should_be: action.name.to_string(), }; } } } - PreprocessorError::ActionNotFound { - action_name: action_name.to_string(), - } + PreprocessorError::ActionNotFound { action_name } } } diff --git a/crates/edit_prediction_button/src/edit_prediction_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs index 21c934fefa..4f69af7ee4 100644 --- a/crates/edit_prediction_button/src/edit_prediction_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -168,7 +168,7 @@ impl Render for EditPredictionButton { let account_status = agent.account_status.clone(); match account_status { AccountStatus::NeedsActivation { activate_url } => { - SupermavenButtonStatus::NeedsActivation(activate_url.clone()) + SupermavenButtonStatus::NeedsActivation(activate_url) } AccountStatus::Unknown => SupermavenButtonStatus::Initializing, AccountStatus::Ready => SupermavenButtonStatus::Ready, diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 4847bc2565..96809d6877 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -514,7 +514,7 @@ impl CompletionsMenu { // Expand the range to resolve more completions than are predicted to be visible, to reduce // jank on navigation. let entry_indices = util::expanded_and_wrapped_usize_range( - entry_range.clone(), + entry_range, RESOLVE_BEFORE_ITEMS, RESOLVE_AFTER_ITEMS, entries.len(), diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 0d31398a54..1e0cdc34ac 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -2156,7 +2156,7 @@ mod tests { } let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font, font_size, Some(wrap_width), cx); @@ -2275,7 +2275,7 @@ mod tests { new_heights.insert(block_ids[0], 3); block_map_writer.resize(new_heights); - let snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); + let snapshot = block_map.read(wraps_snapshot, Default::default()); // Same height as before, should remain the same assert_eq!(snapshot.text(), "aaa\n\n\n\n\n\nbbb\nccc\nddd\n\n\n"); } @@ -2360,16 +2360,14 @@ mod tests { buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx); buffer.snapshot(cx) }); - let (inlay_snapshot, inlay_edits) = inlay_map.sync( - buffer_snapshot.clone(), - buffer_subscription.consume().into_inner(), - ); + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot, buffer_subscription.consume().into_inner()); let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { wrap_map.sync(tab_snapshot, tab_edits, cx) }); - let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits); + let blocks_snapshot = block_map.read(wraps_snapshot, wrap_edits); assert_eq!(blocks_snapshot.text(), "line1\n\n\n\n\nline5"); let buffer_snapshot = buffer.update(cx, |buffer, cx| { @@ -2454,7 +2452,7 @@ mod tests { // Removing the replace block shows all the hidden blocks again. let mut writer = block_map.write(wraps_snapshot.clone(), Default::default()); writer.remove(HashSet::from_iter([replace_block_id])); - let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); + let blocks_snapshot = block_map.read(wraps_snapshot, Default::default()); assert_eq!( blocks_snapshot.text(), "\nline1\n\nline2\n\n\nline 2.1\nline2.2\nline 2.3\nline 2.4\n\nline4\n\nline5" @@ -2793,7 +2791,7 @@ mod tests { buffer.read_with(cx, |buffer, cx| { writer.fold_buffers([buffer_id_3], buffer, cx); }); - let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default()); + let blocks_snapshot = block_map.read(wrap_snapshot, Patch::default()); let blocks = blocks_snapshot .blocks_in_range(0..u32::MAX) .collect::<Vec<_>>(); @@ -2846,7 +2844,7 @@ mod tests { assert_eq!(buffer_ids.len(), 1); let buffer_id = buffer_ids[0]; - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); let (_, wrap_snapshot) = @@ -2860,7 +2858,7 @@ mod tests { buffer.read_with(cx, |buffer, cx| { writer.fold_buffers([buffer_id], buffer, cx); }); - let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default()); + let blocks_snapshot = block_map.read(wrap_snapshot, Patch::default()); let blocks = blocks_snapshot .blocks_in_range(0..u32::MAX) .collect::<Vec<_>>(); @@ -3527,7 +3525,7 @@ mod tests { ..buffer_snapshot.anchor_after(Point::new(1, 0))], false, ); - let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); + let blocks_snapshot = block_map.read(wraps_snapshot, Default::default()); assert_eq!(blocks_snapshot.text(), "abc\n\ndef\nghi\njkl\nmno"); } diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 3dcd172c3c..42f46fb749 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1557,7 +1557,7 @@ mod tests { let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot); let mut map = FoldMap::new(inlay_snapshot.clone()).0; let (mut writer, _, _) = map.write(inlay_snapshot, vec![]); @@ -1636,7 +1636,7 @@ mod tests { let buffer = MultiBuffer::build_simple("abcdefghijkl", cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot); { let mut map = FoldMap::new(inlay_snapshot.clone()).0; @@ -1712,7 +1712,7 @@ mod tests { let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot); let mut map = FoldMap::new(inlay_snapshot.clone()).0; let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); @@ -1720,7 +1720,7 @@ mod tests { (Point::new(0, 2)..Point::new(2, 2), FoldPlaceholder::test()), (Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()), ]); - let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); + let (snapshot, _) = map.read(inlay_snapshot, vec![]); assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee"); let buffer_snapshot = buffer.update(cx, |buffer, cx| { @@ -1747,7 +1747,7 @@ mod tests { (Point::new(1, 2)..Point::new(3, 2), FoldPlaceholder::test()), (Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()), ]); - let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); + let (snapshot, _) = map.read(inlay_snapshot, vec![]); let fold_ranges = snapshot .folds_in_range(Point::new(1, 0)..Point::new(1, 3)) .map(|fold| { @@ -1782,7 +1782,7 @@ mod tests { let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); let mut map = FoldMap::new(inlay_snapshot.clone()).0; - let (mut initial_snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); + let (mut initial_snapshot, _) = map.read(inlay_snapshot, vec![]); let mut snapshot_edits = Vec::new(); let mut next_inlay_id = 0; diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index eb5d57d484..6f5df9bb8e 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -116,7 +116,7 @@ impl TabMap { state.new.end = edit.new.end; Some(None) // Skip this edit, it's merged } else { - let new_state = edit.clone(); + let new_state = edit; let result = Some(Some(state.clone())); // Yield the previous edit **state = new_state; result @@ -611,7 +611,7 @@ mod tests { fn test_expand_tabs(cx: &mut gpui::App) { let buffer = MultiBuffer::build_simple("", cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -628,7 +628,7 @@ mod tests { let buffer = MultiBuffer::build_simple(input, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -675,7 +675,7 @@ mod tests { let buffer = MultiBuffer::build_simple(input, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -689,7 +689,7 @@ mod tests { let buffer = MultiBuffer::build_simple(input, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -749,7 +749,7 @@ mod tests { let buffer_snapshot = buffer.read(cx).snapshot(cx); log::info!("Buffer text: {:?}", buffer_snapshot.text()); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot); log::info!("InlayMap text: {:?}", inlay_snapshot.text()); let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone()); fold_map.randomly_mutate(&mut rng); @@ -758,7 +758,7 @@ mod tests { let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng); log::info!("InlayMap text: {:?}", inlay_snapshot.text()); - let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); + let (mut tab_map, _) = TabMap::new(fold_snapshot, tab_size); let tabs_snapshot = tab_map.set_max_expansion_column(32); let text = text::Rope::from(tabs_snapshot.text().as_str()); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2f3ced65dc..5fc017dcfc 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4528,7 +4528,7 @@ impl Editor { let mut char_position = 0u32; let mut end_tag_offset = None; - 'outer: for chunk in snapshot.text_for_range(range.clone()) { + 'outer: for chunk in snapshot.text_for_range(range) { if let Some(byte_pos) = chunk.find(&**end_tag) { let chars_before_match = chunk[..byte_pos].chars().count() as u32; @@ -4881,7 +4881,7 @@ impl Editor { let multibuffer = self.buffer.read(cx); let Some(buffer) = position .buffer_id - .and_then(|buffer_id| multibuffer.buffer(buffer_id).clone()) + .and_then(|buffer_id| multibuffer.buffer(buffer_id)) else { return false; }; @@ -6269,7 +6269,7 @@ impl Editor { })) } CodeActionsItem::DebugScenario(scenario) => { - let context = actions_menu.actions.context.clone(); + let context = actions_menu.actions.context; workspace.update(cx, |workspace, cx| { dap::send_telemetry(&scenario, TelemetrySpawnLocation::Gutter, cx); @@ -6469,7 +6469,7 @@ impl Editor { fn refresh_code_actions(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<()> { let newest_selection = self.selections.newest_anchor().clone(); - let newest_selection_adjusted = self.selections.newest_adjusted(cx).clone(); + let newest_selection_adjusted = self.selections.newest_adjusted(cx); let buffer = self.buffer.read(cx); if newest_selection.head().diff_base_anchor.is_some() { return None; @@ -8188,8 +8188,6 @@ impl Editor { .icon_color(color) .style(ButtonStyle::Transparent) .on_click(cx.listener({ - let breakpoint = breakpoint.clone(); - move |editor, event: &ClickEvent, window, cx| { let edit_action = if event.modifiers().platform || breakpoint.is_disabled() { BreakpointEditAction::InvertState @@ -14837,7 +14835,7 @@ impl Editor { if parent == child { return None; } - let text = buffer.text_for_range(child.clone()).collect::<String>(); + let text = buffer.text_for_range(child).collect::<String>(); Some((selection.id, parent, text)) }) .collect::<Vec<_>>(); @@ -15940,7 +15938,7 @@ impl Editor { if !split && Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() { - editor.go_to_singleton_buffer_range(range.clone(), window, cx); + editor.go_to_singleton_buffer_range(range, window, cx); } else { window.defer(cx, move |window, cx| { let target_editor: Entity<Self> = @@ -16198,14 +16196,14 @@ impl Editor { let item_id = item.item_id(); if split { - workspace.split_item(SplitDirection::Right, item.clone(), window, cx); + workspace.split_item(SplitDirection::Right, item, window, cx); } else if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation { let (preview_item_id, preview_item_idx) = workspace.active_pane().read_with(cx, |pane, _| { (pane.preview_item_id(), pane.preview_item_idx()) }); - workspace.add_item_to_active_pane(item.clone(), preview_item_idx, true, window, cx); + workspace.add_item_to_active_pane(item, preview_item_idx, true, window, cx); if let Some(preview_item_id) = preview_item_id { workspace.active_pane().update(cx, |pane, cx| { @@ -16213,7 +16211,7 @@ impl Editor { }); } } else { - workspace.add_item_to_active_pane(item.clone(), None, true, window, cx); + workspace.add_item_to_active_pane(item, None, true, window, cx); } workspace.active_pane().update(cx, |pane, cx| { pane.set_preview_item_id(Some(item_id), cx); @@ -19004,10 +19002,7 @@ impl Editor { let selection = text::ToPoint::to_point(&range.start, buffer).row ..text::ToPoint::to_point(&range.end, buffer).row; - Some(( - multi_buffer.buffer(buffer.remote_id()).unwrap().clone(), - selection, - )) + Some((multi_buffer.buffer(buffer.remote_id()).unwrap(), selection)) }); let Some((buffer, selection)) = buffer_and_selection else { @@ -19249,7 +19244,7 @@ impl Editor { row_highlights.insert( ix, RowHighlight { - range: range.clone(), + range, index, color, options, @@ -21676,7 +21671,7 @@ fn wrap_with_prefix( let subsequent_lines_prefix_len = char_len_with_expanded_tabs(0, &subsequent_lines_prefix, tab_size); let mut wrapped_text = String::new(); - let mut current_line = first_line_prefix.clone(); + let mut current_line = first_line_prefix; let mut is_first_line = true; let tokenizer = WordBreakingTokenizer::new(&unwrapped_text); diff --git a/crates/editor/src/editor_settings_controls.rs b/crates/editor/src/editor_settings_controls.rs index dc5557b052..91022d94a8 100644 --- a/crates/editor/src/editor_settings_controls.rs +++ b/crates/editor/src/editor_settings_controls.rs @@ -88,7 +88,7 @@ impl RenderOnce for BufferFontFamilyControl { .child(Icon::new(IconName::Font)) .child(DropdownMenu::new( "buffer-font-family", - value.clone(), + value, ContextMenu::build(window, cx, |mut menu, _, cx| { let font_family_cache = FontFamilyCache::global(cx); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 1f1239ba0a..955ade04cd 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -708,7 +708,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) { _ = workspace.update(cx, |_v, window, cx| { cx.new(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); - let mut editor = build_editor(buffer.clone(), window, cx); + let mut editor = build_editor(buffer, window, cx); let handle = cx.entity(); editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle))); @@ -898,7 +898,7 @@ fn test_fold_action(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -989,7 +989,7 @@ fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -1074,7 +1074,7 @@ fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -1173,7 +1173,7 @@ fn test_fold_at_level(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -1335,7 +1335,7 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); assert_eq!('🟥'.len_utf8(), 4); @@ -1452,7 +1452,7 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { @@ -2479,7 +2479,7 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("one two three four", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -2527,7 +2527,7 @@ fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); let del_to_prev_word_start = DeleteToPreviousWordStart { ignore_newlines: false, @@ -2563,7 +2563,7 @@ fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); let del_to_next_word_end = DeleteToNextWordEnd { ignore_newlines: false, @@ -2608,7 +2608,7 @@ fn test_newline(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -2644,7 +2644,7 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) { .as_str(), cx, ); - let mut editor = build_editor(buffer.clone(), window, cx); + let mut editor = build_editor(buffer, window, cx); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([ Point::new(2, 4)..Point::new(2, 5), @@ -3175,7 +3175,7 @@ fn test_insert_with_old_selections(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); - let mut editor = build_editor(buffer.clone(), window, cx); + let mut editor = build_editor(buffer, window, cx); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([3..4, 11..12, 19..20]) }); @@ -5562,7 +5562,7 @@ async fn test_rewrap(cx: &mut TestAppContext) { # ˇThis is a long comment using a pound # sign. "}, - python_language.clone(), + python_language, &mut cx, ); @@ -5669,7 +5669,7 @@ async fn test_rewrap(cx: &mut TestAppContext) { also very long and should not merge with the numbered item.ˇ» "}, - markdown_language.clone(), + markdown_language, &mut cx, ); @@ -5700,7 +5700,7 @@ async fn test_rewrap(cx: &mut TestAppContext) { // This is the second long comment block // to be wrapped.ˇ» "}, - rust_language.clone(), + rust_language, &mut cx, ); @@ -5723,7 +5723,7 @@ async fn test_rewrap(cx: &mut TestAppContext) { «\tThis is a very long indented line \tthat will be wrapped.ˇ» "}, - plaintext_language.clone(), + plaintext_language, &mut cx, ); @@ -8889,7 +8889,7 @@ async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) { )); cx.language_registry().add(html_language.clone()); - cx.language_registry().add(javascript_language.clone()); + cx.language_registry().add(javascript_language); cx.executor().run_until_parked(); cx.update_buffer(|buffer, cx| { @@ -9633,7 +9633,7 @@ async fn test_snippets(cx: &mut TestAppContext) { .selections .all(cx) .iter() - .map(|s| s.range().clone()) + .map(|s| s.range()) .collect::<Vec<_>>(); editor .insert_snippet(&insertion_ranges, snippet, window, cx) @@ -9713,7 +9713,7 @@ async fn test_snippet_indentation(cx: &mut TestAppContext) { .selections .all(cx) .iter() - .map(|s| s.range().clone()) + .map(|s| s.range()) .collect::<Vec<_>>(); editor .insert_snippet(&insertion_ranges, snippet, window, cx) @@ -10782,7 +10782,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { kind: Some("code-action-2".into()), edit: Some(lsp::WorkspaceEdit::new( [( - uri.clone(), + uri, vec![lsp::TextEdit::new( lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), "applied-code-action-2-edit\n".to_string(), @@ -14366,7 +14366,7 @@ async fn test_toggle_block_comment(cx: &mut TestAppContext) { )); cx.language_registry().add(html_language.clone()); - cx.language_registry().add(javascript_language.clone()); + cx.language_registry().add(javascript_language); cx.update_buffer(|buffer, cx| { buffer.set_language(Some(html_language), cx); }); @@ -14543,7 +14543,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { ); let excerpt_ranges = markers.into_iter().map(|marker| { let context = excerpt_ranges.remove(&marker).unwrap()[0].clone(); - ExcerptRange::new(context.clone()) + ExcerptRange::new(context) }); let buffer = cx.new(|cx| Buffer::local(initial_text, cx)); let multibuffer = cx.new(|cx| { @@ -14828,7 +14828,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -15750,8 +15750,7 @@ async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppCon cx.simulate_keystroke("\n"); cx.run_until_parked(); - let buffer_cloned = - cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone()); + let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap()); let mut request = cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| { let buffer_cloned = buffer_cloned.clone(); @@ -19455,7 +19454,7 @@ async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestApp let buffer_id = hunks[0].buffer_id; hunks .into_iter() - .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone())) + .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range)) .collect::<Vec<_>>() }); assert_eq!(hunk_ranges.len(), 2); @@ -19546,7 +19545,7 @@ async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestApp let buffer_id = hunks[0].buffer_id; hunks .into_iter() - .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone())) + .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range)) .collect::<Vec<_>>() }); assert_eq!(hunk_ranges.len(), 2); @@ -19612,7 +19611,7 @@ async fn test_toggle_deletion_hunk_at_start_of_file( let buffer_id = hunks[0].buffer_id; hunks .into_iter() - .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone())) + .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range)) .collect::<Vec<_>>() }); assert_eq!(hunk_ranges.len(), 1); @@ -19635,7 +19634,7 @@ async fn test_toggle_deletion_hunk_at_start_of_file( }); executor.run_until_parked(); - cx.assert_state_with_diff(hunk_expanded.clone()); + cx.assert_state_with_diff(hunk_expanded); } #[gpui::test] @@ -21150,7 +21149,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { let abs_path = project.read_with(cx, |project, cx| { project .absolute_path(&project_path, cx) - .map(|path_buf| Arc::from(path_buf.to_owned())) + .map(Arc::from) .unwrap() }); @@ -21168,7 +21167,6 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints.len()); @@ -21193,7 +21191,6 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints.len()); @@ -21215,7 +21212,6 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(0, breakpoints.len()); @@ -21267,7 +21263,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { let abs_path = project.read_with(cx, |project, cx| { project .absolute_path(&project_path, cx) - .map(|path_buf| Arc::from(path_buf.to_owned())) + .map(Arc::from) .unwrap() }); @@ -21282,7 +21278,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_breakpoint( @@ -21303,7 +21298,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_breakpoint(&breakpoints, &abs_path, vec![]); @@ -21323,7 +21317,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_breakpoint( @@ -21346,7 +21339,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_breakpoint( @@ -21369,7 +21361,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_breakpoint( @@ -21442,7 +21433,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { let abs_path = project.read_with(cx, |project, cx| { project .absolute_path(&project_path, cx) - .map(|path_buf| Arc::from(path_buf.to_owned())) + .map(Arc::from) .unwrap() }); @@ -21462,7 +21453,6 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints.len()); @@ -21494,7 +21484,6 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); let disable_breakpoint = { @@ -21530,7 +21519,6 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints.len()); @@ -22509,10 +22497,7 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) { let closing_range = buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8)); let mut linked_ranges = HashMap::default(); - linked_ranges.insert( - buffer_id, - vec![(opening_range.clone(), vec![closing_range.clone()])], - ); + linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]); editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges); }); let mut completion_handle = diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index f1ebd2c3df..b18d1ceae1 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -7209,7 +7209,7 @@ fn render_blame_entry_popover( ) -> Option<AnyElement> { let renderer = cx.global::<GlobalBlameRenderer>().0.clone(); let blame = blame.read(cx); - let repository = blame.repository(cx)?.clone(); + let repository = blame.repository(cx)?; renderer.render_blame_entry_popover( blame_entry, scroll_handle, @@ -9009,7 +9009,7 @@ impl Element for EditorElement { .as_ref() .map(|layout| (layout.bounds, layout.entry.clone())), display_hunks: display_hunks.clone(), - diff_hunk_control_bounds: diff_hunk_control_bounds.clone(), + diff_hunk_control_bounds, }); self.editor.update(cx, |editor, _| { @@ -9894,7 +9894,7 @@ impl CursorLayout { .px_0p5() .line_height(text_size + px(2.)) .text_color(cursor_name.color) - .child(cursor_name.string.clone()) + .child(cursor_name.string) .into_any_element(); name_element.prepaint_as_root(name_origin, AvailableSpace::min_size(), window, cx); diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index bb3fd2830d..497f193cb4 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -623,7 +623,7 @@ pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { let mut base_text_style = window.text_style(); base_text_style.refine(&TextStyleRefinement { - font_family: Some(ui_font_family.clone()), + font_family: Some(ui_font_family), font_fallbacks: ui_font_fallbacks, color: Some(cx.theme().colors().editor_foreground), ..Default::default() @@ -672,7 +672,7 @@ pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { let mut base_text_style = window.text_style(); base_text_style.refine(&TextStyleRefinement { - font_family: Some(ui_font_family.clone()), + font_family: Some(ui_font_family), font_fallbacks: ui_font_fallbacks, color: Some(cx.theme().colors().editor_foreground), ..Default::default() diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index a3fc41228f..83ab02814f 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -507,7 +507,7 @@ pub(crate) fn handle_from( { let selections = this - .read_with(cx, |this, _| this.selections.disjoint_anchors().clone()) + .read_with(cx, |this, _| this.selections.disjoint_anchors()) .ok()?; for selection in selections.iter() { let Some(selection_buffer_offset_head) = diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index dbb519c40e..88721c59e7 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -119,13 +119,7 @@ impl EditorTestContext { for excerpt in excerpts.into_iter() { let (text, ranges) = marked_text_ranges(excerpt, false); let buffer = cx.new(|cx| Buffer::local(text, cx)); - multibuffer.push_excerpts( - buffer, - ranges - .into_iter() - .map(|range| ExcerptRange::new(range.clone())), - cx, - ); + multibuffer.push_excerpts(buffer, ranges.into_iter().map(ExcerptRange::new), cx); } multibuffer }); diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index 1d2bece5cc..c5a072eea1 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -103,7 +103,7 @@ fn main() { let languages: HashSet<String> = args.languages.into_iter().collect(); let http_client = Arc::new(ReqwestClient::new()); - let app = Application::headless().with_http_client(http_client.clone()); + let app = Application::headless().with_http_client(http_client); let all_threads = examples::all(&examples_dir); app.run(move |cx| { @@ -416,11 +416,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> { language::init(cx); debug_adapter_extension::init(extension_host_proxy.clone(), cx); - language_extension::init( - LspAccess::Noop, - extension_host_proxy.clone(), - languages.clone(), - ); + language_extension::init(LspAccess::Noop, extension_host_proxy, languages.clone()); language_model::init(client.clone(), cx); language_models::init(user_store.clone(), client.clone(), cx); languages::init(languages.clone(), node_runtime.clone(), cx); @@ -530,7 +526,7 @@ async fn judge_example( example_name = example.name.clone(), example_repetition = example.repetition, diff_evaluation = judge_output.diff.clone(), - thread_evaluation = judge_output.thread.clone(), + thread_evaluation = judge_output.thread, tool_metrics = run_output.tool_metrics, response_count = run_output.response_count, token_usage = run_output.token_usage, diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs index 53ce6088c0..074cb121d3 100644 --- a/crates/eval/src/instance.rs +++ b/crates/eval/src/instance.rs @@ -90,11 +90,8 @@ impl ExampleInstance { worktrees_dir: &Path, repetition: usize, ) -> Self { - let name = thread.meta().name.to_string(); - let run_directory = run_dir - .join(&name) - .join(repetition.to_string()) - .to_path_buf(); + let name = thread.meta().name; + let run_directory = run_dir.join(&name).join(repetition.to_string()); let repo_path = repo_path_for_url(repos_dir, &thread.meta().url); @@ -772,7 +769,7 @@ pub async fn query_lsp_diagnostics( } fn parse_assertion_result(response: &str) -> Result<RanAssertionResult> { - let analysis = get_tag("analysis", response)?.to_string(); + let analysis = get_tag("analysis", response)?; let passed = match get_tag("passed", response)?.to_lowercase().as_str() { "true" => true, "false" => false, diff --git a/crates/extension_host/src/capability_granter.rs b/crates/extension_host/src/capability_granter.rs index 5a2093c1dd..5491967e08 100644 --- a/crates/extension_host/src/capability_granter.rs +++ b/crates/extension_host/src/capability_granter.rs @@ -145,7 +145,7 @@ mod tests { command: "*".to_string(), args: vec!["**".to_string()], })], - manifest.clone(), + manifest, ); assert!(granter.grant_exec("ls", &["-la"]).is_ok()); } diff --git a/crates/extensions_ui/src/components/feature_upsell.rs b/crates/extensions_ui/src/components/feature_upsell.rs index 573b0b992d..0515dd46d3 100644 --- a/crates/extensions_ui/src/components/feature_upsell.rs +++ b/crates/extensions_ui/src/components/feature_upsell.rs @@ -61,7 +61,6 @@ impl RenderOnce for FeatureUpsell { .icon_size(IconSize::Small) .icon_position(IconPosition::End) .on_click({ - let docs_url = docs_url.clone(); move |_event, _window, cx| { telemetry::event!( "Documentation Viewed", diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index a6ee84eb60..fd504764b6 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -694,7 +694,7 @@ impl ExtensionsPage { cx.open_url(&repository_url); } })) - .tooltip(Tooltip::text(repository_url.clone())) + .tooltip(Tooltip::text(repository_url)) })), ) } @@ -827,7 +827,7 @@ impl ExtensionsPage { cx.open_url(&repository_url); } })) - .tooltip(Tooltip::text(repository_url.clone())), + .tooltip(Tooltip::text(repository_url)), ) .child( PopoverMenu::new(SharedString::from(format!( diff --git a/crates/feedback/src/system_specs.rs b/crates/feedback/src/system_specs.rs index b5ccaca689..87642ab929 100644 --- a/crates/feedback/src/system_specs.rs +++ b/crates/feedback/src/system_specs.rs @@ -31,7 +31,7 @@ impl SystemSpecs { let architecture = env::consts::ARCH; let commit_sha = match release_channel { ReleaseChannel::Dev | ReleaseChannel::Nightly => { - AppCommitSha::try_global(cx).map(|sha| sha.full().clone()) + AppCommitSha::try_global(cx).map(|sha| sha.full()) } _ => None, }; diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 3a08ec08e0..40acf012c9 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1750,7 +1750,7 @@ impl PickerDelegate for FileFinderDelegate { Some(ContextMenu::build(window, cx, { let focus_handle = focus_handle.clone(); move |menu, _, _| { - menu.context(focus_handle.clone()) + menu.context(focus_handle) .action( "Split Left", pane::SplitLeft.boxed_clone(), diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index ffe3d42a27..4625872e46 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -653,7 +653,7 @@ impl PickerDelegate for OpenPathDelegate { if parent_path == &self.prompt_root { format!("{}{}", self.prompt_root, candidate.path.string) } else { - candidate.path.string.clone() + candidate.path.string }, match_positions, )), @@ -684,7 +684,7 @@ impl PickerDelegate for OpenPathDelegate { }; StyledText::new(label) .with_default_highlights( - &window.text_style().clone(), + &window.text_style(), vec![( delta..delta + label_len, HighlightStyle::color(Color::Conflict.color(cx)), @@ -694,7 +694,7 @@ impl PickerDelegate for OpenPathDelegate { } else { StyledText::new(format!("{label} (create)")) .with_default_highlights( - &window.text_style().clone(), + &window.text_style(), vec![( delta..delta + label_len, HighlightStyle::color(Color::Created.color(cx)), diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 5b093ac6a0..8a67eddcd7 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -345,7 +345,7 @@ impl GitRepository for FakeGitRepository { fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>> { self.with_state_async(true, move |state| { - state.branches.insert(name.to_owned()); + state.branches.insert(name); Ok(()) }) } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 11177512c3..75312c5c0c 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -1960,7 +1960,7 @@ impl FileHandle for FakeHandle { }; if state.try_entry(&target, false).is_some() { - return Ok(target.clone()); + return Ok(target); } anyhow::bail!("fake fd target not found") } @@ -2256,7 +2256,7 @@ impl Fs for FakeFs { async fn load(&self, path: &Path) -> Result<String> { let content = self.load_internal(path).await?; - Ok(String::from_utf8(content.clone())?) + Ok(String::from_utf8(content)?) } async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>> { @@ -2412,7 +2412,7 @@ impl Fs for FakeFs { tx, original_path: path.to_owned(), fs_state: self.state.clone(), - prefixes: Mutex::new(vec![path.to_owned()]), + prefixes: Mutex::new(vec![path]), }); ( Box::pin(futures::StreamExt::filter(rx, { diff --git a/crates/fs/src/fs_watcher.rs b/crates/fs/src/fs_watcher.rs index a5ce21294f..6ad03ba6df 100644 --- a/crates/fs/src/fs_watcher.rs +++ b/crates/fs/src/fs_watcher.rs @@ -159,7 +159,7 @@ impl GlobalWatcher { path: path.clone(), }; state.watchers.insert(id, registration_state); - *state.path_registrations.entry(path.clone()).or_insert(0) += 1; + *state.path_registrations.entry(path).or_insert(0) += 1; Ok(id) } diff --git a/crates/git_ui/src/blame_ui.rs b/crates/git_ui/src/blame_ui.rs index f910de7bbe..2768e3dc68 100644 --- a/crates/git_ui/src/blame_ui.rs +++ b/crates/git_ui/src/blame_ui.rs @@ -172,7 +172,7 @@ impl BlameRenderer for GitBlameRenderer { .clone() .unwrap_or("<no name>".to_string()) .into(), - author_email: blame.author_mail.clone().unwrap_or("".to_string()).into(), + author_email: blame.author_mail.unwrap_or("".to_string()).into(), message: details, }; @@ -186,7 +186,7 @@ impl BlameRenderer for GitBlameRenderer { .get(0..8) .map(|sha| sha.to_string().into()) .unwrap_or_else(|| commit_details.sha.clone()); - let full_sha = commit_details.sha.to_string().clone(); + let full_sha = commit_details.sha.to_string(); let absolute_timestamp = format_local_timestamp( commit_details.commit_time, OffsetDateTime::now_utc(), @@ -377,7 +377,7 @@ impl BlameRenderer for GitBlameRenderer { has_parent: true, }, repository.downgrade(), - workspace.clone(), + workspace, window, cx, ) diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index 6bb84db834..fb56cdcc5d 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -48,7 +48,7 @@ pub fn open( window: &mut Window, cx: &mut Context<Workspace>, ) { - let repository = workspace.project().read(cx).active_repository(cx).clone(); + let repository = workspace.project().read(cx).active_repository(cx); let style = BranchListStyle::Modal; workspace.toggle_modal(window, cx, |window, cx| { BranchList::new(repository, style, rems(34.), window, cx) @@ -144,7 +144,7 @@ impl BranchList { }) .detach_and_log_err(cx); - let delegate = BranchListDelegate::new(repository.clone(), style); + let delegate = BranchListDelegate::new(repository, style); let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); let _subscription = cx.subscribe(&picker, |_, _, _, cx| { diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index 4303f53275..e1e6cee93c 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -35,7 +35,7 @@ impl ModalContainerProperties { // Calculate width based on character width let mut modal_width = 460.0; - let style = window.text_style().clone(); + let style = window.text_style(); let font_id = window.text_system().resolve_font(&style.font()); let font_size = style.font_size.to_pixels(window.rem_size()); @@ -179,7 +179,7 @@ impl CommitModal { let commit_editor = git_panel.update(cx, |git_panel, cx| { git_panel.set_modal_open(true, cx); - let buffer = git_panel.commit_message_buffer(cx).clone(); + let buffer = git_panel.commit_message_buffer(cx); let panel_editor = git_panel.commit_editor.clone(); let project = git_panel.project.clone(); @@ -285,7 +285,7 @@ impl CommitModal { Some(ContextMenu::build(window, cx, |context_menu, _, _| { context_menu .when_some(keybinding_target.clone(), |el, keybinding_target| { - el.context(keybinding_target.clone()) + el.context(keybinding_target) }) .when(has_previous_commit, |this| { this.toggleable_entry( @@ -482,7 +482,7 @@ impl CommitModal { }), self.render_git_commit_menu( ElementId::Name(format!("split-button-right-{}", commit_label).into()), - Some(focus_handle.clone()), + Some(focus_handle), ) .into_any_element(), )), diff --git a/crates/git_ui/src/commit_tooltip.rs b/crates/git_ui/src/commit_tooltip.rs index 00ab911610..a470bc6925 100644 --- a/crates/git_ui/src/commit_tooltip.rs +++ b/crates/git_ui/src/commit_tooltip.rs @@ -181,7 +181,7 @@ impl Render for CommitTooltip { .get(0..8) .map(|sha| sha.to_string().into()) .unwrap_or_else(|| self.commit.sha.clone()); - let full_sha = self.commit.sha.to_string().clone(); + let full_sha = self.commit.sha.to_string(); let absolute_timestamp = format_local_timestamp( self.commit.commit_time, OffsetDateTime::now_utc(), diff --git a/crates/git_ui/src/conflict_view.rs b/crates/git_ui/src/conflict_view.rs index 5c1b1325a5..ee1b82920d 100644 --- a/crates/git_ui/src/conflict_view.rs +++ b/crates/git_ui/src/conflict_view.rs @@ -55,7 +55,7 @@ pub fn register_editor(editor: &mut Editor, buffer: Entity<MultiBuffer>, cx: &mu buffers: Default::default(), }); - let buffers = buffer.read(cx).all_buffers().clone(); + let buffers = buffer.read(cx).all_buffers(); for buffer in buffers { buffer_added(editor, buffer, cx); } @@ -129,7 +129,7 @@ fn buffer_added(editor: &mut Editor, buffer: Entity<Buffer>, cx: &mut Context<Ed let subscription = cx.subscribe(&conflict_set, conflicts_updated); BufferConflicts { block_ids: Vec::new(), - conflict_set: conflict_set.clone(), + conflict_set, _subscription: subscription, } }); @@ -437,7 +437,6 @@ fn render_conflict_buttons( Button::new("both", "Use Both") .label_size(LabelSize::Small) .on_click({ - let editor = editor.clone(); let conflict = conflict.clone(); let ours = conflict.ours.clone(); let theirs = conflict.theirs.clone(); diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 3eae1acb04..5a01514185 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1467,7 +1467,6 @@ impl GitPanel { .read(cx) .as_singleton() .unwrap() - .clone() } fn toggle_staged_for_selected( @@ -3207,7 +3206,7 @@ impl GitPanel { Some(ContextMenu::build(window, cx, |context_menu, _, _| { context_menu .when_some(keybinding_target.clone(), |el, keybinding_target| { - el.context(keybinding_target.clone()) + el.context(keybinding_target) }) .when(has_previous_commit, |this| { this.toggleable_entry( @@ -3387,7 +3386,7 @@ impl GitPanel { let enable_coauthors = self.render_co_authors(cx); let editor_focus_handle = self.commit_editor.focus_handle(cx); - let expand_tooltip_focus_handle = editor_focus_handle.clone(); + let expand_tooltip_focus_handle = editor_focus_handle; let branch = active_repository.read(cx).branch.clone(); let head_commit = active_repository.read(cx).head_commit.clone(); @@ -3416,7 +3415,7 @@ impl GitPanel { display_name, branch, head_commit, - Some(git_panel.clone()), + Some(git_panel), )) .child( panel_editor_container(window, cx) @@ -3567,7 +3566,7 @@ impl GitPanel { }), self.render_git_commit_menu( ElementId::Name(format!("split-button-right-{}", title).into()), - Some(commit_tooltip_focus_handle.clone()), + Some(commit_tooltip_focus_handle), cx, ) .into_any_element(), @@ -3633,7 +3632,7 @@ impl GitPanel { CommitView::open( commit.clone(), repo.clone(), - workspace.clone().clone(), + workspace.clone(), window, cx, ); @@ -4341,7 +4340,7 @@ impl GitPanel { } }) .child( - self.entry_label(display_name.clone(), label_color) + self.entry_label(display_name, label_color) .when(status.is_deleted(), |this| this.strikethrough()), ), ) @@ -4690,7 +4689,7 @@ impl GitPanelMessageTooltip { author_email: details.author_email.clone(), commit_time: OffsetDateTime::from_unix_timestamp(details.commit_timestamp)?, message: Some(ParsedCommitMessage { - message: details.message.clone(), + message: details.message, ..Default::default() }), }; @@ -4823,7 +4822,7 @@ impl RenderOnce for PanelRepoFooter { }; let truncated_branch_name = if branch_actual_len <= branch_display_len { - branch_name.to_string() + branch_name } else { util::truncate_and_trailoff(branch_name.trim_ascii(), branch_display_len) }; @@ -4836,7 +4835,7 @@ impl RenderOnce for PanelRepoFooter { let repo_selector = PopoverMenu::new("repository-switcher") .menu({ - let project = project.clone(); + let project = project; move |window, cx| { let project = project.clone()?; Some(cx.new(|cx| RepositorySelector::new(project, rems(16.), window, cx))) @@ -5007,10 +5006,7 @@ impl Component for PanelRepoFooter { div() .w(example_width) .overflow_hidden() - .child(PanelRepoFooter::new_preview( - active_repository(1).clone(), - None, - )) + .child(PanelRepoFooter::new_preview(active_repository(1), None)) .into_any_element(), ), single_example( @@ -5019,7 +5015,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(2).clone(), + active_repository(2), Some(branch(unknown_upstream)), )) .into_any_element(), @@ -5030,7 +5026,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(3).clone(), + active_repository(3), Some(branch(no_remote_upstream)), )) .into_any_element(), @@ -5041,7 +5037,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(4).clone(), + active_repository(4), Some(branch(not_ahead_or_behind_upstream)), )) .into_any_element(), @@ -5052,7 +5048,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(5).clone(), + active_repository(5), Some(branch(behind_upstream)), )) .into_any_element(), @@ -5063,7 +5059,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(6).clone(), + active_repository(6), Some(branch(ahead_of_upstream)), )) .into_any_element(), @@ -5074,7 +5070,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(7).clone(), + active_repository(7), Some(branch(ahead_and_behind_upstream)), )) .into_any_element(), diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 3b4196b8ec..5369b8b404 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -245,12 +245,12 @@ fn render_remote_button( } (0, 0) => None, (ahead, 0) => Some(remote_button::render_push_button( - keybinding_target.clone(), + keybinding_target, id, ahead, )), (ahead, behind) => Some(remote_button::render_pull_button( - keybinding_target.clone(), + keybinding_target, id, ahead, behind, @@ -425,16 +425,9 @@ mod remote_button { let command = command.into(); if let Some(handle) = focus_handle { - Tooltip::with_meta_in( - label.clone(), - Some(action), - command.clone(), - &handle, - window, - cx, - ) + Tooltip::with_meta_in(label, Some(action), command, &handle, window, cx) } else { - Tooltip::with_meta(label.clone(), Some(action), command.clone(), window, cx) + Tooltip::with_meta(label, Some(action), command, window, cx) } } @@ -457,7 +450,7 @@ mod remote_button { Some(ContextMenu::build(window, cx, |context_menu, _, _| { context_menu .when_some(keybinding_target.clone(), |el, keybinding_target| { - el.context(keybinding_target.clone()) + el.context(keybinding_target) }) .action("Fetch", git::Fetch.boxed_clone()) .action("Fetch From", git::FetchFrom.boxed_clone()) diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index c1521004a2..524dbf13d3 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -242,7 +242,7 @@ impl ProjectDiff { TRACKED_NAMESPACE }; - let path_key = PathKey::namespaced(namespace, entry.repo_path.0.clone()); + let path_key = PathKey::namespaced(namespace, entry.repo_path.0); self.move_to_path(path_key, window, cx) } @@ -448,10 +448,10 @@ impl ProjectDiff { let diff = diff.read(cx); let diff_hunk_ranges = diff .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx) - .map(|diff_hunk| diff_hunk.buffer_range.clone()); + .map(|diff_hunk| diff_hunk.buffer_range); let conflicts = conflict_addon .conflict_set(snapshot.remote_id()) - .map(|conflict_set| conflict_set.read(cx).snapshot().conflicts.clone()) + .map(|conflict_set| conflict_set.read(cx).snapshot().conflicts) .unwrap_or_default(); let conflicts = conflicts.iter().map(|conflict| conflict.range.clone()); @@ -737,7 +737,7 @@ impl Render for ProjectDiff { } else { None }; - let keybinding_focus_handle = self.focus_handle(cx).clone(); + let keybinding_focus_handle = self.focus_handle(cx); el.child( v_flex() .gap_1() diff --git a/crates/git_ui/src/text_diff_view.rs b/crates/git_ui/src/text_diff_view.rs index e38e3698d5..ebf32d1b99 100644 --- a/crates/git_ui/src/text_diff_view.rs +++ b/crates/git_ui/src/text_diff_view.rs @@ -48,7 +48,7 @@ impl TextDiffView { let selection_data = source_editor.update(cx, |editor, cx| { let multibuffer = editor.buffer().read(cx); - let source_buffer = multibuffer.as_singleton()?.clone(); + let source_buffer = multibuffer.as_singleton()?; let selections = editor.selections.all::<Point>(cx); let buffer_snapshot = source_buffer.read(cx); let first_selection = selections.first()?; @@ -259,7 +259,7 @@ async fn update_diff_buffer( let source_buffer_snapshot = source_buffer.read_with(cx, |buffer, _| buffer.snapshot())?; let base_buffer_snapshot = clipboard_buffer.read_with(cx, |buffer, _| buffer.snapshot())?; - let base_text = base_buffer_snapshot.text().to_string(); + let base_text = base_buffer_snapshot.text(); let diff_snapshot = cx .update(|cx| { diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 908e61cac7..1913646aa1 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -712,7 +712,7 @@ mod tests { ) -> Entity<GoToLine> { cx.dispatch_action(editor::actions::ToggleGoToLine); workspace.update(cx, |workspace, cx| { - workspace.active_modal::<GoToLine>(cx).unwrap().clone() + workspace.active_modal::<GoToLine>(cx).unwrap() }) } diff --git a/crates/gpui/examples/input.rs b/crates/gpui/examples/input.rs index ae635c94b8..37115feaa5 100644 --- a/crates/gpui/examples/input.rs +++ b/crates/gpui/examples/input.rs @@ -446,7 +446,7 @@ impl Element for TextElement { let (display_text, text_color) = if content.is_empty() { (input.placeholder.clone(), hsla(0., 0., 0., 0.2)) } else { - (content.clone(), style.color) + (content, style.color) }; let run = TextRun { @@ -474,7 +474,7 @@ impl Element for TextElement { }, TextRun { len: display_text.len() - marked_range.end, - ..run.clone() + ..run }, ] .into_iter() diff --git a/crates/gpui/examples/text.rs b/crates/gpui/examples/text.rs index 1166bb2795..66e9cff0aa 100644 --- a/crates/gpui/examples/text.rs +++ b/crates/gpui/examples/text.rs @@ -155,7 +155,7 @@ impl RenderOnce for Specimen { .text_size(px(font_size * scale)) .line_height(relative(line_height)) .p(px(10.0)) - .child(self.string.clone()) + .child(self.string) } } diff --git a/crates/gpui/src/app/async_context.rs b/crates/gpui/src/app/async_context.rs index d9d21c0244..5eb4362904 100644 --- a/crates/gpui/src/app/async_context.rs +++ b/crates/gpui/src/app/async_context.rs @@ -465,7 +465,7 @@ impl VisualContext for AsyncWindowContext { V: Focusable, { self.window.update(self, |_, window, cx| { - view.read(cx).focus_handle(cx).clone().focus(window); + view.read(cx).focus_handle(cx).focus(window); }) } } diff --git a/crates/gpui/src/app/entity_map.rs b/crates/gpui/src/app/entity_map.rs index 6099ee5857..ea52b46d9f 100644 --- a/crates/gpui/src/app/entity_map.rs +++ b/crates/gpui/src/app/entity_map.rs @@ -231,14 +231,15 @@ impl AnyEntity { Self { entity_id: id, entity_type, - entity_map: entity_map.clone(), #[cfg(any(test, feature = "leak-detection"))] handle_id: entity_map + .clone() .upgrade() .unwrap() .write() .leak_detector .handle_created(id), + entity_map, } } diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index a69d9d1e26..c65c045f6b 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -134,7 +134,7 @@ impl TestAppContext { app: App::new_app(platform.clone(), asset_source, http_client), background_executor, foreground_executor, - dispatcher: dispatcher.clone(), + dispatcher, test_platform: platform, text_system, fn_name, @@ -339,7 +339,7 @@ impl TestAppContext { /// Returns all windows open in the test. pub fn windows(&self) -> Vec<AnyWindowHandle> { - self.app.borrow().windows().clone() + self.app.borrow().windows() } /// Run the given task on the main thread. @@ -619,7 +619,7 @@ impl<V> Entity<V> { } }), cx.subscribe(self, { - let mut tx = tx.clone(); + let mut tx = tx; move |_, _: &Evt, _| { tx.blocking_send(()).ok(); } @@ -1026,7 +1026,7 @@ impl VisualContext for VisualTestContext { fn focus<V: crate::Focusable>(&mut self, view: &Entity<V>) -> Self::Result<()> { self.window .update(&mut self.cx, |_, window, cx| { - view.read(cx).focus_handle(cx).clone().focus(window) + view.read(cx).focus_handle(cx).focus(window) }) .unwrap() } diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index ae63819ca2..893860d7e1 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -475,7 +475,7 @@ impl Element for Img { .paint_image( new_bounds, corner_radii, - data.clone(), + data, layout_state.frame_index, self.style.grayscale, ) diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index ef446a073e..87cabc8cd9 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -1046,7 +1046,7 @@ where size: self.size.clone() + size( amount.left.clone() + amount.right.clone(), - amount.top.clone() + amount.bottom.clone(), + amount.top.clone() + amount.bottom, ), } } @@ -1159,10 +1159,10 @@ where /// Computes the space available within outer bounds. pub fn space_within(&self, outer: &Self) -> Edges<T> { Edges { - top: self.top().clone() - outer.top().clone(), - right: outer.right().clone() - self.right().clone(), - bottom: outer.bottom().clone() - self.bottom().clone(), - left: self.left().clone() - outer.left().clone(), + top: self.top() - outer.top(), + right: outer.right() - self.right(), + bottom: outer.bottom() - self.bottom(), + left: self.left() - outer.left(), } } } @@ -1712,7 +1712,7 @@ where top: self.top.clone() * rhs.top, right: self.right.clone() * rhs.right, bottom: self.bottom.clone() * rhs.bottom, - left: self.left.clone() * rhs.left, + left: self.left * rhs.left, } } } @@ -2411,7 +2411,7 @@ where top_left: self.top_left.clone() * rhs.top_left, top_right: self.top_right.clone() * rhs.top_right, bottom_right: self.bottom_right.clone() * rhs.bottom_right, - bottom_left: self.bottom_left.clone() * rhs.bottom_left, + bottom_left: self.bottom_left * rhs.bottom_left, } } } diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index d007876590..757205fcc3 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -264,7 +264,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let (result, pending) = keymap.bindings_for_input( &[Keystroke::parse("ctrl-a").unwrap()], @@ -290,7 +290,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); // binding is only enabled in a specific context assert!( @@ -344,7 +344,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let space = || Keystroke::parse("space").unwrap(); let w = || Keystroke::parse("w").unwrap(); @@ -396,7 +396,7 @@ mod tests { KeyBinding::new("space w x", ActionAlpha {}, Some("editor")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); @@ -410,7 +410,7 @@ mod tests { KeyBinding::new("space w w", NoAction {}, Some("editor")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); @@ -424,7 +424,7 @@ mod tests { KeyBinding::new("space w w", NoAction {}, Some("editor")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); @@ -439,7 +439,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); // Ensure `space` results in pending input on the workspace, but not editor let (result, pending) = keymap.bindings_for_input( @@ -455,7 +455,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); // Ensure `space` results in pending input on the workspace, but not editor let (result, pending) = keymap.bindings_for_input( @@ -474,7 +474,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); // Ensure `space` results in pending input on the workspace, but not editor let (result, pending) = keymap.bindings_for_input( @@ -494,7 +494,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); // Ensure `space` results in pending input on the workspace, but not editor let (result, pending) = keymap.bindings_for_input( @@ -516,7 +516,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); // Ensure `space` results in pending input on the workspace, but not editor let (result, pending) = keymap.bindings_for_input( @@ -537,7 +537,7 @@ mod tests { KeyBinding::new("ctrl-x 0", ActionAlpha, Some("Workspace")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let matched = keymap.bindings_for_input( &[Keystroke::parse("ctrl-x")].map(Result::unwrap), @@ -560,7 +560,7 @@ mod tests { KeyBinding::new("ctrl-x 0", NoAction, Some("Workspace")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let matched = keymap.bindings_for_input( &[Keystroke::parse("ctrl-x")].map(Result::unwrap), @@ -579,7 +579,7 @@ mod tests { KeyBinding::new("ctrl-x 0", NoAction, Some("vim_mode == normal")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let matched = keymap.bindings_for_input( &[Keystroke::parse("ctrl-x")].map(Result::unwrap), @@ -602,7 +602,7 @@ mod tests { KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); let matched = keymap.bindings_for_input( &[Keystroke::parse("ctrl-x")].map(Result::unwrap), @@ -629,7 +629,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings.clone()); + keymap.add_bindings(bindings); assert_bindings(&keymap, &ActionAlpha {}, &["ctrl-a"]); assert_bindings(&keymap, &ActionBeta {}, &[]); diff --git a/crates/gpui/src/keymap/context.rs b/crates/gpui/src/keymap/context.rs index 976f99c26e..960bd1752f 100644 --- a/crates/gpui/src/keymap/context.rs +++ b/crates/gpui/src/keymap/context.rs @@ -668,11 +668,7 @@ mod tests { let contexts = vec![other_context.clone(), child_context.clone()]; assert!(!predicate.eval(&contexts)); - let contexts = vec![ - parent_context.clone(), - other_context.clone(), - child_context.clone(), - ]; + let contexts = vec![parent_context.clone(), other_context, child_context.clone()]; assert!(predicate.eval(&contexts)); assert!(!predicate.eval(&[])); @@ -681,7 +677,7 @@ mod tests { let zany_predicate = KeyBindingContextPredicate::parse("child > child").unwrap(); assert!(!zany_predicate.eval(slice::from_ref(&child_context))); - assert!(zany_predicate.eval(&[child_context.clone(), child_context.clone()])); + assert!(zany_predicate.eval(&[child_context.clone(), child_context])); } #[test] @@ -718,7 +714,7 @@ mod tests { let not_descendant = KeyBindingContextPredicate::parse("parent > !child").unwrap(); assert!(!not_descendant.eval(slice::from_ref(&parent_context))); assert!(!not_descendant.eval(slice::from_ref(&child_context))); - assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); + assert!(!not_descendant.eval(&[parent_context, child_context])); let double_not = KeyBindingContextPredicate::parse("!!editor").unwrap(); assert!(double_not.eval(slice::from_ref(&editor_context))); diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 399411843b..3fb1ef4572 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -108,13 +108,13 @@ impl LinuxCommon { let callbacks = PlatformHandlers::default(); - let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone())); + let dispatcher = Arc::new(LinuxDispatcher::new(main_sender)); let background_executor = BackgroundExecutor::new(dispatcher.clone()); let common = LinuxCommon { background_executor, - foreground_executor: ForegroundExecutor::new(dispatcher.clone()), + foreground_executor: ForegroundExecutor::new(dispatcher), text_system, appearance: WindowAppearance::Light, auto_hide_scrollbars: false, diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 2fe1da067b..189cfa1954 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -1280,7 +1280,6 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr { let Some(focused_window) = focused_window else { return; }; - let focused_window = focused_window.clone(); let keymap_state = state.keymap_state.as_ref().unwrap(); let keycode = Keycode::from(key + MIN_KEYCODE); diff --git a/crates/gpui/src/platform/linux/wayland/cursor.rs b/crates/gpui/src/platform/linux/wayland/cursor.rs index a21263ccfe..c7c9139dea 100644 --- a/crates/gpui/src/platform/linux/wayland/cursor.rs +++ b/crates/gpui/src/platform/linux/wayland/cursor.rs @@ -67,7 +67,7 @@ impl Cursor { { self.loaded_theme = Some(LoadedTheme { theme, - name: theme_name.map(|name| name.to_string()), + name: theme_name, scaled_size: self.scaled_size, }); } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 68198a285f..d501170892 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1329,7 +1329,7 @@ impl X11Client { state.composing = false; drop(state); if let Some(mut keystroke) = keystroke { - keystroke.key_char = Some(text.clone()); + keystroke.key_char = Some(text); window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent { keystroke, is_held: false, diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index 49a5edceb2..9e5d6ec5ff 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -332,7 +332,7 @@ impl MetalRenderer { self.path_intermediate_texture = Some(self.device.new_texture(&texture_descriptor)); if self.path_sample_count > 1 { - let mut msaa_descriptor = texture_descriptor.clone(); + let mut msaa_descriptor = texture_descriptor; msaa_descriptor.set_texture_type(metal::MTLTextureType::D2Multisample); msaa_descriptor.set_storage_mode(metal::MTLStorageMode::Private); msaa_descriptor.set_sample_count(self.path_sample_count as _); diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index 2b4914baed..00afcd81b5 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -187,14 +187,14 @@ impl TestPlatform { .push_back(TestPrompt { msg: msg.to_string(), detail: detail.map(|s| s.to_string()), - answers: answers.clone(), + answers, tx, }); rx } pub(crate) fn set_active_window(&self, window: Option<TestWindow>) { - let executor = self.foreground_executor().clone(); + let executor = self.foreground_executor(); let previous_window = self.active_window.borrow_mut().take(); self.active_window.borrow_mut().clone_from(&window); diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 607163b577..4def6a11a5 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -956,7 +956,7 @@ impl WindowsWindowInner { click_count, first_mouse: false, }); - let result = func(input.clone()); + let result = func(input); let handled = !result.propagate || result.default_prevented; self.state.borrow_mut().callbacks.input = Some(func); diff --git a/crates/gpui/src/shared_string.rs b/crates/gpui/src/shared_string.rs index a34b7502f0..350184d350 100644 --- a/crates/gpui/src/shared_string.rs +++ b/crates/gpui/src/shared_string.rs @@ -108,7 +108,7 @@ impl From<SharedString> for Arc<str> { fn from(val: SharedString) -> Self { match val.0 { ArcCow::Borrowed(borrowed) => Arc::from(borrowed), - ArcCow::Owned(owned) => owned.clone(), + ArcCow::Owned(owned) => owned, } } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 89c1595a3f..0791dcc621 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -2453,7 +2453,7 @@ impl Window { /// time. pub fn get_asset<A: Asset>(&mut self, source: &A::Source, cx: &mut App) -> Option<A::Output> { let (task, _) = cx.fetch_asset::<A>(source); - task.clone().now_or_never() + task.now_or_never() } /// Obtain the current element offset. This method should only be called during the /// prepaint phase of element drawing. @@ -3044,7 +3044,7 @@ impl Window { let tile = self .sprite_atlas - .get_or_insert_with(¶ms.clone().into(), &mut || { + .get_or_insert_with(¶ms.into(), &mut || { Ok(Some(( data.size(frame_index), Cow::Borrowed( @@ -3731,7 +3731,7 @@ impl Window { self.dispatch_keystroke_observers( event, Some(binding.action), - match_result.context_stack.clone(), + match_result.context_stack, cx, ); self.pending_input_changed(cx); @@ -4442,7 +4442,7 @@ impl Window { if let Some((_, inspector_id)) = self.hovered_inspector_hitbox(inspector, &self.rendered_frame) { - inspector.set_active_element_id(inspector_id.clone(), self); + inspector.set_active_element_id(inspector_id, self); } } }); @@ -4583,7 +4583,7 @@ impl<V: 'static + Render> WindowHandle<V> { where C: AppContext, { - cx.read_window(self, |root_view, _cx| root_view.clone()) + cx.read_window(self, |root_view, _cx| root_view) } /// Check if this window is 'active'. diff --git a/crates/gpui_macros/tests/derive_inspector_reflection.rs b/crates/gpui_macros/tests/derive_inspector_reflection.rs index 522c0a62c4..aab44a70ce 100644 --- a/crates/gpui_macros/tests/derive_inspector_reflection.rs +++ b/crates/gpui_macros/tests/derive_inspector_reflection.rs @@ -106,9 +106,7 @@ fn test_derive_inspector_reflection() { .invoke(num.clone()); assert_eq!(incremented, Number(6)); - let quadrupled = find_method::<Number>("quadruple") - .unwrap() - .invoke(num.clone()); + let quadrupled = find_method::<Number>("quadruple").unwrap().invoke(num); assert_eq!(quadrupled, Number(20)); // Try to invoke a non-existent method diff --git a/crates/http_client/src/async_body.rs b/crates/http_client/src/async_body.rs index 473849f3cd..6b99a54a7d 100644 --- a/crates/http_client/src/async_body.rs +++ b/crates/http_client/src/async_body.rs @@ -40,7 +40,7 @@ impl AsyncBody { } pub fn from_bytes(bytes: Bytes) -> Self { - Self(Inner::Bytes(Cursor::new(bytes.clone()))) + Self(Inner::Bytes(Cursor::new(bytes))) } } diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 81dc36093b..c09ab6f764 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -123,7 +123,7 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap } let app_state = workspace.app_state().clone(); - let view_snapshot = workspace.weak_handle().clone(); + let view_snapshot = workspace.weak_handle(); window .spawn(cx, async move |cx| { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index cc96022e63..b106110c33 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -974,8 +974,6 @@ impl Buffer { TextBuffer::new_normalized(0, buffer_id, Default::default(), text).snapshot(); let mut syntax = SyntaxMap::new(&text).snapshot(); if let Some(language) = language.clone() { - let text = text.clone(); - let language = language.clone(); let language_registry = language_registry.clone(); syntax.reparse(&text, language_registry, language); } @@ -1020,9 +1018,6 @@ impl Buffer { let text = TextBuffer::new_normalized(0, buffer_id, Default::default(), text).snapshot(); let mut syntax = SyntaxMap::new(&text).snapshot(); if let Some(language) = language.clone() { - let text = text.clone(); - let language = language.clone(); - let language_registry = language_registry.clone(); syntax.reparse(&text, language_registry, language); } BufferSnapshot { @@ -2206,7 +2201,7 @@ impl Buffer { self.remote_selections.insert( AGENT_REPLICA_ID, SelectionSet { - selections: selections.clone(), + selections, lamport_timestamp, line_mode, cursor_shape, @@ -3006,9 +3001,9 @@ impl BufferSnapshot { } let mut error_ranges = Vec::<Range<Point>>::new(); - let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| { - grammar.error_query.as_ref() - }); + let mut matches = self + .syntax + .matches(range, &self.text, |grammar| grammar.error_query.as_ref()); while let Some(mat) = matches.peek() { let node = mat.captures[0].node; let start = Point::from_ts_point(node.start_position()); @@ -4075,7 +4070,7 @@ impl BufferSnapshot { // Get the ranges of the innermost pair of brackets. let mut result: Option<(Range<usize>, Range<usize>)> = None; - for pair in self.enclosing_bracket_ranges(range.clone()) { + for pair in self.enclosing_bracket_ranges(range) { if let Some(range_filter) = range_filter && !range_filter(pair.open_range.clone(), pair.close_range.clone()) { @@ -4248,7 +4243,7 @@ impl BufferSnapshot { .map(|(range, name)| { ( name.to_string(), - self.text_for_range(range.clone()).collect::<String>(), + self.text_for_range(range).collect::<String>(), ) }) .collect(); diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 2e2df7e658..ce65afa628 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -1744,7 +1744,7 @@ fn test_autoindent_block_mode(cx: &mut App) { buffer.edit( [(Point::new(2, 8)..Point::new(2, 8), inserted_text)], Some(AutoindentMode::Block { - original_indent_columns: original_indent_columns.clone(), + original_indent_columns, }), cx, ); @@ -1790,9 +1790,9 @@ fn test_autoindent_block_mode_with_newline(cx: &mut App) { "# .unindent(); buffer.edit( - [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())], + [(Point::new(2, 0)..Point::new(2, 0), inserted_text)], Some(AutoindentMode::Block { - original_indent_columns: original_indent_columns.clone(), + original_indent_columns, }), cx, ); @@ -1843,7 +1843,7 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut App) { buffer.edit( [(Point::new(2, 0)..Point::new(2, 0), inserted_text)], Some(AutoindentMode::Block { - original_indent_columns: original_indent_columns.clone(), + original_indent_columns, }), cx, ); @@ -2030,7 +2030,7 @@ fn test_autoindent_with_injected_languages(cx: &mut App) { let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); language_registry.add(html_language.clone()); - language_registry.add(javascript_language.clone()); + language_registry.add(javascript_language); cx.new(|cx| { let (text, ranges) = marked_text_ranges( diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 87fc846a53..7ae77c9141 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -206,7 +206,7 @@ impl CachedLspAdapter { } pub fn name(&self) -> LanguageServerName { - self.adapter.name().clone() + self.adapter.name() } pub async fn get_language_server_command( diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index be68dc1e9f..4f07240e44 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -432,7 +432,7 @@ impl LanguageRegistry { let mut state = self.state.write(); state .lsp_adapters - .entry(language_name.clone()) + .entry(language_name) .or_default() .push(adapter.clone()); state.all_lsp_adapters.insert(adapter.name(), adapter); @@ -454,7 +454,7 @@ impl LanguageRegistry { let cached_adapter = CachedLspAdapter::new(Arc::new(adapter)); state .lsp_adapters - .entry(language_name.clone()) + .entry(language_name) .or_default() .push(cached_adapter.clone()); state @@ -1167,8 +1167,7 @@ impl LanguageRegistryState { soft_wrap: language.config.soft_wrap, auto_indent_on_paste: language.config.auto_indent_on_paste, ..Default::default() - } - .clone(), + }, ); self.languages.push(language); self.version += 1; diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index fbb67a9818..90a59ce066 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -199,7 +199,7 @@ impl LanguageSettings { if language_server.0.as_ref() == Self::REST_OF_LANGUAGE_SERVERS { rest.clone() } else { - vec![language_server.clone()] + vec![language_server] } }) .collect::<Vec<_>>() @@ -1793,7 +1793,7 @@ mod tests { assert!(!settings.enabled_for_file(&dot_env_file, &cx)); // Test tilde expansion - let home = shellexpand::tilde("~").into_owned().to_string(); + let home = shellexpand::tilde("~").into_owned(); let home_file = make_test_file(&[&home, "test.rs"]); let settings = build_settings(&["~/test.rs"]); assert!(!settings.enabled_for_file(&home_file, &cx)); diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index f10056af13..38aad007fe 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -832,7 +832,7 @@ impl SyntaxSnapshot { query: fn(&Grammar) -> Option<&Query>, ) -> SyntaxMapCaptures<'a> { SyntaxMapCaptures::new( - range.clone(), + range, text, [SyntaxLayer { language, diff --git a/crates/language/src/syntax_map/syntax_map_tests.rs b/crates/language/src/syntax_map/syntax_map_tests.rs index d576c95cd5..622731b781 100644 --- a/crates/language/src/syntax_map/syntax_map_tests.rs +++ b/crates/language/src/syntax_map/syntax_map_tests.rs @@ -58,8 +58,7 @@ fn test_splice_included_ranges() { assert_eq!(change, 0..1); // does not create overlapping ranges - let (new_ranges, change) = - splice_included_ranges(ranges.clone(), &[0..18], &[ts_range(20..32)]); + let (new_ranges, change) = splice_included_ranges(ranges, &[0..18], &[ts_range(20..32)]); assert_eq!( new_ranges, &[ts_range(20..32), ts_range(50..60), ts_range(80..90)] @@ -104,7 +103,7 @@ fn test_syntax_map_layers_for_range(cx: &mut App) { ); let mut syntax_map = SyntaxMap::new(&buffer); - syntax_map.set_language_registry(registry.clone()); + syntax_map.set_language_registry(registry); syntax_map.reparse(language.clone(), &buffer); assert_layers_for_range( @@ -165,7 +164,7 @@ fn test_syntax_map_layers_for_range(cx: &mut App) { // Put the vec! macro back, adding back the syntactic layer. buffer.undo(); syntax_map.interpolate(&buffer); - syntax_map.reparse(language.clone(), &buffer); + syntax_map.reparse(language, &buffer); assert_layers_for_range( &syntax_map, @@ -252,8 +251,8 @@ fn test_dynamic_language_injection(cx: &mut App) { assert!(syntax_map.contains_unknown_injections()); registry.add(Arc::new(html_lang())); - syntax_map.reparse(markdown.clone(), &buffer); - syntax_map.reparse(markdown_inline.clone(), &buffer); + syntax_map.reparse(markdown, &buffer); + syntax_map.reparse(markdown_inline, &buffer); assert_layers_for_range( &syntax_map, &buffer, @@ -862,7 +861,7 @@ fn test_syntax_map_languages_loading_with_erb(cx: &mut App) { log::info!("editing"); buffer.edit_via_marked_text(&text); syntax_map.interpolate(&buffer); - syntax_map.reparse(language.clone(), &buffer); + syntax_map.reparse(language, &buffer); assert_capture_ranges( &syntax_map, @@ -986,7 +985,7 @@ fn test_random_edits( syntax_map.reparse(language.clone(), &buffer); let mut reference_syntax_map = SyntaxMap::new(&buffer); - reference_syntax_map.set_language_registry(registry.clone()); + reference_syntax_map.set_language_registry(registry); log::info!("initial text:\n{}", buffer.text()); diff --git a/crates/language/src/text_diff.rs b/crates/language/src/text_diff.rs index 1e3e12758d..cb2242a6b1 100644 --- a/crates/language/src/text_diff.rs +++ b/crates/language/src/text_diff.rs @@ -88,11 +88,11 @@ pub fn text_diff_with_options( let new_offset = new_byte_range.start; hunk_input.clear(); hunk_input.update_before(tokenize( - &old_text[old_byte_range.clone()], + &old_text[old_byte_range], options.language_scope.clone(), )); hunk_input.update_after(tokenize( - &new_text[new_byte_range.clone()], + &new_text[new_byte_range], options.language_scope.clone(), )); diff_internal(&hunk_input, |old_byte_range, new_byte_range, _, _| { @@ -103,7 +103,7 @@ pub fn text_diff_with_options( let replacement_text = if new_byte_range.is_empty() { empty.clone() } else { - new_text[new_byte_range.clone()].into() + new_text[new_byte_range].into() }; edits.push((old_byte_range, replacement_text)); }); @@ -111,9 +111,9 @@ pub fn text_diff_with_options( let replacement_text = if new_byte_range.is_empty() { empty.clone() } else { - new_text[new_byte_range.clone()].into() + new_text[new_byte_range].into() }; - edits.push((old_byte_range.clone(), replacement_text)); + edits.push((old_byte_range, replacement_text)); } }, ); diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index b10529c3d9..158bebcbbf 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -54,7 +54,7 @@ pub const ZED_CLOUD_PROVIDER_NAME: LanguageModelProviderName = pub fn init(client: Arc<Client>, cx: &mut App) { init_settings(cx); - RefreshLlmTokenListener::register(client.clone(), cx); + RefreshLlmTokenListener::register(client, cx); } pub fn init_settings(cx: &mut App) { @@ -538,7 +538,7 @@ pub trait LanguageModel: Send + Sync { if let Some(first_event) = events.next().await { match first_event { Ok(LanguageModelCompletionEvent::StartMessage { message_id: id }) => { - message_id = Some(id.clone()); + message_id = Some(id); } Ok(LanguageModelCompletionEvent::Text(text)) => { first_item_text = Some(text); diff --git a/crates/language_model/src/model/cloud_model.rs b/crates/language_model/src/model/cloud_model.rs index 0e10050dae..8a7f3456fb 100644 --- a/crates/language_model/src/model/cloud_model.rs +++ b/crates/language_model/src/model/cloud_model.rs @@ -82,7 +82,7 @@ impl LlmApiToken { let response = client.cloud_client().create_llm_token(system_id).await?; *lock = Some(response.token.0.clone()); - Ok(response.token.0.clone()) + Ok(response.token.0) } } diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index 18e6f47ed0..738b72b0c9 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -104,7 +104,7 @@ fn register_language_model_providers( cx: &mut Context<LanguageModelRegistry>, ) { registry.register_provider( - CloudLanguageModelProvider::new(user_store.clone(), client.clone(), cx), + CloudLanguageModelProvider::new(user_store, client.clone(), cx), cx, ); diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index 193d218094..178c767950 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -917,7 +917,7 @@ pub fn map_to_language_model_completion_events( Some(ContentBlockDelta::ReasoningContent(thinking)) => match thinking { ReasoningContentBlockDelta::Text(thoughts) => { Some(Ok(LanguageModelCompletionEvent::Thinking { - text: thoughts.clone(), + text: thoughts, signature: None, })) } @@ -968,7 +968,7 @@ pub fn map_to_language_model_completion_events( id: tool_use.id.into(), name: tool_use.name.into(), is_input_complete: true, - raw_input: tool_use.input_json.clone(), + raw_input: tool_use.input_json, input, }, )) @@ -1086,21 +1086,18 @@ impl ConfigurationView { .access_key_id_editor .read(cx) .text(cx) - .to_string() .trim() .to_string(); let secret_access_key = self .secret_access_key_editor .read(cx) .text(cx) - .to_string() .trim() .to_string(); let session_token = self .session_token_editor .read(cx) .text(cx) - .to_string() .trim() .to_string(); let session_token = if session_token.is_empty() { @@ -1108,13 +1105,7 @@ impl ConfigurationView { } else { Some(session_token) }; - let region = self - .region_editor - .read(cx) - .text(cx) - .to_string() - .trim() - .to_string(); + let region = self.region_editor.read(cx).text(cx).trim().to_string(); let region = if region.is_empty() { "us-east-1".to_string() } else { diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index d3fee7b63b..b1b5ff3eb3 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -140,7 +140,7 @@ impl State { Self { client: client.clone(), llm_api_token: LlmApiToken::default(), - user_store: user_store.clone(), + user_store, status, accept_terms_of_service_task: None, models: Vec::new(), @@ -307,7 +307,7 @@ impl CloudLanguageModelProvider { Self { client, - state: state.clone(), + state, _maintain_client_status: maintain_client_status, } } @@ -320,7 +320,7 @@ impl CloudLanguageModelProvider { Arc::new(CloudLanguageModel { id: LanguageModelId(SharedString::from(model.id.0.clone())), model, - llm_api_token: llm_api_token.clone(), + llm_api_token, client: self.client.clone(), request_limiter: RateLimiter::new(4), }) diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index a36ce949b1..c8d4151e8b 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -387,7 +387,7 @@ impl LanguageModel for GoogleLanguageModel { cx: &App, ) -> BoxFuture<'static, Result<u64>> { let model_id = self.model.request_id().to_string(); - let request = into_google(request, model_id.clone(), self.model.mode()); + let request = into_google(request, model_id, self.model.mode()); let http_client = self.http_client.clone(); let api_key = self.state.read(cx).api_key.clone(); diff --git a/crates/language_models/src/provider/lmstudio.rs b/crates/language_models/src/provider/lmstudio.rs index 7ac08f2c15..80b28a396b 100644 --- a/crates/language_models/src/provider/lmstudio.rs +++ b/crates/language_models/src/provider/lmstudio.rs @@ -210,7 +210,7 @@ impl LanguageModelProvider for LmStudioLanguageModelProvider { .map(|model| { Arc::new(LmStudioLanguageModel { id: LanguageModelId::from(model.name.clone()), - model: model.clone(), + model, http_client: self.http_client.clone(), request_limiter: RateLimiter::new(4), }) as Arc<dyn LanguageModel> diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index 93844542ea..3f2d47fba3 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -237,7 +237,7 @@ impl LanguageModelProvider for OllamaLanguageModelProvider { .map(|model| { Arc::new(OllamaLanguageModel { id: LanguageModelId::from(model.name.clone()), - model: model.clone(), + model, http_client: self.http_client.clone(), request_limiter: RateLimiter::new(4), }) as Arc<dyn LanguageModel> diff --git a/crates/language_models/src/ui/instruction_list_item.rs b/crates/language_models/src/ui/instruction_list_item.rs index 3dee97aff6..bdb5fbe242 100644 --- a/crates/language_models/src/ui/instruction_list_item.rs +++ b/crates/language_models/src/ui/instruction_list_item.rs @@ -37,7 +37,7 @@ impl IntoElement for InstructionListItem { let item_content = if let (Some(button_label), Some(button_link)) = (self.button_label, self.button_link) { - let link = button_link.clone(); + let link = button_link; let unique_id = SharedString::from(format!("{}-button", self.label)); h_flex() diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 3285efaaef..43c0365291 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -406,10 +406,7 @@ impl LogStore { server_state.worktree_id = Some(worktree_id); } - if let Some(server) = server - .clone() - .filter(|_| server_state.io_logs_subscription.is_none()) - { + if let Some(server) = server.filter(|_| server_state.io_logs_subscription.is_none()) { let io_tx = self.io_tx.clone(); let server_id = server.server_id(); server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| { @@ -930,7 +927,7 @@ impl LspLogView { let state = log_store.language_servers.get(&server_id)?; Some(LogMenuItem { server_id, - server_name: name.clone(), + server_name: name, server_kind: state.kind.clone(), worktree_root_name: "supplementary".to_string(), rpc_trace_enabled: state.rpc_state.is_some(), @@ -1527,7 +1524,7 @@ impl Render for LspLogToolbarItemView { .icon_color(Color::Muted), ) .menu({ - let log_view = log_view.clone(); + let log_view = log_view; move |window, cx| { let id = log_view.read(cx).current_server_id?; @@ -1595,7 +1592,7 @@ impl Render for LspLogToolbarItemView { .icon_color(Color::Muted), ) .menu({ - let log_view = log_view.clone(); + let log_view = log_view; move |window, cx| { let id = log_view.read(cx).current_server_id?; diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 4fe8e11f94..cf84ac34c4 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -156,7 +156,7 @@ impl SyntaxTreeView { .buffer_snapshot .range_to_buffer_ranges(selection_range) .pop()?; - let buffer = multi_buffer.buffer(buffer.remote_id()).unwrap().clone(); + let buffer = multi_buffer.buffer(buffer.remote_id()).unwrap(); Some((buffer, range, excerpt_id)) })?; diff --git a/crates/languages/src/c.rs b/crates/languages/src/c.rs index 999d4a74c3..2820f55a49 100644 --- a/crates/languages/src/c.rs +++ b/crates/languages/src/c.rs @@ -22,7 +22,7 @@ impl CLspAdapter { #[async_trait(?Send)] impl super::LspAdapter for CLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn check_if_user_installed( @@ -253,8 +253,7 @@ impl super::LspAdapter for CLspAdapter { .grammar() .and_then(|g| g.highlight_id_for_name(highlight_name?)) { - let mut label = - CodeLabel::plain(label.to_string(), completion.filter_text.as_deref()); + let mut label = CodeLabel::plain(label, completion.filter_text.as_deref()); label.runs.push(( 0..label.text.rfind('(').unwrap_or(label.text.len()), highlight_id, @@ -264,10 +263,7 @@ impl super::LspAdapter for CLspAdapter { } _ => {} } - Some(CodeLabel::plain( - label.to_string(), - completion.filter_text.as_deref(), - )) + Some(CodeLabel::plain(label, completion.filter_text.as_deref())) } async fn label_for_symbol( diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index d6f9538ee4..24e2ca2f56 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -53,7 +53,7 @@ const BINARY: &str = if cfg!(target_os = "windows") { #[async_trait(?Send)] impl super::LspAdapter for GoLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn fetch_latest_server_version( @@ -525,7 +525,7 @@ impl ContextProvider for GoContextProvider { }) .unwrap_or_else(|| format!("{}", buffer_dir.to_string_lossy())); - (GO_PACKAGE_TASK_VARIABLE.clone(), package_name.to_string()) + (GO_PACKAGE_TASK_VARIABLE.clone(), package_name) }); let go_module_root_variable = local_abs_path @@ -702,7 +702,7 @@ impl ContextProvider for GoContextProvider { label: format!("go generate {}", GO_PACKAGE_TASK_VARIABLE.template_value()), command: "go".into(), args: vec!["generate".into()], - cwd: package_cwd.clone(), + cwd: package_cwd, tags: vec!["go-generate".to_owned()], ..TaskTemplate::default() }, @@ -710,7 +710,7 @@ impl ContextProvider for GoContextProvider { label: "go generate ./...".into(), command: "go".into(), args: vec!["generate".into(), "./...".into()], - cwd: module_cwd.clone(), + cwd: module_cwd, ..TaskTemplate::default() }, ]))) diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index ac653d5b2e..4fcf865568 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -488,7 +488,7 @@ impl NodeVersionAdapter { #[async_trait(?Send)] impl LspAdapter for NodeVersionAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn fetch_latest_server_version( diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 75289dd59d..d391e67d33 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -104,7 +104,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) { let typescript_context = Arc::new(typescript::TypeScriptContextProvider::new()); let typescript_lsp_adapter = Arc::new(typescript::TypeScriptLspAdapter::new(node.clone())); let vtsls_adapter = Arc::new(vtsls::VtslsLspAdapter::new(node.clone())); - let yaml_lsp_adapter = Arc::new(yaml::YamlLspAdapter::new(node.clone())); + let yaml_lsp_adapter = Arc::new(yaml::YamlLspAdapter::new(node)); let built_in_languages = [ LanguageInfo { @@ -119,12 +119,12 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) { }, LanguageInfo { name: "cpp", - adapters: vec![c_lsp_adapter.clone()], + adapters: vec![c_lsp_adapter], ..Default::default() }, LanguageInfo { name: "css", - adapters: vec![css_lsp_adapter.clone()], + adapters: vec![css_lsp_adapter], ..Default::default() }, LanguageInfo { @@ -146,20 +146,20 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) { }, LanguageInfo { name: "gowork", - adapters: vec![go_lsp_adapter.clone()], - context: Some(go_context_provider.clone()), + adapters: vec![go_lsp_adapter], + context: Some(go_context_provider), ..Default::default() }, LanguageInfo { name: "json", - adapters: vec![json_lsp_adapter.clone(), node_version_lsp_adapter.clone()], + adapters: vec![json_lsp_adapter.clone(), node_version_lsp_adapter], context: Some(json_context_provider.clone()), ..Default::default() }, LanguageInfo { name: "jsonc", - adapters: vec![json_lsp_adapter.clone()], - context: Some(json_context_provider.clone()), + adapters: vec![json_lsp_adapter], + context: Some(json_context_provider), ..Default::default() }, LanguageInfo { @@ -174,7 +174,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) { }, LanguageInfo { name: "python", - adapters: vec![python_lsp_adapter.clone(), py_lsp_adapter.clone()], + adapters: vec![python_lsp_adapter, py_lsp_adapter], context: Some(python_context_provider), toolchain: Some(python_toolchain_provider), manifest_name: Some(SharedString::new_static("pyproject.toml").into()), @@ -201,7 +201,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) { LanguageInfo { name: "javascript", adapters: vec![typescript_lsp_adapter.clone(), vtsls_adapter.clone()], - context: Some(typescript_context.clone()), + context: Some(typescript_context), ..Default::default() }, LanguageInfo { @@ -277,13 +277,13 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) { move || adapter.clone() }); languages.register_available_lsp_adapter(LanguageServerName("vtsls".into()), { - let adapter = vtsls_adapter.clone(); + let adapter = vtsls_adapter; move || adapter.clone() }); languages.register_available_lsp_adapter( LanguageServerName("typescript-language-server".into()), { - let adapter = typescript_lsp_adapter.clone(); + let adapter = typescript_lsp_adapter; move || adapter.clone() }, ); diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 6c92d78525..d21b5dabd3 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -103,7 +103,7 @@ impl PythonLspAdapter { #[async_trait(?Send)] impl LspAdapter for PythonLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn initialization_options( @@ -1026,7 +1026,7 @@ const BINARY_DIR: &str = if cfg!(target_os = "windows") { #[async_trait(?Send)] impl LspAdapter for PyLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn check_if_user_installed( @@ -1318,7 +1318,7 @@ impl BasedPyrightLspAdapter { #[async_trait(?Send)] impl LspAdapter for BasedPyrightLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn initialization_options( diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index eb5e0cee7c..c6c7357148 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -106,7 +106,7 @@ impl ManifestProvider for CargoManifestProvider { #[async_trait(?Send)] impl LspAdapter for RustLspAdapter { fn name(&self) -> LanguageServerName { - SERVER_NAME.clone() + SERVER_NAME } async fn check_if_user_installed( @@ -659,7 +659,7 @@ impl ContextProvider for RustContextProvider { .variables .get(CUSTOM_TARGET_DIR) .cloned(); - let run_task_args = if let Some(package_to_run) = package_to_run.clone() { + let run_task_args = if let Some(package_to_run) = package_to_run { vec!["run".into(), "-p".into(), package_to_run] } else { vec!["run".into()] @@ -1019,8 +1019,8 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ let path = last.context("no cached binary")?; let path = match RustLspAdapter::GITHUB_ASSET_KIND { - AssetKind::TarGz | AssetKind::Gz => path.clone(), // Tar and gzip extract in place. - AssetKind::Zip => path.clone().join("rust-analyzer.exe"), // zip contains a .exe + AssetKind::TarGz | AssetKind::Gz => path, // Tar and gzip extract in place. + AssetKind::Zip => path.join("rust-analyzer.exe"), // zip contains a .exe }; anyhow::Ok(LanguageServerBinary { diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index 29a96d9515..47eb254053 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -44,7 +44,7 @@ impl TailwindLspAdapter { #[async_trait(?Send)] impl LspAdapter for TailwindLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn check_if_user_installed( diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index afc84c3aff..77cf1a64f1 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -557,7 +557,7 @@ struct TypeScriptVersions { #[async_trait(?Send)] impl LspAdapter for TypeScriptLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn fetch_latest_server_version( @@ -879,7 +879,7 @@ impl LspAdapter for EsLintLspAdapter { } fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn fetch_latest_server_version( diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index fd227e267d..f7152b0b5d 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -67,7 +67,7 @@ const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vtsls"); #[async_trait(?Send)] impl LspAdapter for VtslsLspAdapter { fn name(&self) -> LanguageServerName { - SERVER_NAME.clone() + SERVER_NAME } async fn fetch_latest_server_version( diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 6ac92e0b2b..b9197b12ae 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -38,7 +38,7 @@ impl YamlLspAdapter { #[async_trait(?Send)] impl LspAdapter for YamlLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME.clone() + Self::SERVER_NAME } async fn fetch_latest_server_version( diff --git a/crates/livekit_client/examples/test_app.rs b/crates/livekit_client/examples/test_app.rs index 51f335c2db..7580642990 100644 --- a/crates/livekit_client/examples/test_app.rs +++ b/crates/livekit_client/examples/test_app.rs @@ -183,7 +183,7 @@ impl LivekitWindow { match track { livekit_client::RemoteTrack::Audio(track) => { output.audio_output_stream = Some(( - publication.clone(), + publication, room.play_remote_audio_track(&track, cx).unwrap(), )); } diff --git a/crates/livekit_client/src/livekit_client/playback.rs b/crates/livekit_client/src/livekit_client/playback.rs index d1eec42f8f..e13fb7bd81 100644 --- a/crates/livekit_client/src/livekit_client/playback.rs +++ b/crates/livekit_client/src/livekit_client/playback.rs @@ -117,7 +117,6 @@ impl AudioStack { let (frame_tx, mut frame_rx) = futures::channel::mpsc::unbounded(); let transmit_task = self.executor.spawn({ - let source = source.clone(); async move { while let Some(frame) = frame_rx.next().await { source.capture_frame(&frame).await.log_err(); @@ -132,12 +131,12 @@ impl AudioStack { drop(transmit_task); drop(capture_task); }); - return Ok(( + Ok(( super::LocalAudioTrack(track), AudioStream::Output { _drop: Box::new(on_drop), }, - )); + )) } fn start_output(&self) -> Arc<Task<()>> { diff --git a/crates/markdown/examples/markdown_as_child.rs b/crates/markdown/examples/markdown_as_child.rs index 862b657c8c..16c198601a 100644 --- a/crates/markdown/examples/markdown_as_child.rs +++ b/crates/markdown/examples/markdown_as_child.rs @@ -30,7 +30,7 @@ pub fn main() { let node_runtime = NodeRuntime::unavailable(); let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone())); - languages::init(language_registry.clone(), node_runtime, cx); + languages::init(language_registry, node_runtime, cx); theme::init(LoadThemes::JustBase, cx); Assets.load_fonts(cx).unwrap(); diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index a161ddd074..755506bd12 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -1323,8 +1323,7 @@ fn render_copy_code_block_button( .shape(ui::IconButtonShape::Square) .tooltip(Tooltip::text("Copy Code")) .on_click({ - let id = id.clone(); - let markdown = markdown.clone(); + let markdown = markdown; move |_event, _window, cx| { let id = id.clone(); markdown.update(cx, |this, cx| { diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index 8c8d9e177f..b51b98a2ed 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -178,7 +178,6 @@ impl<'a> MarkdownParser<'a> { _ => None, }, Event::Rule => { - let source_range = source_range.clone(); self.cursor += 1; Some(vec![ParsedMarkdownElement::HorizontalRule(source_range)]) } @@ -401,7 +400,7 @@ impl<'a> MarkdownParser<'a> { } if !text.is_empty() { markdown_text_like.push(MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: source_range.clone(), + source_range, contents: text, highlights, regions, @@ -420,7 +419,7 @@ impl<'a> MarkdownParser<'a> { self.cursor += 1; ParsedMarkdownHeading { - source_range: source_range.clone(), + source_range, level: match level { pulldown_cmark::HeadingLevel::H1 => HeadingLevel::H1, pulldown_cmark::HeadingLevel::H2 => HeadingLevel::H2, diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index c2b98f69c8..1121d64655 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -115,8 +115,7 @@ impl MarkdownPreviewView { pane.activate_item(existing_follow_view_idx, true, true, window, cx); }); } else { - let view = - Self::create_following_markdown_view(workspace, editor.clone(), window, cx); + let view = Self::create_following_markdown_view(workspace, editor, window, cx); workspace.active_pane().update(cx, |pane, cx| { pane.add_item(Box::new(view.clone()), true, true, None, window, cx) }); diff --git a/crates/migrator/src/migrations/m_2025_01_02/settings.rs b/crates/migrator/src/migrations/m_2025_01_02/settings.rs index 3ce85e6b26..a35b1ebd2e 100644 --- a/crates/migrator/src/migrations/m_2025_01_02/settings.rs +++ b/crates/migrator/src/migrations/m_2025_01_02/settings.rs @@ -20,14 +20,14 @@ fn replace_deprecated_settings_values( .nodes_for_capture_index(parent_object_capture_ix) .next()? .byte_range(); - let parent_object_name = contents.get(parent_object_range.clone())?; + let parent_object_name = contents.get(parent_object_range)?; let setting_name_ix = query.capture_index_for_name("setting_name")?; let setting_name_range = mat .nodes_for_capture_index(setting_name_ix) .next()? .byte_range(); - let setting_name = contents.get(setting_name_range.clone())?; + let setting_name = contents.get(setting_name_range)?; let setting_value_ix = query.capture_index_for_name("setting_value")?; let setting_value_range = mat diff --git a/crates/migrator/src/migrations/m_2025_01_29/keymap.rs b/crates/migrator/src/migrations/m_2025_01_29/keymap.rs index c32da88229..eed2c46e08 100644 --- a/crates/migrator/src/migrations/m_2025_01_29/keymap.rs +++ b/crates/migrator/src/migrations/m_2025_01_29/keymap.rs @@ -279,7 +279,7 @@ fn rename_context_key( new_predicate = new_predicate.replace(old_key, new_key); } if new_predicate != old_predicate { - Some((context_predicate_range, new_predicate.to_string())) + Some((context_predicate_range, new_predicate)) } else { None } diff --git a/crates/migrator/src/migrations/m_2025_01_29/settings.rs b/crates/migrator/src/migrations/m_2025_01_29/settings.rs index 8d3261676b..46cfe2f178 100644 --- a/crates/migrator/src/migrations/m_2025_01_29/settings.rs +++ b/crates/migrator/src/migrations/m_2025_01_29/settings.rs @@ -57,7 +57,7 @@ pub fn replace_edit_prediction_provider_setting( .nodes_for_capture_index(parent_object_capture_ix) .next()? .byte_range(); - let parent_object_name = contents.get(parent_object_range.clone())?; + let parent_object_name = contents.get(parent_object_range)?; let setting_name_ix = query.capture_index_for_name("setting_name")?; let setting_range = mat diff --git a/crates/migrator/src/migrations/m_2025_01_30/settings.rs b/crates/migrator/src/migrations/m_2025_01_30/settings.rs index 23a3243b82..2d763e4722 100644 --- a/crates/migrator/src/migrations/m_2025_01_30/settings.rs +++ b/crates/migrator/src/migrations/m_2025_01_30/settings.rs @@ -25,7 +25,7 @@ fn replace_tab_close_button_setting_key( .nodes_for_capture_index(parent_object_capture_ix) .next()? .byte_range(); - let parent_object_name = contents.get(parent_object_range.clone())?; + let parent_object_name = contents.get(parent_object_range)?; let setting_name_ix = query.capture_index_for_name("setting_name")?; let setting_range = mat @@ -51,14 +51,14 @@ fn replace_tab_close_button_setting_value( .nodes_for_capture_index(parent_object_capture_ix) .next()? .byte_range(); - let parent_object_name = contents.get(parent_object_range.clone())?; + let parent_object_name = contents.get(parent_object_range)?; let setting_name_ix = query.capture_index_for_name("setting_name")?; let setting_name_range = mat .nodes_for_capture_index(setting_name_ix) .next()? .byte_range(); - let setting_name = contents.get(setting_name_range.clone())?; + let setting_name = contents.get(setting_name_range)?; let setting_value_ix = query.capture_index_for_name("setting_value")?; let setting_value_range = mat diff --git a/crates/migrator/src/migrations/m_2025_03_29/settings.rs b/crates/migrator/src/migrations/m_2025_03_29/settings.rs index 47f65b407d..8f83d8e39e 100644 --- a/crates/migrator/src/migrations/m_2025_03_29/settings.rs +++ b/crates/migrator/src/migrations/m_2025_03_29/settings.rs @@ -19,7 +19,7 @@ fn replace_setting_value( .nodes_for_capture_index(setting_capture_ix) .next()? .byte_range(); - let setting_name = contents.get(setting_name_range.clone())?; + let setting_name = contents.get(setting_name_range)?; if setting_name != "hide_mouse_while_typing" { return None; diff --git a/crates/migrator/src/migrations/m_2025_05_29/settings.rs b/crates/migrator/src/migrations/m_2025_05_29/settings.rs index 56d72836fa..37ef0e45cc 100644 --- a/crates/migrator/src/migrations/m_2025_05_29/settings.rs +++ b/crates/migrator/src/migrations/m_2025_05_29/settings.rs @@ -19,7 +19,7 @@ fn replace_preferred_completion_mode_value( .nodes_for_capture_index(parent_object_capture_ix) .next()? .byte_range(); - let parent_object_name = contents.get(parent_object_range.clone())?; + let parent_object_name = contents.get(parent_object_range)?; if parent_object_name != "agent" { return None; @@ -30,7 +30,7 @@ fn replace_preferred_completion_mode_value( .nodes_for_capture_index(setting_name_capture_ix) .next()? .byte_range(); - let setting_name = contents.get(setting_name_range.clone())?; + let setting_name = contents.get(setting_name_range)?; if setting_name != "preferred_completion_mode" { return None; diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 0cc2f654ea..6b6d17a246 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -2427,7 +2427,7 @@ impl MultiBuffer { cx.emit(match event { language::BufferEvent::Edited => Event::Edited { singleton_buffer_edited: true, - edited_buffer: Some(buffer.clone()), + edited_buffer: Some(buffer), }, language::BufferEvent::DirtyChanged => Event::DirtyChanged, language::BufferEvent::Saved => Event::Saved, @@ -3560,9 +3560,7 @@ impl MultiBuffer { let multi = cx.new(|_| Self::new(Capability::ReadWrite)); for (text, ranges) in excerpts { let buffer = cx.new(|cx| Buffer::local(text, cx)); - let excerpt_ranges = ranges - .into_iter() - .map(|range| ExcerptRange::new(range.clone())); + let excerpt_ranges = ranges.into_iter().map(ExcerptRange::new); multi.update(cx, |multi, cx| { multi.push_excerpts(buffer, excerpt_ranges, cx) }); diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 77a70dfc8d..441d2ca4b7 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -126,7 +126,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement .gap_1() .child( h_flex() - .id(name.clone()) + .id(name) .relative() .w_full() .border_2() @@ -201,7 +201,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement }); } else { let appearance = *SystemAppearance::global(cx); - settings.set_theme(theme.clone(), appearance); + settings.set_theme(theme, appearance); } }); } diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 60a9856abe..8fae695854 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -104,7 +104,7 @@ fn write_ui_font_family(font: SharedString, cx: &mut App) { "Welcome Font Changed", type = "ui font", old = theme_settings.ui_font_family, - new = font.clone() + new = font ); theme_settings.ui_font_family = Some(FontFamilyName(font.into())); }); @@ -134,7 +134,7 @@ fn write_buffer_font_family(font_family: SharedString, cx: &mut App) { "Welcome Font Changed", type = "editor font", old = theme_settings.buffer_font_family, - new = font_family.clone() + new = font_family ); theme_settings.buffer_font_family = Some(FontFamilyName(font_family.into())); @@ -314,7 +314,7 @@ fn render_font_customization_section( .child( PopoverMenu::new("ui-font-picker") .menu({ - let ui_font_picker = ui_font_picker.clone(); + let ui_font_picker = ui_font_picker; move |_window, _cx| Some(ui_font_picker.clone()) }) .trigger( @@ -378,7 +378,7 @@ fn render_font_customization_section( .child( PopoverMenu::new("buffer-font-picker") .menu({ - let buffer_font_picker = buffer_font_picker.clone(); + let buffer_font_picker = buffer_font_picker; move |_window, _cx| Some(buffer_font_picker.clone()) }) .trigger( diff --git a/crates/onboarding/src/theme_preview.rs b/crates/onboarding/src/theme_preview.rs index 6a072b00e9..d84bc9b0e5 100644 --- a/crates/onboarding/src/theme_preview.rs +++ b/crates/onboarding/src/theme_preview.rs @@ -206,7 +206,7 @@ impl ThemePreviewTile { sidebar_width, skeleton_height.clone(), )) - .child(Self::render_pane(seed, theme, skeleton_height.clone())) + .child(Self::render_pane(seed, theme, skeleton_height)) } fn render_borderless(seed: f32, theme: Arc<Theme>) -> impl IntoElement { @@ -260,7 +260,7 @@ impl ThemePreviewTile { .overflow_hidden() .child(div().size_full().child(Self::render_editor( seed, - theme.clone(), + theme, sidebar_width, Self::SKELETON_HEIGHT_DEFAULT, ))) @@ -329,9 +329,9 @@ impl Component for ThemePreviewTile { let themes_to_preview = vec![ one_dark.clone().ok(), - one_light.clone().ok(), - gruvbox_dark.clone().ok(), - gruvbox_light.clone().ok(), + one_light.ok(), + gruvbox_dark.ok(), + gruvbox_light.ok(), ] .into_iter() .flatten() @@ -348,7 +348,7 @@ impl Component for ThemePreviewTile { div() .w(px(240.)) .h(px(180.)) - .child(ThemePreviewTile::new(one_dark.clone(), 0.42)) + .child(ThemePreviewTile::new(one_dark, 0.42)) .into_any_element(), )])] } else { diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 832b7f09d1..59c43f945f 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -5091,7 +5091,7 @@ impl Panel for OutlinePanel { impl Focusable for OutlinePanel { fn focus_handle(&self, cx: &App) -> FocusHandle { - self.filter_editor.focus_handle(cx).clone() + self.filter_editor.focus_handle(cx) } } diff --git a/crates/panel/src/panel.rs b/crates/panel/src/panel.rs index 658a51167b..1930f654e9 100644 --- a/crates/panel/src/panel.rs +++ b/crates/panel/src/panel.rs @@ -52,7 +52,7 @@ impl RenderOnce for PanelTab { pub fn panel_button(label: impl Into<SharedString>) -> ui::Button { let label = label.into(); - let id = ElementId::Name(label.clone().to_lowercase().replace(' ', "_").into()); + let id = ElementId::Name(label.to_lowercase().replace(' ', "_").into()); ui::Button::new(id, label) .label_size(ui::LabelSize::Small) .icon_size(ui::IconSize::Small) diff --git a/crates/picker/src/popover_menu.rs b/crates/picker/src/popover_menu.rs index d05308ee71..baf0918fd6 100644 --- a/crates/picker/src/popover_menu.rs +++ b/crates/picker/src/popover_menu.rs @@ -85,7 +85,7 @@ where .menu(move |_window, _cx| Some(picker.clone())) .trigger_with_tooltip(self.trigger, self.tooltip) .anchor(self.anchor) - .when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle)) + .when_some(self.handle, |menu, handle| menu.with_handle(handle)) .offset(gpui::Point { x: px(0.0), y: px(-2.0), diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index d365089377..a171b193d0 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -168,7 +168,7 @@ impl RemoteBufferStore { .with_context(|| { format!("no worktree found for id {}", file.worktree_id) })?; - buffer_file = Some(Arc::new(File::from_proto(file, worktree.clone(), cx)?) + buffer_file = Some(Arc::new(File::from_proto(file, worktree, cx)?) as Arc<dyn language::File>); } Buffer::from_proto(replica_id, capability, state, buffer_file) @@ -591,7 +591,7 @@ impl LocalBufferStore { else { return Task::ready(Err(anyhow!("no such worktree"))); }; - self.save_local_buffer(buffer, worktree, path.path.clone(), true, cx) + self.save_local_buffer(buffer, worktree, path.path, true, cx) } fn open_buffer( @@ -845,7 +845,7 @@ impl BufferStore { ) -> Task<Result<()>> { match &mut self.state { BufferStoreState::Local(this) => this.save_buffer(buffer, cx), - BufferStoreState::Remote(this) => this.save_remote_buffer(buffer.clone(), None, cx), + BufferStoreState::Remote(this) => this.save_remote_buffer(buffer, None, cx), } } @@ -1138,7 +1138,7 @@ impl BufferStore { envelope: TypedEnvelope<proto::UpdateBuffer>, mut cx: AsyncApp, ) -> Result<proto::Ack> { - let payload = envelope.payload.clone(); + let payload = envelope.payload; let buffer_id = BufferId::new(payload.buffer_id)?; let ops = payload .operations diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index e826f44b7b..49a430c261 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -760,7 +760,7 @@ mod tests { &store, vec![ (server_1_id.clone(), ContextServerStatus::Starting), - (server_1_id.clone(), ContextServerStatus::Running), + (server_1_id, ContextServerStatus::Running), (server_2_id.clone(), ContextServerStatus::Starting), (server_2_id.clone(), ContextServerStatus::Running), (server_2_id.clone(), ContextServerStatus::Stopped), diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index 343ee83ccb..c47e5d35d5 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -192,7 +192,7 @@ impl BreakpointStore { } pub(crate) fn shared(&mut self, project_id: u64, downstream_client: AnyProtoClient) { - self.downstream_client = Some((downstream_client.clone(), project_id)); + self.downstream_client = Some((downstream_client, project_id)); } pub(crate) fn unshared(&mut self, cx: &mut Context<Self>) { @@ -450,9 +450,9 @@ impl BreakpointStore { }); if let Some(found_bp) = found_bp { - found_bp.message = Some(log_message.clone()); + found_bp.message = Some(log_message); } else { - breakpoint.bp.message = Some(log_message.clone()); + breakpoint.bp.message = Some(log_message); // We did not remove any breakpoint, hence let's toggle one. breakpoint_set .breakpoints @@ -482,9 +482,9 @@ impl BreakpointStore { }); if let Some(found_bp) = found_bp { - found_bp.hit_condition = Some(hit_condition.clone()); + found_bp.hit_condition = Some(hit_condition); } else { - breakpoint.bp.hit_condition = Some(hit_condition.clone()); + breakpoint.bp.hit_condition = Some(hit_condition); // We did not remove any breakpoint, hence let's toggle one. breakpoint_set .breakpoints @@ -514,9 +514,9 @@ impl BreakpointStore { }); if let Some(found_bp) = found_bp { - found_bp.condition = Some(condition.clone()); + found_bp.condition = Some(condition); } else { - breakpoint.bp.condition = Some(condition.clone()); + breakpoint.bp.condition = Some(condition); // We did not remove any breakpoint, hence let's toggle one. breakpoint_set .breakpoints @@ -591,7 +591,7 @@ impl BreakpointStore { cx: &mut Context<Self>, ) { if let Some(breakpoints) = self.breakpoints.remove(&old_path) { - self.breakpoints.insert(new_path.clone(), breakpoints); + self.breakpoints.insert(new_path, breakpoints); cx.notify(); } diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index 3be3192369..772ff2dcfe 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -1454,7 +1454,7 @@ impl DapCommand for EvaluateCommand { variables_reference: message.variable_reference, named_variables: message.named_variables, indexed_variables: message.indexed_variables, - memory_reference: message.memory_reference.clone(), + memory_reference: message.memory_reference, value_location_reference: None, //TODO }) } diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 382e83587a..45e1c7f291 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -721,7 +721,7 @@ impl DapStore { downstream_client: AnyProtoClient, _: &mut Context<Self>, ) { - self.downstream_client = Some((downstream_client.clone(), project_id)); + self.downstream_client = Some((downstream_client, project_id)); } pub fn unshared(&mut self, cx: &mut Context<Self>) { diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index cd792877b6..81cb3ade2e 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -1394,7 +1394,7 @@ impl Session { let breakpoint_store = self.breakpoint_store.clone(); if let Some((local, path)) = self.as_running_mut().and_then(|local| { let breakpoint = local.tmp_breakpoint.take()?; - let path = breakpoint.path.clone(); + let path = breakpoint.path; Some((local, path)) }) { local @@ -1710,7 +1710,7 @@ impl Session { this.threads = result .into_iter() - .map(|thread| (ThreadId(thread.id), Thread::from(thread.clone()))) + .map(|thread| (ThreadId(thread.id), Thread::from(thread))) .collect(); this.invalidate_command_type::<StackTraceCommand>(); @@ -2553,10 +2553,7 @@ impl Session { mode: Option<String>, cx: &mut Context<Self>, ) -> Task<Option<dap::DataBreakpointInfoResponse>> { - let command = DataBreakpointInfoCommand { - context: context.clone(), - mode, - }; + let command = DataBreakpointInfoCommand { context, mode }; self.request(command, |_, response, _| response.ok(), cx) } diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index edc6b00a7b..5cf298a8bf 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -769,7 +769,7 @@ impl GitStore { .as_ref() .and_then(|weak| weak.upgrade()) { - let conflict_set = conflict_set.clone(); + let conflict_set = conflict_set; let buffer_snapshot = buffer.read(cx).text_snapshot(); git_state.update(cx, |state, cx| { @@ -912,7 +912,7 @@ impl GitStore { return Task::ready(Err(anyhow!("failed to find a git repository for buffer"))); }; let content = match &version { - Some(version) => buffer.rope_for_version(version).clone(), + Some(version) => buffer.rope_for_version(version), None => buffer.as_rope().clone(), }; let version = version.unwrap_or(buffer.version()); @@ -1506,10 +1506,7 @@ impl GitStore { let mut update = envelope.payload; let id = RepositoryId::from_proto(update.id); - let client = this - .upstream_client() - .context("no upstream client")? - .clone(); + let client = this.upstream_client().context("no upstream client")?; let mut is_new = false; let repo = this.repositories.entry(id).or_insert_with(|| { @@ -3418,7 +3415,6 @@ impl Repository { reset_mode: ResetMode, _cx: &mut App, ) -> oneshot::Receiver<Result<()>> { - let commit = commit.to_string(); let id = self.id; self.send_job(None, move |git_repo, _| async move { @@ -3644,7 +3640,7 @@ impl Repository { let to_stage = self .cached_status() .filter(|entry| !entry.status.staging().is_fully_staged()) - .map(|entry| entry.repo_path.clone()) + .map(|entry| entry.repo_path) .collect(); self.stage_entries(to_stage, cx) } @@ -3653,16 +3649,13 @@ impl Repository { let to_unstage = self .cached_status() .filter(|entry| entry.status.staging().has_staged()) - .map(|entry| entry.repo_path.clone()) + .map(|entry| entry.repo_path) .collect(); self.unstage_entries(to_unstage, cx) } pub fn stash_all(&mut self, cx: &mut Context<Self>) -> Task<anyhow::Result<()>> { - let to_stash = self - .cached_status() - .map(|entry| entry.repo_path.clone()) - .collect(); + let to_stash = self.cached_status().map(|entry| entry.repo_path).collect(); self.stash_entries(to_stash, cx) } diff --git a/crates/project/src/git_store/conflict_set.rs b/crates/project/src/git_store/conflict_set.rs index 9d7bd26a92..313a1e90ad 100644 --- a/crates/project/src/git_store/conflict_set.rs +++ b/crates/project/src/git_store/conflict_set.rs @@ -369,7 +369,7 @@ mod tests { .unindent(); let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(0, buffer_id, test_content.to_string()); + let buffer = Buffer::new(0, buffer_id, test_content); let snapshot = buffer.snapshot(); let conflict_snapshot = ConflictSet::parse(&snapshot); @@ -400,7 +400,7 @@ mod tests { >>>>>>> "# .unindent(); let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(0, buffer_id, test_content.to_string()); + let buffer = Buffer::new(0, buffer_id, test_content); let snapshot = buffer.snapshot(); let conflict_snapshot = ConflictSet::parse(&snapshot); diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs index c5a198954e..e499d4e026 100644 --- a/crates/project/src/image_store.rs +++ b/crates/project/src/image_store.rs @@ -244,7 +244,7 @@ impl ProjectItem for ImageItem { } fn project_path(&self, cx: &App) -> Option<ProjectPath> { - Some(self.project_path(cx).clone()) + Some(self.project_path(cx)) } fn is_dirty(&self) -> bool { @@ -375,7 +375,6 @@ impl ImageStore { let (mut tx, rx) = postage::watch::channel(); entry.insert(rx.clone()); - let project_path = project_path.clone(); let load_image = self .state .open_image(project_path.path.clone(), worktree, cx); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index de6848701f..a91e3fb402 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2739,7 +2739,7 @@ impl GetCodeActions { Some(lsp::CodeActionProviderCapability::Options(CodeActionOptions { code_action_kinds: Some(supported_action_kinds), .. - })) => Some(supported_action_kinds.clone()), + })) => Some(supported_action_kinds), _ => capabilities.code_action_kinds, } } @@ -3793,7 +3793,7 @@ impl GetDocumentDiagnostics { }, uri: lsp::Url::parse(&info.location_url.unwrap()).unwrap(), }, - message: info.message.clone(), + message: info.message, } }) .collect::<Vec<_>>(); @@ -4491,9 +4491,8 @@ mod tests { data: Some(json!({"detail": "test detail"})), }; - let proto_diagnostic = - GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic.clone()) - .expect("Failed to serialize diagnostic"); + let proto_diagnostic = GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic) + .expect("Failed to serialize diagnostic"); let start = proto_diagnostic.start.unwrap(); let end = proto_diagnostic.end.unwrap(); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index aa2398e29b..7a44ad3f87 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -917,7 +917,7 @@ impl LocalLspStore { message: params.message, actions: vec![], response_channel: tx, - lsp_name: name.clone(), + lsp_name: name, }; let _ = this.update(&mut cx, |_, cx| { @@ -2954,7 +2954,7 @@ impl LocalLspStore { .update(cx, |this, cx| { let path = buffer_to_edit.read(cx).project_path(cx); let active_entry = this.active_entry; - let is_active_entry = path.clone().is_some_and(|project_path| { + let is_active_entry = path.is_some_and(|project_path| { this.worktree_store .read(cx) .entry_for_path(&project_path, cx) @@ -5688,10 +5688,7 @@ impl LspStore { let all_actions_task = self.request_multiple_lsp_locally( buffer, Some(range.start), - GetCodeActions { - range: range.clone(), - kinds: kinds.clone(), - }, + GetCodeActions { range, kinds }, cx, ); cx.background_spawn(async move { @@ -7221,7 +7218,7 @@ impl LspStore { worktree = tree; path = rel_path; } else { - worktree = source_worktree.clone(); + worktree = source_worktree; path = relativize_path(&result.worktree_abs_path, &abs_path); } @@ -10338,7 +10335,7 @@ impl LspStore { let name = self .language_server_statuses .remove(&server_id) - .map(|status| status.name.clone()) + .map(|status| status.name) .or_else(|| { if let Some(LanguageServerState::Running { adapter, .. }) = server_state.as_ref() { Some(adapter.name()) diff --git a/crates/project/src/lsp_store/clangd_ext.rs b/crates/project/src/lsp_store/clangd_ext.rs index 274b1b8980..b02f68dd4d 100644 --- a/crates/project/src/lsp_store/clangd_ext.rs +++ b/crates/project/src/lsp_store/clangd_ext.rs @@ -58,7 +58,7 @@ pub fn register_notifications( language_server .on_notification::<InactiveRegions, _>({ - let adapter = adapter.clone(); + let adapter = adapter; let this = lsp_store; move |params: InactiveRegionsParams, cx| { diff --git a/crates/project/src/lsp_store/rust_analyzer_ext.rs b/crates/project/src/lsp_store/rust_analyzer_ext.rs index 6c425717a8..e5e6338d3c 100644 --- a/crates/project/src/lsp_store/rust_analyzer_ext.rs +++ b/crates/project/src/lsp_store/rust_analyzer_ext.rs @@ -34,7 +34,6 @@ pub fn register_notifications(lsp_store: WeakEntity<LspStore>, language_server: language_server .on_notification::<ServerStatus, _>({ - let name = name.clone(); move |params, cx| { let message = params.message; let log_message = message.as_ref().map(|message| { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9cd83647ac..af5fd0d675 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2502,7 +2502,7 @@ impl Project { path: ProjectPath, cx: &mut Context<Self>, ) -> Task<Result<(Option<ProjectEntryId>, Entity<Buffer>)>> { - let task = self.open_buffer(path.clone(), cx); + let task = self.open_buffer(path, cx); cx.spawn(async move |_project, cx| { let buffer = task.await?; let project_entry_id = buffer.read_with(cx, |buffer, cx| { @@ -3170,7 +3170,7 @@ impl Project { if let ImageItemEvent::ReloadNeeded = event && !self.is_via_collab() { - self.reload_images([image.clone()].into_iter().collect(), cx) + self.reload_images([image].into_iter().collect(), cx) .detach_and_log_err(cx); } @@ -3652,7 +3652,7 @@ impl Project { cx: &mut Context<Self>, ) -> Task<Result<Vec<CodeAction>>> { let snapshot = buffer.read(cx).snapshot(); - let range = range.clone().to_owned().to_point(&snapshot); + let range = range.to_point(&snapshot); let range_start = snapshot.anchor_before(range.start); let range_end = if range.start == range.end { range_start diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 70eb6d34f8..8b0b21fcd6 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1818,7 +1818,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp buffer .snapshot() .diagnostics_in_range::<_, usize>(0..1, false) - .map(|entry| entry.diagnostic.message.clone()) + .map(|entry| entry.diagnostic.message) .collect::<Vec<_>>(), ["the message".to_string()] ); @@ -1844,7 +1844,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp buffer .snapshot() .diagnostics_in_range::<_, usize>(0..1, false) - .map(|entry| entry.diagnostic.message.clone()) + .map(|entry| entry.diagnostic.message) .collect::<Vec<_>>(), Vec::<String>::new(), ); @@ -3712,7 +3712,7 @@ async fn test_save_file_spawns_language_server(cx: &mut gpui::TestAppContext) { async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/dir"), json!({ @@ -3767,7 +3767,7 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/dir"), json!({ @@ -5897,7 +5897,7 @@ async fn test_search_with_unicode(cx: &mut gpui::TestAppContext) { async fn test_create_entry(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/one/two", json!({ diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index e51f8e0b3b..15e6024808 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -760,7 +760,7 @@ impl Inventory { TaskSettingsLocation::Global(path) => { previously_existing_scenarios = parsed_scenarios .global_scenarios() - .map(|(_, scenario)| scenario.label.clone()) + .map(|(_, scenario)| scenario.label) .collect::<HashSet<_>>(); parsed_scenarios .global @@ -770,7 +770,7 @@ impl Inventory { TaskSettingsLocation::Worktree(location) => { previously_existing_scenarios = parsed_scenarios .worktree_scenarios(location.worktree_id) - .map(|(_, scenario)| scenario.label.clone()) + .map(|(_, scenario)| scenario.label) .collect::<HashSet<_>>(); if new_templates.is_empty() { diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index b2556d7584..e9582e73fd 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -89,7 +89,7 @@ impl Project { let ssh_client = ssh_client.read(cx); if let Some((SshArgs { arguments, envs }, path_style)) = ssh_client.ssh_info() { return Some(SshDetails { - host: ssh_client.connection_options().host.clone(), + host: ssh_client.connection_options().host, ssh_command: SshCommand { arguments }, envs, path_style, diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index 16e42e90cb..b8905c73bc 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -457,7 +457,7 @@ impl WorktreeStore { }) .collect::<HashMap<_, _>>(); - let (client, project_id) = self.upstream_client().clone().context("invalid project")?; + let (client, project_id) = self.upstream_client().context("invalid project")?; for worktree in worktrees { if let Some(old_worktree) = diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index bb612ac475..a5bfa883d5 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -447,7 +447,7 @@ impl ProjectPanel { cx.subscribe(&project, |this, project, event, cx| match event { project::Event::ActiveEntryChanged(Some(entry_id)) => { if ProjectPanelSettings::get_global(cx).auto_reveal_entries { - this.reveal_entry(project.clone(), *entry_id, true, cx).ok(); + this.reveal_entry(project, *entry_id, true, cx).ok(); } } project::Event::ActiveEntryChanged(None) => { @@ -462,10 +462,7 @@ impl ProjectPanel { } } project::Event::RevealInProjectPanel(entry_id) => { - if let Some(()) = this - .reveal_entry(project.clone(), *entry_id, false, cx) - .log_err() - { + if let Some(()) = this.reveal_entry(project, *entry_id, false, cx).log_err() { cx.emit(PanelEvent::Activate); } } @@ -813,7 +810,7 @@ impl ProjectPanel { diagnostic_severity: DiagnosticSeverity, ) { diagnostics - .entry((project_path.worktree_id, path_buffer.clone())) + .entry((project_path.worktree_id, path_buffer)) .and_modify(|strongest_diagnostic_severity| { *strongest_diagnostic_severity = cmp::min(*strongest_diagnostic_severity, diagnostic_severity); @@ -2780,7 +2777,7 @@ impl ProjectPanel { let destination_worktree = self.project.update(cx, |project, cx| { let entry_path = project.path_for_entry(entry_to_move, cx)?; - let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone(); + let destination_entry_path = project.path_for_entry(destination, cx)?.path; let mut destination_path = destination_entry_path.as_ref(); if destination_is_file { @@ -4023,8 +4020,8 @@ impl ProjectPanel { .as_ref() .map_or(ValidationState::None, |e| e.validation_state.clone()) { - ValidationState::Error(msg) => Some((Color::Error.color(cx), msg.clone())), - ValidationState::Warning(msg) => Some((Color::Warning.color(cx), msg.clone())), + ValidationState::Error(msg) => Some((Color::Error.color(cx), msg)), + ValidationState::Warning(msg) => Some((Color::Warning.color(cx), msg)), ValidationState::None => None, } } else { @@ -5505,7 +5502,7 @@ impl Render for ProjectPanel { .with_priority(3) })) } else { - let focus_handle = self.focus_handle(cx).clone(); + let focus_handle = self.focus_handle(cx); v_flex() .id("empty-project_panel") diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index de3316e357..49b482e02c 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -17,7 +17,7 @@ use workspace::{ async fn test_visible_list(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -106,7 +106,7 @@ async fn test_visible_list(cx: &mut gpui::TestAppContext) { async fn test_opening_file(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/src"), json!({ @@ -276,7 +276,7 @@ async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) { async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root1"), json!({ @@ -459,7 +459,7 @@ async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) { async fn test_editing_files(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -877,7 +877,7 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) { async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -1010,7 +1010,7 @@ async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root1"), json!({ @@ -1137,7 +1137,7 @@ async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) { async fn test_copy_paste(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -1235,7 +1235,7 @@ async fn test_copy_paste(cx: &mut gpui::TestAppContext) { async fn test_cut_paste(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -1320,7 +1320,7 @@ async fn test_cut_paste(cx: &mut gpui::TestAppContext) { async fn test_cut_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -1416,7 +1416,7 @@ async fn test_cut_paste_between_different_worktrees(cx: &mut gpui::TestAppContex async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -1551,7 +1551,7 @@ async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppConte async fn test_copy_paste_directory(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -1692,7 +1692,7 @@ async fn test_copy_paste_directory(cx: &mut gpui::TestAppContext) { async fn test_copy_paste_directory_with_sibling_file(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/test", json!({ @@ -1797,7 +1797,7 @@ async fn test_copy_paste_directory_with_sibling_file(cx: &mut gpui::TestAppConte async fn test_copy_paste_nested_and_root_entries(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/test", json!({ @@ -1876,7 +1876,7 @@ async fn test_copy_paste_nested_and_root_entries(cx: &mut gpui::TestAppContext) async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/src"), json!({ @@ -1968,7 +1968,7 @@ async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) { async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/src", json!({ @@ -2161,7 +2161,7 @@ async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { async fn test_select_git_entry(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), json!({ @@ -2440,7 +2440,7 @@ async fn test_select_git_entry(cx: &mut gpui::TestAppContext) { async fn test_select_directory(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/project_root", json!({ @@ -2541,7 +2541,7 @@ async fn test_select_directory(cx: &mut gpui::TestAppContext) { async fn test_select_first_last(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/project_root", json!({ @@ -2651,7 +2651,7 @@ async fn test_select_first_last(cx: &mut gpui::TestAppContext) { async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/project_root", json!({ @@ -2693,7 +2693,7 @@ async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) { async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/project_root", json!({ @@ -2751,7 +2751,7 @@ async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { async fn test_new_file_move(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.as_fake().insert_tree(path!("/root"), json!({})).await; let project = Project::test(fs, [path!("/root").as_ref()], cx).await; let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); @@ -2819,7 +2819,7 @@ async fn test_new_file_move(cx: &mut gpui::TestAppContext) { async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -2895,7 +2895,7 @@ async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) { async fn test_rename_with_hide_root(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -2989,7 +2989,7 @@ async fn test_rename_with_hide_root(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/project_root", json!({ @@ -3731,7 +3731,7 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) { register_project_item::<TestProjectItemView>(cx); }); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -3914,7 +3914,7 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) { async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/src", json!({ @@ -3982,7 +3982,7 @@ async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppC async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -4105,7 +4105,7 @@ async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) { async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), json!({ @@ -4206,7 +4206,7 @@ async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) { async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), json!({ @@ -4271,7 +4271,7 @@ async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) { async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -4382,7 +4382,7 @@ async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) { async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -4457,7 +4457,7 @@ async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) { async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -4523,7 +4523,7 @@ async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) { async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); // First worktree fs.insert_tree( "/root1", @@ -4666,7 +4666,7 @@ async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) { async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -4766,7 +4766,7 @@ async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root_b", json!({ @@ -4859,7 +4859,7 @@ fn toggle_expand_dir( async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), json!({ @@ -5050,7 +5050,7 @@ async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) { async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), json!({ @@ -5234,7 +5234,7 @@ async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) { async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), json!({ @@ -5299,7 +5299,7 @@ async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) { async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), json!({ @@ -5448,7 +5448,7 @@ async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppC async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -5516,7 +5516,7 @@ async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) { async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -5647,7 +5647,7 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext) async fn test_hide_root(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root1", json!({ @@ -5825,7 +5825,7 @@ async fn test_hide_root(cx: &mut gpui::TestAppContext) { async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -5923,7 +5923,7 @@ async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) { async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor().clone()); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -6152,7 +6152,7 @@ fn init_test_with_editor(cx: &mut TestAppContext) { language::init(cx); editor::init(cx); crate::init(cx); - workspace::init(app_state.clone(), cx); + workspace::init(app_state, cx); Project::init_settings(cx); cx.update_global::<SettingsStore, _>(|store, cx| { diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 9d0f54bc01..72029e55a0 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -233,7 +233,7 @@ impl PickerDelegate for ProjectSymbolsDelegate { } } let label = symbol.label.text.clone(); - let path = path.to_string().clone(); + let path = path.to_string(); let highlights = gpui::combine_highlights( string_match @@ -257,10 +257,8 @@ impl PickerDelegate for ProjectSymbolsDelegate { v_flex() .child( LabelLike::new().child( - StyledText::new(label).with_default_highlights( - &window.text_style().clone(), - highlights, - ), + StyledText::new(label) + .with_default_highlights(&window.text_style(), highlights), ), ) .child(Label::new(path).color(Color::Muted)), diff --git a/crates/prompt_store/src/prompts.rs b/crates/prompt_store/src/prompts.rs index 4ab867ab64..9a9b2fc3de 100644 --- a/crates/prompt_store/src/prompts.rs +++ b/crates/prompt_store/src/prompts.rs @@ -403,7 +403,7 @@ impl PromptBuilder { ContentPromptDiagnosticContext { line_number: (start.row + 1) as usize, error_message: entry.diagnostic.message.clone(), - code_content: buffer.text_for_range(entry.range.clone()).collect(), + code_content: buffer.text_for_range(entry.range).collect(), } }) .collect(); diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 0f43d83d86..a9c3284d0b 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -119,7 +119,7 @@ impl EditNicknameState { let starting_text = SshSettings::get_global(cx) .ssh_connections() .nth(index) - .and_then(|state| state.nickname.clone()) + .and_then(|state| state.nickname) .filter(|text| !text.is_empty()); this.editor.update(cx, |this, cx| { this.set_placeholder_text("Add a nickname for this server", cx); @@ -165,7 +165,7 @@ impl ProjectPicker { let nickname = connection.nickname.clone().map(|nick| nick.into()); let _path_task = cx .spawn_in(window, { - let workspace = workspace.clone(); + let workspace = workspace; async move |this, cx| { let Ok(Some(paths)) = rx.await else { workspace @@ -520,7 +520,7 @@ impl RemoteServerProjects { self.mode = Mode::CreateRemoteServer(CreateRemoteServer { address_editor: editor, address_error: None, - ssh_prompt: Some(ssh_prompt.clone()), + ssh_prompt: Some(ssh_prompt), _creating: Some(creating), }); } @@ -843,7 +843,7 @@ impl RemoteServerProjects { .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) .child(Label::new("Open Folder")) .on_click(cx.listener({ - let ssh_connection = connection.clone(); + let ssh_connection = connection; let host = host.clone(); move |this, _, window, cx| { let new_ix = this.create_host_from_ssh_config(&host, cx); @@ -1376,7 +1376,7 @@ impl RemoteServerProjects { }; let connection_string = connection.host.clone(); - let nickname = connection.nickname.clone().map(|s| s.into()); + let nickname = connection.nickname.map(|s| s.into()); v_flex() .id("ssh-edit-nickname") diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index 670fcb4800..d07ea48c7e 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -681,7 +681,7 @@ pub async fn open_ssh_project( window .update(cx, |workspace, _, cx| { - if let Some(client) = workspace.project().read(cx).ssh_client().clone() { + if let Some(client) = workspace.project().read(cx).ssh_client() { ExtensionStore::global(cx) .update(cx, |store, cx| store.register_ssh_client(client, cx)); } diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index fddf47660d..5fa3a5f715 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -233,8 +233,8 @@ impl SshConnectionOptions { }; Ok(Self { - host: hostname.to_string(), - username: username.clone(), + host: hostname, + username, port, port_forwards, args: Some(args), @@ -1363,7 +1363,7 @@ impl ConnectionPool { impl From<SshRemoteClient> for AnyProtoClient { fn from(client: SshRemoteClient) -> Self { - AnyProtoClient::new(client.client.clone()) + AnyProtoClient::new(client.client) } } diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 3bcdcbd73c..83caebe62f 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -237,11 +237,11 @@ impl HeadlessProject { session.add_entity_message_handler(BufferStore::handle_close_buffer); session.add_request_handler( - extensions.clone().downgrade(), + extensions.downgrade(), HeadlessExtensionStore::handle_sync_extensions, ); session.add_request_handler( - extensions.clone().downgrade(), + extensions.downgrade(), HeadlessExtensionStore::handle_install_extension, ); diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 4ce133cbb1..b8a7351552 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -160,7 +160,7 @@ fn init_panic_hook(session_id: String) { let panic_data = telemetry_events::Panic { thread: thread_name.into(), - payload: payload.clone(), + payload, location_data: info.location().map(|location| LocationData { file: location.file().into(), line: location.line(), @@ -799,7 +799,6 @@ fn initialize_settings( watch_config_file(cx.background_executor(), fs, paths::settings_file().clone()); handle_settings_file_changes(user_settings_file_rx, cx, { - let session = session.clone(); move |err, _cx| { if let Some(e) = err { log::info!("Server settings failed to change: {}", e); diff --git a/crates/repl/src/components/kernel_options.rs b/crates/repl/src/components/kernel_options.rs index 714cb3aed3..bceefd08cc 100644 --- a/crates/repl/src/components/kernel_options.rs +++ b/crates/repl/src/components/kernel_options.rs @@ -187,7 +187,7 @@ impl PickerDelegate for KernelPickerDelegate { .size(LabelSize::Default), ), ) - .when_some(path_or_url.clone(), |flex, path| { + .when_some(path_or_url, |flex, path| { flex.text_ellipsis().child( Label::new(path) .size(LabelSize::Small) diff --git a/crates/repl/src/kernels/remote_kernels.rs b/crates/repl/src/kernels/remote_kernels.rs index 1bef6c24db..6bc8b0d1b1 100644 --- a/crates/repl/src/kernels/remote_kernels.rs +++ b/crates/repl/src/kernels/remote_kernels.rs @@ -95,7 +95,7 @@ pub async fn list_remote_kernelspecs( .kernelspecs .into_iter() .map(|(name, spec)| RemoteKernelSpecification { - name: name.clone(), + name, url: remote_server.base_url.clone(), token: remote_server.token.clone(), kernelspec: spec.spec, @@ -103,7 +103,7 @@ pub async fn list_remote_kernelspecs( .collect::<Vec<RemoteKernelSpecification>>(); anyhow::ensure!(!remote_kernelspecs.is_empty(), "No kernel specs found"); - Ok(remote_kernelspecs.clone()) + Ok(remote_kernelspecs) } impl PartialEq for RemoteKernelSpecification { diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs index 1508c2b531..767b103435 100644 --- a/crates/repl/src/outputs.rs +++ b/crates/repl/src/outputs.rs @@ -228,26 +228,23 @@ impl Output { .child(div().flex_1().children(content)) .children(match self { Self::Plain { content, .. } => { - Self::render_output_controls(content.clone(), workspace.clone(), window, cx) + Self::render_output_controls(content.clone(), workspace, window, cx) } Self::Markdown { content, .. } => { - Self::render_output_controls(content.clone(), workspace.clone(), window, cx) + Self::render_output_controls(content.clone(), workspace, window, cx) } Self::Stream { content, .. } => { - Self::render_output_controls(content.clone(), workspace.clone(), window, cx) + Self::render_output_controls(content.clone(), workspace, window, cx) } Self::Image { content, .. } => { - Self::render_output_controls(content.clone(), workspace.clone(), window, cx) + Self::render_output_controls(content.clone(), workspace, window, cx) + } + Self::ErrorOutput(err) => { + Self::render_output_controls(err.traceback.clone(), workspace, window, cx) } - Self::ErrorOutput(err) => Self::render_output_controls( - err.traceback.clone(), - workspace.clone(), - window, - cx, - ), Self::Message(_) => None, Self::Table { content, .. } => { - Self::render_output_controls(content.clone(), workspace.clone(), window, cx) + Self::render_output_controls(content.clone(), workspace, window, cx) } Self::ClearOutputWaitMarker => None, }) diff --git a/crates/repl/src/outputs/markdown.rs b/crates/repl/src/outputs/markdown.rs index 118260ae94..bd88f4e159 100644 --- a/crates/repl/src/outputs/markdown.rs +++ b/crates/repl/src/outputs/markdown.rs @@ -35,7 +35,7 @@ impl MarkdownView { }); Self { - raw_text: text.clone(), + raw_text: text, image_cache: RetainAllImageCache::new(cx), contents: None, parsing_markdown_task: Some(task), diff --git a/crates/repl/src/repl_editor.rs b/crates/repl/src/repl_editor.rs index e97223ceb9..b4c928c33e 100644 --- a/crates/repl/src/repl_editor.rs +++ b/crates/repl/src/repl_editor.rs @@ -202,7 +202,7 @@ pub fn session(editor: WeakEntity<Editor>, cx: &mut App) -> SessionSupport { return SessionSupport::Unsupported; }; - let worktree_id = worktree_id_for_editor(editor.clone(), cx); + let worktree_id = worktree_id_for_editor(editor, cx); let Some(worktree_id) = worktree_id else { return SessionSupport::Unsupported; @@ -216,7 +216,7 @@ pub fn session(editor: WeakEntity<Editor>, cx: &mut App) -> SessionSupport { Some(kernelspec) => SessionSupport::Inactive(kernelspec), None => { // For language_supported, need to check available kernels for language - if language_supported(&language.clone(), cx) { + if language_supported(&language, cx) { SessionSupport::RequiresSetup(language.name()) } else { SessionSupport::Unsupported @@ -326,7 +326,7 @@ pub fn setup_editor_session_actions(editor: &mut Editor, editor_handle: WeakEnti editor .register_action({ - let editor_handle = editor_handle.clone(); + let editor_handle = editor_handle; move |_: &Restart, window, cx| { if !JupyterSettings::enabled(cx) { return; @@ -420,7 +420,7 @@ fn runnable_ranges( if let Some(language) = buffer.language() && language.name() == "Markdown".into() { - return (markdown_code_blocks(buffer, range.clone(), cx), None); + return (markdown_code_blocks(buffer, range, cx), None); } let (jupytext_snippets, next_cursor) = jupytext_cells(buffer, range.clone()); @@ -685,8 +685,8 @@ mod tests { let python = languages::language("python", tree_sitter_python::LANGUAGE.into()); let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone())); language_registry.add(markdown.clone()); - language_registry.add(typescript.clone()); - language_registry.add(python.clone()); + language_registry.add(typescript); + language_registry.add(python); // Two code blocks intersecting with selection let buffer = cx.new(|cx| { diff --git a/crates/repl/src/repl_sessions_ui.rs b/crates/repl/src/repl_sessions_ui.rs index f57dd64770..493b8aa950 100644 --- a/crates/repl/src/repl_sessions_ui.rs +++ b/crates/repl/src/repl_sessions_ui.rs @@ -129,7 +129,6 @@ pub fn init(cx: &mut App) { editor .register_action({ - let editor_handle = editor_handle.clone(); move |_: &RunInPlace, window, cx| { if !JupyterSettings::enabled(cx) { return; diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index f945e5ed9f..674639c402 100644 --- a/crates/repl/src/session.rs +++ b/crates/repl/src/session.rs @@ -460,7 +460,6 @@ impl Session { Kernel::StartingKernel(task) => { // Queue up the execution as a task to run after the kernel starts let task = task.clone(); - let message = message.clone(); cx.spawn(async move |this, cx| { task.await; @@ -568,7 +567,7 @@ impl Session { match kernel { Kernel::RunningKernel(mut kernel) => { - let mut request_tx = kernel.request_tx().clone(); + let mut request_tx = kernel.request_tx(); let forced = kernel.force_shutdown(window, cx); @@ -605,7 +604,7 @@ impl Session { // Do nothing if already restarting } Kernel::RunningKernel(mut kernel) => { - let mut request_tx = kernel.request_tx().clone(); + let mut request_tx = kernel.request_tx(); let forced = kernel.force_shutdown(window, cx); diff --git a/crates/rope/src/chunk.rs b/crates/rope/src/chunk.rs index 379daa4224..00679d8cf5 100644 --- a/crates/rope/src/chunk.rs +++ b/crates/rope/src/chunk.rs @@ -408,7 +408,7 @@ impl<'a> ChunkSlice<'a> { } let row_offset_range = self.offset_range_for_row(point.0.row); - let line = self.slice(row_offset_range.clone()); + let line = self.slice(row_offset_range); if point.0.column == 0 { Point::new(point.0.row, 0) } else if point.0.column >= line.len_utf16().0 as u32 { diff --git a/crates/rpc/src/conn.rs b/crates/rpc/src/conn.rs index 78db80e398..e598e5f7bc 100644 --- a/crates/rpc/src/conn.rs +++ b/crates/rpc/src/conn.rs @@ -80,7 +80,6 @@ impl Connection { }); let rx = rx.then({ - let executor = executor.clone(); move |msg| { let killed = killed.clone(); let executor = executor.clone(); diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 98f5fa40e9..73be0f19fe 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -378,7 +378,6 @@ impl Peer { impl Future<Output = anyhow::Result<()>> + Send + use<>, BoxStream<'static, Box<dyn AnyTypedEnvelope>>, ) { - let executor = executor.clone(); self.add_connection(connection, move |duration| executor.timer(duration)) } diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index bebe4315e4..5ad3996e78 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -418,7 +418,7 @@ impl RulesLibrary { } else { None }, - store: store.clone(), + store, language_registry, rule_editors: HashMap::default(), active_rule_id: None, @@ -1136,7 +1136,7 @@ impl RulesLibrary { .child( Label::new(format!( "{} tokens", - label_token_count.clone() + label_token_count )) .color(Color::Muted), ) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 75042f184f..a38dc8c35b 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -716,10 +716,10 @@ impl BufferSearchBar { self.replace_enabled = deploy.replace_enabled; self.selection_search_enabled = deploy.selection_search_enabled; if deploy.focus { - let mut handle = self.query_editor.focus_handle(cx).clone(); + let mut handle = self.query_editor.focus_handle(cx); let mut select_query = true; if deploy.replace_enabled && handle.is_focused(window) { - handle = self.replacement_editor.focus_handle(cx).clone(); + handle = self.replacement_editor.focus_handle(cx); select_query = false; }; diff --git a/crates/search/src/buffer_search/registrar.rs b/crates/search/src/buffer_search/registrar.rs index 4351e38618..0e227cbb7c 100644 --- a/crates/search/src/buffer_search/registrar.rs +++ b/crates/search/src/buffer_search/registrar.rs @@ -42,7 +42,6 @@ impl<T: 'static> SearchActionsRegistrar for DivRegistrar<'_, '_, T> { self.div = self.div.take().map(|div| { div.on_action(self.cx.listener(move |this, action, window, cx| { let should_notify = (getter)(this, window, cx) - .clone() .map(|search_bar| { search_bar.update(cx, |search_bar, cx| { callback.execute(search_bar, action, window, cx) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 0886654d62..c4ba9b5154 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -3716,7 +3716,7 @@ pub mod tests { window .update(cx, |_, _, cx| { search_view.update(cx, |search_view, cx| { - search_view.query_editor.read(cx).text(cx).to_string() + search_view.query_editor.read(cx).text(cx) }) }) .unwrap() @@ -3883,7 +3883,6 @@ pub mod tests { // Add a project search item to the second pane window .update(cx, { - let search_bar = search_bar.clone(); |workspace, window, cx| { assert_eq!(workspace.panes().len(), 2); second_pane.update(cx, |pane, cx| { diff --git a/crates/semantic_index/examples/index.rs b/crates/semantic_index/examples/index.rs index da27b8ad22..86f1e53a60 100644 --- a/crates/semantic_index/examples/index.rs +++ b/crates/semantic_index/examples/index.rs @@ -35,7 +35,7 @@ fn main() { None, )); let client = client::Client::new(clock, http.clone(), cx); - Client::set_global(client.clone(), cx); + Client::set_global(client, cx); let args: Vec<String> = std::env::args().collect(); if args.len() < 2 { @@ -49,7 +49,7 @@ fn main() { let api_key = std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY not set"); let embedding_provider = Arc::new(OpenAiEmbeddingProvider::new( - http.clone(), + http, OpenAiEmbeddingModel::TextEmbedding3Small, open_ai::OPEN_AI_API_URL.to_string(), api_key, diff --git a/crates/semantic_index/src/embedding_index.rs b/crates/semantic_index/src/embedding_index.rs index eeb3c91fcd..c54cd9d3c3 100644 --- a/crates/semantic_index/src/embedding_index.rs +++ b/crates/semantic_index/src/embedding_index.rs @@ -88,7 +88,7 @@ impl EmbeddingIndex { let worktree = self.worktree.read(cx).snapshot(); let worktree_abs_path = worktree.abs_path().clone(); - let scan = self.scan_updated_entries(worktree, updated_entries.clone(), cx); + let scan = self.scan_updated_entries(worktree, updated_entries, cx); let chunk = self.chunk_files(worktree_abs_path, scan.updated_entries, cx); let embed = Self::embed_files(self.embedding_provider.clone(), chunk.files, cx); let persist = self.persist_embeddings(scan.deleted_entry_ranges, embed.files, cx); @@ -406,7 +406,7 @@ impl EmbeddingIndex { .context("failed to create read transaction")?; let result = db .iter(&tx)? - .map(|entry| Ok(entry?.1.path.clone())) + .map(|entry| Ok(entry?.1.path)) .collect::<Result<Vec<Arc<Path>>>>(); drop(tx); result @@ -423,8 +423,7 @@ impl EmbeddingIndex { Ok(db .get(&tx, &db_key_for_path(&path))? .context("no such path")? - .chunks - .clone()) + .chunks) }) } } diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 1dafeb072f..439791047a 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -434,7 +434,7 @@ mod tests { .await; let range = search_result.range.clone(); - let content = content[range.clone()].to_owned(); + let content = content[range].to_owned(); assert!(content.contains("garbage in, garbage out")); } diff --git a/crates/semantic_index/src/summary_index.rs b/crates/semantic_index/src/summary_index.rs index d1c9a3abac..9a3eb302ed 100644 --- a/crates/semantic_index/src/summary_index.rs +++ b/crates/semantic_index/src/summary_index.rs @@ -205,7 +205,7 @@ impl SummaryIndex { let worktree = self.worktree.read(cx).snapshot(); let worktree_abs_path = worktree.abs_path().clone(); - backlogged = self.scan_updated_entries(worktree, updated_entries.clone(), cx); + backlogged = self.scan_updated_entries(worktree, updated_entries, cx); digest = self.digest_files(backlogged.paths_to_digest, worktree_abs_path, cx); needs_summary = self.check_summary_cache(digest.files, cx); summaries = self.summarize_files(needs_summary.files, cx); diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs index a472c50e6c..8080ec8d5f 100644 --- a/crates/settings/src/settings_json.rs +++ b/crates/settings/src/settings_json.rs @@ -361,7 +361,7 @@ pub fn replace_top_level_array_value_in_json_text( let needs_indent = range.start_point.row > 0; if new_value.is_none() && key_path.is_empty() { - let mut remove_range = text_range.clone(); + let mut remove_range = text_range; if index == 0 { while cursor.goto_next_sibling() && (cursor.node().is_extra() || cursor.node().is_missing()) @@ -582,7 +582,7 @@ mod tests { expected: String, ) { let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None); - let mut result_str = input.to_string(); + let mut result_str = input; result_str.replace_range(result.0, &result.1); pretty_assertions::assert_eq!(expected, result_str); } diff --git a/crates/settings_profile_selector/src/settings_profile_selector.rs b/crates/settings_profile_selector/src/settings_profile_selector.rs index 25be67bfd7..d7ebd6488d 100644 --- a/crates/settings_profile_selector/src/settings_profile_selector.rs +++ b/crates/settings_profile_selector/src/settings_profile_selector.rs @@ -135,7 +135,7 @@ impl SettingsProfileSelectorDelegate { ) -> Option<String> { if let Some(profile_name) = profile_name { cx.set_global(ActiveSettingsProfileName(profile_name.clone())); - return Some(profile_name.clone()); + return Some(profile_name); } if cx.has_global::<ActiveSettingsProfileName>() { diff --git a/crates/settings_ui/src/appearance_settings_controls.rs b/crates/settings_ui/src/appearance_settings_controls.rs index 141ae13182..255f5a36b5 100644 --- a/crates/settings_ui/src/appearance_settings_controls.rs +++ b/crates/settings_ui/src/appearance_settings_controls.rs @@ -83,7 +83,7 @@ impl RenderOnce for ThemeControl { DropdownMenu::new( "theme", - value.clone(), + value, ContextMenu::build(window, cx, |mut menu, _, cx| { let theme_registry = ThemeRegistry::global(cx); @@ -204,7 +204,7 @@ impl RenderOnce for UiFontFamilyControl { .child(Icon::new(IconName::Font)) .child(DropdownMenu::new( "ui-font-family", - value.clone(), + value, ContextMenu::build(window, cx, |mut menu, _, cx| { let font_family_cache = FontFamilyCache::global(cx); diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 12e3c0c274..9a2d33ef7c 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -1182,8 +1182,8 @@ impl KeymapEditor { return; }; - telemetry::event!("Keybinding Context Copied", context = context.clone()); - cx.write_to_clipboard(gpui::ClipboardItem::new_string(context.clone())); + telemetry::event!("Keybinding Context Copied", context = context); + cx.write_to_clipboard(gpui::ClipboardItem::new_string(context)); } fn copy_action_to_clipboard( @@ -1199,8 +1199,8 @@ impl KeymapEditor { return; }; - telemetry::event!("Keybinding Action Copied", action = action.clone()); - cx.write_to_clipboard(gpui::ClipboardItem::new_string(action.clone())); + telemetry::event!("Keybinding Action Copied", action = action); + cx.write_to_clipboard(gpui::ClipboardItem::new_string(action)); } fn toggle_conflict_filter( @@ -1464,7 +1464,7 @@ impl RenderOnce for KeybindContextString { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { match self { KeybindContextString::Global => { - muted_styled_text(KeybindContextString::GLOBAL.clone(), cx).into_any_element() + muted_styled_text(KeybindContextString::GLOBAL, cx).into_any_element() } KeybindContextString::Local(name, language) => { SyntaxHighlightedText::new(name, language).into_any_element() @@ -1748,7 +1748,7 @@ impl Render for KeymapEditor { } else { const NULL: SharedString = SharedString::new_static("<null>"); - muted_styled_text(NULL.clone(), cx) + muted_styled_text(NULL, cx) .into_any_element() } }) diff --git a/crates/story/src/story.rs b/crates/story/src/story.rs index 6fed0ab12d..b59cb6fb99 100644 --- a/crates/story/src/story.rs +++ b/crates/story/src/story.rs @@ -194,7 +194,7 @@ impl RenderOnce for StorySection { // Section title .py_2() // Section description - .when_some(self.description.clone(), |section, description| { + .when_some(self.description, |section, description| { section.child(Story::description(description, cx)) }) .child(div().flex().flex_col().gap_2().children(children)) diff --git a/crates/supermaven/src/supermaven.rs b/crates/supermaven/src/supermaven.rs index 743c0d4c7d..7a9963dbc4 100644 --- a/crates/supermaven/src/supermaven.rs +++ b/crates/supermaven/src/supermaven.rs @@ -384,9 +384,7 @@ impl SupermavenAgent { match message { SupermavenMessage::ActivationRequest(request) => { self.account_status = match request.activate_url { - Some(activate_url) => AccountStatus::NeedsActivation { - activate_url: activate_url.clone(), - }, + Some(activate_url) => AccountStatus::NeedsActivation { activate_url }, None => AccountStatus::Ready, }; } diff --git a/crates/supermaven/src/supermaven_completion_provider.rs b/crates/supermaven/src/supermaven_completion_provider.rs index 1b1fc54a7a..eb54c83f81 100644 --- a/crates/supermaven/src/supermaven_completion_provider.rs +++ b/crates/supermaven/src/supermaven_completion_provider.rs @@ -45,9 +45,7 @@ fn completion_from_diff( position: Anchor, delete_range: Range<Anchor>, ) -> EditPrediction { - let buffer_text = snapshot - .text_for_range(delete_range.clone()) - .collect::<String>(); + let buffer_text = snapshot.text_for_range(delete_range).collect::<String>(); let mut edits: Vec<(Range<language::Anchor>, String)> = Vec::new(); diff --git a/crates/task/src/static_source.rs b/crates/task/src/static_source.rs index 0e7a021b06..9e4051ef97 100644 --- a/crates/task/src/static_source.rs +++ b/crates/task/src/static_source.rs @@ -75,7 +75,6 @@ impl<T: PartialEq + 'static + Sync> TrackedFile<T> { { let parsed_contents: Arc<RwLock<T>> = Arc::default(); cx.background_spawn({ - let parsed_contents = parsed_contents.clone(); async move { while let Some(new_contents) = tracker.next().await { if Arc::strong_count(&parsed_contents) == 1 { diff --git a/crates/task/src/task.rs b/crates/task/src/task.rs index aae28ab874..85e654eff4 100644 --- a/crates/task/src/task.rs +++ b/crates/task/src/task.rs @@ -100,7 +100,7 @@ impl SpawnInTerminal { command: proto.command.clone(), args: proto.args.clone(), env: proto.env.into_iter().collect(), - cwd: proto.cwd.map(PathBuf::from).clone(), + cwd: proto.cwd.map(PathBuf::from), ..Default::default() } } diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 24e11d7715..3d1d180557 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -183,6 +183,10 @@ impl TaskTemplate { &mut substituted_variables, )? } else { + #[allow( + clippy::redundant_clone, + reason = "We want to clone the full_label to avoid borrowing it in the fold closure" + )] full_label.clone() } .lines() @@ -453,7 +457,7 @@ mod tests { TaskTemplate { label: "".to_string(), command: "".to_string(), - ..task_with_all_properties.clone() + ..task_with_all_properties }, ] { assert_eq!( @@ -521,7 +525,7 @@ mod tests { ); let cx = TaskContext { - cwd: Some(context_cwd.clone()), + cwd: Some(context_cwd), task_variables: TaskVariables::default(), project_env: HashMap::default(), }; @@ -768,7 +772,7 @@ mod tests { "test_env_key".to_string(), format!("test_env_var_{}", VariableName::Symbol.template_value()), )]), - ..task_with_all_properties.clone() + ..task_with_all_properties }, ] .into_iter() @@ -871,7 +875,7 @@ mod tests { let context = TaskContext { cwd: None, - task_variables: TaskVariables::from_iter(all_variables.clone()), + task_variables: TaskVariables::from_iter(all_variables), project_env, }; diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index a4fdc24e17..3f3a4cc116 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -434,7 +434,7 @@ mod tests { ) .await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let worktree_store = project.read_with(cx, |project, _| project.worktree_store().clone()); + let worktree_store = project.read_with(cx, |project, _| project.worktree_store()); let rust_language = Arc::new( Language::new( LanguageConfig::default(), diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 16c1efabba..b38a69f095 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -486,7 +486,7 @@ impl TerminalBuilder { //And connect them together let event_loop = EventLoop::new( term.clone(), - ZedListener(events_tx.clone()), + ZedListener(events_tx), pty, pty_options.drain_on_exit, false, @@ -1661,7 +1661,7 @@ impl Terminal { #[cfg(any(target_os = "linux", target_os = "freebsd"))] MouseButton::Middle => { if let Some(item) = _cx.read_from_primary() { - let text = item.text().unwrap_or_default().to_string(); + let text = item.text().unwrap_or_default(); self.input(text.into_bytes()); } } diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 1c38dbc877..c2fbeb7ee6 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -653,7 +653,7 @@ impl TerminalElement { let terminal = self.terminal.clone(); let hitbox = hitbox.clone(); let focus = focus.clone(); - let terminal_view = terminal_view.clone(); + let terminal_view = terminal_view; move |e: &MouseMoveEvent, phase, window, cx| { if phase != DispatchPhase::Bubble { return; @@ -1838,8 +1838,7 @@ mod tests { }; let font_size = AbsoluteLength::Pixels(px(12.0)); - let batch = - BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'a', style1.clone(), font_size); + let batch = BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'a', style1, font_size); // Should be able to append same style assert!(batch.can_append(&style2)); diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 1d76f70152..f40c4870f1 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -181,7 +181,6 @@ impl TerminalPanel { .anchor(Corner::TopRight) .with_handle(pane.split_item_context_menu_handle.clone()) .menu({ - let split_context = split_context.clone(); move |window, cx| { ContextMenu::build(window, cx, |menu, _, _| { menu.when_some( diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index e02324a142..c54010b4b0 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -257,9 +257,9 @@ pub fn refine_theme_family(theme_family_content: ThemeFamilyContent) -> ThemeFam let author = theme_family_content.author.clone(); let mut theme_family = ThemeFamily { - id: id.clone(), - name: name.clone().into(), - author: author.clone().into(), + id, + name: name.into(), + author: author.into(), themes: vec![], scales: default_color_scales(), }; diff --git a/crates/theme_importer/src/vscode/converter.rs b/crates/theme_importer/src/vscode/converter.rs index 0249bdc7c9..b3b846d91d 100644 --- a/crates/theme_importer/src/vscode/converter.rs +++ b/crates/theme_importer/src/vscode/converter.rs @@ -158,7 +158,7 @@ impl VsCodeThemeConverter { .tab .active_background .clone() - .or(vscode_tab_inactive_background.clone()), + .or(vscode_tab_inactive_background), search_match_background: vscode_colors.editor.find_match_background.clone(), panel_background: vscode_colors.panel.background.clone(), pane_group_border: vscode_colors.editor_group.border.clone(), @@ -171,22 +171,20 @@ impl VsCodeThemeConverter { .scrollbar_slider .active_background .clone(), - scrollbar_thumb_border: vscode_scrollbar_slider_background.clone(), + scrollbar_thumb_border: vscode_scrollbar_slider_background, scrollbar_track_background: vscode_editor_background.clone(), scrollbar_track_border: vscode_colors.editor_overview_ruler.border.clone(), minimap_thumb_background: vscode_colors.minimap_slider.background.clone(), minimap_thumb_hover_background: vscode_colors.minimap_slider.hover_background.clone(), minimap_thumb_active_background: vscode_colors.minimap_slider.active_background.clone(), - editor_foreground: vscode_editor_foreground - .clone() - .or(vscode_token_colors_foreground.clone()), + editor_foreground: vscode_editor_foreground.or(vscode_token_colors_foreground), editor_background: vscode_editor_background.clone(), - editor_gutter_background: vscode_editor_background.clone(), + editor_gutter_background: vscode_editor_background, editor_active_line_background: vscode_colors.editor.line_highlight_background.clone(), editor_line_number: vscode_colors.editor_line_number.foreground.clone(), editor_active_line_number: vscode_colors.editor.foreground.clone(), editor_wrap_guide: vscode_panel_border.clone(), - editor_active_wrap_guide: vscode_panel_border.clone(), + editor_active_wrap_guide: vscode_panel_border, editor_document_highlight_bracket_background: vscode_colors .editor_bracket_match .background diff --git a/crates/title_bar/src/application_menu.rs b/crates/title_bar/src/application_menu.rs index d8b0b8dc6b..4a8cac2435 100644 --- a/crates/title_bar/src/application_menu.rs +++ b/crates/title_bar/src/application_menu.rs @@ -186,7 +186,7 @@ impl ApplicationMenu { .trigger( Button::new( SharedString::from(format!("{}-menu-trigger", menu_name)), - menu_name.clone(), + menu_name, ) .style(ButtonStyle::Subtle) .label_size(LabelSize::Small), diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index 5be68afeb4..c667edb509 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -155,7 +155,7 @@ impl TitleBar { .gap_1() .overflow_x_scroll() .when_some( - current_user.clone().zip(client.peer_id()).zip(room.clone()), + current_user.zip(client.peer_id()).zip(room), |this, ((current_user, peer_id), room)| { let player_colors = cx.theme().players(); let room = room.read(cx); diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 5bd6a17e4b..35b33f39be 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -305,7 +305,6 @@ impl TitleBar { let nickname = options .nickname - .clone() .map(|nick| nick.into()) .unwrap_or_else(|| host.clone()); @@ -351,11 +350,7 @@ impl TitleBar { .indicator_border_color(Some(cx.theme().colors().title_bar_background)) .into_any_element(), ) - .child( - Label::new(nickname.clone()) - .size(LabelSize::Small) - .truncate(), - ), + .child(Label::new(nickname).size(LabelSize::Small).truncate()), ) .tooltip(move |window, cx| { Tooltip::with_meta( diff --git a/crates/toolchain_selector/src/toolchain_selector.rs b/crates/toolchain_selector/src/toolchain_selector.rs index cdd3db99e0..feeca8cf52 100644 --- a/crates/toolchain_selector/src/toolchain_selector.rs +++ b/crates/toolchain_selector/src/toolchain_selector.rs @@ -167,7 +167,6 @@ impl ToolchainSelectorDelegate { cx: &mut Context<Picker<Self>>, ) -> Self { let _fetch_candidates_task = cx.spawn_in(window, { - let project = project.clone(); async move |this, cx| { let term = project .read_with(cx, |this, _| { diff --git a/crates/ui/src/components/dropdown_menu.rs b/crates/ui/src/components/dropdown_menu.rs index 7ad9400f0d..f276d483a6 100644 --- a/crates/ui/src/components/dropdown_menu.rs +++ b/crates/ui/src/components/dropdown_menu.rs @@ -96,7 +96,7 @@ impl RenderOnce for DropdownMenu { .style(self.style), ) .attach(Corner::BottomLeft) - .when_some(self.handle.clone(), |el, handle| el.with_handle(handle)) + .when_some(self.handle, |el, handle| el.with_handle(handle)) } } @@ -169,7 +169,7 @@ impl Component for DropdownMenu { "States", vec![single_example( "Disabled", - DropdownMenu::new("disabled", "Disabled Dropdown", menu.clone()) + DropdownMenu::new("disabled", "Disabled Dropdown", menu) .disabled(true) .into_any_element(), )], diff --git a/crates/ui/src/components/indent_guides.rs b/crates/ui/src/components/indent_guides.rs index 5e6f4ee8ba..60aa23b44c 100644 --- a/crates/ui/src/components/indent_guides.rs +++ b/crates/ui/src/components/indent_guides.rs @@ -195,7 +195,7 @@ mod uniform_list { impl UniformListDecoration for IndentGuides { fn compute( &self, - visible_range: Range<usize>, + mut visible_range: Range<usize>, bounds: Bounds<Pixels>, _scroll_offset: Point<Pixels>, item_height: Pixels, @@ -203,7 +203,6 @@ mod uniform_list { window: &mut Window, cx: &mut App, ) -> AnyElement { - let mut visible_range = visible_range.clone(); let includes_trailing_indent = visible_range.end < item_count; // Check if we have entries after the visible range, // if so extend the visible range so we can fetch a trailing indent, diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index bbce6101f4..1e7bb40c40 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -325,7 +325,7 @@ impl RenderOnce for Key { .text_size(size) .line_height(relative(1.)) .text_color(self.color.unwrap_or(Color::Muted).color(cx)) - .child(self.key.clone()) + .child(self.key) } } diff --git a/crates/ui/src/components/keybinding_hint.rs b/crates/ui/src/components/keybinding_hint.rs index a34ca40ed8..d7491b27b1 100644 --- a/crates/ui/src/components/keybinding_hint.rs +++ b/crates/ui/src/components/keybinding_hint.rs @@ -269,7 +269,7 @@ impl Component for KeybindingHint { ), single_example( "Large", - KeybindingHint::new(enter.clone(), bg_color) + KeybindingHint::new(enter, bg_color) .size(Pixels::from(20.0)) .prefix("Large:") .suffix("Size") diff --git a/crates/ui/src/components/notification/alert_modal.rs b/crates/ui/src/components/notification/alert_modal.rs index acba0c7e9a..9990dc1ce5 100644 --- a/crates/ui/src/components/notification/alert_modal.rs +++ b/crates/ui/src/components/notification/alert_modal.rs @@ -64,7 +64,7 @@ impl RenderOnce for AlertModal { ) .child(Button::new( self.primary_action.clone(), - self.primary_action.clone(), + self.primary_action, )), ), ) diff --git a/crates/ui/src/components/sticky_items.rs b/crates/ui/src/components/sticky_items.rs index c3e0886404..bf64622b29 100644 --- a/crates/ui/src/components/sticky_items.rs +++ b/crates/ui/src/components/sticky_items.rs @@ -28,7 +28,7 @@ where T: StickyCandidate + Clone + 'static, { let entity_compute = entity.clone(); - let entity_render = entity.clone(); + let entity_render = entity; let compute_fn = Rc::new( move |range: Range<usize>, window: &mut Window, cx: &mut App| -> SmallVec<[T; 8]> { diff --git a/crates/ui/src/utils/format_distance.rs b/crates/ui/src/utils/format_distance.rs index a8f27f01da..6ec497edee 100644 --- a/crates/ui/src/utils/format_distance.rs +++ b/crates/ui/src/utils/format_distance.rs @@ -159,7 +159,6 @@ fn distance_string( } else { format!("about {} hours", hours) } - .to_string() } else if distance < 172_800 { "1 day".to_string() } else if distance < 2_592_000 { @@ -206,21 +205,16 @@ fn distance_string( } else { format!("about {} years", years) } - .to_string() } else if remaining_months < 9 { if hide_prefix { format!("{} years", years) } else { format!("over {} years", years) } - .to_string() + } else if hide_prefix { + format!("{} years", years + 1) } else { - if hide_prefix { - format!("{} years", years + 1) - } else { - format!("almost {} years", years + 1) - } - .to_string() + format!("almost {} years", years + 1) } }; diff --git a/crates/ui_input/src/ui_input.rs b/crates/ui_input/src/ui_input.rs index 1a5bebaf1e..02f8ef89f3 100644 --- a/crates/ui_input/src/ui_input.rs +++ b/crates/ui_input/src/ui_input.rs @@ -202,11 +202,11 @@ impl Component for SingleLineInput { .children(vec![example_group(vec![ single_example( "Small Label (Default)", - div().child(input_small.clone()).into_any_element(), + div().child(input_small).into_any_element(), ), single_example( "Regular Label", - div().child(input_regular.clone()).into_any_element(), + div().child(input_regular).into_any_element(), ), ])]) .into_any_element(), diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 7269fc8bec..680c87f9e5 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1648,7 +1648,7 @@ impl OnMatchingLines { }); window.dispatch_action(action, cx); cx.defer_in(window, move |editor, window, cx| { - let newest = editor.selections.newest::<Point>(cx).clone(); + let newest = editor.selections.newest::<Point>(cx); editor.change_selections( SelectionEffects::no_scroll(), window, diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 714b74f239..da25919342 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -74,11 +74,7 @@ impl ModeIndicator { .map(|count| format!("{}", count)), ) .chain(vim.selected_register.map(|reg| format!("\"{reg}"))) - .chain( - vim.operator_stack - .iter() - .map(|item| item.status().to_string()), - ) + .chain(vim.operator_stack.iter().map(|item| item.status())) .chain( cx.global::<VimGlobals>() .post_count diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 350ffd666b..a2f165e9fe 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -719,21 +719,14 @@ impl Vim { target: Some(SurroundsType::Motion(motion)), }); } else { - self.normal_motion( - motion.clone(), - active_operator.clone(), - count, - forced_motion, - window, - cx, - ) + self.normal_motion(motion, active_operator, count, forced_motion, window, cx) } } Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { - self.visual_motion(motion.clone(), count, window, cx) + self.visual_motion(motion, count, window, cx) } - Mode::HelixNormal => self.helix_normal_motion(motion.clone(), count, window, cx), + Mode::HelixNormal => self.helix_normal_motion(motion, count, window, cx), } self.clear_operator(window, cx); if let Some(operator) = waiting_operator { @@ -1327,7 +1320,7 @@ impl Motion { pub fn range( &self, map: &DisplaySnapshot, - selection: Selection<DisplayPoint>, + mut selection: Selection<DisplayPoint>, times: Option<usize>, text_layout_details: &TextLayoutDetails, forced_motion: bool, @@ -1372,7 +1365,6 @@ impl Motion { (None, true) => Some((selection.head(), selection.goal)), }?; - let mut selection = selection.clone(); selection.set_head(new_head, goal); let mut kind = match (self.default_kind(), forced_motion) { @@ -2401,9 +2393,7 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint let line_range = map.prev_line_boundary(point).0..line_end; let visible_line_range = line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1)); - let ranges = map - .buffer_snapshot - .bracket_ranges(visible_line_range.clone()); + let ranges = map.buffer_snapshot.bracket_ranges(visible_line_range); if let Some(ranges) = ranges { let line_range = line_range.start.to_offset(&map.buffer_snapshot) ..line_range.end.to_offset(&map.buffer_snapshot); diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 0fd17f310e..933b119d37 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -474,8 +474,7 @@ mod test { Mode::Normal, ); assert_eq!( - cx.read_from_clipboard() - .map(|item| item.text().unwrap().to_string()), + cx.read_from_clipboard().map(|item| item.text().unwrap()), Some("jumps".into()) ); cx.simulate_keystrokes("d d p"); @@ -487,8 +486,7 @@ mod test { Mode::Normal, ); assert_eq!( - cx.read_from_clipboard() - .map(|item| item.text().unwrap().to_string()), + cx.read_from_clipboard().map(|item| item.text().unwrap()), Some("jumps".into()) ); cx.write_to_clipboard(ClipboardItem::new_string("test-copy".to_string())); diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index c65da4f90b..693de9f697 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -187,9 +187,7 @@ fn find_mini_delimiters( }; // Try to find delimiters in visible range first - let ranges = map - .buffer_snapshot - .bracket_ranges(visible_line_range.clone()); + let ranges = map.buffer_snapshot.bracket_ranges(visible_line_range); if let Some(candidate) = cover_or_next(ranges, display_point, map, Some(&bracket_filter)) { return Some( DelimiterRange { diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 23efd39139..c0176cb12c 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -400,7 +400,7 @@ impl MarksState { } else { HashMap::default() }; - let old_points = self.serialized_marks.get(&path.clone()); + let old_points = self.serialized_marks.get(&path); if old_points == Some(&new_points) { return; } @@ -543,7 +543,7 @@ impl MarksState { .insert(name.clone(), anchors); if self.is_global_mark(&name) { self.global_marks - .insert(name.clone(), MarkLocation::Buffer(multibuffer.entity_id())); + .insert(name, MarkLocation::Buffer(multibuffer.entity_id())); } if let Some(buffer) = buffer { let buffer_id = buffer.read(cx).remote_id(); @@ -559,7 +559,7 @@ impl MarksState { let buffer_id = buffer.read(cx).remote_id(); self.buffer_marks.entry(buffer_id).or_default().insert( - name.clone(), + name, anchors .into_iter() .map(|anchor| anchor.text_anchor) @@ -654,9 +654,9 @@ impl MarksState { return; } }; - self.global_marks.remove(&mark_name.clone()); + self.global_marks.remove(&mark_name); self.serialized_marks - .get_mut(&path.clone()) + .get_mut(&path) .map(|m| m.remove(&mark_name.clone())); if let Some(workspace_id) = self.workspace_id(cx) { cx.background_spawn(async move { DB.delete_mark(workspace_id, path, mark_name).await }) @@ -1282,7 +1282,7 @@ impl RegistersView { if let Some(register) = register { matches.push(RegisterMatch { name: '%', - contents: register.text.clone(), + contents: register.text, }) } } @@ -1374,7 +1374,7 @@ impl PickerDelegate for MarksViewDelegate { _: &mut Window, cx: &mut Context<Picker<Self>>, ) -> gpui::Task<()> { - let Some(workspace) = self.workspace.upgrade().clone() else { + let Some(workspace) = self.workspace.upgrade() else { return Task::ready(()); }; cx.spawn(async move |picker, cx| { diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 505cdaa910..6c9df164e0 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -292,12 +292,7 @@ impl NeovimBackedTestContext { register: '"', state: self.shared_state().await, neovim: self.neovim.read_register('"').await, - editor: self - .read_from_clipboard() - .unwrap() - .text() - .unwrap() - .to_owned(), + editor: self.read_from_clipboard().unwrap().text().unwrap(), } } diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index f87ccc283f..13b3e8b58d 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -453,7 +453,7 @@ impl NeovimConnection { }; if self.data.back() != Some(&state) { - self.data.push_back(state.clone()); + self.data.push_back(state); } (mode, ranges) diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 5b6cb55e8c..e7ac692df1 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -225,7 +225,7 @@ impl VimTestContext { VimClipboard { editor: self .read_from_clipboard() - .map(|item| item.text().unwrap().to_string()) + .map(|item| item.text().unwrap()) .unwrap_or_default(), } } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 11d6d89bac..9da01e6f44 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1693,7 +1693,7 @@ impl Vim { }) { editor.do_paste( ®ister.text.to_string(), - register.clipboard_selections.clone(), + register.clipboard_selections, false, window, cx, diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index ffbae3ff76..fcce00f0c0 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -1203,7 +1203,7 @@ mod test { the lazy dog"}); assert_eq!( cx.read_from_clipboard() - .map(|item| item.text().unwrap().to_string()) + .map(|item| item.text().unwrap()) .unwrap(), "The q" ); diff --git a/crates/watch/src/watch.rs b/crates/watch/src/watch.rs index c0741e4a20..f0ed5b4a18 100644 --- a/crates/watch/src/watch.rs +++ b/crates/watch/src/watch.rs @@ -218,7 +218,7 @@ mod tests { let mut tasks = Vec::new(); tasks.push(cx.background_spawn({ - let executor = cx.executor().clone(); + let executor = cx.executor(); let next_id = next_id.clone(); let closed = closed.clone(); async move { diff --git a/crates/web_search/src/web_search.rs b/crates/web_search/src/web_search.rs index 8578cfe4aa..c381b91f39 100644 --- a/crates/web_search/src/web_search.rs +++ b/crates/web_search/src/web_search.rs @@ -57,7 +57,7 @@ impl WebSearchRegistry { ) { let id = provider.id(); let provider = Arc::new(provider); - self.providers.insert(id.clone(), provider.clone()); + self.providers.insert(id, provider.clone()); if self.active_provider.is_none() { self.active_provider = Some(provider); } diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 1d9170684e..7a8de6e910 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -171,7 +171,7 @@ where } fn panel_focus_handle(&self, cx: &App) -> FocusHandle { - self.read(cx).focus_handle(cx).clone() + self.read(cx).focus_handle(cx) } fn activation_priority(&self, cx: &App) -> u32 { @@ -340,7 +340,7 @@ impl Dock { pub fn panel<T: Panel>(&self) -> Option<Entity<T>> { self.panel_entries .iter() - .find_map(|entry| entry.panel.to_any().clone().downcast().ok()) + .find_map(|entry| entry.panel.to_any().downcast().ok()) } pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> { diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 8af39be3e7..039aec5199 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -1012,7 +1012,6 @@ where let message: SharedString = format!("Error: {err}").into(); log::error!("Showing error notification in app: {message}"); show_app_notification(workspace_error_notification_id(), cx, { - let message = message.clone(); move |cx| { cx.new({ let message = message.clone(); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index d42b59f08e..e49eb0a345 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -480,7 +480,7 @@ impl Pane { forward_stack: Default::default(), closed_stack: Default::default(), paths_by_item: Default::default(), - pane: handle.clone(), + pane: handle, next_timestamp, }))), toolbar: cx.new(|_| Toolbar::new()), @@ -2516,7 +2516,7 @@ impl Pane { this.handle_external_paths_drop(paths, window, cx) })) .when_some(item.tab_tooltip_content(cx), |tab, content| match content { - TabTooltipContent::Text(text) => tab.tooltip(Tooltip::text(text.clone())), + TabTooltipContent::Text(text) => tab.tooltip(Tooltip::text(text)), TabTooltipContent::Custom(element_fn) => { tab.tooltip(move |window, cx| element_fn(window, cx)) } diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index bd2aafb7f4..9c2d09fd26 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -1175,7 +1175,7 @@ mod element { bounding_boxes.clear(); let mut layout = PaneAxisLayout { - dragged_handle: dragged_handle.clone(), + dragged_handle, children: Vec::new(), }; for (ix, mut child) in mem::take(&mut self.children).into_iter().enumerate() { diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 4a6b9ccdf4..da8a3070fc 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -620,7 +620,7 @@ mod tests { ]); let order = vec![2, 0, 1]; let serialized = - SerializedWorkspaceLocation::Local(LocalPaths(paths.clone()), LocalPathsOrder(order)); + SerializedWorkspaceLocation::Local(LocalPaths(paths), LocalPathsOrder(order)); assert_eq!( serialized.sorted_paths(), Arc::new(vec![ diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index e89e949f16..b21ba7a4b1 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -371,13 +371,13 @@ impl<T: SearchableItem> SearchableItemHandle for Entity<T> { impl From<Box<dyn SearchableItemHandle>> for AnyView { fn from(this: Box<dyn SearchableItemHandle>) -> Self { - this.to_any().clone() + this.to_any() } } impl From<&Box<dyn SearchableItemHandle>> for AnyView { fn from(this: &Box<dyn SearchableItemHandle>) -> Self { - this.to_any().clone() + this.to_any() } } diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index edeb382de7..187e720d9c 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -108,7 +108,7 @@ impl StatusBar { self.left_items .iter() .chain(self.right_items.iter()) - .find_map(|item| item.to_any().clone().downcast().log_err()) + .find_map(|item| item.to_any().downcast().log_err()) } pub fn position_of_item<T>(&self) -> Option<usize> @@ -217,6 +217,6 @@ impl<T: StatusItemView> StatusItemViewHandle for Entity<T> { impl From<&dyn StatusItemViewHandle> for AnyView { fn from(val: &dyn StatusItemViewHandle) -> Self { - val.to_any().clone() + val.to_any() } } diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs index 03164e0a64..09a5415ca0 100644 --- a/crates/workspace/src/theme_preview.rs +++ b/crates/workspace/src/theme_preview.rs @@ -303,7 +303,6 @@ impl ThemePreview { .gap_1() .children(all_colors.into_iter().map(|(color, name)| { let id = ElementId::Name(format!("{:?}-preview", color).into()); - let name = name.clone(); div().size_8().flex_none().child( ButtonLike::new(id) .child( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d64a4472a0..64cf77a4fd 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -903,7 +903,7 @@ impl AppState { let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); let clock = Arc::new(clock::FakeSystemClock::new()); let http_client = http_client::FakeHttpClient::with_404_response(); - let client = Client::new(clock, http_client.clone(), cx); + let client = Client::new(clock, http_client, cx); let session = cx.new(|cx| AppSession::new(Session::test(), cx)); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx)); @@ -1323,7 +1323,6 @@ impl Workspace { let mut active_call = None; if let Some(call) = ActiveCall::try_global(cx) { - let call = call.clone(); let subscriptions = vec![cx.subscribe_in(&call, window, Self::on_active_call_event)]; active_call = Some((call, subscriptions)); } @@ -4116,7 +4115,6 @@ impl Workspace { .unwrap_or_else(|| { self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx) }) - .clone() } pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<Entity<Pane>> { @@ -6713,7 +6711,7 @@ impl WorkspaceStore { .update(cx, |workspace, window, cx| { let handler_response = workspace.handle_follow(follower.project_id, window, cx); - if let Some(active_view) = handler_response.active_view.clone() + if let Some(active_view) = handler_response.active_view && workspace.project.read(cx).remote_id() == follower.project_id { response.active_view = Some(active_view) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index b12fd13767..cf61ee2669 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -1968,7 +1968,7 @@ impl LocalWorktree { cx: &Context<Worktree>, ) -> Option<Task<Result<()>>> { let path = self.entry_for_id(entry_id).unwrap().path.clone(); - let mut rx = self.add_path_prefix_to_scan(path.clone()); + let mut rx = self.add_path_prefix_to_scan(path); Some(cx.background_spawn(async move { rx.next().await; Ok(()) @@ -3952,7 +3952,7 @@ impl BackgroundScanner { .iter() .map(|path| { if path.file_name().is_some() { - root_canonical_path.as_path().join(path).to_path_buf() + root_canonical_path.as_path().join(path) } else { root_canonical_path.as_path().to_path_buf() } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index ca9debb647..c46e14f077 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -1254,7 +1254,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { let snapshot = Arc::new(Mutex::new(tree.snapshot())); tree.observe_updates(0, cx, { let snapshot = snapshot.clone(); - let settings = tree.settings().clone(); + let settings = tree.settings(); move |update| { snapshot .lock() diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 851c4e79f1..45c67153eb 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -242,7 +242,7 @@ pub fn main() { if args.system_specs { let system_specs = feedback::system_specs::SystemSpecs::new_stateless( app_version, - app_commit_sha.clone(), + app_commit_sha, *release_channel::RELEASE_CHANNEL, ); println!("Zed System Specs (from CLI):\n{}", system_specs); @@ -367,7 +367,7 @@ pub fn main() { if let Some(app_state) = AppState::try_global(cx).and_then(|app_state| app_state.upgrade()) { cx.spawn({ - let app_state = app_state.clone(); + let app_state = app_state; async move |cx| { if let Err(e) = restore_or_create_workspace(app_state, cx).await { fail_to_open_window_async(e, cx) @@ -523,13 +523,13 @@ pub fn main() { let app_session = cx.new(|cx| AppSession::new(session, cx)); let app_state = Arc::new(AppState { - languages: languages.clone(), + languages, client: client.clone(), - user_store: user_store.clone(), + user_store, fs: fs.clone(), build_window_options, workspace_store, - node_runtime: node_runtime.clone(), + node_runtime, session: app_session, }); AppState::set_global(Arc::downgrade(&app_state), cx); @@ -751,7 +751,6 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut if let Some(kind) = request.kind { match kind { OpenRequestKind::CliConnection(connection) => { - let app_state = app_state.clone(); cx.spawn(async move |cx| handle_cli_connection(connection, app_state, cx).await) .detach(); } @@ -1313,7 +1312,6 @@ fn eager_load_active_theme_and_icon_theme(fs: Arc<dyn Fs>, cx: &App) { .path_to_extension_icon_theme(icon_theme_name) { cx.spawn({ - let theme_registry = theme_registry.clone(); let fs = fs.clone(); async move |cx| { theme_registry @@ -1335,9 +1333,7 @@ fn load_user_themes_in_background(fs: Arc<dyn fs::Fs>, cx: &mut App) { cx.spawn({ let fs = fs.clone(); async move |cx| { - if let Some(theme_registry) = - cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err() - { + if let Some(theme_registry) = cx.update(|cx| ThemeRegistry::global(cx)).log_err() { let themes_dir = paths::themes_dir().as_ref(); match fs .metadata(themes_dir) @@ -1376,7 +1372,7 @@ fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut App) { for event in paths { if fs.metadata(&event.path).await.ok().flatten().is_some() && let Some(theme_registry) = - cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err() + cx.update(|cx| ThemeRegistry::global(cx)).log_err() && let Some(()) = theme_registry .load_user_theme(&event.path, fs.clone()) .await diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 232dfc42a3..0972973b89 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -526,8 +526,6 @@ fn initialize_panels( window: &mut Window, cx: &mut Context<Workspace>, ) { - let prompt_builder = prompt_builder.clone(); - cx.spawn_in(window, async move |workspace_handle, cx| { let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone()); @@ -1394,7 +1392,7 @@ fn show_keymap_file_load_error( cx: &mut App, ) { show_markdown_app_notification( - notification_id.clone(), + notification_id, error_message, "Open Keymap File".into(), |window, cx| { @@ -4786,7 +4784,7 @@ mod tests { cx.background_executor.run_until_parked(); // 5. Critical: Verify .zed is actually excluded from worktree - let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().clone()); + let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap()); let has_zed_entry = cx.update(|cx| worktree.read(cx).entry_for_path(".zed").is_some()); @@ -4822,7 +4820,7 @@ mod tests { .await .unwrap(); - let new_content_str = new_content.clone(); + let new_content_str = new_content; eprintln!("New settings content: {}", new_content_str); // The bug causes the settings to be overwritten with empty settings diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index d855fc3af7..5b3a951d43 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/crates/zed/src/zed/component_preview.rs @@ -33,8 +33,6 @@ use workspace::{ pub fn init(app_state: Arc<AppState>, cx: &mut App) { workspace::register_serializable_item::<ComponentPreview>(cx); - let app_state = app_state.clone(); - cx.observe_new(move |workspace: &mut Workspace, _window, cx| { let app_state = app_state.clone(); let project = workspace.project().clone(); @@ -462,12 +460,12 @@ impl ComponentPreview { Vec::new() }; if valid_positions.is_empty() { - Label::new(name.clone()).into_any_element() + Label::new(name).into_any_element() } else { - HighlightedLabel::new(name.clone(), valid_positions).into_any_element() + HighlightedLabel::new(name, valid_positions).into_any_element() } } else { - Label::new(name.clone()).into_any_element() + Label::new(name).into_any_element() }) .selectable(true) .toggle_state(selected) @@ -685,7 +683,7 @@ impl ComponentPreview { .h_full() .py_8() .bg(cx.theme().colors().panel_background) - .children(self.active_thread.clone().map(|thread| thread.clone())) + .children(self.active_thread.clone()) .when_none(&self.active_thread.clone(), |this| { this.child("No active thread") }), @@ -716,7 +714,7 @@ impl Render for ComponentPreview { if input.is_empty(cx) { String::new() } else { - input.editor().read(cx).text(cx).to_string() + input.editor().read(cx).text(cx) } }); @@ -929,7 +927,7 @@ impl SerializableItem for ComponentPreview { Err(_) => ActivePageId::default(), }; - let user_store = project.read(cx).user_store().clone(); + let user_store = project.read(cx).user_store(); let language_registry = project.read(cx).languages().clone(); let preview_page = if deserialized_active_page.0 == ActivePageId::default().0 { Some(PreviewPage::default()) @@ -940,7 +938,7 @@ impl SerializableItem for ComponentPreview { let found_component = all_components.iter().find(|c| c.id().0 == component_str); if let Some(component) = found_component { - Some(PreviewPage::Component(component.id().clone())) + Some(PreviewPage::Component(component.id())) } else { Some(PreviewPage::default()) } @@ -1057,7 +1055,7 @@ impl ComponentPreviewPage { .rounded_sm() .bg(color.color(cx).alpha(0.12)) .child( - Label::new(status.clone().to_string()) + Label::new(status.to_string()) .size(LabelSize::Small) .color(color), ), diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index 1123e53ddd..a9abd9bc74 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -60,23 +60,16 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) { cx.subscribe(&user_store, { let editors = editors.clone(); let client = client.clone(); + move |user_store, event, cx| { if let client::user::Event::PrivateUserInfoUpdated = event { - assign_edit_prediction_providers( - &editors, - provider, - &client, - user_store.clone(), - cx, - ); + assign_edit_prediction_providers(&editors, provider, &client, user_store, cx); } } }) .detach(); cx.observe_global::<SettingsStore>({ - let editors = editors.clone(); - let client = client.clone(); let user_store = user_store.clone(); move |cx| { let new_provider = all_language_settings(None, cx).edit_predictions.provider; diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 5baf76b64c..827c7754fa 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -102,11 +102,8 @@ impl OpenRequest { self.open_paths.is_empty(), "cannot open both local and ssh paths" ); - let mut connection_options = SshSettings::get_global(cx).connection_options_for( - host.clone(), - port, - username.clone(), - ); + let mut connection_options = + SshSettings::get_global(cx).connection_options_for(host, port, username); if let Some(password) = url.password() { connection_options.password = Some(password.to_string()); } diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 10d60fcd9d..e57d5d3889 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -161,7 +161,7 @@ impl Render for QuickActionBar { IconName::ZedAssistant, false, Box::new(InlineAssist::default()), - focus_handle.clone(), + focus_handle, "Inline Assist", move |_, window, cx| { window.dispatch_action(Box::new(InlineAssist::default()), cx); @@ -215,7 +215,7 @@ impl Render for QuickActionBar { ) }) .on_click({ - let focus = focus.clone(); + let focus = focus; move |_, window, cx| { focus.dispatch_action( &ToggleCodeActions { diff --git a/crates/zed/src/zed/quick_action_bar/repl_menu.rs b/crates/zed/src/zed/quick_action_bar/repl_menu.rs index ca180dccdd..eaa989f88d 100644 --- a/crates/zed/src/zed/quick_action_bar/repl_menu.rs +++ b/crates/zed/src/zed/quick_action_bar/repl_menu.rs @@ -196,7 +196,6 @@ impl QuickActionBar { .into_any_element() }, { - let editor = editor.clone(); move |window, cx| { repl::restart(editor.clone(), window, cx); } @@ -346,7 +345,7 @@ impl QuickActionBar { ), Tooltip::text("Select Kernel"), ) - .with_handle(menu_handle.clone()) + .with_handle(menu_handle) .into_any_element() } @@ -362,7 +361,7 @@ impl QuickActionBar { .shape(ui::IconButtonShape::Square) .icon_size(ui::IconSize::Small) .icon_color(Color::Muted) - .tooltip(Tooltip::text(tooltip.clone())) + .tooltip(Tooltip::text(tooltip)) .on_click(|_, _window, cx| { cx.open_url(&format!("{}#installation", ZED_REPL_DOCUMENTATION)) }), diff --git a/crates/zeta/src/input_excerpt.rs b/crates/zeta/src/input_excerpt.rs index 8ca6d39407..f4add6593e 100644 --- a/crates/zeta/src/input_excerpt.rs +++ b/crates/zeta/src/input_excerpt.rs @@ -90,7 +90,7 @@ fn expand_range( range: Range<Point>, mut remaining_tokens: usize, ) -> Range<Point> { - let mut expanded_range = range.clone(); + let mut expanded_range = range; expanded_range.start.column = 0; expanded_range.end.column = snapshot.line_len(expanded_range.end.row); loop { diff --git a/crates/zeta_cli/src/headless.rs b/crates/zeta_cli/src/headless.rs index d6ee085d18..cfa7d606ba 100644 --- a/crates/zeta_cli/src/headless.rs +++ b/crates/zeta_cli/src/headless.rs @@ -107,11 +107,7 @@ pub fn init(cx: &mut App) -> ZetaCliAppState { language::init(cx); debug_adapter_extension::init(extension_host_proxy.clone(), cx); - language_extension::init( - LspAccess::Noop, - extension_host_proxy.clone(), - languages.clone(), - ); + language_extension::init(LspAccess::Noop, extension_host_proxy, languages.clone()); language_model::init(client.clone(), cx); language_models::init(user_store.clone(), client.clone(), cx); languages::init(languages.clone(), node_runtime.clone(), cx); diff --git a/crates/zlog/src/filter.rs b/crates/zlog/src/filter.rs index 36a77e37bd..ee3c241079 100644 --- a/crates/zlog/src/filter.rs +++ b/crates/zlog/src/filter.rs @@ -293,7 +293,7 @@ impl ScopeMap { sub_items_start + 1, sub_items_end, "Expected one item: got: {:?}", - &items[items_range.clone()] + &items[items_range] ); enabled = Some(items[sub_items_start].1); } else { diff --git a/extensions/glsl/src/glsl.rs b/extensions/glsl/src/glsl.rs index 695fd7a053..77865564cc 100644 --- a/extensions/glsl/src/glsl.rs +++ b/extensions/glsl/src/glsl.rs @@ -119,7 +119,7 @@ impl zed::Extension for GlslExtension { ) -> Result<Option<serde_json::Value>> { let settings = LspSettings::for_worktree("glsl_analyzer", worktree) .ok() - .and_then(|lsp_settings| lsp_settings.settings.clone()) + .and_then(|lsp_settings| lsp_settings.settings) .unwrap_or_default(); Ok(Some(serde_json::json!({ diff --git a/extensions/html/src/html.rs b/extensions/html/src/html.rs index 07d4642ff4..371824c830 100644 --- a/extensions/html/src/html.rs +++ b/extensions/html/src/html.rs @@ -94,7 +94,7 @@ impl zed::Extension for HtmlExtension { ) -> Result<Option<zed::serde_json::Value>> { let settings = LspSettings::for_worktree(server_id.as_ref(), worktree) .ok() - .and_then(|lsp_settings| lsp_settings.settings.clone()) + .and_then(|lsp_settings| lsp_settings.settings) .unwrap_or_default(); Ok(Some(settings)) } diff --git a/extensions/ruff/src/ruff.rs b/extensions/ruff/src/ruff.rs index b918c52686..cc3c3f6550 100644 --- a/extensions/ruff/src/ruff.rs +++ b/extensions/ruff/src/ruff.rs @@ -151,7 +151,7 @@ impl zed::Extension for RuffExtension { ) -> Result<Option<zed_extension_api::serde_json::Value>> { let settings = LspSettings::for_worktree(server_id.as_ref(), worktree) .ok() - .and_then(|lsp_settings| lsp_settings.initialization_options.clone()) + .and_then(|lsp_settings| lsp_settings.initialization_options) .unwrap_or_default(); Ok(Some(settings)) } @@ -163,7 +163,7 @@ impl zed::Extension for RuffExtension { ) -> Result<Option<zed_extension_api::serde_json::Value>> { let settings = LspSettings::for_worktree(server_id.as_ref(), worktree) .ok() - .and_then(|lsp_settings| lsp_settings.settings.clone()) + .and_then(|lsp_settings| lsp_settings.settings) .unwrap_or_default(); Ok(Some(settings)) } diff --git a/extensions/snippets/src/snippets.rs b/extensions/snippets/src/snippets.rs index b2d68b6e1a..05e1ebca38 100644 --- a/extensions/snippets/src/snippets.rs +++ b/extensions/snippets/src/snippets.rs @@ -113,7 +113,7 @@ impl zed::Extension for SnippetExtension { ) -> Result<Option<zed_extension_api::serde_json::Value>> { let settings = LspSettings::for_worktree(server_id.as_ref(), worktree) .ok() - .and_then(|lsp_settings| lsp_settings.settings.clone()) + .and_then(|lsp_settings| lsp_settings.settings) .unwrap_or_else(|| { json!({ "max_completion_items": 20, From f80a0ba056a0e674ef4f5ff1fe0be096bc9787b1 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:26:45 +0300 Subject: [PATCH 524/693] Move clippy lints which aren't apart of the style category (#36579) Move lints which aren't apart of the style category. Motivation: They might get accidentally get reverted when we turn the style category on again and remove the manual lint enforcements. Release Notes: - N/A --- Cargo.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c3c7091279..c259a96912 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -806,6 +806,9 @@ todo = "deny" # warning on this rule produces a lot of noise. single_range_in_vec_init = "allow" +redundant_clone = "warn" +declare_interior_mutable_const = "deny" + # These are all of the rules that currently have violations in the Zed # codebase. # @@ -840,12 +843,10 @@ match_like_matches_macro = "warn" module_inception = { level = "deny" } question_mark = { level = "deny" } single_match = "warn" -redundant_clone = "warn" redundant_closure = { level = "deny" } redundant_static_lifetimes = { level = "warn" } redundant_pattern_matching = "warn" redundant_field_names = "warn" -declare_interior_mutable_const = { level = "deny" } collapsible_if = { level = "warn"} collapsible_else_if = { level = "warn" } needless_borrow = { level = "warn"} From 4ee565cd392b0563206eb2d2e61be214fa57ba03 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra <me@as-cii.com> Date: Wed, 20 Aug 2025 14:03:20 +0200 Subject: [PATCH 525/693] Fix mentions roundtrip from/to database and other history bugs (#36575) Release Notes: - N/A --- crates/agent2/src/agent.rs | 170 +++++++++++++++++++++++++++++++++++- crates/agent2/src/thread.rs | 58 ++++++------ 2 files changed, 200 insertions(+), 28 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 5496ecea7b..1fa307511f 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -577,6 +577,10 @@ impl NativeAgent { } fn save_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) { + if thread.read(cx).is_empty() { + return; + } + let database_future = ThreadsDatabase::connect(cx); let (id, db_thread) = thread.update(cx, |thread, cx| (thread.id().clone(), thread.to_db(cx))); @@ -989,12 +993,19 @@ impl acp_thread::AgentSessionResume for NativeAgentSessionResume { #[cfg(test)] mod tests { + use crate::HistoryEntryId; + use super::*; - use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelId, AgentModelInfo}; + use acp_thread::{ + AgentConnection, AgentModelGroupName, AgentModelId, AgentModelInfo, MentionUri, + }; use fs::FakeFs; use gpui::TestAppContext; + use indoc::indoc; + use language_model::fake_provider::FakeLanguageModel; use serde_json::json; use settings::SettingsStore; + use util::path; #[gpui::test] async fn test_maintaining_project_context(cx: &mut TestAppContext) { @@ -1179,6 +1190,163 @@ mod tests { ); } + #[gpui::test] + #[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows + async fn test_save_load_thread(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/", + json!({ + "a": { + "b.md": "Lorem" + } + }), + ) + .await; + let project = Project::test(fs.clone(), [path!("/a").as_ref()], cx).await; + let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let agent = NativeAgent::new( + project.clone(), + history_store.clone(), + Templates::new(), + None, + fs.clone(), + &mut cx.to_async(), + ) + .await + .unwrap(); + let connection = Rc::new(NativeAgentConnection(agent.clone())); + + let acp_thread = cx + .update(|cx| { + connection + .clone() + .new_thread(project.clone(), Path::new(""), cx) + }) + .await + .unwrap(); + let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone()); + let thread = agent.read_with(cx, |agent, _| { + agent.sessions.get(&session_id).unwrap().thread.clone() + }); + + // Ensure empty threads are not saved, even if they get mutated. + let model = Arc::new(FakeLanguageModel::default()); + let summary_model = Arc::new(FakeLanguageModel::default()); + thread.update(cx, |thread, cx| { + thread.set_model(model, cx); + thread.set_summarization_model(Some(summary_model), cx); + }); + cx.run_until_parked(); + assert_eq!(history_entries(&history_store, cx), vec![]); + + let model = thread.read_with(cx, |thread, _| thread.model().unwrap().clone()); + let model = model.as_fake(); + let summary_model = thread.read_with(cx, |thread, _| { + thread.summarization_model().unwrap().clone() + }); + let summary_model = summary_model.as_fake(); + let send = acp_thread.update(cx, |thread, cx| { + thread.send( + vec![ + "What does ".into(), + acp::ContentBlock::ResourceLink(acp::ResourceLink { + name: "b.md".into(), + uri: MentionUri::File { + abs_path: path!("/a/b.md").into(), + } + .to_uri() + .to_string(), + annotations: None, + description: None, + mime_type: None, + size: None, + title: None, + }), + " mean?".into(), + ], + cx, + ) + }); + let send = cx.foreground_executor().spawn(send); + cx.run_until_parked(); + + model.send_last_completion_stream_text_chunk("Lorem."); + model.end_last_completion_stream(); + cx.run_until_parked(); + summary_model.send_last_completion_stream_text_chunk("Explaining /a/b.md"); + summary_model.end_last_completion_stream(); + + send.await.unwrap(); + acp_thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc! {" + ## User + + What does [@b.md](file:///a/b.md) mean? + + ## Assistant + + Lorem. + + "} + ) + }); + + // Drop the ACP thread, which should cause the session to be dropped as well. + cx.update(|_| { + drop(thread); + drop(acp_thread); + }); + agent.read_with(cx, |agent, _| { + assert_eq!(agent.sessions.keys().cloned().collect::<Vec<_>>(), []); + }); + + // Ensure the thread can be reloaded from disk. + assert_eq!( + history_entries(&history_store, cx), + vec![( + HistoryEntryId::AcpThread(session_id.clone()), + "Explaining /a/b.md".into() + )] + ); + let acp_thread = agent + .update(cx, |agent, cx| agent.open_thread(session_id.clone(), cx)) + .await + .unwrap(); + acp_thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc! {" + ## User + + What does [@b.md](file:///a/b.md) mean? + + ## Assistant + + Lorem. + + "} + ) + }); + } + + fn history_entries( + history: &Entity<HistoryStore>, + cx: &mut TestAppContext, + ) -> Vec<(HistoryEntryId, String)> { + history.read_with(cx, |history, cx| { + history + .entries(cx) + .iter() + .map(|e| (e.id(), e.title().to_string())) + .collect::<Vec<_>>() + }) + } + fn init_test(cx: &mut TestAppContext) { env_logger::try_init().ok(); cx.update(|cx| { diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index cd97fa2060..c7b1a08b92 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -720,7 +720,7 @@ impl Thread { pub fn to_db(&self, cx: &App) -> Task<DbThread> { let initial_project_snapshot = self.initial_project_snapshot.clone(); let mut thread = DbThread { - title: self.title.clone().unwrap_or_default(), + title: self.title(), messages: self.messages.clone(), updated_at: self.updated_at, detailed_summary: self.summary.clone(), @@ -870,6 +870,10 @@ impl Thread { &self.action_log } + pub fn is_empty(&self) -> bool { + self.messages.is_empty() && self.title.is_none() + } + pub fn model(&self) -> Option<&Arc<dyn LanguageModel>> { self.model.as_ref() } @@ -884,6 +888,10 @@ impl Thread { cx.notify() } + pub fn summarization_model(&self) -> Option<&Arc<dyn LanguageModel>> { + self.summarization_model.as_ref() + } + pub fn set_summarization_model( &mut self, model: Option<Arc<dyn LanguageModel>>, @@ -1068,6 +1076,7 @@ impl Thread { event_stream: event_stream.clone(), _task: cx.spawn(async move |this, cx| { log::info!("Starting agent turn execution"); + let mut update_title = None; let turn_result: Result<StopReason> = async { let mut completion_intent = CompletionIntent::UserPrompt; loop { @@ -1122,10 +1131,15 @@ impl Thread { this.pending_message() .tool_results .insert(tool_result.tool_use_id.clone(), tool_result); - }) - .ok(); + })?; } + this.update(cx, |this, cx| { + if this.title.is_none() && update_title.is_none() { + update_title = Some(this.update_title(&event_stream, cx)); + } + })?; + if tool_use_limit_reached { log::info!("Tool use limit reached, completing turn"); this.update(cx, |this, _cx| this.tool_use_limit_reached = true)?; @@ -1146,10 +1160,6 @@ impl Thread { Ok(reason) => { log::info!("Turn execution completed: {:?}", reason); - let update_title = this - .update(cx, |this, cx| this.update_title(&event_stream, cx)) - .ok() - .flatten(); if let Some(update_title) = update_title { update_title.await.context("update title failed").log_err(); } @@ -1593,17 +1603,14 @@ impl Thread { &mut self, event_stream: &ThreadEventStream, cx: &mut Context<Self>, - ) -> Option<Task<Result<()>>> { - if self.title.is_some() { - log::debug!("Skipping title generation because we already have one."); - return None; - } - + ) -> Task<Result<()>> { log::info!( "Generating title with model: {:?}", self.summarization_model.as_ref().map(|model| model.name()) ); - let model = self.summarization_model.clone()?; + let Some(model) = self.summarization_model.clone() else { + return Task::ready(Ok(())); + }; let event_stream = event_stream.clone(); let mut request = LanguageModelRequest { intent: Some(CompletionIntent::ThreadSummarization), @@ -1620,7 +1627,7 @@ impl Thread { content: vec![SUMMARIZE_THREAD_PROMPT.into()], cache: false, }); - Some(cx.spawn(async move |this, cx| { + cx.spawn(async move |this, cx| { let mut title = String::new(); let mut messages = model.stream_completion(request, cx).await?; while let Some(event) = messages.next().await { @@ -1655,7 +1662,7 @@ impl Thread { this.title = Some(title); cx.notify(); }) - })) + }) } fn last_user_message(&self) -> Option<&UserMessage> { @@ -2457,18 +2464,15 @@ impl From<UserMessageContent> for acp::ContentBlock { uri: None, }), UserMessageContent::Mention { uri, content } => { - acp::ContentBlock::ResourceLink(acp::ResourceLink { - uri: uri.to_uri().to_string(), - name: uri.name(), + acp::ContentBlock::Resource(acp::EmbeddedResource { + resource: acp::EmbeddedResourceResource::TextResourceContents( + acp::TextResourceContents { + mime_type: None, + text: content, + uri: uri.to_uri().to_string(), + }, + ), annotations: None, - description: if content.is_empty() { - None - } else { - Some(content) - }, - mime_type: None, - size: None, - title: None, }) } } From 6ed29fbc34b0ade21decea68c93ecd88420810a0 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Wed, 20 Aug 2025 15:07:37 +0300 Subject: [PATCH 526/693] Enforce style lints which do not have violations (#36580) Release Notes: - N/A --- Cargo.toml | 96 ++++++++++++++++--- crates/action_log/src/action_log.rs | 2 +- .../src/activity_indicator.rs | 2 +- crates/agent_ui/src/inline_prompt_editor.rs | 8 +- crates/client/src/client.rs | 16 +--- crates/copilot/src/copilot.rs | 12 +-- crates/edit_prediction/src/edit_prediction.rs | 2 +- crates/editor/src/editor.rs | 62 ++++++------ crates/editor/src/element.rs | 2 +- crates/editor/src/scroll.rs | 2 +- crates/editor/src/scroll/actions.rs | 2 +- crates/git_ui/src/git_panel.rs | 4 +- crates/go_to_line/src/cursor_position.rs | 2 +- crates/go_to_line/src/go_to_line.rs | 2 +- crates/onboarding/src/theme_preview.rs | 7 +- crates/project/src/lsp_store.rs | 4 +- crates/project/src/project.rs | 2 +- crates/remote/src/ssh_session.rs | 2 +- crates/title_bar/src/title_bar.rs | 4 +- crates/workspace/src/workspace.rs | 2 +- 20 files changed, 146 insertions(+), 89 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c259a96912..d69e87fd6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -825,36 +825,106 @@ declare_interior_mutable_const = "deny" style = { level = "allow", priority = -1 } # Temporary list of style lints that we've fixed so far. +# Progress is being tracked in #36577 +blocks_in_conditions = "warn" bool_assert_comparison = "warn" +borrow_interior_mutable_const = "warn" +box_default = "warn" +builtin_type_shadow = "warn" +bytes_nth = "warn" +chars_next_cmp = "warn" +cmp_null = "warn" +collapsible_else_if = "warn" +collapsible_if = "warn" comparison_to_empty = "warn" +default_instead_of_iter_empty = "warn" +disallowed_macros = "warn" +disallowed_methods = "warn" +disallowed_names = "warn" +disallowed_types = "warn" doc_lazy_continuation = "warn" doc_overindented_list_items = "warn" -inherent_to_string = "warn" +duplicate_underscore_argument = "warn" +err_expect = "warn" +fn_to_numeric_cast = "warn" +fn_to_numeric_cast_with_truncation = "warn" for_kv_map = "warn" +implicit_saturating_add = "warn" +implicit_saturating_sub = "warn" +inconsistent_digit_grouping = "warn" +infallible_destructuring_match = "warn" +inherent_to_string = "warn" +init_numbered_fields = "warn" into_iter_on_ref = "warn" io_other_error = "warn" +items_after_test_module = "warn" iter_cloned_collect = "warn" iter_next_slice = "warn" iter_nth = "warn" iter_nth_zero = "warn" iter_skip_next = "warn" +just_underscores_and_digits = "warn" let_and_return = "warn" +main_recursion = "warn" +manual_bits = "warn" +manual_dangling_ptr = "warn" +manual_is_ascii_check = "warn" +manual_is_finite = "warn" +manual_is_infinite = "warn" +manual_next_back = "warn" +manual_non_exhaustive = "warn" +manual_ok_or = "warn" +manual_pattern_char_comparison = "warn" +manual_rotate = "warn" +manual_slice_fill = "warn" +manual_while_let_some = "warn" +map_collect_result_unit = "warn" match_like_matches_macro = "warn" -module_inception = { level = "deny" } -question_mark = { level = "deny" } -single_match = "warn" -redundant_closure = { level = "deny" } -redundant_static_lifetimes = { level = "warn" } -redundant_pattern_matching = "warn" +match_overlapping_arm = "warn" +mem_replace_option_with_none = "warn" +mem_replace_option_with_some = "warn" +missing_enforced_import_renames = "warn" +missing_safety_doc = "warn" +mixed_attributes_style = "warn" +mixed_case_hex_literals = "warn" +module_inception = "warn" +must_use_unit = "warn" +mut_mutex_lock = "warn" +needless_borrow = "warn" +needless_doctest_main = "warn" +needless_else = "warn" +needless_parens_on_range_literals = "warn" +needless_pub_self = "warn" +needless_return = "warn" +needless_return_with_question_mark = "warn" +ok_expect = "warn" +owned_cow = "warn" +print_literal = "warn" +print_with_newline = "warn" +ptr_eq = "warn" +question_mark = "warn" +redundant_closure = "warn" redundant_field_names = "warn" -collapsible_if = { level = "warn"} -collapsible_else_if = { level = "warn" } -needless_borrow = { level = "warn"} -needless_return = { level = "warn" } -unnecessary_mut_passed = {level = "warn"} -unnecessary_map_or = { level = "warn" } +redundant_pattern_matching = "warn" +redundant_static_lifetimes = "warn" +result_map_or_into_option = "warn" +self_named_constructors = "warn" +single_match = "warn" +tabs_in_doc_comments = "warn" +to_digit_is_some = "warn" +toplevel_ref_arg = "warn" +unnecessary_fold = "warn" +unnecessary_map_or = "warn" +unnecessary_mut_passed = "warn" +unnecessary_owned_empty_strings = "warn" +unneeded_struct_pattern = "warn" +unsafe_removed_from_name = "warn" unused_unit = "warn" +unusual_byte_groupings = "warn" +write_literal = "warn" +writeln_empty_string = "warn" wrong_self_convention = "warn" +zero_ptr = "warn" # Individual rules that have violations in the codebase: type_complexity = "allow" diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs index a1f332fc7c..9ec10f4dbb 100644 --- a/crates/action_log/src/action_log.rs +++ b/crates/action_log/src/action_log.rs @@ -190,7 +190,7 @@ impl ActionLog { cx: &mut Context<Self>, ) { match event { - BufferEvent::Edited { .. } => self.handle_buffer_edited(buffer, cx), + BufferEvent::Edited => self.handle_buffer_edited(buffer, cx), BufferEvent::FileHandleChanged => { self.handle_buffer_file_changed(buffer, cx); } diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 324480f5b4..6641db0805 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -104,7 +104,7 @@ impl ActivityIndicator { &workspace_handle, window, |activity_indicator, _, event, window, cx| { - if let workspace::Event::ClearActivityIndicator { .. } = event + if let workspace::Event::ClearActivityIndicator = event && activity_indicator.statuses.pop().is_some() { activity_indicator.dismiss_error_message(&DismissErrorMessage, window, cx); diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index 5608143464..a626122769 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -1229,27 +1229,27 @@ pub enum GenerationMode { impl GenerationMode { fn start_label(self) -> &'static str { match self { - GenerationMode::Generate { .. } => "Generate", + GenerationMode::Generate => "Generate", GenerationMode::Transform => "Transform", } } fn tooltip_interrupt(self) -> &'static str { match self { - GenerationMode::Generate { .. } => "Interrupt Generation", + GenerationMode::Generate => "Interrupt Generation", GenerationMode::Transform => "Interrupt Transform", } } fn tooltip_restart(self) -> &'static str { match self { - GenerationMode::Generate { .. } => "Restart Generation", + GenerationMode::Generate => "Restart Generation", GenerationMode::Transform => "Restart Transform", } } fn tooltip_accept(self) -> &'static str { match self { - GenerationMode::Generate { .. } => "Accept Generation", + GenerationMode::Generate => "Accept Generation", GenerationMode::Transform => "Accept Transform", } } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index b6ce9d24e9..ed3f114943 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1029,11 +1029,11 @@ impl Client { Status::SignedOut | Status::Authenticated => true, Status::ConnectionError | Status::ConnectionLost - | Status::Authenticating { .. } + | Status::Authenticating | Status::AuthenticationError - | Status::Reauthenticating { .. } + | Status::Reauthenticating | Status::ReconnectionError { .. } => false, - Status::Connected { .. } | Status::Connecting { .. } | Status::Reconnecting { .. } => { + Status::Connected { .. } | Status::Connecting | Status::Reconnecting => { return ConnectionResult::Result(Ok(())); } Status::UpgradeRequired => { @@ -1902,10 +1902,7 @@ mod tests { assert!(matches!(status.next().await, Some(Status::Connecting))); executor.advance_clock(CONNECTION_TIMEOUT); - assert!(matches!( - status.next().await, - Some(Status::ConnectionError { .. }) - )); + assert!(matches!(status.next().await, Some(Status::ConnectionError))); auth_and_connect.await.into_response().unwrap_err(); // Allow the connection to be established. @@ -1929,10 +1926,7 @@ mod tests { }) }); executor.advance_clock(2 * INITIAL_RECONNECTION_DELAY); - assert!(matches!( - status.next().await, - Some(Status::Reconnecting { .. }) - )); + assert!(matches!(status.next().await, Some(Status::Reconnecting))); executor.advance_clock(CONNECTION_TIMEOUT); assert!(matches!( diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 33455f5e52..b7d8423fd7 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -126,7 +126,7 @@ impl CopilotServer { fn as_authenticated(&mut self) -> Result<&mut RunningCopilotServer> { let server = self.as_running()?; anyhow::ensure!( - matches!(server.sign_in_status, SignInStatus::Authorized { .. }), + matches!(server.sign_in_status, SignInStatus::Authorized), "must sign in before using copilot" ); Ok(server) @@ -578,12 +578,12 @@ impl Copilot { pub(crate) fn sign_in(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> { if let CopilotServer::Running(server) = &mut self.server { let task = match &server.sign_in_status { - SignInStatus::Authorized { .. } => Task::ready(Ok(())).shared(), + SignInStatus::Authorized => Task::ready(Ok(())).shared(), SignInStatus::SigningIn { task, .. } => { cx.notify(); task.clone() } - SignInStatus::SignedOut { .. } | SignInStatus::Unauthorized { .. } => { + SignInStatus::SignedOut { .. } | SignInStatus::Unauthorized => { let lsp = server.lsp.clone(); let task = cx .spawn(async move |this, cx| { @@ -727,7 +727,7 @@ impl Copilot { .. }) = &mut self.server { - if !matches!(status, SignInStatus::Authorized { .. }) { + if !matches!(status, SignInStatus::Authorized) { return; } @@ -1009,8 +1009,8 @@ impl Copilot { CopilotServer::Error(error) => Status::Error(error.clone()), CopilotServer::Running(RunningCopilotServer { sign_in_status, .. }) => { match sign_in_status { - SignInStatus::Authorized { .. } => Status::Authorized, - SignInStatus::Unauthorized { .. } => Status::Unauthorized, + SignInStatus::Authorized => Status::Authorized, + SignInStatus::Unauthorized => Status::Unauthorized, SignInStatus::SigningIn { prompt, .. } => Status::SigningIn { prompt: prompt.clone(), }, diff --git a/crates/edit_prediction/src/edit_prediction.rs b/crates/edit_prediction/src/edit_prediction.rs index c8502f75de..964f202934 100644 --- a/crates/edit_prediction/src/edit_prediction.rs +++ b/crates/edit_prediction/src/edit_prediction.rs @@ -34,7 +34,7 @@ pub enum DataCollectionState { impl DataCollectionState { pub fn is_supported(&self) -> bool { - !matches!(self, DataCollectionState::Unsupported { .. }) + !matches!(self, DataCollectionState::Unsupported) } pub fn is_enabled(&self) -> bool { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5fc017dcfc..2136d5f4b3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1854,8 +1854,8 @@ impl Editor { blink_manager }); - let soft_wrap_mode_override = matches!(mode, EditorMode::SingleLine { .. }) - .then(|| language_settings::SoftWrap::None); + let soft_wrap_mode_override = + matches!(mode, EditorMode::SingleLine).then(|| language_settings::SoftWrap::None); let mut project_subscriptions = Vec::new(); if full_mode && let Some(project) = project.as_ref() { @@ -1980,14 +1980,12 @@ impl Editor { .detach(); } - let show_indent_guides = if matches!( - mode, - EditorMode::SingleLine { .. } | EditorMode::Minimap { .. } - ) { - Some(false) - } else { - None - }; + let show_indent_guides = + if matches!(mode, EditorMode::SingleLine | EditorMode::Minimap { .. }) { + Some(false) + } else { + None + }; let breakpoint_store = match (&mode, project.as_ref()) { (EditorMode::Full { .. }, Some(project)) => Some(project.read(cx).breakpoint_store()), @@ -2047,7 +2045,7 @@ impl Editor { vertical: full_mode, }, minimap_visibility: MinimapVisibility::for_mode(&mode, cx), - offset_content: !matches!(mode, EditorMode::SingleLine { .. }), + offset_content: !matches!(mode, EditorMode::SingleLine), show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs, show_gutter: full_mode, show_line_numbers: (!full_mode).then_some(false), @@ -2401,7 +2399,7 @@ impl Editor { let mut key_context = KeyContext::new_with_defaults(); key_context.add("Editor"); let mode = match self.mode { - EditorMode::SingleLine { .. } => "single_line", + EditorMode::SingleLine => "single_line", EditorMode::AutoHeight { .. } => "auto_height", EditorMode::Minimap { .. } => "minimap", EditorMode::Full { .. } => "full", @@ -6772,7 +6770,7 @@ impl Editor { &mut self, cx: &mut Context<Editor>, ) -> Option<(String, Range<Anchor>)> { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { return None; } if !EditorSettings::get_global(cx).selection_highlight { @@ -12601,7 +12599,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -12725,7 +12723,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13209,7 +13207,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13230,7 +13228,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13251,7 +13249,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13272,7 +13270,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13293,7 +13291,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13318,7 +13316,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13343,7 +13341,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13368,7 +13366,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13393,7 +13391,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13414,7 +13412,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13435,7 +13433,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13456,7 +13454,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13477,7 +13475,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -13502,7 +13500,7 @@ impl Editor { } pub fn move_to_end(&mut self, _: &MoveToEnd, window: &mut Window, cx: &mut Context<Self>) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -14551,7 +14549,7 @@ impl Editor { let advance_downwards = action.advance_downwards && selections_on_single_row && !selections_selecting - && !matches!(this.mode, EditorMode::SingleLine { .. }); + && !matches!(this.mode, EditorMode::SingleLine); if advance_downwards { let snapshot = this.buffer.read(cx).snapshot(cx); @@ -22867,7 +22865,7 @@ impl Render for Editor { let settings = ThemeSettings::get_global(cx); let mut text_style = match self.mode { - EditorMode::SingleLine { .. } | EditorMode::AutoHeight { .. } => TextStyle { + EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle { color: cx.theme().colors().editor_foreground, font_family: settings.ui_font.family.clone(), font_features: settings.ui_font.features.clone(), @@ -22893,7 +22891,7 @@ impl Render for Editor { } let background = match self.mode { - EditorMode::SingleLine { .. } => cx.theme().system().transparent, + EditorMode::SingleLine => cx.theme().system().transparent, EditorMode::AutoHeight { .. } => cx.theme().system().transparent, EditorMode::Full { .. } => cx.theme().colors().editor_background, EditorMode::Minimap { .. } => cx.theme().colors().editor_background.opacity(0.7), diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b18d1ceae1..416f35d7a7 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -8105,7 +8105,7 @@ impl Element for EditorElement { // The max scroll position for the top of the window let max_scroll_top = if matches!( snapshot.mode, - EditorMode::SingleLine { .. } + EditorMode::SingleLine | EditorMode::AutoHeight { .. } | EditorMode::Full { sized_by_content: true, diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index b47f1cd711..8231448618 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -675,7 +675,7 @@ impl Editor { window: &mut Window, cx: &mut Context<Self>, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } diff --git a/crates/editor/src/scroll/actions.rs b/crates/editor/src/scroll/actions.rs index 72827b2fee..f8104665f9 100644 --- a/crates/editor/src/scroll/actions.rs +++ b/crates/editor/src/scroll/actions.rs @@ -16,7 +16,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 5a01514185..79d182eb22 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -2983,9 +2983,7 @@ impl GitPanel { let status_toast = StatusToast::new(message, cx, move |this, _cx| { use remote_output::SuccessStyle::*; match style { - Toast { .. } => { - this.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)) - } + Toast => this.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)), ToastWithLog { output } => this .icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)) .action("View Log", move |window, cx| { diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index 23729be062..e60a3651aa 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -106,7 +106,7 @@ impl CursorPosition { cursor_position.selected_count.selections = editor.selections.count(); match editor.mode() { editor::EditorMode::AutoHeight { .. } - | editor::EditorMode::SingleLine { .. } + | editor::EditorMode::SingleLine | editor::EditorMode::Minimap { .. } => { cursor_position.position = None; cursor_position.context = None; diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 1913646aa1..2afc72e989 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -157,7 +157,7 @@ impl GoToLine { self.prev_scroll_position.take(); cx.emit(DismissEvent) } - editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx), + editor::EditorEvent::BufferEdited => self.highlight_current_line(cx), _ => {} } } diff --git a/crates/onboarding/src/theme_preview.rs b/crates/onboarding/src/theme_preview.rs index d84bc9b0e5..8bd65d8a27 100644 --- a/crates/onboarding/src/theme_preview.rs +++ b/crates/onboarding/src/theme_preview.rs @@ -362,13 +362,12 @@ impl Component for ThemePreviewTile { .gap_4() .children( themes_to_preview - .iter() - .enumerate() - .map(|(_, theme)| { + .into_iter() + .map(|theme| { div() .w(px(200.)) .h(px(140.)) - .child(ThemePreviewTile::new(theme.clone(), 0.42)) + .child(ThemePreviewTile::new(theme, 0.42)) }) .collect::<Vec<_>>(), ) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 7a44ad3f87..e989b974e1 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3924,9 +3924,7 @@ impl LspStore { _: &mut Context<Self>, ) { match event { - ToolchainStoreEvent::ToolchainActivated { .. } => { - self.request_workspace_config_refresh() - } + ToolchainStoreEvent::ToolchainActivated => self.request_workspace_config_refresh(), } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index af5fd0d675..e47c020a42 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3119,7 +3119,7 @@ impl Project { event: &BufferEvent, cx: &mut Context<Self>, ) -> Option<()> { - if matches!(event, BufferEvent::Edited { .. } | BufferEvent::Reloaded) { + if matches!(event, BufferEvent::Edited | BufferEvent::Reloaded) { self.request_buffer_diff_recalculation(&buffer, cx); } diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 5fa3a5f715..1c4409aec3 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -948,7 +948,7 @@ impl SshRemoteClient { if old_state.is_reconnecting() { match &new_state { State::Connecting - | State::Reconnecting { .. } + | State::Reconnecting | State::HeartbeatMissed { .. } | State::ServerNotRunning => {} State::Connected { .. } => { diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 35b33f39be..b84a2800b6 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -563,8 +563,8 @@ impl TitleBar { match status { client::Status::ConnectionError | client::Status::ConnectionLost - | client::Status::Reauthenticating { .. } - | client::Status::Reconnecting { .. } + | client::Status::Reauthenticating + | client::Status::Reconnecting | client::Status::ReconnectionError { .. } => Some( div() .id("disconnected") diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 64cf77a4fd..b52687f335 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -7670,7 +7670,7 @@ pub fn client_side_decorations( match decorations { Decorations::Client { .. } => window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW), - Decorations::Server { .. } => window.set_client_inset(px(0.0)), + Decorations::Server => window.set_client_inset(px(0.0)), } struct GlobalResizeEdge(ResizeEdge); From de12633591a79af3bac5fc030ee3d54bb4270920 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra <me@as-cii.com> Date: Wed, 20 Aug 2025 15:02:40 +0200 Subject: [PATCH 527/693] Wait for agent2 feature flag before loading panel (#36583) Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 3c4c403a77..286d3b1c26 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -523,6 +523,7 @@ impl AgentPanel { anyhow::Ok(()) })); } + pub fn load( workspace: WeakEntity<Workspace>, prompt_builder: Arc<PromptBuilder>, @@ -572,6 +573,17 @@ impl AgentPanel { None }; + // Wait for the Gemini/Native feature flag to be available. + let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?; + if !client.status().borrow().is_signed_out() { + cx.update(|_, cx| { + cx.wait_for_flag_or_timeout::<feature_flags::GeminiAndNativeFeatureFlag>( + Duration::from_secs(2), + ) + })? + .await; + } + let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = cx.new(|cx| { Self::new( From bc79076ad3767e004bc6c5ff7efa9673400329d2 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Wed, 20 Aug 2025 16:17:28 +0300 Subject: [PATCH 528/693] Fix `clippy::manual_map` lint violations (#36584) #36577 Release Notes: - N/A --- Cargo.toml | 1 + crates/acp_thread/src/acp_thread.rs | 8 +++---- crates/agent_ui/src/acp/thread_view.rs | 7 ++---- crates/agent_ui/src/active_thread.rs | 10 ++++---- crates/agent_ui/src/inline_assistant.rs | 10 ++++---- .../src/edit_agent/streaming_fuzzy_matcher.rs | 8 +++---- crates/editor/src/editor_tests.rs | 8 +------ crates/editor/src/hover_popover.rs | 24 +++++++------------ crates/file_finder/src/file_finder.rs | 7 +++--- crates/git_ui/src/commit_modal.rs | 12 +++------- crates/gpui/src/platform/windows/window.rs | 5 +--- crates/multi_buffer/src/multi_buffer.rs | 10 +++----- crates/project/src/lsp_command.rs | 15 +++++------- crates/project/src/lsp_store.rs | 15 ++++-------- crates/project_panel/src/project_panel.rs | 20 +++++++--------- crates/vim/src/command.rs | 6 +---- crates/workspace/src/pane.rs | 4 +--- crates/workspace/src/workspace.rs | 10 ++++---- 18 files changed, 62 insertions(+), 118 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d69e87fd6b..9cd206cebf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -871,6 +871,7 @@ manual_dangling_ptr = "warn" manual_is_ascii_check = "warn" manual_is_finite = "warn" manual_is_infinite = "warn" +manual_map = "warn" manual_next_back = "warn" manual_non_exhaustive = "warn" manual_ok_or = "warn" diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 4f20dbd587..b8908fa0da 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -301,11 +301,9 @@ impl ToolCall { ) -> Option<AgentLocation> { let buffer = project .update(cx, |project, cx| { - if let Some(path) = project.project_path_for_absolute_path(&location.path, cx) { - Some(project.open_buffer(path, cx)) - } else { - None - } + project + .project_path_for_absolute_path(&location.path, cx) + .map(|path| project.open_buffer(path, cx)) }) .ok()??; let buffer = buffer.await.log_err()?; diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index b527775850..f89198c84b 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -4012,12 +4012,9 @@ impl Render for AcpThreadView { .children( if let Some(usage_callout) = self.render_usage_callout(line_height, cx) { Some(usage_callout.into_any_element()) - } else if let Some(token_limit_callout) = - self.render_token_limit_callout(line_height, cx) - { - Some(token_limit_callout.into_any_element()) } else { - None + self.render_token_limit_callout(line_height, cx) + .map(|token_limit_callout| token_limit_callout.into_any_element()) }, ) .child(self.render_message_editor(window, cx)) diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index bb5b47f0d6..e214986b82 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -779,13 +779,11 @@ impl ActiveThread { let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.)); - let workspace_subscription = if let Some(workspace) = workspace.upgrade() { - Some(cx.observe_release(&workspace, |this, _, cx| { + let workspace_subscription = workspace.upgrade().map(|workspace| { + cx.observe_release(&workspace, |this, _, cx| { this.dismiss_notifications(cx); - })) - } else { - None - }; + }) + }); let mut this = Self { language_registry, diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 2111553340..13f1234b4d 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -1532,13 +1532,11 @@ impl InlineAssistant { .and_then(|item| item.act_as::<Editor>(cx)) { Some(InlineAssistTarget::Editor(workspace_editor)) - } else if let Some(terminal_view) = workspace - .active_item(cx) - .and_then(|item| item.act_as::<TerminalView>(cx)) - { - Some(InlineAssistTarget::Terminal(terminal_view)) } else { - None + workspace + .active_item(cx) + .and_then(|item| item.act_as::<TerminalView>(cx)) + .map(InlineAssistTarget::Terminal) } } } diff --git a/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs b/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs index 2dba8a2b6d..33b37679f0 100644 --- a/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs +++ b/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs @@ -794,10 +794,8 @@ mod tests { fn finish(mut finder: StreamingFuzzyMatcher) -> Option<String> { let snapshot = finder.snapshot.clone(); let matches = finder.finish(); - if let Some(range) = matches.first() { - Some(snapshot.text_for_range(range.clone()).collect::<String>()) - } else { - None - } + matches + .first() + .map(|range| snapshot.text_for_range(range.clone()).collect::<String>()) } } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 955ade04cd..44c05dbc14 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -21065,13 +21065,7 @@ fn add_log_breakpoint_at_cursor( let (anchor, bp) = editor .breakpoints_at_cursors(window, cx) .first() - .and_then(|(anchor, bp)| { - if let Some(bp) = bp { - Some((*anchor, bp.clone())) - } else { - None - } - }) + .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone()))) .unwrap_or_else(|| { let cursor_position: Point = editor.selections.newest(cx).head(); diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 497f193cb4..28a09e947f 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -174,11 +174,9 @@ pub fn hover_at_inlay( let subscription = this .update(cx, |_, cx| { - if let Some(parsed_content) = &parsed_content { - Some(cx.observe(parsed_content, |_, _, cx| cx.notify())) - } else { - None - } + parsed_content.as_ref().map(|parsed_content| { + cx.observe(parsed_content, |_, _, cx| cx.notify()) + }) }) .ok() .flatten(); @@ -450,11 +448,9 @@ fn show_hover( let scroll_handle = ScrollHandle::new(); let subscription = this .update(cx, |_, cx| { - if let Some(parsed_content) = &parsed_content { - Some(cx.observe(parsed_content, |_, _, cx| cx.notify())) - } else { - None - } + parsed_content.as_ref().map(|parsed_content| { + cx.observe(parsed_content, |_, _, cx| cx.notify()) + }) }) .ok() .flatten(); @@ -502,11 +498,9 @@ fn show_hover( hover_highlights.push(range.clone()); let subscription = this .update(cx, |_, cx| { - if let Some(parsed_content) = &parsed_content { - Some(cx.observe(parsed_content, |_, _, cx| cx.notify())) - } else { - None - } + parsed_content.as_ref().map(|parsed_content| { + cx.observe(parsed_content, |_, _, cx| cx.notify()) + }) }) .ok() .flatten(); diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 40acf012c9..8aaaa04729 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -267,10 +267,9 @@ impl FileFinder { ) { self.picker.update(cx, |picker, cx| { picker.delegate.include_ignored = match picker.delegate.include_ignored { - Some(true) => match FileFinderSettings::get_global(cx).include_ignored { - Some(_) => Some(false), - None => None, - }, + Some(true) => FileFinderSettings::get_global(cx) + .include_ignored + .map(|_| false), Some(false) => Some(true), None => Some(true), }; diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index e1e6cee93c..cae4d28a83 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -391,15 +391,9 @@ impl CommitModal { }); let focus_handle = self.focus_handle(cx); - let close_kb_hint = - if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) { - Some( - KeybindingHint::new(close_kb, cx.theme().colors().editor_background) - .suffix("Cancel"), - ) - } else { - None - }; + let close_kb_hint = ui::KeyBinding::for_action(&menu::Cancel, window, cx).map(|close_kb| { + KeybindingHint::new(close_kb, cx.theme().colors().editor_background).suffix("Cancel") + }); h_flex() .group("commit_editor_footer") diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index 32a6da2391..99e5073371 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -592,10 +592,7 @@ impl PlatformWindow for WindowsWindow { ) -> Option<Receiver<usize>> { let (done_tx, done_rx) = oneshot::channel(); let msg = msg.to_string(); - let detail_string = match detail { - Some(info) => Some(info.to_string()), - None => None, - }; + let detail_string = detail.map(|detail| detail.to_string()); let handle = self.0.hwnd; let answers = answers.to_vec(); self.0 diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 6b6d17a246..60e9c14c34 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -4069,13 +4069,9 @@ impl MultiBufferSnapshot { buffer_end = buffer_end.min(end_buffer_offset); } - if let Some(iterator) = - get_buffer_metadata(&excerpt.buffer, buffer_start..buffer_end) - { - Some(&mut current_excerpt_metadata.insert((excerpt.id, iterator)).1) - } else { - None - } + get_buffer_metadata(&excerpt.buffer, buffer_start..buffer_end).map(|iterator| { + &mut current_excerpt_metadata.insert((excerpt.id, iterator)).1 + }) }; // Visit each metadata item. diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index a91e3fb402..c90d85358a 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2595,11 +2595,9 @@ impl LspCommand for GetCodeActions { server_id: LanguageServerId, cx: AsyncApp, ) -> Result<Vec<CodeAction>> { - let requested_kinds_set = if let Some(kinds) = self.kinds { - Some(kinds.into_iter().collect::<HashSet<_>>()) - } else { - None - }; + let requested_kinds_set = self + .kinds + .map(|kinds| kinds.into_iter().collect::<HashSet<_>>()); let language_server = cx.update(|cx| { lsp_store @@ -3821,12 +3819,11 @@ impl GetDocumentDiagnostics { _ => None, }, code, - code_description: match diagnostic.code_description { - Some(code_description) => Some(CodeDescription { + code_description: diagnostic + .code_description + .map(|code_description| CodeDescription { href: Some(lsp::Url::parse(&code_description).unwrap()), }), - None => None, - }, related_information: Some(related_information), tags: Some(tags), source: diagnostic.source.clone(), diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index e989b974e1..1b46117897 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -12270,11 +12270,10 @@ async fn populate_labels_for_completions( let lsp_completions = new_completions .iter() .filter_map(|new_completion| { - if let Some(lsp_completion) = new_completion.source.lsp_completion(true) { - Some(lsp_completion.into_owned()) - } else { - None - } + new_completion + .source + .lsp_completion(true) + .map(|lsp_completion| lsp_completion.into_owned()) }) .collect::<Vec<_>>(); @@ -12294,11 +12293,7 @@ async fn populate_labels_for_completions( for completion in new_completions { match completion.source.lsp_completion(true) { Some(lsp_completion) => { - let documentation = if let Some(docs) = lsp_completion.documentation.clone() { - Some(docs.into()) - } else { - None - }; + let documentation = lsp_completion.documentation.clone().map(|docs| docs.into()); let mut label = labels.next().flatten().unwrap_or_else(|| { CodeLabel::fallback_for_completion(&lsp_completion, language.as_deref()) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index a5bfa883d5..52ec7a9880 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -3895,14 +3895,12 @@ impl ProjectPanel { // Always highlight directory or parent directory if it's file if target_entry.is_dir() { Some(target_entry.id) - } else if let Some(parent_entry) = target_entry - .path - .parent() - .and_then(|parent_path| target_worktree.entry_for_path(parent_path)) - { - Some(parent_entry.id) } else { - None + target_entry + .path + .parent() + .and_then(|parent_path| target_worktree.entry_for_path(parent_path)) + .map(|parent_entry| parent_entry.id) } } @@ -3939,12 +3937,10 @@ impl ProjectPanel { // Always highlight directory or parent directory if it's file if target_entry.is_dir() { Some(target_entry.id) - } else if let Some(parent_entry) = - target_parent_path.and_then(|parent_path| target_worktree.entry_for_path(parent_path)) - { - Some(parent_entry.id) } else { - None + target_parent_path + .and_then(|parent_path| target_worktree.entry_for_path(parent_path)) + .map(|parent_entry| parent_entry.id) } } diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 680c87f9e5..79d18a85e9 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1408,11 +1408,7 @@ pub fn command_interceptor(mut input: &str, cx: &App) -> Vec<CommandInterceptRes start: Position::Line { row: 0, offset: 0 }, end: Some(Position::LastLine { offset: 0 }), }); - if let Some(action) = OnMatchingLines::parse(query, invert, range, cx) { - Some(action.boxed_clone()) - } else { - None - } + OnMatchingLines::parse(query, invert, range, cx).map(|action| action.boxed_clone()) } else if query.contains('!') { ShellExec::parse(query, range.clone()) } else { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index e49eb0a345..dea18ddbe2 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2583,10 +2583,8 @@ impl Pane { .children( std::iter::once(if let Some(decorated_icon) = decorated_icon { Some(div().child(decorated_icon.into_any_element())) - } else if let Some(icon) = icon { - Some(div().child(icon.into_any_element())) } else { - None + icon.map(|icon| div().child(icon.into_any_element())) }) .flatten(), ) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b52687f335..499e4f4619 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4732,14 +4732,12 @@ impl Workspace { }) }); - if let Some(view) = view { - Some(entry.insert(FollowerView { + view.map(|view| { + entry.insert(FollowerView { view, location: None, - })) - } else { - None - } + }) + }) } }; From c5040bd0a43f5835b3bb93d33ce26139c1dd0e51 Mon Sep 17 00:00:00 2001 From: Lukas Wirth <lukas@zed.dev> Date: Wed, 20 Aug 2025 15:41:58 +0200 Subject: [PATCH 529/693] remote: Do not leave client hanging on unhandled proto message (#36590) Otherwise the client will wait for a response that never arrives, causing the task to lock up Release Notes: - N/A --- crates/remote/src/ssh_session.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 1c4409aec3..a26f4be661 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -2353,6 +2353,7 @@ impl ChannelClient { build_typed_envelope(peer_id, Instant::now(), incoming) { let type_name = envelope.payload_type_name(); + let message_id = envelope.message_id(); if let Some(future) = ProtoMessageHandlerSet::handle_message( &this.message_handlers, envelope, @@ -2391,6 +2392,15 @@ impl ChannelClient { .detach() } else { log::error!("{}:unhandled ssh message name:{type_name}", this.name); + if let Err(e) = AnyProtoClient::from(this.clone()).send_response( + message_id, + anyhow::anyhow!("no handler registered for {type_name}").to_proto(), + ) { + log::error!( + "{}:error sending error response for {type_name}:{e:#}", + this.name + ); + } } } } From 85865fc9509d7c336325a0825f990a2c6d3267ca Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Wed, 20 Aug 2025 15:54:00 +0200 Subject: [PATCH 530/693] agent2: New thread from summary (#36578) Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga <agus@zed.dev> Co-authored-by: Cole Miller <cole@zed.dev> --- crates/agent2/src/history_store.rs | 4 ++ crates/agent_ui/src/acp/message_editor.rs | 30 ++++++++ crates/agent_ui/src/acp/thread_view.rs | 25 +++++-- crates/agent_ui/src/agent_panel.rs | 83 +++++++++++++++++++---- crates/agent_ui/src/agent_ui.rs | 7 ++ crates/zed/src/zed.rs | 1 + 6 files changed, 131 insertions(+), 19 deletions(-) diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs index 3df4eddde4..870c2607c4 100644 --- a/crates/agent2/src/history_store.rs +++ b/crates/agent2/src/history_store.rs @@ -111,6 +111,10 @@ impl HistoryStore { } } + pub fn thread_from_session_id(&self, session_id: &acp::SessionId) -> Option<&DbThreadMetadata> { + self.threads.iter().find(|thread| &thread.id == session_id) + } + pub fn delete_thread( &mut self, id: acp::SessionId, diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index b5282bf891..a50e33dc31 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -163,6 +163,36 @@ impl MessageEditor { } } + pub fn insert_thread_summary( + &mut self, + thread: agent2::DbThreadMetadata, + window: &mut Window, + cx: &mut Context<Self>, + ) { + let start = self.editor.update(cx, |editor, cx| { + editor.set_text(format!("{}\n", thread.title), window, cx); + editor + .buffer() + .read(cx) + .snapshot(cx) + .anchor_before(Point::zero()) + .text_anchor + }); + + self.confirm_completion( + thread.title.clone(), + start, + thread.title.len(), + MentionUri::Thread { + id: thread.id.clone(), + name: thread.title.to_string(), + }, + window, + cx, + ) + .detach(); + } + #[cfg(test)] pub(crate) fn editor(&self) -> &Entity<Editor> { &self.editor diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index f89198c84b..8d7f9c53ca 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -155,6 +155,7 @@ impl AcpThreadView { pub fn new( agent: Rc<dyn AgentServer>, resume_thread: Option<DbThreadMetadata>, + summarize_thread: Option<DbThreadMetadata>, workspace: WeakEntity<Workspace>, project: Entity<Project>, history_store: Entity<HistoryStore>, @@ -164,7 +165,7 @@ impl AcpThreadView { ) -> Self { let prevent_slash_commands = agent.clone().downcast::<ClaudeCode>().is_some(); let message_editor = cx.new(|cx| { - MessageEditor::new( + let mut editor = MessageEditor::new( workspace.clone(), project.clone(), history_store.clone(), @@ -177,7 +178,11 @@ impl AcpThreadView { }, window, cx, - ) + ); + if let Some(entry) = summarize_thread { + editor.insert_thread_summary(entry, window, cx); + } + editor }); let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0)); @@ -3636,8 +3641,18 @@ impl AcpThreadView { .child( Button::new("start-new-thread", "Start New Thread") .label_size(LabelSize::Small) - .on_click(cx.listener(|_this, _, _window, _cx| { - // todo: Once thread summarization is implemented, start a new thread from a summary. + .on_click(cx.listener(|this, _, window, cx| { + let Some(thread) = this.thread() else { + return; + }; + let session_id = thread.read(cx).session_id().clone(); + window.dispatch_action( + crate::NewNativeAgentThreadFromSummary { + from_session_id: session_id, + } + .boxed_clone(), + cx, + ); })), ) .when(burn_mode_available, |this| { @@ -4320,6 +4335,7 @@ pub(crate) mod tests { AcpThreadView::new( Rc::new(agent), None, + None, workspace.downgrade(), project, history_store, @@ -4526,6 +4542,7 @@ pub(crate) mod tests { AcpThreadView::new( Rc::new(StubAgentServer::new(connection.as_ref().clone())), None, + None, workspace.downgrade(), project.clone(), history_store.clone(), diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 286d3b1c26..e2c4acb1ce 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -30,7 +30,7 @@ use crate::{ thread_history::{HistoryEntryElement, ThreadHistory}, ui::{AgentOnboardingModal, EndTrialUpsell}, }; -use crate::{ExternalAgent, NewExternalAgentThread}; +use crate::{ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary}; use agent::{ Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio, context_store::ContextStore, @@ -98,6 +98,16 @@ pub fn init(cx: &mut App) { workspace.focus_panel::<AgentPanel>(window, cx); } }) + .register_action( + |workspace, action: &NewNativeAgentThreadFromSummary, window, cx| { + if let Some(panel) = workspace.panel::<AgentPanel>(cx) { + panel.update(cx, |panel, cx| { + panel.new_native_agent_thread_from_summary(action, window, cx) + }); + workspace.focus_panel::<AgentPanel>(window, cx); + } + }, + ) .register_action(|workspace, _: &OpenHistory, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { workspace.focus_panel::<AgentPanel>(window, cx); @@ -120,7 +130,7 @@ pub fn init(cx: &mut App) { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { workspace.focus_panel::<AgentPanel>(window, cx); panel.update(cx, |panel, cx| { - panel.external_thread(action.agent, None, window, cx) + panel.external_thread(action.agent, None, None, window, cx) }); } }) @@ -670,6 +680,7 @@ impl AgentPanel { this.external_thread( Some(crate::ExternalAgent::NativeAgent), Some(thread.clone()), + None, window, cx, ); @@ -974,6 +985,29 @@ impl AgentPanel { AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx); } + fn new_native_agent_thread_from_summary( + &mut self, + action: &NewNativeAgentThreadFromSummary, + window: &mut Window, + cx: &mut Context<Self>, + ) { + let Some(thread) = self + .acp_history_store + .read(cx) + .thread_from_session_id(&action.from_session_id) + else { + return; + }; + + self.external_thread( + Some(ExternalAgent::NativeAgent), + None, + Some(thread.clone()), + window, + cx, + ); + } + fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) { let context = self .context_store @@ -1015,6 +1049,7 @@ impl AgentPanel { &mut self, agent_choice: Option<crate::ExternalAgent>, resume_thread: Option<DbThreadMetadata>, + summarize_thread: Option<DbThreadMetadata>, window: &mut Window, cx: &mut Context<Self>, ) { @@ -1083,6 +1118,7 @@ impl AgentPanel { crate::acp::AcpThreadView::new( server, resume_thread, + summarize_thread, workspace.clone(), project, this.acp_history_store.clone(), @@ -1754,6 +1790,7 @@ impl AgentPanel { agent2::HistoryEntry::AcpThread(entry) => this.external_thread( Some(ExternalAgent::NativeAgent), Some(entry.clone()), + None, window, cx, ), @@ -1823,15 +1860,23 @@ impl AgentPanel { AgentType::TextThread => { window.dispatch_action(NewTextThread.boxed_clone(), cx); } - AgentType::NativeAgent => { - self.external_thread(Some(crate::ExternalAgent::NativeAgent), None, window, cx) - } + AgentType::NativeAgent => self.external_thread( + Some(crate::ExternalAgent::NativeAgent), + None, + None, + window, + cx, + ), AgentType::Gemini => { - self.external_thread(Some(crate::ExternalAgent::Gemini), None, window, cx) - } - AgentType::ClaudeCode => { - self.external_thread(Some(crate::ExternalAgent::ClaudeCode), None, window, cx) + self.external_thread(Some(crate::ExternalAgent::Gemini), None, None, window, cx) } + AgentType::ClaudeCode => self.external_thread( + Some(crate::ExternalAgent::ClaudeCode), + None, + None, + window, + cx, + ), } } @@ -1841,7 +1886,13 @@ impl AgentPanel { window: &mut Window, cx: &mut Context<Self>, ) { - self.external_thread(Some(ExternalAgent::NativeAgent), Some(thread), window, cx); + self.external_thread( + Some(ExternalAgent::NativeAgent), + Some(thread), + None, + window, + cx, + ); } } @@ -2358,8 +2409,10 @@ impl AgentPanel { let focus_handle = self.focus_handle(cx); let active_thread = match &self.active_view { - ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()), - ActiveView::ExternalAgentThread { .. } + ActiveView::ExternalAgentThread { thread_view } => { + thread_view.read(cx).as_native_thread(cx) + } + ActiveView::Thread { .. } | ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None, @@ -2396,15 +2449,15 @@ impl AgentPanel { let thread = active_thread.read(cx); if !thread.is_empty() { - let thread_id = thread.id().clone(); + let session_id = thread.id().clone(); this.item( ContextMenuEntry::new("New From Summary") .icon(IconName::ThreadFromSummary) .icon_color(Color::Muted) .handler(move |window, cx| { window.dispatch_action( - Box::new(NewThread { - from_thread_id: Some(thread_id.clone()), + Box::new(NewNativeAgentThreadFromSummary { + from_session_id: session_id.clone(), }), cx, ); diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 01a248994d..7b6557245f 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -146,6 +146,13 @@ pub struct NewExternalAgentThread { agent: Option<ExternalAgent>, } +#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] +#[action(namespace = agent)] +#[serde(deny_unknown_fields)] +pub struct NewNativeAgentThreadFromSummary { + from_session_id: agent_client_protocol::SessionId, +} + #[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] enum ExternalAgent { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 0972973b89..0f6d236c65 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4362,6 +4362,7 @@ mod tests { | "workspace::MoveItemToPaneInDirection" | "workspace::OpenTerminal" | "workspace::SendKeystrokes" + | "agent::NewNativeAgentThreadFromSummary" | "zed::OpenBrowser" | "zed::OpenZedUrl" => {} _ => { From eaf6b56163c2b987e06981e332e06d68aed5608b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra <me@as-cii.com> Date: Wed, 20 Aug 2025 15:56:39 +0200 Subject: [PATCH 531/693] Miscellaneous UX fixes for agent2 (#36591) Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 97 ++++++++++++++++++++++++++ crates/agent_ui/src/acp/thread_view.rs | 44 +++++++----- 2 files changed, 123 insertions(+), 18 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index b8908fa0da..a1f9b32eba 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1394,6 +1394,17 @@ impl AcpThread { this.send_task.take(); } + // Truncate entries if the last prompt was refused. + if let Ok(Ok(acp::PromptResponse { + stop_reason: acp::StopReason::Refusal, + })) = result + && let Some((ix, _)) = this.last_user_message() + { + let range = ix..this.entries.len(); + this.entries.truncate(ix); + cx.emit(AcpThreadEvent::EntriesRemoved(range)); + } + cx.emit(AcpThreadEvent::Stopped); Ok(()) } @@ -2369,6 +2380,92 @@ mod tests { assert_eq!(fs.files(), vec![Path::new(path!("/test/file-0"))]); } + #[gpui::test] + async fn test_refusal(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree(path!("/"), json!({})).await; + let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await; + + let refuse_next = Arc::new(AtomicBool::new(false)); + let connection = Rc::new(FakeAgentConnection::new().on_user_message({ + let refuse_next = refuse_next.clone(); + move |request, thread, mut cx| { + let refuse_next = refuse_next.clone(); + async move { + if refuse_next.load(SeqCst) { + return Ok(acp::PromptResponse { + stop_reason: acp::StopReason::Refusal, + }); + } + + let acp::ContentBlock::Text(content) = &request.prompt[0] else { + panic!("expected text content block"); + }; + thread.update(&mut cx, |thread, cx| { + thread + .handle_session_update( + acp::SessionUpdate::AgentMessageChunk { + content: content.text.to_uppercase().into(), + }, + cx, + ) + .unwrap(); + })?; + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) + } + .boxed_local() + } + })); + let thread = cx + .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) + .await + .unwrap(); + + cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["hello".into()], cx))) + .await + .unwrap(); + thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc! {" + ## User + + hello + + ## Assistant + + HELLO + + "} + ); + }); + + // Simulate refusing the second message, ensuring the conversation gets + // truncated to before sending it. + refuse_next.store(true, SeqCst); + cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["world".into()], cx))) + .await + .unwrap(); + thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + indoc! {" + ## User + + hello + + ## Assistant + + HELLO + + "} + ); + }); + } + async fn run_until_first_tool_call( thread: &Entity<AcpThread>, cx: &mut TestAppContext, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 8d7f9c53ca..9bb5953eaf 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2398,7 +2398,6 @@ impl AcpThreadView { }) .when(!changed_buffers.is_empty(), |this| { this.child(self.render_edits_summary( - action_log, &changed_buffers, self.edits_expanded, pending_edits, @@ -2550,7 +2549,6 @@ impl AcpThreadView { fn render_edits_summary( &self, - action_log: &Entity<ActionLog>, changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>, expanded: bool, pending_edits: bool, @@ -2661,14 +2659,9 @@ impl AcpThreadView { ) .map(|kb| kb.size(rems_from_px(10.))), ) - .on_click({ - let action_log = action_log.clone(); - cx.listener(move |_, _, _, cx| { - action_log.update(cx, |action_log, cx| { - action_log.reject_all_edits(cx).detach(); - }) - }) - }), + .on_click(cx.listener(move |this, _, window, cx| { + this.reject_all(&RejectAll, window, cx); + })), ) .child( Button::new("keep-all-changes", "Keep All") @@ -2681,14 +2674,9 @@ impl AcpThreadView { KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx) .map(|kb| kb.size(rems_from_px(10.))), ) - .on_click({ - let action_log = action_log.clone(); - cx.listener(move |_, _, _, cx| { - action_log.update(cx, |action_log, cx| { - action_log.keep_all_edits(cx); - }) - }) - }), + .on_click(cx.listener(move |this, _, window, cx| { + this.keep_all(&KeepAll, window, cx); + })), ), ) } @@ -3014,6 +3002,24 @@ impl AcpThreadView { }); } + fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) { + let Some(thread) = self.thread() else { + return; + }; + let action_log = thread.read(cx).action_log().clone(); + action_log.update(cx, |action_log, cx| action_log.keep_all_edits(cx)); + } + + fn reject_all(&mut self, _: &RejectAll, _window: &mut Window, cx: &mut Context<Self>) { + let Some(thread) = self.thread() else { + return; + }; + let action_log = thread.read(cx).action_log().clone(); + action_log + .update(cx, |action_log, cx| action_log.reject_all_edits(cx)) + .detach(); + } + fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> { let thread = self.as_native_thread(cx)?.read(cx); @@ -3952,6 +3958,8 @@ impl Render for AcpThreadView { .key_context("AcpThread") .on_action(cx.listener(Self::open_agent_diff)) .on_action(cx.listener(Self::toggle_burn_mode)) + .on_action(cx.listener(Self::keep_all)) + .on_action(cx.listener(Self::reject_all)) .bg(cx.theme().colors().panel_background) .child(match &self.thread_state { ThreadState::Unauthenticated { From 92352f97ad966df29cbac117b9c9ca6a697676f4 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Wed, 20 Aug 2025 17:34:52 +0300 Subject: [PATCH 532/693] Fix `clippy::map_clone` lint violations (#36585) #36577 Release Notes: - N/A --- Cargo.toml | 1 + crates/git_ui/src/git_panel.rs | 2 +- crates/gpui/src/platform/linux/x11/client.rs | 2 +- crates/workspace/src/pane.rs | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9cd206cebf..a049940772 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -879,6 +879,7 @@ manual_pattern_char_comparison = "warn" manual_rotate = "warn" manual_slice_fill = "warn" manual_while_let_some = "warn" +map_clone = "warn" map_collect_result_unit = "warn" match_like_matches_macro = "warn" match_overlapping_arm = "warn" diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 79d182eb22..cc947bcb72 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1335,7 +1335,7 @@ impl GitPanel { section.contains(status_entry, repository) && status_entry.staging.as_bool() != Some(goal_staged_state) }) - .map(|status_entry| status_entry.clone()) + .cloned() .collect::<Vec<_>>(); (goal_staged_state, entries) diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index d501170892..9a43bd6470 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -2108,7 +2108,7 @@ fn current_pointer_device_states( .classes .iter() .filter_map(|class| class.data.as_scroll()) - .map(|class| *class) + .copied() .rev() .collect::<Vec<_>>(); let old_state = scroll_values_to_preserve.get(&info.deviceid); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index dea18ddbe2..23c8c0b185 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -3082,7 +3082,7 @@ impl Pane { .read(cx) .items() .find(|item| item.item_id() == item_id) - .map(|item| item.clone()) + .cloned() else { return; }; From 1e6cefaa56dc3dd62efd29ffc58262e710d6dbc1 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Wed, 20 Aug 2025 20:05:59 +0530 Subject: [PATCH 533/693] Fix `clippy::len_zero` lint style violations (#36589) Related: #36577 Release Notes: - N/A --------- Signed-off-by: Umesh Yadav <git@umesh.dev> --- Cargo.toml | 1 + crates/agent2/src/tools/find_path_tool.rs | 2 +- crates/agent_servers/src/claude.rs | 2 +- crates/agent_ui/src/message_editor.rs | 4 ++-- .../src/agent_panel_onboarding_content.rs | 2 +- crates/assistant_tools/src/find_path_tool.rs | 2 +- crates/buffer_diff/src/buffer_diff.rs | 2 +- crates/collab/src/tests/editor_tests.rs | 2 +- crates/debugger_ui/src/session/running.rs | 7 +++---- .../src/session/running/breakpoint_list.rs | 8 ++++---- .../src/session/running/module_list.rs | 8 ++++---- .../src/session/running/stack_frame_list.rs | 8 ++++---- .../src/session/running/variable_list.rs | 4 ++-- crates/diagnostics/src/diagnostics_tests.rs | 2 +- crates/editor/src/editor_tests.rs | 2 +- crates/editor/src/jsx_tag_auto_close.rs | 4 ++-- crates/editor/src/test/editor_test_context.rs | 2 +- crates/git_ui/src/git_panel.rs | 16 ++++++++-------- crates/language_models/src/provider/google.rs | 2 +- crates/project/src/debugger/dap_store.rs | 4 ++-- crates/tasks_ui/src/modal.rs | 2 +- crates/vim/src/digraph.rs | 2 +- crates/workspace/src/persistence/model.rs | 2 +- crates/zed/src/zed.rs | 2 +- 24 files changed, 46 insertions(+), 46 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a049940772..a2de4aaaed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -864,6 +864,7 @@ iter_nth = "warn" iter_nth_zero = "warn" iter_skip_next = "warn" just_underscores_and_digits = "warn" +len_zero = "warn" let_and_return = "warn" main_recursion = "warn" manual_bits = "warn" diff --git a/crates/agent2/src/tools/find_path_tool.rs b/crates/agent2/src/tools/find_path_tool.rs index 552de144a7..deccf37ab7 100644 --- a/crates/agent2/src/tools/find_path_tool.rs +++ b/crates/agent2/src/tools/find_path_tool.rs @@ -116,7 +116,7 @@ impl AgentTool for FindPathTool { ..cmp::min(input.offset + RESULTS_PER_PAGE, matches.len())]; event_stream.update_fields(acp::ToolCallUpdateFields { - title: Some(if paginated_matches.len() == 0 { + title: Some(if paginated_matches.is_empty() { "No matches".into() } else if paginated_matches.len() == 1 { "1 match".into() diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index df2a24e698..6b9732b468 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -1117,7 +1117,7 @@ pub(crate) mod tests { thread.read_with(cx, |thread, _| { entries_len = thread.plan().entries.len(); - assert!(thread.plan().entries.len() > 0, "Empty plan"); + assert!(!thread.plan().entries.is_empty(), "Empty plan"); }); thread diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index fdbce14415..bed10e90a7 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -1682,7 +1682,7 @@ impl Render for MessageEditor { let has_history = self .history_store .as_ref() - .and_then(|hs| hs.update(cx, |hs, cx| hs.entries(cx).len() > 0).ok()) + .and_then(|hs| hs.update(cx, |hs, cx| !hs.entries(cx).is_empty()).ok()) .unwrap_or(false) || self .thread @@ -1695,7 +1695,7 @@ impl Render for MessageEditor { !has_history && is_signed_out && has_configured_providers, |this| this.child(cx.new(ApiKeysWithProviders::new)), ) - .when(changed_buffers.len() > 0, |parent| { + .when(!changed_buffers.is_empty(), |parent| { parent.child(self.render_edits_bar(&changed_buffers, window, cx)) }) .child(self.render_editor(window, cx)) diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs index 1a44fa3c17..77f41d1a73 100644 --- a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs +++ b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs @@ -74,7 +74,7 @@ impl Render for AgentPanelOnboarding { }), ) .map(|this| { - if enrolled_in_trial || is_pro_user || self.configured_providers.len() >= 1 { + if enrolled_in_trial || is_pro_user || !self.configured_providers.is_empty() { this } else { this.child(ApiKeysWithoutProviders::new()) diff --git a/crates/assistant_tools/src/find_path_tool.rs b/crates/assistant_tools/src/find_path_tool.rs index 6b62638a4c..ac2c7a32ab 100644 --- a/crates/assistant_tools/src/find_path_tool.rs +++ b/crates/assistant_tools/src/find_path_tool.rs @@ -234,7 +234,7 @@ impl ToolCard for FindPathToolCard { workspace: WeakEntity<Workspace>, cx: &mut Context<Self>, ) -> impl IntoElement { - let matches_label: SharedString = if self.paths.len() == 0 { + let matches_label: SharedString = if self.paths.is_empty() { "No matches".into() } else if self.paths.len() == 1 { "1 match".into() diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 6a9ca026e7..bef0c5cfc3 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -2129,7 +2129,7 @@ mod tests { diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &working_copy, cx) .collect::<Vec<_>>() }); - if hunks.len() == 0 { + if hunks.is_empty() { return; } diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 4e7996ce3b..1b0c581983 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -2908,7 +2908,7 @@ async fn test_lsp_pull_diagnostics( { assert!( - diagnostics_pulls_result_ids.lock().await.len() > 0, + !diagnostics_pulls_result_ids.lock().await.is_empty(), "Initial diagnostics pulls should report None at least" ); assert_eq!( diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 4306104877..0574091851 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -1113,9 +1113,8 @@ impl RunningState { }; let session = self.session.read(cx); - let cwd = Some(&request.cwd) - .filter(|cwd| cwd.len() > 0) - .map(PathBuf::from) + let cwd = (!request.cwd.is_empty()) + .then(|| PathBuf::from(&request.cwd)) .or_else(|| session.binary().unwrap().cwd.clone()); let mut envs: HashMap<String, String> = @@ -1150,7 +1149,7 @@ impl RunningState { } else { None } - } else if args.len() > 0 { + } else if !args.is_empty() { Some(args.remove(0)) } else { None diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index d04443e201..233dba4c52 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -244,7 +244,7 @@ impl BreakpointList { return; } let ix = match self.selected_ix { - _ if self.breakpoints.len() == 0 => None, + _ if self.breakpoints.is_empty() => None, None => Some(0), Some(ix) => { if ix == self.breakpoints.len() - 1 { @@ -268,7 +268,7 @@ impl BreakpointList { return; } let ix = match self.selected_ix { - _ if self.breakpoints.len() == 0 => None, + _ if self.breakpoints.is_empty() => None, None => Some(self.breakpoints.len() - 1), Some(ix) => { if ix == 0 { @@ -286,7 +286,7 @@ impl BreakpointList { cx.propagate(); return; } - let ix = if self.breakpoints.len() > 0 { + let ix = if !self.breakpoints.is_empty() { Some(0) } else { None @@ -299,7 +299,7 @@ impl BreakpointList { cx.propagate(); return; } - let ix = if self.breakpoints.len() > 0 { + let ix = if !self.breakpoints.is_empty() { Some(self.breakpoints.len() - 1) } else { None diff --git a/crates/debugger_ui/src/session/running/module_list.rs b/crates/debugger_ui/src/session/running/module_list.rs index 1c1e0f3efc..7743cfbdee 100644 --- a/crates/debugger_ui/src/session/running/module_list.rs +++ b/crates/debugger_ui/src/session/running/module_list.rs @@ -223,7 +223,7 @@ impl ModuleList { fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) { let ix = match self.selected_ix { - _ if self.entries.len() == 0 => None, + _ if self.entries.is_empty() => None, None => Some(0), Some(ix) => { if ix == self.entries.len() - 1 { @@ -243,7 +243,7 @@ impl ModuleList { cx: &mut Context<Self>, ) { let ix = match self.selected_ix { - _ if self.entries.len() == 0 => None, + _ if self.entries.is_empty() => None, None => Some(self.entries.len() - 1), Some(ix) => { if ix == 0 { @@ -262,7 +262,7 @@ impl ModuleList { _window: &mut Window, cx: &mut Context<Self>, ) { - let ix = if self.entries.len() > 0 { + let ix = if !self.entries.is_empty() { Some(0) } else { None @@ -271,7 +271,7 @@ impl ModuleList { } fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) { - let ix = if self.entries.len() > 0 { + let ix = if !self.entries.is_empty() { Some(self.entries.len() - 1) } else { None diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index f9b5ed5e3f..a4ea4ab654 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -621,7 +621,7 @@ impl StackFrameList { fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) { let ix = match self.selected_ix { - _ if self.entries.len() == 0 => None, + _ if self.entries.is_empty() => None, None => Some(0), Some(ix) => { if ix == self.entries.len() - 1 { @@ -641,7 +641,7 @@ impl StackFrameList { cx: &mut Context<Self>, ) { let ix = match self.selected_ix { - _ if self.entries.len() == 0 => None, + _ if self.entries.is_empty() => None, None => Some(self.entries.len() - 1), Some(ix) => { if ix == 0 { @@ -660,7 +660,7 @@ impl StackFrameList { _window: &mut Window, cx: &mut Context<Self>, ) { - let ix = if self.entries.len() > 0 { + let ix = if !self.entries.is_empty() { Some(0) } else { None @@ -669,7 +669,7 @@ impl StackFrameList { } fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) { - let ix = if self.entries.len() > 0 { + let ix = if !self.entries.is_empty() { Some(self.entries.len() - 1) } else { None diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index 18f574389e..b396f0921e 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -291,7 +291,7 @@ impl VariableList { } self.session.update(cx, |session, cx| { - session.variables(scope.variables_reference, cx).len() > 0 + !session.variables(scope.variables_reference, cx).is_empty() }) }) .map(|scope| { @@ -997,7 +997,7 @@ impl VariableList { DapEntry::Watcher { .. } => continue, DapEntry::Variable(dap) => scopes[idx].1.push(dap.clone()), DapEntry::Scope(scope) => { - if scopes.len() > 0 { + if !scopes.is_empty() { idx += 1; } diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 5df1b13897..4a544f9ea7 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -862,7 +862,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S 21..=50 => mutated_diagnostics.update_in(cx, |diagnostics, window, cx| { diagnostics.editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(window, cx); - if snapshot.buffer_snapshot.len() > 0 { + if !snapshot.buffer_snapshot.is_empty() { let position = rng.gen_range(0..snapshot.buffer_snapshot.len()); let position = snapshot.buffer_snapshot.clip_offset(position, Bias::Left); log::info!( diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 44c05dbc14..96261fdb2c 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -21030,7 +21030,7 @@ fn assert_breakpoint( path: &Arc<Path>, expected: Vec<(u32, Breakpoint)>, ) { - if expected.len() == 0usize { + if expected.is_empty() { assert!(!breakpoints.contains_key(path), "{}", path.display()); } else { let mut breakpoint = breakpoints diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index 83ab02814f..e6c518beae 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -181,7 +181,7 @@ pub(crate) fn generate_auto_close_edits( */ { let tag_node_name_equals = |node: &Node, name: &str| { - let is_empty = name.len() == 0; + let is_empty = name.is_empty(); if let Some(node_name) = node.named_child(TS_NODE_TAG_NAME_CHILD_INDEX) { let range = node_name.byte_range(); return buffer.text_for_range(range).equals_str(name); @@ -207,7 +207,7 @@ pub(crate) fn generate_auto_close_edits( cur = descendant; } - assert!(ancestors.len() > 0); + assert!(!ancestors.is_empty()); let mut tree_root_node = open_tag; diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 88721c59e7..8c54c265ed 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -420,7 +420,7 @@ impl EditorTestContext { if expected_text == "[FOLDED]\n" { assert!(is_folded, "excerpt {} should be folded", ix); let is_selected = selections.iter().any(|s| s.head().excerpt_id == excerpt_id); - if expected_selections.len() > 0 { + if !expected_selections.is_empty() { assert!( is_selected, "excerpt {ix} should be selected. got {:?}", diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index cc947bcb72..4ecb4a8829 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -2175,7 +2175,7 @@ impl GitPanel { let worktree = if worktrees.len() == 1 { Task::ready(Some(worktrees.first().unwrap().clone())) - } else if worktrees.len() == 0 { + } else if worktrees.is_empty() { let result = window.prompt( PromptLevel::Warning, "Unable to initialize a git repository", @@ -2758,22 +2758,22 @@ impl GitPanel { } } - if conflict_entries.len() == 0 && staged_count == 1 && pending_staged_count == 0 { + if conflict_entries.is_empty() && staged_count == 1 && pending_staged_count == 0 { match pending_status_for_single_staged { Some(TargetStatus::Staged) | None => { self.single_staged_entry = single_staged_entry; } _ => {} } - } else if conflict_entries.len() == 0 && pending_staged_count == 1 { + } else if conflict_entries.is_empty() && pending_staged_count == 1 { self.single_staged_entry = last_pending_staged; } - if conflict_entries.len() == 0 && changed_entries.len() == 1 { + if conflict_entries.is_empty() && changed_entries.len() == 1 { self.single_tracked_entry = changed_entries.first().cloned(); } - if conflict_entries.len() > 0 { + if !conflict_entries.is_empty() { self.entries.push(GitListEntry::Header(GitHeaderEntry { header: Section::Conflict, })); @@ -2781,7 +2781,7 @@ impl GitPanel { .extend(conflict_entries.into_iter().map(GitListEntry::Status)); } - if changed_entries.len() > 0 { + if !changed_entries.is_empty() { if !sort_by_path { self.entries.push(GitListEntry::Header(GitHeaderEntry { header: Section::Tracked, @@ -2790,7 +2790,7 @@ impl GitPanel { self.entries .extend(changed_entries.into_iter().map(GitListEntry::Status)); } - if new_entries.len() > 0 { + if !new_entries.is_empty() { self.entries.push(GitListEntry::Header(GitHeaderEntry { header: Section::New, })); @@ -4476,7 +4476,7 @@ fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn Language impl Render for GitPanel { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let project = self.project.read(cx); - let has_entries = self.entries.len() > 0; + let has_entries = !self.entries.is_empty(); let room = self .workspace .upgrade() diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index c8d4151e8b..1ac12b4cd4 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -577,7 +577,7 @@ pub fn into_google( top_k: None, }), safety_settings: None, - tools: (request.tools.len() > 0).then(|| { + tools: (!request.tools.is_empty()).then(|| { vec![google_ai::Tool { function_declarations: request .tools diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 45e1c7f291..834bf2c2d2 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -684,7 +684,7 @@ impl DapStore { let shutdown_id = parent_session.update(cx, |parent_session, _| { parent_session.remove_child_session_id(session_id); - if parent_session.child_session_ids().len() == 0 { + if parent_session.child_session_ids().is_empty() { Some(parent_session.session_id()) } else { None @@ -701,7 +701,7 @@ impl DapStore { cx.emit(DapStoreEvent::DebugClientShutdown(session_id)); cx.background_spawn(async move { - if shutdown_children.len() > 0 { + if !shutdown_children.is_empty() { let _ = join_all(shutdown_children).await; } diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 9fbdc152f3..423c28c710 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -461,7 +461,7 @@ impl PickerDelegate for TasksModalDelegate { tooltip_label_text.push_str(&resolved_task.resolved.command_label); } - if template.tags.len() > 0 { + if !template.tags.is_empty() { tooltip_label_text.push('\n'); tooltip_label_text.push_str( template diff --git a/crates/vim/src/digraph.rs b/crates/vim/src/digraph.rs index 248047bb55..796dad94c0 100644 --- a/crates/vim/src/digraph.rs +++ b/crates/vim/src/digraph.rs @@ -89,7 +89,7 @@ impl Vim { return; }; - if prefix.len() > 0 { + if !prefix.is_empty() { self.handle_literal_input(prefix, "", window, cx); } else { self.pop_operator(window, cx); diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index da8a3070fc..15a54ac62f 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -235,7 +235,7 @@ impl SerializedWorkspaceLocation { pub fn sorted_paths(&self) -> Arc<Vec<PathBuf>> { match self { SerializedWorkspaceLocation::Local(paths, order) => { - if order.order().len() == 0 { + if order.order().is_empty() { paths.paths().clone() } else { Arc::new( diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 0f6d236c65..958149825a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4377,7 +4377,7 @@ mod tests { } } } - if errors.len() > 0 { + if !errors.is_empty() { panic!( "Failed to build actions using {{}} as input: {:?}. Errors:\n{}", failing_names, From 699f58aeba56b10e99f789f9fb492c76fbeea81b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra <me@as-cii.com> Date: Wed, 20 Aug 2025 18:04:32 +0200 Subject: [PATCH 534/693] Capture telemetry when requesting completions in agent2 (#36600) Release Notes: - N/A --- Cargo.lock | 1 + crates/agent2/Cargo.toml | 1 + crates/agent2/src/thread.rs | 26 ++++++++++++++++++++++++-- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fdc858ef50..342bb1058f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -239,6 +239,7 @@ dependencies = [ "smol", "sqlez", "task", + "telemetry", "tempfile", "terminal", "text", diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index bc32a79622..2a5d879e9e 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -58,6 +58,7 @@ settings.workspace = true smol.workspace = true sqlez.workspace = true task.workspace = true +telemetry.workspace = true terminal.workspace = true text.workspace = true ui.workspace = true diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index c7b1a08b92..f407ee7de5 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1195,6 +1195,15 @@ impl Thread { let mut attempt = None; 'retry: loop { + telemetry::event!( + "Agent Thread Completion", + thread_id = this.read_with(cx, |this, _| this.id.to_string())?, + prompt_id = this.read_with(cx, |this, _| this.prompt_id.to_string())?, + model = model.telemetry_id(), + model_provider = model.provider_id().to_string(), + attempt + ); + let mut events = model.stream_completion(request.clone(), cx).await?; let mut tool_uses = FuturesUnordered::new(); while let Some(event) = events.next().await { @@ -1211,8 +1220,21 @@ impl Thread { this.update_model_request_usage(amount, limit, cx) })?; } - Ok(LanguageModelCompletionEvent::UsageUpdate(token_usage)) => { - this.update(cx, |this, cx| this.update_token_usage(token_usage, cx))?; + Ok(LanguageModelCompletionEvent::UsageUpdate(usage)) => { + telemetry::event!( + "Agent Thread Completion Usage Updated", + thread_id = this.read_with(cx, |this, _| this.id.to_string())?, + prompt_id = this.read_with(cx, |this, _| this.prompt_id.to_string())?, + model = model.telemetry_id(), + model_provider = model.provider_id().to_string(), + attempt, + input_tokens = usage.input_tokens, + output_tokens = usage.output_tokens, + cache_creation_input_tokens = usage.cache_creation_input_tokens, + cache_read_input_tokens = usage.cache_read_input_tokens, + ); + + this.update(cx, |this, cx| this.update_token_usage(usage, cx))?; } Ok(LanguageModelCompletionEvent::Stop(StopReason::Refusal)) => { *refusal = true; From d0fb6120d9583fd46b17aed9d2b9a5b08e302f7e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra <me@as-cii.com> Date: Wed, 20 Aug 2025 18:39:46 +0200 Subject: [PATCH 535/693] Fix scrollbar flicker when streaming agent2 response (#36606) This was caused by calling `list_state.splice` on updated entries. We don't need to splice the entry, as we'll recompute its measurements automatically when we render it. Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 9bb5953eaf..87fe133bba 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -793,7 +793,6 @@ impl AcpThreadView { self.entry_view_state.update(cx, |view_state, cx| { view_state.sync_entry(*index, thread, window, cx) }); - self.list_state.splice(*index..index + 1, 1); } AcpThreadEvent::EntriesRemoved(range) => { self.entry_view_state From 8334cdb35805ca00c574daa623f62dc1867adb67 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Wed, 20 Aug 2025 19:10:43 +0200 Subject: [PATCH 536/693] agent2: Port feedback (#36603) Release Notes: - N/A --------- Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com> --- crates/acp_thread/src/connection.rs | 17 ++ crates/agent/src/thread.rs | 53 ----- crates/agent2/src/agent.rs | 25 +++ crates/agent_ui/src/acp/thread_view.rs | 283 ++++++++++++++++++++++++- crates/agent_ui/src/active_thread.rs | 1 - 5 files changed, 321 insertions(+), 58 deletions(-) diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 8cae975ce5..dc1a41c81e 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -64,6 +64,10 @@ pub trait AgentConnection { None } + fn telemetry(&self) -> Option<Rc<dyn AgentTelemetry>> { + None + } + fn into_any(self: Rc<Self>) -> Rc<dyn Any>; } @@ -81,6 +85,19 @@ pub trait AgentSessionResume { fn run(&self, cx: &mut App) -> Task<Result<acp::PromptResponse>>; } +pub trait AgentTelemetry { + /// The name of the agent used for telemetry. + fn agent_name(&self) -> String; + + /// A representation of the current thread state that can be serialized for + /// storage with telemetry events. + fn thread_data( + &self, + session_id: &acp::SessionId, + cx: &mut App, + ) -> Task<Result<serde_json::Value>>; +} + #[derive(Debug)] pub struct AuthRequired { pub description: Option<String>, diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index a584fba881..7b70fde56a 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -387,7 +387,6 @@ pub struct Thread { cumulative_token_usage: TokenUsage, exceeded_window_error: Option<ExceededWindowError>, tool_use_limit_reached: bool, - feedback: Option<ThreadFeedback>, retry_state: Option<RetryState>, message_feedback: HashMap<MessageId, ThreadFeedback>, last_received_chunk_at: Option<Instant>, @@ -487,7 +486,6 @@ impl Thread { cumulative_token_usage: TokenUsage::default(), exceeded_window_error: None, tool_use_limit_reached: false, - feedback: None, retry_state: None, message_feedback: HashMap::default(), last_error_context: None, @@ -612,7 +610,6 @@ impl Thread { cumulative_token_usage: serialized.cumulative_token_usage, exceeded_window_error: None, tool_use_limit_reached: serialized.tool_use_limit_reached, - feedback: None, message_feedback: HashMap::default(), last_error_context: None, last_received_chunk_at: None, @@ -2787,10 +2784,6 @@ impl Thread { cx.emit(ThreadEvent::CancelEditing); } - pub fn feedback(&self) -> Option<ThreadFeedback> { - self.feedback - } - pub fn message_feedback(&self, message_id: MessageId) -> Option<ThreadFeedback> { self.message_feedback.get(&message_id).copied() } @@ -2852,52 +2845,6 @@ impl Thread { }) } - pub fn report_feedback( - &mut self, - feedback: ThreadFeedback, - cx: &mut Context<Self>, - ) -> Task<Result<()>> { - let last_assistant_message_id = self - .messages - .iter() - .rev() - .find(|msg| msg.role == Role::Assistant) - .map(|msg| msg.id); - - if let Some(message_id) = last_assistant_message_id { - self.report_message_feedback(message_id, feedback, cx) - } else { - let final_project_snapshot = Self::project_snapshot(self.project.clone(), cx); - let serialized_thread = self.serialize(cx); - let thread_id = self.id().clone(); - let client = self.project.read(cx).client(); - self.feedback = Some(feedback); - cx.notify(); - - cx.background_spawn(async move { - let final_project_snapshot = final_project_snapshot.await; - let serialized_thread = serialized_thread.await?; - let thread_data = serde_json::to_value(serialized_thread) - .unwrap_or_else(|_| serde_json::Value::Null); - - let rating = match feedback { - ThreadFeedback::Positive => "positive", - ThreadFeedback::Negative => "negative", - }; - telemetry::event!( - "Assistant Thread Rated", - rating, - thread_id, - thread_data, - final_project_snapshot - ); - client.telemetry().flush_events().await; - - Ok(()) - }) - } - } - /// Create a snapshot of the current project state including git information and unsaved buffers. fn project_snapshot( project: Entity<Project>, diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 1fa307511f..2f5f15399e 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -948,11 +948,36 @@ impl acp_thread::AgentConnection for NativeAgentConnection { }) } + fn telemetry(&self) -> Option<Rc<dyn acp_thread::AgentTelemetry>> { + Some(Rc::new(self.clone()) as Rc<dyn acp_thread::AgentTelemetry>) + } + fn into_any(self: Rc<Self>) -> Rc<dyn Any> { self } } +impl acp_thread::AgentTelemetry for NativeAgentConnection { + fn agent_name(&self) -> String { + "Zed".into() + } + + fn thread_data( + &self, + session_id: &acp::SessionId, + cx: &mut App, + ) -> Task<Result<serde_json::Value>> { + let Some(session) = self.0.read(cx).sessions.get(session_id) else { + return Task::ready(Err(anyhow!("Session not found"))); + }; + + let task = session.thread.read(cx).to_db(cx); + cx.background_spawn(async move { + serde_json::to_value(task.await).context("Failed to serialize thread") + }) + } +} + struct NativeAgentSessionEditor { thread: Entity<Thread>, acp_thread: WeakEntity<AcpThread>, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 87fe133bba..4ce55cce56 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -65,6 +65,12 @@ const RESPONSE_PADDING_X: Pixels = px(19.); pub const MIN_EDITOR_LINES: usize = 4; pub const MAX_EDITOR_LINES: usize = 8; +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum ThreadFeedback { + Positive, + Negative, +} + enum ThreadError { PaymentRequired, ModelRequestLimitReached(cloud_llm_client::Plan), @@ -106,6 +112,128 @@ impl ProfileProvider for Entity<agent2::Thread> { } } +#[derive(Default)] +struct ThreadFeedbackState { + feedback: Option<ThreadFeedback>, + comments_editor: Option<Entity<Editor>>, +} + +impl ThreadFeedbackState { + pub fn submit( + &mut self, + thread: Entity<AcpThread>, + feedback: ThreadFeedback, + window: &mut Window, + cx: &mut App, + ) { + let Some(telemetry) = thread.read(cx).connection().telemetry() else { + return; + }; + + if self.feedback == Some(feedback) { + return; + } + + self.feedback = Some(feedback); + match feedback { + ThreadFeedback::Positive => { + self.comments_editor = None; + } + ThreadFeedback::Negative => { + self.comments_editor = Some(Self::build_feedback_comments_editor(window, cx)); + } + } + let session_id = thread.read(cx).session_id().clone(); + let agent_name = telemetry.agent_name(); + let task = telemetry.thread_data(&session_id, cx); + let rating = match feedback { + ThreadFeedback::Positive => "positive", + ThreadFeedback::Negative => "negative", + }; + cx.background_spawn(async move { + let thread = task.await?; + telemetry::event!( + "Agent Thread Rated", + session_id = session_id, + rating = rating, + agent = agent_name, + thread = thread + ); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + + pub fn submit_comments(&mut self, thread: Entity<AcpThread>, cx: &mut App) { + let Some(telemetry) = thread.read(cx).connection().telemetry() else { + return; + }; + + let Some(comments) = self + .comments_editor + .as_ref() + .map(|editor| editor.read(cx).text(cx)) + .filter(|text| !text.trim().is_empty()) + else { + return; + }; + + self.comments_editor.take(); + + let session_id = thread.read(cx).session_id().clone(); + let agent_name = telemetry.agent_name(); + let task = telemetry.thread_data(&session_id, cx); + cx.background_spawn(async move { + let thread = task.await?; + telemetry::event!( + "Agent Thread Feedback Comments", + session_id = session_id, + comments = comments, + agent = agent_name, + thread = thread + ); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + + pub fn clear(&mut self) { + *self = Self::default() + } + + pub fn dismiss_comments(&mut self) { + self.comments_editor.take(); + } + + fn build_feedback_comments_editor(window: &mut Window, cx: &mut App) -> Entity<Editor> { + let buffer = cx.new(|cx| { + let empty_string = String::new(); + MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx) + }); + + let editor = cx.new(|cx| { + let mut editor = Editor::new( + editor::EditorMode::AutoHeight { + min_lines: 1, + max_lines: Some(4), + }, + buffer, + None, + window, + cx, + ); + editor.set_placeholder_text( + "What went wrong? Share your feedback so we can improve.", + cx, + ); + editor + }); + + editor.read(cx).focus_handle(cx).focus(window); + editor + } +} + pub struct AcpThreadView { agent: Rc<dyn AgentServer>, workspace: WeakEntity<Workspace>, @@ -120,6 +248,7 @@ pub struct AcpThreadView { notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>, thread_retry_status: Option<RetryStatus>, thread_error: Option<ThreadError>, + thread_feedback: ThreadFeedbackState, list_state: ListState, scrollbar_state: ScrollbarState, auth_task: Option<Task<()>>, @@ -218,6 +347,7 @@ impl AcpThreadView { scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), thread_retry_status: None, thread_error: None, + thread_feedback: Default::default(), auth_task: None, expanded_tool_calls: HashSet::default(), expanded_thinking_blocks: HashSet::default(), @@ -615,6 +745,7 @@ impl AcpThreadView { ) { self.thread_error.take(); self.editing_message.take(); + self.thread_feedback.clear(); let Some(thread) = self.thread().cloned() else { return; @@ -1087,6 +1218,12 @@ impl AcpThreadView { .w_full() .child(primary) .child(self.render_thread_controls(cx)) + .when_some( + self.thread_feedback.comments_editor.clone(), + |this, editor| { + this.child(Self::render_feedback_feedback_editor(editor, window, cx)) + }, + ) .into_any_element() } else { primary @@ -3556,7 +3693,9 @@ impl AcpThreadView { this.scroll_to_top(cx); })); - h_flex() + let mut container = h_flex() + .id("thread-controls-container") + .group("thread-controls-container") .w_full() .mr_1() .pb_2() @@ -3564,9 +3703,145 @@ impl AcpThreadView { .opacity(0.4) .hover(|style| style.opacity(1.)) .flex_wrap() - .justify_end() - .child(open_as_markdown) - .child(scroll_to_top) + .justify_end(); + + if AgentSettings::get_global(cx).enable_feedback { + let feedback = self.thread_feedback.feedback; + container = container.child( + div().visible_on_hover("thread-controls-container").child( + Label::new( + match feedback { + Some(ThreadFeedback::Positive) => "Thanks for your feedback!", + Some(ThreadFeedback::Negative) => "We appreciate your feedback and will use it to improve.", + None => "Rating the thread sends all of your current conversation to the Zed team.", + } + ) + .color(Color::Muted) + .size(LabelSize::XSmall) + .truncate(), + ), + ).child( + h_flex() + .child( + IconButton::new("feedback-thumbs-up", IconName::ThumbsUp) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) + .icon_color(match feedback { + Some(ThreadFeedback::Positive) => Color::Accent, + _ => Color::Ignored, + }) + .tooltip(Tooltip::text("Helpful Response")) + .on_click(cx.listener(move |this, _, window, cx| { + this.handle_feedback_click( + ThreadFeedback::Positive, + window, + cx, + ); + })), + ) + .child( + IconButton::new("feedback-thumbs-down", IconName::ThumbsDown) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) + .icon_color(match feedback { + Some(ThreadFeedback::Negative) => Color::Accent, + _ => Color::Ignored, + }) + .tooltip(Tooltip::text("Not Helpful")) + .on_click(cx.listener(move |this, _, window, cx| { + this.handle_feedback_click( + ThreadFeedback::Negative, + window, + cx, + ); + })), + ) + ) + } + + container.child(open_as_markdown).child(scroll_to_top) + } + + fn render_feedback_feedback_editor( + editor: Entity<Editor>, + window: &mut Window, + cx: &Context<Self>, + ) -> Div { + let focus_handle = editor.focus_handle(cx); + v_flex() + .key_context("AgentFeedbackMessageEditor") + .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| { + this.thread_feedback.dismiss_comments(); + cx.notify(); + })) + .on_action(cx.listener(move |this, _: &menu::Confirm, _window, cx| { + this.submit_feedback_message(cx); + })) + .mb_2() + .mx_4() + .p_2() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().editor_background) + .child(editor) + .child( + h_flex() + .gap_1() + .justify_end() + .child( + Button::new("dismiss-feedback-message", "Cancel") + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in(&menu::Cancel, &focus_handle, window, cx) + .map(|kb| kb.size(rems_from_px(10.))), + ) + .on_click(cx.listener(move |this, _, _window, cx| { + this.thread_feedback.dismiss_comments(); + cx.notify(); + })), + ) + .child( + Button::new("submit-feedback-message", "Share Feedback") + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in( + &menu::Confirm, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(10.))), + ) + .on_click(cx.listener(move |this, _, _window, cx| { + this.submit_feedback_message(cx); + })), + ), + ) + } + + fn handle_feedback_click( + &mut self, + feedback: ThreadFeedback, + window: &mut Window, + cx: &mut Context<Self>, + ) { + let Some(thread) = self.thread().cloned() else { + return; + }; + + self.thread_feedback.submit(thread, feedback, window, cx); + cx.notify(); + } + + fn submit_feedback_message(&mut self, cx: &mut Context<Self>) { + let Some(thread) = self.thread().cloned() else { + return; + }; + + self.thread_feedback.submit_comments(thread, cx); + cx.notify(); } fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> { diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index e214986b82..2cad913295 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -2349,7 +2349,6 @@ impl ActiveThread { this.submit_feedback_message(message_id, cx); cx.notify(); })) - .on_action(cx.listener(Self::confirm_editing_message)) .mb_2() .mx_4() .p_2() From 41e28a71855c9e5595d3764423e56517e5315931 Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Wed, 20 Aug 2025 14:01:18 -0400 Subject: [PATCH 537/693] Add tracked buffers for agent2 mentions (#36608) Release Notes: - N/A --- crates/agent_ui/src/acp/message_editor.rs | 151 ++++++++++++++-------- crates/agent_ui/src/acp/thread_view.rs | 13 +- 2 files changed, 107 insertions(+), 57 deletions(-) diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index a50e33dc31..ccd33c9247 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -34,7 +34,7 @@ use settings::Settings; use std::{ cell::Cell, ffi::OsStr, - fmt::{Display, Write}, + fmt::Write, ops::Range, path::{Path, PathBuf}, rc::Rc, @@ -391,30 +391,33 @@ impl MessageEditor { let rope = buffer .read_with(cx, |buffer, _cx| buffer.as_rope().clone()) .log_err()?; - Some(rope) + Some((rope, buffer)) }); cx.background_spawn(async move { - let rope = rope_task.await?; - Some((rel_path, full_path, rope.to_string())) + let (rope, buffer) = rope_task.await?; + Some((rel_path, full_path, rope.to_string(), buffer)) }) })) })?; let contents = cx .background_spawn(async move { - let contents = descendants_future.await.into_iter().flatten(); - contents.collect() + let (contents, tracked_buffers) = descendants_future + .await + .into_iter() + .flatten() + .map(|(rel_path, full_path, rope, buffer)| { + ((rel_path, full_path, rope), buffer) + }) + .unzip(); + (render_directory_contents(contents), tracked_buffers) }) .await; anyhow::Ok(contents) }); let task = cx - .spawn(async move |_, _| { - task.await - .map(|contents| DirectoryContents(contents).to_string()) - .map_err(|e| e.to_string()) - }) + .spawn(async move |_, _| task.await.map_err(|e| e.to_string())) .shared(); self.mention_set @@ -663,7 +666,7 @@ impl MessageEditor { &self, window: &mut Window, cx: &mut Context<Self>, - ) -> Task<Result<Vec<acp::ContentBlock>>> { + ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> { let contents = self.mention_set .contents(&self.project, self.prompt_store.as_ref(), window, cx); @@ -672,6 +675,7 @@ impl MessageEditor { cx.spawn(async move |_, cx| { let contents = contents.await?; + let mut all_tracked_buffers = Vec::new(); editor.update(cx, |editor, cx| { let mut ix = 0; @@ -702,7 +706,12 @@ impl MessageEditor { chunks.push(chunk); } let chunk = match mention { - Mention::Text { uri, content } => { + Mention::Text { + uri, + content, + tracked_buffers, + } => { + all_tracked_buffers.extend(tracked_buffers.iter().cloned()); acp::ContentBlock::Resource(acp::EmbeddedResource { annotations: None, resource: acp::EmbeddedResourceResource::TextResourceContents( @@ -745,7 +754,7 @@ impl MessageEditor { } }); - chunks + (chunks, all_tracked_buffers) }) }) } @@ -1043,7 +1052,7 @@ impl MessageEditor { .add_fetch_result(url, Task::ready(Ok(text)).shared()); } MentionUri::Directory { abs_path } => { - let task = Task::ready(Ok(text)).shared(); + let task = Task::ready(Ok((text, Vec::new()))).shared(); self.mention_set.directories.insert(abs_path, task); } MentionUri::File { .. } @@ -1153,16 +1162,13 @@ impl MessageEditor { } } -struct DirectoryContents(Arc<[(Arc<Path>, PathBuf, String)]>); - -impl Display for DirectoryContents { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for (_relative_path, full_path, content) in self.0.iter() { - let fence = codeblock_fence_for_path(Some(full_path), None); - write!(f, "\n{fence}\n{content}\n```")?; - } - Ok(()) +fn render_directory_contents(entries: Vec<(Arc<Path>, PathBuf, String)>) -> String { + let mut output = String::new(); + for (_relative_path, full_path, content) in entries { + let fence = codeblock_fence_for_path(Some(&full_path), None); + write!(output, "\n{fence}\n{content}\n```").unwrap(); } + output } impl Focusable for MessageEditor { @@ -1328,7 +1334,11 @@ impl Render for ImageHover { #[derive(Debug, Eq, PartialEq)] pub enum Mention { - Text { uri: MentionUri, content: String }, + Text { + uri: MentionUri, + content: String, + tracked_buffers: Vec<Entity<Buffer>>, + }, Image(MentionImage), } @@ -1346,7 +1356,7 @@ pub struct MentionSet { images: HashMap<CreaseId, Shared<Task<Result<MentionImage, String>>>>, thread_summaries: HashMap<acp::SessionId, Shared<Task<Result<SharedString, String>>>>, text_thread_summaries: HashMap<PathBuf, Shared<Task<Result<String, String>>>>, - directories: HashMap<PathBuf, Shared<Task<Result<String, String>>>>, + directories: HashMap<PathBuf, Shared<Task<Result<(String, Vec<Entity<Buffer>>), String>>>>, } impl MentionSet { @@ -1382,6 +1392,7 @@ impl MentionSet { self.fetch_results.clear(); self.thread_summaries.clear(); self.text_thread_summaries.clear(); + self.directories.clear(); self.uri_by_crease_id .drain() .map(|(id, _)| id) @@ -1424,7 +1435,14 @@ impl MentionSet { let buffer = buffer_task?.await?; let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; - anyhow::Ok((crease_id, Mention::Text { uri, content })) + anyhow::Ok(( + crease_id, + Mention::Text { + uri, + content, + tracked_buffers: vec![buffer], + }, + )) }) } MentionUri::Directory { abs_path } => { @@ -1433,11 +1451,14 @@ impl MentionSet { }; let uri = uri.clone(); cx.spawn(async move |_| { + let (content, tracked_buffers) = + content.await.map_err(|e| anyhow::anyhow!("{e}"))?; Ok(( crease_id, Mention::Text { uri, - content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, + content, + tracked_buffers, }, )) }) @@ -1473,7 +1494,14 @@ impl MentionSet { .collect() })?; - anyhow::Ok((crease_id, Mention::Text { uri, content })) + anyhow::Ok(( + crease_id, + Mention::Text { + uri, + content, + tracked_buffers: vec![buffer], + }, + )) }) } MentionUri::Thread { id, .. } => { @@ -1490,6 +1518,7 @@ impl MentionSet { .await .map_err(|e| anyhow::anyhow!("{e}"))? .to_string(), + tracked_buffers: Vec::new(), }, )) }) @@ -1505,6 +1534,7 @@ impl MentionSet { Mention::Text { uri, content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, + tracked_buffers: Vec::new(), }, )) }) @@ -1518,7 +1548,14 @@ impl MentionSet { cx.spawn(async move |_| { // TODO: report load errors instead of just logging let text = text_task.await?; - anyhow::Ok((crease_id, Mention::Text { uri, content: text })) + anyhow::Ok(( + crease_id, + Mention::Text { + uri, + content: text, + tracked_buffers: Vec::new(), + }, + )) }) } MentionUri::Fetch { url } => { @@ -1532,6 +1569,7 @@ impl MentionSet { Mention::Text { uri, content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, + tracked_buffers: Vec::new(), }, )) }) @@ -1703,6 +1741,7 @@ impl Addon for MessageEditorAddon { mod tests { use std::{ops::Range, path::Path, sync::Arc}; + use acp_thread::MentionUri; use agent_client_protocol as acp; use agent2::HistoryStore; use assistant_context::ContextStore; @@ -1815,7 +1854,7 @@ mod tests { editor.backspace(&Default::default(), window, cx); }); - let content = message_editor + let (content, _) = message_editor .update_in(cx, |message_editor, window, cx| { message_editor.contents(window, cx) }) @@ -2046,13 +2085,13 @@ mod tests { .into_values() .collect::<Vec<_>>(); - pretty_assertions::assert_eq!( - contents, - [Mention::Text { - content: "1".into(), - uri: url_one.parse().unwrap() - }] - ); + { + let [Mention::Text { content, uri, .. }] = contents.as_slice() else { + panic!("Unexpected mentions"); + }; + pretty_assertions::assert_eq!(content, "1"); + pretty_assertions::assert_eq!(uri, &url_one.parse::<MentionUri>().unwrap()); + } cx.simulate_input(" "); @@ -2098,15 +2137,15 @@ mod tests { .into_values() .collect::<Vec<_>>(); - assert_eq!(contents.len(), 2); let url_eight = uri!("file:///dir/b/eight.txt"); - pretty_assertions::assert_eq!( - contents[1], - Mention::Text { - content: "8".to_string(), - uri: url_eight.parse().unwrap(), - } - ); + + { + let [_, Mention::Text { content, uri, .. }] = contents.as_slice() else { + panic!("Unexpected mentions"); + }; + pretty_assertions::assert_eq!(content, "8"); + pretty_assertions::assert_eq!(uri, &url_eight.parse::<MentionUri>().unwrap()); + } editor.update(&mut cx, |editor, cx| { assert_eq!( @@ -2208,14 +2247,18 @@ mod tests { .into_values() .collect::<Vec<_>>(); - assert_eq!(contents.len(), 3); - pretty_assertions::assert_eq!( - contents[2], - Mention::Text { - content: "1".into(), - uri: format!("{url_one}?symbol=MySymbol#L1:1").parse().unwrap(), - } - ); + { + let [_, _, Mention::Text { content, uri, .. }] = contents.as_slice() else { + panic!("Unexpected mentions"); + }; + pretty_assertions::assert_eq!(content, "1"); + pretty_assertions::assert_eq!( + uri, + &format!("{url_one}?symbol=MySymbol#L1:1") + .parse::<MentionUri>() + .unwrap() + ); + } cx.run_until_parked(); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 4ce55cce56..14f9cacd15 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -739,7 +739,7 @@ impl AcpThreadView { fn send_impl( &mut self, - contents: Task<anyhow::Result<Vec<acp::ContentBlock>>>, + contents: Task<anyhow::Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>>, window: &mut Window, cx: &mut Context<Self>, ) { @@ -751,7 +751,7 @@ impl AcpThreadView { return; }; let task = cx.spawn_in(window, async move |this, cx| { - let contents = contents.await?; + let (contents, tracked_buffers) = contents.await?; if contents.is_empty() { return Ok(()); @@ -764,7 +764,14 @@ impl AcpThreadView { message_editor.clear(window, cx); }); })?; - let send = thread.update(cx, |thread, cx| thread.send(contents, cx))?; + let send = thread.update(cx, |thread, cx| { + thread.action_log().update(cx, |action_log, cx| { + for buffer in tracked_buffers { + action_log.buffer_read(buffer, cx) + } + }); + thread.send(contents, cx) + })?; send.await }); From ec8106d1dbe8937a0b0cf7c9250b1491c22c1338 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Wed, 20 Aug 2025 23:44:30 +0530 Subject: [PATCH 538/693] Fix `clippy::println_empty_string`, `clippy::while_let_on_iterator`, `clippy::while_let_on_iterator` lint style violations (#36613) Related: #36577 Release Notes: - N/A --- Cargo.toml | 3 +++ crates/agent/src/context.rs | 6 +++--- crates/agent2/src/thread.rs | 2 +- crates/buffer_diff/src/buffer_diff.rs | 4 ++-- crates/editor/src/display_map/block_map.rs | 4 ++-- crates/editor/src/editor.rs | 4 ++-- crates/editor/src/indent_guides.rs | 4 ++-- crates/editor/src/items.rs | 4 ++-- crates/eval/src/eval.rs | 2 +- crates/eval/src/instance.rs | 4 ++-- crates/git/src/repository.rs | 2 +- crates/language/src/text_diff.rs | 2 +- crates/multi_buffer/src/multi_buffer_tests.rs | 4 ++-- crates/project/src/git_store/git_traversal.rs | 4 ++-- crates/project/src/lsp_store.rs | 4 ++-- crates/settings/src/settings_json.rs | 2 +- crates/tab_switcher/src/tab_switcher.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 4 ++-- crates/vim/src/command.rs | 2 +- crates/vim/src/normal/increment.rs | 4 ++-- 20 files changed, 35 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a2de4aaaed..6218e8dbb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -904,6 +904,7 @@ ok_expect = "warn" owned_cow = "warn" print_literal = "warn" print_with_newline = "warn" +println_empty_string = "warn" ptr_eq = "warn" question_mark = "warn" redundant_closure = "warn" @@ -924,7 +925,9 @@ unneeded_struct_pattern = "warn" unsafe_removed_from_name = "warn" unused_unit = "warn" unusual_byte_groupings = "warn" +while_let_on_iterator = "warn" write_literal = "warn" +write_with_newline = "warn" writeln_empty_string = "warn" wrong_self_convention = "warn" zero_ptr = "warn" diff --git a/crates/agent/src/context.rs b/crates/agent/src/context.rs index 9bb8fc0eae..a94a933d86 100644 --- a/crates/agent/src/context.rs +++ b/crates/agent/src/context.rs @@ -362,7 +362,7 @@ impl Display for DirectoryContext { let mut is_first = true; for descendant in &self.descendants { if !is_first { - write!(f, "\n")?; + writeln!(f)?; } else { is_first = false; } @@ -650,7 +650,7 @@ impl TextThreadContextHandle { impl Display for TextThreadContext { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { // TODO: escape title? - write!(f, "<text_thread title=\"{}\">\n", self.title)?; + writeln!(f, "<text_thread title=\"{}\">", self.title)?; write!(f, "{}", self.text.trim())?; write!(f, "\n</text_thread>") } @@ -716,7 +716,7 @@ impl RulesContextHandle { impl Display for RulesContext { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Some(title) = &self.title { - write!(f, "Rules title: {}\n", title)?; + writeln!(f, "Rules title: {}", title)?; } let code_block = MarkdownCodeBlock { tag: "", diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index f407ee7de5..01c9ab03ba 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -163,7 +163,7 @@ impl UserMessage { if !content.is_empty() { let _ = write!(&mut markdown, "{}\n\n{}\n", uri.as_link(), content); } else { - let _ = write!(&mut markdown, "{}\n", uri.as_link()); + let _ = writeln!(&mut markdown, "{}", uri.as_link()); } } } diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index bef0c5cfc3..10b59d0ba2 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -2024,8 +2024,8 @@ mod tests { fn gen_working_copy(rng: &mut StdRng, head: &str) -> String { let mut old_lines = { let mut old_lines = Vec::new(); - let mut old_lines_iter = head.lines(); - while let Some(line) = old_lines_iter.next() { + let old_lines_iter = head.lines(); + for line in old_lines_iter { assert!(!line.ends_with("\n")); old_lines.push(line.to_owned()); } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 1e0cdc34ac..e32a4e45db 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -3183,9 +3183,9 @@ mod tests { // so we special case row 0 to assume a leading '\n'. // // Linehood is the birthright of strings. - let mut input_text_lines = input_text.split('\n').enumerate().peekable(); + let input_text_lines = input_text.split('\n').enumerate().peekable(); let mut block_row = 0; - while let Some((wrap_row, input_line)) = input_text_lines.next() { + for (wrap_row, input_line) in input_text_lines { let wrap_row = wrap_row as u32; let multibuffer_row = wraps_snapshot .to_point(WrapPoint::new(wrap_row, 0), Bias::Left) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2136d5f4b3..45a90b843b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -11021,7 +11021,7 @@ impl Editor { let mut col = 0; let mut changed = false; - while let Some(ch) = chars.next() { + for ch in chars.by_ref() { match ch { ' ' => { reindented_line.push(' '); @@ -11077,7 +11077,7 @@ impl Editor { let mut first_non_indent_char = None; let mut changed = false; - while let Some(ch) = chars.next() { + for ch in chars.by_ref() { match ch { ' ' => { // Keep track of spaces. Append \t when we reach tab_size diff --git a/crates/editor/src/indent_guides.rs b/crates/editor/src/indent_guides.rs index a1de2b604b..23717eeb15 100644 --- a/crates/editor/src/indent_guides.rs +++ b/crates/editor/src/indent_guides.rs @@ -164,8 +164,8 @@ pub fn indent_guides_in_range( let end_anchor = snapshot.buffer_snapshot.anchor_after(end_offset); let mut fold_ranges = Vec::<Range<Point>>::new(); - let mut folds = snapshot.folds_in_range(start_offset..end_offset).peekable(); - while let Some(fold) = folds.next() { + let folds = snapshot.folds_in_range(start_offset..end_offset).peekable(); + for fold in folds { let start = fold.range.start.to_point(&snapshot.buffer_snapshot); let end = fold.range.end.to_point(&snapshot.buffer_snapshot); if let Some(last_range) = fold_ranges.last_mut() diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 62889c638f..afc5767de0 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -103,9 +103,9 @@ impl FollowableItem for Editor { multibuffer = MultiBuffer::new(project.read(cx).capability()); let mut sorted_excerpts = state.excerpts.clone(); sorted_excerpts.sort_by_key(|e| e.id); - let mut sorted_excerpts = sorted_excerpts.into_iter().peekable(); + let sorted_excerpts = sorted_excerpts.into_iter().peekable(); - while let Some(excerpt) = sorted_excerpts.next() { + for excerpt in sorted_excerpts { let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else { continue; }; diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index c5a072eea1..9e0504abca 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -706,7 +706,7 @@ fn print_report( println!("Average thread score: {average_thread_score}%"); } - println!(""); + println!(); print_h2("CUMULATIVE TOOL METRICS"); println!("{}", cumulative_tool_metrics); diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs index 074cb121d3..c6e4e0b6ec 100644 --- a/crates/eval/src/instance.rs +++ b/crates/eval/src/instance.rs @@ -913,9 +913,9 @@ impl RequestMarkdown { for tool in &request.tools { write!(&mut tools, "# {}\n\n", tool.name).unwrap(); write!(&mut tools, "{}\n\n", tool.description).unwrap(); - write!( + writeln!( &mut tools, - "{}\n", + "{}", MarkdownCodeBlock { tag: "json", text: &format!("{:#}", tool.input_schema) diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 9c125d2c47..fd12dafa98 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -916,7 +916,7 @@ impl GitRepository for RealGitRepository { .context("no stdin for git cat-file subprocess")?; let mut stdin = BufWriter::new(stdin); for rev in &revs { - write!(&mut stdin, "{rev}\n")?; + writeln!(&mut stdin, "{rev}")?; } stdin.flush()?; drop(stdin); diff --git a/crates/language/src/text_diff.rs b/crates/language/src/text_diff.rs index cb2242a6b1..11d8a070d2 100644 --- a/crates/language/src/text_diff.rs +++ b/crates/language/src/text_diff.rs @@ -186,7 +186,7 @@ fn tokenize(text: &str, language_scope: Option<LanguageScope>) -> impl Iterator< let mut prev = None; let mut start_ix = 0; iter::from_fn(move || { - while let Some((ix, c)) = chars.next() { + for (ix, c) in chars.by_ref() { let mut token = None; let kind = classifier.kind(c); if let Some((prev_char, prev_kind)) = prev diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index 598ee0f9cb..61b4b0520f 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -2250,11 +2250,11 @@ impl ReferenceMultibuffer { let base_buffer = diff.base_text(); let mut offset = buffer_range.start; - let mut hunks = diff + let hunks = diff .hunks_intersecting_range(excerpt.range.clone(), buffer, cx) .peekable(); - while let Some(hunk) = hunks.next() { + for hunk in hunks { // Ignore hunks that are outside the excerpt range. let mut hunk_range = hunk.buffer_range.to_offset(buffer); diff --git a/crates/project/src/git_store/git_traversal.rs b/crates/project/src/git_store/git_traversal.rs index 9eadaeac82..eee492e482 100644 --- a/crates/project/src/git_store/git_traversal.rs +++ b/crates/project/src/git_store/git_traversal.rs @@ -42,8 +42,8 @@ impl<'a> GitTraversal<'a> { // other_repo/ // .git/ // our_query.txt - let mut query = path.ancestors(); - while let Some(query) = query.next() { + let query = path.ancestors(); + for query in query { let (_, snapshot) = self .repo_root_to_snapshot .range(Path::new("")..=query) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 1b46117897..0b58009f37 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -13149,10 +13149,10 @@ fn ensure_uniform_list_compatible_label(label: &mut CodeLabel) { let mut offset_map = vec![0; label.text.len() + 1]; let mut last_char_was_space = false; let mut new_idx = 0; - let mut chars = label.text.char_indices().fuse(); + let chars = label.text.char_indices().fuse(); let mut newlines_removed = false; - while let Some((idx, c)) = chars.next() { + for (idx, c) in chars { offset_map[idx] = new_idx; match c { diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs index 8080ec8d5f..f112ec811d 100644 --- a/crates/settings/src/settings_json.rs +++ b/crates/settings/src/settings_json.rs @@ -209,7 +209,7 @@ fn replace_value_in_json_text( if ch == ',' { removal_end = existing_value_range.end + offset + 1; // Also consume whitespace after the comma - while let Some((_, next_ch)) = chars.next() { + for (_, next_ch) in chars.by_ref() { if next_ch.is_whitespace() { removal_end += next_ch.len_utf8(); } else { diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index 12af124ec7..655b8a2e8f 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -307,7 +307,7 @@ impl TabSwitcherDelegate { (Reverse(history.get(&item.item.item_id())), item.item_index) ) } - eprintln!(""); + eprintln!(); all_items .sort_by_key(|tab| (Reverse(history.get(&tab.item.item_id())), tab.item_index)); all_items diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 0c16e3fb9d..5b4d327140 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1397,8 +1397,8 @@ fn possible_open_target( let found_entry = worktree .update(cx, |worktree, _| { let worktree_root = worktree.abs_path(); - let mut traversal = worktree.traverse_from_path(true, true, false, "".as_ref()); - while let Some(entry) = traversal.next() { + let traversal = worktree.traverse_from_path(true, true, false, "".as_ref()); + for entry in traversal { if let Some(path_in_worktree) = worktree_paths_to_check .iter() .find(|path_to_check| entry.path.ends_with(&path_to_check.path)) diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 79d18a85e9..b57c916db9 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1492,7 +1492,7 @@ impl OnMatchingLines { let mut search = String::new(); let mut escaped = false; - while let Some(c) = chars.next() { + for c in chars.by_ref() { if escaped { escaped = false; // unescape escaped parens diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index 115aef1dab..1d2a4e9b61 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -274,9 +274,9 @@ fn find_boolean(snapshot: &MultiBufferSnapshot, start: Point) -> Option<(Range<P let mut end = None; let mut word = String::new(); - let mut chars = snapshot.chars_at(offset); + let chars = snapshot.chars_at(offset); - while let Some(ch) = chars.next() { + for ch in chars { if ch.is_ascii_alphabetic() { if begin.is_none() { begin = Some(offset); From b6722ca3c8de3921f150e83294e84fbc9bdb9016 Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Wed, 20 Aug 2025 14:43:29 -0400 Subject: [PATCH 539/693] Remove special case for singleton buffers from `MultiBufferSnapshot::anchor_at` (#36524) This may be responsible for a panic that we've been seeing with increased frequency in agent2 threads. Release Notes: - N/A Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com> --- crates/editor/src/editor.rs | 8 +-- crates/multi_buffer/src/multi_buffer.rs | 66 +++++++++++++++---------- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 45a90b843b..25fddf5cf1 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4876,11 +4876,7 @@ impl Editor { cx: &mut Context<Self>, ) -> bool { let position = self.selections.newest_anchor().head(); - let multibuffer = self.buffer.read(cx); - let Some(buffer) = position - .buffer_id - .and_then(|buffer_id| multibuffer.buffer(buffer_id)) - else { + let Some(buffer) = self.buffer.read(cx).buffer_for_anchor(position, cx) else { return false; }; @@ -5844,7 +5840,7 @@ impl Editor { multibuffer_anchor.start.to_offset(&snapshot) ..multibuffer_anchor.end.to_offset(&snapshot) }; - if newest_anchor.head().buffer_id != Some(buffer.remote_id()) { + if snapshot.buffer_id_for_anchor(newest_anchor.head()) != Some(buffer.remote_id()) { return None; } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 60e9c14c34..f73014a6ff 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -2196,6 +2196,15 @@ impl MultiBuffer { }) } + pub fn buffer_for_anchor(&self, anchor: Anchor, cx: &App) -> Option<Entity<Buffer>> { + if let Some(buffer_id) = anchor.buffer_id { + self.buffer(buffer_id) + } else { + let (_, buffer, _) = self.excerpt_containing(anchor, cx)?; + Some(buffer) + } + } + // If point is at the end of the buffer, the last excerpt is returned pub fn point_to_buffer_offset<T: ToOffset>( &self, @@ -5228,15 +5237,6 @@ impl MultiBufferSnapshot { excerpt_offset += ExcerptOffset::new(offset_in_transform); }; - if let Some((excerpt_id, buffer_id, buffer)) = self.as_singleton() { - return Anchor { - buffer_id: Some(buffer_id), - excerpt_id: *excerpt_id, - text_anchor: buffer.anchor_at(excerpt_offset.value, bias), - diff_base_anchor, - }; - } - let mut excerpts = self .excerpts .cursor::<Dimensions<ExcerptOffset, Option<ExcerptId>>>(&()); @@ -5260,10 +5260,17 @@ impl MultiBufferSnapshot { text_anchor, diff_base_anchor, } - } else if excerpt_offset.is_zero() && bias == Bias::Left { - Anchor::min() } else { - Anchor::max() + let mut anchor = if excerpt_offset.is_zero() && bias == Bias::Left { + Anchor::min() + } else { + Anchor::max() + }; + // TODO this is a hack, remove it + if let Some((excerpt_id, _, _)) = self.as_singleton() { + anchor.excerpt_id = *excerpt_id; + } + anchor } } @@ -6305,6 +6312,14 @@ impl MultiBufferSnapshot { }) } + pub fn buffer_id_for_anchor(&self, anchor: Anchor) -> Option<BufferId> { + if let Some(id) = anchor.buffer_id { + return Some(id); + } + let excerpt = self.excerpt_containing(anchor..anchor)?; + Some(excerpt.buffer_id()) + } + pub fn selections_in_range<'a>( &'a self, range: &'a Range<Anchor>, @@ -6983,19 +6998,20 @@ impl Excerpt { } fn contains(&self, anchor: &Anchor) -> bool { - Some(self.buffer_id) == anchor.buffer_id - && self - .range - .context - .start - .cmp(&anchor.text_anchor, &self.buffer) - .is_le() - && self - .range - .context - .end - .cmp(&anchor.text_anchor, &self.buffer) - .is_ge() + anchor.buffer_id == None + || anchor.buffer_id == Some(self.buffer_id) + && self + .range + .context + .start + .cmp(&anchor.text_anchor, &self.buffer) + .is_le() + && self + .range + .context + .end + .cmp(&anchor.text_anchor, &self.buffer) + .is_ge() } /// The [`Excerpt`]'s start offset in its [`Buffer`] From 74ce543d8b16c33fb418db668ae403909eed4c2e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 20 Aug 2025 20:45:40 +0200 Subject: [PATCH 540/693] clippy: println_empty_string & non_minimal_cfg (#36614) - **clippy: Fix println-empty-string** - **clippy: non-minimal-cfg** Related to #36577 Release Notes: - N/A --- Cargo.toml | 1 + crates/agent2/src/thread.rs | 2 +- crates/gpui/src/taffy.rs | 1 - .../tests/derive_inspector_reflection.rs | 14 +------------- crates/tab_switcher/src/tab_switcher.rs | 1 - 5 files changed, 3 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6218e8dbb9..dcf07b7079 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -900,6 +900,7 @@ needless_parens_on_range_literals = "warn" needless_pub_self = "warn" needless_return = "warn" needless_return_with_question_mark = "warn" +non_minimal_cfg = "warn" ok_expect = "warn" owned_cow = "warn" print_literal = "warn" diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 01c9ab03ba..62174fd3b4 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -161,7 +161,7 @@ impl UserMessage { } UserMessageContent::Mention { uri, content } => { if !content.is_empty() { - let _ = write!(&mut markdown, "{}\n\n{}\n", uri.as_link(), content); + let _ = writeln!(&mut markdown, "{}\n\n{}", uri.as_link(), content); } else { let _ = writeln!(&mut markdown, "{}", uri.as_link()); } diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index f198bb7718..58386ad1f5 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -164,7 +164,6 @@ impl TaffyLayoutEngine { // for (a, b) in self.get_edges(id)? { // println!("N{} --> N{}", u64::from(a), u64::from(b)); // } - // println!(""); // if !self.computed_layouts.insert(id) { diff --git a/crates/gpui_macros/tests/derive_inspector_reflection.rs b/crates/gpui_macros/tests/derive_inspector_reflection.rs index aab44a70ce..a0adcb7801 100644 --- a/crates/gpui_macros/tests/derive_inspector_reflection.rs +++ b/crates/gpui_macros/tests/derive_inspector_reflection.rs @@ -34,13 +34,6 @@ trait Transform: Clone { /// Adds one to the value fn add_one(self) -> Self; - - /// cfg attributes are respected - #[cfg(all())] - fn cfg_included(self) -> Self; - - #[cfg(any())] - fn cfg_omitted(self) -> Self; } #[derive(Debug, Clone, PartialEq)] @@ -70,10 +63,6 @@ impl Transform for Number { fn add_one(self) -> Self { Number(self.0 + 1) } - - fn cfg_included(self) -> Self { - Number(self.0) - } } #[test] @@ -83,14 +72,13 @@ fn test_derive_inspector_reflection() { // Get all methods that match the pattern fn(self) -> Self or fn(mut self) -> Self let methods = methods::<Number>(); - assert_eq!(methods.len(), 6); + assert_eq!(methods.len(), 5); let method_names: Vec<_> = methods.iter().map(|m| m.name).collect(); assert!(method_names.contains(&"double")); assert!(method_names.contains(&"triple")); assert!(method_names.contains(&"increment")); assert!(method_names.contains(&"quadruple")); assert!(method_names.contains(&"add_one")); - assert!(method_names.contains(&"cfg_included")); // Invoke methods by name let num = Number(5); diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index 655b8a2e8f..11e32523b4 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -307,7 +307,6 @@ impl TabSwitcherDelegate { (Reverse(history.get(&item.item.item_id())), item.item_index) ) } - eprintln!(); all_items .sort_by_key(|tab| (Reverse(history.get(&tab.item.item_id())), tab.item_index)); all_items From 2813073d7b642bc40c6a2f4188dec8445f9688ae Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Wed, 20 Aug 2025 16:04:10 -0300 Subject: [PATCH 541/693] message editor: Only allow types of content the agent can handle (#36616) Uses the new [`acp::PromptCapabilities`](https://github.com/zed-industries/agent-client-protocol/blob/a39b7f635d67528f0a4e05e086ab283b9fc5cb93/rust/agent.rs#L194-L215) to disable non-file mentions and images for agents that don't support them. Release Notes: - N/A --- Cargo.lock | 4 +- Cargo.toml | 2 +- crates/acp_thread/src/acp_thread.rs | 8 ++ crates/acp_thread/src/connection.rs | 10 ++ crates/agent2/src/agent.rs | 8 ++ crates/agent_servers/src/acp/v0.rs | 8 ++ crates/agent_servers/src/acp/v1.rs | 6 + crates/agent_servers/src/claude.rs | 8 ++ .../agent_ui/src/acp/completion_provider.rs | 122 ++++++++++++------ crates/agent_ui/src/acp/message_editor.rs | 93 +++++++++++-- crates/agent_ui/src/acp/thread_view.rs | 13 ++ 11 files changed, 233 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 342bb1058f..70b8f630f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,9 +171,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.26" +version = "0.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "160971bb53ca0b2e70ebc857c21e24eb448745f1396371015f4c59e9a9e51ed0" +checksum = "4c887e795097665ab95119580534e7cc1335b59e1a7fec296943e534b970f4ed" dependencies = [ "anyhow", "futures 0.3.31", diff --git a/Cargo.toml b/Cargo.toml index dcf07b7079..436d4a7f5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -423,7 +423,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.26" +agent-client-protocol = "0.0.28" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index a1f9b32eba..9833e1957c 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -2598,6 +2598,14 @@ mod tests { } } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + } + } + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { let sessions = self.sessions.lock(); let thread = sessions.get(session_id).unwrap().clone(); diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index dc1a41c81e..791b161417 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -38,6 +38,8 @@ pub trait AgentConnection { cx: &mut App, ) -> Task<Result<acp::PromptResponse>>; + fn prompt_capabilities(&self) -> acp::PromptCapabilities; + fn resume( &self, _session_id: &acp::SessionId, @@ -334,6 +336,14 @@ mod test_support { Task::ready(Ok(thread)) } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + } + } + fn authenticate( &self, _method_id: acp::AuthMethodId, diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 2f5f15399e..c15048ad8c 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -913,6 +913,14 @@ impl acp_thread::AgentConnection for NativeAgentConnection { }) } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: true, + audio: false, + embedded_context: true, + } + } + fn resume( &self, session_id: &acp::SessionId, diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs index 30643dd005..be96048929 100644 --- a/crates/agent_servers/src/acp/v0.rs +++ b/crates/agent_servers/src/acp/v0.rs @@ -498,6 +498,14 @@ impl AgentConnection for AcpConnection { }) } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: false, + audio: false, + embedded_context: false, + } + } + fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) { let task = self .connection diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index e0e92f29ba..2e70a5f37a 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -21,6 +21,7 @@ pub struct AcpConnection { connection: Rc<acp::ClientSideConnection>, sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>, auth_methods: Vec<acp::AuthMethod>, + prompt_capabilities: acp::PromptCapabilities, _io_task: Task<Result<()>>, } @@ -119,6 +120,7 @@ impl AcpConnection { connection: connection.into(), server_name, sessions, + prompt_capabilities: response.agent_capabilities.prompt_capabilities, _io_task: io_task, }) } @@ -206,6 +208,10 @@ impl AgentConnection for AcpConnection { }) } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + self.prompt_capabilities + } + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { let conn = self.connection.clone(); let params = acp::CancelNotification { diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 6b9732b468..8d93557e1c 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -319,6 +319,14 @@ impl AgentConnection for ClaudeAgentConnection { cx.foreground_executor().spawn(async move { end_rx.await? }) } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: true, + audio: false, + embedded_context: true, + } + } + fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) { let sessions = self.sessions.borrow(); let Some(session) = sessions.get(session_id) else { diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index d90520d26a..bf0a3f7a5a 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -1,8 +1,11 @@ +use std::cell::Cell; use std::ops::Range; +use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::AtomicBool; use acp_thread::MentionUri; +use agent_client_protocol as acp; use agent2::{HistoryEntry, HistoryStore}; use anyhow::Result; use editor::{CompletionProvider, Editor, ExcerptId}; @@ -63,6 +66,7 @@ pub struct ContextPickerCompletionProvider { workspace: WeakEntity<Workspace>, history_store: Entity<HistoryStore>, prompt_store: Option<Entity<PromptStore>>, + prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>, } impl ContextPickerCompletionProvider { @@ -71,12 +75,14 @@ impl ContextPickerCompletionProvider { workspace: WeakEntity<Workspace>, history_store: Entity<HistoryStore>, prompt_store: Option<Entity<PromptStore>>, + prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>, ) -> Self { Self { message_editor, workspace, history_store, prompt_store, + prompt_capabilities, } } @@ -544,17 +550,19 @@ impl ContextPickerCompletionProvider { }), ); - const RECENT_COUNT: usize = 2; - let threads = self - .history_store - .read(cx) - .recently_opened_entries(cx) - .into_iter() - .filter(|thread| !mentions.contains(&thread.mention_uri())) - .take(RECENT_COUNT) - .collect::<Vec<_>>(); + if self.prompt_capabilities.get().embedded_context { + const RECENT_COUNT: usize = 2; + let threads = self + .history_store + .read(cx) + .recently_opened_entries(cx) + .into_iter() + .filter(|thread| !mentions.contains(&thread.mention_uri())) + .take(RECENT_COUNT) + .collect::<Vec<_>>(); - recent.extend(threads.into_iter().map(Match::RecentThread)); + recent.extend(threads.into_iter().map(Match::RecentThread)); + } recent } @@ -564,11 +572,17 @@ impl ContextPickerCompletionProvider { workspace: &Entity<Workspace>, cx: &mut App, ) -> Vec<ContextPickerEntry> { - let mut entries = vec![ - ContextPickerEntry::Mode(ContextPickerMode::File), - ContextPickerEntry::Mode(ContextPickerMode::Symbol), - ContextPickerEntry::Mode(ContextPickerMode::Thread), - ]; + let embedded_context = self.prompt_capabilities.get().embedded_context; + let mut entries = if embedded_context { + vec![ + ContextPickerEntry::Mode(ContextPickerMode::File), + ContextPickerEntry::Mode(ContextPickerMode::Symbol), + ContextPickerEntry::Mode(ContextPickerMode::Thread), + ] + } else { + // File is always available, but we don't need a mode entry + vec![] + }; let has_selection = workspace .read(cx) @@ -583,11 +597,13 @@ impl ContextPickerCompletionProvider { )); } - if self.prompt_store.is_some() { - entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules)); - } + if embedded_context { + if self.prompt_store.is_some() { + entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules)); + } - entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch)); + entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch)); + } entries } @@ -625,7 +641,11 @@ impl CompletionProvider for ContextPickerCompletionProvider { let offset_to_line = buffer.point_to_offset(line_start); let mut lines = buffer.text_for_range(line_start..position).lines(); let line = lines.next()?; - MentionCompletion::try_parse(line, offset_to_line) + MentionCompletion::try_parse( + self.prompt_capabilities.get().embedded_context, + line, + offset_to_line, + ) }); let Some(state) = state else { return Task::ready(Ok(Vec::new())); @@ -745,12 +765,16 @@ impl CompletionProvider for ContextPickerCompletionProvider { let offset_to_line = buffer.point_to_offset(line_start); let mut lines = buffer.text_for_range(line_start..position).lines(); if let Some(line) = lines.next() { - MentionCompletion::try_parse(line, offset_to_line) - .map(|completion| { - completion.source_range.start <= offset_to_line + position.column as usize - && completion.source_range.end >= offset_to_line + position.column as usize - }) - .unwrap_or(false) + MentionCompletion::try_parse( + self.prompt_capabilities.get().embedded_context, + line, + offset_to_line, + ) + .map(|completion| { + completion.source_range.start <= offset_to_line + position.column as usize + && completion.source_range.end >= offset_to_line + position.column as usize + }) + .unwrap_or(false) } else { false } @@ -841,7 +865,7 @@ struct MentionCompletion { } impl MentionCompletion { - fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> { + fn try_parse(allow_non_file_mentions: bool, line: &str, offset_to_line: usize) -> Option<Self> { let last_mention_start = line.rfind('@')?; if last_mention_start >= line.len() { return Some(Self::default()); @@ -865,7 +889,9 @@ impl MentionCompletion { if let Some(mode_text) = parts.next() { end += mode_text.len(); - if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok() { + if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok() + && (allow_non_file_mentions || matches!(parsed_mode, ContextPickerMode::File)) + { mode = Some(parsed_mode); } else { argument = Some(mode_text.to_string()); @@ -898,10 +924,10 @@ mod tests { #[test] fn test_mention_completion_parse() { - assert_eq!(MentionCompletion::try_parse("Lorem Ipsum", 0), None); + assert_eq!(MentionCompletion::try_parse(true, "Lorem Ipsum", 0), None); assert_eq!( - MentionCompletion::try_parse("Lorem @", 0), + MentionCompletion::try_parse(true, "Lorem @", 0), Some(MentionCompletion { source_range: 6..7, mode: None, @@ -910,7 +936,7 @@ mod tests { ); assert_eq!( - MentionCompletion::try_parse("Lorem @file", 0), + MentionCompletion::try_parse(true, "Lorem @file", 0), Some(MentionCompletion { source_range: 6..11, mode: Some(ContextPickerMode::File), @@ -919,7 +945,7 @@ mod tests { ); assert_eq!( - MentionCompletion::try_parse("Lorem @file ", 0), + MentionCompletion::try_parse(true, "Lorem @file ", 0), Some(MentionCompletion { source_range: 6..12, mode: Some(ContextPickerMode::File), @@ -928,7 +954,7 @@ mod tests { ); assert_eq!( - MentionCompletion::try_parse("Lorem @file main.rs", 0), + MentionCompletion::try_parse(true, "Lorem @file main.rs", 0), Some(MentionCompletion { source_range: 6..19, mode: Some(ContextPickerMode::File), @@ -937,7 +963,7 @@ mod tests { ); assert_eq!( - MentionCompletion::try_parse("Lorem @file main.rs ", 0), + MentionCompletion::try_parse(true, "Lorem @file main.rs ", 0), Some(MentionCompletion { source_range: 6..19, mode: Some(ContextPickerMode::File), @@ -946,7 +972,7 @@ mod tests { ); assert_eq!( - MentionCompletion::try_parse("Lorem @file main.rs Ipsum", 0), + MentionCompletion::try_parse(true, "Lorem @file main.rs Ipsum", 0), Some(MentionCompletion { source_range: 6..19, mode: Some(ContextPickerMode::File), @@ -955,7 +981,7 @@ mod tests { ); assert_eq!( - MentionCompletion::try_parse("Lorem @main", 0), + MentionCompletion::try_parse(true, "Lorem @main", 0), Some(MentionCompletion { source_range: 6..11, mode: None, @@ -963,6 +989,28 @@ mod tests { }) ); - assert_eq!(MentionCompletion::try_parse("test@", 0), None); + assert_eq!(MentionCompletion::try_parse(true, "test@", 0), None); + + // Allowed non-file mentions + + assert_eq!( + MentionCompletion::try_parse(true, "Lorem @symbol main", 0), + Some(MentionCompletion { + source_range: 6..18, + mode: Some(ContextPickerMode::Symbol), + argument: Some("main".to_string()), + }) + ); + + // Disallowed non-file mentions + + assert_eq!( + MentionCompletion::try_parse(false, "Lorem @symbol main", 0), + Some(MentionCompletion { + source_range: 6..18, + mode: None, + argument: Some("main".to_string()), + }) + ); } } diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index ccd33c9247..5eab1a4e2d 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -51,7 +51,10 @@ use ui::{ }; use url::Url; use util::ResultExt; -use workspace::{Workspace, notifications::NotifyResultExt as _}; +use workspace::{ + Toast, Workspace, + notifications::{NotificationId, NotifyResultExt as _}, +}; use zed_actions::agent::Chat; const PARSE_SLASH_COMMAND_DEBOUNCE: Duration = Duration::from_millis(50); @@ -64,6 +67,7 @@ pub struct MessageEditor { history_store: Entity<HistoryStore>, prompt_store: Option<Entity<PromptStore>>, prevent_slash_commands: bool, + prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>, _subscriptions: Vec<Subscription>, _parse_slash_command_task: Task<()>, } @@ -96,11 +100,13 @@ impl MessageEditor { }, None, ); + let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default())); let completion_provider = ContextPickerCompletionProvider::new( cx.weak_entity(), workspace.clone(), history_store.clone(), prompt_store.clone(), + prompt_capabilities.clone(), ); let semantics_provider = Rc::new(SlashCommandSemanticsProvider { range: Cell::new(None), @@ -158,6 +164,7 @@ impl MessageEditor { history_store, prompt_store, prevent_slash_commands, + prompt_capabilities, _subscriptions: subscriptions, _parse_slash_command_task: Task::ready(()), } @@ -193,6 +200,10 @@ impl MessageEditor { .detach(); } + pub fn set_prompt_capabilities(&mut self, capabilities: acp::PromptCapabilities) { + self.prompt_capabilities.set(capabilities); + } + #[cfg(test)] pub(crate) fn editor(&self) -> &Entity<Editor> { &self.editor @@ -230,7 +241,7 @@ impl MessageEditor { let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else { return Task::ready(()); }; - let Some(anchor) = snapshot + let Some(start_anchor) = snapshot .buffer_snapshot .anchor_in_excerpt(*excerpt_id, start) else { @@ -244,6 +255,33 @@ impl MessageEditor { .unwrap_or_default(); if Img::extensions().contains(&extension) && !extension.contains("svg") { + if !self.prompt_capabilities.get().image { + struct ImagesNotAllowed; + + let end_anchor = snapshot.buffer_snapshot.anchor_before( + start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1, + ); + + self.editor.update(cx, |editor, cx| { + // Remove mention + editor.edit([((start_anchor..end_anchor), "")], cx); + }); + + self.workspace + .update(cx, |workspace, cx| { + workspace.show_toast( + Toast::new( + NotificationId::unique::<ImagesNotAllowed>(), + "This agent does not support images yet", + ) + .autohide(), + cx, + ); + }) + .ok(); + return Task::ready(()); + } + let project = self.project.clone(); let Some(project_path) = project .read(cx) @@ -277,7 +315,7 @@ impl MessageEditor { }; return self.confirm_mention_for_image( crease_id, - anchor, + start_anchor, Some(abs_path.clone()), image, window, @@ -301,17 +339,22 @@ impl MessageEditor { match mention_uri { MentionUri::Fetch { url } => { - self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx) + self.confirm_mention_for_fetch(crease_id, start_anchor, url, window, cx) } MentionUri::Directory { abs_path } => { - self.confirm_mention_for_directory(crease_id, anchor, abs_path, window, cx) + self.confirm_mention_for_directory(crease_id, start_anchor, abs_path, window, cx) } MentionUri::Thread { id, name } => { - self.confirm_mention_for_thread(crease_id, anchor, id, name, window, cx) - } - MentionUri::TextThread { path, name } => { - self.confirm_mention_for_text_thread(crease_id, anchor, path, name, window, cx) + self.confirm_mention_for_thread(crease_id, start_anchor, id, name, window, cx) } + MentionUri::TextThread { path, name } => self.confirm_mention_for_text_thread( + crease_id, + start_anchor, + path, + name, + window, + cx, + ), MentionUri::File { .. } | MentionUri::Symbol { .. } | MentionUri::Rule { .. } @@ -778,6 +821,10 @@ impl MessageEditor { } fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) { + if !self.prompt_capabilities.get().image { + return; + } + let images = cx .read_from_clipboard() .map(|item| { @@ -2009,6 +2056,34 @@ mod tests { (message_editor, editor) }); + cx.simulate_input("Lorem @"); + + editor.update_in(&mut cx, |editor, window, cx| { + assert_eq!(editor.text(cx), "Lorem @"); + assert!(editor.has_visible_completions_menu()); + + // Only files since we have default capabilities + assert_eq!( + current_completion_labels(editor), + &[ + "eight.txt dir/b/", + "seven.txt dir/b/", + "six.txt dir/b/", + "five.txt dir/b/", + ] + ); + editor.set_text("", window, cx); + }); + + message_editor.update(&mut cx, |editor, _cx| { + // Enable all prompt capabilities + editor.set_prompt_capabilities(acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + }); + }); + cx.simulate_input("Lorem "); editor.update(&mut cx, |editor, cx| { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 14f9cacd15..81a56165c8 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -492,6 +492,11 @@ impl AcpThreadView { }) }); + this.message_editor.update(cx, |message_editor, _cx| { + message_editor + .set_prompt_capabilities(connection.prompt_capabilities()); + }); + cx.notify(); } Err(err) => { @@ -4762,6 +4767,14 @@ pub(crate) mod tests { &[] } + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + } + } + fn authenticate( &self, _method_id: acp::AuthMethodId, From b0bef3a9a279e98abc499d4e2f7850ff7f5959ea Mon Sep 17 00:00:00 2001 From: Ben Brandt <benjamin.j.brandt@gmail.com> Date: Wed, 20 Aug 2025 21:17:07 +0200 Subject: [PATCH 542/693] agent2: Clean up tool descriptions (#36619) schemars was passing along the newlines from the doc comments. This should make these closer to the markdown file versions we had in the old agent. Release Notes: - N/A --- crates/agent2/src/tools/copy_path_tool.rs | 15 ++++----------- .../agent2/src/tools/create_directory_tool.rs | 7 ++----- crates/agent2/src/tools/delete_path_tool.rs | 3 +-- crates/agent2/src/tools/edit_file_tool.rs | 19 ++++++------------- crates/agent2/src/tools/find_path_tool.rs | 1 - crates/agent2/src/tools/grep_tool.rs | 3 +-- .../agent2/src/tools/list_directory_tool.rs | 6 ++---- crates/agent2/src/tools/move_path_tool.rs | 9 +++------ crates/agent2/src/tools/open_tool.rs | 10 +++------- crates/agent2/src/tools/read_file_tool.rs | 5 +---- crates/agent2/src/tools/thinking_tool.rs | 3 +-- crates/agent2/src/tools/web_search_tool.rs | 2 +- 12 files changed, 25 insertions(+), 58 deletions(-) diff --git a/crates/agent2/src/tools/copy_path_tool.rs b/crates/agent2/src/tools/copy_path_tool.rs index f973b86990..4b40a9842f 100644 --- a/crates/agent2/src/tools/copy_path_tool.rs +++ b/crates/agent2/src/tools/copy_path_tool.rs @@ -8,16 +8,11 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; use util::markdown::MarkdownInlineCode; -/// Copies a file or directory in the project, and returns confirmation that the -/// copy succeeded. -/// +/// Copies a file or directory in the project, and returns confirmation that the copy succeeded. /// Directory contents will be copied recursively (like `cp -r`). /// -/// This tool should be used when it's desirable to create a copy of a file or -/// directory without modifying the original. It's much more efficient than -/// doing this by separately reading and then writing the file or directory's -/// contents, so this tool should be preferred over that approach whenever -/// copying is the goal. +/// This tool should be used when it's desirable to create a copy of a file or directory without modifying the original. +/// It's much more efficient than doing this by separately reading and then writing the file or directory's contents, so this tool should be preferred over that approach whenever copying is the goal. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct CopyPathToolInput { /// The source path of the file or directory to copy. @@ -33,12 +28,10 @@ pub struct CopyPathToolInput { /// You can copy the first file by providing a source_path of "directory1/a/something.txt" /// </example> pub source_path: String, - /// The destination path where the file or directory should be copied to. /// /// <example> - /// To copy "directory1/a/something.txt" to "directory2/b/copy.txt", - /// provide a destination_path of "directory2/b/copy.txt" + /// To copy "directory1/a/something.txt" to "directory2/b/copy.txt", provide a destination_path of "directory2/b/copy.txt" /// </example> pub destination_path: String, } diff --git a/crates/agent2/src/tools/create_directory_tool.rs b/crates/agent2/src/tools/create_directory_tool.rs index c173c5ae67..7720eb3595 100644 --- a/crates/agent2/src/tools/create_directory_tool.rs +++ b/crates/agent2/src/tools/create_directory_tool.rs @@ -9,12 +9,9 @@ use util::markdown::MarkdownInlineCode; use crate::{AgentTool, ToolCallEventStream}; -/// Creates a new directory at the specified path within the project. Returns -/// confirmation that the directory was created. +/// Creates a new directory at the specified path within the project. Returns confirmation that the directory was created. /// -/// This tool creates a directory and all necessary parent directories (similar -/// to `mkdir -p`). It should be used whenever you need to create new -/// directories within the project. +/// This tool creates a directory and all necessary parent directories (similar to `mkdir -p`). It should be used whenever you need to create new directories within the project. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct CreateDirectoryToolInput { /// The path of the new directory. diff --git a/crates/agent2/src/tools/delete_path_tool.rs b/crates/agent2/src/tools/delete_path_tool.rs index e013b3a3e7..c281f1b5b6 100644 --- a/crates/agent2/src/tools/delete_path_tool.rs +++ b/crates/agent2/src/tools/delete_path_tool.rs @@ -9,8 +9,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::sync::Arc; -/// Deletes the file or directory (and the directory's contents, recursively) at -/// the specified path in the project, and returns confirmation of the deletion. +/// Deletes the file or directory (and the directory's contents, recursively) at the specified path in the project, and returns confirmation of the deletion. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct DeletePathToolInput { /// The path of the file or directory to delete. diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 24fedda4eb..f89cace9a8 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -34,25 +34,21 @@ const DEFAULT_UI_TEXT: &str = "Editing file"; /// - Use the `list_directory` tool to verify the parent directory exists and is the correct location #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct EditFileToolInput { - /// A one-line, user-friendly markdown description of the edit. This will be - /// shown in the UI and also passed to another model to perform the edit. + /// A one-line, user-friendly markdown description of the edit. This will be shown in the UI and also passed to another model to perform the edit. /// - /// Be terse, but also descriptive in what you want to achieve with this - /// edit. Avoid generic instructions. + /// Be terse, but also descriptive in what you want to achieve with this edit. Avoid generic instructions. /// /// NEVER mention the file path in this description. /// /// <example>Fix API endpoint URLs</example> /// <example>Update copyright year in `page_footer`</example> /// - /// Make sure to include this field before all the others in the input object - /// so that we can display it immediately. + /// Make sure to include this field before all the others in the input object so that we can display it immediately. pub display_description: String, /// The full path of the file to create or modify in the project. /// - /// WARNING: When specifying which file path need changing, you MUST - /// start each path with one of the project's root directories. + /// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories. /// /// The following examples assume we have two root directories in the project: /// - /a/b/backend @@ -61,22 +57,19 @@ pub struct EditFileToolInput { /// <example> /// `backend/src/main.rs` /// - /// Notice how the file path starts with `backend`. Without that, the path - /// would be ambiguous and the call would fail! + /// Notice how the file path starts with `backend`. Without that, the path would be ambiguous and the call would fail! /// </example> /// /// <example> /// `frontend/db.js` /// </example> pub path: PathBuf, - /// The mode of operation on the file. Possible values: /// - 'edit': Make granular edits to an existing file. /// - 'create': Create a new file if it doesn't exist. /// - 'overwrite': Replace the entire contents of an existing file. /// - /// When a file already exists or you just created it, prefer editing - /// it as opposed to recreating it from scratch. + /// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch. pub mode: EditFileMode, } diff --git a/crates/agent2/src/tools/find_path_tool.rs b/crates/agent2/src/tools/find_path_tool.rs index deccf37ab7..9e11ca6a37 100644 --- a/crates/agent2/src/tools/find_path_tool.rs +++ b/crates/agent2/src/tools/find_path_tool.rs @@ -31,7 +31,6 @@ pub struct FindPathToolInput { /// You can get back the first two paths by providing a glob of "*thing*.txt" /// </example> pub glob: String, - /// Optional starting position for paginated results (0-based). /// When not provided, starts from the beginning. #[serde(default)] diff --git a/crates/agent2/src/tools/grep_tool.rs b/crates/agent2/src/tools/grep_tool.rs index 265c26926d..955dae7235 100644 --- a/crates/agent2/src/tools/grep_tool.rs +++ b/crates/agent2/src/tools/grep_tool.rs @@ -27,8 +27,7 @@ use util::paths::PathMatcher; /// - DO NOT use HTML entities solely to escape characters in the tool parameters. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct GrepToolInput { - /// A regex pattern to search for in the entire project. Note that the regex - /// will be parsed by the Rust `regex` crate. + /// A regex pattern to search for in the entire project. Note that the regex will be parsed by the Rust `regex` crate. /// /// Do NOT specify a path here! This will only be matched against the code **content**. pub regex: String, diff --git a/crates/agent2/src/tools/list_directory_tool.rs b/crates/agent2/src/tools/list_directory_tool.rs index 61f21d8f95..31575a92e4 100644 --- a/crates/agent2/src/tools/list_directory_tool.rs +++ b/crates/agent2/src/tools/list_directory_tool.rs @@ -10,14 +10,12 @@ use std::fmt::Write; use std::{path::Path, sync::Arc}; use util::markdown::MarkdownInlineCode; -/// Lists files and directories in a given path. Prefer the `grep` or -/// `find_path` tools when searching the codebase. +/// Lists files and directories in a given path. Prefer the `grep` or `find_path` tools when searching the codebase. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct ListDirectoryToolInput { /// The fully-qualified path of the directory to list in the project. /// - /// This path should never be absolute, and the first component - /// of the path should always be a root directory in a project. + /// This path should never be absolute, and the first component of the path should always be a root directory in a project. /// /// <example> /// If the project has the following root directories: diff --git a/crates/agent2/src/tools/move_path_tool.rs b/crates/agent2/src/tools/move_path_tool.rs index f8d5d0d176..2a173a4404 100644 --- a/crates/agent2/src/tools/move_path_tool.rs +++ b/crates/agent2/src/tools/move_path_tool.rs @@ -8,14 +8,11 @@ use serde::{Deserialize, Serialize}; use std::{path::Path, sync::Arc}; use util::markdown::MarkdownInlineCode; -/// Moves or rename a file or directory in the project, and returns confirmation -/// that the move succeeded. +/// Moves or rename a file or directory in the project, and returns confirmation that the move succeeded. /// -/// If the source and destination directories are the same, but the filename is -/// different, this performs a rename. Otherwise, it performs a move. +/// If the source and destination directories are the same, but the filename is different, this performs a rename. Otherwise, it performs a move. /// -/// This tool should be used when it's desirable to move or rename a file or -/// directory without changing its contents at all. +/// This tool should be used when it's desirable to move or rename a file or directory without changing its contents at all. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct MovePathToolInput { /// The source path of the file or directory to move/rename. diff --git a/crates/agent2/src/tools/open_tool.rs b/crates/agent2/src/tools/open_tool.rs index 36420560c1..c20369c2d8 100644 --- a/crates/agent2/src/tools/open_tool.rs +++ b/crates/agent2/src/tools/open_tool.rs @@ -8,19 +8,15 @@ use serde::{Deserialize, Serialize}; use std::{path::PathBuf, sync::Arc}; use util::markdown::MarkdownEscaped; -/// This tool opens a file or URL with the default application associated with -/// it on the user's operating system: +/// This tool opens a file or URL with the default application associated with it on the user's operating system: /// /// - On macOS, it's equivalent to the `open` command /// - On Windows, it's equivalent to `start` /// - On Linux, it uses something like `xdg-open`, `gio open`, `gnome-open`, `kde-open`, `wslview` as appropriate /// -/// For example, it can open a web browser with a URL, open a PDF file with the -/// default PDF viewer, etc. +/// For example, it can open a web browser with a URL, open a PDF file with the default PDF viewer, etc. /// -/// You MUST ONLY use this tool when the user has explicitly requested opening -/// something. You MUST NEVER assume that the user would like for you to use -/// this tool. +/// You MUST ONLY use this tool when the user has explicitly requested opening something. You MUST NEVER assume that the user would like for you to use this tool. #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct OpenToolInput { /// The path or URL to open with the default application. diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index f37dff4f47..11a57506fb 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -21,8 +21,7 @@ use crate::{AgentTool, ToolCallEventStream}; pub struct ReadFileToolInput { /// The relative path of the file to read. /// - /// This path should never be absolute, and the first component - /// of the path should always be a root directory in a project. + /// This path should never be absolute, and the first component of the path should always be a root directory in a project. /// /// <example> /// If the project has the following root directories: @@ -34,11 +33,9 @@ pub struct ReadFileToolInput { /// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`. /// </example> pub path: String, - /// Optional line number to start reading on (1-based index) #[serde(default)] pub start_line: Option<u32>, - /// Optional line number to end reading on (1-based index, inclusive) #[serde(default)] pub end_line: Option<u32>, diff --git a/crates/agent2/src/tools/thinking_tool.rs b/crates/agent2/src/tools/thinking_tool.rs index 43647bb468..c5e9451162 100644 --- a/crates/agent2/src/tools/thinking_tool.rs +++ b/crates/agent2/src/tools/thinking_tool.rs @@ -11,8 +11,7 @@ use crate::{AgentTool, ToolCallEventStream}; /// Use this tool when you need to work through complex problems, develop strategies, or outline approaches before taking action. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct ThinkingToolInput { - /// Content to think about. This should be a description of what to think about or - /// a problem to solve. + /// Content to think about. This should be a description of what to think about or a problem to solve. content: String, } diff --git a/crates/agent2/src/tools/web_search_tool.rs b/crates/agent2/src/tools/web_search_tool.rs index d71a128bfe..ffcd4ad3be 100644 --- a/crates/agent2/src/tools/web_search_tool.rs +++ b/crates/agent2/src/tools/web_search_tool.rs @@ -14,7 +14,7 @@ use ui::prelude::*; use web_search::WebSearchRegistry; /// Search the web for information using your query. -/// Use this when you need real-time information, facts, or data that might not be in your training. \ +/// Use this when you need real-time information, facts, or data that might not be in your training. /// Results will include snippets and links from relevant web pages. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct WebSearchToolInput { From 739e4551da857800cf5fb862e98d0e72e9779551 Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Wed, 20 Aug 2025 15:30:11 -0400 Subject: [PATCH 543/693] Fix typo in `Excerpt::contains` (#36621) Follow-up to #36524 Release Notes: - N/A --- crates/multi_buffer/src/multi_buffer.rs | 27 ++++++++++++------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index f73014a6ff..a54d38163d 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -6998,20 +6998,19 @@ impl Excerpt { } fn contains(&self, anchor: &Anchor) -> bool { - anchor.buffer_id == None - || anchor.buffer_id == Some(self.buffer_id) - && self - .range - .context - .start - .cmp(&anchor.text_anchor, &self.buffer) - .is_le() - && self - .range - .context - .end - .cmp(&anchor.text_anchor, &self.buffer) - .is_ge() + (anchor.buffer_id == None || anchor.buffer_id == Some(self.buffer_id)) + && self + .range + .context + .start + .cmp(&anchor.text_anchor, &self.buffer) + .is_le() + && self + .range + .context + .end + .cmp(&anchor.text_anchor, &self.buffer) + .is_ge() } /// The [`Excerpt`]'s start offset in its [`Buffer`] From fa8bef1496efa8047b600fc65fcd662797ea6fb1 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" <JosephTLyons@gmail.com> Date: Wed, 20 Aug 2025 16:05:30 -0400 Subject: [PATCH 544/693] Bump Zed to v0.202 (#36622) Release Notes: -N/A --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 70b8f630f7..7df5304d92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20387,7 +20387,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.201.0" +version = "0.202.0" dependencies = [ "activity_indicator", "agent", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index d69efaf6c0..ac4cd72124 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition.workspace = true name = "zed" -version = "0.201.0" +version = "0.202.0" publish.workspace = true license = "GPL-3.0-or-later" authors = ["Zed Team <hi@zed.dev>"] From 02dabbb9fa4a87721a76d3d6e498378f2965bd1e Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Wed, 20 Aug 2025 17:05:53 -0300 Subject: [PATCH 545/693] acp thread view: Do not go into editing mode if unsupported (#36623) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 81a56165c8..2b87144fcd 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -669,8 +669,14 @@ impl AcpThreadView { ) { match &event.view_event { ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => { - self.editing_message = Some(event.entry_index); - cx.notify(); + if let Some(thread) = self.thread() + && let Some(AgentThreadEntry::UserMessage(user_message)) = + thread.read(cx).entries().get(event.entry_index) + && user_message.id.is_some() + { + self.editing_message = Some(event.entry_index); + cx.notify(); + } } ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => { self.regenerate(event.entry_index, editor, window, cx); @@ -1116,16 +1122,18 @@ impl AcpThreadView { .when(editing && !editor_focus, |this| this.border_dashed()) .border_color(cx.theme().colors().border) .map(|this|{ - if editor_focus { + if editing && editor_focus { this.border_color(focus_border) - } else { + } else if message.id.is_some() { this.hover(|s| s.border_color(focus_border.opacity(0.8))) + } else { + this } }) .text_xs() .child(editor.clone().into_any_element()), ) - .when(editor_focus, |this| + .when(editing && editor_focus, |this| this.child( h_flex() .absolute() From fb7edbfb464eb4ae0e66008b8e681ed0360aa474 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 20 Aug 2025 18:01:22 -0300 Subject: [PATCH 546/693] thread_view: Add recent history entries & adjust empty state (#36625) Release Notes: - N/A --- assets/icons/menu_alt.svg | 2 +- assets/icons/zed_agent.svg | 34 ++-- assets/icons/zed_assistant.svg | 4 +- crates/agent2/src/history_store.rs | 4 + crates/agent2/src/native_agent_server.rs | 2 +- crates/agent_servers/src/gemini.rs | 4 +- crates/agent_ui/src/acp/thread_history.rs | 149 ++++++++++++++- crates/agent_ui/src/acp/thread_view.rs | 198 +++++++++++++++----- crates/agent_ui/src/ui.rs | 2 - crates/agent_ui/src/ui/new_thread_button.rs | 75 -------- 10 files changed, 325 insertions(+), 149 deletions(-) delete mode 100644 crates/agent_ui/src/ui/new_thread_button.rs diff --git a/assets/icons/menu_alt.svg b/assets/icons/menu_alt.svg index 87add13216..b9cc19e22f 100644 --- a/assets/icons/menu_alt.svg +++ b/assets/icons/menu_alt.svg @@ -1,3 +1,3 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M13.333 10H8M13.333 6H2.66701" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M2.66699 8H10.667M2.66699 4H13.333M2.66699 12H7.99999" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> </svg> diff --git a/assets/icons/zed_agent.svg b/assets/icons/zed_agent.svg index b6e120a0b6..0c80e22c51 100644 --- a/assets/icons/zed_agent.svg +++ b/assets/icons/zed_agent.svg @@ -1,27 +1,27 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M11 8.75V10.5C8.93097 10.5 8.06903 10.5 6 10.5V10L11 6V5.5H6V7.25" stroke="black" stroke-width="1.2"/> +<path d="M11 8.75V10.5C8.93097 10.5 8.06903 10.5 6 10.5V10L11 6V5.5H6V7.25" stroke="black" stroke-width="1.5"/> <path d="M2 8.5C2.27614 8.5 2.5 8.27614 2.5 8C2.5 7.72386 2.27614 7.5 2 7.5C1.72386 7.5 1.5 7.72386 1.5 8C1.5 8.27614 1.72386 8.5 2 8.5Z" fill="black"/> -<path d="M2.99976 6.33002C3.2759 6.33002 3.49976 6.10616 3.49976 5.83002C3.49976 5.55387 3.2759 5.33002 2.99976 5.33002C2.72361 5.33002 2.49976 5.55387 2.49976 5.83002C2.49976 6.10616 2.72361 6.33002 2.99976 6.33002Z" fill="black"/> -<path d="M2.99976 10.66C3.2759 10.66 3.49976 10.4361 3.49976 10.16C3.49976 9.88383 3.2759 9.65997 2.99976 9.65997C2.72361 9.65997 2.49976 9.88383 2.49976 10.16C2.49976 10.4361 2.72361 10.66 2.99976 10.66Z" fill="black"/> +<path opacity="0.6" d="M2.99976 6.33002C3.2759 6.33002 3.49976 6.10616 3.49976 5.83002C3.49976 5.55387 3.2759 5.33002 2.99976 5.33002C2.72361 5.33002 2.49976 5.55387 2.49976 5.83002C2.49976 6.10616 2.72361 6.33002 2.99976 6.33002Z" fill="black"/> +<path opacity="0.6" d="M2.99976 10.66C3.2759 10.66 3.49976 10.4361 3.49976 10.16C3.49976 9.88383 3.2759 9.65997 2.99976 9.65997C2.72361 9.65997 2.49976 9.88383 2.49976 10.16C2.49976 10.4361 2.72361 10.66 2.99976 10.66Z" fill="black"/> <path d="M15 8.5C15.2761 8.5 15.5 8.27614 15.5 8C15.5 7.72386 15.2761 7.5 15 7.5C14.7239 7.5 14.5 7.72386 14.5 8C14.5 8.27614 14.7239 8.5 15 8.5Z" fill="black"/> -<path d="M14 6.33002C14.2761 6.33002 14.5 6.10616 14.5 5.83002C14.5 5.55387 14.2761 5.33002 14 5.33002C13.7239 5.33002 13.5 5.55387 13.5 5.83002C13.5 6.10616 13.7239 6.33002 14 6.33002Z" fill="black"/> -<path d="M14 10.66C14.2761 10.66 14.5 10.4361 14.5 10.16C14.5 9.88383 14.2761 9.65997 14 9.65997C13.7239 9.65997 13.5 9.88383 13.5 10.16C13.5 10.4361 13.7239 10.66 14 10.66Z" fill="black"/> +<path opacity="0.6" d="M14 6.33002C14.2761 6.33002 14.5 6.10616 14.5 5.83002C14.5 5.55387 14.2761 5.33002 14 5.33002C13.7239 5.33002 13.5 5.55387 13.5 5.83002C13.5 6.10616 13.7239 6.33002 14 6.33002Z" fill="black"/> +<path opacity="0.6" d="M14 10.66C14.2761 10.66 14.5 10.4361 14.5 10.16C14.5 9.88383 14.2761 9.65997 14 9.65997C13.7239 9.65997 13.5 9.88383 13.5 10.16C13.5 10.4361 13.7239 10.66 14 10.66Z" fill="black"/> <path d="M8.49219 2C8.76833 2 8.99219 1.77614 8.99219 1.5C8.99219 1.22386 8.76833 1 8.49219 1C8.21605 1 7.99219 1.22386 7.99219 1.5C7.99219 1.77614 8.21605 2 8.49219 2Z" fill="black"/> -<path d="M6 3C6.27614 3 6.5 2.77614 6.5 2.5C6.5 2.22386 6.27614 2 6 2C5.72386 2 5.5 2.22386 5.5 2.5C5.5 2.77614 5.72386 3 6 3Z" fill="black"/> +<path opacity="0.6" d="M6 3C6.27614 3 6.5 2.77614 6.5 2.5C6.5 2.22386 6.27614 2 6 2C5.72386 2 5.5 2.22386 5.5 2.5C5.5 2.77614 5.72386 3 6 3Z" fill="black"/> <path d="M4 4C4.27614 4 4.5 3.77614 4.5 3.5C4.5 3.22386 4.27614 3 4 3C3.72386 3 3.5 3.22386 3.5 3.5C3.5 3.77614 3.72386 4 4 4Z" fill="black"/> <path d="M3.99976 13C4.2759 13 4.49976 12.7761 4.49976 12.5C4.49976 12.2239 4.2759 12 3.99976 12C3.72361 12 3.49976 12.2239 3.49976 12.5C3.49976 12.7761 3.72361 13 3.99976 13Z" fill="black"/> -<path d="M2 12.5C2.27614 12.5 2.5 12.2761 2.5 12C2.5 11.7239 2.27614 11.5 2 11.5C1.72386 11.5 1.5 11.7239 1.5 12C1.5 12.2761 1.72386 12.5 2 12.5Z" fill="black"/> -<path d="M2 4.5C2.27614 4.5 2.5 4.27614 2.5 4C2.5 3.72386 2.27614 3.5 2 3.5C1.72386 3.5 1.5 3.72386 1.5 4C1.5 4.27614 1.72386 4.5 2 4.5Z" fill="black"/> -<path d="M15 12.5C15.2761 12.5 15.5 12.2761 15.5 12C15.5 11.7239 15.2761 11.5 15 11.5C14.7239 11.5 14.5 11.7239 14.5 12C14.5 12.2761 14.7239 12.5 15 12.5Z" fill="black"/> -<path d="M15 4.5C15.2761 4.5 15.5 4.27614 15.5 4C15.5 3.72386 15.2761 3.5 15 3.5C14.7239 3.5 14.5 3.72386 14.5 4C14.5 4.27614 14.7239 4.5 15 4.5Z" fill="black"/> -<path d="M3.99976 15C4.2759 15 4.49976 14.7761 4.49976 14.5C4.49976 14.2239 4.2759 14 3.99976 14C3.72361 14 3.49976 14.2239 3.49976 14.5C3.49976 14.7761 3.72361 15 3.99976 15Z" fill="black"/> -<path d="M4 2C4.27614 2 4.5 1.77614 4.5 1.5C4.5 1.22386 4.27614 1 4 1C3.72386 1 3.5 1.22386 3.5 1.5C3.5 1.77614 3.72386 2 4 2Z" fill="black"/> -<path d="M13 15C13.2761 15 13.5 14.7761 13.5 14.5C13.5 14.2239 13.2761 14 13 14C12.7239 14 12.5 14.2239 12.5 14.5C12.5 14.7761 12.7239 15 13 15Z" fill="black"/> -<path d="M13 2C13.2761 2 13.5 1.77614 13.5 1.5C13.5 1.22386 13.2761 1 13 1C12.7239 1 12.5 1.22386 12.5 1.5C12.5 1.77614 12.7239 2 13 2Z" fill="black"/> +<path opacity="0.2" d="M2 12.5C2.27614 12.5 2.5 12.2761 2.5 12C2.5 11.7239 2.27614 11.5 2 11.5C1.72386 11.5 1.5 11.7239 1.5 12C1.5 12.2761 1.72386 12.5 2 12.5Z" fill="black"/> +<path opacity="0.2" d="M2 4.5C2.27614 4.5 2.5 4.27614 2.5 4C2.5 3.72386 2.27614 3.5 2 3.5C1.72386 3.5 1.5 3.72386 1.5 4C1.5 4.27614 1.72386 4.5 2 4.5Z" fill="black"/> +<path opacity="0.2" d="M15 12.5C15.2761 12.5 15.5 12.2761 15.5 12C15.5 11.7239 15.2761 11.5 15 11.5C14.7239 11.5 14.5 11.7239 14.5 12C14.5 12.2761 14.7239 12.5 15 12.5Z" fill="black"/> +<path opacity="0.2" d="M15 4.5C15.2761 4.5 15.5 4.27614 15.5 4C15.5 3.72386 15.2761 3.5 15 3.5C14.7239 3.5 14.5 3.72386 14.5 4C14.5 4.27614 14.7239 4.5 15 4.5Z" fill="black"/> +<path opacity="0.5" d="M3.99976 15C4.2759 15 4.49976 14.7761 4.49976 14.5C4.49976 14.2239 4.2759 14 3.99976 14C3.72361 14 3.49976 14.2239 3.49976 14.5C3.49976 14.7761 3.72361 15 3.99976 15Z" fill="black"/> +<path opacity="0.5" d="M4 2C4.27614 2 4.5 1.77614 4.5 1.5C4.5 1.22386 4.27614 1 4 1C3.72386 1 3.5 1.22386 3.5 1.5C3.5 1.77614 3.72386 2 4 2Z" fill="black"/> +<path opacity="0.5" d="M13 15C13.2761 15 13.5 14.7761 13.5 14.5C13.5 14.2239 13.2761 14 13 14C12.7239 14 12.5 14.2239 12.5 14.5C12.5 14.7761 12.7239 15 13 15Z" fill="black"/> +<path opacity="0.5" d="M13 2C13.2761 2 13.5 1.77614 13.5 1.5C13.5 1.22386 13.2761 1 13 1C12.7239 1 12.5 1.22386 12.5 1.5C12.5 1.77614 12.7239 2 13 2Z" fill="black"/> <path d="M13 4C13.2761 4 13.5 3.77614 13.5 3.5C13.5 3.22386 13.2761 3 13 3C12.7239 3 12.5 3.22386 12.5 3.5C12.5 3.77614 12.7239 4 13 4Z" fill="black"/> <path d="M13 13C13.2761 13 13.5 12.7761 13.5 12.5C13.5 12.2239 13.2761 12 13 12C12.7239 12 12.5 12.2239 12.5 12.5C12.5 12.7761 12.7239 13 13 13Z" fill="black"/> -<path d="M11 3C11.2761 3 11.5 2.77614 11.5 2.5C11.5 2.22386 11.2761 2 11 2C10.7239 2 10.5 2.22386 10.5 2.5C10.5 2.77614 10.7239 3 11 3Z" fill="black"/> +<path opacity="0.6" d="M11 3C11.2761 3 11.5 2.77614 11.5 2.5C11.5 2.22386 11.2761 2 11 2C10.7239 2 10.5 2.22386 10.5 2.5C10.5 2.77614 10.7239 3 11 3Z" fill="black"/> <path d="M8.5 15C8.77614 15 9 14.7761 9 14.5C9 14.2239 8.77614 14 8.5 14C8.22386 14 8 14.2239 8 14.5C8 14.7761 8.22386 15 8.5 15Z" fill="black"/> -<path d="M6 14C6.27614 14 6.5 13.7761 6.5 13.5C6.5 13.2239 6.27614 13 6 13C5.72386 13 5.5 13.2239 5.5 13.5C5.5 13.7761 5.72386 14 6 14Z" fill="black"/> -<path d="M11 14C11.2761 14 11.5 13.7761 11.5 13.5C11.5 13.2239 11.2761 13 11 13C10.7239 13 10.5 13.2239 10.5 13.5C10.5 13.7761 10.7239 14 11 14Z" fill="black"/> +<path opacity="0.6" d="M6 14C6.27614 14 6.5 13.7761 6.5 13.5C6.5 13.2239 6.27614 13 6 13C5.72386 13 5.5 13.2239 5.5 13.5C5.5 13.7761 5.72386 14 6 14Z" fill="black"/> +<path opacity="0.6" d="M11 14C11.2761 14 11.5 13.7761 11.5 13.5C11.5 13.2239 11.2761 13 11 13C10.7239 13 10.5 13.2239 10.5 13.5C10.5 13.7761 10.7239 14 11 14Z" fill="black"/> </svg> diff --git a/assets/icons/zed_assistant.svg b/assets/icons/zed_assistant.svg index 470eb0fede..812277a100 100644 --- a/assets/icons/zed_assistant.svg +++ b/assets/icons/zed_assistant.svg @@ -1,5 +1,5 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M8 2.93652L6.9243 6.20697C6.86924 6.37435 6.77565 6.52646 6.65105 6.65105C6.52646 6.77565 6.37435 6.86924 6.20697 6.9243L2.93652 8L6.20697 9.0757C6.37435 9.13076 6.52646 9.22435 6.65105 9.34895C6.77565 9.47354 6.86924 9.62565 6.9243 9.79306L8 13.0635L9.0757 9.79306C9.13076 9.62565 9.22435 9.47354 9.34895 9.34895C9.47354 9.22435 9.62565 9.13076 9.79306 9.0757L13.0635 8L9.79306 6.9243C9.62565 6.86924 9.47354 6.77565 9.34895 6.65105C9.22435 6.52646 9.13076 6.37435 9.0757 6.20697L8 2.93652Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M3.33334 2V4.66666M2 3.33334H4.66666" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M12.6665 11.3333V14M11.3333 12.6666H13.9999" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M3.33334 2V4.66666M2 3.33334H4.66666" stroke="black" stroke-opacity="0.75" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M12.6665 11.3333V14M11.3333 12.6666H13.9999" stroke="black" stroke-opacity="0.75" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> </svg> diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs index 870c2607c4..2d70164a66 100644 --- a/crates/agent2/src/history_store.rs +++ b/crates/agent2/src/history_store.rs @@ -345,4 +345,8 @@ impl HistoryStore { .retain(|old_entry| old_entry != entry); self.save_recently_opened_entries(cx); } + + pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> { + self.entries(cx).into_iter().take(limit).collect() + } } diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index 74d24efb13..a1f935589a 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -27,7 +27,7 @@ impl AgentServer for NativeAgentServer { } fn empty_state_headline(&self) -> &'static str { - "" + "Welcome to the Agent Panel" } fn empty_state_message(&self) -> &'static str { diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 813f8b1fe0..dcbeaa1d63 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -18,11 +18,11 @@ const ACP_ARG: &str = "--experimental-acp"; impl AgentServer for Gemini { fn name(&self) -> &'static str { - "Gemini" + "Gemini CLI" } fn empty_state_headline(&self) -> &'static str { - "Welcome to Gemini" + "Welcome to Gemini CLI" } fn empty_state_message(&self) -> &'static str { diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs index 8a05801139..68a41f31d0 100644 --- a/crates/agent_ui/src/acp/thread_history.rs +++ b/crates/agent_ui/src/acp/thread_history.rs @@ -1,11 +1,12 @@ -use crate::RemoveSelectedThread; +use crate::acp::AcpThreadView; +use crate::{AgentPanel, RemoveSelectedThread}; use agent2::{HistoryEntry, HistoryStore}; use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; use editor::{Editor, EditorEvent}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ App, Empty, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, - UniformListScrollHandle, Window, uniform_list, + UniformListScrollHandle, WeakEntity, Window, uniform_list, }; use std::{fmt::Display, ops::Range, sync::Arc}; use time::{OffsetDateTime, UtcOffset}; @@ -639,6 +640,150 @@ impl Render for AcpThreadHistory { } } +#[derive(IntoElement)] +pub struct AcpHistoryEntryElement { + entry: HistoryEntry, + thread_view: WeakEntity<AcpThreadView>, + selected: bool, + hovered: bool, + on_hover: Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>, +} + +impl AcpHistoryEntryElement { + pub fn new(entry: HistoryEntry, thread_view: WeakEntity<AcpThreadView>) -> Self { + Self { + entry, + thread_view, + selected: false, + hovered: false, + on_hover: Box::new(|_, _, _| {}), + } + } + + pub fn hovered(mut self, hovered: bool) -> Self { + self.hovered = hovered; + self + } + + pub fn on_hover(mut self, on_hover: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { + self.on_hover = Box::new(on_hover); + self + } +} + +impl RenderOnce for AcpHistoryEntryElement { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + let (id, title, timestamp) = match &self.entry { + HistoryEntry::AcpThread(thread) => ( + thread.id.to_string(), + thread.title.clone(), + thread.updated_at, + ), + HistoryEntry::TextThread(context) => ( + context.path.to_string_lossy().to_string(), + context.title.clone(), + context.mtime.to_utc(), + ), + }; + + let formatted_time = { + let now = chrono::Utc::now(); + let duration = now.signed_duration_since(timestamp); + + if duration.num_days() > 0 { + format!("{}d", duration.num_days()) + } else if duration.num_hours() > 0 { + format!("{}h ago", duration.num_hours()) + } else if duration.num_minutes() > 0 { + format!("{}m ago", duration.num_minutes()) + } else { + "Just now".to_string() + } + }; + + ListItem::new(SharedString::from(id)) + .rounded() + .toggle_state(self.selected) + .spacing(ListItemSpacing::Sparse) + .start_slot( + h_flex() + .w_full() + .gap_2() + .justify_between() + .child(Label::new(title).size(LabelSize::Small).truncate()) + .child( + Label::new(formatted_time) + .color(Color::Muted) + .size(LabelSize::XSmall), + ), + ) + .on_hover(self.on_hover) + .end_slot::<IconButton>(if self.hovered || self.selected { + Some( + IconButton::new("delete", IconName::Trash) + .shape(IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .tooltip(move |window, cx| { + Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx) + }) + .on_click({ + let thread_view = self.thread_view.clone(); + let entry = self.entry.clone(); + + move |_event, _window, cx| { + if let Some(thread_view) = thread_view.upgrade() { + thread_view.update(cx, |thread_view, cx| { + thread_view.delete_history_entry(entry.clone(), cx); + }); + } + } + }), + ) + } else { + None + }) + .on_click({ + let thread_view = self.thread_view.clone(); + let entry = self.entry; + + move |_event, window, cx| { + if let Some(workspace) = thread_view + .upgrade() + .and_then(|view| view.read(cx).workspace().upgrade()) + { + match &entry { + HistoryEntry::AcpThread(thread_metadata) => { + if let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) { + panel.update(cx, |panel, cx| { + panel.load_agent_thread( + thread_metadata.clone(), + window, + cx, + ); + }); + } + } + HistoryEntry::TextThread(context) => { + if let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) { + panel.update(cx, |panel, cx| { + panel + .open_saved_prompt_editor( + context.path.clone(), + window, + cx, + ) + .detach_and_log_err(cx); + }); + } + } + } + } + } + }) + } +} + #[derive(Clone, Copy)] pub enum EntryTimeFormat { DateAndTime, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 2b87144fcd..35da9b8c85 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -8,7 +8,7 @@ use action_log::ActionLog; use agent_client_protocol::{self as acp}; use agent_servers::{AgentServer, ClaudeCode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; -use agent2::{DbThreadMetadata, HistoryEntryId, HistoryStore}; +use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore}; use anyhow::bail; use audio::{Audio, Sound}; use buffer_diff::BufferDiff; @@ -54,11 +54,12 @@ use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent}; use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; use crate::agent_diff::AgentDiff; use crate::profile_selector::{ProfileProvider, ProfileSelector}; + use crate::ui::preview::UsageCallout; use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip}; use crate::{ AgentDiffPane, AgentPanel, ContinueThread, ContinueWithBurnMode, ExpandMessageEditor, Follow, - KeepAll, OpenAgentDiff, RejectAll, ToggleBurnMode, ToggleProfileSelector, + KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector, }; const RESPONSE_PADDING_X: Pixels = px(19.); @@ -240,6 +241,7 @@ pub struct AcpThreadView { project: Entity<Project>, thread_state: ThreadState, history_store: Entity<HistoryStore>, + hovered_recent_history_item: Option<usize>, entry_view_state: Entity<EntryViewState>, message_editor: Entity<MessageEditor>, model_selector: Option<Entity<AcpModelSelectorPopover>>, @@ -357,6 +359,7 @@ impl AcpThreadView { editor_expanded: false, terminal_expanded: true, history_store, + hovered_recent_history_item: None, _subscriptions: subscriptions, _cancel_task: None, } @@ -582,6 +585,10 @@ impl AcpThreadView { cx.notify(); } + pub fn workspace(&self) -> &WeakEntity<Workspace> { + &self.workspace + } + pub fn thread(&self) -> Option<&Entity<AcpThread>> { match &self.thread_state { ThreadState::Ready { thread, .. } => Some(thread), @@ -2284,51 +2291,132 @@ impl AcpThreadView { ) } - fn render_empty_state(&self, cx: &App) -> AnyElement { + fn render_empty_state_section_header( + &self, + label: impl Into<SharedString>, + action_slot: Option<AnyElement>, + cx: &mut Context<Self>, + ) -> impl IntoElement { + div().pl_1().pr_1p5().child( + h_flex() + .mt_2() + .pl_1p5() + .pb_1() + .w_full() + .justify_between() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .child( + Label::new(label.into()) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .children(action_slot), + ) + } + + fn render_empty_state(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement { let loading = matches!(&self.thread_state, ThreadState::Loading { .. }); + let recent_history = self + .history_store + .update(cx, |history_store, cx| history_store.recent_entries(3, cx)); + let no_history = self + .history_store + .update(cx, |history_store, cx| history_store.is_empty(cx)); v_flex() .size_full() - .items_center() - .justify_center() - .child(if loading { - h_flex() - .justify_center() - .child(self.render_agent_logo()) - .with_animation( - "pulsating_icon", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.4, 1.0)), - |icon, delta| icon.opacity(delta), - ) - .into_any() - } else { - self.render_agent_logo().into_any_element() - }) - .child(h_flex().mt_4().mb_1().justify_center().child(if loading { - div() - .child(LoadingLabel::new("").size(LabelSize::Large)) - .into_any_element() - } else { - Headline::new(self.agent.empty_state_headline()) - .size(HeadlineSize::Medium) - .into_any_element() - })) - .child( - div() - .max_w_1_2() - .text_sm() - .text_center() - .map(|this| { - if loading { - this.invisible() + .when(no_history, |this| { + this.child( + v_flex() + .size_full() + .items_center() + .justify_center() + .child(if loading { + h_flex() + .justify_center() + .child(self.render_agent_logo()) + .with_animation( + "pulsating_icon", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.4, 1.0)), + |icon, delta| icon.opacity(delta), + ) + .into_any() } else { - this.text_color(cx.theme().colors().text_muted) - } - }) - .child(self.agent.empty_state_message()), - ) + self.render_agent_logo().into_any_element() + }) + .child(h_flex().mt_4().mb_2().justify_center().child(if loading { + div() + .child(LoadingLabel::new("").size(LabelSize::Large)) + .into_any_element() + } else { + Headline::new(self.agent.empty_state_headline()) + .size(HeadlineSize::Medium) + .into_any_element() + })), + ) + }) + .when(!no_history, |this| { + this.justify_end().child( + v_flex() + .child( + self.render_empty_state_section_header( + "Recent", + Some( + Button::new("view-history", "View All") + .style(ButtonStyle::Subtle) + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in( + &OpenHistory, + &self.focus_handle(cx), + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(12.))), + ) + .on_click(move |_event, window, cx| { + window.dispatch_action(OpenHistory.boxed_clone(), cx); + }) + .into_any_element(), + ), + cx, + ), + ) + .child( + v_flex().p_1().pr_1p5().gap_1().children( + recent_history + .into_iter() + .enumerate() + .map(|(index, entry)| { + // TODO: Add keyboard navigation. + let is_hovered = + self.hovered_recent_history_item == Some(index); + crate::acp::thread_history::AcpHistoryEntryElement::new( + entry, + cx.entity().downgrade(), + ) + .hovered(is_hovered) + .on_hover(cx.listener( + move |this, is_hovered, _window, cx| { + if *is_hovered { + this.hovered_recent_history_item = Some(index); + } else if this.hovered_recent_history_item + == Some(index) + { + this.hovered_recent_history_item = None; + } + cx.notify(); + }, + )) + .into_any_element() + }), + ), + ), + ) + }) .into_any() } @@ -2351,9 +2439,11 @@ impl AcpThreadView { .items_center() .justify_center() .child(self.render_error_agent_logo()) - .child(h_flex().mt_4().mb_1().justify_center().child( - Headline::new(self.agent.empty_state_headline()).size(HeadlineSize::Medium), - )) + .child( + h_flex().mt_4().mb_1().justify_center().child( + Headline::new("Authentication Required").size(HeadlineSize::Medium), + ), + ) .into_any(), ) .children(description.map(|desc| { @@ -4234,6 +4324,18 @@ impl AcpThreadView { ); cx.notify(); } + + pub fn delete_history_entry(&mut self, entry: HistoryEntry, cx: &mut Context<Self>) { + let task = match entry { + HistoryEntry::AcpThread(thread) => self.history_store.update(cx, |history, cx| { + history.delete_thread(thread.id.clone(), cx) + }), + HistoryEntry::TextThread(context) => self.history_store.update(cx, |history, cx| { + history.delete_text_thread(context.path.clone(), cx) + }), + }; + task.detach_and_log_err(cx); + } } impl Focusable for AcpThreadView { @@ -4268,7 +4370,9 @@ impl Render for AcpThreadView { window, cx, ), - ThreadState::Loading { .. } => v_flex().flex_1().child(self.render_empty_state(cx)), + ThreadState::Loading { .. } => { + v_flex().flex_1().child(self.render_empty_state(window, cx)) + } ThreadState::LoadError(e) => v_flex() .p_2() .flex_1() @@ -4310,7 +4414,7 @@ impl Render for AcpThreadView { }, ) } else { - this.child(self.render_empty_state(cx)) + this.child(self.render_empty_state(window, cx)) } }) } diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index beeaf0c43b..e27a224240 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -2,7 +2,6 @@ mod agent_notification; mod burn_mode_tooltip; mod context_pill; mod end_trial_upsell; -// mod new_thread_button; mod onboarding_modal; pub mod preview; @@ -10,5 +9,4 @@ pub use agent_notification::*; pub use burn_mode_tooltip::*; pub use context_pill::*; pub use end_trial_upsell::*; -// pub use new_thread_button::*; pub use onboarding_modal::*; diff --git a/crates/agent_ui/src/ui/new_thread_button.rs b/crates/agent_ui/src/ui/new_thread_button.rs deleted file mode 100644 index 347d6adcaf..0000000000 --- a/crates/agent_ui/src/ui/new_thread_button.rs +++ /dev/null @@ -1,75 +0,0 @@ -use gpui::{ClickEvent, ElementId, IntoElement, ParentElement, Styled}; -use ui::prelude::*; - -#[derive(IntoElement)] -pub struct NewThreadButton { - id: ElementId, - label: SharedString, - icon: IconName, - keybinding: Option<ui::KeyBinding>, - on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>, -} - -impl NewThreadButton { - fn new(id: impl Into<ElementId>, label: impl Into<SharedString>, icon: IconName) -> Self { - Self { - id: id.into(), - label: label.into(), - icon, - keybinding: None, - on_click: None, - } - } - - fn keybinding(mut self, keybinding: Option<ui::KeyBinding>) -> Self { - self.keybinding = keybinding; - self - } - - fn on_click<F>(mut self, handler: F) -> Self - where - F: Fn(&mut Window, &mut App) + 'static, - { - self.on_click = Some(Box::new( - move |_: &ClickEvent, window: &mut Window, cx: &mut App| handler(window, cx), - )); - self - } -} - -impl RenderOnce for NewThreadButton { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - h_flex() - .id(self.id) - .w_full() - .py_1p5() - .px_2() - .gap_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border.opacity(0.4)) - .bg(cx.theme().colors().element_active.opacity(0.2)) - .hover(|style| { - style - .bg(cx.theme().colors().element_hover) - .border_color(cx.theme().colors().border) - }) - .child( - h_flex() - .gap_1p5() - .child( - Icon::new(self.icon) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .child(Label::new(self.label).size(LabelSize::Small)), - ) - .when_some(self.keybinding, |this, keybinding| { - this.child(keybinding.size(rems_from_px(10.))) - }) - .when_some(self.on_click, |this, on_click| { - this.on_click(move |event, window, cx| on_click(event, window, cx)) - }) - } -} From d1820b183a08549927164e9a0791d7e7053ab484 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Wed, 20 Aug 2025 18:26:07 -0300 Subject: [PATCH 547/693] acp: Suggest installing gemini@preview instead of latest (#36629) Release Notes: - N/A --- crates/agent_servers/src/gemini.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index dcbeaa1d63..25c654db9b 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -53,7 +53,7 @@ impl AgentServer for Gemini { return Err(LoadError::NotInstalled { error_message: "Failed to find Gemini CLI binary".into(), install_message: "Install Gemini CLI".into(), - install_command: "npm install -g @google/gemini-cli@latest".into() + install_command: "npm install -g @google/gemini-cli@preview".into() }.into()); }; From 595cf1c6c3ce6980c4937fbb7a17229c31ff398f Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Wed, 20 Aug 2025 17:31:25 -0400 Subject: [PATCH 548/693] acp: Rename `assistant::QuoteSelection` and support it in agent2 threads (#36628) Release Notes: - N/A --- assets/keymaps/default-linux.json | 4 +- assets/keymaps/default-macos.json | 4 +- assets/keymaps/linux/cursor.json | 4 +- assets/keymaps/macos/cursor.json | 4 +- .../agent_ui/src/acp/completion_provider.rs | 122 ++++++++++-------- crates/agent_ui/src/acp/message_editor.rs | 54 ++++++-- crates/agent_ui/src/acp/thread_view.rs | 6 + crates/agent_ui/src/agent_panel.rs | 16 ++- crates/agent_ui/src/agent_ui.rs | 6 + crates/agent_ui/src/text_thread_editor.rs | 3 +- docs/src/ai/text-threads.md | 4 +- 11 files changed, 148 insertions(+), 79 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index b4efa70572..955e68f5a9 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -138,7 +138,7 @@ "find": "buffer_search::Deploy", "ctrl-f": "buffer_search::Deploy", "ctrl-h": "buffer_search::DeployReplace", - "ctrl->": "assistant::QuoteSelection", + "ctrl->": "agent::QuoteSelection", "ctrl-<": "assistant::InsertIntoEditor", "ctrl-alt-e": "editor::SelectEnclosingSymbol", "ctrl-shift-backspace": "editor::GoToPreviousChange", @@ -241,7 +241,7 @@ "ctrl-shift-i": "agent::ToggleOptionsMenu", "ctrl-alt-shift-n": "agent::ToggleNewThreadMenu", "shift-alt-escape": "agent::ExpandMessageEditor", - "ctrl->": "assistant::QuoteSelection", + "ctrl->": "agent::QuoteSelection", "ctrl-alt-e": "agent::RemoveAllContext", "ctrl-shift-e": "project_panel::ToggleFocus", "ctrl-shift-enter": "agent::ContinueThread", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index ad2ab2ba89..8b18299a91 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -162,7 +162,7 @@ "cmd-alt-f": "buffer_search::DeployReplace", "cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }], "cmd-e": ["buffer_search::Deploy", { "focus": false }], - "cmd->": "assistant::QuoteSelection", + "cmd->": "agent::QuoteSelection", "cmd-<": "assistant::InsertIntoEditor", "cmd-alt-e": "editor::SelectEnclosingSymbol", "alt-enter": "editor::OpenSelectionsInMultibuffer" @@ -281,7 +281,7 @@ "cmd-shift-i": "agent::ToggleOptionsMenu", "cmd-alt-shift-n": "agent::ToggleNewThreadMenu", "shift-alt-escape": "agent::ExpandMessageEditor", - "cmd->": "assistant::QuoteSelection", + "cmd->": "agent::QuoteSelection", "cmd-alt-e": "agent::RemoveAllContext", "cmd-shift-e": "project_panel::ToggleFocus", "cmd-ctrl-b": "agent::ToggleBurnMode", diff --git a/assets/keymaps/linux/cursor.json b/assets/keymaps/linux/cursor.json index 1c381b0cf0..2e27158e11 100644 --- a/assets/keymaps/linux/cursor.json +++ b/assets/keymaps/linux/cursor.json @@ -17,8 +17,8 @@ "bindings": { "ctrl-i": "agent::ToggleFocus", "ctrl-shift-i": "agent::ToggleFocus", - "ctrl-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode - "ctrl-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode + "ctrl-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode + "ctrl-l": "agent::QuoteSelection", // In cursor uses "Agent" mode "ctrl-k": "assistant::InlineAssist", "ctrl-shift-k": "assistant::InsertIntoEditor" } diff --git a/assets/keymaps/macos/cursor.json b/assets/keymaps/macos/cursor.json index fdf9c437cf..1d723bd75b 100644 --- a/assets/keymaps/macos/cursor.json +++ b/assets/keymaps/macos/cursor.json @@ -17,8 +17,8 @@ "bindings": { "cmd-i": "agent::ToggleFocus", "cmd-shift-i": "agent::ToggleFocus", - "cmd-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode - "cmd-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode + "cmd-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode + "cmd-l": "agent::QuoteSelection", // In cursor uses "Agent" mode "cmd-k": "assistant::InlineAssist", "cmd-shift-k": "assistant::InsertIntoEditor" } diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index bf0a3f7a5a..3587e5144e 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -108,62 +108,7 @@ impl ContextPickerCompletionProvider { confirm: Some(Arc::new(|_, _, _| true)), }), ContextPickerEntry::Action(action) => { - let (new_text, on_action) = match action { - ContextPickerAction::AddSelections => { - const PLACEHOLDER: &str = "selection "; - let selections = selection_ranges(workspace, cx) - .into_iter() - .enumerate() - .map(|(ix, (buffer, range))| { - ( - buffer, - range, - (PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1), - ) - }) - .collect::<Vec<_>>(); - - let new_text: String = PLACEHOLDER.repeat(selections.len()); - - let callback = Arc::new({ - let source_range = source_range.clone(); - move |_, window: &mut Window, cx: &mut App| { - let selections = selections.clone(); - let message_editor = message_editor.clone(); - let source_range = source_range.clone(); - window.defer(cx, move |window, cx| { - message_editor - .update(cx, |message_editor, cx| { - message_editor.confirm_mention_for_selection( - source_range, - selections, - window, - cx, - ) - }) - .ok(); - }); - false - } - }); - - (new_text, callback) - } - }; - - Some(Completion { - replace_range: source_range, - new_text, - label: CodeLabel::plain(action.label().to_string(), None), - icon_path: Some(action.icon().path().into()), - documentation: None, - source: project::CompletionSource::Custom, - insert_text_mode: None, - // This ensures that when a user accepts this completion, the - // completion menu will still be shown after "@category " is - // inserted - confirm: Some(on_action), - }) + Self::completion_for_action(action, source_range, message_editor, workspace, cx) } } } @@ -359,6 +304,71 @@ impl ContextPickerCompletionProvider { }) } + pub(crate) fn completion_for_action( + action: ContextPickerAction, + source_range: Range<Anchor>, + message_editor: WeakEntity<MessageEditor>, + workspace: &Entity<Workspace>, + cx: &mut App, + ) -> Option<Completion> { + let (new_text, on_action) = match action { + ContextPickerAction::AddSelections => { + const PLACEHOLDER: &str = "selection "; + let selections = selection_ranges(workspace, cx) + .into_iter() + .enumerate() + .map(|(ix, (buffer, range))| { + ( + buffer, + range, + (PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1), + ) + }) + .collect::<Vec<_>>(); + + let new_text: String = PLACEHOLDER.repeat(selections.len()); + + let callback = Arc::new({ + let source_range = source_range.clone(); + move |_, window: &mut Window, cx: &mut App| { + let selections = selections.clone(); + let message_editor = message_editor.clone(); + let source_range = source_range.clone(); + window.defer(cx, move |window, cx| { + message_editor + .update(cx, |message_editor, cx| { + message_editor.confirm_mention_for_selection( + source_range, + selections, + window, + cx, + ) + }) + .ok(); + }); + false + } + }); + + (new_text, callback) + } + }; + + Some(Completion { + replace_range: source_range, + new_text, + label: CodeLabel::plain(action.label().to_string(), None), + icon_path: Some(action.icon().path().into()), + documentation: None, + source: project::CompletionSource::Custom, + insert_text_mode: None, + // This ensures that when a user accepts this completion, the + // completion menu will still be shown after "@category " is + // inserted + confirm: Some(on_action), + }) + } + fn search( &self, mode: Option<ContextPickerMode>, diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 5eab1a4e2d..be133808b7 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1,6 +1,6 @@ use crate::{ acp::completion_provider::ContextPickerCompletionProvider, - context_picker::fetch_context_picker::fetch_url_content, + context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content}, }; use acp_thread::{MentionUri, selection_name}; use agent_client_protocol as acp; @@ -27,7 +27,7 @@ use gpui::{ }; use language::{Buffer, Language}; use language_model::LanguageModelImage; -use project::{Project, ProjectPath, Worktree}; +use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree}; use prompt_store::PromptStore; use rope::Point; use settings::Settings; @@ -561,21 +561,24 @@ impl MessageEditor { let range = snapshot.anchor_after(offset + range_to_fold.start) ..snapshot.anchor_after(offset + range_to_fold.end); - let path = buffer - .read(cx) - .file() - .map_or(PathBuf::from("untitled"), |file| file.path().to_path_buf()); + // TODO support selections from buffers with no path + let Some(project_path) = buffer.read(cx).project_path(cx) else { + continue; + }; + let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else { + continue; + }; let snapshot = buffer.read(cx).snapshot(); let point_range = selection_range.to_point(&snapshot); let line_range = point_range.start.row..point_range.end.row; let uri = MentionUri::Selection { - path: path.clone(), + path: abs_path.clone(), line_range: line_range.clone(), }; let crease = crate::context_picker::crease_for_mention( - selection_name(&path, &line_range).into(), + selection_name(&abs_path, &line_range).into(), uri.icon_path(cx), range, self.editor.downgrade(), @@ -587,8 +590,7 @@ impl MessageEditor { crease_ids.first().copied().unwrap() }); - self.mention_set - .insert_uri(crease_id, MentionUri::Selection { path, line_range }); + self.mention_set.insert_uri(crease_id, uri); } } @@ -948,6 +950,38 @@ impl MessageEditor { .detach(); } + pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context<Self>) { + let buffer = self.editor.read(cx).buffer().clone(); + let Some(buffer) = buffer.read(cx).as_singleton() else { + return; + }; + let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len())); + let Some(workspace) = self.workspace.upgrade() else { + return; + }; + let Some(completion) = ContextPickerCompletionProvider::completion_for_action( + ContextPickerAction::AddSelections, + anchor..anchor, + cx.weak_entity(), + &workspace, + cx, + ) else { + return; + }; + self.editor.update(cx, |message_editor, cx| { + message_editor.edit( + [( + multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), + completion.new_text, + )], + cx, + ); + }); + if let Some(confirm) = completion.confirm { + confirm(CompletionIntent::Complete, window, cx); + } + } + pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) { self.editor.update(cx, |message_editor, cx| { message_editor.set_read_only(read_only); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 35da9b8c85..0dfa3d259e 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -4097,6 +4097,12 @@ impl AcpThreadView { }) } + pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context<Self>) { + self.message_editor.update(cx, |message_editor, cx| { + message_editor.insert_selections(window, cx); + }) + } + fn render_thread_retry_status_callout( &self, _window: &mut Window, diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index e2c4acb1ce..65a9da573a 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -903,6 +903,16 @@ impl AgentPanel { } } + fn active_thread_view(&self) -> Option<&Entity<AcpThreadView>> { + match &self.active_view { + ActiveView::ExternalAgentThread { thread_view } => Some(thread_view), + ActiveView::Thread { .. } + | ActiveView::TextThread { .. } + | ActiveView::History + | ActiveView::Configuration => None, + } + } + fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) { if cx.has_flag::<GeminiAndNativeFeatureFlag>() { return self.new_agent_thread(AgentType::NativeAgent, window, cx); @@ -3882,7 +3892,11 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate { // Wait to create a new context until the workspace is no longer // being updated. cx.defer_in(window, move |panel, window, cx| { - if let Some(message_editor) = panel.active_message_editor() { + if let Some(thread_view) = panel.active_thread_view() { + thread_view.update(cx, |thread_view, cx| { + thread_view.insert_selections(window, cx); + }); + } else if let Some(message_editor) = panel.active_message_editor() { message_editor.update(cx, |message_editor, cx| { message_editor.context_store().update(cx, |store, cx| { let buffer = buffer.read(cx); diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 7b6557245f..6084fd6423 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -128,6 +128,12 @@ actions!( ] ); +#[derive(Clone, Copy, Debug, PartialEq, Eq, Action)] +#[action(namespace = agent)] +#[action(deprecated_aliases = ["assistant::QuoteSelection"])] +/// Quotes the current selection in the agent panel's message editor. +pub struct QuoteSelection; + /// Creates a new conversation thread, optionally based on an existing thread. #[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)] #[action(namespace = agent)] diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index a928f7af54..9fbd90c4a6 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -1,4 +1,5 @@ use crate::{ + QuoteSelection, language_model_selector::{LanguageModelSelector, language_model_selector}, ui::BurnModeTooltip, }; @@ -89,8 +90,6 @@ actions!( CycleMessageRole, /// Inserts the selected text into the active editor. InsertIntoEditor, - /// Quotes the current selection in the assistant conversation. - QuoteSelection, /// Splits the conversation at the current cursor position. Split, ] diff --git a/docs/src/ai/text-threads.md b/docs/src/ai/text-threads.md index 65a5dcba03..ed439252b4 100644 --- a/docs/src/ai/text-threads.md +++ b/docs/src/ai/text-threads.md @@ -16,7 +16,7 @@ To begin, type a message in a `You` block. As you type, the remaining tokens count for the selected model is updated. -Inserting text from an editor is as simple as highlighting the text and running `assistant: quote selection` ({#kb assistant::QuoteSelection}); Zed will wrap it in a fenced code block if it is code. +Inserting text from an editor is as simple as highlighting the text and running `agent: quote selection` ({#kb agent::QuoteSelection}); Zed will wrap it in a fenced code block if it is code. ![Quoting a selection](https://zed.dev/img/assistant/quoting-a-selection.png) @@ -148,7 +148,7 @@ Usage: `/terminal [<number>]` The `/selection` command inserts the selected text in the editor into the context. This is useful for referencing specific parts of your code. -This is equivalent to the `assistant: quote selection` command ({#kb assistant::QuoteSelection}). +This is equivalent to the `agent: quote selection` command ({#kb agent::QuoteSelection}). Usage: `/selection` From 9e34bb3f058982f060face485186eba9a739afca Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Wed, 20 Aug 2025 18:35:48 -0300 Subject: [PATCH 549/693] acp: Hide feedback buttons for external agents (#36630) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 0dfa3d259e..f4c0ce9784 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -3815,7 +3815,11 @@ impl AcpThreadView { .flex_wrap() .justify_end(); - if AgentSettings::get_global(cx).enable_feedback { + if AgentSettings::get_global(cx).enable_feedback + && self + .thread() + .is_some_and(|thread| thread.read(cx).connection().telemetry().is_some()) + { let feedback = self.thread_feedback.feedback; container = container.child( div().visible_on_hover("thread-controls-container").child( From c9c708ff08571ceab3d8aad7354042230d99750c Mon Sep 17 00:00:00 2001 From: Julia Ryan <juliaryan3.14@gmail.com> Date: Wed, 20 Aug 2025 16:43:53 -0500 Subject: [PATCH 550/693] nix: Re-enable nightly builds (#36632) Release Notes: - N/A --- .github/workflows/release_nightly.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 0cc6737a45..5d63c34edd 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -206,9 +206,6 @@ jobs: runs-on: github-8vcpu-ubuntu-2404 needs: tests name: Build Zed on FreeBSD - # env: - # MYTOKEN : ${{ secrets.MYTOKEN }} - # MYTOKEN2: "value2" steps: - uses: actions/checkout@v4 - name: Build FreeBSD remote-server @@ -243,7 +240,6 @@ jobs: bundle-nix: name: Build and cache Nix package - if: false needs: tests secrets: inherit uses: ./.github/workflows/nix.yml From 5120b6b7f9962daf0000618a06e4e1522c575334 Mon Sep 17 00:00:00 2001 From: Conrad Irwin <conrad.irwin@gmail.com> Date: Wed, 20 Aug 2025 16:12:41 -0600 Subject: [PATCH 551/693] acp: Handle Gemini Auth Better (#36631) Release Notes: - N/A --------- Co-authored-by: Danilo Leal <daniloleal09@gmail.com> --- crates/agent_servers/src/gemini.rs | 7 +- crates/agent_ui/src/acp/thread_view.rs | 154 ++++++++++++++++-- crates/language_models/src/provider/google.rs | 53 +++++- 3 files changed, 195 insertions(+), 19 deletions(-) diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 25c654db9b..d30525328b 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -5,6 +5,7 @@ use crate::{AgentServer, AgentServerCommand}; use acp_thread::{AgentConnection, LoadError}; use anyhow::Result; use gpui::{Entity, Task}; +use language_models::provider::google::GoogleLanguageModelProvider; use project::Project; use settings::SettingsStore; use ui::App; @@ -47,7 +48,7 @@ impl AgentServer for Gemini { settings.get::<AllAgentServersSettings>(None).gemini.clone() })?; - let Some(command) = + let Some(mut command) = AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await else { return Err(LoadError::NotInstalled { @@ -57,6 +58,10 @@ impl AgentServer for Gemini { }.into()); }; + if let Some(api_key)= cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() { + command.env.get_or_insert_default().insert("GEMINI_API_KEY".to_owned(), api_key.key); + } + let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await; if result.is_err() { let version_fut = util::command::new_smol_command(&command.path) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index f4c0ce9784..12a33d022e 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -278,6 +278,7 @@ enum ThreadState { connection: Rc<dyn AgentConnection>, description: Option<Entity<Markdown>>, configuration_view: Option<AnyView>, + pending_auth_method: Option<acp::AuthMethodId>, _subscription: Option<Subscription>, }, } @@ -563,6 +564,7 @@ impl AcpThreadView { this.update(cx, |this, cx| { this.thread_state = ThreadState::Unauthenticated { + pending_auth_method: None, connection, configuration_view, description: err @@ -999,12 +1001,74 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context<Self>, ) { - let ThreadState::Unauthenticated { ref connection, .. } = self.thread_state else { + let ThreadState::Unauthenticated { + connection, + pending_auth_method, + configuration_view, + .. + } = &mut self.thread_state + else { return; }; + if method.0.as_ref() == "gemini-api-key" { + let registry = LanguageModelRegistry::global(cx); + let provider = registry + .read(cx) + .provider(&language_model::GOOGLE_PROVIDER_ID) + .unwrap(); + if !provider.is_authenticated(cx) { + let this = cx.weak_entity(); + let agent = self.agent.clone(); + let connection = connection.clone(); + window.defer(cx, |window, cx| { + Self::handle_auth_required( + this, + AuthRequired { + description: Some("GEMINI_API_KEY must be set".to_owned()), + provider_id: Some(language_model::GOOGLE_PROVIDER_ID), + }, + agent, + connection, + window, + cx, + ); + }); + return; + } + } else if method.0.as_ref() == "vertex-ai" + && std::env::var("GOOGLE_API_KEY").is_err() + && (std::env::var("GOOGLE_CLOUD_PROJECT").is_err() + || (std::env::var("GOOGLE_CLOUD_PROJECT").is_err())) + { + let this = cx.weak_entity(); + let agent = self.agent.clone(); + let connection = connection.clone(); + + window.defer(cx, |window, cx| { + Self::handle_auth_required( + this, + AuthRequired { + description: Some( + "GOOGLE_API_KEY must be set in the environment to use Vertex AI authentication for Gemini CLI. Please export it and restart Zed." + .to_owned(), + ), + provider_id: None, + }, + agent, + connection, + window, + cx, + ) + }); + return; + } + self.thread_error.take(); + configuration_view.take(); + pending_auth_method.replace(method.clone()); let authenticate = connection.authenticate(method, cx); + cx.notify(); self.auth_task = Some(cx.spawn_in(window, { let project = self.project.clone(); let agent = self.agent.clone(); @@ -2425,6 +2489,7 @@ impl AcpThreadView { connection: &Rc<dyn AgentConnection>, description: Option<&Entity<Markdown>>, configuration_view: Option<&AnyView>, + pending_auth_method: Option<&acp::AuthMethodId>, window: &mut Window, cx: &Context<Self>, ) -> Div { @@ -2456,17 +2521,80 @@ impl AcpThreadView { .cloned() .map(|view| div().px_4().w_full().max_w_128().child(view)), ) - .child(h_flex().mt_1p5().justify_center().children( - connection.auth_methods().iter().map(|method| { - Button::new(SharedString::from(method.id.0.clone()), method.name.clone()) - .on_click({ - let method_id = method.id.clone(); - cx.listener(move |this, _, window, cx| { - this.authenticate(method_id.clone(), window, cx) + .when( + configuration_view.is_none() + && description.is_none() + && pending_auth_method.is_none(), + |el| { + el.child( + div() + .text_ui(cx) + .text_center() + .px_4() + .w_full() + .max_w_128() + .child(Label::new("Authentication required")), + ) + }, + ) + .when_some(pending_auth_method, |el, _| { + let spinner_icon = div() + .px_0p5() + .id("generating") + .tooltip(Tooltip::text("Generating Changes…")) + .child( + Icon::new(IconName::ArrowCircle) + .size(IconSize::Small) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate(percentage(delta))) + }, + ) + .into_any_element(), + ) + .into_any(); + el.child( + h_flex() + .text_ui(cx) + .text_center() + .justify_center() + .gap_2() + .px_4() + .w_full() + .max_w_128() + .child(Label::new("Authenticating...")) + .child(spinner_icon), + ) + }) + .child( + h_flex() + .mt_1p5() + .gap_1() + .flex_wrap() + .justify_center() + .children(connection.auth_methods().iter().enumerate().rev().map( + |(ix, method)| { + Button::new( + SharedString::from(method.id.0.clone()), + method.name.clone(), + ) + .style(ButtonStyle::Outlined) + .when(ix == 0, |el| { + el.style(ButtonStyle::Tinted(ui::TintColor::Accent)) }) - }) - }), - )) + .size(ButtonSize::Medium) + .label_size(LabelSize::Small) + .on_click({ + let method_id = method.id.clone(); + cx.listener(move |this, _, window, cx| { + this.authenticate(method_id.clone(), window, cx) + }) + }) + }, + )), + ) } fn render_load_error(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement { @@ -2551,6 +2679,8 @@ impl AcpThreadView { let install_command = install_command.clone(); container = container.child( Button::new("install", install_message) + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .size(ButtonSize::Medium) .tooltip(Tooltip::text(install_command.clone())) .on_click(cx.listener(move |this, _, window, cx| { let task = this @@ -4372,11 +4502,13 @@ impl Render for AcpThreadView { connection, description, configuration_view, + pending_auth_method, .. } => self.render_auth_required_state( connection, description.as_ref(), configuration_view.as_ref(), + pending_auth_method.as_ref(), window, cx, ), diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index 1ac12b4cd4..566620675e 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -12,9 +12,9 @@ use gpui::{ }; use http_client::HttpClient; use language_model::{ - AuthenticateError, LanguageModelCompletionError, LanguageModelCompletionEvent, - LanguageModelToolChoice, LanguageModelToolSchemaFormat, LanguageModelToolUse, - LanguageModelToolUseId, MessageContent, StopReason, + AuthenticateError, ConfigurationViewTargetAgent, LanguageModelCompletionError, + LanguageModelCompletionEvent, LanguageModelToolChoice, LanguageModelToolSchemaFormat, + LanguageModelToolUse, LanguageModelToolUseId, MessageContent, StopReason, }; use language_model::{ LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider, @@ -37,6 +37,8 @@ use util::ResultExt; use crate::AllLanguageModelSettings; use crate::ui::InstructionListItem; +use super::anthropic::ApiKey; + const PROVIDER_ID: LanguageModelProviderId = language_model::GOOGLE_PROVIDER_ID; const PROVIDER_NAME: LanguageModelProviderName = language_model::GOOGLE_PROVIDER_NAME; @@ -198,6 +200,33 @@ impl GoogleLanguageModelProvider { request_limiter: RateLimiter::new(4), }) } + + pub fn api_key(cx: &mut App) -> Task<Result<ApiKey>> { + let credentials_provider = <dyn CredentialsProvider>::global(cx); + let api_url = AllLanguageModelSettings::get_global(cx) + .google + .api_url + .clone(); + + if let Ok(key) = std::env::var(GEMINI_API_KEY_VAR) { + Task::ready(Ok(ApiKey { + key, + from_env: true, + })) + } else { + cx.spawn(async move |cx| { + let (_, api_key) = credentials_provider + .read_credentials(&api_url, cx) + .await? + .ok_or(AuthenticateError::CredentialsNotFound)?; + + Ok(ApiKey { + key: String::from_utf8(api_key).context("invalid {PROVIDER_NAME} API key")?, + from_env: false, + }) + }) + } + } } impl LanguageModelProviderState for GoogleLanguageModelProvider { @@ -279,11 +308,11 @@ impl LanguageModelProvider for GoogleLanguageModelProvider { fn configuration_view( &self, - _target_agent: language_model::ConfigurationViewTargetAgent, + target_agent: language_model::ConfigurationViewTargetAgent, window: &mut Window, cx: &mut App, ) -> AnyView { - cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) + cx.new(|cx| ConfigurationView::new(self.state.clone(), target_agent, window, cx)) .into() } @@ -776,11 +805,17 @@ fn convert_usage(usage: &UsageMetadata) -> language_model::TokenUsage { struct ConfigurationView { api_key_editor: Entity<Editor>, state: gpui::Entity<State>, + target_agent: language_model::ConfigurationViewTargetAgent, load_credentials_task: Option<Task<()>>, } impl ConfigurationView { - fn new(state: gpui::Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self { + fn new( + state: gpui::Entity<State>, + target_agent: language_model::ConfigurationViewTargetAgent, + window: &mut Window, + cx: &mut Context<Self>, + ) -> Self { cx.observe(&state, |_, _, cx| { cx.notify(); }) @@ -810,6 +845,7 @@ impl ConfigurationView { editor.set_placeholder_text("AIzaSy...", cx); editor }), + target_agent, state, load_credentials_task, } @@ -885,7 +921,10 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new("To use Zed's agent with Google AI, you need to add an API key. Follow these steps:")) + .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match self.target_agent { + ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Google AI", + ConfigurationViewTargetAgent::Other(agent) => agent, + }))) .child( List::new() .child(InstructionListItem::new( From ffb995181ef0d1034f89108ce50be4a8c679f41f Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Wed, 20 Aug 2025 19:30:25 -0300 Subject: [PATCH 552/693] acp: Supress gemini aborted errors (#36633) This PR adds a temporary workaround to supress "Aborted" errors from Gemini when cancelling generation. This won't be needed once https://github.com/google-gemini/gemini-cli/pull/6656 is generally available. Release Notes: - N/A --- Cargo.lock | 4 +- Cargo.toml | 2 +- crates/agent_servers/src/acp/v1.rs | 61 ++++++++++++++++++++++++++++-- 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7df5304d92..bfb135d32c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,9 +171,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.28" +version = "0.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c887e795097665ab95119580534e7cc1335b59e1a7fec296943e534b970f4ed" +checksum = "89a2cd7e0bd2bb7ed27687cfcf6561b91542c1ce23e52fd54ee59b7568c9bd84" dependencies = [ "anyhow", "futures 0.3.31", diff --git a/Cargo.toml b/Cargo.toml index 436d4a7f5c..3f54745900 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -423,7 +423,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.28" +agent-client-protocol = "0.0.29" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index 2e70a5f37a..2cad1b5a87 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -1,11 +1,12 @@ use action_log::ActionLog; -use agent_client_protocol::{self as acp, Agent as _}; +use agent_client_protocol::{self as acp, Agent as _, ErrorCode}; use anyhow::anyhow; use collections::HashMap; use futures::AsyncBufReadExt as _; use futures::channel::oneshot; use futures::io::BufReader; use project::Project; +use serde::Deserialize; use std::path::Path; use std::rc::Rc; use std::{any::Any, cell::RefCell}; @@ -27,6 +28,7 @@ pub struct AcpConnection { pub struct AcpSession { thread: WeakEntity<AcpThread>, + pending_cancel: bool, } const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1; @@ -171,6 +173,7 @@ impl AgentConnection for AcpConnection { let session = AcpSession { thread: thread.downgrade(), + pending_cancel: false, }; sessions.borrow_mut().insert(session_id, session); @@ -202,9 +205,48 @@ impl AgentConnection for AcpConnection { cx: &mut App, ) -> Task<Result<acp::PromptResponse>> { let conn = self.connection.clone(); + let sessions = self.sessions.clone(); + let session_id = params.session_id.clone(); cx.foreground_executor().spawn(async move { - let response = conn.prompt(params).await?; - Ok(response) + match conn.prompt(params).await { + Ok(response) => Ok(response), + Err(err) => { + if err.code != ErrorCode::INTERNAL_ERROR.code { + anyhow::bail!(err) + } + + let Some(data) = &err.data else { + anyhow::bail!(err) + }; + + // Temporary workaround until the following PR is generally available: + // https://github.com/google-gemini/gemini-cli/pull/6656 + + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct ErrorDetails { + details: Box<str>, + } + + match serde_json::from_value(data.clone()) { + Ok(ErrorDetails { details }) => { + if sessions + .borrow() + .get(&session_id) + .is_some_and(|session| session.pending_cancel) + && details.contains("This operation was aborted") + { + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::Canceled, + }) + } else { + Err(anyhow!(details)) + } + } + Err(_) => Err(anyhow!(err)), + } + } + } }) } @@ -213,12 +255,23 @@ impl AgentConnection for AcpConnection { } fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { + if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) { + session.pending_cancel = true; + } let conn = self.connection.clone(); let params = acp::CancelNotification { session_id: session_id.clone(), }; + let sessions = self.sessions.clone(); + let session_id = session_id.clone(); cx.foreground_executor() - .spawn(async move { conn.cancel(params).await }) + .spawn(async move { + let resp = conn.cancel(params).await; + if let Some(session) = sessions.borrow_mut().get_mut(&session_id) { + session.pending_cancel = false; + } + resp + }) .detach(); } From c20233e0b4fcaf0459ef0ff6b7ea3c3f72cce837 Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Wed, 20 Aug 2025 19:09:09 -0400 Subject: [PATCH 553/693] agent_ui: Fix signed-in check in Zed provider configuration (#36639) This PR fixes the check for if the user is signed in in the Agent panel configuration. Supersedes https://github.com/zed-industries/zed/pull/36634. Release Notes: - Fixed the user's plan badge near the Zed provider in the Agent panel not showing despite being signed in. --- crates/agent_ui/src/agent_configuration.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 6da84758ee..00e48efdac 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -192,7 +192,7 @@ impl AgentConfiguration { let is_signed_in = self .workspace .read_with(cx, |workspace, _| { - workspace.client().status().borrow().is_connected() + !workspace.client().status().borrow().is_signed_out() }) .unwrap_or(false); From 74c0ba980b6a561e514fecd4c93fd8cbe7e045c2 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Wed, 20 Aug 2025 20:32:17 -0300 Subject: [PATCH 554/693] acp: Reliably suppress gemini abort error (#36640) https://github.com/zed-industries/zed/pull/36633 relied on the prompt request responding before cancel, but that's not guaranteed Release Notes: - N/A --- crates/agent_servers/src/acp/v1.rs | 33 ++++++++++++++---------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index 2cad1b5a87..bc11a3748a 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -28,7 +28,7 @@ pub struct AcpConnection { pub struct AcpSession { thread: WeakEntity<AcpThread>, - pending_cancel: bool, + suppress_abort_err: bool, } const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1; @@ -173,7 +173,7 @@ impl AgentConnection for AcpConnection { let session = AcpSession { thread: thread.downgrade(), - pending_cancel: false, + suppress_abort_err: false, }; sessions.borrow_mut().insert(session_id, session); @@ -208,7 +208,16 @@ impl AgentConnection for AcpConnection { let sessions = self.sessions.clone(); let session_id = params.session_id.clone(); cx.foreground_executor().spawn(async move { - match conn.prompt(params).await { + let result = conn.prompt(params).await; + + let mut suppress_abort_err = false; + + if let Some(session) = sessions.borrow_mut().get_mut(&session_id) { + suppress_abort_err = session.suppress_abort_err; + session.suppress_abort_err = false; + } + + match result { Ok(response) => Ok(response), Err(err) => { if err.code != ErrorCode::INTERNAL_ERROR.code { @@ -230,11 +239,7 @@ impl AgentConnection for AcpConnection { match serde_json::from_value(data.clone()) { Ok(ErrorDetails { details }) => { - if sessions - .borrow() - .get(&session_id) - .is_some_and(|session| session.pending_cancel) - && details.contains("This operation was aborted") + if suppress_abort_err && details.contains("This operation was aborted") { Ok(acp::PromptResponse { stop_reason: acp::StopReason::Canceled, @@ -256,22 +261,14 @@ impl AgentConnection for AcpConnection { fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) { - session.pending_cancel = true; + session.suppress_abort_err = true; } let conn = self.connection.clone(); let params = acp::CancelNotification { session_id: session_id.clone(), }; - let sessions = self.sessions.clone(); - let session_id = session_id.clone(); cx.foreground_executor() - .spawn(async move { - let resp = conn.cancel(params).await; - if let Some(session) = sessions.borrow_mut().get_mut(&session_id) { - session.pending_cancel = false; - } - resp - }) + .spawn(async move { conn.cancel(params).await }) .detach(); } From 3dd362978a2b5fa6c41da9368252491d1c638fab Mon Sep 17 00:00:00 2001 From: Ben Kunkle <ben@zed.dev> Date: Wed, 20 Aug 2025 18:41:06 -0500 Subject: [PATCH 555/693] docs: Add table of all actions (#36642) Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/docs_preprocessor/src/main.rs | 66 ++++++++++++++++++++++++++++ docs/src/SUMMARY.md | 1 + docs/src/all-actions.md | 3 ++ 3 files changed, 70 insertions(+) create mode 100644 docs/src/all-actions.md diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index 33158577c4..c900eb692a 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -99,6 +99,7 @@ fn handle_preprocessing() -> Result<()> { let mut errors = HashSet::<PreprocessorError>::new(); handle_frontmatter(&mut book, &mut errors); + template_big_table_of_actions(&mut book); template_and_validate_keybindings(&mut book, &mut errors); template_and_validate_actions(&mut book, &mut errors); @@ -147,6 +148,18 @@ fn handle_frontmatter(book: &mut Book, errors: &mut HashSet<PreprocessorError>) }); } +fn template_big_table_of_actions(book: &mut Book) { + for_each_chapter_mut(book, |chapter| { + let needle = "{#ACTIONS_TABLE#}"; + if let Some(start) = chapter.content.rfind(needle) { + chapter.content.replace_range( + start..start + needle.len(), + &generate_big_table_of_actions(), + ); + } + }); +} + fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<PreprocessorError>) { let regex = Regex::new(r"\{#kb (.*?)\}").unwrap(); @@ -277,6 +290,7 @@ struct ActionDef { name: &'static str, human_name: String, deprecated_aliases: &'static [&'static str], + docs: Option<&'static str>, } fn dump_all_gpui_actions() -> Vec<ActionDef> { @@ -285,6 +299,7 @@ fn dump_all_gpui_actions() -> Vec<ActionDef> { name: action.name, human_name: command_palette::humanize_action_name(action.name), deprecated_aliases: action.deprecated_aliases, + docs: action.documentation, }) .collect::<Vec<ActionDef>>(); @@ -418,3 +433,54 @@ fn title_regex() -> &'static Regex { static TITLE_REGEX: OnceLock<Regex> = OnceLock::new(); TITLE_REGEX.get_or_init(|| Regex::new(r"<title>\s*(.*?)\s*").unwrap()) } + +fn generate_big_table_of_actions() -> String { + let actions = &*ALL_ACTIONS; + let mut output = String::new(); + + let mut actions_sorted = actions.iter().collect::>(); + actions_sorted.sort_by_key(|a| a.name); + + // Start the definition list with custom styling for better spacing + output.push_str("
\n"); + + for action in actions_sorted.into_iter() { + // Add the humanized action name as the term with margin + output.push_str( + "
", + ); + output.push_str(&action.human_name); + output.push_str("
\n"); + + // Add the definition with keymap name and description + output.push_str("
\n"); + + // Add the description, escaping HTML if needed + if let Some(description) = action.docs { + output.push_str( + &description + .replace("&", "&") + .replace("<", "<") + .replace(">", ">"), + ); + output.push_str("
\n"); + } + output.push_str("Keymap Name: "); + output.push_str(action.name); + output.push_str("
\n"); + if !action.deprecated_aliases.is_empty() { + output.push_str("Deprecated Aliases:"); + for alias in action.deprecated_aliases.iter() { + output.push_str(""); + output.push_str(alias); + output.push_str(", "); + } + } + output.push_str("\n
\n"); + } + + // Close the definition list + output.push_str("
\n"); + + output +} diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index c7af36f431..251cad6234 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -16,6 +16,7 @@ - [Configuring Zed](./configuring-zed.md) - [Configuring Languages](./configuring-languages.md) - [Key bindings](./key-bindings.md) + - [All Actions](./all-actions.md) - [Snippets](./snippets.md) - [Themes](./themes.md) - [Icon Themes](./icon-themes.md) diff --git a/docs/src/all-actions.md b/docs/src/all-actions.md new file mode 100644 index 0000000000..d20f7cfd63 --- /dev/null +++ b/docs/src/all-actions.md @@ -0,0 +1,3 @@ +## All Actions + +{#ACTIONS_TABLE#} From 8ef9ecc91f6c6b2eaf65fd0d8a93e2f49af876de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Thu, 21 Aug 2025 08:08:54 +0800 Subject: [PATCH 556/693] windows: Fix `RevealInFileManager` (#36592) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #36314 This PR takes inspiration from [Electron’s implementation](https://github.com/electron/electron/blob/dd54e84a58531b52680f7f736f593ee887eff6a7/shell/common/platform_util_win.cc#L268-L314). Before and after: https://github.com/user-attachments/assets/53eec5d3-23c7-4ee1-8477-e524b0538f60 Release Notes: - N/A --- crates/gpui/src/platform/windows/platform.rs | 111 +++++++++++-------- 1 file changed, 67 insertions(+), 44 deletions(-) diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index b13b9915f1..6202e05fb3 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -1,5 +1,6 @@ use std::{ cell::RefCell, + ffi::OsStr, mem::ManuallyDrop, path::{Path, PathBuf}, rc::Rc, @@ -460,13 +461,15 @@ impl Platform for WindowsPlatform { } fn open_url(&self, url: &str) { + if url.is_empty() { + return; + } let url_string = url.to_string(); self.background_executor() .spawn(async move { - if url_string.is_empty() { - return; - } - open_target(url_string.as_str()); + open_target(&url_string) + .with_context(|| format!("Opening url: {}", url_string)) + .log_err(); }) .detach(); } @@ -514,37 +517,29 @@ impl Platform for WindowsPlatform { } fn reveal_path(&self, path: &Path) { - let Ok(file_full_path) = path.canonicalize() else { - log::error!("unable to parse file path"); + if path.as_os_str().is_empty() { return; - }; + } + let path = path.to_path_buf(); self.background_executor() .spawn(async move { - let Some(path) = file_full_path.to_str() else { - return; - }; - if path.is_empty() { - return; - } - open_target_in_explorer(path); + open_target_in_explorer(&path) + .with_context(|| format!("Revealing path {} in explorer", path.display())) + .log_err(); }) .detach(); } fn open_with_system(&self, path: &Path) { - let Ok(full_path) = path.canonicalize() else { - log::error!("unable to parse file full path: {}", path.display()); + if path.as_os_str().is_empty() { return; - }; + } + let path = path.to_path_buf(); self.background_executor() .spawn(async move { - let Some(full_path_str) = full_path.to_str() else { - return; - }; - if full_path_str.is_empty() { - return; - }; - open_target(full_path_str); + open_target(&path) + .with_context(|| format!("Opening {} with system", path.display())) + .log_err(); }) .detach(); } @@ -735,39 +730,67 @@ pub(crate) struct WindowCreationInfo { pub(crate) disable_direct_composition: bool, } -fn open_target(target: &str) { - unsafe { - let ret = ShellExecuteW( +fn open_target(target: impl AsRef) -> Result<()> { + let target = target.as_ref(); + let ret = unsafe { + ShellExecuteW( None, windows::core::w!("open"), &HSTRING::from(target), None, None, SW_SHOWDEFAULT, - ); - if ret.0 as isize <= 32 { - log::error!("Unable to open target: {}", std::io::Error::last_os_error()); - } + ) + }; + if ret.0 as isize <= 32 { + Err(anyhow::anyhow!( + "Unable to open target: {}", + std::io::Error::last_os_error() + )) + } else { + Ok(()) } } -fn open_target_in_explorer(target: &str) { +fn open_target_in_explorer(target: &Path) -> Result<()> { + let dir = target.parent().context("No parent folder found")?; + let desktop = unsafe { SHGetDesktopFolder()? }; + + let mut dir_item = std::ptr::null_mut(); unsafe { - let ret = ShellExecuteW( + desktop.ParseDisplayName( + HWND::default(), None, - windows::core::w!("open"), - windows::core::w!("explorer.exe"), - &HSTRING::from(format!("/select,{}", target).as_str()), + &HSTRING::from(dir), None, - SW_SHOWDEFAULT, - ); - if ret.0 as isize <= 32 { - log::error!( - "Unable to open target in explorer: {}", - std::io::Error::last_os_error() - ); - } + &mut dir_item, + std::ptr::null_mut(), + )?; } + + let mut file_item = std::ptr::null_mut(); + unsafe { + desktop.ParseDisplayName( + HWND::default(), + None, + &HSTRING::from(target), + None, + &mut file_item, + std::ptr::null_mut(), + )?; + } + + let highlight = [file_item as *const _]; + unsafe { SHOpenFolderAndSelectItems(dir_item as _, Some(&highlight), 0) }.or_else(|err| { + if err.code().0 == ERROR_FILE_NOT_FOUND.0 as i32 { + // On some systems, the above call mysteriously fails with "file not + // found" even though the file is there. In these cases, ShellExecute() + // seems to work as a fallback (although it won't select the file). + open_target(dir).context("Opening target parent folder") + } else { + Err(anyhow::anyhow!("Can not open target path: {}", err)) + } + }) } fn file_open_dialog( From 6f242772cccaf3a8b2dc372cc1c2f94713faedf3 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 20 Aug 2025 21:10:36 -0300 Subject: [PATCH 557/693] acp: Update to 0.0.30 (#36643) See: https://github.com/zed-industries/agent-client-protocol/pull/20 Release Notes: - N/A --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- crates/acp_thread/src/acp_thread.rs | 2 +- crates/acp_thread/src/connection.rs | 2 +- crates/agent2/src/tests/mod.rs | 4 ++-- crates/agent2/src/thread.rs | 2 +- crates/agent_servers/src/acp/v1.rs | 4 ++-- crates/agent_servers/src/claude.rs | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bfb135d32c..f3e821fb5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,9 +171,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.29" +version = "0.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a2cd7e0bd2bb7ed27687cfcf6561b91542c1ce23e52fd54ee59b7568c9bd84" +checksum = "5f792e009ba59b137ee1db560bc37e567887ad4b5af6f32181d381fff690e2d4" dependencies = [ "anyhow", "futures 0.3.31", diff --git a/Cargo.toml b/Cargo.toml index 3f54745900..d458a4752c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -423,7 +423,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.29" +agent-client-protocol = "0.0.30" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 9833e1957c..61bc50576a 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1381,7 +1381,7 @@ impl AcpThread { let canceled = matches!( result, Ok(Ok(acp::PromptResponse { - stop_reason: acp::StopReason::Canceled + stop_reason: acp::StopReason::Cancelled })) ); diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 791b161417..2bbd364873 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -420,7 +420,7 @@ mod test_support { .response_tx .take() { - end_turn_tx.send(acp::StopReason::Canceled).unwrap(); + end_turn_tx.send(acp::StopReason::Cancelled).unwrap(); } } diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 478604b14a..3bd1be497e 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -975,7 +975,7 @@ async fn test_cancellation(cx: &mut TestAppContext) { assert!( matches!( last_event, - Some(Ok(ThreadEvent::Stop(acp::StopReason::Canceled))) + Some(Ok(ThreadEvent::Stop(acp::StopReason::Cancelled))) ), "unexpected event {last_event:?}" ); @@ -1029,7 +1029,7 @@ async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) { fake_model.end_last_completion_stream(); let events_1 = events_1.collect::>().await; - assert_eq!(stop_events(events_1), vec![acp::StopReason::Canceled]); + assert_eq!(stop_events(events_1), vec![acp::StopReason::Cancelled]); let events_2 = events_2.collect::>().await; assert_eq!(stop_events(events_2), vec![acp::StopReason::EndTurn]); } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 62174fd3b4..d34c929152 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -2248,7 +2248,7 @@ impl ThreadEventStream { fn send_canceled(&self) { self.0 - .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::Canceled))) + .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::Cancelled))) .ok(); } diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index bc11a3748a..29f389547d 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -242,7 +242,7 @@ impl AgentConnection for AcpConnection { if suppress_abort_err && details.contains("This operation was aborted") { Ok(acp::PromptResponse { - stop_reason: acp::StopReason::Canceled, + stop_reason: acp::StopReason::Cancelled, }) } else { Err(anyhow!(details)) @@ -302,7 +302,7 @@ impl acp::Client for ClientDelegate { let outcome = match result { Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option }, - Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Canceled, + Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled, }; Ok(acp::RequestPermissionResponse { outcome }) diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 8d93557e1c..c9290e0ba5 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -705,7 +705,7 @@ impl ClaudeAgentSession { let stop_reason = match subtype { ResultErrorType::Success => acp::StopReason::EndTurn, ResultErrorType::ErrorMaxTurns => acp::StopReason::MaxTurnRequests, - ResultErrorType::ErrorDuringExecution => acp::StopReason::Canceled, + ResultErrorType::ErrorDuringExecution => acp::StopReason::Cancelled, }; end_turn_tx .send(Ok(acp::PromptResponse { stop_reason })) From 568e1d0a42a517b62ede343f31cee7779b09e9ea Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 21 Aug 2025 02:36:50 +0200 Subject: [PATCH 558/693] acp: Add e2e test support for NativeAgent (#36635) Release Notes: - N/A --- Cargo.lock | 4 + crates/agent2/Cargo.toml | 2 + crates/agent2/src/native_agent_server.rs | 49 ++++++++ crates/agent_servers/Cargo.toml | 11 +- crates/agent_servers/src/agent_servers.rs | 4 +- crates/agent_servers/src/claude.rs | 2 +- crates/agent_servers/src/e2e_tests.rs | 134 +++++++++++++++++----- crates/agent_servers/src/gemini.rs | 2 +- 8 files changed, 172 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f3e821fb5f..76f8672d4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -268,11 +268,14 @@ dependencies = [ "agent_settings", "agentic-coding-protocol", "anyhow", + "client", "collections", "context_server", "env_logger 0.11.8", + "fs", "futures 0.3.31", "gpui", + "gpui_tokio", "indoc", "itertools 0.14.0", "language", @@ -284,6 +287,7 @@ dependencies = [ "paths", "project", "rand 0.8.5", + "reqwest_client", "schemars", "semver", "serde", diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 2a5d879e9e..8dd79062f8 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -10,6 +10,7 @@ path = "src/agent2.rs" [features] test-support = ["db/test-support"] +e2e = [] [lints] workspace = true @@ -72,6 +73,7 @@ zstd.workspace = true [dev-dependencies] agent = { workspace = true, "features" = ["test-support"] } +agent_servers = { workspace = true, "features" = ["test-support"] } assistant_context = { workspace = true, "features" = ["test-support"] } ctor.workspace = true client = { workspace = true, "features" = ["test-support"] } diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index a1f935589a..ac5aa95c04 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -73,3 +73,52 @@ impl AgentServer for NativeAgentServer { self } } + +#[cfg(test)] +mod tests { + use super::*; + + use assistant_context::ContextStore; + use gpui::AppContext; + + agent_servers::e2e_tests::common_e2e_tests!( + async |fs, project, cx| { + let auth = cx.update(|cx| { + prompt_store::init(cx); + terminal::init(cx); + + let registry = language_model::LanguageModelRegistry::read_global(cx); + let auth = registry + .provider(&language_model::ANTHROPIC_PROVIDER_ID) + .unwrap() + .authenticate(cx); + + cx.spawn(async move |_| auth.await) + }); + + auth.await.unwrap(); + + cx.update(|cx| { + let registry = language_model::LanguageModelRegistry::global(cx); + + registry.update(cx, |registry, cx| { + registry.select_default_model( + Some(&language_model::SelectedModel { + provider: language_model::ANTHROPIC_PROVIDER_ID, + model: language_model::LanguageModelId("claude-sonnet-4-latest".into()), + }), + cx, + ); + }); + }); + + let history = cx.update(|cx| { + let context_store = cx.new(move |cx| ContextStore::fake(project.clone(), cx)); + cx.new(move |cx| HistoryStore::new(context_store, cx)) + }); + + NativeAgentServer::new(fs.clone(), history) + }, + allow_option_id = "allow" + ); +} diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index b654486cb6..60dd796463 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -6,7 +6,7 @@ publish.workspace = true license = "GPL-3.0-or-later" [features] -test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support"] +test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:env_logger", "fs", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"] e2e = [] [lints] @@ -23,10 +23,14 @@ agent-client-protocol.workspace = true agent_settings.workspace = true agentic-coding-protocol.workspace = true anyhow.workspace = true +client = { workspace = true, optional = true } collections.workspace = true context_server.workspace = true +env_logger = { workspace = true, optional = true } +fs = { workspace = true, optional = true } futures.workspace = true gpui.workspace = true +gpui_tokio = { workspace = true, optional = true } indoc.workspace = true itertools.workspace = true language.workspace = true @@ -36,6 +40,7 @@ log.workspace = true paths.workspace = true project.workspace = true rand.workspace = true +reqwest_client = { workspace = true, optional = true } schemars.workspace = true semver.workspace = true serde.workspace = true @@ -57,8 +62,12 @@ libc.workspace = true nix.workspace = true [dev-dependencies] +client = { workspace = true, features = ["test-support"] } env_logger.workspace = true +fs.workspace = true language.workspace = true indoc.workspace = true acp_thread = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } +gpui_tokio.workspace = true +reqwest_client = { workspace = true, features = ["test-support"] } diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index cebf82cddb..2f5ec478ae 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -3,8 +3,8 @@ mod claude; mod gemini; mod settings; -#[cfg(test)] -mod e2e_tests; +#[cfg(any(test, feature = "test-support"))] +pub mod e2e_tests; pub use claude::*; pub use gemini::*; diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index c9290e0ba5..ef666974f1 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -1093,7 +1093,7 @@ pub(crate) mod tests { use gpui::TestAppContext; use serde_json::json; - crate::common_e2e_tests!(ClaudeCode, allow_option_id = "allow"); + crate::common_e2e_tests!(async |_, _, _| ClaudeCode, allow_option_id = "allow"); pub fn local_command() -> AgentServerCommand { AgentServerCommand { diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 8b2703575d..c271079071 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -4,21 +4,30 @@ use std::{ time::Duration, }; -use crate::{AgentServer, AgentServerSettings, AllAgentServersSettings}; +use crate::AgentServer; use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus}; use agent_client_protocol as acp; use futures::{FutureExt, StreamExt, channel::mpsc, select}; -use gpui::{Entity, TestAppContext}; +use gpui::{AppContext, Entity, TestAppContext}; use indoc::indoc; use project::{FakeFs, Project}; -use settings::{Settings, SettingsStore}; use util::path; -pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppContext) { - let fs = init_test(cx).await; - let project = Project::test(fs, [], cx).await; - let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; +pub async fn test_basic(server: F, cx: &mut TestAppContext) +where + T: AgentServer + 'static, + F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, +{ + let fs = init_test(cx).await as Arc; + let project = Project::test(fs.clone(), [], cx).await; + let thread = new_test_thread( + server(&fs, &project, cx).await, + project.clone(), + "/private/tmp", + cx, + ) + .await; thread .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx)) @@ -42,8 +51,12 @@ pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppCont }); } -pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut TestAppContext) { - let _fs = init_test(cx).await; +pub async fn test_path_mentions(server: F, cx: &mut TestAppContext) +where + T: AgentServer + 'static, + F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, +{ + let fs = init_test(cx).await as _; let tempdir = tempfile::tempdir().unwrap(); std::fs::write( @@ -56,7 +69,13 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes ) .expect("failed to write file"); let project = Project::example([tempdir.path()], &mut cx.to_async()).await; - let thread = new_test_thread(server, project.clone(), tempdir.path(), cx).await; + let thread = new_test_thread( + server(&fs, &project, cx).await, + project.clone(), + tempdir.path(), + cx, + ) + .await; thread .update(cx, |thread, cx| { thread.send( @@ -110,15 +129,25 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes drop(tempdir); } -pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestAppContext) { - let _fs = init_test(cx).await; +pub async fn test_tool_call(server: F, cx: &mut TestAppContext) +where + T: AgentServer + 'static, + F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, +{ + let fs = init_test(cx).await as _; let tempdir = tempfile::tempdir().unwrap(); let foo_path = tempdir.path().join("foo"); std::fs::write(&foo_path, "Lorem ipsum dolor").expect("failed to write file"); let project = Project::example([tempdir.path()], &mut cx.to_async()).await; - let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; + let thread = new_test_thread( + server(&fs, &project, cx).await, + project.clone(), + "/private/tmp", + cx, + ) + .await; thread .update(cx, |thread, cx| { @@ -152,14 +181,23 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp drop(tempdir); } -pub async fn test_tool_call_with_permission( - server: impl AgentServer + 'static, +pub async fn test_tool_call_with_permission( + server: F, allow_option_id: acp::PermissionOptionId, cx: &mut TestAppContext, -) { - let fs = init_test(cx).await; - let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; - let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; +) where + T: AgentServer + 'static, + F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, +{ + let fs = init_test(cx).await as Arc; + let project = Project::test(fs.clone(), [path!("/private/tmp").as_ref()], cx).await; + let thread = new_test_thread( + server(&fs, &project, cx).await, + project.clone(), + "/private/tmp", + cx, + ) + .await; let full_turn = thread.update(cx, |thread, cx| { thread.send_raw( r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#, @@ -247,11 +285,21 @@ pub async fn test_tool_call_with_permission( }); } -pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppContext) { - let fs = init_test(cx).await; +pub async fn test_cancel(server: F, cx: &mut TestAppContext) +where + T: AgentServer + 'static, + F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, +{ + let fs = init_test(cx).await as Arc; - let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; - let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; + let project = Project::test(fs.clone(), [path!("/private/tmp").as_ref()], cx).await; + let thread = new_test_thread( + server(&fs, &project, cx).await, + project.clone(), + "/private/tmp", + cx, + ) + .await; let _ = thread.update(cx, |thread, cx| { thread.send_raw( r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#, @@ -316,10 +364,20 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon }); } -pub async fn test_thread_drop(server: impl AgentServer + 'static, cx: &mut TestAppContext) { - let fs = init_test(cx).await; - let project = Project::test(fs, [], cx).await; - let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; +pub async fn test_thread_drop(server: F, cx: &mut TestAppContext) +where + T: AgentServer + 'static, + F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, +{ + let fs = init_test(cx).await as Arc; + let project = Project::test(fs.clone(), [], cx).await; + let thread = new_test_thread( + server(&fs, &project, cx).await, + project.clone(), + "/private/tmp", + cx, + ) + .await; thread .update(cx, |thread, cx| thread.send_raw("Hello from test!", cx)) @@ -386,25 +444,39 @@ macro_rules! common_e2e_tests { } }; } +pub use common_e2e_tests; // Helpers pub async fn init_test(cx: &mut TestAppContext) -> Arc { + #[cfg(test)] + use settings::Settings; + env_logger::try_init().ok(); cx.update(|cx| { - let settings_store = SettingsStore::test(cx); + let settings_store = settings::SettingsStore::test(cx); cx.set_global(settings_store); Project::init_settings(cx); language::init(cx); + gpui_tokio::init(cx); + let http_client = reqwest_client::ReqwestClient::user_agent("agent tests").unwrap(); + cx.set_http_client(Arc::new(http_client)); + client::init_settings(cx); + let client = client::Client::production(cx); + let user_store = cx.new(|cx| client::UserStore::new(client.clone(), cx)); + language_model::init(client.clone(), cx); + language_models::init(user_store, client, cx); + agent_settings::init(cx); crate::settings::init(cx); + #[cfg(test)] crate::AllAgentServersSettings::override_global( - AllAgentServersSettings { - claude: Some(AgentServerSettings { + crate::AllAgentServersSettings { + claude: Some(crate::AgentServerSettings { command: crate::claude::tests::local_command(), }), - gemini: Some(AgentServerSettings { + gemini: Some(crate::AgentServerSettings { command: crate::gemini::tests::local_command(), }), }, diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index d30525328b..1a63322fac 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -108,7 +108,7 @@ pub(crate) mod tests { use crate::AgentServerCommand; use std::path::Path; - crate::common_e2e_tests!(Gemini, allow_option_id = "proceed_once"); + crate::common_e2e_tests!(async |_, _, _| Gemini, allow_option_id = "proceed_once"); pub fn local_command() -> AgentServerCommand { let cli_path = Path::new(env!("CARGO_MANIFEST_DIR")) From 9a3e4c47d03ab8579601ce55d066518a0e867c3a Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 20 Aug 2025 21:52:38 -0300 Subject: [PATCH 559/693] acp: Suggest upgrading to preview instead of latest (#36648) A previous PR changed the install command from `@latest` to `@preview`, but the upgrade command kept suggesting `@latest`. Release Notes: - N/A --- crates/agent_servers/src/gemini.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 1a63322fac..3b892e7931 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -89,7 +89,7 @@ impl AgentServer for Gemini { current_version ).into(), upgrade_message: "Upgrade Gemini CLI to latest".into(), - upgrade_command: "npm install -g @google/gemini-cli@latest".into(), + upgrade_command: "npm install -g @google/gemini-cli@preview".into(), }.into()) } } From 4b03d791b5ed73d9dd28bf1279b807648d38b399 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 20 Aug 2025 20:38:30 -0600 Subject: [PATCH 560/693] Remove style lints for now (#36651) Closes #36577 Release Notes: - N/A --- Cargo.toml | 151 +++++------------------------------------------------ 1 file changed, 13 insertions(+), 138 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d458a4752c..b6104303b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -802,147 +802,26 @@ unexpected_cfgs = { level = "allow" } dbg_macro = "deny" todo = "deny" -# Motivation: We use `vec![a..b]` a lot when dealing with ranges in text, so -# warning on this rule produces a lot of noise. -single_range_in_vec_init = "allow" - -redundant_clone = "warn" -declare_interior_mutable_const = "deny" - -# These are all of the rules that currently have violations in the Zed -# codebase. +# We currently do not restrict any style rules +# as it slows down shipping code to Zed. # -# We'll want to drive this list down by either: -# 1. fixing violations of the rule and begin enforcing it -# 2. deciding we want to allow the rule permanently, at which point -# we should codify that separately above. +# Running ./script/clippy can take several minutes, and so it's +# common to skip that step and let CI do it. Any unexpected failures +# (which also take minutes to discover) thus require switching back +# to an old branch, manual fixing, and re-pushing. # -# This list shouldn't be added to; it should only get shorter. -# ============================================================================= - -# There are a bunch of rules currently failing in the `style` group, so -# allow all of those, for now. +# In the future we could improve this by either making sure +# Zed can surface clippy errors in diagnostics (in addition to the +# rust-analyzer errors), or by having CI fix style nits automatically. style = { level = "allow", priority = -1 } -# Temporary list of style lints that we've fixed so far. -# Progress is being tracked in #36577 -blocks_in_conditions = "warn" -bool_assert_comparison = "warn" -borrow_interior_mutable_const = "warn" -box_default = "warn" -builtin_type_shadow = "warn" -bytes_nth = "warn" -chars_next_cmp = "warn" -cmp_null = "warn" -collapsible_else_if = "warn" -collapsible_if = "warn" -comparison_to_empty = "warn" -default_instead_of_iter_empty = "warn" -disallowed_macros = "warn" -disallowed_methods = "warn" -disallowed_names = "warn" -disallowed_types = "warn" -doc_lazy_continuation = "warn" -doc_overindented_list_items = "warn" -duplicate_underscore_argument = "warn" -err_expect = "warn" -fn_to_numeric_cast = "warn" -fn_to_numeric_cast_with_truncation = "warn" -for_kv_map = "warn" -implicit_saturating_add = "warn" -implicit_saturating_sub = "warn" -inconsistent_digit_grouping = "warn" -infallible_destructuring_match = "warn" -inherent_to_string = "warn" -init_numbered_fields = "warn" -into_iter_on_ref = "warn" -io_other_error = "warn" -items_after_test_module = "warn" -iter_cloned_collect = "warn" -iter_next_slice = "warn" -iter_nth = "warn" -iter_nth_zero = "warn" -iter_skip_next = "warn" -just_underscores_and_digits = "warn" -len_zero = "warn" -let_and_return = "warn" -main_recursion = "warn" -manual_bits = "warn" -manual_dangling_ptr = "warn" -manual_is_ascii_check = "warn" -manual_is_finite = "warn" -manual_is_infinite = "warn" -manual_map = "warn" -manual_next_back = "warn" -manual_non_exhaustive = "warn" -manual_ok_or = "warn" -manual_pattern_char_comparison = "warn" -manual_rotate = "warn" -manual_slice_fill = "warn" -manual_while_let_some = "warn" -map_clone = "warn" -map_collect_result_unit = "warn" -match_like_matches_macro = "warn" -match_overlapping_arm = "warn" -mem_replace_option_with_none = "warn" -mem_replace_option_with_some = "warn" -missing_enforced_import_renames = "warn" -missing_safety_doc = "warn" -mixed_attributes_style = "warn" -mixed_case_hex_literals = "warn" -module_inception = "warn" -must_use_unit = "warn" -mut_mutex_lock = "warn" -needless_borrow = "warn" -needless_doctest_main = "warn" -needless_else = "warn" -needless_parens_on_range_literals = "warn" -needless_pub_self = "warn" -needless_return = "warn" -needless_return_with_question_mark = "warn" -non_minimal_cfg = "warn" -ok_expect = "warn" -owned_cow = "warn" -print_literal = "warn" -print_with_newline = "warn" -println_empty_string = "warn" -ptr_eq = "warn" -question_mark = "warn" -redundant_closure = "warn" -redundant_field_names = "warn" -redundant_pattern_matching = "warn" -redundant_static_lifetimes = "warn" -result_map_or_into_option = "warn" -self_named_constructors = "warn" -single_match = "warn" -tabs_in_doc_comments = "warn" -to_digit_is_some = "warn" -toplevel_ref_arg = "warn" -unnecessary_fold = "warn" -unnecessary_map_or = "warn" -unnecessary_mut_passed = "warn" -unnecessary_owned_empty_strings = "warn" -unneeded_struct_pattern = "warn" -unsafe_removed_from_name = "warn" -unused_unit = "warn" -unusual_byte_groupings = "warn" -while_let_on_iterator = "warn" -write_literal = "warn" -write_with_newline = "warn" -writeln_empty_string = "warn" -wrong_self_convention = "warn" -zero_ptr = "warn" - # Individual rules that have violations in the codebase: type_complexity = "allow" -# We often return trait objects from `new` functions. -new_ret_no_self = { level = "allow" } -# We have a few `next` functions that differ in lifetimes -# compared to Iterator::next. Yet, clippy complains about those. -should_implement_trait = { level = "allow" } let_underscore_future = "allow" -# It doesn't make sense to implement `Default` unilaterally. -new_without_default = "allow" + +# Motivation: We use `vec![a..b]` a lot when dealing with ranges in text, so +# warning on this rule produces a lot of noise. +single_range_in_vec_init = "allow" # in Rust it can be very tedious to reduce argument count without # running afoul of the borrow checker. @@ -951,10 +830,6 @@ too_many_arguments = "allow" # We often have large enum variants yet we rarely actually bother with splitting them up. large_enum_variant = "allow" -# `enum_variant_names` fires for all enums, even when they derive serde traits. -# Adhering to this lint would be a breaking change. -enum_variant_names = "allow" - [workspace.metadata.cargo-machete] ignored = [ "bindgen", From c731bb6d91d0d8c1c0bf29d17c8cba8eed3b51a5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 20 Aug 2025 21:08:49 -0600 Subject: [PATCH 561/693] Re-add redundant clone (#36652) Although I said I'd do this, I actually didn't... Updates #36651 Release Notes: - N/A --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index b6104303b7..400ce791aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -802,6 +802,9 @@ unexpected_cfgs = { level = "allow" } dbg_macro = "deny" todo = "deny" +# trying this out +redundant_clone = "deny" + # We currently do not restrict any style rules # as it slows down shipping code to Zed. # From 5dcb90858effc47c7f2768b03ddb2a81b443ec8e Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 21 Aug 2025 09:24:34 +0300 Subject: [PATCH 562/693] Stop waiting for part of LSP responses on remote Collab clients' part (#36557) Instead of holding a connection for potentially long LSP queries (e.g. rust-analyzer might take minutes to look up a definition), disconnect right after sending the initial request and handle the follow-up responses later. As a bonus, this allows to cancel previously sent request on the local Collab clients' side due to this, as instead of holding and serving the old connection, local clients now can stop previous requests, if needed. Current PR does not convert all LSP requests to the new paradigm, but the problematic ones, deprecating `MultiLspQuery` and moving all its requests to the new paradigm. Release Notes: - Improved resource usage when querying LSP over Collab --------- Co-authored-by: David Kleingeld Co-authored-by: Mikayla Maki Co-authored-by: David Kleingeld --- crates/agent_ui/src/acp/message_editor.rs | 8 +- crates/collab/src/rpc.rs | 20 + crates/collab/src/tests/editor_tests.rs | 208 ++- crates/collab/src/tests/integration_tests.rs | 12 +- crates/editor/src/editor.rs | 24 +- crates/editor/src/hover_links.rs | 2 +- crates/editor/src/hover_popover.rs | 2 +- crates/editor/src/proposed_changes_editor.rs | 4 +- crates/editor/src/signature_help.rs | 4 +- crates/lsp/src/lsp.rs | 2 +- crates/project/src/lsp_command.rs | 3 +- crates/project/src/lsp_store.rs | 1202 ++++++++++-------- crates/project/src/project.rs | 49 +- crates/project/src/project_tests.rs | 8 +- crates/proto/proto/lsp.proto | 91 +- crates/proto/proto/zed.proto | 5 +- crates/proto/src/macros.rs | 29 + crates/proto/src/proto.rs | 45 + crates/proto/src/typed_envelope.rs | 52 + crates/rpc/src/proto_client.rs | 306 ++++- 20 files changed, 1395 insertions(+), 681 deletions(-) diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index be133808b7..1155285d09 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1691,7 +1691,7 @@ impl SemanticsProvider for SlashCommandSemanticsProvider { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>> { + ) -> Option>>> { let snapshot = buffer.read(cx).snapshot(); let offset = position.to_offset(&snapshot); let (start, end) = self.range.get()?; @@ -1699,14 +1699,14 @@ impl SemanticsProvider for SlashCommandSemanticsProvider { return None; } let range = snapshot.anchor_after(start)..snapshot.anchor_after(end); - Some(Task::ready(vec![project::Hover { + Some(Task::ready(Some(vec![project::Hover { contents: vec![project::HoverBlock { text: "Slash commands are not supported".into(), kind: project::HoverBlockKind::PlainText, }], range: Some(range), language: None, - }])) + }]))) } fn inline_values( @@ -1756,7 +1756,7 @@ impl SemanticsProvider for SlashCommandSemanticsProvider { _position: text::Anchor, _kind: editor::GotoDefinitionKind, _cx: &mut App, - ) -> Option>>> { + ) -> Option>>>> { None } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 06eb68610f..73f327166a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -400,6 +400,8 @@ impl Server { .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(multi_lsp_query) + .add_request_handler(lsp_query) + .add_message_handler(broadcast_project_message_from_host::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) @@ -910,7 +912,9 @@ impl Server { user_id=field::Empty, login=field::Empty, impersonator=field::Empty, + // todo(lsp) remove after Zed Stable hits v0.204.x multi_lsp_query_request=field::Empty, + lsp_query_request=field::Empty, release_channel=field::Empty, { TOTAL_DURATION_MS }=field::Empty, { PROCESSING_DURATION_MS }=field::Empty, @@ -2356,6 +2360,7 @@ where Ok(()) } +// todo(lsp) remove after Zed Stable hits v0.204.x async fn multi_lsp_query( request: MultiLspQuery, response: Response, @@ -2366,6 +2371,21 @@ async fn multi_lsp_query( forward_mutating_project_request(request, response, session).await } +async fn lsp_query( + request: proto::LspQuery, + response: Response, + session: MessageContext, +) -> Result<()> { + let (name, should_write) = request.query_name_and_write_permissions(); + tracing::Span::current().record("lsp_query_request", name); + tracing::info!("lsp_query message received"); + if should_write { + forward_mutating_project_request(request, response, session).await + } else { + forward_read_only_project_request(request, response, session).await + } +} + /// Notify other participants that a new buffer has been created async fn create_buffer_for_peer( request: proto::CreateBufferForPeer, diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 1b0c581983..59d66f1821 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -15,13 +15,14 @@ use editor::{ }, }; use fs::Fs; -use futures::{StreamExt, lock::Mutex}; +use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex}; use gpui::{App, Rgba, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext}; use indoc::indoc; use language::{ FakeLspAdapter, language_settings::{AllLanguageSettings, InlayHintSettings}, }; +use lsp::LSP_REQUEST_TIMEOUT; use project::{ ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT, lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro}, @@ -1017,6 +1018,211 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T }) } +#[gpui::test] +async fn test_slow_lsp_server(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let mut server = TestServer::start(cx_a.executor()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + cx_b.update(editor::init); + + let command_name = "test_command"; + let capabilities = lsp::ServerCapabilities { + code_lens_provider: Some(lsp::CodeLensOptions { + resolve_provider: None, + }), + execute_command_provider: Some(lsp::ExecuteCommandOptions { + commands: vec![command_name.to_string()], + ..lsp::ExecuteCommandOptions::default() + }), + ..lsp::ServerCapabilities::default() + }; + client_a.language_registry().add(rust_lang()); + let mut fake_language_servers = client_a.language_registry().register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: capabilities.clone(), + ..FakeLspAdapter::default() + }, + ); + client_b.language_registry().add(rust_lang()); + client_b.language_registry().register_fake_lsp_adapter( + "Rust", + FakeLspAdapter { + capabilities, + ..FakeLspAdapter::default() + }, + ); + + client_a + .fs() + .insert_tree( + path!("/dir"), + json!({ + "one.rs": "const ONE: usize = 1;" + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.join_remote_project(project_id, cx_b).await; + + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let editor_b = workspace_b + .update_in(cx_b, |workspace, window, cx| { + workspace.open_path((worktree_id, "one.rs"), None, true, window, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let (lsp_store_b, buffer_b) = editor_b.update(cx_b, |editor, cx| { + let lsp_store = editor.project().unwrap().read(cx).lsp_store(); + let buffer = editor.buffer().read(cx).as_singleton().unwrap(); + (lsp_store, buffer) + }); + let fake_language_server = fake_language_servers.next().await.unwrap(); + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + let long_request_time = LSP_REQUEST_TIMEOUT / 2; + let (request_started_tx, mut request_started_rx) = mpsc::unbounded(); + let requests_started = Arc::new(AtomicUsize::new(0)); + let requests_completed = Arc::new(AtomicUsize::new(0)); + let _lens_requests = fake_language_server + .set_request_handler::({ + let request_started_tx = request_started_tx.clone(); + let requests_started = requests_started.clone(); + let requests_completed = requests_completed.clone(); + move |params, cx| { + let mut request_started_tx = request_started_tx.clone(); + let requests_started = requests_started.clone(); + let requests_completed = requests_completed.clone(); + async move { + assert_eq!( + params.text_document.uri.as_str(), + uri!("file:///dir/one.rs") + ); + requests_started.fetch_add(1, atomic::Ordering::Release); + request_started_tx.send(()).await.unwrap(); + cx.background_executor().timer(long_request_time).await; + let i = requests_completed.fetch_add(1, atomic::Ordering::Release) + 1; + Ok(Some(vec![lsp::CodeLens { + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 9)), + command: Some(lsp::Command { + title: format!("LSP Command {i}"), + command: command_name.to_string(), + arguments: None, + }), + data: None, + }])) + } + } + }); + + // Move cursor to a location, this should trigger the code lens call. + editor_b.update_in(cx_b, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([7..7]) + }); + }); + let () = request_started_rx.next().await.unwrap(); + assert_eq!( + requests_started.load(atomic::Ordering::Acquire), + 1, + "Selection change should have initiated the first request" + ); + assert_eq!( + requests_completed.load(atomic::Ordering::Acquire), + 0, + "Slow requests should be running still" + ); + let _first_task = lsp_store_b.update(cx_b, |lsp_store, cx| { + lsp_store + .forget_code_lens_task(buffer_b.read(cx).remote_id()) + .expect("Should have the fetch task started") + }); + + editor_b.update_in(cx_b, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([1..1]) + }); + }); + let () = request_started_rx.next().await.unwrap(); + assert_eq!( + requests_started.load(atomic::Ordering::Acquire), + 2, + "Selection change should have initiated the second request" + ); + assert_eq!( + requests_completed.load(atomic::Ordering::Acquire), + 0, + "Slow requests should be running still" + ); + let _second_task = lsp_store_b.update(cx_b, |lsp_store, cx| { + lsp_store + .forget_code_lens_task(buffer_b.read(cx).remote_id()) + .expect("Should have the fetch task started for the 2nd time") + }); + + editor_b.update_in(cx_b, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([2..2]) + }); + }); + let () = request_started_rx.next().await.unwrap(); + assert_eq!( + requests_started.load(atomic::Ordering::Acquire), + 3, + "Selection change should have initiated the third request" + ); + assert_eq!( + requests_completed.load(atomic::Ordering::Acquire), + 0, + "Slow requests should be running still" + ); + + _first_task.await.unwrap(); + _second_task.await.unwrap(); + cx_b.run_until_parked(); + assert_eq!( + requests_started.load(atomic::Ordering::Acquire), + 3, + "No selection changes should trigger no more code lens requests" + ); + assert_eq!( + requests_completed.load(atomic::Ordering::Acquire), + 3, + "After enough time, all 3 LSP requests should have been served by the language server" + ); + let resulting_lens_actions = editor_b + .update(cx_b, |editor, cx| { + let lsp_store = editor.project().unwrap().read(cx).lsp_store(); + lsp_store.update(cx, |lsp_store, cx| { + lsp_store.code_lens_actions(&buffer_b, cx) + }) + }) + .await + .unwrap() + .unwrap(); + assert_eq!( + resulting_lens_actions.len(), + 1, + "Should have fetched one code lens action, but got: {resulting_lens_actions:?}" + ); + assert_eq!( + resulting_lens_actions.first().unwrap().lsp_action.title(), + "LSP Command 3", + "Only the final code lens action should be in the data" + ) +} + #[gpui::test(iterations = 10)] async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.executor()).await; diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index e01736f0ef..5c73253048 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -4850,6 +4850,7 @@ async fn test_definition( let definitions_1 = project_b .update(cx_b, |p, cx| p.definitions(&buffer_b, 23, cx)) .await + .unwrap() .unwrap(); cx_b.read(|cx| { assert_eq!( @@ -4885,6 +4886,7 @@ async fn test_definition( let definitions_2 = project_b .update(cx_b, |p, cx| p.definitions(&buffer_b, 33, cx)) .await + .unwrap() .unwrap(); cx_b.read(|cx| { assert_eq!(definitions_2.len(), 1); @@ -4922,6 +4924,7 @@ async fn test_definition( let type_definitions = project_b .update(cx_b, |p, cx| p.type_definitions(&buffer_b, 7, cx)) .await + .unwrap() .unwrap(); cx_b.read(|cx| { assert_eq!( @@ -5060,7 +5063,7 @@ async fn test_references( ]))) .unwrap(); - let references = references.await.unwrap(); + let references = references.await.unwrap().unwrap(); executor.run_until_parked(); project_b.read_with(cx_b, |project, cx| { // User is informed that a request is no longer pending. @@ -5104,7 +5107,7 @@ async fn test_references( lsp_response_tx .unbounded_send(Err(anyhow!("can't find references"))) .unwrap(); - assert_eq!(references.await.unwrap(), []); + assert_eq!(references.await.unwrap().unwrap(), []); // User is informed that the request is no longer pending. executor.run_until_parked(); @@ -5505,7 +5508,8 @@ async fn test_lsp_hover( // Request hover information as the guest. let mut hovers = project_b .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx)) - .await; + .await + .unwrap(); assert_eq!( hovers.len(), 2, @@ -5764,7 +5768,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx)); } - let definitions = definitions.await.unwrap(); + let definitions = definitions.await.unwrap().unwrap(); assert_eq!( definitions.len(), 1, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 25fddf5cf1..e32ea1cb3a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -15710,7 +15710,9 @@ impl Editor { }; cx.spawn_in(window, async move |editor, cx| { - let definitions = definitions.await?; + let Some(definitions) = definitions.await? else { + return Ok(Navigated::No); + }; let navigated = editor .update_in(cx, |editor, window, cx| { editor.navigate_to_hover_links( @@ -16052,7 +16054,9 @@ impl Editor { } }); - let locations = references.await?; + let Some(locations) = references.await? else { + return anyhow::Ok(Navigated::No); + }; if locations.is_empty() { return anyhow::Ok(Navigated::No); } @@ -21837,7 +21841,7 @@ pub trait SemanticsProvider { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>>; + ) -> Option>>>; fn inline_values( &self, @@ -21876,7 +21880,7 @@ pub trait SemanticsProvider { position: text::Anchor, kind: GotoDefinitionKind, cx: &mut App, - ) -> Option>>>; + ) -> Option>>>>; fn range_for_rename( &self, @@ -21989,7 +21993,13 @@ impl CodeActionProvider for Entity { Ok(code_lens_actions .context("code lens fetch")? .into_iter() - .chain(code_actions.context("code action fetch")?) + .flatten() + .chain( + code_actions + .context("code action fetch")? + .into_iter() + .flatten(), + ) .collect()) }) }) @@ -22284,7 +22294,7 @@ impl SemanticsProvider for Entity { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>> { + ) -> Option>>> { Some(self.update(cx, |project, cx| project.hover(buffer, position, cx))) } @@ -22305,7 +22315,7 @@ impl SemanticsProvider for Entity { position: text::Anchor, kind: GotoDefinitionKind, cx: &mut App, - ) -> Option>>> { + ) -> Option>>>> { Some(self.update(cx, |project, cx| match kind { GotoDefinitionKind::Symbol => project.definitions(buffer, position, cx), GotoDefinitionKind::Declaration => project.declarations(buffer, position, cx), diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 04e66a234c..1d7d56e67d 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -559,7 +559,7 @@ pub fn show_link_definition( provider.definitions(&buffer, buffer_position, preferred_kind, cx) })?; if let Some(task) = task { - task.await.ok().map(|definition_result| { + task.await.ok().flatten().map(|definition_result| { ( definition_result.iter().find_map(|link| { link.origin.as_ref().and_then(|origin| { diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 28a09e947f..fab5345787 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -428,7 +428,7 @@ fn show_hover( }; let hovers_response = if let Some(hover_request) = hover_request { - hover_request.await + hover_request.await.unwrap_or_default() } else { Vec::new() }; diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index c79feccb4b..2d4710a8d4 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -431,7 +431,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>> { + ) -> Option>>> { let buffer = self.to_base(buffer, &[position], cx)?; self.0.hover(&buffer, position, cx) } @@ -490,7 +490,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { position: text::Anchor, kind: crate::GotoDefinitionKind, cx: &mut App, - ) -> Option>>> { + ) -> Option>>>> { let buffer = self.to_base(buffer, &[position], cx)?; self.0.definitions(&buffer, position, kind, cx) } diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs index 5c9800ab55..cb21f35d7e 100644 --- a/crates/editor/src/signature_help.rs +++ b/crates/editor/src/signature_help.rs @@ -182,7 +182,9 @@ impl Editor { let signature_help = task.await; editor .update(cx, |editor, cx| { - let Some(mut signature_help) = signature_help.into_iter().next() else { + let Some(mut signature_help) = + signature_help.unwrap_or_default().into_iter().next() + else { editor .signature_help_state .hide(SignatureHelpHiddenBy::AutoClose); diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index ce9e2fe229..942225d098 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -45,7 +45,7 @@ use util::{ConnectionResult, ResultExt, TryFutureExt, redact}; const JSON_RPC_VERSION: &str = "2.0"; const CONTENT_LEN_HEADER: &str = "Content-Length: "; -const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2); +pub const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2); const SERVER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); type NotificationHandler = Box, Value, &mut AsyncApp)>; diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index c90d85358a..ce7a871d1a 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -3444,8 +3444,7 @@ impl LspCommand for GetCodeLens { capabilities .server_capabilities .code_lens_provider - .as_ref() - .is_some_and(|code_lens_options| code_lens_options.resolve_provider.unwrap_or(false)) + .is_some() } fn to_lsp( diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 0b58009f37..bcfd9d386b 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -72,10 +72,11 @@ use lsp::{ AdapterServerCapabilities, CodeActionKind, CompletionContext, DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter, FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher, - LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, - LanguageServerName, LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, - OneOf, RenameFilesParams, SymbolKind, TextDocumentSyncSaveOptions, TextEdit, WillRenameFiles, - WorkDoneProgressCancelParams, WorkspaceFolder, notification::DidRenameFiles, + LSP_REQUEST_TIMEOUT, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, + LanguageServerId, LanguageServerName, LanguageServerSelector, LspRequestFuture, + MessageActionItem, MessageType, OneOf, RenameFilesParams, SymbolKind, + TextDocumentSyncSaveOptions, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams, + WorkspaceFolder, notification::DidRenameFiles, }; use node_runtime::read_package_installed_version; use parking_lot::Mutex; @@ -84,7 +85,7 @@ use rand::prelude::*; use rpc::{ AnyProtoClient, - proto::{FromProto, ToProto}, + proto::{FromProto, LspRequestId, LspRequestMessage as _, ToProto}, }; use serde::Serialize; use settings::{Settings, SettingsLocation, SettingsStore}; @@ -92,7 +93,7 @@ use sha2::{Digest, Sha256}; use smol::channel::Sender; use snippet::Snippet; use std::{ - any::Any, + any::{Any, TypeId}, borrow::Cow, cell::RefCell, cmp::{Ordering, Reverse}, @@ -3490,6 +3491,7 @@ pub struct LspStore { pub(super) lsp_server_capabilities: HashMap, lsp_document_colors: HashMap, lsp_code_lens: HashMap, + running_lsp_requests: HashMap>)>, } #[derive(Debug, Default, Clone)] @@ -3499,7 +3501,7 @@ pub struct DocumentColors { } type DocumentColorTask = Shared>>>; -type CodeLensTask = Shared, Arc>>>; +type CodeLensTask = Shared>, Arc>>>; #[derive(Debug, Default)] struct DocumentColorData { @@ -3579,6 +3581,8 @@ struct CoreSymbol { impl LspStore { pub fn init(client: &AnyProtoClient) { + client.add_entity_request_handler(Self::handle_lsp_query); + client.add_entity_message_handler(Self::handle_lsp_query_response); client.add_entity_request_handler(Self::handle_multi_lsp_query); client.add_entity_request_handler(Self::handle_restart_language_servers); client.add_entity_request_handler(Self::handle_stop_language_servers); @@ -3758,6 +3762,7 @@ impl LspStore { lsp_server_capabilities: HashMap::default(), lsp_document_colors: HashMap::default(), lsp_code_lens: HashMap::default(), + running_lsp_requests: HashMap::default(), active_entry: None, _maintain_workspace_config, _maintain_buffer_languages: Self::maintain_buffer_languages(languages, cx), @@ -3819,6 +3824,7 @@ impl LspStore { lsp_server_capabilities: HashMap::default(), lsp_document_colors: HashMap::default(), lsp_code_lens: HashMap::default(), + running_lsp_requests: HashMap::default(), active_entry: None, _maintain_workspace_config, @@ -4381,8 +4387,6 @@ impl LspStore { } } - // TODO: remove MultiLspQuery: instead, the proto handler should pick appropriate server(s) - // Then, use `send_lsp_proto_request` or analogue for most of the LSP proto requests and inline this check inside fn is_capable_for_proto_request( &self, buffer: &Entity, @@ -5233,154 +5237,130 @@ impl LspStore { pub fn definitions( &mut self, - buffer_handle: &Entity, + buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetDefinitions { position }; - if !self.is_capable_for_proto_request(buffer_handle, &request, cx) { - return Task::ready(Ok(Vec::new())); + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Ok(None)); } - let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer_handle.read(cx).remote_id().into(), - version: serialize_version(&buffer_handle.read(cx).version()), + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetDefinition( - request.to_proto(project_id, buffer_handle.read(cx)), - )), - }); - let buffer = buffer_handle.clone(); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); + let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(Vec::new()); + return Ok(None); }; - let responses = request_task.await?.responses; - let actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetDefinitionResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|definitions_response| { - GetDefinitions { position }.response_from_proto( - definitions_response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - }), - ) + let Some(responses) = request_task.await? else { + return Ok(None); + }; + let actions = join_all(responses.payload.into_iter().map(|response| { + GetDefinitions { position }.response_from_proto( + response.response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + })) .await; - Ok(actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect()) + Ok(Some( + actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect(), + )) }) } else { let definitions_task = self.request_multiple_lsp_locally( - buffer_handle, + buffer, Some(position), GetDefinitions { position }, cx, ); cx.background_spawn(async move { - Ok(definitions_task - .await - .into_iter() - .flat_map(|(_, definitions)| definitions) - .dedup() - .collect()) + Ok(Some( + definitions_task + .await + .into_iter() + .flat_map(|(_, definitions)| definitions) + .dedup() + .collect(), + )) }) } } pub fn declarations( &mut self, - buffer_handle: &Entity, + buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetDeclarations { position }; - if !self.is_capable_for_proto_request(buffer_handle, &request, cx) { - return Task::ready(Ok(Vec::new())); + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Ok(None)); } - let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer_handle.read(cx).remote_id().into(), - version: serialize_version(&buffer_handle.read(cx).version()), + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetDeclaration( - request.to_proto(project_id, buffer_handle.read(cx)), - )), - }); - let buffer = buffer_handle.clone(); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); + let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(Vec::new()); + return Ok(None); }; - let responses = request_task.await?.responses; - let actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetDeclarationResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|declarations_response| { - GetDeclarations { position }.response_from_proto( - declarations_response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - }), - ) + let Some(responses) = request_task.await? else { + return Ok(None); + }; + let actions = join_all(responses.payload.into_iter().map(|response| { + GetDeclarations { position }.response_from_proto( + response.response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + })) .await; - Ok(actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect()) + Ok(Some( + actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect(), + )) }) } else { let declarations_task = self.request_multiple_lsp_locally( - buffer_handle, + buffer, Some(position), GetDeclarations { position }, cx, ); cx.background_spawn(async move { - Ok(declarations_task - .await - .into_iter() - .flat_map(|(_, declarations)| declarations) - .dedup() - .collect()) + Ok(Some( + declarations_task + .await + .into_iter() + .flat_map(|(_, declarations)| declarations) + .dedup() + .collect(), + )) }) } } @@ -5390,59 +5370,45 @@ impl LspStore { buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetTypeDefinitions { position }; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(Vec::new())); + return Task::ready(Ok(None)); } - let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetTypeDefinition( - request.to_proto(project_id, buffer.read(cx)), - )), - }); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(Vec::new()); + return Ok(None); }; - let responses = request_task.await?.responses; - let actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetTypeDefinitionResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|type_definitions_response| { - GetTypeDefinitions { position }.response_from_proto( - type_definitions_response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - }), - ) + let Some(responses) = request_task.await? else { + return Ok(None); + }; + let actions = join_all(responses.payload.into_iter().map(|response| { + GetTypeDefinitions { position }.response_from_proto( + response.response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + })) .await; - Ok(actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect()) + Ok(Some( + actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect(), + )) }) } else { let type_definitions_task = self.request_multiple_lsp_locally( @@ -5452,12 +5418,14 @@ impl LspStore { cx, ); cx.background_spawn(async move { - Ok(type_definitions_task - .await - .into_iter() - .flat_map(|(_, type_definitions)| type_definitions) - .dedup() - .collect()) + Ok(Some( + type_definitions_task + .await + .into_iter() + .flat_map(|(_, type_definitions)| type_definitions) + .dedup() + .collect(), + )) }) } } @@ -5467,59 +5435,45 @@ impl LspStore { buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetImplementations { position }; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(Vec::new())); + return Task::ready(Ok(None)); } - let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetImplementation( - request.to_proto(project_id, buffer.read(cx)), - )), - }); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(Vec::new()); + return Ok(None); }; - let responses = request_task.await?.responses; - let actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetImplementationResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|implementations_response| { - GetImplementations { position }.response_from_proto( - implementations_response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - }), - ) + let Some(responses) = request_task.await? else { + return Ok(None); + }; + let actions = join_all(responses.payload.into_iter().map(|response| { + GetImplementations { position }.response_from_proto( + response.response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + })) .await; - Ok(actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect()) + Ok(Some( + actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect(), + )) }) } else { let implementations_task = self.request_multiple_lsp_locally( @@ -5529,12 +5483,14 @@ impl LspStore { cx, ); cx.background_spawn(async move { - Ok(implementations_task - .await - .into_iter() - .flat_map(|(_, implementations)| implementations) - .dedup() - .collect()) + Ok(Some( + implementations_task + .await + .into_iter() + .flat_map(|(_, implementations)| implementations) + .dedup() + .collect(), + )) }) } } @@ -5544,59 +5500,44 @@ impl LspStore { buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetReferences { position }; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(Vec::new())); + return Task::ready(Ok(None)); } - let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), + + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetReferences( - request.to_proto(project_id, buffer.read(cx)), - )), - }); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(Vec::new()); + return Ok(None); + }; + let Some(responses) = request_task.await? else { + return Ok(None); }; - let responses = request_task.await?.responses; - let actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetReferencesResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|references_response| { - GetReferences { position }.response_from_proto( - references_response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - }), - ) - .await; - Ok(actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect()) + let locations = join_all(responses.payload.into_iter().map(|lsp_response| { + GetReferences { position }.response_from_proto( + lsp_response.response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + })) + .await + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect(); + Ok(Some(locations)) }) } else { let references_task = self.request_multiple_lsp_locally( @@ -5606,12 +5547,14 @@ impl LspStore { cx, ); cx.background_spawn(async move { - Ok(references_task - .await - .into_iter() - .flat_map(|(_, references)| references) - .dedup() - .collect()) + Ok(Some( + references_task + .await + .into_iter() + .flat_map(|(_, references)| references) + .dedup() + .collect(), + )) }) } } @@ -5622,65 +5565,51 @@ impl LspStore { range: Range, kinds: Option>, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetCodeActions { range: range.clone(), kinds: kinds.clone(), }; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(Vec::new())); + return Task::ready(Ok(None)); } - let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetCodeActions( - request.to_proto(project_id, buffer.read(cx)), - )), - }); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(Vec::new()); + return Ok(None); }; - let responses = request_task.await?.responses; - let actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetCodeActionsResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|code_actions_response| { - GetCodeActions { - range: range.clone(), - kinds: kinds.clone(), - } - .response_from_proto( - code_actions_response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - }), - ) + let Some(responses) = request_task.await? else { + return Ok(None); + }; + let actions = join_all(responses.payload.into_iter().map(|response| { + GetCodeActions { + range: range.clone(), + kinds: kinds.clone(), + } + .response_from_proto( + response.response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + })) .await; - Ok(actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .collect()) + Ok(Some( + actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .collect(), + )) }) } else { let all_actions_task = self.request_multiple_lsp_locally( @@ -5690,11 +5619,13 @@ impl LspStore { cx, ); cx.background_spawn(async move { - Ok(all_actions_task - .await - .into_iter() - .flat_map(|(_, actions)| actions) - .collect()) + Ok(Some( + all_actions_task + .await + .into_iter() + .flat_map(|(_, actions)| actions) + .collect(), + )) }) } } @@ -5719,8 +5650,10 @@ impl LspStore { != cached_data.lens.keys().copied().collect() }); if !has_different_servers { - return Task::ready(Ok(cached_data.lens.values().flatten().cloned().collect())) - .shared(); + return Task::ready(Ok(Some( + cached_data.lens.values().flatten().cloned().collect(), + ))) + .shared(); } } @@ -5758,17 +5691,19 @@ impl LspStore { lsp_store .update(cx, |lsp_store, _| { let lsp_data = lsp_store.lsp_code_lens.entry(buffer_id).or_default(); - if lsp_data.lens_for_version == query_version_queried_for { - lsp_data.lens.extend(fetched_lens.clone()); - } else if !lsp_data - .lens_for_version - .changed_since(&query_version_queried_for) - { - lsp_data.lens_for_version = query_version_queried_for; - lsp_data.lens = fetched_lens.clone(); + if let Some(fetched_lens) = fetched_lens { + if lsp_data.lens_for_version == query_version_queried_for { + lsp_data.lens.extend(fetched_lens.clone()); + } else if !lsp_data + .lens_for_version + .changed_since(&query_version_queried_for) + { + lsp_data.lens_for_version = query_version_queried_for; + lsp_data.lens = fetched_lens.clone(); + } } lsp_data.update = None; - lsp_data.lens.values().flatten().cloned().collect() + Some(lsp_data.lens.values().flatten().cloned().collect()) }) .map_err(Arc::new) }) @@ -5781,64 +5716,40 @@ impl LspStore { &mut self, buffer: &Entity, cx: &mut Context, - ) -> Task>>> { + ) -> Task>>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetCodeLens; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(HashMap::default())); + return Task::ready(Ok(None)); } - let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), + let request_task = upstream_client.request_lsp( project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetCodeLens( - request.to_proto(project_id, buffer.read(cx)), - )), - }); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_lsp_store, cx| { let Some(lsp_store) = weak_lsp_store.upgrade() else { - return Ok(HashMap::default()); + return Ok(None); }; - let responses = request_task.await?.responses; - let code_lens_actions = join_all( - responses - .into_iter() - .filter_map(|lsp_response| { - let response = match lsp_response.response? { - proto::lsp_response::Response::GetCodeLensResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }?; - let server_id = LanguageServerId::from_proto(lsp_response.server_id); - Some((server_id, response)) - }) - .map(|(server_id, code_lens_response)| { - let lsp_store = lsp_store.clone(); - let buffer = buffer.clone(); - let cx = cx.clone(); - async move { - ( - server_id, - GetCodeLens - .response_from_proto( - code_lens_response, - lsp_store, - buffer, - cx, - ) - .await, - ) - } - }), - ) + let Some(responses) = request_task.await? else { + return Ok(None); + }; + + let code_lens_actions = join_all(responses.payload.into_iter().map(|response| { + let lsp_store = lsp_store.clone(); + let buffer = buffer.clone(); + let cx = cx.clone(); + async move { + ( + LanguageServerId::from_proto(response.server_id), + GetCodeLens + .response_from_proto(response.response, lsp_store, buffer, cx) + .await, + ) + } + })) .await; let mut has_errors = false; @@ -5857,14 +5768,14 @@ impl LspStore { !has_errors || !code_lens_actions.is_empty(), "Failed to fetch code lens" ); - Ok(code_lens_actions) + Ok(Some(code_lens_actions)) }) } else { let code_lens_actions_task = self.request_multiple_lsp_locally(buffer, None::, GetCodeLens, cx); - cx.background_spawn( - async move { Ok(code_lens_actions_task.await.into_iter().collect()) }, - ) + cx.background_spawn(async move { + Ok(Some(code_lens_actions_task.await.into_iter().collect())) + }) } } @@ -6480,48 +6391,23 @@ impl LspStore { let buffer_id = buffer.read(cx).remote_id(); if let Some((client, upstream_project_id)) = self.upstream_client() { - if !self.is_capable_for_proto_request( - &buffer, - &GetDocumentDiagnostics { - previous_result_id: None, - }, - cx, - ) { + let request = GetDocumentDiagnostics { + previous_result_id: None, + }; + if !self.is_capable_for_proto_request(&buffer, &request, cx) { return Task::ready(Ok(None)); } - let request_task = client.request(proto::MultiLspQuery { - buffer_id: buffer_id.to_proto(), - version: serialize_version(&buffer.read(cx).version()), - project_id: upstream_project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetDocumentDiagnostics( - proto::GetDocumentDiagnostics { - project_id: upstream_project_id, - buffer_id: buffer_id.to_proto(), - version: serialize_version(&buffer.read(cx).version()), - }, - )), - }); + let request_task = client.request_lsp( + upstream_project_id, + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(upstream_project_id, buffer.read(cx)), + ); cx.background_spawn(async move { - let _proto_responses = request_task - .await? - .responses - .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetDocumentDiagnosticsResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .collect::>(); // Proto requests cause the diagnostics to be pulled from language server(s) on the local side // and then, buffer state updated with the diagnostics received, which will be later propagated to the client. // Do not attempt to further process the dummy responses here. + let _response = request_task.await?; Ok(None) }) } else { @@ -6806,16 +6692,18 @@ impl LspStore { .update(cx, |lsp_store, _| { let lsp_data = lsp_store.lsp_document_colors.entry(buffer_id).or_default(); - if lsp_data.colors_for_version == query_version_queried_for { - lsp_data.colors.extend(fetched_colors.clone()); - lsp_data.cache_version += 1; - } else if !lsp_data - .colors_for_version - .changed_since(&query_version_queried_for) - { - lsp_data.colors_for_version = query_version_queried_for; - lsp_data.colors = fetched_colors.clone(); - lsp_data.cache_version += 1; + if let Some(fetched_colors) = fetched_colors { + if lsp_data.colors_for_version == query_version_queried_for { + lsp_data.colors.extend(fetched_colors.clone()); + lsp_data.cache_version += 1; + } else if !lsp_data + .colors_for_version + .changed_since(&query_version_queried_for) + { + lsp_data.colors_for_version = query_version_queried_for; + lsp_data.colors = fetched_colors.clone(); + lsp_data.cache_version += 1; + } } lsp_data.colors_update = None; let colors = lsp_data @@ -6840,56 +6728,45 @@ impl LspStore { &mut self, buffer: &Entity, cx: &mut Context, - ) -> Task>>> { + ) -> Task>>>> { if let Some((client, project_id)) = self.upstream_client() { let request = GetDocumentColor {}; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(HashMap::default())); + return Task::ready(Ok(None)); } - let request_task = client.request(proto::MultiLspQuery { + let request_task = client.request_lsp( project_id, - buffer_id: buffer.read(cx).remote_id().to_proto(), - version: serialize_version(&buffer.read(cx).version()), - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetDocumentColor( - request.to_proto(project_id, buffer.read(cx)), - )), - }); + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); - cx.spawn(async move |project, cx| { - let Some(project) = project.upgrade() else { - return Ok(HashMap::default()); + cx.spawn(async move |lsp_store, cx| { + let Some(project) = lsp_store.upgrade() else { + return Ok(None); }; let colors = join_all( request_task .await .log_err() - .map(|response| response.responses) + .flatten() + .map(|response| response.payload) .unwrap_or_default() .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetDocumentColorResponse(response) => { - Some(( - LanguageServerId::from_proto(lsp_response.server_id), - response, - )) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|(server_id, color_response)| { + .map(|color_response| { let response = request.response_from_proto( - color_response, + color_response.response, project.clone(), buffer.clone(), cx.clone(), ); - async move { (server_id, response.await.log_err().unwrap_or_default()) } + async move { + ( + LanguageServerId::from_proto(color_response.server_id), + response.await.log_err().unwrap_or_default(), + ) + } }), ) .await @@ -6900,23 +6777,25 @@ impl LspStore { .extend(colors); acc }); - Ok(colors) + Ok(Some(colors)) }) } else { let document_colors_task = self.request_multiple_lsp_locally(buffer, None::, GetDocumentColor, cx); cx.background_spawn(async move { - Ok(document_colors_task - .await - .into_iter() - .fold(HashMap::default(), |mut acc, (server_id, colors)| { - acc.entry(server_id) - .or_insert_with(HashSet::default) - .extend(colors); - acc - }) - .into_iter() - .collect()) + Ok(Some( + document_colors_task + .await + .into_iter() + .fold(HashMap::default(), |mut acc, (server_id, colors)| { + acc.entry(server_id) + .or_insert_with(HashSet::default) + .extend(colors); + acc + }) + .into_iter() + .collect(), + )) }) } } @@ -6926,49 +6805,34 @@ impl LspStore { buffer: &Entity, position: T, cx: &mut Context, - ) -> Task> { + ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); if let Some((client, upstream_project_id)) = self.upstream_client() { let request = GetSignatureHelp { position }; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Vec::new()); + return Task::ready(None); } - let request_task = client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), - project_id: upstream_project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetSignatureHelp( - request.to_proto(upstream_project_id, buffer.read(cx)), - )), - }); + let request_task = client.request_lsp( + upstream_project_id, + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(upstream_project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { - let Some(project) = weak_project.upgrade() else { - return Vec::new(); - }; - join_all( + let project = weak_project.upgrade()?; + let signatures = join_all( request_task .await .log_err() - .map(|response| response.responses) + .flatten() + .map(|response| response.payload) .unwrap_or_default() .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetSignatureHelpResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|signature_response| { + .map(|response| { let response = GetSignatureHelp { position }.response_from_proto( - signature_response, + response.response, project.clone(), buffer.clone(), cx.clone(), @@ -6979,7 +6843,8 @@ impl LspStore { .await .into_iter() .flatten() - .collect() + .collect(); + Some(signatures) }) } else { let all_actions_task = self.request_multiple_lsp_locally( @@ -6989,11 +6854,13 @@ impl LspStore { cx, ); cx.background_spawn(async move { - all_actions_task - .await - .into_iter() - .flat_map(|(_, actions)| actions) - .collect::>() + Some( + all_actions_task + .await + .into_iter() + .flat_map(|(_, actions)| actions) + .collect::>(), + ) }) } } @@ -7003,47 +6870,32 @@ impl LspStore { buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task> { + ) -> Task>> { if let Some((client, upstream_project_id)) = self.upstream_client() { let request = GetHover { position }; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Vec::new()); + return Task::ready(None); } - let request_task = client.request(proto::MultiLspQuery { - buffer_id: buffer.read(cx).remote_id().into(), - version: serialize_version(&buffer.read(cx).version()), - project_id: upstream_project_id, - strategy: Some(proto::multi_lsp_query::Strategy::All( - proto::AllLanguageServers {}, - )), - request: Some(proto::multi_lsp_query::Request::GetHover( - request.to_proto(upstream_project_id, buffer.read(cx)), - )), - }); + let request_task = client.request_lsp( + upstream_project_id, + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(upstream_project_id, buffer.read(cx)), + ); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { - let Some(project) = weak_project.upgrade() else { - return Vec::new(); - }; - join_all( + let project = weak_project.upgrade()?; + let hovers = join_all( request_task .await .log_err() - .map(|response| response.responses) + .flatten() + .map(|response| response.payload) .unwrap_or_default() .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetHoverResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } - }) - .map(|hover_response| { + .map(|response| { let response = GetHover { position }.response_from_proto( - hover_response, + response.response, project.clone(), buffer.clone(), cx.clone(), @@ -7060,7 +6912,8 @@ impl LspStore { .await .into_iter() .flatten() - .collect() + .collect(); + Some(hovers) }) } else { let all_actions_task = self.request_multiple_lsp_locally( @@ -7070,11 +6923,13 @@ impl LspStore { cx, ); cx.background_spawn(async move { - all_actions_task - .await - .into_iter() - .filter_map(|(_, hover)| remove_empty_hover_blocks(hover?)) - .collect::>() + Some( + all_actions_task + .await + .into_iter() + .filter_map(|(_, hover)| remove_empty_hover_blocks(hover?)) + .collect::>(), + ) }) } } @@ -8137,6 +7992,203 @@ impl LspStore { })? } + async fn handle_lsp_query( + lsp_store: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + use proto::lsp_query::Request; + let sender_id = envelope.original_sender_id().unwrap_or_default(); + let lsp_query = envelope.payload; + let lsp_request_id = LspRequestId(lsp_query.lsp_request_id); + match lsp_query.request.context("invalid LSP query request")? { + Request::GetReferences(get_references) => { + let position = get_references.position.clone().and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_references, + position, + cx.clone(), + ) + .await?; + } + Request::GetDocumentColor(get_document_color) => { + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_document_color, + None, + cx.clone(), + ) + .await?; + } + Request::GetHover(get_hover) => { + let position = get_hover.position.clone().and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_hover, + position, + cx.clone(), + ) + .await?; + } + Request::GetCodeActions(get_code_actions) => { + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_code_actions, + None, + cx.clone(), + ) + .await?; + } + Request::GetSignatureHelp(get_signature_help) => { + let position = get_signature_help + .position + .clone() + .and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_signature_help, + position, + cx.clone(), + ) + .await?; + } + Request::GetCodeLens(get_code_lens) => { + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_code_lens, + None, + cx.clone(), + ) + .await?; + } + Request::GetDefinition(get_definition) => { + let position = get_definition.position.clone().and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_definition, + position, + cx.clone(), + ) + .await?; + } + Request::GetDeclaration(get_declaration) => { + let position = get_declaration + .position + .clone() + .and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_declaration, + position, + cx.clone(), + ) + .await?; + } + Request::GetTypeDefinition(get_type_definition) => { + let position = get_type_definition + .position + .clone() + .and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_type_definition, + position, + cx.clone(), + ) + .await?; + } + Request::GetImplementation(get_implementation) => { + let position = get_implementation + .position + .clone() + .and_then(deserialize_anchor); + Self::query_lsp_locally::( + lsp_store, + sender_id, + lsp_request_id, + get_implementation, + position, + cx.clone(), + ) + .await?; + } + // Diagnostics pull synchronizes internally via the buffer state, and cannot be handled generically as the other requests. + Request::GetDocumentDiagnostics(get_document_diagnostics) => { + let buffer_id = BufferId::new(get_document_diagnostics.buffer_id())?; + let version = deserialize_version(get_document_diagnostics.buffer_version()); + let buffer = lsp_store.update(&mut cx, |this, cx| { + this.buffer_store.read(cx).get_existing(buffer_id) + })??; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(version.clone()) + })? + .await?; + lsp_store.update(&mut cx, |lsp_store, cx| { + let existing_queries = lsp_store + .running_lsp_requests + .entry(TypeId::of::()) + .or_default(); + if ::ProtoRequest::stop_previous_requests( + ) || buffer.read(cx).version.changed_since(&existing_queries.0) + { + existing_queries.1.clear(); + } + existing_queries.1.insert( + lsp_request_id, + cx.spawn(async move |lsp_store, cx| { + let diagnostics_pull = lsp_store + .update(cx, |lsp_store, cx| { + lsp_store.pull_diagnostics_for_buffer(buffer, cx) + }) + .ok(); + if let Some(diagnostics_pull) = diagnostics_pull { + match diagnostics_pull.await { + Ok(()) => {} + Err(e) => log::error!("Failed to pull diagnostics: {e:#}"), + }; + } + }), + ); + })?; + } + } + Ok(proto::Ack {}) + } + + async fn handle_lsp_query_response( + lsp_store: Entity, + envelope: TypedEnvelope, + cx: AsyncApp, + ) -> Result<()> { + lsp_store.read_with(&cx, |lsp_store, _| { + if let Some((upstream_client, _)) = lsp_store.upstream_client() { + upstream_client.handle_lsp_response(envelope.clone()); + } + })?; + Ok(()) + } + + // todo(lsp) remove after Zed Stable hits v0.204.x async fn handle_multi_lsp_query( lsp_store: Entity, envelope: TypedEnvelope, @@ -12012,6 +12064,88 @@ impl LspStore { Ok(()) } + async fn query_lsp_locally( + lsp_store: Entity, + sender_id: proto::PeerId, + lsp_request_id: LspRequestId, + proto_request: T::ProtoRequest, + position: Option, + mut cx: AsyncApp, + ) -> Result<()> + where + T: LspCommand + Clone, + T::ProtoRequest: proto::LspRequestMessage, + ::Response: + Into<::Response>, + { + let buffer_id = BufferId::new(proto_request.buffer_id())?; + let version = deserialize_version(proto_request.buffer_version()); + let buffer = lsp_store.update(&mut cx, |this, cx| { + this.buffer_store.read(cx).get_existing(buffer_id) + })??; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(version.clone()) + })? + .await?; + let buffer_version = buffer.read_with(&cx, |buffer, _| buffer.version())?; + let request = + T::from_proto(proto_request, lsp_store.clone(), buffer.clone(), cx.clone()).await?; + lsp_store.update(&mut cx, |lsp_store, cx| { + let request_task = + lsp_store.request_multiple_lsp_locally(&buffer, position, request, cx); + let existing_queries = lsp_store + .running_lsp_requests + .entry(TypeId::of::()) + .or_default(); + if T::ProtoRequest::stop_previous_requests() + || buffer_version.changed_since(&existing_queries.0) + { + existing_queries.1.clear(); + } + existing_queries.1.insert( + lsp_request_id, + cx.spawn(async move |lsp_store, cx| { + let response = request_task.await; + lsp_store + .update(cx, |lsp_store, cx| { + if let Some((client, project_id)) = lsp_store.downstream_client.clone() + { + let response = response + .into_iter() + .map(|(server_id, response)| { + ( + server_id.to_proto(), + T::response_to_proto( + response, + lsp_store, + sender_id, + &buffer_version, + cx, + ) + .into(), + ) + }) + .collect::>(); + match client.send_lsp_response::( + project_id, + lsp_request_id, + response, + ) { + Ok(()) => {} + Err(e) => { + log::error!("Failed to send LSP response: {e:#}",) + } + } + } + }) + .ok(); + }), + ); + })?; + Ok(()) + } + fn take_text_document_sync_options( capabilities: &mut lsp::ServerCapabilities, ) -> lsp::TextDocumentSyncOptions { @@ -12025,6 +12159,12 @@ impl LspStore { None => lsp::TextDocumentSyncOptions::default(), } } + + #[cfg(any(test, feature = "test-support"))] + pub fn forget_code_lens_task(&mut self, buffer_id: BufferId) -> Option { + let data = self.lsp_code_lens.get_mut(&buffer_id)?; + Some(data.update.take()?.1) + } } // Registration with registerOptions as null, should fallback to true. diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e47c020a42..ee4bfcb8cc 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3415,7 +3415,7 @@ impl Project { buffer: &Entity, position: T, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { let position = position.to_point_utf16(buffer.read(cx)); let guard = self.retain_remotely_created_models(cx); let task = self.lsp_store.update(cx, |lsp_store, cx| { @@ -3433,7 +3433,7 @@ impl Project { buffer: &Entity, position: T, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { let position = position.to_point_utf16(buffer.read(cx)); let guard = self.retain_remotely_created_models(cx); let task = self.lsp_store.update(cx, |lsp_store, cx| { @@ -3451,7 +3451,7 @@ impl Project { buffer: &Entity, position: T, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { let position = position.to_point_utf16(buffer.read(cx)); let guard = self.retain_remotely_created_models(cx); let task = self.lsp_store.update(cx, |lsp_store, cx| { @@ -3469,7 +3469,7 @@ impl Project { buffer: &Entity, position: T, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { let position = position.to_point_utf16(buffer.read(cx)); let guard = self.retain_remotely_created_models(cx); let task = self.lsp_store.update(cx, |lsp_store, cx| { @@ -3487,7 +3487,7 @@ impl Project { buffer: &Entity, position: T, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { let position = position.to_point_utf16(buffer.read(cx)); let guard = self.retain_remotely_created_models(cx); let task = self.lsp_store.update(cx, |lsp_store, cx| { @@ -3585,23 +3585,12 @@ impl Project { }) } - pub fn signature_help( - &self, - buffer: &Entity, - position: T, - cx: &mut Context, - ) -> Task> { - self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.signature_help(buffer, position, cx) - }) - } - pub fn hover( &self, buffer: &Entity, position: T, cx: &mut Context, - ) -> Task> { + ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); self.lsp_store .update(cx, |lsp_store, cx| lsp_store.hover(buffer, position, cx)) @@ -3637,7 +3626,7 @@ impl Project { range: Range, kinds: Option>, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { let buffer = buffer_handle.read(cx); let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end); self.lsp_store.update(cx, |lsp_store, cx| { @@ -3650,7 +3639,7 @@ impl Project { buffer: &Entity, range: Range, cx: &mut Context, - ) -> Task>> { + ) -> Task>>> { let snapshot = buffer.read(cx).snapshot(); let range = range.to_point(&snapshot); let range_start = snapshot.anchor_before(range.start); @@ -3668,16 +3657,18 @@ impl Project { let mut code_lens_actions = code_lens_actions .await .map_err(|e| anyhow!("code lens fetch failed: {e:#}"))?; - code_lens_actions.retain(|code_lens_action| { - range - .start - .cmp(&code_lens_action.range.start, &snapshot) - .is_ge() - && range - .end - .cmp(&code_lens_action.range.end, &snapshot) - .is_le() - }); + if let Some(code_lens_actions) = &mut code_lens_actions { + code_lens_actions.retain(|code_lens_action| { + range + .start + .cmp(&code_lens_action.range.start, &snapshot) + .is_ge() + && range + .end + .cmp(&code_lens_action.range.end, &snapshot) + .is_le() + }); + } Ok(code_lens_actions) }) } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 8b0b21fcd6..282f1facc2 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -3005,6 +3005,7 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { let mut definitions = project .update(cx, |project, cx| project.definitions(&buffer, 22, cx)) .await + .unwrap() .unwrap(); // Assert no new language server started @@ -3519,7 +3520,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { .next() .await; - let action = actions.await.unwrap()[0].clone(); + let action = actions.await.unwrap().unwrap()[0].clone(); let apply = project.update(cx, |project, cx| { project.apply_code_action(buffer.clone(), action, true, cx) }); @@ -6110,6 +6111,7 @@ async fn test_multiple_language_server_hovers(cx: &mut gpui::TestAppContext) { hover_task .await .into_iter() + .flatten() .map(|hover| hover.contents.iter().map(|block| &block.text).join("|")) .sorted() .collect::>(), @@ -6183,6 +6185,7 @@ async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) { hover_task .await .into_iter() + .flatten() .map(|hover| hover.contents.iter().map(|block| &block.text).join("|")) .sorted() .collect::>(), @@ -6261,7 +6264,7 @@ async fn test_code_actions_only_kinds(cx: &mut gpui::TestAppContext) { .await .expect("The code action request should have been triggered"); - let code_actions = code_actions_task.await.unwrap(); + let code_actions = code_actions_task.await.unwrap().unwrap(); assert_eq!(code_actions.len(), 1); assert_eq!( code_actions[0].lsp_action.action_kind(), @@ -6420,6 +6423,7 @@ async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) { code_actions_task .await .unwrap() + .unwrap() .into_iter() .map(|code_action| code_action.lsp_action.title().to_owned()) .sorted() diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index ea9647feff..ac9c275aa2 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -753,28 +753,47 @@ message TextEdit { PointUtf16 lsp_range_end = 3; } -message MultiLspQuery { +message LspQuery { uint64 project_id = 1; - uint64 buffer_id = 2; - repeated VectorClockEntry version = 3; - oneof strategy { - AllLanguageServers all = 4; - } + uint64 lsp_request_id = 2; oneof request { + GetReferences get_references = 3; + GetDocumentColor get_document_color = 4; GetHover get_hover = 5; GetCodeActions get_code_actions = 6; GetSignatureHelp get_signature_help = 7; GetCodeLens get_code_lens = 8; GetDocumentDiagnostics get_document_diagnostics = 9; - GetDocumentColor get_document_color = 10; - GetDefinition get_definition = 11; - GetDeclaration get_declaration = 12; - GetTypeDefinition get_type_definition = 13; - GetImplementation get_implementation = 14; - GetReferences get_references = 15; + GetDefinition get_definition = 10; + GetDeclaration get_declaration = 11; + GetTypeDefinition get_type_definition = 12; + GetImplementation get_implementation = 13; } } +message LspQueryResponse { + uint64 project_id = 1; + uint64 lsp_request_id = 2; + repeated LspResponse responses = 3; +} + +message LspResponse { + oneof response { + GetHoverResponse get_hover_response = 1; + GetCodeActionsResponse get_code_actions_response = 2; + GetSignatureHelpResponse get_signature_help_response = 3; + GetCodeLensResponse get_code_lens_response = 4; + GetDocumentDiagnosticsResponse get_document_diagnostics_response = 5; + GetDocumentColorResponse get_document_color_response = 6; + GetDefinitionResponse get_definition_response = 8; + GetDeclarationResponse get_declaration_response = 9; + GetTypeDefinitionResponse get_type_definition_response = 10; + GetImplementationResponse get_implementation_response = 11; + GetReferencesResponse get_references_response = 12; + } + uint64 server_id = 7; +} + message AllLanguageServers {} message LanguageServerSelector { @@ -798,27 +817,6 @@ message StopLanguageServers { bool all = 4; } -message MultiLspQueryResponse { - repeated LspResponse responses = 1; -} - -message LspResponse { - oneof response { - GetHoverResponse get_hover_response = 1; - GetCodeActionsResponse get_code_actions_response = 2; - GetSignatureHelpResponse get_signature_help_response = 3; - GetCodeLensResponse get_code_lens_response = 4; - GetDocumentDiagnosticsResponse get_document_diagnostics_response = 5; - GetDocumentColorResponse get_document_color_response = 6; - GetDefinitionResponse get_definition_response = 8; - GetDeclarationResponse get_declaration_response = 9; - GetTypeDefinitionResponse get_type_definition_response = 10; - GetImplementationResponse get_implementation_response = 11; - GetReferencesResponse get_references_response = 12; - } - uint64 server_id = 7; -} - message LspExtRunnables { uint64 project_id = 1; uint64 buffer_id = 2; @@ -909,3 +907,30 @@ message PullWorkspaceDiagnostics { uint64 project_id = 1; uint64 server_id = 2; } + +// todo(lsp) remove after Zed Stable hits v0.204.x +message MultiLspQuery { + uint64 project_id = 1; + uint64 buffer_id = 2; + repeated VectorClockEntry version = 3; + oneof strategy { + AllLanguageServers all = 4; + } + oneof request { + GetHover get_hover = 5; + GetCodeActions get_code_actions = 6; + GetSignatureHelp get_signature_help = 7; + GetCodeLens get_code_lens = 8; + GetDocumentDiagnostics get_document_diagnostics = 9; + GetDocumentColor get_document_color = 10; + GetDefinition get_definition = 11; + GetDeclaration get_declaration = 12; + GetTypeDefinition get_type_definition = 13; + GetImplementation get_implementation = 14; + GetReferences get_references = 15; + } +} + +message MultiLspQueryResponse { + repeated LspResponse responses = 1; +} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 310fcf584e..70689bcd63 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -393,7 +393,10 @@ message Envelope { GetCrashFilesResponse get_crash_files_response = 362; GitClone git_clone = 363; - GitCloneResponse git_clone_response = 364; // current max + GitCloneResponse git_clone_response = 364; + + LspQuery lsp_query = 365; + LspQueryResponse lsp_query_response = 366; // current max } reserved 87 to 88; diff --git a/crates/proto/src/macros.rs b/crates/proto/src/macros.rs index 2ce0c0df25..59e984d7db 100644 --- a/crates/proto/src/macros.rs +++ b/crates/proto/src/macros.rs @@ -69,3 +69,32 @@ macro_rules! entity_messages { })* }; } + +#[macro_export] +macro_rules! lsp_messages { + ($(($request_name:ident, $response_name:ident, $stop_previous_requests:expr)),* $(,)?) => { + $(impl LspRequestMessage for $request_name { + type Response = $response_name; + + fn to_proto_query(self) -> $crate::lsp_query::Request { + $crate::lsp_query::Request::$request_name(self) + } + + fn response_to_proto_query(response: Self::Response) -> $crate::lsp_response::Response { + $crate::lsp_response::Response::$response_name(response) + } + + fn buffer_id(&self) -> u64 { + self.buffer_id + } + + fn buffer_version(&self) -> &[$crate::VectorClockEntry] { + &self.version + } + + fn stop_previous_requests() -> bool { + $stop_previous_requests + } + })* + }; +} diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 802db09590..d38e54685f 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -169,6 +169,9 @@ messages!( (MarkNotificationRead, Foreground), (MoveChannel, Foreground), (ReorderChannel, Foreground), + (LspQuery, Background), + (LspQueryResponse, Background), + // todo(lsp) remove after Zed Stable hits v0.204.x (MultiLspQuery, Background), (MultiLspQueryResponse, Background), (OnTypeFormatting, Background), @@ -426,7 +429,10 @@ request_messages!( (SetRoomParticipantRole, Ack), (BlameBuffer, BlameBufferResponse), (RejoinRemoteProjects, RejoinRemoteProjectsResponse), + // todo(lsp) remove after Zed Stable hits v0.204.x (MultiLspQuery, MultiLspQueryResponse), + (LspQuery, Ack), + (LspQueryResponse, Ack), (RestartLanguageServers, Ack), (StopLanguageServers, Ack), (OpenContext, OpenContextResponse), @@ -478,6 +484,20 @@ request_messages!( (GitClone, GitCloneResponse) ); +lsp_messages!( + (GetReferences, GetReferencesResponse, true), + (GetDocumentColor, GetDocumentColorResponse, true), + (GetHover, GetHoverResponse, true), + (GetCodeActions, GetCodeActionsResponse, true), + (GetSignatureHelp, GetSignatureHelpResponse, true), + (GetCodeLens, GetCodeLensResponse, true), + (GetDocumentDiagnostics, GetDocumentDiagnosticsResponse, true), + (GetDefinition, GetDefinitionResponse, true), + (GetDeclaration, GetDeclarationResponse, true), + (GetTypeDefinition, GetTypeDefinitionResponse, true), + (GetImplementation, GetImplementationResponse, true), +); + entity_messages!( {project_id, ShareProject}, AddProjectCollaborator, @@ -520,6 +540,9 @@ entity_messages!( LeaveProject, LinkedEditingRange, LoadCommitDiff, + LspQuery, + LspQueryResponse, + // todo(lsp) remove after Zed Stable hits v0.204.x MultiLspQuery, RestartLanguageServers, StopLanguageServers, @@ -777,6 +800,28 @@ pub fn split_repository_update( }]) } +impl LspQuery { + pub fn query_name_and_write_permissions(&self) -> (&str, bool) { + match self.request { + Some(lsp_query::Request::GetHover(_)) => ("GetHover", false), + Some(lsp_query::Request::GetCodeActions(_)) => ("GetCodeActions", true), + Some(lsp_query::Request::GetSignatureHelp(_)) => ("GetSignatureHelp", false), + Some(lsp_query::Request::GetCodeLens(_)) => ("GetCodeLens", true), + Some(lsp_query::Request::GetDocumentDiagnostics(_)) => { + ("GetDocumentDiagnostics", false) + } + Some(lsp_query::Request::GetDefinition(_)) => ("GetDefinition", false), + Some(lsp_query::Request::GetDeclaration(_)) => ("GetDeclaration", false), + Some(lsp_query::Request::GetTypeDefinition(_)) => ("GetTypeDefinition", false), + Some(lsp_query::Request::GetImplementation(_)) => ("GetImplementation", false), + Some(lsp_query::Request::GetReferences(_)) => ("GetReferences", false), + Some(lsp_query::Request::GetDocumentColor(_)) => ("GetDocumentColor", false), + None => ("", true), + } + } +} + +// todo(lsp) remove after Zed Stable hits v0.204.x impl MultiLspQuery { pub fn request_str(&self) -> &str { match self.request { diff --git a/crates/proto/src/typed_envelope.rs b/crates/proto/src/typed_envelope.rs index 381a6379dc..f677a3b967 100644 --- a/crates/proto/src/typed_envelope.rs +++ b/crates/proto/src/typed_envelope.rs @@ -31,6 +31,58 @@ pub trait RequestMessage: EnvelopedMessage { type Response: EnvelopedMessage; } +/// A trait to bind LSP request and responses for the proto layer. +/// Should be used for every LSP request that has to traverse through the proto layer. +/// +/// `lsp_messages` macro in the same crate provides a convenient way to implement this. +pub trait LspRequestMessage: EnvelopedMessage { + type Response: EnvelopedMessage; + + fn to_proto_query(self) -> crate::lsp_query::Request; + + fn response_to_proto_query(response: Self::Response) -> crate::lsp_response::Response; + + fn buffer_id(&self) -> u64; + + fn buffer_version(&self) -> &[crate::VectorClockEntry]; + + /// Whether to deduplicate the requests, or keep the previous ones running when another + /// request of the same kind is processed. + fn stop_previous_requests() -> bool; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct LspRequestId(pub u64); + +/// A response from a single language server. +/// There could be multiple responses for a single LSP request, +/// from different servers. +pub struct ProtoLspResponse { + pub server_id: u64, + pub response: R, +} + +impl ProtoLspResponse> { + pub fn into_response(self) -> Result> { + let envelope = self + .response + .into_any() + .downcast::>() + .map_err(|_| { + anyhow::anyhow!( + "cannot downcast LspResponse to {} for message {}", + T::Response::NAME, + T::NAME, + ) + })?; + + Ok(ProtoLspResponse { + server_id: self.server_id, + response: envelope.payload, + }) + } +} + pub trait AnyTypedEnvelope: Any + Send + Sync { fn payload_type_id(&self) -> TypeId; fn payload_type_name(&self) -> &'static str; diff --git a/crates/rpc/src/proto_client.rs b/crates/rpc/src/proto_client.rs index 05b6bd1439..791b7db9c0 100644 --- a/crates/rpc/src/proto_client.rs +++ b/crates/rpc/src/proto_client.rs @@ -1,35 +1,48 @@ -use anyhow::Context; +use anyhow::{Context, Result}; use collections::HashMap; use futures::{ Future, FutureExt as _, + channel::oneshot, future::{BoxFuture, LocalBoxFuture}, }; -use gpui::{AnyEntity, AnyWeakEntity, AsyncApp, Entity}; +use gpui::{AnyEntity, AnyWeakEntity, AsyncApp, BackgroundExecutor, Entity, FutureExt as _}; +use parking_lot::Mutex; use proto::{ - AnyTypedEnvelope, EntityMessage, Envelope, EnvelopedMessage, RequestMessage, TypedEnvelope, - error::ErrorExt as _, + AnyTypedEnvelope, EntityMessage, Envelope, EnvelopedMessage, LspRequestId, LspRequestMessage, + RequestMessage, TypedEnvelope, error::ErrorExt as _, }; use std::{ any::{Any, TypeId}, - sync::{Arc, Weak}, + sync::{ + Arc, OnceLock, + atomic::{self, AtomicU64}, + }, + time::Duration, }; #[derive(Clone)] -pub struct AnyProtoClient(Arc); +pub struct AnyProtoClient(Arc); -impl AnyProtoClient { - pub fn downgrade(&self) -> AnyWeakProtoClient { - AnyWeakProtoClient(Arc::downgrade(&self.0)) - } -} +type RequestIds = Arc< + Mutex< + HashMap< + LspRequestId, + oneshot::Sender< + Result< + Option>>>>, + >, + >, + >, + >, +>; -#[derive(Clone)] -pub struct AnyWeakProtoClient(Weak); +static NEXT_LSP_REQUEST_ID: OnceLock> = OnceLock::new(); +static REQUEST_IDS: OnceLock = OnceLock::new(); -impl AnyWeakProtoClient { - pub fn upgrade(&self) -> Option { - self.0.upgrade().map(AnyProtoClient) - } +struct State { + client: Arc, + next_lsp_request_id: Arc, + request_ids: RequestIds, } pub trait ProtoClient: Send + Sync { @@ -37,11 +50,11 @@ pub trait ProtoClient: Send + Sync { &self, envelope: Envelope, request_type: &'static str, - ) -> BoxFuture<'static, anyhow::Result>; + ) -> BoxFuture<'static, Result>; - fn send(&self, envelope: Envelope, message_type: &'static str) -> anyhow::Result<()>; + fn send(&self, envelope: Envelope, message_type: &'static str) -> Result<()>; - fn send_response(&self, envelope: Envelope, message_type: &'static str) -> anyhow::Result<()>; + fn send_response(&self, envelope: Envelope, message_type: &'static str) -> Result<()>; fn message_handler_set(&self) -> &parking_lot::Mutex; @@ -65,7 +78,7 @@ pub type ProtoMessageHandler = Arc< Box, AnyProtoClient, AsyncApp, - ) -> LocalBoxFuture<'static, anyhow::Result<()>>, + ) -> LocalBoxFuture<'static, Result<()>>, >; impl ProtoMessageHandlerSet { @@ -113,7 +126,7 @@ impl ProtoMessageHandlerSet { message: Box, client: AnyProtoClient, cx: AsyncApp, - ) -> Option>> { + ) -> Option>> { let payload_type_id = message.payload_type_id(); let mut this = this.lock(); let handler = this.message_handlers.get(&payload_type_id)?.clone(); @@ -169,43 +182,195 @@ where T: ProtoClient + 'static, { fn from(client: Arc) -> Self { - Self(client) + Self::new(client) } } impl AnyProtoClient { pub fn new(client: Arc) -> Self { - Self(client) + Self(Arc::new(State { + client, + next_lsp_request_id: NEXT_LSP_REQUEST_ID + .get_or_init(|| Arc::new(AtomicU64::new(0))) + .clone(), + request_ids: REQUEST_IDS.get_or_init(RequestIds::default).clone(), + })) } pub fn is_via_collab(&self) -> bool { - self.0.is_via_collab() + self.0.client.is_via_collab() } pub fn request( &self, request: T, - ) -> impl Future> + use { + ) -> impl Future> + use { let envelope = request.into_envelope(0, None, None); - let response = self.0.request(envelope, T::NAME); + let response = self.0.client.request(envelope, T::NAME); async move { T::Response::from_envelope(response.await?) .context("received response of the wrong type") } } - pub fn send(&self, request: T) -> anyhow::Result<()> { + pub fn send(&self, request: T) -> Result<()> { let envelope = request.into_envelope(0, None, None); - self.0.send(envelope, T::NAME) + self.0.client.send(envelope, T::NAME) } - pub fn send_response( - &self, - request_id: u32, - request: T, - ) -> anyhow::Result<()> { + pub fn send_response(&self, request_id: u32, request: T) -> Result<()> { let envelope = request.into_envelope(0, Some(request_id), None); - self.0.send(envelope, T::NAME) + self.0.client.send(envelope, T::NAME) + } + + pub fn request_lsp( + &self, + project_id: u64, + timeout: Duration, + executor: BackgroundExecutor, + request: T, + ) -> impl Future< + Output = Result>>>>, + > + use + where + T: LspRequestMessage, + { + let new_id = LspRequestId( + self.0 + .next_lsp_request_id + .fetch_add(1, atomic::Ordering::Acquire), + ); + let (tx, rx) = oneshot::channel(); + { + self.0.request_ids.lock().insert(new_id, tx); + } + + let query = proto::LspQuery { + project_id, + lsp_request_id: new_id.0, + request: Some(request.clone().to_proto_query()), + }; + let request = self.request(query); + let request_ids = self.0.request_ids.clone(); + async move { + match request.await { + Ok(_request_enqueued) => {} + Err(e) => { + request_ids.lock().remove(&new_id); + return Err(e).context("sending LSP proto request"); + } + } + + let response = rx.with_timeout(timeout, &executor).await; + { + request_ids.lock().remove(&new_id); + } + match response { + Ok(Ok(response)) => { + let response = response + .context("waiting for LSP proto response")? + .map(|response| { + anyhow::Ok(TypedEnvelope { + payload: response + .payload + .into_iter() + .map(|lsp_response| lsp_response.into_response::()) + .collect::>>()?, + sender_id: response.sender_id, + original_sender_id: response.original_sender_id, + message_id: response.message_id, + received_at: response.received_at, + }) + }) + .transpose() + .context("converting LSP proto response")?; + Ok(response) + } + Err(_cancelled_due_timeout) => Ok(None), + Ok(Err(_channel_dropped)) => Ok(None), + } + } + } + + pub fn send_lsp_response( + &self, + project_id: u64, + lsp_request_id: LspRequestId, + server_responses: HashMap, + ) -> Result<()> { + self.send(proto::LspQueryResponse { + project_id, + lsp_request_id: lsp_request_id.0, + responses: server_responses + .into_iter() + .map(|(server_id, response)| proto::LspResponse { + server_id, + response: Some(T::response_to_proto_query(response)), + }) + .collect(), + }) + } + + pub fn handle_lsp_response(&self, mut envelope: TypedEnvelope) { + let request_id = LspRequestId(envelope.payload.lsp_request_id); + let mut response_senders = self.0.request_ids.lock(); + if let Some(tx) = response_senders.remove(&request_id) { + let responses = envelope.payload.responses.drain(..).collect::>(); + tx.send(Ok(Some(proto::TypedEnvelope { + sender_id: envelope.sender_id, + original_sender_id: envelope.original_sender_id, + message_id: envelope.message_id, + received_at: envelope.received_at, + payload: responses + .into_iter() + .filter_map(|response| { + use proto::lsp_response::Response; + + let server_id = response.server_id; + let response = match response.response? { + Response::GetReferencesResponse(response) => { + to_any_envelope(&envelope, response) + } + Response::GetDocumentColorResponse(response) => { + to_any_envelope(&envelope, response) + } + Response::GetHoverResponse(response) => { + to_any_envelope(&envelope, response) + } + Response::GetCodeActionsResponse(response) => { + to_any_envelope(&envelope, response) + } + Response::GetSignatureHelpResponse(response) => { + to_any_envelope(&envelope, response) + } + Response::GetCodeLensResponse(response) => { + to_any_envelope(&envelope, response) + } + Response::GetDocumentDiagnosticsResponse(response) => { + to_any_envelope(&envelope, response) + } + Response::GetDefinitionResponse(response) => { + to_any_envelope(&envelope, response) + } + Response::GetDeclarationResponse(response) => { + to_any_envelope(&envelope, response) + } + Response::GetTypeDefinitionResponse(response) => { + to_any_envelope(&envelope, response) + } + Response::GetImplementationResponse(response) => { + to_any_envelope(&envelope, response) + } + }; + Some(proto::ProtoLspResponse { + server_id, + response, + }) + }) + .collect(), + }))) + .ok(); + } } pub fn add_request_handler(&self, entity: gpui::WeakEntity, handler: H) @@ -213,31 +378,35 @@ impl AnyProtoClient { M: RequestMessage, E: 'static, H: 'static + Sync + Fn(Entity, TypedEnvelope, AsyncApp) -> F + Send + Sync, - F: 'static + Future>, + F: 'static + Future>, { - self.0.message_handler_set().lock().add_message_handler( - TypeId::of::(), - entity.into(), - Arc::new(move |entity, envelope, client, cx| { - let entity = entity.downcast::().unwrap(); - let envelope = envelope.into_any().downcast::>().unwrap(); - let request_id = envelope.message_id(); - handler(entity, *envelope, cx) - .then(move |result| async move { - match result { - Ok(response) => { - client.send_response(request_id, response)?; - Ok(()) + self.0 + .client + .message_handler_set() + .lock() + .add_message_handler( + TypeId::of::(), + entity.into(), + Arc::new(move |entity, envelope, client, cx| { + let entity = entity.downcast::().unwrap(); + let envelope = envelope.into_any().downcast::>().unwrap(); + let request_id = envelope.message_id(); + handler(entity, *envelope, cx) + .then(move |result| async move { + match result { + Ok(response) => { + client.send_response(request_id, response)?; + Ok(()) + } + Err(error) => { + client.send_response(request_id, error.to_proto())?; + Err(error) + } } - Err(error) => { - client.send_response(request_id, error.to_proto())?; - Err(error) - } - } - }) - .boxed_local() - }), - ) + }) + .boxed_local() + }), + ) } pub fn add_entity_request_handler(&self, handler: H) @@ -245,7 +414,7 @@ impl AnyProtoClient { M: EnvelopedMessage + RequestMessage + EntityMessage, E: 'static, H: 'static + Sync + Send + Fn(gpui::Entity, TypedEnvelope, AsyncApp) -> F, - F: 'static + Future>, + F: 'static + Future>, { let message_type_id = TypeId::of::(); let entity_type_id = TypeId::of::(); @@ -257,6 +426,7 @@ impl AnyProtoClient { .remote_entity_id() }; self.0 + .client .message_handler_set() .lock() .add_entity_message_handler( @@ -290,7 +460,7 @@ impl AnyProtoClient { M: EnvelopedMessage + EntityMessage, E: 'static, H: 'static + Sync + Send + Fn(gpui::Entity, TypedEnvelope, AsyncApp) -> F, - F: 'static + Future>, + F: 'static + Future>, { let message_type_id = TypeId::of::(); let entity_type_id = TypeId::of::(); @@ -302,6 +472,7 @@ impl AnyProtoClient { .remote_entity_id() }; self.0 + .client .message_handler_set() .lock() .add_entity_message_handler( @@ -319,7 +490,7 @@ impl AnyProtoClient { pub fn subscribe_to_entity(&self, remote_id: u64, entity: &Entity) { let id = (TypeId::of::(), remote_id); - let mut message_handlers = self.0.message_handler_set().lock(); + let mut message_handlers = self.0.client.message_handler_set().lock(); if message_handlers .entities_by_type_and_remote_id .contains_key(&id) @@ -335,3 +506,16 @@ impl AnyProtoClient { ); } } + +fn to_any_envelope( + envelope: &TypedEnvelope, + response: T, +) -> Box { + Box::new(proto::TypedEnvelope { + sender_id: envelope.sender_id, + original_sender_id: envelope.original_sender_id, + message_id: envelope.message_id, + received_at: envelope.received_at, + payload: response, + }) as Box<_> +} From 68f97d6069ad7f35929c2e0e2d7265bbc96c6e56 Mon Sep 17 00:00:00 2001 From: Sachith Shetty Date: Wed, 20 Aug 2025 23:27:41 -0700 Subject: [PATCH 563/693] editor: Use `highlight_text` to highlight matching brackets, fix unnecessary inlay hint highlighting (#36540) Closes #35981 Release Notes: - Fixed bracket highlights overly including parts of inlays when highlighting Before - Screenshot from 2025-08-19 17-15-06 After - Screenshot from 2025-08-19 17-24-26 --- .../editor/src/highlight_matching_bracket.rs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/crates/editor/src/highlight_matching_bracket.rs b/crates/editor/src/highlight_matching_bracket.rs index e38197283d..aa4e616924 100644 --- a/crates/editor/src/highlight_matching_bracket.rs +++ b/crates/editor/src/highlight_matching_bracket.rs @@ -1,6 +1,7 @@ use crate::{Editor, RangeToAnchorExt}; -use gpui::{Context, Window}; +use gpui::{Context, HighlightStyle, Window}; use language::CursorShape; +use theme::ActiveTheme; enum MatchingBracketHighlight {} @@ -9,7 +10,7 @@ pub fn refresh_matching_bracket_highlights( window: &mut Window, cx: &mut Context, ) { - editor.clear_background_highlights::(cx); + editor.clear_highlights::(cx); let newest_selection = editor.selections.newest::(cx); // Don't highlight brackets if the selection isn't empty @@ -35,12 +36,19 @@ pub fn refresh_matching_bracket_highlights( .buffer_snapshot .innermost_enclosing_bracket_ranges(head..tail, None) { - editor.highlight_background::( - &[ + editor.highlight_text::( + vec![ opening_range.to_anchors(&snapshot.buffer_snapshot), closing_range.to_anchors(&snapshot.buffer_snapshot), ], - |theme| theme.colors().editor_document_highlight_bracket_background, + HighlightStyle { + background_color: Some( + cx.theme() + .colors() + .editor_document_highlight_bracket_background, + ), + ..Default::default() + }, cx, ) } @@ -104,7 +112,7 @@ mod tests { another_test(1, 2, 3); } "#}); - cx.assert_editor_background_highlights::(indoc! {r#" + cx.assert_editor_text_highlights::(indoc! {r#" pub fn test«(»"Test argument"«)» { another_test(1, 2, 3); } @@ -115,7 +123,7 @@ mod tests { another_test(1, ˇ2, 3); } "#}); - cx.assert_editor_background_highlights::(indoc! {r#" + cx.assert_editor_text_highlights::(indoc! {r#" pub fn test("Test argument") { another_test«(»1, 2, 3«)»; } @@ -126,7 +134,7 @@ mod tests { anotherˇ_test(1, 2, 3); } "#}); - cx.assert_editor_background_highlights::(indoc! {r#" + cx.assert_editor_text_highlights::(indoc! {r#" pub fn test("Test argument") «{» another_test(1, 2, 3); «}» @@ -138,7 +146,7 @@ mod tests { another_test(1, 2, 3); } "#}); - cx.assert_editor_background_highlights::(indoc! {r#" + cx.assert_editor_text_highlights::(indoc! {r#" pub fn test("Test argument") { another_test(1, 2, 3); } @@ -150,8 +158,8 @@ mod tests { another_test(1, 2, 3); } "#}); - cx.assert_editor_background_highlights::(indoc! {r#" - pub fn test("Test argument") { + cx.assert_editor_text_highlights::(indoc! {r#" + pub fn test«("Test argument") { another_test(1, 2, 3); } "#}); From cde0a5dd27c7f29e389cf8d518983d21f3376071 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 21 Aug 2025 09:36:57 +0300 Subject: [PATCH 564/693] Add a non-style lint exclusion (#36658) Follow-up of https://github.com/zed-industries/zed/pull/36651 Restores https://github.com/zed-industries/zed/pull/35955 footgun guard. Release Notes: - N/A --- Cargo.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 400ce791aa..b13795e1e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -802,7 +802,10 @@ unexpected_cfgs = { level = "allow" } dbg_macro = "deny" todo = "deny" -# trying this out +# This is not a style lint, see https://github.com/rust-lang/rust-clippy/pull/15454 +# Remove when the lint gets promoted to `suspicious`. +declare_interior_mutable_const = "deny" + redundant_clone = "deny" # We currently do not restrict any style rules From ed84767c9d1d597c8b81e8e927ad1be35bb59add Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 21 Aug 2025 09:48:04 +0300 Subject: [PATCH 565/693] Fix overlooked Clippy lints (#36659) Follow-up of https://github.com/zed-industries/zed/pull/36557 that is needed after https://github.com/zed-industries/zed/pull/36652 Release Notes: - N/A --- crates/project/src/lsp_store.rs | 8 ++++---- crates/rpc/src/proto_client.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index bcfd9d386b..072f4396c1 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -5693,13 +5693,13 @@ impl LspStore { let lsp_data = lsp_store.lsp_code_lens.entry(buffer_id).or_default(); if let Some(fetched_lens) = fetched_lens { if lsp_data.lens_for_version == query_version_queried_for { - lsp_data.lens.extend(fetched_lens.clone()); + lsp_data.lens.extend(fetched_lens); } else if !lsp_data .lens_for_version .changed_since(&query_version_queried_for) { lsp_data.lens_for_version = query_version_queried_for; - lsp_data.lens = fetched_lens.clone(); + lsp_data.lens = fetched_lens; } } lsp_data.update = None; @@ -6694,14 +6694,14 @@ impl LspStore { if let Some(fetched_colors) = fetched_colors { if lsp_data.colors_for_version == query_version_queried_for { - lsp_data.colors.extend(fetched_colors.clone()); + lsp_data.colors.extend(fetched_colors); lsp_data.cache_version += 1; } else if !lsp_data .colors_for_version .changed_since(&query_version_queried_for) { lsp_data.colors_for_version = query_version_queried_for; - lsp_data.colors = fetched_colors.clone(); + lsp_data.colors = fetched_colors; lsp_data.cache_version += 1; } } diff --git a/crates/rpc/src/proto_client.rs b/crates/rpc/src/proto_client.rs index 791b7db9c0..a90797ff5d 100644 --- a/crates/rpc/src/proto_client.rs +++ b/crates/rpc/src/proto_client.rs @@ -248,7 +248,7 @@ impl AnyProtoClient { let query = proto::LspQuery { project_id, lsp_request_id: new_id.0, - request: Some(request.clone().to_proto_query()), + request: Some(request.to_proto_query()), }; let request = self.request(query); let request_ids = self.0.request_ids.clone(); From fda6eda3c2abcbe90af48bd112ee560eb63706e7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Aug 2025 10:57:28 +0200 Subject: [PATCH 566/693] Fix @-mentioning threads when their summary isn't ready yet (#36664) Release Notes: - N/A --- crates/agent2/src/agent.rs | 10 ++-------- crates/agent_ui/src/acp/message_editor.rs | 9 +++------ 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index c15048ad8c..d5bc0fea63 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1269,18 +1269,12 @@ mod tests { let model = Arc::new(FakeLanguageModel::default()); let summary_model = Arc::new(FakeLanguageModel::default()); thread.update(cx, |thread, cx| { - thread.set_model(model, cx); - thread.set_summarization_model(Some(summary_model), cx); + thread.set_model(model.clone(), cx); + thread.set_summarization_model(Some(summary_model.clone()), cx); }); cx.run_until_parked(); assert_eq!(history_entries(&history_store, cx), vec![]); - let model = thread.read_with(cx, |thread, _| thread.model().unwrap().clone()); - let model = model.as_fake(); - let summary_model = thread.read_with(cx, |thread, _| { - thread.summarization_model().unwrap().clone() - }); - let summary_model = summary_model.as_fake(); let send = acp_thread.update(cx, |thread, cx| { thread.send( vec![ diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 1155285d09..3116a40be5 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -629,15 +629,11 @@ impl MessageEditor { .shared(); self.mention_set.insert_thread(id.clone(), task.clone()); + self.mention_set.insert_uri(crease_id, uri); let editor = self.editor.clone(); cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_some() { - this.update(cx, |this, _| { - this.mention_set.insert_uri(crease_id, uri); - }) - .ok(); - } else { + if task.await.notify_async_err(cx).is_none() { editor .update(cx, |editor, cx| { editor.display_map.update(cx, |display_map, cx| { @@ -648,6 +644,7 @@ impl MessageEditor { .ok(); this.update(cx, |this, _| { this.mention_set.thread_summaries.remove(&id); + this.mention_set.uri_by_crease_id.remove(&crease_id); }) .ok(); } From 62f2ef86dca7e4d171050be9951585199a25aa32 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Thu, 21 Aug 2025 11:25:00 +0200 Subject: [PATCH 567/693] agent2: Allow expanding terminals individually (#36670) Release Notes: - N/A --- crates/agent_ui/src/acp/entry_view_state.rs | 10 ++++-- crates/agent_ui/src/acp/thread_view.rs | 36 ++++++++++++++------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index 67acbb8b5b..fb15d8bed8 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -121,14 +121,19 @@ impl EntryViewState { for terminal in terminals { views.entry(terminal.entity_id()).or_insert_with(|| { - create_terminal( + let element = create_terminal( self.workspace.clone(), self.project.clone(), terminal.clone(), window, cx, ) - .into_any() + .into_any(); + cx.emit(EntryViewEvent { + entry_index: index, + view_event: ViewEvent::NewTerminal(terminal.entity_id()), + }); + element }); } @@ -187,6 +192,7 @@ pub struct EntryViewEvent { } pub enum ViewEvent { + NewTerminal(EntityId), MessageEditorEvent(Entity, MessageEditorEvent), } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 12a33d022e..432ba4e0e8 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -20,11 +20,11 @@ use file_icons::FileIcons; use fs::Fs; use gpui::{ Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, - EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, - ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, - Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, - WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, point, - prelude::*, pulsating_between, + EdgesRefinement, ElementId, Empty, Entity, EntityId, FocusHandle, Focusable, Hsla, Length, + ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, + Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, + Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, + point, prelude::*, pulsating_between, }; use language::Buffer; @@ -256,10 +256,10 @@ pub struct AcpThreadView { auth_task: Option>, expanded_tool_calls: HashSet, expanded_thinking_blocks: HashSet<(usize, usize)>, + expanded_terminals: HashSet, edits_expanded: bool, plan_expanded: bool, editor_expanded: bool, - terminal_expanded: bool, editing_message: Option, _cancel_task: Option>, _subscriptions: [Subscription; 3], @@ -354,11 +354,11 @@ impl AcpThreadView { auth_task: None, expanded_tool_calls: HashSet::default(), expanded_thinking_blocks: HashSet::default(), + expanded_terminals: HashSet::default(), editing_message: None, edits_expanded: false, plan_expanded: false, editor_expanded: false, - terminal_expanded: true, history_store, hovered_recent_history_item: None, _subscriptions: subscriptions, @@ -677,6 +677,11 @@ impl AcpThreadView { cx: &mut Context, ) { match &event.view_event { + ViewEvent::NewTerminal(terminal_id) => { + if AgentSettings::get_global(cx).expand_terminal_card { + self.expanded_terminals.insert(*terminal_id); + } + } ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => { if let Some(thread) = self.thread() && let Some(AgentThreadEntry::UserMessage(user_message)) = @@ -2009,6 +2014,8 @@ impl AcpThreadView { .map(|path| format!("{}", path.display())) .unwrap_or_else(|| "current directory".to_string()); + let is_expanded = self.expanded_terminals.contains(&terminal.entity_id()); + let header = h_flex() .id(SharedString::from(format!( "terminal-tool-header-{}", @@ -2142,12 +2149,19 @@ impl AcpThreadView { "terminal-tool-disclosure-{}", terminal.entity_id() )), - self.terminal_expanded, + is_expanded, ) .opened_icon(IconName::ChevronUp) .closed_icon(IconName::ChevronDown) - .on_click(cx.listener(move |this, _event, _window, _cx| { - this.terminal_expanded = !this.terminal_expanded; + .on_click(cx.listener({ + let terminal_id = terminal.entity_id(); + move |this, _event, _window, _cx| { + if is_expanded { + this.expanded_terminals.remove(&terminal_id); + } else { + this.expanded_terminals.insert(terminal_id); + } + } })), ); @@ -2156,7 +2170,7 @@ impl AcpThreadView { .read(cx) .entry(entry_ix) .and_then(|entry| entry.terminal(terminal)); - let show_output = self.terminal_expanded && terminal_view.is_some(); + let show_output = is_expanded && terminal_view.is_some(); v_flex() .mb_2() From 7f1bd2f15eb6684c7c63c09f2520c9a6a344a6c8 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 21 Aug 2025 11:37:45 +0200 Subject: [PATCH 568/693] remote: Fix toolchain RPC messages not being handled because of the entity getting dropped (#36665) Release Notes: - N/A --- crates/project/src/toolchain_store.rs | 73 ++++++++++++------- crates/remote_server/src/headless_project.rs | 4 + .../src/active_toolchain.rs | 11 --- 3 files changed, 49 insertions(+), 39 deletions(-) diff --git a/crates/project/src/toolchain_store.rs b/crates/project/src/toolchain_store.rs index 05531ebe9a..ac87e64248 100644 --- a/crates/project/src/toolchain_store.rs +++ b/crates/project/src/toolchain_store.rs @@ -34,7 +34,10 @@ enum ToolchainStoreInner { Entity, #[allow(dead_code)] Subscription, ), - Remote(Entity), + Remote( + Entity, + #[allow(dead_code)] Subscription, + ), } impl EventEmitter for ToolchainStore {} @@ -65,10 +68,12 @@ impl ToolchainStore { Self(ToolchainStoreInner::Local(entity, subscription)) } - pub(super) fn remote(project_id: u64, client: AnyProtoClient, cx: &mut App) -> Self { - Self(ToolchainStoreInner::Remote( - cx.new(|_| RemoteToolchainStore { client, project_id }), - )) + pub(super) fn remote(project_id: u64, client: AnyProtoClient, cx: &mut Context) -> Self { + let entity = cx.new(|_| RemoteToolchainStore { client, project_id }); + let _subscription = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| { + cx.emit(e.clone()) + }); + Self(ToolchainStoreInner::Remote(entity, _subscription)) } pub(crate) fn activate_toolchain( &self, @@ -80,8 +85,8 @@ impl ToolchainStore { ToolchainStoreInner::Local(local, _) => { local.update(cx, |this, cx| this.activate_toolchain(path, toolchain, cx)) } - ToolchainStoreInner::Remote(remote) => { - remote.read(cx).activate_toolchain(path, toolchain, cx) + ToolchainStoreInner::Remote(remote, _) => { + remote.update(cx, |this, cx| this.activate_toolchain(path, toolchain, cx)) } } } @@ -95,7 +100,7 @@ impl ToolchainStore { ToolchainStoreInner::Local(local, _) => { local.update(cx, |this, cx| this.list_toolchains(path, language_name, cx)) } - ToolchainStoreInner::Remote(remote) => { + ToolchainStoreInner::Remote(remote, _) => { remote.read(cx).list_toolchains(path, language_name, cx) } } @@ -112,7 +117,7 @@ impl ToolchainStore { &path.path, language_name, )), - ToolchainStoreInner::Remote(remote) => { + ToolchainStoreInner::Remote(remote, _) => { remote.read(cx).active_toolchain(path, language_name, cx) } } @@ -234,13 +239,13 @@ impl ToolchainStore { pub fn as_language_toolchain_store(&self) -> Arc { match &self.0 { ToolchainStoreInner::Local(local, _) => Arc::new(LocalStore(local.downgrade())), - ToolchainStoreInner::Remote(remote) => Arc::new(RemoteStore(remote.downgrade())), + ToolchainStoreInner::Remote(remote, _) => Arc::new(RemoteStore(remote.downgrade())), } } pub fn as_local_store(&self) -> Option<&Entity> { match &self.0 { ToolchainStoreInner::Local(local, _) => Some(local), - ToolchainStoreInner::Remote(_) => None, + ToolchainStoreInner::Remote(_, _) => None, } } } @@ -415,6 +420,8 @@ impl LocalToolchainStore { .cloned() } } + +impl EventEmitter for RemoteToolchainStore {} struct RemoteToolchainStore { client: AnyProtoClient, project_id: u64, @@ -425,27 +432,37 @@ impl RemoteToolchainStore { &self, project_path: ProjectPath, toolchain: Toolchain, - cx: &App, + cx: &mut Context, ) -> Task> { let project_id = self.project_id; let client = self.client.clone(); - cx.background_spawn(async move { - let path = PathBuf::from(toolchain.path.to_string()); - let _ = client - .request(proto::ActivateToolchain { - project_id, - worktree_id: project_path.worktree_id.to_proto(), - language_name: toolchain.language_name.into(), - toolchain: Some(proto::Toolchain { - name: toolchain.name.into(), - path: path.to_proto(), - raw_json: toolchain.as_json.to_string(), - }), - path: Some(project_path.path.to_string_lossy().into_owned()), + cx.spawn(async move |this, cx| { + let did_activate = cx + .background_spawn(async move { + let path = PathBuf::from(toolchain.path.to_string()); + let _ = client + .request(proto::ActivateToolchain { + project_id, + worktree_id: project_path.worktree_id.to_proto(), + language_name: toolchain.language_name.into(), + toolchain: Some(proto::Toolchain { + name: toolchain.name.into(), + path: path.to_proto(), + raw_json: toolchain.as_json.to_string(), + }), + path: Some(project_path.path.to_string_lossy().into_owned()), + }) + .await + .log_err()?; + Some(()) }) - .await - .log_err()?; - Some(()) + .await; + did_activate.and_then(|_| { + this.update(cx, |_, cx| { + cx.emit(ToolchainStoreEvent::ToolchainActivated); + }) + .ok() + }) }) } diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 83caebe62f..6216ff7728 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -46,6 +46,9 @@ pub struct HeadlessProject { pub languages: Arc, pub extensions: Entity, pub git_store: Entity, + // Used mostly to keep alive the toolchain store for RPC handlers. + // Local variant is used within LSP store, but that's a separate entity. + pub _toolchain_store: Entity, } pub struct HeadlessAppState { @@ -269,6 +272,7 @@ impl HeadlessProject { languages, extensions, git_store, + _toolchain_store: toolchain_store, } } diff --git a/crates/toolchain_selector/src/active_toolchain.rs b/crates/toolchain_selector/src/active_toolchain.rs index ea5dcc2a19..bf45bffea3 100644 --- a/crates/toolchain_selector/src/active_toolchain.rs +++ b/crates/toolchain_selector/src/active_toolchain.rs @@ -38,7 +38,6 @@ impl ActiveToolchain { .ok() .flatten(); if let Some(editor) = editor { - this.active_toolchain.take(); this.update_lister(editor, window, cx); } }, @@ -124,16 +123,6 @@ impl ActiveToolchain { if let Some((_, buffer, _)) = editor.active_excerpt(cx) && let Some(worktree_id) = buffer.read(cx).file().map(|file| file.worktree_id(cx)) { - if self - .active_buffer - .as_ref() - .is_some_and(|(old_worktree_id, old_buffer, _)| { - (old_worktree_id, old_buffer.entity_id()) == (&worktree_id, buffer.entity_id()) - }) - { - return; - } - let subscription = cx.subscribe_in( &buffer, window, From c5ee3f3e2e51936910f9ad284d14a7974f064616 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Thu, 21 Aug 2025 05:33:45 -0500 Subject: [PATCH 569/693] Avoid suspending panicking thread while crashing (#36645) On the latest build @maxbrunsfeld got a panic that hung zed. It appeared that the hang occured after the minidump had been successfully written, so our theory on what happened is that the `suspend_all_other_threads` call in the crash handler suspended the panicking thread (due to the signal from simulate_exception being received on a different thread), and then when the crash handler returned everything was suspended so the panic hook never made it to the `process::abort`. This change makes the crash handler avoid _both_ the current and the panicking thread which should avoid that scenario. Release Notes: - N/A --- crates/crashes/src/crashes.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/crashes/src/crashes.rs b/crates/crashes/src/crashes.rs index 4e4b69f639..b1afc5ae45 100644 --- a/crates/crashes/src/crashes.rs +++ b/crates/crashes/src/crashes.rs @@ -4,6 +4,8 @@ use minidumper::{Client, LoopAction, MinidumpBinary}; use release_channel::{RELEASE_CHANNEL, ReleaseChannel}; use serde::{Deserialize, Serialize}; +#[cfg(target_os = "macos")] +use std::sync::atomic::AtomicU32; use std::{ env, fs::{self, File}, @@ -26,6 +28,9 @@ pub static REQUESTED_MINIDUMP: AtomicBool = AtomicBool::new(false); const CRASH_HANDLER_PING_TIMEOUT: Duration = Duration::from_secs(60); const CRASH_HANDLER_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); +#[cfg(target_os = "macos")] +static PANIC_THREAD_ID: AtomicU32 = AtomicU32::new(0); + pub async fn init(crash_init: InitCrashHandler) { if *RELEASE_CHANNEL == ReleaseChannel::Dev && env::var("ZED_GENERATE_MINIDUMPS").is_err() { return; @@ -110,9 +115,10 @@ unsafe fn suspend_all_other_threads() { mach2::task::task_threads(task, &raw mut threads, &raw mut count); } let current = unsafe { mach2::mach_init::mach_thread_self() }; + let panic_thread = PANIC_THREAD_ID.load(Ordering::SeqCst); for i in 0..count { let t = unsafe { *threads.add(i as usize) }; - if t != current { + if t != current && t != panic_thread { unsafe { mach2::thread_act::thread_suspend(t) }; } } @@ -238,6 +244,13 @@ pub fn handle_panic(message: String, span: Option<&Location>) { ) .ok(); log::error!("triggering a crash to generate a minidump..."); + + #[cfg(target_os = "macos")] + PANIC_THREAD_ID.store( + unsafe { mach2::mach_init::mach_thread_self() }, + Ordering::SeqCst, + ); + #[cfg(target_os = "linux")] CrashHandler.simulate_signal(crash_handler::Signal::Trap as u32); #[cfg(not(target_os = "linux"))] From f435af2fdeeda60e24d08bcee56d3b6c5df07ca4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Aug 2025 12:59:51 +0200 Subject: [PATCH 570/693] acp: Use unstaged style for diffs (#36674) Release Notes: - N/A --- crates/acp_thread/src/diff.rs | 55 +++++++++++++++++------------------ 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs index 70367e340a..130bc3ab6b 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -28,12 +28,7 @@ impl Diff { cx: &mut Context, ) -> Self { let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly)); - - let new_buffer = cx.new(|cx| Buffer::local(new_text, cx)); - let old_buffer = cx.new(|cx| Buffer::local(old_text.unwrap_or("".into()), cx)); - let new_buffer_snapshot = new_buffer.read(cx).text_snapshot(); - let buffer_diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot, cx)); - + let buffer = cx.new(|cx| Buffer::local(new_text, cx)); let task = cx.spawn({ let multibuffer = multibuffer.clone(); let path = path.clone(); @@ -43,42 +38,34 @@ impl Diff { .await .log_err(); - new_buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?; + buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?; - let old_buffer_snapshot = old_buffer.update(cx, |buffer, cx| { - buffer.set_language(language, cx); - buffer.snapshot() - })?; - - buffer_diff - .update(cx, |diff, cx| { - diff.set_base_text( - old_buffer_snapshot, - Some(language_registry), - new_buffer_snapshot, - cx, - ) - })? - .await?; + let diff = build_buffer_diff( + old_text.unwrap_or("".into()).into(), + &buffer, + Some(language_registry.clone()), + cx, + ) + .await?; multibuffer .update(cx, |multibuffer, cx| { let hunk_ranges = { - let buffer = new_buffer.read(cx); - let diff = buffer_diff.read(cx); + let buffer = buffer.read(cx); + let diff = diff.read(cx); diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx) .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer)) .collect::>() }; multibuffer.set_excerpts_for_path( - PathKey::for_buffer(&new_buffer, cx), - new_buffer.clone(), + PathKey::for_buffer(&buffer, cx), + buffer.clone(), hunk_ranges, editor::DEFAULT_MULTIBUFFER_CONTEXT, cx, ); - multibuffer.add_diff(buffer_diff, cx); + multibuffer.add_diff(diff, cx); }) .log_err(); @@ -106,6 +93,15 @@ impl Diff { text_snapshot, cx, ); + let snapshot = diff.snapshot(cx); + + let secondary_diff = cx.new(|cx| { + let mut diff = BufferDiff::new(&buffer_snapshot, cx); + diff.set_snapshot(snapshot, &buffer_snapshot, cx); + diff + }); + diff.set_secondary_diff(secondary_diff); + diff }); @@ -204,7 +200,10 @@ impl PendingDiff { ) .await?; buffer_diff.update(cx, |diff, cx| { - diff.set_snapshot(diff_snapshot, &text_snapshot, cx) + diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx); + diff.secondary_diff().unwrap().update(cx, |diff, cx| { + diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx); + }); })?; diff.update(cx, |diff, cx| { if let Diff::Pending(diff) = diff { From ad64a71f04fb2b1a585e26dfa6825545728188a6 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Thu, 21 Aug 2025 13:05:41 +0200 Subject: [PATCH 571/693] acp: Allow collapsing edit file tool calls (#36675) Release Notes: - N/A --- crates/agent_ui/src/acp/entry_view_state.rs | 18 ++++++++--- crates/agent_ui/src/acp/thread_view.rs | 34 +++++++++++---------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index fb15d8bed8..c310473259 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -1,6 +1,7 @@ use std::ops::Range; use acp_thread::{AcpThread, AgentThreadEntry}; +use agent_client_protocol::ToolCallId; use agent2::HistoryStore; use collections::HashMap; use editor::{Editor, EditorMode, MinimapVisibility}; @@ -106,6 +107,7 @@ impl EntryViewState { } } AgentThreadEntry::ToolCall(tool_call) => { + let id = tool_call.id.clone(); let terminals = tool_call.terminals().cloned().collect::>(); let diffs = tool_call.diffs().cloned().collect::>(); @@ -131,16 +133,21 @@ impl EntryViewState { .into_any(); cx.emit(EntryViewEvent { entry_index: index, - view_event: ViewEvent::NewTerminal(terminal.entity_id()), + view_event: ViewEvent::NewTerminal(id.clone()), }); element }); } for diff in diffs { - views - .entry(diff.entity_id()) - .or_insert_with(|| create_editor_diff(diff.clone(), window, cx).into_any()); + views.entry(diff.entity_id()).or_insert_with(|| { + let element = create_editor_diff(diff.clone(), window, cx).into_any(); + cx.emit(EntryViewEvent { + entry_index: index, + view_event: ViewEvent::NewDiff(id.clone()), + }); + element + }); } } AgentThreadEntry::AssistantMessage(_) => { @@ -192,7 +199,8 @@ pub struct EntryViewEvent { } pub enum ViewEvent { - NewTerminal(EntityId), + NewDiff(ToolCallId), + NewTerminal(ToolCallId), MessageEditorEvent(Entity, MessageEditorEvent), } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 432ba4e0e8..9c9e2ee4dd 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -20,11 +20,11 @@ use file_icons::FileIcons; use fs::Fs; use gpui::{ Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, - EdgesRefinement, ElementId, Empty, Entity, EntityId, FocusHandle, Focusable, Hsla, Length, - ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, - Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, - Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, - point, prelude::*, pulsating_between, + EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, + ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, + Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, + WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, point, + prelude::*, pulsating_between, }; use language::Buffer; @@ -256,7 +256,6 @@ pub struct AcpThreadView { auth_task: Option>, expanded_tool_calls: HashSet, expanded_thinking_blocks: HashSet<(usize, usize)>, - expanded_terminals: HashSet, edits_expanded: bool, plan_expanded: bool, editor_expanded: bool, @@ -354,7 +353,6 @@ impl AcpThreadView { auth_task: None, expanded_tool_calls: HashSet::default(), expanded_thinking_blocks: HashSet::default(), - expanded_terminals: HashSet::default(), editing_message: None, edits_expanded: false, plan_expanded: false, @@ -677,9 +675,14 @@ impl AcpThreadView { cx: &mut Context, ) { match &event.view_event { - ViewEvent::NewTerminal(terminal_id) => { + ViewEvent::NewDiff(tool_call_id) => { + if AgentSettings::get_global(cx).expand_edit_card { + self.expanded_tool_calls.insert(tool_call_id.clone()); + } + } + ViewEvent::NewTerminal(tool_call_id) => { if AgentSettings::get_global(cx).expand_terminal_card { - self.expanded_terminals.insert(*terminal_id); + self.expanded_tool_calls.insert(tool_call_id.clone()); } } ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => { @@ -1559,10 +1562,9 @@ impl AcpThreadView { matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some(); let use_card_layout = needs_confirmation || is_edit; - let is_collapsible = !tool_call.content.is_empty() && !use_card_layout; + let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation; - let is_open = - needs_confirmation || is_edit || self.expanded_tool_calls.contains(&tool_call.id); + let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id); let gradient_overlay = |color: Hsla| { div() @@ -2014,7 +2016,7 @@ impl AcpThreadView { .map(|path| format!("{}", path.display())) .unwrap_or_else(|| "current directory".to_string()); - let is_expanded = self.expanded_terminals.contains(&terminal.entity_id()); + let is_expanded = self.expanded_tool_calls.contains(&tool_call.id); let header = h_flex() .id(SharedString::from(format!( @@ -2154,12 +2156,12 @@ impl AcpThreadView { .opened_icon(IconName::ChevronUp) .closed_icon(IconName::ChevronDown) .on_click(cx.listener({ - let terminal_id = terminal.entity_id(); + let id = tool_call.id.clone(); move |this, _event, _window, _cx| { if is_expanded { - this.expanded_terminals.remove(&terminal_id); + this.expanded_tool_calls.remove(&id); } else { - this.expanded_terminals.insert(terminal_id); + this.expanded_tool_calls.insert(id.clone()); } } })), From f63d8e4c538d69d3b76ed7ec93bdd88f57e6cee0 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 21 Aug 2025 09:23:56 -0400 Subject: [PATCH 572/693] Show excerpt dividers in `without_headers` multibuffers (#36647) Release Notes: - Fixed diff cards in agent threads not showing dividers between disjoint edited regions. --- crates/editor/src/display_map/block_map.rs | 99 ++++++++++++++-------- crates/editor/src/element.rs | 72 +++++++++------- crates/editor/src/test.rs | 35 ++++---- 3 files changed, 122 insertions(+), 84 deletions(-) diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index e32a4e45db..b073fe7be7 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -290,7 +290,10 @@ pub enum Block { ExcerptBoundary { excerpt: ExcerptInfo, height: u32, - starts_new_buffer: bool, + }, + BufferHeader { + excerpt: ExcerptInfo, + height: u32, }, } @@ -303,27 +306,37 @@ impl Block { .. } => BlockId::ExcerptBoundary(next_excerpt.id), Block::FoldedBuffer { first_excerpt, .. } => BlockId::FoldedBuffer(first_excerpt.id), + Block::BufferHeader { + excerpt: next_excerpt, + .. + } => BlockId::ExcerptBoundary(next_excerpt.id), } } pub fn has_height(&self) -> bool { match self { Block::Custom(block) => block.height.is_some(), - Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } => true, + Block::ExcerptBoundary { .. } + | Block::FoldedBuffer { .. } + | Block::BufferHeader { .. } => true, } } pub fn height(&self) -> u32 { match self { Block::Custom(block) => block.height.unwrap_or(0), - Block::ExcerptBoundary { height, .. } | Block::FoldedBuffer { height, .. } => *height, + Block::ExcerptBoundary { height, .. } + | Block::FoldedBuffer { height, .. } + | Block::BufferHeader { height, .. } => *height, } } pub fn style(&self) -> BlockStyle { match self { Block::Custom(block) => block.style, - Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } => BlockStyle::Sticky, + Block::ExcerptBoundary { .. } + | Block::FoldedBuffer { .. } + | Block::BufferHeader { .. } => BlockStyle::Sticky, } } @@ -332,6 +345,7 @@ impl Block { Block::Custom(block) => matches!(block.placement, BlockPlacement::Above(_)), Block::FoldedBuffer { .. } => false, Block::ExcerptBoundary { .. } => true, + Block::BufferHeader { .. } => true, } } @@ -340,6 +354,7 @@ impl Block { Block::Custom(block) => matches!(block.placement, BlockPlacement::Near(_)), Block::FoldedBuffer { .. } => false, Block::ExcerptBoundary { .. } => false, + Block::BufferHeader { .. } => false, } } @@ -351,6 +366,7 @@ impl Block { ), Block::FoldedBuffer { .. } => false, Block::ExcerptBoundary { .. } => false, + Block::BufferHeader { .. } => false, } } @@ -359,6 +375,7 @@ impl Block { Block::Custom(block) => matches!(block.placement, BlockPlacement::Replace(_)), Block::FoldedBuffer { .. } => true, Block::ExcerptBoundary { .. } => false, + Block::BufferHeader { .. } => false, } } @@ -367,6 +384,7 @@ impl Block { Block::Custom(_) => false, Block::FoldedBuffer { .. } => true, Block::ExcerptBoundary { .. } => true, + Block::BufferHeader { .. } => true, } } @@ -374,9 +392,8 @@ impl Block { match self { Block::Custom(_) => false, Block::FoldedBuffer { .. } => true, - Block::ExcerptBoundary { - starts_new_buffer, .. - } => *starts_new_buffer, + Block::ExcerptBoundary { .. } => false, + Block::BufferHeader { .. } => true, } } } @@ -393,14 +410,14 @@ impl Debug for Block { .field("first_excerpt", &first_excerpt) .field("height", height) .finish(), - Self::ExcerptBoundary { - starts_new_buffer, - excerpt, - height, - } => f + Self::ExcerptBoundary { excerpt, height } => f .debug_struct("ExcerptBoundary") .field("excerpt", excerpt) - .field("starts_new_buffer", starts_new_buffer) + .field("height", height) + .finish(), + Self::BufferHeader { excerpt, height } => f + .debug_struct("BufferHeader") + .field("excerpt", excerpt) .field("height", height) .finish(), } @@ -662,13 +679,11 @@ impl BlockMap { }), ); - if buffer.show_headers() { - blocks_in_edit.extend(self.header_and_footer_blocks( - buffer, - (start_bound, end_bound), - wrap_snapshot, - )); - } + blocks_in_edit.extend(self.header_and_footer_blocks( + buffer, + (start_bound, end_bound), + wrap_snapshot, + )); BlockMap::sort_blocks(&mut blocks_in_edit); @@ -771,7 +786,7 @@ impl BlockMap { if self.buffers_with_disabled_headers.contains(&new_buffer_id) { continue; } - if self.folded_buffers.contains(&new_buffer_id) { + if self.folded_buffers.contains(&new_buffer_id) && buffer.show_headers() { let mut last_excerpt_end_row = first_excerpt.end_row; while let Some(next_boundary) = boundaries.peek() { @@ -804,20 +819,24 @@ impl BlockMap { } } - if new_buffer_id.is_some() { + let starts_new_buffer = new_buffer_id.is_some(); + let block = if starts_new_buffer && buffer.show_headers() { height += self.buffer_header_height; - } else { + Block::BufferHeader { + excerpt: excerpt_boundary.next, + height, + } + } else if excerpt_boundary.prev.is_some() { height += self.excerpt_header_height; - } - - return Some(( - BlockPlacement::Above(WrapRow(wrap_row)), Block::ExcerptBoundary { excerpt: excerpt_boundary.next, height, - starts_new_buffer: new_buffer_id.is_some(), - }, - )); + } + } else { + continue; + }; + + return Some((BlockPlacement::Above(WrapRow(wrap_row)), block)); } }) } @@ -842,13 +861,25 @@ impl BlockMap { ( Block::ExcerptBoundary { excerpt: excerpt_a, .. + } + | Block::BufferHeader { + excerpt: excerpt_a, .. }, Block::ExcerptBoundary { excerpt: excerpt_b, .. + } + | Block::BufferHeader { + excerpt: excerpt_b, .. }, ) => Some(excerpt_a.id).cmp(&Some(excerpt_b.id)), - (Block::ExcerptBoundary { .. }, Block::Custom(_)) => Ordering::Less, - (Block::Custom(_), Block::ExcerptBoundary { .. }) => Ordering::Greater, + ( + Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }, + Block::Custom(_), + ) => Ordering::Less, + ( + Block::Custom(_), + Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }, + ) => Ordering::Greater, (Block::Custom(block_a), Block::Custom(block_b)) => block_a .priority .cmp(&block_b.priority) @@ -1377,7 +1408,9 @@ impl BlockSnapshot { while let Some(transform) = cursor.item() { match &transform.block { - Some(Block::ExcerptBoundary { excerpt, .. }) => { + Some( + Block::ExcerptBoundary { excerpt, .. } | Block::BufferHeader { excerpt, .. }, + ) => { return Some(StickyHeaderExcerpt { excerpt }); } Some(block) if block.is_buffer_header() => return None, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 416f35d7a7..797b0d6634 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2749,7 +2749,10 @@ impl EditorElement { let mut block_offset = 0; let mut found_excerpt_header = false; for (_, block) in snapshot.blocks_in_range(prev_line..row_range.start) { - if matches!(block, Block::ExcerptBoundary { .. }) { + if matches!( + block, + Block::ExcerptBoundary { .. } | Block::BufferHeader { .. } + ) { found_excerpt_header = true; break; } @@ -2766,7 +2769,10 @@ impl EditorElement { let mut block_height = 0; let mut found_excerpt_header = false; for (_, block) in snapshot.blocks_in_range(row_range.end..cons_line) { - if matches!(block, Block::ExcerptBoundary { .. }) { + if matches!( + block, + Block::ExcerptBoundary { .. } | Block::BufferHeader { .. } + ) { found_excerpt_header = true; } block_height += block.height(); @@ -3452,42 +3458,41 @@ impl EditorElement { .into_any_element() } - Block::ExcerptBoundary { - excerpt, - height, - starts_new_buffer, - .. - } => { + Block::ExcerptBoundary { .. } => { let color = cx.theme().colors().clone(); let mut result = v_flex().id(block_id).w_full(); + result = result.child( + h_flex().relative().child( + div() + .top(line_height / 2.) + .absolute() + .w_full() + .h_px() + .bg(color.border_variant), + ), + ); + + result.into_any() + } + + Block::BufferHeader { excerpt, height } => { + let mut result = v_flex().id(block_id).w_full(); + let jump_data = header_jump_data(snapshot, block_row_start, *height, excerpt); - if *starts_new_buffer { - if sticky_header_excerpt_id != Some(excerpt.id) { - let selected = selected_buffer_ids.contains(&excerpt.buffer_id); + if sticky_header_excerpt_id != Some(excerpt.id) { + let selected = selected_buffer_ids.contains(&excerpt.buffer_id); - result = result.child(div().pr(editor_margins.right).child( - self.render_buffer_header( - excerpt, false, selected, false, jump_data, window, cx, - ), - )); - } else { - result = - result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height())); - } - } else { - result = result.child( - h_flex().relative().child( - div() - .top(line_height / 2.) - .absolute() - .w_full() - .h_px() - .bg(color.border_variant), + result = result.child(div().pr(editor_margins.right).child( + self.render_buffer_header( + excerpt, false, selected, false, jump_data, window, cx, ), - ); - }; + )); + } else { + result = + result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height())); + } result.into_any() } @@ -5708,7 +5713,10 @@ impl EditorElement { let end_row_in_current_excerpt = snapshot .blocks_in_range(start_row..end_row) .find_map(|(start_row, block)| { - if matches!(block, Block::ExcerptBoundary { .. }) { + if matches!( + block, + Block::ExcerptBoundary { .. } | Block::BufferHeader { .. } + ) { Some(start_row) } else { None diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index d388e8f3b7..960fecf59a 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -230,26 +230,23 @@ pub fn editor_content_with_blocks(editor: &Entity, cx: &mut VisualTestCo lines[row as usize].push_str("§ -----"); } } - Block::ExcerptBoundary { - excerpt, - height, - starts_new_buffer, - } => { - if starts_new_buffer { - lines[row.0 as usize].push_str(&cx.update(|_, cx| { - format!( - "§ {}", - excerpt - .buffer - .file() - .unwrap() - .file_name(cx) - .to_string_lossy() - ) - })); - } else { - lines[row.0 as usize].push_str("§ -----") + Block::ExcerptBoundary { height, .. } => { + for row in row.0..row.0 + height { + lines[row as usize].push_str("§ -----"); } + } + Block::BufferHeader { excerpt, height } => { + lines[row.0 as usize].push_str(&cx.update(|_, cx| { + format!( + "§ {}", + excerpt + .buffer + .file() + .unwrap() + .file_name(cx) + .to_string_lossy() + ) + })); for row in row.0 + 1..row.0 + height { lines[row as usize].push_str("§ -----"); } From 1dd237139cfb4f12982f1db86c87ab8b85c9593f Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 21 Aug 2025 09:24:34 -0400 Subject: [PATCH 573/693] Fix more improper uses of the `buffer_id` field of `Anchor` (#36636) Follow-up to #36524 Release Notes: - N/A --- crates/editor/src/editor.rs | 73 +++++++++------------- crates/editor/src/hover_links.rs | 5 +- crates/editor/src/inlay_hint_cache.rs | 5 +- crates/editor/src/linked_editing_ranges.rs | 2 +- crates/editor/src/mouse_context_menu.rs | 18 +++--- crates/outline_panel/src/outline_panel.rs | 5 +- 6 files changed, 49 insertions(+), 59 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e32ea1cb3a..05ee295360 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6697,7 +6697,6 @@ impl Editor { return; } - let buffer_id = cursor_position.buffer_id; let buffer = this.buffer.read(cx); if buffer .text_anchor_for_position(cursor_position, cx) @@ -6710,8 +6709,8 @@ impl Editor { let mut write_ranges = Vec::new(); let mut read_ranges = Vec::new(); for highlight in highlights { - for (excerpt_id, excerpt_range) in - buffer.excerpts_for_buffer(cursor_buffer.read(cx).remote_id(), cx) + let buffer_id = cursor_buffer.read(cx).remote_id(); + for (excerpt_id, excerpt_range) in buffer.excerpts_for_buffer(buffer_id, cx) { let start = highlight .range @@ -6726,12 +6725,12 @@ impl Editor { } let range = Anchor { - buffer_id, + buffer_id: Some(buffer_id), excerpt_id, text_anchor: start, diff_base_anchor: None, }..Anchor { - buffer_id, + buffer_id: Some(buffer_id), excerpt_id, text_anchor: end, diff_base_anchor: None, @@ -9496,17 +9495,21 @@ impl Editor { selection: Range, cx: &mut Context, ) { - let buffer_id = match (&selection.start.buffer_id, &selection.end.buffer_id) { - (Some(a), Some(b)) if a == b => a, - _ => { - log::error!("expected anchor range to have matching buffer IDs"); - return; - } - }; - let multi_buffer = self.buffer().read(cx); - let Some(buffer) = multi_buffer.buffer(*buffer_id) else { + let Some((_, buffer, _)) = self + .buffer() + .read(cx) + .excerpt_containing(selection.start, cx) + else { return; }; + let Some((_, end_buffer, _)) = self.buffer().read(cx).excerpt_containing(selection.end, cx) + else { + return; + }; + if buffer != end_buffer { + log::error!("expected anchor range to have matching buffer IDs"); + return; + } let id = post_inc(&mut self.next_completion_id); let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; @@ -10593,16 +10596,12 @@ impl Editor { snapshot: &EditorSnapshot, cx: &mut Context, ) -> Option<(Anchor, Breakpoint)> { - let project = self.project.clone()?; - - let buffer_id = breakpoint_position.buffer_id.or_else(|| { - snapshot - .buffer_snapshot - .buffer_id_for_excerpt(breakpoint_position.excerpt_id) - })?; + let buffer = self + .buffer + .read(cx) + .buffer_for_anchor(breakpoint_position, cx)?; let enclosing_excerpt = breakpoint_position.excerpt_id; - let buffer = project.read(cx).buffer_for_id(buffer_id, cx)?; let buffer_snapshot = buffer.read(cx).snapshot(); let row = buffer_snapshot @@ -10775,21 +10774,11 @@ impl Editor { return; }; - let Some(buffer_id) = breakpoint_position.buffer_id.or_else(|| { - if breakpoint_position == Anchor::min() { - self.buffer() - .read(cx) - .excerpt_buffer_ids() - .into_iter() - .next() - } else { - None - } - }) else { - return; - }; - - let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else { + let Some(buffer) = self + .buffer + .read(cx) + .buffer_for_anchor(breakpoint_position, cx) + else { return; }; @@ -15432,7 +15421,8 @@ impl Editor { return; }; - let Some(buffer_id) = buffer.anchor_after(next_diagnostic.range.start).buffer_id else { + let next_diagnostic_start = buffer.anchor_after(next_diagnostic.range.start); + let Some(buffer_id) = buffer.buffer_id_for_anchor(next_diagnostic_start) else { return; }; self.change_selections(Default::default(), window, cx, |s| { @@ -20425,11 +20415,8 @@ impl Editor { .range_to_buffer_ranges_with_deleted_hunks(selection.range()) { if let Some(anchor) = anchor { - // selection is in a deleted hunk - let Some(buffer_id) = anchor.buffer_id else { - continue; - }; - let Some(buffer_handle) = multi_buffer.buffer(buffer_id) else { + let Some(buffer_handle) = multi_buffer.buffer_for_anchor(anchor, cx) + else { continue; }; let offset = text::ToOffset::to_offset( diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 1d7d56e67d..94f49f601a 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -321,7 +321,10 @@ pub fn update_inlay_link_and_hover_points( if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) { match cached_hint.resolve_state { ResolveState::CanResolve(_, _) => { - if let Some(buffer_id) = previous_valid_anchor.buffer_id { + if let Some(buffer_id) = snapshot + .buffer_snapshot + .buffer_id_for_anchor(previous_valid_anchor) + { inlay_hint_cache.spawn_hint_resolve( buffer_id, excerpt_id, diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index cea0e32d7f..dbf5ac95b7 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -475,10 +475,7 @@ impl InlayHintCache { let excerpt_cached_hints = excerpt_cached_hints.read(); let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable(); shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| { - let Some(buffer) = shown_anchor - .buffer_id - .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) - else { + let Some(buffer) = multi_buffer.buffer_for_anchor(*shown_anchor, cx) else { return false; }; let buffer_snapshot = buffer.read(cx).snapshot(); diff --git a/crates/editor/src/linked_editing_ranges.rs b/crates/editor/src/linked_editing_ranges.rs index aaf9032b04..4f1313797f 100644 --- a/crates/editor/src/linked_editing_ranges.rs +++ b/crates/editor/src/linked_editing_ranges.rs @@ -72,7 +72,7 @@ pub(super) fn refresh_linked_ranges( // Throw away selections spanning multiple buffers. continue; } - if let Some(buffer) = end_position.buffer_id.and_then(|id| buffer.buffer(id)) { + if let Some(buffer) = buffer.buffer_for_anchor(end_position, cx) { applicable_selections.push(( buffer, start_position.text_anchor, diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 5cf22de537..3bc334c54c 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -190,14 +190,16 @@ pub fn deploy_context_menu( .all::(cx) .into_iter() .any(|s| !s.is_empty()); - let has_git_repo = anchor.buffer_id.is_some_and(|buffer_id| { - project - .read(cx) - .git_store() - .read(cx) - .repository_and_path_for_buffer_id(buffer_id, cx) - .is_some() - }); + let has_git_repo = buffer + .buffer_id_for_anchor(anchor) + .is_some_and(|buffer_id| { + project + .read(cx) + .git_store() + .read(cx) + .repository_and_path_for_buffer_id(buffer_id, cx) + .is_some() + }); let evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx); let run_to_cursor = window.is_action_available(&RunToCursor, cx); diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 59c43f945f..10698cead8 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -4393,12 +4393,13 @@ impl OutlinePanel { }) .filter(|(match_range, _)| { let editor = active_editor.read(cx); - if let Some(buffer_id) = match_range.start.buffer_id + let snapshot = editor.buffer().read(cx).snapshot(cx); + if let Some(buffer_id) = snapshot.buffer_id_for_anchor(match_range.start) && editor.is_buffer_folded(buffer_id, cx) { return false; } - if let Some(buffer_id) = match_range.start.buffer_id + if let Some(buffer_id) = snapshot.buffer_id_for_anchor(match_range.end) && editor.is_buffer_folded(buffer_id, cx) { return false; From e0613cbd0f203a845cc622d04f47d9a54931a160 Mon Sep 17 00:00:00 2001 From: David Kleingeld Date: Thu, 21 Aug 2025 15:56:16 +0200 Subject: [PATCH 574/693] Add Rodio audio pipeline as alternative to current LiveKit pipeline (#36607) Rodio parts are well tested and need less configuration then the livekit parts. I suspect there is a bug in the livekit configuration regarding resampling. Rather then investigate that it seemed faster & easier to swap in Rodio. This opens the door to using other Rodio parts like: - Decibel based volume control - Limiter (prevents sound from becoming too loud) - Automatic gain control To use this add to settings: ``` "audio": { "experimental.rodio_audio": true } ``` Release Notes: - N/A Co-authored-by: Mikayla Co-authored-by: Antonio Scandurra --- Cargo.lock | 7 +- crates/audio/Cargo.toml | 5 +- crates/audio/src/assets.rs | 54 ------------- crates/audio/src/audio.rs | 76 ++++++++++++------- crates/audio/src/audio_settings.rs | 33 ++++++++ crates/livekit_client/Cargo.toml | 2 + crates/livekit_client/src/lib.rs | 7 +- crates/livekit_client/src/livekit_client.rs | 14 +++- .../src/livekit_client/playback.rs | 64 +++++++++++----- .../src/livekit_client/playback/source.rs | 67 ++++++++++++++++ crates/settings/src/settings_store.rs | 5 ++ crates/zed/src/main.rs | 2 +- crates/zed/src/zed.rs | 2 +- 13 files changed, 226 insertions(+), 112 deletions(-) delete mode 100644 crates/audio/src/assets.rs create mode 100644 crates/audio/src/audio_settings.rs create mode 100644 crates/livekit_client/src/livekit_client/playback/source.rs diff --git a/Cargo.lock b/Cargo.lock index 76f8672d4d..ddeaebd0bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1379,10 +1379,11 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", - "derive_more 0.99.19", "gpui", - "parking_lot", "rodio", + "schemars", + "serde", + "settings", "util", "workspace-hack", ] @@ -9621,6 +9622,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "audio", "collections", "core-foundation 0.10.0", "core-video", @@ -9643,6 +9645,7 @@ dependencies = [ "scap", "serde", "serde_json", + "settings", "sha2", "simplelog", "smallvec", diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index 5146396b92..ae7eb52fd3 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -15,9 +15,10 @@ doctest = false [dependencies] anyhow.workspace = true collections.workspace = true -derive_more.workspace = true gpui.workspace = true -parking_lot.workspace = true +settings.workspace = true +schemars.workspace = true +serde.workspace = true rodio = { workspace = true, features = [ "wav", "playback", "tracing" ] } util.workspace = true workspace-hack.workspace = true diff --git a/crates/audio/src/assets.rs b/crates/audio/src/assets.rs deleted file mode 100644 index fd5c935d87..0000000000 --- a/crates/audio/src/assets.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::{io::Cursor, sync::Arc}; - -use anyhow::{Context as _, Result}; -use collections::HashMap; -use gpui::{App, AssetSource, Global}; -use rodio::{Decoder, Source, source::Buffered}; - -type Sound = Buffered>>>; - -pub struct SoundRegistry { - cache: Arc>>, - assets: Box, -} - -struct GlobalSoundRegistry(Arc); - -impl Global for GlobalSoundRegistry {} - -impl SoundRegistry { - pub fn new(source: impl AssetSource) -> Arc { - Arc::new(Self { - cache: Default::default(), - assets: Box::new(source), - }) - } - - pub fn global(cx: &App) -> Arc { - cx.global::().0.clone() - } - - pub(crate) fn set_global(source: impl AssetSource, cx: &mut App) { - cx.set_global(GlobalSoundRegistry(SoundRegistry::new(source))); - } - - pub fn get(&self, name: &str) -> Result + use<>> { - if let Some(wav) = self.cache.lock().get(name) { - return Ok(wav.clone()); - } - - let path = format!("sounds/{}.wav", name); - let bytes = self - .assets - .load(&path)? - .map(anyhow::Ok) - .with_context(|| format!("No asset available for path {path}"))?? - .into_owned(); - let cursor = Cursor::new(bytes); - let source = Decoder::new(cursor)?.buffered(); - - self.cache.lock().insert(name.to_string(), source.clone()); - - Ok(source) - } -} diff --git a/crates/audio/src/audio.rs b/crates/audio/src/audio.rs index 44baa16aa2..b4f2c24fef 100644 --- a/crates/audio/src/audio.rs +++ b/crates/audio/src/audio.rs @@ -1,16 +1,19 @@ -use assets::SoundRegistry; -use derive_more::{Deref, DerefMut}; -use gpui::{App, AssetSource, BorrowAppContext, Global}; -use rodio::{OutputStream, OutputStreamBuilder}; +use anyhow::{Context as _, Result, anyhow}; +use collections::HashMap; +use gpui::{App, BorrowAppContext, Global}; +use rodio::{Decoder, OutputStream, OutputStreamBuilder, Source, source::Buffered}; +use settings::Settings; +use std::io::Cursor; use util::ResultExt; -mod assets; +mod audio_settings; +pub use audio_settings::AudioSettings; -pub fn init(source: impl AssetSource, cx: &mut App) { - SoundRegistry::set_global(source, cx); - cx.set_global(GlobalAudio(Audio::new())); +pub fn init(cx: &mut App) { + AudioSettings::register(cx); } +#[derive(Copy, Clone, Eq, Hash, PartialEq)] pub enum Sound { Joined, Leave, @@ -38,18 +41,12 @@ impl Sound { #[derive(Default)] pub struct Audio { output_handle: Option, + source_cache: HashMap>>>>, } -#[derive(Deref, DerefMut)] -struct GlobalAudio(Audio); - -impl Global for GlobalAudio {} +impl Global for Audio {} impl Audio { - pub fn new() -> Self { - Self::default() - } - fn ensure_output_exists(&mut self) -> Option<&OutputStream> { if self.output_handle.is_none() { self.output_handle = OutputStreamBuilder::open_default_stream().log_err(); @@ -58,26 +55,51 @@ impl Audio { self.output_handle.as_ref() } - pub fn play_sound(sound: Sound, cx: &mut App) { - if !cx.has_global::() { - return; - } + pub fn play_source( + source: impl rodio::Source + Send + 'static, + cx: &mut App, + ) -> anyhow::Result<()> { + cx.update_default_global(|this: &mut Self, _cx| { + let output_handle = this + .ensure_output_exists() + .ok_or_else(|| anyhow!("Could not open audio output"))?; + output_handle.mixer().add(source); + Ok(()) + }) + } - cx.update_global::(|this, cx| { + pub fn play_sound(sound: Sound, cx: &mut App) { + cx.update_default_global(|this: &mut Self, cx| { + let source = this.sound_source(sound, cx).log_err()?; let output_handle = this.ensure_output_exists()?; - let source = SoundRegistry::global(cx).get(sound.file()).log_err()?; output_handle.mixer().add(source); Some(()) }); } pub fn end_call(cx: &mut App) { - if !cx.has_global::() { - return; - } - - cx.update_global::(|this, _| { + cx.update_default_global(|this: &mut Self, _cx| { this.output_handle.take(); }); } + + fn sound_source(&mut self, sound: Sound, cx: &App) -> Result> { + if let Some(wav) = self.source_cache.get(&sound) { + return Ok(wav.clone()); + } + + let path = format!("sounds/{}.wav", sound.file()); + let bytes = cx + .asset_source() + .load(&path)? + .map(anyhow::Ok) + .with_context(|| format!("No asset available for path {path}"))?? + .into_owned(); + let cursor = Cursor::new(bytes); + let source = Decoder::new(cursor)?.buffered(); + + self.source_cache.insert(sound, source.clone()); + + Ok(source) + } } diff --git a/crates/audio/src/audio_settings.rs b/crates/audio/src/audio_settings.rs new file mode 100644 index 0000000000..807179881c --- /dev/null +++ b/crates/audio/src/audio_settings.rs @@ -0,0 +1,33 @@ +use anyhow::Result; +use gpui::App; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::{Settings, SettingsSources}; + +#[derive(Deserialize, Debug)] +pub struct AudioSettings { + /// Opt into the new audio system. + #[serde(rename = "experimental.rodio_audio", default)] + pub rodio_audio: bool, // default is false +} + +/// Configuration of audio in Zed. +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +#[serde(default)] +pub struct AudioSettingsContent { + /// Whether to use the experimental audio system + #[serde(rename = "experimental.rodio_audio", default)] + pub rodio_audio: bool, +} + +impl Settings for AudioSettings { + const KEY: Option<&'static str> = Some("audio"); + + type FileContent = AudioSettingsContent; + + fn load(sources: SettingsSources, _cx: &mut App) -> Result { + sources.json_merge() + } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} +} diff --git a/crates/livekit_client/Cargo.toml b/crates/livekit_client/Cargo.toml index 58059967b7..3575325ac0 100644 --- a/crates/livekit_client/Cargo.toml +++ b/crates/livekit_client/Cargo.toml @@ -25,6 +25,7 @@ async-trait.workspace = true collections.workspace = true cpal.workspace = true futures.workspace = true +audio.workspace = true gpui = { workspace = true, features = ["screen-capture", "x11", "wayland", "windows-manifest"] } gpui_tokio.workspace = true http_client_tls.workspace = true @@ -35,6 +36,7 @@ nanoid.workspace = true parking_lot.workspace = true postage.workspace = true smallvec.workspace = true +settings.workspace = true tokio-tungstenite.workspace = true util.workspace = true workspace-hack.workspace = true diff --git a/crates/livekit_client/src/lib.rs b/crates/livekit_client/src/lib.rs index e3934410e1..055aa3704e 100644 --- a/crates/livekit_client/src/lib.rs +++ b/crates/livekit_client/src/lib.rs @@ -24,8 +24,11 @@ mod livekit_client; )))] pub use livekit_client::*; -// If you need proper LSP in livekit_client you've got to comment out -// the mocks and test +// If you need proper LSP in livekit_client you've got to comment +// - the cfg blocks above +// - the mods: mock_client & test and their conditional blocks +// - the pub use mock_client::* and their conditional blocks + #[cfg(any( test, feature = "test-support", diff --git a/crates/livekit_client/src/livekit_client.rs b/crates/livekit_client/src/livekit_client.rs index adeea4f512..0751b014f4 100644 --- a/crates/livekit_client/src/livekit_client.rs +++ b/crates/livekit_client/src/livekit_client.rs @@ -1,15 +1,16 @@ use std::sync::Arc; use anyhow::{Context as _, Result}; +use audio::AudioSettings; use collections::HashMap; use futures::{SinkExt, channel::mpsc}; use gpui::{App, AsyncApp, ScreenCaptureSource, ScreenCaptureStream, Task}; use gpui_tokio::Tokio; +use log::info; use playback::capture_local_video_track; +use settings::Settings; mod playback; -#[cfg(feature = "record-microphone")] -mod record; use crate::{LocalTrack, Participant, RemoteTrack, RoomEvent, TrackPublication}; pub use playback::AudioStream; @@ -125,9 +126,14 @@ impl Room { pub fn play_remote_audio_track( &self, track: &RemoteAudioTrack, - _cx: &App, + cx: &mut App, ) -> Result { - Ok(self.playback.play_remote_audio_track(&track.0)) + if AudioSettings::get_global(cx).rodio_audio { + info!("Using experimental.rodio_audio audio pipeline"); + playback::play_remote_audio_track(&track.0, cx) + } else { + Ok(self.playback.play_remote_audio_track(&track.0)) + } } } diff --git a/crates/livekit_client/src/livekit_client/playback.rs b/crates/livekit_client/src/livekit_client/playback.rs index e13fb7bd81..d6b64dbaca 100644 --- a/crates/livekit_client/src/livekit_client/playback.rs +++ b/crates/livekit_client/src/livekit_client/playback.rs @@ -18,13 +18,16 @@ use livekit::webrtc::{ video_stream::native::NativeVideoStream, }; use parking_lot::Mutex; +use rodio::Source; use std::cell::RefCell; use std::sync::Weak; -use std::sync::atomic::{self, AtomicI32}; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use std::time::Duration; use std::{borrow::Cow, collections::VecDeque, sync::Arc, thread}; use util::{ResultExt as _, maybe}; +mod source; + pub(crate) struct AudioStack { executor: BackgroundExecutor, apm: Arc>, @@ -40,6 +43,29 @@ pub(crate) struct AudioStack { const SAMPLE_RATE: u32 = 48000; const NUM_CHANNELS: u32 = 2; +pub(crate) fn play_remote_audio_track( + track: &livekit::track::RemoteAudioTrack, + cx: &mut gpui::App, +) -> Result { + let stop_handle = Arc::new(AtomicBool::new(false)); + let stop_handle_clone = stop_handle.clone(); + let stream = source::LiveKitStream::new(cx.background_executor(), track) + .stoppable() + .periodic_access(Duration::from_millis(50), move |s| { + if stop_handle.load(Ordering::Relaxed) { + s.stop(); + } + }); + audio::Audio::play_source(stream, cx).context("Could not play audio")?; + + let on_drop = util::defer(move || { + stop_handle_clone.store(true, Ordering::Relaxed); + }); + Ok(AudioStream::Output { + _drop: Box::new(on_drop), + }) +} + impl AudioStack { pub(crate) fn new(executor: BackgroundExecutor) -> Self { let apm = Arc::new(Mutex::new(apm::AudioProcessingModule::new( @@ -61,7 +87,7 @@ impl AudioStack { ) -> AudioStream { let output_task = self.start_output(); - let next_ssrc = self.next_ssrc.fetch_add(1, atomic::Ordering::Relaxed); + let next_ssrc = self.next_ssrc.fetch_add(1, Ordering::Relaxed); let source = AudioMixerSource { ssrc: next_ssrc, sample_rate: SAMPLE_RATE, @@ -97,6 +123,23 @@ impl AudioStack { } } + fn start_output(&self) -> Arc> { + if let Some(task) = self._output_task.borrow().upgrade() { + return task; + } + let task = Arc::new(self.executor.spawn({ + let apm = self.apm.clone(); + let mixer = self.mixer.clone(); + async move { + Self::play_output(apm, mixer, SAMPLE_RATE, NUM_CHANNELS) + .await + .log_err(); + } + })); + *self._output_task.borrow_mut() = Arc::downgrade(&task); + task + } + pub(crate) fn capture_local_microphone_track( &self, ) -> Result<(crate::LocalAudioTrack, AudioStream)> { @@ -139,23 +182,6 @@ impl AudioStack { )) } - fn start_output(&self) -> Arc> { - if let Some(task) = self._output_task.borrow().upgrade() { - return task; - } - let task = Arc::new(self.executor.spawn({ - let apm = self.apm.clone(); - let mixer = self.mixer.clone(); - async move { - Self::play_output(apm, mixer, SAMPLE_RATE, NUM_CHANNELS) - .await - .log_err(); - } - })); - *self._output_task.borrow_mut() = Arc::downgrade(&task); - task - } - async fn play_output( apm: Arc>, mixer: Arc>, diff --git a/crates/livekit_client/src/livekit_client/playback/source.rs b/crates/livekit_client/src/livekit_client/playback/source.rs new file mode 100644 index 0000000000..021640247d --- /dev/null +++ b/crates/livekit_client/src/livekit_client/playback/source.rs @@ -0,0 +1,67 @@ +use futures::StreamExt; +use libwebrtc::{audio_stream::native::NativeAudioStream, prelude::AudioFrame}; +use livekit::track::RemoteAudioTrack; +use rodio::{Source, buffer::SamplesBuffer, conversions::SampleTypeConverter}; + +use crate::livekit_client::playback::{NUM_CHANNELS, SAMPLE_RATE}; + +fn frame_to_samplesbuffer(frame: AudioFrame) -> SamplesBuffer { + let samples = frame.data.iter().copied(); + let samples = SampleTypeConverter::<_, _>::new(samples); + let samples: Vec = samples.collect(); + SamplesBuffer::new(frame.num_channels as u16, frame.sample_rate, samples) +} + +pub struct LiveKitStream { + // shared_buffer: SharedBuffer, + inner: rodio::queue::SourcesQueueOutput, + _receiver_task: gpui::Task<()>, +} + +impl LiveKitStream { + pub fn new(executor: &gpui::BackgroundExecutor, track: &RemoteAudioTrack) -> Self { + let mut stream = + NativeAudioStream::new(track.rtc_track(), SAMPLE_RATE as i32, NUM_CHANNELS as i32); + let (queue_input, queue_output) = rodio::queue::queue(true); + // spawn rtc stream + let receiver_task = executor.spawn({ + async move { + while let Some(frame) = stream.next().await { + let samples = frame_to_samplesbuffer(frame); + queue_input.append(samples); + } + } + }); + + LiveKitStream { + _receiver_task: receiver_task, + inner: queue_output, + } + } +} + +impl Iterator for LiveKitStream { + type Item = rodio::Sample; + + fn next(&mut self) -> Option { + self.inner.next() + } +} + +impl Source for LiveKitStream { + fn current_span_len(&self) -> Option { + self.inner.current_span_len() + } + + fn channels(&self) -> rodio::ChannelCount { + self.inner.channels() + } + + fn sample_rate(&self) -> rodio::SampleRate { + self.inner.sample_rate() + } + + fn total_duration(&self) -> Option { + self.inner.total_duration() + } +} diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 211db46c6c..3deaed8b9d 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -60,6 +60,11 @@ pub trait Settings: 'static + Send + Sync { /// The logic for combining together values from one or more JSON files into the /// final value for this setting. + /// + /// # Warning + /// `Self::FileContent` deserialized field names should match with `Self` deserialized field names + /// otherwise the field won't be deserialized properly and you will get the error: + /// "A default setting must be added to the `default.json` file" fn load(sources: SettingsSources, cx: &mut App) -> Result where Self: Sized; diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 45c67153eb..7ab76b71de 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -598,7 +598,7 @@ pub fn main() { repl::notebook::init(cx); diagnostics::init(cx); - audio::init(Assets, cx); + audio::init(cx); workspace::init(app_state.clone(), cx); ui_prompt::init(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 958149825a..3b5f99f9bd 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4614,7 +4614,7 @@ mod tests { gpui_tokio::init(cx); vim_mode_setting::init(cx); theme::init(theme::LoadThemes::JustBase, cx); - audio::init((), cx); + audio::init(cx); channel::init(&app_state.client, app_state.user_store.clone(), cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); From 2781a3097161c7bd5447fe82a4d9f8490b42af68 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 21 Aug 2025 09:59:18 -0400 Subject: [PATCH 575/693] collab: Add Orb subscription status and period to `billing_subscriptions` table (#36682) This PR adds the following new columns to the `billing_subscriptions` table: - `orb_subscription_status` - `orb_current_billing_period_start_date` - `orb_current_billing_period_end_date` Release Notes: - N/A --- ...ubscription_status_and_period_to_billing_subscriptions.sql | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 crates/collab/migrations/20250821133754_add_orb_subscription_status_and_period_to_billing_subscriptions.sql diff --git a/crates/collab/migrations/20250821133754_add_orb_subscription_status_and_period_to_billing_subscriptions.sql b/crates/collab/migrations/20250821133754_add_orb_subscription_status_and_period_to_billing_subscriptions.sql new file mode 100644 index 0000000000..89a42ab82b --- /dev/null +++ b/crates/collab/migrations/20250821133754_add_orb_subscription_status_and_period_to_billing_subscriptions.sql @@ -0,0 +1,4 @@ +alter table billing_subscriptions + add column orb_subscription_status text, + add column orb_current_billing_period_start_date timestamp without time zone, + add column orb_current_billing_period_end_date timestamp without time zone; From 001ec97c0e0c13deda49c49ded89826fab514a7c Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Thu, 21 Aug 2025 16:18:22 +0200 Subject: [PATCH 576/693] acp: Use file icons for edit tool cards when ToolCallLocation is known (#36684) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 33 ++++++++++++++++---------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 9c9e2ee4dd..f8c616c9e0 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1469,19 +1469,26 @@ impl AcpThreadView { tool_call: &ToolCall, cx: &Context, ) -> Div { - let tool_icon = Icon::new(match tool_call.kind { - acp::ToolKind::Read => IconName::ToolRead, - acp::ToolKind::Edit => IconName::ToolPencil, - acp::ToolKind::Delete => IconName::ToolDeleteFile, - acp::ToolKind::Move => IconName::ArrowRightLeft, - acp::ToolKind::Search => IconName::ToolSearch, - acp::ToolKind::Execute => IconName::ToolTerminal, - acp::ToolKind::Think => IconName::ToolThink, - acp::ToolKind::Fetch => IconName::ToolWeb, - acp::ToolKind::Other => IconName::ToolHammer, - }) - .size(IconSize::Small) - .color(Color::Muted); + let tool_icon = + if tool_call.kind == acp::ToolKind::Edit && tool_call.locations.len() == 1 { + FileIcons::get_icon(&tool_call.locations[0].path, cx) + .map(Icon::from_path) + .unwrap_or(Icon::new(IconName::ToolPencil)) + } else { + Icon::new(match tool_call.kind { + acp::ToolKind::Read => IconName::ToolRead, + acp::ToolKind::Edit => IconName::ToolPencil, + acp::ToolKind::Delete => IconName::ToolDeleteFile, + acp::ToolKind::Move => IconName::ArrowRightLeft, + acp::ToolKind::Search => IconName::ToolSearch, + acp::ToolKind::Execute => IconName::ToolTerminal, + acp::ToolKind::Think => IconName::ToolThink, + acp::ToolKind::Fetch => IconName::ToolWeb, + acp::ToolKind::Other => IconName::ToolHammer, + }) + } + .size(IconSize::Small) + .color(Color::Muted); let base_container = h_flex().size_4().justify_center(); From d8fc779a6758f6cf3b375af65350e7166e22b0b8 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 21 Aug 2025 08:43:57 -0600 Subject: [PATCH 577/693] acp: Hide history unless in native agent (#36644) Release Notes: - N/A --- crates/agent2/src/history_store.rs | 10 ++++++++++ crates/agent_ui/src/acp/thread_history.rs | 17 ++++------------- crates/agent_ui/src/acp/thread_view.rs | 21 +++++++++++++-------- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs index 2d70164a66..78d83cc1d0 100644 --- a/crates/agent2/src/history_store.rs +++ b/crates/agent2/src/history_store.rs @@ -10,6 +10,7 @@ use itertools::Itertools; use paths::contexts_dir; use serde::{Deserialize, Serialize}; use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration}; +use ui::ElementId; use util::ResultExt as _; const MAX_RECENTLY_OPENED_ENTRIES: usize = 6; @@ -68,6 +69,15 @@ pub enum HistoryEntryId { TextThread(Arc), } +impl Into for HistoryEntryId { + fn into(self) -> ElementId { + match self { + HistoryEntryId::AcpThread(session_id) => ElementId::Name(session_id.0.into()), + HistoryEntryId::TextThread(path) => ElementId::Path(path), + } + } +} + #[derive(Serialize, Deserialize, Debug)] enum SerializedRecentOpen { AcpThread(String), diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs index 68a41f31d0..d76969378c 100644 --- a/crates/agent_ui/src/acp/thread_history.rs +++ b/crates/agent_ui/src/acp/thread_history.rs @@ -673,18 +673,9 @@ impl AcpHistoryEntryElement { impl RenderOnce for AcpHistoryEntryElement { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let (id, title, timestamp) = match &self.entry { - HistoryEntry::AcpThread(thread) => ( - thread.id.to_string(), - thread.title.clone(), - thread.updated_at, - ), - HistoryEntry::TextThread(context) => ( - context.path.to_string_lossy().to_string(), - context.title.clone(), - context.mtime.to_utc(), - ), - }; + let id = self.entry.id(); + let title = self.entry.title(); + let timestamp = self.entry.updated_at(); let formatted_time = { let now = chrono::Utc::now(); @@ -701,7 +692,7 @@ impl RenderOnce for AcpHistoryEntryElement { } }; - ListItem::new(SharedString::from(id)) + ListItem::new(id) .rounded() .toggle_state(self.selected) .spacing(ListItemSpacing::Sparse) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index f8c616c9e0..090e224b4d 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2404,16 +2404,18 @@ impl AcpThreadView { fn render_empty_state(&self, window: &mut Window, cx: &mut Context) -> AnyElement { let loading = matches!(&self.thread_state, ThreadState::Loading { .. }); - let recent_history = self - .history_store - .update(cx, |history_store, cx| history_store.recent_entries(3, cx)); - let no_history = self - .history_store - .update(cx, |history_store, cx| history_store.is_empty(cx)); + let render_history = self + .agent + .clone() + .downcast::() + .is_some() + && self + .history_store + .update(cx, |history_store, cx| !history_store.is_empty(cx)); v_flex() .size_full() - .when(no_history, |this| { + .when(!render_history, |this| { this.child( v_flex() .size_full() @@ -2445,7 +2447,10 @@ impl AcpThreadView { })), ) }) - .when(!no_history, |this| { + .when(render_history, |this| { + let recent_history = self + .history_store + .update(cx, |history_store, cx| history_store.recent_entries(3, cx)); this.justify_end().child( v_flex() .child( From d9ea97ee9cf1bec19741c597e482aa35eef2f816 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 21 Aug 2025 08:44:04 -0600 Subject: [PATCH 578/693] acp: Detect gemini auth errors and show a button (#36641) Closes #ISSUE Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 63 ++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 090e224b4d..7e330b7e6f 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -76,11 +76,12 @@ enum ThreadError { PaymentRequired, ModelRequestLimitReached(cloud_llm_client::Plan), ToolUseLimitReached, + AuthenticationRequired(SharedString), Other(SharedString), } impl ThreadError { - fn from_err(error: anyhow::Error) -> Self { + fn from_err(error: anyhow::Error, agent: &Rc) -> Self { if error.is::() { Self::PaymentRequired } else if error.is::() { @@ -90,7 +91,17 @@ impl ThreadError { { Self::ModelRequestLimitReached(error.plan) } else { - Self::Other(error.to_string().into()) + let string = error.to_string(); + // TODO: we should have Gemini return better errors here. + if agent.clone().downcast::().is_some() + && string.contains("Could not load the default credentials") + || string.contains("API key not valid") + || string.contains("Request had invalid authentication credentials") + { + Self::AuthenticationRequired(string.into()) + } else { + Self::Other(error.to_string().into()) + } } } } @@ -930,7 +941,7 @@ impl AcpThreadView { } fn handle_thread_error(&mut self, error: anyhow::Error, cx: &mut Context) { - self.thread_error = Some(ThreadError::from_err(error)); + self.thread_error = Some(ThreadError::from_err(error, &self.agent)); cx.notify(); } @@ -4310,6 +4321,9 @@ impl AcpThreadView { fn render_thread_error(&self, window: &mut Window, cx: &mut Context) -> Option
{ let content = match self.thread_error.as_ref()? { ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx), + ThreadError::AuthenticationRequired(error) => { + self.render_authentication_required_error(error.clone(), cx) + } ThreadError::PaymentRequired => self.render_payment_required_error(cx), ThreadError::ModelRequestLimitReached(plan) => { self.render_model_request_limit_reached_error(*plan, cx) @@ -4348,6 +4362,24 @@ impl AcpThreadView { .dismiss_action(self.dismiss_error_button(cx)) } + fn render_authentication_required_error( + &self, + error: SharedString, + cx: &mut Context, + ) -> Callout { + Callout::new() + .severity(Severity::Error) + .title("Authentication Required") + .description(error.clone()) + .actions_slot( + h_flex() + .gap_0p5() + .child(self.authenticate_button(cx)) + .child(self.create_copy_button(error)), + ) + .dismiss_action(self.dismiss_error_button(cx)) + } + fn render_model_request_limit_reached_error( &self, plan: cloud_llm_client::Plan, @@ -4469,6 +4501,31 @@ impl AcpThreadView { })) } + fn authenticate_button(&self, cx: &mut Context) -> impl IntoElement { + Button::new("authenticate", "Authenticate") + .label_size(LabelSize::Small) + .style(ButtonStyle::Filled) + .on_click(cx.listener({ + move |this, _, window, cx| { + let agent = this.agent.clone(); + let ThreadState::Ready { thread, .. } = &this.thread_state else { + return; + }; + + let connection = thread.read(cx).connection().clone(); + let err = AuthRequired { + description: None, + provider_id: None, + }; + this.clear_thread_error(cx); + let this = cx.weak_entity(); + window.defer(cx, |window, cx| { + Self::handle_auth_required(this, err, agent, connection, window, cx); + }) + } + })) + } + fn upgrade_button(&self, cx: &mut Context) -> impl IntoElement { Button::new("upgrade", "Upgrade") .label_size(LabelSize::Small) From 697a39c2511469e49e8af1974618d552410b1c38 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Thu, 21 Aug 2025 20:19:17 +0530 Subject: [PATCH 579/693] Fix issue where renaming a file would not update imports in related files if they are not open (#36681) Closes #34445 Now we open a multi-buffer consisting of buffers that have updated, renamed file imports. Only local is handled, for now. Release Notes: - Fixed an issue where renaming a file would not update imports in related files if they are not already open. --- crates/editor/src/editor.rs | 58 ++++++++++++++++++++++++++++-- crates/project/src/buffer_store.rs | 11 +++++- crates/project/src/lsp_store.rs | 18 ++++++---- crates/project/src/project.rs | 11 ++++-- 4 files changed, 87 insertions(+), 11 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 05ee295360..2af8e6c0e4 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1900,6 +1900,60 @@ impl Editor { editor.update_lsp_data(false, Some(*buffer_id), window, cx); } } + + project::Event::EntryRenamed(transaction) => { + let Some(workspace) = editor.workspace() else { + return; + }; + let Some(active_editor) = workspace.read(cx).active_item_as::(cx) + else { + return; + }; + if active_editor.entity_id() == cx.entity_id() { + let edited_buffers_already_open = { + let other_editors: Vec> = workspace + .read(cx) + .panes() + .iter() + .flat_map(|pane| pane.read(cx).items_of_type::()) + .filter(|editor| editor.entity_id() != cx.entity_id()) + .collect(); + + transaction.0.keys().all(|buffer| { + other_editors.iter().any(|editor| { + let multi_buffer = editor.read(cx).buffer(); + multi_buffer.read(cx).is_singleton() + && multi_buffer.read(cx).as_singleton().map_or( + false, + |singleton| { + singleton.entity_id() == buffer.entity_id() + }, + ) + }) + }) + }; + + if !edited_buffers_already_open { + let workspace = workspace.downgrade(); + let transaction = transaction.clone(); + cx.defer_in(window, move |_, window, cx| { + cx.spawn_in(window, async move |editor, cx| { + Self::open_project_transaction( + &editor, + workspace, + transaction, + "Rename".to_string(), + cx, + ) + .await + .ok() + }) + .detach(); + }); + } + } + } + _ => {} }, )); @@ -6282,7 +6336,7 @@ impl Editor { } pub async fn open_project_transaction( - this: &WeakEntity, + editor: &WeakEntity, workspace: WeakEntity, transaction: ProjectTransaction, title: String, @@ -6300,7 +6354,7 @@ impl Editor { if let Some((buffer, transaction)) = entries.first() { if entries.len() == 1 { - let excerpt = this.update(cx, |editor, cx| { + let excerpt = editor.update(cx, |editor, cx| { editor .buffer() .read(cx) diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index a171b193d0..295bad6e59 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -88,9 +88,18 @@ pub enum BufferStoreEvent { }, } -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone)] pub struct ProjectTransaction(pub HashMap, language::Transaction>); +impl PartialEq for ProjectTransaction { + fn eq(&self, other: &Self) -> bool { + self.0.len() == other.0.len() + && self.0.iter().all(|(buffer, transaction)| { + other.0.get(buffer).is_some_and(|t| t.id == transaction.id) + }) + } +} + impl EventEmitter for BufferStore {} impl RemoteBufferStore { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 072f4396c1..709bd10358 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -8762,7 +8762,7 @@ impl LspStore { (root_path.join(&old_path), root_path.join(&new_path)) }; - Self::will_rename_entry( + let _transaction = Self::will_rename_entry( this.downgrade(), worktree_id, &old_abs_path, @@ -9224,7 +9224,7 @@ impl LspStore { new_path: &Path, is_dir: bool, cx: AsyncApp, - ) -> Task<()> { + ) -> Task { let old_uri = lsp::Url::from_file_path(old_path).ok().map(String::from); let new_uri = lsp::Url::from_file_path(new_path).ok().map(String::from); cx.spawn(async move |cx| { @@ -9257,7 +9257,7 @@ impl LspStore { .log_err() .flatten()?; - LocalLspStore::deserialize_workspace_edit( + let transaction = LocalLspStore::deserialize_workspace_edit( this.upgrade()?, edit, false, @@ -9265,8 +9265,8 @@ impl LspStore { cx, ) .await - .ok(); - Some(()) + .ok()?; + Some(transaction) } }); tasks.push(apply_edit); @@ -9276,11 +9276,17 @@ impl LspStore { }) .ok() .flatten(); + let mut merged_transaction = ProjectTransaction::default(); for task in tasks { // Await on tasks sequentially so that the order of application of edits is deterministic // (at least with regards to the order of registration of language servers) - task.await; + if let Some(transaction) = task.await { + for (buffer, buffer_transaction) in transaction.0 { + merged_transaction.0.insert(buffer, buffer_transaction); + } + } } + merged_transaction }) } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ee4bfcb8cc..9fd4eed641 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -327,6 +327,7 @@ pub enum Event { RevealInProjectPanel(ProjectEntryId), SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>), ExpandedAllForEntry(WorktreeId, ProjectEntryId), + EntryRenamed(ProjectTransaction), AgentLocationChanged, } @@ -2119,7 +2120,7 @@ impl Project { let is_root_entry = self.entry_is_worktree_root(entry_id, cx); let lsp_store = self.lsp_store().downgrade(); - cx.spawn(async move |_, cx| { + cx.spawn(async move |project, cx| { let (old_abs_path, new_abs_path) = { let root_path = worktree.read_with(cx, |this, _| this.abs_path())?; let new_abs_path = if is_root_entry { @@ -2129,7 +2130,7 @@ impl Project { }; (root_path.join(&old_path), new_abs_path) }; - LspStore::will_rename_entry( + let transaction = LspStore::will_rename_entry( lsp_store.clone(), worktree_id, &old_abs_path, @@ -2145,6 +2146,12 @@ impl Project { })? .await?; + project + .update(cx, |_, cx| { + cx.emit(Event::EntryRenamed(transaction)); + }) + .ok(); + lsp_store .read_with(cx, |this, _| { this.did_rename_entry(worktree_id, &old_abs_path, &new_abs_path, is_dir); From f23314bef4514f3208c712d8604278262b310e37 Mon Sep 17 00:00:00 2001 From: Ryan Drew Date: Thu, 21 Aug 2025 08:55:43 -0600 Subject: [PATCH 580/693] editor: Use editorconfig's max_line_length for hard wrap (#36426) PR #20198, "Do not alter soft wrap based on .editorconfig contents" removed support for setting line lengths for both soft and hard wrap, not just soft wrap. This causes the `max_line_length` property within a `.editorconfig` file to be ignored by Zed. This commit restores allowing for hard wrap limits to be set using `max_line_length` without impacting soft wrap limits. This is done by merging the `max_line_length` property from an editorconfig file into Zed's `preferred_line_length` property. Release Notes: - Added support for .editorconfig's `max_line_length` property Signed-off-by: Ryan Drew --- crates/language/src/language_settings.rs | 7 ++++++- crates/project/src/project_tests.rs | 11 ++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 90a59ce066..386ad19747 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -5,7 +5,7 @@ use anyhow::Result; use collections::{FxHashMap, HashMap, HashSet}; use ec4rs::{ Properties as EditorconfigProperties, - property::{FinalNewline, IndentSize, IndentStyle, TabWidth, TrimTrailingWs}, + property::{FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs}, }; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use gpui::{App, Modifiers}; @@ -1131,6 +1131,10 @@ impl AllLanguageSettings { } fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) { + let preferred_line_length = cfg.get::().ok().and_then(|v| match v { + MaxLineLen::Value(u) => Some(u as u32), + MaxLineLen::Off => None, + }); let tab_size = cfg.get::().ok().and_then(|v| match v { IndentSize::Value(u) => NonZeroU32::new(u as u32), IndentSize::UseTabWidth => cfg.get::().ok().and_then(|w| match w { @@ -1158,6 +1162,7 @@ fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigPr *target = value; } } + merge(&mut settings.preferred_line_length, preferred_line_length); merge(&mut settings.tab_size, tab_size); merge(&mut settings.hard_tabs, hard_tabs); merge( diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 282f1facc2..7bb1537be8 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -140,8 +140,10 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true + max_line_length = 120 [*.js] tab_width = 10 + max_line_length = off "#, ".zed": { "settings.json": r#"{ @@ -149,7 +151,8 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { "hard_tabs": false, "ensure_final_newline_on_save": false, "remove_trailing_whitespace_on_save": false, - "soft_wrap": "editor_width" + "preferred_line_length": 64, + "soft_wrap": "editor_width", }"#, }, "a.rs": "fn a() {\n A\n}", @@ -157,6 +160,7 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { ".editorconfig": r#" [*.rs] indent_size = 2 + max_line_length = off, "#, "b.rs": "fn b() {\n B\n}", }, @@ -205,6 +209,7 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { assert_eq!(settings_a.hard_tabs, true); assert_eq!(settings_a.ensure_final_newline_on_save, true); assert_eq!(settings_a.remove_trailing_whitespace_on_save, true); + assert_eq!(settings_a.preferred_line_length, 120); // .editorconfig in b/ overrides .editorconfig in root assert_eq!(Some(settings_b.tab_size), NonZeroU32::new(2)); @@ -212,6 +217,10 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { // "indent_size" is not set, so "tab_width" is used assert_eq!(Some(settings_c.tab_size), NonZeroU32::new(10)); + // When max_line_length is "off", default to .zed/settings.json + assert_eq!(settings_b.preferred_line_length, 64); + assert_eq!(settings_c.preferred_line_length, 64); + // README.md should not be affected by .editorconfig's globe "*.rs" assert_eq!(Some(settings_readme.tab_size), NonZeroU32::new(8)); }); From 4bee06e507516d4a72501cb4fc2b9d30612f21d4 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Thu, 21 Aug 2025 11:57:46 -0300 Subject: [PATCH 581/693] acp: Use `ResourceLink` for agents that don't support embedded context (#36687) The completion provider was already limiting the mention kinds according to `acp::PromptCapabilities`. However, it was still using `ContentBlock::EmbeddedResource` when `acp::PromptCapabilities::embedded_context` was `false`. We will now use `ResourceLink` in that case making it more complaint with the specification. Release Notes: - N/A --- crates/agent_ui/src/acp/message_editor.rs | 91 ++++++++++++++++++++--- 1 file changed, 79 insertions(+), 12 deletions(-) diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 3116a40be5..dc31c5fe10 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -709,9 +709,13 @@ impl MessageEditor { window: &mut Window, cx: &mut Context, ) -> Task, Vec>)>> { - let contents = - self.mention_set - .contents(&self.project, self.prompt_store.as_ref(), window, cx); + let contents = self.mention_set.contents( + &self.project, + self.prompt_store.as_ref(), + &self.prompt_capabilities.get(), + window, + cx, + ); let editor = self.editor.clone(); let prevent_slash_commands = self.prevent_slash_commands; @@ -776,6 +780,17 @@ impl MessageEditor { .map(|path| format!("file://{}", path.display())), }) } + Mention::UriOnly(uri) => { + acp::ContentBlock::ResourceLink(acp::ResourceLink { + name: uri.name(), + uri: uri.to_uri().to_string(), + annotations: None, + description: None, + mime_type: None, + size: None, + title: None, + }) + } }; chunks.push(chunk); ix = crease_range.end; @@ -1418,6 +1433,7 @@ pub enum Mention { tracked_buffers: Vec>, }, Image(MentionImage), + UriOnly(MentionUri), } #[derive(Clone, Debug, Eq, PartialEq)] @@ -1481,9 +1497,20 @@ impl MentionSet { &self, project: &Entity, prompt_store: Option<&Entity>, + prompt_capabilities: &acp::PromptCapabilities, _window: &mut Window, cx: &mut App, ) -> Task>> { + if !prompt_capabilities.embedded_context { + let mentions = self + .uri_by_crease_id + .iter() + .map(|(crease_id, uri)| (*crease_id, Mention::UriOnly(uri.clone()))) + .collect(); + + return Task::ready(Ok(mentions)); + } + let mut processed_image_creases = HashSet::default(); let mut contents = self @@ -2180,11 +2207,21 @@ mod tests { assert_eq!(fold_ranges(editor, cx).len(), 1); }); + let all_prompt_capabilities = acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + }; + let contents = message_editor .update_in(&mut cx, |message_editor, window, cx| { - message_editor - .mention_set() - .contents(&project, None, window, cx) + message_editor.mention_set().contents( + &project, + None, + &all_prompt_capabilities, + window, + cx, + ) }) .await .unwrap() @@ -2199,6 +2236,28 @@ mod tests { pretty_assertions::assert_eq!(uri, &url_one.parse::().unwrap()); } + let contents = message_editor + .update_in(&mut cx, |message_editor, window, cx| { + message_editor.mention_set().contents( + &project, + None, + &acp::PromptCapabilities::default(), + window, + cx, + ) + }) + .await + .unwrap() + .into_values() + .collect::>(); + + { + let [Mention::UriOnly(uri)] = contents.as_slice() else { + panic!("Unexpected mentions"); + }; + pretty_assertions::assert_eq!(uri, &url_one.parse::().unwrap()); + } + cx.simulate_input(" "); editor.update(&mut cx, |editor, cx| { @@ -2234,9 +2293,13 @@ mod tests { let contents = message_editor .update_in(&mut cx, |message_editor, window, cx| { - message_editor - .mention_set() - .contents(&project, None, window, cx) + message_editor.mention_set().contents( + &project, + None, + &all_prompt_capabilities, + window, + cx, + ) }) .await .unwrap() @@ -2344,9 +2407,13 @@ mod tests { let contents = message_editor .update_in(&mut cx, |message_editor, window, cx| { - message_editor - .mention_set() - .contents(&project, None, window, cx) + message_editor.mention_set().contents( + &project, + None, + &all_prompt_capabilities, + window, + cx, + ) }) .await .unwrap() From 132daef9f669c1ffc27ff7344649b27090ea2163 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 21 Aug 2025 17:52:17 +0200 Subject: [PATCH 582/693] lsp: Add basic test for server tree toolchain use (#36692) Closes #ISSUE Release Notes: - N/A --- crates/language/src/toolchain.rs | 2 +- crates/project/src/lsp_store.rs | 2 - .../project/src/manifest_tree/server_tree.rs | 2 + crates/project/src/project_tests.rs | 260 +++++++++++++++++- 4 files changed, 262 insertions(+), 4 deletions(-) diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index 979513bc96..73c142c8ca 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -96,7 +96,7 @@ impl LanguageToolchainStore for T { } type DefaultIndex = usize; -#[derive(Default, Clone)] +#[derive(Default, Clone, Debug)] pub struct ToolchainList { pub toolchains: Vec, pub default: Option, diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 709bd10358..cc3a0a05bb 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -4643,7 +4643,6 @@ impl LspStore { Some((file, language, raw_buffer.remote_id())) }) .sorted_by_key(|(file, _, _)| Reverse(file.worktree.read(cx).is_visible())); - for (file, language, buffer_id) in buffers { let worktree_id = file.worktree_id(cx); let Some(worktree) = local @@ -4685,7 +4684,6 @@ impl LspStore { cx, ) .collect::>(); - for node in nodes { let server_id = node.server_id_or_init(|disposition| { let path = &disposition.path; diff --git a/crates/project/src/manifest_tree/server_tree.rs b/crates/project/src/manifest_tree/server_tree.rs index 5e5f4bab49..48e2007d47 100644 --- a/crates/project/src/manifest_tree/server_tree.rs +++ b/crates/project/src/manifest_tree/server_tree.rs @@ -181,6 +181,7 @@ impl LanguageServerTree { &root_path.path, language_name.clone(), ); + ( Arc::new(InnerTreeNode::new( adapter.name(), @@ -408,6 +409,7 @@ impl ServerTreeRebase { if live_node.id.get().is_some() { return Some(node); } + let disposition = &live_node.disposition; let Some((existing_node, _)) = self .old_contents diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 7bb1537be8..6dcd07482e 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -4,6 +4,7 @@ use crate::{ Event, git_store::StatusEntry, task_inventory::TaskContexts, task_store::TaskSettingsLocation, *, }; +use async_trait::async_trait; use buffer_diff::{ BufferDiffEvent, CALCULATE_DIFF_TASK, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind, assert_hunks, @@ -21,7 +22,8 @@ use http_client::Url; use itertools::Itertools; use language::{ Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, DiskState, FakeLspAdapter, - LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint, + LanguageConfig, LanguageMatcher, LanguageName, LineEnding, ManifestName, ManifestProvider, + ManifestQuery, OffsetRangeExt, Point, ToPoint, ToolchainLister, language_settings::{AllLanguageSettings, LanguageSettingsContent, language_settings}, tree_sitter_rust, tree_sitter_typescript, }; @@ -596,6 +598,203 @@ async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_running_multiple_instances_of_a_single_server_in_one_worktree( + cx: &mut gpui::TestAppContext, +) { + pub(crate) struct PyprojectTomlManifestProvider; + + impl ManifestProvider for PyprojectTomlManifestProvider { + fn name(&self) -> ManifestName { + SharedString::new_static("pyproject.toml").into() + } + + fn search( + &self, + ManifestQuery { + path, + depth, + delegate, + }: ManifestQuery, + ) -> Option> { + for path in path.ancestors().take(depth) { + let p = path.join("pyproject.toml"); + if delegate.exists(&p, Some(false)) { + return Some(path.into()); + } + } + + None + } + } + + init_test(cx); + let fs = FakeFs::new(cx.executor()); + + fs.insert_tree( + path!("/the-root"), + json!({ + ".zed": { + "settings.json": r#" + { + "languages": { + "Python": { + "language_servers": ["ty"] + } + } + }"# + }, + "project-a": { + ".venv": {}, + "file.py": "", + "pyproject.toml": "" + }, + "project-b": { + ".venv": {}, + "source_file.py":"", + "another_file.py": "", + "pyproject.toml": "" + } + }), + ) + .await; + cx.update(|cx| { + ManifestProvidersStore::global(cx).register(Arc::new(PyprojectTomlManifestProvider)) + }); + + let project = Project::test(fs.clone(), [path!("/the-root").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + let _fake_python_server = language_registry.register_fake_lsp( + "Python", + FakeLspAdapter { + name: "ty", + capabilities: lsp::ServerCapabilities { + ..Default::default() + }, + ..Default::default() + }, + ); + + language_registry.add(python_lang(fs.clone())); + let (first_buffer, _handle) = project + .update(cx, |project, cx| { + project.open_local_buffer_with_lsp(path!("/the-root/project-a/file.py"), cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + let servers = project.update(cx, |project, cx| { + project.lsp_store.update(cx, |this, cx| { + first_buffer.update(cx, |buffer, cx| { + this.language_servers_for_local_buffer(buffer, cx) + .map(|(adapter, server)| (adapter.clone(), server.clone())) + .collect::>() + }) + }) + }); + cx.executor().run_until_parked(); + assert_eq!(servers.len(), 1); + let (adapter, server) = servers.into_iter().next().unwrap(); + assert_eq!(adapter.name(), LanguageServerName::new_static("ty")); + assert_eq!(server.server_id(), LanguageServerId(0)); + // `workspace_folders` are set to the rooting point. + assert_eq!( + server.workspace_folders(), + BTreeSet::from_iter( + [Url::from_file_path(path!("/the-root/project-a")).unwrap()].into_iter() + ) + ); + + let (second_project_buffer, _other_handle) = project + .update(cx, |project, cx| { + project.open_local_buffer_with_lsp(path!("/the-root/project-b/source_file.py"), cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + let servers = project.update(cx, |project, cx| { + project.lsp_store.update(cx, |this, cx| { + second_project_buffer.update(cx, |buffer, cx| { + this.language_servers_for_local_buffer(buffer, cx) + .map(|(adapter, server)| (adapter.clone(), server.clone())) + .collect::>() + }) + }) + }); + cx.executor().run_until_parked(); + assert_eq!(servers.len(), 1); + let (adapter, server) = servers.into_iter().next().unwrap(); + assert_eq!(adapter.name(), LanguageServerName::new_static("ty")); + // We're not using venvs at all here, so both folders should fall under the same root. + assert_eq!(server.server_id(), LanguageServerId(0)); + // Now, let's select a different toolchain for one of subprojects. + let (available_toolchains_for_b, root_path) = project + .update(cx, |this, cx| { + let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id(); + this.available_toolchains( + ProjectPath { + worktree_id, + path: Arc::from("project-b/source_file.py".as_ref()), + }, + LanguageName::new("Python"), + cx, + ) + }) + .await + .expect("A toolchain to be discovered"); + assert_eq!(root_path.as_ref(), Path::new("project-b")); + assert_eq!(available_toolchains_for_b.toolchains().len(), 1); + let currently_active_toolchain = project + .update(cx, |this, cx| { + let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id(); + this.active_toolchain( + ProjectPath { + worktree_id, + path: Arc::from("project-b/source_file.py".as_ref()), + }, + LanguageName::new("Python"), + cx, + ) + }) + .await; + + assert!(currently_active_toolchain.is_none()); + let _ = project + .update(cx, |this, cx| { + let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id(); + this.activate_toolchain( + ProjectPath { + worktree_id, + path: root_path, + }, + available_toolchains_for_b + .toolchains + .into_iter() + .next() + .unwrap(), + cx, + ) + }) + .await + .unwrap(); + cx.run_until_parked(); + let servers = project.update(cx, |project, cx| { + project.lsp_store.update(cx, |this, cx| { + second_project_buffer.update(cx, |buffer, cx| { + this.language_servers_for_local_buffer(buffer, cx) + .map(|(adapter, server)| (adapter.clone(), server.clone())) + .collect::>() + }) + }) + }); + cx.executor().run_until_parked(); + assert_eq!(servers.len(), 1); + let (adapter, server) = servers.into_iter().next().unwrap(); + assert_eq!(adapter.name(), LanguageServerName::new_static("ty")); + // There's a new language server in town. + assert_eq!(server.server_id(), LanguageServerId(1)); +} + #[gpui::test] async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { init_test(cx); @@ -8982,6 +9181,65 @@ fn rust_lang() -> Arc { )) } +fn python_lang(fs: Arc) -> Arc { + struct PythonMootToolchainLister(Arc); + #[async_trait] + impl ToolchainLister for PythonMootToolchainLister { + async fn list( + &self, + worktree_root: PathBuf, + subroot_relative_path: Option>, + _: Option>, + ) -> ToolchainList { + // This lister will always return a path .venv directories within ancestors + let ancestors = subroot_relative_path + .into_iter() + .flat_map(|path| path.ancestors().map(ToOwned::to_owned).collect::>()); + let mut toolchains = vec![]; + for ancestor in ancestors { + let venv_path = worktree_root.join(ancestor).join(".venv"); + if self.0.is_dir(&venv_path).await { + toolchains.push(Toolchain { + name: SharedString::new("Python Venv"), + path: venv_path.to_string_lossy().into_owned().into(), + language_name: LanguageName(SharedString::new_static("Python")), + as_json: serde_json::Value::Null, + }) + } + } + ToolchainList { + toolchains, + ..Default::default() + } + } + // Returns a term which we should use in UI to refer to a toolchain. + fn term(&self) -> SharedString { + SharedString::new_static("virtual environment") + } + /// Returns the name of the manifest file for this toolchain. + fn manifest_name(&self) -> ManifestName { + SharedString::new_static("pyproject.toml").into() + } + } + Arc::new( + Language::new( + LanguageConfig { + name: "Python".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["py".to_string()], + ..Default::default() + }, + ..Default::default() + }, + None, // We're not testing Python parsing with this language. + ) + .with_manifest(Some(ManifestName::from(SharedString::new_static( + "pyproject.toml", + )))) + .with_toolchain_lister(Some(Arc::new(PythonMootToolchainLister(fs)))), + ) +} + fn typescript_lang() -> Arc { Arc::new(Language::new( LanguageConfig { From 190217a43bfc2384ec3cd86d82d0ffd3975b0901 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Aug 2025 18:11:05 +0200 Subject: [PATCH 583/693] acp: Refactor agent2 `send` to have a clearer control flow (#36689) Release Notes: - N/A --- Cargo.lock | 1 + crates/agent2/Cargo.toml | 1 + crates/agent2/src/thread.rs | 295 ++++++++++++++++-------------------- 3 files changed, 134 insertions(+), 163 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ddeaebd0bf..6063530e9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -244,6 +244,7 @@ dependencies = [ "terminal", "text", "theme", + "thiserror 2.0.12", "tree-sitter-rust", "ui", "unindent", diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 8dd79062f8..68246a96b0 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -61,6 +61,7 @@ sqlez.workspace = true task.workspace = true telemetry.workspace = true terminal.workspace = true +thiserror.workspace = true text.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index d34c929152..6f560cd390 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -499,6 +499,16 @@ pub struct ToolCallAuthorization { pub response: oneshot::Sender, } +#[derive(Debug, thiserror::Error)] +enum CompletionError { + #[error("max tokens")] + MaxTokens, + #[error("refusal")] + Refusal, + #[error(transparent)] + Other(#[from] anyhow::Error), +} + pub struct Thread { id: acp::SessionId, prompt_id: PromptId, @@ -1077,101 +1087,62 @@ impl Thread { _task: cx.spawn(async move |this, cx| { log::info!("Starting agent turn execution"); let mut update_title = None; - let turn_result: Result = async { - let mut completion_intent = CompletionIntent::UserPrompt; + let turn_result: Result<()> = async { + let mut intent = CompletionIntent::UserPrompt; loop { - log::debug!( - "Building completion request with intent: {:?}", - completion_intent - ); - let request = this.update(cx, |this, cx| { - this.build_completion_request(completion_intent, cx) - })??; - - log::info!("Calling model.stream_completion"); - - let mut tool_use_limit_reached = false; - let mut refused = false; - let mut reached_max_tokens = false; - let mut tool_uses = Self::stream_completion_with_retries( - this.clone(), - model.clone(), - request, - &event_stream, - &mut tool_use_limit_reached, - &mut refused, - &mut reached_max_tokens, - cx, - ) - .await?; - - if refused { - return Ok(StopReason::Refusal); - } else if reached_max_tokens { - return Ok(StopReason::MaxTokens); - } - - let end_turn = tool_uses.is_empty(); - while let Some(tool_result) = tool_uses.next().await { - log::info!("Tool finished {:?}", tool_result); - - event_stream.update_tool_call_fields( - &tool_result.tool_use_id, - acp::ToolCallUpdateFields { - status: Some(if tool_result.is_error { - acp::ToolCallStatus::Failed - } else { - acp::ToolCallStatus::Completed - }), - raw_output: tool_result.output.clone(), - ..Default::default() - }, - ); - this.update(cx, |this, _cx| { - this.pending_message() - .tool_results - .insert(tool_result.tool_use_id.clone(), tool_result); - })?; - } + Self::stream_completion(&this, &model, intent, &event_stream, cx).await?; + let mut end_turn = true; this.update(cx, |this, cx| { + // Generate title if needed. if this.title.is_none() && update_title.is_none() { update_title = Some(this.update_title(&event_stream, cx)); } + + // End the turn if the model didn't use tools. + let message = this.pending_message.as_ref(); + end_turn = + message.map_or(true, |message| message.tool_results.is_empty()); + this.flush_pending_message(cx); })?; - if tool_use_limit_reached { + if this.read_with(cx, |this, _| this.tool_use_limit_reached)? { log::info!("Tool use limit reached, completing turn"); - this.update(cx, |this, _cx| this.tool_use_limit_reached = true)?; return Err(language_model::ToolUseLimitReachedError.into()); } else if end_turn { log::info!("No tool uses found, completing turn"); - return Ok(StopReason::EndTurn); + return Ok(()); } else { - this.update(cx, |this, cx| this.flush_pending_message(cx))?; - completion_intent = CompletionIntent::ToolResults; + intent = CompletionIntent::ToolResults; } } } .await; _ = this.update(cx, |this, cx| this.flush_pending_message(cx)); + if let Some(update_title) = update_title { + update_title.await.context("update title failed").log_err(); + } + match turn_result { - Ok(reason) => { - log::info!("Turn execution completed: {:?}", reason); - - if let Some(update_title) = update_title { - update_title.await.context("update title failed").log_err(); - } - - event_stream.send_stop(reason); - if reason == StopReason::Refusal { - _ = this.update(cx, |this, _| this.messages.truncate(message_ix)); - } + Ok(()) => { + log::info!("Turn execution completed"); + event_stream.send_stop(acp::StopReason::EndTurn); } Err(error) => { log::error!("Turn execution failed: {:?}", error); - event_stream.send_error(error); + match error.downcast::() { + Ok(CompletionError::Refusal) => { + event_stream.send_stop(acp::StopReason::Refusal); + _ = this.update(cx, |this, _| this.messages.truncate(message_ix)); + } + Ok(CompletionError::MaxTokens) => { + event_stream.send_stop(acp::StopReason::MaxTokens); + } + Ok(CompletionError::Other(error)) | Err(error) => { + event_stream.send_error(error); + } + } } } @@ -1181,17 +1152,17 @@ impl Thread { Ok(events_rx) } - async fn stream_completion_with_retries( - this: WeakEntity, - model: Arc, - request: LanguageModelRequest, + async fn stream_completion( + this: &WeakEntity, + model: &Arc, + completion_intent: CompletionIntent, event_stream: &ThreadEventStream, - tool_use_limit_reached: &mut bool, - refusal: &mut bool, - max_tokens_reached: &mut bool, cx: &mut AsyncApp, - ) -> Result>> { + ) -> Result<()> { log::debug!("Stream completion started successfully"); + let request = this.update(cx, |this, cx| { + this.build_completion_request(completion_intent, cx) + })??; let mut attempt = None; 'retry: loop { @@ -1204,68 +1175,33 @@ impl Thread { attempt ); - let mut events = model.stream_completion(request.clone(), cx).await?; - let mut tool_uses = FuturesUnordered::new(); + log::info!( + "Calling model.stream_completion, attempt {}", + attempt.unwrap_or(0) + ); + let mut events = model + .stream_completion(request.clone(), cx) + .await + .map_err(|error| anyhow!(error))?; + let mut tool_results = FuturesUnordered::new(); + while let Some(event) = events.next().await { match event { - Ok(LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::ToolUseLimitReached, - )) => { - *tool_use_limit_reached = true; - } - Ok(LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::UsageUpdated { amount, limit }, - )) => { - this.update(cx, |this, cx| { - this.update_model_request_usage(amount, limit, cx) - })?; - } - Ok(LanguageModelCompletionEvent::UsageUpdate(usage)) => { - telemetry::event!( - "Agent Thread Completion Usage Updated", - thread_id = this.read_with(cx, |this, _| this.id.to_string())?, - prompt_id = this.read_with(cx, |this, _| this.prompt_id.to_string())?, - model = model.telemetry_id(), - model_provider = model.provider_id().to_string(), - attempt, - input_tokens = usage.input_tokens, - output_tokens = usage.output_tokens, - cache_creation_input_tokens = usage.cache_creation_input_tokens, - cache_read_input_tokens = usage.cache_read_input_tokens, - ); - - this.update(cx, |this, cx| this.update_token_usage(usage, cx))?; - } - Ok(LanguageModelCompletionEvent::Stop(StopReason::Refusal)) => { - *refusal = true; - return Ok(FuturesUnordered::default()); - } - Ok(LanguageModelCompletionEvent::Stop(StopReason::MaxTokens)) => { - *max_tokens_reached = true; - return Ok(FuturesUnordered::default()); - } - Ok(LanguageModelCompletionEvent::Stop( - StopReason::ToolUse | StopReason::EndTurn, - )) => break, Ok(event) => { log::trace!("Received completion event: {:?}", event); - this.update(cx, |this, cx| { - tool_uses.extend(this.handle_streamed_completion_event( - event, - event_stream, - cx, - )); - })?; + tool_results.extend(this.update(cx, |this, cx| { + this.handle_streamed_completion_event(event, event_stream, cx) + })??); } Err(error) => { let completion_mode = this.read_with(cx, |thread, _cx| thread.completion_mode())?; if completion_mode == CompletionMode::Normal { - return Err(error.into()); + return Err(anyhow!(error))?; } let Some(strategy) = Self::retry_strategy_for(&error) else { - return Err(error.into()); + return Err(anyhow!(error))?; }; let max_attempts = match &strategy { @@ -1279,7 +1215,7 @@ impl Thread { let attempt = *attempt; if attempt > max_attempts { - return Err(error.into()); + return Err(anyhow!(error))?; } let delay = match &strategy { @@ -1306,7 +1242,29 @@ impl Thread { } } - return Ok(tool_uses); + while let Some(tool_result) = tool_results.next().await { + log::info!("Tool finished {:?}", tool_result); + + event_stream.update_tool_call_fields( + &tool_result.tool_use_id, + acp::ToolCallUpdateFields { + status: Some(if tool_result.is_error { + acp::ToolCallStatus::Failed + } else { + acp::ToolCallStatus::Completed + }), + raw_output: tool_result.output.clone(), + ..Default::default() + }, + ); + this.update(cx, |this, _cx| { + this.pending_message() + .tool_results + .insert(tool_result.tool_use_id.clone(), tool_result); + })?; + } + + return Ok(()); } } @@ -1328,14 +1286,14 @@ impl Thread { } /// A helper method that's called on every streamed completion event. - /// Returns an optional tool result task, which the main agentic loop in - /// send will send back to the model when it resolves. + /// Returns an optional tool result task, which the main agentic loop will + /// send back to the model when it resolves. fn handle_streamed_completion_event( &mut self, event: LanguageModelCompletionEvent, event_stream: &ThreadEventStream, cx: &mut Context, - ) -> Option> { + ) -> Result>> { log::trace!("Handling streamed completion event: {:?}", event); use LanguageModelCompletionEvent::*; @@ -1350,7 +1308,7 @@ impl Thread { } RedactedThinking { data } => self.handle_redacted_thinking_event(data, cx), ToolUse(tool_use) => { - return self.handle_tool_use_event(tool_use, event_stream, cx); + return Ok(self.handle_tool_use_event(tool_use, event_stream, cx)); } ToolUseJsonParseError { id, @@ -1358,18 +1316,46 @@ impl Thread { raw_input, json_parse_error, } => { - return Some(Task::ready(self.handle_tool_use_json_parse_error_event( - id, - tool_name, - raw_input, - json_parse_error, + return Ok(Some(Task::ready( + self.handle_tool_use_json_parse_error_event( + id, + tool_name, + raw_input, + json_parse_error, + ), ))); } - StatusUpdate(_) => {} - UsageUpdate(_) | Stop(_) => unreachable!(), + UsageUpdate(usage) => { + telemetry::event!( + "Agent Thread Completion Usage Updated", + thread_id = self.id.to_string(), + prompt_id = self.prompt_id.to_string(), + model = self.model.as_ref().map(|m| m.telemetry_id()), + model_provider = self.model.as_ref().map(|m| m.provider_id().to_string()), + input_tokens = usage.input_tokens, + output_tokens = usage.output_tokens, + cache_creation_input_tokens = usage.cache_creation_input_tokens, + cache_read_input_tokens = usage.cache_read_input_tokens, + ); + self.update_token_usage(usage, cx); + } + StatusUpdate(CompletionRequestStatus::UsageUpdated { amount, limit }) => { + self.update_model_request_usage(amount, limit, cx); + } + StatusUpdate( + CompletionRequestStatus::Started + | CompletionRequestStatus::Queued { .. } + | CompletionRequestStatus::Failed { .. }, + ) => {} + StatusUpdate(CompletionRequestStatus::ToolUseLimitReached) => { + self.tool_use_limit_reached = true; + } + Stop(StopReason::Refusal) => return Err(CompletionError::Refusal.into()), + Stop(StopReason::MaxTokens) => return Err(CompletionError::MaxTokens.into()), + Stop(StopReason::ToolUse | StopReason::EndTurn) => {} } - None + Ok(None) } fn handle_text_event( @@ -2225,25 +2211,8 @@ impl ThreadEventStream { self.0.unbounded_send(Ok(ThreadEvent::Retry(status))).ok(); } - fn send_stop(&self, reason: StopReason) { - match reason { - StopReason::EndTurn => { - self.0 - .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::EndTurn))) - .ok(); - } - StopReason::MaxTokens => { - self.0 - .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::MaxTokens))) - .ok(); - } - StopReason::Refusal => { - self.0 - .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::Refusal))) - .ok(); - } - StopReason::ToolUse => {} - } + fn send_stop(&self, reason: acp::StopReason) { + self.0.unbounded_send(Ok(ThreadEvent::Stop(reason))).ok(); } fn send_canceled(&self) { From 6f32d36ec95f973b6d7866f28d4c4310f4f9f4f9 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Thu, 21 Aug 2025 12:03:30 -0500 Subject: [PATCH 584/693] Upload telemetry event on crashes (#36695) This will let us track crashes-per-launch using the new minidump-based crash reporting. Release Notes: - N/A Co-authored-by: Conrad Irwin Co-authored-by: Marshall Bowers --- crates/zed/src/reliability.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index f55468280c..646a3af5bb 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -251,6 +251,7 @@ pub fn init( endpoint, minidump_contents, &metadata, + installation_id.clone(), ) .await .log_err(); @@ -478,7 +479,9 @@ fn upload_panics_and_crashes( return; } cx.background_spawn(async move { - upload_previous_minidumps(http.clone()).await.warn_on_err(); + upload_previous_minidumps(http.clone(), installation_id.clone()) + .await + .warn_on_err(); let most_recent_panic = upload_previous_panics(http.clone(), &panic_report_url) .await .log_err() @@ -546,7 +549,10 @@ async fn upload_previous_panics( Ok(most_recent_panic) } -pub async fn upload_previous_minidumps(http: Arc) -> anyhow::Result<()> { +pub async fn upload_previous_minidumps( + http: Arc, + installation_id: Option, +) -> anyhow::Result<()> { let Some(minidump_endpoint) = MINIDUMP_ENDPOINT.as_ref() else { log::warn!("Minidump endpoint not set"); return Ok(()); @@ -569,6 +575,7 @@ pub async fn upload_previous_minidumps(http: Arc) -> anyhow:: .await .context("Failed to read minidump")?, &metadata, + installation_id.clone(), ) .await .log_err() @@ -586,6 +593,7 @@ async fn upload_minidump( endpoint: &str, minidump: Vec, metadata: &crashes::CrashInfo, + installation_id: Option, ) -> Result<()> { let mut form = Form::new() .part( @@ -601,7 +609,9 @@ async fn upload_minidump( .text("sentry[tags][version]", metadata.init.zed_version.clone()) .text("sentry[release]", metadata.init.commit_sha.clone()) .text("platform", "rust"); + let mut panic_message = "".to_owned(); if let Some(panic_info) = metadata.panic.as_ref() { + panic_message = panic_info.message.clone(); form = form.text("sentry[logentry][formatted]", panic_info.message.clone()); form = form.text("span", panic_info.span.clone()); // TODO: add gpu-context, feature-flag-context, and more of device-context like gpu @@ -610,6 +620,16 @@ async fn upload_minidump( if let Some(minidump_error) = metadata.minidump_error.clone() { form = form.text("minidump_error", minidump_error); } + if let Some(id) = installation_id.clone() { + form = form.text("sentry[user][id]", id) + } + + ::telemetry::event!( + "Minidump Uploaded", + panic_message = panic_message, + crashed_version = metadata.init.zed_version.clone(), + commit_sha = metadata.init.commit_sha.clone(), + ); let mut response_text = String::new(); let mut response = http.send_multipart_form(endpoint, form).await?; From b284b1a0b86715d9ac945034f6923f2551ce630b Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 21 Aug 2025 19:08:26 +0200 Subject: [PATCH 585/693] remote: Fetch shell on ssh remote to use for preparing commands (#36690) Prerequisite for https://github.com/zed-industries/zed/pull/36576 to allow us to differentiate the shell in a remote. Release Notes: - N/A --- crates/debugger_ui/src/session/running.rs | 7 ++- crates/project/src/debugger/dap_store.rs | 13 +++-- crates/project/src/debugger/locators/cargo.rs | 4 +- crates/project/src/terminals.rs | 36 ++++++++----- crates/remote/src/remote.rs | 4 +- crates/remote/src/ssh_session.rs | 38 ++++++++++++- crates/task/src/shell_builder.rs | 54 +++++++++++-------- crates/task/src/task.rs | 2 +- crates/terminal_view/src/terminal_panel.rs | 13 +++-- crates/util/src/paths.rs | 2 +- 10 files changed, 121 insertions(+), 52 deletions(-) diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 0574091851..9991395f35 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -916,7 +916,10 @@ impl RunningState { let task_store = project.read(cx).task_store().downgrade(); let weak_project = project.downgrade(); let weak_workspace = workspace.downgrade(); - let is_local = project.read(cx).is_local(); + let ssh_info = project + .read(cx) + .ssh_client() + .and_then(|it| it.read(cx).ssh_info()); cx.spawn_in(window, async move |this, cx| { let DebugScenario { @@ -1000,7 +1003,7 @@ impl RunningState { None }; - let builder = ShellBuilder::new(is_local, &task.resolved.shell); + let builder = ShellBuilder::new(ssh_info.as_ref().map(|info| &*info.shell), &task.resolved.shell); let command_label = builder.command_label(&task.resolved.command_label); let (command, args) = builder.build(task.resolved.command.clone(), &task.resolved.args); diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 834bf2c2d2..2906c32ff4 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -34,7 +34,7 @@ use http_client::HttpClient; use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind}; use node_runtime::NodeRuntime; -use remote::{SshRemoteClient, ssh_session::SshArgs}; +use remote::{SshInfo, SshRemoteClient, ssh_session::SshArgs}; use rpc::{ AnyProtoClient, TypedEnvelope, proto::{self}, @@ -254,14 +254,18 @@ impl DapStore { cx.spawn(async move |_, cx| { let response = request.await?; let binary = DebugAdapterBinary::from_proto(response)?; - let (mut ssh_command, envs, path_style) = + let (mut ssh_command, envs, path_style, ssh_shell) = ssh_client.read_with(cx, |ssh, _| { - let (SshArgs { arguments, envs }, path_style) = - ssh.ssh_info().context("SSH arguments not found")?; + let SshInfo { + args: SshArgs { arguments, envs }, + path_style, + shell, + } = ssh.ssh_info().context("SSH arguments not found")?; anyhow::Ok(( SshCommand { arguments }, envs.unwrap_or_default(), path_style, + shell, )) })??; @@ -280,6 +284,7 @@ impl DapStore { } let (program, args) = wrap_for_ssh( + &ssh_shell, &ssh_command, binary .command diff --git a/crates/project/src/debugger/locators/cargo.rs b/crates/project/src/debugger/locators/cargo.rs index 3e28fac8af..b2f9580f9c 100644 --- a/crates/project/src/debugger/locators/cargo.rs +++ b/crates/project/src/debugger/locators/cargo.rs @@ -117,7 +117,7 @@ impl DapLocator for CargoLocator { .cwd .clone() .context("Couldn't get cwd from debug config which is needed for locators")?; - let builder = ShellBuilder::new(true, &build_config.shell).non_interactive(); + let builder = ShellBuilder::new(None, &build_config.shell).non_interactive(); let (program, args) = builder.build( Some("cargo".into()), &build_config @@ -126,7 +126,7 @@ impl DapLocator for CargoLocator { .cloned() .take_while(|arg| arg != "--") .chain(Some("--message-format=json".to_owned())) - .collect(), + .collect::>(), ); let mut child = util::command::new_smol_command(program) .args(args) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index e9582e73fd..b009b357fe 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -4,7 +4,7 @@ use collections::HashMap; use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity}; use itertools::Itertools; use language::LanguageName; -use remote::ssh_session::SshArgs; +use remote::{SshInfo, ssh_session::SshArgs}; use settings::{Settings, SettingsLocation}; use smol::channel::bounded; use std::{ @@ -13,7 +13,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use task::{DEFAULT_REMOTE_SHELL, Shell, ShellBuilder, SpawnInTerminal}; +use task::{Shell, ShellBuilder, SpawnInTerminal}; use terminal::{ TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::{self, ActivateScript, TerminalSettings, VenvSettings}, @@ -58,11 +58,13 @@ impl SshCommand { } } +#[derive(Debug)] pub struct SshDetails { pub host: String, pub ssh_command: SshCommand, pub envs: Option>, pub path_style: PathStyle, + pub shell: String, } impl Project { @@ -87,12 +89,18 @@ impl Project { pub fn ssh_details(&self, cx: &App) -> Option { if let Some(ssh_client) = &self.ssh_client { let ssh_client = ssh_client.read(cx); - if let Some((SshArgs { arguments, envs }, path_style)) = ssh_client.ssh_info() { + if let Some(SshInfo { + args: SshArgs { arguments, envs }, + path_style, + shell, + }) = ssh_client.ssh_info() + { return Some(SshDetails { host: ssh_client.connection_options().host, ssh_command: SshCommand { arguments }, envs, path_style, + shell, }); } } @@ -165,7 +173,9 @@ impl Project { let ssh_details = self.ssh_details(cx); let settings = self.terminal_settings(&path, cx).clone(); - let builder = ShellBuilder::new(ssh_details.is_none(), &settings.shell).non_interactive(); + let builder = + ShellBuilder::new(ssh_details.as_ref().map(|ssh| &*ssh.shell), &settings.shell) + .non_interactive(); let (command, args) = builder.build(Some(command), &Vec::new()); let mut env = self @@ -180,9 +190,11 @@ impl Project { ssh_command, envs, path_style, + shell, .. }) => { let (command, args) = wrap_for_ssh( + &shell, &ssh_command, Some((&command, &args)), path.as_deref(), @@ -280,6 +292,7 @@ impl Project { ssh_command, envs, path_style, + shell, }) => { log::debug!("Connecting to a remote server: {ssh_command:?}"); @@ -291,6 +304,7 @@ impl Project { .or_insert_with(|| "xterm-256color".to_string()); let (program, args) = wrap_for_ssh( + &shell, &ssh_command, None, path.as_deref(), @@ -343,11 +357,13 @@ impl Project { ssh_command, envs, path_style, + shell, }) => { log::debug!("Connecting to a remote server: {ssh_command:?}"); env.entry("TERM".to_string()) .or_insert_with(|| "xterm-256color".to_string()); let (program, args) = wrap_for_ssh( + &shell, &ssh_command, spawn_task .command @@ -637,6 +653,7 @@ impl Project { } pub fn wrap_for_ssh( + shell: &str, ssh_command: &SshCommand, command: Option<(&String, &Vec)>, path: Option<&Path>, @@ -645,16 +662,11 @@ pub fn wrap_for_ssh( path_style: PathStyle, ) -> (String, Vec) { let to_run = if let Some((command, args)) = command { - // DEFAULT_REMOTE_SHELL is '"${SHELL:-sh}"' so must not be escaped - let command: Option> = if command == DEFAULT_REMOTE_SHELL { - Some(command.into()) - } else { - shlex::try_quote(command).ok() - }; + let command: Option> = shlex::try_quote(command).ok(); let args = args.iter().filter_map(|arg| shlex::try_quote(arg).ok()); command.into_iter().chain(args).join(" ") } else { - "exec ${SHELL:-sh} -l".to_string() + format!("exec {shell} -l") }; let mut env_changes = String::new(); @@ -688,7 +700,7 @@ pub fn wrap_for_ssh( } else { format!("cd; {env_changes} {to_run}") }; - let shell_invocation = format!("sh -c {}", shlex::try_quote(&commands).unwrap()); + let shell_invocation = format!("{shell} -c {}", shlex::try_quote(&commands).unwrap()); let program = "ssh".to_string(); let mut args = ssh_command.arguments.clone(); diff --git a/crates/remote/src/remote.rs b/crates/remote/src/remote.rs index 43eb59c0ae..71895f1678 100644 --- a/crates/remote/src/remote.rs +++ b/crates/remote/src/remote.rs @@ -4,6 +4,6 @@ pub mod proxy; pub mod ssh_session; pub use ssh_session::{ - ConnectionState, SshClientDelegate, SshConnectionOptions, SshPlatform, SshRemoteClient, - SshRemoteEvent, + ConnectionState, SshClientDelegate, SshConnectionOptions, SshInfo, SshPlatform, + SshRemoteClient, SshRemoteEvent, }; diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index a26f4be661..c02d0ad7e7 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -89,11 +89,19 @@ pub struct SshConnectionOptions { pub upload_binary_over_ssh: bool, } +#[derive(Debug, Clone, PartialEq, Eq)] pub struct SshArgs { pub arguments: Vec, pub envs: Option>, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SshInfo { + pub args: SshArgs, + pub path_style: PathStyle, + pub shell: String, +} + #[macro_export] macro_rules! shell_script { ($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{ @@ -471,6 +479,16 @@ impl SshSocket { Ok(SshPlatform { os, arch }) } + + async fn shell(&self) -> String { + match self.run_command("sh", &["-c", "echo $SHELL"]).await { + Ok(shell) => shell.trim().to_owned(), + Err(e) => { + log::error!("Failed to get shell: {e}"); + "sh".to_owned() + } + } + } } const MAX_MISSED_HEARTBEATS: usize = 5; @@ -1152,12 +1170,16 @@ impl SshRemoteClient { cx.notify(); } - pub fn ssh_info(&self) -> Option<(SshArgs, PathStyle)> { + pub fn ssh_info(&self) -> Option { self.state .lock() .as_ref() .and_then(|state| state.ssh_connection()) - .map(|ssh_connection| (ssh_connection.ssh_args(), ssh_connection.path_style())) + .map(|ssh_connection| SshInfo { + args: ssh_connection.ssh_args(), + path_style: ssh_connection.path_style(), + shell: ssh_connection.shell(), + }) } pub fn upload_directory( @@ -1392,6 +1414,7 @@ trait RemoteConnection: Send + Sync { fn ssh_args(&self) -> SshArgs; fn connection_options(&self) -> SshConnectionOptions; fn path_style(&self) -> PathStyle; + fn shell(&self) -> String; #[cfg(any(test, feature = "test-support"))] fn simulate_disconnect(&self, _: &AsyncApp) {} @@ -1403,6 +1426,7 @@ struct SshRemoteConnection { remote_binary_path: Option, ssh_platform: SshPlatform, ssh_path_style: PathStyle, + ssh_shell: String, _temp_dir: TempDir, } @@ -1429,6 +1453,10 @@ impl RemoteConnection for SshRemoteConnection { self.socket.connection_options.clone() } + fn shell(&self) -> String { + self.ssh_shell.clone() + } + fn upload_directory( &self, src_path: PathBuf, @@ -1642,6 +1670,7 @@ impl SshRemoteConnection { "windows" => PathStyle::Windows, _ => PathStyle::Posix, }; + let ssh_shell = socket.shell().await; let mut this = Self { socket, @@ -1650,6 +1679,7 @@ impl SshRemoteConnection { remote_binary_path: None, ssh_path_style, ssh_platform, + ssh_shell, }; let (release_channel, version, commit) = cx.update(|cx| { @@ -2686,6 +2716,10 @@ mod fake { fn path_style(&self) -> PathStyle { PathStyle::current() } + + fn shell(&self) -> String { + "sh".to_owned() + } } pub(super) struct Delegate; diff --git a/crates/task/src/shell_builder.rs b/crates/task/src/shell_builder.rs index 770312bafc..de4ddc00f4 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -1,26 +1,40 @@ use crate::Shell; -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -enum ShellKind { +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ShellKind { #[default] Posix, + Csh, + Fish, Powershell, Nushell, Cmd, } impl ShellKind { - fn new(program: &str) -> Self { + pub fn system() -> Self { + Self::new(&system_shell()) + } + + pub fn new(program: &str) -> Self { + #[cfg(windows)] + let (_, program) = program.rsplit_once('\\').unwrap_or(("", program)); + #[cfg(not(windows))] + let (_, program) = program.rsplit_once('/').unwrap_or(("", program)); if program == "powershell" - || program.ends_with("powershell.exe") + || program == "powershell.exe" || program == "pwsh" - || program.ends_with("pwsh.exe") + || program == "pwsh.exe" { ShellKind::Powershell - } else if program == "cmd" || program.ends_with("cmd.exe") { + } else if program == "cmd" || program == "cmd.exe" { ShellKind::Cmd } else if program == "nu" { ShellKind::Nushell + } else if program == "fish" { + ShellKind::Fish + } else if program == "csh" { + ShellKind::Csh } else { // Someother shell detected, the user might install and use a // unix-like shell. @@ -33,6 +47,8 @@ impl ShellKind { Self::Powershell => Self::to_powershell_variable(input), Self::Cmd => Self::to_cmd_variable(input), Self::Posix => input.to_owned(), + Self::Fish => input.to_owned(), + Self::Csh => input.to_owned(), Self::Nushell => Self::to_nushell_variable(input), } } @@ -153,7 +169,7 @@ impl ShellKind { match self { ShellKind::Powershell => vec!["-C".to_owned(), combined_command], ShellKind::Cmd => vec!["/C".to_owned(), combined_command], - ShellKind::Posix | ShellKind::Nushell => interactive + ShellKind::Posix | ShellKind::Nushell | ShellKind::Fish | ShellKind::Csh => interactive .then(|| "-i".to_owned()) .into_iter() .chain(["-c".to_owned(), combined_command]) @@ -184,19 +200,14 @@ pub struct ShellBuilder { kind: ShellKind, } -pub static DEFAULT_REMOTE_SHELL: &str = "\"${SHELL:-sh}\""; - impl ShellBuilder { /// Create a new ShellBuilder as configured. - pub fn new(is_local: bool, shell: &Shell) -> Self { + pub fn new(remote_system_shell: Option<&str>, shell: &Shell) -> Self { let (program, args) = match shell { - Shell::System => { - if is_local { - (system_shell(), Vec::new()) - } else { - (DEFAULT_REMOTE_SHELL.to_string(), Vec::new()) - } - } + Shell::System => match remote_system_shell { + Some(remote_shell) => (remote_shell.to_string(), Vec::new()), + None => (system_shell(), Vec::new()), + }, Shell::Program(shell) => (shell.clone(), Vec::new()), Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()), }; @@ -212,6 +223,7 @@ impl ShellBuilder { self.interactive = false; self } + /// Returns the label to show in the terminal tab pub fn command_label(&self, command_label: &str) -> String { match self.kind { @@ -221,7 +233,7 @@ impl ShellBuilder { ShellKind::Cmd => { format!("{} /C '{}'", self.program, command_label) } - ShellKind::Posix | ShellKind::Nushell => { + ShellKind::Posix | ShellKind::Nushell | ShellKind::Fish | ShellKind::Csh => { let interactivity = self.interactive.then_some("-i ").unwrap_or_default(); format!( "{} {interactivity}-c '$\"{}\"'", @@ -234,7 +246,7 @@ impl ShellBuilder { pub fn build( mut self, task_command: Option, - task_args: &Vec, + task_args: &[String], ) -> (String, Vec) { if let Some(task_command) = task_command { let combined_command = task_args.iter().fold(task_command, |mut command, arg| { @@ -258,11 +270,11 @@ mod test { #[test] fn test_nu_shell_variable_substitution() { let shell = Shell::Program("nu".to_owned()); - let shell_builder = ShellBuilder::new(true, &shell); + let shell_builder = ShellBuilder::new(None, &shell); let (program, args) = shell_builder.build( Some("echo".into()), - &vec![ + &[ "${hello}".to_string(), "$world".to_string(), "nothing".to_string(), diff --git a/crates/task/src/task.rs b/crates/task/src/task.rs index 85e654eff4..eb9e59f087 100644 --- a/crates/task/src/task.rs +++ b/crates/task/src/task.rs @@ -22,7 +22,7 @@ pub use debug_format::{ AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, Request, TcpArgumentsTemplate, ZedDebugConfig, }; -pub use shell_builder::{DEFAULT_REMOTE_SHELL, ShellBuilder}; +pub use shell_builder::{ShellBuilder, ShellKind}; pub use task_template::{ DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates, substitute_variables_in_map, substitute_variables_in_str, diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index f40c4870f1..6b17911487 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -481,14 +481,17 @@ impl TerminalPanel { window: &mut Window, cx: &mut Context, ) -> Task>> { - let Ok(is_local) = self - .workspace - .update(cx, |workspace, cx| workspace.project().read(cx).is_local()) - else { + let Ok((ssh_client, false)) = self.workspace.update(cx, |workspace, cx| { + let project = workspace.project().read(cx); + ( + project.ssh_client().and_then(|it| it.read(cx).ssh_info()), + project.is_via_collab(), + ) + }) else { return Task::ready(Err(anyhow!("Project is not local"))); }; - let builder = ShellBuilder::new(is_local, &task.shell); + let builder = ShellBuilder::new(ssh_client.as_ref().map(|info| &*info.shell), &task.shell); let command_label = builder.command_label(&task.command_label); let (command, args) = builder.build(task.command.clone(), &task.args); diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index b430120314..1192b14812 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -166,7 +166,7 @@ impl> From for SanitizedPath { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PathStyle { Posix, Windows, From d166ab95a1bca5a4b4351b50ce96faaa585b1784 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Thu, 21 Aug 2025 13:09:14 -0400 Subject: [PATCH 586/693] ci: Switch Windows jobs to target explicit tag (#36693) The previous tags are non-customizable (added by default). This will enable us to pull specific runs out of the pool for maintenance. Also disable actionlint invoking shellcheck because it chokes on PowerShell. Release Notes: - N/A --------- Co-authored-by: Cole Miller --- .github/actionlint.yml | 12 ++++++++++++ .github/workflows/ci.yml | 4 ++-- .github/workflows/release_nightly.yml | 4 ++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/actionlint.yml b/.github/actionlint.yml index 0ee6af8a1d..bc02d312f8 100644 --- a/.github/actionlint.yml +++ b/.github/actionlint.yml @@ -30,3 +30,15 @@ self-hosted-runner: # Self Hosted Runners - self-mini-macos - self-32vcpu-windows-2022 + +# Disable shellcheck because it doesn't like powershell +# This should have been triggered with initial rollout of actionlint +# but https://github.com/zed-industries/zed/pull/36693 +# somehow caused actionlint to actually check those windows jobs +# where previously they were being skipped. Likely caused by an +# unknown bug in actionlint where parsing of `runs-on: [ ]` +# breaks something else. (yuck) +paths: + .github/workflows/{ci,release_nightly}.yml: + ignore: + - "shellcheck" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4ba227168..a45c0a14f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -418,7 +418,7 @@ jobs: if: | github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_tests == 'true' - runs-on: [self-hosted, Windows, X64] + runs-on: [self-32vcpu-windows-2022] steps: - name: Environment Setup run: | @@ -784,7 +784,7 @@ jobs: bundle-windows-x64: timeout-minutes: 120 name: Create a Windows installer - runs-on: [self-hosted, Windows, X64] + runs-on: [self-32vcpu-windows-2022] if: contains(github.event.pull_request.labels.*.name, 'run-bundling') # if: (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling')) needs: [windows_tests] diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 5d63c34edd..d646c68cfa 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -59,7 +59,7 @@ jobs: timeout-minutes: 60 name: Run tests on Windows if: github.repository_owner == 'zed-industries' - runs-on: [self-hosted, Windows, X64] + runs-on: [self-32vcpu-windows-2022] steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -248,7 +248,7 @@ jobs: timeout-minutes: 60 name: Create a Windows installer if: github.repository_owner == 'zed-industries' - runs-on: [self-hosted, Windows, X64] + runs-on: [self-32vcpu-windows-2022] needs: windows-tests env: AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }} From 1b2ceae7efb2b871d19025582cabc4619eee1bdc Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Thu, 21 Aug 2025 12:19:57 -0500 Subject: [PATCH 587/693] Use Tokio::spawn instead of getting an executor handle (#36701) This was causing panics due to the handles being dropped out of order. It doesn't seem possible to guarantee the correct drop ordering given that we're holding them over await points, so lets just spawn on the tokio executor itself which gives us access to the state we needed those handles for in the first place. Fixes: ZED-1R Release Notes: - N/A Co-authored-by: Conrad Irwin Co-authored-by: Marshall Bowers --- Cargo.lock | 1 + crates/client/src/client.rs | 24 ++++++++++--------- .../cloud_api_client/src/cloud_api_client.rs | 8 +------ crates/gpui_tokio/Cargo.toml | 1 + crates/gpui_tokio/src/gpui_tokio.rs | 22 +++++++++++++++++ 5 files changed, 38 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6063530e9f..61f6f42498 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7539,6 +7539,7 @@ dependencies = [ name = "gpui_tokio" version = "0.1.0" dependencies = [ + "anyhow", "gpui", "tokio", "util", diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index ed3f114943..f9b8a10610 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1290,19 +1290,21 @@ impl Client { "http" => Http, _ => Err(anyhow!("invalid rpc url: {}", rpc_url))?, }; - let rpc_host = rpc_url - .host_str() - .zip(rpc_url.port_or_known_default()) - .context("missing host in rpc url")?; - let stream = { - let handle = cx.update(|cx| gpui_tokio::Tokio::handle(cx)).ok().unwrap(); - let _guard = handle.enter(); - match proxy { - Some(proxy) => connect_proxy_stream(&proxy, rpc_host).await?, - None => Box::new(TcpStream::connect(rpc_host).await?), + let stream = gpui_tokio::Tokio::spawn_result(cx, { + let rpc_url = rpc_url.clone(); + async move { + let rpc_host = rpc_url + .host_str() + .zip(rpc_url.port_or_known_default()) + .context("missing host in rpc url")?; + Ok(match proxy { + Some(proxy) => connect_proxy_stream(&proxy, rpc_host).await?, + None => Box::new(TcpStream::connect(rpc_host).await?), + }) } - }; + })? + .await?; log::info!("connected to rpc endpoint {}", rpc_url); diff --git a/crates/cloud_api_client/src/cloud_api_client.rs b/crates/cloud_api_client/src/cloud_api_client.rs index 92417d8319..205f3e2432 100644 --- a/crates/cloud_api_client/src/cloud_api_client.rs +++ b/crates/cloud_api_client/src/cloud_api_client.rs @@ -102,13 +102,7 @@ impl CloudApiClient { let credentials = credentials.as_ref().context("no credentials provided")?; let authorization_header = format!("{} {}", credentials.user_id, credentials.access_token); - Ok(cx.spawn(async move |cx| { - let handle = cx - .update(|cx| Tokio::handle(cx)) - .ok() - .context("failed to get Tokio handle")?; - let _guard = handle.enter(); - + Ok(Tokio::spawn_result(cx, async move { let ws = WebSocket::connect(connect_url) .with_request( request::Builder::new() diff --git a/crates/gpui_tokio/Cargo.toml b/crates/gpui_tokio/Cargo.toml index 46d5eafd5a..2d4abf4063 100644 --- a/crates/gpui_tokio/Cargo.toml +++ b/crates/gpui_tokio/Cargo.toml @@ -13,6 +13,7 @@ path = "src/gpui_tokio.rs" doctest = false [dependencies] +anyhow.workspace = true util.workspace = true gpui.workspace = true tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } diff --git a/crates/gpui_tokio/src/gpui_tokio.rs b/crates/gpui_tokio/src/gpui_tokio.rs index fffe18a616..8384f2a88e 100644 --- a/crates/gpui_tokio/src/gpui_tokio.rs +++ b/crates/gpui_tokio/src/gpui_tokio.rs @@ -52,6 +52,28 @@ impl Tokio { }) } + /// Spawns the given future on Tokio's thread pool, and returns it via a GPUI task + /// Note that the Tokio task will be cancelled if the GPUI task is dropped + pub fn spawn_result(cx: &C, f: Fut) -> C::Result>> + where + C: AppContext, + Fut: Future> + Send + 'static, + R: Send + 'static, + { + cx.read_global(|tokio: &GlobalTokio, cx| { + let join_handle = tokio.runtime.spawn(f); + let abort_handle = join_handle.abort_handle(); + let cancel = defer(move || { + abort_handle.abort(); + }); + cx.background_spawn(async move { + let result = join_handle.await?; + drop(cancel); + result + }) + }) + } + pub fn handle(cx: &App) -> tokio::runtime::Handle { GlobalTokio::global(cx).runtime.handle().clone() } From f2899bf34b136ce9dfc14fe1d3531a99b4899a27 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Thu, 21 Aug 2025 13:21:37 -0400 Subject: [PATCH 588/693] ci: Switch from ubuntu-latest to namespace (2) (#36702) In response to ongoing [github actions incident](https://www.githubstatus.com/incidents/c7kq3ctclddp) Supercedes: https://github.com/zed-industries/zed/pull/36698 Release Notes: - N/A --- .github/actionlint.yml | 3 ++- .github/workflows/bump_collab_staging.yml | 2 +- .github/workflows/ci.yml | 6 +++--- .github/workflows/danger.yml | 2 +- .github/workflows/release_nightly.yml | 2 +- .github/workflows/script_checks.yml | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/actionlint.yml b/.github/actionlint.yml index bc02d312f8..6d8e0107e9 100644 --- a/.github/actionlint.yml +++ b/.github/actionlint.yml @@ -19,11 +19,12 @@ self-hosted-runner: - namespace-profile-16x32-ubuntu-2004-arm - namespace-profile-32x64-ubuntu-2004-arm # Namespace Ubuntu 22.04 (Everything else) - - namespace-profile-2x4-ubuntu-2204 - namespace-profile-4x8-ubuntu-2204 - namespace-profile-8x16-ubuntu-2204 - namespace-profile-16x32-ubuntu-2204 - namespace-profile-32x64-ubuntu-2204 + # Namespace Ubuntu 24.04 (like ubuntu-latest) + - namespace-profile-2x4-ubuntu-2404 # Namespace Limited Preview - namespace-profile-8x16-ubuntu-2004-arm-m4 - namespace-profile-8x32-ubuntu-2004-arm-m4 diff --git a/.github/workflows/bump_collab_staging.yml b/.github/workflows/bump_collab_staging.yml index d8eaa6019e..d400905b4d 100644 --- a/.github/workflows/bump_collab_staging.yml +++ b/.github/workflows/bump_collab_staging.yml @@ -8,7 +8,7 @@ on: jobs: update-collab-staging-tag: if: github.repository_owner == 'zed-industries' - runs-on: ubuntu-latest + runs-on: namespace-profile-2x4-ubuntu-2404 steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a45c0a14f1..a34833d0fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: run_nix: ${{ steps.filter.outputs.run_nix }} run_actionlint: ${{ steps.filter.outputs.run_actionlint }} runs-on: - - ubuntu-latest + - namespace-profile-2x4-ubuntu-2404 steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -237,7 +237,7 @@ jobs: uses: ./.github/actions/build_docs actionlint: - runs-on: ubuntu-latest + runs-on: namespace-profile-2x4-ubuntu-2404 if: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_actionlint == 'true' needs: [job_spec] steps: @@ -458,7 +458,7 @@ jobs: tests_pass: name: Tests Pass - runs-on: ubuntu-latest + runs-on: namespace-profile-2x4-ubuntu-2404 needs: - job_spec - style diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 15c82643ae..3f84179278 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -12,7 +12,7 @@ on: jobs: danger: if: github.repository_owner == 'zed-industries' - runs-on: ubuntu-latest + runs-on: namespace-profile-2x4-ubuntu-2404 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index d646c68cfa..2026ee7b73 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -290,7 +290,7 @@ jobs: update-nightly-tag: name: Update nightly tag if: github.repository_owner == 'zed-industries' - runs-on: ubuntu-latest + runs-on: namespace-profile-2x4-ubuntu-2404 needs: - bundle-mac - bundle-linux-x86 diff --git a/.github/workflows/script_checks.yml b/.github/workflows/script_checks.yml index c32a433e46..5dbfc9cb7f 100644 --- a/.github/workflows/script_checks.yml +++ b/.github/workflows/script_checks.yml @@ -12,7 +12,7 @@ jobs: shellcheck: name: "ShellCheck Scripts" if: github.repository_owner == 'zed-industries' - runs-on: ubuntu-latest + runs-on: namespace-profile-2x4-ubuntu-2404 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 From 81cb24810b88080b8cffcb0f75ae6500ef1e654e Mon Sep 17 00:00:00 2001 From: Vitaly Slobodin Date: Thu, 21 Aug 2025 19:23:41 +0200 Subject: [PATCH 589/693] ruby: Improve Ruby test and debug task configurations (#36691) Hi! This pull request adds missing `cwd` field to all Ruby test tasks otherwise `rdbg` will be broken when the user tries to debug a test. Thanks! Release Notes: - N/A --- docs/src/languages/ruby.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/src/languages/ruby.md b/docs/src/languages/ruby.md index 6f530433bd..ef4b026db1 100644 --- a/docs/src/languages/ruby.md +++ b/docs/src/languages/ruby.md @@ -299,6 +299,7 @@ To run tests in your Ruby project, you can set up custom tasks in your local `.z "-n", "\"$ZED_CUSTOM_RUBY_TEST_NAME\"" ], + "cwd": "$ZED_WORKTREE_ROOT", "tags": ["ruby-test"] } ] @@ -321,6 +322,7 @@ Plain minitest does not support running tests by line number, only by name, so w "-n", "\"$ZED_CUSTOM_RUBY_TEST_NAME\"" ], + "cwd": "$ZED_WORKTREE_ROOT", "tags": ["ruby-test"] } ] @@ -334,6 +336,7 @@ Plain minitest does not support running tests by line number, only by name, so w "label": "test $ZED_RELATIVE_FILE:$ZED_ROW", "command": "bundle", "args": ["exec", "rspec", "\"$ZED_RELATIVE_FILE:$ZED_ROW\""], + "cwd": "$ZED_WORKTREE_ROOT", "tags": ["ruby-test"] } ] @@ -369,7 +372,7 @@ The Ruby extension provides a debug adapter for debugging Ruby code. Zed's name "label": "Debug Rails server", "adapter": "rdbg", "request": "launch", - "command": "$ZED_WORKTREE_ROOT/bin/rails", + "command": "./bin/rails", "args": ["server"], "cwd": "$ZED_WORKTREE_ROOT", "env": { From c1e749906febe241a3138280fafcc3ff3fca7416 Mon Sep 17 00:00:00 2001 From: Dave Waggoner Date: Thu, 21 Aug 2025 11:41:32 -0700 Subject: [PATCH 590/693] Add terminal view path like target tests (#35422) Part of - #28238 This PR refactors `Event::NewNavigationTarget` and `Event::Open` handling of `PathLikeTarget` and associated code in `terminal_view.rs` into its own file, `terminal_path_like_target.rs` for improved testability, and adds tests which cover cases from: - #28339 - #28407 - #33498 - #34027 - #34078 Release Notes: - N/A --- .../src/terminal_path_like_target.rs | 825 ++++++++++++++++++ crates/terminal_view/src/terminal_view.rs | 370 +------- 2 files changed, 844 insertions(+), 351 deletions(-) create mode 100644 crates/terminal_view/src/terminal_path_like_target.rs diff --git a/crates/terminal_view/src/terminal_path_like_target.rs b/crates/terminal_view/src/terminal_path_like_target.rs new file mode 100644 index 0000000000..e20df7f001 --- /dev/null +++ b/crates/terminal_view/src/terminal_path_like_target.rs @@ -0,0 +1,825 @@ +use super::{HoverTarget, HoveredWord, TerminalView}; +use anyhow::{Context as _, Result}; +use editor::Editor; +use gpui::{App, AppContext, Context, Task, WeakEntity, Window}; +use itertools::Itertools; +use project::{Entry, Metadata}; +use std::path::PathBuf; +use terminal::PathLikeTarget; +use util::{ResultExt, debug_panic, paths::PathWithPosition}; +use workspace::{OpenOptions, OpenVisible, Workspace}; + +#[derive(Debug, Clone)] +enum OpenTarget { + Worktree(PathWithPosition, Entry), + File(PathWithPosition, Metadata), +} + +impl OpenTarget { + fn is_file(&self) -> bool { + match self { + OpenTarget::Worktree(_, entry) => entry.is_file(), + OpenTarget::File(_, metadata) => !metadata.is_dir, + } + } + + fn is_dir(&self) -> bool { + match self { + OpenTarget::Worktree(_, entry) => entry.is_dir(), + OpenTarget::File(_, metadata) => metadata.is_dir, + } + } + + fn path(&self) -> &PathWithPosition { + match self { + OpenTarget::Worktree(path, _) => path, + OpenTarget::File(path, _) => path, + } + } +} + +pub(super) fn hover_path_like_target( + workspace: &WeakEntity, + hovered_word: HoveredWord, + path_like_target: &PathLikeTarget, + cx: &mut Context, +) -> Task<()> { + let file_to_open_task = possible_open_target(workspace, path_like_target, cx); + cx.spawn(async move |terminal_view, cx| { + let file_to_open = file_to_open_task.await; + terminal_view + .update(cx, |terminal_view, _| match file_to_open { + Some(OpenTarget::File(path, _) | OpenTarget::Worktree(path, _)) => { + terminal_view.hover = Some(HoverTarget { + tooltip: path.to_string(|path| path.to_string_lossy().to_string()), + hovered_word, + }); + } + None => { + terminal_view.hover = None; + } + }) + .ok(); + }) +} + +fn possible_open_target( + workspace: &WeakEntity, + path_like_target: &PathLikeTarget, + cx: &App, +) -> Task> { + let Some(workspace) = workspace.upgrade() else { + return Task::ready(None); + }; + // We have to check for both paths, as on Unix, certain paths with positions are valid file paths too. + // We can be on FS remote part, without real FS, so cannot canonicalize or check for existence the path right away. + let mut potential_paths = Vec::new(); + let cwd = path_like_target.terminal_dir.as_ref(); + let maybe_path = &path_like_target.maybe_path; + let original_path = PathWithPosition::from_path(PathBuf::from(maybe_path)); + let path_with_position = PathWithPosition::parse_str(maybe_path); + let worktree_candidates = workspace + .read(cx) + .worktrees(cx) + .sorted_by_key(|worktree| { + let worktree_root = worktree.read(cx).abs_path(); + match cwd.and_then(|cwd| worktree_root.strip_prefix(cwd).ok()) { + Some(cwd_child) => cwd_child.components().count(), + None => usize::MAX, + } + }) + .collect::>(); + // Since we do not check paths via FS and joining, we need to strip off potential `./`, `a/`, `b/` prefixes out of it. + const GIT_DIFF_PATH_PREFIXES: &[&str] = &["a", "b"]; + for prefix_str in GIT_DIFF_PATH_PREFIXES.iter().chain(std::iter::once(&".")) { + if let Some(stripped) = original_path.path.strip_prefix(prefix_str).ok() { + potential_paths.push(PathWithPosition { + path: stripped.to_owned(), + row: original_path.row, + column: original_path.column, + }); + } + if let Some(stripped) = path_with_position.path.strip_prefix(prefix_str).ok() { + potential_paths.push(PathWithPosition { + path: stripped.to_owned(), + row: path_with_position.row, + column: path_with_position.column, + }); + } + } + + let insert_both_paths = original_path != path_with_position; + potential_paths.insert(0, original_path); + if insert_both_paths { + potential_paths.insert(1, path_with_position); + } + + // If we won't find paths "easily", we can traverse the entire worktree to look what ends with the potential path suffix. + // That will be slow, though, so do the fast checks first. + let mut worktree_paths_to_check = Vec::new(); + for worktree in &worktree_candidates { + let worktree_root = worktree.read(cx).abs_path(); + let mut paths_to_check = Vec::with_capacity(potential_paths.len()); + + for path_with_position in &potential_paths { + let path_to_check = if worktree_root.ends_with(&path_with_position.path) { + let root_path_with_position = PathWithPosition { + path: worktree_root.to_path_buf(), + row: path_with_position.row, + column: path_with_position.column, + }; + match worktree.read(cx).root_entry() { + Some(root_entry) => { + return Task::ready(Some(OpenTarget::Worktree( + root_path_with_position, + root_entry.clone(), + ))); + } + None => root_path_with_position, + } + } else { + PathWithPosition { + path: path_with_position + .path + .strip_prefix(&worktree_root) + .unwrap_or(&path_with_position.path) + .to_owned(), + row: path_with_position.row, + column: path_with_position.column, + } + }; + + if path_to_check.path.is_relative() + && let Some(entry) = worktree.read(cx).entry_for_path(&path_to_check.path) + { + return Task::ready(Some(OpenTarget::Worktree( + PathWithPosition { + path: worktree_root.join(&entry.path), + row: path_to_check.row, + column: path_to_check.column, + }, + entry.clone(), + ))); + } + + paths_to_check.push(path_to_check); + } + + if !paths_to_check.is_empty() { + worktree_paths_to_check.push((worktree.clone(), paths_to_check)); + } + } + + // Before entire worktree traversal(s), make an attempt to do FS checks if available. + let fs_paths_to_check = if workspace.read(cx).project().read(cx).is_local() { + potential_paths + .into_iter() + .flat_map(|path_to_check| { + let mut paths_to_check = Vec::new(); + let maybe_path = &path_to_check.path; + if maybe_path.starts_with("~") { + if let Some(home_path) = + maybe_path + .strip_prefix("~") + .ok() + .and_then(|stripped_maybe_path| { + Some(dirs::home_dir()?.join(stripped_maybe_path)) + }) + { + paths_to_check.push(PathWithPosition { + path: home_path, + row: path_to_check.row, + column: path_to_check.column, + }); + } + } else { + paths_to_check.push(PathWithPosition { + path: maybe_path.clone(), + row: path_to_check.row, + column: path_to_check.column, + }); + if maybe_path.is_relative() { + if let Some(cwd) = &cwd { + paths_to_check.push(PathWithPosition { + path: cwd.join(maybe_path), + row: path_to_check.row, + column: path_to_check.column, + }); + } + for worktree in &worktree_candidates { + paths_to_check.push(PathWithPosition { + path: worktree.read(cx).abs_path().join(maybe_path), + row: path_to_check.row, + column: path_to_check.column, + }); + } + } + } + paths_to_check + }) + .collect() + } else { + Vec::new() + }; + + let worktree_check_task = cx.spawn(async move |cx| { + for (worktree, worktree_paths_to_check) in worktree_paths_to_check { + let found_entry = worktree + .update(cx, |worktree, _| { + let worktree_root = worktree.abs_path(); + let traversal = worktree.traverse_from_path(true, true, false, "".as_ref()); + for entry in traversal { + if let Some(path_in_worktree) = worktree_paths_to_check + .iter() + .find(|path_to_check| entry.path.ends_with(&path_to_check.path)) + { + return Some(OpenTarget::Worktree( + PathWithPosition { + path: worktree_root.join(&entry.path), + row: path_in_worktree.row, + column: path_in_worktree.column, + }, + entry.clone(), + )); + } + } + None + }) + .ok()?; + if let Some(found_entry) = found_entry { + return Some(found_entry); + } + } + None + }); + + let fs = workspace.read(cx).project().read(cx).fs().clone(); + cx.background_spawn(async move { + for mut path_to_check in fs_paths_to_check { + if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok() + && let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten() + { + path_to_check.path = fs_path_to_check; + return Some(OpenTarget::File(path_to_check, metadata)); + } + } + + worktree_check_task.await + }) +} + +pub(super) fn open_path_like_target( + workspace: &WeakEntity, + terminal_view: &mut TerminalView, + path_like_target: &PathLikeTarget, + window: &mut Window, + cx: &mut Context, +) { + possibly_open_target(workspace, terminal_view, path_like_target, window, cx) + .detach_and_log_err(cx) +} + +fn possibly_open_target( + workspace: &WeakEntity, + terminal_view: &mut TerminalView, + path_like_target: &PathLikeTarget, + window: &mut Window, + cx: &mut Context, +) -> Task>> { + if terminal_view.hover.is_none() { + return Task::ready(Ok(None)); + } + let workspace = workspace.clone(); + let path_like_target = path_like_target.clone(); + cx.spawn_in(window, async move |terminal_view, cx| { + let Some(open_target) = terminal_view + .update(cx, |_, cx| { + possible_open_target(&workspace, &path_like_target, cx) + })? + .await + else { + return Ok(None); + }; + + let path_to_open = open_target.path(); + let opened_items = workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_paths( + vec![path_to_open.path.clone()], + OpenOptions { + visible: Some(OpenVisible::OnlyDirectories), + ..Default::default() + }, + None, + window, + cx, + ) + }) + .context("workspace update")? + .await; + if opened_items.len() != 1 { + debug_panic!( + "Received {} items for one path {path_to_open:?}", + opened_items.len(), + ); + } + + if let Some(opened_item) = opened_items.first() { + if open_target.is_file() { + if let Some(Ok(opened_item)) = opened_item { + if let Some(row) = path_to_open.row { + let col = path_to_open.column.unwrap_or(0); + if let Some(active_editor) = opened_item.downcast::() { + active_editor + .downgrade() + .update_in(cx, |editor, window, cx| { + editor.go_to_singleton_buffer_point( + language::Point::new( + row.saturating_sub(1), + col.saturating_sub(1), + ), + window, + cx, + ) + }) + .log_err(); + } + } + return Ok(Some(open_target)); + } + } else if open_target.is_dir() { + workspace.update(cx, |workspace, cx| { + workspace.project().update(cx, |_, cx| { + cx.emit(project::Event::ActivateProjectPanel); + }) + })?; + return Ok(Some(open_target)); + } + } + Ok(None) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::TestAppContext; + use project::{Project, terminals::TerminalKind}; + use serde_json::json; + use std::path::{Path, PathBuf}; + use terminal::{HoveredWord, alacritty_terminal::index::Point as AlacPoint}; + use util::path; + use workspace::AppState; + + async fn init_test( + app_cx: &mut TestAppContext, + trees: impl IntoIterator, + worktree_roots: impl IntoIterator, + ) -> impl AsyncFnMut(HoveredWord, PathLikeTarget) -> (Option, Option) + { + let fs = app_cx.update(AppState::test).fs.as_fake().clone(); + + app_cx.update(|cx| { + terminal::init(cx); + theme::init(theme::LoadThemes::JustBase, cx); + Project::init_settings(cx); + language::init(cx); + editor::init(cx); + }); + + for (path, tree) in trees { + fs.insert_tree(path, tree).await; + } + + let project = Project::test( + fs.clone(), + worktree_roots + .into_iter() + .map(Path::new) + .collect::>(), + app_cx, + ) + .await; + + let (workspace, cx) = + app_cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + let terminal = project + .update(cx, |project, cx| { + project.create_terminal(TerminalKind::Shell(None), cx) + }) + .await + .expect("Failed to create a terminal"); + + let workspace_a = workspace.clone(); + let (terminal_view, cx) = app_cx.add_window_view(|window, cx| { + TerminalView::new( + terminal, + workspace_a.downgrade(), + None, + project.downgrade(), + window, + cx, + ) + }); + + async move |hovered_word: HoveredWord, + path_like_target: PathLikeTarget| + -> (Option, Option) { + let workspace_a = workspace.clone(); + terminal_view + .update(cx, |_, cx| { + hover_path_like_target( + &workspace_a.downgrade(), + hovered_word, + &path_like_target, + cx, + ) + }) + .await; + + let hover_target = + terminal_view.read_with(cx, |terminal_view, _| terminal_view.hover.clone()); + + let open_target = terminal_view + .update_in(cx, |terminal_view, window, cx| { + possibly_open_target( + &workspace.downgrade(), + terminal_view, + &path_like_target, + window, + cx, + ) + }) + .await + .expect("Failed to possibly open target"); + + (hover_target, open_target) + } + } + + async fn test_path_like_simple( + test_path_like: &mut impl AsyncFnMut( + HoveredWord, + PathLikeTarget, + ) -> (Option, Option), + maybe_path: &str, + tooltip: &str, + terminal_dir: Option, + file: &str, + line: u32, + ) { + let (hover_target, open_target) = test_path_like( + HoveredWord { + word: maybe_path.to_string(), + word_match: AlacPoint::default()..=AlacPoint::default(), + id: 0, + }, + PathLikeTarget { + maybe_path: maybe_path.to_string(), + terminal_dir, + }, + ) + .await; + + let Some(hover_target) = hover_target else { + assert!( + hover_target.is_some(), + "Hover target should not be `None` at {file}:{line}:" + ); + return; + }; + + assert_eq!( + hover_target.tooltip, tooltip, + "Tooltip mismatch at {file}:{line}:" + ); + assert_eq!( + hover_target.hovered_word.word, maybe_path, + "Hovered word mismatch at {file}:{line}:" + ); + + let Some(open_target) = open_target else { + assert!( + open_target.is_some(), + "Open target should not be `None` at {file}:{line}:" + ); + return; + }; + + assert_eq!( + open_target.path().path, + Path::new(tooltip), + "Open target path mismatch at {file}:{line}:" + ); + } + + macro_rules! none_or_some { + () => { + None + }; + ($some:expr) => { + Some($some) + }; + } + + macro_rules! test_path_like { + ($test_path_like:expr, $maybe_path:literal, $tooltip:literal $(, $cwd:literal)?) => { + test_path_like_simple( + &mut $test_path_like, + path!($maybe_path), + path!($tooltip), + none_or_some!($($crate::PathBuf::from(path!($cwd)))?), + std::file!(), + std::line!(), + ) + .await + }; + } + + #[doc = "test_path_likes!(, , , { $(;)+ })"] + macro_rules! test_path_likes { + ($cx:expr, $trees:expr, $worktrees:expr, { $($tests:expr;)+ }) => { { + let mut test_path_like = init_test($cx, $trees, $worktrees).await; + #[doc ="test!(, , )"] + macro_rules! test { + ($maybe_path:literal, $tooltip:literal) => { + test_path_like!(test_path_like, $maybe_path, $tooltip) + }; + ($maybe_path:literal, $tooltip:literal, $cwd:literal) => { + test_path_like!(test_path_like, $maybe_path, $tooltip, $cwd) + } + } + $($tests);+ + } } + } + + #[gpui::test] + async fn one_folder_worktree(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![( + path!("/test"), + json!({ + "lib.rs": "", + "test.rs": "", + }), + )], + vec![path!("/test")], + { + test!("lib.rs", "/test/lib.rs"); + test!("test.rs", "/test/test.rs"); + } + ) + } + + #[gpui::test] + async fn mixed_worktrees(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![ + ( + path!("/"), + json!({ + "file.txt": "", + }), + ), + ( + path!("/test"), + json!({ + "lib.rs": "", + "test.rs": "", + "file.txt": "", + }), + ), + ], + vec![path!("/file.txt"), path!("/test")], + { + test!("file.txt", "/file.txt", "/"); + test!("lib.rs", "/test/lib.rs", "/test"); + test!("test.rs", "/test/test.rs", "/test"); + test!("file.txt", "/test/file.txt", "/test"); + } + ) + } + + #[gpui::test] + async fn worktree_file_preferred(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![ + ( + path!("/"), + json!({ + "file.txt": "", + }), + ), + ( + path!("/test"), + json!({ + "file.txt": "", + }), + ), + ], + vec![path!("/test")], + { + test!("file.txt", "/test/file.txt", "/test"); + } + ) + } + + mod issues { + use super::*; + + // https://github.com/zed-industries/zed/issues/28407 + #[gpui::test] + async fn issue_28407_siblings(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![( + path!("/dir1"), + json!({ + "dir 2": { + "C.py": "" + }, + "dir 3": { + "C.py": "" + }, + }), + )], + vec![path!("/dir1")], + { + test!("C.py", "/dir1/dir 2/C.py", "/dir1"); + test!("C.py", "/dir1/dir 2/C.py", "/dir1/dir 2"); + test!("C.py", "/dir1/dir 3/C.py", "/dir1/dir 3"); + } + ) + } + + // https://github.com/zed-industries/zed/issues/28407 + // See https://github.com/zed-industries/zed/issues/34027 + // See https://github.com/zed-industries/zed/issues/33498 + #[gpui::test] + #[should_panic(expected = "Tooltip mismatch")] + async fn issue_28407_nesting(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![( + path!("/project"), + json!({ + "lib": { + "src": { + "main.rs": "" + }, + }, + "src": { + "main.rs": "" + }, + }), + )], + vec![path!("/project")], + { + // Failing currently + test!("main.rs", "/project/src/main.rs", "/project"); + test!("main.rs", "/project/src/main.rs", "/project/src"); + test!("main.rs", "/project/lib/src/main.rs", "/project/lib"); + test!("main.rs", "/project/lib/src/main.rs", "/project/lib/src"); + + test!("src/main.rs", "/project/src/main.rs", "/project"); + test!("src/main.rs", "/project/src/main.rs", "/project/src"); + // Failing currently + test!("src/main.rs", "/project/lib/src/main.rs", "/project/lib"); + // Failing currently + test!( + "src/main.rs", + "/project/lib/src/main.rs", + "/project/lib/src" + ); + + test!("lib/src/main.rs", "/project/lib/src/main.rs", "/project"); + test!( + "lib/src/main.rs", + "/project/lib/src/main.rs", + "/project/src" + ); + test!( + "lib/src/main.rs", + "/project/lib/src/main.rs", + "/project/lib" + ); + test!( + "lib/src/main.rs", + "/project/lib/src/main.rs", + "/project/lib/src" + ); + } + ) + } + + // https://github.com/zed-industries/zed/issues/28339 + #[gpui::test] + async fn issue_28339(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![( + path!("/tmp"), + json!({ + "issue28339": { + "foo": { + "bar.txt": "" + }, + }, + }), + )], + vec![path!("/tmp")], + { + test!( + "foo/./bar.txt", + "/tmp/issue28339/foo/bar.txt", + "/tmp/issue28339" + ); + test!( + "foo/../foo/bar.txt", + "/tmp/issue28339/foo/bar.txt", + "/tmp/issue28339" + ); + test!( + "foo/..///foo/bar.txt", + "/tmp/issue28339/foo/bar.txt", + "/tmp/issue28339" + ); + test!( + "issue28339/../issue28339/foo/../foo/bar.txt", + "/tmp/issue28339/foo/bar.txt", + "/tmp/issue28339" + ); + test!( + "./bar.txt", + "/tmp/issue28339/foo/bar.txt", + "/tmp/issue28339/foo" + ); + test!( + "../foo/bar.txt", + "/tmp/issue28339/foo/bar.txt", + "/tmp/issue28339/foo" + ); + } + ) + } + + // https://github.com/zed-industries/zed/issues/34027 + #[gpui::test] + #[should_panic(expected = "Tooltip mismatch")] + async fn issue_34027(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![( + path!("/tmp/issue34027"), + json!({ + "test.txt": "", + "foo": { + "test.txt": "", + } + }), + ),], + vec![path!("/tmp/issue34027")], + { + test!("test.txt", "/tmp/issue34027/test.txt", "/tmp/issue34027"); + test!( + "test.txt", + "/tmp/issue34027/foo/test.txt", + "/tmp/issue34027/foo" + ); + } + ) + } + + // https://github.com/zed-industries/zed/issues/34027 + #[gpui::test] + #[should_panic(expected = "Tooltip mismatch")] + async fn issue_34027_non_worktree_file(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![ + ( + path!("/"), + json!({ + "file.txt": "", + }), + ), + ( + path!("/test"), + json!({ + "file.txt": "", + }), + ), + ], + vec![path!("/test")], + { + test!("file.txt", "/file.txt", "/"); + test!("file.txt", "/test/file.txt", "/test"); + } + ) + } + } +} diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 5b4d327140..e2f9ba818d 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -2,21 +2,21 @@ mod color_contrast; mod persistence; pub mod terminal_element; pub mod terminal_panel; +mod terminal_path_like_target; pub mod terminal_scrollbar; mod terminal_slash_command; pub mod terminal_tab_tooltip; use assistant_slash_command::SlashCommandRegistry; -use editor::{Editor, EditorSettings, actions::SelectAll, scroll::ScrollbarAutoHide}; +use editor::{EditorSettings, actions::SelectAll, scroll::ScrollbarAutoHide}; use gpui::{ Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render, ScrollWheelEvent, Stateful, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred, div, }; -use itertools::Itertools; use persistence::TERMINAL_DB; -use project::{Entry, Metadata, Project, search::SearchQuery, terminals::TerminalKind}; +use project::{Project, search::SearchQuery, terminals::TerminalKind}; use schemars::JsonSchema; use task::TaskId; use terminal::{ @@ -31,16 +31,17 @@ use terminal::{ }; use terminal_element::TerminalElement; use terminal_panel::TerminalPanel; +use terminal_path_like_target::{hover_path_like_target, open_path_like_target}; use terminal_scrollbar::TerminalScrollHandle; use terminal_slash_command::TerminalSlashCommand; use terminal_tab_tooltip::TerminalTooltip; use ui::{ ContextMenu, Icon, IconName, Label, Scrollbar, ScrollbarState, Tooltip, h_flex, prelude::*, }; -use util::{ResultExt, debug_panic, paths::PathWithPosition}; +use util::ResultExt; use workspace::{ - CloseActiveItem, NewCenterTerminal, NewTerminal, OpenOptions, OpenVisible, ToolbarItemLocation, - Workspace, WorkspaceId, delete_unloaded_items, + CloseActiveItem, NewCenterTerminal, NewTerminal, ToolbarItemLocation, Workspace, WorkspaceId, + delete_unloaded_items, item::{ BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams, TabTooltipContent, }, @@ -48,7 +49,6 @@ use workspace::{ searchable::{Direction, SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, }; -use anyhow::Context as _; use serde::Deserialize; use settings::{Settings, SettingsStore}; use smol::Timer; @@ -64,7 +64,6 @@ use std::{ }; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); -const GIT_DIFF_PATH_PREFIXES: &[&str] = &["a", "b"]; const TERMINAL_SCROLLBAR_WIDTH: Pixels = px(12.); /// Event to transmit the scroll from the element to the view @@ -181,6 +180,7 @@ impl ContentMode { } #[derive(Debug)] +#[cfg_attr(test, derive(Clone, Eq, PartialEq))] struct HoverTarget { tooltip: String, hovered_word: HoveredWord, @@ -1066,37 +1066,13 @@ fn subscribe_for_terminal_events( .as_ref() .map(|hover| &hover.hovered_word) { - let valid_files_to_open_task = possible_open_target( + terminal_view.hover = None; + terminal_view.hover_tooltip_update = hover_path_like_target( &workspace, - &path_like_target.terminal_dir, - &path_like_target.maybe_path, + hovered_word.clone(), + path_like_target, cx, ); - let hovered_word = hovered_word.clone(); - - terminal_view.hover = None; - terminal_view.hover_tooltip_update = - cx.spawn(async move |terminal_view, cx| { - let file_to_open = valid_files_to_open_task.await; - terminal_view - .update(cx, |terminal_view, _| match file_to_open { - Some( - OpenTarget::File(path, _) - | OpenTarget::Worktree(path, _), - ) => { - terminal_view.hover = Some(HoverTarget { - tooltip: path.to_string(|path| { - path.to_string_lossy().to_string() - }), - hovered_word, - }); - } - None => { - terminal_view.hover = None; - } - }) - .ok(); - }); cx.notify(); } } @@ -1110,86 +1086,13 @@ fn subscribe_for_terminal_events( Event::Open(maybe_navigation_target) => match maybe_navigation_target { MaybeNavigationTarget::Url(url) => cx.open_url(url), - - MaybeNavigationTarget::PathLike(path_like_target) => { - if terminal_view.hover.is_none() { - return; - } - let task_workspace = workspace.clone(); - let path_like_target = path_like_target.clone(); - cx.spawn_in(window, async move |terminal_view, cx| { - let open_target = terminal_view - .update(cx, |_, cx| { - possible_open_target( - &task_workspace, - &path_like_target.terminal_dir, - &path_like_target.maybe_path, - cx, - ) - })? - .await; - if let Some(open_target) = open_target { - let path_to_open = open_target.path(); - let opened_items = task_workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_paths( - vec![path_to_open.path.clone()], - OpenOptions { - visible: Some(OpenVisible::OnlyDirectories), - ..Default::default() - }, - None, - window, - cx, - ) - }) - .context("workspace update")? - .await; - if opened_items.len() != 1 { - debug_panic!( - "Received {} items for one path {path_to_open:?}", - opened_items.len(), - ); - } - - if let Some(opened_item) = opened_items.first() { - if open_target.is_file() { - if let Some(Ok(opened_item)) = opened_item - && let Some(row) = path_to_open.row - { - let col = path_to_open.column.unwrap_or(0); - if let Some(active_editor) = - opened_item.downcast::() - { - active_editor - .downgrade() - .update_in(cx, |editor, window, cx| { - editor.go_to_singleton_buffer_point( - language::Point::new( - row.saturating_sub(1), - col.saturating_sub(1), - ), - window, - cx, - ) - }) - .log_err(); - } - } - } else if open_target.is_dir() { - task_workspace.update(cx, |workspace, cx| { - workspace.project().update(cx, |_, cx| { - cx.emit(project::Event::ActivateProjectPanel); - }) - })?; - } - } - } - - anyhow::Ok(()) - }) - .detach_and_log_err(cx) - } + MaybeNavigationTarget::PathLike(path_like_target) => open_path_like_target( + &workspace, + terminal_view, + path_like_target, + window, + cx, + ), }, Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs), Event::CloseTerminal => cx.emit(ItemEvent::CloseItem), @@ -1203,241 +1106,6 @@ fn subscribe_for_terminal_events( vec![terminal_subscription, terminal_events_subscription] } -#[derive(Debug, Clone)] -enum OpenTarget { - Worktree(PathWithPosition, Entry), - File(PathWithPosition, Metadata), -} - -impl OpenTarget { - fn is_file(&self) -> bool { - match self { - OpenTarget::Worktree(_, entry) => entry.is_file(), - OpenTarget::File(_, metadata) => !metadata.is_dir, - } - } - - fn is_dir(&self) -> bool { - match self { - OpenTarget::Worktree(_, entry) => entry.is_dir(), - OpenTarget::File(_, metadata) => metadata.is_dir, - } - } - - fn path(&self) -> &PathWithPosition { - match self { - OpenTarget::Worktree(path, _) => path, - OpenTarget::File(path, _) => path, - } - } -} - -fn possible_open_target( - workspace: &WeakEntity, - cwd: &Option, - maybe_path: &str, - cx: &App, -) -> Task> { - let Some(workspace) = workspace.upgrade() else { - return Task::ready(None); - }; - // We have to check for both paths, as on Unix, certain paths with positions are valid file paths too. - // We can be on FS remote part, without real FS, so cannot canonicalize or check for existence the path right away. - let mut potential_paths = Vec::new(); - let original_path = PathWithPosition::from_path(PathBuf::from(maybe_path)); - let path_with_position = PathWithPosition::parse_str(maybe_path); - let worktree_candidates = workspace - .read(cx) - .worktrees(cx) - .sorted_by_key(|worktree| { - let worktree_root = worktree.read(cx).abs_path(); - match cwd - .as_ref() - .and_then(|cwd| worktree_root.strip_prefix(cwd).ok()) - { - Some(cwd_child) => cwd_child.components().count(), - None => usize::MAX, - } - }) - .collect::>(); - // Since we do not check paths via FS and joining, we need to strip off potential `./`, `a/`, `b/` prefixes out of it. - for prefix_str in GIT_DIFF_PATH_PREFIXES.iter().chain(std::iter::once(&".")) { - if let Some(stripped) = original_path.path.strip_prefix(prefix_str).ok() { - potential_paths.push(PathWithPosition { - path: stripped.to_owned(), - row: original_path.row, - column: original_path.column, - }); - } - if let Some(stripped) = path_with_position.path.strip_prefix(prefix_str).ok() { - potential_paths.push(PathWithPosition { - path: stripped.to_owned(), - row: path_with_position.row, - column: path_with_position.column, - }); - } - } - - let insert_both_paths = original_path != path_with_position; - potential_paths.insert(0, original_path); - if insert_both_paths { - potential_paths.insert(1, path_with_position); - } - - // If we won't find paths "easily", we can traverse the entire worktree to look what ends with the potential path suffix. - // That will be slow, though, so do the fast checks first. - let mut worktree_paths_to_check = Vec::new(); - for worktree in &worktree_candidates { - let worktree_root = worktree.read(cx).abs_path(); - let mut paths_to_check = Vec::with_capacity(potential_paths.len()); - - for path_with_position in &potential_paths { - let path_to_check = if worktree_root.ends_with(&path_with_position.path) { - let root_path_with_position = PathWithPosition { - path: worktree_root.to_path_buf(), - row: path_with_position.row, - column: path_with_position.column, - }; - match worktree.read(cx).root_entry() { - Some(root_entry) => { - return Task::ready(Some(OpenTarget::Worktree( - root_path_with_position, - root_entry.clone(), - ))); - } - None => root_path_with_position, - } - } else { - PathWithPosition { - path: path_with_position - .path - .strip_prefix(&worktree_root) - .unwrap_or(&path_with_position.path) - .to_owned(), - row: path_with_position.row, - column: path_with_position.column, - } - }; - - if path_to_check.path.is_relative() - && let Some(entry) = worktree.read(cx).entry_for_path(&path_to_check.path) - { - return Task::ready(Some(OpenTarget::Worktree( - PathWithPosition { - path: worktree_root.join(&entry.path), - row: path_to_check.row, - column: path_to_check.column, - }, - entry.clone(), - ))); - } - - paths_to_check.push(path_to_check); - } - - if !paths_to_check.is_empty() { - worktree_paths_to_check.push((worktree.clone(), paths_to_check)); - } - } - - // Before entire worktree traversal(s), make an attempt to do FS checks if available. - let fs_paths_to_check = if workspace.read(cx).project().read(cx).is_local() { - potential_paths - .into_iter() - .flat_map(|path_to_check| { - let mut paths_to_check = Vec::new(); - let maybe_path = &path_to_check.path; - if maybe_path.starts_with("~") { - if let Some(home_path) = - maybe_path - .strip_prefix("~") - .ok() - .and_then(|stripped_maybe_path| { - Some(dirs::home_dir()?.join(stripped_maybe_path)) - }) - { - paths_to_check.push(PathWithPosition { - path: home_path, - row: path_to_check.row, - column: path_to_check.column, - }); - } - } else { - paths_to_check.push(PathWithPosition { - path: maybe_path.clone(), - row: path_to_check.row, - column: path_to_check.column, - }); - if maybe_path.is_relative() { - if let Some(cwd) = &cwd { - paths_to_check.push(PathWithPosition { - path: cwd.join(maybe_path), - row: path_to_check.row, - column: path_to_check.column, - }); - } - for worktree in &worktree_candidates { - paths_to_check.push(PathWithPosition { - path: worktree.read(cx).abs_path().join(maybe_path), - row: path_to_check.row, - column: path_to_check.column, - }); - } - } - } - paths_to_check - }) - .collect() - } else { - Vec::new() - }; - - let worktree_check_task = cx.spawn(async move |cx| { - for (worktree, worktree_paths_to_check) in worktree_paths_to_check { - let found_entry = worktree - .update(cx, |worktree, _| { - let worktree_root = worktree.abs_path(); - let traversal = worktree.traverse_from_path(true, true, false, "".as_ref()); - for entry in traversal { - if let Some(path_in_worktree) = worktree_paths_to_check - .iter() - .find(|path_to_check| entry.path.ends_with(&path_to_check.path)) - { - return Some(OpenTarget::Worktree( - PathWithPosition { - path: worktree_root.join(&entry.path), - row: path_in_worktree.row, - column: path_in_worktree.column, - }, - entry.clone(), - )); - } - } - None - }) - .ok()?; - if let Some(found_entry) = found_entry { - return Some(found_entry); - } - } - None - }); - - let fs = workspace.read(cx).project().read(cx).fs().clone(); - cx.background_spawn(async move { - for mut path_to_check in fs_paths_to_check { - if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok() - && let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten() - { - path_to_check.path = fs_path_to_check; - return Some(OpenTarget::File(path_to_check, metadata)); - } - } - - worktree_check_task.await - }) -} - fn regex_search_for_query(query: &project::search::SearchQuery) -> Option { let str = query.as_str(); if query.is_regex() { From 33e05f15b254b9d25aa0ddb03cdcc5a191afb7d7 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 21 Aug 2025 20:50:06 +0200 Subject: [PATCH 591/693] collab_ui: Fix channel text bleeding through buttons on hover (#36710) Release Notes: - N/A --- crates/collab_ui/src/collab_panel.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index cd37549783..d85a6610a5 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2905,6 +2905,8 @@ impl CollabPanel { h_flex().absolute().right(rems(0.)).h_full().child( h_flex() .h_full() + .bg(cx.theme().colors().background) + .rounded_l_sm() .gap_1() .px_1() .child( @@ -2920,8 +2922,7 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, window, cx| { this.join_channel_chat(channel_id, window, cx) })) - .tooltip(Tooltip::text("Open channel chat")) - .visible_on_hover(""), + .tooltip(Tooltip::text("Open channel chat")), ) .child( IconButton::new("channel_notes", IconName::Reader) @@ -2936,9 +2937,9 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, window, cx| { this.open_channel_notes(channel_id, window, cx) })) - .tooltip(Tooltip::text("Open channel notes")) - .visible_on_hover(""), - ), + .tooltip(Tooltip::text("Open channel notes")), + ) + .visible_on_hover(""), ), ) .tooltip({ From d0583ede48fb1918da41beca68dd3aacd7174cb6 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 21 Aug 2025 12:06:27 -0700 Subject: [PATCH 592/693] acp: Move ignored integration tests behind e2e flag (#36711) Release Notes: - N/A --- crates/agent2/src/tests/mod.rs | 44 +++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 3bd1be497e..edba227da7 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -32,17 +32,22 @@ mod test_tools; use test_tools::*; #[gpui::test] -#[ignore = "can't run on CI yet"] async fn test_echo(cx: &mut TestAppContext) { - let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); let events = thread .update(cx, |thread, cx| { thread.send(UserMessageId::new(), ["Testing: Reply with 'Hello'"], cx) }) - .unwrap() - .collect() - .await; + .unwrap(); + cx.run_until_parked(); + fake_model.send_last_completion_stream_text_chunk("Hello"); + fake_model + .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)); + fake_model.end_last_completion_stream(); + + let events = events.collect().await; thread.update(cx, |thread, _cx| { assert_eq!( thread.last_message().unwrap().to_markdown(), @@ -57,9 +62,9 @@ async fn test_echo(cx: &mut TestAppContext) { } #[gpui::test] -#[ignore = "can't run on CI yet"] async fn test_thinking(cx: &mut TestAppContext) { - let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4Thinking).await; + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); let events = thread .update(cx, |thread, cx| { @@ -74,9 +79,18 @@ async fn test_thinking(cx: &mut TestAppContext) { cx, ) }) - .unwrap() - .collect() - .await; + .unwrap(); + cx.run_until_parked(); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Thinking { + text: "Think".to_string(), + signature: None, + }); + fake_model.send_last_completion_stream_text_chunk("Hello"); + fake_model + .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)); + fake_model.end_last_completion_stream(); + + let events = events.collect().await; thread.update(cx, |thread, _cx| { assert_eq!( thread.last_message().unwrap().to_markdown(), @@ -271,7 +285,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) { } #[gpui::test] -#[ignore = "can't run on CI yet"] +#[cfg_attr(not(feature = "e2e"), ignore)] async fn test_basic_tool_calls(cx: &mut TestAppContext) { let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -331,7 +345,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) { } #[gpui::test] -#[ignore = "can't run on CI yet"] +#[cfg_attr(not(feature = "e2e"), ignore)] async fn test_streaming_tool_calls(cx: &mut TestAppContext) { let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -794,7 +808,7 @@ async fn next_tool_call_authorization( } #[gpui::test] -#[ignore = "can't run on CI yet"] +#[cfg_attr(not(feature = "e2e"), ignore)] async fn test_concurrent_tool_calls(cx: &mut TestAppContext) { let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -919,7 +933,7 @@ async fn test_profiles(cx: &mut TestAppContext) { } #[gpui::test] -#[ignore = "can't run on CI yet"] +#[cfg_attr(not(feature = "e2e"), ignore)] async fn test_cancellation(cx: &mut TestAppContext) { let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; @@ -1797,7 +1811,6 @@ struct ThreadTest { enum TestModel { Sonnet4, - Sonnet4Thinking, Fake, } @@ -1805,7 +1818,6 @@ impl TestModel { fn id(&self) -> LanguageModelId { match self { TestModel::Sonnet4 => LanguageModelId("claude-sonnet-4-latest".into()), - TestModel::Sonnet4Thinking => LanguageModelId("claude-sonnet-4-thinking-latest".into()), TestModel::Fake => unreachable!(), } } From 725ed5dd01f18d6b2994435152d7fad37ed9765b Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Thu, 21 Aug 2025 16:56:15 -0300 Subject: [PATCH 593/693] acp: Hide loading diff animation for external agents and update in place (#36699) The loading diff animation can be jarring for external agents because they stream the diff at the same time the tool call is pushed, so it's only displayed while we're asynchronously calculating the diff. We'll now only show it for the native agent. Also, we'll now only update the diff when it changes, which avoids unnecessarily hiding it for a few frames. Release Notes: - N/A Co-authored-by: Bennet Bo Fenner --- crates/acp_thread/src/acp_thread.rs | 41 ++++++++++++++++-- crates/acp_thread/src/diff.rs | 59 +++++++++++++++++++------- crates/agent_ui/src/acp/thread_view.rs | 6 ++- 3 files changed, 85 insertions(+), 21 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 61bc50576a..a45787f039 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -238,10 +238,21 @@ impl ToolCall { } if let Some(content) = content { - self.content = content - .into_iter() - .map(|chunk| ToolCallContent::from_acp(chunk, language_registry.clone(), cx)) - .collect(); + let new_content_len = content.len(); + let mut content = content.into_iter(); + + // Reuse existing content if we can + for (old, new) in self.content.iter_mut().zip(content.by_ref()) { + old.update_from_acp(new, language_registry.clone(), cx); + } + for new in content { + self.content.push(ToolCallContent::from_acp( + new, + language_registry.clone(), + cx, + )) + } + self.content.truncate(new_content_len); } if let Some(locations) = locations { @@ -551,6 +562,28 @@ impl ToolCallContent { } } + pub fn update_from_acp( + &mut self, + new: acp::ToolCallContent, + language_registry: Arc, + cx: &mut App, + ) { + let needs_update = match (&self, &new) { + (Self::Diff(old_diff), acp::ToolCallContent::Diff { diff: new_diff }) => { + old_diff.read(cx).needs_update( + new_diff.old_text.as_deref().unwrap_or(""), + &new_diff.new_text, + cx, + ) + } + _ => true, + }; + + if needs_update { + *self = Self::from_acp(new, language_registry, cx); + } + } + pub fn to_markdown(&self, cx: &App) -> String { match self { Self::ContentBlock(content) => content.to_markdown(cx).to_string(), diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs index 130bc3ab6b..59f907dcc4 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -28,10 +28,12 @@ impl Diff { cx: &mut Context, ) -> Self { let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly)); - let buffer = cx.new(|cx| Buffer::local(new_text, cx)); + let new_buffer = cx.new(|cx| Buffer::local(new_text, cx)); + let base_text = old_text.clone().unwrap_or(String::new()).into(); let task = cx.spawn({ let multibuffer = multibuffer.clone(); let path = path.clone(); + let buffer = new_buffer.clone(); async move |_, cx| { let language = language_registry .language_for_file_path(&path) @@ -76,6 +78,8 @@ impl Diff { Self::Finalized(FinalizedDiff { multibuffer, path, + base_text, + new_buffer, _update_diff: task, }) } @@ -119,7 +123,7 @@ impl Diff { diff.update(cx); } }), - buffer, + new_buffer: buffer, diff: buffer_diff, revealed_ranges: Vec::new(), update_diff: Task::ready(Ok(())), @@ -154,9 +158,9 @@ impl Diff { .map(|buffer| buffer.read(cx).text()) .join("\n"); let path = match self { - Diff::Pending(PendingDiff { buffer, .. }) => { - buffer.read(cx).file().map(|file| file.path().as_ref()) - } + Diff::Pending(PendingDiff { + new_buffer: buffer, .. + }) => buffer.read(cx).file().map(|file| file.path().as_ref()), Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_path()), }; format!( @@ -169,12 +173,33 @@ impl Diff { pub fn has_revealed_range(&self, cx: &App) -> bool { self.multibuffer().read(cx).excerpt_paths().next().is_some() } + + pub fn needs_update(&self, old_text: &str, new_text: &str, cx: &App) -> bool { + match self { + Diff::Pending(PendingDiff { + base_text, + new_buffer, + .. + }) => { + base_text.as_str() != old_text + || !new_buffer.read(cx).as_rope().chunks().equals_str(new_text) + } + Diff::Finalized(FinalizedDiff { + base_text, + new_buffer, + .. + }) => { + base_text.as_str() != old_text + || !new_buffer.read(cx).as_rope().chunks().equals_str(new_text) + } + } + } } pub struct PendingDiff { multibuffer: Entity, base_text: Arc, - buffer: Entity, + new_buffer: Entity, diff: Entity, revealed_ranges: Vec>, _subscription: Subscription, @@ -183,7 +208,7 @@ pub struct PendingDiff { impl PendingDiff { pub fn update(&mut self, cx: &mut Context) { - let buffer = self.buffer.clone(); + let buffer = self.new_buffer.clone(); let buffer_diff = self.diff.clone(); let base_text = self.base_text.clone(); self.update_diff = cx.spawn(async move |diff, cx| { @@ -221,10 +246,10 @@ impl PendingDiff { fn finalize(&self, cx: &mut Context) -> FinalizedDiff { let ranges = self.excerpt_ranges(cx); let base_text = self.base_text.clone(); - let language_registry = self.buffer.read(cx).language_registry(); + let language_registry = self.new_buffer.read(cx).language_registry(); let path = self - .buffer + .new_buffer .read(cx) .file() .map(|file| file.path().as_ref()) @@ -233,12 +258,12 @@ impl PendingDiff { // Replace the buffer in the multibuffer with the snapshot let buffer = cx.new(|cx| { - let language = self.buffer.read(cx).language().cloned(); + let language = self.new_buffer.read(cx).language().cloned(); let buffer = TextBuffer::new_normalized( 0, cx.entity_id().as_non_zero_u64().into(), - self.buffer.read(cx).line_ending(), - self.buffer.read(cx).as_rope().clone(), + self.new_buffer.read(cx).line_ending(), + self.new_buffer.read(cx).as_rope().clone(), ); let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite); buffer.set_language(language, cx); @@ -274,7 +299,9 @@ impl PendingDiff { FinalizedDiff { path, + base_text: self.base_text.clone(), multibuffer: self.multibuffer.clone(), + new_buffer: self.new_buffer.clone(), _update_diff: update_diff, } } @@ -283,8 +310,8 @@ impl PendingDiff { let ranges = self.excerpt_ranges(cx); self.multibuffer.update(cx, |multibuffer, cx| { multibuffer.set_excerpts_for_path( - PathKey::for_buffer(&self.buffer, cx), - self.buffer.clone(), + PathKey::for_buffer(&self.new_buffer, cx), + self.new_buffer.clone(), ranges, editor::DEFAULT_MULTIBUFFER_CONTEXT, cx, @@ -296,7 +323,7 @@ impl PendingDiff { } fn excerpt_ranges(&self, cx: &App) -> Vec> { - let buffer = self.buffer.read(cx); + let buffer = self.new_buffer.read(cx); let diff = self.diff.read(cx); let mut ranges = diff .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx) @@ -330,6 +357,8 @@ impl PendingDiff { pub struct FinalizedDiff { path: PathBuf, + base_text: Arc, + new_buffer: Entity, multibuffer: Entity, _update_diff: Task>, } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 7e330b7e6f..a15f764375 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1625,7 +1625,9 @@ impl AcpThreadView { .into_any() } ToolCallStatus::Pending | ToolCallStatus::InProgress - if is_edit && tool_call.content.is_empty() => + if is_edit + && tool_call.content.is_empty() + && self.as_native_connection(cx).is_some() => { self.render_diff_loading(cx).into_any() } @@ -1981,7 +1983,7 @@ impl AcpThreadView { && diff.read(cx).has_revealed_range(cx) { editor.into_any_element() - } else if tool_progress { + } else if tool_progress && self.as_native_connection(cx).is_some() { self.render_diff_loading(cx) } else { Empty.into_any() From 2234f91b7b335f43105a3f323b94db85c11eb126 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Thu, 21 Aug 2025 16:56:40 -0300 Subject: [PATCH 594/693] acp: Remove invalid creases on edit (#36708) Release Notes: - N/A Co-authored-by: Bennet Bo Fenner --- crates/agent_ui/src/acp/message_editor.rs | 54 +++++++++++++---------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index dc31c5fe10..8f5044cb21 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -11,7 +11,7 @@ use assistant_slash_commands::codeblock_fence_for_path; use collections::{HashMap, HashSet}; use editor::{ Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, - EditorEvent, EditorMode, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, + EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, SemanticsProvider, ToOffset, actions::Paste, display_map::{Crease, CreaseId, FoldId}, @@ -140,11 +140,11 @@ impl MessageEditor { .detach(); let mut subscriptions = Vec::new(); - if prevent_slash_commands { - subscriptions.push(cx.subscribe_in(&editor, window, { - let semantics_provider = semantics_provider.clone(); - move |this, editor, event, window, cx| { - if let EditorEvent::Edited { .. } = event { + subscriptions.push(cx.subscribe_in(&editor, window, { + let semantics_provider = semantics_provider.clone(); + move |this, editor, event, window, cx| { + if let EditorEvent::Edited { .. } = event { + if prevent_slash_commands { this.highlight_slash_command( semantics_provider.clone(), editor.clone(), @@ -152,9 +152,12 @@ impl MessageEditor { cx, ); } + let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx)); + this.mention_set.remove_invalid(snapshot); + cx.notify(); } - })); - } + } + })); Self { editor, @@ -730,11 +733,6 @@ impl MessageEditor { editor.display_map.update(cx, |map, cx| { let snapshot = map.snapshot(cx); for (crease_id, crease) in snapshot.crease_snapshot.creases() { - // Skip creases that have been edited out of the message buffer. - if !crease.range().start.is_valid(&snapshot.buffer_snapshot) { - continue; - } - let Some(mention) = contents.get(&crease_id) else { continue; }; @@ -1482,17 +1480,6 @@ impl MentionSet { self.text_thread_summaries.insert(path, task); } - pub fn drain(&mut self) -> impl Iterator { - self.fetch_results.clear(); - self.thread_summaries.clear(); - self.text_thread_summaries.clear(); - self.directories.clear(); - self.uri_by_crease_id - .drain() - .map(|(id, _)| id) - .chain(self.images.drain().map(|(id, _)| id)) - } - pub fn contents( &self, project: &Entity, @@ -1703,6 +1690,25 @@ impl MentionSet { anyhow::Ok(contents) }) } + + pub fn drain(&mut self) -> impl Iterator { + self.fetch_results.clear(); + self.thread_summaries.clear(); + self.text_thread_summaries.clear(); + self.directories.clear(); + self.uri_by_crease_id + .drain() + .map(|(id, _)| id) + .chain(self.images.drain().map(|(id, _)| id)) + } + + pub fn remove_invalid(&mut self, snapshot: EditorSnapshot) { + for (crease_id, crease) in snapshot.crease_snapshot.creases() { + if !crease.range().start.is_valid(&snapshot.buffer_snapshot) { + self.uri_by_crease_id.remove(&crease_id); + } + } + } } struct SlashCommandSemanticsProvider { From 555692fac6b8e2002296f661ebea8ac50cd42a87 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 21 Aug 2025 17:05:29 -0300 Subject: [PATCH 595/693] thread view: Add improvements to the UI (#36680) Release Notes: - N/A --- crates/agent_servers/src/claude.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 108 +++++----- crates/ui/src/components/disclosure.rs | 14 +- crates/ui/src/components/label.rs | 2 + .../ui/src/components/label/spinner_label.rs | 192 ++++++++++++++++++ 5 files changed, 269 insertions(+), 49 deletions(-) create mode 100644 crates/ui/src/components/label/spinner_label.rs diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index ef666974f1..d6ccabb130 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -44,7 +44,7 @@ pub struct ClaudeCode; impl AgentServer for ClaudeCode { fn name(&self) -> &'static str { - "Claude Code" + "Welcome to Claude Code" } fn empty_state_headline(&self) -> &'static str { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index a15f764375..05d31051b2 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -41,7 +41,7 @@ use text::Anchor; use theme::ThemeSettings; use ui::{ Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle, - Scrollbar, ScrollbarState, Tooltip, prelude::*, + Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*, }; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, Workspace}; @@ -1205,7 +1205,7 @@ impl AcpThreadView { div() .py_3() .px_2() - .rounded_lg() + .rounded_md() .shadow_md() .bg(cx.theme().colors().editor_background) .border_1() @@ -1263,7 +1263,7 @@ impl AcpThreadView { .into_any() } AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => { - let style = default_markdown_style(false, window, cx); + let style = default_markdown_style(false, false, window, cx); let message_body = v_flex() .w_full() .gap_2p5() @@ -1398,8 +1398,6 @@ impl AcpThreadView { .relative() .w_full() .gap_1p5() - .opacity(0.8) - .hover(|style| style.opacity(1.)) .child( h_flex() .size_4() @@ -1440,6 +1438,7 @@ impl AcpThreadView { .child( div() .text_size(self.tool_name_font_size()) + .text_color(cx.theme().colors().text_muted) .child("Thinking"), ) .on_click(cx.listener({ @@ -1463,9 +1462,10 @@ impl AcpThreadView { .border_l_1() .border_color(self.tool_card_border_color(cx)) .text_ui_sm(cx) - .child( - self.render_markdown(chunk, default_markdown_style(false, window, cx)), - ), + .child(self.render_markdown( + chunk, + default_markdown_style(false, false, window, cx), + )), ) }) .into_any_element() @@ -1555,11 +1555,11 @@ impl AcpThreadView { | ToolCallStatus::Completed => None, ToolCallStatus::InProgress => Some( Icon::new(IconName::ArrowCircle) - .color(Color::Accent) + .color(Color::Muted) .size(IconSize::Small) .with_animation( "running", - Animation::new(Duration::from_secs(2)).repeat(), + Animation::new(Duration::from_secs(3)).repeat(), |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), ) .into_any(), @@ -1572,6 +1572,10 @@ impl AcpThreadView { ), }; + let failed_tool_call = matches!( + tool_call.status, + ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed + ); let needs_confirmation = matches!( tool_call.status, ToolCallStatus::WaitingForConfirmation { .. } @@ -1652,7 +1656,7 @@ impl AcpThreadView { v_flex() .when(use_card_layout, |this| { - this.rounded_lg() + this.rounded_md() .border_1() .border_color(self.tool_card_border_color(cx)) .bg(cx.theme().colors().editor_background) @@ -1664,20 +1668,16 @@ impl AcpThreadView { .w_full() .gap_1() .justify_between() - .map(|this| { - if use_card_layout { - this.pl_2() - .pr_1p5() - .py_1() - .rounded_t_md() - .when(is_open, |this| { - this.border_b_1() - .border_color(self.tool_card_border_color(cx)) - }) - .bg(self.tool_card_header_bg(cx)) - } else { - this.opacity(0.8).hover(|style| style.opacity(1.)) - } + .when(use_card_layout, |this| { + this.pl_2() + .pr_1p5() + .py_1() + .rounded_t_md() + .when(is_open && !failed_tool_call, |this| { + this.border_b_1() + .border_color(self.tool_card_border_color(cx)) + }) + .bg(self.tool_card_header_bg(cx)) }) .child( h_flex() @@ -1709,13 +1709,15 @@ impl AcpThreadView { .px_1p5() .rounded_sm() .overflow_x_scroll() - .opacity(0.8) .hover(|label| { - label.opacity(1.).bg(cx - .theme() - .colors() - .element_hover - .opacity(0.5)) + label.bg(cx.theme().colors().element_hover.opacity(0.5)) + }) + .map(|this| { + if use_card_layout { + this.text_color(cx.theme().colors().text) + } else { + this.text_color(cx.theme().colors().text_muted) + } }) .child(name) .tooltip(Tooltip::text("Jump to File")) @@ -1738,7 +1740,7 @@ impl AcpThreadView { .overflow_x_scroll() .child(self.render_markdown( tool_call.label.clone(), - default_markdown_style(false, window, cx), + default_markdown_style(false, true, window, cx), )), ) .child(gradient_overlay(gradient_color)) @@ -1804,9 +1806,9 @@ impl AcpThreadView { .border_color(self.tool_card_border_color(cx)) .text_sm() .text_color(cx.theme().colors().text_muted) - .child(self.render_markdown(markdown, default_markdown_style(false, window, cx))) + .child(self.render_markdown(markdown, default_markdown_style(false, false, window, cx))) .child( - Button::new(button_id, "Collapse Output") + Button::new(button_id, "Collapse") .full_width() .style(ButtonStyle::Outlined) .label_size(LabelSize::Small) @@ -2131,7 +2133,7 @@ impl AcpThreadView { .to_string() } else { format!( - "Output is {} long—to avoid unexpected token usage, \ + "Output is {} long, and to avoid unexpected token usage, \ only 16 KB was sent back to the model.", format_file_size(output.original_content_len as u64, true), ) @@ -2199,7 +2201,7 @@ impl AcpThreadView { .border_1() .when(tool_failed || command_failed, |card| card.border_dashed()) .border_color(border_color) - .rounded_lg() + .rounded_md() .overflow_hidden() .child( v_flex() @@ -2553,9 +2555,10 @@ impl AcpThreadView { .into_any(), ) .children(description.map(|desc| { - div().text_ui(cx).text_center().child( - self.render_markdown(desc.clone(), default_markdown_style(false, window, cx)), - ) + div().text_ui(cx).text_center().child(self.render_markdown( + desc.clone(), + default_markdown_style(false, false, window, cx), + )) })) .children( configuration_view @@ -3379,7 +3382,7 @@ impl AcpThreadView { "used-tokens-label", Animation::new(Duration::from_secs(2)) .repeat() - .with_easing(pulsating_between(0.6, 1.)), + .with_easing(pulsating_between(0.3, 0.8)), |label, delta| label.alpha(delta), ) .into_any() @@ -4636,9 +4639,9 @@ impl Render for AcpThreadView { ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => None, ThreadStatus::Generating => div() - .px_5() .py_2() - .child(LoadingLabel::new("").size(LabelSize::Small)) + .px(rems_from_px(22.)) + .child(SpinnerLabel::new().size(LabelSize::Small)) .into(), }, ) @@ -4671,7 +4674,12 @@ impl Render for AcpThreadView { } } -fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> MarkdownStyle { +fn default_markdown_style( + buffer_font: bool, + muted_text: bool, + window: &Window, + cx: &App, +) -> MarkdownStyle { let theme_settings = ThemeSettings::get_global(cx); let colors = cx.theme().colors(); @@ -4692,20 +4700,26 @@ fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> Markd TextSize::Default.rems(cx) }; + let text_color = if muted_text { + colors.text_muted + } else { + colors.text + }; + text_style.refine(&TextStyleRefinement { font_family: Some(font_family), font_fallbacks: theme_settings.ui_font.fallbacks.clone(), font_features: Some(theme_settings.ui_font.features.clone()), font_size: Some(font_size.into()), line_height: Some(line_height.into()), - color: Some(cx.theme().colors().text), + color: Some(text_color), ..Default::default() }); MarkdownStyle { base_text_style: text_style.clone(), syntax: cx.theme().syntax().clone(), - selection_background_color: cx.theme().colors().element_selection_background, + selection_background_color: colors.element_selection_background, code_block_overflow_x_scroll: true, table_overflow_x_scroll: true, heading_level_styles: Some(HeadingLevelStyles { @@ -4791,7 +4805,7 @@ fn plan_label_markdown_style( window: &Window, cx: &App, ) -> MarkdownStyle { - let default_md_style = default_markdown_style(false, window, cx); + let default_md_style = default_markdown_style(false, false, window, cx); MarkdownStyle { base_text_style: TextStyle { @@ -4811,7 +4825,7 @@ fn plan_label_markdown_style( } fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { - let default_md_style = default_markdown_style(true, window, cx); + let default_md_style = default_markdown_style(true, false, window, cx); MarkdownStyle { base_text_style: TextStyle { diff --git a/crates/ui/src/components/disclosure.rs b/crates/ui/src/components/disclosure.rs index 98406cd1e2..4bb3419176 100644 --- a/crates/ui/src/components/disclosure.rs +++ b/crates/ui/src/components/disclosure.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use gpui::{ClickEvent, CursorStyle}; +use gpui::{ClickEvent, CursorStyle, SharedString}; use crate::{Color, IconButton, IconButtonShape, IconName, IconSize, prelude::*}; @@ -14,6 +14,7 @@ pub struct Disclosure { cursor_style: CursorStyle, opened_icon: IconName, closed_icon: IconName, + visible_on_hover: Option, } impl Disclosure { @@ -27,6 +28,7 @@ impl Disclosure { cursor_style: CursorStyle::PointingHand, opened_icon: IconName::ChevronDown, closed_icon: IconName::ChevronRight, + visible_on_hover: None, } } @@ -73,6 +75,13 @@ impl Clickable for Disclosure { } } +impl VisibleOnHover for Disclosure { + fn visible_on_hover(mut self, group_name: impl Into) -> Self { + self.visible_on_hover = Some(group_name.into()); + self + } +} + impl RenderOnce for Disclosure { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { IconButton::new( @@ -87,6 +96,9 @@ impl RenderOnce for Disclosure { .icon_size(IconSize::Small) .disabled(self.disabled) .toggle_state(self.selected) + .when_some(self.visible_on_hover.clone(), |this, group_name| { + this.visible_on_hover(group_name) + }) .when_some(self.on_toggle, move |this, on_toggle| { this.on_click(move |event, window, cx| on_toggle(event, window, cx)) }) diff --git a/crates/ui/src/components/label.rs b/crates/ui/src/components/label.rs index 8c9ea62424..dc830559ca 100644 --- a/crates/ui/src/components/label.rs +++ b/crates/ui/src/components/label.rs @@ -2,8 +2,10 @@ mod highlighted_label; mod label; mod label_like; mod loading_label; +mod spinner_label; pub use highlighted_label::*; pub use label::*; pub use label_like::*; pub use loading_label::*; +pub use spinner_label::*; diff --git a/crates/ui/src/components/label/spinner_label.rs b/crates/ui/src/components/label/spinner_label.rs new file mode 100644 index 0000000000..b7b65fbcc9 --- /dev/null +++ b/crates/ui/src/components/label/spinner_label.rs @@ -0,0 +1,192 @@ +use crate::prelude::*; +use gpui::{Animation, AnimationExt, FontWeight}; +use std::time::Duration; + +/// Different types of spinner animations +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub enum SpinnerVariant { + #[default] + Dots, + DotsVariant, +} + +/// A spinner indication, based on the label component, that loops through +/// frames of the specified animation. It implements `LabelCommon` as well. +/// +/// # Default Example +/// +/// ``` +/// use ui::{SpinnerLabel}; +/// +/// SpinnerLabel::new(); +/// ``` +/// +/// # Variant Example +/// +/// ``` +/// use ui::{SpinnerLabel}; +/// +/// SpinnerLabel::dots_variant(); +/// ``` +#[derive(IntoElement, RegisterComponent)] +pub struct SpinnerLabel { + base: Label, + variant: SpinnerVariant, + frames: Vec<&'static str>, + duration: Duration, +} + +impl SpinnerVariant { + fn frames(&self) -> Vec<&'static str> { + match self { + SpinnerVariant::Dots => vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"], + SpinnerVariant::DotsVariant => vec!["⣼", "⣹", "⢻", "⠿", "⡟", "⣏", "⣧", "⣶"], + } + } + + fn duration(&self) -> Duration { + match self { + SpinnerVariant::Dots => Duration::from_millis(1000), + SpinnerVariant::DotsVariant => Duration::from_millis(1000), + } + } + + fn animation_id(&self) -> &'static str { + match self { + SpinnerVariant::Dots => "spinner_label_dots", + SpinnerVariant::DotsVariant => "spinner_label_dots_variant", + } + } +} + +impl SpinnerLabel { + pub fn new() -> Self { + Self::with_variant(SpinnerVariant::default()) + } + + pub fn with_variant(variant: SpinnerVariant) -> Self { + let frames = variant.frames(); + let duration = variant.duration(); + + SpinnerLabel { + base: Label::new(frames[0]), + variant, + frames, + duration, + } + } + + pub fn dots() -> Self { + Self::with_variant(SpinnerVariant::Dots) + } + + pub fn dots_variant() -> Self { + Self::with_variant(SpinnerVariant::DotsVariant) + } +} + +impl LabelCommon for SpinnerLabel { + fn size(mut self, size: LabelSize) -> Self { + self.base = self.base.size(size); + self + } + + fn weight(mut self, weight: FontWeight) -> Self { + self.base = self.base.weight(weight); + self + } + + fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self { + self.base = self.base.line_height_style(line_height_style); + self + } + + fn color(mut self, color: Color) -> Self { + self.base = self.base.color(color); + self + } + + fn strikethrough(mut self) -> Self { + self.base = self.base.strikethrough(); + self + } + + fn italic(mut self) -> Self { + self.base = self.base.italic(); + self + } + + fn alpha(mut self, alpha: f32) -> Self { + self.base = self.base.alpha(alpha); + self + } + + fn underline(mut self) -> Self { + self.base = self.base.underline(); + self + } + + fn truncate(mut self) -> Self { + self.base = self.base.truncate(); + self + } + + fn single_line(mut self) -> Self { + self.base = self.base.single_line(); + self + } + + fn buffer_font(mut self, cx: &App) -> Self { + self.base = self.base.buffer_font(cx); + self + } + + fn inline_code(mut self, cx: &App) -> Self { + self.base = self.base.inline_code(cx); + self + } +} + +impl RenderOnce for SpinnerLabel { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + let frames = self.frames.clone(); + let duration = self.duration; + + self.base.color(Color::Muted).with_animation( + self.variant.animation_id(), + Animation::new(duration).repeat(), + move |mut label, delta| { + let frame_index = (delta * frames.len() as f32) as usize % frames.len(); + + label.set_text(frames[frame_index]); + label + }, + ) + } +} + +impl Component for SpinnerLabel { + fn scope() -> ComponentScope { + ComponentScope::Loading + } + + fn name() -> &'static str { + "Spinner Label" + } + + fn sort_name() -> &'static str { + "Spinner Label" + } + + fn preview(_window: &mut Window, _cx: &mut App) -> Option { + let examples = vec![ + single_example("Default", SpinnerLabel::new().into_any_element()), + single_example( + "Dots Variant", + SpinnerLabel::dots_variant().into_any_element(), + ), + ]; + + Some(example_group(examples).vertical().into_any_element()) + } +} From 731b5d0def52d39a2a4fa6a31b9e21160d71fb13 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 Aug 2025 22:24:13 +0200 Subject: [PATCH 596/693] acp: Allow editing of thread titles in agent2 (#36706) Release Notes: - N/A --------- Co-authored-by: Richard Feldman --- crates/acp_thread/src/acp_thread.rs | 36 ++++---- crates/acp_thread/src/connection.rs | 28 +++++-- crates/agent2/src/agent.rs | 71 ++++++++++++---- crates/agent2/src/tests/mod.rs | 1 + crates/agent2/src/thread.rs | 109 +++++++++++++------------ crates/agent_ui/src/acp/thread_view.rs | 83 +++++++++++++++++-- crates/agent_ui/src/agent_panel.rs | 31 ++++++- 7 files changed, 254 insertions(+), 105 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index a45787f039..c748f22275 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1020,10 +1020,19 @@ impl AcpThread { cx.emit(AcpThreadEvent::NewEntry); } - pub fn update_title(&mut self, title: SharedString, cx: &mut Context) -> Result<()> { - self.title = title; - cx.emit(AcpThreadEvent::TitleUpdated); - Ok(()) + pub fn can_set_title(&mut self, cx: &mut Context) -> bool { + self.connection.set_title(&self.session_id, cx).is_some() + } + + pub fn set_title(&mut self, title: SharedString, cx: &mut Context) -> Task> { + if title != self.title { + self.title = title.clone(); + cx.emit(AcpThreadEvent::TitleUpdated); + if let Some(set_title) = self.connection.set_title(&self.session_id, cx) { + return set_title.run(title, cx); + } + } + Task::ready(Ok(())) } pub fn update_token_usage(&mut self, usage: Option, cx: &mut Context) { @@ -1326,11 +1335,7 @@ impl AcpThread { }; let git_store = self.project.read(cx).git_store().clone(); - let message_id = if self - .connection - .session_editor(&self.session_id, cx) - .is_some() - { + let message_id = if self.connection.truncate(&self.session_id, cx).is_some() { Some(UserMessageId::new()) } else { None @@ -1476,7 +1481,7 @@ impl AcpThread { /// Rewinds this thread to before the entry at `index`, removing it and all /// subsequent entries while reverting any changes made from that point. pub fn rewind(&mut self, id: UserMessageId, cx: &mut Context) -> Task> { - let Some(session_editor) = self.connection.session_editor(&self.session_id, cx) else { + let Some(truncate) = self.connection.truncate(&self.session_id, cx) else { return Task::ready(Err(anyhow!("not supported"))); }; let Some(message) = self.user_message(&id) else { @@ -1496,8 +1501,7 @@ impl AcpThread { .await?; } - cx.update(|cx| session_editor.truncate(id.clone(), cx))? - .await?; + cx.update(|cx| truncate.run(id.clone(), cx))?.await?; this.update(cx, |this, cx| { if let Some((ix, _)) = this.user_message_mut(&id) { let range = ix..this.entries.len(); @@ -2652,11 +2656,11 @@ mod tests { .detach(); } - fn session_editor( + fn truncate( &self, session_id: &acp::SessionId, _cx: &mut App, - ) -> Option> { + ) -> Option> { Some(Rc::new(FakeAgentSessionEditor { _session_id: session_id.clone(), })) @@ -2671,8 +2675,8 @@ mod tests { _session_id: acp::SessionId, } - impl AgentSessionEditor for FakeAgentSessionEditor { - fn truncate(&self, _message_id: UserMessageId, _cx: &mut App) -> Task> { + impl AgentSessionTruncate for FakeAgentSessionEditor { + fn run(&self, _message_id: UserMessageId, _cx: &mut App) -> Task> { Task::ready(Ok(())) } } diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 2bbd364873..91e46dbac1 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -50,11 +50,19 @@ pub trait AgentConnection { fn cancel(&self, session_id: &acp::SessionId, cx: &mut App); - fn session_editor( + fn truncate( &self, _session_id: &acp::SessionId, _cx: &mut App, - ) -> Option> { + ) -> Option> { + None + } + + fn set_title( + &self, + _session_id: &acp::SessionId, + _cx: &mut App, + ) -> Option> { None } @@ -79,14 +87,18 @@ impl dyn AgentConnection { } } -pub trait AgentSessionEditor { - fn truncate(&self, message_id: UserMessageId, cx: &mut App) -> Task>; +pub trait AgentSessionTruncate { + fn run(&self, message_id: UserMessageId, cx: &mut App) -> Task>; } pub trait AgentSessionResume { fn run(&self, cx: &mut App) -> Task>; } +pub trait AgentSessionSetTitle { + fn run(&self, title: SharedString, cx: &mut App) -> Task>; +} + pub trait AgentTelemetry { /// The name of the agent used for telemetry. fn agent_name(&self) -> String; @@ -424,11 +436,11 @@ mod test_support { } } - fn session_editor( + fn truncate( &self, _session_id: &agent_client_protocol::SessionId, _cx: &mut App, - ) -> Option> { + ) -> Option> { Some(Rc::new(StubAgentSessionEditor)) } @@ -439,8 +451,8 @@ mod test_support { struct StubAgentSessionEditor; - impl AgentSessionEditor for StubAgentSessionEditor { - fn truncate(&self, _: UserMessageId, _: &mut App) -> Task> { + impl AgentSessionTruncate for StubAgentSessionEditor { + fn run(&self, _: UserMessageId, _: &mut App) -> Task> { Task::ready(Ok(())) } } diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index d5bc0fea63..bbc30b74bc 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -2,7 +2,7 @@ use crate::{ ContextServerRegistry, Thread, ThreadEvent, ThreadsDatabase, ToolCallAuthorization, UserMessageContent, templates::Templates, }; -use crate::{HistoryStore, TokenUsageUpdated}; +use crate::{HistoryStore, TitleUpdated, TokenUsageUpdated}; use acp_thread::{AcpThread, AgentModelSelector}; use action_log::ActionLog; use agent_client_protocol as acp; @@ -253,6 +253,7 @@ impl NativeAgent { cx.observe_release(&acp_thread, |this, acp_thread, _cx| { this.sessions.remove(acp_thread.session_id()); }), + cx.subscribe(&thread_handle, Self::handle_thread_title_updated), cx.subscribe(&thread_handle, Self::handle_thread_token_usage_updated), cx.observe(&thread_handle, move |this, thread, cx| { this.save_thread(thread, cx) @@ -441,6 +442,26 @@ impl NativeAgent { }) } + fn handle_thread_title_updated( + &mut self, + thread: Entity, + _: &TitleUpdated, + cx: &mut Context, + ) { + let session_id = thread.read(cx).id(); + let Some(session) = self.sessions.get(session_id) else { + return; + }; + let thread = thread.downgrade(); + let acp_thread = session.acp_thread.clone(); + cx.spawn(async move |_, cx| { + let title = thread.read_with(cx, |thread, _| thread.title())?; + let task = acp_thread.update(cx, |acp_thread, cx| acp_thread.set_title(title, cx))?; + task.await + }) + .detach_and_log_err(cx); + } + fn handle_thread_token_usage_updated( &mut self, thread: Entity, @@ -717,10 +738,6 @@ impl NativeAgentConnection { thread.update_tool_call(update, cx) })??; } - ThreadEvent::TitleUpdate(title) => { - acp_thread - .update(cx, |thread, cx| thread.update_title(title, cx))??; - } ThreadEvent::Retry(status) => { acp_thread.update(cx, |thread, cx| { thread.update_retry_status(status, cx) @@ -856,8 +873,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { .models .model_from_id(&LanguageModels::model_id(&default_model.model)) }); - - let thread = cx.new(|cx| { + Ok(cx.new(|cx| { Thread::new( project.clone(), agent.project_context.clone(), @@ -867,9 +883,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { default_model, cx, ) - }); - - Ok(thread) + })) }, )??; agent.update(cx, |agent, cx| agent.register_session(thread, cx)) @@ -941,11 +955,11 @@ impl acp_thread::AgentConnection for NativeAgentConnection { }); } - fn session_editor( + fn truncate( &self, session_id: &agent_client_protocol::SessionId, cx: &mut App, - ) -> Option> { + ) -> Option> { self.0.update(cx, |agent, _cx| { agent.sessions.get(session_id).map(|session| { Rc::new(NativeAgentSessionEditor { @@ -956,6 +970,17 @@ impl acp_thread::AgentConnection for NativeAgentConnection { }) } + fn set_title( + &self, + session_id: &acp::SessionId, + _cx: &mut App, + ) -> Option> { + Some(Rc::new(NativeAgentSessionSetTitle { + connection: self.clone(), + session_id: session_id.clone(), + }) as _) + } + fn telemetry(&self) -> Option> { Some(Rc::new(self.clone()) as Rc) } @@ -991,8 +1016,8 @@ struct NativeAgentSessionEditor { acp_thread: WeakEntity, } -impl acp_thread::AgentSessionEditor for NativeAgentSessionEditor { - fn truncate(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task> { +impl acp_thread::AgentSessionTruncate for NativeAgentSessionEditor { + fn run(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task> { match self.thread.update(cx, |thread, cx| { thread.truncate(message_id.clone(), cx)?; Ok(thread.latest_token_usage()) @@ -1024,6 +1049,22 @@ impl acp_thread::AgentSessionResume for NativeAgentSessionResume { } } +struct NativeAgentSessionSetTitle { + connection: NativeAgentConnection, + session_id: acp::SessionId, +} + +impl acp_thread::AgentSessionSetTitle for NativeAgentSessionSetTitle { + fn run(&self, title: SharedString, cx: &mut App) -> Task> { + let Some(session) = self.connection.0.read(cx).sessions.get(&self.session_id) else { + return Task::ready(Err(anyhow!("session not found"))); + }; + let thread = session.thread.clone(); + thread.update(cx, |thread, cx| thread.set_title(title, cx)); + Task::ready(Ok(())) + } +} + #[cfg(test)] mod tests { use crate::HistoryEntryId; @@ -1323,6 +1364,8 @@ mod tests { ) }); + cx.run_until_parked(); + // Drop the ACP thread, which should cause the session to be dropped as well. cx.update(|_| { drop(thread); diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index edba227da7..e7e28f495e 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1383,6 +1383,7 @@ async fn test_title_generation(cx: &mut TestAppContext) { summary_model.send_last_completion_stream_text_chunk("oodnight Moon"); summary_model.end_last_completion_stream(); send.collect::>().await; + cx.run_until_parked(); thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world")); // Send another message, ensuring no title is generated this time. diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 6f560cd390..f6ef11c20b 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -487,7 +487,6 @@ pub enum ThreadEvent { ToolCall(acp::ToolCall), ToolCallUpdate(acp_thread::ToolCallUpdate), ToolCallAuthorization(ToolCallAuthorization), - TitleUpdate(SharedString), Retry(acp_thread::RetryStatus), Stop(acp::StopReason), } @@ -514,6 +513,7 @@ pub struct Thread { prompt_id: PromptId, updated_at: DateTime, title: Option, + pending_title_generation: Option>, summary: Option, messages: Vec, completion_mode: CompletionMode, @@ -555,6 +555,7 @@ impl Thread { prompt_id: PromptId::new(), updated_at: Utc::now(), title: None, + pending_title_generation: None, summary: None, messages: Vec::new(), completion_mode: AgentSettings::get_global(cx).preferred_completion_mode, @@ -705,6 +706,7 @@ impl Thread { } else { Some(db_thread.title.clone()) }, + pending_title_generation: None, summary: db_thread.detailed_summary, messages: db_thread.messages, completion_mode: db_thread.completion_mode.unwrap_or_default(), @@ -1086,7 +1088,7 @@ impl Thread { event_stream: event_stream.clone(), _task: cx.spawn(async move |this, cx| { log::info!("Starting agent turn execution"); - let mut update_title = None; + let turn_result: Result<()> = async { let mut intent = CompletionIntent::UserPrompt; loop { @@ -1095,8 +1097,8 @@ impl Thread { let mut end_turn = true; this.update(cx, |this, cx| { // Generate title if needed. - if this.title.is_none() && update_title.is_none() { - update_title = Some(this.update_title(&event_stream, cx)); + if this.title.is_none() && this.pending_title_generation.is_none() { + this.generate_title(cx); } // End the turn if the model didn't use tools. @@ -1120,10 +1122,6 @@ impl Thread { .await; _ = this.update(cx, |this, cx| this.flush_pending_message(cx)); - if let Some(update_title) = update_title { - update_title.await.context("update title failed").log_err(); - } - match turn_result { Ok(()) => { log::info!("Turn execution completed"); @@ -1607,19 +1605,15 @@ impl Thread { }) } - fn update_title( - &mut self, - event_stream: &ThreadEventStream, - cx: &mut Context, - ) -> Task> { + fn generate_title(&mut self, cx: &mut Context) { + let Some(model) = self.summarization_model.clone() else { + return; + }; + log::info!( "Generating title with model: {:?}", self.summarization_model.as_ref().map(|model| model.name()) ); - let Some(model) = self.summarization_model.clone() else { - return Task::ready(Ok(())); - }; - let event_stream = event_stream.clone(); let mut request = LanguageModelRequest { intent: Some(CompletionIntent::ThreadSummarization), temperature: AgentSettings::temperature_for_model(&model, cx), @@ -1635,42 +1629,51 @@ impl Thread { content: vec![SUMMARIZE_THREAD_PROMPT.into()], cache: false, }); - cx.spawn(async move |this, cx| { + self.pending_title_generation = Some(cx.spawn(async move |this, cx| { let mut title = String::new(); - let mut messages = model.stream_completion(request, cx).await?; - while let Some(event) = messages.next().await { - let event = event?; - let text = match event { - LanguageModelCompletionEvent::Text(text) => text, - LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::UsageUpdated { amount, limit }, - ) => { - this.update(cx, |thread, cx| { - thread.update_model_request_usage(amount, limit, cx); - })?; - continue; + + let generate = async { + let mut messages = model.stream_completion(request, cx).await?; + while let Some(event) = messages.next().await { + let event = event?; + let text = match event { + LanguageModelCompletionEvent::Text(text) => text, + LanguageModelCompletionEvent::StatusUpdate( + CompletionRequestStatus::UsageUpdated { amount, limit }, + ) => { + this.update(cx, |thread, cx| { + thread.update_model_request_usage(amount, limit, cx); + })?; + continue; + } + _ => continue, + }; + + let mut lines = text.lines(); + title.extend(lines.next()); + + // Stop if the LLM generated multiple lines. + if lines.next().is_some() { + break; } - _ => continue, - }; - - let mut lines = text.lines(); - title.extend(lines.next()); - - // Stop if the LLM generated multiple lines. - if lines.next().is_some() { - break; } + anyhow::Ok(()) + }; + + if generate.await.context("failed to generate title").is_ok() { + _ = this.update(cx, |this, cx| this.set_title(title.into(), cx)); } + _ = this.update(cx, |this, _| this.pending_title_generation = None); + })); + } - log::info!("Setting title: {}", title); - - this.update(cx, |this, cx| { - let title = SharedString::from(title); - event_stream.send_title_update(title.clone()); - this.title = Some(title); - cx.notify(); - }) - }) + pub fn set_title(&mut self, title: SharedString, cx: &mut Context) { + self.pending_title_generation = None; + if Some(&title) != self.title.as_ref() { + self.title = Some(title); + cx.emit(TitleUpdated); + cx.notify(); + } } fn last_user_message(&self) -> Option<&UserMessage> { @@ -1975,6 +1978,10 @@ pub struct TokenUsageUpdated(pub Option); impl EventEmitter for Thread {} +pub struct TitleUpdated; + +impl EventEmitter for Thread {} + pub trait AgentTool where Self: 'static + Sized, @@ -2132,12 +2139,6 @@ where struct ThreadEventStream(mpsc::UnboundedSender>); impl ThreadEventStream { - fn send_title_update(&self, text: SharedString) { - self.0 - .unbounded_send(Ok(ThreadEvent::TitleUpdate(text))) - .ok(); - } - fn send_user_message(&self, message: &UserMessage) { self.0 .unbounded_send(Ok(ThreadEvent::UserMessage(message.clone()))) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 05d31051b2..936f987864 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -15,7 +15,7 @@ use buffer_diff::BufferDiff; use client::zed_urls; use collections::{HashMap, HashSet}; use editor::scroll::Autoscroll; -use editor::{Editor, EditorMode, MultiBuffer, PathKey, SelectionEffects}; +use editor::{Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects}; use file_icons::FileIcons; use fs::Fs; use gpui::{ @@ -281,7 +281,8 @@ enum ThreadState { }, Ready { thread: Entity, - _subscription: [Subscription; 2], + title_editor: Option>, + _subscriptions: Vec, }, LoadError(LoadError), Unauthenticated { @@ -445,12 +446,7 @@ impl AcpThreadView { this.update_in(cx, |this, window, cx| { match result { Ok(thread) => { - let thread_subscription = - cx.subscribe_in(&thread, window, Self::handle_thread_event); - let action_log = thread.read(cx).action_log().clone(); - let action_log_subscription = - cx.observe(&action_log, |_, _, cx| cx.notify()); let count = thread.read(cx).entries().len(); this.list_state.splice(0..0, count); @@ -489,9 +485,31 @@ impl AcpThreadView { }) }); + let mut subscriptions = vec![ + cx.subscribe_in(&thread, window, Self::handle_thread_event), + cx.observe(&action_log, |_, _, cx| cx.notify()), + ]; + + let title_editor = + if thread.update(cx, |thread, cx| thread.can_set_title(cx)) { + let editor = cx.new(|cx| { + let mut editor = Editor::single_line(window, cx); + editor.set_text(thread.read(cx).title(), window, cx); + editor + }); + subscriptions.push(cx.subscribe_in( + &editor, + window, + Self::handle_title_editor_event, + )); + Some(editor) + } else { + None + }; this.thread_state = ThreadState::Ready { thread, - _subscription: [thread_subscription, action_log_subscription], + title_editor, + _subscriptions: subscriptions, }; this.profile_selector = this.as_native_thread(cx).map(|thread| { @@ -618,6 +636,14 @@ impl AcpThreadView { } } + pub fn title_editor(&self) -> Option> { + if let ThreadState::Ready { title_editor, .. } = &self.thread_state { + title_editor.clone() + } else { + None + } + } + pub fn cancel_generation(&mut self, cx: &mut Context) { self.thread_error.take(); self.thread_retry_status.take(); @@ -662,6 +688,35 @@ impl AcpThreadView { cx.notify(); } + pub fn handle_title_editor_event( + &mut self, + title_editor: &Entity, + event: &EditorEvent, + window: &mut Window, + cx: &mut Context, + ) { + let Some(thread) = self.thread() else { return }; + + match event { + EditorEvent::BufferEdited => { + let new_title = title_editor.read(cx).text(cx); + thread.update(cx, |thread, cx| { + thread + .set_title(new_title.into(), cx) + .detach_and_log_err(cx); + }) + } + EditorEvent::Blurred => { + if title_editor.read(cx).text(cx).is_empty() { + title_editor.update(cx, |editor, cx| { + editor.set_text("New Thread", window, cx); + }); + } + } + _ => {} + } + } + pub fn handle_message_editor_event( &mut self, _: &Entity, @@ -1009,7 +1064,17 @@ impl AcpThreadView { self.thread_retry_status.take(); self.thread_state = ThreadState::LoadError(error.clone()); } - AcpThreadEvent::TitleUpdated | AcpThreadEvent::TokenUsageUpdated => {} + AcpThreadEvent::TitleUpdated => { + let title = thread.read(cx).title(); + if let Some(title_editor) = self.title_editor() { + title_editor.update(cx, |editor, cx| { + if editor.text(cx) != title { + editor.set_text(title, window, cx); + } + }); + } + } + AcpThreadEvent::TokenUsageUpdated => {} } cx.notify(); } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 65a9da573a..d2ff6aa4f3 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -905,7 +905,7 @@ impl AgentPanel { fn active_thread_view(&self) -> Option<&Entity> { match &self.active_view { - ActiveView::ExternalAgentThread { thread_view } => Some(thread_view), + ActiveView::ExternalAgentThread { thread_view, .. } => Some(thread_view), ActiveView::Thread { .. } | ActiveView::TextThread { .. } | ActiveView::History @@ -2075,9 +2075,32 @@ impl AgentPanel { } } ActiveView::ExternalAgentThread { thread_view } => { - Label::new(thread_view.read(cx).title(cx)) - .truncate() - .into_any_element() + if let Some(title_editor) = thread_view.read(cx).title_editor() { + div() + .w_full() + .on_action({ + let thread_view = thread_view.downgrade(); + move |_: &menu::Confirm, window, cx| { + if let Some(thread_view) = thread_view.upgrade() { + thread_view.focus_handle(cx).focus(window); + } + } + }) + .on_action({ + let thread_view = thread_view.downgrade(); + move |_: &editor::actions::Cancel, window, cx| { + if let Some(thread_view) = thread_view.upgrade() { + thread_view.focus_handle(cx).focus(window); + } + } + }) + .child(title_editor) + .into_any_element() + } else { + Label::new(thread_view.read(cx).title(cx)) + .truncate() + .into_any_element() + } } ActiveView::TextThread { title_editor, From 20a0c3e92050c417f242d5e909d4f9ea548494dc Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Thu, 21 Aug 2025 15:27:09 -0500 Subject: [PATCH 597/693] Disable minidump generation on dev builds (again) (#36716) We accidentally deleted this in #36267 Release Notes: - N/A --- crates/zed/src/reliability.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index 646a3af5bb..e9acaa588d 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -60,7 +60,9 @@ pub fn init_panic_hook( .or_else(|| info.payload().downcast_ref::().cloned()) .unwrap_or_else(|| "Box".to_string()); - crashes::handle_panic(payload.clone(), info.location()); + if *release_channel::RELEASE_CHANNEL != ReleaseChannel::Dev { + crashes::handle_panic(payload.clone(), info.location()); + } let thread = thread::current(); let thread_name = thread.name().unwrap_or(""); From 0beb919bbb8c662ee7ff3302bfb5e49bec1e3fba Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Thu, 21 Aug 2025 17:29:53 -0300 Subject: [PATCH 598/693] acp: Fix `MessageEditor::set_message` for sent messages (#36715) The `PromptCapabilities` introduced in previous PRs were only getting set on the main message editor and not for the editors in user messages. This caused a bug where mentions would disappear after resending the message, and for the completion provider to be limited to files. Release Notes: - N/A --- crates/agent_ui/src/acp/entry_view_state.rs | 9 ++++-- crates/agent_ui/src/acp/message_editor.rs | 34 +++++++++++---------- crates/agent_ui/src/acp/thread_view.rs | 16 ++++++---- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index c310473259..0e4080d689 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -1,7 +1,7 @@ -use std::ops::Range; +use std::{cell::Cell, ops::Range, rc::Rc}; use acp_thread::{AcpThread, AgentThreadEntry}; -use agent_client_protocol::ToolCallId; +use agent_client_protocol::{PromptCapabilities, ToolCallId}; use agent2::HistoryStore; use collections::HashMap; use editor::{Editor, EditorMode, MinimapVisibility}; @@ -27,6 +27,7 @@ pub struct EntryViewState { prompt_store: Option>, entries: Vec, prevent_slash_commands: bool, + prompt_capabilities: Rc>, } impl EntryViewState { @@ -35,6 +36,7 @@ impl EntryViewState { project: Entity, history_store: Entity, prompt_store: Option>, + prompt_capabilities: Rc>, prevent_slash_commands: bool, ) -> Self { Self { @@ -44,6 +46,7 @@ impl EntryViewState { prompt_store, entries: Vec::new(), prevent_slash_commands, + prompt_capabilities, } } @@ -81,6 +84,7 @@ impl EntryViewState { self.project.clone(), self.history_store.clone(), self.prompt_store.clone(), + self.prompt_capabilities.clone(), "Edit message - @ to include context", self.prevent_slash_commands, editor::EditorMode::AutoHeight { @@ -403,6 +407,7 @@ mod tests { project.clone(), history_store, None, + Default::default(), false, ) }); diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 8f5044cb21..7d73ebeb19 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -87,6 +87,7 @@ impl MessageEditor { project: Entity, history_store: Entity, prompt_store: Option>, + prompt_capabilities: Rc>, placeholder: impl Into>, prevent_slash_commands: bool, mode: EditorMode, @@ -100,7 +101,6 @@ impl MessageEditor { }, None, ); - let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default())); let completion_provider = ContextPickerCompletionProvider::new( cx.weak_entity(), workspace.clone(), @@ -203,10 +203,6 @@ impl MessageEditor { .detach(); } - pub fn set_prompt_capabilities(&mut self, capabilities: acp::PromptCapabilities) { - self.prompt_capabilities.set(capabilities); - } - #[cfg(test)] pub(crate) fn editor(&self) -> &Entity { &self.editor @@ -1095,15 +1091,21 @@ impl MessageEditor { mentions.push((start..end, mention_uri, resource.text)); } } + acp::ContentBlock::ResourceLink(resource) => { + if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() { + let start = text.len(); + write!(&mut text, "{}", mention_uri.as_link()).ok(); + let end = text.len(); + mentions.push((start..end, mention_uri, resource.uri)); + } + } acp::ContentBlock::Image(content) => { let start = text.len(); text.push_str("image"); let end = text.len(); images.push((start..end, content)); } - acp::ContentBlock::Audio(_) - | acp::ContentBlock::Resource(_) - | acp::ContentBlock::ResourceLink(_) => {} + acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => {} } } @@ -1850,7 +1852,7 @@ impl Addon for MessageEditorAddon { #[cfg(test)] mod tests { - use std::{ops::Range, path::Path, sync::Arc}; + use std::{cell::Cell, ops::Range, path::Path, rc::Rc, sync::Arc}; use acp_thread::MentionUri; use agent_client_protocol as acp; @@ -1896,6 +1898,7 @@ mod tests { project.clone(), history_store.clone(), None, + Default::default(), "Test", false, EditorMode::AutoHeight { @@ -2086,6 +2089,7 @@ mod tests { let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx)); let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default())); let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| { let workspace_handle = cx.weak_entity(); @@ -2095,6 +2099,7 @@ mod tests { project.clone(), history_store.clone(), None, + prompt_capabilities.clone(), "Test", false, EditorMode::AutoHeight { @@ -2139,13 +2144,10 @@ mod tests { editor.set_text("", window, cx); }); - message_editor.update(&mut cx, |editor, _cx| { - // Enable all prompt capabilities - editor.set_prompt_capabilities(acp::PromptCapabilities { - image: true, - audio: true, - embedded_context: true, - }); + prompt_capabilities.set(acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, }); cx.simulate_input("Lorem "); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 936f987864..c7d6bb439f 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -5,7 +5,7 @@ use acp_thread::{ }; use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; -use agent_client_protocol::{self as acp}; +use agent_client_protocol::{self as acp, PromptCapabilities}; use agent_servers::{AgentServer, ClaudeCode}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore}; @@ -34,6 +34,7 @@ use project::{Project, ProjectEntryId}; use prompt_store::{PromptId, PromptStore}; use rope::Point; use settings::{Settings as _, SettingsStore}; +use std::cell::Cell; use std::sync::Arc; use std::time::Instant; use std::{collections::BTreeMap, rc::Rc, time::Duration}; @@ -271,6 +272,7 @@ pub struct AcpThreadView { plan_expanded: bool, editor_expanded: bool, editing_message: Option, + prompt_capabilities: Rc>, _cancel_task: Option>, _subscriptions: [Subscription; 3], } @@ -306,6 +308,7 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) -> Self { + let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default())); let prevent_slash_commands = agent.clone().downcast::().is_some(); let message_editor = cx.new(|cx| { let mut editor = MessageEditor::new( @@ -313,6 +316,7 @@ impl AcpThreadView { project.clone(), history_store.clone(), prompt_store.clone(), + prompt_capabilities.clone(), "Message the agent — @ to include context", prevent_slash_commands, editor::EditorMode::AutoHeight { @@ -336,6 +340,7 @@ impl AcpThreadView { project.clone(), history_store.clone(), prompt_store.clone(), + prompt_capabilities.clone(), prevent_slash_commands, ) }); @@ -371,6 +376,7 @@ impl AcpThreadView { editor_expanded: false, history_store, hovered_recent_history_item: None, + prompt_capabilities, _subscriptions: subscriptions, _cancel_task: None, } @@ -448,6 +454,9 @@ impl AcpThreadView { Ok(thread) => { let action_log = thread.read(cx).action_log().clone(); + this.prompt_capabilities + .set(connection.prompt_capabilities()); + let count = thread.read(cx).entries().len(); this.list_state.splice(0..0, count); this.entry_view_state.update(cx, |view_state, cx| { @@ -523,11 +532,6 @@ impl AcpThreadView { }) }); - this.message_editor.update(cx, |message_editor, _cx| { - message_editor - .set_prompt_capabilities(connection.prompt_capabilities()); - }); - cx.notify(); } Err(err) => { From 06c0e593790d7fae184c31b02423a9d3bb0ccfee Mon Sep 17 00:00:00 2001 From: David Kleingeld Date: Fri, 22 Aug 2025 00:21:36 +0200 Subject: [PATCH 599/693] Make tab switcher show preview of selected tab (#36718) Similar to nvim's telescope this makes it easier to find the right tab in the list. The preview takes place in the pane where the tab resides. - on dismiss: We restore all panes. - on confirm: We restore all panes except the one where the selected tab resides. For this reason we collect the active item for each pane before the tabswither starts. Release Notes: - Improved tab switcher, it now shows a preview of the selected tab Co-authored-by: Julia Ryan --- crates/tab_switcher/src/tab_switcher.rs | 55 +++++++++++++++++++++---- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index 11e32523b4..7c70bcd5b5 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -113,7 +113,13 @@ impl TabSwitcher { } let weak_workspace = workspace.weak_handle(); + let project = workspace.project().clone(); + let original_items: Vec<_> = workspace + .panes() + .iter() + .map(|p| (p.clone(), p.read(cx).active_item_index())) + .collect(); workspace.toggle_modal(window, cx, |window, cx| { let delegate = TabSwitcherDelegate::new( project, @@ -124,6 +130,7 @@ impl TabSwitcher { is_global, window, cx, + original_items, ); TabSwitcher::new(delegate, window, is_global, cx) }); @@ -221,7 +228,9 @@ pub struct TabSwitcherDelegate { workspace: WeakEntity, project: Entity, matches: Vec, + original_items: Vec<(Entity, usize)>, is_all_panes: bool, + restored_items: bool, } impl TabSwitcherDelegate { @@ -235,6 +244,7 @@ impl TabSwitcherDelegate { is_all_panes: bool, window: &mut Window, cx: &mut Context, + original_items: Vec<(Entity, usize)>, ) -> Self { Self::subscribe_to_updates(&pane, window, cx); Self { @@ -246,6 +256,8 @@ impl TabSwitcherDelegate { project, matches: Vec::new(), is_all_panes, + original_items, + restored_items: false, } } @@ -300,13 +312,6 @@ impl TabSwitcherDelegate { let matches = if query.is_empty() { let history = workspace.read(cx).recently_activated_items(cx); - for item in &all_items { - eprintln!( - "{:?} {:?}", - item.item.tab_content_text(0, cx), - (Reverse(history.get(&item.item.item_id())), item.item_index) - ) - } all_items .sort_by_key(|tab| (Reverse(history.get(&tab.item.item_id())), tab.item_index)); all_items @@ -473,8 +478,25 @@ impl PickerDelegate for TabSwitcherDelegate { self.selected_index } - fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context>) { + fn set_selected_index( + &mut self, + ix: usize, + window: &mut Window, + cx: &mut Context>, + ) { self.selected_index = ix; + + let Some(selected_match) = self.matches.get(self.selected_index()) else { + return; + }; + selected_match + .pane + .update(cx, |pane, cx| { + if let Some(index) = pane.index_for_item(selected_match.item.as_ref()) { + pane.activate_item(index, false, false, window, cx); + } + }) + .ok(); cx.notify(); } @@ -501,6 +523,13 @@ impl PickerDelegate for TabSwitcherDelegate { let Some(selected_match) = self.matches.get(self.selected_index()) else { return; }; + + self.restored_items = true; + for (pane, index) in self.original_items.iter() { + pane.update(cx, |this, cx| { + this.activate_item(*index, false, false, window, cx); + }) + } selected_match .pane .update(cx, |pane, cx| { @@ -511,7 +540,15 @@ impl PickerDelegate for TabSwitcherDelegate { .ok(); } - fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { + fn dismissed(&mut self, window: &mut Window, cx: &mut Context>) { + if !self.restored_items { + for (pane, index) in self.original_items.iter() { + pane.update(cx, |this, cx| { + this.activate_item(*index, false, false, window, cx); + }) + } + } + self.tab_switcher .update(cx, |_, cx| cx.emit(DismissEvent)) .log_err(); From a977fbc5b09e3fc23181fe9c30246de6c6e9c9bc Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Thu, 21 Aug 2025 18:40:07 -0400 Subject: [PATCH 600/693] Document project_panel.sticky_scroll (#36721) Hat tip to: @watercubz in https://github.com/zed-industries/zed/issues/22869#issuecomment-3183850576 Release Notes: - N/A --- docs/src/configuring-zed.md | 1 + docs/src/visual-customization.md | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 39d172ea5f..696370e310 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -3234,6 +3234,7 @@ Run the `theme selector: toggle` action in the command palette to see a current "scrollbar": { "show": null }, + "sticky_scroll": true, "show_diagnostics": "all", "indent_guides": { "show": "always" diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 3ad1e381d9..24b2a9d769 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -430,6 +430,7 @@ Project panel can be shown/hidden with {#action project_panel::ToggleFocus} ({#k "indent_size": 20, // Pixels for each successive indent "auto_reveal_entries": true, // Show file in panel when activating its buffer "auto_fold_dirs": true, // Fold dirs with single subdir + "sticky_scroll": true, // Stick parent directories at top of the project panel. "scrollbar": { // Project panel scrollbar settings "show": null // Show/hide: (auto, system, always, never) }, From 18fe68d991b8f63ef7f5d276eb052e055feea70a Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 21 Aug 2025 20:51:36 -0300 Subject: [PATCH 601/693] thread view: Add small refinements to tool call UI (#36723) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 66 ++++++++++++++------------ crates/markdown/src/markdown.rs | 3 +- 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index c7d6bb439f..4d89a55139 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1372,7 +1372,7 @@ impl AcpThreadView { AgentThreadEntry::ToolCall(tool_call) => { let has_terminals = tool_call.terminals().next().is_some(); - div().w_full().py_1p5().px_5().map(|this| { + div().w_full().py_1().px_5().map(|this| { if has_terminals { this.children(tool_call.terminals().map(|terminal| { self.render_terminal_tool_call( @@ -1570,7 +1570,7 @@ impl AcpThreadView { .size(IconSize::Small) .color(Color::Muted); - let base_container = h_flex().size_4().justify_center(); + let base_container = h_flex().flex_shrink_0().size_4().justify_center(); if is_collapsible { base_container @@ -1623,20 +1623,32 @@ impl AcpThreadView { | ToolCallStatus::WaitingForConfirmation { .. } | ToolCallStatus::Completed => None, ToolCallStatus::InProgress => Some( - Icon::new(IconName::ArrowCircle) - .color(Color::Muted) - .size(IconSize::Small) - .with_animation( - "running", - Animation::new(Duration::from_secs(3)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + div() + .absolute() + .right_2() + .child( + Icon::new(IconName::ArrowCircle) + .color(Color::Muted) + .size(IconSize::Small) + .with_animation( + "running", + Animation::new(Duration::from_secs(3)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate(percentage(delta))) + }, + ), ) .into_any(), ), ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => Some( - Icon::new(IconName::Close) - .color(Color::Error) - .size(IconSize::Small) + div() + .absolute() + .right_2() + .child( + Icon::new(IconName::Close) + .color(Color::Error) + .size(IconSize::Small), + ) .into_any_element(), ), }; @@ -1734,13 +1746,14 @@ impl AcpThreadView { .child( h_flex() .id(header_id) + .relative() .w_full() + .max_w_full() .gap_1() - .justify_between() .when(use_card_layout, |this| { - this.pl_2() - .pr_1p5() - .py_1() + this.pl_1p5() + .pr_1() + .py_0p5() .rounded_t_md() .when(is_open && !failed_tool_call, |this| { this.border_b_1() @@ -1753,7 +1766,7 @@ impl AcpThreadView { .group(&card_header_id) .relative() .w_full() - .min_h_6() + .h(window.line_height() - px(2.)) .text_size(self.tool_name_font_size()) .child(self.render_tool_call_icon( card_header_id, @@ -1797,21 +1810,14 @@ impl AcpThreadView { } else { h_flex() .id("non-card-label-container") - .w_full() .relative() + .w_full() + .max_w_full() .ml_1p5() - .overflow_hidden() - .child( - h_flex() - .id("non-card-label") - .pr_8() - .w_full() - .overflow_x_scroll() - .child(self.render_markdown( - tool_call.label.clone(), - default_markdown_style(false, true, window, cx), - )), - ) + .child(h_flex().pr_8().child(self.render_markdown( + tool_call.label.clone(), + default_markdown_style(false, true, window, cx), + ))) .child(gradient_overlay(gradient_color)) .on_click(cx.listener({ let id = tool_call.id.clone(); diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 755506bd12..39a438c512 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -1089,7 +1089,7 @@ impl Element for MarkdownElement { .absolute() .top_1() .right_1() - .justify_center() + .justify_end() .child(codeblock), ) }); @@ -1320,6 +1320,7 @@ fn render_copy_code_block_button( ) .icon_color(Color::Muted) .icon_size(IconSize::Small) + .style(ButtonStyle::Filled) .shape(ui::IconButtonShape::Square) .tooltip(Tooltip::text("Copy Code")) .on_click({ From eeaadc098f189121d840849d4833dec4398364cb Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Thu, 21 Aug 2025 18:59:42 -0500 Subject: [PATCH 602/693] Add GPU info to Sentry crashes (#36624) Closes #ISSUE Adds system GPU collection to crash reporting. Currently this is Linux only. The system GPUs are determined by reading the `/sys/class/drm` directory structure, rather than using the exisiting `gpui::Window::gpu_specs()` method in order to gather more information, and so that the GPU context is not dependent on Vulkan context initialization (i.e. we still get GPU info when Zed fails to start because Vulkan failed to initialize). Unfortunately, the `blade` APIs do not support querying which GPU _will_ be used, so we do not know which GPU was attempted to be used when Vulkan context initialization fails, however, when Vulkan initialization succeeds, we send a message to the crash handler containing the result of `gpui::Window::gpu_specs()` to include the "Active" gpu in any crash report that may occur Release Notes: - N/A *or* Added/Fixed/Improved ... --- Cargo.lock | 31 ++++- Cargo.toml | 5 + crates/client/src/telemetry.rs | 2 +- crates/crashes/Cargo.toml | 2 + crates/crashes/src/crashes.rs | 26 +++- crates/feedback/Cargo.toml | 6 +- crates/feedback/src/feedback.rs | 6 +- crates/gpui/src/gpui.rs | 2 +- crates/system_specs/Cargo.toml | 28 ++++ crates/system_specs/LICENSE-GPL | 1 + .../src/system_specs.rs | 122 +++++++++++++++++- crates/workspace/Cargo.toml | 2 +- crates/zed/Cargo.toml | 2 + crates/zed/src/main.rs | 4 +- crates/zed/src/reliability.rs | 93 ++++++++++++- crates/zed/src/zed.rs | 12 +- 16 files changed, 315 insertions(+), 29 deletions(-) create mode 100644 crates/system_specs/Cargo.toml create mode 120000 crates/system_specs/LICENSE-GPL rename crates/{feedback => system_specs}/src/system_specs.rs (59%) diff --git a/Cargo.lock b/Cargo.lock index 61f6f42498..2b3d7b2691 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4050,6 +4050,7 @@ dependencies = [ name = "crashes" version = "0.1.0" dependencies = [ + "bincode", "crash-handler", "log", "mach2 0.5.0", @@ -4059,6 +4060,7 @@ dependencies = [ "serde", "serde_json", "smol", + "system_specs", "workspace-hack", ] @@ -5738,14 +5740,10 @@ dependencies = [ name = "feedback" version = "0.1.0" dependencies = [ - "client", "editor", "gpui", - "human_bytes", "menu", - "release_channel", - "serde", - "sysinfo", + "system_specs", "ui", "urlencoding", "util", @@ -11634,6 +11632,12 @@ dependencies = [ "hmac", ] +[[package]] +name = "pciid-parser" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0008e816fcdaf229cdd540e9b6ca2dc4a10d65c31624abb546c6420a02846e61" + [[package]] name = "pem" version = "3.0.5" @@ -16154,6 +16158,21 @@ dependencies = [ "winx", ] +[[package]] +name = "system_specs" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "gpui", + "human_bytes", + "pciid-parser", + "release_channel", + "serde", + "sysinfo", + "workspace-hack", +] + [[package]] name = "tab_switcher" version = "0.1.0" @@ -20413,6 +20432,7 @@ dependencies = [ "auto_update", "auto_update_ui", "backtrace", + "bincode", "breadcrumbs", "call", "channel", @@ -20511,6 +20531,7 @@ dependencies = [ "supermaven", "svg_preview", "sysinfo", + "system_specs", "tab_switcher", "task", "tasks_ui", diff --git a/Cargo.toml b/Cargo.toml index b13795e1e1..84de9b30ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,6 +155,7 @@ members = [ "crates/streaming_diff", "crates/sum_tree", "crates/supermaven", + "crates/system_specs", "crates/supermaven_api", "crates/svg_preview", "crates/tab_switcher", @@ -381,6 +382,7 @@ streaming_diff = { path = "crates/streaming_diff" } sum_tree = { path = "crates/sum_tree" } supermaven = { path = "crates/supermaven" } supermaven_api = { path = "crates/supermaven_api" } +system_specs = { path = "crates/system_specs" } tab_switcher = { path = "crates/tab_switcher" } task = { path = "crates/task" } tasks_ui = { path = "crates/tasks_ui" } @@ -450,6 +452,7 @@ aws-sdk-bedrockruntime = { version = "1.80.0", features = [ aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] } aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] } base64 = "0.22" +bincode = "1.2.1" bitflags = "2.6.0" blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" } blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" } @@ -493,6 +496,7 @@ handlebars = "4.3" heck = "0.5" heed = { version = "0.21.0", features = ["read-txn-no-tls"] } hex = "0.4.3" +human_bytes = "0.4.1" html5ever = "0.27.0" http = "1.1" http-body = "1.0" @@ -532,6 +536,7 @@ palette = { version = "0.7.5", default-features = false, features = ["std"] } parking_lot = "0.12.1" partial-json-fixer = "0.5.3" parse_int = "0.9" +pciid-parser = "0.8.0" pathdiff = "0.2" pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index f3142a0af6..a5c1532c75 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -76,7 +76,7 @@ static ZED_CLIENT_CHECKSUM_SEED: LazyLock>> = LazyLock::new(|| { pub static MINIDUMP_ENDPOINT: LazyLock> = LazyLock::new(|| { option_env!("ZED_MINIDUMP_ENDPOINT") - .map(|s| s.to_owned()) + .map(str::to_string) .or_else(|| env::var("ZED_MINIDUMP_ENDPOINT").ok()) }); diff --git a/crates/crashes/Cargo.toml b/crates/crashes/Cargo.toml index f12913d1cb..370f0bb5f6 100644 --- a/crates/crashes/Cargo.toml +++ b/crates/crashes/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true license = "GPL-3.0-or-later" [dependencies] +bincode.workspace = true crash-handler.workspace = true log.workspace = true minidumper.workspace = true @@ -14,6 +15,7 @@ release_channel.workspace = true smol.workspace = true serde.workspace = true serde_json.workspace = true +system_specs.workspace = true workspace-hack.workspace = true [target.'cfg(target_os = "macos")'.dependencies] diff --git a/crates/crashes/src/crashes.rs b/crates/crashes/src/crashes.rs index b1afc5ae45..f7bc96bff9 100644 --- a/crates/crashes/src/crashes.rs +++ b/crates/crashes/src/crashes.rs @@ -127,6 +127,7 @@ unsafe fn suspend_all_other_threads() { pub struct CrashServer { initialization_params: OnceLock, panic_info: OnceLock, + active_gpu: OnceLock, has_connection: Arc, } @@ -135,6 +136,8 @@ pub struct CrashInfo { pub init: InitCrashHandler, pub panic: Option, pub minidump_error: Option, + pub gpus: Vec, + pub active_gpu: Option, } #[derive(Debug, Deserialize, Serialize, Clone)] @@ -143,7 +146,6 @@ pub struct InitCrashHandler { pub zed_version: String, pub release_channel: String, pub commit_sha: String, - // pub gpu: String, } #[derive(Deserialize, Serialize, Debug, Clone)] @@ -178,6 +180,18 @@ impl minidumper::ServerHandler for CrashServer { Err(e) => Some(format!("{e:?}")), }; + #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] + let gpus = vec![]; + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + let gpus = match system_specs::read_gpu_info_from_sys_class_drm() { + Ok(gpus) => gpus, + Err(err) => { + log::warn!("Failed to collect GPU information for crash report: {err}"); + vec![] + } + }; + let crash_info = CrashInfo { init: self .initialization_params @@ -186,6 +200,8 @@ impl minidumper::ServerHandler for CrashServer { .clone(), panic: self.panic_info.get().cloned(), minidump_error, + active_gpu: self.active_gpu.get().cloned(), + gpus, }; let crash_data_path = paths::logs_dir() @@ -211,6 +227,13 @@ impl minidumper::ServerHandler for CrashServer { serde_json::from_slice::(&buffer).expect("invalid panic data"); self.panic_info.set(panic_data).expect("already panicked"); } + 3 => { + let gpu_specs: system_specs::GpuSpecs = + bincode::deserialize(&buffer).expect("gpu specs"); + self.active_gpu + .set(gpu_specs) + .expect("already set active gpu"); + } _ => { panic!("invalid message kind"); } @@ -287,6 +310,7 @@ pub fn crash_server(socket: &Path) { initialization_params: OnceLock::new(), panic_info: OnceLock::new(), has_connection, + active_gpu: OnceLock::new(), }), &shutdown, Some(CRASH_HANDLER_PING_TIMEOUT), diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index 3a2c1fd713..db872f7a15 100644 --- a/crates/feedback/Cargo.toml +++ b/crates/feedback/Cargo.toml @@ -15,13 +15,9 @@ path = "src/feedback.rs" test-support = [] [dependencies] -client.workspace = true gpui.workspace = true -human_bytes = "0.4.1" menu.workspace = true -release_channel.workspace = true -serde.workspace = true -sysinfo.workspace = true +system_specs.workspace = true ui.workspace = true urlencoding.workspace = true util.workspace = true diff --git a/crates/feedback/src/feedback.rs b/crates/feedback/src/feedback.rs index 40c2707d34..3822dd7ba3 100644 --- a/crates/feedback/src/feedback.rs +++ b/crates/feedback/src/feedback.rs @@ -1,18 +1,14 @@ use gpui::{App, ClipboardItem, PromptLevel, actions}; -use system_specs::SystemSpecs; +use system_specs::{CopySystemSpecsIntoClipboard, SystemSpecs}; use util::ResultExt; use workspace::Workspace; use zed_actions::feedback::FileBugReport; pub mod feedback_modal; -pub mod system_specs; - actions!( zed, [ - /// Copies system specifications to the clipboard for bug reports. - CopySystemSpecsIntoClipboard, /// Opens email client to send feedback to Zed support. EmailZed, /// Opens the Zed repository on GitHub. diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 5e4b5fe6e9..0f5b98df39 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -352,7 +352,7 @@ impl Flatten for Result { } /// Information about the GPU GPUI is running on. -#[derive(Default, Debug)] +#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)] pub struct GpuSpecs { /// Whether the GPU is really a fake (like `llvmpipe`) running on the CPU. pub is_software_emulated: bool, diff --git a/crates/system_specs/Cargo.toml b/crates/system_specs/Cargo.toml new file mode 100644 index 0000000000..8ef1b581ae --- /dev/null +++ b/crates/system_specs/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "system_specs" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/system_specs.rs" + +[features] +default = [] + +[dependencies] +anyhow.workspace = true +client.workspace = true +gpui.workspace = true +human_bytes.workspace = true +release_channel.workspace = true +serde.workspace = true +sysinfo.workspace = true +workspace-hack.workspace = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies] +pciid-parser.workspace = true diff --git a/crates/system_specs/LICENSE-GPL b/crates/system_specs/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/system_specs/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/feedback/src/system_specs.rs b/crates/system_specs/src/system_specs.rs similarity index 59% rename from crates/feedback/src/system_specs.rs rename to crates/system_specs/src/system_specs.rs index 87642ab929..731d335232 100644 --- a/crates/feedback/src/system_specs.rs +++ b/crates/system_specs/src/system_specs.rs @@ -1,11 +1,22 @@ +//! # system_specs + use client::telemetry; -use gpui::{App, AppContext as _, SemanticVersion, Task, Window}; +pub use gpui::GpuSpecs; +use gpui::{App, AppContext as _, SemanticVersion, Task, Window, actions}; use human_bytes::human_bytes; use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use serde::Serialize; use std::{env, fmt::Display}; use sysinfo::{MemoryRefreshKind, RefreshKind, System}; +actions!( + zed, + [ + /// Copies system specifications to the clipboard for bug reports. + CopySystemSpecsIntoClipboard, + ] +); + #[derive(Clone, Debug, Serialize)] pub struct SystemSpecs { app_version: String, @@ -158,6 +169,115 @@ fn try_determine_available_gpus() -> Option { } } +#[derive(Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize, Clone)] +pub struct GpuInfo { + pub device_name: Option, + pub device_pci_id: u16, + pub vendor_name: Option, + pub vendor_pci_id: u16, + pub driver_version: Option, + pub driver_name: Option, +} + +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +pub fn read_gpu_info_from_sys_class_drm() -> anyhow::Result> { + use anyhow::Context as _; + use pciid_parser; + let dir_iter = std::fs::read_dir("/sys/class/drm").context("Failed to read /sys/class/drm")?; + let mut pci_addresses = vec![]; + let mut gpus = Vec::::new(); + let pci_db = pciid_parser::Database::read().ok(); + for entry in dir_iter { + let Ok(entry) = entry else { + continue; + }; + + let device_path = entry.path().join("device"); + let Some(pci_address) = device_path.read_link().ok().and_then(|pci_address| { + pci_address + .file_name() + .and_then(std::ffi::OsStr::to_str) + .map(str::trim) + .map(str::to_string) + }) else { + continue; + }; + let Ok(device_pci_id) = read_pci_id_from_path(device_path.join("device")) else { + continue; + }; + let Ok(vendor_pci_id) = read_pci_id_from_path(device_path.join("vendor")) else { + continue; + }; + let driver_name = std::fs::read_link(device_path.join("driver")) + .ok() + .and_then(|driver_link| { + driver_link + .file_name() + .and_then(std::ffi::OsStr::to_str) + .map(str::trim) + .map(str::to_string) + }); + let driver_version = driver_name + .as_ref() + .and_then(|driver_name| { + std::fs::read_to_string(format!("/sys/module/{driver_name}/version")).ok() + }) + .as_deref() + .map(str::trim) + .map(str::to_string); + + let already_found = gpus + .iter() + .zip(&pci_addresses) + .any(|(gpu, gpu_pci_address)| { + gpu_pci_address == &pci_address + && gpu.driver_version == driver_version + && gpu.driver_name == driver_name + }); + + if already_found { + continue; + } + + let vendor = pci_db + .as_ref() + .and_then(|db| db.vendors.get(&vendor_pci_id)); + let vendor_name = vendor.map(|vendor| vendor.name.clone()); + let device_name = vendor + .and_then(|vendor| vendor.devices.get(&device_pci_id)) + .map(|device| device.name.clone()); + + gpus.push(GpuInfo { + device_name, + device_pci_id, + vendor_name, + vendor_pci_id, + driver_version, + driver_name, + }); + pci_addresses.push(pci_address); + } + + Ok(gpus) +} + +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +fn read_pci_id_from_path(path: impl AsRef) -> anyhow::Result { + use anyhow::Context as _; + let id = std::fs::read_to_string(path)?; + let id = id + .trim() + .strip_prefix("0x") + .context("Not a device ID") + .context(id.clone())?; + anyhow::ensure!( + id.len() == 4, + "Not a device id, expected 4 digits, found {}", + id.len() + ); + u16::from_str_radix(id, 16).context("Failed to parse device ID") +} + /// Returns value of `ZED_BUNDLE_TYPE` set at compiletime or else at runtime. /// /// The compiletime value is used by flatpak since it doesn't seem to have a way to provide a diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index e1bda7ad36..570657ba8f 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -29,7 +29,7 @@ test-support = [ any_vec.workspace = true anyhow.workspace = true async-recursion.workspace = true -bincode = "1.2.1" +bincode.workspace = true call.workspace = true client.workspace = true clock.workspace = true diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index ac4cd72124..c61e23f0a1 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -33,6 +33,7 @@ audio.workspace = true auto_update.workspace = true auto_update_ui.workspace = true backtrace = "0.3" +bincode.workspace = true breadcrumbs.workspace = true call.workspace = true channel.workspace = true @@ -60,6 +61,7 @@ extensions_ui.workspace = true feature_flags.workspace = true feedback.workspace = true file_finder.workspace = true +system_specs.workspace = true fs.workspace = true futures.workspace = true git.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 7ab76b71de..8beefd5891 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -16,7 +16,7 @@ use extension_host::ExtensionStore; use fs::{Fs, RealFs}; use futures::{StreamExt, channel::oneshot, future}; use git::GitHostingProviderRegistry; -use gpui::{App, AppContext as _, Application, AsyncApp, Focusable as _, UpdateGlobal as _}; +use gpui::{App, AppContext, Application, AsyncApp, Focusable as _, UpdateGlobal as _}; use gpui_tokio::Tokio; use http_client::{Url, read_proxy_from_env}; @@ -240,7 +240,7 @@ pub fn main() { option_env!("ZED_COMMIT_SHA").map(|commit_sha| AppCommitSha::new(commit_sha.to_string())); if args.system_specs { - let system_specs = feedback::system_specs::SystemSpecs::new_stateless( + let system_specs = system_specs::SystemSpecs::new_stateless( app_version, app_commit_sha, *release_channel::RELEASE_CHANNEL, diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index e9acaa588d..ac06f1fd9f 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -89,7 +89,9 @@ pub fn init_panic_hook( }, backtrace, ); - std::process::exit(-1); + if MINIDUMP_ENDPOINT.is_none() { + std::process::exit(-1); + } } let main_module_base_address = get_main_module_base_address(); @@ -148,7 +150,9 @@ pub fn init_panic_hook( } zlog::flush(); - if !is_pty && let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() { + if (!is_pty || MINIDUMP_ENDPOINT.is_some()) + && let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() + { let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string(); let panic_file_path = paths::logs_dir().join(format!("zed-{timestamp}.panic")); let panic_file = fs::OpenOptions::new() @@ -614,10 +618,9 @@ async fn upload_minidump( let mut panic_message = "".to_owned(); if let Some(panic_info) = metadata.panic.as_ref() { panic_message = panic_info.message.clone(); - form = form.text("sentry[logentry][formatted]", panic_info.message.clone()); - form = form.text("span", panic_info.span.clone()); - // TODO: add gpu-context, feature-flag-context, and more of device-context like gpu - // name, screen resolution, available ram, device model, etc + form = form + .text("sentry[logentry][formatted]", panic_info.message.clone()) + .text("span", panic_info.span.clone()); } if let Some(minidump_error) = metadata.minidump_error.clone() { form = form.text("minidump_error", minidump_error); @@ -633,6 +636,63 @@ async fn upload_minidump( commit_sha = metadata.init.commit_sha.clone(), ); + let gpu_count = metadata.gpus.len(); + for (index, gpu) in metadata.gpus.iter().cloned().enumerate() { + let system_specs::GpuInfo { + device_name, + device_pci_id, + vendor_name, + vendor_pci_id, + driver_version, + driver_name, + } = gpu; + let num = if gpu_count == 1 && metadata.active_gpu.is_none() { + String::new() + } else { + index.to_string() + }; + let name = format!("gpu{num}"); + let root = format!("sentry[contexts][{name}]"); + form = form + .text( + format!("{root}[Description]"), + "A GPU found on the users system. May or may not be the GPU Zed is running on", + ) + .text(format!("{root}[type]"), "gpu") + .text(format!("{root}[name]"), device_name.unwrap_or(name)) + .text(format!("{root}[id]"), format!("{:#06x}", device_pci_id)) + .text( + format!("{root}[vendor_id]"), + format!("{:#06x}", vendor_pci_id), + ) + .text_if_some(format!("{root}[vendor_name]"), vendor_name) + .text_if_some(format!("{root}[driver_version]"), driver_version) + .text_if_some(format!("{root}[driver_name]"), driver_name); + } + if let Some(active_gpu) = metadata.active_gpu.clone() { + form = form + .text( + "sentry[contexts][Active_GPU][Description]", + "The GPU Zed is running on", + ) + .text("sentry[contexts][Active_GPU][type]", "gpu") + .text("sentry[contexts][Active_GPU][name]", active_gpu.device_name) + .text( + "sentry[contexts][Active_GPU][driver_version]", + active_gpu.driver_info, + ) + .text( + "sentry[contexts][Active_GPU][driver_name]", + active_gpu.driver_name, + ) + .text( + "sentry[contexts][Active_GPU][is_software_emulated]", + active_gpu.is_software_emulated.to_string(), + ); + } + + // TODO: feature-flag-context, and more of device-context like screen resolution, available ram, device model, etc + let mut response_text = String::new(); let mut response = http.send_multipart_form(endpoint, form).await?; response @@ -646,6 +706,27 @@ async fn upload_minidump( Ok(()) } +trait FormExt { + fn text_if_some( + self, + label: impl Into>, + value: Option>>, + ) -> Self; +} + +impl FormExt for Form { + fn text_if_some( + self, + label: impl Into>, + value: Option>>, + ) -> Self { + match value { + Some(value) => self.text(label.into(), value.into()), + None => self, + } + } +} + async fn upload_panic( http: &Arc, panic_report_url: &Url, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 3b5f99f9bd..638e1dca0e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -344,7 +344,17 @@ pub fn initialize_workspace( if let Some(specs) = window.gpu_specs() { log::info!("Using GPU: {:?}", specs); - show_software_emulation_warning_if_needed(specs, window, cx); + show_software_emulation_warning_if_needed(specs.clone(), window, cx); + if let Some((crash_server, message)) = crashes::CRASH_HANDLER + .get() + .zip(bincode::serialize(&specs).ok()) + && let Err(err) = crash_server.send_message(3, message) + { + log::warn!( + "Failed to store active gpu info for crash reporting: {}", + err + ); + } } let edit_prediction_menu_handle = PopoverMenuHandle::default(); From ca139b701e20517260baf31602e2840e483b772b Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Thu, 21 Aug 2025 19:18:25 -0500 Subject: [PATCH 603/693] keymap_ui: Improve conflict resolution for semantically equal contexts (#36204) Closes #ISSUE Creates a function named `normalized_ctx_eq` that compares `gpui::KeybindContextPredicate`'s while taking into account the associativity of the binary operators. This function is now used to compare context predicates in the keymap editor, greatly improving the number of cases caught by our overloading and conflict detection Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/settings_ui/src/keybindings.rs | 397 +++++++++++++++++++++++--- 1 file changed, 353 insertions(+), 44 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 9a2d33ef7c..9c76725972 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -12,9 +12,11 @@ use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity, - EventEmitter, FocusHandle, Focusable, Global, IsZero, KeyContext, Keystroke, MouseButton, - Point, ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task, - TextStyleRefinement, WeakEntity, actions, anchored, deferred, div, + EventEmitter, FocusHandle, Focusable, Global, IsZero, + KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or}, + KeyContext, Keystroke, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful, + StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions, anchored, deferred, + div, }; use language::{Language, LanguageConfig, ToOffset as _}; use notifications::status_toast::{StatusToast, ToastIcon}; @@ -182,15 +184,6 @@ struct KeybindConflict { remaining_conflict_amount: usize, } -impl KeybindConflict { - fn from_iter<'a>(mut indices: impl Iterator) -> Option { - indices.next().map(|origin| Self { - first_conflict_index: origin.index, - remaining_conflict_amount: indices.count(), - }) - } -} - #[derive(Clone, Copy, PartialEq)] struct ConflictOrigin { override_source: KeybindSource, @@ -238,13 +231,21 @@ impl ConflictOrigin { #[derive(Default)] struct ConflictState { conflicts: Vec>, - keybind_mapping: HashMap>, + keybind_mapping: ConflictKeybindMapping, has_user_conflicts: bool, } +type ConflictKeybindMapping = HashMap< + Vec, + Vec<( + Option, + Vec, + )>, +>; + impl ConflictState { fn new(key_bindings: &[ProcessedBinding]) -> Self { - let mut action_keybind_mapping: HashMap<_, Vec> = HashMap::default(); + let mut action_keybind_mapping = ConflictKeybindMapping::default(); let mut largest_index = 0; for (index, binding) in key_bindings @@ -252,29 +253,48 @@ impl ConflictState { .enumerate() .flat_map(|(index, binding)| Some(index).zip(binding.keybind_information())) { - action_keybind_mapping - .entry(binding.get_action_mapping()) - .or_default() - .push(ConflictOrigin::new(binding.source, index)); + let mapping = binding.get_action_mapping(); + let predicate = mapping + .context + .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok()); + let entry = action_keybind_mapping + .entry(mapping.keystrokes) + .or_default(); + let origin = ConflictOrigin::new(binding.source, index); + if let Some((_, origins)) = + entry + .iter_mut() + .find(|(other_predicate, _)| match (&predicate, other_predicate) { + (None, None) => true, + (Some(a), Some(b)) => normalized_ctx_eq(a, b), + _ => false, + }) + { + origins.push(origin); + } else { + entry.push((predicate, vec![origin])); + } largest_index = index; } let mut conflicts = vec![None; largest_index + 1]; let mut has_user_conflicts = false; - for indices in action_keybind_mapping.values_mut() { - indices.sort_unstable_by_key(|origin| origin.override_source); - let Some((fst, snd)) = indices.get(0).zip(indices.get(1)) else { - continue; - }; + for entries in action_keybind_mapping.values_mut() { + for (_, indices) in entries.iter_mut() { + indices.sort_unstable_by_key(|origin| origin.override_source); + let Some((fst, snd)) = indices.get(0).zip(indices.get(1)) else { + continue; + }; - for origin in indices.iter() { - conflicts[origin.index] = - origin.get_conflict_with(if origin == fst { snd } else { fst }) + for origin in indices.iter() { + conflicts[origin.index] = + origin.get_conflict_with(if origin == fst { snd } else { fst }) + } + + has_user_conflicts |= fst.override_source == KeybindSource::User + && snd.override_source == KeybindSource::User; } - - has_user_conflicts |= fst.override_source == KeybindSource::User - && snd.override_source == KeybindSource::User; } Self { @@ -289,15 +309,34 @@ impl ConflictState { action_mapping: &ActionMapping, keybind_idx: Option, ) -> Option { - self.keybind_mapping - .get(action_mapping) - .and_then(|indices| { - KeybindConflict::from_iter( - indices + let ActionMapping { + keystrokes, + context, + } = action_mapping; + let predicate = context + .as_deref() + .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok()); + self.keybind_mapping.get(keystrokes).and_then(|entries| { + entries + .iter() + .find_map(|(other_predicate, indices)| { + match (&predicate, other_predicate) { + (None, None) => true, + (Some(pred), Some(other)) => normalized_ctx_eq(pred, other), + _ => false, + } + .then_some(indices) + }) + .and_then(|indices| { + let mut indices = indices .iter() - .filter(|&conflict| Some(conflict.index) != keybind_idx), - ) - }) + .filter(|&conflict| Some(conflict.index) != keybind_idx); + indices.next().map(|origin| KeybindConflict { + first_conflict_index: origin.index, + remaining_conflict_amount: indices.count(), + }) + }) + }) } fn conflict_for_idx(&self, idx: usize) -> Option { @@ -3089,29 +3128,29 @@ fn collect_contexts_from_assets() -> Vec { queue.push(root_context); while let Some(context) = queue.pop() { match context { - gpui::KeyBindingContextPredicate::Identifier(ident) => { + Identifier(ident) => { contexts.insert(ident); } - gpui::KeyBindingContextPredicate::Equal(ident_a, ident_b) => { + Equal(ident_a, ident_b) => { contexts.insert(ident_a); contexts.insert(ident_b); } - gpui::KeyBindingContextPredicate::NotEqual(ident_a, ident_b) => { + NotEqual(ident_a, ident_b) => { contexts.insert(ident_a); contexts.insert(ident_b); } - gpui::KeyBindingContextPredicate::Descendant(ctx_a, ctx_b) => { + Descendant(ctx_a, ctx_b) => { queue.push(*ctx_a); queue.push(*ctx_b); } - gpui::KeyBindingContextPredicate::Not(ctx) => { + Not(ctx) => { queue.push(*ctx); } - gpui::KeyBindingContextPredicate::And(ctx_a, ctx_b) => { + And(ctx_a, ctx_b) => { queue.push(*ctx_a); queue.push(*ctx_b); } - gpui::KeyBindingContextPredicate::Or(ctx_a, ctx_b) => { + Or(ctx_a, ctx_b) => { queue.push(*ctx_a); queue.push(*ctx_b); } @@ -3126,6 +3165,127 @@ fn collect_contexts_from_assets() -> Vec { contexts } +fn normalized_ctx_eq( + a: &gpui::KeyBindingContextPredicate, + b: &gpui::KeyBindingContextPredicate, +) -> bool { + use gpui::KeyBindingContextPredicate::*; + return match (a, b) { + (Identifier(_), Identifier(_)) => a == b, + (Equal(a_left, a_right), Equal(b_left, b_right)) => { + (a_left == b_left && a_right == b_right) || (a_left == b_right && a_right == b_left) + } + (NotEqual(a_left, a_right), NotEqual(b_left, b_right)) => { + (a_left == b_left && a_right == b_right) || (a_left == b_right && a_right == b_left) + } + (Descendant(a_parent, a_child), Descendant(b_parent, b_child)) => { + normalized_ctx_eq(a_parent, b_parent) && normalized_ctx_eq(a_child, b_child) + } + (Not(a_expr), Not(b_expr)) => normalized_ctx_eq(a_expr, b_expr), + // Handle double negation: !(!a) == a + (Not(a_expr), b) if matches!(a_expr.as_ref(), Not(_)) => { + let Not(a_inner) = a_expr.as_ref() else { + unreachable!(); + }; + normalized_ctx_eq(b, a_inner) + } + (a, Not(b_expr)) if matches!(b_expr.as_ref(), Not(_)) => { + let Not(b_inner) = b_expr.as_ref() else { + unreachable!(); + }; + normalized_ctx_eq(a, b_inner) + } + (And(a_left, a_right), And(b_left, b_right)) + if matches!(a_left.as_ref(), And(_, _)) + || matches!(a_right.as_ref(), And(_, _)) + || matches!(b_left.as_ref(), And(_, _)) + || matches!(b_right.as_ref(), And(_, _)) => + { + let mut a_operands = Vec::new(); + flatten_and(a, &mut a_operands); + let mut b_operands = Vec::new(); + flatten_and(b, &mut b_operands); + compare_operand_sets(&a_operands, &b_operands) + } + (And(a_left, a_right), And(b_left, b_right)) => { + (normalized_ctx_eq(a_left, b_left) && normalized_ctx_eq(a_right, b_right)) + || (normalized_ctx_eq(a_left, b_right) && normalized_ctx_eq(a_right, b_left)) + } + (Or(a_left, a_right), Or(b_left, b_right)) + if matches!(a_left.as_ref(), Or(_, _)) + || matches!(a_right.as_ref(), Or(_, _)) + || matches!(b_left.as_ref(), Or(_, _)) + || matches!(b_right.as_ref(), Or(_, _)) => + { + let mut a_operands = Vec::new(); + flatten_or(a, &mut a_operands); + let mut b_operands = Vec::new(); + flatten_or(b, &mut b_operands); + compare_operand_sets(&a_operands, &b_operands) + } + (Or(a_left, a_right), Or(b_left, b_right)) => { + (normalized_ctx_eq(a_left, b_left) && normalized_ctx_eq(a_right, b_right)) + || (normalized_ctx_eq(a_left, b_right) && normalized_ctx_eq(a_right, b_left)) + } + _ => false, + }; + + fn flatten_and<'a>( + pred: &'a gpui::KeyBindingContextPredicate, + operands: &mut Vec<&'a gpui::KeyBindingContextPredicate>, + ) { + use gpui::KeyBindingContextPredicate::*; + match pred { + And(left, right) => { + flatten_and(left, operands); + flatten_and(right, operands); + } + _ => operands.push(pred), + } + } + + fn flatten_or<'a>( + pred: &'a gpui::KeyBindingContextPredicate, + operands: &mut Vec<&'a gpui::KeyBindingContextPredicate>, + ) { + use gpui::KeyBindingContextPredicate::*; + match pred { + Or(left, right) => { + flatten_or(left, operands); + flatten_or(right, operands); + } + _ => operands.push(pred), + } + } + + fn compare_operand_sets( + a: &[&gpui::KeyBindingContextPredicate], + b: &[&gpui::KeyBindingContextPredicate], + ) -> bool { + if a.len() != b.len() { + return false; + } + + // For each operand in a, find a matching operand in b + let mut b_matched = vec![false; b.len()]; + for a_operand in a { + let mut found = false; + for (b_idx, b_operand) in b.iter().enumerate() { + if !b_matched[b_idx] && normalized_ctx_eq(a_operand, b_operand) { + b_matched[b_idx] = true; + found = true; + break; + } + } + if !found { + return false; + } + } + + true + } +} + impl SerializableItem for KeymapEditor { fn serialized_item_kind() -> &'static str { "KeymapEditor" @@ -3228,3 +3388,152 @@ mod persistence { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn normalized_ctx_cmp() { + #[track_caller] + fn cmp(a: &str, b: &str) -> bool { + let a = gpui::KeyBindingContextPredicate::parse(a) + .expect("Failed to parse keybinding context a"); + let b = gpui::KeyBindingContextPredicate::parse(b) + .expect("Failed to parse keybinding context b"); + normalized_ctx_eq(&a, &b) + } + + // Basic equality - identical expressions + assert!(cmp("a && b", "a && b")); + assert!(cmp("a || b", "a || b")); + assert!(cmp("a == b", "a == b")); + assert!(cmp("a != b", "a != b")); + assert!(cmp("a > b", "a > b")); + assert!(cmp("!a", "!a")); + + // AND operator - associative/commutative + assert!(cmp("a && b", "b && a")); + assert!(cmp("a && b && c", "c && b && a")); + assert!(cmp("a && b && c", "b && a && c")); + assert!(cmp("a && b && c && d", "d && c && b && a")); + + // OR operator - associative/commutative + assert!(cmp("a || b", "b || a")); + assert!(cmp("a || b || c", "c || b || a")); + assert!(cmp("a || b || c", "b || a || c")); + assert!(cmp("a || b || c || d", "d || c || b || a")); + + // Equality operator - associative/commutative + assert!(cmp("a == b", "b == a")); + assert!(cmp("x == y", "y == x")); + + // Inequality operator - associative/commutative + assert!(cmp("a != b", "b != a")); + assert!(cmp("x != y", "y != x")); + + // Complex nested expressions with associative operators + assert!(cmp("(a && b) || c", "c || (a && b)")); + assert!(cmp("(a && b) || c", "c || (b && a)")); + assert!(cmp("(a || b) && c", "c && (a || b)")); + assert!(cmp("(a || b) && c", "c && (b || a)")); + assert!(cmp("(a && b) || (c && d)", "(c && d) || (a && b)")); + assert!(cmp("(a && b) || (c && d)", "(d && c) || (b && a)")); + + // Multiple levels of nesting + assert!(cmp("((a && b) || c) && d", "d && ((a && b) || c)")); + assert!(cmp("((a && b) || c) && d", "d && (c || (b && a))")); + assert!(cmp("a && (b || (c && d))", "(b || (c && d)) && a")); + assert!(cmp("a && (b || (c && d))", "(b || (d && c)) && a")); + + // Negation with associative operators + assert!(cmp("!a && b", "b && !a")); + assert!(cmp("!a || b", "b || !a")); + assert!(cmp("!(a && b) || c", "c || !(a && b)")); + assert!(cmp("!(a && b) || c", "c || !(b && a)")); + + // Descendant operator (>) - NOT associative/commutative + assert!(cmp("a > b", "a > b")); + assert!(!cmp("a > b", "b > a")); + assert!(!cmp("a > b > c", "c > b > a")); + assert!(!cmp("a > b > c", "a > c > b")); + + // Mixed operators with descendant + assert!(cmp("(a > b) && c", "c && (a > b)")); + assert!(!cmp("(a > b) && c", "c && (b > a)")); + assert!(cmp("(a > b) || (c > d)", "(c > d) || (a > b)")); + assert!(!cmp("(a > b) || (c > d)", "(b > a) || (d > c)")); + + // Negative cases - different operators + assert!(!cmp("a && b", "a || b")); + assert!(!cmp("a == b", "a != b")); + assert!(!cmp("a && b", "a > b")); + assert!(!cmp("a || b", "a > b")); + assert!(!cmp("a == b", "a && b")); + assert!(!cmp("a != b", "a || b")); + + // Negative cases - different operands + assert!(!cmp("a && b", "a && c")); + assert!(!cmp("a && b", "c && d")); + assert!(!cmp("a || b", "a || c")); + assert!(!cmp("a || b", "c || d")); + assert!(!cmp("a == b", "a == c")); + assert!(!cmp("a != b", "a != c")); + assert!(!cmp("a > b", "a > c")); + assert!(!cmp("a > b", "c > b")); + + // Negative cases - with negation + assert!(!cmp("!a", "a")); + assert!(!cmp("!a && b", "a && b")); + assert!(!cmp("!(a && b)", "a && b")); + assert!(!cmp("!a || b", "a || b")); + assert!(!cmp("!(a || b)", "a || b")); + + // Negative cases - complex expressions + assert!(!cmp("(a && b) || c", "(a || b) && c")); + assert!(!cmp("a && (b || c)", "a || (b && c)")); + assert!(!cmp("(a && b) || (c && d)", "(a || b) && (c || d)")); + assert!(!cmp("a > b && c", "a && b > c")); + + // Edge cases - multiple same operands + assert!(cmp("a && a", "a && a")); + assert!(cmp("a || a", "a || a")); + assert!(cmp("a && a && b", "b && a && a")); + assert!(cmp("a || a || b", "b || a || a")); + + // Edge cases - deeply nested + assert!(cmp( + "((a && b) || (c && d)) && ((e || f) && g)", + "((e || f) && g) && ((c && d) || (a && b))" + )); + assert!(cmp( + "((a && b) || (c && d)) && ((e || f) && g)", + "(g && (f || e)) && ((d && c) || (b && a))" + )); + + // Edge cases - repeated patterns + assert!(cmp("(a && b) || (a && b)", "(b && a) || (b && a)")); + assert!(cmp("(a || b) && (a || b)", "(b || a) && (b || a)")); + + // Negative cases - subtle differences + assert!(!cmp("a && b && c", "a && b")); + assert!(!cmp("a || b || c", "a || b")); + assert!(!cmp("(a && b) || c", "a && (b || c)")); + + // a > b > c is not the same as a > c, should not be equal + assert!(!cmp("a > b > c", "a > c")); + + // Double negation with complex expressions + assert!(cmp("!(!(a && b))", "a && b")); + assert!(cmp("!(!(a || b))", "a || b")); + assert!(cmp("!(!(a > b))", "a > b")); + assert!(cmp("!(!a) && b", "a && b")); + assert!(cmp("!(!a) || b", "a || b")); + assert!(cmp("!(!(a && b)) || c", "(a && b) || c")); + assert!(cmp("!(!(a && b)) || c", "(b && a) || c")); + assert!(cmp("!(!a)", "a")); + assert!(cmp("a", "!(!a)")); + assert!(cmp("!(!(!a))", "!a")); + assert!(cmp("!(!(!(!a)))", "a")); + } +} From e1a96b68f0e3d995e57cd7ca5c7d8fd5b313944d Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 21 Aug 2025 17:37:41 -0700 Subject: [PATCH 604/693] acp: Tool name prep (#36726) Prep work for deduping tool names Release Notes: - N/A --- crates/agent2/src/agent.rs | 2 - crates/agent2/src/tests/mod.rs | 55 +++++++++---------- crates/agent2/src/tests/test_tools.rs | 30 +++++----- crates/agent2/src/thread.rs | 16 +++--- crates/agent2/src/tools.rs | 25 +++++++++ crates/agent2/src/tools/copy_path_tool.rs | 8 +-- .../agent2/src/tools/create_directory_tool.rs | 6 +- crates/agent2/src/tools/delete_path_tool.rs | 6 +- crates/agent2/src/tools/diagnostics_tool.rs | 6 +- crates/agent2/src/tools/edit_file_tool.rs | 29 ++-------- crates/agent2/src/tools/fetch_tool.rs | 6 +- crates/agent2/src/tools/find_path_tool.rs | 6 +- crates/agent2/src/tools/grep_tool.rs | 6 +- .../agent2/src/tools/list_directory_tool.rs | 6 +- crates/agent2/src/tools/move_path_tool.rs | 6 +- crates/agent2/src/tools/now_tool.rs | 6 +- crates/agent2/src/tools/open_tool.rs | 6 +- crates/agent2/src/tools/read_file_tool.rs | 6 +- crates/agent2/src/tools/terminal_tool.rs | 6 +- crates/agent2/src/tools/thinking_tool.rs | 6 +- crates/agent2/src/tools/web_search_tool.rs | 6 +- 21 files changed, 126 insertions(+), 123 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index bbc30b74bc..215f8f454b 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -857,7 +857,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection { cx.spawn(async move |cx| { log::debug!("Starting thread creation in async context"); - let action_log = cx.new(|_cx| ActionLog::new(project.clone()))?; // Create Thread let thread = agent.update( cx, @@ -878,7 +877,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection { project.clone(), agent.project_context.clone(), agent.context_server_registry.clone(), - action_log.clone(), agent.templates.clone(), default_model, cx, diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index e7e28f495e..ac7b40c64f 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1,6 +1,5 @@ use super::*; use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelList, UserMessageId}; -use action_log::ActionLog; use agent_client_protocol::{self as acp}; use agent_settings::AgentProfileId; use anyhow::Result; @@ -224,7 +223,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) { let tool_use = LanguageModelToolUse { id: "tool_1".into(), - name: EchoTool.name().into(), + name: EchoTool::name().into(), raw_input: json!({"text": "test"}).to_string(), input: json!({"text": "test"}), is_input_complete: true, @@ -237,7 +236,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) { let completion = fake_model.pending_completions().pop().unwrap(); let tool_result = LanguageModelToolResult { tool_use_id: "tool_1".into(), - tool_name: EchoTool.name().into(), + tool_name: EchoTool::name().into(), is_error: false, content: "test".into(), output: Some("test".into()), @@ -307,7 +306,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) { // Test a tool calls that's likely to complete *after* streaming stops. let events = thread .update(cx, |thread, cx| { - thread.remove_tool(&AgentTool::name(&EchoTool)); + thread.remove_tool(&EchoTool::name()); thread.add_tool(DelayTool); thread.send( UserMessageId::new(), @@ -411,7 +410,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { id: "tool_id_1".into(), - name: ToolRequiringPermission.name().into(), + name: ToolRequiringPermission::name().into(), raw_input: "{}".into(), input: json!({}), is_input_complete: true, @@ -420,7 +419,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { id: "tool_id_2".into(), - name: ToolRequiringPermission.name().into(), + name: ToolRequiringPermission::name().into(), raw_input: "{}".into(), input: json!({}), is_input_complete: true, @@ -451,14 +450,14 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { vec![ language_model::MessageContent::ToolResult(LanguageModelToolResult { tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(), - tool_name: ToolRequiringPermission.name().into(), + tool_name: ToolRequiringPermission::name().into(), is_error: false, content: "Allowed".into(), output: Some("Allowed".into()) }), language_model::MessageContent::ToolResult(LanguageModelToolResult { tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(), - tool_name: ToolRequiringPermission.name().into(), + tool_name: ToolRequiringPermission::name().into(), is_error: true, content: "Permission to run tool denied by user".into(), output: None @@ -470,7 +469,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { id: "tool_id_3".into(), - name: ToolRequiringPermission.name().into(), + name: ToolRequiringPermission::name().into(), raw_input: "{}".into(), input: json!({}), is_input_complete: true, @@ -492,7 +491,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { vec![language_model::MessageContent::ToolResult( LanguageModelToolResult { tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(), - tool_name: ToolRequiringPermission.name().into(), + tool_name: ToolRequiringPermission::name().into(), is_error: false, content: "Allowed".into(), output: Some("Allowed".into()) @@ -504,7 +503,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { id: "tool_id_4".into(), - name: ToolRequiringPermission.name().into(), + name: ToolRequiringPermission::name().into(), raw_input: "{}".into(), input: json!({}), is_input_complete: true, @@ -519,7 +518,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { vec![language_model::MessageContent::ToolResult( LanguageModelToolResult { tool_use_id: "tool_id_4".into(), - tool_name: ToolRequiringPermission.name().into(), + tool_name: ToolRequiringPermission::name().into(), is_error: false, content: "Allowed".into(), output: Some("Allowed".into()) @@ -571,7 +570,7 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) { cx.run_until_parked(); let tool_use = LanguageModelToolUse { id: "tool_id_1".into(), - name: EchoTool.name().into(), + name: EchoTool::name().into(), raw_input: "{}".into(), input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(), is_input_complete: true, @@ -584,7 +583,7 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) { let completion = fake_model.pending_completions().pop().unwrap(); let tool_result = LanguageModelToolResult { tool_use_id: "tool_id_1".into(), - tool_name: EchoTool.name().into(), + tool_name: EchoTool::name().into(), is_error: false, content: "def".into(), output: Some("def".into()), @@ -690,14 +689,14 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) { let tool_use = LanguageModelToolUse { id: "tool_id_1".into(), - name: EchoTool.name().into(), + name: EchoTool::name().into(), raw_input: "{}".into(), input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(), is_input_complete: true, }; let tool_result = LanguageModelToolResult { tool_use_id: "tool_id_1".into(), - tool_name: EchoTool.name().into(), + tool_name: EchoTool::name().into(), is_error: false, content: "def".into(), output: Some("def".into()), @@ -874,14 +873,14 @@ async fn test_profiles(cx: &mut TestAppContext) { "test-1": { "name": "Test Profile 1", "tools": { - EchoTool.name(): true, - DelayTool.name(): true, + EchoTool::name(): true, + DelayTool::name(): true, } }, "test-2": { "name": "Test Profile 2", "tools": { - InfiniteTool.name(): true, + InfiniteTool::name(): true, } } } @@ -910,7 +909,7 @@ async fn test_profiles(cx: &mut TestAppContext) { .iter() .map(|tool| tool.name.clone()) .collect(); - assert_eq!(tool_names, vec![DelayTool.name(), EchoTool.name()]); + assert_eq!(tool_names, vec![DelayTool::name(), EchoTool::name()]); fake_model.end_last_completion_stream(); // Switch to test-2 profile, and verify that it has only the infinite tool. @@ -929,7 +928,7 @@ async fn test_profiles(cx: &mut TestAppContext) { .iter() .map(|tool| tool.name.clone()) .collect(); - assert_eq!(tool_names, vec![InfiniteTool.name()]); + assert_eq!(tool_names, vec![InfiniteTool::name()]); } #[gpui::test] @@ -1552,7 +1551,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) { fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { id: "1".into(), - name: ThinkingTool.name().into(), + name: ThinkingTool::name().into(), raw_input: input.to_string(), input, is_input_complete: false, @@ -1840,11 +1839,11 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { "test-profile": { "name": "Test Profile", "tools": { - EchoTool.name(): true, - DelayTool.name(): true, - WordListTool.name(): true, - ToolRequiringPermission.name(): true, - InfiniteTool.name(): true, + EchoTool::name(): true, + DelayTool::name(): true, + WordListTool::name(): true, + ToolRequiringPermission::name(): true, + InfiniteTool::name(): true, } } } @@ -1903,13 +1902,11 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { let project_context = cx.new(|_cx| ProjectContext::default()); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let thread = cx.new(|cx| { Thread::new( project, project_context.clone(), context_server_registry, - action_log, templates, Some(model.clone()), cx, diff --git a/crates/agent2/src/tests/test_tools.rs b/crates/agent2/src/tests/test_tools.rs index cbff44cedf..27be7b6ac3 100644 --- a/crates/agent2/src/tests/test_tools.rs +++ b/crates/agent2/src/tests/test_tools.rs @@ -16,11 +16,11 @@ impl AgentTool for EchoTool { type Input = EchoToolInput; type Output = String; - fn name(&self) -> SharedString { - "echo".into() + fn name() -> &'static str { + "echo" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Other } @@ -51,8 +51,8 @@ impl AgentTool for DelayTool { type Input = DelayToolInput; type Output = String; - fn name(&self) -> SharedString { - "delay".into() + fn name() -> &'static str { + "delay" } fn initial_title(&self, input: Result) -> SharedString { @@ -63,7 +63,7 @@ impl AgentTool for DelayTool { } } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Other } @@ -92,11 +92,11 @@ impl AgentTool for ToolRequiringPermission { type Input = ToolRequiringPermissionInput; type Output = String; - fn name(&self) -> SharedString { - "tool_requiring_permission".into() + fn name() -> &'static str { + "tool_requiring_permission" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Other } @@ -127,11 +127,11 @@ impl AgentTool for InfiniteTool { type Input = InfiniteToolInput; type Output = String; - fn name(&self) -> SharedString { - "infinite".into() + fn name() -> &'static str { + "infinite" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Other } @@ -178,11 +178,11 @@ impl AgentTool for WordListTool { type Input = WordListInput; type Output = String; - fn name(&self) -> SharedString { - "word_list".into() + fn name() -> &'static str { + "word_list" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Other } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index f6ef11c20b..af18afa055 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -544,12 +544,12 @@ impl Thread { project: Entity, project_context: Entity, context_server_registry: Entity, - action_log: Entity, templates: Arc, model: Option>, cx: &mut Context, ) -> Self { let profile_id = AgentSettings::get_global(cx).default_profile.clone(); + let action_log = cx.new(|_cx| ActionLog::new(project.clone())); Self { id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()), prompt_id: PromptId::new(), @@ -959,11 +959,11 @@ impl Thread { )); self.add_tool(TerminalTool::new(self.project.clone(), cx)); self.add_tool(ThinkingTool); - self.add_tool(WebSearchTool); // TODO: Enable this only if it's a zed model. + self.add_tool(WebSearchTool); } - pub fn add_tool(&mut self, tool: impl AgentTool) { - self.tools.insert(tool.name(), tool.erase()); + pub fn add_tool(&mut self, tool: T) { + self.tools.insert(T::name().into(), tool.erase()); } pub fn remove_tool(&mut self, name: &str) -> bool { @@ -1989,7 +1989,7 @@ where type Input: for<'de> Deserialize<'de> + Serialize + JsonSchema; type Output: for<'de> Deserialize<'de> + Serialize + Into; - fn name(&self) -> SharedString; + fn name() -> &'static str; fn description(&self) -> SharedString { let schema = schemars::schema_for!(Self::Input); @@ -2001,7 +2001,7 @@ where ) } - fn kind(&self) -> acp::ToolKind; + fn kind() -> acp::ToolKind; /// The initial tool title to display. Can be updated during the tool run. fn initial_title(&self, input: Result) -> SharedString; @@ -2077,7 +2077,7 @@ where T: AgentTool, { fn name(&self) -> SharedString { - self.0.name() + T::name().into() } fn description(&self) -> SharedString { @@ -2085,7 +2085,7 @@ where } fn kind(&self) -> agent_client_protocol::ToolKind { - self.0.kind() + T::kind() } fn initial_title(&self, input: serde_json::Value) -> SharedString { diff --git a/crates/agent2/src/tools.rs b/crates/agent2/src/tools.rs index d1f2b3b1c7..bcca7eecd1 100644 --- a/crates/agent2/src/tools.rs +++ b/crates/agent2/src/tools.rs @@ -16,6 +16,29 @@ mod terminal_tool; mod thinking_tool; mod web_search_tool; +/// A list of all built in tool names, for use in deduplicating MCP tool names +pub fn default_tool_names() -> impl Iterator { + [ + CopyPathTool::name(), + CreateDirectoryTool::name(), + DeletePathTool::name(), + DiagnosticsTool::name(), + EditFileTool::name(), + FetchTool::name(), + FindPathTool::name(), + GrepTool::name(), + ListDirectoryTool::name(), + MovePathTool::name(), + NowTool::name(), + OpenTool::name(), + ReadFileTool::name(), + TerminalTool::name(), + ThinkingTool::name(), + WebSearchTool::name(), + ] + .into_iter() +} + pub use context_server_registry::*; pub use copy_path_tool::*; pub use create_directory_tool::*; @@ -33,3 +56,5 @@ pub use read_file_tool::*; pub use terminal_tool::*; pub use thinking_tool::*; pub use web_search_tool::*; + +use crate::AgentTool; diff --git a/crates/agent2/src/tools/copy_path_tool.rs b/crates/agent2/src/tools/copy_path_tool.rs index 4b40a9842f..819a6ff209 100644 --- a/crates/agent2/src/tools/copy_path_tool.rs +++ b/crates/agent2/src/tools/copy_path_tool.rs @@ -1,7 +1,7 @@ use crate::{AgentTool, ToolCallEventStream}; use agent_client_protocol::ToolKind; use anyhow::{Context as _, Result, anyhow}; -use gpui::{App, AppContext, Entity, SharedString, Task}; +use gpui::{App, AppContext, Entity, Task}; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -50,11 +50,11 @@ impl AgentTool for CopyPathTool { type Input = CopyPathToolInput; type Output = String; - fn name(&self) -> SharedString { - "copy_path".into() + fn name() -> &'static str { + "copy_path" } - fn kind(&self) -> ToolKind { + fn kind() -> ToolKind { ToolKind::Move } diff --git a/crates/agent2/src/tools/create_directory_tool.rs b/crates/agent2/src/tools/create_directory_tool.rs index 7720eb3595..652363d5fa 100644 --- a/crates/agent2/src/tools/create_directory_tool.rs +++ b/crates/agent2/src/tools/create_directory_tool.rs @@ -41,11 +41,11 @@ impl AgentTool for CreateDirectoryTool { type Input = CreateDirectoryToolInput; type Output = String; - fn name(&self) -> SharedString { - "create_directory".into() + fn name() -> &'static str { + "create_directory" } - fn kind(&self) -> ToolKind { + fn kind() -> ToolKind { ToolKind::Read } diff --git a/crates/agent2/src/tools/delete_path_tool.rs b/crates/agent2/src/tools/delete_path_tool.rs index c281f1b5b6..0f9641127f 100644 --- a/crates/agent2/src/tools/delete_path_tool.rs +++ b/crates/agent2/src/tools/delete_path_tool.rs @@ -44,11 +44,11 @@ impl AgentTool for DeletePathTool { type Input = DeletePathToolInput; type Output = String; - fn name(&self) -> SharedString { - "delete_path".into() + fn name() -> &'static str { + "delete_path" } - fn kind(&self) -> ToolKind { + fn kind() -> ToolKind { ToolKind::Delete } diff --git a/crates/agent2/src/tools/diagnostics_tool.rs b/crates/agent2/src/tools/diagnostics_tool.rs index 6ba8b7b377..558bb918ce 100644 --- a/crates/agent2/src/tools/diagnostics_tool.rs +++ b/crates/agent2/src/tools/diagnostics_tool.rs @@ -63,11 +63,11 @@ impl AgentTool for DiagnosticsTool { type Input = DiagnosticsToolInput; type Output = String; - fn name(&self) -> SharedString { - "diagnostics".into() + fn name() -> &'static str { + "diagnostics" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Read } diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index f89cace9a8..5a68d0c70a 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -186,11 +186,11 @@ impl AgentTool for EditFileTool { type Input = EditFileToolInput; type Output = EditFileToolOutput; - fn name(&self) -> SharedString { - "edit_file".into() + fn name() -> &'static str { + "edit_file" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Edit } @@ -517,7 +517,6 @@ fn resolve_path( mod tests { use super::*; use crate::{ContextServerRegistry, Templates}; - use action_log::ActionLog; use client::TelemetrySettings; use fs::Fs; use gpui::{TestAppContext, UpdateGlobal}; @@ -535,7 +534,6 @@ mod tests { fs.insert_tree("/root", json!({})).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); @@ -544,7 +542,6 @@ mod tests { project, cx.new(|_cx| ProjectContext::default()), context_server_registry, - action_log, Templates::new(), Some(model), cx, @@ -735,7 +732,6 @@ mod tests { } }); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); @@ -744,7 +740,6 @@ mod tests { project, cx.new(|_cx| ProjectContext::default()), context_server_registry, - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -801,7 +796,9 @@ mod tests { "Code should be formatted when format_on_save is enabled" ); - let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count()); + let stale_buffer_count = thread + .read_with(cx, |thread, _cx| thread.action_log.clone()) + .read_with(cx, |log, cx| log.stale_buffers(cx).count()); assert_eq!( stale_buffer_count, 0, @@ -879,14 +876,12 @@ mod tests { let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let model = Arc::new(FakeLanguageModel::default()); let thread = cx.new(|cx| { Thread::new( project, cx.new(|_cx| ProjectContext::default()), context_server_registry, - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1008,14 +1003,12 @@ mod tests { let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let model = Arc::new(FakeLanguageModel::default()); let thread = cx.new(|cx| { Thread::new( project, cx.new(|_cx| ProjectContext::default()), context_server_registry, - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1146,14 +1139,12 @@ mod tests { let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let model = Arc::new(FakeLanguageModel::default()); let thread = cx.new(|cx| { Thread::new( project, cx.new(|_cx| ProjectContext::default()), context_server_registry, - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1254,7 +1245,6 @@ mod tests { ) .await; let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); @@ -1263,7 +1253,6 @@ mod tests { project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry.clone(), - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1336,7 +1325,6 @@ mod tests { .await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); @@ -1345,7 +1333,6 @@ mod tests { project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry.clone(), - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1421,7 +1408,6 @@ mod tests { .await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); @@ -1430,7 +1416,6 @@ mod tests { project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry.clone(), - action_log.clone(), Templates::new(), Some(model.clone()), cx, @@ -1503,7 +1488,6 @@ mod tests { let fs = project::FakeFs::new(cx.executor()); let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); let model = Arc::new(FakeLanguageModel::default()); @@ -1512,7 +1496,6 @@ mod tests { project.clone(), cx.new(|_cx| ProjectContext::default()), context_server_registry, - action_log.clone(), Templates::new(), Some(model.clone()), cx, diff --git a/crates/agent2/src/tools/fetch_tool.rs b/crates/agent2/src/tools/fetch_tool.rs index ae26c5fe19..0313c4e4c2 100644 --- a/crates/agent2/src/tools/fetch_tool.rs +++ b/crates/agent2/src/tools/fetch_tool.rs @@ -118,11 +118,11 @@ impl AgentTool for FetchTool { type Input = FetchToolInput; type Output = String; - fn name(&self) -> SharedString { - "fetch".into() + fn name() -> &'static str { + "fetch" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Fetch } diff --git a/crates/agent2/src/tools/find_path_tool.rs b/crates/agent2/src/tools/find_path_tool.rs index 9e11ca6a37..5b35c40f85 100644 --- a/crates/agent2/src/tools/find_path_tool.rs +++ b/crates/agent2/src/tools/find_path_tool.rs @@ -85,11 +85,11 @@ impl AgentTool for FindPathTool { type Input = FindPathToolInput; type Output = FindPathToolOutput; - fn name(&self) -> SharedString { - "find_path".into() + fn name() -> &'static str { + "find_path" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Search } diff --git a/crates/agent2/src/tools/grep_tool.rs b/crates/agent2/src/tools/grep_tool.rs index 955dae7235..b24e773903 100644 --- a/crates/agent2/src/tools/grep_tool.rs +++ b/crates/agent2/src/tools/grep_tool.rs @@ -67,11 +67,11 @@ impl AgentTool for GrepTool { type Input = GrepToolInput; type Output = String; - fn name(&self) -> SharedString { - "grep".into() + fn name() -> &'static str { + "grep" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Search } diff --git a/crates/agent2/src/tools/list_directory_tool.rs b/crates/agent2/src/tools/list_directory_tool.rs index 31575a92e4..e6fa8d7431 100644 --- a/crates/agent2/src/tools/list_directory_tool.rs +++ b/crates/agent2/src/tools/list_directory_tool.rs @@ -51,11 +51,11 @@ impl AgentTool for ListDirectoryTool { type Input = ListDirectoryToolInput; type Output = String; - fn name(&self) -> SharedString { - "list_directory".into() + fn name() -> &'static str { + "list_directory" } - fn kind(&self) -> ToolKind { + fn kind() -> ToolKind { ToolKind::Read } diff --git a/crates/agent2/src/tools/move_path_tool.rs b/crates/agent2/src/tools/move_path_tool.rs index 2a173a4404..d9fb60651b 100644 --- a/crates/agent2/src/tools/move_path_tool.rs +++ b/crates/agent2/src/tools/move_path_tool.rs @@ -52,11 +52,11 @@ impl AgentTool for MovePathTool { type Input = MovePathToolInput; type Output = String; - fn name(&self) -> SharedString { - "move_path".into() + fn name() -> &'static str { + "move_path" } - fn kind(&self) -> ToolKind { + fn kind() -> ToolKind { ToolKind::Move } diff --git a/crates/agent2/src/tools/now_tool.rs b/crates/agent2/src/tools/now_tool.rs index a72ede26fe..9467e7db68 100644 --- a/crates/agent2/src/tools/now_tool.rs +++ b/crates/agent2/src/tools/now_tool.rs @@ -32,11 +32,11 @@ impl AgentTool for NowTool { type Input = NowToolInput; type Output = String; - fn name(&self) -> SharedString { - "now".into() + fn name() -> &'static str { + "now" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Other } diff --git a/crates/agent2/src/tools/open_tool.rs b/crates/agent2/src/tools/open_tool.rs index c20369c2d8..df7b04c787 100644 --- a/crates/agent2/src/tools/open_tool.rs +++ b/crates/agent2/src/tools/open_tool.rs @@ -37,11 +37,11 @@ impl AgentTool for OpenTool { type Input = OpenToolInput; type Output = String; - fn name(&self) -> SharedString { - "open".into() + fn name() -> &'static str { + "open" } - fn kind(&self) -> ToolKind { + fn kind() -> ToolKind { ToolKind::Execute } diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index 11a57506fb..903e1582ac 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -59,11 +59,11 @@ impl AgentTool for ReadFileTool { type Input = ReadFileToolInput; type Output = LanguageModelToolResultContent; - fn name(&self) -> SharedString { - "read_file".into() + fn name() -> &'static str { + "read_file" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Read } diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs index 3d4faf2e03..f41b909d0b 100644 --- a/crates/agent2/src/tools/terminal_tool.rs +++ b/crates/agent2/src/tools/terminal_tool.rs @@ -63,11 +63,11 @@ impl AgentTool for TerminalTool { type Input = TerminalToolInput; type Output = String; - fn name(&self) -> SharedString { - "terminal".into() + fn name() -> &'static str { + "terminal" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Execute } diff --git a/crates/agent2/src/tools/thinking_tool.rs b/crates/agent2/src/tools/thinking_tool.rs index c5e9451162..61fb9eb0d6 100644 --- a/crates/agent2/src/tools/thinking_tool.rs +++ b/crates/agent2/src/tools/thinking_tool.rs @@ -21,11 +21,11 @@ impl AgentTool for ThinkingTool { type Input = ThinkingToolInput; type Output = String; - fn name(&self) -> SharedString { - "thinking".into() + fn name() -> &'static str { + "thinking" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Think } diff --git a/crates/agent2/src/tools/web_search_tool.rs b/crates/agent2/src/tools/web_search_tool.rs index ffcd4ad3be..d7a34bec29 100644 --- a/crates/agent2/src/tools/web_search_tool.rs +++ b/crates/agent2/src/tools/web_search_tool.rs @@ -40,11 +40,11 @@ impl AgentTool for WebSearchTool { type Input = WebSearchToolInput; type Output = WebSearchToolOutput; - fn name(&self) -> SharedString { - "web_search".into() + fn name() -> &'static str { + "web_search" } - fn kind(&self) -> acp::ToolKind { + fn kind() -> acp::ToolKind { acp::ToolKind::Fetch } From f5fd4ac6701239ec7620651bb2185fc3b1774bfa Mon Sep 17 00:00:00 2001 From: Kaem <46230985+kaem-e@users.noreply.github.com> Date: Fri, 22 Aug 2025 08:02:47 +0500 Subject: [PATCH 605/693] vim: Implement partial increment/decrement for visual selection (#36553) This change adds the ability to increment / decrement numbers that are part of a visual selection. Previously Zed would resolve to the entire number under visual selection for increment as oppposed to only incrementing the part of the number that is selected Release Notes: - vim: Fixed increment/decrement in visual mode --- crates/vim/src/normal/increment.rs | 111 +++++++++++++++++- .../test_increment_visual_partial_number.json | 20 ++++ 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 crates/vim/test_data/test_increment_visual_partial_number.json diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index 1d2a4e9b61..34ac4aab1f 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -70,8 +70,19 @@ impl Vim { } else { Point::new(row, 0) }; + let end = if row == selection.end.row { + selection.end + } else { + Point::new(row, snapshot.line_len(multi_buffer::MultiBufferRow(row))) + }; - if let Some((range, num, radix)) = find_number(&snapshot, start) { + let number_result = if !selection.is_empty() { + find_number_in_range(&snapshot, start, end) + } else { + find_number(&snapshot, start) + }; + + if let Some((range, num, radix)) = number_result { let replace = match radix { 10 => increment_decimal_string(&num, delta), 16 => increment_hex_string(&num, delta), @@ -189,6 +200,90 @@ fn increment_binary_string(num: &str, delta: i64) -> String { format!("{:0width$b}", result, width = num.len()) } +fn find_number_in_range( + snapshot: &MultiBufferSnapshot, + start: Point, + end: Point, +) -> Option<(Range, String, u32)> { + let start_offset = start.to_offset(snapshot); + let end_offset = end.to_offset(snapshot); + + let mut offset = start_offset; + + // Backward scan to find the start of the number, but stop at start_offset + for ch in snapshot.reversed_chars_at(offset) { + if ch.is_ascii_hexdigit() || ch == '-' || ch == 'b' || ch == 'x' { + if offset == 0 { + break; + } + offset -= ch.len_utf8(); + if offset < start_offset { + offset = start_offset; + break; + } + } else { + break; + } + } + + let mut begin = None; + let mut end_num = None; + let mut num = String::new(); + let mut radix = 10; + + let mut chars = snapshot.chars_at(offset).peekable(); + + while let Some(ch) = chars.next() { + if offset >= end_offset { + break; // stop at end of selection + } + + if num == "0" && ch == 'b' && chars.peek().is_some() && chars.peek().unwrap().is_digit(2) { + radix = 2; + begin = None; + num = String::new(); + } else if num == "0" + && ch == 'x' + && chars.peek().is_some() + && chars.peek().unwrap().is_ascii_hexdigit() + { + radix = 16; + begin = None; + num = String::new(); + } + + if ch.is_digit(radix) + || (begin.is_none() + && ch == '-' + && chars.peek().is_some() + && chars.peek().unwrap().is_digit(radix)) + { + if begin.is_none() { + begin = Some(offset); + } + num.push(ch); + } else if begin.is_some() { + end_num = Some(offset); + break; + } else if ch == '\n' { + break; + } + + offset += ch.len_utf8(); + } + + if let Some(begin) = begin { + let end_num = end_num.unwrap_or(offset); + Some(( + begin.to_point(snapshot)..end_num.to_point(snapshot), + num, + radix, + )) + } else { + None + } +} + fn find_number( snapshot: &MultiBufferSnapshot, start: Point, @@ -764,4 +859,18 @@ mod test { cx.simulate_keystrokes("v b ctrl-a"); cx.assert_state("let enabled = ˇOff;", Mode::Normal); } + + #[gpui::test] + async fn test_increment_visual_partial_number(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇ123").await; + cx.simulate_shared_keystrokes("v l ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {"ˇ133"}); + cx.simulate_shared_keystrokes("l v l ctrl-a").await; + cx.shared_state().await.assert_eq(indoc! {"1ˇ34"}); + cx.simulate_shared_keystrokes("shift-v y p p ctrl-v k k l ctrl-a") + .await; + cx.shared_state().await.assert_eq(indoc! {"ˇ144\n144\n144"}); + } } diff --git a/crates/vim/test_data/test_increment_visual_partial_number.json b/crates/vim/test_data/test_increment_visual_partial_number.json new file mode 100644 index 0000000000..ebb4eece78 --- /dev/null +++ b/crates/vim/test_data/test_increment_visual_partial_number.json @@ -0,0 +1,20 @@ +{"Put":{"state":"ˇ123"}} +{"Key":"v"} +{"Key":"l"} +{"Key":"ctrl-a"} +{"Get":{"state":"ˇ133","mode":"Normal"}} +{"Key":"l"} +{"Key":"v"} +{"Key":"l"} +{"Key":"ctrl-a"} +{"Get":{"state":"1ˇ34","mode":"Normal"}} +{"Key":"shift-v"} +{"Key":"y"} +{"Key":"p"} +{"Key":"p"} +{"Key":"ctrl-v"} +{"Key":"k"} +{"Key":"k"} +{"Key":"l"} +{"Key":"ctrl-a"} +{"Get":{"state":"ˇ144\n144\n144","mode":"Normal"}} From 852439452cb5816e4afa5bd42e2b98a2edae0bec Mon Sep 17 00:00:00 2001 From: Adam Mulvany Date: Fri, 22 Aug 2025 13:20:22 +1000 Subject: [PATCH 606/693] vim: Fix cursor jumping past empty lines with inlay hints in visual mode (#35757) **Summary** Fixes #29134 - Visual mode cursor incorrectly jumps past empty lines that contain inlay hints (type hints). **Problem** When in VIM visual mode, pressing j to move down from a longer line to an empty line that contains an inlay hint would cause the cursor to skip the empty line entirely and jump to the next line. This only occurred when moving down (not up) and only in visual mode. **Root Cause** The issue was introduced by commit f9ee28db5e which added bias-based navigation for handling multi-line inlay hints. When using Bias::Right while moving down, the clipping logic would place the cursor past the inlay hint, causing it to jump to the next line. **Solution** Added logic in up_down_buffer_rows to detect when clipping would place the cursor within an inlay hint position. When detected, it uses the buffer column position instead of the display column to avoid jumping past the hint. **Testing** - Added comprehensive test case test_visual_mode_with_inlay_hints_on_empty_line that reproduces the exact scenario - Manually verified the fix with the reproduction case from the issue - All 356 tests pass with `cargo test -p vim` **Release Notes:** - Fixed VIM visual mode cursor jumping past empty lines with type hints when navigating down --- crates/vim/src/motion.rs | 96 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index a2f165e9fe..a54d3caa60 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -1610,10 +1610,20 @@ fn up_down_buffer_rows( map.line_len(begin_folded_line.row()) }; - ( - map.clip_point(DisplayPoint::new(begin_folded_line.row(), new_col), bias), - goal, - ) + let point = DisplayPoint::new(begin_folded_line.row(), new_col); + let mut clipped_point = map.clip_point(point, bias); + + // When navigating vertically in vim mode with inlay hints present, + // we need to handle the case where clipping moves us to a different row. + // This can happen when moving down (Bias::Right) and hitting an inlay hint. + // Re-clip with opposite bias to stay on the intended line. + // + // See: https://github.com/zed-industries/zed/issues/29134 + if clipped_point.row() > point.row() { + clipped_point = map.clip_point(point, Bias::Left); + } + + (clipped_point, goal) } fn down_display( @@ -3842,6 +3852,84 @@ mod test { ); } + #[gpui::test] + async fn test_visual_mode_with_inlay_hints_on_empty_line(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // Test the exact scenario from issue #29134 + cx.set_state( + indoc! {" + fn main() { + let this_is_a_long_name = Vec::::new(); + let new_oneˇ = this_is_a_long_name + .iter() + .map(|i| i + 1) + .map(|i| i * 2) + .collect::>(); + } + "}, + Mode::Normal, + ); + + // Add type hint inlay on the empty line (line 3, after "this_is_a_long_name") + cx.update_editor(|editor, _window, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + // The empty line is at line 3 (0-indexed) + let line_start = snapshot.anchor_after(Point::new(3, 0)); + let inlay_text = ": Vec"; + let inlay = Inlay::edit_prediction(1, line_start, inlay_text); + editor.splice_inlays(&[], vec![inlay], cx); + }); + + // Enter visual mode + cx.simulate_keystrokes("v"); + cx.assert_state( + indoc! {" + fn main() { + let this_is_a_long_name = Vec::::new(); + let new_one« ˇ»= this_is_a_long_name + .iter() + .map(|i| i + 1) + .map(|i| i * 2) + .collect::>(); + } + "}, + Mode::Visual, + ); + + // Move down - should go to the beginning of line 4, not skip to line 5 + cx.simulate_keystrokes("j"); + cx.assert_state( + indoc! {" + fn main() { + let this_is_a_long_name = Vec::::new(); + let new_one« = this_is_a_long_name + ˇ» .iter() + .map(|i| i + 1) + .map(|i| i * 2) + .collect::>(); + } + "}, + Mode::Visual, + ); + + // Test with multiple movements + cx.set_state("let aˇ = 1;\nlet b = 2;\n\nlet c = 3;", Mode::Normal); + + // Add type hint on the empty line + cx.update_editor(|editor, _window, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let empty_line_start = snapshot.anchor_after(Point::new(2, 0)); + let inlay_text = ": i32"; + let inlay = Inlay::edit_prediction(2, empty_line_start, inlay_text); + editor.splice_inlays(&[], vec![inlay], cx); + }); + + // Enter visual mode and move down twice + cx.simulate_keystrokes("v j j"); + cx.assert_state("let a« = 1;\nlet b = 2;\n\nˇ»let c = 3;", Mode::Visual); + } + #[gpui::test] async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; From e15856a37f5668433cbfb61a1e7950cf27ec3793 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Fri, 22 Aug 2025 10:17:37 +0530 Subject: [PATCH 607/693] Move APCA contrast from terminal_view to ui utils (#36731) In prep for using this in the editor search/select highlighting. Release Notes: - N/A --- crates/terminal_view/src/terminal_element.rs | 25 ++++++++++--------- crates/terminal_view/src/terminal_view.rs | 1 - crates/ui/src/utils.rs | 2 ++ .../src/utils/apca_contrast.rs} | 0 4 files changed, 15 insertions(+), 13 deletions(-) rename crates/{terminal_view/src/color_contrast.rs => ui/src/utils/apca_contrast.rs} (100%) diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index c2fbeb7ee6..fe3301fb89 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1,4 +1,3 @@ -use crate::color_contrast; use editor::{CursorLayout, HighlightedRange, HighlightedRangeLine}; use gpui::{ AbsoluteLength, AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, DispatchPhase, @@ -27,6 +26,7 @@ use terminal::{ terminal_settings::TerminalSettings, }; use theme::{ActiveTheme, Theme, ThemeSettings}; +use ui::utils::ensure_minimum_contrast; use ui::{ParentElement, Tooltip}; use util::ResultExt; use workspace::Workspace; @@ -534,7 +534,7 @@ impl TerminalElement { // Only apply contrast adjustment to non-decorative characters if !Self::is_decorative_character(indexed.c) { - fg = color_contrast::ensure_minimum_contrast(fg, bg, minimum_contrast); + fg = ensure_minimum_contrast(fg, bg, minimum_contrast); } // Ghostty uses (175/255) as the multiplier (~0.69), Alacritty uses 0.66, Kitty @@ -1598,6 +1598,7 @@ pub fn convert_color(fg: &terminal::alacritty_terminal::vte::ansi::Color, theme: mod tests { use super::*; use gpui::{AbsoluteLength, Hsla, font}; + use ui::utils::apca_contrast; #[test] fn test_is_decorative_character() { @@ -1713,7 +1714,7 @@ mod tests { }; // Should have poor contrast - let actual_contrast = color_contrast::apca_contrast(white_fg, light_gray_bg).abs(); + let actual_contrast = apca_contrast(white_fg, light_gray_bg).abs(); assert!( actual_contrast < 30.0, "White on light gray should have poor APCA contrast: {}", @@ -1721,12 +1722,12 @@ mod tests { ); // After adjustment with minimum APCA contrast of 45, should be darker - let adjusted = color_contrast::ensure_minimum_contrast(white_fg, light_gray_bg, 45.0); + let adjusted = ensure_minimum_contrast(white_fg, light_gray_bg, 45.0); assert!( adjusted.l < white_fg.l, "Adjusted color should be darker than original" ); - let adjusted_contrast = color_contrast::apca_contrast(adjusted, light_gray_bg).abs(); + let adjusted_contrast = apca_contrast(adjusted, light_gray_bg).abs(); assert!(adjusted_contrast >= 45.0, "Should meet minimum contrast"); // Test case 2: Dark colors (poor contrast) @@ -1744,7 +1745,7 @@ mod tests { }; // Should have poor contrast - let actual_contrast = color_contrast::apca_contrast(black_fg, dark_gray_bg).abs(); + let actual_contrast = apca_contrast(black_fg, dark_gray_bg).abs(); assert!( actual_contrast < 30.0, "Black on dark gray should have poor APCA contrast: {}", @@ -1752,16 +1753,16 @@ mod tests { ); // After adjustment with minimum APCA contrast of 45, should be lighter - let adjusted = color_contrast::ensure_minimum_contrast(black_fg, dark_gray_bg, 45.0); + let adjusted = ensure_minimum_contrast(black_fg, dark_gray_bg, 45.0); assert!( adjusted.l > black_fg.l, "Adjusted color should be lighter than original" ); - let adjusted_contrast = color_contrast::apca_contrast(adjusted, dark_gray_bg).abs(); + let adjusted_contrast = apca_contrast(adjusted, dark_gray_bg).abs(); assert!(adjusted_contrast >= 45.0, "Should meet minimum contrast"); // Test case 3: Already good contrast - let good_contrast = color_contrast::ensure_minimum_contrast(black_fg, white_fg, 45.0); + let good_contrast = ensure_minimum_contrast(black_fg, white_fg, 45.0); assert_eq!( good_contrast, black_fg, "Good contrast should not be adjusted" @@ -1788,11 +1789,11 @@ mod tests { }; // With minimum contrast of 0.0, no adjustment should happen - let no_adjust = color_contrast::ensure_minimum_contrast(white_fg, white_bg, 0.0); + let no_adjust = ensure_minimum_contrast(white_fg, white_bg, 0.0); assert_eq!(no_adjust, white_fg, "No adjustment with min_contrast 0.0"); // With minimum APCA contrast of 15, it should adjust to a darker color - let adjusted = color_contrast::ensure_minimum_contrast(white_fg, white_bg, 15.0); + let adjusted = ensure_minimum_contrast(white_fg, white_bg, 15.0); assert!( adjusted.l < white_fg.l, "White on white should become darker, got l={}", @@ -1800,7 +1801,7 @@ mod tests { ); // Verify the contrast is now acceptable - let new_contrast = color_contrast::apca_contrast(adjusted, white_bg).abs(); + let new_contrast = apca_contrast(adjusted, white_bg).abs(); assert!( new_contrast >= 15.0, "Adjusted APCA contrast {} should be >= 15.0", diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index e2f9ba818d..9aa855acb7 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1,4 +1,3 @@ -mod color_contrast; mod persistence; pub mod terminal_element; pub mod terminal_panel; diff --git a/crates/ui/src/utils.rs b/crates/ui/src/utils.rs index 26a59001f6..cd7d8eb497 100644 --- a/crates/ui/src/utils.rs +++ b/crates/ui/src/utils.rs @@ -3,12 +3,14 @@ use gpui::App; use theme::ActiveTheme; +mod apca_contrast; mod color_contrast; mod corner_solver; mod format_distance; mod search_input; mod with_rem_size; +pub use apca_contrast::*; pub use color_contrast::*; pub use corner_solver::{CornerSolver, inner_corner_radius}; pub use format_distance::*; diff --git a/crates/terminal_view/src/color_contrast.rs b/crates/ui/src/utils/apca_contrast.rs similarity index 100% rename from crates/terminal_view/src/color_contrast.rs rename to crates/ui/src/utils/apca_contrast.rs From b349a8f34c9bbd2297633aae820bf8432b4f9c63 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 22 Aug 2025 01:12:12 -0400 Subject: [PATCH 608/693] ai: Auto select user model when there's no default (#36722) This PR identifies automatic configuration options that users can select from the agent panel. If no default provider is set in their settings, the PR defaults to the first recommended option. Additionally, it updates the selected provider for a thread when a user changes the default provider through the settings file, if the thread hasn't had any queries yet. Release Notes: - agent: automatically select a language model provider if there's no user set provider. --------- Co-authored-by: Michael Sloan --- crates/agent/src/thread.rs | 17 ++- crates/agent2/src/agent.rs | 4 +- crates/agent2/src/tests/mod.rs | 4 +- .../agent_ui/src/language_model_selector.rs | 55 +-------- crates/git_ui/src/git_panel.rs | 2 +- crates/language_model/src/registry.rs | 114 ++++++++++-------- crates/language_models/Cargo.toml | 1 + crates/language_models/src/language_models.rs | 103 +++++++++++++++- crates/language_models/src/provider/cloud.rs | 6 +- 9 files changed, 184 insertions(+), 122 deletions(-) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 7b70fde56a..899e360ab0 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -664,7 +664,7 @@ impl Thread { } pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option { - if self.configured_model.is_none() { + if self.configured_model.is_none() || self.messages.is_empty() { self.configured_model = LanguageModelRegistry::read_global(cx).default_model(); } self.configured_model.clone() @@ -2097,7 +2097,7 @@ impl Thread { } pub fn summarize(&mut self, cx: &mut Context) { - let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else { + let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else { println!("No thread summary model"); return; }; @@ -2416,7 +2416,7 @@ impl Thread { } let Some(ConfiguredModel { model, provider }) = - LanguageModelRegistry::read_global(cx).thread_summary_model() + LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else { return; }; @@ -5410,13 +5410,10 @@ fn main() {{ }), cx, ); - registry.set_thread_summary_model( - Some(ConfiguredModel { - provider, - model: model.clone(), - }), - cx, - ); + registry.set_thread_summary_model(Some(ConfiguredModel { + provider, + model: model.clone(), + })); }) }); diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 215f8f454b..3502cf0ba9 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -228,7 +228,7 @@ impl NativeAgent { ) -> Entity { let connection = Rc::new(NativeAgentConnection(cx.entity())); let registry = LanguageModelRegistry::read_global(cx); - let summarization_model = registry.thread_summary_model().map(|c| c.model); + let summarization_model = registry.thread_summary_model(cx).map(|c| c.model); thread_handle.update(cx, |thread, cx| { thread.set_summarization_model(summarization_model, cx); @@ -521,7 +521,7 @@ impl NativeAgent { let registry = LanguageModelRegistry::read_global(cx); let default_model = registry.default_model().map(|m| m.model); - let summarization_model = registry.thread_summary_model().map(|m| m.model); + let summarization_model = registry.thread_summary_model(cx).map(|m| m.model); for session in self.sessions.values_mut() { session.thread.update(cx, |thread, cx| { diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index ac7b40c64f..09048488c8 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1414,11 +1414,11 @@ async fn test_agent_connection(cx: &mut TestAppContext) { let clock = Arc::new(clock::FakeSystemClock::new()); let client = Client::new(clock, http_client, cx); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); + Project::init_settings(cx); + agent_settings::init(cx); language_model::init(client.clone(), cx); language_models::init(user_store, client.clone(), cx); - Project::init_settings(cx); LanguageModelRegistry::test(cx); - agent_settings::init(cx); }); cx.executor().forbid_parking(); diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index 3633e533da..aceca79dbf 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -6,8 +6,7 @@ use feature_flags::ZedProFeatureFlag; use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task}; use language_model::{ - AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId, - LanguageModelRegistry, + ConfiguredModel, LanguageModel, LanguageModelProviderId, LanguageModelRegistry, }; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; @@ -77,7 +76,6 @@ pub struct LanguageModelPickerDelegate { all_models: Arc, filtered_entries: Vec, selected_index: usize, - _authenticate_all_providers_task: Task<()>, _subscriptions: Vec, } @@ -98,7 +96,6 @@ impl LanguageModelPickerDelegate { selected_index: Self::get_active_model_index(&entries, get_active_model(cx)), filtered_entries: entries, get_active_model: Arc::new(get_active_model), - _authenticate_all_providers_task: Self::authenticate_all_providers(cx), _subscriptions: vec![cx.subscribe_in( &LanguageModelRegistry::global(cx), window, @@ -142,56 +139,6 @@ impl LanguageModelPickerDelegate { .unwrap_or(0) } - /// Authenticates all providers in the [`LanguageModelRegistry`]. - /// - /// We do this so that we can populate the language selector with all of the - /// models from the configured providers. - fn authenticate_all_providers(cx: &mut App) -> Task<()> { - let authenticate_all_providers = LanguageModelRegistry::global(cx) - .read(cx) - .providers() - .iter() - .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx))) - .collect::>(); - - cx.spawn(async move |_cx| { - for (provider_id, provider_name, authenticate_task) in authenticate_all_providers { - if let Err(err) = authenticate_task.await { - if matches!(err, AuthenticateError::CredentialsNotFound) { - // Since we're authenticating these providers in the - // background for the purposes of populating the - // language selector, we don't care about providers - // where the credentials are not found. - } else { - // Some providers have noisy failure states that we - // don't want to spam the logs with every time the - // language model selector is initialized. - // - // Ideally these should have more clear failure modes - // that we know are safe to ignore here, like what we do - // with `CredentialsNotFound` above. - match provider_id.0.as_ref() { - "lmstudio" | "ollama" => { - // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated". - // - // These fail noisily, so we don't log them. - } - "copilot_chat" => { - // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors. - } - _ => { - log::error!( - "Failed to authenticate provider: {}: {err}", - provider_name.0 - ); - } - } - } - } - } - }) - } - pub fn active_model(&self, cx: &App) -> Option { (self.get_active_model)(cx) } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 4ecb4a8829..958a609a09 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -4466,7 +4466,7 @@ fn current_language_model(cx: &Context<'_, GitPanel>) -> Option, - default_fast_model: Option, + /// This model is automatically configured by a user's environment after + /// authenticating all providers. It's only used when default_model is not available. + environment_fallback_model: Option, inline_assistant_model: Option, commit_message_model: Option, thread_summary_model: Option, @@ -104,9 +105,6 @@ impl ConfiguredModel { pub enum Event { DefaultModelChanged, - InlineAssistantModelChanged, - CommitMessageModelChanged, - ThreadSummaryModelChanged, ProviderStateChanged(LanguageModelProviderId), AddedProvider(LanguageModelProviderId), RemovedProvider(LanguageModelProviderId), @@ -238,7 +236,7 @@ impl LanguageModelRegistry { cx: &mut Context, ) { let configured_model = model.and_then(|model| self.select_model(model, cx)); - self.set_inline_assistant_model(configured_model, cx); + self.set_inline_assistant_model(configured_model); } pub fn select_commit_message_model( @@ -247,7 +245,7 @@ impl LanguageModelRegistry { cx: &mut Context, ) { let configured_model = model.and_then(|model| self.select_model(model, cx)); - self.set_commit_message_model(configured_model, cx); + self.set_commit_message_model(configured_model); } pub fn select_thread_summary_model( @@ -256,7 +254,7 @@ impl LanguageModelRegistry { cx: &mut Context, ) { let configured_model = model.and_then(|model| self.select_model(model, cx)); - self.set_thread_summary_model(configured_model, cx); + self.set_thread_summary_model(configured_model); } /// Selects and sets the inline alternatives for language models based on @@ -290,68 +288,60 @@ impl LanguageModelRegistry { } pub fn set_default_model(&mut self, model: Option, cx: &mut Context) { - match (self.default_model.as_ref(), model.as_ref()) { + match (self.default_model(), model.as_ref()) { (Some(old), Some(new)) if old.is_same_as(new) => {} (None, None) => {} _ => cx.emit(Event::DefaultModelChanged), } - self.default_fast_model = maybe!({ - let provider = &model.as_ref()?.provider; - let fast_model = provider.default_fast_model(cx)?; - Some(ConfiguredModel { - provider: provider.clone(), - model: fast_model, - }) - }); self.default_model = model; } - pub fn set_inline_assistant_model( + pub fn set_environment_fallback_model( &mut self, model: Option, cx: &mut Context, ) { - match (self.inline_assistant_model.as_ref(), model.as_ref()) { - (Some(old), Some(new)) if old.is_same_as(new) => {} - (None, None) => {} - _ => cx.emit(Event::InlineAssistantModelChanged), + if self.default_model.is_none() { + match (self.environment_fallback_model.as_ref(), model.as_ref()) { + (Some(old), Some(new)) if old.is_same_as(new) => {} + (None, None) => {} + _ => cx.emit(Event::DefaultModelChanged), + } } + self.environment_fallback_model = model; + } + + pub fn set_inline_assistant_model(&mut self, model: Option) { self.inline_assistant_model = model; } - pub fn set_commit_message_model( - &mut self, - model: Option, - cx: &mut Context, - ) { - match (self.commit_message_model.as_ref(), model.as_ref()) { - (Some(old), Some(new)) if old.is_same_as(new) => {} - (None, None) => {} - _ => cx.emit(Event::CommitMessageModelChanged), - } + pub fn set_commit_message_model(&mut self, model: Option) { self.commit_message_model = model; } - pub fn set_thread_summary_model( - &mut self, - model: Option, - cx: &mut Context, - ) { - match (self.thread_summary_model.as_ref(), model.as_ref()) { - (Some(old), Some(new)) if old.is_same_as(new) => {} - (None, None) => {} - _ => cx.emit(Event::ThreadSummaryModelChanged), - } + pub fn set_thread_summary_model(&mut self, model: Option) { self.thread_summary_model = model; } + #[track_caller] pub fn default_model(&self) -> Option { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { return None; } - self.default_model.clone() + self.default_model + .clone() + .or_else(|| self.environment_fallback_model.clone()) + } + + pub fn default_fast_model(&self, cx: &App) -> Option { + let provider = self.default_model()?.provider; + let fast_model = provider.default_fast_model(cx)?; + Some(ConfiguredModel { + provider, + model: fast_model, + }) } pub fn inline_assistant_model(&self) -> Option { @@ -365,7 +355,7 @@ impl LanguageModelRegistry { .or_else(|| self.default_model.clone()) } - pub fn commit_message_model(&self) -> Option { + pub fn commit_message_model(&self, cx: &App) -> Option { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { return None; @@ -373,11 +363,11 @@ impl LanguageModelRegistry { self.commit_message_model .clone() - .or_else(|| self.default_fast_model.clone()) + .or_else(|| self.default_fast_model(cx)) .or_else(|| self.default_model.clone()) } - pub fn thread_summary_model(&self) -> Option { + pub fn thread_summary_model(&self, cx: &App) -> Option { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { return None; @@ -385,7 +375,7 @@ impl LanguageModelRegistry { self.thread_summary_model .clone() - .or_else(|| self.default_fast_model.clone()) + .or_else(|| self.default_fast_model(cx)) .or_else(|| self.default_model.clone()) } @@ -422,4 +412,34 @@ mod tests { let providers = registry.read(cx).providers(); assert!(providers.is_empty()); } + + #[gpui::test] + async fn test_configure_environment_fallback_model(cx: &mut gpui::TestAppContext) { + let registry = cx.new(|_| LanguageModelRegistry::default()); + + let provider = FakeLanguageModelProvider::default(); + registry.update(cx, |registry, cx| { + registry.register_provider(provider.clone(), cx); + }); + + cx.update(|cx| provider.authenticate(cx)).await.unwrap(); + + registry.update(cx, |registry, cx| { + let provider = registry.provider(&provider.id()).unwrap(); + + registry.set_environment_fallback_model( + Some(ConfiguredModel { + provider: provider.clone(), + model: provider.default_model(cx).unwrap(), + }), + cx, + ); + + let default_model = registry.default_model().unwrap(); + let fallback_model = registry.environment_fallback_model.clone().unwrap(); + + assert_eq!(default_model.model.id(), fallback_model.model.id()); + assert_eq!(default_model.provider.id(), fallback_model.provider.id()); + }); + } } diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index b5bfb870f6..cd41478668 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -44,6 +44,7 @@ ollama = { workspace = true, features = ["schemars"] } open_ai = { workspace = true, features = ["schemars"] } open_router = { workspace = true, features = ["schemars"] } partial-json-fixer.workspace = true +project.workspace = true release_channel.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index 738b72b0c9..beed306e74 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -3,8 +3,12 @@ use std::sync::Arc; use ::settings::{Settings, SettingsStore}; use client::{Client, UserStore}; use collections::HashSet; -use gpui::{App, Context, Entity}; -use language_model::{LanguageModelProviderId, LanguageModelRegistry}; +use futures::future; +use gpui::{App, AppContext as _, Context, Entity}; +use language_model::{ + AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry, +}; +use project::DisableAiSettings; use provider::deepseek::DeepSeekLanguageModelProvider; pub mod provider; @@ -13,7 +17,7 @@ pub mod ui; use crate::provider::anthropic::AnthropicLanguageModelProvider; use crate::provider::bedrock::BedrockLanguageModelProvider; -use crate::provider::cloud::CloudLanguageModelProvider; +use crate::provider::cloud::{self, CloudLanguageModelProvider}; use crate::provider::copilot_chat::CopilotChatLanguageModelProvider; use crate::provider::google::GoogleLanguageModelProvider; use crate::provider::lmstudio::LmStudioLanguageModelProvider; @@ -48,6 +52,13 @@ pub fn init(user_store: Entity, client: Arc, cx: &mut App) { cx, ); }); + + let mut already_authenticated = false; + if !DisableAiSettings::get_global(cx).disable_ai { + authenticate_all_providers(registry.clone(), cx); + already_authenticated = true; + } + cx.observe_global::(move |cx| { let openai_compatible_providers_new = AllLanguageModelSettings::get_global(cx) .openai_compatible @@ -65,6 +76,12 @@ pub fn init(user_store: Entity, client: Arc, cx: &mut App) { ); }); openai_compatible_providers = openai_compatible_providers_new; + already_authenticated = false; + } + + if !DisableAiSettings::get_global(cx).disable_ai && !already_authenticated { + authenticate_all_providers(registry.clone(), cx); + already_authenticated = true; } }) .detach(); @@ -151,3 +168,83 @@ fn register_language_model_providers( registry.register_provider(XAiLanguageModelProvider::new(client.http_client(), cx), cx); registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx); } + +/// Authenticates all providers in the [`LanguageModelRegistry`]. +/// +/// We do this so that we can populate the language selector with all of the +/// models from the configured providers. +/// +/// This function won't do anything if AI is disabled. +fn authenticate_all_providers(registry: Entity, cx: &mut App) { + let providers_to_authenticate = registry + .read(cx) + .providers() + .iter() + .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx))) + .collect::>(); + + let mut tasks = Vec::with_capacity(providers_to_authenticate.len()); + + for (provider_id, provider_name, authenticate_task) in providers_to_authenticate { + tasks.push(cx.background_spawn(async move { + if let Err(err) = authenticate_task.await { + if matches!(err, AuthenticateError::CredentialsNotFound) { + // Since we're authenticating these providers in the + // background for the purposes of populating the + // language selector, we don't care about providers + // where the credentials are not found. + } else { + // Some providers have noisy failure states that we + // don't want to spam the logs with every time the + // language model selector is initialized. + // + // Ideally these should have more clear failure modes + // that we know are safe to ignore here, like what we do + // with `CredentialsNotFound` above. + match provider_id.0.as_ref() { + "lmstudio" | "ollama" => { + // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated". + // + // These fail noisily, so we don't log them. + } + "copilot_chat" => { + // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors. + } + _ => { + log::error!( + "Failed to authenticate provider: {}: {err}", + provider_name.0 + ); + } + } + } + } + })); + } + + let all_authenticated_future = future::join_all(tasks); + + cx.spawn(async move |cx| { + all_authenticated_future.await; + + registry + .update(cx, |registry, cx| { + let cloud_provider = registry.provider(&cloud::PROVIDER_ID); + let fallback_model = cloud_provider + .iter() + .chain(registry.providers().iter()) + .find(|provider| provider.is_authenticated(cx)) + .and_then(|provider| { + Some(ConfiguredModel { + provider: provider.clone(), + model: provider + .default_model(cx) + .or_else(|| provider.recommended_models(cx).first().cloned())?, + }) + }); + registry.set_environment_fallback_model(fallback_model, cx); + }) + .ok(); + }) + .detach(); +} diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index b1b5ff3eb3..8e4b786935 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -44,8 +44,8 @@ use crate::provider::anthropic::{AnthropicEventMapper, count_anthropic_tokens, i use crate::provider::google::{GoogleEventMapper, into_google}; use crate::provider::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai}; -const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID; -const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME; +pub const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID; +pub const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME; #[derive(Default, Clone, Debug, PartialEq)] pub struct ZedDotDevSettings { @@ -148,7 +148,7 @@ impl State { default_fast_model: None, recommended_models: Vec::new(), _fetch_models_task: cx.spawn(async move |this, cx| { - maybe!(async move { + maybe!(async { let (client, llm_api_token) = this .read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?; From e36069110659ad113876c7fa5c85338176bf7172 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 22 Aug 2025 01:54:25 -0400 Subject: [PATCH 609/693] telemetry: Add panel button clicked event (#36735) The event has two fields 1. name: The name of the panel being clicked 2. toggle_state: true if clicking to open, otherwise false cc @katie-z-geer Release Notes: - N/A --- crates/workspace/src/dock.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 7a8de6e910..149a122c0c 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -915,6 +915,11 @@ impl Render for PanelButtons { .on_click({ let action = action.boxed_clone(); move |_, window, cx| { + telemetry::event!( + "Panel Button Clicked", + name = name, + toggle_state = !is_open + ); window.focus(&focus_handle); window.dispatch_action(action.boxed_clone(), cx) } From f4ba7997a7d7e9da61b98fda8e28542d3e29f518 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 21 Aug 2025 23:57:30 -0600 Subject: [PATCH 610/693] acp: Fix history search (#36734) Release Notes: - N/A --- crates/agent2/src/agent.rs | 5 +- crates/agent2/src/history_store.rs | 31 +- .../agent_ui/src/acp/completion_provider.rs | 2 +- crates/agent_ui/src/acp/thread_history.rs | 495 ++++++++---------- crates/agent_ui/src/acp/thread_view.rs | 6 +- 5 files changed, 228 insertions(+), 311 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 3502cf0ba9..4eaf87e218 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1406,10 +1406,9 @@ mod tests { history: &Entity, cx: &mut TestAppContext, ) -> Vec<(HistoryEntryId, String)> { - history.read_with(cx, |history, cx| { + history.read_with(cx, |history, _| { history - .entries(cx) - .iter() + .entries() .map(|e| (e.id(), e.title().to_string())) .collect::>() }) diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs index 78d83cc1d0..c656456e01 100644 --- a/crates/agent2/src/history_store.rs +++ b/crates/agent2/src/history_store.rs @@ -86,6 +86,7 @@ enum SerializedRecentOpen { pub struct HistoryStore { threads: Vec, + entries: Vec, context_store: Entity, recently_opened_entries: VecDeque, _subscriptions: Vec, @@ -97,7 +98,7 @@ impl HistoryStore { context_store: Entity, cx: &mut Context, ) -> Self { - let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())]; + let subscriptions = vec![cx.observe(&context_store, |this, _, cx| this.update_entries(cx))]; cx.spawn(async move |this, cx| { let entries = Self::load_recently_opened_entries(cx).await; @@ -116,6 +117,7 @@ impl HistoryStore { context_store, recently_opened_entries: VecDeque::default(), threads: Vec::default(), + entries: Vec::default(), _subscriptions: subscriptions, _save_recently_opened_entries_task: Task::ready(()), } @@ -181,20 +183,18 @@ impl HistoryStore { } } this.threads = threads; - cx.notify(); + this.update_entries(cx); }) }) .detach_and_log_err(cx); } - pub fn entries(&self, cx: &App) -> Vec { - let mut history_entries = Vec::new(); - + fn update_entries(&mut self, cx: &mut Context) { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() { - return history_entries; + return; } - + let mut history_entries = Vec::new(); history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread)); history_entries.extend( self.context_store @@ -205,17 +205,12 @@ impl HistoryStore { ); history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at())); - history_entries + self.entries = history_entries; + cx.notify() } - pub fn is_empty(&self, cx: &App) -> bool { - self.threads.is_empty() - && self - .context_store - .read(cx) - .unordered_contexts() - .next() - .is_none() + pub fn is_empty(&self, _cx: &App) -> bool { + self.entries.is_empty() } pub fn recently_opened_entries(&self, cx: &App) -> Vec { @@ -356,7 +351,7 @@ impl HistoryStore { self.save_recently_opened_entries(cx); } - pub fn recent_entries(&self, limit: usize, cx: &mut Context) -> Vec { - self.entries(cx).into_iter().take(limit).collect() + pub fn entries(&self) -> impl Iterator { + self.entries.iter().cloned() } } diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 3587e5144e..22a9ea6773 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -805,7 +805,7 @@ pub(crate) fn search_threads( history_store: &Entity, cx: &mut App, ) -> Task> { - let threads = history_store.read(cx).entries(cx); + let threads = history_store.read(cx).entries().collect(); if query.is_empty() { return Task::ready(threads); } diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs index d76969378c..5d852f0ddc 100644 --- a/crates/agent_ui/src/acp/thread_history.rs +++ b/crates/agent_ui/src/acp/thread_history.rs @@ -3,18 +3,18 @@ use crate::{AgentPanel, RemoveSelectedThread}; use agent2::{HistoryEntry, HistoryStore}; use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; use editor::{Editor, EditorEvent}; -use fuzzy::{StringMatch, StringMatchCandidate}; +use fuzzy::StringMatchCandidate; use gpui::{ - App, Empty, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, + App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, UniformListScrollHandle, WeakEntity, Window, uniform_list, }; -use std::{fmt::Display, ops::Range, sync::Arc}; +use std::{fmt::Display, ops::Range}; +use text::Bias; use time::{OffsetDateTime, UtcOffset}; use ui::{ HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, Tooltip, prelude::*, }; -use util::ResultExt; pub struct AcpThreadHistory { pub(crate) history_store: Entity, @@ -22,38 +22,38 @@ pub struct AcpThreadHistory { selected_index: usize, hovered_index: Option, search_editor: Entity, - all_entries: Arc>, - // When the search is empty, we display date separators between history entries - // This vector contains an enum of either a separator or an actual entry - separated_items: Vec, - // Maps entry indexes to list item indexes - separated_item_indexes: Vec, - _separated_items_task: Option>, - search_state: SearchState, + search_query: SharedString, + + visible_items: Vec, + scrollbar_visibility: bool, scrollbar_state: ScrollbarState, local_timezone: UtcOffset, - _subscriptions: Vec, -} -enum SearchState { - Empty, - Searching { - query: SharedString, - _task: Task<()>, - }, - Searched { - query: SharedString, - matches: Vec, - }, + _update_task: Task<()>, + _subscriptions: Vec, } enum ListItemType { BucketSeparator(TimeBucket), Entry { - index: usize, + entry: HistoryEntry, format: EntryTimeFormat, }, + SearchResult { + entry: HistoryEntry, + positions: Vec, + }, +} + +impl ListItemType { + fn history_entry(&self) -> Option<&HistoryEntry> { + match self { + ListItemType::Entry { entry, .. } => Some(entry), + ListItemType::SearchResult { entry, .. } => Some(entry), + _ => None, + } + } } pub enum ThreadHistoryEvent { @@ -78,12 +78,15 @@ impl AcpThreadHistory { cx.subscribe(&search_editor, |this, search_editor, event, cx| { if let EditorEvent::BufferEdited = event { let query = search_editor.read(cx).text(cx); - this.search(query.into(), cx); + if this.search_query != query { + this.search_query = query.into(); + this.update_visible_items(false, cx); + } } }); let history_store_subscription = cx.observe(&history_store, |this, _, cx| { - this.update_all_entries(cx); + this.update_visible_items(true, cx); }); let scroll_handle = UniformListScrollHandle::default(); @@ -94,10 +97,7 @@ impl AcpThreadHistory { scroll_handle, selected_index: 0, hovered_index: None, - search_state: SearchState::Empty, - all_entries: Default::default(), - separated_items: Default::default(), - separated_item_indexes: Default::default(), + visible_items: Default::default(), search_editor, scrollbar_visibility: true, scrollbar_state, @@ -105,29 +105,61 @@ impl AcpThreadHistory { chrono::Local::now().offset().local_minus_utc(), ) .unwrap(), + search_query: SharedString::default(), _subscriptions: vec![search_editor_subscription, history_store_subscription], - _separated_items_task: None, + _update_task: Task::ready(()), }; - this.update_all_entries(cx); + this.update_visible_items(false, cx); this } - fn update_all_entries(&mut self, cx: &mut Context) { - let new_entries: Arc> = self + fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context) { + let entries = self .history_store - .update(cx, |store, cx| store.entries(cx)) - .into(); + .update(cx, |store, _| store.entries().collect()); + let new_list_items = if self.search_query.is_empty() { + self.add_list_separators(entries, cx) + } else { + self.filter_search_results(entries, cx) + }; + let selected_history_entry = if preserve_selected_item { + self.selected_history_entry().cloned() + } else { + None + }; - self._separated_items_task.take(); + self._update_task = cx.spawn(async move |this, cx| { + let new_visible_items = new_list_items.await; + this.update(cx, |this, cx| { + let new_selected_index = if let Some(history_entry) = selected_history_entry { + let history_entry_id = history_entry.id(); + new_visible_items + .iter() + .position(|visible_entry| { + visible_entry + .history_entry() + .is_some_and(|entry| entry.id() == history_entry_id) + }) + .unwrap_or(0) + } else { + 0 + }; - let mut items = Vec::with_capacity(new_entries.len() + 1); - let mut indexes = Vec::with_capacity(new_entries.len() + 1); + this.visible_items = new_visible_items; + this.set_selected_index(new_selected_index, Bias::Right, cx); + cx.notify(); + }) + .ok(); + }); + } - let bg_task = cx.background_spawn(async move { + fn add_list_separators(&self, entries: Vec, cx: &App) -> Task> { + cx.background_spawn(async move { + let mut items = Vec::with_capacity(entries.len() + 1); let mut bucket = None; let today = Local::now().naive_local().date(); - for (index, entry) in new_entries.iter().enumerate() { + for entry in entries.into_iter() { let entry_date = entry .updated_at() .with_timezone(&Local) @@ -140,75 +172,33 @@ impl AcpThreadHistory { items.push(ListItemType::BucketSeparator(entry_bucket)); } - indexes.push(items.len() as u32); items.push(ListItemType::Entry { - index, + entry, format: entry_bucket.into(), }); } - (new_entries, items, indexes) - }); - - let task = cx.spawn(async move |this, cx| { - let (new_entries, items, indexes) = bg_task.await; - this.update(cx, |this, cx| { - let previously_selected_entry = - this.all_entries.get(this.selected_index).map(|e| e.id()); - - this.all_entries = new_entries; - this.separated_items = items; - this.separated_item_indexes = indexes; - - match &this.search_state { - SearchState::Empty => { - if this.selected_index >= this.all_entries.len() { - this.set_selected_entry_index( - this.all_entries.len().saturating_sub(1), - cx, - ); - } else if let Some(prev_id) = previously_selected_entry - && let Some(new_ix) = this - .all_entries - .iter() - .position(|probe| probe.id() == prev_id) - { - this.set_selected_entry_index(new_ix, cx); - } - } - SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => { - this.search(query.clone(), cx); - } - } - - cx.notify(); - }) - .log_err(); - }); - self._separated_items_task = Some(task); + items + }) } - fn search(&mut self, query: SharedString, cx: &mut Context) { - if query.is_empty() { - self.search_state = SearchState::Empty; - cx.notify(); - return; - } - - let all_entries = self.all_entries.clone(); - - let fuzzy_search_task = cx.background_spawn({ - let query = query.clone(); + fn filter_search_results( + &self, + entries: Vec, + cx: &App, + ) -> Task> { + let query = self.search_query.clone(); + cx.background_spawn({ let executor = cx.background_executor().clone(); async move { - let mut candidates = Vec::with_capacity(all_entries.len()); + let mut candidates = Vec::with_capacity(entries.len()); - for (idx, entry) in all_entries.iter().enumerate() { + for (idx, entry) in entries.iter().enumerate() { candidates.push(StringMatchCandidate::new(idx, entry.title())); } const MAX_MATCHES: usize = 100; - fuzzy::match_strings( + let matches = fuzzy::match_strings( &candidates, &query, false, @@ -217,74 +207,61 @@ impl AcpThreadHistory { &Default::default(), executor, ) - .await + .await; + + matches + .into_iter() + .map(|search_match| ListItemType::SearchResult { + entry: entries[search_match.candidate_id].clone(), + positions: search_match.positions, + }) + .collect() } - }); - - let task = cx.spawn({ - let query = query.clone(); - async move |this, cx| { - let matches = fuzzy_search_task.await; - - this.update(cx, |this, cx| { - let SearchState::Searching { - query: current_query, - _task, - } = &this.search_state - else { - return; - }; - - if &query == current_query { - this.search_state = SearchState::Searched { - query: query.clone(), - matches, - }; - - this.set_selected_entry_index(0, cx); - cx.notify(); - }; - }) - .log_err(); - } - }); - - self.search_state = SearchState::Searching { query, _task: task }; - cx.notify(); - } - - fn matched_count(&self) -> usize { - match &self.search_state { - SearchState::Empty => self.all_entries.len(), - SearchState::Searching { .. } => 0, - SearchState::Searched { matches, .. } => matches.len(), - } - } - - fn list_item_count(&self) -> usize { - match &self.search_state { - SearchState::Empty => self.separated_items.len(), - SearchState::Searching { .. } => 0, - SearchState::Searched { matches, .. } => matches.len(), - } + }) } fn search_produced_no_matches(&self) -> bool { - match &self.search_state { - SearchState::Empty => false, - SearchState::Searching { .. } => false, - SearchState::Searched { matches, .. } => matches.is_empty(), - } + self.visible_items.is_empty() && !self.search_query.is_empty() } - fn get_match(&self, ix: usize) -> Option<&HistoryEntry> { - match &self.search_state { - SearchState::Empty => self.all_entries.get(ix), - SearchState::Searching { .. } => None, - SearchState::Searched { matches, .. } => matches - .get(ix) - .and_then(|m| self.all_entries.get(m.candidate_id)), + fn selected_history_entry(&self) -> Option<&HistoryEntry> { + self.get_history_entry(self.selected_index) + } + + fn get_history_entry(&self, visible_items_ix: usize) -> Option<&HistoryEntry> { + self.visible_items.get(visible_items_ix)?.history_entry() + } + + fn set_selected_index(&mut self, mut index: usize, bias: Bias, cx: &mut Context) { + if self.visible_items.len() == 0 { + self.selected_index = 0; + return; } + while matches!( + self.visible_items.get(index), + None | Some(ListItemType::BucketSeparator(..)) + ) { + index = match bias { + Bias::Left => { + if index == 0 { + self.visible_items.len() - 1 + } else { + index - 1 + } + } + Bias::Right => { + if index >= self.visible_items.len() - 1 { + 0 + } else { + index + 1 + } + } + }; + } + self.selected_index = index; + self.scroll_handle + .scroll_to_item(index, ScrollStrategy::Top); + cx.notify() } pub fn select_previous( @@ -293,13 +270,10 @@ impl AcpThreadHistory { _window: &mut Window, cx: &mut Context, ) { - let count = self.matched_count(); - if count > 0 { - if self.selected_index == 0 { - self.set_selected_entry_index(count - 1, cx); - } else { - self.set_selected_entry_index(self.selected_index - 1, cx); - } + if self.selected_index == 0 { + self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx); + } else { + self.set_selected_index(self.selected_index - 1, Bias::Left, cx); } } @@ -309,13 +283,10 @@ impl AcpThreadHistory { _window: &mut Window, cx: &mut Context, ) { - let count = self.matched_count(); - if count > 0 { - if self.selected_index == count - 1 { - self.set_selected_entry_index(0, cx); - } else { - self.set_selected_entry_index(self.selected_index + 1, cx); - } + if self.selected_index == self.visible_items.len() - 1 { + self.set_selected_index(0, Bias::Right, cx); + } else { + self.set_selected_index(self.selected_index + 1, Bias::Right, cx); } } @@ -325,35 +296,47 @@ impl AcpThreadHistory { _window: &mut Window, cx: &mut Context, ) { - let count = self.matched_count(); - if count > 0 { - self.set_selected_entry_index(0, cx); - } + self.set_selected_index(0, Bias::Right, cx); } fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context) { - let count = self.matched_count(); - if count > 0 { - self.set_selected_entry_index(count - 1, cx); - } + self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx); } - fn set_selected_entry_index(&mut self, entry_index: usize, cx: &mut Context) { - self.selected_index = entry_index; + fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context) { + self.confirm_entry(self.selected_index, cx); + } - let scroll_ix = match self.search_state { - SearchState::Empty | SearchState::Searching { .. } => self - .separated_item_indexes - .get(entry_index) - .map(|ix| *ix as usize) - .unwrap_or(entry_index + 1), - SearchState::Searched { .. } => entry_index, + fn confirm_entry(&mut self, ix: usize, cx: &mut Context) { + let Some(entry) = self.get_history_entry(ix) else { + return; + }; + cx.emit(ThreadHistoryEvent::Open(entry.clone())); + } + + fn remove_selected_thread( + &mut self, + _: &RemoveSelectedThread, + _window: &mut Window, + cx: &mut Context, + ) { + self.remove_thread(self.selected_index, cx) + } + + fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context) { + let Some(entry) = self.get_history_entry(visible_item_ix) else { + return; }; - self.scroll_handle - .scroll_to_item(scroll_ix, ScrollStrategy::Top); - - cx.notify(); + let task = match entry { + HistoryEntry::AcpThread(thread) => self + .history_store + .update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)), + HistoryEntry::TextThread(context) => self.history_store.update(cx, |this, cx| { + this.delete_text_thread(context.path.clone(), cx) + }), + }; + task.detach_and_log_err(cx); } fn render_scrollbar(&self, cx: &mut Context) -> Option> { @@ -393,91 +376,33 @@ impl AcpThreadHistory { ) } - fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context) { - self.confirm_entry(self.selected_index, cx); - } - - fn confirm_entry(&mut self, ix: usize, cx: &mut Context) { - let Some(entry) = self.get_match(ix) else { - return; - }; - cx.emit(ThreadHistoryEvent::Open(entry.clone())); - } - - fn remove_selected_thread( - &mut self, - _: &RemoveSelectedThread, - _window: &mut Window, - cx: &mut Context, - ) { - self.remove_thread(self.selected_index, cx) - } - - fn remove_thread(&mut self, ix: usize, cx: &mut Context) { - let Some(entry) = self.get_match(ix) else { - return; - }; - - let task = match entry { - HistoryEntry::AcpThread(thread) => self - .history_store - .update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)), - HistoryEntry::TextThread(context) => self.history_store.update(cx, |this, cx| { - this.delete_text_thread(context.path.clone(), cx) - }), - }; - task.detach_and_log_err(cx); - } - - fn list_items( + fn render_list_items( &mut self, range: Range, _window: &mut Window, cx: &mut Context, ) -> Vec { - match &self.search_state { - SearchState::Empty => self - .separated_items - .get(range) - .iter() - .flat_map(|items| { - items - .iter() - .map(|item| self.render_list_item(item, vec![], cx)) - }) - .collect(), - SearchState::Searched { matches, .. } => matches[range] - .iter() - .filter_map(|m| { - let entry = self.all_entries.get(m.candidate_id)?; - Some(self.render_history_entry( - entry, - EntryTimeFormat::DateAndTime, - m.candidate_id, - m.positions.clone(), - cx, - )) - }) - .collect(), - SearchState::Searching { .. } => { - vec![] - } - } + self.visible_items + .get(range.clone()) + .into_iter() + .flatten() + .enumerate() + .map(|(ix, item)| self.render_list_item(item, range.start + ix, cx)) + .collect() } - fn render_list_item( - &self, - item: &ListItemType, - highlight_positions: Vec, - cx: &Context, - ) -> AnyElement { + fn render_list_item(&self, item: &ListItemType, ix: usize, cx: &Context) -> AnyElement { match item { - ListItemType::Entry { index, format } => match self.all_entries.get(*index) { - Some(entry) => self - .render_history_entry(entry, *format, *index, highlight_positions, cx) - .into_any(), - None => Empty.into_any_element(), - }, + ListItemType::Entry { entry, format } => self + .render_history_entry(entry, *format, ix, Vec::default(), cx) + .into_any(), + ListItemType::SearchResult { entry, positions } => self.render_history_entry( + entry, + EntryTimeFormat::DateAndTime, + ix, + positions.clone(), + cx, + ), ListItemType::BucketSeparator(bucket) => div() .px(DynamicSpacing::Base06.rems(cx)) .pt_2() @@ -495,12 +420,12 @@ impl AcpThreadHistory { &self, entry: &HistoryEntry, format: EntryTimeFormat, - list_entry_ix: usize, + ix: usize, highlight_positions: Vec, cx: &Context, ) -> AnyElement { - let selected = list_entry_ix == self.selected_index; - let hovered = Some(list_entry_ix) == self.hovered_index; + let selected = ix == self.selected_index; + let hovered = Some(ix) == self.hovered_index; let timestamp = entry.updated_at().timestamp(); let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone); @@ -508,7 +433,7 @@ impl AcpThreadHistory { .w_full() .pb_1() .child( - ListItem::new(list_entry_ix) + ListItem::new(ix) .rounded() .toggle_state(selected) .spacing(ListItemSpacing::Sparse) @@ -530,8 +455,8 @@ impl AcpThreadHistory { ) .on_hover(cx.listener(move |this, is_hovered, _window, cx| { if *is_hovered { - this.hovered_index = Some(list_entry_ix); - } else if this.hovered_index == Some(list_entry_ix) { + this.hovered_index = Some(ix); + } else if this.hovered_index == Some(ix) { this.hovered_index = None; } @@ -546,16 +471,14 @@ impl AcpThreadHistory { .tooltip(move |window, cx| { Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx) }) - .on_click(cx.listener(move |this, _, _, cx| { - this.remove_thread(list_entry_ix, cx) - })), + .on_click( + cx.listener(move |this, _, _, cx| this.remove_thread(ix, cx)), + ), ) } else { None }) - .on_click( - cx.listener(move |this, _, _, cx| this.confirm_entry(list_entry_ix, cx)), - ), + .on_click(cx.listener(move |this, _, _, cx| this.confirm_entry(ix, cx))), ) .into_any_element() } @@ -578,7 +501,7 @@ impl Render for AcpThreadHistory { .on_action(cx.listener(Self::select_last)) .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::remove_selected_thread)) - .when(!self.all_entries.is_empty(), |parent| { + .when(!self.history_store.read(cx).is_empty(cx), |parent| { parent.child( h_flex() .h(px(41.)) // Match the toolbar perfectly @@ -604,7 +527,7 @@ impl Render for AcpThreadHistory { .overflow_hidden() .flex_grow(); - if self.all_entries.is_empty() { + if self.history_store.read(cx).is_empty(cx) { view.justify_center() .child( h_flex().w_full().justify_center().child( @@ -623,9 +546,9 @@ impl Render for AcpThreadHistory { .child( uniform_list( "thread-history", - self.list_item_count(), + self.visible_items.len(), cx.processor(|this, range: Range, window, cx| { - this.list_items(range, window, cx) + this.render_list_items(range, window, cx) }), ) .p_1() diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 4d89a55139..dae89b3283 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2538,9 +2538,9 @@ impl AcpThreadView { ) }) .when(render_history, |this| { - let recent_history = self - .history_store - .update(cx, |history_store, cx| history_store.recent_entries(3, cx)); + let recent_history: Vec<_> = self.history_store.update(cx, |history_store, _| { + history_store.entries().take(3).collect() + }); this.justify_end().child( v_flex() .child( From d88fd00e87673263eefdbe6fa5b3d582a05f2aee Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 22 Aug 2025 03:48:47 -0400 Subject: [PATCH 611/693] acp: Fix panic with edit file tool (#36732) We had a frequent panic when the agent was using our edit file tool. The root cause was that we were constructing a `BufferDiff` with `BufferDiff::new`, then calling `set_base_text`, but not waiting for that asynchronous operation to finish. This means there was a window of time where the diff's base text was set to the initial value of `""`--that's not a problem in itself, but it was possible for us to call `PendingDiff::update` during that window, which calls `BufferDiff::update_diff`, which calls `BufferDiffSnapshot::new_with_base_buffer`, which takes two arguments `base_text` and `base_text_snapshot` that are supposed to represent the same text. We were getting the first of those arguments from the `base_text` field of `PendingDiff`, which is set immediately to the target base text without waiting for `BufferDiff::set_base_text` to run to completion; and the second from the `BufferDiff` itself, which still has the empty base text during that window. As a result of that mismatch, we could end up adding `DeletedHunk` diff transforms to the multibuffer for the diff card even though the multibuffer's base text was empty, ultimately leading to a panic very far away in rendering code. I've fixed this by adding a new `BufferDiff` constructor for the case where the buffer contents and the base text are (initially) the same, like for the diff cards, and so we don't need an async diff calculation. I also added a debug assertion to catch the basic issue here earlier, when `BufferDiffSnapshot::new_with_base_buffer` is called with two base texts that don't match. Release Notes: - N/A --------- Co-authored-by: Conrad --- crates/acp_thread/src/diff.rs | 40 +++++++++++++++++---------- crates/buffer_diff/src/buffer_diff.rs | 33 +++++++++++++++++++++- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs index 59f907dcc4..0fec6809e0 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -85,27 +85,19 @@ impl Diff { } pub fn new(buffer: Entity, cx: &mut Context) -> Self { - let buffer_snapshot = buffer.read(cx).snapshot(); - let base_text = buffer_snapshot.text(); - let language_registry = buffer.read(cx).language_registry(); - let text_snapshot = buffer.read(cx).text_snapshot(); + let buffer_text_snapshot = buffer.read(cx).text_snapshot(); + let base_text_snapshot = buffer.read(cx).snapshot(); + let base_text = base_text_snapshot.text(); + debug_assert_eq!(buffer_text_snapshot.text(), base_text); let buffer_diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&text_snapshot, cx); - let _ = diff.set_base_text( - buffer_snapshot.clone(), - language_registry, - text_snapshot, - cx, - ); + let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, base_text_snapshot); let snapshot = diff.snapshot(cx); - let secondary_diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&buffer_snapshot, cx); - diff.set_snapshot(snapshot, &buffer_snapshot, cx); + let mut diff = BufferDiff::new(&buffer_text_snapshot, cx); + diff.set_snapshot(snapshot, &buffer_text_snapshot, cx); diff }); diff.set_secondary_diff(secondary_diff); - diff }); @@ -412,3 +404,21 @@ async fn build_buffer_diff( diff }) } + +#[cfg(test)] +mod tests { + use gpui::{AppContext as _, TestAppContext}; + use language::Buffer; + + use crate::Diff; + + #[gpui::test] + async fn test_pending_diff(cx: &mut TestAppContext) { + let buffer = cx.new(|cx| Buffer::local("hello!", cx)); + let _diff = cx.new(|cx| Diff::new(buffer.clone(), cx)); + buffer.update(cx, |buffer, cx| { + buffer.set_text("HELLO!", cx); + }); + cx.run_until_parked(); + } +} diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 10b59d0ba2..b20dad4ebb 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -162,6 +162,22 @@ impl BufferDiffSnapshot { } } + fn unchanged( + buffer: &text::BufferSnapshot, + base_text: language::BufferSnapshot, + ) -> BufferDiffSnapshot { + debug_assert_eq!(buffer.text(), base_text.text()); + BufferDiffSnapshot { + inner: BufferDiffInner { + base_text, + hunks: SumTree::new(buffer), + pending_hunks: SumTree::new(buffer), + base_text_exists: false, + }, + secondary_diff: None, + } + } + fn new_with_base_text( buffer: text::BufferSnapshot, base_text: Option>, @@ -213,7 +229,10 @@ impl BufferDiffSnapshot { cx: &App, ) -> impl Future + use<> { let base_text_exists = base_text.is_some(); - let base_text_pair = base_text.map(|text| (text, base_text_snapshot.as_rope().clone())); + let base_text_pair = base_text.map(|text| { + debug_assert_eq!(&*text, &base_text_snapshot.text()); + (text, base_text_snapshot.as_rope().clone()) + }); cx.background_executor() .spawn_labeled(*CALCULATE_DIFF_TASK, async move { Self { @@ -873,6 +892,18 @@ impl BufferDiff { } } + pub fn new_unchanged( + buffer: &text::BufferSnapshot, + base_text: language::BufferSnapshot, + ) -> Self { + debug_assert_eq!(buffer.text(), base_text.text()); + BufferDiff { + buffer_id: buffer.remote_id(), + inner: BufferDiffSnapshot::unchanged(buffer, base_text).inner, + secondary_diff: None, + } + } + #[cfg(any(test, feature = "test-support"))] pub fn new_with_base_text( base_text: &str, From 27a26d53b1ea1d83ab16c840a5ba1f05da96edea Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 22 Aug 2025 08:28:03 -0300 Subject: [PATCH 612/693] thread view: Inform when editing previous messages is unavailable (#36727) Release Notes: - N/A --- assets/icons/pencil_unavailable.svg | 6 ++ crates/agent_ui/src/acp/thread_view.rs | 97 ++++++++++++------- crates/agent_ui/src/ui.rs | 2 + .../src/ui/unavailable_editing_tooltip.rs | 29 ++++++ crates/icons/src/icons.rs | 1 + 5 files changed, 98 insertions(+), 37 deletions(-) create mode 100644 assets/icons/pencil_unavailable.svg create mode 100644 crates/agent_ui/src/ui/unavailable_editing_tooltip.rs diff --git a/assets/icons/pencil_unavailable.svg b/assets/icons/pencil_unavailable.svg new file mode 100644 index 0000000000..4241d766ac --- /dev/null +++ b/assets/icons/pencil_unavailable.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index dae89b3283..619885144a 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -57,7 +57,9 @@ use crate::agent_diff::AgentDiff; use crate::profile_selector::{ProfileProvider, ProfileSelector}; use crate::ui::preview::UsageCallout; -use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip}; +use crate::ui::{ + AgentNotification, AgentNotificationEvent, BurnModeTooltip, UnavailableEditingTooltip, +}; use crate::{ AgentDiffPane, AgentPanel, ContinueThread, ContinueWithBurnMode, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector, @@ -1239,6 +1241,8 @@ impl AcpThreadView { None }; + let agent_name = self.agent.name(); + v_flex() .id(("user_message", entry_ix)) .pt_2() @@ -1292,42 +1296,61 @@ impl AcpThreadView { .text_xs() .child(editor.clone().into_any_element()), ) - .when(editing && editor_focus, |this| - this.child( - h_flex() - .absolute() - .top_neg_3p5() - .right_3() - .gap_1() - .rounded_sm() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().editor_background) - .overflow_hidden() - .child( - IconButton::new("cancel", IconName::Close) - .icon_color(Color::Error) - .icon_size(IconSize::XSmall) - .on_click(cx.listener(Self::cancel_editing)) - ) - .child( - IconButton::new("regenerate", IconName::Return) - .icon_color(Color::Muted) - .icon_size(IconSize::XSmall) - .tooltip(Tooltip::text( - "Editing will restart the thread from this point." - )) - .on_click(cx.listener({ - let editor = editor.clone(); - move |this, _, window, cx| { - this.regenerate( - entry_ix, &editor, window, cx, - ); - } - })), - ) - ) - ), + .when(editor_focus, |this| { + let base_container = h_flex() + .absolute() + .top_neg_3p5() + .right_3() + .gap_1() + .rounded_sm() + .border_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().editor_background) + .overflow_hidden(); + + if message.id.is_some() { + this.child( + base_container + .child( + IconButton::new("cancel", IconName::Close) + .icon_color(Color::Error) + .icon_size(IconSize::XSmall) + .on_click(cx.listener(Self::cancel_editing)) + ) + .child( + IconButton::new("regenerate", IconName::Return) + .icon_color(Color::Muted) + .icon_size(IconSize::XSmall) + .tooltip(Tooltip::text( + "Editing will restart the thread from this point." + )) + .on_click(cx.listener({ + let editor = editor.clone(); + move |this, _, window, cx| { + this.regenerate( + entry_ix, &editor, window, cx, + ); + } + })), + ) + ) + } else { + this.child( + base_container + .border_dashed() + .child( + IconButton::new("editing_unavailable", IconName::PencilUnavailable) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .style(ButtonStyle::Transparent) + .tooltip(move |_window, cx| { + cx.new(|_| UnavailableEditingTooltip::new(agent_name.into())) + .into() + }) + ) + ) + } + }), ) .into_any() } diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index e27a224240..ada973cddf 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -4,9 +4,11 @@ mod context_pill; mod end_trial_upsell; mod onboarding_modal; pub mod preview; +mod unavailable_editing_tooltip; pub use agent_notification::*; pub use burn_mode_tooltip::*; pub use context_pill::*; pub use end_trial_upsell::*; pub use onboarding_modal::*; +pub use unavailable_editing_tooltip::*; diff --git a/crates/agent_ui/src/ui/unavailable_editing_tooltip.rs b/crates/agent_ui/src/ui/unavailable_editing_tooltip.rs new file mode 100644 index 0000000000..78d4c64e0a --- /dev/null +++ b/crates/agent_ui/src/ui/unavailable_editing_tooltip.rs @@ -0,0 +1,29 @@ +use gpui::{Context, IntoElement, Render, Window}; +use ui::{prelude::*, tooltip_container}; + +pub struct UnavailableEditingTooltip { + agent_name: SharedString, +} + +impl UnavailableEditingTooltip { + pub fn new(agent_name: SharedString) -> Self { + Self { agent_name } + } +} + +impl Render for UnavailableEditingTooltip { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + tooltip_container(window, cx, |this, _, _| { + this.child(Label::new("Unavailable Editing")).child( + div().max_w_64().child( + Label::new(format!( + "Editing previous messages is not available for {} yet.", + self.agent_name + )) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + }) + } +} diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 38f02c2206..b5f891713a 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -164,6 +164,7 @@ pub enum IconName { PageDown, PageUp, Pencil, + PencilUnavailable, Person, Pin, PlayOutlined, From 3b7c1744b424c9127267e8935ac668ece52394e4 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 22 Aug 2025 09:52:44 -0300 Subject: [PATCH 613/693] thread view: Add more UI improvements (#36750) Release Notes: - N/A --- assets/icons/attach.svg | 3 ++ assets/icons/tool_think.svg | 2 +- crates/agent_servers/src/claude.rs | 2 +- crates/agent_servers/src/gemini.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 68 +++++++++----------------- crates/agent_ui/src/agent_panel.rs | 5 ++ crates/icons/src/icons.rs | 1 + 7 files changed, 36 insertions(+), 47 deletions(-) create mode 100644 assets/icons/attach.svg diff --git a/assets/icons/attach.svg b/assets/icons/attach.svg new file mode 100644 index 0000000000..f923a3c7c8 --- /dev/null +++ b/assets/icons/attach.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/tool_think.svg b/assets/icons/tool_think.svg index efd5908a90..773f5e7fa7 100644 --- a/assets/icons/tool_think.svg +++ b/assets/icons/tool_think.svg @@ -1,3 +1,3 @@ - + diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index d6ccabb130..ef666974f1 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -44,7 +44,7 @@ pub struct ClaudeCode; impl AgentServer for ClaudeCode { fn name(&self) -> &'static str { - "Welcome to Claude Code" + "Claude Code" } fn empty_state_headline(&self) -> &'static str { diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 3b892e7931..29120fff6e 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -23,7 +23,7 @@ impl AgentServer for Gemini { } fn empty_state_headline(&self) -> &'static str { - "Welcome to Gemini CLI" + self.name() } fn empty_state_message(&self) -> &'static str { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 619885144a..d27dee1fe6 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1697,7 +1697,7 @@ impl AcpThreadView { .absolute() .top_0() .right_0() - .w_12() + .w_16() .h_full() .bg(linear_gradient( 90., @@ -1837,6 +1837,7 @@ impl AcpThreadView { .w_full() .max_w_full() .ml_1p5() + .overflow_hidden() .child(h_flex().pr_8().child(self.render_markdown( tool_call.label.clone(), default_markdown_style(false, true, window, cx), @@ -1906,13 +1907,10 @@ impl AcpThreadView { .text_color(cx.theme().colors().text_muted) .child(self.render_markdown(markdown, default_markdown_style(false, false, window, cx))) .child( - Button::new(button_id, "Collapse") + IconButton::new(button_id, IconName::ChevronUp) .full_width() .style(ButtonStyle::Outlined) - .label_size(LabelSize::Small) - .icon(IconName::ChevronUp) .icon_color(Color::Muted) - .icon_position(IconPosition::Start) .on_click(cx.listener({ move |this: &mut Self, _, _, cx: &mut Context| { this.expanded_tool_calls.remove(&tool_call_id); @@ -2414,39 +2412,32 @@ impl AcpThreadView { return None; } + let has_both = user_rules_text.is_some() && rules_file_text.is_some(); + Some( - v_flex() + h_flex() .px_2p5() - .gap_1() + .pb_1() + .child( + Icon::new(IconName::Attach) + .size(IconSize::XSmall) + .color(Color::Disabled), + ) .when_some(user_rules_text, |parent, user_rules_text| { parent.child( h_flex() - .group("user-rules") .id("user-rules") - .w_full() - .child( - Icon::new(IconName::Reader) - .size(IconSize::XSmall) - .color(Color::Disabled), - ) + .ml_1() + .mr_1p5() .child( Label::new(user_rules_text) .size(LabelSize::XSmall) .color(Color::Muted) .truncate() - .buffer_font(cx) - .ml_1p5() - .mr_0p5(), - ) - .child( - IconButton::new("open-prompt-library", IconName::ArrowUpRight) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(Color::Ignored) - .visible_on_hover("user-rules") - // TODO: Figure out a way to pass focus handle here so we can display the `OpenRulesLibrary` keybinding - .tooltip(Tooltip::text("View User Rules")), + .buffer_font(cx), ) + .hover(|s| s.bg(cx.theme().colors().element_hover)) + .tooltip(Tooltip::text("View User Rules")) .on_click(move |_event, window, cx| { window.dispatch_action( Box::new(OpenRulesLibrary { @@ -2457,33 +2448,20 @@ impl AcpThreadView { }), ) }) + .when(has_both, |this| this.child(Divider::vertical())) .when_some(rules_file_text, |parent, rules_file_text| { parent.child( h_flex() - .group("project-rules") .id("project-rules") - .w_full() - .child( - Icon::new(IconName::Reader) - .size(IconSize::XSmall) - .color(Color::Disabled), - ) + .ml_1p5() .child( Label::new(rules_file_text) .size(LabelSize::XSmall) .color(Color::Muted) - .buffer_font(cx) - .ml_1p5() - .mr_0p5(), - ) - .child( - IconButton::new("open-rule", IconName::ArrowUpRight) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(Color::Ignored) - .visible_on_hover("project-rules") - .tooltip(Tooltip::text("View Project Rules")), + .buffer_font(cx), ) + .hover(|s| s.bg(cx.theme().colors().element_hover)) + .tooltip(Tooltip::text("View Project Rules")) .on_click(cx.listener(Self::handle_open_rules)), ) }) @@ -4080,8 +4058,10 @@ impl AcpThreadView { .group("thread-controls-container") .w_full() .mr_1() + .pt_1() .pb_2() .px(RESPONSE_PADDING_X) + .gap_px() .opacity(0.4) .hover(|style| style.opacity(1.)) .flex_wrap() diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index d2ff6aa4f3..469898d10f 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -2041,9 +2041,11 @@ impl AgentPanel { match state { ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT) .truncate() + .color(Color::Muted) .into_any_element(), ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER) .truncate() + .color(Color::Muted) .into_any_element(), ThreadSummary::Ready(_) => div() .w_full() @@ -2098,6 +2100,7 @@ impl AgentPanel { .into_any_element() } else { Label::new(thread_view.read(cx).title(cx)) + .color(Color::Muted) .truncate() .into_any_element() } @@ -2111,6 +2114,7 @@ impl AgentPanel { match summary { ContextSummary::Pending => Label::new(ContextSummary::DEFAULT) + .color(Color::Muted) .truncate() .into_any_element(), ContextSummary::Content(summary) => { @@ -2122,6 +2126,7 @@ impl AgentPanel { } else { Label::new(LOADING_SUMMARY_PLACEHOLDER) .truncate() + .color(Color::Muted) .into_any_element() } } diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index b5f891713a..4fc6039fd7 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -34,6 +34,7 @@ pub enum IconName { ArrowRightLeft, ArrowUp, ArrowUpRight, + Attach, AudioOff, AudioOn, Backspace, From 4f0fad69960d0aad5cfd9840592d70fa82df5d91 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 22 Aug 2025 15:16:42 +0200 Subject: [PATCH 614/693] acp: Support calling tools provided by MCP servers (#36752) Release Notes: - N/A --- crates/agent2/src/tests/mod.rs | 441 +++++++++++++++++++++++++++++- crates/agent2/src/thread.rs | 148 +++++++--- crates/context_server/src/test.rs | 36 ++- 3 files changed, 561 insertions(+), 64 deletions(-) diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 09048488c8..60b3198081 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -4,26 +4,35 @@ use agent_client_protocol::{self as acp}; use agent_settings::AgentProfileId; use anyhow::Result; use client::{Client, UserStore}; +use context_server::{ContextServer, ContextServerCommand, ContextServerId}; use fs::{FakeFs, Fs}; -use futures::{StreamExt, channel::mpsc::UnboundedReceiver}; +use futures::{ + StreamExt, + channel::{ + mpsc::{self, UnboundedReceiver}, + oneshot, + }, +}; use gpui::{ App, AppContext, Entity, Task, TestAppContext, UpdateGlobal, http_client::FakeHttpClient, }; use indoc::indoc; use language_model::{ LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, - LanguageModelProviderName, LanguageModelRegistry, LanguageModelRequestMessage, - LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role, StopReason, - fake_provider::FakeLanguageModel, + LanguageModelProviderName, LanguageModelRegistry, LanguageModelRequest, + LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolSchemaFormat, + LanguageModelToolUse, MessageContent, Role, StopReason, fake_provider::FakeLanguageModel, }; use pretty_assertions::assert_eq; -use project::Project; +use project::{ + Project, context_server_store::ContextServerStore, project_settings::ProjectSettings, +}; use prompt_store::ProjectContext; use reqwest_client::ReqwestClient; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::json; -use settings::SettingsStore; +use settings::{Settings, SettingsStore}; use std::{path::Path, rc::Rc, sync::Arc, time::Duration}; use util::path; @@ -931,6 +940,334 @@ async fn test_profiles(cx: &mut TestAppContext) { assert_eq!(tool_names, vec![InfiniteTool::name()]); } +#[gpui::test] +async fn test_mcp_tools(cx: &mut TestAppContext) { + let ThreadTest { + model, + thread, + context_server_store, + fs, + .. + } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + // Override profiles and wait for settings to be loaded. + fs.insert_file( + paths::settings_file(), + json!({ + "agent": { + "profiles": { + "test": { + "name": "Test Profile", + "enable_all_context_servers": true, + "tools": { + EchoTool::name(): true, + } + }, + } + } + }) + .to_string() + .into_bytes(), + ) + .await; + cx.run_until_parked(); + thread.update(cx, |thread, _| { + thread.set_profile(AgentProfileId("test".into())) + }); + + let mut mcp_tool_calls = setup_context_server( + "test_server", + vec![context_server::types::Tool { + name: "echo".into(), + description: None, + input_schema: serde_json::to_value( + EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema), + ) + .unwrap(), + output_schema: None, + annotations: None, + }], + &context_server_store, + cx, + ); + + let events = thread.update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Hey"], cx).unwrap() + }); + cx.run_until_parked(); + + // Simulate the model calling the MCP tool. + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!(tool_names_for_completion(&completion), vec!["echo"]); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + LanguageModelToolUse { + id: "tool_1".into(), + name: "echo".into(), + raw_input: json!({"text": "test"}).to_string(), + input: json!({"text": "test"}), + is_input_complete: true, + }, + )); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + let (tool_call_params, tool_call_response) = mcp_tool_calls.next().await.unwrap(); + assert_eq!(tool_call_params.name, "echo"); + assert_eq!(tool_call_params.arguments, Some(json!({"text": "test"}))); + tool_call_response + .send(context_server::types::CallToolResponse { + content: vec![context_server::types::ToolResponseContent::Text { + text: "test".into(), + }], + is_error: None, + meta: None, + structured_content: None, + }) + .unwrap(); + cx.run_until_parked(); + + assert_eq!(tool_names_for_completion(&completion), vec!["echo"]); + fake_model.send_last_completion_stream_text_chunk("Done!"); + fake_model.end_last_completion_stream(); + events.collect::>().await; + + // Send again after adding the echo tool, ensuring the name collision is resolved. + let events = thread.update(cx, |thread, cx| { + thread.add_tool(EchoTool); + thread.send(UserMessageId::new(), ["Go"], cx).unwrap() + }); + cx.run_until_parked(); + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!( + tool_names_for_completion(&completion), + vec!["echo", "test_server_echo"] + ); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + LanguageModelToolUse { + id: "tool_2".into(), + name: "test_server_echo".into(), + raw_input: json!({"text": "mcp"}).to_string(), + input: json!({"text": "mcp"}), + is_input_complete: true, + }, + )); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + LanguageModelToolUse { + id: "tool_3".into(), + name: "echo".into(), + raw_input: json!({"text": "native"}).to_string(), + input: json!({"text": "native"}), + is_input_complete: true, + }, + )); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + let (tool_call_params, tool_call_response) = mcp_tool_calls.next().await.unwrap(); + assert_eq!(tool_call_params.name, "echo"); + assert_eq!(tool_call_params.arguments, Some(json!({"text": "mcp"}))); + tool_call_response + .send(context_server::types::CallToolResponse { + content: vec![context_server::types::ToolResponseContent::Text { text: "mcp".into() }], + is_error: None, + meta: None, + structured_content: None, + }) + .unwrap(); + cx.run_until_parked(); + + // Ensure the tool results were inserted with the correct names. + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!( + completion.messages.last().unwrap().content, + vec![ + MessageContent::ToolResult(LanguageModelToolResult { + tool_use_id: "tool_3".into(), + tool_name: "echo".into(), + is_error: false, + content: "native".into(), + output: Some("native".into()), + },), + MessageContent::ToolResult(LanguageModelToolResult { + tool_use_id: "tool_2".into(), + tool_name: "test_server_echo".into(), + is_error: false, + content: "mcp".into(), + output: Some("mcp".into()), + },), + ] + ); + fake_model.end_last_completion_stream(); + events.collect::>().await; +} + +#[gpui::test] +async fn test_mcp_tool_truncation(cx: &mut TestAppContext) { + let ThreadTest { + model, + thread, + context_server_store, + fs, + .. + } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + // Set up a profile with all tools enabled + fs.insert_file( + paths::settings_file(), + json!({ + "agent": { + "profiles": { + "test": { + "name": "Test Profile", + "enable_all_context_servers": true, + "tools": { + EchoTool::name(): true, + DelayTool::name(): true, + WordListTool::name(): true, + ToolRequiringPermission::name(): true, + InfiniteTool::name(): true, + } + }, + } + } + }) + .to_string() + .into_bytes(), + ) + .await; + cx.run_until_parked(); + + thread.update(cx, |thread, _| { + thread.set_profile(AgentProfileId("test".into())); + thread.add_tool(EchoTool); + thread.add_tool(DelayTool); + thread.add_tool(WordListTool); + thread.add_tool(ToolRequiringPermission); + thread.add_tool(InfiniteTool); + }); + + // Set up multiple context servers with some overlapping tool names + let _server1_calls = setup_context_server( + "xxx", + vec![ + context_server::types::Tool { + name: "echo".into(), // Conflicts with native EchoTool + description: None, + input_schema: serde_json::to_value( + EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema), + ) + .unwrap(), + output_schema: None, + annotations: None, + }, + context_server::types::Tool { + name: "unique_tool_1".into(), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + ], + &context_server_store, + cx, + ); + + let _server2_calls = setup_context_server( + "yyy", + vec![ + context_server::types::Tool { + name: "echo".into(), // Also conflicts with native EchoTool + description: None, + input_schema: serde_json::to_value( + EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema), + ) + .unwrap(), + output_schema: None, + annotations: None, + }, + context_server::types::Tool { + name: "unique_tool_2".into(), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + context_server::types::Tool { + name: "a".repeat(MAX_TOOL_NAME_LENGTH - 2), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + context_server::types::Tool { + name: "b".repeat(MAX_TOOL_NAME_LENGTH - 1), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + ], + &context_server_store, + cx, + ); + let _server3_calls = setup_context_server( + "zzz", + vec![ + context_server::types::Tool { + name: "a".repeat(MAX_TOOL_NAME_LENGTH - 2), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + context_server::types::Tool { + name: "b".repeat(MAX_TOOL_NAME_LENGTH - 1), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + context_server::types::Tool { + name: "c".repeat(MAX_TOOL_NAME_LENGTH + 1), + description: None, + input_schema: json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + }, + ], + &context_server_store, + cx, + ); + + thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Go"], cx) + }) + .unwrap(); + cx.run_until_parked(); + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!( + tool_names_for_completion(&completion), + vec![ + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "delay", + "echo", + "infinite", + "tool_requiring_permission", + "unique_tool_1", + "unique_tool_2", + "word_list", + "xxx_echo", + "y_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "yyy_echo", + "z_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ] + ); +} + #[gpui::test] #[cfg_attr(not(feature = "e2e"), ignore)] async fn test_cancellation(cx: &mut TestAppContext) { @@ -1806,6 +2143,7 @@ struct ThreadTest { model: Arc, thread: Entity, project_context: Entity, + context_server_store: Entity, fs: Arc, } @@ -1844,6 +2182,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { WordListTool::name(): true, ToolRequiringPermission::name(): true, InfiniteTool::name(): true, + ThinkingTool::name(): true, } } } @@ -1900,8 +2239,9 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { .await; let project_context = cx.new(|_cx| ProjectContext::default()); + let context_server_store = project.read_with(cx, |project, _| project.context_server_store()); let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); + cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx)); let thread = cx.new(|cx| { Thread::new( project, @@ -1916,6 +2256,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { model, thread, project_context, + context_server_store, fs, } } @@ -1950,3 +2291,89 @@ fn watch_settings(fs: Arc, cx: &mut App) { }) .detach(); } + +fn tool_names_for_completion(completion: &LanguageModelRequest) -> Vec { + completion + .tools + .iter() + .map(|tool| tool.name.clone()) + .collect() +} + +fn setup_context_server( + name: &'static str, + tools: Vec, + context_server_store: &Entity, + cx: &mut TestAppContext, +) -> mpsc::UnboundedReceiver<( + context_server::types::CallToolParams, + oneshot::Sender, +)> { + cx.update(|cx| { + let mut settings = ProjectSettings::get_global(cx).clone(); + settings.context_servers.insert( + name.into(), + project::project_settings::ContextServerSettings::Custom { + enabled: true, + command: ContextServerCommand { + path: "somebinary".into(), + args: Vec::new(), + env: None, + }, + }, + ); + ProjectSettings::override_global(settings, cx); + }); + + let (mcp_tool_calls_tx, mcp_tool_calls_rx) = mpsc::unbounded(); + let fake_transport = context_server::test::create_fake_transport(name, cx.executor()) + .on_request::(move |_params| async move { + context_server::types::InitializeResponse { + protocol_version: context_server::types::ProtocolVersion( + context_server::types::LATEST_PROTOCOL_VERSION.to_string(), + ), + server_info: context_server::types::Implementation { + name: name.into(), + version: "1.0.0".to_string(), + }, + capabilities: context_server::types::ServerCapabilities { + tools: Some(context_server::types::ToolsCapabilities { + list_changed: Some(true), + }), + ..Default::default() + }, + meta: None, + } + }) + .on_request::(move |_params| { + let tools = tools.clone(); + async move { + context_server::types::ListToolsResponse { + tools, + next_cursor: None, + meta: None, + } + } + }) + .on_request::(move |params| { + let mcp_tool_calls_tx = mcp_tool_calls_tx.clone(); + async move { + let (response_tx, response_rx) = oneshot::channel(); + mcp_tool_calls_tx + .unbounded_send((params, response_tx)) + .unwrap(); + response_rx.await.unwrap() + } + }); + context_server_store.update(cx, |store, cx| { + store.start_server( + Arc::new(ContextServer::new( + ContextServerId(name.into()), + Arc::new(fake_transport), + )), + cx, + ); + }); + cx.run_until_parked(); + mcp_tool_calls_rx +} diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index af18afa055..c89e5875f9 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -9,15 +9,15 @@ use action_log::ActionLog; use agent::thread::{GitState, ProjectSnapshot, WorktreeSnapshot}; use agent_client_protocol as acp; use agent_settings::{ - AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_DETAILED_PROMPT, - SUMMARIZE_THREAD_PROMPT, + AgentProfileId, AgentProfileSettings, AgentSettings, CompletionMode, + SUMMARIZE_THREAD_DETAILED_PROMPT, SUMMARIZE_THREAD_PROMPT, }; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::adapt_schema_to_format; use chrono::{DateTime, Utc}; use client::{ModelRequestUsage, RequestUsage}; use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit}; -use collections::{HashMap, IndexMap}; +use collections::{HashMap, HashSet, IndexMap}; use fs::Fs; use futures::{ FutureExt, @@ -56,6 +56,7 @@ use util::{ResultExt, markdown::MarkdownCodeBlock}; use uuid::Uuid; const TOOL_CANCELED_MESSAGE: &str = "Tool canceled by user"; +pub const MAX_TOOL_NAME_LENGTH: usize = 64; /// The ID of the user prompt that initiated a request. /// @@ -627,7 +628,20 @@ impl Thread { stream: &ThreadEventStream, cx: &mut Context, ) { - let Some(tool) = self.tools.get(tool_use.name.as_ref()) else { + let tool = self.tools.get(tool_use.name.as_ref()).cloned().or_else(|| { + self.context_server_registry + .read(cx) + .servers() + .find_map(|(_, tools)| { + if let Some(tool) = tools.get(tool_use.name.as_ref()) { + Some(tool.clone()) + } else { + None + } + }) + }); + + let Some(tool) = tool else { stream .0 .unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall { @@ -1079,6 +1093,10 @@ impl Thread { self.cancel(cx); let model = self.model.clone().context("No language model configured")?; + let profile = AgentSettings::get_global(cx) + .profiles + .get(&self.profile_id) + .context("Profile not found")?; let (events_tx, events_rx) = mpsc::unbounded::>(); let event_stream = ThreadEventStream(events_tx); let message_ix = self.messages.len().saturating_sub(1); @@ -1086,6 +1104,7 @@ impl Thread { self.summary = None; self.running_turn = Some(RunningTurn { event_stream: event_stream.clone(), + tools: self.enabled_tools(profile, &model, cx), _task: cx.spawn(async move |this, cx| { log::info!("Starting agent turn execution"); @@ -1417,7 +1436,7 @@ impl Thread { ) -> Option> { cx.notify(); - let tool = self.tools.get(tool_use.name.as_ref()).cloned(); + let tool = self.tool(tool_use.name.as_ref()); let mut title = SharedString::from(&tool_use.name); let mut kind = acp::ToolKind::Other; if let Some(tool) = tool.as_ref() { @@ -1727,6 +1746,21 @@ impl Thread { cx: &mut App, ) -> Result { let model = self.model().context("No language model configured")?; + let tools = if let Some(turn) = self.running_turn.as_ref() { + turn.tools + .iter() + .filter_map(|(tool_name, tool)| { + log::trace!("Including tool: {}", tool_name); + Some(LanguageModelRequestTool { + name: tool_name.to_string(), + description: tool.description().to_string(), + input_schema: tool.input_schema(model.tool_input_format()).log_err()?, + }) + }) + .collect::>() + } else { + Vec::new() + }; log::debug!("Building completion request"); log::debug!("Completion intent: {:?}", completion_intent); @@ -1734,23 +1768,6 @@ impl Thread { let messages = self.build_request_messages(cx); log::info!("Request will include {} messages", messages.len()); - - let tools = if let Some(tools) = self.tools(cx).log_err() { - tools - .filter_map(|tool| { - let tool_name = tool.name().to_string(); - log::trace!("Including tool: {}", tool_name); - Some(LanguageModelRequestTool { - name: tool_name, - description: tool.description().to_string(), - input_schema: tool.input_schema(model.tool_input_format()).log_err()?, - }) - }) - .collect() - } else { - Vec::new() - }; - log::info!("Request includes {} tools", tools.len()); let request = LanguageModelRequest { @@ -1770,37 +1787,76 @@ impl Thread { Ok(request) } - fn tools<'a>(&'a self, cx: &'a App) -> Result>> { - let model = self.model().context("No language model configured")?; + fn enabled_tools( + &self, + profile: &AgentProfileSettings, + model: &Arc, + cx: &App, + ) -> BTreeMap> { + fn truncate(tool_name: &SharedString) -> SharedString { + if tool_name.len() > MAX_TOOL_NAME_LENGTH { + let mut truncated = tool_name.to_string(); + truncated.truncate(MAX_TOOL_NAME_LENGTH); + truncated.into() + } else { + tool_name.clone() + } + } - let profile = AgentSettings::get_global(cx) - .profiles - .get(&self.profile_id) - .context("profile not found")?; - let provider_id = model.provider_id(); - - Ok(self + let mut tools = self .tools .iter() - .filter(move |(_, tool)| tool.supported_provider(&provider_id)) .filter_map(|(tool_name, tool)| { - if profile.is_tool_enabled(tool_name) { - Some(tool) + if tool.supported_provider(&model.provider_id()) + && profile.is_tool_enabled(tool_name) + { + Some((truncate(tool_name), tool.clone())) } else { None } }) - .chain(self.context_server_registry.read(cx).servers().flat_map( - |(server_id, tools)| { - tools.iter().filter_map(|(tool_name, tool)| { - if profile.is_context_server_tool_enabled(&server_id.0, tool_name) { - Some(tool) - } else { - None - } - }) - }, - ))) + .collect::>(); + + let mut context_server_tools = Vec::new(); + let mut seen_tools = tools.keys().cloned().collect::>(); + let mut duplicate_tool_names = HashSet::default(); + for (server_id, server_tools) in self.context_server_registry.read(cx).servers() { + for (tool_name, tool) in server_tools { + if profile.is_context_server_tool_enabled(&server_id.0, &tool_name) { + let tool_name = truncate(tool_name); + if !seen_tools.insert(tool_name.clone()) { + duplicate_tool_names.insert(tool_name.clone()); + } + context_server_tools.push((server_id.clone(), tool_name, tool.clone())); + } + } + } + + // When there are duplicate tool names, disambiguate by prefixing them + // with the server ID. In the rare case there isn't enough space for the + // disambiguated tool name, keep only the last tool with this name. + for (server_id, tool_name, tool) in context_server_tools { + if duplicate_tool_names.contains(&tool_name) { + let available = MAX_TOOL_NAME_LENGTH.saturating_sub(tool_name.len()); + if available >= 2 { + let mut disambiguated = server_id.0.to_string(); + disambiguated.truncate(available - 1); + disambiguated.push('_'); + disambiguated.push_str(&tool_name); + tools.insert(disambiguated.into(), tool.clone()); + } else { + tools.insert(tool_name, tool.clone()); + } + } else { + tools.insert(tool_name, tool.clone()); + } + } + + tools + } + + fn tool(&self, name: &str) -> Option> { + self.running_turn.as_ref()?.tools.get(name).cloned() } fn build_request_messages(&self, cx: &App) -> Vec { @@ -1965,6 +2021,8 @@ struct RunningTurn { /// The current event stream for the running turn. Used to report a final /// cancellation event if we cancel the turn. event_stream: ThreadEventStream, + /// The tools that were enabled for this turn. + tools: BTreeMap>, } impl RunningTurn { diff --git a/crates/context_server/src/test.rs b/crates/context_server/src/test.rs index dedf589664..008542ab24 100644 --- a/crates/context_server/src/test.rs +++ b/crates/context_server/src/test.rs @@ -1,6 +1,6 @@ use anyhow::Context as _; use collections::HashMap; -use futures::{Stream, StreamExt as _, lock::Mutex}; +use futures::{FutureExt, Stream, StreamExt as _, future::BoxFuture, lock::Mutex}; use gpui::BackgroundExecutor; use std::{pin::Pin, sync::Arc}; @@ -14,9 +14,12 @@ pub fn create_fake_transport( executor: BackgroundExecutor, ) -> FakeTransport { let name = name.into(); - FakeTransport::new(executor).on_request::(move |_params| { - create_initialize_response(name.clone()) - }) + FakeTransport::new(executor).on_request::( + move |_params| { + let name = name.clone(); + async move { create_initialize_response(name.clone()) } + }, + ) } fn create_initialize_response(server_name: String) -> InitializeResponse { @@ -32,8 +35,10 @@ fn create_initialize_response(server_name: String) -> InitializeResponse { } pub struct FakeTransport { - request_handlers: - HashMap<&'static str, Arc serde_json::Value + Send + Sync>>, + request_handlers: HashMap< + &'static str, + Arc BoxFuture<'static, serde_json::Value>>, + >, tx: futures::channel::mpsc::UnboundedSender, rx: Arc>>, executor: BackgroundExecutor, @@ -50,18 +55,25 @@ impl FakeTransport { } } - pub fn on_request( + pub fn on_request( mut self, - handler: impl Fn(T::Params) -> T::Response + Send + Sync + 'static, - ) -> Self { + handler: impl 'static + Send + Sync + Fn(T::Params) -> Fut, + ) -> Self + where + T: crate::types::Request, + Fut: 'static + Send + Future, + { self.request_handlers.insert( T::METHOD, Arc::new(move |value| { - let params = value.get("params").expect("Missing parameters").clone(); + let params = value + .get("params") + .cloned() + .unwrap_or(serde_json::Value::Null); let params: T::Params = serde_json::from_value(params).expect("Invalid parameters received"); let response = handler(params); - serde_json::to_value(response).unwrap() + async move { serde_json::to_value(response.await).unwrap() }.boxed() }), ); self @@ -77,7 +89,7 @@ impl Transport for FakeTransport { if let Some(method) = msg.get("method") { let method = method.as_str().expect("Invalid method received"); if let Some(handler) = self.request_handlers.get(method) { - let payload = handler(msg); + let payload = handler(msg).await; let response = serde_json::json!({ "jsonrpc": "2.0", "id": id, From 54df43e06f5340c6e9ae5540550a3a2f102a521f Mon Sep 17 00:00:00 2001 From: Sarah Price <83782422+Louis454545@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:18:46 +0200 Subject: [PATCH 615/693] Fix cursor movement in protected files on backspace/delete (#36753) ## Summary Fixes cursor movement behavior in protected files (like Default Settings) when pressing backspace or delete keys. Previously, these keys would cause unwanted cursor movement instead of being ignored as expected in read-only files. ## Changes - Added read-only checks to `backspace()` and `delete()` methods in the editor - Consistent with existing pattern used by other editing methods (`indent()`, `outdent()`, `undo()`, etc.) ## Test Plan 1. Open Default Settings in Zed 2. Place cursor at arbitrary position (not at start/end of file) 3. Press backspace - cursor should remain in place (no movement) 4. Press delete - cursor should remain in place (no movement) Fixes #36302 Release Notes: - Fixed backspace and delete keys moving caret in protected files Co-authored-by: Claude --- crates/editor/src/editor.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2af8e6c0e4..216aa2463b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9809,6 +9809,9 @@ impl Editor { } pub fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context) { + if self.read_only(cx) { + return; + } self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.select_autoclose_pair(window, cx); @@ -9902,6 +9905,9 @@ impl Editor { } pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context) { + if self.read_only(cx) { + return; + } self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.change_selections(Default::default(), window, cx, |s| { From 92bbcdeb7daeaaea5dbb2148a457861fa7947603 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:34:55 +0200 Subject: [PATCH 616/693] workspace: Do not prompt for hanging up current call when replacing last visible project (#36697) This fixes a bug where in order to open a new project in a call (even if it's not shared), you need to hang up. Release Notes: - N/A --- crates/workspace/src/workspace.rs | 50 ++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 499e4f4619..44aa94fe61 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2249,27 +2249,43 @@ impl Workspace { })?; if let Some(active_call) = active_call - && close_intent != CloseIntent::Quit && workspace_count == 1 && active_call.read_with(cx, |call, _| call.room().is_some())? { - let answer = cx.update(|window, cx| { - window.prompt( - PromptLevel::Warning, - "Do you want to leave the current call?", - None, - &["Close window and hang up", "Cancel"], - cx, - ) - })?; + if close_intent == CloseIntent::CloseWindow { + let answer = cx.update(|window, cx| { + window.prompt( + PromptLevel::Warning, + "Do you want to leave the current call?", + None, + &["Close window and hang up", "Cancel"], + cx, + ) + })?; - if answer.await.log_err() == Some(1) { - return anyhow::Ok(false); - } else { - active_call - .update(cx, |call, cx| call.hang_up(cx))? - .await - .log_err(); + if answer.await.log_err() == Some(1) { + return anyhow::Ok(false); + } else { + active_call + .update(cx, |call, cx| call.hang_up(cx))? + .await + .log_err(); + } + } + if close_intent == CloseIntent::ReplaceWindow { + _ = active_call.update(cx, |this, cx| { + let workspace = cx + .windows() + .iter() + .filter_map(|window| window.downcast::()) + .next() + .unwrap(); + let project = workspace.read(cx)?.project.clone(); + if project.read(cx).is_shared() { + this.unshare_project(project, cx)?; + } + Ok::<_, anyhow::Error>(()) + })?; } } From 3d2fa72d1fcf177e2beee1433c97e3bfa7adc09a Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 22 Aug 2025 16:58:17 +0300 Subject: [PATCH 617/693] Make word completions less intrusive (#36745) Introduce `min_words_query_len` threshold for automatic word completion display, and set it to 3 by default. Re-enable word completions in Markdown and Plaintext. Release Notes: - Introduced `min_words_query_len` threshold for automatic word completion display, and set it to 3 by default to make them less intrusive --- assets/settings/default.json | 11 ++-- .../src/copilot_completion_provider.rs | 2 + crates/editor/src/editor.rs | 35 ++++++++---- crates/editor/src/editor_tests.rs | 57 +++++++++++++++++++ crates/language/src/language_settings.rs | 13 ++++- docs/src/configuring-zed.md | 12 ++++ 6 files changed, 109 insertions(+), 21 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index c290baf003..014b483250 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1503,6 +1503,11 @@ // // Default: fallback "words": "fallback", + // Minimum number of characters required to automatically trigger word-based completions. + // Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command. + // + // Default: 3 + "words_min_length": 3, // Whether to fetch LSP completions or not. // // Default: true @@ -1642,9 +1647,6 @@ "use_on_type_format": false, "allow_rewrap": "anywhere", "soft_wrap": "editor_width", - "completions": { - "words": "disabled" - }, "prettier": { "allowed": true } @@ -1658,9 +1660,6 @@ } }, "Plain Text": { - "completions": { - "words": "disabled" - }, "allow_rewrap": "anywhere" }, "Python": { diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 9308500ed4..52d75175e5 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -301,6 +301,7 @@ mod tests { init_test(cx, |settings| { settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -533,6 +534,7 @@ mod tests { init_test(cx, |settings| { settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 216aa2463b..a59eb930c3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5576,6 +5576,11 @@ impl Editor { .as_ref() .is_none_or(|query| !query.chars().any(|c| c.is_digit(10))); + let omit_word_completions = match &query { + Some(query) => query.chars().count() < completion_settings.words_min_length, + None => completion_settings.words_min_length != 0, + }; + let (mut words, provider_responses) = match &provider { Some(provider) => { let provider_responses = provider.completions( @@ -5587,9 +5592,11 @@ impl Editor { cx, ); - let words = match completion_settings.words { - WordsCompletionMode::Disabled => Task::ready(BTreeMap::default()), - WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => cx + let words = match (omit_word_completions, completion_settings.words) { + (true, _) | (_, WordsCompletionMode::Disabled) => { + Task::ready(BTreeMap::default()) + } + (false, WordsCompletionMode::Enabled | WordsCompletionMode::Fallback) => cx .background_spawn(async move { buffer_snapshot.words_in_range(WordsQuery { fuzzy_contents: None, @@ -5601,16 +5608,20 @@ impl Editor { (words, provider_responses) } - None => ( - cx.background_spawn(async move { - buffer_snapshot.words_in_range(WordsQuery { - fuzzy_contents: None, - range: word_search_range, - skip_digits, + None => { + let words = if omit_word_completions { + Task::ready(BTreeMap::default()) + } else { + cx.background_spawn(async move { + buffer_snapshot.words_in_range(WordsQuery { + fuzzy_contents: None, + range: word_search_range, + skip_digits, + }) }) - }), - Task::ready(Ok(Vec::new())), - ), + }; + (words, Task::ready(Ok(Vec::new()))) + } }; let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 96261fdb2c..5b854e3a97 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -12237,6 +12237,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { settings.defaults.completions = Some(CompletionSettings { lsp_insert_mode, words: WordsCompletionMode::Disabled, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, }); @@ -12295,6 +12296,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) update_test_language_settings(&mut cx, |settings| { settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, + words_min_length: 0, // set the opposite here to ensure that the action is overriding the default behavior lsp_insert_mode: LspInsertMode::Insert, lsp: true, @@ -12331,6 +12333,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) update_test_language_settings(&mut cx, |settings| { settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, + words_min_length: 0, // set the opposite here to ensure that the action is overriding the default behavior lsp_insert_mode: LspInsertMode::Replace, lsp: true, @@ -13072,6 +13075,7 @@ async fn test_word_completion(cx: &mut TestAppContext) { init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Fallback, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 10, lsp_insert_mode: LspInsertMode::Insert, @@ -13168,6 +13172,7 @@ async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Enabled, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -13231,6 +13236,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) { init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -13304,6 +13310,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Fallback, + words_min_length: 0, lsp: false, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -13361,6 +13368,56 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) { + init_test(cx, |language_settings| { + language_settings.defaults.completions = Some(CompletionSettings { + words: WordsCompletionMode::Enabled, + words_min_length: 3, + lsp: true, + lsp_fetch_timeout_ms: 0, + lsp_insert_mode: LspInsertMode::Insert, + }); + }); + + let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await; + cx.set_state(indoc! {"ˇ + wow + wowen + wowser + "}); + cx.simulate_keystroke("w"); + cx.executor().run_until_parked(); + cx.update_editor(|editor, _, _| { + if editor.context_menu.borrow_mut().is_some() { + panic!( + "expected completion menu to be hidden, as words completion threshold is not met" + ); + } + }); + + cx.simulate_keystroke("o"); + cx.executor().run_until_parked(); + cx.update_editor(|editor, _, _| { + if editor.context_menu.borrow_mut().is_some() { + panic!( + "expected completion menu to be hidden, as words completion threshold is not met still" + ); + } + }); + + cx.simulate_keystroke("w"); + cx.executor().run_until_parked(); + cx.update_editor(|editor, _, _| { + if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() + { + assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word"); + } else { + panic!("expected completion menu to be open after the word completions threshold is met"); + } + }); +} + fn gen_text_edit(params: &CompletionParams, text: &str) -> Option { let position = || lsp::Position { line: params.text_document_position.position.line, diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 386ad19747..0f82d3997f 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -350,6 +350,12 @@ pub struct CompletionSettings { /// Default: `fallback` #[serde(default = "default_words_completion_mode")] pub words: WordsCompletionMode, + /// How many characters has to be in the completions query to automatically show the words-based completions. + /// Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command. + /// + /// Default: 3 + #[serde(default = "default_3")] + pub words_min_length: usize, /// Whether to fetch LSP completions or not. /// /// Default: true @@ -359,7 +365,7 @@ pub struct CompletionSettings { /// When set to 0, waits indefinitely. /// /// Default: 0 - #[serde(default = "default_lsp_fetch_timeout_ms")] + #[serde(default)] pub lsp_fetch_timeout_ms: u64, /// Controls how LSP completions are inserted. /// @@ -405,8 +411,8 @@ fn default_lsp_insert_mode() -> LspInsertMode { LspInsertMode::ReplaceSuffix } -fn default_lsp_fetch_timeout_ms() -> u64 { - 0 +fn default_3() -> usize { + 3 } /// The settings for a particular language. @@ -1468,6 +1474,7 @@ impl settings::Settings for AllLanguageSettings { } else { d.completions = Some(CompletionSettings { words: mode, + words_min_length: 3, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::ReplaceSuffix, diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 696370e310..fb139db6e4 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -2425,6 +2425,7 @@ Examples: { "completions": { "words": "fallback", + "words_min_length": 3, "lsp": true, "lsp_fetch_timeout_ms": 0, "lsp_insert_mode": "replace_suffix" @@ -2444,6 +2445,17 @@ Examples: 2. `fallback` - Only if LSP response errors or times out, use document's words to show completions 3. `disabled` - Never fetch or complete document's words for completions (word-based completions can still be queried via a separate action) +### Min Words Query Length + +- Description: Minimum number of characters required to automatically trigger word-based completions. + Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command. +- Setting: `words_min_length` +- Default: `3` + +**Options** + +Positive integer values + ### LSP - Description: Whether to fetch LSP completions or not. From 8204ef1e51cb89dc46415e5efe12c8705d51dfdf Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:45:47 -0400 Subject: [PATCH 618/693] onboarding: Remove accept AI ToS from within Zed (#36612) Users now accept ToS from Zed's website when they sign in to Zed the first time. So it's no longer possible that a signed in account could not have accepted the ToS. Release Notes: - N/A --------- Co-authored-by: Mikayla Maki --- crates/agent_ui/src/active_thread.rs | 5 - crates/agent_ui/src/agent_configuration.rs | 10 +- crates/agent_ui/src/agent_panel.rs | 15 +- crates/agent_ui/src/message_editor.rs | 7 +- crates/agent_ui/src/text_thread_editor.rs | 16 -- crates/ai_onboarding/src/ai_onboarding.rs | 77 +------ crates/client/src/user.rs | 44 +--- .../cloud_api_client/src/cloud_api_client.rs | 28 --- crates/edit_prediction/src/edit_prediction.rs | 8 - .../src/edit_prediction_button.rs | 8 +- crates/editor/src/editor.rs | 41 ---- crates/language_model/src/language_model.rs | 12 +- crates/language_model/src/registry.rs | 12 - crates/language_models/src/provider/cloud.rs | 213 ++---------------- .../zed/src/zed/edit_prediction_registry.rs | 25 -- crates/zeta/src/zeta.rs | 22 +- 16 files changed, 44 insertions(+), 499 deletions(-) diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 2cad913295..e0cecad6e2 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -1595,11 +1595,6 @@ impl ActiveThread { return; }; - if model.provider.must_accept_terms(cx) { - cx.notify(); - return; - } - let edited_text = state.editor.read(cx).text(cx); let creases = state.editor.update(cx, extract_message_creases); diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 00e48efdac..f33f0ba032 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -93,14 +93,6 @@ impl AgentConfiguration { let scroll_handle = ScrollHandle::new(); let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); - let mut expanded_provider_configurations = HashMap::default(); - if LanguageModelRegistry::read_global(cx) - .provider(&ZED_CLOUD_PROVIDER_ID) - .is_some_and(|cloud_provider| cloud_provider.must_accept_terms(cx)) - { - expanded_provider_configurations.insert(ZED_CLOUD_PROVIDER_ID, true); - } - let mut this = Self { fs, language_registry, @@ -109,7 +101,7 @@ impl AgentConfiguration { configuration_views_by_provider: HashMap::default(), context_server_store, expanded_context_server_tools: HashMap::default(), - expanded_provider_configurations, + expanded_provider_configurations: HashMap::default(), tools, _registry_subscription: registry_subscription, scroll_handle, diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 469898d10f..d0fb676fd2 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -54,9 +54,7 @@ use gpui::{ Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between, }; use language::LanguageRegistry; -use language_model::{ - ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry, -}; +use language_model::{ConfigurationError, ConfiguredModel, LanguageModelRegistry}; use project::{DisableAiSettings, Project, ProjectPath, Worktree}; use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; use rules_library::{RulesLibrary, open_rules_library}; @@ -3203,17 +3201,6 @@ impl AgentPanel { ConfigurationError::ModelNotFound | ConfigurationError::ProviderNotAuthenticated(_) | ConfigurationError::NoProvider => callout.into_any_element(), - ConfigurationError::ProviderPendingTermsAcceptance(provider) => { - Banner::new() - .severity(Severity::Warning) - .child(h_flex().w_full().children( - provider.render_accept_terms( - LanguageModelProviderTosView::ThreadEmptyState, - cx, - ), - )) - .into_any_element() - } } } diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index bed10e90a7..45e7529ec2 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -378,18 +378,13 @@ impl MessageEditor { } fn send_to_model(&mut self, window: &mut Window, cx: &mut Context) { - let Some(ConfiguredModel { model, provider }) = self + let Some(ConfiguredModel { model, .. }) = self .thread .update(cx, |thread, cx| thread.get_or_init_configured_model(cx)) else { return; }; - if provider.must_accept_terms(cx) { - cx.notify(); - return; - } - let (user_message, user_message_creases) = self.editor.update(cx, |editor, cx| { let creases = extract_message_creases(editor, cx); let text = editor.text(cx); diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 9fbd90c4a6..edb672a872 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -190,7 +190,6 @@ pub struct TextThreadEditor { invoked_slash_command_creases: HashMap, _subscriptions: Vec, last_error: Option, - show_accept_terms: bool, pub(crate) slash_menu_handle: PopoverMenuHandle>, // dragged_file_worktrees is used to keep references to worktrees that were added @@ -289,7 +288,6 @@ impl TextThreadEditor { invoked_slash_command_creases: HashMap::default(), _subscriptions, last_error: None, - show_accept_terms: false, slash_menu_handle: Default::default(), dragged_file_worktrees: Vec::new(), language_model_selector: cx.new(|cx| { @@ -367,20 +365,7 @@ impl TextThreadEditor { } fn send_to_model(&mut self, window: &mut Window, cx: &mut Context) { - let provider = LanguageModelRegistry::read_global(cx) - .default_model() - .map(|default| default.provider); - if provider - .as_ref() - .is_some_and(|provider| provider.must_accept_terms(cx)) - { - self.show_accept_terms = true; - cx.notify(); - return; - } - self.last_error = None; - if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) { let new_selection = { let cursor = user_message @@ -1930,7 +1915,6 @@ impl TextThreadEditor { ConfigurationError::NoProvider | ConfigurationError::ModelNotFound | ConfigurationError::ProviderNotAuthenticated(_) => true, - ConfigurationError::ProviderPendingTermsAcceptance(_) => self.show_accept_terms, } } diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index 717abebfd1..6d8ac64725 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -19,7 +19,7 @@ use std::sync::Arc; use client::{Client, UserStore, zed_urls}; use gpui::{AnyElement, Entity, IntoElement, ParentElement}; -use ui::{Divider, RegisterComponent, TintColor, Tooltip, prelude::*}; +use ui::{Divider, RegisterComponent, Tooltip, prelude::*}; #[derive(PartialEq)] pub enum SignInStatus { @@ -43,12 +43,10 @@ impl From for SignInStatus { #[derive(RegisterComponent, IntoElement)] pub struct ZedAiOnboarding { pub sign_in_status: SignInStatus, - pub has_accepted_terms_of_service: bool, pub plan: Option, pub account_too_young: bool, pub continue_with_zed_ai: Arc, pub sign_in: Arc, - pub accept_terms_of_service: Arc, pub dismiss_onboarding: Option>, } @@ -64,17 +62,9 @@ impl ZedAiOnboarding { Self { sign_in_status: status.into(), - has_accepted_terms_of_service: store.has_accepted_terms_of_service(), plan: store.plan(), account_too_young: store.account_too_young(), continue_with_zed_ai, - accept_terms_of_service: Arc::new({ - let store = user_store.clone(); - move |_window, cx| { - let task = store.update(cx, |store, cx| store.accept_terms_of_service(cx)); - task.detach_and_log_err(cx); - } - }), sign_in: Arc::new(move |_window, cx| { cx.spawn({ let client = client.clone(); @@ -94,42 +84,6 @@ impl ZedAiOnboarding { self } - fn render_accept_terms_of_service(&self) -> AnyElement { - v_flex() - .gap_1() - .w_full() - .child(Headline::new("Accept Terms of Service")) - .child( - Label::new("We don’t sell your data, track you across the web, or compromise your privacy.") - .color(Color::Muted) - .mb_2(), - ) - .child( - Button::new("terms_of_service", "Review Terms of Service") - .full_width() - .style(ButtonStyle::Outlined) - .icon(IconName::ArrowUpRight) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .on_click(move |_, _window, cx| { - telemetry::event!("Review Terms of Service Clicked"); - cx.open_url(&zed_urls::terms_of_service(cx)) - }), - ) - .child( - Button::new("accept_terms", "Accept") - .full_width() - .style(ButtonStyle::Tinted(TintColor::Accent)) - .on_click({ - let callback = self.accept_terms_of_service.clone(); - move |_, window, cx| { - telemetry::event!("Terms of Service Accepted"); - (callback)(window, cx)} - }), - ) - .into_any_element() - } - fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement { let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn); let plan_definitions = PlanDefinitions; @@ -359,14 +313,10 @@ impl ZedAiOnboarding { impl RenderOnce for ZedAiOnboarding { fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement { if matches!(self.sign_in_status, SignInStatus::SignedIn) { - if self.has_accepted_terms_of_service { - match self.plan { - None | Some(Plan::ZedFree) => self.render_free_plan_state(cx), - Some(Plan::ZedProTrial) => self.render_trial_state(cx), - Some(Plan::ZedPro) => self.render_pro_plan_state(cx), - } - } else { - self.render_accept_terms_of_service() + match self.plan { + None | Some(Plan::ZedFree) => self.render_free_plan_state(cx), + Some(Plan::ZedProTrial) => self.render_trial_state(cx), + Some(Plan::ZedPro) => self.render_pro_plan_state(cx), } } else { self.render_sign_in_disclaimer(cx) @@ -390,18 +340,15 @@ impl Component for ZedAiOnboarding { fn preview(_window: &mut Window, _cx: &mut App) -> Option { fn onboarding( sign_in_status: SignInStatus, - has_accepted_terms_of_service: bool, plan: Option, account_too_young: bool, ) -> AnyElement { ZedAiOnboarding { sign_in_status, - has_accepted_terms_of_service, plan, account_too_young, continue_with_zed_ai: Arc::new(|_, _| {}), sign_in: Arc::new(|_, _| {}), - accept_terms_of_service: Arc::new(|_, _| {}), dismiss_onboarding: None, } .into_any_element() @@ -415,27 +362,23 @@ impl Component for ZedAiOnboarding { .children(vec![ single_example( "Not Signed-in", - onboarding(SignInStatus::SignedOut, false, None, false), - ), - single_example( - "Not Accepted ToS", - onboarding(SignInStatus::SignedIn, false, None, false), + onboarding(SignInStatus::SignedOut, None, false), ), single_example( "Young Account", - onboarding(SignInStatus::SignedIn, true, None, true), + onboarding(SignInStatus::SignedIn, None, true), ), single_example( "Free Plan", - onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedFree), false), + onboarding(SignInStatus::SignedIn, Some(Plan::ZedFree), false), ), single_example( "Pro Trial", - onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedProTrial), false), + onboarding(SignInStatus::SignedIn, Some(Plan::ZedProTrial), false), ), single_example( "Pro Plan", - onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedPro), false), + onboarding(SignInStatus::SignedIn, Some(Plan::ZedPro), false), ), ]) .into_any_element(), diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 20f99e3944..1f8174dbc3 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,5 +1,5 @@ use super::{Client, Status, TypedEnvelope, proto}; -use anyhow::{Context as _, Result, anyhow}; +use anyhow::{Context as _, Result}; use chrono::{DateTime, Utc}; use cloud_api_client::websocket_protocol::MessageToClient; use cloud_api_client::{GetAuthenticatedUserResponse, PlanInfo}; @@ -116,7 +116,6 @@ pub struct UserStore { edit_prediction_usage: Option, plan_info: Option, current_user: watch::Receiver>>, - accepted_tos_at: Option>, contacts: Vec>, incoming_contact_requests: Vec>, outgoing_contact_requests: Vec>, @@ -194,7 +193,6 @@ impl UserStore { plan_info: None, model_request_usage: None, edit_prediction_usage: None, - accepted_tos_at: None, contacts: Default::default(), incoming_contact_requests: Default::default(), participant_indices: Default::default(), @@ -271,7 +269,6 @@ impl UserStore { Status::SignedOut => { current_user_tx.send(None).await.ok(); this.update(cx, |this, cx| { - this.accepted_tos_at = None; cx.emit(Event::PrivateUserInfoUpdated); cx.notify(); this.clear_contacts() @@ -791,19 +788,6 @@ impl UserStore { .set_authenticated_user_info(Some(response.user.metrics_id.clone()), staff); } - let accepted_tos_at = { - #[cfg(debug_assertions)] - if std::env::var("ZED_IGNORE_ACCEPTED_TOS").is_ok() { - None - } else { - response.user.accepted_tos_at - } - - #[cfg(not(debug_assertions))] - response.user.accepted_tos_at - }; - - self.accepted_tos_at = Some(accepted_tos_at); self.model_request_usage = Some(ModelRequestUsage(RequestUsage { limit: response.plan.usage.model_requests.limit, amount: response.plan.usage.model_requests.used as i32, @@ -846,32 +830,6 @@ impl UserStore { self.current_user.clone() } - pub fn has_accepted_terms_of_service(&self) -> bool { - self.accepted_tos_at - .is_some_and(|accepted_tos_at| accepted_tos_at.is_some()) - } - - pub fn accept_terms_of_service(&self, cx: &Context) -> Task> { - if self.current_user().is_none() { - return Task::ready(Err(anyhow!("no current user"))); - }; - - let client = self.client.clone(); - cx.spawn(async move |this, cx| -> anyhow::Result<()> { - let client = client.upgrade().context("client not found")?; - let response = client - .cloud_client() - .accept_terms_of_service() - .await - .context("error accepting tos")?; - this.update(cx, |this, cx| { - this.accepted_tos_at = Some(response.user.accepted_tos_at); - cx.emit(Event::PrivateUserInfoUpdated); - })?; - Ok(()) - }) - } - fn load_users( &self, request: impl RequestMessage, diff --git a/crates/cloud_api_client/src/cloud_api_client.rs b/crates/cloud_api_client/src/cloud_api_client.rs index 205f3e2432..7fd96fcef0 100644 --- a/crates/cloud_api_client/src/cloud_api_client.rs +++ b/crates/cloud_api_client/src/cloud_api_client.rs @@ -115,34 +115,6 @@ impl CloudApiClient { })) } - pub async fn accept_terms_of_service(&self) -> Result { - let request = self.build_request( - Request::builder().method(Method::POST).uri( - self.http_client - .build_zed_cloud_url("/client/terms_of_service/accept", &[])? - .as_ref(), - ), - AsyncBody::default(), - )?; - - let mut response = self.http_client.send(request).await?; - - if !response.status().is_success() { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - anyhow::bail!( - "Failed to accept terms of service.\nStatus: {:?}\nBody: {body}", - response.status() - ) - } - - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - Ok(serde_json::from_str(&body)?) - } - pub async fn create_llm_token( &self, system_id: Option, diff --git a/crates/edit_prediction/src/edit_prediction.rs b/crates/edit_prediction/src/edit_prediction.rs index 964f202934..6b695af1ae 100644 --- a/crates/edit_prediction/src/edit_prediction.rs +++ b/crates/edit_prediction/src/edit_prediction.rs @@ -89,9 +89,6 @@ pub trait EditPredictionProvider: 'static + Sized { debounce: bool, cx: &mut Context, ); - fn needs_terms_acceptance(&self, _cx: &App) -> bool { - false - } fn cycle( &mut self, buffer: Entity, @@ -124,7 +121,6 @@ pub trait EditPredictionProviderHandle { fn data_collection_state(&self, cx: &App) -> DataCollectionState; fn usage(&self, cx: &App) -> Option; fn toggle_data_collection(&self, cx: &mut App); - fn needs_terms_acceptance(&self, cx: &App) -> bool; fn is_refreshing(&self, cx: &App) -> bool; fn refresh( &self, @@ -196,10 +192,6 @@ where self.read(cx).is_enabled(buffer, cursor_position, cx) } - fn needs_terms_acceptance(&self, cx: &App) -> bool { - self.read(cx).needs_terms_acceptance(cx) - } - fn is_refreshing(&self, cx: &App) -> bool { self.read(cx).is_refreshing() } diff --git a/crates/edit_prediction_button/src/edit_prediction_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs index 4f69af7ee4..0e3fe8cb1a 100644 --- a/crates/edit_prediction_button/src/edit_prediction_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -242,13 +242,9 @@ impl Render for EditPredictionButton { IconName::ZedPredictDisabled }; - if zeta::should_show_upsell_modal(&self.user_store, cx) { + if zeta::should_show_upsell_modal() { let tooltip_meta = if self.user_store.read(cx).current_user().is_some() { - if self.user_store.read(cx).has_accepted_terms_of_service() { - "Choose a Plan" - } else { - "Accept the Terms of Service" - } + "Choose a Plan" } else { "Sign In" }; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a59eb930c3..29e009fdf8 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -253,7 +253,6 @@ pub type RenderDiffHunkControlsFn = Arc< enum ReportEditorEvent { Saved { auto_saved: bool }, EditorOpened, - ZetaTosClicked, Closed, } @@ -262,7 +261,6 @@ impl ReportEditorEvent { match self { Self::Saved { .. } => "Editor Saved", Self::EditorOpened => "Editor Opened", - Self::ZetaTosClicked => "Edit Prediction Provider ToS Clicked", Self::Closed => "Editor Closed", } } @@ -9180,45 +9178,6 @@ impl Editor { let provider = self.edit_prediction_provider.as_ref()?; let provider_icon = Self::get_prediction_provider_icon_name(&self.edit_prediction_provider); - if provider.provider.needs_terms_acceptance(cx) { - return Some( - h_flex() - .min_w(min_width) - .flex_1() - .px_2() - .py_1() - .gap_3() - .elevation_2(cx) - .hover(|style| style.bg(cx.theme().colors().element_hover)) - .id("accept-terms") - .cursor_pointer() - .on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default()) - .on_click(cx.listener(|this, _event, window, cx| { - cx.stop_propagation(); - this.report_editor_event(ReportEditorEvent::ZetaTosClicked, None, cx); - window.dispatch_action( - zed_actions::OpenZedPredictOnboarding.boxed_clone(), - cx, - ); - })) - .child( - h_flex() - .flex_1() - .gap_2() - .child(Icon::new(provider_icon)) - .child(Label::new("Accept Terms of Service")) - .child(div().w_full()) - .child( - Icon::new(IconName::ArrowUpRight) - .color(Color::Muted) - .size(IconSize::Small), - ) - .into_any_element(), - ) - .into_any(), - ); - } - let is_refreshing = provider.provider.is_refreshing(cx); fn pending_completion_container(icon: IconName) -> Div { diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index 158bebcbbf..e0a3866443 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -14,7 +14,7 @@ use client::Client; use cloud_llm_client::{CompletionMode, CompletionRequestStatus}; use futures::FutureExt; use futures::{StreamExt, future::BoxFuture, stream::BoxStream}; -use gpui::{AnyElement, AnyView, App, AsyncApp, SharedString, Task, Window}; +use gpui::{AnyView, App, AsyncApp, SharedString, Task, Window}; use http_client::{StatusCode, http}; use icons::IconName; use parking_lot::Mutex; @@ -640,16 +640,6 @@ pub trait LanguageModelProvider: 'static { window: &mut Window, cx: &mut App, ) -> AnyView; - fn must_accept_terms(&self, _cx: &App) -> bool { - false - } - fn render_accept_terms( - &self, - _view: LanguageModelProviderTosView, - _cx: &mut App, - ) -> Option { - None - } fn reset_credentials(&self, cx: &mut App) -> Task>; } diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index bcbb3404a8..c7693a64c7 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -24,9 +24,6 @@ pub enum ConfigurationError { ModelNotFound, #[error("{} LLM provider is not configured.", .0.name().0)] ProviderNotAuthenticated(Arc), - #[error("Using the {} LLM provider requires accepting the Terms of Service.", - .0.name().0)] - ProviderPendingTermsAcceptance(Arc), } impl std::fmt::Debug for ConfigurationError { @@ -37,9 +34,6 @@ impl std::fmt::Debug for ConfigurationError { Self::ProviderNotAuthenticated(provider) => { write!(f, "ProviderNotAuthenticated({})", provider.id()) } - Self::ProviderPendingTermsAcceptance(provider) => { - write!(f, "ProviderPendingTermsAcceptance({})", provider.id()) - } } } } @@ -198,12 +192,6 @@ impl LanguageModelRegistry { return Some(ConfigurationError::ProviderNotAuthenticated(model.provider)); } - if model.provider.must_accept_terms(cx) { - return Some(ConfigurationError::ProviderPendingTermsAcceptance( - model.provider, - )); - } - None } diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 8e4b786935..fb6e2fb1e4 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -23,9 +23,9 @@ use language_model::{ AuthenticateError, LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, - LanguageModelProviderState, LanguageModelProviderTosView, LanguageModelRequest, - LanguageModelToolChoice, LanguageModelToolSchemaFormat, LlmApiToken, - ModelRequestLimitReachedError, PaymentRequiredError, RateLimiter, RefreshLlmTokenListener, + LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, + LanguageModelToolSchemaFormat, LlmApiToken, ModelRequestLimitReachedError, + PaymentRequiredError, RateLimiter, RefreshLlmTokenListener, }; use release_channel::AppVersion; use schemars::JsonSchema; @@ -118,7 +118,6 @@ pub struct State { llm_api_token: LlmApiToken, user_store: Entity, status: client::Status, - accept_terms_of_service_task: Option>>, models: Vec>, default_model: Option>, default_fast_model: Option>, @@ -142,7 +141,6 @@ impl State { llm_api_token: LlmApiToken::default(), user_store, status, - accept_terms_of_service_task: None, models: Vec::new(), default_model: None, default_fast_model: None, @@ -197,24 +195,6 @@ impl State { state.update(cx, |_, cx| cx.notify()) }) } - - fn has_accepted_terms_of_service(&self, cx: &App) -> bool { - self.user_store.read(cx).has_accepted_terms_of_service() - } - - fn accept_terms_of_service(&mut self, cx: &mut Context) { - let user_store = self.user_store.clone(); - self.accept_terms_of_service_task = Some(cx.spawn(async move |this, cx| { - let _ = user_store - .update(cx, |store, cx| store.accept_terms_of_service(cx))? - .await; - this.update(cx, |this, cx| { - this.accept_terms_of_service_task = None; - cx.notify() - }) - })); - } - fn update_models(&mut self, response: ListModelsResponse, cx: &mut Context) { let mut models = Vec::new(); @@ -384,7 +364,7 @@ impl LanguageModelProvider for CloudLanguageModelProvider { fn is_authenticated(&self, cx: &App) -> bool { let state = self.state.read(cx); - !state.is_signed_out(cx) && state.has_accepted_terms_of_service(cx) + !state.is_signed_out(cx) } fn authenticate(&self, _cx: &mut App) -> Task> { @@ -401,112 +381,11 @@ impl LanguageModelProvider for CloudLanguageModelProvider { .into() } - fn must_accept_terms(&self, cx: &App) -> bool { - !self.state.read(cx).has_accepted_terms_of_service(cx) - } - - fn render_accept_terms( - &self, - view: LanguageModelProviderTosView, - cx: &mut App, - ) -> Option { - let state = self.state.read(cx); - if state.has_accepted_terms_of_service(cx) { - return None; - } - Some( - render_accept_terms(view, state.accept_terms_of_service_task.is_some(), { - let state = self.state.clone(); - move |_window, cx| { - state.update(cx, |state, cx| state.accept_terms_of_service(cx)); - } - }) - .into_any_element(), - ) - } - fn reset_credentials(&self, _cx: &mut App) -> Task> { Task::ready(Ok(())) } } -fn render_accept_terms( - view_kind: LanguageModelProviderTosView, - accept_terms_of_service_in_progress: bool, - accept_terms_callback: impl Fn(&mut Window, &mut App) + 'static, -) -> impl IntoElement { - let thread_fresh_start = matches!(view_kind, LanguageModelProviderTosView::ThreadFreshStart); - let thread_empty_state = matches!(view_kind, LanguageModelProviderTosView::ThreadEmptyState); - - let terms_button = Button::new("terms_of_service", "Terms of Service") - .style(ButtonStyle::Subtle) - .icon(IconName::ArrowUpRight) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .when(thread_empty_state, |this| this.label_size(LabelSize::Small)) - .on_click(move |_, _window, cx| cx.open_url("https://zed.dev/terms-of-service")); - - let button_container = h_flex().child( - Button::new("accept_terms", "I accept the Terms of Service") - .when(!thread_empty_state, |this| { - this.full_width() - .style(ButtonStyle::Tinted(TintColor::Accent)) - .icon(IconName::Check) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - }) - .when(thread_empty_state, |this| { - this.style(ButtonStyle::Tinted(TintColor::Warning)) - .label_size(LabelSize::Small) - }) - .disabled(accept_terms_of_service_in_progress) - .on_click(move |_, window, cx| (accept_terms_callback)(window, cx)), - ); - - if thread_empty_state { - h_flex() - .w_full() - .flex_wrap() - .justify_between() - .child( - h_flex() - .child( - Label::new("To start using Zed AI, please read and accept the") - .size(LabelSize::Small), - ) - .child(terms_button), - ) - .child(button_container) - } else { - v_flex() - .w_full() - .gap_2() - .child( - h_flex() - .flex_wrap() - .when(thread_fresh_start, |this| this.justify_center()) - .child(Label::new( - "To start using Zed AI, please read and accept the", - )) - .child(terms_button), - ) - .child({ - match view_kind { - LanguageModelProviderTosView::TextThreadPopup => { - button_container.w_full().justify_end() - } - LanguageModelProviderTosView::Configuration => { - button_container.w_full().justify_start() - } - LanguageModelProviderTosView::ThreadFreshStart => { - button_container.w_full().justify_center() - } - LanguageModelProviderTosView::ThreadEmptyState => div().w_0(), - } - }) - } -} - pub struct CloudLanguageModel { id: LanguageModelId, model: Arc, @@ -1107,10 +986,7 @@ struct ZedAiConfiguration { plan: Option, subscription_period: Option<(DateTime, DateTime)>, eligible_for_trial: bool, - has_accepted_terms_of_service: bool, account_too_young: bool, - accept_terms_of_service_in_progress: bool, - accept_terms_of_service_callback: Arc, sign_in_callback: Arc, } @@ -1176,58 +1052,30 @@ impl RenderOnce for ZedAiConfiguration { ); } - v_flex() - .gap_2() - .w_full() - .when(!self.has_accepted_terms_of_service, |this| { - this.child(render_accept_terms( - LanguageModelProviderTosView::Configuration, - self.accept_terms_of_service_in_progress, - { - let callback = self.accept_terms_of_service_callback.clone(); - move |window, cx| (callback)(window, cx) - }, - )) - }) - .map(|this| { - if self.has_accepted_terms_of_service && self.account_too_young { - this.child(young_account_banner).child( - Button::new("upgrade", "Upgrade to Pro") - .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent)) - .full_width() - .on_click(|_, _, cx| { - cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)) - }), - ) - } else if self.has_accepted_terms_of_service { - this.text_sm() - .child(subscription_text) - .child(manage_subscription_buttons) - } else { - this - } - }) - .when(self.has_accepted_terms_of_service, |this| this) + v_flex().gap_2().w_full().map(|this| { + if self.account_too_young { + this.child(young_account_banner).child( + Button::new("upgrade", "Upgrade to Pro") + .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent)) + .full_width() + .on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))), + ) + } else { + this.text_sm() + .child(subscription_text) + .child(manage_subscription_buttons) + } + }) } } struct ConfigurationView { state: Entity, - accept_terms_of_service_callback: Arc, sign_in_callback: Arc, } impl ConfigurationView { fn new(state: Entity) -> Self { - let accept_terms_of_service_callback = Arc::new({ - let state = state.clone(); - move |_window: &mut Window, cx: &mut App| { - state.update(cx, |state, cx| { - state.accept_terms_of_service(cx); - }); - } - }); - let sign_in_callback = Arc::new({ let state = state.clone(); move |_window: &mut Window, cx: &mut App| { @@ -1239,7 +1087,6 @@ impl ConfigurationView { Self { state, - accept_terms_of_service_callback, sign_in_callback, } } @@ -1255,10 +1102,7 @@ impl Render for ConfigurationView { plan: user_store.plan(), subscription_period: user_store.subscription_period(), eligible_for_trial: user_store.trial_started_at().is_none(), - has_accepted_terms_of_service: state.has_accepted_terms_of_service(cx), account_too_young: user_store.account_too_young(), - accept_terms_of_service_in_progress: state.accept_terms_of_service_task.is_some(), - accept_terms_of_service_callback: self.accept_terms_of_service_callback.clone(), sign_in_callback: self.sign_in_callback.clone(), } } @@ -1283,7 +1127,6 @@ impl Component for ZedAiConfiguration { plan: Option, eligible_for_trial: bool, account_too_young: bool, - has_accepted_terms_of_service: bool, ) -> AnyElement { ZedAiConfiguration { is_connected, @@ -1292,10 +1135,7 @@ impl Component for ZedAiConfiguration { .is_some() .then(|| (Utc::now(), Utc::now() + chrono::Duration::days(7))), eligible_for_trial, - has_accepted_terms_of_service, account_too_young, - accept_terms_of_service_in_progress: false, - accept_terms_of_service_callback: Arc::new(|_, _| {}), sign_in_callback: Arc::new(|_, _| {}), } .into_any_element() @@ -1306,33 +1146,30 @@ impl Component for ZedAiConfiguration { .p_4() .gap_4() .children(vec![ - single_example( - "Not connected", - configuration(false, None, false, false, true), - ), + single_example("Not connected", configuration(false, None, false, false)), single_example( "Accept Terms of Service", - configuration(true, None, true, false, false), + configuration(true, None, true, false), ), single_example( "No Plan - Not eligible for trial", - configuration(true, None, false, false, true), + configuration(true, None, false, false), ), single_example( "No Plan - Eligible for trial", - configuration(true, None, true, false, true), + configuration(true, None, true, false), ), single_example( "Free Plan", - configuration(true, Some(Plan::ZedFree), true, false, true), + configuration(true, Some(Plan::ZedFree), true, false), ), single_example( "Zed Pro Trial Plan", - configuration(true, Some(Plan::ZedProTrial), true, false, true), + configuration(true, Some(Plan::ZedProTrial), true, false), ), single_example( "Zed Pro Plan", - configuration(true, Some(Plan::ZedPro), true, false, true), + configuration(true, Some(Plan::ZedPro), true, false), ), ]) .into_any_element(), diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs index a9abd9bc74..bc2d757fd1 100644 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ b/crates/zed/src/zed/edit_prediction_registry.rs @@ -75,13 +75,10 @@ pub fn init(client: Arc, user_store: Entity, cx: &mut App) { let new_provider = all_language_settings(None, cx).edit_predictions.provider; if new_provider != provider { - let tos_accepted = user_store.read(cx).has_accepted_terms_of_service(); - telemetry::event!( "Edit Prediction Provider Changed", from = provider, to = new_provider, - zed_ai_tos_accepted = tos_accepted, ); provider = new_provider; @@ -92,28 +89,6 @@ pub fn init(client: Arc, user_store: Entity, cx: &mut App) { user_store.clone(), cx, ); - - if !tos_accepted { - match provider { - EditPredictionProvider::Zed => { - let Some(window) = cx.active_window() else { - return; - }; - - window - .update(cx, |_, window, cx| { - window.dispatch_action( - Box::new(zed_actions::OpenZedPredictOnboarding), - cx, - ); - }) - .ok(); - } - EditPredictionProvider::None - | EditPredictionProvider::Copilot - | EditPredictionProvider::Supermaven => {} - } - } } } }) diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 916699d29b..7b14d12796 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -118,12 +118,8 @@ impl Dismissable for ZedPredictUpsell { } } -pub fn should_show_upsell_modal(user_store: &Entity, cx: &App) -> bool { - if user_store.read(cx).has_accepted_terms_of_service() { - !ZedPredictUpsell::dismissed() - } else { - true - } +pub fn should_show_upsell_modal() -> bool { + !ZedPredictUpsell::dismissed() } #[derive(Clone)] @@ -1547,16 +1543,6 @@ impl edit_prediction::EditPredictionProvider for ZetaEditPredictionProvider { ) -> bool { true } - - fn needs_terms_acceptance(&self, cx: &App) -> bool { - !self - .zeta - .read(cx) - .user_store - .read(cx) - .has_accepted_terms_of_service() - } - fn is_refreshing(&self) -> bool { !self.pending_completions.is_empty() } @@ -1569,10 +1555,6 @@ impl edit_prediction::EditPredictionProvider for ZetaEditPredictionProvider { _debounce: bool, cx: &mut Context, ) { - if self.needs_terms_acceptance(cx) { - return; - } - if self.zeta.read(cx).update_required { return; } From ac9fdaa1dad22e67731315f972177d860278600f Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Fri, 22 Aug 2025 11:51:01 -0400 Subject: [PATCH 619/693] onboarding: Improve Windows/Linux keyboard shortcuts; example ligature (#36712) Small fixes to onboarding. Correct ligature example. Replace`ctrl-escape` and `alt-tab` since they are reserved on windows (and often on linux) and so are caught by the OS. Release Notes: - N/A --- assets/keymaps/default-linux.json | 5 ++--- crates/onboarding/src/editing_page.rs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 955e68f5a9..fdc1403eb8 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -16,7 +16,6 @@ "up": "menu::SelectPrevious", "enter": "menu::Confirm", "ctrl-enter": "menu::SecondaryConfirm", - "ctrl-escape": "menu::Cancel", "ctrl-c": "menu::Cancel", "escape": "menu::Cancel", "alt-shift-enter": "menu::Restart", @@ -1195,8 +1194,8 @@ "ctrl-1": "onboarding::ActivateBasicsPage", "ctrl-2": "onboarding::ActivateEditingPage", "ctrl-3": "onboarding::ActivateAISetupPage", - "ctrl-escape": "onboarding::Finish", - "alt-tab": "onboarding::SignIn", + "ctrl-enter": "onboarding::Finish", + "alt-shift-l": "onboarding::SignIn", "alt-shift-a": "onboarding::OpenAccount" } } diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 8fae695854..47dfd84894 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -606,7 +606,7 @@ fn render_popular_settings_section( cx: &mut App, ) -> impl IntoElement { const LIGATURE_TOOLTIP: &str = - "Font ligatures combine two characters into one. For example, turning =/= into ≠."; + "Font ligatures combine two characters into one. For example, turning != into ≠."; v_flex() .pt_6() From eb0f9ddcdc1305991b59adee2d87b3b1bea5b562 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Fri, 22 Aug 2025 19:03:47 +0300 Subject: [PATCH 620/693] themes: Implement Bright Black and Bright White colors (#36761) Before: image After: image Release Notes: - Fixed ANSI Bright Black and Bright White colors --- assets/themes/ayu/ayu.json | 6 +++--- assets/themes/gruvbox/gruvbox.json | 12 ++++++------ assets/themes/one/one.json | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/assets/themes/ayu/ayu.json b/assets/themes/ayu/ayu.json index f9f8720729..0ffbb9f61e 100644 --- a/assets/themes/ayu/ayu.json +++ b/assets/themes/ayu/ayu.json @@ -93,7 +93,7 @@ "terminal.ansi.bright_cyan": "#4c806fff", "terminal.ansi.dim_cyan": "#cbf2e4ff", "terminal.ansi.white": "#bfbdb6ff", - "terminal.ansi.bright_white": "#bfbdb6ff", + "terminal.ansi.bright_white": "#fafafaff", "terminal.ansi.dim_white": "#787876ff", "link_text.hover": "#5ac1feff", "conflict": "#feb454ff", @@ -479,7 +479,7 @@ "terminal.ansi.bright_cyan": "#ace0cbff", "terminal.ansi.dim_cyan": "#2a5f4aff", "terminal.ansi.white": "#fcfcfcff", - "terminal.ansi.bright_white": "#fcfcfcff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#bcbec0ff", "link_text.hover": "#3b9ee5ff", "conflict": "#f1ad49ff", @@ -865,7 +865,7 @@ "terminal.ansi.bright_cyan": "#4c806fff", "terminal.ansi.dim_cyan": "#cbf2e4ff", "terminal.ansi.white": "#cccac2ff", - "terminal.ansi.bright_white": "#cccac2ff", + "terminal.ansi.bright_white": "#fafafaff", "terminal.ansi.dim_white": "#898a8aff", "link_text.hover": "#72cffeff", "conflict": "#fecf72ff", diff --git a/assets/themes/gruvbox/gruvbox.json b/assets/themes/gruvbox/gruvbox.json index 459825c733..f0f0358b76 100644 --- a/assets/themes/gruvbox/gruvbox.json +++ b/assets/themes/gruvbox/gruvbox.json @@ -94,7 +94,7 @@ "terminal.ansi.bright_cyan": "#45603eff", "terminal.ansi.dim_cyan": "#c7dfbdff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#fbf1c7ff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", @@ -494,7 +494,7 @@ "terminal.ansi.bright_cyan": "#45603eff", "terminal.ansi.dim_cyan": "#c7dfbdff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#fbf1c7ff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", @@ -894,7 +894,7 @@ "terminal.ansi.bright_cyan": "#45603eff", "terminal.ansi.dim_cyan": "#c7dfbdff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#fbf1c7ff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", @@ -1294,7 +1294,7 @@ "terminal.ansi.bright_cyan": "#9fbca8ff", "terminal.ansi.dim_cyan": "#253e2eff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#fbf1c7ff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", @@ -1694,7 +1694,7 @@ "terminal.ansi.bright_cyan": "#9fbca8ff", "terminal.ansi.dim_cyan": "#253e2eff", "terminal.ansi.white": "#f9f5d7ff", - "terminal.ansi.bright_white": "#f9f5d7ff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", @@ -2094,7 +2094,7 @@ "terminal.ansi.bright_cyan": "#9fbca8ff", "terminal.ansi.dim_cyan": "#253e2eff", "terminal.ansi.white": "#f2e5bcff", - "terminal.ansi.bright_white": "#f2e5bcff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", diff --git a/assets/themes/one/one.json b/assets/themes/one/one.json index 23ebbcc67e..33f6d3c622 100644 --- a/assets/themes/one/one.json +++ b/assets/themes/one/one.json @@ -93,7 +93,7 @@ "terminal.ansi.bright_cyan": "#3a565bff", "terminal.ansi.dim_cyan": "#b9d9dfff", "terminal.ansi.white": "#dce0e5ff", - "terminal.ansi.bright_white": "#dce0e5ff", + "terminal.ansi.bright_white": "#fafafaff", "terminal.ansi.dim_white": "#575d65ff", "link_text.hover": "#74ade8ff", "version_control.added": "#27a657ff", @@ -468,7 +468,7 @@ "terminal.bright_foreground": "#242529ff", "terminal.dim_foreground": "#fafafaff", "terminal.ansi.black": "#242529ff", - "terminal.ansi.bright_black": "#242529ff", + "terminal.ansi.bright_black": "#747579ff", "terminal.ansi.dim_black": "#97979aff", "terminal.ansi.red": "#d36151ff", "terminal.ansi.bright_red": "#f0b0a4ff", @@ -489,7 +489,7 @@ "terminal.ansi.bright_cyan": "#a3bedaff", "terminal.ansi.dim_cyan": "#254058ff", "terminal.ansi.white": "#fafafaff", - "terminal.ansi.bright_white": "#fafafaff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#aaaaaaff", "link_text.hover": "#5c78e2ff", "version_control.added": "#27a657ff", From 42ae3301d01602514daf09b10b2ba5396fe5a731 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 22 Aug 2025 20:04:39 +0300 Subject: [PATCH 621/693] Show file open error view instead of the modal (#36764) Closes https://github.com/zed-industries/zed/issues/36672 Before: either image (when opening from the project panel) or image (for the rest of the cases) After: Screenshot 2025-08-22 at 19 34 10 (the unified error view) Release Notes: - Improved unsupported file opening in Zed --------- Co-authored-by: Conrad Irwin --- assets/keymaps/default-linux.json | 9 +- assets/keymaps/default-macos.json | 9 +- assets/keymaps/vim.json | 2 +- crates/editor/src/editor_tests.rs | 37 +++++++ crates/editor/src/items.rs | 11 ++ crates/project_panel/src/project_panel.rs | 3 +- crates/workspace/src/invalid_buffer_view.rs | 111 ++++++++++++++++++++ crates/workspace/src/item.rs | 18 ++++ crates/workspace/src/pane.rs | 100 +++++++++++++----- crates/workspace/src/workspace.rs | 56 +++++++--- crates/zed_actions/src/lib.rs | 5 +- 11 files changed, 316 insertions(+), 45 deletions(-) create mode 100644 crates/workspace/src/invalid_buffer_view.rs diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index fdc1403eb8..e84f4834af 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -855,7 +855,7 @@ "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }], "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }], "alt-ctrl-r": "project_panel::RevealInFileManager", - "ctrl-shift-enter": "project_panel::OpenWithSystem", + "ctrl-shift-enter": "workspace::OpenWithSystem", "alt-d": "project_panel::CompareMarkedFiles", "shift-find": "project_panel::NewSearchInDirectory", "ctrl-alt-shift-f": "project_panel::NewSearchInDirectory", @@ -1198,5 +1198,12 @@ "alt-shift-l": "onboarding::SignIn", "alt-shift-a": "onboarding::OpenAccount" } + }, + { + "context": "InvalidBuffer", + "use_key_equivalents": true, + "bindings": { + "ctrl-shift-enter": "workspace::OpenWithSystem" + } } ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 8b18299a91..e72f4174ff 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -915,7 +915,7 @@ "cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }], "cmd-delete": ["project_panel::Delete", { "skip_prompt": false }], "alt-cmd-r": "project_panel::RevealInFileManager", - "ctrl-shift-enter": "project_panel::OpenWithSystem", + "ctrl-shift-enter": "workspace::OpenWithSystem", "alt-d": "project_panel::CompareMarkedFiles", "cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }], "cmd-alt-shift-f": "project_panel::NewSearchInDirectory", @@ -1301,5 +1301,12 @@ "alt-tab": "onboarding::SignIn", "alt-shift-a": "onboarding::OpenAccount" } + }, + { + "context": "InvalidBuffer", + "use_key_equivalents": true, + "bindings": { + "ctrl-shift-enter": "workspace::OpenWithSystem" + } } ] diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index be6d34a134..62e50b3c8c 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -819,7 +819,7 @@ "v": "project_panel::OpenPermanent", "p": "project_panel::Open", "x": "project_panel::RevealInFileManager", - "s": "project_panel::OpenWithSystem", + "s": "workspace::OpenWithSystem", "z d": "project_panel::CompareMarkedFiles", "] c": "project_panel::SelectNextGitEntry", "[ c": "project_panel::SelectPrevGitEntry", diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 5b854e3a97..03f5da9a20 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -57,7 +57,9 @@ use util::{ use workspace::{ CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry, OpenOptions, ViewId, + invalid_buffer_view::InvalidBufferView, item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions}, + register_project_item, }; #[gpui::test] @@ -24348,6 +24350,41 @@ async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_non_utf_8_opens(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + cx.update(|cx| { + register_project_item::(cx); + }); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/root1", json!({})).await; + fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd]) + .await; + + let project = Project::test(fs, ["/root1".as_ref()], cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + let worktree_id = project.update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }); + + let handle = workspace + .update_in(cx, |workspace, window, cx| { + let project_path = (worktree_id, "one.pdf"); + workspace.open_path(project_path, None, true, window, cx) + }) + .await + .unwrap(); + + assert_eq!( + handle.to_any().entity_type(), + TypeId::of::() + ); +} + #[track_caller] fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec { editor diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index afc5767de0..641e8a97ed 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -42,6 +42,7 @@ use ui::{IconDecorationKind, prelude::*}; use util::{ResultExt, TryFutureExt, paths::PathExt}; use workspace::{ CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, + invalid_buffer_view::InvalidBufferView, item::{FollowableItem, Item, ItemEvent, ProjectItem, SaveOptions}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, }; @@ -1401,6 +1402,16 @@ impl ProjectItem for Editor { editor } + + fn for_broken_project_item( + abs_path: PathBuf, + is_local: bool, + e: &anyhow::Error, + window: &mut Window, + cx: &mut App, + ) -> Option { + Some(InvalidBufferView::new(abs_path, is_local, e, window, cx)) + } } fn clip_ranges<'a>( diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 52ec7a9880..c99f5f8172 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -69,6 +69,7 @@ use workspace::{ notifications::{DetachAndPromptErr, NotifyTaskExt}, }; use worktree::CreatedEntry; +use zed_actions::workspace::OpenWithSystem; const PROJECT_PANEL_KEY: &str = "ProjectPanel"; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; @@ -255,8 +256,6 @@ actions!( RevealInFileManager, /// Removes the selected folder from the project. RemoveFromProject, - /// Opens the selected file with the system's default application. - OpenWithSystem, /// Cuts the selected file or directory. Cut, /// Pastes the previously cut or copied item. diff --git a/crates/workspace/src/invalid_buffer_view.rs b/crates/workspace/src/invalid_buffer_view.rs new file mode 100644 index 0000000000..e2361d5967 --- /dev/null +++ b/crates/workspace/src/invalid_buffer_view.rs @@ -0,0 +1,111 @@ +use std::{path::PathBuf, sync::Arc}; + +use gpui::{EventEmitter, FocusHandle, Focusable}; +use ui::{ + App, Button, ButtonCommon, ButtonStyle, Clickable, Context, FluentBuilder, InteractiveElement, + KeyBinding, ParentElement, Render, SharedString, Styled as _, Window, h_flex, v_flex, +}; +use zed_actions::workspace::OpenWithSystem; + +use crate::Item; + +/// A view to display when a certain buffer fails to open. +pub struct InvalidBufferView { + /// Which path was attempted to open. + pub abs_path: Arc, + /// An error message, happened when opening the buffer. + pub error: SharedString, + is_local: bool, + focus_handle: FocusHandle, +} + +impl InvalidBufferView { + pub fn new( + abs_path: PathBuf, + is_local: bool, + e: &anyhow::Error, + _: &mut Window, + cx: &mut App, + ) -> Self { + Self { + is_local, + abs_path: Arc::new(abs_path), + error: format!("{e}").into(), + focus_handle: cx.focus_handle(), + } + } +} + +impl Item for InvalidBufferView { + type Event = (); + + fn tab_content_text(&self, mut detail: usize, _: &App) -> SharedString { + // Ensure we always render at least the filename. + detail += 1; + + let path = self.abs_path.as_path(); + + let mut prefix = path; + while detail > 0 { + if let Some(parent) = prefix.parent() { + prefix = parent; + detail -= 1; + } else { + break; + } + } + + let path = if detail > 0 { + path + } else { + path.strip_prefix(prefix).unwrap_or(path) + }; + + SharedString::new(path.to_string_lossy()) + } +} + +impl EventEmitter<()> for InvalidBufferView {} + +impl Focusable for InvalidBufferView { + fn focus_handle(&self, _: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for InvalidBufferView { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { + let abs_path = self.abs_path.clone(); + v_flex() + .size_full() + .track_focus(&self.focus_handle(cx)) + .flex_none() + .justify_center() + .overflow_hidden() + .key_context("InvalidBuffer") + .child( + h_flex().size_full().justify_center().child( + v_flex() + .justify_center() + .gap_2() + .child("Cannot display the file contents in Zed") + .when(self.is_local, |contents| { + contents.child( + h_flex().justify_center().child( + Button::new("open-with-system", "Open in Default App") + .on_click(move |_, _, cx| { + cx.open_with_system(&abs_path); + }) + .style(ButtonStyle::Outlined) + .key_binding(KeyBinding::for_action( + &OpenWithSystem, + window, + cx, + )), + ), + ) + }), + ), + ) + } +} diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 5a497398f9..3485fcca43 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -1,6 +1,7 @@ use crate::{ CollaboratorId, DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory, SerializableItemRegistry, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, + invalid_buffer_view::InvalidBufferView, pane::{self, Pane}, persistence::model::ItemId, searchable::SearchableItemHandle, @@ -22,6 +23,7 @@ use std::{ any::{Any, TypeId}, cell::RefCell, ops::Range, + path::PathBuf, rc::Rc, sync::Arc, time::Duration, @@ -1161,6 +1163,22 @@ pub trait ProjectItem: Item { ) -> Self where Self: Sized; + + /// A fallback handler, which will be called after [`project::ProjectItem::try_open`] fails, + /// with the error from that failure as an argument. + /// Allows to open an item that can gracefully display and handle errors. + fn for_broken_project_item( + _abs_path: PathBuf, + _is_local: bool, + _e: &anyhow::Error, + _window: &mut Window, + _cx: &mut App, + ) -> Option + where + Self: Sized, + { + None + } } #[derive(Debug)] diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 23c8c0b185..e88402adc0 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2,6 +2,7 @@ use crate::{ CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible, SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace, WorkspaceItemBuilder, + invalid_buffer_view::InvalidBufferView, item::{ ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings, ProjectItemKind, SaveOptions, ShowCloseButton, ShowDiagnostics, TabContentParams, @@ -897,19 +898,43 @@ impl Pane { } } } + + let set_up_existing_item = + |index: usize, pane: &mut Self, window: &mut Window, cx: &mut Context| { + // If the item is already open, and the item is a preview item + // and we are not allowing items to open as preview, mark the item as persistent. + if let Some(preview_item_id) = pane.preview_item_id + && let Some(tab) = pane.items.get(index) + && tab.item_id() == preview_item_id + && !allow_preview + { + pane.set_preview_item_id(None, cx); + } + if activate { + pane.activate_item(index, focus_item, focus_item, window, cx); + } + }; + let set_up_new_item = |new_item: Box, + destination_index: Option, + pane: &mut Self, + window: &mut Window, + cx: &mut Context| { + if allow_preview { + pane.set_preview_item_id(Some(new_item.item_id()), cx); + } + pane.add_item_inner( + new_item, + true, + focus_item, + activate, + destination_index, + window, + cx, + ); + }; + if let Some((index, existing_item)) = existing_item { - // If the item is already open, and the item is a preview item - // and we are not allowing items to open as preview, mark the item as persistent. - if let Some(preview_item_id) = self.preview_item_id - && let Some(tab) = self.items.get(index) - && tab.item_id() == preview_item_id - && !allow_preview - { - self.set_preview_item_id(None, cx); - } - if activate { - self.activate_item(index, focus_item, focus_item, window, cx); - } + set_up_existing_item(index, self, window, cx); existing_item } else { // If the item is being opened as preview and we have an existing preview tab, @@ -921,21 +946,46 @@ impl Pane { }; let new_item = build_item(self, window, cx); + // A special case that won't ever get a `project_entry_id` but has to be deduplicated nonetheless. + if let Some(invalid_buffer_view) = new_item.downcast::() { + let mut already_open_view = None; + let mut views_to_close = HashSet::default(); + for existing_error_view in self + .items_of_type::() + .filter(|item| item.read(cx).abs_path == invalid_buffer_view.read(cx).abs_path) + { + if already_open_view.is_none() + && existing_error_view.read(cx).error == invalid_buffer_view.read(cx).error + { + already_open_view = Some(existing_error_view); + } else { + views_to_close.insert(existing_error_view.item_id()); + } + } - if allow_preview { - self.set_preview_item_id(Some(new_item.item_id()), cx); + let resulting_item = match already_open_view { + Some(already_open_view) => { + if let Some(index) = self.index_for_item_id(already_open_view.item_id()) { + set_up_existing_item(index, self, window, cx); + } + Box::new(already_open_view) as Box<_> + } + None => { + set_up_new_item(new_item.clone(), destination_index, self, window, cx); + new_item + } + }; + + self.close_items(window, cx, SaveIntent::Skip, |existing_item| { + views_to_close.contains(&existing_item) + }) + .detach(); + + resulting_item + } else { + set_up_new_item(new_item.clone(), destination_index, self, window, cx); + new_item } - self.add_item_inner( - new_item.clone(), - true, - focus_item, - activate, - destination_index, - window, - cx, - ); - - new_item } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 44aa94fe61..d31aae2c59 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1,5 +1,6 @@ pub mod dock; pub mod history_manager; +pub mod invalid_buffer_view; pub mod item; mod modal_layer; pub mod notifications; @@ -612,21 +613,49 @@ impl ProjectItemRegistry { ); self.build_project_item_for_path_fns .push(|project, project_path, window, cx| { + let project_path = project_path.clone(); + let abs_path = project.read(cx).absolute_path(&project_path, cx); + let is_local = project.read(cx).is_local(); let project_item = - ::try_open(project, project_path, cx)?; + ::try_open(project, &project_path, cx)?; let project = project.clone(); - Some(window.spawn(cx, async move |cx| { - let project_item = project_item.await?; - let project_entry_id: Option = - project_item.read_with(cx, project::ProjectItem::entry_id)?; - let build_workspace_item = Box::new( - |pane: &mut Pane, window: &mut Window, cx: &mut Context| { - Box::new(cx.new(|cx| { - T::for_project_item(project, Some(pane), project_item, window, cx) - })) as Box + Some(window.spawn(cx, async move |cx| match project_item.await { + Ok(project_item) => { + let project_item = project_item; + let project_entry_id: Option = + project_item.read_with(cx, project::ProjectItem::entry_id)?; + let build_workspace_item = Box::new( + |pane: &mut Pane, window: &mut Window, cx: &mut Context| { + Box::new(cx.new(|cx| { + T::for_project_item( + project, + Some(pane), + project_item, + window, + cx, + ) + })) as Box + }, + ) as Box<_>; + Ok((project_entry_id, build_workspace_item)) + } + Err(e) => match abs_path { + Some(abs_path) => match cx.update(|window, cx| { + T::for_broken_project_item(abs_path, is_local, &e, window, cx) + })? { + Some(broken_project_item_view) => { + let build_workspace_item = Box::new( + move |_: &mut Pane, _: &mut Window, cx: &mut Context| { + cx.new(|_| broken_project_item_view).boxed_clone() + }, + ) + as Box<_>; + Ok((None, build_workspace_item)) + } + None => Err(e)?, }, - ) as Box<_>; - Ok((project_entry_id, build_workspace_item)) + None => Err(e)?, + }, })) }); } @@ -3379,9 +3408,8 @@ impl Workspace { window: &mut Window, cx: &mut App, ) -> Task, WorkspaceItemBuilder)>> { - let project = self.project().clone(); let registry = cx.default_global::().clone(); - registry.open_path(&project, &path, window, cx) + registry.open_path(self.project(), &path, window, cx) } pub fn find_project_item( diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 9455369e9a..069abc0a12 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -156,7 +156,10 @@ pub mod workspace { #[action(deprecated_aliases = ["editor::CopyPath", "outline_panel::CopyPath", "project_panel::CopyPath"])] CopyPath, #[action(deprecated_aliases = ["editor::CopyRelativePath", "outline_panel::CopyRelativePath", "project_panel::CopyRelativePath"])] - CopyRelativePath + CopyRelativePath, + /// Opens the selected file with the system's default application. + #[action(deprecated_aliases = ["project_panel::OpenWithSystem"])] + OpenWithSystem, ] ); } From 72bd248544c58f7bee885bf2ec3f527772e25db5 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 22 Aug 2025 20:49:12 +0200 Subject: [PATCH 622/693] editor: Fix multi buffer header context menu not handling absolute paths (#36769) Release Notes: - N/A --- crates/editor/src/element.rs | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 797b0d6634..32582ba941 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -74,6 +74,7 @@ use std::{ fmt::{self, Write}, iter, mem, ops::{Deref, Range}, + path::Path, rc::Rc, sync::Arc, time::{Duration, Instant}, @@ -3693,7 +3694,12 @@ impl EditorElement { }) .take(1), ) - .children(indicator) + .child( + h_flex() + .size(Pixels(12.0)) + .justify_center() + .children(indicator), + ) .child( h_flex() .cursor_pointer() @@ -3782,25 +3788,31 @@ impl EditorElement { && let Some(worktree) = project.read(cx).worktree_for_id(file.worktree_id(cx), cx) { + let worktree = worktree.read(cx); let relative_path = file.path(); - let entry_for_path = worktree.read(cx).entry_for_path(relative_path); - let abs_path = entry_for_path.and_then(|e| e.canonical_path.as_deref()); - let has_relative_path = - worktree.read(cx).root_entry().is_some_and(Entry::is_dir); + let entry_for_path = worktree.entry_for_path(relative_path); + let abs_path = entry_for_path.map(|e| { + e.canonical_path.as_deref().map_or_else( + || worktree.abs_path().join(relative_path), + Path::to_path_buf, + ) + }); + let has_relative_path = worktree.root_entry().is_some_and(Entry::is_dir); - let parent_abs_path = - abs_path.and_then(|abs_path| Some(abs_path.parent()?.to_path_buf())); + let parent_abs_path = abs_path + .as_ref() + .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf())); let relative_path = has_relative_path .then_some(relative_path) .map(ToOwned::to_owned); let visible_in_project_panel = - relative_path.is_some() && worktree.read(cx).is_visible(); + relative_path.is_some() && worktree.is_visible(); let reveal_in_project_panel = entry_for_path .filter(|_| visible_in_project_panel) .map(|entry| entry.id); menu = menu - .when_some(abs_path.map(ToOwned::to_owned), |menu, abs_path| { + .when_some(abs_path, |menu, abs_path| { menu.entry( "Copy Path", Some(Box::new(zed_actions::workspace::CopyPath)), From 18ac4ac5ef0548e66cd6785ab218dce7eb1de267 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Fri, 22 Aug 2025 16:32:49 -0300 Subject: [PATCH 623/693] ACP debug tools pane (#36768) Adds a new "acp: open debug tools" action that opens a new workspace item with a log of ACP messages for the active connection. Release Notes: - N/A --- Cargo.lock | 27 +- Cargo.toml | 4 +- crates/acp_tools/Cargo.toml | 30 ++ crates/acp_tools/LICENSE-GPL | 1 + crates/acp_tools/src/acp_tools.rs | 494 +++++++++++++++++++++++++++++ crates/agent_servers/Cargo.toml | 1 + crates/agent_servers/src/acp/v1.rs | 11 +- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + crates/zed/src/zed.rs | 1 + script/squawk | 12 +- tooling/workspace-hack/Cargo.toml | 8 +- 12 files changed, 574 insertions(+), 17 deletions(-) create mode 100644 crates/acp_tools/Cargo.toml create mode 120000 crates/acp_tools/LICENSE-GPL create mode 100644 crates/acp_tools/src/acp_tools.rs diff --git a/Cargo.lock b/Cargo.lock index 2b3d7b2691..cd1018d4c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,6 +39,26 @@ dependencies = [ "workspace-hack", ] +[[package]] +name = "acp_tools" +version = "0.1.0" +dependencies = [ + "agent-client-protocol", + "collections", + "gpui", + "language", + "markdown", + "project", + "serde", + "serde_json", + "settings", + "theme", + "ui", + "util", + "workspace", + "workspace-hack", +] + [[package]] name = "action_log" version = "0.1.0" @@ -171,11 +191,12 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.30" +version = "0.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f792e009ba59b137ee1db560bc37e567887ad4b5af6f32181d381fff690e2d4" +checksum = "289eb34ee17213dadcca47eedadd386a5e7678094095414e475965d1bcca2860" dependencies = [ "anyhow", + "async-broadcast", "futures 0.3.31", "log", "parking_lot", @@ -264,6 +285,7 @@ name = "agent_servers" version = "0.1.0" dependencies = [ "acp_thread", + "acp_tools", "action_log", "agent-client-protocol", "agent_settings", @@ -20417,6 +20439,7 @@ dependencies = [ name = "zed" version = "0.202.0" dependencies = [ + "acp_tools", "activity_indicator", "agent", "agent_servers", diff --git a/Cargo.toml b/Cargo.toml index 84de9b30ad..7668d18752 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = [ + "crates/acp_tools", "crates/acp_thread", "crates/action_log", "crates/activity_indicator", @@ -227,6 +228,7 @@ edition = "2024" # Workspace member crates # +acp_tools = { path = "crates/acp_tools" } acp_thread = { path = "crates/acp_thread" } action_log = { path = "crates/action_log" } agent = { path = "crates/agent" } @@ -425,7 +427,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" -agent-client-protocol = "0.0.30" +agent-client-protocol = "0.0.31" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" diff --git a/crates/acp_tools/Cargo.toml b/crates/acp_tools/Cargo.toml new file mode 100644 index 0000000000..7a6d8c21a0 --- /dev/null +++ b/crates/acp_tools/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "acp_tools" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + + +[lints] +workspace = true + +[lib] +path = "src/acp_tools.rs" +doctest = false + +[dependencies] +agent-client-protocol.workspace = true +collections.workspace = true +gpui.workspace = true +language.workspace= true +markdown.workspace = true +project.workspace = true +serde.workspace = true +serde_json.workspace = true +settings.workspace = true +theme.workspace = true +ui.workspace = true +util.workspace = true +workspace-hack.workspace = true +workspace.workspace = true diff --git a/crates/acp_tools/LICENSE-GPL b/crates/acp_tools/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/acp_tools/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs new file mode 100644 index 0000000000..ca5e57e85a --- /dev/null +++ b/crates/acp_tools/src/acp_tools.rs @@ -0,0 +1,494 @@ +use std::{ + cell::RefCell, + collections::HashSet, + fmt::Display, + rc::{Rc, Weak}, + sync::Arc, +}; + +use agent_client_protocol as acp; +use collections::HashMap; +use gpui::{ + App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState, + StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, prelude::*, +}; +use language::LanguageRegistry; +use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle}; +use project::Project; +use settings::Settings; +use theme::ThemeSettings; +use ui::prelude::*; +use util::ResultExt as _; +use workspace::{Item, Workspace}; + +actions!(acp, [OpenDebugTools]); + +pub fn init(cx: &mut App) { + cx.observe_new( + |workspace: &mut Workspace, _window, _cx: &mut Context| { + workspace.register_action(|workspace, _: &OpenDebugTools, window, cx| { + let acp_tools = + Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx))); + workspace.add_item_to_active_pane(acp_tools, None, true, window, cx); + }); + }, + ) + .detach(); +} + +struct GlobalAcpConnectionRegistry(Entity); + +impl Global for GlobalAcpConnectionRegistry {} + +#[derive(Default)] +pub struct AcpConnectionRegistry { + active_connection: RefCell>, +} + +struct ActiveConnection { + server_name: &'static str, + connection: Weak, +} + +impl AcpConnectionRegistry { + pub fn default_global(cx: &mut App) -> Entity { + if cx.has_global::() { + cx.global::().0.clone() + } else { + let registry = cx.new(|_cx| AcpConnectionRegistry::default()); + cx.set_global(GlobalAcpConnectionRegistry(registry.clone())); + registry + } + } + + pub fn set_active_connection( + &self, + server_name: &'static str, + connection: &Rc, + cx: &mut Context, + ) { + self.active_connection.replace(Some(ActiveConnection { + server_name, + connection: Rc::downgrade(connection), + })); + cx.notify(); + } +} + +struct AcpTools { + project: Entity, + focus_handle: FocusHandle, + expanded: HashSet, + watched_connection: Option, + connection_registry: Entity, + _subscription: Subscription, +} + +struct WatchedConnection { + server_name: &'static str, + messages: Vec, + list_state: ListState, + connection: Weak, + incoming_request_methods: HashMap>, + outgoing_request_methods: HashMap>, + _task: Task<()>, +} + +impl AcpTools { + fn new(project: Entity, cx: &mut Context) -> Self { + let connection_registry = AcpConnectionRegistry::default_global(cx); + + let subscription = cx.observe(&connection_registry, |this, _, cx| { + this.update_connection(cx); + cx.notify(); + }); + + let mut this = Self { + project, + focus_handle: cx.focus_handle(), + expanded: HashSet::default(), + watched_connection: None, + connection_registry, + _subscription: subscription, + }; + this.update_connection(cx); + this + } + + fn update_connection(&mut self, cx: &mut Context) { + let active_connection = self.connection_registry.read(cx).active_connection.borrow(); + let Some(active_connection) = active_connection.as_ref() else { + return; + }; + + if let Some(watched_connection) = self.watched_connection.as_ref() { + if Weak::ptr_eq( + &watched_connection.connection, + &active_connection.connection, + ) { + return; + } + } + + if let Some(connection) = active_connection.connection.upgrade() { + let mut receiver = connection.subscribe(); + let task = cx.spawn(async move |this, cx| { + while let Ok(message) = receiver.recv().await { + this.update(cx, |this, cx| { + this.push_stream_message(message, cx); + }) + .ok(); + } + }); + + self.watched_connection = Some(WatchedConnection { + server_name: active_connection.server_name, + messages: vec![], + list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)), + connection: active_connection.connection.clone(), + incoming_request_methods: HashMap::default(), + outgoing_request_methods: HashMap::default(), + _task: task, + }); + } + } + + fn push_stream_message(&mut self, stream_message: acp::StreamMessage, cx: &mut Context) { + let Some(connection) = self.watched_connection.as_mut() else { + return; + }; + let language_registry = self.project.read(cx).languages().clone(); + let index = connection.messages.len(); + + let (request_id, method, message_type, params) = match stream_message.message { + acp::StreamMessageContent::Request { id, method, params } => { + let method_map = match stream_message.direction { + acp::StreamMessageDirection::Incoming => { + &mut connection.incoming_request_methods + } + acp::StreamMessageDirection::Outgoing => { + &mut connection.outgoing_request_methods + } + }; + + method_map.insert(id, method.clone()); + (Some(id), method.into(), MessageType::Request, Ok(params)) + } + acp::StreamMessageContent::Response { id, result } => { + let method_map = match stream_message.direction { + acp::StreamMessageDirection::Incoming => { + &mut connection.outgoing_request_methods + } + acp::StreamMessageDirection::Outgoing => { + &mut connection.incoming_request_methods + } + }; + + if let Some(method) = method_map.remove(&id) { + (Some(id), method.into(), MessageType::Response, result) + } else { + ( + Some(id), + "[unrecognized response]".into(), + MessageType::Response, + result, + ) + } + } + acp::StreamMessageContent::Notification { method, params } => { + (None, method.into(), MessageType::Notification, Ok(params)) + } + }; + + let message = WatchedConnectionMessage { + name: method, + message_type, + request_id, + direction: stream_message.direction, + collapsed_params_md: match params.as_ref() { + Ok(params) => params + .as_ref() + .map(|params| collapsed_params_md(params, &language_registry, cx)), + Err(err) => { + if let Ok(err) = &serde_json::to_value(err) { + Some(collapsed_params_md(&err, &language_registry, cx)) + } else { + None + } + } + }, + + expanded_params_md: None, + params, + }; + + connection.messages.push(message); + connection.list_state.splice(index..index, 1); + cx.notify(); + } + + fn render_message( + &mut self, + index: usize, + window: &mut Window, + cx: &mut Context, + ) -> AnyElement { + let Some(connection) = self.watched_connection.as_ref() else { + return Empty.into_any(); + }; + + let Some(message) = connection.messages.get(index) else { + return Empty.into_any(); + }; + + let base_size = TextSize::Editor.rems(cx); + + let theme_settings = ThemeSettings::get_global(cx); + let text_style = window.text_style(); + + let colors = cx.theme().colors(); + let expanded = self.expanded.contains(&index); + + v_flex() + .w_full() + .px_4() + .py_3() + .border_color(colors.border) + .border_b_1() + .gap_2() + .items_start() + .font_buffer(cx) + .text_size(base_size) + .id(index) + .group("message") + .hover(|this| this.bg(colors.element_background.opacity(0.5))) + .on_click(cx.listener(move |this, _, _, cx| { + if this.expanded.contains(&index) { + this.expanded.remove(&index); + } else { + this.expanded.insert(index); + let Some(connection) = &mut this.watched_connection else { + return; + }; + let Some(message) = connection.messages.get_mut(index) else { + return; + }; + message.expanded(this.project.read(cx).languages().clone(), cx); + connection.list_state.scroll_to_reveal_item(index); + } + cx.notify() + })) + .child( + h_flex() + .w_full() + .gap_2() + .items_center() + .flex_shrink_0() + .child(match message.direction { + acp::StreamMessageDirection::Incoming => { + ui::Icon::new(ui::IconName::ArrowDown).color(Color::Error) + } + acp::StreamMessageDirection::Outgoing => { + ui::Icon::new(ui::IconName::ArrowUp).color(Color::Success) + } + }) + .child( + Label::new(message.name.clone()) + .buffer_font(cx) + .color(Color::Muted), + ) + .child(div().flex_1()) + .child( + div() + .child(ui::Chip::new(message.message_type.to_string())) + .visible_on_hover("message"), + ) + .children( + message + .request_id + .map(|req_id| div().child(ui::Chip::new(req_id.to_string()))), + ), + ) + // I'm aware using markdown is a hack. Trying to get something working for the demo. + // Will clean up soon! + .when_some( + if expanded { + message.expanded_params_md.clone() + } else { + message.collapsed_params_md.clone() + }, + |this, params| { + this.child( + div().pl_6().w_full().child( + MarkdownElement::new( + params, + MarkdownStyle { + base_text_style: text_style, + selection_background_color: colors.element_selection_background, + syntax: cx.theme().syntax().clone(), + code_block_overflow_x_scroll: true, + code_block: StyleRefinement { + text: Some(TextStyleRefinement { + font_family: Some( + theme_settings.buffer_font.family.clone(), + ), + font_size: Some((base_size * 0.8).into()), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }, + ) + .code_block_renderer( + CodeBlockRenderer::Default { + copy_button: false, + copy_button_on_hover: expanded, + border: false, + }, + ), + ), + ) + }, + ) + .into_any() + } +} + +struct WatchedConnectionMessage { + name: SharedString, + request_id: Option, + direction: acp::StreamMessageDirection, + message_type: MessageType, + params: Result, acp::Error>, + collapsed_params_md: Option>, + expanded_params_md: Option>, +} + +impl WatchedConnectionMessage { + fn expanded(&mut self, language_registry: Arc, cx: &mut App) { + let params_md = match &self.params { + Ok(Some(params)) => Some(expanded_params_md(params, &language_registry, cx)), + Err(err) => { + if let Some(err) = &serde_json::to_value(err).log_err() { + Some(expanded_params_md(&err, &language_registry, cx)) + } else { + None + } + } + _ => None, + }; + self.expanded_params_md = params_md; + } +} + +fn collapsed_params_md( + params: &serde_json::Value, + language_registry: &Arc, + cx: &mut App, +) -> Entity { + let params_json = serde_json::to_string(params).unwrap_or_default(); + let mut spaced_out_json = String::with_capacity(params_json.len() + params_json.len() / 4); + + for ch in params_json.chars() { + match ch { + '{' => spaced_out_json.push_str("{ "), + '}' => spaced_out_json.push_str(" }"), + ':' => spaced_out_json.push_str(": "), + ',' => spaced_out_json.push_str(", "), + c => spaced_out_json.push(c), + } + } + + let params_md = format!("```json\n{}\n```", spaced_out_json); + cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx)) +} + +fn expanded_params_md( + params: &serde_json::Value, + language_registry: &Arc, + cx: &mut App, +) -> Entity { + let params_json = serde_json::to_string_pretty(params).unwrap_or_default(); + let params_md = format!("```json\n{}\n```", params_json); + cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx)) +} + +enum MessageType { + Request, + Response, + Notification, +} + +impl Display for MessageType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MessageType::Request => write!(f, "Request"), + MessageType::Response => write!(f, "Response"), + MessageType::Notification => write!(f, "Notification"), + } + } +} + +enum AcpToolsEvent {} + +impl EventEmitter for AcpTools {} + +impl Item for AcpTools { + type Event = AcpToolsEvent; + + fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString { + format!( + "ACP: {}", + self.watched_connection + .as_ref() + .map_or("Disconnected", |connection| connection.server_name) + ) + .into() + } + + fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { + Some(ui::Icon::new(IconName::Thread)) + } +} + +impl Focusable for AcpTools { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for AcpTools { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .track_focus(&self.focus_handle) + .size_full() + .bg(cx.theme().colors().editor_background) + .child(match self.watched_connection.as_ref() { + Some(connection) => { + if connection.messages.is_empty() { + h_flex() + .size_full() + .justify_center() + .items_center() + .child("No messages recorded yet") + .into_any() + } else { + list( + connection.list_state.clone(), + cx.processor(Self::render_message), + ) + .with_sizing_behavior(gpui::ListSizingBehavior::Auto) + .flex_grow() + .into_any() + } + } + None => h_flex() + .size_full() + .justify_center() + .items_center() + .child("No active connection") + .into_any(), + }) + } +} diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index 60dd796463..8ea4a27f4c 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -17,6 +17,7 @@ path = "src/agent_servers.rs" doctest = false [dependencies] +acp_tools.workspace = true acp_thread.workspace = true action_log.workspace = true agent-client-protocol.workspace = true diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index 29f389547d..1945ad2483 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -1,3 +1,4 @@ +use acp_tools::AcpConnectionRegistry; use action_log::ActionLog; use agent_client_protocol::{self as acp, Agent as _, ErrorCode}; use anyhow::anyhow; @@ -101,6 +102,14 @@ impl AcpConnection { }) .detach(); + let connection = Rc::new(connection); + + cx.update(|cx| { + AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| { + registry.set_active_connection(server_name, &connection, cx) + }); + })?; + let response = connection .initialize(acp::InitializeRequest { protocol_version: acp::VERSION, @@ -119,7 +128,7 @@ impl AcpConnection { Ok(Self { auth_methods: response.auth_methods, - connection: connection.into(), + connection, server_name, sessions, prompt_capabilities: response.agent_capabilities.prompt_capabilities, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index c61e23f0a1..6f4ead9ebb 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -20,6 +20,7 @@ path = "src/main.rs" [dependencies] activity_indicator.workspace = true +acp_tools.workspace = true agent.workspace = true agent_ui.workspace = true agent_settings.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 8beefd5891..b8150a600d 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -566,6 +566,7 @@ pub fn main() { language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx); agent_settings::init(cx); agent_servers::init(cx); + acp_tools::init(cx); web_search::init(cx); web_search_providers::init(app_state.client.clone(), cx); snippet_provider::init(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 638e1dca0e..1b9657dcc6 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4434,6 +4434,7 @@ mod tests { assert_eq!(actions_without_namespace, Vec::<&str>::new()); let expected_namespaces = vec![ + "acp", "activity_indicator", "agent", #[cfg(not(target_os = "macos"))] diff --git a/script/squawk b/script/squawk index 8489206f14..497fcff089 100755 --- a/script/squawk +++ b/script/squawk @@ -15,13 +15,11 @@ SQUAWK_VERSION=0.26.0 SQUAWK_BIN="./target/squawk-$SQUAWK_VERSION" SQUAWK_ARGS="--assume-in-transaction --config script/lib/squawk.toml" -if [ ! -f "$SQUAWK_BIN" ]; then - pkgutil --pkg-info com.apple.pkg.RosettaUpdateAuto || /usr/sbin/softwareupdate --install-rosetta --agree-to-license - # When bootstrapping a brand new CI machine, the `target` directory may not exist yet. - mkdir -p "./target" - curl -L -o "$SQUAWK_BIN" "https://github.com/sbdchd/squawk/releases/download/v$SQUAWK_VERSION/squawk-darwin-x86_64" - chmod +x "$SQUAWK_BIN" -fi +pkgutil --pkg-info com.apple.pkg.RosettaUpdateAuto || /usr/sbin/softwareupdate --install-rosetta --agree-to-license +# When bootstrapping a brand new CI machine, the `target` directory may not exist yet. +mkdir -p "./target" +curl -L -o "$SQUAWK_BIN" "https://github.com/sbdchd/squawk/releases/download/v$SQUAWK_VERSION/squawk-darwin-x86_64" +chmod +x "$SQUAWK_BIN" if [ -n "$SQUAWK_GITHUB_TOKEN" ]; then export SQUAWK_GITHUB_REPO_OWNER=$(echo $GITHUB_REPOSITORY | awk -F/ '{print $1}') diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index 054e757056..bf44fc195e 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -54,6 +54,7 @@ digest = { version = "0.10", features = ["mac", "oid", "std"] } either = { version = "1", features = ["serde", "use_std"] } euclid = { version = "0.22" } event-listener = { version = "5" } +event-listener-strategy = { version = "0.5" } flate2 = { version = "1", features = ["zlib-rs"] } form_urlencoded = { version = "1" } futures = { version = "0.3", features = ["io-compat"] } @@ -183,6 +184,7 @@ digest = { version = "0.10", features = ["mac", "oid", "std"] } either = { version = "1", features = ["serde", "use_std"] } euclid = { version = "0.22" } event-listener = { version = "5" } +event-listener-strategy = { version = "0.5" } flate2 = { version = "1", features = ["zlib-rs"] } form_urlencoded = { version = "1" } futures = { version = "0.3", features = ["io-compat"] } @@ -403,7 +405,6 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } -event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -444,7 +445,6 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } -event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -483,7 +483,6 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } -event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -524,7 +523,6 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } -event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -610,7 +608,6 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } -event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } @@ -651,7 +648,6 @@ bytemuck = { version = "1", default-features = false, features = ["min_const_gen cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } codespan-reporting = { version = "0.12" } crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } -event-listener-strategy = { version = "0.5" } flume = { version = "0.11" } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } From 4560d1ec58af7bbd4eed1eae55fca0854c455fc8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 22 Aug 2025 23:09:37 +0300 Subject: [PATCH 624/693] Use a better message for the InvalidBufferView (#36770) Follow-up of https://github.com/zed-industries/zed/pull/36764 Release Notes: - N/A --- crates/workspace/src/invalid_buffer_view.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/workspace/src/invalid_buffer_view.rs b/crates/workspace/src/invalid_buffer_view.rs index e2361d5967..b017373474 100644 --- a/crates/workspace/src/invalid_buffer_view.rs +++ b/crates/workspace/src/invalid_buffer_view.rs @@ -88,7 +88,7 @@ impl Render for InvalidBufferView { v_flex() .justify_center() .gap_2() - .child("Cannot display the file contents in Zed") + .child(h_flex().justify_center().child("Unsupported file type")) .when(self.is_local, |contents| { contents.child( h_flex().justify_center().child( From 896a35f7befce468427a30489adf88c851b9507d Mon Sep 17 00:00:00 2001 From: Jonathan Andersson Date: Fri, 22 Aug 2025 22:16:43 +0200 Subject: [PATCH 625/693] Capture `shorthand_field_initializer` and modules in Rust highlights (#35842) Currently shorthand field initializers are not captured the same way as the full initializers, leading to awkward and mismatching highlighting. This PR addresses this fact, in addition to capturing new highlights: - Tags the `!` as part of a macro invocation. - Tags the identifier part of a lifetime as `@lifetime`. - Tag module definitions as a new capture group, `@module`. - Shorthand initializers are now properly tagged as `@property`. Here's what the current version of Zed looks like: image With the new highlighting applied: image Release Notes: - Improved highlighting of Rust files, including new highlight groups for modules and shorthand initializers. --- crates/languages/src/rust/highlights.scm | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/languages/src/rust/highlights.scm b/crates/languages/src/rust/highlights.scm index 1c46061827..9c02fbedaa 100644 --- a/crates/languages/src/rust/highlights.scm +++ b/crates/languages/src/rust/highlights.scm @@ -6,6 +6,9 @@ (self) @variable.special (field_identifier) @property +(shorthand_field_initializer + (identifier) @property) + (trait_item name: (type_identifier) @type.interface) (impl_item trait: (type_identifier) @type.interface) (abstract_type trait: (type_identifier) @type.interface) @@ -38,11 +41,20 @@ (identifier) @function.special (scoped_identifier name: (identifier) @function.special) - ]) + ] + "!" @function.special) (macro_definition name: (identifier) @function.special.definition) +(mod_item + name: (identifier) @module) + +(visibility_modifier [ + (crate) @keyword + (super) @keyword +]) + ; Identifier conventions ; Assume uppercase names are types/enum-constructors @@ -115,9 +127,7 @@ "where" "while" "yield" - (crate) (mutable_specifier) - (super) ] @keyword [ @@ -189,6 +199,7 @@ operator: "/" @operator (lifetime) @lifetime +(lifetime (identifier) @lifetime) (parameter (identifier) @variable.parameter) From 639417c2bc2dec345b79024f243ce15bd60638a9 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:40:52 -0300 Subject: [PATCH 626/693] thread_view: Adjust empty state and error displays (#36774) Also changes the message editor placeholder depending on the agent. Release Notes: - N/A --------- Co-authored-by: Conrad Irwin --- crates/agent2/src/native_agent_server.rs | 4 +- crates/agent_ui/src/acp/thread_view.rs | 514 +++++++++++------------ crates/agent_ui/src/agent_panel.rs | 2 +- crates/ui/src/components/callout.rs | 1 + 4 files changed, 254 insertions(+), 267 deletions(-) diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index ac5aa95c04..4ce467d6fd 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -23,11 +23,11 @@ impl NativeAgentServer { impl AgentServer for NativeAgentServer { fn name(&self) -> &'static str { - "Native Agent" + "Zed Agent" } fn empty_state_headline(&self) -> &'static str { - "Welcome to the Agent Panel" + self.name() } fn empty_state_message(&self) -> &'static str { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index d27dee1fe6..2a83a4ab5b 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -258,6 +258,7 @@ pub struct AcpThreadView { hovered_recent_history_item: Option, entry_view_state: Entity, message_editor: Entity, + focus_handle: FocusHandle, model_selector: Option>, profile_selector: Option>, notifications: Vec>, @@ -312,6 +313,13 @@ impl AcpThreadView { ) -> Self { let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default())); let prevent_slash_commands = agent.clone().downcast::().is_some(); + + let placeholder = if agent.name() == "Zed Agent" { + format!("Message the {} — @ to include context", agent.name()) + } else { + format!("Message {} — @ to include context", agent.name()) + }; + let message_editor = cx.new(|cx| { let mut editor = MessageEditor::new( workspace.clone(), @@ -319,7 +327,7 @@ impl AcpThreadView { history_store.clone(), prompt_store.clone(), prompt_capabilities.clone(), - "Message the agent — @ to include context", + placeholder, prevent_slash_commands, editor::EditorMode::AutoHeight { min_lines: MIN_EDITOR_LINES, @@ -381,6 +389,7 @@ impl AcpThreadView { prompt_capabilities, _subscriptions: subscriptions, _cancel_task: None, + focus_handle: cx.focus_handle(), } } @@ -404,8 +413,12 @@ impl AcpThreadView { let connection = match connect_task.await { Ok(connection) => connection, Err(err) => { - this.update(cx, |this, cx| { - this.handle_load_error(err, cx); + this.update_in(cx, |this, window, cx| { + if err.downcast_ref::().is_some() { + this.handle_load_error(err, window, cx); + } else { + this.handle_thread_error(err, cx); + } cx.notify(); }) .log_err(); @@ -522,6 +535,7 @@ impl AcpThreadView { title_editor, _subscriptions: subscriptions, }; + this.message_editor.focus_handle(cx).focus(window); this.profile_selector = this.as_native_thread(cx).map(|thread| { cx.new(|cx| { @@ -537,7 +551,7 @@ impl AcpThreadView { cx.notify(); } Err(err) => { - this.handle_load_error(err, cx); + this.handle_load_error(err, window, cx); } }; }) @@ -606,17 +620,28 @@ impl AcpThreadView { .map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))), _subscription: subscription, }; + if this.message_editor.focus_handle(cx).is_focused(window) { + this.focus_handle.focus(window) + } cx.notify(); }) .ok(); } - fn handle_load_error(&mut self, err: anyhow::Error, cx: &mut Context) { + fn handle_load_error( + &mut self, + err: anyhow::Error, + window: &mut Window, + cx: &mut Context, + ) { if let Some(load_err) = err.downcast_ref::() { self.thread_state = ThreadState::LoadError(load_err.clone()); } else { self.thread_state = ThreadState::LoadError(LoadError::Other(err.to_string().into())) } + if self.message_editor.focus_handle(cx).is_focused(window) { + self.focus_handle.focus(window) + } cx.notify(); } @@ -633,12 +658,11 @@ impl AcpThreadView { } } - pub fn title(&self, cx: &App) -> SharedString { + pub fn title(&self) -> SharedString { match &self.thread_state { - ThreadState::Ready { thread, .. } => thread.read(cx).title(), + ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(), ThreadState::Loading { .. } => "Loading…".into(), ThreadState::LoadError(_) => "Failed to load".into(), - ThreadState::Unauthenticated { .. } => "Authentication Required".into(), } } @@ -1069,6 +1093,9 @@ impl AcpThreadView { AcpThreadEvent::LoadError(error) => { self.thread_retry_status.take(); self.thread_state = ThreadState::LoadError(error.clone()); + if self.message_editor.focus_handle(cx).is_focused(window) { + self.focus_handle.focus(window) + } } AcpThreadEvent::TitleUpdated => { let title = thread.read(cx).title(); @@ -2338,33 +2365,6 @@ impl AcpThreadView { .into_any() } - fn render_agent_logo(&self) -> AnyElement { - Icon::new(self.agent.logo()) - .color(Color::Muted) - .size(IconSize::XLarge) - .into_any_element() - } - - fn render_error_agent_logo(&self) -> AnyElement { - let logo = Icon::new(self.agent.logo()) - .color(Color::Muted) - .size(IconSize::XLarge) - .into_any_element(); - - h_flex() - .relative() - .justify_center() - .child(div().opacity(0.3).child(logo)) - .child( - h_flex() - .absolute() - .right_1() - .bottom_0() - .child(Icon::new(IconName::XCircleFilled).color(Color::Error)), - ) - .into_any_element() - } - fn render_rules_item(&self, cx: &Context) -> Option { let project_context = self .as_native_thread(cx)? @@ -2493,8 +2493,7 @@ impl AcpThreadView { ) } - fn render_empty_state(&self, window: &mut Window, cx: &mut Context) -> AnyElement { - let loading = matches!(&self.thread_state, ThreadState::Loading { .. }); + fn render_recent_history(&self, window: &mut Window, cx: &mut Context) -> AnyElement { let render_history = self .agent .clone() @@ -2506,38 +2505,6 @@ impl AcpThreadView { v_flex() .size_full() - .when(!render_history, |this| { - this.child( - v_flex() - .size_full() - .items_center() - .justify_center() - .child(if loading { - h_flex() - .justify_center() - .child(self.render_agent_logo()) - .with_animation( - "pulsating_icon", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.4, 1.0)), - |icon, delta| icon.opacity(delta), - ) - .into_any() - } else { - self.render_agent_logo().into_any_element() - }) - .child(h_flex().mt_4().mb_2().justify_center().child(if loading { - div() - .child(LoadingLabel::new("").size(LabelSize::Large)) - .into_any_element() - } else { - Headline::new(self.agent.empty_state_headline()) - .size(HeadlineSize::Medium) - .into_any_element() - })), - ) - }) .when(render_history, |this| { let recent_history: Vec<_> = self.history_store.update(cx, |history_store, _| { history_store.entries().take(3).collect() @@ -2612,196 +2579,118 @@ impl AcpThreadView { window: &mut Window, cx: &Context, ) -> Div { - v_flex() - .p_2() - .gap_2() - .flex_1() - .items_center() - .justify_center() - .child( - v_flex() - .items_center() - .justify_center() - .child(self.render_error_agent_logo()) - .child( - h_flex().mt_4().mb_1().justify_center().child( - Headline::new("Authentication Required").size(HeadlineSize::Medium), + v_flex().flex_1().size_full().justify_end().child( + v_flex() + .p_2() + .pr_3() + .w_full() + .border_t_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().status().warning.opacity(0.04)) + .child( + h_flex() + .gap_1p5() + .child( + Icon::new(IconName::Warning) + .color(Color::Warning) + .size(IconSize::Small), + ) + .child(Label::new("Authentication Required")), + ) + .children(description.map(|desc| { + div().text_ui(cx).child(self.render_markdown( + desc.clone(), + default_markdown_style(false, false, window, cx), + )) + })) + .children( + configuration_view + .cloned() + .map(|view| div().w_full().child(view)), + ) + .when( + configuration_view.is_none() + && description.is_none() + && pending_auth_method.is_none(), + |el| { + el.child( + Label::new(format!( + "You are not currently authenticated with {}. Please choose one of the following options:", + self.agent.name() + )) + .color(Color::Muted) + .mb_1() + .ml_5(), + ) + }, + ) + .when(!connection.auth_methods().is_empty(), |this| { + this.child( + h_flex().justify_end().flex_wrap().gap_1().children( + connection.auth_methods().iter().enumerate().rev().map( + |(ix, method)| { + Button::new( + SharedString::from(method.id.0.clone()), + method.name.clone(), + ) + .when(ix == 0, |el| { + el.style(ButtonStyle::Tinted(ui::TintColor::Warning)) + }) + .on_click({ + let method_id = method.id.clone(); + cx.listener(move |this, _, window, cx| { + this.authenticate(method_id.clone(), window, cx) + }) + }) + }, + ), ), ) - .into_any(), - ) - .children(description.map(|desc| { - div().text_ui(cx).text_center().child(self.render_markdown( - desc.clone(), - default_markdown_style(false, false, window, cx), - )) - })) - .children( - configuration_view - .cloned() - .map(|view| div().px_4().w_full().max_w_128().child(view)), - ) - .when( - configuration_view.is_none() - && description.is_none() - && pending_auth_method.is_none(), - |el| { + }) + .when_some(pending_auth_method, |el, _| { el.child( - div() - .text_ui(cx) - .text_center() - .px_4() + h_flex() + .py_4() .w_full() - .max_w_128() - .child(Label::new("Authentication required")), - ) - }, - ) - .when_some(pending_auth_method, |el, _| { - let spinner_icon = div() - .px_0p5() - .id("generating") - .tooltip(Tooltip::text("Generating Changes…")) - .child( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage(delta))) - }, + .justify_center() + .gap_1() + .child( + Icon::new(IconName::ArrowCircle) + .size(IconSize::Small) + .color(Color::Muted) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate(percentage( + delta, + ))) + }, + ) + .into_any_element(), ) - .into_any_element(), + .child(Label::new("Authenticating…")), ) - .into_any(); - el.child( - h_flex() - .text_ui(cx) - .text_center() - .justify_center() - .gap_2() - .px_4() - .w_full() - .max_w_128() - .child(Label::new("Authenticating...")) - .child(spinner_icon), - ) - }) - .child( - h_flex() - .mt_1p5() - .gap_1() - .flex_wrap() - .justify_center() - .children(connection.auth_methods().iter().enumerate().rev().map( - |(ix, method)| { - Button::new( - SharedString::from(method.id.0.clone()), - method.name.clone(), - ) - .style(ButtonStyle::Outlined) - .when(ix == 0, |el| { - el.style(ButtonStyle::Tinted(ui::TintColor::Accent)) - }) - .size(ButtonSize::Medium) - .label_size(LabelSize::Small) - .on_click({ - let method_id = method.id.clone(); - cx.listener(move |this, _, window, cx| { - this.authenticate(method_id.clone(), window, cx) - }) - }) - }, - )), - ) + }), + ) } fn render_load_error(&self, e: &LoadError, cx: &Context) -> AnyElement { - let mut container = v_flex() - .items_center() - .justify_center() - .child(self.render_error_agent_logo()) - .child( - v_flex() - .mt_4() - .mb_2() - .gap_0p5() - .text_center() - .items_center() - .child(Headline::new("Failed to launch").size(HeadlineSize::Medium)) - .child( - Label::new(e.to_string()) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ); - - if let LoadError::Unsupported { - upgrade_message, - upgrade_command, - .. - } = &e - { - let upgrade_message = upgrade_message.clone(); - let upgrade_command = upgrade_command.clone(); - container = container.child( - Button::new("upgrade", upgrade_message) - .tooltip(Tooltip::text(upgrade_command.clone())) - .on_click(cx.listener(move |this, _, window, cx| { - let task = this - .workspace - .update(cx, |workspace, cx| { - let project = workspace.project().read(cx); - let cwd = project.first_project_directory(cx); - let shell = project.terminal_settings(&cwd, cx).shell.clone(); - let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId("upgrade".to_string()), - full_label: upgrade_command.clone(), - label: upgrade_command.clone(), - command: Some(upgrade_command.clone()), - args: Vec::new(), - command_label: upgrade_command.clone(), - cwd, - env: Default::default(), - use_new_terminal: true, - allow_concurrent_runs: true, - reveal: Default::default(), - reveal_target: Default::default(), - hide: Default::default(), - shell, - show_summary: true, - show_command: true, - show_rerun: false, - }; - workspace.spawn_in_terminal(spawn_in_terminal, window, cx) - }) - .ok(); - let Some(task) = task else { return }; - cx.spawn_in(window, async move |this, cx| { - if let Some(Ok(_)) = task.await { - this.update_in(cx, |this, window, cx| { - this.reset(window, cx); - }) - .ok(); - } - }) - .detach() - })), - ); - } else if let LoadError::NotInstalled { - install_message, - install_command, - .. - } = e - { - let install_message = install_message.clone(); - let install_command = install_command.clone(); - container = container.child( - Button::new("install", install_message) - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .size(ButtonSize::Medium) + let (message, action_slot) = match e { + LoadError::NotInstalled { + error_message, + install_message, + install_command, + } => { + let install_command = install_command.clone(); + let button = Button::new("install", install_message) .tooltip(Tooltip::text(install_command.clone())) + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .icon(IconName::Download) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .icon_position(IconPosition::Start) .on_click(cx.listener(move |this, _, window, cx| { let task = this .workspace @@ -2841,11 +2730,81 @@ impl AcpThreadView { } }) .detach() - })), - ); - } + })); - container.into_any() + (error_message.clone(), Some(button.into_any_element())) + } + LoadError::Unsupported { + error_message, + upgrade_message, + upgrade_command, + } => { + let upgrade_command = upgrade_command.clone(); + let button = Button::new("upgrade", upgrade_message) + .tooltip(Tooltip::text(upgrade_command.clone())) + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .icon(IconName::Download) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .icon_position(IconPosition::Start) + .on_click(cx.listener(move |this, _, window, cx| { + let task = this + .workspace + .update(cx, |workspace, cx| { + let project = workspace.project().read(cx); + let cwd = project.first_project_directory(cx); + let shell = project.terminal_settings(&cwd, cx).shell.clone(); + let spawn_in_terminal = task::SpawnInTerminal { + id: task::TaskId("upgrade".to_string()), + full_label: upgrade_command.clone(), + label: upgrade_command.clone(), + command: Some(upgrade_command.clone()), + args: Vec::new(), + command_label: upgrade_command.clone(), + cwd, + env: Default::default(), + use_new_terminal: true, + allow_concurrent_runs: true, + reveal: Default::default(), + reveal_target: Default::default(), + hide: Default::default(), + shell, + show_summary: true, + show_command: true, + show_rerun: false, + }; + workspace.spawn_in_terminal(spawn_in_terminal, window, cx) + }) + .ok(); + let Some(task) = task else { return }; + cx.spawn_in(window, async move |this, cx| { + if let Some(Ok(_)) = task.await { + this.update_in(cx, |this, window, cx| { + this.reset(window, cx); + }) + .ok(); + } + }) + .detach() + })); + + (error_message.clone(), Some(button.into_any_element())) + } + LoadError::Exited { .. } => ("Server exited with status {status}".into(), None), + LoadError::Other(msg) => ( + msg.into(), + Some(self.create_copy_button(msg.to_string()).into_any_element()), + ), + }; + + Callout::new() + .severity(Severity::Error) + .icon(IconName::XCircleFilled) + .title("Failed to Launch") + .description(message) + .actions_slot(div().children(action_slot)) + .into_any_element() } fn render_activity_bar( @@ -3336,6 +3295,19 @@ impl AcpThreadView { (IconName::Maximize, "Expand Message Editor") }; + let backdrop = div() + .size_full() + .absolute() + .inset_0() + .bg(cx.theme().colors().panel_background) + .opacity(0.8) + .block_mouse_except_scroll(); + + let enable_editor = match self.thread_state { + ThreadState::Loading { .. } | ThreadState::Ready { .. } => true, + ThreadState::Unauthenticated { .. } | ThreadState::LoadError(..) => false, + }; + v_flex() .on_action(cx.listener(Self::expand_message_editor)) .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| { @@ -3411,6 +3383,7 @@ impl AcpThreadView { .child(self.render_send_button(cx)), ), ) + .when(!enable_editor, |this| this.child(backdrop)) .into_any() } @@ -3913,18 +3886,19 @@ impl AcpThreadView { return; } - let title = self.title(cx); + // TODO: Change this once we have title summarization for external agents. + let title = self.agent.name(); match AgentSettings::get_global(cx).notify_when_agent_waiting { NotifyWhenAgentWaiting::PrimaryScreen => { if let Some(primary) = cx.primary_display() { - self.pop_up(icon, caption.into(), title, window, primary, cx); + self.pop_up(icon, caption.into(), title.into(), window, primary, cx); } } NotifyWhenAgentWaiting::AllScreens => { let caption = caption.into(); for screen in cx.displays() { - self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx); + self.pop_up(icon, caption.clone(), title.into(), window, screen, cx); } } NotifyWhenAgentWaiting::Never => { @@ -4423,6 +4397,7 @@ impl AcpThreadView { Callout::new() .severity(Severity::Error) .title("Error") + .icon(IconName::XCircle) .description(error.clone()) .actions_slot(self.create_copy_button(error.to_string())) .dismiss_action(self.dismiss_error_button(cx)) @@ -4434,6 +4409,7 @@ impl AcpThreadView { Callout::new() .severity(Severity::Error) + .icon(IconName::XCircle) .title("Free Usage Exceeded") .description(ERROR_MESSAGE) .actions_slot( @@ -4453,6 +4429,7 @@ impl AcpThreadView { Callout::new() .severity(Severity::Error) .title("Authentication Required") + .icon(IconName::XCircle) .description(error.clone()) .actions_slot( h_flex() @@ -4478,6 +4455,7 @@ impl AcpThreadView { Callout::new() .severity(Severity::Error) .title("Model Prompt Limit Reached") + .icon(IconName::XCircle) .description(error_message) .actions_slot( h_flex() @@ -4648,7 +4626,14 @@ impl AcpThreadView { impl Focusable for AcpThreadView { fn focus_handle(&self, cx: &App) -> FocusHandle { - self.message_editor.focus_handle(cx) + match self.thread_state { + ThreadState::Loading { .. } | ThreadState::Ready { .. } => { + self.message_editor.focus_handle(cx) + } + ThreadState::LoadError(_) | ThreadState::Unauthenticated { .. } => { + self.focus_handle.clone() + } + } } } @@ -4664,6 +4649,7 @@ impl Render for AcpThreadView { .on_action(cx.listener(Self::toggle_burn_mode)) .on_action(cx.listener(Self::keep_all)) .on_action(cx.listener(Self::reject_all)) + .track_focus(&self.focus_handle) .bg(cx.theme().colors().panel_background) .child(match &self.thread_state { ThreadState::Unauthenticated { @@ -4680,14 +4666,14 @@ impl Render for AcpThreadView { window, cx, ), - ThreadState::Loading { .. } => { - v_flex().flex_1().child(self.render_empty_state(window, cx)) - } - ThreadState::LoadError(e) => v_flex() - .p_2() + ThreadState::Loading { .. } => v_flex() .flex_1() + .child(self.render_recent_history(window, cx)), + ThreadState::LoadError(e) => v_flex() + .flex_1() + .size_full() .items_center() - .justify_center() + .justify_end() .child(self.render_load_error(e, cx)), ThreadState::Ready { thread, .. } => { let thread_clone = thread.clone(); @@ -4724,7 +4710,7 @@ impl Render for AcpThreadView { }, ) } else { - this.child(self.render_empty_state(window, cx)) + this.child(self.render_recent_history(window, cx)) } }) } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index d0fb676fd2..0e611d0db9 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -2097,7 +2097,7 @@ impl AgentPanel { .child(title_editor) .into_any_element() } else { - Label::new(thread_view.read(cx).title(cx)) + Label::new(thread_view.read(cx).title()) .color(Color::Muted) .truncate() .into_any_element() diff --git a/crates/ui/src/components/callout.rs b/crates/ui/src/components/callout.rs index 7ffeda881c..b1ead18ee7 100644 --- a/crates/ui/src/components/callout.rs +++ b/crates/ui/src/components/callout.rs @@ -132,6 +132,7 @@ impl RenderOnce for Callout { h_flex() .min_w_0() + .w_full() .p_2() .gap_2() .items_start() From f649c31bf94ac56757aff1394c5a0926232285af Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Aug 2025 14:10:45 -0700 Subject: [PATCH 627/693] Restructure persistence of remote workspaces to make room for WSL and other non-ssh remote projects (#36714) This is another pure refactor, to prepare for adding direct WSL support. ### Todo * [x] Represent `paths` in the same way for all workspaces, instead of having a completely separate SSH representation * [x] Adjust sqlite tables * [x] `ssh_projects` -> `ssh_connections` (drop paths) * [x] `workspaces.local_paths` -> `paths` * [x] remove duplicate path columns on `workspaces` * [x] Add migrations for backward-compatibility Release Notes: - N/A --------- Co-authored-by: Mikayla Maki --- Cargo.lock | 2 +- crates/client/src/user.rs | 5 - .../src/disconnected_overlay.rs | 14 +- crates/recent_projects/src/recent_projects.rs | 62 +- crates/workspace/Cargo.toml | 2 +- crates/workspace/src/history_manager.rs | 18 +- crates/workspace/src/path_list.rs | 121 ++ crates/workspace/src/persistence.rs | 1121 ++++++++--------- crates/workspace/src/persistence/model.rs | 305 +---- crates/workspace/src/workspace.rs | 182 +-- crates/zed/src/main.rs | 15 +- crates/zed/src/zed/open_listener.rs | 17 +- 12 files changed, 784 insertions(+), 1080 deletions(-) create mode 100644 crates/workspace/src/path_list.rs diff --git a/Cargo.lock b/Cargo.lock index cd1018d4c9..4043666823 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19829,7 +19829,6 @@ dependencies = [ "any_vec", "anyhow", "async-recursion", - "bincode", "call", "client", "clock", @@ -19848,6 +19847,7 @@ dependencies = [ "node_runtime", "parking_lot", "postage", + "pretty_assertions", "project", "remote", "schemars", diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 1f8174dbc3..d23eb37519 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -46,11 +46,6 @@ impl ProjectId { } } -#[derive( - Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, -)] -pub struct DevServerProjectId(pub u64); - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ParticipantIndex(pub u32); diff --git a/crates/recent_projects/src/disconnected_overlay.rs b/crates/recent_projects/src/disconnected_overlay.rs index dd4d788cfd..8ffe0ef07c 100644 --- a/crates/recent_projects/src/disconnected_overlay.rs +++ b/crates/recent_projects/src/disconnected_overlay.rs @@ -1,5 +1,3 @@ -use std::path::PathBuf; - use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, Focusable, Render, WeakEntity}; use project::project_settings::ProjectSettings; use remote::SshConnectionOptions; @@ -103,17 +101,17 @@ impl DisconnectedOverlay { return; }; - let Some(ssh_project) = workspace.read(cx).serialized_ssh_project() else { - return; - }; - let Some(window_handle) = window.window_handle().downcast::() else { return; }; let app_state = workspace.read(cx).app_state().clone(); - - let paths = ssh_project.paths.iter().map(PathBuf::from).collect(); + let paths = workspace + .read(cx) + .root_paths(cx) + .iter() + .map(|path| path.to_path_buf()) + .collect(); cx.spawn_in(window, async move |_, cx| { open_ssh_project( diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 2093e96cae..fa57b588cd 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -19,15 +19,12 @@ use picker::{ pub use remote_servers::RemoteServerProjects; use settings::Settings; pub use ssh_connections::SshSettings; -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; +use std::{path::Path, sync::Arc}; use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, tooltip_container}; use util::{ResultExt, paths::PathExt}; use workspace::{ - CloseIntent, HistoryManager, ModalView, OpenOptions, SerializedWorkspaceLocation, WORKSPACE_DB, - Workspace, WorkspaceId, with_active_or_new_workspace, + CloseIntent, HistoryManager, ModalView, OpenOptions, PathList, SerializedWorkspaceLocation, + WORKSPACE_DB, Workspace, WorkspaceId, with_active_or_new_workspace, }; use zed_actions::{OpenRecent, OpenRemote}; @@ -154,7 +151,7 @@ impl Render for RecentProjects { pub struct RecentProjectsDelegate { workspace: WeakEntity, - workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>, + workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>, selected_match_index: usize, matches: Vec, render_paths: bool, @@ -178,12 +175,15 @@ impl RecentProjectsDelegate { } } - pub fn set_workspaces(&mut self, workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>) { + pub fn set_workspaces( + &mut self, + workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>, + ) { self.workspaces = workspaces; self.has_any_non_local_projects = !self .workspaces .iter() - .all(|(_, location)| matches!(location, SerializedWorkspaceLocation::Local(_, _))); + .all(|(_, location, _)| matches!(location, SerializedWorkspaceLocation::Local)); } } impl EventEmitter for RecentProjectsDelegate {} @@ -236,15 +236,14 @@ impl PickerDelegate for RecentProjectsDelegate { .workspaces .iter() .enumerate() - .filter(|(_, (id, _))| !self.is_current_workspace(*id, cx)) - .map(|(id, (_, location))| { - let combined_string = location - .sorted_paths() + .filter(|(_, (id, _, _))| !self.is_current_workspace(*id, cx)) + .map(|(id, (_, _, paths))| { + let combined_string = paths + .paths() .iter() .map(|path| path.compact().to_string_lossy().into_owned()) .collect::>() .join(""); - StringMatchCandidate::new(id, &combined_string) }) .collect::>(); @@ -279,7 +278,7 @@ impl PickerDelegate for RecentProjectsDelegate { .get(self.selected_index()) .zip(self.workspace.upgrade()) { - let (candidate_workspace_id, candidate_workspace_location) = + let (candidate_workspace_id, candidate_workspace_location, candidate_workspace_paths) = &self.workspaces[selected_match.candidate_id]; let replace_current_window = if self.create_new_window { secondary @@ -292,8 +291,8 @@ impl PickerDelegate for RecentProjectsDelegate { Task::ready(Ok(())) } else { match candidate_workspace_location { - SerializedWorkspaceLocation::Local(paths, _) => { - let paths = paths.paths().to_vec(); + SerializedWorkspaceLocation::Local => { + let paths = candidate_workspace_paths.paths().to_vec(); if replace_current_window { cx.spawn_in(window, async move |workspace, cx| { let continue_replacing = workspace @@ -321,7 +320,7 @@ impl PickerDelegate for RecentProjectsDelegate { workspace.open_workspace_for_paths(false, paths, window, cx) } } - SerializedWorkspaceLocation::Ssh(ssh_project) => { + SerializedWorkspaceLocation::Ssh(connection) => { let app_state = workspace.app_state().clone(); let replace_window = if replace_current_window { @@ -337,12 +336,12 @@ impl PickerDelegate for RecentProjectsDelegate { let connection_options = SshSettings::get_global(cx) .connection_options_for( - ssh_project.host.clone(), - ssh_project.port, - ssh_project.user.clone(), + connection.host.clone(), + connection.port, + connection.user.clone(), ); - let paths = ssh_project.paths.iter().map(PathBuf::from).collect(); + let paths = candidate_workspace_paths.paths().to_vec(); cx.spawn_in(window, async move |_, cx| { open_ssh_project( @@ -383,12 +382,12 @@ impl PickerDelegate for RecentProjectsDelegate { ) -> Option { let hit = self.matches.get(ix)?; - let (_, location) = self.workspaces.get(hit.candidate_id)?; + let (_, location, paths) = self.workspaces.get(hit.candidate_id)?; let mut path_start_offset = 0; - let (match_labels, paths): (Vec<_>, Vec<_>) = location - .sorted_paths() + let (match_labels, paths): (Vec<_>, Vec<_>) = paths + .paths() .iter() .map(|p| p.compact()) .map(|path| { @@ -416,11 +415,9 @@ impl PickerDelegate for RecentProjectsDelegate { .gap_3() .when(self.has_any_non_local_projects, |this| { this.child(match location { - SerializedWorkspaceLocation::Local(_, _) => { - Icon::new(IconName::Screen) - .color(Color::Muted) - .into_any_element() - } + SerializedWorkspaceLocation::Local => Icon::new(IconName::Screen) + .color(Color::Muted) + .into_any_element(), SerializedWorkspaceLocation::Ssh(_) => Icon::new(IconName::Server) .color(Color::Muted) .into_any_element(), @@ -568,7 +565,7 @@ impl RecentProjectsDelegate { cx: &mut Context>, ) { if let Some(selected_match) = self.matches.get(ix) { - let (workspace_id, _) = self.workspaces[selected_match.candidate_id]; + let (workspace_id, _, _) = self.workspaces[selected_match.candidate_id]; cx.spawn_in(window, async move |this, cx| { let _ = WORKSPACE_DB.delete_workspace_by_id(workspace_id).await; let workspaces = WORKSPACE_DB @@ -707,7 +704,8 @@ mod tests { }]; delegate.set_workspaces(vec![( WorkspaceId::default(), - SerializedWorkspaceLocation::from_local_paths(vec![path!("/test/path/")]), + SerializedWorkspaceLocation::Local, + PathList::new(&[path!("/test/path")]), )]); }); }) diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 570657ba8f..869aa5322e 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -29,7 +29,6 @@ test-support = [ any_vec.workspace = true anyhow.workspace = true async-recursion.workspace = true -bincode.workspace = true call.workspace = true client.workspace = true clock.workspace = true @@ -80,5 +79,6 @@ project = { workspace = true, features = ["test-support"] } session = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } http_client = { workspace = true, features = ["test-support"] } +pretty_assertions.workspace = true tempfile.workspace = true zlog.workspace = true diff --git a/crates/workspace/src/history_manager.rs b/crates/workspace/src/history_manager.rs index a8387369f4..f68b58ff82 100644 --- a/crates/workspace/src/history_manager.rs +++ b/crates/workspace/src/history_manager.rs @@ -5,7 +5,9 @@ use smallvec::SmallVec; use ui::App; use util::{ResultExt, paths::PathExt}; -use crate::{NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId}; +use crate::{ + NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId, path_list::PathList, +}; pub fn init(cx: &mut App) { let manager = cx.new(|_| HistoryManager::new()); @@ -44,7 +46,13 @@ impl HistoryManager { .unwrap_or_default() .into_iter() .rev() - .map(|(id, location)| HistoryManagerEntry::new(id, &location)) + .filter_map(|(id, location, paths)| { + if matches!(location, SerializedWorkspaceLocation::Local) { + Some(HistoryManagerEntry::new(id, &paths)) + } else { + None + } + }) .collect::>(); this.update(cx, |this, cx| { this.history = recent_folders; @@ -118,9 +126,9 @@ impl HistoryManager { } impl HistoryManagerEntry { - pub fn new(id: WorkspaceId, location: &SerializedWorkspaceLocation) -> Self { - let path = location - .sorted_paths() + pub fn new(id: WorkspaceId, paths: &PathList) -> Self { + let path = paths + .paths() .iter() .map(|path| path.compact()) .collect::>(); diff --git a/crates/workspace/src/path_list.rs b/crates/workspace/src/path_list.rs new file mode 100644 index 0000000000..4f9ed42312 --- /dev/null +++ b/crates/workspace/src/path_list.rs @@ -0,0 +1,121 @@ +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + +use util::paths::SanitizedPath; + +/// A list of absolute paths, in a specific order. +/// +/// The paths are stored in lexicographic order, so that they can be compared to +/// other path lists without regard to the order of the paths. +#[derive(Default, PartialEq, Eq, Debug, Clone)] +pub struct PathList { + paths: Arc<[PathBuf]>, + order: Arc<[usize]>, +} + +#[derive(Debug)] +pub struct SerializedPathList { + pub paths: String, + pub order: String, +} + +impl PathList { + pub fn new>(paths: &[P]) -> Self { + let mut indexed_paths: Vec<(usize, PathBuf)> = paths + .iter() + .enumerate() + .map(|(ix, path)| (ix, SanitizedPath::from(path).into())) + .collect(); + indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b)); + let order = indexed_paths.iter().map(|e| e.0).collect::>().into(); + let paths = indexed_paths + .into_iter() + .map(|e| e.1) + .collect::>() + .into(); + Self { order, paths } + } + + pub fn is_empty(&self) -> bool { + self.paths.is_empty() + } + + pub fn paths(&self) -> &[PathBuf] { + self.paths.as_ref() + } + + pub fn order(&self) -> &[usize] { + self.order.as_ref() + } + + pub fn is_lexicographically_ordered(&self) -> bool { + self.order.iter().enumerate().all(|(i, &j)| i == j) + } + + pub fn deserialize(serialized: &SerializedPathList) -> Self { + let mut paths: Vec = if serialized.paths.is_empty() { + Vec::new() + } else { + serde_json::from_str::>(&serialized.paths) + .unwrap_or(Vec::new()) + .into_iter() + .map(|s| SanitizedPath::from(s).into()) + .collect() + }; + + let mut order: Vec = serialized + .order + .split(',') + .filter_map(|s| s.parse().ok()) + .collect(); + + if !paths.is_sorted() || order.len() != paths.len() { + order = (0..paths.len()).collect(); + paths.sort(); + } + + Self { + paths: paths.into(), + order: order.into(), + } + } + + pub fn serialize(&self) -> SerializedPathList { + use std::fmt::Write as _; + + let paths = serde_json::to_string(&self.paths).unwrap_or_default(); + + let mut order = String::new(); + for ix in self.order.iter() { + if !order.is_empty() { + order.push(','); + } + write!(&mut order, "{}", *ix).unwrap(); + } + SerializedPathList { paths, order } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_path_list() { + let list1 = PathList::new(&["a/d", "a/c"]); + let list2 = PathList::new(&["a/c", "a/d"]); + + assert_eq!(list1.paths(), list2.paths()); + assert_ne!(list1, list2); + assert_eq!(list1.order(), &[1, 0]); + assert_eq!(list2.order(), &[0, 1]); + + let list1_deserialized = PathList::deserialize(&list1.serialize()); + assert_eq!(list1_deserialized, list1); + + let list2_deserialized = PathList::deserialize(&list2.serialize()); + assert_eq!(list2_deserialized, list2); + } +} diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index b2d1340a7b..de8f63957c 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -9,10 +9,8 @@ use std::{ }; use anyhow::{Context as _, Result, bail}; -use client::DevServerProjectId; use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size}; -use itertools::Itertools; use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}; use language::{LanguageName, Toolchain}; @@ -28,14 +26,17 @@ use ui::{App, px}; use util::{ResultExt, maybe}; use uuid::Uuid; -use crate::WorkspaceId; - -use model::{ - GroupId, ItemId, LocalPaths, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, - SerializedSshProject, SerializedWorkspace, +use crate::{ + WorkspaceId, + path_list::{PathList, SerializedPathList}, }; -use self::model::{DockStructure, LocalPathsOrder, SerializedWorkspaceLocation}; +use model::{ + GroupId, ItemId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, + SerializedSshConnection, SerializedWorkspace, +}; + +use self::model::{DockStructure, SerializedWorkspaceLocation}; #[derive(Copy, Clone, Debug, PartialEq)] pub(crate) struct SerializedAxis(pub(crate) gpui::Axis); @@ -275,70 +276,9 @@ impl sqlez::bindable::Bind for SerializedPixels { } define_connection! { - // Current schema shape using pseudo-rust syntax: - // - // workspaces( - // workspace_id: usize, // Primary key for workspaces - // local_paths: Bincode>, - // local_paths_order: Bincode>, - // dock_visible: bool, // Deprecated - // dock_anchor: DockAnchor, // Deprecated - // dock_pane: Option, // Deprecated - // left_sidebar_open: boolean, - // timestamp: String, // UTC YYYY-MM-DD HH:MM:SS - // window_state: String, // WindowBounds Discriminant - // window_x: Option, // WindowBounds::Fixed RectF x - // window_y: Option, // WindowBounds::Fixed RectF y - // window_width: Option, // WindowBounds::Fixed RectF width - // window_height: Option, // WindowBounds::Fixed RectF height - // display: Option, // Display id - // fullscreen: Option, // Is the window fullscreen? - // centered_layout: Option, // Is the Centered Layout mode activated? - // session_id: Option, // Session id - // window_id: Option, // Window Id - // ) - // - // pane_groups( - // group_id: usize, // Primary key for pane_groups - // workspace_id: usize, // References workspaces table - // parent_group_id: Option, // None indicates that this is the root node - // position: Option, // None indicates that this is the root node - // axis: Option, // 'Vertical', 'Horizontal' - // flexes: Option>, // A JSON array of floats - // ) - // - // panes( - // pane_id: usize, // Primary key for panes - // workspace_id: usize, // References workspaces table - // active: bool, - // ) - // - // center_panes( - // pane_id: usize, // Primary key for center_panes - // parent_group_id: Option, // References pane_groups. If none, this is the root - // position: Option, // None indicates this is the root - // ) - // - // CREATE TABLE items( - // item_id: usize, // This is the item's view id, so this is not unique - // workspace_id: usize, // References workspaces table - // pane_id: usize, // References panes table - // kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global - // position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column - // active: bool, // Indicates if this item is the active one in the pane - // preview: bool // Indicates if this item is a preview item - // ) - // - // CREATE TABLE breakpoints( - // workspace_id: usize Foreign Key, // References workspace table - // path: PathBuf, // The absolute path of the file that this breakpoint belongs to - // breakpoint_location: Vec, // A list of the locations of breakpoints - // kind: int, // The kind of breakpoint (standard, log) - // log_message: String, // log message for log breakpoints, otherwise it's Null - // ) pub static ref DB: WorkspaceDb<()> = &[ - sql!( + sql!( CREATE TABLE workspaces( workspace_id INTEGER PRIMARY KEY, workspace_location BLOB UNIQUE, @@ -555,7 +495,109 @@ define_connection! { SELECT * FROM toolchains; DROP TABLE toolchains; ALTER TABLE toolchains2 RENAME TO toolchains; - ) + ), + sql!( + CREATE TABLE ssh_connections ( + id INTEGER PRIMARY KEY, + host TEXT NOT NULL, + port INTEGER, + user TEXT + ); + + INSERT INTO ssh_connections (host, port, user) + SELECT DISTINCT host, port, user + FROM ssh_projects; + + CREATE TABLE workspaces_2( + workspace_id INTEGER PRIMARY KEY, + paths TEXT, + paths_order TEXT, + ssh_connection_id INTEGER REFERENCES ssh_connections(id), + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + window_state TEXT, + window_x REAL, + window_y REAL, + window_width REAL, + window_height REAL, + display BLOB, + left_dock_visible INTEGER, + left_dock_active_panel TEXT, + right_dock_visible INTEGER, + right_dock_active_panel TEXT, + bottom_dock_visible INTEGER, + bottom_dock_active_panel TEXT, + left_dock_zoom INTEGER, + right_dock_zoom INTEGER, + bottom_dock_zoom INTEGER, + fullscreen INTEGER, + centered_layout INTEGER, + session_id TEXT, + window_id INTEGER + ) STRICT; + + INSERT + INTO workspaces_2 + SELECT + workspaces.workspace_id, + CASE + WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths + ELSE + CASE + WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN + NULL + ELSE + json('[' || '"' || replace(workspaces.local_paths_array, ',', '"' || "," || '"') || '"' || ']') + END + END as paths, + + CASE + WHEN ssh_projects.id IS NOT NULL THEN "" + ELSE workspaces.local_paths_order_array + END as paths_order, + + CASE + WHEN ssh_projects.id IS NOT NULL THEN ( + SELECT ssh_connections.id + FROM ssh_connections + WHERE + ssh_connections.host IS ssh_projects.host AND + ssh_connections.port IS ssh_projects.port AND + ssh_connections.user IS ssh_projects.user + ) + ELSE NULL + END as ssh_connection_id, + + workspaces.timestamp, + workspaces.window_state, + workspaces.window_x, + workspaces.window_y, + workspaces.window_width, + workspaces.window_height, + workspaces.display, + workspaces.left_dock_visible, + workspaces.left_dock_active_panel, + workspaces.right_dock_visible, + workspaces.right_dock_active_panel, + workspaces.bottom_dock_visible, + workspaces.bottom_dock_active_panel, + workspaces.left_dock_zoom, + workspaces.right_dock_zoom, + workspaces.bottom_dock_zoom, + workspaces.fullscreen, + workspaces.centered_layout, + workspaces.session_id, + workspaces.window_id + FROM + workspaces LEFT JOIN + ssh_projects ON + workspaces.ssh_project_id = ssh_projects.id; + + DROP TABLE ssh_projects; + DROP TABLE workspaces; + ALTER TABLE workspaces_2 RENAME TO workspaces; + + CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths); + ), ]; } @@ -566,17 +608,33 @@ impl WorkspaceDb { pub(crate) fn workspace_for_roots>( &self, worktree_roots: &[P], + ) -> Option { + self.workspace_for_roots_internal(worktree_roots, None) + } + + pub(crate) fn ssh_workspace_for_roots>( + &self, + worktree_roots: &[P], + ssh_project_id: SshProjectId, + ) -> Option { + self.workspace_for_roots_internal(worktree_roots, Some(ssh_project_id)) + } + + pub(crate) fn workspace_for_roots_internal>( + &self, + worktree_roots: &[P], + ssh_connection_id: Option, ) -> Option { // paths are sorted before db interactions to ensure that the order of the paths // doesn't affect the workspace selection for existing workspaces - let local_paths = LocalPaths::new(worktree_roots); + let root_paths = PathList::new(worktree_roots); // Note that we re-assign the workspace_id here in case it's empty // and we've grabbed the most recent workspace let ( workspace_id, - local_paths, - local_paths_order, + paths, + paths_order, window_bounds, display, centered_layout, @@ -584,8 +642,8 @@ impl WorkspaceDb { window_id, ): ( WorkspaceId, - Option, - Option, + String, + String, Option, Option, Option, @@ -595,8 +653,8 @@ impl WorkspaceDb { .select_row_bound(sql! { SELECT workspace_id, - local_paths, - local_paths_order, + paths, + paths_order, window_state, window_x, window_y, @@ -615,25 +673,31 @@ impl WorkspaceDb { bottom_dock_zoom, window_id FROM workspaces - WHERE local_paths = ? + WHERE + paths IS ? AND + ssh_connection_id IS ? + LIMIT 1 + }) + .map(|mut prepared_statement| { + (prepared_statement)(( + root_paths.serialize().paths, + ssh_connection_id.map(|id| id.0 as i32), + )) + .unwrap() }) - .and_then(|mut prepared_statement| (prepared_statement)(&local_paths)) .context("No workspaces found") .warn_on_err() .flatten()?; - let local_paths = local_paths?; - let location = match local_paths_order { - Some(order) => SerializedWorkspaceLocation::Local(local_paths, order), - None => { - let order = LocalPathsOrder::default_for_paths(&local_paths); - SerializedWorkspaceLocation::Local(local_paths, order) - } - }; + let paths = PathList::deserialize(&SerializedPathList { + paths, + order: paths_order, + }); Some(SerializedWorkspace { id: workspace_id, - location, + location: SerializedWorkspaceLocation::Local, + paths, center_group: self .get_center_pane_group(workspace_id) .context("Getting center group") @@ -648,63 +712,6 @@ impl WorkspaceDb { }) } - pub(crate) fn workspace_for_ssh_project( - &self, - ssh_project: &SerializedSshProject, - ) -> Option { - let (workspace_id, window_bounds, display, centered_layout, docks, window_id): ( - WorkspaceId, - Option, - Option, - Option, - DockStructure, - Option, - ) = self - .select_row_bound(sql! { - SELECT - workspace_id, - window_state, - window_x, - window_y, - window_width, - window_height, - display, - centered_layout, - left_dock_visible, - left_dock_active_panel, - left_dock_zoom, - right_dock_visible, - right_dock_active_panel, - right_dock_zoom, - bottom_dock_visible, - bottom_dock_active_panel, - bottom_dock_zoom, - window_id - FROM workspaces - WHERE ssh_project_id = ? - }) - .and_then(|mut prepared_statement| (prepared_statement)(ssh_project.id.0)) - .context("No workspaces found") - .warn_on_err() - .flatten()?; - - Some(SerializedWorkspace { - id: workspace_id, - location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()), - center_group: self - .get_center_pane_group(workspace_id) - .context("Getting center group") - .log_err()?, - window_bounds, - centered_layout: centered_layout.unwrap_or(false), - breakpoints: self.breakpoints(workspace_id), - display, - docks, - session_id: None, - window_id, - }) - } - fn breakpoints(&self, workspace_id: WorkspaceId) -> BTreeMap, Vec> { let breakpoints: Result> = self .select_bound(sql! { @@ -754,6 +761,13 @@ impl WorkspaceDb { /// Saves a workspace using the worktree roots. Will garbage collect any workspaces /// that used this workspace previously pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) { + let paths = workspace.paths.serialize(); + let ssh_connection_id = match &workspace.location { + SerializedWorkspaceLocation::Local => None, + SerializedWorkspaceLocation::Ssh(serialized_ssh_connection) => { + Some(serialized_ssh_connection.id.0) + } + }; log::debug!("Saving workspace at location: {:?}", workspace.location); self.write(move |conn| { conn.with_savepoint("update_worktrees", || { @@ -763,7 +777,12 @@ impl WorkspaceDb { DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id) .context("Clearing old panes")?; - conn.exec_bound(sql!(DELETE FROM breakpoints WHERE workspace_id = ?1))?(workspace.id).context("Clearing old breakpoints")?; + conn.exec_bound( + sql!( + DELETE FROM breakpoints WHERE workspace_id = ?1; + DELETE FROM toolchains WHERE workspace_id = ?1; + ) + )?(workspace.id).context("Clearing old breakpoints")?; for (path, breakpoints) in workspace.breakpoints { for bp in breakpoints { @@ -790,115 +809,73 @@ impl WorkspaceDb { } } } - } + conn.exec_bound(sql!( + DELETE + FROM workspaces + WHERE + workspace_id != ?1 AND + paths IS ?2 AND + ssh_connection_id IS ?3 + ))?(( + workspace.id, + paths.paths.clone(), + ssh_connection_id, + )) + .context("clearing out old locations")?; - match workspace.location { - SerializedWorkspaceLocation::Local(local_paths, local_paths_order) => { - conn.exec_bound(sql!( - DELETE FROM toolchains WHERE workspace_id = ?1; - DELETE FROM workspaces WHERE local_paths = ? AND workspace_id != ? - ))?((&local_paths, workspace.id)) - .context("clearing out old locations")?; + // Upsert + let query = sql!( + INSERT INTO workspaces( + workspace_id, + paths, + paths_order, + ssh_connection_id, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom, + session_id, + window_id, + timestamp + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, CURRENT_TIMESTAMP) + ON CONFLICT DO + UPDATE SET + paths = ?2, + paths_order = ?3, + ssh_connection_id = ?4, + left_dock_visible = ?5, + left_dock_active_panel = ?6, + left_dock_zoom = ?7, + right_dock_visible = ?8, + right_dock_active_panel = ?9, + right_dock_zoom = ?10, + bottom_dock_visible = ?11, + bottom_dock_active_panel = ?12, + bottom_dock_zoom = ?13, + session_id = ?14, + window_id = ?15, + timestamp = CURRENT_TIMESTAMP + ); + let mut prepared_query = conn.exec_bound(query)?; + let args = ( + workspace.id, + paths.paths.clone(), + paths.order.clone(), + ssh_connection_id, + workspace.docks, + workspace.session_id, + workspace.window_id, + ); - // Upsert - let query = sql!( - INSERT INTO workspaces( - workspace_id, - local_paths, - local_paths_order, - left_dock_visible, - left_dock_active_panel, - left_dock_zoom, - right_dock_visible, - right_dock_active_panel, - right_dock_zoom, - bottom_dock_visible, - bottom_dock_active_panel, - bottom_dock_zoom, - session_id, - window_id, - timestamp, - local_paths_array, - local_paths_order_array - ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, CURRENT_TIMESTAMP, ?15, ?16) - ON CONFLICT DO - UPDATE SET - local_paths = ?2, - local_paths_order = ?3, - left_dock_visible = ?4, - left_dock_active_panel = ?5, - left_dock_zoom = ?6, - right_dock_visible = ?7, - right_dock_active_panel = ?8, - right_dock_zoom = ?9, - bottom_dock_visible = ?10, - bottom_dock_active_panel = ?11, - bottom_dock_zoom = ?12, - session_id = ?13, - window_id = ?14, - timestamp = CURRENT_TIMESTAMP, - local_paths_array = ?15, - local_paths_order_array = ?16 - ); - let mut prepared_query = conn.exec_bound(query)?; - let args = (workspace.id, &local_paths, &local_paths_order, workspace.docks, workspace.session_id, workspace.window_id, local_paths.paths().iter().map(|path| path.to_string_lossy().to_string()).join(","), local_paths_order.order().iter().map(|order| order.to_string()).join(",")); - - prepared_query(args).context("Updating workspace")?; - } - SerializedWorkspaceLocation::Ssh(ssh_project) => { - conn.exec_bound(sql!( - DELETE FROM toolchains WHERE workspace_id = ?1; - DELETE FROM workspaces WHERE ssh_project_id = ? AND workspace_id != ? - ))?((ssh_project.id.0, workspace.id)) - .context("clearing out old locations")?; - - // Upsert - conn.exec_bound(sql!( - INSERT INTO workspaces( - workspace_id, - ssh_project_id, - left_dock_visible, - left_dock_active_panel, - left_dock_zoom, - right_dock_visible, - right_dock_active_panel, - right_dock_zoom, - bottom_dock_visible, - bottom_dock_active_panel, - bottom_dock_zoom, - session_id, - window_id, - timestamp - ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, CURRENT_TIMESTAMP) - ON CONFLICT DO - UPDATE SET - ssh_project_id = ?2, - left_dock_visible = ?3, - left_dock_active_panel = ?4, - left_dock_zoom = ?5, - right_dock_visible = ?6, - right_dock_active_panel = ?7, - right_dock_zoom = ?8, - bottom_dock_visible = ?9, - bottom_dock_active_panel = ?10, - bottom_dock_zoom = ?11, - session_id = ?12, - window_id = ?13, - timestamp = CURRENT_TIMESTAMP - ))?(( - workspace.id, - ssh_project.id.0, - workspace.docks, - workspace.session_id, - workspace.window_id - )) - .context("Updating workspace")?; - } - } + prepared_query(args).context("Updating workspace")?; // Save center pane group Self::save_pane_group(conn, workspace.id, &workspace.center_group, None) @@ -911,89 +888,100 @@ impl WorkspaceDb { .await; } - pub(crate) async fn get_or_create_ssh_project( + pub(crate) async fn get_or_create_ssh_connection( &self, host: String, port: Option, - paths: Vec, user: Option, - ) -> Result { - let paths = serde_json::to_string(&paths)?; - if let Some(project) = self - .get_ssh_project(host.clone(), port, paths.clone(), user.clone()) + ) -> Result { + if let Some(id) = self + .get_ssh_connection(host.clone(), port, user.clone()) .await? { - Ok(project) + Ok(SshProjectId(id)) } else { log::debug!("Inserting SSH project at host {host}"); - self.insert_ssh_project(host, port, paths, user) + let id = self + .insert_ssh_connection(host, port, user) .await? - .context("failed to insert ssh project") + .context("failed to insert ssh project")?; + Ok(SshProjectId(id)) } } query! { - async fn get_ssh_project(host: String, port: Option, paths: String, user: Option) -> Result> { - SELECT id, host, port, paths, user - FROM ssh_projects - WHERE host IS ? AND port IS ? AND paths IS ? AND user IS ? + async fn get_ssh_connection(host: String, port: Option, user: Option) -> Result> { + SELECT id + FROM ssh_connections + WHERE host IS ? AND port IS ? AND user IS ? LIMIT 1 } } query! { - async fn insert_ssh_project(host: String, port: Option, paths: String, user: Option) -> Result> { - INSERT INTO ssh_projects( + async fn insert_ssh_connection(host: String, port: Option, user: Option) -> Result> { + INSERT INTO ssh_connections ( host, port, - paths, user - ) VALUES (?1, ?2, ?3, ?4) - RETURNING id, host, port, paths, user + ) VALUES (?1, ?2, ?3) + RETURNING id } } - query! { - pub async fn update_ssh_project_paths_query(ssh_project_id: u64, paths: String) -> Result> { - UPDATE ssh_projects - SET paths = ?2 - WHERE id = ?1 - RETURNING id, host, port, paths, user - } - } - - pub(crate) async fn update_ssh_project_paths( - &self, - ssh_project_id: SshProjectId, - new_paths: Vec, - ) -> Result { - let paths = serde_json::to_string(&new_paths)?; - self.update_ssh_project_paths_query(ssh_project_id.0, paths) - .await? - .context("failed to update ssh project paths") - } - query! { pub async fn next_id() -> Result { INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id } } + fn recent_workspaces(&self) -> Result)>> { + Ok(self + .recent_workspaces_query()? + .into_iter() + .map(|(id, paths, order, ssh_connection_id)| { + ( + id, + PathList::deserialize(&SerializedPathList { paths, order }), + ssh_connection_id, + ) + }) + .collect()) + } + query! { - fn recent_workspaces() -> Result)>> { - SELECT workspace_id, local_paths, local_paths_order, ssh_project_id + fn recent_workspaces_query() -> Result)>> { + SELECT workspace_id, paths, paths_order, ssh_connection_id FROM workspaces - WHERE local_paths IS NOT NULL - OR ssh_project_id IS NOT NULL + WHERE + paths IS NOT NULL OR + ssh_connection_id IS NOT NULL ORDER BY timestamp DESC } } + fn session_workspaces( + &self, + session_id: String, + ) -> Result, Option)>> { + Ok(self + .session_workspaces_query(session_id)? + .into_iter() + .map(|(paths, order, window_id, ssh_connection_id)| { + ( + PathList::deserialize(&SerializedPathList { paths, order }), + window_id, + ssh_connection_id.map(SshProjectId), + ) + }) + .collect()) + } + query! { - fn session_workspaces(session_id: String) -> Result, Option)>> { - SELECT local_paths, local_paths_order, window_id, ssh_project_id + fn session_workspaces_query(session_id: String) -> Result, Option)>> { + SELECT paths, paths_order, window_id, ssh_connection_id FROM workspaces - WHERE session_id = ?1 AND dev_server_project_id IS NULL + WHERE session_id = ?1 ORDER BY timestamp DESC } } @@ -1013,17 +1001,40 @@ impl WorkspaceDb { } } - query! { - fn ssh_projects() -> Result> { - SELECT id, host, port, paths, user - FROM ssh_projects - } + fn ssh_connections(&self) -> Result> { + Ok(self + .ssh_connections_query()? + .into_iter() + .map(|(id, host, port, user)| SerializedSshConnection { + id: SshProjectId(id), + host, + port, + user, + }) + .collect()) } query! { - fn ssh_project(id: u64) -> Result { - SELECT id, host, port, paths, user - FROM ssh_projects + pub fn ssh_connections_query() -> Result, Option)>> { + SELECT id, host, port, user + FROM ssh_connections + } + } + + pub fn ssh_connection(&self, id: SshProjectId) -> Result { + let row = self.ssh_connection_query(id.0)?; + Ok(SerializedSshConnection { + id: SshProjectId(row.0), + host: row.1, + port: row.2, + user: row.3, + }) + } + + query! { + fn ssh_connection_query(id: u64) -> Result<(u64, String, Option, Option)> { + SELECT id, host, port, user + FROM ssh_connections WHERE id = ? } } @@ -1037,7 +1048,7 @@ impl WorkspaceDb { display, window_state, window_x, window_y, window_width, window_height FROM workspaces - WHERE local_paths + WHERE paths IS NOT NULL ORDER BY timestamp DESC LIMIT 1 @@ -1054,46 +1065,35 @@ impl WorkspaceDb { } } - pub async fn delete_workspace_by_dev_server_project_id( - &self, - id: DevServerProjectId, - ) -> Result<()> { - self.write(move |conn| { - conn.exec_bound(sql!( - DELETE FROM dev_server_projects WHERE id = ? - ))?(id.0)?; - conn.exec_bound(sql!( - DELETE FROM toolchains WHERE workspace_id = ?1; - DELETE FROM workspaces - WHERE dev_server_project_id IS ? - ))?(id.0) - }) - .await - } - // Returns the recent locations which are still valid on disk and deletes ones which no longer // exist. pub async fn recent_workspaces_on_disk( &self, - ) -> Result> { + ) -> Result> { let mut result = Vec::new(); let mut delete_tasks = Vec::new(); - let ssh_projects = self.ssh_projects()?; + let ssh_connections = self.ssh_connections()?; - for (id, location, order, ssh_project_id) in self.recent_workspaces()? { - if let Some(ssh_project_id) = ssh_project_id.map(SshProjectId) { - if let Some(ssh_project) = ssh_projects.iter().find(|rp| rp.id == ssh_project_id) { - result.push((id, SerializedWorkspaceLocation::Ssh(ssh_project.clone()))); + for (id, paths, ssh_connection_id) in self.recent_workspaces()? { + if let Some(ssh_connection_id) = ssh_connection_id.map(SshProjectId) { + if let Some(ssh_connection) = + ssh_connections.iter().find(|rp| rp.id == ssh_connection_id) + { + result.push(( + id, + SerializedWorkspaceLocation::Ssh(ssh_connection.clone()), + paths, + )); } else { delete_tasks.push(self.delete_workspace_by_id(id)); } continue; } - if location.paths().iter().all(|path| path.exists()) - && location.paths().iter().any(|path| path.is_dir()) + if paths.paths().iter().all(|path| path.exists()) + && paths.paths().iter().any(|path| path.is_dir()) { - result.push((id, SerializedWorkspaceLocation::Local(location, order))); + result.push((id, SerializedWorkspaceLocation::Local, paths)); } else { delete_tasks.push(self.delete_workspace_by_id(id)); } @@ -1103,13 +1103,13 @@ impl WorkspaceDb { Ok(result) } - pub async fn last_workspace(&self) -> Result> { + pub async fn last_workspace(&self) -> Result> { Ok(self .recent_workspaces_on_disk() .await? .into_iter() .next() - .map(|(_, location)| location)) + .map(|(_, location, paths)| (location, paths))) } // Returns the locations of the workspaces that were still opened when the last @@ -1120,25 +1120,31 @@ impl WorkspaceDb { &self, last_session_id: &str, last_session_window_stack: Option>, - ) -> Result> { + ) -> Result> { let mut workspaces = Vec::new(); - for (location, order, window_id, ssh_project_id) in + for (paths, window_id, ssh_connection_id) in self.session_workspaces(last_session_id.to_owned())? { - if let Some(ssh_project_id) = ssh_project_id { - let location = SerializedWorkspaceLocation::Ssh(self.ssh_project(ssh_project_id)?); - workspaces.push((location, window_id.map(WindowId::from))); - } else if location.paths().iter().all(|path| path.exists()) - && location.paths().iter().any(|path| path.is_dir()) + if let Some(ssh_connection_id) = ssh_connection_id { + workspaces.push(( + SerializedWorkspaceLocation::Ssh(self.ssh_connection(ssh_connection_id)?), + paths, + window_id.map(WindowId::from), + )); + } else if paths.paths().iter().all(|path| path.exists()) + && paths.paths().iter().any(|path| path.is_dir()) { - let location = SerializedWorkspaceLocation::Local(location, order); - workspaces.push((location, window_id.map(WindowId::from))); + workspaces.push(( + SerializedWorkspaceLocation::Local, + paths, + window_id.map(WindowId::from), + )); } } if let Some(stack) = last_session_window_stack { - workspaces.sort_by_key(|(_, window_id)| { + workspaces.sort_by_key(|(_, _, window_id)| { window_id .and_then(|id| stack.iter().position(|&order_id| order_id == id)) .unwrap_or(usize::MAX) @@ -1147,7 +1153,7 @@ impl WorkspaceDb { Ok(workspaces .into_iter() - .map(|(paths, _)| paths) + .map(|(location, paths, _)| (location, paths)) .collect::>()) } @@ -1499,13 +1505,13 @@ pub fn delete_unloaded_items( #[cfg(test)] mod tests { - use std::thread; - use std::time::Duration; - use super::*; - use crate::persistence::model::SerializedWorkspace; - use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup}; + use crate::persistence::model::{ + SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, + }; use gpui; + use pretty_assertions::assert_eq; + use std::{thread, time::Duration}; #[gpui::test] async fn test_breakpoints() { @@ -1558,7 +1564,8 @@ mod tests { let workspace = SerializedWorkspace { id, - location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), + paths: PathList::new(&["/tmp"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1711,7 +1718,8 @@ mod tests { let workspace = SerializedWorkspace { id, - location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), + paths: PathList::new(&["/tmp"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1757,7 +1765,8 @@ mod tests { let workspace_without_breakpoint = SerializedWorkspace { id, - location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), + paths: PathList::new(&["/tmp"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1851,7 +1860,8 @@ mod tests { let mut workspace_1 = SerializedWorkspace { id: WorkspaceId(1), - location: SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]), + paths: PathList::new(&["/tmp", "/tmp2"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1864,7 +1874,8 @@ mod tests { let workspace_2 = SerializedWorkspace { id: WorkspaceId(2), - location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), + paths: PathList::new(&["/tmp"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1893,7 +1904,7 @@ mod tests { }) .await; - workspace_1.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp3"]); + workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]); db.save_workspace(workspace_1.clone()).await; db.save_workspace(workspace_1).await; db.save_workspace(workspace_2).await; @@ -1969,10 +1980,8 @@ mod tests { let workspace = SerializedWorkspace { id: WorkspaceId(5), - location: SerializedWorkspaceLocation::Local( - LocalPaths::new(["/tmp", "/tmp2"]), - LocalPathsOrder::new([1, 0]), - ), + paths: PathList::new(&["/tmp", "/tmp2"]), + location: SerializedWorkspaceLocation::Local, center_group, window_bounds: Default::default(), breakpoints: Default::default(), @@ -2004,10 +2013,8 @@ mod tests { let workspace_1 = SerializedWorkspace { id: WorkspaceId(1), - location: SerializedWorkspaceLocation::Local( - LocalPaths::new(["/tmp", "/tmp2"]), - LocalPathsOrder::new([0, 1]), - ), + paths: PathList::new(&["/tmp", "/tmp2"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), breakpoints: Default::default(), @@ -2020,7 +2027,8 @@ mod tests { let mut workspace_2 = SerializedWorkspace { id: WorkspaceId(2), - location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), + paths: PathList::new(&["/tmp"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2049,7 +2057,7 @@ mod tests { assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None); // Test 'mutate' case of updating a pre-existing id - workspace_2.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]); + workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]); db.save_workspace(workspace_2.clone()).await; assert_eq!( @@ -2060,10 +2068,8 @@ mod tests { // Test other mechanism for mutating let mut workspace_3 = SerializedWorkspace { id: WorkspaceId(3), - location: SerializedWorkspaceLocation::Local( - LocalPaths::new(["/tmp", "/tmp2"]), - LocalPathsOrder::new([1, 0]), - ), + paths: PathList::new(&["/tmp2", "/tmp"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), breakpoints: Default::default(), @@ -2081,8 +2087,7 @@ mod tests { ); // Make sure that updating paths differently also works - workspace_3.location = - SerializedWorkspaceLocation::from_local_paths(["/tmp3", "/tmp4", "/tmp2"]); + workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]); db.save_workspace(workspace_3.clone()).await; assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None); assert_eq!( @@ -2100,7 +2105,8 @@ mod tests { let workspace_1 = SerializedWorkspace { id: WorkspaceId(1), - location: SerializedWorkspaceLocation::from_local_paths(["/tmp1"]), + paths: PathList::new(&["/tmp1"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2113,7 +2119,8 @@ mod tests { let workspace_2 = SerializedWorkspace { id: WorkspaceId(2), - location: SerializedWorkspaceLocation::from_local_paths(["/tmp2"]), + paths: PathList::new(&["/tmp2"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2126,7 +2133,8 @@ mod tests { let workspace_3 = SerializedWorkspace { id: WorkspaceId(3), - location: SerializedWorkspaceLocation::from_local_paths(["/tmp3"]), + paths: PathList::new(&["/tmp3"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2139,7 +2147,8 @@ mod tests { let workspace_4 = SerializedWorkspace { id: WorkspaceId(4), - location: SerializedWorkspaceLocation::from_local_paths(["/tmp4"]), + paths: PathList::new(&["/tmp4"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2150,14 +2159,15 @@ mod tests { window_id: None, }; - let ssh_project = db - .get_or_create_ssh_project("my-host".to_string(), Some(1234), vec![], None) + let connection_id = db + .get_or_create_ssh_connection("my-host".to_string(), Some(1234), None) .await .unwrap(); let workspace_5 = SerializedWorkspace { id: WorkspaceId(5), - location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()), + paths: PathList::default(), + location: SerializedWorkspaceLocation::Ssh(db.ssh_connection(connection_id).unwrap()), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2170,10 +2180,8 @@ mod tests { let workspace_6 = SerializedWorkspace { id: WorkspaceId(6), - location: SerializedWorkspaceLocation::Local( - LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]), - LocalPathsOrder::new([2, 1, 0]), - ), + paths: PathList::new(&["/tmp6a", "/tmp6b", "/tmp6c"]), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), breakpoints: Default::default(), @@ -2195,41 +2203,36 @@ mod tests { let locations = db.session_workspaces("session-id-1".to_owned()).unwrap(); assert_eq!(locations.len(), 2); - assert_eq!(locations[0].0, LocalPaths::new(["/tmp2"])); - assert_eq!(locations[0].1, LocalPathsOrder::new([0])); - assert_eq!(locations[0].2, Some(20)); - assert_eq!(locations[1].0, LocalPaths::new(["/tmp1"])); - assert_eq!(locations[1].1, LocalPathsOrder::new([0])); - assert_eq!(locations[1].2, Some(10)); + assert_eq!(locations[0].0, PathList::new(&["/tmp2"])); + assert_eq!(locations[0].1, Some(20)); + assert_eq!(locations[1].0, PathList::new(&["/tmp1"])); + assert_eq!(locations[1].1, Some(10)); let locations = db.session_workspaces("session-id-2".to_owned()).unwrap(); assert_eq!(locations.len(), 2); - let empty_paths: Vec<&str> = Vec::new(); - assert_eq!(locations[0].0, LocalPaths::new(empty_paths.iter())); - assert_eq!(locations[0].1, LocalPathsOrder::new([])); - assert_eq!(locations[0].2, Some(50)); - assert_eq!(locations[0].3, Some(ssh_project.id.0)); - assert_eq!(locations[1].0, LocalPaths::new(["/tmp3"])); - assert_eq!(locations[1].1, LocalPathsOrder::new([0])); - assert_eq!(locations[1].2, Some(30)); + assert_eq!(locations[0].0, PathList::default()); + assert_eq!(locations[0].1, Some(50)); + assert_eq!(locations[0].2, Some(connection_id)); + assert_eq!(locations[1].0, PathList::new(&["/tmp3"])); + assert_eq!(locations[1].1, Some(30)); let locations = db.session_workspaces("session-id-3".to_owned()).unwrap(); assert_eq!(locations.len(), 1); assert_eq!( locations[0].0, - LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]), + PathList::new(&["/tmp6a", "/tmp6b", "/tmp6c"]), ); - assert_eq!(locations[0].1, LocalPathsOrder::new([2, 1, 0])); - assert_eq!(locations[0].2, Some(60)); + assert_eq!(locations[0].1, Some(60)); } fn default_workspace>( - workspace_id: &[P], + paths: &[P], center_group: &SerializedPaneGroup, ) -> SerializedWorkspace { SerializedWorkspace { id: WorkspaceId(4), - location: SerializedWorkspaceLocation::from_local_paths(workspace_id), + paths: PathList::new(paths), + location: SerializedWorkspaceLocation::Local, center_group: center_group.clone(), window_bounds: Default::default(), display: Default::default(), @@ -2252,30 +2255,18 @@ mod tests { WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await; let workspaces = [ - (1, vec![dir1.path()], vec![0], 9), - (2, vec![dir2.path()], vec![0], 5), - (3, vec![dir3.path()], vec![0], 8), - (4, vec![dir4.path()], vec![0], 2), - ( - 5, - vec![dir1.path(), dir2.path(), dir3.path()], - vec![0, 1, 2], - 3, - ), - ( - 6, - vec![dir2.path(), dir3.path(), dir4.path()], - vec![2, 1, 0], - 4, - ), + (1, vec![dir1.path()], 9), + (2, vec![dir2.path()], 5), + (3, vec![dir3.path()], 8), + (4, vec![dir4.path()], 2), + (5, vec![dir1.path(), dir2.path(), dir3.path()], 3), + (6, vec![dir4.path(), dir3.path(), dir2.path()], 4), ] .into_iter() - .map(|(id, locations, order, window_id)| SerializedWorkspace { + .map(|(id, paths, window_id)| SerializedWorkspace { id: WorkspaceId(id), - location: SerializedWorkspaceLocation::Local( - LocalPaths::new(locations), - LocalPathsOrder::new(order), - ), + paths: PathList::new(paths.as_slice()), + location: SerializedWorkspaceLocation::Local, center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2300,39 +2291,37 @@ mod tests { WindowId::from(4), // Bottom ])); - let have = db + let locations = db .last_session_workspace_locations("one-session", stack) .unwrap(); - assert_eq!(have.len(), 6); assert_eq!( - have[0], - SerializedWorkspaceLocation::from_local_paths(&[dir4.path()]) - ); - assert_eq!( - have[1], - SerializedWorkspaceLocation::from_local_paths([dir3.path()]) - ); - assert_eq!( - have[2], - SerializedWorkspaceLocation::from_local_paths([dir2.path()]) - ); - assert_eq!( - have[3], - SerializedWorkspaceLocation::from_local_paths([dir1.path()]) - ); - assert_eq!( - have[4], - SerializedWorkspaceLocation::Local( - LocalPaths::new([dir1.path(), dir2.path(), dir3.path()]), - LocalPathsOrder::new([0, 1, 2]), - ), - ); - assert_eq!( - have[5], - SerializedWorkspaceLocation::Local( - LocalPaths::new([dir2.path(), dir3.path(), dir4.path()]), - LocalPathsOrder::new([2, 1, 0]), - ), + locations, + [ + ( + SerializedWorkspaceLocation::Local, + PathList::new(&[dir4.path()]) + ), + ( + SerializedWorkspaceLocation::Local, + PathList::new(&[dir3.path()]) + ), + ( + SerializedWorkspaceLocation::Local, + PathList::new(&[dir2.path()]) + ), + ( + SerializedWorkspaceLocation::Local, + PathList::new(&[dir1.path()]) + ), + ( + SerializedWorkspaceLocation::Local, + PathList::new(&[dir1.path(), dir2.path(), dir3.path()]) + ), + ( + SerializedWorkspaceLocation::Local, + PathList::new(&[dir4.path(), dir3.path(), dir2.path()]) + ), + ] ); } @@ -2343,7 +2332,7 @@ mod tests { ) .await; - let ssh_projects = [ + let ssh_connections = [ ("host-1", "my-user-1"), ("host-2", "my-user-2"), ("host-3", "my-user-3"), @@ -2351,24 +2340,32 @@ mod tests { ] .into_iter() .map(|(host, user)| async { - db.get_or_create_ssh_project(host.to_string(), None, vec![], Some(user.to_string())) + let id = db + .get_or_create_ssh_connection(host.to_string(), None, Some(user.to_string())) .await - .unwrap() + .unwrap(); + SerializedSshConnection { + id, + host: host.into(), + port: None, + user: Some(user.into()), + } }) .collect::>(); - let ssh_projects = futures::future::join_all(ssh_projects).await; + let ssh_connections = futures::future::join_all(ssh_connections).await; let workspaces = [ - (1, ssh_projects[0].clone(), 9), - (2, ssh_projects[1].clone(), 5), - (3, ssh_projects[2].clone(), 8), - (4, ssh_projects[3].clone(), 2), + (1, ssh_connections[0].clone(), 9), + (2, ssh_connections[1].clone(), 5), + (3, ssh_connections[2].clone(), 8), + (4, ssh_connections[3].clone(), 2), ] .into_iter() - .map(|(id, ssh_project, window_id)| SerializedWorkspace { + .map(|(id, ssh_connection, window_id)| SerializedWorkspace { id: WorkspaceId(id), - location: SerializedWorkspaceLocation::Ssh(ssh_project), + paths: PathList::default(), + location: SerializedWorkspaceLocation::Ssh(ssh_connection), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2397,19 +2394,31 @@ mod tests { assert_eq!(have.len(), 4); assert_eq!( have[0], - SerializedWorkspaceLocation::Ssh(ssh_projects[3].clone()) + ( + SerializedWorkspaceLocation::Ssh(ssh_connections[3].clone()), + PathList::default() + ) ); assert_eq!( have[1], - SerializedWorkspaceLocation::Ssh(ssh_projects[2].clone()) + ( + SerializedWorkspaceLocation::Ssh(ssh_connections[2].clone()), + PathList::default() + ) ); assert_eq!( have[2], - SerializedWorkspaceLocation::Ssh(ssh_projects[1].clone()) + ( + SerializedWorkspaceLocation::Ssh(ssh_connections[1].clone()), + PathList::default() + ) ); assert_eq!( have[3], - SerializedWorkspaceLocation::Ssh(ssh_projects[0].clone()) + ( + SerializedWorkspaceLocation::Ssh(ssh_connections[0].clone()), + PathList::default() + ) ); } @@ -2417,116 +2426,102 @@ mod tests { async fn test_get_or_create_ssh_project() { let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await; - let (host, port, paths, user) = ( - "example.com".to_string(), - Some(22_u16), - vec!["/home/user".to_string(), "/etc/nginx".to_string()], - Some("user".to_string()), - ); + let host = "example.com".to_string(); + let port = Some(22_u16); + let user = Some("user".to_string()); - let project = db - .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone()) + let connection_id = db + .get_or_create_ssh_connection(host.clone(), port, user.clone()) .await .unwrap(); - assert_eq!(project.host, host); - assert_eq!(project.paths, paths); - assert_eq!(project.user, user); - // Test that calling the function again with the same parameters returns the same project - let same_project = db - .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone()) + let same_connection = db + .get_or_create_ssh_connection(host.clone(), port, user.clone()) .await .unwrap(); - assert_eq!(project.id, same_project.id); + assert_eq!(connection_id, same_connection); // Test with different parameters - let (host2, paths2, user2) = ( - "otherexample.com".to_string(), - vec!["/home/otheruser".to_string()], - Some("otheruser".to_string()), - ); + let host2 = "otherexample.com".to_string(); + let port2 = None; + let user2 = Some("otheruser".to_string()); - let different_project = db - .get_or_create_ssh_project(host2.clone(), None, paths2.clone(), user2.clone()) + let different_connection = db + .get_or_create_ssh_connection(host2.clone(), port2, user2.clone()) .await .unwrap(); - assert_ne!(project.id, different_project.id); - assert_eq!(different_project.host, host2); - assert_eq!(different_project.paths, paths2); - assert_eq!(different_project.user, user2); + assert_ne!(connection_id, different_connection); } #[gpui::test] async fn test_get_or_create_ssh_project_with_null_user() { let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await; - let (host, port, paths, user) = ( - "example.com".to_string(), - None, - vec!["/home/user".to_string()], - None, - ); + let (host, port, user) = ("example.com".to_string(), None, None); - let project = db - .get_or_create_ssh_project(host.clone(), port, paths.clone(), None) + let connection_id = db + .get_or_create_ssh_connection(host.clone(), port, None) .await .unwrap(); - assert_eq!(project.host, host); - assert_eq!(project.paths, paths); - assert_eq!(project.user, None); - - // Test that calling the function again with the same parameters returns the same project - let same_project = db - .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone()) + let same_connection_id = db + .get_or_create_ssh_connection(host.clone(), port, user.clone()) .await .unwrap(); - assert_eq!(project.id, same_project.id); + assert_eq!(connection_id, same_connection_id); } #[gpui::test] - async fn test_get_ssh_projects() { - let db = WorkspaceDb::open_test_db("test_get_ssh_projects").await; + async fn test_get_ssh_connections() { + let db = WorkspaceDb::open_test_db("test_get_ssh_connections").await; - let projects = vec![ - ( - "example.com".to_string(), - None, - vec!["/home/user".to_string()], - None, - ), + let connections = [ + ("example.com".to_string(), None, None), ( "anotherexample.com".to_string(), Some(123_u16), - vec!["/home/user2".to_string()], Some("user2".to_string()), ), - ( - "yetanother.com".to_string(), - Some(345_u16), - vec!["/home/user3".to_string(), "/proc/1234/exe".to_string()], - None, - ), + ("yetanother.com".to_string(), Some(345_u16), None), ]; - for (host, port, paths, user) in projects.iter() { - let project = db - .get_or_create_ssh_project(host.clone(), *port, paths.clone(), user.clone()) - .await - .unwrap(); - - assert_eq!(&project.host, host); - assert_eq!(&project.port, port); - assert_eq!(&project.paths, paths); - assert_eq!(&project.user, user); + let mut ids = Vec::new(); + for (host, port, user) in connections.iter() { + ids.push( + db.get_or_create_ssh_connection(host.clone(), *port, user.clone()) + .await + .unwrap(), + ); } - let stored_projects = db.ssh_projects().unwrap(); - assert_eq!(stored_projects.len(), projects.len()); + let stored_projects = db.ssh_connections().unwrap(); + assert_eq!( + stored_projects, + &[ + SerializedSshConnection { + id: ids[0], + host: "example.com".into(), + port: None, + user: None, + }, + SerializedSshConnection { + id: ids[1], + host: "anotherexample.com".into(), + port: Some(123), + user: Some("user2".into()), + }, + SerializedSshConnection { + id: ids[2], + host: "yetanother.com".into(), + port: Some(345), + user: None, + }, + ] + ); } #[gpui::test] @@ -2659,56 +2654,4 @@ mod tests { assert_eq!(workspace.center_group, new_workspace.center_group); } - - #[gpui::test] - async fn test_update_ssh_project_paths() { - zlog::init_test(); - - let db = WorkspaceDb::open_test_db("test_update_ssh_project_paths").await; - - let (host, port, initial_paths, user) = ( - "example.com".to_string(), - Some(22_u16), - vec!["/home/user".to_string(), "/etc/nginx".to_string()], - Some("user".to_string()), - ); - - let project = db - .get_or_create_ssh_project(host.clone(), port, initial_paths.clone(), user.clone()) - .await - .unwrap(); - - assert_eq!(project.host, host); - assert_eq!(project.paths, initial_paths); - assert_eq!(project.user, user); - - let new_paths = vec![ - "/home/user".to_string(), - "/etc/nginx".to_string(), - "/var/log".to_string(), - "/opt/app".to_string(), - ]; - - let updated_project = db - .update_ssh_project_paths(project.id, new_paths.clone()) - .await - .unwrap(); - - assert_eq!(updated_project.id, project.id); - assert_eq!(updated_project.paths, new_paths); - - let retrieved_project = db - .get_ssh_project( - host.clone(), - port, - serde_json::to_string(&new_paths).unwrap(), - user.clone(), - ) - .await - .unwrap() - .unwrap(); - - assert_eq!(retrieved_project.id, project.id); - assert_eq!(retrieved_project.paths, new_paths); - } } diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 15a54ac62f..afe4ae6235 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -1,15 +1,16 @@ use super::{SerializedAxis, SerializedWindowBounds}; use crate::{ Member, Pane, PaneAxis, SerializableItemRegistry, Workspace, WorkspaceId, item::ItemHandle, + path_list::PathList, }; -use anyhow::{Context as _, Result}; +use anyhow::Result; use async_recursion::async_recursion; use db::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; use gpui::{AsyncWindowContext, Entity, WeakEntity}; -use itertools::Itertools as _; + use project::{Project, debugger::breakpoint_store::SourceBreakpoint}; use remote::ssh_session::SshProjectId; use serde::{Deserialize, Serialize}; @@ -18,239 +19,27 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use util::{ResultExt, paths::SanitizedPath}; +use util::ResultExt; use uuid::Uuid; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] -pub struct SerializedSshProject { +pub struct SerializedSshConnection { pub id: SshProjectId, pub host: String, pub port: Option, - pub paths: Vec, pub user: Option, } -impl SerializedSshProject { - pub fn ssh_urls(&self) -> Vec { - self.paths - .iter() - .map(|path| { - let mut result = String::new(); - if let Some(user) = &self.user { - result.push_str(user); - result.push('@'); - } - result.push_str(&self.host); - if let Some(port) = &self.port { - result.push(':'); - result.push_str(&port.to_string()); - } - result.push_str(path); - PathBuf::from(result) - }) - .collect() - } -} - -impl StaticColumnCount for SerializedSshProject { - fn column_count() -> usize { - 5 - } -} - -impl Bind for &SerializedSshProject { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let next_index = statement.bind(&self.id.0, start_index)?; - let next_index = statement.bind(&self.host, next_index)?; - let next_index = statement.bind(&self.port, next_index)?; - let raw_paths = serde_json::to_string(&self.paths)?; - let next_index = statement.bind(&raw_paths, next_index)?; - statement.bind(&self.user, next_index) - } -} - -impl Column for SerializedSshProject { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let id = statement.column_int64(start_index)?; - let host = statement.column_text(start_index + 1)?.to_string(); - let (port, _) = Option::::column(statement, start_index + 2)?; - let raw_paths = statement.column_text(start_index + 3)?.to_string(); - let paths: Vec = serde_json::from_str(&raw_paths)?; - - let (user, _) = Option::::column(statement, start_index + 4)?; - - Ok(( - Self { - id: SshProjectId(id as u64), - host, - port, - paths, - user, - }, - start_index + 5, - )) - } -} - -#[derive(Debug, PartialEq, Clone)] -pub struct LocalPaths(Arc>); - -impl LocalPaths { - pub fn new>(paths: impl IntoIterator) -> Self { - let mut paths: Vec = paths - .into_iter() - .map(|p| SanitizedPath::from(p).into()) - .collect(); - // Ensure all future `zed workspace1 workspace2` and `zed workspace2 workspace1` calls are using the same workspace. - // The actual workspace order is stored in the `LocalPathsOrder` struct. - paths.sort(); - Self(Arc::new(paths)) - } - - pub fn paths(&self) -> &Arc> { - &self.0 - } -} - -impl StaticColumnCount for LocalPaths {} -impl Bind for &LocalPaths { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement.bind(&bincode::serialize(&self.0)?, start_index) - } -} - -impl Column for LocalPaths { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let path_blob = statement.column_blob(start_index)?; - let paths: Arc> = if path_blob.is_empty() { - Default::default() - } else { - bincode::deserialize(path_blob).context("Bincode deserialization of paths failed")? - }; - - Ok((Self(paths), start_index + 1)) - } -} - -#[derive(Debug, PartialEq, Clone)] -pub struct LocalPathsOrder(Vec); - -impl LocalPathsOrder { - pub fn new(order: impl IntoIterator) -> Self { - Self(order.into_iter().collect()) - } - - pub fn order(&self) -> &[usize] { - self.0.as_slice() - } - - pub fn default_for_paths(paths: &LocalPaths) -> Self { - Self::new(0..paths.0.len()) - } -} - -impl StaticColumnCount for LocalPathsOrder {} -impl Bind for &LocalPathsOrder { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement.bind(&bincode::serialize(&self.0)?, start_index) - } -} - -impl Column for LocalPathsOrder { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let order_blob = statement.column_blob(start_index)?; - let order = if order_blob.is_empty() { - Vec::new() - } else { - bincode::deserialize(order_blob).context("deserializing workspace root order")? - }; - - Ok((Self(order), start_index + 1)) - } -} - #[derive(Debug, PartialEq, Clone)] pub enum SerializedWorkspaceLocation { - Local(LocalPaths, LocalPathsOrder), - Ssh(SerializedSshProject), + Local, + Ssh(SerializedSshConnection), } impl SerializedWorkspaceLocation { - /// Create a new `SerializedWorkspaceLocation` from a list of local paths. - /// - /// The paths will be sorted and the order will be stored in the `LocalPathsOrder` struct. - /// - /// # Examples - /// - /// ``` - /// use std::path::Path; - /// use zed_workspace::SerializedWorkspaceLocation; - /// - /// let location = SerializedWorkspaceLocation::from_local_paths(vec![ - /// Path::new("path/to/workspace1"), - /// Path::new("path/to/workspace2"), - /// ]); - /// assert_eq!(location, SerializedWorkspaceLocation::Local( - /// LocalPaths::new(vec![ - /// Path::new("path/to/workspace1"), - /// Path::new("path/to/workspace2"), - /// ]), - /// LocalPathsOrder::new(vec![0, 1]), - /// )); - /// ``` - /// - /// ``` - /// use std::path::Path; - /// use zed_workspace::SerializedWorkspaceLocation; - /// - /// let location = SerializedWorkspaceLocation::from_local_paths(vec![ - /// Path::new("path/to/workspace2"), - /// Path::new("path/to/workspace1"), - /// ]); - /// - /// assert_eq!(location, SerializedWorkspaceLocation::Local( - /// LocalPaths::new(vec![ - /// Path::new("path/to/workspace1"), - /// Path::new("path/to/workspace2"), - /// ]), - /// LocalPathsOrder::new(vec![1, 0]), - /// )); - /// ``` - pub fn from_local_paths>(paths: impl IntoIterator) -> Self { - let mut indexed_paths: Vec<_> = paths - .into_iter() - .map(|p| p.as_ref().to_path_buf()) - .enumerate() - .collect(); - - indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b)); - - let sorted_paths: Vec<_> = indexed_paths.iter().map(|(_, path)| path.clone()).collect(); - let order: Vec<_> = indexed_paths.iter().map(|(index, _)| *index).collect(); - - Self::Local(LocalPaths::new(sorted_paths), LocalPathsOrder::new(order)) - } - /// Get sorted paths pub fn sorted_paths(&self) -> Arc> { - match self { - SerializedWorkspaceLocation::Local(paths, order) => { - if order.order().is_empty() { - paths.paths().clone() - } else { - Arc::new( - order - .order() - .iter() - .zip(paths.paths().iter()) - .sorted_by_key(|(i, _)| **i) - .map(|(_, p)| p.clone()) - .collect(), - ) - } - } - SerializedWorkspaceLocation::Ssh(ssh_project) => Arc::new(ssh_project.ssh_urls()), - } + unimplemented!() } } @@ -258,6 +47,7 @@ impl SerializedWorkspaceLocation { pub(crate) struct SerializedWorkspace { pub(crate) id: WorkspaceId, pub(crate) location: SerializedWorkspaceLocation, + pub(crate) paths: PathList, pub(crate) center_group: SerializedPaneGroup, pub(crate) window_bounds: Option, pub(crate) centered_layout: bool, @@ -581,80 +371,3 @@ impl Column for SerializedItem { )) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_serialize_local_paths() { - let paths = vec!["b", "a", "c"]; - let serialized = SerializedWorkspaceLocation::from_local_paths(paths); - - assert_eq!( - serialized, - SerializedWorkspaceLocation::Local( - LocalPaths::new(vec!["a", "b", "c"]), - LocalPathsOrder::new(vec![1, 0, 2]) - ) - ); - } - - #[test] - fn test_sorted_paths() { - let paths = vec!["b", "a", "c"]; - let serialized = SerializedWorkspaceLocation::from_local_paths(paths); - assert_eq!( - serialized.sorted_paths(), - Arc::new(vec![ - PathBuf::from("b"), - PathBuf::from("a"), - PathBuf::from("c"), - ]) - ); - - let paths = Arc::new(vec![ - PathBuf::from("a"), - PathBuf::from("b"), - PathBuf::from("c"), - ]); - let order = vec![2, 0, 1]; - let serialized = - SerializedWorkspaceLocation::Local(LocalPaths(paths), LocalPathsOrder(order)); - assert_eq!( - serialized.sorted_paths(), - Arc::new(vec![ - PathBuf::from("b"), - PathBuf::from("c"), - PathBuf::from("a"), - ]) - ); - - let paths = Arc::new(vec![ - PathBuf::from("a"), - PathBuf::from("b"), - PathBuf::from("c"), - ]); - let order = vec![]; - let serialized = - SerializedWorkspaceLocation::Local(LocalPaths(paths.clone()), LocalPathsOrder(order)); - assert_eq!(serialized.sorted_paths(), paths); - - let urls = ["/a", "/b", "/c"]; - let serialized = SerializedWorkspaceLocation::Ssh(SerializedSshProject { - id: SshProjectId(0), - host: "host".to_string(), - port: Some(22), - paths: urls.iter().map(|s| s.to_string()).collect(), - user: Some("user".to_string()), - }); - assert_eq!( - serialized.sorted_paths(), - Arc::new( - urls.iter() - .map(|p| PathBuf::from(format!("user@host:22{}", p))) - .collect() - ) - ); - } -} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d31aae2c59..d07ea30cf9 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -6,6 +6,7 @@ mod modal_layer; pub mod notifications; pub mod pane; pub mod pane_group; +mod path_list; mod persistence; pub mod searchable; pub mod shared_screen; @@ -18,6 +19,7 @@ mod workspace_settings; pub use crate::notifications::NotificationFrame; pub use dock::Panel; +pub use path_list::PathList; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; use anyhow::{Context as _, Result, anyhow}; @@ -62,20 +64,20 @@ use notifications::{ }; pub use pane::*; pub use pane_group::*; -use persistence::{ - DB, SerializedWindowBounds, - model::{SerializedSshProject, SerializedWorkspace}, -}; +use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace}; pub use persistence::{ DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items, - model::{ItemId, LocalPaths, SerializedWorkspaceLocation}, + model::{ItemId, SerializedSshConnection, SerializedWorkspaceLocation}, }; use postage::stream::Stream; use project::{ DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId, debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus}, }; -use remote::{SshClientDelegate, SshConnectionOptions, ssh_session::ConnectionIdentifier}; +use remote::{ + SshClientDelegate, SshConnectionOptions, + ssh_session::{ConnectionIdentifier, SshProjectId}, +}; use schemars::JsonSchema; use serde::Deserialize; use session::AppSession; @@ -1042,7 +1044,7 @@ pub enum OpenVisible { enum WorkspaceLocation { // Valid local paths or SSH project to serialize - Location(SerializedWorkspaceLocation), + Location(SerializedWorkspaceLocation, PathList), // No valid location found hence clear session id DetachFromSession, // No valid location found to serialize @@ -1126,7 +1128,7 @@ pub struct Workspace { terminal_provider: Option>, debugger_provider: Option>, serializable_items_tx: UnboundedSender>, - serialized_ssh_project: Option, + serialized_ssh_connection_id: Option, _items_serializer: Task>, session_id: Option, scheduled_tasks: Vec>, @@ -1175,8 +1177,6 @@ impl Workspace { project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => { this.update_window_title(window, cx); - this.update_ssh_paths(cx); - this.serialize_ssh_paths(window, cx); this.serialize_workspace(window, cx); // This event could be triggered by `AddFolderToProject` or `RemoveFromProject`. this.update_history(cx); @@ -1461,7 +1461,7 @@ impl Workspace { serializable_items_tx, _items_serializer, session_id: Some(session_id), - serialized_ssh_project: None, + serialized_ssh_connection_id: None, scheduled_tasks: Vec::new(), } } @@ -1501,20 +1501,9 @@ impl Workspace { let serialized_workspace = persistence::DB.workspace_for_roots(paths_to_open.as_slice()); - let workspace_location = serialized_workspace - .as_ref() - .map(|ws| &ws.location) - .and_then(|loc| match loc { - SerializedWorkspaceLocation::Local(_, order) => { - Some((loc.sorted_paths(), order.order())) - } - _ => None, - }); - - if let Some((paths, order)) = workspace_location { - paths_to_open = paths.iter().cloned().collect(); - - if order.iter().enumerate().any(|(i, &j)| i != j) { + if let Some(paths) = serialized_workspace.as_ref().map(|ws| &ws.paths) { + paths_to_open = paths.paths().to_vec(); + if !paths.is_lexicographically_ordered() { project_handle .update(cx, |project, cx| { project.set_worktrees_reordered(true, cx); @@ -2034,14 +2023,6 @@ impl Workspace { self.debugger_provider.clone() } - pub fn serialized_ssh_project(&self) -> Option { - self.serialized_ssh_project.clone() - } - - pub fn set_serialized_ssh_project(&mut self, serialized_ssh_project: SerializedSshProject) { - self.serialized_ssh_project = Some(serialized_ssh_project); - } - pub fn prompt_for_open_path( &mut self, path_prompt_options: PathPromptOptions, @@ -5088,59 +5069,12 @@ impl Workspace { self.session_id.clone() } - fn local_paths(&self, cx: &App) -> Option>> { + pub fn root_paths(&self, cx: &App) -> Vec> { let project = self.project().read(cx); - - if project.is_local() { - Some( - project - .visible_worktrees(cx) - .map(|worktree| worktree.read(cx).abs_path()) - .collect::>(), - ) - } else { - None - } - } - - fn update_ssh_paths(&mut self, cx: &App) { - let project = self.project().read(cx); - if !project.is_local() { - let paths: Vec = project - .visible_worktrees(cx) - .map(|worktree| worktree.read(cx).abs_path().to_string_lossy().to_string()) - .collect(); - if let Some(ssh_project) = &mut self.serialized_ssh_project { - ssh_project.paths = paths; - } - } - } - - fn serialize_ssh_paths(&mut self, window: &mut Window, cx: &mut Context) { - if self._schedule_serialize_ssh_paths.is_none() { - self._schedule_serialize_ssh_paths = - Some(cx.spawn_in(window, async move |this, cx| { - cx.background_executor() - .timer(SERIALIZATION_THROTTLE_TIME) - .await; - this.update_in(cx, |this, window, cx| { - let task = if let Some(ssh_project) = &this.serialized_ssh_project { - let ssh_project_id = ssh_project.id; - let ssh_project_paths = ssh_project.paths.clone(); - window.spawn(cx, async move |_| { - persistence::DB - .update_ssh_project_paths(ssh_project_id, ssh_project_paths) - .await - }) - } else { - Task::ready(Err(anyhow::anyhow!("No SSH project to serialize"))) - }; - task.detach(); - this._schedule_serialize_ssh_paths.take(); - }) - .log_err(); - })); - } + project + .visible_worktrees(cx) + .map(|worktree| worktree.read(cx).abs_path()) + .collect::>() } fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context) { @@ -5313,7 +5247,7 @@ impl Workspace { } match self.serialize_workspace_location(cx) { - WorkspaceLocation::Location(location) => { + WorkspaceLocation::Location(location, paths) => { let breakpoints = self.project.update(cx, |project, cx| { project .breakpoint_store() @@ -5327,6 +5261,7 @@ impl Workspace { let serialized_workspace = SerializedWorkspace { id: database_id, location, + paths, center_group, window_bounds, display: Default::default(), @@ -5352,13 +5287,21 @@ impl Workspace { } fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation { - if let Some(ssh_project) = &self.serialized_ssh_project { - WorkspaceLocation::Location(SerializedWorkspaceLocation::Ssh(ssh_project.clone())) - } else if let Some(local_paths) = self.local_paths(cx) { - if !local_paths.is_empty() { - WorkspaceLocation::Location(SerializedWorkspaceLocation::from_local_paths( - local_paths, - )) + let paths = PathList::new(&self.root_paths(cx)); + let connection = self.project.read(cx).ssh_connection_options(cx); + if let Some((id, connection)) = self.serialized_ssh_connection_id.zip(connection) { + WorkspaceLocation::Location( + SerializedWorkspaceLocation::Ssh(SerializedSshConnection { + id, + host: connection.host, + port: connection.port, + user: connection.username, + }), + paths, + ) + } else if self.project.read(cx).is_local() { + if !paths.is_empty() { + WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths) } else { WorkspaceLocation::DetachFromSession } @@ -5371,13 +5314,13 @@ impl Workspace { let Some(id) = self.database_id() else { return; }; - let location = match self.serialize_workspace_location(cx) { - WorkspaceLocation::Location(location) => location, - _ => return, - }; + if !self.project.read(cx).is_local() { + return; + } if let Some(manager) = HistoryManager::global(cx) { + let paths = PathList::new(&self.root_paths(cx)); manager.update(cx, |this, cx| { - this.update_history(id, HistoryManagerEntry::new(id, &location), cx); + this.update_history(id, HistoryManagerEntry::new(id, &paths), cx); }); } } @@ -6843,14 +6786,14 @@ impl WorkspaceHandle for Entity { } } -pub async fn last_opened_workspace_location() -> Option { +pub async fn last_opened_workspace_location() -> Option<(SerializedWorkspaceLocation, PathList)> { DB.last_workspace().await.log_err().flatten() } pub fn last_session_workspace_locations( last_session_id: &str, last_session_window_stack: Option>, -) -> Option> { +) -> Option> { DB.last_session_workspace_locations(last_session_id, last_session_window_stack) .log_err() } @@ -7353,7 +7296,7 @@ pub fn open_ssh_project_with_new_connection( cx: &mut App, ) -> Task> { cx.spawn(async move |cx| { - let (serialized_ssh_project, workspace_id, serialized_workspace) = + let (workspace_id, serialized_workspace) = serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?; let session = match cx @@ -7387,7 +7330,6 @@ pub fn open_ssh_project_with_new_connection( open_ssh_project_inner( project, paths, - serialized_ssh_project, workspace_id, serialized_workspace, app_state, @@ -7407,13 +7349,12 @@ pub fn open_ssh_project_with_existing_connection( cx: &mut AsyncApp, ) -> Task> { cx.spawn(async move |cx| { - let (serialized_ssh_project, workspace_id, serialized_workspace) = + let (workspace_id, serialized_workspace) = serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?; open_ssh_project_inner( project, paths, - serialized_ssh_project, workspace_id, serialized_workspace, app_state, @@ -7427,7 +7368,6 @@ pub fn open_ssh_project_with_existing_connection( async fn open_ssh_project_inner( project: Entity, paths: Vec, - serialized_ssh_project: SerializedSshProject, workspace_id: WorkspaceId, serialized_workspace: Option, app_state: Arc, @@ -7480,7 +7420,6 @@ async fn open_ssh_project_inner( let mut workspace = Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx); - workspace.set_serialized_ssh_project(serialized_ssh_project); workspace.update_history(cx); if let Some(ref serialized) = serialized_workspace { @@ -7517,28 +7456,18 @@ fn serialize_ssh_project( connection_options: SshConnectionOptions, paths: Vec, cx: &AsyncApp, -) -> Task< - Result<( - SerializedSshProject, - WorkspaceId, - Option, - )>, -> { +) -> Task)>> { cx.background_spawn(async move { - let serialized_ssh_project = persistence::DB - .get_or_create_ssh_project( + let ssh_connection_id = persistence::DB + .get_or_create_ssh_connection( connection_options.host.clone(), connection_options.port, - paths - .iter() - .map(|path| path.to_string_lossy().to_string()) - .collect::>(), connection_options.username.clone(), ) .await?; let serialized_workspace = - persistence::DB.workspace_for_ssh_project(&serialized_ssh_project); + persistence::DB.ssh_workspace_for_roots(&paths, ssh_connection_id); let workspace_id = if let Some(workspace_id) = serialized_workspace.as_ref().map(|workspace| workspace.id) @@ -7548,7 +7477,7 @@ fn serialize_ssh_project( persistence::DB.next_id().await? }; - Ok((serialized_ssh_project, workspace_id, serialized_workspace)) + Ok((workspace_id, serialized_workspace)) }) } @@ -8095,18 +8024,15 @@ pub fn ssh_workspace_position_from_db( paths_to_open: &[PathBuf], cx: &App, ) -> Task> { - let paths = paths_to_open - .iter() - .map(|path| path.to_string_lossy().to_string()) - .collect::>(); + let paths = paths_to_open.to_vec(); cx.background_spawn(async move { - let serialized_ssh_project = persistence::DB - .get_or_create_ssh_project(host, port, paths, user) + let ssh_connection_id = persistence::DB + .get_or_create_ssh_connection(host, port, user) .await .context("fetching serialized ssh project")?; let serialized_workspace = - persistence::DB.workspace_for_ssh_project(&serialized_ssh_project); + persistence::DB.ssh_workspace_for_roots(&paths, ssh_connection_id); let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() { (Some(WindowBounds::Windowed(bounds)), None) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b8150a600d..e99c8b564b 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -47,8 +47,8 @@ use theme::{ use util::{ResultExt, TryFutureExt, maybe}; use uuid::Uuid; use workspace::{ - AppState, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, WorkspaceStore, - notifications::NotificationId, + AppState, PathList, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, + WorkspaceStore, notifications::NotificationId, }; use zed::{ OpenListener, OpenRequest, RawOpenRequest, app_menus, build_window_options, @@ -949,15 +949,14 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp if let Some(locations) = restorable_workspace_locations(cx, &app_state).await { let mut tasks = Vec::new(); - for location in locations { + for (location, paths) in locations { match location { - SerializedWorkspaceLocation::Local(location, _) => { + SerializedWorkspaceLocation::Local => { let app_state = app_state.clone(); - let paths = location.paths().to_vec(); let task = cx.spawn(async move |cx| { let open_task = cx.update(|cx| { workspace::open_paths( - &paths, + &paths.paths(), app_state, workspace::OpenOptions::default(), cx, @@ -979,7 +978,7 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp match connection_options { Ok(connection_options) => recent_projects::open_ssh_project( connection_options, - ssh.paths.into_iter().map(PathBuf::from).collect(), + paths.paths().into_iter().map(PathBuf::from).collect(), app_state, workspace::OpenOptions::default(), cx, @@ -1070,7 +1069,7 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp pub(crate) async fn restorable_workspace_locations( cx: &mut AsyncApp, app_state: &Arc, -) -> Option> { +) -> Option> { let mut restore_behavior = cx .update(|cx| WorkspaceSettings::get(None, cx).restore_on_startup) .ok()?; diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 827c7754fa..2194fb7af5 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -26,6 +26,7 @@ use std::thread; use std::time::Duration; use util::ResultExt; use util::paths::PathWithPosition; +use workspace::PathList; use workspace::item::ItemHandle; use workspace::{AppState, OpenOptions, SerializedWorkspaceLocation, Workspace}; @@ -361,12 +362,14 @@ async fn open_workspaces( if open_new_workspace == Some(true) { Vec::new() } else { - let locations = restorable_workspace_locations(cx, &app_state).await; - locations.unwrap_or_default() + restorable_workspace_locations(cx, &app_state) + .await + .unwrap_or_default() } } else { - vec![SerializedWorkspaceLocation::from_local_paths( - paths.into_iter().map(PathBuf::from), + vec![( + SerializedWorkspaceLocation::Local, + PathList::new(&paths.into_iter().map(PathBuf::from).collect::>()), )] }; @@ -394,9 +397,9 @@ async fn open_workspaces( // If there are paths to open, open a workspace for each grouping of paths let mut errored = false; - for location in grouped_locations { + for (location, workspace_paths) in grouped_locations { match location { - SerializedWorkspaceLocation::Local(workspace_paths, _) => { + SerializedWorkspaceLocation::Local => { let workspace_paths = workspace_paths .paths() .iter() @@ -429,7 +432,7 @@ async fn open_workspaces( cx.spawn(async move |cx| { open_ssh_project( connection_options, - ssh.paths.into_iter().map(PathBuf::from).collect(), + workspace_paths.paths().to_vec(), app_state, OpenOptions::default(), cx, From e6267c42f70233542f09429337d688cc96ceee90 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Fri, 22 Aug 2025 23:28:55 +0200 Subject: [PATCH 628/693] Ensure `pane: swap item right` does not panic (#36765) This fixes a panic I randomly ran into whilst mistyping in the command palette: I accidentally ran `pane: swap item right`in a state where no items were opened in my active pane. We were checking for `index + 1 == self.items.len()` there when it really should be `>=`, as otherwise in the case of no items this panics. This PR fixes the bug, adds a test for both the panic as well as the actions themselves (they were untested previously). Lastly (and mostly), this also cleans up a bit around existing actions to update them with how we generally handle actions now. Release Notes: - Fixed a panic that could occur with the `pane: swap item right` action. --- crates/collab/src/tests/following_tests.rs | 8 +- crates/editor/src/editor_tests.rs | 4 +- crates/search/src/project_search.rs | 6 +- crates/workspace/src/pane.rs | 149 ++++++++++++++------- 4 files changed, 110 insertions(+), 57 deletions(-) diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index d9fd8ffeb2..1e0c915bcb 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -970,7 +970,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T // the follow. workspace_b.update_in(cx_b, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_prev_item(true, window, cx); + pane.activate_previous_item(&Default::default(), window, cx); }); }); executor.run_until_parked(); @@ -1073,7 +1073,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T // Client A cycles through some tabs. workspace_a.update_in(cx_a, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_prev_item(true, window, cx); + pane.activate_previous_item(&Default::default(), window, cx); }); }); executor.run_until_parked(); @@ -1117,7 +1117,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T workspace_a.update_in(cx_a, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_prev_item(true, window, cx); + pane.activate_previous_item(&Default::default(), window, cx); }); }); executor.run_until_parked(); @@ -1164,7 +1164,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T workspace_a.update_in(cx_a, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_prev_item(true, window, cx); + pane.activate_previous_item(&Default::default(), window, cx); }); }); executor.run_until_parked(); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 03f5da9a20..2cfdb92593 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -22715,7 +22715,7 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) { .await .unwrap(); pane.update_in(cx, |pane, window, cx| { - pane.navigate_backward(window, cx); + pane.navigate_backward(&Default::default(), window, cx); }); cx.run_until_parked(); pane.update(cx, |pane, cx| { @@ -24302,7 +24302,7 @@ async fn test_document_colors(cx: &mut TestAppContext) { workspace .update(cx, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.navigate_backward(window, cx); + pane.navigate_backward(&Default::default(), window, cx); }) }) .unwrap(); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index c4ba9b5154..8ac12588af 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -3905,7 +3905,7 @@ pub mod tests { assert_eq!(workspace.active_pane(), &second_pane); second_pane.update(cx, |this, cx| { assert_eq!(this.active_item_index(), 1); - this.activate_prev_item(false, window, cx); + this.activate_previous_item(&Default::default(), window, cx); assert_eq!(this.active_item_index(), 0); }); workspace.activate_pane_in_direction(workspace::SplitDirection::Left, window, cx); @@ -3940,7 +3940,9 @@ pub mod tests { // Focus the second pane's non-search item window .update(cx, |_workspace, window, cx| { - second_pane.update(cx, |pane, cx| pane.activate_next_item(true, window, cx)); + second_pane.update(cx, |pane, cx| { + pane.activate_next_item(&Default::default(), window, cx) + }); }) .unwrap(); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index e88402adc0..fe8014d9f7 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -514,7 +514,7 @@ impl Pane { } } - fn alternate_file(&mut self, window: &mut Window, cx: &mut Context) { + fn alternate_file(&mut self, _: &AlternateFile, window: &mut Window, cx: &mut Context) { let (_, alternative) = &self.alternate_file_items; if let Some(alternative) = alternative { let existing = self @@ -788,7 +788,7 @@ impl Pane { !self.nav_history.0.lock().forward_stack.is_empty() } - pub fn navigate_backward(&mut self, window: &mut Window, cx: &mut Context) { + pub fn navigate_backward(&mut self, _: &GoBack, window: &mut Window, cx: &mut Context) { if let Some(workspace) = self.workspace.upgrade() { let pane = cx.entity().downgrade(); window.defer(cx, move |window, cx| { @@ -799,7 +799,7 @@ impl Pane { } } - fn navigate_forward(&mut self, window: &mut Window, cx: &mut Context) { + fn navigate_forward(&mut self, _: &GoForward, window: &mut Window, cx: &mut Context) { if let Some(workspace) = self.workspace.upgrade() { let pane = cx.entity().downgrade(); window.defer(cx, move |window, cx| { @@ -1283,9 +1283,9 @@ impl Pane { } } - pub fn activate_prev_item( + pub fn activate_previous_item( &mut self, - activate_pane: bool, + _: &ActivatePreviousItem, window: &mut Window, cx: &mut Context, ) { @@ -1295,12 +1295,12 @@ impl Pane { } else if !self.items.is_empty() { index = self.items.len() - 1; } - self.activate_item(index, activate_pane, activate_pane, window, cx); + self.activate_item(index, true, true, window, cx); } pub fn activate_next_item( &mut self, - activate_pane: bool, + _: &ActivateNextItem, window: &mut Window, cx: &mut Context, ) { @@ -1310,10 +1310,15 @@ impl Pane { } else { index = 0; } - self.activate_item(index, activate_pane, activate_pane, window, cx); + self.activate_item(index, true, true, window, cx); } - pub fn swap_item_left(&mut self, window: &mut Window, cx: &mut Context) { + pub fn swap_item_left( + &mut self, + _: &SwapItemLeft, + window: &mut Window, + cx: &mut Context, + ) { let index = self.active_item_index; if index == 0 { return; @@ -1323,9 +1328,14 @@ impl Pane { self.activate_item(index - 1, true, true, window, cx); } - pub fn swap_item_right(&mut self, window: &mut Window, cx: &mut Context) { + pub fn swap_item_right( + &mut self, + _: &SwapItemRight, + window: &mut Window, + cx: &mut Context, + ) { let index = self.active_item_index; - if index + 1 == self.items.len() { + if index + 1 >= self.items.len() { return; } @@ -1333,6 +1343,16 @@ impl Pane { self.activate_item(index + 1, true, true, window, cx); } + pub fn activate_last_item( + &mut self, + _: &ActivateLastItem, + window: &mut Window, + cx: &mut Context, + ) { + let index = self.items.len().saturating_sub(1); + self.activate_item(index, true, true, window, cx); + } + pub fn close_active_item( &mut self, action: &CloseActiveItem, @@ -2881,7 +2901,9 @@ impl Pane { .on_click({ let entity = cx.entity(); move |_, window, cx| { - entity.update(cx, |pane, cx| pane.navigate_backward(window, cx)) + entity.update(cx, |pane, cx| { + pane.navigate_backward(&Default::default(), window, cx) + }) } }) .disabled(!self.can_navigate_backward()) @@ -2896,7 +2918,11 @@ impl Pane { .icon_size(IconSize::Small) .on_click({ let entity = cx.entity(); - move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx)) + move |_, window, cx| { + entity.update(cx, |pane, cx| { + pane.navigate_forward(&Default::default(), window, cx) + }) + } }) .disabled(!self.can_navigate_forward()) .tooltip({ @@ -3528,9 +3554,6 @@ impl Render for Pane { .size_full() .flex_none() .overflow_hidden() - .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| { - pane.alternate_file(window, cx); - })) .on_action( cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)), ) @@ -3547,12 +3570,6 @@ impl Render for Pane { .on_action( cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)), ) - .on_action( - cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)), - ) - .on_action( - cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)), - ) .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| { cx.emit(Event::JoinIntoNext); })) @@ -3560,6 +3577,8 @@ impl Render for Pane { cx.emit(Event::JoinAll); })) .on_action(cx.listener(Pane::toggle_zoom)) + .on_action(cx.listener(Self::navigate_backward)) + .on_action(cx.listener(Self::navigate_forward)) .on_action( cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| { pane.activate_item( @@ -3571,33 +3590,14 @@ impl Render for Pane { ); }), ) - .on_action( - cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| { - pane.activate_item(pane.items.len().saturating_sub(1), true, true, window, cx); - }), - ) - .on_action( - cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| { - pane.activate_prev_item(true, window, cx); - }), - ) - .on_action( - cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| { - pane.activate_next_item(true, window, cx); - }), - ) - .on_action( - cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)), - ) - .on_action( - cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)), - ) - .on_action(cx.listener(|pane, action, window, cx| { - pane.toggle_pin_tab(action, window, cx); - })) - .on_action(cx.listener(|pane, action, window, cx| { - pane.unpin_all_tabs(action, window, cx); - })) + .on_action(cx.listener(Self::alternate_file)) + .on_action(cx.listener(Self::activate_last_item)) + .on_action(cx.listener(Self::activate_previous_item)) + .on_action(cx.listener(Self::activate_next_item)) + .on_action(cx.listener(Self::swap_item_left)) + .on_action(cx.listener(Self::swap_item_right)) + .on_action(cx.listener(Self::toggle_pin_tab)) + .on_action(cx.listener(Self::unpin_all_tabs)) .when(PreviewTabsSettings::get_global(cx).enabled, |this| { this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| { if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) { @@ -6452,6 +6452,57 @@ mod tests { .unwrap(); } + #[gpui::test] + async fn test_item_swapping_actions(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, None, cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + assert_item_labels(&pane, [], cx); + + // Test that these actions do not panic + pane.update_in(cx, |pane, window, cx| { + pane.swap_item_right(&Default::default(), window, cx); + }); + + pane.update_in(cx, |pane, window, cx| { + pane.swap_item_left(&Default::default(), window, cx); + }); + + add_labeled_item(&pane, "A", false, cx); + add_labeled_item(&pane, "B", false, cx); + add_labeled_item(&pane, "C", false, cx); + assert_item_labels(&pane, ["A", "B", "C*"], cx); + + pane.update_in(cx, |pane, window, cx| { + pane.swap_item_right(&Default::default(), window, cx); + }); + assert_item_labels(&pane, ["A", "B", "C*"], cx); + + pane.update_in(cx, |pane, window, cx| { + pane.swap_item_left(&Default::default(), window, cx); + }); + assert_item_labels(&pane, ["A", "C*", "B"], cx); + + pane.update_in(cx, |pane, window, cx| { + pane.swap_item_left(&Default::default(), window, cx); + }); + assert_item_labels(&pane, ["C*", "A", "B"], cx); + + pane.update_in(cx, |pane, window, cx| { + pane.swap_item_left(&Default::default(), window, cx); + }); + assert_item_labels(&pane, ["C*", "A", "B"], cx); + + pane.update_in(cx, |pane, window, cx| { + pane.swap_item_right(&Default::default(), window, cx); + }); + assert_item_labels(&pane, ["A", "C*", "B"], cx); + } + fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); From 91b2a84001930c00e41462d87279d8ddc87a3b5b Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 22 Aug 2025 15:17:02 -0700 Subject: [PATCH 629/693] Add a few more testing features (#36778) Release Notes: - N/A --------- Co-authored-by: Marshall --- Procfile.web | 2 ++ crates/client/src/client.rs | 12 ++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 Procfile.web diff --git a/Procfile.web b/Procfile.web new file mode 100644 index 0000000000..8140555144 --- /dev/null +++ b/Procfile.web @@ -0,0 +1,2 @@ +postgrest_llm: postgrest crates/collab/postgrest_llm.conf +website: cd ../zed.dev; npm run dev -- --port=3000 diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index f9b8a10610..2bbe7dd1b5 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -66,6 +66,8 @@ pub static IMPERSONATE_LOGIN: LazyLock> = LazyLock::new(|| { .and_then(|s| if s.is_empty() { None } else { Some(s) }) }); +pub static USE_WEB_LOGIN: LazyLock = LazyLock::new(|| std::env::var("ZED_WEB_LOGIN").is_ok()); + pub static ADMIN_API_TOKEN: LazyLock> = LazyLock::new(|| { std::env::var("ZED_ADMIN_API_TOKEN") .ok() @@ -1392,11 +1394,13 @@ impl Client { if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) { - eprintln!("authenticate as admin {login}, {token}"); + if !*USE_WEB_LOGIN { + eprintln!("authenticate as admin {login}, {token}"); - return this - .authenticate_as_admin(http, login.clone(), token.clone()) - .await; + return this + .authenticate_as_admin(http, login.clone(), token.clone()) + .await; + } } // Start an HTTP server to receive the redirect from Zed's sign-in page. From bc566fe18e2e7fe84df7475029ad480561e87d78 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Sat, 23 Aug 2025 00:35:26 +0200 Subject: [PATCH 630/693] agent2: Tweak usage callout border (#36777) Release Notes: - N/A --- .../agent_ui/src/ui/preview/usage_callouts.rs | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/crates/agent_ui/src/ui/preview/usage_callouts.rs b/crates/agent_ui/src/ui/preview/usage_callouts.rs index 29b12ea627..d4d037b976 100644 --- a/crates/agent_ui/src/ui/preview/usage_callouts.rs +++ b/crates/agent_ui/src/ui/preview/usage_callouts.rs @@ -86,23 +86,18 @@ impl RenderOnce for UsageCallout { (IconName::Warning, Severity::Warning) }; - div() - .border_t_1() - .border_color(cx.theme().colors().border) - .child( - Callout::new() - .icon(icon) - .severity(severity) - .icon(icon) - .title(title) - .description(message) - .actions_slot( - Button::new("upgrade", button_text) - .label_size(LabelSize::Small) - .on_click(move |_, _, cx| { - cx.open_url(&url); - }), - ), + Callout::new() + .icon(icon) + .severity(severity) + .icon(icon) + .title(title) + .description(message) + .actions_slot( + Button::new("upgrade", button_text) + .label_size(LabelSize::Small) + .on_click(move |_, _, cx| { + cx.open_url(&url); + }), ) .into_any_element() } From 153724aad3709abc8bbbc59d584fe139d4ec801f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Aug 2025 15:44:58 -0700 Subject: [PATCH 631/693] Clean up handling of serialized ssh connection ids (#36781) Small follow-up to #36714 Release Notes: - N/A --- crates/remote/src/ssh_session.rs | 5 - crates/workspace/src/persistence.rs | 166 +++++++++++----------- crates/workspace/src/persistence/model.rs | 7 +- crates/workspace/src/workspace.rs | 12 +- 4 files changed, 93 insertions(+), 97 deletions(-) diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index c02d0ad7e7..b9af528643 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -52,11 +52,6 @@ use util::{ paths::{PathStyle, RemotePathBuf}, }; -#[derive( - Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, -)] -pub struct SshProjectId(pub u64); - #[derive(Clone)] pub struct SshSocket { connection_options: SshConnectionOptions, diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index de8f63957c..39a1e08c93 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -9,13 +9,13 @@ use std::{ }; use anyhow::{Context as _, Result, bail}; +use collections::HashMap; use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size}; use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}; use language::{LanguageName, Toolchain}; use project::WorktreeId; -use remote::ssh_session::SshProjectId; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::{SqlType, Statement}, @@ -33,7 +33,7 @@ use crate::{ use model::{ GroupId, ItemId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, - SerializedSshConnection, SerializedWorkspace, + SerializedSshConnection, SerializedWorkspace, SshConnectionId, }; use self::model::{DockStructure, SerializedWorkspaceLocation}; @@ -615,7 +615,7 @@ impl WorkspaceDb { pub(crate) fn ssh_workspace_for_roots>( &self, worktree_roots: &[P], - ssh_project_id: SshProjectId, + ssh_project_id: SshConnectionId, ) -> Option { self.workspace_for_roots_internal(worktree_roots, Some(ssh_project_id)) } @@ -623,7 +623,7 @@ impl WorkspaceDb { pub(crate) fn workspace_for_roots_internal>( &self, worktree_roots: &[P], - ssh_connection_id: Option, + ssh_connection_id: Option, ) -> Option { // paths are sorted before db interactions to ensure that the order of the paths // doesn't affect the workspace selection for existing workspaces @@ -762,15 +762,21 @@ impl WorkspaceDb { /// that used this workspace previously pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) { let paths = workspace.paths.serialize(); - let ssh_connection_id = match &workspace.location { - SerializedWorkspaceLocation::Local => None, - SerializedWorkspaceLocation::Ssh(serialized_ssh_connection) => { - Some(serialized_ssh_connection.id.0) - } - }; log::debug!("Saving workspace at location: {:?}", workspace.location); self.write(move |conn| { conn.with_savepoint("update_worktrees", || { + let ssh_connection_id = match &workspace.location { + SerializedWorkspaceLocation::Local => None, + SerializedWorkspaceLocation::Ssh(connection) => { + Some(Self::get_or_create_ssh_connection_query( + conn, + connection.host.clone(), + connection.port, + connection.user.clone(), + )?.0) + } + }; + // Clear out panes and pane_groups conn.exec_bound(sql!( DELETE FROM pane_groups WHERE workspace_id = ?1; @@ -893,39 +899,34 @@ impl WorkspaceDb { host: String, port: Option, user: Option, - ) -> Result { - if let Some(id) = self - .get_ssh_connection(host.clone(), port, user.clone()) - .await? + ) -> Result { + self.write(move |conn| Self::get_or_create_ssh_connection_query(conn, host, port, user)) + .await + } + + fn get_or_create_ssh_connection_query( + this: &Connection, + host: String, + port: Option, + user: Option, + ) -> Result { + if let Some(id) = this.select_row_bound(sql!( + SELECT id FROM ssh_connections WHERE host IS ? AND port IS ? AND user IS ? LIMIT 1 + ))?((host.clone(), port, user.clone()))? { - Ok(SshProjectId(id)) + Ok(SshConnectionId(id)) } else { log::debug!("Inserting SSH project at host {host}"); - let id = self - .insert_ssh_connection(host, port, user) - .await? - .context("failed to insert ssh project")?; - Ok(SshProjectId(id)) - } - } - - query! { - async fn get_ssh_connection(host: String, port: Option, user: Option) -> Result> { - SELECT id - FROM ssh_connections - WHERE host IS ? AND port IS ? AND user IS ? - LIMIT 1 - } - } - - query! { - async fn insert_ssh_connection(host: String, port: Option, user: Option) -> Result> { - INSERT INTO ssh_connections ( - host, - port, - user - ) VALUES (?1, ?2, ?3) - RETURNING id + let id = this.select_row_bound(sql!( + INSERT INTO ssh_connections ( + host, + port, + user + ) VALUES (?1, ?2, ?3) + RETURNING id + ))?((host, port, user))? + .context("failed to insert ssh project")?; + Ok(SshConnectionId(id)) } } @@ -963,7 +964,7 @@ impl WorkspaceDb { fn session_workspaces( &self, session_id: String, - ) -> Result, Option)>> { + ) -> Result, Option)>> { Ok(self .session_workspaces_query(session_id)? .into_iter() @@ -971,7 +972,7 @@ impl WorkspaceDb { ( PathList::deserialize(&SerializedPathList { paths, order }), window_id, - ssh_connection_id.map(SshProjectId), + ssh_connection_id.map(SshConnectionId), ) }) .collect()) @@ -1001,15 +1002,15 @@ impl WorkspaceDb { } } - fn ssh_connections(&self) -> Result> { + fn ssh_connections(&self) -> Result> { Ok(self .ssh_connections_query()? .into_iter() - .map(|(id, host, port, user)| SerializedSshConnection { - id: SshProjectId(id), - host, - port, - user, + .map(|(id, host, port, user)| { + ( + SshConnectionId(id), + SerializedSshConnection { host, port, user }, + ) }) .collect()) } @@ -1021,19 +1022,18 @@ impl WorkspaceDb { } } - pub fn ssh_connection(&self, id: SshProjectId) -> Result { + pub(crate) fn ssh_connection(&self, id: SshConnectionId) -> Result { let row = self.ssh_connection_query(id.0)?; Ok(SerializedSshConnection { - id: SshProjectId(row.0), - host: row.1, - port: row.2, - user: row.3, + host: row.0, + port: row.1, + user: row.2, }) } query! { - fn ssh_connection_query(id: u64) -> Result<(u64, String, Option, Option)> { - SELECT id, host, port, user + fn ssh_connection_query(id: u64) -> Result<(String, Option, Option)> { + SELECT host, port, user FROM ssh_connections WHERE id = ? } @@ -1075,10 +1075,8 @@ impl WorkspaceDb { let ssh_connections = self.ssh_connections()?; for (id, paths, ssh_connection_id) in self.recent_workspaces()? { - if let Some(ssh_connection_id) = ssh_connection_id.map(SshProjectId) { - if let Some(ssh_connection) = - ssh_connections.iter().find(|rp| rp.id == ssh_connection_id) - { + if let Some(ssh_connection_id) = ssh_connection_id.map(SshConnectionId) { + if let Some(ssh_connection) = ssh_connections.get(&ssh_connection_id) { result.push(( id, SerializedWorkspaceLocation::Ssh(ssh_connection.clone()), @@ -2340,12 +2338,10 @@ mod tests { ] .into_iter() .map(|(host, user)| async { - let id = db - .get_or_create_ssh_connection(host.to_string(), None, Some(user.to_string())) + db.get_or_create_ssh_connection(host.to_string(), None, Some(user.to_string())) .await .unwrap(); SerializedSshConnection { - id, host: host.into(), port: None, user: Some(user.into()), @@ -2501,26 +2497,34 @@ mod tests { let stored_projects = db.ssh_connections().unwrap(); assert_eq!( stored_projects, - &[ - SerializedSshConnection { - id: ids[0], - host: "example.com".into(), - port: None, - user: None, - }, - SerializedSshConnection { - id: ids[1], - host: "anotherexample.com".into(), - port: Some(123), - user: Some("user2".into()), - }, - SerializedSshConnection { - id: ids[2], - host: "yetanother.com".into(), - port: Some(345), - user: None, - }, + [ + ( + ids[0], + SerializedSshConnection { + host: "example.com".into(), + port: None, + user: None, + } + ), + ( + ids[1], + SerializedSshConnection { + host: "anotherexample.com".into(), + port: Some(123), + user: Some("user2".into()), + } + ), + ( + ids[2], + SerializedSshConnection { + host: "yetanother.com".into(), + port: Some(345), + user: None, + } + ), ] + .into_iter() + .collect::>(), ); } diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index afe4ae6235..04757d0495 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -12,7 +12,6 @@ use db::sqlez::{ use gpui::{AsyncWindowContext, Entity, WeakEntity}; use project::{Project, debugger::breakpoint_store::SourceBreakpoint}; -use remote::ssh_session::SshProjectId; use serde::{Deserialize, Serialize}; use std::{ collections::BTreeMap, @@ -22,9 +21,13 @@ use std::{ use util::ResultExt; use uuid::Uuid; +#[derive( + Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, +)] +pub(crate) struct SshConnectionId(pub u64); + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct SerializedSshConnection { - pub id: SshProjectId, pub host: String, pub port: Option, pub user: Option, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d07ea30cf9..bf58786d67 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -74,10 +74,7 @@ use project::{ DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId, debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus}, }; -use remote::{ - SshClientDelegate, SshConnectionOptions, - ssh_session::{ConnectionIdentifier, SshProjectId}, -}; +use remote::{SshClientDelegate, SshConnectionOptions, ssh_session::ConnectionIdentifier}; use schemars::JsonSchema; use serde::Deserialize; use session::AppSession; @@ -1128,7 +1125,6 @@ pub struct Workspace { terminal_provider: Option>, debugger_provider: Option>, serializable_items_tx: UnboundedSender>, - serialized_ssh_connection_id: Option, _items_serializer: Task>, session_id: Option, scheduled_tasks: Vec>, @@ -1461,7 +1457,7 @@ impl Workspace { serializable_items_tx, _items_serializer, session_id: Some(session_id), - serialized_ssh_connection_id: None, + scheduled_tasks: Vec::new(), } } @@ -5288,11 +5284,9 @@ impl Workspace { fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation { let paths = PathList::new(&self.root_paths(cx)); - let connection = self.project.read(cx).ssh_connection_options(cx); - if let Some((id, connection)) = self.serialized_ssh_connection_id.zip(connection) { + if let Some(connection) = self.project.read(cx).ssh_connection_options(cx) { WorkspaceLocation::Location( SerializedWorkspaceLocation::Ssh(SerializedSshConnection { - id, host: connection.host, port: connection.port, user: connection.username, From d24cad30f3805f03a4030703701ef77639a028bc Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 23 Aug 2025 01:55:50 +0300 Subject: [PATCH 632/693] Be more lenient when dealing with rust-analyzer's flycheck commands (#36782) Flycheck commands are global and makes sense to fall back to looking up project's rust-analyzer even if the commands are run on a non-rust buffer. If multiple rust-analyzers are found in the project, avoid ambiguous commands and bail (as before). Closes #ISSUE Release Notes: - Made it possible to run rust-analyzer's flycheck actions from anywhere in the project --- crates/diagnostics/src/diagnostics.rs | 4 +- crates/editor/src/rust_analyzer_ext.rs | 35 +++--- crates/project/src/lsp_store.rs | 23 ++-- .../src/lsp_store/rust_analyzer_ext.rs | 108 ++++++++++++------ crates/proto/proto/lsp.proto | 8 +- 5 files changed, 114 insertions(+), 64 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 2e20118381..037e4fc0fd 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -438,7 +438,7 @@ impl ProjectDiagnosticsEditor { for buffer_path in diagnostics_sources.iter().cloned() { if cx .update(|cx| { - fetch_tasks.push(run_flycheck(project.clone(), buffer_path, cx)); + fetch_tasks.push(run_flycheck(project.clone(), Some(buffer_path), cx)); }) .is_err() { @@ -462,7 +462,7 @@ impl ProjectDiagnosticsEditor { .iter() .cloned() { - cancel_gasks.push(cancel_flycheck(self.project.clone(), buffer_path, cx)); + cancel_gasks.push(cancel_flycheck(self.project.clone(), Some(buffer_path), cx)); } self.cargo_diagnostics_fetch.cancel_task = Some(cx.background_spawn(async move { diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index e3d83ab160..cf74ee0a9e 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -26,6 +26,17 @@ fn is_rust_language(language: &Language) -> bool { } pub fn apply_related_actions(editor: &Entity, window: &mut Window, cx: &mut App) { + if editor.read(cx).project().is_some_and(|project| { + project + .read(cx) + .language_server_statuses(cx) + .any(|(_, status)| status.name == RUST_ANALYZER_NAME) + }) { + register_action(editor, window, cancel_flycheck_action); + register_action(editor, window, run_flycheck_action); + register_action(editor, window, clear_flycheck_action); + } + if editor .read(cx) .buffer() @@ -38,9 +49,6 @@ pub fn apply_related_actions(editor: &Entity, window: &mut Window, cx: & register_action(editor, window, go_to_parent_module); register_action(editor, window, expand_macro_recursively); register_action(editor, window, open_docs); - register_action(editor, window, cancel_flycheck_action); - register_action(editor, window, run_flycheck_action); - register_action(editor, window, clear_flycheck_action); } } @@ -309,7 +317,7 @@ fn cancel_flycheck_action( let Some(project) = &editor.project else { return; }; - let Some(buffer_id) = editor + let buffer_id = editor .selections .disjoint_anchors() .iter() @@ -321,10 +329,7 @@ fn cancel_flycheck_action( .read(cx) .entry_id(cx)?; project.path_for_entry(entry_id, cx) - }) - else { - return; - }; + }); cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); } @@ -337,7 +342,7 @@ fn run_flycheck_action( let Some(project) = &editor.project else { return; }; - let Some(buffer_id) = editor + let buffer_id = editor .selections .disjoint_anchors() .iter() @@ -349,10 +354,7 @@ fn run_flycheck_action( .read(cx) .entry_id(cx)?; project.path_for_entry(entry_id, cx) - }) - else { - return; - }; + }); run_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); } @@ -365,7 +367,7 @@ fn clear_flycheck_action( let Some(project) = &editor.project else { return; }; - let Some(buffer_id) = editor + let buffer_id = editor .selections .disjoint_anchors() .iter() @@ -377,9 +379,6 @@ fn clear_flycheck_action( .read(cx) .entry_id(cx)?; project.path_for_entry(entry_id, cx) - }) - else { - return; - }; + }); clear_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index cc3a0a05bb..fb1fae3736 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -9029,13 +9029,22 @@ impl LspStore { lsp_store.update(&mut cx, |lsp_store, cx| { if let Some(server) = lsp_store.language_server_for_id(server_id) { let text_document = if envelope.payload.current_file_only { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - lsp_store - .buffer_store() - .read(cx) - .get(buffer_id) - .and_then(|buffer| Some(buffer.read(cx).file()?.as_local()?.abs_path(cx))) - .map(|path| make_text_document_identifier(&path)) + let buffer_id = envelope + .payload + .buffer_id + .map(|id| BufferId::new(id)) + .transpose()?; + buffer_id + .and_then(|buffer_id| { + lsp_store + .buffer_store() + .read(cx) + .get(buffer_id) + .and_then(|buffer| { + Some(buffer.read(cx).file()?.as_local()?.abs_path(cx)) + }) + .map(|path| make_text_document_identifier(&path)) + }) .transpose()? } else { None diff --git a/crates/project/src/lsp_store/rust_analyzer_ext.rs b/crates/project/src/lsp_store/rust_analyzer_ext.rs index e5e6338d3c..54f63220b1 100644 --- a/crates/project/src/lsp_store/rust_analyzer_ext.rs +++ b/crates/project/src/lsp_store/rust_analyzer_ext.rs @@ -1,8 +1,8 @@ use ::serde::{Deserialize, Serialize}; use anyhow::Context as _; -use gpui::{App, Entity, Task, WeakEntity}; -use language::ServerHealth; -use lsp::{LanguageServer, LanguageServerName}; +use gpui::{App, AsyncApp, Entity, Task, WeakEntity}; +use language::{Buffer, ServerHealth}; +use lsp::{LanguageServer, LanguageServerId, LanguageServerName}; use rpc::proto; use crate::{LspStore, LspStoreEvent, Project, ProjectPath, lsp_store}; @@ -83,31 +83,32 @@ pub fn register_notifications(lsp_store: WeakEntity, language_server: pub fn cancel_flycheck( project: Entity, - buffer_path: ProjectPath, + buffer_path: Option, cx: &mut App, ) -> Task> { let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); let lsp_store = project.read(cx).lsp_store(); - let buffer = project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) + let buffer = buffer_path.map(|buffer_path| { + project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) + }) }) }); cx.spawn(async move |cx| { - let buffer = buffer.await?; - let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| { - project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) - })? + let buffer = match buffer { + Some(buffer) => Some(buffer.await?), + None => None, + }; + let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) else { return Ok(()); }; - let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?; if let Some((client, project_id)) = upstream_client { let request = proto::LspExtCancelFlycheck { project_id, - buffer_id, language_server_id: rust_analyzer_server.to_proto(), }; client @@ -130,28 +131,33 @@ pub fn cancel_flycheck( pub fn run_flycheck( project: Entity, - buffer_path: ProjectPath, + buffer_path: Option, cx: &mut App, ) -> Task> { let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); let lsp_store = project.read(cx).lsp_store(); - let buffer = project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) + let buffer = buffer_path.map(|buffer_path| { + project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) + }) }) }); cx.spawn(async move |cx| { - let buffer = buffer.await?; - let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| { - project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) - })? + let buffer = match buffer { + Some(buffer) => Some(buffer.await?), + None => None, + }; + let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) else { return Ok(()); }; - let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?; if let Some((client, project_id)) = upstream_client { + let buffer_id = buffer + .map(|buffer| buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())) + .transpose()?; let request = proto::LspExtRunFlycheck { project_id, buffer_id, @@ -182,31 +188,32 @@ pub fn run_flycheck( pub fn clear_flycheck( project: Entity, - buffer_path: ProjectPath, + buffer_path: Option, cx: &mut App, ) -> Task> { let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); let lsp_store = project.read(cx).lsp_store(); - let buffer = project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) + let buffer = buffer_path.map(|buffer_path| { + project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) + }) }) }); cx.spawn(async move |cx| { - let buffer = buffer.await?; - let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| { - project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) - })? + let buffer = match buffer { + Some(buffer) => Some(buffer.await?), + None => None, + }; + let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) else { return Ok(()); }; - let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?; if let Some((client, project_id)) = upstream_client { let request = proto::LspExtClearFlycheck { project_id, - buffer_id, language_server_id: rust_analyzer_server.to_proto(), }; client @@ -226,3 +233,40 @@ pub fn clear_flycheck( anyhow::Ok(()) }) } + +fn find_rust_analyzer_server( + project: &Entity, + buffer: Option<&Entity>, + cx: &mut AsyncApp, +) -> Option { + project + .read_with(cx, |project, cx| { + buffer + .and_then(|buffer| { + project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) + }) + // If no rust-analyzer found for the current buffer (e.g. `settings.json`), fall back to the project lookup + // and use project's rust-analyzer if it's the only one. + .or_else(|| { + let rust_analyzer_servers = project + .lsp_store() + .read(cx) + .language_server_statuses + .iter() + .filter_map(|(server_id, server_status)| { + if server_status.name == RUST_ANALYZER_NAME { + Some(*server_id) + } else { + None + } + }) + .collect::>(); + if rust_analyzer_servers.len() == 1 { + rust_analyzer_servers.first().copied() + } else { + None + } + }) + }) + .ok()? +} diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index ac9c275aa2..473ef5c38c 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -834,21 +834,19 @@ message LspRunnable { message LspExtCancelFlycheck { uint64 project_id = 1; - uint64 buffer_id = 2; - uint64 language_server_id = 3; + uint64 language_server_id = 2; } message LspExtRunFlycheck { uint64 project_id = 1; - uint64 buffer_id = 2; + optional uint64 buffer_id = 2; uint64 language_server_id = 3; bool current_file_only = 4; } message LspExtClearFlycheck { uint64 project_id = 1; - uint64 buffer_id = 2; - uint64 language_server_id = 3; + uint64 language_server_id = 2; } message LspDiagnosticRelatedInformation { From f48a8f2b6a702fe1051016817097ee2c08ad7e22 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 22 Aug 2025 20:10:26 -0300 Subject: [PATCH 633/693] thread view: Simplify tool call & improve required auth state UIs (#36783) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 145 ++++++++++++++----------- 1 file changed, 82 insertions(+), 63 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 2a83a4ab5b..0e1d4123b9 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1668,39 +1668,14 @@ impl AcpThreadView { let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix)); let card_header_id = SharedString::from("inner-tool-call-header"); - let status_icon = match &tool_call.status { - ToolCallStatus::Pending - | ToolCallStatus::WaitingForConfirmation { .. } - | ToolCallStatus::Completed => None, - ToolCallStatus::InProgress => Some( - div() - .absolute() - .right_2() - .child( - Icon::new(IconName::ArrowCircle) - .color(Color::Muted) - .size(IconSize::Small) - .with_animation( - "running", - Animation::new(Duration::from_secs(3)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage(delta))) - }, - ), - ) - .into_any(), - ), - ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => Some( - div() - .absolute() - .right_2() - .child( - Icon::new(IconName::Close) - .color(Color::Error) - .size(IconSize::Small), - ) - .into_any_element(), - ), + let in_progress = match &tool_call.status { + ToolCallStatus::InProgress => true, + _ => false, + }; + + let failed_or_canceled = match &tool_call.status { + ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true, + _ => false, }; let failed_tool_call = matches!( @@ -1884,7 +1859,33 @@ impl AcpThreadView { .into_any() }), ) - .children(status_icon), + .when(in_progress && use_card_layout, |this| { + this.child( + div().absolute().right_2().child( + Icon::new(IconName::ArrowCircle) + .color(Color::Muted) + .size(IconSize::Small) + .with_animation( + "running", + Animation::new(Duration::from_secs(3)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate(percentage( + delta, + ))) + }, + ), + ), + ) + }) + .when(failed_or_canceled, |this| { + this.child( + div().absolute().right_2().child( + Icon::new(IconName::Close) + .color(Color::Error) + .size(IconSize::Small), + ), + ) + }), ) .children(tool_output_display) } @@ -2579,11 +2580,15 @@ impl AcpThreadView { window: &mut Window, cx: &Context, ) -> Div { + let show_description = + configuration_view.is_none() && description.is_none() && pending_auth_method.is_none(); + v_flex().flex_1().size_full().justify_end().child( v_flex() .p_2() .pr_3() .w_full() + .gap_1() .border_t_1() .border_color(cx.theme().colors().border) .bg(cx.theme().status().warning.opacity(0.04)) @@ -2595,7 +2600,7 @@ impl AcpThreadView { .color(Color::Warning) .size(IconSize::Small), ) - .child(Label::new("Authentication Required")), + .child(Label::new("Authentication Required").size(LabelSize::Small)), ) .children(description.map(|desc| { div().text_ui(cx).child(self.render_markdown( @@ -2609,44 +2614,20 @@ impl AcpThreadView { .map(|view| div().w_full().child(view)), ) .when( - configuration_view.is_none() - && description.is_none() - && pending_auth_method.is_none(), + show_description, |el| { el.child( Label::new(format!( "You are not currently authenticated with {}. Please choose one of the following options:", self.agent.name() )) + .size(LabelSize::Small) .color(Color::Muted) .mb_1() .ml_5(), ) }, ) - .when(!connection.auth_methods().is_empty(), |this| { - this.child( - h_flex().justify_end().flex_wrap().gap_1().children( - connection.auth_methods().iter().enumerate().rev().map( - |(ix, method)| { - Button::new( - SharedString::from(method.id.0.clone()), - method.name.clone(), - ) - .when(ix == 0, |el| { - el.style(ButtonStyle::Tinted(ui::TintColor::Warning)) - }) - .on_click({ - let method_id = method.id.clone(); - cx.listener(move |this, _, window, cx| { - this.authenticate(method_id.clone(), window, cx) - }) - }) - }, - ), - ), - ) - }) .when_some(pending_auth_method, |el, _| { el.child( h_flex() @@ -2669,9 +2650,47 @@ impl AcpThreadView { ) .into_any_element(), ) - .child(Label::new("Authenticating…")), + .child(Label::new("Authenticating…").size(LabelSize::Small)), ) - }), + }) + .when(!connection.auth_methods().is_empty(), |this| { + this.child( + h_flex() + .justify_end() + .flex_wrap() + .gap_1() + .when(!show_description, |this| { + this.border_t_1() + .mt_1() + .pt_2() + .border_color(cx.theme().colors().border.opacity(0.8)) + }) + .children( + connection + .auth_methods() + .iter() + .enumerate() + .rev() + .map(|(ix, method)| { + Button::new( + SharedString::from(method.id.0.clone()), + method.name.clone(), + ) + .when(ix == 0, |el| { + el.style(ButtonStyle::Tinted(ui::TintColor::Warning)) + }) + .label_size(LabelSize::Small) + .on_click({ + let method_id = method.id.clone(); + cx.listener(move |this, _, window, cx| { + this.authenticate(method_id.clone(), window, cx) + }) + }) + }), + ), + ) + }) + ) } From 5da31fdb725d41f62900a8317e0919d30fa54f15 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 22 Aug 2025 22:09:08 -0600 Subject: [PATCH 634/693] acp: Remove ACP v0 (#36785) We had a few people confused about why some features weren't working due to the fallback logic. It's gone. Release Notes: - N/A --- Cargo.lock | 61 +---- Cargo.toml | 1 - crates/agent_servers/Cargo.toml | 1 - crates/agent_servers/src/acp.rs | 387 ++++++++++++++++++++++++++++-- tooling/workspace-hack/Cargo.toml | 2 - 5 files changed, 381 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4043666823..aa3a910390 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -289,7 +289,6 @@ dependencies = [ "action_log", "agent-client-protocol", "agent_settings", - "agentic-coding-protocol", "anyhow", "client", "collections", @@ -443,24 +442,6 @@ dependencies = [ "zed_actions", ] -[[package]] -name = "agentic-coding-protocol" -version = "0.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e6ae951b36fa2f8d9dd6e1af6da2fcaba13d7c866cf6a9e65deda9dc6c5fe4" -dependencies = [ - "anyhow", - "chrono", - "derive_more 2.0.1", - "futures 0.3.31", - "log", - "parking_lot", - "schemars", - "semver", - "serde", - "serde_json", -] - [[package]] name = "ahash" version = "0.7.8" @@ -876,7 +857,7 @@ dependencies = [ "anyhow", "async-trait", "collections", - "derive_more 0.99.19", + "derive_more", "extension", "futures 0.3.31", "gpui", @@ -939,7 +920,7 @@ dependencies = [ "clock", "collections", "ctor", - "derive_more 0.99.19", + "derive_more", "gpui", "icons", "indoc", @@ -976,7 +957,7 @@ dependencies = [ "cloud_llm_client", "collections", "component", - "derive_more 0.99.19", + "derive_more", "diffy", "editor", "feature_flags", @@ -3089,7 +3070,7 @@ dependencies = [ "cocoa 0.26.0", "collections", "credentials_provider", - "derive_more 0.99.19", + "derive_more", "feature_flags", "fs", "futures 0.3.31", @@ -3521,7 +3502,7 @@ name = "command_palette_hooks" version = "0.1.0" dependencies = [ "collections", - "derive_more 0.99.19", + "derive_more", "gpui", "workspace-hack", ] @@ -4684,27 +4665,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "derive_more" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", - "unicode-xid", -] - [[package]] name = "derive_refineable" version = "0.1.0" @@ -6441,7 +6401,7 @@ dependencies = [ "askpass", "async-trait", "collections", - "derive_more 0.99.19", + "derive_more", "futures 0.3.31", "git2", "gpui", @@ -7471,7 +7431,7 @@ dependencies = [ "core-video", "cosmic-text", "ctor", - "derive_more 0.99.19", + "derive_more", "embed-resource", "env_logger 0.11.8", "etagere", @@ -7996,7 +7956,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bytes 1.10.1", - "derive_more 0.99.19", + "derive_more", "futures 0.3.31", "http 1.3.1", "http-body 1.0.1", @@ -14399,12 +14359,10 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984" dependencies = [ - "chrono", "dyn-clone", "indexmap", "ref-cast", "schemars_derive", - "semver", "serde", "serde_json", ] @@ -16488,7 +16446,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", - "derive_more 0.99.19", + "derive_more", "fs", "futures 0.3.31", "gpui", @@ -20003,7 +19961,6 @@ dependencies = [ "rustix 1.0.7", "rustls 0.23.26", "rustls-webpki 0.103.1", - "schemars", "scopeguard", "sea-orm", "sea-query-binder", diff --git a/Cargo.toml b/Cargo.toml index 7668d18752..6ec243a9b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -426,7 +426,6 @@ zlog_settings = { path = "crates/zlog_settings" } # External crates # -agentic-coding-protocol = "0.0.10" agent-client-protocol = "0.0.31" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index 8ea4a27f4c..9f90f3a78a 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -22,7 +22,6 @@ acp_thread.workspace = true action_log.workspace = true agent-client-protocol.workspace = true agent_settings.workspace = true -agentic-coding-protocol.workspace = true anyhow.workspace = true client = { workspace = true, optional = true } collections.workspace = true diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index 1cfb1fcabf..a99a401431 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -1,34 +1,391 @@ -use std::{path::Path, rc::Rc}; - use crate::AgentServerCommand; use acp_thread::AgentConnection; -use anyhow::Result; -use gpui::AsyncApp; +use acp_tools::AcpConnectionRegistry; +use action_log::ActionLog; +use agent_client_protocol::{self as acp, Agent as _, ErrorCode}; +use anyhow::anyhow; +use collections::HashMap; +use futures::AsyncBufReadExt as _; +use futures::channel::oneshot; +use futures::io::BufReader; +use project::Project; +use serde::Deserialize; +use std::{any::Any, cell::RefCell}; +use std::{path::Path, rc::Rc}; use thiserror::Error; -mod v0; -mod v1; +use anyhow::{Context as _, Result}; +use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity}; + +use acp_thread::{AcpThread, AuthRequired, LoadError}; #[derive(Debug, Error)] #[error("Unsupported version")] pub struct UnsupportedVersion; +pub struct AcpConnection { + server_name: &'static str, + connection: Rc, + sessions: Rc>>, + auth_methods: Vec, + prompt_capabilities: acp::PromptCapabilities, + _io_task: Task>, +} + +pub struct AcpSession { + thread: WeakEntity, + suppress_abort_err: bool, +} + pub async fn connect( server_name: &'static str, command: AgentServerCommand, root_dir: &Path, cx: &mut AsyncApp, ) -> Result> { - let conn = v1::AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await; + let conn = AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await?; + Ok(Rc::new(conn) as _) +} - match conn { - Ok(conn) => Ok(Rc::new(conn) as _), - Err(err) if err.is::() => { - // Consider re-using initialize response and subprocess when adding another version here - let conn: Rc = - Rc::new(v0::AcpConnection::stdio(server_name, command, root_dir, cx).await?); - Ok(conn) +const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1; + +impl AcpConnection { + pub async fn stdio( + server_name: &'static str, + command: AgentServerCommand, + root_dir: &Path, + cx: &mut AsyncApp, + ) -> Result { + let mut child = util::command::new_smol_command(&command.path) + .args(command.args.iter().map(|arg| arg.as_str())) + .envs(command.env.iter().flatten()) + .current_dir(root_dir) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .kill_on_drop(true) + .spawn()?; + + let stdout = child.stdout.take().context("Failed to take stdout")?; + let stdin = child.stdin.take().context("Failed to take stdin")?; + let stderr = child.stderr.take().context("Failed to take stderr")?; + log::trace!("Spawned (pid: {})", child.id()); + + let sessions = Rc::new(RefCell::new(HashMap::default())); + + let client = ClientDelegate { + sessions: sessions.clone(), + cx: cx.clone(), + }; + let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, { + let foreground_executor = cx.foreground_executor().clone(); + move |fut| { + foreground_executor.spawn(fut).detach(); + } + }); + + let io_task = cx.background_spawn(io_task); + + cx.background_spawn(async move { + let mut stderr = BufReader::new(stderr); + let mut line = String::new(); + while let Ok(n) = stderr.read_line(&mut line).await + && n > 0 + { + log::warn!("agent stderr: {}", &line); + line.clear(); + } + }) + .detach(); + + cx.spawn({ + let sessions = sessions.clone(); + async move |cx| { + let status = child.status().await?; + + for session in sessions.borrow().values() { + session + .thread + .update(cx, |thread, cx| { + thread.emit_load_error(LoadError::Exited { status }, cx) + }) + .ok(); + } + + anyhow::Ok(()) + } + }) + .detach(); + + let connection = Rc::new(connection); + + cx.update(|cx| { + AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| { + registry.set_active_connection(server_name, &connection, cx) + }); + })?; + + let response = connection + .initialize(acp::InitializeRequest { + protocol_version: acp::VERSION, + client_capabilities: acp::ClientCapabilities { + fs: acp::FileSystemCapability { + read_text_file: true, + write_text_file: true, + }, + }, + }) + .await?; + + if response.protocol_version < MINIMUM_SUPPORTED_VERSION { + return Err(UnsupportedVersion.into()); } - Err(err) => Err(err), + + Ok(Self { + auth_methods: response.auth_methods, + connection, + server_name, + sessions, + prompt_capabilities: response.agent_capabilities.prompt_capabilities, + _io_task: io_task, + }) + } +} + +impl AgentConnection for AcpConnection { + fn new_thread( + self: Rc, + project: Entity, + cwd: &Path, + cx: &mut App, + ) -> Task>> { + let conn = self.connection.clone(); + let sessions = self.sessions.clone(); + let cwd = cwd.to_path_buf(); + cx.spawn(async move |cx| { + let response = conn + .new_session(acp::NewSessionRequest { + mcp_servers: vec![], + cwd, + }) + .await + .map_err(|err| { + if err.code == acp::ErrorCode::AUTH_REQUIRED.code { + let mut error = AuthRequired::new(); + + if err.message != acp::ErrorCode::AUTH_REQUIRED.message { + error = error.with_description(err.message); + } + + anyhow!(error) + } else { + anyhow!(err) + } + })?; + + let session_id = response.session_id; + let action_log = cx.new(|_| ActionLog::new(project.clone()))?; + let thread = cx.new(|_cx| { + AcpThread::new( + self.server_name, + self.clone(), + project, + action_log, + session_id.clone(), + ) + })?; + + let session = AcpSession { + thread: thread.downgrade(), + suppress_abort_err: false, + }; + sessions.borrow_mut().insert(session_id, session); + + Ok(thread) + }) + } + + fn auth_methods(&self) -> &[acp::AuthMethod] { + &self.auth_methods + } + + fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task> { + let conn = self.connection.clone(); + cx.foreground_executor().spawn(async move { + let result = conn + .authenticate(acp::AuthenticateRequest { + method_id: method_id.clone(), + }) + .await?; + + Ok(result) + }) + } + + fn prompt( + &self, + _id: Option, + params: acp::PromptRequest, + cx: &mut App, + ) -> Task> { + let conn = self.connection.clone(); + let sessions = self.sessions.clone(); + let session_id = params.session_id.clone(); + cx.foreground_executor().spawn(async move { + let result = conn.prompt(params).await; + + let mut suppress_abort_err = false; + + if let Some(session) = sessions.borrow_mut().get_mut(&session_id) { + suppress_abort_err = session.suppress_abort_err; + session.suppress_abort_err = false; + } + + match result { + Ok(response) => Ok(response), + Err(err) => { + if err.code != ErrorCode::INTERNAL_ERROR.code { + anyhow::bail!(err) + } + + let Some(data) = &err.data else { + anyhow::bail!(err) + }; + + // Temporary workaround until the following PR is generally available: + // https://github.com/google-gemini/gemini-cli/pull/6656 + + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct ErrorDetails { + details: Box, + } + + match serde_json::from_value(data.clone()) { + Ok(ErrorDetails { details }) => { + if suppress_abort_err && details.contains("This operation was aborted") + { + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::Cancelled, + }) + } else { + Err(anyhow!(details)) + } + } + Err(_) => Err(anyhow!(err)), + } + } + } + }) + } + + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + self.prompt_capabilities + } + + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { + if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) { + session.suppress_abort_err = true; + } + let conn = self.connection.clone(); + let params = acp::CancelNotification { + session_id: session_id.clone(), + }; + cx.foreground_executor() + .spawn(async move { conn.cancel(params).await }) + .detach(); + } + + fn into_any(self: Rc) -> Rc { + self + } +} + +struct ClientDelegate { + sessions: Rc>>, + cx: AsyncApp, +} + +impl acp::Client for ClientDelegate { + async fn request_permission( + &self, + arguments: acp::RequestPermissionRequest, + ) -> Result { + let cx = &mut self.cx.clone(); + let rx = self + .sessions + .borrow() + .get(&arguments.session_id) + .context("Failed to get session")? + .thread + .update(cx, |thread, cx| { + thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx) + })?; + + let result = rx?.await; + + let outcome = match result { + Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option }, + Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled, + }; + + Ok(acp::RequestPermissionResponse { outcome }) + } + + async fn write_text_file( + &self, + arguments: acp::WriteTextFileRequest, + ) -> Result<(), acp::Error> { + let cx = &mut self.cx.clone(); + let task = self + .sessions + .borrow() + .get(&arguments.session_id) + .context("Failed to get session")? + .thread + .update(cx, |thread, cx| { + thread.write_text_file(arguments.path, arguments.content, cx) + })?; + + task.await?; + + Ok(()) + } + + async fn read_text_file( + &self, + arguments: acp::ReadTextFileRequest, + ) -> Result { + let cx = &mut self.cx.clone(); + let task = self + .sessions + .borrow() + .get(&arguments.session_id) + .context("Failed to get session")? + .thread + .update(cx, |thread, cx| { + thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx) + })?; + + let content = task.await?; + + Ok(acp::ReadTextFileResponse { content }) + } + + async fn session_notification( + &self, + notification: acp::SessionNotification, + ) -> Result<(), acp::Error> { + let cx = &mut self.cx.clone(); + let sessions = self.sessions.borrow(); + let session = sessions + .get(¬ification.session_id) + .context("Failed to get session")?; + + session.thread.update(cx, |thread, cx| { + thread.handle_session_update(notification.update, cx) + })??; + + Ok(()) } } diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index bf44fc195e..2f9a963abc 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -109,7 +109,6 @@ rustc-hash = { version = "1" } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] } rustls = { version = "0.23", features = ["ring"] } rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] } -schemars = { version = "1", features = ["chrono04", "indexmap2", "semver1"] } sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] } sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] } semver = { version = "1", features = ["serde"] } @@ -244,7 +243,6 @@ rustc-hash = { version = "1" } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] } rustls = { version = "0.23", features = ["ring"] } rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] } -schemars = { version = "1", features = ["chrono04", "indexmap2", "semver1"] } sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] } sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] } semver = { version = "1", features = ["serde"] } From ea42013746f1533a49c32c0a6a5d6b84920f85b2 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Sat, 23 Aug 2025 01:21:20 -0400 Subject: [PATCH 635/693] acp: Eagerly load all kinds of mentions (#36741) This PR makes it so that all kinds of @-mentions start loading their context as soon as they are confirmed. Previously, we were waiting to load the context for file, symbol, selection, and rule mentions until the user's message was sent. By kicking off loading immediately for these kinds of context, we can support adding selections from unsaved buffers, and we make the semantics of @-mentions more consistent. Loading all kinds of context eagerly also makes it possible to simplify the structure of the MentionSet and the code around it. Now MentionSet is just a single hash map, all the management of creases happens in a uniform way in `MessageEditor::confirm_completion`, and the helper methods for loading different kinds of context are much more focused and orthogonal. Release Notes: - N/A --------- Co-authored-by: Conrad --- crates/acp_thread/src/mention.rs | 154 +- crates/agent/src/thread_store.rs | 13 +- crates/agent2/src/db.rs | 13 +- crates/agent2/src/thread.rs | 54 +- .../agent_ui/src/acp/completion_provider.rs | 4 +- crates/agent_ui/src/acp/message_editor.rs | 1252 +++++++---------- crates/agent_ui/src/acp/thread_view.rs | 46 +- 7 files changed, 699 insertions(+), 837 deletions(-) diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index a1e713cffa..6fa0887e22 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -5,7 +5,7 @@ use prompt_store::{PromptId, UserPromptId}; use serde::{Deserialize, Serialize}; use std::{ fmt, - ops::Range, + ops::RangeInclusive, path::{Path, PathBuf}, str::FromStr, }; @@ -17,13 +17,14 @@ pub enum MentionUri { File { abs_path: PathBuf, }, + PastedImage, Directory { abs_path: PathBuf, }, Symbol { - path: PathBuf, + abs_path: PathBuf, name: String, - line_range: Range, + line_range: RangeInclusive, }, Thread { id: acp::SessionId, @@ -38,8 +39,9 @@ pub enum MentionUri { name: String, }, Selection { - path: PathBuf, - line_range: Range, + #[serde(default, skip_serializing_if = "Option::is_none")] + abs_path: Option, + line_range: RangeInclusive, }, Fetch { url: Url, @@ -48,36 +50,44 @@ pub enum MentionUri { impl MentionUri { pub fn parse(input: &str) -> Result { + fn parse_line_range(fragment: &str) -> Result> { + let range = fragment + .strip_prefix("L") + .context("Line range must start with \"L\"")?; + let (start, end) = range + .split_once(":") + .context("Line range must use colon as separator")?; + let range = start + .parse::() + .context("Parsing line range start")? + .checked_sub(1) + .context("Line numbers should be 1-based")? + ..=end + .parse::() + .context("Parsing line range end")? + .checked_sub(1) + .context("Line numbers should be 1-based")?; + Ok(range) + } + let url = url::Url::parse(input)?; let path = url.path(); match url.scheme() { "file" => { let path = url.to_file_path().ok().context("Extracting file path")?; if let Some(fragment) = url.fragment() { - let range = fragment - .strip_prefix("L") - .context("Line range must start with \"L\"")?; - let (start, end) = range - .split_once(":") - .context("Line range must use colon as separator")?; - let line_range = start - .parse::() - .context("Parsing line range start")? - .checked_sub(1) - .context("Line numbers should be 1-based")? - ..end - .parse::() - .context("Parsing line range end")? - .checked_sub(1) - .context("Line numbers should be 1-based")?; + let line_range = parse_line_range(fragment)?; if let Some(name) = single_query_param(&url, "symbol")? { Ok(Self::Symbol { name, - path, + abs_path: path, line_range, }) } else { - Ok(Self::Selection { path, line_range }) + Ok(Self::Selection { + abs_path: Some(path), + line_range, + }) } } else if input.ends_with("/") { Ok(Self::Directory { abs_path: path }) @@ -105,6 +115,17 @@ impl MentionUri { id: rule_id.into(), name, }) + } else if path.starts_with("/agent/pasted-image") { + Ok(Self::PastedImage) + } else if path.starts_with("/agent/untitled-buffer") { + let fragment = url + .fragment() + .context("Missing fragment for untitled buffer selection")?; + let line_range = parse_line_range(fragment)?; + Ok(Self::Selection { + abs_path: None, + line_range, + }) } else { bail!("invalid zed url: {:?}", input); } @@ -121,13 +142,16 @@ impl MentionUri { .unwrap_or_default() .to_string_lossy() .into_owned(), + MentionUri::PastedImage => "Image".to_string(), MentionUri::Symbol { name, .. } => name.clone(), MentionUri::Thread { name, .. } => name.clone(), MentionUri::TextThread { name, .. } => name.clone(), MentionUri::Rule { name, .. } => name.clone(), MentionUri::Selection { - path, line_range, .. - } => selection_name(path, line_range), + abs_path: path, + line_range, + .. + } => selection_name(path.as_deref(), line_range), MentionUri::Fetch { url } => url.to_string(), } } @@ -137,6 +161,7 @@ impl MentionUri { MentionUri::File { abs_path } => { FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into()) } + MentionUri::PastedImage => IconName::Image.path().into(), MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx) .unwrap_or_else(|| IconName::Folder.path().into()), MentionUri::Symbol { .. } => IconName::Code.path().into(), @@ -157,29 +182,40 @@ impl MentionUri { MentionUri::File { abs_path } => { Url::from_file_path(abs_path).expect("mention path should be absolute") } + MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(), MentionUri::Directory { abs_path } => { Url::from_directory_path(abs_path).expect("mention path should be absolute") } MentionUri::Symbol { - path, + abs_path, name, line_range, } => { - let mut url = Url::from_file_path(path).expect("mention path should be absolute"); + let mut url = + Url::from_file_path(abs_path).expect("mention path should be absolute"); url.query_pairs_mut().append_pair("symbol", name); url.set_fragment(Some(&format!( "L{}:{}", - line_range.start + 1, - line_range.end + 1 + line_range.start() + 1, + line_range.end() + 1 ))); url } - MentionUri::Selection { path, line_range } => { - let mut url = Url::from_file_path(path).expect("mention path should be absolute"); + MentionUri::Selection { + abs_path: path, + line_range, + } => { + let mut url = if let Some(path) = path { + Url::from_file_path(path).expect("mention path should be absolute") + } else { + let mut url = Url::parse("zed:///").unwrap(); + url.set_path("/agent/untitled-buffer"); + url + }; url.set_fragment(Some(&format!( "L{}:{}", - line_range.start + 1, - line_range.end + 1 + line_range.start() + 1, + line_range.end() + 1 ))); url } @@ -191,7 +227,10 @@ impl MentionUri { } MentionUri::TextThread { path, name } => { let mut url = Url::parse("zed:///").unwrap(); - url.set_path(&format!("/agent/text-thread/{}", path.to_string_lossy())); + url.set_path(&format!( + "/agent/text-thread/{}", + path.to_string_lossy().trim_start_matches('/') + )); url.query_pairs_mut().append_pair("name", name); url } @@ -237,12 +276,14 @@ fn single_query_param(url: &Url, name: &'static str) -> Result> { } } -pub fn selection_name(path: &Path, line_range: &Range) -> String { +pub fn selection_name(path: Option<&Path>, line_range: &RangeInclusive) -> String { format!( "{} ({}:{})", - path.file_name().unwrap_or_default().display(), - line_range.start + 1, - line_range.end + 1 + path.and_then(|path| path.file_name()) + .unwrap_or("Untitled".as_ref()) + .display(), + *line_range.start() + 1, + *line_range.end() + 1 ) } @@ -302,14 +343,14 @@ mod tests { let parsed = MentionUri::parse(symbol_uri).unwrap(); match &parsed { MentionUri::Symbol { - path, + abs_path: path, name, line_range, } => { assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs")); assert_eq!(name, "MySymbol"); - assert_eq!(line_range.start, 9); - assert_eq!(line_range.end, 19); + assert_eq!(line_range.start(), &9); + assert_eq!(line_range.end(), &19); } _ => panic!("Expected Symbol variant"), } @@ -321,16 +362,39 @@ mod tests { let selection_uri = uri!("file:///path/to/file.rs#L5:15"); let parsed = MentionUri::parse(selection_uri).unwrap(); match &parsed { - MentionUri::Selection { path, line_range } => { - assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs")); - assert_eq!(line_range.start, 4); - assert_eq!(line_range.end, 14); + MentionUri::Selection { + abs_path: path, + line_range, + } => { + assert_eq!( + path.as_ref().unwrap().to_str().unwrap(), + path!("/path/to/file.rs") + ); + assert_eq!(line_range.start(), &4); + assert_eq!(line_range.end(), &14); } _ => panic!("Expected Selection variant"), } assert_eq!(parsed.to_uri().to_string(), selection_uri); } + #[test] + fn test_parse_untitled_selection_uri() { + let selection_uri = uri!("zed:///agent/untitled-buffer#L1:10"); + let parsed = MentionUri::parse(selection_uri).unwrap(); + match &parsed { + MentionUri::Selection { + abs_path: None, + line_range, + } => { + assert_eq!(line_range.start(), &0); + assert_eq!(line_range.end(), &9); + } + _ => panic!("Expected Selection variant without path"), + } + assert_eq!(parsed.to_uri().to_string(), selection_uri); + } + #[test] fn test_parse_thread_uri() { let thread_uri = "zed:///agent/thread/session123?name=Thread+name"; diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index 45e551dbdf..cba2457566 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -893,8 +893,19 @@ impl ThreadsDatabase { let needs_migration_from_heed = mdb_path.exists(); - let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) { + let connection = if *ZED_STATELESS { Connection::open_memory(Some("THREAD_FALLBACK_DB")) + } else if cfg!(any(feature = "test-support", test)) { + // rust stores the name of the test on the current thread. + // We use this to automatically create a database that will + // be shared within the test (for the test_retrieve_old_thread) + // but not with concurrent tests. + let thread = std::thread::current(); + let test_name = thread.name(); + Connection::open_memory(Some(&format!( + "THREAD_FALLBACK_{}", + test_name.unwrap_or_default() + ))) } else { Connection::open_file(&sqlite_path.to_string_lossy()) }; diff --git a/crates/agent2/src/db.rs b/crates/agent2/src/db.rs index 1b88955a24..e7d31c0c7a 100644 --- a/crates/agent2/src/db.rs +++ b/crates/agent2/src/db.rs @@ -266,8 +266,19 @@ impl ThreadsDatabase { } pub fn new(executor: BackgroundExecutor) -> Result { - let connection = if *ZED_STATELESS || cfg!(any(feature = "test-support", test)) { + let connection = if *ZED_STATELESS { Connection::open_memory(Some("THREAD_FALLBACK_DB")) + } else if cfg!(any(feature = "test-support", test)) { + // rust stores the name of the test on the current thread. + // We use this to automatically create a database that will + // be shared within the test (for the test_retrieve_old_thread) + // but not with concurrent tests. + let thread = std::thread::current(); + let test_name = thread.name(); + Connection::open_memory(Some(&format!( + "THREAD_FALLBACK_{}", + test_name.unwrap_or_default() + ))) } else { let threads_dir = paths::data_dir().join("threads"); std::fs::create_dir_all(&threads_dir)?; diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index c89e5875f9..6d616f73fc 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -45,14 +45,15 @@ use schemars::{JsonSchema, Schema}; use serde::{Deserialize, Serialize}; use settings::{Settings, update_settings_file}; use smol::stream::StreamExt; +use std::fmt::Write; use std::{ collections::BTreeMap, + ops::RangeInclusive, path::Path, sync::Arc, time::{Duration, Instant}, }; -use std::{fmt::Write, ops::Range}; -use util::{ResultExt, markdown::MarkdownCodeBlock}; +use util::{ResultExt, debug_panic, markdown::MarkdownCodeBlock}; use uuid::Uuid; const TOOL_CANCELED_MESSAGE: &str = "Tool canceled by user"; @@ -187,6 +188,7 @@ impl UserMessage { const OPEN_FILES_TAG: &str = ""; const OPEN_DIRECTORIES_TAG: &str = ""; const OPEN_SYMBOLS_TAG: &str = ""; + const OPEN_SELECTIONS_TAG: &str = ""; const OPEN_THREADS_TAG: &str = ""; const OPEN_FETCH_TAG: &str = ""; const OPEN_RULES_TAG: &str = @@ -195,6 +197,7 @@ impl UserMessage { let mut file_context = OPEN_FILES_TAG.to_string(); let mut directory_context = OPEN_DIRECTORIES_TAG.to_string(); let mut symbol_context = OPEN_SYMBOLS_TAG.to_string(); + let mut selection_context = OPEN_SELECTIONS_TAG.to_string(); let mut thread_context = OPEN_THREADS_TAG.to_string(); let mut fetch_context = OPEN_FETCH_TAG.to_string(); let mut rules_context = OPEN_RULES_TAG.to_string(); @@ -211,7 +214,7 @@ impl UserMessage { match uri { MentionUri::File { abs_path } => { write!( - &mut symbol_context, + &mut file_context, "\n{}", MarkdownCodeBlock { tag: &codeblock_tag(abs_path, None), @@ -220,17 +223,19 @@ impl UserMessage { ) .ok(); } + MentionUri::PastedImage => { + debug_panic!("pasted image URI should not be used in mention content") + } MentionUri::Directory { .. } => { write!(&mut directory_context, "\n{}\n", content).ok(); } MentionUri::Symbol { - path, line_range, .. - } - | MentionUri::Selection { - path, line_range, .. + abs_path: path, + line_range, + .. } => { write!( - &mut rules_context, + &mut symbol_context, "\n{}", MarkdownCodeBlock { tag: &codeblock_tag(path, Some(line_range)), @@ -239,6 +244,24 @@ impl UserMessage { ) .ok(); } + MentionUri::Selection { + abs_path: path, + line_range, + .. + } => { + write!( + &mut selection_context, + "\n{}", + MarkdownCodeBlock { + tag: &codeblock_tag( + path.as_deref().unwrap_or("Untitled".as_ref()), + Some(line_range) + ), + text: content + } + ) + .ok(); + } MentionUri::Thread { .. } => { write!(&mut thread_context, "\n{}\n", content).ok(); } @@ -291,6 +314,13 @@ impl UserMessage { .push(language_model::MessageContent::Text(symbol_context)); } + if selection_context.len() > OPEN_SELECTIONS_TAG.len() { + selection_context.push_str("\n"); + message + .content + .push(language_model::MessageContent::Text(selection_context)); + } + if thread_context.len() > OPEN_THREADS_TAG.len() { thread_context.push_str("\n"); message @@ -326,7 +356,7 @@ impl UserMessage { } } -fn codeblock_tag(full_path: &Path, line_range: Option<&Range>) -> String { +fn codeblock_tag(full_path: &Path, line_range: Option<&RangeInclusive>) -> String { let mut result = String::new(); if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) { @@ -336,10 +366,10 @@ fn codeblock_tag(full_path: &Path, line_range: Option<&Range>) -> String { let _ = write!(result, "{}", full_path.display()); if let Some(range) = line_range { - if range.start == range.end { - let _ = write!(result, ":{}", range.start + 1); + if range.start() == range.end() { + let _ = write!(result, ":{}", range.start() + 1); } else { - let _ = write!(result, ":{}-{}", range.start + 1, range.end + 1); + let _ = write!(result, ":{}-{}", range.start() + 1, range.end() + 1); } } diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 22a9ea6773..5b40967069 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -247,9 +247,9 @@ impl ContextPickerCompletionProvider { let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?; let uri = MentionUri::Symbol { - path: abs_path, + abs_path, name: symbol.name.clone(), - line_range: symbol.range.start.0.row..symbol.range.end.0.row, + line_range: symbol.range.start.0.row..=symbol.range.end.0.row, }; let new_text = format!("{} ", uri.as_link()); let new_text_len = new_text.len(); diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 7d73ebeb19..115008cf52 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -6,7 +6,7 @@ use acp_thread::{MentionUri, selection_name}; use agent_client_protocol as acp; use agent_servers::AgentServer; use agent2::HistoryStore; -use anyhow::{Context as _, Result, anyhow}; +use anyhow::{Result, anyhow}; use assistant_slash_commands::codeblock_fence_for_path; use collections::{HashMap, HashSet}; use editor::{ @@ -17,8 +17,8 @@ use editor::{ display_map::{Crease, CreaseId, FoldId}, }; use futures::{ - FutureExt as _, TryFutureExt as _, - future::{Shared, join_all, try_join_all}, + FutureExt as _, + future::{Shared, join_all}, }; use gpui::{ AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, @@ -28,14 +28,14 @@ use gpui::{ use language::{Buffer, Language}; use language_model::LanguageModelImage; use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree}; -use prompt_store::PromptStore; +use prompt_store::{PromptId, PromptStore}; use rope::Point; use settings::Settings; use std::{ cell::Cell, ffi::OsStr, fmt::Write, - ops::Range, + ops::{Range, RangeInclusive}, path::{Path, PathBuf}, rc::Rc, sync::Arc, @@ -49,12 +49,8 @@ use ui::{ Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div, h_flex, px, }; -use url::Url; -use util::ResultExt; -use workspace::{ - Toast, Workspace, - notifications::{NotificationId, NotifyResultExt as _}, -}; +use util::{ResultExt, debug_panic}; +use workspace::{Workspace, notifications::NotifyResultExt as _}; use zed_actions::agent::Chat; const PARSE_SLASH_COMMAND_DEBOUNCE: Duration = Duration::from_millis(50); @@ -219,9 +215,9 @@ impl MessageEditor { pub fn mentions(&self) -> HashSet { self.mention_set - .uri_by_crease_id + .mentions .values() - .cloned() + .map(|(uri, _)| uri.clone()) .collect() } @@ -246,132 +242,168 @@ impl MessageEditor { else { return Task::ready(()); }; + let end_anchor = snapshot + .buffer_snapshot + .anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1); - if let MentionUri::File { abs_path, .. } = &mention_uri { - let extension = abs_path - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default(); - - if Img::extensions().contains(&extension) && !extension.contains("svg") { - if !self.prompt_capabilities.get().image { - struct ImagesNotAllowed; - - let end_anchor = snapshot.buffer_snapshot.anchor_before( - start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1, - ); - - self.editor.update(cx, |editor, cx| { - // Remove mention - editor.edit([((start_anchor..end_anchor), "")], cx); - }); - - self.workspace - .update(cx, |workspace, cx| { - workspace.show_toast( - Toast::new( - NotificationId::unique::(), - "This agent does not support images yet", - ) - .autohide(), - cx, - ); - }) - .ok(); - return Task::ready(()); - } - - let project = self.project.clone(); - let Some(project_path) = project - .read(cx) - .project_path_for_absolute_path(abs_path, cx) - else { - return Task::ready(()); - }; - let image = cx - .spawn(async move |_, cx| { - let image = project - .update(cx, |project, cx| project.open_image(project_path, cx)) - .map_err(|e| e.to_string())? - .await - .map_err(|e| e.to_string())?; - image - .read_with(cx, |image, _cx| image.image.clone()) - .map_err(|e| e.to_string()) - }) - .shared(); - let Some(crease_id) = insert_crease_for_image( - *excerpt_id, - start, - content_len, - Some(abs_path.as_path().into()), - image.clone(), - self.editor.clone(), - window, - cx, - ) else { - return Task::ready(()); - }; - return self.confirm_mention_for_image( - crease_id, - start_anchor, - Some(abs_path.clone()), - image, - window, - cx, - ); - } - } - - let Some(crease_id) = crate::context_picker::insert_crease_for_mention( - *excerpt_id, - start, - content_len, - crease_text, - mention_uri.icon_path(cx), - self.editor.clone(), - window, - cx, - ) else { + let crease_id = if let MentionUri::File { abs_path } = &mention_uri + && let Some(extension) = abs_path.extension() + && let Some(extension) = extension.to_str() + && Img::extensions().contains(&extension) + && !extension.contains("svg") + { + let Some(project_path) = self + .project + .read(cx) + .project_path_for_absolute_path(&abs_path, cx) + else { + log::error!("project path not found"); + return Task::ready(()); + }; + let image = self + .project + .update(cx, |project, cx| project.open_image(project_path, cx)); + let image = cx + .spawn(async move |_, cx| { + let image = image.await.map_err(|e| e.to_string())?; + let image = image + .update(cx, |image, _| image.image.clone()) + .map_err(|e| e.to_string())?; + Ok(image) + }) + .shared(); + insert_crease_for_image( + *excerpt_id, + start, + content_len, + Some(abs_path.as_path().into()), + image, + self.editor.clone(), + window, + cx, + ) + } else { + crate::context_picker::insert_crease_for_mention( + *excerpt_id, + start, + content_len, + crease_text, + mention_uri.icon_path(cx), + self.editor.clone(), + window, + cx, + ) + }; + let Some(crease_id) = crease_id else { return Task::ready(()); }; - match mention_uri { - MentionUri::Fetch { url } => { - self.confirm_mention_for_fetch(crease_id, start_anchor, url, window, cx) + let task = match mention_uri.clone() { + MentionUri::Fetch { url } => self.confirm_mention_for_fetch(url, cx), + MentionUri::Directory { abs_path } => self.confirm_mention_for_directory(abs_path, cx), + MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx), + MentionUri::TextThread { path, .. } => self.confirm_mention_for_text_thread(path, cx), + MentionUri::File { abs_path } => self.confirm_mention_for_file(abs_path, cx), + MentionUri::Symbol { + abs_path, + line_range, + .. + } => self.confirm_mention_for_symbol(abs_path, line_range, cx), + MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx), + MentionUri::PastedImage => { + debug_panic!("pasted image URI should not be included in completions"); + Task::ready(Err(anyhow!( + "pasted imaged URI should not be included in completions" + ))) } - MentionUri::Directory { abs_path } => { - self.confirm_mention_for_directory(crease_id, start_anchor, abs_path, window, cx) + MentionUri::Selection { .. } => { + // Handled elsewhere + debug_panic!("unexpected selection URI"); + Task::ready(Err(anyhow!("unexpected selection URI"))) } - MentionUri::Thread { id, name } => { - self.confirm_mention_for_thread(crease_id, start_anchor, id, name, window, cx) + }; + let task = cx + .spawn(async move |_, _| task.await.map_err(|e| e.to_string())) + .shared(); + self.mention_set + .mentions + .insert(crease_id, (mention_uri, task.clone())); + + // Notify the user if we failed to load the mentioned context + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_none() { + this.update(cx, |this, cx| { + this.editor.update(cx, |editor, cx| { + // Remove mention + editor.edit([(start_anchor..end_anchor, "")], cx); + }); + this.mention_set.mentions.remove(&crease_id); + }) + .ok(); } - MentionUri::TextThread { path, name } => self.confirm_mention_for_text_thread( - crease_id, - start_anchor, - path, - name, - window, - cx, - ), - MentionUri::File { .. } - | MentionUri::Symbol { .. } - | MentionUri::Rule { .. } - | MentionUri::Selection { .. } => { - self.mention_set.insert_uri(crease_id, mention_uri.clone()); - Task::ready(()) + }) + } + + fn confirm_mention_for_file( + &mut self, + abs_path: PathBuf, + cx: &mut Context, + ) -> Task> { + let Some(project_path) = self + .project + .read(cx) + .project_path_for_absolute_path(&abs_path, cx) + else { + return Task::ready(Err(anyhow!("project path not found"))); + }; + let extension = abs_path + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default(); + + if Img::extensions().contains(&extension) && !extension.contains("svg") { + if !self.prompt_capabilities.get().image { + return Task::ready(Err(anyhow!("This agent does not support images yet"))); } + let task = self + .project + .update(cx, |project, cx| project.open_image(project_path, cx)); + return cx.spawn(async move |_, cx| { + let image = task.await?; + let image = image.update(cx, |image, _| image.image.clone())?; + let format = image.format; + let image = cx + .update(|cx| LanguageModelImage::from_image(image, cx))? + .await; + if let Some(image) = image { + Ok(Mention::Image(MentionImage { + data: image.source, + format, + })) + } else { + Err(anyhow!("Failed to convert image")) + } + }); } + + let buffer = self + .project + .update(cx, |project, cx| project.open_buffer(project_path, cx)); + cx.spawn(async move |_, cx| { + let buffer = buffer.await?; + let mention = buffer.update(cx, |buffer, cx| Mention::Text { + content: buffer.text(), + tracked_buffers: vec![cx.entity()], + })?; + anyhow::Ok(mention) + }) } fn confirm_mention_for_directory( &mut self, - crease_id: CreaseId, - anchor: Anchor, abs_path: PathBuf, - window: &mut Window, cx: &mut Context, - ) -> Task<()> { + ) -> Task> { fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc, PathBuf)> { let mut files = Vec::new(); @@ -386,24 +418,21 @@ impl MessageEditor { files } - let uri = MentionUri::Directory { - abs_path: abs_path.clone(), - }; let Some(project_path) = self .project .read(cx) .project_path_for_absolute_path(&abs_path, cx) else { - return Task::ready(()); + return Task::ready(Err(anyhow!("project path not found"))); }; let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else { - return Task::ready(()); + return Task::ready(Err(anyhow!("project entry not found"))); }; let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else { - return Task::ready(()); + return Task::ready(Err(anyhow!("worktree not found"))); }; let project = self.project.clone(); - let task = cx.spawn(async move |_, cx| { + cx.spawn(async move |_, cx| { let directory_path = entry.path.clone(); let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?; @@ -453,89 +482,83 @@ impl MessageEditor { ((rel_path, full_path, rope), buffer) }) .unzip(); - (render_directory_contents(contents), tracked_buffers) + Mention::Text { + content: render_directory_contents(contents), + tracked_buffers, + } }) .await; anyhow::Ok(contents) - }); - let task = cx - .spawn(async move |_, _| task.await.map_err(|e| e.to_string())) - .shared(); - - self.mention_set - .directories - .insert(abs_path.clone(), task.clone()); - - let editor = self.editor.clone(); - cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_some() { - this.update(cx, |this, _| { - this.mention_set.insert_uri(crease_id, uri); - }) - .ok(); - } else { - editor - .update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - this.update(cx, |this, _cx| { - this.mention_set.directories.remove(&abs_path); - }) - .ok(); - } }) } fn confirm_mention_for_fetch( &mut self, - crease_id: CreaseId, - anchor: Anchor, url: url::Url, - window: &mut Window, cx: &mut Context, - ) -> Task<()> { - let Some(http_client) = self + ) -> Task> { + let http_client = match self .workspace - .update(cx, |workspace, _cx| workspace.client().http_client()) - .ok() - else { - return Task::ready(()); + .update(cx, |workspace, _| workspace.client().http_client()) + { + Ok(http_client) => http_client, + Err(e) => return Task::ready(Err(e)), }; - - let url_string = url.to_string(); - let fetch = cx - .background_executor() - .spawn(async move { - fetch_url_content(http_client, url_string) - .map_err(|e| e.to_string()) - .await + cx.background_executor().spawn(async move { + let content = fetch_url_content(http_client, url.to_string()).await?; + Ok(Mention::Text { + content, + tracked_buffers: Vec::new(), }) - .shared(); - self.mention_set - .add_fetch_result(url.clone(), fetch.clone()); + }) + } - cx.spawn_in(window, async move |this, cx| { - let fetch = fetch.await.notify_async_err(cx); - this.update(cx, |this, cx| { - if fetch.is_some() { - this.mention_set - .insert_uri(crease_id, MentionUri::Fetch { url }); - } else { - // Remove crease if we failed to fetch - this.editor.update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }); - this.mention_set.fetch_results.remove(&url); + fn confirm_mention_for_symbol( + &mut self, + abs_path: PathBuf, + line_range: RangeInclusive, + cx: &mut Context, + ) -> Task> { + let Some(project_path) = self + .project + .read(cx) + .project_path_for_absolute_path(&abs_path, cx) + else { + return Task::ready(Err(anyhow!("project path not found"))); + }; + let buffer = self + .project + .update(cx, |project, cx| project.open_buffer(project_path, cx)); + cx.spawn(async move |_, cx| { + let buffer = buffer.await?; + let mention = buffer.update(cx, |buffer, cx| { + let start = Point::new(*line_range.start(), 0).min(buffer.max_point()); + let end = Point::new(*line_range.end() + 1, 0).min(buffer.max_point()); + let content = buffer.text_for_range(start..end).collect(); + Mention::Text { + content, + tracked_buffers: vec![cx.entity()], } + })?; + anyhow::Ok(mention) + }) + } + + fn confirm_mention_for_rule( + &mut self, + id: PromptId, + cx: &mut Context, + ) -> Task> { + let Some(prompt_store) = self.prompt_store.clone() else { + return Task::ready(Err(anyhow!("missing prompt store"))); + }; + let prompt = prompt_store.read(cx).load(id, cx); + cx.spawn(async move |_, _| { + let prompt = prompt.await?; + Ok(Mention::Text { + content: prompt, + tracked_buffers: Vec::new(), }) - .ok(); }) } @@ -560,24 +583,24 @@ impl MessageEditor { let range = snapshot.anchor_after(offset + range_to_fold.start) ..snapshot.anchor_after(offset + range_to_fold.end); - // TODO support selections from buffers with no path - let Some(project_path) = buffer.read(cx).project_path(cx) else { - continue; - }; - let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else { - continue; - }; + let abs_path = buffer + .read(cx) + .project_path(cx) + .and_then(|project_path| self.project.read(cx).absolute_path(&project_path, cx)); let snapshot = buffer.read(cx).snapshot(); + let text = snapshot + .text_for_range(selection_range.clone()) + .collect::(); let point_range = selection_range.to_point(&snapshot); - let line_range = point_range.start.row..point_range.end.row; + let line_range = point_range.start.row..=point_range.end.row; let uri = MentionUri::Selection { - path: abs_path.clone(), + abs_path: abs_path.clone(), line_range: line_range.clone(), }; let crease = crate::context_picker::crease_for_mention( - selection_name(&abs_path, &line_range).into(), + selection_name(abs_path.as_deref(), &line_range).into(), uri.icon_path(cx), range, self.editor.downgrade(), @@ -589,132 +612,69 @@ impl MessageEditor { crease_ids.first().copied().unwrap() }); - self.mention_set.insert_uri(crease_id, uri); + self.mention_set.mentions.insert( + crease_id, + ( + uri, + Task::ready(Ok(Mention::Text { + content: text, + tracked_buffers: vec![buffer], + })) + .shared(), + ), + ); } } fn confirm_mention_for_thread( &mut self, - crease_id: CreaseId, - anchor: Anchor, id: acp::SessionId, - name: String, - window: &mut Window, cx: &mut Context, - ) -> Task<()> { - let uri = MentionUri::Thread { - id: id.clone(), - name, - }; + ) -> Task> { let server = Rc::new(agent2::NativeAgentServer::new( self.project.read(cx).fs().clone(), self.history_store.clone(), )); let connection = server.connect(Path::new(""), &self.project, cx); - let load_summary = cx.spawn({ - let id = id.clone(); - async move |_, cx| { - let agent = connection.await?; - let agent = agent.downcast::().unwrap(); - let summary = agent - .0 - .update(cx, |agent, cx| agent.thread_summary(id, cx))? - .await?; - anyhow::Ok(summary) - } - }); - let task = cx - .spawn(async move |_, _| load_summary.await.map_err(|e| format!("{e}"))) - .shared(); - - self.mention_set.insert_thread(id.clone(), task.clone()); - self.mention_set.insert_uri(crease_id, uri); - - let editor = self.editor.clone(); - cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_none() { - editor - .update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - this.update(cx, |this, _| { - this.mention_set.thread_summaries.remove(&id); - this.mention_set.uri_by_crease_id.remove(&crease_id); - }) - .ok(); - } + cx.spawn(async move |_, cx| { + let agent = connection.await?; + let agent = agent.downcast::().unwrap(); + let summary = agent + .0 + .update(cx, |agent, cx| agent.thread_summary(id, cx))? + .await?; + anyhow::Ok(Mention::Text { + content: summary.to_string(), + tracked_buffers: Vec::new(), + }) }) } fn confirm_mention_for_text_thread( &mut self, - crease_id: CreaseId, - anchor: Anchor, path: PathBuf, - name: String, - window: &mut Window, cx: &mut Context, - ) -> Task<()> { - let uri = MentionUri::TextThread { - path: path.clone(), - name, - }; + ) -> Task> { let context = self.history_store.update(cx, |text_thread_store, cx| { text_thread_store.load_text_thread(path.as_path().into(), cx) }); - let task = cx - .spawn(async move |_, cx| { - let context = context.await.map_err(|e| e.to_string())?; - let xml = context - .update(cx, |context, cx| context.to_xml(cx)) - .map_err(|e| e.to_string())?; - Ok(xml) + cx.spawn(async move |_, cx| { + let context = context.await?; + let xml = context.update(cx, |context, cx| context.to_xml(cx))?; + Ok(Mention::Text { + content: xml, + tracked_buffers: Vec::new(), }) - .shared(); - - self.mention_set - .insert_text_thread(path.clone(), task.clone()); - - let editor = self.editor.clone(); - cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_some() { - this.update(cx, |this, _| { - this.mention_set.insert_uri(crease_id, uri); - }) - .ok(); - } else { - editor - .update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - this.update(cx, |this, _| { - this.mention_set.text_thread_summaries.remove(&path); - }) - .ok(); - } }) } pub fn contents( &self, - window: &mut Window, cx: &mut Context, ) -> Task, Vec>)>> { - let contents = self.mention_set.contents( - &self.project, - self.prompt_store.as_ref(), - &self.prompt_capabilities.get(), - window, - cx, - ); + let contents = self + .mention_set + .contents(&self.prompt_capabilities.get(), cx); let editor = self.editor.clone(); let prevent_slash_commands = self.prevent_slash_commands; @@ -729,7 +689,7 @@ impl MessageEditor { editor.display_map.update(cx, |map, cx| { let snapshot = map.snapshot(cx); for (crease_id, crease) in snapshot.crease_snapshot.creases() { - let Some(mention) = contents.get(&crease_id) else { + let Some((uri, mention)) = contents.get(&crease_id) else { continue; }; @@ -747,7 +707,6 @@ impl MessageEditor { } let chunk = match mention { Mention::Text { - uri, content, tracked_buffers, } => { @@ -764,17 +723,25 @@ impl MessageEditor { }) } Mention::Image(mention_image) => { + let uri = match uri { + MentionUri::File { .. } => Some(uri.to_uri().to_string()), + MentionUri::PastedImage => None, + other => { + debug_panic!( + "unexpected mention uri for image: {:?}", + other + ); + None + } + }; acp::ContentBlock::Image(acp::ImageContent { annotations: None, data: mention_image.data.to_string(), mime_type: mention_image.format.mime_type().into(), - uri: mention_image - .abs_path - .as_ref() - .map(|path| format!("file://{}", path.display())), + uri, }) } - Mention::UriOnly(uri) => { + Mention::UriOnly => { acp::ContentBlock::ResourceLink(acp::ResourceLink { name: uri.name(), uri: uri.to_uri().to_string(), @@ -813,7 +780,13 @@ impl MessageEditor { pub fn clear(&mut self, window: &mut Window, cx: &mut Context) { self.editor.update(cx, |editor, cx| { editor.clear(window, cx); - editor.remove_creases(self.mention_set.drain(), cx) + editor.remove_creases( + self.mention_set + .mentions + .drain() + .map(|(crease_id, _)| crease_id), + cx, + ) }); } @@ -853,7 +826,7 @@ impl MessageEditor { } cx.stop_propagation(); - let replacement_text = "image"; + let replacement_text = MentionUri::PastedImage.as_link().to_string(); for image in images { let (excerpt_id, text_anchor, multibuffer_anchor) = self.editor.update(cx, |message_editor, cx| { @@ -876,24 +849,62 @@ impl MessageEditor { }); let content_len = replacement_text.len(); - let Some(anchor) = multibuffer_anchor else { - return; + let Some(start_anchor) = multibuffer_anchor else { + continue; }; - let task = Task::ready(Ok(Arc::new(image))).shared(); + let end_anchor = self.editor.update(cx, |editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len) + }); + let image = Arc::new(image); let Some(crease_id) = insert_crease_for_image( excerpt_id, text_anchor, content_len, None.clone(), - task.clone(), + Task::ready(Ok(image.clone())).shared(), self.editor.clone(), window, cx, ) else { - return; + continue; }; - self.confirm_mention_for_image(crease_id, anchor, None, task, window, cx) - .detach(); + let task = cx + .spawn_in(window, { + async move |_, cx| { + let format = image.format; + let image = cx + .update(|_, cx| LanguageModelImage::from_image(image, cx)) + .map_err(|e| e.to_string())? + .await; + if let Some(image) = image { + Ok(Mention::Image(MentionImage { + data: image.source, + format, + })) + } else { + Err("Failed to convert image".into()) + } + } + }) + .shared(); + + self.mention_set + .mentions + .insert(crease_id, (MentionUri::PastedImage, task.clone())); + + cx.spawn_in(window, async move |this, cx| { + if task.await.notify_async_err(cx).is_none() { + this.update(cx, |this, cx| { + this.editor.update(cx, |editor, cx| { + editor.edit([(start_anchor..end_anchor, "")], cx); + }); + this.mention_set.mentions.remove(&crease_id); + }) + .ok(); + } + }) + .detach(); } } @@ -995,67 +1006,6 @@ impl MessageEditor { }) } - fn confirm_mention_for_image( - &mut self, - crease_id: CreaseId, - anchor: Anchor, - abs_path: Option, - image: Shared, String>>>, - window: &mut Window, - cx: &mut Context, - ) -> Task<()> { - let editor = self.editor.clone(); - let task = cx - .spawn_in(window, { - let abs_path = abs_path.clone(); - async move |_, cx| { - let image = image.await?; - let format = image.format; - let image = cx - .update(|_, cx| LanguageModelImage::from_image(image, cx)) - .map_err(|e| e.to_string())? - .await; - if let Some(image) = image { - Ok(MentionImage { - abs_path, - data: image.source, - format, - }) - } else { - Err("Failed to convert image".into()) - } - } - }) - .shared(); - - self.mention_set.insert_image(crease_id, task.clone()); - - cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_some() { - if let Some(abs_path) = abs_path.clone() { - this.update(cx, |this, _cx| { - this.mention_set - .insert_uri(crease_id, MentionUri::File { abs_path }); - }) - .ok(); - } - } else { - editor - .update(cx, |editor, cx| { - editor.display_map.update(cx, |display_map, cx| { - display_map.unfold_intersecting(vec![anchor..anchor], true, cx); - }); - editor.remove_creases([crease_id], cx); - }) - .ok(); - this.update(cx, |this, _cx| { - this.mention_set.images.remove(&crease_id); - }) - .ok(); - } - }) - } - pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context) { self.editor.update(cx, |editor, cx| { editor.set_mode(mode); @@ -1073,7 +1023,6 @@ impl MessageEditor { let mut text = String::new(); let mut mentions = Vec::new(); - let mut images = Vec::new(); for chunk in message { match chunk { @@ -1084,26 +1033,58 @@ impl MessageEditor { resource: acp::EmbeddedResourceResource::TextResourceContents(resource), .. }) => { - if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() { - let start = text.len(); - write!(&mut text, "{}", mention_uri.as_link()).ok(); - let end = text.len(); - mentions.push((start..end, mention_uri, resource.text)); - } + let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() else { + continue; + }; + let start = text.len(); + write!(&mut text, "{}", mention_uri.as_link()).ok(); + let end = text.len(); + mentions.push(( + start..end, + mention_uri, + Mention::Text { + content: resource.text, + tracked_buffers: Vec::new(), + }, + )); } acp::ContentBlock::ResourceLink(resource) => { if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() { let start = text.len(); write!(&mut text, "{}", mention_uri.as_link()).ok(); let end = text.len(); - mentions.push((start..end, mention_uri, resource.uri)); + mentions.push((start..end, mention_uri, Mention::UriOnly)); } } - acp::ContentBlock::Image(content) => { + acp::ContentBlock::Image(acp::ImageContent { + uri, + data, + mime_type, + annotations: _, + }) => { + let mention_uri = if let Some(uri) = uri { + MentionUri::parse(&uri) + } else { + Ok(MentionUri::PastedImage) + }; + let Some(mention_uri) = mention_uri.log_err() else { + continue; + }; + let Some(format) = ImageFormat::from_mime_type(&mime_type) else { + log::error!("failed to parse MIME type for image: {mime_type:?}"); + continue; + }; let start = text.len(); - text.push_str("image"); + write!(&mut text, "{}", mention_uri.as_link()).ok(); let end = text.len(); - images.push((start..end, content)); + mentions.push(( + start..end, + mention_uri, + Mention::Image(MentionImage { + data: data.into(), + format, + }), + )); } acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => {} } @@ -1114,9 +1095,9 @@ impl MessageEditor { editor.buffer().read(cx).snapshot(cx) }); - for (range, mention_uri, text) in mentions { + for (range, mention_uri, mention) in mentions { let anchor = snapshot.anchor_before(range.start); - let crease_id = crate::context_picker::insert_crease_for_mention( + let Some(crease_id) = crate::context_picker::insert_crease_for_mention( anchor.excerpt_id, anchor.text_anchor, range.end - range.start, @@ -1125,77 +1106,14 @@ impl MessageEditor { self.editor.clone(), window, cx, - ); - - if let Some(crease_id) = crease_id { - self.mention_set.insert_uri(crease_id, mention_uri.clone()); - } - - match mention_uri { - MentionUri::Thread { id, .. } => { - self.mention_set - .insert_thread(id, Task::ready(Ok(text.into())).shared()); - } - MentionUri::TextThread { path, .. } => { - self.mention_set - .insert_text_thread(path, Task::ready(Ok(text)).shared()); - } - MentionUri::Fetch { url } => { - self.mention_set - .add_fetch_result(url, Task::ready(Ok(text)).shared()); - } - MentionUri::Directory { abs_path } => { - let task = Task::ready(Ok((text, Vec::new()))).shared(); - self.mention_set.directories.insert(abs_path, task); - } - MentionUri::File { .. } - | MentionUri::Symbol { .. } - | MentionUri::Rule { .. } - | MentionUri::Selection { .. } => {} - } - } - for (range, content) in images { - let Some(format) = ImageFormat::from_mime_type(&content.mime_type) else { + ) else { continue; }; - let anchor = snapshot.anchor_before(range.start); - let abs_path = content - .uri - .as_ref() - .and_then(|uri| uri.strip_prefix("file://").map(|s| Path::new(s).into())); - let name = content - .uri - .as_ref() - .and_then(|uri| { - uri.strip_prefix("file://") - .and_then(|path| Path::new(path).file_name()) - }) - .map(|name| name.to_string_lossy().to_string()) - .unwrap_or("Image".to_owned()); - let crease_id = crate::context_picker::insert_crease_for_mention( - anchor.excerpt_id, - anchor.text_anchor, - range.end - range.start, - name.into(), - IconName::Image.path().into(), - self.editor.clone(), - window, - cx, + self.mention_set.mentions.insert( + crease_id, + (mention_uri.clone(), Task::ready(Ok(mention)).shared()), ); - let data: SharedString = content.data.to_string().into(); - - if let Some(crease_id) = crease_id { - self.mention_set.insert_image( - crease_id, - Task::ready(Ok(MentionImage { - abs_path, - data, - format, - })) - .shared(), - ); - } } cx.notify(); } @@ -1425,289 +1343,60 @@ impl Render for ImageHover { } } -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] pub enum Mention { Text { - uri: MentionUri, content: String, tracked_buffers: Vec>, }, Image(MentionImage), - UriOnly(MentionUri), + UriOnly, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct MentionImage { - pub abs_path: Option, pub data: SharedString, pub format: ImageFormat, } #[derive(Default)] pub struct MentionSet { - uri_by_crease_id: HashMap, - fetch_results: HashMap>>>, - images: HashMap>>>, - thread_summaries: HashMap>>>, - text_thread_summaries: HashMap>>>, - directories: HashMap>), String>>>>, + mentions: HashMap>>)>, } impl MentionSet { - pub fn insert_uri(&mut self, crease_id: CreaseId, uri: MentionUri) { - self.uri_by_crease_id.insert(crease_id, uri); - } - - pub fn add_fetch_result(&mut self, url: Url, content: Shared>>) { - self.fetch_results.insert(url, content); - } - - pub fn insert_image( - &mut self, - crease_id: CreaseId, - task: Shared>>, - ) { - self.images.insert(crease_id, task); - } - - fn insert_thread( - &mut self, - id: acp::SessionId, - task: Shared>>, - ) { - self.thread_summaries.insert(id, task); - } - - fn insert_text_thread(&mut self, path: PathBuf, task: Shared>>) { - self.text_thread_summaries.insert(path, task); - } - - pub fn contents( + fn contents( &self, - project: &Entity, - prompt_store: Option<&Entity>, prompt_capabilities: &acp::PromptCapabilities, - _window: &mut Window, cx: &mut App, - ) -> Task>> { + ) -> Task>> { if !prompt_capabilities.embedded_context { let mentions = self - .uri_by_crease_id + .mentions .iter() - .map(|(crease_id, uri)| (*crease_id, Mention::UriOnly(uri.clone()))) + .map(|(crease_id, (uri, _))| (*crease_id, (uri.clone(), Mention::UriOnly))) .collect(); return Task::ready(Ok(mentions)); } - let mut processed_image_creases = HashSet::default(); - - let mut contents = self - .uri_by_crease_id - .iter() - .map(|(&crease_id, uri)| { - match uri { - MentionUri::File { abs_path, .. } => { - let uri = uri.clone(); - let abs_path = abs_path.to_path_buf(); - - if let Some(task) = self.images.get(&crease_id).cloned() { - processed_image_creases.insert(crease_id); - return cx.spawn(async move |_| { - let image = task.await.map_err(|e| anyhow!("{e}"))?; - anyhow::Ok((crease_id, Mention::Image(image))) - }); - } - - let buffer_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(abs_path, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_buffer(path, cx)) - }); - cx.spawn(async move |cx| { - let buffer = buffer_task?.await?; - let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; - - anyhow::Ok(( - crease_id, - Mention::Text { - uri, - content, - tracked_buffers: vec![buffer], - }, - )) - }) - } - MentionUri::Directory { abs_path } => { - let Some(content) = self.directories.get(abs_path).cloned() else { - return Task::ready(Err(anyhow!("missing directory load task"))); - }; - let uri = uri.clone(); - cx.spawn(async move |_| { - let (content, tracked_buffers) = - content.await.map_err(|e| anyhow::anyhow!("{e}"))?; - Ok(( - crease_id, - Mention::Text { - uri, - content, - tracked_buffers, - }, - )) - }) - } - MentionUri::Symbol { - path, line_range, .. - } - | MentionUri::Selection { - path, line_range, .. - } => { - let uri = uri.clone(); - let path_buf = path.clone(); - let line_range = line_range.clone(); - - let buffer_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(&path_buf, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_buffer(path, cx)) - }); - - cx.spawn(async move |cx| { - let buffer = buffer_task?.await?; - let content = buffer.read_with(cx, |buffer, _cx| { - buffer - .text_for_range( - Point::new(line_range.start, 0) - ..Point::new( - line_range.end, - buffer.line_len(line_range.end), - ), - ) - .collect() - })?; - - anyhow::Ok(( - crease_id, - Mention::Text { - uri, - content, - tracked_buffers: vec![buffer], - }, - )) - }) - } - MentionUri::Thread { id, .. } => { - let Some(content) = self.thread_summaries.get(id).cloned() else { - return Task::ready(Err(anyhow!("missing thread summary"))); - }; - let uri = uri.clone(); - cx.spawn(async move |_| { - Ok(( - crease_id, - Mention::Text { - uri, - content: content - .await - .map_err(|e| anyhow::anyhow!("{e}"))? - .to_string(), - tracked_buffers: Vec::new(), - }, - )) - }) - } - MentionUri::TextThread { path, .. } => { - let Some(content) = self.text_thread_summaries.get(path).cloned() else { - return Task::ready(Err(anyhow!("missing text thread summary"))); - }; - let uri = uri.clone(); - cx.spawn(async move |_| { - Ok(( - crease_id, - Mention::Text { - uri, - content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, - tracked_buffers: Vec::new(), - }, - )) - }) - } - MentionUri::Rule { id: prompt_id, .. } => { - let Some(prompt_store) = prompt_store else { - return Task::ready(Err(anyhow!("missing prompt store"))); - }; - let text_task = prompt_store.read(cx).load(*prompt_id, cx); - let uri = uri.clone(); - cx.spawn(async move |_| { - // TODO: report load errors instead of just logging - let text = text_task.await?; - anyhow::Ok(( - crease_id, - Mention::Text { - uri, - content: text, - tracked_buffers: Vec::new(), - }, - )) - }) - } - MentionUri::Fetch { url } => { - let Some(content) = self.fetch_results.get(url).cloned() else { - return Task::ready(Err(anyhow!("missing fetch result"))); - }; - let uri = uri.clone(); - cx.spawn(async move |_| { - Ok(( - crease_id, - Mention::Text { - uri, - content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, - tracked_buffers: Vec::new(), - }, - )) - }) - } - } - }) - .collect::>(); - - // Handle images that didn't have a mention URI (because they were added by the paste handler). - contents.extend(self.images.iter().filter_map(|(crease_id, image)| { - if processed_image_creases.contains(crease_id) { - return None; - } - let crease_id = *crease_id; - let image = image.clone(); - Some(cx.spawn(async move |_| { - Ok(( - crease_id, - Mention::Image(image.await.map_err(|e| anyhow::anyhow!("{e}"))?), - )) - })) - })); - + let mentions = self.mentions.clone(); cx.spawn(async move |_cx| { - let contents = try_join_all(contents).await?.into_iter().collect(); - anyhow::Ok(contents) + let mut contents = HashMap::default(); + for (crease_id, (mention_uri, task)) in mentions { + contents.insert( + crease_id, + (mention_uri, task.await.map_err(|e| anyhow!("{e}"))?), + ); + } + Ok(contents) }) } - pub fn drain(&mut self) -> impl Iterator { - self.fetch_results.clear(); - self.thread_summaries.clear(); - self.text_thread_summaries.clear(); - self.directories.clear(); - self.uri_by_crease_id - .drain() - .map(|(id, _)| id) - .chain(self.images.drain().map(|(id, _)| id)) - } - - pub fn remove_invalid(&mut self, snapshot: EditorSnapshot) { + fn remove_invalid(&mut self, snapshot: EditorSnapshot) { for (crease_id, crease) in snapshot.crease_snapshot.creases() { if !crease.range().start.is_valid(&snapshot.buffer_snapshot) { - self.uri_by_crease_id.remove(&crease_id); + self.mentions.remove(&crease_id); } } } @@ -1969,9 +1658,7 @@ mod tests { }); let (content, _) = message_editor - .update_in(cx, |message_editor, window, cx| { - message_editor.contents(window, cx) - }) + .update(cx, |message_editor, cx| message_editor.contents(cx)) .await .unwrap(); @@ -2038,7 +1725,8 @@ mod tests { "six.txt": "6", "seven.txt": "7", "eight.txt": "8", - } + }, + "x.png": "", }), ) .await; @@ -2222,14 +1910,10 @@ mod tests { }; let contents = message_editor - .update_in(&mut cx, |message_editor, window, cx| { - message_editor.mention_set().contents( - &project, - None, - &all_prompt_capabilities, - window, - cx, - ) + .update(&mut cx, |message_editor, cx| { + message_editor + .mention_set() + .contents(&all_prompt_capabilities, cx) }) .await .unwrap() @@ -2237,7 +1921,7 @@ mod tests { .collect::>(); { - let [Mention::Text { content, uri, .. }] = contents.as_slice() else { + let [(uri, Mention::Text { content, .. })] = contents.as_slice() else { panic!("Unexpected mentions"); }; pretty_assertions::assert_eq!(content, "1"); @@ -2245,14 +1929,10 @@ mod tests { } let contents = message_editor - .update_in(&mut cx, |message_editor, window, cx| { - message_editor.mention_set().contents( - &project, - None, - &acp::PromptCapabilities::default(), - window, - cx, - ) + .update(&mut cx, |message_editor, cx| { + message_editor + .mention_set() + .contents(&acp::PromptCapabilities::default(), cx) }) .await .unwrap() @@ -2260,7 +1940,7 @@ mod tests { .collect::>(); { - let [Mention::UriOnly(uri)] = contents.as_slice() else { + let [(uri, Mention::UriOnly)] = contents.as_slice() else { panic!("Unexpected mentions"); }; pretty_assertions::assert_eq!(uri, &url_one.parse::().unwrap()); @@ -2300,14 +1980,10 @@ mod tests { cx.run_until_parked(); let contents = message_editor - .update_in(&mut cx, |message_editor, window, cx| { - message_editor.mention_set().contents( - &project, - None, - &all_prompt_capabilities, - window, - cx, - ) + .update(&mut cx, |message_editor, cx| { + message_editor + .mention_set() + .contents(&all_prompt_capabilities, cx) }) .await .unwrap() @@ -2317,7 +1993,7 @@ mod tests { let url_eight = uri!("file:///dir/b/eight.txt"); { - let [_, Mention::Text { content, uri, .. }] = contents.as_slice() else { + let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else { panic!("Unexpected mentions"); }; pretty_assertions::assert_eq!(content, "8"); @@ -2414,14 +2090,10 @@ mod tests { }); let contents = message_editor - .update_in(&mut cx, |message_editor, window, cx| { - message_editor.mention_set().contents( - &project, - None, - &all_prompt_capabilities, - window, - cx, - ) + .update(&mut cx, |message_editor, cx| { + message_editor + .mention_set() + .contents(&all_prompt_capabilities, cx) }) .await .unwrap() @@ -2429,7 +2101,7 @@ mod tests { .collect::>(); { - let [_, _, Mention::Text { content, uri, .. }] = contents.as_slice() else { + let [_, _, (uri, Mention::Text { content, .. })] = contents.as_slice() else { panic!("Unexpected mentions"); }; pretty_assertions::assert_eq!(content, "1"); @@ -2444,11 +2116,85 @@ mod tests { cx.run_until_parked(); editor.read_with(&cx, |editor, cx| { - assert_eq!( - editor.text(cx), - format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") - ); - }); + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") + ); + }); + + // Try to mention an "image" file that will fail to load + cx.simulate_input("@file x.png"); + + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png") + ); + assert!(editor.has_visible_completions_menu()); + assert_eq!(current_completion_labels(editor), &["x.png dir/"]); + }); + + editor.update_in(&mut cx, |editor, window, cx| { + editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); + }); + + // Getting the message contents fails + message_editor + .update(&mut cx, |message_editor, cx| { + message_editor + .mention_set() + .contents(&all_prompt_capabilities, cx) + }) + .await + .expect_err("Should fail to load x.png"); + + cx.run_until_parked(); + + // Mention was removed + editor.read_with(&cx, |editor, cx| { + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") + ); + }); + + // Once more + cx.simulate_input("@file x.png"); + + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png") + ); + assert!(editor.has_visible_completions_menu()); + assert_eq!(current_completion_labels(editor), &["x.png dir/"]); + }); + + editor.update_in(&mut cx, |editor, window, cx| { + editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); + }); + + // This time don't immediately get the contents, just let the confirmed completion settle + cx.run_until_parked(); + + // Mention was removed + editor.read_with(&cx, |editor, cx| { + assert_eq!( + editor.text(cx), + format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ") + ); + }); + + // Now getting the contents succeeds, because the invalid mention was removed + let contents = message_editor + .update(&mut cx, |message_editor, cx| { + message_editor + .mention_set() + .contents(&all_prompt_capabilities, cx) + }) + .await + .unwrap(); + assert_eq!(contents.len(), 3); } fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec> { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 0e1d4123b9..3ad1234e22 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -274,6 +274,7 @@ pub struct AcpThreadView { edits_expanded: bool, plan_expanded: bool, editor_expanded: bool, + terminal_expanded: bool, editing_message: Option, prompt_capabilities: Rc>, _cancel_task: Option>, @@ -384,6 +385,7 @@ impl AcpThreadView { edits_expanded: false, plan_expanded: false, editor_expanded: false, + terminal_expanded: true, history_store, hovered_recent_history_item: None, prompt_capabilities, @@ -835,7 +837,7 @@ impl AcpThreadView { let contents = self .message_editor - .update(cx, |message_editor, cx| message_editor.contents(window, cx)); + .update(cx, |message_editor, cx| message_editor.contents(cx)); self.send_impl(contents, window, cx) } @@ -848,7 +850,7 @@ impl AcpThreadView { let contents = self .message_editor - .update(cx, |message_editor, cx| message_editor.contents(window, cx)); + .update(cx, |message_editor, cx| message_editor.contents(cx)); cx.spawn_in(window, async move |this, cx| { cancelled.await; @@ -956,8 +958,7 @@ impl AcpThreadView { return; }; - let contents = - message_editor.update(cx, |message_editor, cx| message_editor.contents(window, cx)); + let contents = message_editor.update(cx, |message_editor, cx| message_editor.contents(cx)); let task = cx.foreground_executor().spawn(async move { rewind.await?; @@ -1690,9 +1691,10 @@ impl AcpThreadView { matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some(); let use_card_layout = needs_confirmation || is_edit; - let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation; + let is_collapsible = !tool_call.content.is_empty() && !use_card_layout; - let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id); + let is_open = + needs_confirmation || is_edit || self.expanded_tool_calls.contains(&tool_call.id); let gradient_overlay = |color: Hsla| { div() @@ -2162,8 +2164,6 @@ impl AcpThreadView { .map(|path| format!("{}", path.display())) .unwrap_or_else(|| "current directory".to_string()); - let is_expanded = self.expanded_tool_calls.contains(&tool_call.id); - let header = h_flex() .id(SharedString::from(format!( "terminal-tool-header-{}", @@ -2297,19 +2297,12 @@ impl AcpThreadView { "terminal-tool-disclosure-{}", terminal.entity_id() )), - is_expanded, + self.terminal_expanded, ) .opened_icon(IconName::ChevronUp) .closed_icon(IconName::ChevronDown) - .on_click(cx.listener({ - let id = tool_call.id.clone(); - move |this, _event, _window, _cx| { - if is_expanded { - this.expanded_tool_calls.remove(&id); - } else { - this.expanded_tool_calls.insert(id.clone()); - } - } + .on_click(cx.listener(move |this, _event, _window, _cx| { + this.terminal_expanded = !this.terminal_expanded; })), ); @@ -2318,7 +2311,7 @@ impl AcpThreadView { .read(cx) .entry(entry_ix) .and_then(|entry| entry.terminal(terminal)); - let show_output = is_expanded && terminal_view.is_some(); + let show_output = self.terminal_expanded && terminal_view.is_some(); v_flex() .mb_2() @@ -3655,6 +3648,7 @@ impl AcpThreadView { .open_path(path, None, true, window, cx) .detach_and_log_err(cx); } + MentionUri::PastedImage => {} MentionUri::Directory { abs_path } => { let project = workspace.project(); let Some(entry) = project.update(cx, |project, cx| { @@ -3669,9 +3663,14 @@ impl AcpThreadView { }); } MentionUri::Symbol { - path, line_range, .. + abs_path: path, + line_range, + .. } - | MentionUri::Selection { path, line_range } => { + | MentionUri::Selection { + abs_path: Some(path), + line_range, + } => { let project = workspace.project(); let Some((path, _)) = project.update(cx, |project, cx| { let path = project.find_project_path(path, cx)?; @@ -3687,8 +3686,8 @@ impl AcpThreadView { let Some(editor) = item.await?.downcast::() else { return Ok(()); }; - let range = - Point::new(line_range.start, 0)..Point::new(line_range.start, 0); + let range = Point::new(*line_range.start(), 0) + ..Point::new(*line_range.start(), 0); editor .update_in(cx, |editor, window, cx| { editor.change_selections( @@ -3703,6 +3702,7 @@ impl AcpThreadView { }) .detach_and_log_err(cx); } + MentionUri::Selection { abs_path: None, .. } => {} MentionUri::Thread { id, name } => { if let Some(panel) = workspace.panel::(cx) { panel.update(cx, |panel, cx| { From 70575d1115133988df19d3d2d6c8cc1f35a19a6b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 23 Aug 2025 10:03:36 +0300 Subject: [PATCH 636/693] Remove redundant Cargo diagnostics settings (#36795) Removes `diagnostics.cargo.fetch_cargo_diagnostics` settings as those are not needed for the flycheck diagnostics to run. This setting disabled `checkOnSave` in rust-analyzer and allowed to update diagnostics via flycheck in the project diagnostics editor with the "refresh" button. Instead, `"checkOnSave": false,` can be set manually as https://zed.dev/docs/languages/rust#more-server-configuration example shows and flycheck commands can be called manually from anywhere, including the diagnostics panel, to refresh the diagnostics. Release Notes: - Removed redundant `diagnostics.cargo.fetch_cargo_diagnostics` settings --- Cargo.lock | 1 - assets/settings/default.json | 5 - crates/diagnostics/Cargo.toml | 1 - crates/diagnostics/src/diagnostics.rs | 127 +-------------------- crates/diagnostics/src/toolbar_controls.rs | 38 ++---- crates/languages/src/rust.rs | 14 --- crates/project/src/project_settings.rs | 22 ---- docs/src/languages/rust.md | 17 +-- 8 files changed, 14 insertions(+), 211 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aa3a910390..6964ed4890 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4685,7 +4685,6 @@ dependencies = [ "component", "ctor", "editor", - "futures 0.3.31", "gpui", "indoc", "language", diff --git a/assets/settings/default.json b/assets/settings/default.json index 014b483250..ac26952c7f 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1133,11 +1133,6 @@ // The minimum severity of the diagnostics to show inline. // Inherits editor's diagnostics' max severity settings when `null`. "max_severity": null - }, - "cargo": { - // When enabled, Zed disables rust-analyzer's check on save and starts to query - // Cargo diagnostics separately. - "fetch_cargo_diagnostics": false } }, // Files or globs of files that will be excluded by Zed entirely. They will be skipped during file diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index 53b5792e10..fd678078e8 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -18,7 +18,6 @@ collections.workspace = true component.workspace = true ctor.workspace = true editor.workspace = true -futures.workspace = true gpui.workspace = true indoc.workspace = true language.workspace = true diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 037e4fc0fd..1c27e820a0 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -13,7 +13,6 @@ use editor::{ DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey, display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId}, }; -use futures::future::join_all; use gpui::{ AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, @@ -24,7 +23,6 @@ use language::{ }; use project::{ DiagnosticSummary, Project, ProjectPath, - lsp_store::rust_analyzer_ext::{cancel_flycheck, run_flycheck}, project_settings::{DiagnosticSeverity, ProjectSettings}, }; use settings::Settings; @@ -79,17 +77,10 @@ pub(crate) struct ProjectDiagnosticsEditor { paths_to_update: BTreeSet, include_warnings: bool, update_excerpts_task: Option>>, - cargo_diagnostics_fetch: CargoDiagnosticsFetchState, diagnostic_summary_update: Task<()>, _subscription: Subscription, } -struct CargoDiagnosticsFetchState { - fetch_task: Option>, - cancel_task: Option>, - diagnostic_sources: Arc>, -} - impl EventEmitter for ProjectDiagnosticsEditor {} const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50); @@ -260,11 +251,7 @@ impl ProjectDiagnosticsEditor { ) }); this.diagnostics.clear(); - this.update_all_diagnostics(false, window, cx); - }) - .detach(); - cx.observe_release(&cx.entity(), |editor, _, cx| { - editor.stop_cargo_diagnostics_fetch(cx); + this.update_all_excerpts(window, cx); }) .detach(); @@ -281,15 +268,10 @@ impl ProjectDiagnosticsEditor { editor, paths_to_update: Default::default(), update_excerpts_task: None, - cargo_diagnostics_fetch: CargoDiagnosticsFetchState { - fetch_task: None, - cancel_task: None, - diagnostic_sources: Arc::new(Vec::new()), - }, diagnostic_summary_update: Task::ready(()), _subscription: project_event_subscription, }; - this.update_all_diagnostics(true, window, cx); + this.update_all_excerpts(window, cx); this } @@ -373,20 +355,10 @@ impl ProjectDiagnosticsEditor { window: &mut Window, cx: &mut Context, ) { - let fetch_cargo_diagnostics = ProjectSettings::get_global(cx) - .diagnostics - .fetch_cargo_diagnostics(); - - if fetch_cargo_diagnostics { - if self.cargo_diagnostics_fetch.fetch_task.is_some() { - self.stop_cargo_diagnostics_fetch(cx); - } else { - self.update_all_diagnostics(false, window, cx); - } - } else if self.update_excerpts_task.is_some() { + if self.update_excerpts_task.is_some() { self.update_excerpts_task = None; } else { - self.update_all_diagnostics(false, window, cx); + self.update_all_excerpts(window, cx); } cx.notify(); } @@ -404,73 +376,6 @@ impl ProjectDiagnosticsEditor { } } - fn update_all_diagnostics( - &mut self, - first_launch: bool, - window: &mut Window, - cx: &mut Context, - ) { - let cargo_diagnostics_sources = self.cargo_diagnostics_sources(cx); - if cargo_diagnostics_sources.is_empty() { - self.update_all_excerpts(window, cx); - } else if first_launch && !self.summary.is_empty() { - self.update_all_excerpts(window, cx); - } else { - self.fetch_cargo_diagnostics(Arc::new(cargo_diagnostics_sources), cx); - } - } - - fn fetch_cargo_diagnostics( - &mut self, - diagnostics_sources: Arc>, - cx: &mut Context, - ) { - let project = self.project.clone(); - self.cargo_diagnostics_fetch.cancel_task = None; - self.cargo_diagnostics_fetch.fetch_task = None; - self.cargo_diagnostics_fetch.diagnostic_sources = diagnostics_sources.clone(); - if self.cargo_diagnostics_fetch.diagnostic_sources.is_empty() { - return; - } - - self.cargo_diagnostics_fetch.fetch_task = Some(cx.spawn(async move |editor, cx| { - let mut fetch_tasks = Vec::new(); - for buffer_path in diagnostics_sources.iter().cloned() { - if cx - .update(|cx| { - fetch_tasks.push(run_flycheck(project.clone(), Some(buffer_path), cx)); - }) - .is_err() - { - break; - } - } - - let _ = join_all(fetch_tasks).await; - editor - .update(cx, |editor, _| { - editor.cargo_diagnostics_fetch.fetch_task = None; - }) - .ok(); - })); - } - - fn stop_cargo_diagnostics_fetch(&mut self, cx: &mut App) { - self.cargo_diagnostics_fetch.fetch_task = None; - let mut cancel_gasks = Vec::new(); - for buffer_path in std::mem::take(&mut self.cargo_diagnostics_fetch.diagnostic_sources) - .iter() - .cloned() - { - cancel_gasks.push(cancel_flycheck(self.project.clone(), Some(buffer_path), cx)); - } - - self.cargo_diagnostics_fetch.cancel_task = Some(cx.background_spawn(async move { - let _ = join_all(cancel_gasks).await; - log::info!("Finished fetching cargo diagnostics"); - })); - } - /// Enqueue an update of all excerpts. Updates all paths that either /// currently have diagnostics or are currently present in this view. fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context) { @@ -695,30 +600,6 @@ impl ProjectDiagnosticsEditor { }) }) } - - pub fn cargo_diagnostics_sources(&self, cx: &App) -> Vec { - let fetch_cargo_diagnostics = ProjectSettings::get_global(cx) - .diagnostics - .fetch_cargo_diagnostics(); - if !fetch_cargo_diagnostics { - return Vec::new(); - } - self.project - .read(cx) - .worktrees(cx) - .filter_map(|worktree| { - let _cargo_toml_entry = worktree.read(cx).entry_for_path("Cargo.toml")?; - let rust_file_entry = worktree.read(cx).entries(false, 0).find(|entry| { - entry - .path - .extension() - .and_then(|extension| extension.to_str()) - == Some("rs") - })?; - self.project.read(cx).path_for_entry(rust_file_entry.id, cx) - }) - .collect() - } } impl Focusable for ProjectDiagnosticsEditor { diff --git a/crates/diagnostics/src/toolbar_controls.rs b/crates/diagnostics/src/toolbar_controls.rs index e77b80115f..404db39164 100644 --- a/crates/diagnostics/src/toolbar_controls.rs +++ b/crates/diagnostics/src/toolbar_controls.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use crate::{ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh}; use gpui::{Context, Entity, EventEmitter, ParentElement, Render, WeakEntity, Window}; use ui::prelude::*; @@ -15,26 +13,18 @@ impl Render for ToolbarControls { let mut include_warnings = false; let mut has_stale_excerpts = false; let mut is_updating = false; - let cargo_diagnostics_sources = Arc::new(self.diagnostics().map_or(Vec::new(), |editor| { - editor.read(cx).cargo_diagnostics_sources(cx) - })); - let fetch_cargo_diagnostics = !cargo_diagnostics_sources.is_empty(); if let Some(editor) = self.diagnostics() { let diagnostics = editor.read(cx); include_warnings = diagnostics.include_warnings; has_stale_excerpts = !diagnostics.paths_to_update.is_empty(); - is_updating = if fetch_cargo_diagnostics { - diagnostics.cargo_diagnostics_fetch.fetch_task.is_some() - } else { - diagnostics.update_excerpts_task.is_some() - || diagnostics - .project - .read(cx) - .language_servers_running_disk_based_diagnostics(cx) - .next() - .is_some() - }; + is_updating = diagnostics.update_excerpts_task.is_some() + || diagnostics + .project + .read(cx) + .language_servers_running_disk_based_diagnostics(cx) + .next() + .is_some(); } let tooltip = if include_warnings { @@ -64,7 +54,6 @@ impl Render for ToolbarControls { .on_click(cx.listener(move |toolbar_controls, _, _, cx| { if let Some(diagnostics) = toolbar_controls.diagnostics() { diagnostics.update(cx, |diagnostics, cx| { - diagnostics.stop_cargo_diagnostics_fetch(cx); diagnostics.update_excerpts_task = None; cx.notify(); }); @@ -76,7 +65,7 @@ impl Render for ToolbarControls { IconButton::new("refresh-diagnostics", IconName::ArrowCircle) .icon_color(Color::Info) .shape(IconButtonShape::Square) - .disabled(!has_stale_excerpts && !fetch_cargo_diagnostics) + .disabled(!has_stale_excerpts) .tooltip(Tooltip::for_action_title( "Refresh diagnostics", &ToggleDiagnosticsRefresh, @@ -84,17 +73,8 @@ impl Render for ToolbarControls { .on_click(cx.listener({ move |toolbar_controls, _, window, cx| { if let Some(diagnostics) = toolbar_controls.diagnostics() { - let cargo_diagnostics_sources = - Arc::clone(&cargo_diagnostics_sources); diagnostics.update(cx, move |diagnostics, cx| { - if fetch_cargo_diagnostics { - diagnostics.fetch_cargo_diagnostics( - cargo_diagnostics_sources, - cx, - ); - } else { - diagnostics.update_all_excerpts(window, cx); - } + diagnostics.update_all_excerpts(window, cx); }); } } diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index c6c7357148..3e8dce756b 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -510,20 +510,6 @@ impl LspAdapter for RustLspAdapter { } } - let cargo_diagnostics_fetched_separately = ProjectSettings::get_global(cx) - .diagnostics - .fetch_cargo_diagnostics(); - if cargo_diagnostics_fetched_separately { - let disable_check_on_save = json!({ - "checkOnSave": false, - }); - if let Some(initialization_options) = &mut original.initialization_options { - merge_json_value_into(disable_check_on_save, initialization_options); - } else { - original.initialization_options = Some(disable_check_on_save); - } - } - Ok(original) } } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index a6fea4059c..4447c25129 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -181,17 +181,6 @@ pub struct DiagnosticsSettings { /// Settings for showing inline diagnostics. pub inline: InlineDiagnosticsSettings, - - /// Configuration, related to Rust language diagnostics. - pub cargo: Option, -} - -impl DiagnosticsSettings { - pub fn fetch_cargo_diagnostics(&self) -> bool { - self.cargo - .as_ref() - .is_some_and(|cargo_diagnostics| cargo_diagnostics.fetch_cargo_diagnostics) - } } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] @@ -258,7 +247,6 @@ impl Default for DiagnosticsSettings { include_warnings: true, lsp_pull_diagnostics: LspPullDiagnosticsSettings::default(), inline: InlineDiagnosticsSettings::default(), - cargo: None, } } } @@ -292,16 +280,6 @@ impl Default for GlobalLspSettings { } } -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -pub struct CargoDiagnosticsSettings { - /// When enabled, Zed disables rust-analyzer's check on save and starts to query - /// Cargo diagnostics separately. - /// - /// Default: false - #[serde(default)] - pub fetch_cargo_diagnostics: bool, -} - #[derive( Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema, )] diff --git a/docs/src/languages/rust.md b/docs/src/languages/rust.md index 7695280275..0bfa3ecac7 100644 --- a/docs/src/languages/rust.md +++ b/docs/src/languages/rust.md @@ -136,22 +136,7 @@ This is enabled by default and can be configured as ## Manual Cargo Diagnostics fetch By default, rust-analyzer has `checkOnSave: true` enabled, which causes every buffer save to trigger a `cargo check --workspace --all-targets` command. -For lager projects this might introduce excessive wait times, so a more fine-grained triggering could be enabled by altering the - -```json -"diagnostics": { - "cargo": { - // When enabled, Zed disables rust-analyzer's check on save and starts to query - // Cargo diagnostics separately. - "fetch_cargo_diagnostics": false - } -} -``` - -default settings. - -This will stop rust-analyzer from running `cargo check ...` on save, yet still allow to run -`editor: run/clear/cancel flycheck` commands in Rust files to refresh cargo diagnostics; the project diagnostics editor will also refresh cargo diagnostics with `editor: run flycheck` command when the setting is enabled. +If disabled with `checkOnSave: false` (see the example of the server configuration json above), it's still possible to fetch the diagnostics manually, with the `editor: run/clear/cancel flycheck` commands in Rust files to refresh cargo diagnostics; the project diagnostics editor will also refresh cargo diagnostics with `editor: run flycheck` command when the setting is enabled. ## More server configuration From 61bc1cc44172d61c2fb69d8265bc5d809512f342 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 23 Aug 2025 16:30:54 +0200 Subject: [PATCH 637/693] acp: Support launching custom agent servers (#36805) It's enough to add this to your settings: ```json { "agent_servers": { "Name Of Your Agent": { "command": "/path/to/custom/agent", "args": ["arguments", "that", "you", "want"], } } } ``` Release Notes: - N/A --- crates/acp_tools/src/acp_tools.rs | 12 +- crates/agent2/src/native_agent_server.rs | 12 +- crates/agent_servers/src/acp.rs | 12 +- crates/agent_servers/src/agent_servers.rs | 8 +- crates/agent_servers/src/claude.rs | 12 +- crates/agent_servers/src/custom.rs | 59 ++++++++++ crates/agent_servers/src/e2e_tests.rs | 17 ++- crates/agent_servers/src/gemini.rs | 13 +-- crates/agent_servers/src/settings.rs | 24 +++- crates/agent_ui/src/acp/thread_view.rs | 20 ++-- crates/agent_ui/src/agent_panel.rs | 105 ++++++++++++++---- crates/agent_ui/src/agent_ui.rs | 19 +++- crates/language_model/src/language_model.rs | 4 +- .../language_models/src/provider/anthropic.rs | 6 +- crates/language_models/src/provider/google.rs | 6 +- 15 files changed, 238 insertions(+), 91 deletions(-) create mode 100644 crates/agent_servers/src/custom.rs diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs index ca5e57e85a..ee12b04cde 100644 --- a/crates/acp_tools/src/acp_tools.rs +++ b/crates/acp_tools/src/acp_tools.rs @@ -46,7 +46,7 @@ pub struct AcpConnectionRegistry { } struct ActiveConnection { - server_name: &'static str, + server_name: SharedString, connection: Weak, } @@ -63,12 +63,12 @@ impl AcpConnectionRegistry { pub fn set_active_connection( &self, - server_name: &'static str, + server_name: impl Into, connection: &Rc, cx: &mut Context, ) { self.active_connection.replace(Some(ActiveConnection { - server_name, + server_name: server_name.into(), connection: Rc::downgrade(connection), })); cx.notify(); @@ -85,7 +85,7 @@ struct AcpTools { } struct WatchedConnection { - server_name: &'static str, + server_name: SharedString, messages: Vec, list_state: ListState, connection: Weak, @@ -142,7 +142,7 @@ impl AcpTools { }); self.watched_connection = Some(WatchedConnection { - server_name: active_connection.server_name, + server_name: active_connection.server_name.clone(), messages: vec![], list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)), connection: active_connection.connection.clone(), @@ -442,7 +442,7 @@ impl Item for AcpTools { "ACP: {}", self.watched_connection .as_ref() - .map_or("Disconnected", |connection| connection.server_name) + .map_or("Disconnected", |connection| &connection.server_name) ) .into() } diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index 4ce467d6fd..12d3c79d1b 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -3,7 +3,7 @@ use std::{any::Any, path::Path, rc::Rc, sync::Arc}; use agent_servers::AgentServer; use anyhow::Result; use fs::Fs; -use gpui::{App, Entity, Task}; +use gpui::{App, Entity, SharedString, Task}; use project::Project; use prompt_store::PromptStore; @@ -22,16 +22,16 @@ impl NativeAgentServer { } impl AgentServer for NativeAgentServer { - fn name(&self) -> &'static str { - "Zed Agent" + fn name(&self) -> SharedString { + "Zed Agent".into() } - fn empty_state_headline(&self) -> &'static str { + fn empty_state_headline(&self) -> SharedString { self.name() } - fn empty_state_message(&self) -> &'static str { - "" + fn empty_state_message(&self) -> SharedString { + "".into() } fn logo(&self) -> ui::IconName { diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index a99a401431..c9c938c6c0 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -15,7 +15,7 @@ use std::{path::Path, rc::Rc}; use thiserror::Error; use anyhow::{Context as _, Result}; -use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity}; +use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntity}; use acp_thread::{AcpThread, AuthRequired, LoadError}; @@ -24,7 +24,7 @@ use acp_thread::{AcpThread, AuthRequired, LoadError}; pub struct UnsupportedVersion; pub struct AcpConnection { - server_name: &'static str, + server_name: SharedString, connection: Rc, sessions: Rc>>, auth_methods: Vec, @@ -38,7 +38,7 @@ pub struct AcpSession { } pub async fn connect( - server_name: &'static str, + server_name: SharedString, command: AgentServerCommand, root_dir: &Path, cx: &mut AsyncApp, @@ -51,7 +51,7 @@ const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1; impl AcpConnection { pub async fn stdio( - server_name: &'static str, + server_name: SharedString, command: AgentServerCommand, root_dir: &Path, cx: &mut AsyncApp, @@ -121,7 +121,7 @@ impl AcpConnection { cx.update(|cx| { AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| { - registry.set_active_connection(server_name, &connection, cx) + registry.set_active_connection(server_name.clone(), &connection, cx) }); })?; @@ -187,7 +187,7 @@ impl AgentConnection for AcpConnection { let action_log = cx.new(|_| ActionLog::new(project.clone()))?; let thread = cx.new(|_cx| { AcpThread::new( - self.server_name, + self.server_name.clone(), self.clone(), project, action_log, diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 2f5ec478ae..fa59201338 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -1,5 +1,6 @@ mod acp; mod claude; +mod custom; mod gemini; mod settings; @@ -7,6 +8,7 @@ mod settings; pub mod e2e_tests; pub use claude::*; +pub use custom::*; pub use gemini::*; pub use settings::*; @@ -31,9 +33,9 @@ pub fn init(cx: &mut App) { pub trait AgentServer: Send { fn logo(&self) -> ui::IconName; - fn name(&self) -> &'static str; - fn empty_state_headline(&self) -> &'static str; - fn empty_state_message(&self) -> &'static str; + fn name(&self) -> SharedString; + fn empty_state_headline(&self) -> SharedString; + fn empty_state_message(&self) -> SharedString; fn connect( &self, diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index ef666974f1..048563103f 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -30,7 +30,7 @@ use futures::{ io::BufReader, select_biased, }; -use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; +use gpui::{App, AppContext, AsyncApp, Entity, SharedString, Task, WeakEntity}; use serde::{Deserialize, Serialize}; use util::{ResultExt, debug_panic}; @@ -43,16 +43,16 @@ use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri pub struct ClaudeCode; impl AgentServer for ClaudeCode { - fn name(&self) -> &'static str { - "Claude Code" + fn name(&self) -> SharedString { + "Claude Code".into() } - fn empty_state_headline(&self) -> &'static str { + fn empty_state_headline(&self) -> SharedString { self.name() } - fn empty_state_message(&self) -> &'static str { - "How can I help you today?" + fn empty_state_message(&self) -> SharedString { + "How can I help you today?".into() } fn logo(&self) -> ui::IconName { diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs new file mode 100644 index 0000000000..e544c4f21f --- /dev/null +++ b/crates/agent_servers/src/custom.rs @@ -0,0 +1,59 @@ +use crate::{AgentServerCommand, AgentServerSettings}; +use acp_thread::AgentConnection; +use anyhow::Result; +use gpui::{App, Entity, SharedString, Task}; +use project::Project; +use std::{path::Path, rc::Rc}; +use ui::IconName; + +/// A generic agent server implementation for custom user-defined agents +pub struct CustomAgentServer { + name: SharedString, + command: AgentServerCommand, +} + +impl CustomAgentServer { + pub fn new(name: SharedString, settings: &AgentServerSettings) -> Self { + Self { + name, + command: settings.command.clone(), + } + } +} + +impl crate::AgentServer for CustomAgentServer { + fn name(&self) -> SharedString { + self.name.clone() + } + + fn logo(&self) -> IconName { + IconName::Terminal + } + + fn empty_state_headline(&self) -> SharedString { + "No conversations yet".into() + } + + fn empty_state_message(&self) -> SharedString { + format!("Start a conversation with {}", self.name).into() + } + + fn connect( + &self, + root_dir: &Path, + _project: &Entity, + cx: &mut App, + ) -> Task>> { + let server_name = self.name(); + let command = self.command.clone(); + let root_dir = root_dir.to_path_buf(); + + cx.spawn(async move |mut cx| { + crate::acp::connect(server_name, command, &root_dir, &mut cx).await + }) + } + + fn into_any(self: Rc) -> Rc { + self + } +} diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index c271079071..42264b4b4f 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -1,17 +1,15 @@ +use crate::AgentServer; +use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus}; +use agent_client_protocol as acp; +use futures::{FutureExt, StreamExt, channel::mpsc, select}; +use gpui::{AppContext, Entity, TestAppContext}; +use indoc::indoc; +use project::{FakeFs, Project}; use std::{ path::{Path, PathBuf}, sync::Arc, time::Duration, }; - -use crate::AgentServer; -use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus}; -use agent_client_protocol as acp; - -use futures::{FutureExt, StreamExt, channel::mpsc, select}; -use gpui::{AppContext, Entity, TestAppContext}; -use indoc::indoc; -use project::{FakeFs, Project}; use util::path; pub async fn test_basic(server: F, cx: &mut TestAppContext) @@ -479,6 +477,7 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc { gemini: Some(crate::AgentServerSettings { command: crate::gemini::tests::local_command(), }), + custom: collections::HashMap::default(), }, cx, ); diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 29120fff6e..9ebcee745c 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -4,11 +4,10 @@ use std::{any::Any, path::Path}; use crate::{AgentServer, AgentServerCommand}; use acp_thread::{AgentConnection, LoadError}; use anyhow::Result; -use gpui::{Entity, Task}; +use gpui::{App, Entity, SharedString, Task}; use language_models::provider::google::GoogleLanguageModelProvider; use project::Project; use settings::SettingsStore; -use ui::App; use crate::AllAgentServersSettings; @@ -18,16 +17,16 @@ pub struct Gemini; const ACP_ARG: &str = "--experimental-acp"; impl AgentServer for Gemini { - fn name(&self) -> &'static str { - "Gemini CLI" + fn name(&self) -> SharedString { + "Gemini CLI".into() } - fn empty_state_headline(&self) -> &'static str { + fn empty_state_headline(&self) -> SharedString { self.name() } - fn empty_state_message(&self) -> &'static str { - "Ask questions, edit files, run commands" + fn empty_state_message(&self) -> SharedString { + "Ask questions, edit files, run commands".into() } fn logo(&self) -> ui::IconName { diff --git a/crates/agent_servers/src/settings.rs b/crates/agent_servers/src/settings.rs index 645674b5f1..96ac6e3cbe 100644 --- a/crates/agent_servers/src/settings.rs +++ b/crates/agent_servers/src/settings.rs @@ -1,6 +1,7 @@ use crate::AgentServerCommand; use anyhow::Result; -use gpui::App; +use collections::HashMap; +use gpui::{App, SharedString}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; @@ -13,9 +14,13 @@ pub fn init(cx: &mut App) { pub struct AllAgentServersSettings { pub gemini: Option, pub claude: Option, + + /// Custom agent servers configured by the user + #[serde(flatten)] + pub custom: HashMap, } -#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)] +#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)] pub struct AgentServerSettings { #[serde(flatten)] pub command: AgentServerCommand, @@ -29,13 +34,26 @@ impl settings::Settings for AllAgentServersSettings { fn load(sources: SettingsSources, _: &mut App) -> Result { let mut settings = AllAgentServersSettings::default(); - for AllAgentServersSettings { gemini, claude } in sources.defaults_and_customizations() { + for AllAgentServersSettings { + gemini, + claude, + custom, + } in sources.defaults_and_customizations() + { if gemini.is_some() { settings.gemini = gemini.clone(); } if claude.is_some() { settings.claude = claude.clone(); } + + // Merge custom agents + for (name, config) in custom { + // Skip built-in agent names to avoid conflicts + if name != "gemini" && name != "claude" { + settings.custom.insert(name.clone(), config.clone()); + } + } } Ok(settings) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 3ad1234e22..87928767c6 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -600,7 +600,7 @@ impl AcpThreadView { let view = registry.read(cx).provider(&provider_id).map(|provider| { provider.configuration_view( - language_model::ConfigurationViewTargetAgent::Other(agent_name), + language_model::ConfigurationViewTargetAgent::Other(agent_name.clone()), window, cx, ) @@ -1372,7 +1372,7 @@ impl AcpThreadView { .icon_color(Color::Muted) .style(ButtonStyle::Transparent) .tooltip(move |_window, cx| { - cx.new(|_| UnavailableEditingTooltip::new(agent_name.into())) + cx.new(|_| UnavailableEditingTooltip::new(agent_name.clone())) .into() }) ) @@ -3911,13 +3911,13 @@ impl AcpThreadView { match AgentSettings::get_global(cx).notify_when_agent_waiting { NotifyWhenAgentWaiting::PrimaryScreen => { if let Some(primary) = cx.primary_display() { - self.pop_up(icon, caption.into(), title.into(), window, primary, cx); + self.pop_up(icon, caption.into(), title, window, primary, cx); } } NotifyWhenAgentWaiting::AllScreens => { let caption = caption.into(); for screen in cx.displays() { - self.pop_up(icon, caption.clone(), title.into(), window, screen, cx); + self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx); } } NotifyWhenAgentWaiting::Never => { @@ -5153,16 +5153,16 @@ pub(crate) mod tests { ui::IconName::Ai } - fn name(&self) -> &'static str { - "Test" + fn name(&self) -> SharedString { + "Test".into() } - fn empty_state_headline(&self) -> &'static str { - "Test" + fn empty_state_headline(&self) -> SharedString { + "Test".into() } - fn empty_state_message(&self) -> &'static str { - "Test" + fn empty_state_message(&self) -> SharedString { + "Test".into() } fn connect( diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 0e611d0db9..50f9fc6a45 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use std::time::Duration; use acp_thread::AcpThread; +use agent_servers::AgentServerSettings; use agent2::{DbThreadMetadata, HistoryEntry}; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; @@ -128,7 +129,7 @@ pub fn init(cx: &mut App) { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); panel.update(cx, |panel, cx| { - panel.external_thread(action.agent, None, None, window, cx) + panel.external_thread(action.agent.clone(), None, None, window, cx) }); } }) @@ -239,7 +240,7 @@ enum WhichFontSize { None, } -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub enum AgentType { #[default] Zed, @@ -247,23 +248,29 @@ pub enum AgentType { Gemini, ClaudeCode, NativeAgent, + Custom { + name: SharedString, + settings: AgentServerSettings, + }, } impl AgentType { - fn label(self) -> impl Into { + fn label(&self) -> SharedString { match self { - Self::Zed | Self::TextThread => "Zed Agent", - Self::NativeAgent => "Agent 2", - Self::Gemini => "Gemini CLI", - Self::ClaudeCode => "Claude Code", + Self::Zed | Self::TextThread => "Zed Agent".into(), + Self::NativeAgent => "Agent 2".into(), + Self::Gemini => "Gemini CLI".into(), + Self::ClaudeCode => "Claude Code".into(), + Self::Custom { name, .. } => name.into(), } } - fn icon(self) -> Option { + fn icon(&self) -> Option { match self { Self::Zed | Self::NativeAgent | Self::TextThread => None, Self::Gemini => Some(IconName::AiGemini), Self::ClaudeCode => Some(IconName::AiClaude), + Self::Custom { .. } => Some(IconName::Terminal), } } } @@ -517,7 +524,7 @@ pub struct AgentPanel { impl AgentPanel { fn serialize(&mut self, cx: &mut Context) { let width = self.width; - let selected_agent = self.selected_agent; + let selected_agent = self.selected_agent.clone(); self.pending_serialization = Some(cx.background_spawn(async move { KEY_VALUE_STORE .write_kvp( @@ -607,7 +614,7 @@ impl AgentPanel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width.map(|w| w.round()); if let Some(selected_agent) = serialized_panel.selected_agent { - panel.selected_agent = selected_agent; + panel.selected_agent = selected_agent.clone(); panel.new_agent_thread(selected_agent, window, cx); } cx.notify(); @@ -1077,14 +1084,17 @@ impl AgentPanel { cx.spawn_in(window, async move |this, cx| { let ext_agent = match agent_choice { Some(agent) => { - cx.background_spawn(async move { - if let Some(serialized) = - serde_json::to_string(&LastUsedExternalAgent { agent }).log_err() - { - KEY_VALUE_STORE - .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized) - .await - .log_err(); + cx.background_spawn({ + let agent = agent.clone(); + async move { + if let Some(serialized) = + serde_json::to_string(&LastUsedExternalAgent { agent }).log_err() + { + KEY_VALUE_STORE + .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized) + .await + .log_err(); + } } }) .detach(); @@ -1110,7 +1120,9 @@ impl AgentPanel { this.update_in(cx, |this, window, cx| { match ext_agent { - crate::ExternalAgent::Gemini | crate::ExternalAgent::NativeAgent => { + crate::ExternalAgent::Gemini + | crate::ExternalAgent::NativeAgent + | crate::ExternalAgent::Custom { .. } => { if !cx.has_flag::() { return; } @@ -1839,14 +1851,14 @@ impl AgentPanel { cx: &mut Context, ) { if self.selected_agent != agent { - self.selected_agent = agent; + self.selected_agent = agent.clone(); self.serialize(cx); } self.new_agent_thread(agent, window, cx); } pub fn selected_agent(&self) -> AgentType { - self.selected_agent + self.selected_agent.clone() } pub fn new_agent_thread( @@ -1885,6 +1897,13 @@ impl AgentPanel { window, cx, ), + AgentType::Custom { name, settings } => self.external_thread( + Some(crate::ExternalAgent::Custom { name, settings }), + None, + None, + window, + cx, + ), } } @@ -2610,13 +2629,55 @@ impl AgentPanel { } }), ) + }) + .when(cx.has_flag::(), |mut menu| { + // Add custom agents from settings + let settings = + agent_servers::AllAgentServersSettings::get_global(cx); + for (agent_name, agent_settings) in &settings.custom { + menu = menu.item( + ContextMenuEntry::new(format!("New {} Thread", agent_name)) + .icon(IconName::Terminal) + .icon_color(Color::Muted) + .handler({ + let workspace = workspace.clone(); + let agent_name = agent_name.clone(); + let agent_settings = agent_settings.clone(); + move |window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = + workspace.panel::(cx) + { + panel.update(cx, |panel, cx| { + panel.set_selected_agent( + AgentType::Custom { + name: agent_name + .clone(), + settings: + agent_settings + .clone(), + }, + window, + cx, + ); + }); + } + }); + } + } + }), + ); + } + + menu }); menu })) } }); - let selected_agent_label = self.selected_agent.label().into(); + let selected_agent_label = self.selected_agent.label(); let selected_agent = div() .id("selected_agent_icon") .when_some(self.selected_agent.icon(), |this, icon| { diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 6084fd6423..40f6c6a2bb 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -28,13 +28,14 @@ use std::rc::Rc; use std::sync::Arc; use agent::{Thread, ThreadId}; +use agent_servers::AgentServerSettings; use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection}; use assistant_slash_command::SlashCommandRegistry; use client::Client; use command_palette_hooks::CommandPaletteFilter; use feature_flags::FeatureFlagAppExt as _; use fs::Fs; -use gpui::{Action, App, Entity, actions}; +use gpui::{Action, App, Entity, SharedString, actions}; use language::LanguageRegistry; use language_model::{ ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, @@ -159,13 +160,17 @@ pub struct NewNativeAgentThreadFromSummary { from_session_id: agent_client_protocol::SessionId, } -#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] enum ExternalAgent { #[default] Gemini, ClaudeCode, NativeAgent, + Custom { + name: SharedString, + settings: AgentServerSettings, + }, } impl ExternalAgent { @@ -175,9 +180,13 @@ impl ExternalAgent { history: Entity, ) -> Rc { match self { - ExternalAgent::Gemini => Rc::new(agent_servers::Gemini), - ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode), - ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)), + Self::Gemini => Rc::new(agent_servers::Gemini), + Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode), + Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)), + Self::Custom { name, settings } => Rc::new(agent_servers::CustomAgentServer::new( + name.clone(), + settings, + )), } } } diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index e0a3866443..d5313b6a3a 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -643,11 +643,11 @@ pub trait LanguageModelProvider: 'static { fn reset_credentials(&self, cx: &mut App) -> Task>; } -#[derive(Default, Clone, Copy)] +#[derive(Default, Clone)] pub enum ConfigurationViewTargetAgent { #[default] ZedAgent, - Other(&'static str), + Other(SharedString), } #[derive(PartialEq, Eq)] diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index 0d061c0587..c492edeaf5 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -1041,9 +1041,9 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match self.target_agent { - ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic", - ConfigurationViewTargetAgent::Other(agent) => agent, + .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match &self.target_agent { + ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic".into(), + ConfigurationViewTargetAgent::Other(agent) => agent.clone(), }))) .child( List::new() diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index 566620675e..f252ab7aa3 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -921,9 +921,9 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match self.target_agent { - ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Google AI", - ConfigurationViewTargetAgent::Other(agent) => agent, + .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match &self.target_agent { + ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Google AI".into(), + ConfigurationViewTargetAgent::Other(agent) => agent.clone(), }))) .child( List::new() From 60ea4754b29ea5292539496abf40f1a361dced4a Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Sat, 23 Aug 2025 20:30:16 +0530 Subject: [PATCH 638/693] project: Fix dynamic registration for textDocument/documentColor (#36807) From: https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/protocol/src/common/protocol.colorProvider.ts#L50 Release Notes: - N/A --- crates/project/src/lsp_store.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index fb1fae3736..d2958dce01 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -11913,7 +11913,7 @@ impl LspStore { notify_server_capabilities_updated(&server, cx); } } - "textDocument/colorProvider" => { + "textDocument/documentColor" => { if let Some(caps) = reg .register_options .map(serde_json::from_value) @@ -12064,7 +12064,7 @@ impl LspStore { }); notify_server_capabilities_updated(&server, cx); } - "textDocument/colorProvider" => { + "textDocument/documentColor" => { server.update_capabilities(|capabilities| { capabilities.color_provider = None; }); From d49409caba7f4b39409c38299d61511b6a3bb406 Mon Sep 17 00:00:00 2001 From: itsaphel Date: Sat, 23 Aug 2025 17:11:27 +0100 Subject: [PATCH 639/693] docs: Update settings in diagnostics.md (#36806) For project_panel, the diagnostics key seems to be `show_diagnostics` not `diagnostics` ([source](https://github.com/zed-industries/zed/blob/main/crates/project_panel/src/project_panel_settings.rs#L149-L152)). Updating the docs accordingly Release Notes: - N/A --- docs/src/diagnostics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/diagnostics.md b/docs/src/diagnostics.md index a015fbebf8..9603c8197c 100644 --- a/docs/src/diagnostics.md +++ b/docs/src/diagnostics.md @@ -51,7 +51,7 @@ To configure, use ```json5 "project_panel": { - "diagnostics": "all", + "show_diagnostics": "all", } ``` From 19764794b77c08e828fd7170c7539a9ca9c2b3de Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Sat, 23 Aug 2025 16:39:14 -0400 Subject: [PATCH 640/693] acp: Animate loading context creases (#36814) - Add pulsating animation for context creases while they're loading - Add spinner in message editors (replacing send button) during the window where sending has been requested, but we haven't finished loading the message contents to send to the model - During the same window, ignore further send requests, so we don't end up sending the same message twice if you mash enter while loading is in progress - Wait for context to load before rewinding the thread when sending an edited past message, avoiding an empty-looking state during the same window Release Notes: - N/A --- Cargo.lock | 1 + crates/agent_ui/Cargo.toml | 1 + crates/agent_ui/src/acp/message_editor.rs | 224 ++++++++++++++-------- crates/agent_ui/src/acp/thread_view.rs | 92 +++++++-- 4 files changed, 217 insertions(+), 101 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6964ed4890..0575796034 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -403,6 +403,7 @@ dependencies = [ "parking_lot", "paths", "picker", + "postage", "pretty_assertions", "project", "prompt_store", diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 43e3b25124..6b0979ee69 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -67,6 +67,7 @@ ordered-float.workspace = true parking_lot.workspace = true paths.workspace = true picker.workspace = true +postage.workspace = true project.workspace = true prompt_store.workspace = true proto.workspace = true diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 115008cf52..bab42e3da2 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -21,12 +21,13 @@ use futures::{ future::{Shared, join_all}, }; use gpui::{ - AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, - HighlightStyle, Image, ImageFormat, Img, KeyContext, Subscription, Task, TextStyle, - UnderlineStyle, WeakEntity, + Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Entity, EntityId, + EventEmitter, FocusHandle, Focusable, HighlightStyle, Image, ImageFormat, Img, KeyContext, + Subscription, Task, TextStyle, UnderlineStyle, WeakEntity, pulsating_between, }; use language::{Buffer, Language}; use language_model::LanguageModelImage; +use postage::stream::Stream as _; use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree}; use prompt_store::{PromptId, PromptStore}; use rope::Point; @@ -44,10 +45,10 @@ use std::{ use text::{OffsetRangeExt, ToOffset as _}; use theme::ThemeSettings; use ui::{ - ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Icon, IconName, - IconSize, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement, - Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div, - h_flex, px, + ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Element as _, + FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label, + LabelCommon, LabelSize, ParentElement, Render, SelectableButton, SharedString, Styled, + TextSize, TintColor, Toggleable, Window, div, h_flex, px, }; use util::{ResultExt, debug_panic}; use workspace::{Workspace, notifications::NotifyResultExt as _}; @@ -246,7 +247,7 @@ impl MessageEditor { .buffer_snapshot .anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1); - let crease_id = if let MentionUri::File { abs_path } = &mention_uri + let crease = if let MentionUri::File { abs_path } = &mention_uri && let Some(extension) = abs_path.extension() && let Some(extension) = extension.to_str() && Img::extensions().contains(&extension) @@ -272,29 +273,31 @@ impl MessageEditor { Ok(image) }) .shared(); - insert_crease_for_image( + insert_crease_for_mention( *excerpt_id, start, content_len, - Some(abs_path.as_path().into()), - image, + mention_uri.name().into(), + IconName::Image.path().into(), + Some(image), self.editor.clone(), window, cx, ) } else { - crate::context_picker::insert_crease_for_mention( + insert_crease_for_mention( *excerpt_id, start, content_len, crease_text, mention_uri.icon_path(cx), + None, self.editor.clone(), window, cx, ) }; - let Some(crease_id) = crease_id else { + let Some((crease_id, tx)) = crease else { return Task::ready(()); }; @@ -331,7 +334,9 @@ impl MessageEditor { // Notify the user if we failed to load the mentioned context cx.spawn_in(window, async move |this, cx| { - if task.await.notify_async_err(cx).is_none() { + let result = task.await.notify_async_err(cx); + drop(tx); + if result.is_none() { this.update(cx, |this, cx| { this.editor.update(cx, |editor, cx| { // Remove mention @@ -857,12 +862,13 @@ impl MessageEditor { snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len) }); let image = Arc::new(image); - let Some(crease_id) = insert_crease_for_image( + let Some((crease_id, tx)) = insert_crease_for_mention( excerpt_id, text_anchor, content_len, - None.clone(), - Task::ready(Ok(image.clone())).shared(), + MentionUri::PastedImage.name().into(), + IconName::Image.path().into(), + Some(Task::ready(Ok(image.clone())).shared()), self.editor.clone(), window, cx, @@ -877,6 +883,7 @@ impl MessageEditor { .update(|_, cx| LanguageModelImage::from_image(image, cx)) .map_err(|e| e.to_string())? .await; + drop(tx); if let Some(image) = image { Ok(Mention::Image(MentionImage { data: image.source, @@ -1097,18 +1104,20 @@ impl MessageEditor { for (range, mention_uri, mention) in mentions { let anchor = snapshot.anchor_before(range.start); - let Some(crease_id) = crate::context_picker::insert_crease_for_mention( + let Some((crease_id, tx)) = insert_crease_for_mention( anchor.excerpt_id, anchor.text_anchor, range.end - range.start, mention_uri.name().into(), mention_uri.icon_path(cx), + None, self.editor.clone(), window, cx, ) else { continue; }; + drop(tx); self.mention_set.mentions.insert( crease_id, @@ -1227,23 +1236,21 @@ impl Render for MessageEditor { } } -pub(crate) fn insert_crease_for_image( +pub(crate) fn insert_crease_for_mention( excerpt_id: ExcerptId, anchor: text::Anchor, content_len: usize, - abs_path: Option>, - image: Shared, String>>>, + crease_label: SharedString, + crease_icon: SharedString, + // abs_path: Option>, + image: Option, String>>>>, editor: Entity, window: &mut Window, cx: &mut App, -) -> Option { - let crease_label = abs_path - .as_ref() - .and_then(|path| path.file_name()) - .map(|name| name.to_string_lossy().to_string().into()) - .unwrap_or(SharedString::from("Image")); +) -> Option<(CreaseId, postage::barrier::Sender)> { + let (tx, rx) = postage::barrier::channel(); - editor.update(cx, |editor, cx| { + let crease_id = editor.update(cx, |editor, cx| { let snapshot = editor.buffer().read(cx).snapshot(cx); let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?; @@ -1252,7 +1259,15 @@ pub(crate) fn insert_crease_for_image( let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len); let placeholder = FoldPlaceholder { - render: render_image_fold_icon_button(crease_label, image, cx.weak_entity()), + render: render_fold_icon_button( + crease_label, + crease_icon, + start..end, + rx, + image, + cx.weak_entity(), + cx, + ), merge_adjacent: false, ..Default::default() }; @@ -1269,63 +1284,112 @@ pub(crate) fn insert_crease_for_image( editor.fold_creases(vec![crease], false, window, cx); Some(ids[0]) - }) + })?; + + Some((crease_id, tx)) } -fn render_image_fold_icon_button( +fn render_fold_icon_button( label: SharedString, - image_task: Shared, String>>>, + icon: SharedString, + range: Range, + mut loading_finished: postage::barrier::Receiver, + image_task: Option, String>>>>, editor: WeakEntity, + cx: &mut App, ) -> Arc, &mut App) -> AnyElement> { - Arc::new({ - move |fold_id, fold_range, cx| { - let is_in_text_selection = editor - .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx)) - .unwrap_or_default(); - - ButtonLike::new(fold_id) - .style(ButtonStyle::Filled) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .toggle_state(is_in_text_selection) - .child( - h_flex() - .gap_1() - .child( - Icon::new(IconName::Image) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .child( - Label::new(label.clone()) - .size(LabelSize::Small) - .buffer_font(cx) - .single_line(), - ), - ) - .hoverable_tooltip({ - let image_task = image_task.clone(); - move |_, cx| { - let image = image_task.peek().cloned().transpose().ok().flatten(); - let image_task = image_task.clone(); - cx.new::(|cx| ImageHover { - image, - _task: cx.spawn(async move |this, cx| { - if let Ok(image) = image_task.clone().await { - this.update(cx, |this, cx| { - if this.image.replace(image).is_none() { - cx.notify(); - } - }) - .ok(); - } - }), - }) - .into() - } - }) - .into_any_element() + let loading = cx.new(|cx| { + let loading = cx.spawn(async move |this, cx| { + loading_finished.recv().await; + this.update(cx, |this: &mut LoadingContext, cx| { + this.loading = None; + cx.notify(); + }) + .ok(); + }); + LoadingContext { + id: cx.entity_id(), + label, + icon, + range, + editor, + loading: Some(loading), + image: image_task.clone(), } - }) + }); + Arc::new(move |_fold_id, _fold_range, _cx| loading.clone().into_any_element()) +} + +struct LoadingContext { + id: EntityId, + label: SharedString, + icon: SharedString, + range: Range, + editor: WeakEntity, + loading: Option>, + image: Option, String>>>>, +} + +impl Render for LoadingContext { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let is_in_text_selection = self + .editor + .update(cx, |editor, cx| editor.is_range_selected(&self.range, cx)) + .unwrap_or_default(); + ButtonLike::new(("loading-context", self.id)) + .style(ButtonStyle::Filled) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .toggle_state(is_in_text_selection) + .when_some(self.image.clone(), |el, image_task| { + el.hoverable_tooltip(move |_, cx| { + let image = image_task.peek().cloned().transpose().ok().flatten(); + let image_task = image_task.clone(); + cx.new::(|cx| ImageHover { + image, + _task: cx.spawn(async move |this, cx| { + if let Ok(image) = image_task.clone().await { + this.update(cx, |this, cx| { + if this.image.replace(image).is_none() { + cx.notify(); + } + }) + .ok(); + } + }), + }) + .into() + }) + }) + .child( + h_flex() + .gap_1() + .child( + Icon::from_path(self.icon.clone()) + .size(IconSize::XSmall) + .color(Color::Muted), + ) + .child( + Label::new(self.label.clone()) + .size(LabelSize::Small) + .buffer_font(cx) + .single_line(), + ) + .map(|el| { + if self.loading.is_some() { + el.with_animation( + "loading-context-crease", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.4, 0.8)), + |label, delta| label.opacity(delta), + ) + .into_any() + } else { + el.into_any() + } + }), + ) + } } struct ImageHover { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 87928767c6..3ad3ecbf61 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -277,6 +277,7 @@ pub struct AcpThreadView { terminal_expanded: bool, editing_message: Option, prompt_capabilities: Rc>, + is_loading_contents: bool, _cancel_task: Option>, _subscriptions: [Subscription; 3], } @@ -389,6 +390,7 @@ impl AcpThreadView { history_store, hovered_recent_history_item: None, prompt_capabilities, + is_loading_contents: false, _subscriptions: subscriptions, _cancel_task: None, focus_handle: cx.focus_handle(), @@ -823,6 +825,11 @@ impl AcpThreadView { fn send(&mut self, window: &mut Window, cx: &mut Context) { let Some(thread) = self.thread() else { return }; + + if self.is_loading_contents { + return; + } + self.history_store.update(cx, |history, cx| { history.push_recently_opened_entry( HistoryEntryId::AcpThread(thread.read(cx).session_id().clone()), @@ -876,6 +883,15 @@ impl AcpThreadView { let Some(thread) = self.thread().cloned() else { return; }; + + self.is_loading_contents = true; + let guard = cx.new(|_| ()); + cx.observe_release(&guard, |this, _guard, cx| { + this.is_loading_contents = false; + cx.notify(); + }) + .detach(); + let task = cx.spawn_in(window, async move |this, cx| { let (contents, tracked_buffers) = contents.await?; @@ -896,6 +912,7 @@ impl AcpThreadView { action_log.buffer_read(buffer, cx) } }); + drop(guard); thread.send(contents, cx) })?; send.await @@ -950,19 +967,24 @@ impl AcpThreadView { let Some(thread) = self.thread().cloned() else { return; }; + if self.is_loading_contents { + return; + } - let Some(rewind) = thread.update(cx, |thread, cx| { - let user_message_id = thread.entries().get(entry_ix)?.user_message()?.id.clone()?; - Some(thread.rewind(user_message_id, cx)) + let Some(user_message_id) = thread.update(cx, |thread, _| { + thread.entries().get(entry_ix)?.user_message()?.id.clone() }) else { return; }; let contents = message_editor.update(cx, |message_editor, cx| message_editor.contents(cx)); - let task = cx.foreground_executor().spawn(async move { - rewind.await?; - contents.await + let task = cx.spawn(async move |_, cx| { + let contents = contents.await?; + thread + .update(cx, |thread, cx| thread.rewind(user_message_id, cx))? + .await?; + Ok(contents) }); self.send_impl(task, window, cx); } @@ -1341,25 +1363,34 @@ impl AcpThreadView { base_container .child( IconButton::new("cancel", IconName::Close) + .disabled(self.is_loading_contents) .icon_color(Color::Error) .icon_size(IconSize::XSmall) .on_click(cx.listener(Self::cancel_editing)) ) .child( - IconButton::new("regenerate", IconName::Return) - .icon_color(Color::Muted) - .icon_size(IconSize::XSmall) - .tooltip(Tooltip::text( - "Editing will restart the thread from this point." - )) - .on_click(cx.listener({ - let editor = editor.clone(); - move |this, _, window, cx| { - this.regenerate( - entry_ix, &editor, window, cx, - ); - } - })), + if self.is_loading_contents { + div() + .id("loading-edited-message-content") + .tooltip(Tooltip::text("Loading Added Context…")) + .child(loading_contents_spinner(IconSize::XSmall)) + .into_any_element() + } else { + IconButton::new("regenerate", IconName::Return) + .icon_color(Color::Muted) + .icon_size(IconSize::XSmall) + .tooltip(Tooltip::text( + "Editing will restart the thread from this point." + )) + .on_click(cx.listener({ + let editor = editor.clone(); + move |this, _, window, cx| { + this.regenerate( + entry_ix, &editor, window, cx, + ); + } + })).into_any_element() + } ) ) } else { @@ -3542,7 +3573,14 @@ impl AcpThreadView { .thread() .is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle); - if is_generating && is_editor_empty { + if self.is_loading_contents { + div() + .id("loading-message-content") + .px_1() + .tooltip(Tooltip::text("Loading Added Context…")) + .child(loading_contents_spinner(IconSize::default())) + .into_any_element() + } else if is_generating && is_editor_empty { IconButton::new("stop-generation", IconName::Stop) .icon_color(Color::Error) .style(ButtonStyle::Tinted(ui::TintColor::Error)) @@ -4643,6 +4681,18 @@ impl AcpThreadView { } } +fn loading_contents_spinner(size: IconSize) -> AnyElement { + Icon::new(IconName::LoadCircle) + .size(size) + .color(Color::Accent) + .with_animation( + "load_context_circle", + Animation::new(Duration::from_secs(3)).repeat(), + |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + ) + .into_any_element() +} + impl Focusable for AcpThreadView { fn focus_handle(&self, cx: &App) -> FocusHandle { match self.thread_state { From 1b91f3de41bf86c1792d9bd4a8677a222ca4d903 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Sat, 23 Aug 2025 20:02:23 -0400 Subject: [PATCH 641/693] acp: Fix accidentally reverted thread view changes (#36825) Merge conflict resolution for #36741 accidentally reverted the changes in #36670 to allow expanding terminals individually and in #36675 to allow collapsing edit cards. This PR re-applies those changes, fixing the regression. Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 27 +++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 3ad3ecbf61..d62ccf4cef 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -274,7 +274,6 @@ pub struct AcpThreadView { edits_expanded: bool, plan_expanded: bool, editor_expanded: bool, - terminal_expanded: bool, editing_message: Option, prompt_capabilities: Rc>, is_loading_contents: bool, @@ -386,7 +385,6 @@ impl AcpThreadView { edits_expanded: false, plan_expanded: false, editor_expanded: false, - terminal_expanded: true, history_store, hovered_recent_history_item: None, prompt_capabilities, @@ -1722,10 +1720,9 @@ impl AcpThreadView { matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some(); let use_card_layout = needs_confirmation || is_edit; - let is_collapsible = !tool_call.content.is_empty() && !use_card_layout; + let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation; - let is_open = - needs_confirmation || is_edit || self.expanded_tool_calls.contains(&tool_call.id); + let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id); let gradient_overlay = |color: Hsla| { div() @@ -2195,6 +2192,8 @@ impl AcpThreadView { .map(|path| format!("{}", path.display())) .unwrap_or_else(|| "current directory".to_string()); + let is_expanded = self.expanded_tool_calls.contains(&tool_call.id); + let header = h_flex() .id(SharedString::from(format!( "terminal-tool-header-{}", @@ -2328,21 +2327,27 @@ impl AcpThreadView { "terminal-tool-disclosure-{}", terminal.entity_id() )), - self.terminal_expanded, + is_expanded, ) .opened_icon(IconName::ChevronUp) .closed_icon(IconName::ChevronDown) - .on_click(cx.listener(move |this, _event, _window, _cx| { - this.terminal_expanded = !this.terminal_expanded; - })), - ); + .on_click(cx.listener({ + let id = tool_call.id.clone(); + move |this, _event, _window, _cx| { + if is_expanded { + this.expanded_tool_calls.remove(&id); + } else { + this.expanded_tool_calls.insert(id.clone()); + } + }})), + ); let terminal_view = self .entry_view_state .read(cx) .entry(entry_ix) .and_then(|entry| entry.terminal(terminal)); - let show_output = self.terminal_expanded && terminal_view.is_some(); + let show_output = is_expanded && terminal_view.is_some(); v_flex() .mb_2() From de5f87e8f24eea848baa07fa733134b76a96dbce Mon Sep 17 00:00:00 2001 From: versecafe <147033096+versecafe@users.noreply.github.com> Date: Sat, 23 Aug 2025 23:54:47 -0700 Subject: [PATCH 642/693] languages: Add `module` to TS/JS keywords (#36830) image Release Notes: - Improved syntax highlights for `module` keyword in TS/JS --- crates/languages/src/javascript/highlights.scm | 3 ++- crates/languages/src/tsx/highlights.scm | 3 ++- crates/languages/src/typescript/highlights.scm | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/languages/src/javascript/highlights.scm b/crates/languages/src/javascript/highlights.scm index 9d5ebbaf71..ebeac7efff 100644 --- a/crates/languages/src/javascript/highlights.scm +++ b/crates/languages/src/javascript/highlights.scm @@ -231,6 +231,7 @@ "implements" "interface" "keyof" + "module" "namespace" "private" "protected" @@ -250,4 +251,4 @@ (jsx_closing_element ([""]) @punctuation.bracket.jsx) (jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx) (jsx_attribute "=" @punctuation.delimiter.jsx) -(jsx_text) @text.jsx \ No newline at end of file +(jsx_text) @text.jsx diff --git a/crates/languages/src/tsx/highlights.scm b/crates/languages/src/tsx/highlights.scm index 5e2fbbf63a..f7cb987831 100644 --- a/crates/languages/src/tsx/highlights.scm +++ b/crates/languages/src/tsx/highlights.scm @@ -237,6 +237,7 @@ "implements" "interface" "keyof" + "module" "namespace" "private" "protected" @@ -256,4 +257,4 @@ (jsx_closing_element ([""]) @punctuation.bracket.jsx) (jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx) (jsx_attribute "=" @punctuation.delimiter.jsx) -(jsx_text) @text.jsx \ No newline at end of file +(jsx_text) @text.jsx diff --git a/crates/languages/src/typescript/highlights.scm b/crates/languages/src/typescript/highlights.scm index af37ef6415..84cbbae77d 100644 --- a/crates/languages/src/typescript/highlights.scm +++ b/crates/languages/src/typescript/highlights.scm @@ -248,6 +248,7 @@ "is" "keyof" "let" + "module" "namespace" "new" "of" @@ -272,4 +273,4 @@ "while" "with" "yield" -] @keyword \ No newline at end of file +] @keyword From dd6fce6d4eafdc6b2463e28af029a3e92b41e39f Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Sun, 24 Aug 2025 09:59:32 +0300 Subject: [PATCH 643/693] multi_buffer: Pre-allocate IDs when editing (#36819) Something I came across when looking at `edit_internal`. Potentially saves multiple re-allocations on an edit Release Notes: - N/A --- crates/multi_buffer/src/multi_buffer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index a54d38163d..e27cbf868a 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -835,7 +835,7 @@ impl MultiBuffer { this.convert_edits_to_buffer_edits(edits, &snapshot, &original_indent_columns); drop(snapshot); - let mut buffer_ids = Vec::new(); + let mut buffer_ids = Vec::with_capacity(buffer_edits.len()); for (buffer_id, mut edits) in buffer_edits { buffer_ids.push(buffer_id); edits.sort_by_key(|edit| edit.range.start); From 54c7d9dc5fc915cf979d2df8f514ebd4b8f3fb2d Mon Sep 17 00:00:00 2001 From: Chuqiao Feng Date: Sun, 24 Aug 2025 19:01:42 +0800 Subject: [PATCH 644/693] Fix crash when opening inspector on Windows debug build (#36829) --- Cargo.lock | 1 + crates/inspector_ui/Cargo.toml | 1 + crates/inspector_ui/src/div_inspector.rs | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 0575796034..c835b503ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8468,6 +8468,7 @@ dependencies = [ "theme", "ui", "util", + "util_macros", "workspace", "workspace-hack", "zed_actions", diff --git a/crates/inspector_ui/Cargo.toml b/crates/inspector_ui/Cargo.toml index 8e55a8a477..cefe888974 100644 --- a/crates/inspector_ui/Cargo.toml +++ b/crates/inspector_ui/Cargo.toml @@ -24,6 +24,7 @@ serde_json_lenient.workspace = true theme.workspace = true ui.workspace = true util.workspace = true +util_macros.workspace = true workspace-hack.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/inspector_ui/src/div_inspector.rs b/crates/inspector_ui/src/div_inspector.rs index 0c2b16b9f4..c3d687e57a 100644 --- a/crates/inspector_ui/src/div_inspector.rs +++ b/crates/inspector_ui/src/div_inspector.rs @@ -25,7 +25,7 @@ use util::split_str_with_ranges; /// Path used for unsaved buffer that contains style json. To support the json language server, this /// matches the name used in the generated schemas. -const ZED_INSPECTOR_STYLE_JSON: &str = "/zed-inspector-style.json"; +const ZED_INSPECTOR_STYLE_JSON: &str = util_macros::path!("/zed-inspector-style.json"); pub(crate) struct DivInspector { state: State, From d8bffd7ef298ccf6017e25299ce8d9d0bc1ba4aa Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Sun, 24 Aug 2025 13:05:39 +0200 Subject: [PATCH 645/693] acp: Cancel editing when focus is lost and message was not changed (#36822) Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 2 +- crates/agent_ui/src/acp/message_editor.rs | 16 ++++++++++------ crates/agent_ui/src/acp/thread_view.rs | 13 +++++++++++++ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index c748f22275..029d175054 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -509,7 +509,7 @@ impl ContentBlock { "`Image`".into() } - fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str { + pub fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str { match self { ContentBlock::Empty => "", ContentBlock::Markdown { markdown } => markdown.read(cx).source(), diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index bab42e3da2..70faa0ed27 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -74,6 +74,7 @@ pub enum MessageEditorEvent { Send, Cancel, Focus, + LostFocus, } impl EventEmitter for MessageEditor {} @@ -131,10 +132,14 @@ impl MessageEditor { editor }); - cx.on_focus(&editor.focus_handle(cx), window, |_, _, cx| { + cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| { cx.emit(MessageEditorEvent::Focus) }) .detach(); + cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| { + cx.emit(MessageEditorEvent::LostFocus) + }) + .detach(); let mut subscriptions = Vec::new(); subscriptions.push(cx.subscribe_in(&editor, window, { @@ -1169,17 +1174,16 @@ impl MessageEditor { }) } + pub fn text(&self, cx: &App) -> String { + self.editor.read(cx).text(cx) + } + #[cfg(test)] pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context) { self.editor.update(cx, |editor, cx| { editor.set_text(text, window, cx); }); } - - #[cfg(test)] - pub fn text(&self, cx: &App) -> String { - self.editor.read(cx).text(cx) - } } fn render_directory_contents(entries: Vec<(Arc, PathBuf, String)>) -> String { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index d62ccf4cef..9caa4bad8c 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -762,6 +762,7 @@ impl AcpThreadView { MessageEditorEvent::Focus => { self.cancel_editing(&Default::default(), window, cx); } + MessageEditorEvent::LostFocus => {} } } @@ -793,6 +794,18 @@ impl AcpThreadView { cx.notify(); } } + ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::LostFocus) => { + if let Some(thread) = self.thread() + && let Some(AgentThreadEntry::UserMessage(user_message)) = + thread.read(cx).entries().get(event.entry_index) + && user_message.id.is_some() + { + if editor.read(cx).text(cx).as_str() == user_message.content.to_markdown(cx) { + self.editing_message = None; + cx.notify(); + } + } + } ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => { self.regenerate(event.entry_index, editor, window, cx); } From a79aef7bdd38668215f1e916ef866f029ba8d9cb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sun, 24 Aug 2025 18:30:34 +0200 Subject: [PATCH 646/693] acp: Never build a request with a tool use without its corresponding result (#36847) Release Notes: - N/A --- crates/agent2/src/tests/mod.rs | 76 ++++++++++++++++++++++++++++++++++ crates/agent2/src/thread.rs | 74 ++++++++++++++++----------------- 2 files changed, 113 insertions(+), 37 deletions(-) diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 60b3198081..5b935dae4c 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -4,6 +4,7 @@ use agent_client_protocol::{self as acp}; use agent_settings::AgentProfileId; use anyhow::Result; use client::{Client, UserStore}; +use cloud_llm_client::CompletionIntent; use context_server::{ContextServer, ContextServerCommand, ContextServerId}; use fs::{FakeFs, Fs}; use futures::{ @@ -1737,6 +1738,81 @@ async fn test_title_generation(cx: &mut TestAppContext) { thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world")); } +#[gpui::test] +async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) { + let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let _events = thread + .update(cx, |thread, cx| { + thread.add_tool(ToolRequiringPermission); + thread.add_tool(EchoTool); + thread.send(UserMessageId::new(), ["Hey!"], cx) + }) + .unwrap(); + cx.run_until_parked(); + + let permission_tool_use = LanguageModelToolUse { + id: "tool_id_1".into(), + name: ToolRequiringPermission::name().into(), + raw_input: "{}".into(), + input: json!({}), + is_input_complete: true, + }; + let echo_tool_use = LanguageModelToolUse { + id: "tool_id_2".into(), + name: EchoTool::name().into(), + raw_input: json!({"text": "test"}).to_string(), + input: json!({"text": "test"}), + is_input_complete: true, + }; + fake_model.send_last_completion_stream_text_chunk("Hi!"); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + permission_tool_use, + )); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + echo_tool_use.clone(), + )); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + // Ensure pending tools are skipped when building a request. + let request = thread + .read_with(cx, |thread, cx| { + thread.build_completion_request(CompletionIntent::EditFile, cx) + }) + .unwrap(); + assert_eq!( + request.messages[1..], + vec![ + LanguageModelRequestMessage { + role: Role::User, + content: vec!["Hey!".into()], + cache: true + }, + LanguageModelRequestMessage { + role: Role::Assistant, + content: vec![ + MessageContent::Text("Hi!".into()), + MessageContent::ToolUse(echo_tool_use.clone()) + ], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec![MessageContent::ToolResult(LanguageModelToolResult { + tool_use_id: echo_tool_use.id.clone(), + tool_name: echo_tool_use.name, + is_error: false, + content: "test".into(), + output: Some("test".into()) + })], + cache: false + }, + ], + ); +} + #[gpui::test] async fn test_agent_connection(cx: &mut TestAppContext) { cx.update(settings::init); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 6d616f73fc..c000027368 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -448,24 +448,33 @@ impl AgentMessage { cache: false, }; for chunk in &self.content { - let chunk = match chunk { + match chunk { AgentMessageContent::Text(text) => { - language_model::MessageContent::Text(text.clone()) + assistant_message + .content + .push(language_model::MessageContent::Text(text.clone())); } AgentMessageContent::Thinking { text, signature } => { - language_model::MessageContent::Thinking { - text: text.clone(), - signature: signature.clone(), - } + assistant_message + .content + .push(language_model::MessageContent::Thinking { + text: text.clone(), + signature: signature.clone(), + }); } AgentMessageContent::RedactedThinking(value) => { - language_model::MessageContent::RedactedThinking(value.clone()) + assistant_message.content.push( + language_model::MessageContent::RedactedThinking(value.clone()), + ); } - AgentMessageContent::ToolUse(value) => { - language_model::MessageContent::ToolUse(value.clone()) + AgentMessageContent::ToolUse(tool_use) => { + if self.tool_results.contains_key(&tool_use.id) { + assistant_message + .content + .push(language_model::MessageContent::ToolUse(tool_use.clone())); + } } }; - assistant_message.content.push(chunk); } let mut user_message = LanguageModelRequestMessage { @@ -1315,23 +1324,6 @@ impl Thread { } } - pub fn build_system_message(&self, cx: &App) -> LanguageModelRequestMessage { - log::debug!("Building system message"); - let prompt = SystemPromptTemplate { - project: self.project_context.read(cx), - available_tools: self.tools.keys().cloned().collect(), - } - .render(&self.templates) - .context("failed to build system prompt") - .expect("Invalid template"); - log::debug!("System message built"); - LanguageModelRequestMessage { - role: Role::System, - content: vec![prompt.into()], - cache: true, - } - } - /// A helper method that's called on every streamed completion event. /// Returns an optional tool result task, which the main agentic loop will /// send back to the model when it resolves. @@ -1773,7 +1765,7 @@ impl Thread { pub(crate) fn build_completion_request( &self, completion_intent: CompletionIntent, - cx: &mut App, + cx: &App, ) -> Result { let model = self.model().context("No language model configured")?; let tools = if let Some(turn) = self.running_turn.as_ref() { @@ -1894,21 +1886,29 @@ impl Thread { "Building request messages from {} thread messages", self.messages.len() ); - let mut messages = vec![self.build_system_message(cx)]; + + let system_prompt = SystemPromptTemplate { + project: self.project_context.read(cx), + available_tools: self.tools.keys().cloned().collect(), + } + .render(&self.templates) + .context("failed to build system prompt") + .expect("Invalid template"); + let mut messages = vec![LanguageModelRequestMessage { + role: Role::System, + content: vec![system_prompt.into()], + cache: false, + }]; for message in &self.messages { messages.extend(message.to_request()); } - if let Some(message) = self.pending_message.as_ref() { - messages.extend(message.to_request()); + if let Some(last_message) = messages.last_mut() { + last_message.cache = true; } - if let Some(last_user_message) = messages - .iter_mut() - .rev() - .find(|message| message.role == Role::User) - { - last_user_message.cache = true; + if let Some(message) = self.pending_message.as_ref() { + messages.extend(message.to_request()); } messages From 11545c669e100392a8ca60063476037ab52c7cb5 Mon Sep 17 00:00:00 2001 From: Aleksei Gusev Date: Sun, 24 Aug 2025 19:57:12 +0300 Subject: [PATCH 647/693] Add file icons to multibuffer view (#36836) multi-buffer-icons-git-diff Unfortunately, `cargo format` decided to reformat everything. Probably, because of hitting the right margin, no idea. The essence of this change is the following: ```rust .map(|path_header| { let filename = filename .map(SharedString::from) .unwrap_or_else(|| "untitled".into()); let path = path::Path::new(filename.as_str()); let icon = FileIcons::get_icon(path, cx).unwrap_or_default(); let icon = Icon::from_path(icon).color(Color::Muted); let label = Label::new(filename).single_line().when_some( file_status, |el, status| { el.color(if status.is_conflicted() { Color::Conflict } else if status.is_modified() { Color::Modified } else if status.is_deleted() { Color::Disabled } else { Color::Created }) .when(status.is_deleted(), |el| el.strikethrough()) }, ); path_header.child(icon).child(label) }) ``` Release Notes: - Added file icons to multi buffer view --- crates/editor/src/element.rs | 339 ++++++++++++++++++----------------- 1 file changed, 175 insertions(+), 164 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 32582ba941..4f3580da07 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -74,7 +74,7 @@ use std::{ fmt::{self, Write}, iter, mem, ops::{Deref, Range}, - path::Path, + path::{self, Path}, rc::Rc, sync::Arc, time::{Duration, Instant}, @@ -90,8 +90,8 @@ use unicode_segmentation::UnicodeSegmentation; use util::post_inc; use util::{RangeExt, ResultExt, debug_panic}; use workspace::{ - CollaboratorId, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, item::Item, - notifications::NotifyTaskExt, + CollaboratorId, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, + item::Item, notifications::NotifyTaskExt, }; /// Determines what kinds of highlights should be applied to a lines background. @@ -3603,176 +3603,187 @@ impl EditorElement { let focus_handle = editor.focus_handle(cx); let colors = cx.theme().colors(); - let header = - div() - .p_1() - .w_full() - .h(FILE_HEADER_HEIGHT as f32 * window.line_height()) - .child( - h_flex() - .size_full() - .gap_2() - .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) - .pl_0p5() - .pr_5() - .rounded_sm() - .when(is_sticky, |el| el.shadow_md()) - .border_1() - .map(|div| { - let border_color = if is_selected - && is_folded - && focus_handle.contains_focused(window, cx) - { - colors.border_focused - } else { - colors.border - }; - div.border_color(border_color) - }) - .bg(colors.editor_subheader_background) - .hover(|style| style.bg(colors.element_hover)) - .map(|header| { - let editor = self.editor.clone(); - let buffer_id = for_excerpt.buffer_id; - let toggle_chevron_icon = - FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path); - header.child( - div() - .hover(|style| style.bg(colors.element_selected)) - .rounded_xs() - .child( - ButtonLike::new("toggle-buffer-fold") - .style(ui::ButtonStyle::Transparent) - .height(px(28.).into()) - .width(px(28.)) - .children(toggle_chevron_icon) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::with_meta_in( - "Toggle Excerpt Fold", - Some(&ToggleFold), - "Alt+click to toggle all", - &focus_handle, + let header = div() + .p_1() + .w_full() + .h(FILE_HEADER_HEIGHT as f32 * window.line_height()) + .child( + h_flex() + .size_full() + .gap_2() + .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) + .pl_0p5() + .pr_5() + .rounded_sm() + .when(is_sticky, |el| el.shadow_md()) + .border_1() + .map(|div| { + let border_color = if is_selected + && is_folded + && focus_handle.contains_focused(window, cx) + { + colors.border_focused + } else { + colors.border + }; + div.border_color(border_color) + }) + .bg(colors.editor_subheader_background) + .hover(|style| style.bg(colors.element_hover)) + .map(|header| { + let editor = self.editor.clone(); + let buffer_id = for_excerpt.buffer_id; + let toggle_chevron_icon = + FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path); + header.child( + div() + .hover(|style| style.bg(colors.element_selected)) + .rounded_xs() + .child( + ButtonLike::new("toggle-buffer-fold") + .style(ui::ButtonStyle::Transparent) + .height(px(28.).into()) + .width(px(28.)) + .children(toggle_chevron_icon) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::with_meta_in( + "Toggle Excerpt Fold", + Some(&ToggleFold), + "Alt+click to toggle all", + &focus_handle, + window, + cx, + ) + } + }) + .on_click(move |event, window, cx| { + if event.modifiers().alt { + // Alt+click toggles all buffers + editor.update(cx, |editor, cx| { + editor.toggle_fold_all( + &ToggleFoldAll, window, cx, - ) - } - }) - .on_click(move |event, window, cx| { - if event.modifiers().alt { - // Alt+click toggles all buffers + ); + }); + } else { + // Regular click toggles single buffer + if is_folded { editor.update(cx, |editor, cx| { - editor.toggle_fold_all( - &ToggleFoldAll, - window, - cx, - ); + editor.unfold_buffer(buffer_id, cx); }); } else { - // Regular click toggles single buffer - if is_folded { - editor.update(cx, |editor, cx| { - editor.unfold_buffer(buffer_id, cx); - }); - } else { - editor.update(cx, |editor, cx| { - editor.fold_buffer(buffer_id, cx); - }); - } + editor.update(cx, |editor, cx| { + editor.fold_buffer(buffer_id, cx); + }); } - }), - ), - ) - }) - .children( - editor - .addons - .values() - .filter_map(|addon| { - addon.render_buffer_header_controls(for_excerpt, window, cx) - }) - .take(1), + } + }), + ), ) - .child( - h_flex() - .size(Pixels(12.0)) - .justify_center() - .children(indicator), - ) - .child( - h_flex() - .cursor_pointer() - .id("path header block") - .size_full() - .justify_between() - .overflow_hidden() - .child( - h_flex() - .gap_2() - .child( - Label::new( - filename - .map(SharedString::from) - .unwrap_or_else(|| "untitled".into()), - ) - .single_line() - .when_some(file_status, |el, status| { - el.color(if status.is_conflicted() { - Color::Conflict - } else if status.is_modified() { - Color::Modified - } else if status.is_deleted() { - Color::Disabled - } else { - Color::Created - }) - .when(status.is_deleted(), |el| el.strikethrough()) - }), - ) - .when_some(parent_path, |then, path| { - then.child(div().child(path).text_color( - if file_status.is_some_and(FileStatus::is_deleted) { - colors.text_disabled - } else { - colors.text_muted + }) + .children( + editor + .addons + .values() + .filter_map(|addon| { + addon.render_buffer_header_controls(for_excerpt, window, cx) + }) + .take(1), + ) + .child( + h_flex() + .size(Pixels(12.0)) + .justify_center() + .children(indicator), + ) + .child( + h_flex() + .cursor_pointer() + .id("path header block") + .size_full() + .justify_between() + .overflow_hidden() + .child( + h_flex() + .gap_2() + .map(|path_header| { + let filename = filename + .map(SharedString::from) + .unwrap_or_else(|| "untitled".into()); + + path_header + .when(ItemSettings::get_global(cx).file_icons, |el| { + let path = path::Path::new(filename.as_str()); + let icon = FileIcons::get_icon(path, cx) + .unwrap_or_default(); + let icon = + Icon::from_path(icon).color(Color::Muted); + el.child(icon) + }) + .child(Label::new(filename).single_line().when_some( + file_status, + |el, status| { + el.color(if status.is_conflicted() { + Color::Conflict + } else if status.is_modified() { + Color::Modified + } else if status.is_deleted() { + Color::Disabled + } else { + Color::Created + }) + .when(status.is_deleted(), |el| { + el.strikethrough() + }) }, )) - }), - ) - .when( - can_open_excerpts && is_selected && relative_path.is_some(), - |el| { - el.child( - h_flex() - .id("jump-to-file-button") - .gap_2p5() - .child(Label::new("Jump To File")) - .children( - KeyBinding::for_action_in( - &OpenExcerpts, - &focus_handle, - window, - cx, - ) - .map(|binding| binding.into_any_element()), - ), - ) - }, - ) - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .on_click(window.listener_for(&self.editor, { - move |editor, e: &ClickEvent, window, cx| { - editor.open_excerpts_common( - Some(jump_data.clone()), - e.modifiers().secondary(), - window, - cx, - ); - } - })), - ), - ); + }) + .when_some(parent_path, |then, path| { + then.child(div().child(path).text_color( + if file_status.is_some_and(FileStatus::is_deleted) { + colors.text_disabled + } else { + colors.text_muted + }, + )) + }), + ) + .when( + can_open_excerpts && is_selected && relative_path.is_some(), + |el| { + el.child( + h_flex() + .id("jump-to-file-button") + .gap_2p5() + .child(Label::new("Jump To File")) + .children( + KeyBinding::for_action_in( + &OpenExcerpts, + &focus_handle, + window, + cx, + ) + .map(|binding| binding.into_any_element()), + ), + ) + }, + ) + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .on_click(window.listener_for(&self.editor, { + move |editor, e: &ClickEvent, window, cx| { + editor.open_excerpts_common( + Some(jump_data.clone()), + e.modifiers().secondary(), + window, + cx, + ); + } + })), + ), + ); let file = for_excerpt.buffer.file().cloned(); let editor = self.editor.clone(); From c48197b2804a17ecf8ec46781985d4f9cff35e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hendrik=20M=C3=BCller?= Date: Mon, 25 Aug 2025 11:28:33 +0200 Subject: [PATCH 648/693] util: Fix edge case when parsing paths (#36025) Searching for files broke a couple releases ago. It used to be possible to start typing part of a file name, then select a file (not confirm it yet) and then type in `:` and a line number to navigate directly to that line. The current behavior can be seen in the following screenshots. When the `:` is typed, the selection is lost, since no files match any more. Screenshot From 2025-08-12 10-36-08 Screenshot From 2025-08-12 10-36-25 Screenshot From 2025-08-12 10-36-47 --- With this PR, the previous behavior is restored and can be seen in these screenshots: Screenshot From 2025-08-12 10-36-08 Screenshot From 2025-08-12 10-47-07 Screenshot From 2025-08-12 10-47-21 --- Release Notes: - Adjusted the file finder to show matching file paths when adding the `:row:column` to the query --- crates/file_finder/src/file_finder.rs | 9 ++-- crates/file_finder/src/file_finder_tests.rs | 48 +++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 8aaaa04729..7512152324 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1401,13 +1401,16 @@ impl PickerDelegate for FileFinderDelegate { #[cfg(windows)] let raw_query = raw_query.trim().to_owned().replace("/", "\\"); #[cfg(not(windows))] - let raw_query = raw_query.trim().to_owned(); + let raw_query = raw_query.trim(); - let file_query_end = if path_position.path.to_str().unwrap_or(&raw_query) == raw_query { + let raw_query = raw_query.trim_end_matches(':').to_owned(); + let path = path_position.path.to_str(); + let path_trimmed = path.unwrap_or(&raw_query).trim_end_matches(':'); + let file_query_end = if path_trimmed == raw_query { None } else { // Safe to unwrap as we won't get here when the unwrap in if fails - Some(path_position.path.to_str().unwrap().len()) + Some(path.unwrap().len()) }; let query = FileSearchQuery { diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index 8203d1b1fd..cd0f203d6a 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -218,6 +218,7 @@ async fn test_matching_paths(cx: &mut TestAppContext) { " ndan ", " band ", "a bandana", + "bandana:", ] { picker .update_in(cx, |picker, window, cx| { @@ -252,6 +253,53 @@ async fn test_matching_paths(cx: &mut TestAppContext) { } } +#[gpui::test] +async fn test_matching_paths_with_colon(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + path!("/root"), + json!({ + "a": { + "foo:bar.rs": "", + "foo.rs": "", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; + + let (picker, _, cx) = build_find_picker(project, cx); + + // 'foo:' matches both files + cx.simulate_input("foo:"); + picker.update(cx, |picker, _| { + assert_eq!(picker.delegate.matches.len(), 3); + assert_match_at_position(picker, 0, "foo.rs"); + assert_match_at_position(picker, 1, "foo:bar.rs"); + }); + + // 'foo:b' matches one of the files + cx.simulate_input("b"); + picker.update(cx, |picker, _| { + assert_eq!(picker.delegate.matches.len(), 2); + assert_match_at_position(picker, 0, "foo:bar.rs"); + }); + + cx.dispatch_action(editor::actions::Backspace); + + // 'foo:1' matches both files, specifying which row to jump to + cx.simulate_input("1"); + picker.update(cx, |picker, _| { + assert_eq!(picker.delegate.matches.len(), 3); + assert_match_at_position(picker, 0, "foo.rs"); + assert_match_at_position(picker, 1, "foo:bar.rs"); + }); +} + #[gpui::test] async fn test_unicode_paths(cx: &mut TestAppContext) { let app_state = init_test(cx); From fe5e81203f03e86ada3397b1738b9f0f79801368 Mon Sep 17 00:00:00 2001 From: versecafe <147033096+versecafe@users.noreply.github.com> Date: Mon, 25 Aug 2025 03:55:56 -0700 Subject: [PATCH 649/693] Fix macOS arch reporting from `arch_ios` to `arch_arm` (#36217) ```xml arch_kind arch_arm ``` Closes #36037 Release Notes: - N/A --- crates/zed/resources/info/SupportedPlatforms.plist | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 crates/zed/resources/info/SupportedPlatforms.plist diff --git a/crates/zed/resources/info/SupportedPlatforms.plist b/crates/zed/resources/info/SupportedPlatforms.plist new file mode 100644 index 0000000000..fd2a4101d8 --- /dev/null +++ b/crates/zed/resources/info/SupportedPlatforms.plist @@ -0,0 +1,4 @@ +CFBundleSupportedPlatforms + + MacOSX + From dfc99de7b8c3796ded1c5ec73b585f85ef7bc783 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 25 Aug 2025 08:18:23 -0300 Subject: [PATCH 650/693] thread view: Add a few UI tweaks (#36845) Release Notes: - N/A --- assets/icons/copy.svg | 5 +- crates/agent_ui/src/acp/thread_view.rs | 76 +++++++++++--------------- crates/markdown/src/markdown.rs | 11 ++-- 3 files changed, 43 insertions(+), 49 deletions(-) diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg index bca13f8d56..aba193930b 100644 --- a/assets/icons/copy.svg +++ b/assets/icons/copy.svg @@ -1 +1,4 @@ - + + + + diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 9caa4bad8c..0b987e25b6 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1306,7 +1306,11 @@ impl AcpThreadView { v_flex() .id(("user_message", entry_ix)) - .pt_2() + .map(|this| if rules_item.is_some() { + this.pt_3() + } else { + this.pt_2() + }) .pb_4() .px_2() .gap_1p5() @@ -1315,6 +1319,7 @@ impl AcpThreadView { .children(message.id.clone().and_then(|message_id| { message.checkpoint.as_ref()?.show.then(|| { h_flex() + .px_3() .gap_2() .child(Divider::horizontal()) .child( @@ -1492,9 +1497,7 @@ impl AcpThreadView { .child(self.render_thread_controls(cx)) .when_some( self.thread_feedback.comments_editor.clone(), - |this, editor| { - this.child(Self::render_feedback_feedback_editor(editor, window, cx)) - }, + |this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)), ) .into_any_element() } else { @@ -1725,6 +1728,7 @@ impl AcpThreadView { tool_call.status, ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed ); + let needs_confirmation = matches!( tool_call.status, ToolCallStatus::WaitingForConfirmation { .. } @@ -1742,7 +1746,7 @@ impl AcpThreadView { .absolute() .top_0() .right_0() - .w_16() + .w_12() .h_full() .bg(linear_gradient( 90., @@ -1902,7 +1906,7 @@ impl AcpThreadView { .into_any() }), ) - .when(in_progress && use_card_layout, |this| { + .when(in_progress && use_card_layout && !is_open, |this| { this.child( div().absolute().right_2().child( Icon::new(IconName::ArrowCircle) @@ -2460,7 +2464,6 @@ impl AcpThreadView { Some( h_flex() .px_2p5() - .pb_1() .child( Icon::new(IconName::Attach) .size(IconSize::XSmall) @@ -2476,8 +2479,7 @@ impl AcpThreadView { Label::new(user_rules_text) .size(LabelSize::XSmall) .color(Color::Muted) - .truncate() - .buffer_font(cx), + .truncate(), ) .hover(|s| s.bg(cx.theme().colors().element_hover)) .tooltip(Tooltip::text("View User Rules")) @@ -2491,7 +2493,13 @@ impl AcpThreadView { }), ) }) - .when(has_both, |this| this.child(Divider::vertical())) + .when(has_both, |this| { + this.child( + Label::new("•") + .size(LabelSize::XSmall) + .color(Color::Disabled), + ) + }) .when_some(rules_file_text, |parent, rules_file_text| { parent.child( h_flex() @@ -2500,8 +2508,7 @@ impl AcpThreadView { .child( Label::new(rules_file_text) .size(LabelSize::XSmall) - .color(Color::Muted) - .buffer_font(cx), + .color(Color::Muted), ) .hover(|s| s.bg(cx.theme().colors().element_hover)) .tooltip(Tooltip::text("View Project Rules")) @@ -3078,13 +3085,13 @@ impl AcpThreadView { h_flex() .p_1() .justify_between() + .flex_wrap() .when(expanded, |this| { this.border_b_1().border_color(cx.theme().colors().border) }) .child( h_flex() .id("edits-container") - .w_full() .gap_1() .child(Disclosure::new("edits-disclosure", expanded)) .map(|this| { @@ -4177,13 +4184,8 @@ impl AcpThreadView { container.child(open_as_markdown).child(scroll_to_top) } - fn render_feedback_feedback_editor( - editor: Entity, - window: &mut Window, - cx: &Context, - ) -> Div { - let focus_handle = editor.focus_handle(cx); - v_flex() + fn render_feedback_feedback_editor(editor: Entity, cx: &Context) -> Div { + h_flex() .key_context("AgentFeedbackMessageEditor") .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| { this.thread_feedback.dismiss_comments(); @@ -4192,43 +4194,31 @@ impl AcpThreadView { .on_action(cx.listener(move |this, _: &menu::Confirm, _window, cx| { this.submit_feedback_message(cx); })) - .mb_2() - .mx_4() .p_2() + .mb_2() + .mx_5() + .gap_1() .rounded_md() .border_1() .border_color(cx.theme().colors().border) .bg(cx.theme().colors().editor_background) - .child(editor) + .child(div().w_full().child(editor)) .child( h_flex() - .gap_1() - .justify_end() .child( - Button::new("dismiss-feedback-message", "Cancel") - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in(&menu::Cancel, &focus_handle, window, cx) - .map(|kb| kb.size(rems_from_px(10.))), - ) + IconButton::new("dismiss-feedback-message", IconName::Close) + .icon_color(Color::Error) + .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) .on_click(cx.listener(move |this, _, _window, cx| { this.thread_feedback.dismiss_comments(); cx.notify(); })), ) .child( - Button::new("submit-feedback-message", "Share Feedback") - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in( - &menu::Confirm, - &focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(10.))), - ) + IconButton::new("submit-feedback-message", IconName::Return) + .icon_size(IconSize::XSmall) + .shape(ui::IconButtonShape::Square) .on_click(cx.listener(move |this, _, _window, cx| { this.submit_feedback_message(cx); })), diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 39a438c512..f16da45d79 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -1085,10 +1085,10 @@ impl Element for MarkdownElement { ); el.child( h_flex() - .w_5() + .w_4() .absolute() - .top_1() - .right_1() + .top_1p5() + .right_1p5() .justify_end() .child(codeblock), ) @@ -1115,11 +1115,12 @@ impl Element for MarkdownElement { cx, ); el.child( - div() + h_flex() + .w_4() .absolute() .top_0() .right_0() - .w_5() + .justify_end() .visible_on_hover("code_block") .child(codeblock), ) From 8c83281399d013aab4415b4e425063be5a02b89e Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 25 Aug 2025 08:23:36 -0400 Subject: [PATCH 651/693] acp: Fix read_file tool flickering (#36854) We were rendering a Markdown link like `[Read file x.rs (lines Y-Z)](@selection)` while the tool ran, but then switching to just `x.rs` as soon as we got the file location from the tool call (due to an if/else in the UI code that applies to all tools). This caused a flicker, which is fixed by having `initial_title` return just the filename from the input as it arrives instead of a link that we're going to stop rendering almost immediately anyway. Release Notes: - N/A --- crates/agent2/src/tools/read_file_tool.rs | 29 ++++++----------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index 903e1582ac..fea9732093 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -10,7 +10,7 @@ use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; -use std::sync::Arc; +use std::{path::Path, sync::Arc}; use crate::{AgentTool, ToolCallEventStream}; @@ -68,27 +68,12 @@ impl AgentTool for ReadFileTool { } fn initial_title(&self, input: Result) -> SharedString { - if let Ok(input) = input { - let path = &input.path; - match (input.start_line, input.end_line) { - (Some(start), Some(end)) => { - format!( - "[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))", - path, start, end, path, start, end - ) - } - (Some(start), None) => { - format!( - "[Read file `{}` (from line {})](@selection:{}:({}-{}))", - path, start, path, start, start - ) - } - _ => format!("[Read file `{}`](@file:{})", path, path), - } - .into() - } else { - "Read file".into() - } + input + .ok() + .as_ref() + .and_then(|input| Path::new(&input.path).file_name()) + .map(|file_name| file_name.to_string_lossy().to_string().into()) + .unwrap_or_default() } fn run( From 4c0ad95acc50c8bc509e5845991c811dbcf1a513 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 25 Aug 2025 14:52:25 +0200 Subject: [PATCH 652/693] acp: Show retry button for errors (#36862) Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra --- crates/acp_thread/src/acp_thread.rs | 6 +- crates/acp_thread/src/connection.rs | 8 +- crates/agent2/src/agent.rs | 8 +- crates/agent2/src/tests/mod.rs | 98 ++++++++++++++++--- crates/agent2/src/thread.rs | 124 +++++++++++++------------ crates/agent_ui/src/acp/thread_view.rs | 46 ++++++++- 6 files changed, 212 insertions(+), 78 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 029d175054..d9a7a2582a 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1373,6 +1373,10 @@ impl AcpThread { }) } + pub fn can_resume(&self, cx: &App) -> bool { + self.connection.resume(&self.session_id, cx).is_some() + } + pub fn resume(&mut self, cx: &mut Context) -> BoxFuture<'static, Result<()>> { self.run_turn(cx, async move |this, cx| { this.update(cx, |this, cx| { @@ -2659,7 +2663,7 @@ mod tests { fn truncate( &self, session_id: &acp::SessionId, - _cx: &mut App, + _cx: &App, ) -> Option> { Some(Rc::new(FakeAgentSessionEditor { _session_id: session_id.clone(), diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 91e46dbac1..5f5032e588 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -43,7 +43,7 @@ pub trait AgentConnection { fn resume( &self, _session_id: &acp::SessionId, - _cx: &mut App, + _cx: &App, ) -> Option> { None } @@ -53,7 +53,7 @@ pub trait AgentConnection { fn truncate( &self, _session_id: &acp::SessionId, - _cx: &mut App, + _cx: &App, ) -> Option> { None } @@ -61,7 +61,7 @@ pub trait AgentConnection { fn set_title( &self, _session_id: &acp::SessionId, - _cx: &mut App, + _cx: &App, ) -> Option> { None } @@ -439,7 +439,7 @@ mod test_support { fn truncate( &self, _session_id: &agent_client_protocol::SessionId, - _cx: &mut App, + _cx: &App, ) -> Option> { Some(Rc::new(StubAgentSessionEditor)) } diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 4eaf87e218..415933b7d1 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -936,7 +936,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { fn resume( &self, session_id: &acp::SessionId, - _cx: &mut App, + _cx: &App, ) -> Option> { Some(Rc::new(NativeAgentSessionResume { connection: self.clone(), @@ -956,9 +956,9 @@ impl acp_thread::AgentConnection for NativeAgentConnection { fn truncate( &self, session_id: &agent_client_protocol::SessionId, - cx: &mut App, + cx: &App, ) -> Option> { - self.0.update(cx, |agent, _cx| { + self.0.read_with(cx, |agent, _cx| { agent.sessions.get(session_id).map(|session| { Rc::new(NativeAgentSessionEditor { thread: session.thread.clone(), @@ -971,7 +971,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { fn set_title( &self, session_id: &acp::SessionId, - _cx: &mut App, + _cx: &App, ) -> Option> { Some(Rc::new(NativeAgentSessionSetTitle { connection: self.clone(), diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 5b935dae4c..87ecc1037c 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -5,6 +5,7 @@ use agent_settings::AgentProfileId; use anyhow::Result; use client::{Client, UserStore}; use cloud_llm_client::CompletionIntent; +use collections::IndexMap; use context_server::{ContextServer, ContextServerCommand, ContextServerId}; use fs::{FakeFs, Fs}; use futures::{ @@ -673,15 +674,6 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) { "} ) }); - - // Ensure we error if calling resume when tool use limit was *not* reached. - let error = thread - .update(cx, |thread, cx| thread.resume(cx)) - .unwrap_err(); - assert_eq!( - error.to_string(), - "can only resume after tool use limit is reached" - ) } #[gpui::test] @@ -2105,6 +2097,7 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) { .unwrap(); cx.run_until_parked(); + fake_model.send_last_completion_stream_text_chunk("Hey,"); fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded { provider: LanguageModelProviderName::new("Anthropic"), retry_after: Some(Duration::from_secs(3)), @@ -2114,8 +2107,9 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) { cx.executor().advance_clock(Duration::from_secs(3)); cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Hey!"); + fake_model.send_last_completion_stream_text_chunk("there!"); fake_model.end_last_completion_stream(); + cx.run_until_parked(); let mut retry_events = Vec::new(); while let Some(Ok(event)) = events.next().await { @@ -2143,12 +2137,94 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) { ## Assistant - Hey! + Hey, + + [resume] + + ## Assistant + + there! "} ) }); } +#[gpui::test] +async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) { + let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; + let fake_model = model.as_fake(); + + let events = thread + .update(cx, |thread, cx| { + thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx); + thread.add_tool(EchoTool); + thread.send(UserMessageId::new(), ["Call the echo tool!"], cx) + }) + .unwrap(); + cx.run_until_parked(); + + let tool_use_1 = LanguageModelToolUse { + id: "tool_1".into(), + name: EchoTool::name().into(), + raw_input: json!({"text": "test"}).to_string(), + input: json!({"text": "test"}), + is_input_complete: true, + }; + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + tool_use_1.clone(), + )); + fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded { + provider: LanguageModelProviderName::new("Anthropic"), + retry_after: Some(Duration::from_secs(3)), + }); + fake_model.end_last_completion_stream(); + + cx.executor().advance_clock(Duration::from_secs(3)); + let completion = fake_model.pending_completions().pop().unwrap(); + assert_eq!( + completion.messages[1..], + vec![ + LanguageModelRequestMessage { + role: Role::User, + content: vec!["Call the echo tool!".into()], + cache: false + }, + LanguageModelRequestMessage { + role: Role::Assistant, + content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())], + cache: false + }, + LanguageModelRequestMessage { + role: Role::User, + content: vec![language_model::MessageContent::ToolResult( + LanguageModelToolResult { + tool_use_id: tool_use_1.id.clone(), + tool_name: tool_use_1.name.clone(), + is_error: false, + content: "test".into(), + output: Some("test".into()) + } + )], + cache: true + }, + ] + ); + + fake_model.send_last_completion_stream_text_chunk("Done"); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + events.collect::>().await; + thread.read_with(cx, |thread, _cx| { + assert_eq!( + thread.last_message(), + Some(Message::Agent(AgentMessage { + content: vec![AgentMessageContent::Text("Done".into())], + tool_results: IndexMap::default() + })) + ); + }) +} + #[gpui::test] async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) { let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index c000027368..43f391ca64 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -123,7 +123,7 @@ impl Message { match self { Message::User(message) => message.to_markdown(), Message::Agent(message) => message.to_markdown(), - Message::Resume => "[resumed after tool use limit was reached]".into(), + Message::Resume => "[resume]\n".into(), } } @@ -1085,11 +1085,6 @@ impl Thread { &mut self, cx: &mut Context, ) -> Result>> { - anyhow::ensure!( - self.tool_use_limit_reached, - "can only resume after tool use limit is reached" - ); - self.messages.push(Message::Resume); cx.notify(); @@ -1216,12 +1211,13 @@ impl Thread { cx: &mut AsyncApp, ) -> Result<()> { log::debug!("Stream completion started successfully"); - let request = this.update(cx, |this, cx| { - this.build_completion_request(completion_intent, cx) - })??; let mut attempt = None; - 'retry: loop { + loop { + let request = this.update(cx, |this, cx| { + this.build_completion_request(completion_intent, cx) + })??; + telemetry::event!( "Agent Thread Completion", thread_id = this.read_with(cx, |this, _| this.id.to_string())?, @@ -1236,10 +1232,11 @@ impl Thread { attempt.unwrap_or(0) ); let mut events = model - .stream_completion(request.clone(), cx) + .stream_completion(request, cx) .await .map_err(|error| anyhow!(error))?; let mut tool_results = FuturesUnordered::new(); + let mut error = None; while let Some(event) = events.next().await { match event { @@ -1249,51 +1246,9 @@ impl Thread { this.handle_streamed_completion_event(event, event_stream, cx) })??); } - Err(error) => { - let completion_mode = - this.read_with(cx, |thread, _cx| thread.completion_mode())?; - if completion_mode == CompletionMode::Normal { - return Err(anyhow!(error))?; - } - - let Some(strategy) = Self::retry_strategy_for(&error) else { - return Err(anyhow!(error))?; - }; - - let max_attempts = match &strategy { - RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts, - RetryStrategy::Fixed { max_attempts, .. } => *max_attempts, - }; - - let attempt = attempt.get_or_insert(0u8); - - *attempt += 1; - - let attempt = *attempt; - if attempt > max_attempts { - return Err(anyhow!(error))?; - } - - let delay = match &strategy { - RetryStrategy::ExponentialBackoff { initial_delay, .. } => { - let delay_secs = - initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32); - Duration::from_secs(delay_secs) - } - RetryStrategy::Fixed { delay, .. } => *delay, - }; - log::debug!("Retry attempt {attempt} with delay {delay:?}"); - - event_stream.send_retry(acp_thread::RetryStatus { - last_error: error.to_string().into(), - attempt: attempt as usize, - max_attempts: max_attempts as usize, - started_at: Instant::now(), - duration: delay, - }); - - cx.background_executor().timer(delay).await; - continue 'retry; + Err(err) => { + error = Some(err); + break; } } } @@ -1320,7 +1275,58 @@ impl Thread { })?; } - return Ok(()); + if let Some(error) = error { + let completion_mode = this.read_with(cx, |thread, _cx| thread.completion_mode())?; + if completion_mode == CompletionMode::Normal { + return Err(anyhow!(error))?; + } + + let Some(strategy) = Self::retry_strategy_for(&error) else { + return Err(anyhow!(error))?; + }; + + let max_attempts = match &strategy { + RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts, + RetryStrategy::Fixed { max_attempts, .. } => *max_attempts, + }; + + let attempt = attempt.get_or_insert(0u8); + + *attempt += 1; + + let attempt = *attempt; + if attempt > max_attempts { + return Err(anyhow!(error))?; + } + + let delay = match &strategy { + RetryStrategy::ExponentialBackoff { initial_delay, .. } => { + let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32); + Duration::from_secs(delay_secs) + } + RetryStrategy::Fixed { delay, .. } => *delay, + }; + log::debug!("Retry attempt {attempt} with delay {delay:?}"); + + event_stream.send_retry(acp_thread::RetryStatus { + last_error: error.to_string().into(), + attempt: attempt as usize, + max_attempts: max_attempts as usize, + started_at: Instant::now(), + duration: delay, + }); + cx.background_executor().timer(delay).await; + this.update(cx, |this, cx| { + this.flush_pending_message(cx); + if let Some(Message::Agent(message)) = this.messages.last() { + if message.tool_results.is_empty() { + this.messages.push(Message::Resume); + } + } + })?; + } else { + return Ok(()); + } } } @@ -1737,6 +1743,10 @@ impl Thread { return; }; + if message.content.is_empty() { + return; + } + for content in &message.content { let AgentMessageContent::ToolUse(tool_use) = content else { continue; diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 0b987e25b6..5674b15c98 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -820,6 +820,9 @@ impl AcpThreadView { let Some(thread) = self.thread() else { return; }; + if !thread.read(cx).can_resume(cx) { + return; + } let task = thread.update(cx, |thread, cx| thread.resume(cx)); cx.spawn(async move |this, cx| { @@ -4459,12 +4462,53 @@ impl AcpThreadView { } fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout { + let can_resume = self + .thread() + .map_or(false, |thread| thread.read(cx).can_resume(cx)); + + let can_enable_burn_mode = self.as_native_thread(cx).map_or(false, |thread| { + let thread = thread.read(cx); + let supports_burn_mode = thread + .model() + .map_or(false, |model| model.supports_burn_mode()); + supports_burn_mode && thread.completion_mode() == CompletionMode::Normal + }); + Callout::new() .severity(Severity::Error) .title("Error") .icon(IconName::XCircle) .description(error.clone()) - .actions_slot(self.create_copy_button(error.to_string())) + .actions_slot( + h_flex() + .gap_0p5() + .when(can_resume && can_enable_burn_mode, |this| { + this.child( + Button::new("enable-burn-mode-and-retry", "Enable Burn Mode and Retry") + .icon(IconName::ZedBurnMode) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .label_size(LabelSize::Small) + .on_click(cx.listener(|this, _, window, cx| { + this.toggle_burn_mode(&ToggleBurnMode, window, cx); + this.resume_chat(cx); + })), + ) + }) + .when(can_resume, |this| { + this.child( + Button::new("retry", "Retry") + .icon(IconName::RotateCw) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .label_size(LabelSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.resume_chat(cx); + })), + ) + }) + .child(self.create_copy_button(error.to_string())), + ) .dismiss_action(self.dismiss_error_button(cx)) } From 2b5a3029727f6aa031f6771add3cc4a8cc85515a Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 25 Aug 2025 10:08:48 -0300 Subject: [PATCH 653/693] thread view: Prevent user message controls to be cut-off (#36865) In the thread view, when focusing on the user message, we display the editing control container absolutely-positioned in the top right. However, if there are no rules items and no restore checkpoint button _and_ it is the very first message, the editing controls container would be cut-off. This PR fixes that by giving it a bit more top padding. Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 5674b15c98..25f2745f75 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1305,14 +1305,23 @@ impl AcpThreadView { None }; + let has_checkpoint_button = message + .checkpoint + .as_ref() + .is_some_and(|checkpoint| checkpoint.show); + let agent_name = self.agent.name(); v_flex() .id(("user_message", entry_ix)) - .map(|this| if rules_item.is_some() { - this.pt_3() - } else { - this.pt_2() + .map(|this| { + if entry_ix == 0 && !has_checkpoint_button && rules_item.is_none() { + this.pt_4() + } else if rules_item.is_some() { + this.pt_3() + } else { + this.pt_2() + } }) .pb_4() .px_2() From db949546cf477818da206f12f5e0dfa45f2e038a Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 25 Aug 2025 15:14:48 +0200 Subject: [PATCH 654/693] agent2: Less noisy logs (#36863) Release Notes: - N/A --- crates/agent2/src/agent.rs | 10 ++++----- crates/agent2/src/native_agent_server.rs | 4 ++-- crates/agent2/src/thread.rs | 26 ++++++++++++------------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 415933b7d1..1576c3cf96 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -180,7 +180,7 @@ impl NativeAgent { fs: Arc, cx: &mut AsyncApp, ) -> Result> { - log::info!("Creating new NativeAgent"); + log::debug!("Creating new NativeAgent"); let project_context = cx .update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))? @@ -756,7 +756,7 @@ impl NativeAgentConnection { } } - log::info!("Response stream completed"); + log::debug!("Response stream completed"); anyhow::Ok(acp::PromptResponse { stop_reason: acp::StopReason::EndTurn, }) @@ -781,7 +781,7 @@ impl AgentModelSelector for NativeAgentConnection { model_id: acp_thread::AgentModelId, cx: &mut App, ) -> Task> { - log::info!("Setting model for session {}: {}", session_id, model_id); + log::debug!("Setting model for session {}: {}", session_id, model_id); let Some(thread) = self .0 .read(cx) @@ -852,7 +852,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { cx: &mut App, ) -> Task>> { let agent = self.0.clone(); - log::info!("Creating new thread for project at: {:?}", cwd); + log::debug!("Creating new thread for project at: {:?}", cwd); cx.spawn(async move |cx| { log::debug!("Starting thread creation in async context"); @@ -917,7 +917,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { .into_iter() .map(Into::into) .collect::>(); - log::info!("Converted prompt to message: {} chars", content.len()); + log::debug!("Converted prompt to message: {} chars", content.len()); log::debug!("Message id: {:?}", id); log::debug!("Message content: {:?}", content); diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index 12d3c79d1b..33ee44c9a3 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -44,7 +44,7 @@ impl AgentServer for NativeAgentServer { project: &Entity, cx: &mut App, ) -> Task>> { - log::info!( + log::debug!( "NativeAgentServer::connect called for path: {:?}", _root_dir ); @@ -63,7 +63,7 @@ impl AgentServer for NativeAgentServer { // Create the connection wrapper let connection = NativeAgentConnection(agent); - log::info!("NativeAgentServer connection established successfully"); + log::debug!("NativeAgentServer connection established successfully"); Ok(Rc::new(connection) as Rc) }) diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 43f391ca64..4bbbdbdec7 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1088,7 +1088,7 @@ impl Thread { self.messages.push(Message::Resume); cx.notify(); - log::info!("Total messages in thread: {}", self.messages.len()); + log::debug!("Total messages in thread: {}", self.messages.len()); self.run_turn(cx) } @@ -1106,7 +1106,7 @@ impl Thread { { let model = self.model().context("No language model configured")?; - log::info!("Thread::send called with model: {:?}", model.name()); + log::info!("Thread::send called with model: {}", model.name().0); self.advance_prompt_id(); let content = content.into_iter().map(Into::into).collect::>(); @@ -1116,7 +1116,7 @@ impl Thread { .push(Message::User(UserMessage { id, content })); cx.notify(); - log::info!("Total messages in thread: {}", self.messages.len()); + log::debug!("Total messages in thread: {}", self.messages.len()); self.run_turn(cx) } @@ -1140,7 +1140,7 @@ impl Thread { event_stream: event_stream.clone(), tools: self.enabled_tools(profile, &model, cx), _task: cx.spawn(async move |this, cx| { - log::info!("Starting agent turn execution"); + log::debug!("Starting agent turn execution"); let turn_result: Result<()> = async { let mut intent = CompletionIntent::UserPrompt; @@ -1165,7 +1165,7 @@ impl Thread { log::info!("Tool use limit reached, completing turn"); return Err(language_model::ToolUseLimitReachedError.into()); } else if end_turn { - log::info!("No tool uses found, completing turn"); + log::debug!("No tool uses found, completing turn"); return Ok(()); } else { intent = CompletionIntent::ToolResults; @@ -1177,7 +1177,7 @@ impl Thread { match turn_result { Ok(()) => { - log::info!("Turn execution completed"); + log::debug!("Turn execution completed"); event_stream.send_stop(acp::StopReason::EndTurn); } Err(error) => { @@ -1227,7 +1227,7 @@ impl Thread { attempt ); - log::info!( + log::debug!( "Calling model.stream_completion, attempt {}", attempt.unwrap_or(0) ); @@ -1254,7 +1254,7 @@ impl Thread { } while let Some(tool_result) = tool_results.next().await { - log::info!("Tool finished {:?}", tool_result); + log::debug!("Tool finished {:?}", tool_result); event_stream.update_tool_call_fields( &tool_result.tool_use_id, @@ -1528,7 +1528,7 @@ impl Thread { }); let supports_images = self.model().is_some_and(|model| model.supports_images()); let tool_result = tool.run(tool_use.input, tool_event_stream, cx); - log::info!("Running tool {}", tool_use.name); + log::debug!("Running tool {}", tool_use.name); Some(cx.foreground_executor().spawn(async move { let tool_result = tool_result.await.and_then(|output| { if let LanguageModelToolResultContent::Image(_) = &output.llm_output @@ -1640,7 +1640,7 @@ impl Thread { summary.extend(lines.next()); } - log::info!("Setting summary: {}", summary); + log::debug!("Setting summary: {}", summary); let summary = SharedString::from(summary); this.update(cx, |this, cx| { @@ -1657,7 +1657,7 @@ impl Thread { return; }; - log::info!( + log::debug!( "Generating title with model: {:?}", self.summarization_model.as_ref().map(|model| model.name()) ); @@ -1799,8 +1799,8 @@ impl Thread { log::debug!("Completion mode: {:?}", self.completion_mode); let messages = self.build_request_messages(cx); - log::info!("Request will include {} messages", messages.len()); - log::info!("Request includes {} tools", tools.len()); + log::debug!("Request will include {} messages", messages.len()); + log::debug!("Request includes {} tools", tools.len()); let request = LanguageModelRequest { thread_id: Some(self.id.to_string()), From 69127d2beaf69900505c420adef9310a5aeb9694 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 25 Aug 2025 15:38:19 +0200 Subject: [PATCH 655/693] acp: Simplify control flow for native agent loop (#36868) Release Notes: - N/A Co-authored-by: Bennet Bo Fenner --- crates/agent2/src/thread.rs | 164 ++++++++++++++++-------------------- 1 file changed, 73 insertions(+), 91 deletions(-) diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 4bbbdbdec7..2d1e608297 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1142,37 +1142,7 @@ impl Thread { _task: cx.spawn(async move |this, cx| { log::debug!("Starting agent turn execution"); - let turn_result: Result<()> = async { - let mut intent = CompletionIntent::UserPrompt; - loop { - Self::stream_completion(&this, &model, intent, &event_stream, cx).await?; - - let mut end_turn = true; - this.update(cx, |this, cx| { - // Generate title if needed. - if this.title.is_none() && this.pending_title_generation.is_none() { - this.generate_title(cx); - } - - // End the turn if the model didn't use tools. - let message = this.pending_message.as_ref(); - end_turn = - message.map_or(true, |message| message.tool_results.is_empty()); - this.flush_pending_message(cx); - })?; - - if this.read_with(cx, |this, _| this.tool_use_limit_reached)? { - log::info!("Tool use limit reached, completing turn"); - return Err(language_model::ToolUseLimitReachedError.into()); - } else if end_turn { - log::debug!("No tool uses found, completing turn"); - return Ok(()); - } else { - intent = CompletionIntent::ToolResults; - } - } - } - .await; + let turn_result = Self::run_turn_internal(&this, model, &event_stream, cx).await; _ = this.update(cx, |this, cx| this.flush_pending_message(cx)); match turn_result { @@ -1203,20 +1173,17 @@ impl Thread { Ok(events_rx) } - async fn stream_completion( + async fn run_turn_internal( this: &WeakEntity, - model: &Arc, - completion_intent: CompletionIntent, + model: Arc, event_stream: &ThreadEventStream, cx: &mut AsyncApp, ) -> Result<()> { - log::debug!("Stream completion started successfully"); - - let mut attempt = None; + let mut attempt = 0; + let mut intent = CompletionIntent::UserPrompt; loop { - let request = this.update(cx, |this, cx| { - this.build_completion_request(completion_intent, cx) - })??; + let request = + this.update(cx, |this, cx| this.build_completion_request(intent, cx))??; telemetry::event!( "Agent Thread Completion", @@ -1227,23 +1194,19 @@ impl Thread { attempt ); - log::debug!( - "Calling model.stream_completion, attempt {}", - attempt.unwrap_or(0) - ); + log::debug!("Calling model.stream_completion, attempt {}", attempt); let mut events = model .stream_completion(request, cx) .await .map_err(|error| anyhow!(error))?; let mut tool_results = FuturesUnordered::new(); let mut error = None; - while let Some(event) = events.next().await { + log::trace!("Received completion event: {:?}", event); match event { Ok(event) => { - log::trace!("Received completion event: {:?}", event); tool_results.extend(this.update(cx, |this, cx| { - this.handle_streamed_completion_event(event, event_stream, cx) + this.handle_completion_event(event, event_stream, cx) })??); } Err(err) => { @@ -1253,6 +1216,7 @@ impl Thread { } } + let end_turn = tool_results.is_empty(); while let Some(tool_result) = tool_results.next().await { log::debug!("Tool finished {:?}", tool_result); @@ -1275,65 +1239,83 @@ impl Thread { })?; } + this.update(cx, |this, cx| { + this.flush_pending_message(cx); + if this.title.is_none() && this.pending_title_generation.is_none() { + this.generate_title(cx); + } + })?; + if let Some(error) = error { - let completion_mode = this.read_with(cx, |thread, _cx| thread.completion_mode())?; - if completion_mode == CompletionMode::Normal { - return Err(anyhow!(error))?; - } - - let Some(strategy) = Self::retry_strategy_for(&error) else { - return Err(anyhow!(error))?; - }; - - let max_attempts = match &strategy { - RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts, - RetryStrategy::Fixed { max_attempts, .. } => *max_attempts, - }; - - let attempt = attempt.get_or_insert(0u8); - - *attempt += 1; - - let attempt = *attempt; - if attempt > max_attempts { - return Err(anyhow!(error))?; - } - - let delay = match &strategy { - RetryStrategy::ExponentialBackoff { initial_delay, .. } => { - let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32); - Duration::from_secs(delay_secs) - } - RetryStrategy::Fixed { delay, .. } => *delay, - }; - log::debug!("Retry attempt {attempt} with delay {delay:?}"); - - event_stream.send_retry(acp_thread::RetryStatus { - last_error: error.to_string().into(), - attempt: attempt as usize, - max_attempts: max_attempts as usize, - started_at: Instant::now(), - duration: delay, - }); - cx.background_executor().timer(delay).await; - this.update(cx, |this, cx| { - this.flush_pending_message(cx); + attempt += 1; + let retry = + this.update(cx, |this, _| this.handle_completion_error(error, attempt))??; + let timer = cx.background_executor().timer(retry.duration); + event_stream.send_retry(retry); + timer.await; + this.update(cx, |this, _cx| { if let Some(Message::Agent(message)) = this.messages.last() { if message.tool_results.is_empty() { + intent = CompletionIntent::UserPrompt; this.messages.push(Message::Resume); } } })?; - } else { + } else if this.read_with(cx, |this, _| this.tool_use_limit_reached)? { + return Err(language_model::ToolUseLimitReachedError.into()); + } else if end_turn { return Ok(()); + } else { + intent = CompletionIntent::ToolResults; + attempt = 0; } } } + fn handle_completion_error( + &mut self, + error: LanguageModelCompletionError, + attempt: u8, + ) -> Result { + if self.completion_mode == CompletionMode::Normal { + return Err(anyhow!(error)); + } + + let Some(strategy) = Self::retry_strategy_for(&error) else { + return Err(anyhow!(error)); + }; + + let max_attempts = match &strategy { + RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts, + RetryStrategy::Fixed { max_attempts, .. } => *max_attempts, + }; + + if attempt > max_attempts { + return Err(anyhow!(error)); + } + + let delay = match &strategy { + RetryStrategy::ExponentialBackoff { initial_delay, .. } => { + let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32); + Duration::from_secs(delay_secs) + } + RetryStrategy::Fixed { delay, .. } => *delay, + }; + log::debug!("Retry attempt {attempt} with delay {delay:?}"); + + Ok(acp_thread::RetryStatus { + last_error: error.to_string().into(), + attempt: attempt as usize, + max_attempts: max_attempts as usize, + started_at: Instant::now(), + duration: delay, + }) + } + /// A helper method that's called on every streamed completion event. /// Returns an optional tool result task, which the main agentic loop will /// send back to the model when it resolves. - fn handle_streamed_completion_event( + fn handle_completion_event( &mut self, event: LanguageModelCompletionEvent, event_stream: &ThreadEventStream, From fda5111dc0239e3003d3c0d26346270c356cbc9f Mon Sep 17 00:00:00 2001 From: Zach Riegel Date: Mon, 25 Aug 2025 08:30:09 -0700 Subject: [PATCH 656/693] Add CSS language injections for calls to `styled` (#33966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …emotion). Closes: https://github.com/zed-industries/zed/issues/17026 Release Notes: - Added CSS language injection support for styled-components and emotion in JavaScript, TypeScript, and TSX files. --- crates/languages/src/javascript/injections.scm | 15 +++++++++++++++ crates/languages/src/tsx/injections.scm | 15 +++++++++++++++ crates/languages/src/typescript/injections.scm | 15 +++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/crates/languages/src/javascript/injections.scm b/crates/languages/src/javascript/injections.scm index 7baba5f227..dbec1937b1 100644 --- a/crates/languages/src/javascript/injections.scm +++ b/crates/languages/src/javascript/injections.scm @@ -11,6 +11,21 @@ (#set! injection.language "css")) ) +(call_expression + function: (member_expression + object: (identifier) @_obj (#eq? @_obj "styled") + property: (property_identifier)) + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "css")) +) + +(call_expression + function: (call_expression + function: (identifier) @_name (#eq? @_name "styled")) + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "css")) +) + (call_expression function: (identifier) @_name (#eq? @_name "html") arguments: (template_string) @injection.content diff --git a/crates/languages/src/tsx/injections.scm b/crates/languages/src/tsx/injections.scm index 48da80995b..9eec01cc89 100644 --- a/crates/languages/src/tsx/injections.scm +++ b/crates/languages/src/tsx/injections.scm @@ -11,6 +11,21 @@ (#set! injection.language "css")) ) +(call_expression + function: (member_expression + object: (identifier) @_obj (#eq? @_obj "styled") + property: (property_identifier)) + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "css")) +) + +(call_expression + function: (call_expression + function: (identifier) @_name (#eq? @_name "styled")) + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "css")) +) + (call_expression function: (identifier) @_name (#eq? @_name "html") arguments: (template_string (string_fragment) @injection.content diff --git a/crates/languages/src/typescript/injections.scm b/crates/languages/src/typescript/injections.scm index 7affdc5b75..1ca1e9ad59 100644 --- a/crates/languages/src/typescript/injections.scm +++ b/crates/languages/src/typescript/injections.scm @@ -15,6 +15,21 @@ (#set! injection.language "css")) ) +(call_expression + function: (member_expression + object: (identifier) @_obj (#eq? @_obj "styled") + property: (property_identifier)) + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "css")) +) + +(call_expression + function: (call_expression + function: (identifier) @_name (#eq? @_name "styled")) + arguments: (template_string (string_fragment) @injection.content + (#set! injection.language "css")) +) + (call_expression function: (identifier) @_name (#eq? @_name "html") arguments: (template_string) @injection.content From 2fe3dbed31147cc869bdb01aea4b7fae57f5fdc8 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Mon, 25 Aug 2025 21:00:53 +0530 Subject: [PATCH 657/693] project: Remove redundant Option from parse_register_capabilities (#36874) Release Notes: - N/A --- crates/project/src/lsp_store.rs | 83 +++++++++++++++------------------ 1 file changed, 38 insertions(+), 45 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index d2958dce01..853490ddac 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -11706,12 +11706,11 @@ impl LspStore { // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. } "workspace/symbol" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.workspace_symbol_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.workspace_symbol_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "workspace/fileOperations" => { if let Some(options) = reg.register_options { @@ -11735,12 +11734,11 @@ impl LspStore { } } "textDocument/rangeFormatting" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.document_range_formatting_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.document_range_formatting_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "textDocument/onTypeFormatting" => { if let Some(options) = reg @@ -11755,36 +11753,32 @@ impl LspStore { } } "textDocument/formatting" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.document_formatting_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.document_formatting_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "textDocument/rename" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.rename_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.rename_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "textDocument/inlayHint" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.inlay_hint_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.inlay_hint_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "textDocument/documentSymbol" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.document_symbol_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.document_symbol_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "textDocument/codeAction" => { if let Some(options) = reg @@ -11800,12 +11794,11 @@ impl LspStore { } } "textDocument/definition" => { - if let Some(options) = parse_register_capabilities(reg)? { - server.update_capabilities(|capabilities| { - capabilities.definition_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } + let options = parse_register_capabilities(reg)?; + server.update_capabilities(|capabilities| { + capabilities.definition_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); } "textDocument/completion" => { if let Some(caps) = reg @@ -12184,10 +12177,10 @@ impl LspStore { // https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133 fn parse_register_capabilities( reg: lsp::Registration, -) -> anyhow::Result>> { +) -> Result> { Ok(match reg.register_options { - Some(options) => Some(OneOf::Right(serde_json::from_value::(options)?)), - None => Some(OneOf::Left(true)), + Some(options) => OneOf::Right(serde_json::from_value::(options)?), + None => OneOf::Left(true), }) } From 65fb17e2c9f817e7d7776cc406e3c7f3291fd24d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 25 Aug 2025 09:34:30 -0600 Subject: [PATCH 658/693] acp: Remember following state (#36793) A beta user reported that following was "lost" when asking for confirmation, I suspect they moved their cursor in the agent file while reviewing the change. Now we will resume following when the agent starts up again. Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 73 ++++++++++++++++++++------ 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index d9a7a2582a..cc33879586 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -774,7 +774,7 @@ pub enum AcpThreadEvent { impl EventEmitter for AcpThread {} -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Debug)] pub enum ThreadStatus { Idle, WaitingForToolConfirmation, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 25f2745f75..609777e2d1 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -274,6 +274,7 @@ pub struct AcpThreadView { edits_expanded: bool, plan_expanded: bool, editor_expanded: bool, + should_be_following: bool, editing_message: Option, prompt_capabilities: Rc>, is_loading_contents: bool, @@ -385,6 +386,7 @@ impl AcpThreadView { edits_expanded: false, plan_expanded: false, editor_expanded: false, + should_be_following: false, history_store, hovered_recent_history_item: None, prompt_capabilities, @@ -897,6 +899,13 @@ impl AcpThreadView { let Some(thread) = self.thread().cloned() else { return; }; + if self.should_be_following { + self.workspace + .update(cx, |workspace, cx| { + workspace.follow(CollaboratorId::Agent, window, cx); + }) + .ok(); + } self.is_loading_contents = true; let guard = cx.new(|_| ()); @@ -938,6 +947,16 @@ impl AcpThreadView { this.handle_thread_error(err, cx); }) .ok(); + } else { + this.update(cx, |this, cx| { + this.should_be_following = this + .workspace + .update(cx, |workspace, _| { + workspace.is_being_followed(CollaboratorId::Agent) + }) + .unwrap_or_default(); + }) + .ok(); } }) .detach(); @@ -1254,6 +1273,7 @@ impl AcpThreadView { tool_call_id: acp::ToolCallId, option_id: acp::PermissionOptionId, option_kind: acp::PermissionOptionKind, + window: &mut Window, cx: &mut Context, ) { let Some(thread) = self.thread() else { @@ -1262,6 +1282,13 @@ impl AcpThreadView { thread.update(cx, |thread, cx| { thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx); }); + if self.should_be_following { + self.workspace + .update(cx, |workspace, cx| { + workspace.follow(CollaboratorId::Agent, window, cx); + }) + .ok(); + } cx.notify(); } @@ -2095,11 +2122,12 @@ impl AcpThreadView { let tool_call_id = tool_call_id.clone(); let option_id = option.id.clone(); let option_kind = option.kind; - move |this, _, _, cx| { + move |this, _, window, cx| { this.authorize_tool_call( tool_call_id.clone(), option_id.clone(), option_kind, + window, cx, ); } @@ -3652,13 +3680,34 @@ impl AcpThreadView { } } - fn render_follow_toggle(&self, cx: &mut Context) -> impl IntoElement { - let following = self - .workspace - .read_with(cx, |workspace, _| { - workspace.is_being_followed(CollaboratorId::Agent) + fn is_following(&self, cx: &App) -> bool { + match self.thread().map(|thread| thread.read(cx).status()) { + Some(ThreadStatus::Generating) => self + .workspace + .read_with(cx, |workspace, _| { + workspace.is_being_followed(CollaboratorId::Agent) + }) + .unwrap_or(false), + _ => self.should_be_following, + } + } + + fn toggle_following(&mut self, window: &mut Window, cx: &mut Context) { + let following = self.is_following(cx); + self.should_be_following = !following; + self.workspace + .update(cx, |workspace, cx| { + if following { + workspace.unfollow(CollaboratorId::Agent, window, cx); + } else { + workspace.follow(CollaboratorId::Agent, window, cx); + } }) - .unwrap_or(false); + .ok(); + } + + fn render_follow_toggle(&self, cx: &mut Context) -> impl IntoElement { + let following = self.is_following(cx); IconButton::new("follow-agent", IconName::Crosshair) .icon_size(IconSize::Small) @@ -3679,15 +3728,7 @@ impl AcpThreadView { } }) .on_click(cx.listener(move |this, _, window, cx| { - this.workspace - .update(cx, |workspace, cx| { - if following { - workspace.unfollow(CollaboratorId::Agent, window, cx); - } else { - workspace.follow(CollaboratorId::Agent, window, cx); - } - }) - .ok(); + this.toggle_following(window, cx); })) } From 557753d092e167422ae28df5d4399612dc59e893 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 25 Aug 2025 17:46:07 +0200 Subject: [PATCH 659/693] acp: Add Reauthenticate to dropdown (#36878) Release Notes: - N/A Co-authored-by: Conrad Irwin --- crates/agent_ui/src/acp/thread_view.rs | 18 ++++++++++++++++++ crates/agent_ui/src/agent_panel.rs | 13 +++++++++++++ crates/zed_actions/src/lib.rs | 4 +++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 609777e2d1..18a65ec634 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -4746,6 +4746,24 @@ impl AcpThreadView { })) } + pub(crate) fn reauthenticate(&mut self, window: &mut Window, cx: &mut Context) { + let agent = self.agent.clone(); + let ThreadState::Ready { thread, .. } = &self.thread_state else { + return; + }; + + let connection = thread.read(cx).connection().clone(); + let err = AuthRequired { + description: None, + provider_id: None, + }; + self.clear_thread_error(cx); + let this = cx.weak_entity(); + window.defer(cx, |window, cx| { + Self::handle_auth_required(this, err, agent, connection, window, cx); + }) + } + fn upgrade_button(&self, cx: &mut Context) -> impl IntoElement { Button::new("upgrade", "Upgrade") .label_size(LabelSize::Small) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 50f9fc6a45..f1a8a744ee 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -9,6 +9,7 @@ use agent_servers::AgentServerSettings; use agent2::{DbThreadMetadata, HistoryEntry}; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; +use zed_actions::agent::ReauthenticateAgent; use crate::acp::{AcpThreadHistory, ThreadHistoryEvent}; use crate::agent_diff::AgentDiffThread; @@ -2204,6 +2205,8 @@ impl AgentPanel { "Enable Full Screen" }; + let selected_agent = self.selected_agent.clone(); + PopoverMenu::new("agent-options-menu") .trigger_with_tooltip( IconButton::new("agent-options-menu", IconName::Ellipsis) @@ -2283,6 +2286,11 @@ impl AgentPanel { .action("Settings", Box::new(OpenSettings)) .separator() .action(full_screen_label, Box::new(ToggleZoom)); + + if selected_agent == AgentType::Gemini { + menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent)) + } + menu })) } @@ -3751,6 +3759,11 @@ impl Render for AgentPanel { } })) .on_action(cx.listener(Self::toggle_burn_mode)) + .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| { + if let Some(thread_view) = this.active_thread_view() { + thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx)) + } + })) .child(self.render_toolbar(window, cx)) .children(self.render_onboarding(window, cx)) .map(|parent| match &self.active_view { diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 069abc0a12..a5223a2cdf 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -290,7 +290,9 @@ pub mod agent { Chat, /// Toggles the language model selector dropdown. #[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])] - ToggleModelSelector + ToggleModelSelector, + /// Triggers re-authentication on Gemini + ReauthenticateAgent ] ); } From 2dc4f156b387ccd4698fbf1a5e54ea7050b738ca Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 25 Aug 2025 11:51:31 -0400 Subject: [PATCH 660/693] Revert "Capture `shorthand_field_initializer` and modules in Rust highlights (#35842)" (#36880) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR reverts https://github.com/zed-industries/zed/pull/35842, as it broke the syntax highlighting for `crate`: ### Before Revert Screenshot 2025-08-25 at 11 29 50 AM ### After Revert Screenshot 2025-08-25 at 11 32 17 AM This reverts commit 896a35f7befce468427a30489adf88c851b9507d. Release Notes: - Reverted https://github.com/zed-industries/zed/pull/35842. --- crates/languages/src/rust/highlights.scm | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/crates/languages/src/rust/highlights.scm b/crates/languages/src/rust/highlights.scm index 9c02fbedaa..1c46061827 100644 --- a/crates/languages/src/rust/highlights.scm +++ b/crates/languages/src/rust/highlights.scm @@ -6,9 +6,6 @@ (self) @variable.special (field_identifier) @property -(shorthand_field_initializer - (identifier) @property) - (trait_item name: (type_identifier) @type.interface) (impl_item trait: (type_identifier) @type.interface) (abstract_type trait: (type_identifier) @type.interface) @@ -41,20 +38,11 @@ (identifier) @function.special (scoped_identifier name: (identifier) @function.special) - ] - "!" @function.special) + ]) (macro_definition name: (identifier) @function.special.definition) -(mod_item - name: (identifier) @module) - -(visibility_modifier [ - (crate) @keyword - (super) @keyword -]) - ; Identifier conventions ; Assume uppercase names are types/enum-constructors @@ -127,7 +115,9 @@ "where" "while" "yield" + (crate) (mutable_specifier) + (super) ] @keyword [ @@ -199,7 +189,6 @@ operator: "/" @operator (lifetime) @lifetime -(lifetime (identifier) @lifetime) (parameter (identifier) @variable.parameter) From a102b087438c0424ce32a47177b7e16132aa24df Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 25 Aug 2025 10:03:07 -0600 Subject: [PATCH 661/693] Require confirmation for fetch tool (#36881) Using prompt injection, the agent may be tricked into making a fetch request that includes unexpected data from the conversation in the URL. As agent conversations may contain sensitive information (like private code, or potentially even API keys), this seems bad. The easiest way to prevent this is to require the user to look at the URL before the model is allowed to fetch it. Thanks to @ant4g0nist for bringing this to our attention. Release Notes: - agent panel: The fetch tool now requires confirmation. --- crates/agent2/src/tools/fetch_tool.rs | 9 +++++++-- crates/assistant_tools/src/fetch_tool.rs | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/agent2/src/tools/fetch_tool.rs b/crates/agent2/src/tools/fetch_tool.rs index 0313c4e4c2..dd97271a79 100644 --- a/crates/agent2/src/tools/fetch_tool.rs +++ b/crates/agent2/src/tools/fetch_tool.rs @@ -136,12 +136,17 @@ impl AgentTool for FetchTool { fn run( self: Arc, input: Self::Input, - _event_stream: ToolCallEventStream, + event_stream: ToolCallEventStream, cx: &mut App, ) -> Task> { + let authorize = event_stream.authorize(input.url.clone(), cx); + let text = cx.background_spawn({ let http_client = self.http_client.clone(); - async move { Self::build_message(http_client, &input.url).await } + async move { + authorize.await?; + Self::build_message(http_client, &input.url).await + } }); cx.foreground_executor().spawn(async move { diff --git a/crates/assistant_tools/src/fetch_tool.rs b/crates/assistant_tools/src/fetch_tool.rs index 79e205f205..cc22c9fc09 100644 --- a/crates/assistant_tools/src/fetch_tool.rs +++ b/crates/assistant_tools/src/fetch_tool.rs @@ -118,7 +118,7 @@ impl Tool for FetchTool { } fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - false + true } fn may_perform_edits(&self) -> bool { From 5c346a4ccf3642e8e804db70c59616ac3cb0f86a Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Mon, 25 Aug 2025 19:12:33 +0200 Subject: [PATCH 662/693] kotlin: Specify default language server (#36871) As of https://github.com/zed-extensions/kotlin/commit/db52fc3655df8594a89b3a6b539274f23dfa2f28, the Kotlin extension has two language servers. However, following that change, no default language server for Kotlin was configured within this repo, which led to two language servers being activated for Kotlin by default. This PR makes `kotlin-language-server` the default language server for the extension. This also ensures that the [documentation within the repository](https://github.com/zed-extensions/kotlin?tab=readme-ov-file#kotlin-lsp) matches what is actually the case. Release Notes: - kotlin: Made `kotlin-language-server` the default language server. --- assets/settings/default.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/assets/settings/default.json b/assets/settings/default.json index ac26952c7f..59450dcc15 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1629,6 +1629,9 @@ "allowed": true } }, + "Kotlin": { + "language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."] + }, "LaTeX": { "formatter": "language_server", "language_servers": ["texlab", "..."], From 2e1ca472414792eea4f9a8ae4eabb469b76f1cf3 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 25 Aug 2025 13:21:20 -0400 Subject: [PATCH 663/693] Make fields of `AiUpsellCard` private (#36888) This PR makes the fields of the `AiUpsellCard` private, for better encapsulation. Release Notes: - N/A --- crates/ai_onboarding/src/ai_upsell_card.rs | 15 ++++++++++----- crates/onboarding/src/ai_setup_page.rs | 18 +++++++----------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index e9639ca075..106dcb0aef 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -12,11 +12,11 @@ use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions} #[derive(IntoElement, RegisterComponent)] pub struct AiUpsellCard { - pub sign_in_status: SignInStatus, - pub sign_in: Arc, - pub account_too_young: bool, - pub user_plan: Option, - pub tab_index: Option, + sign_in_status: SignInStatus, + sign_in: Arc, + account_too_young: bool, + user_plan: Option, + tab_index: Option, } impl AiUpsellCard { @@ -43,6 +43,11 @@ impl AiUpsellCard { tab_index: None, } } + + pub fn tab_index(mut self, tab_index: Option) -> Self { + self.tab_index = tab_index; + self + } } impl RenderOnce for AiUpsellCard { diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index 672bcf1cd9..54c49bc72a 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -283,17 +283,13 @@ pub(crate) fn render_ai_setup_page( v_flex() .mt_2() .gap_6() - .child({ - let mut ai_upsell_card = - AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx); - - ai_upsell_card.tab_index = Some({ - tab_index += 1; - tab_index - 1 - }); - - ai_upsell_card - }) + .child( + AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx) + .tab_index(Some({ + tab_index += 1; + tab_index - 1 + })), + ) .child(render_llm_provider_section( &mut tab_index, workspace, From f1204dfc333ceb83b5769c0f3ab876fc96e39252 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 25 Aug 2025 10:46:36 -0700 Subject: [PATCH 664/693] Revert "workspace: Disable padding on zoomed panels" (#36884) Reverts zed-industries/zed#36012 We thought we didn't need this UI, but it turns out it was load bearing :) Release Notes: - Restored the zoomed panel padding --- crates/workspace/src/workspace.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index bf58786d67..3654df09be 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -6622,15 +6622,25 @@ impl Render for Workspace { } }) .children(self.zoomed.as_ref().and_then(|view| { - Some(div() + let zoomed_view = view.upgrade()?; + let div = div() .occlude() .absolute() .overflow_hidden() .border_color(colors.border) .bg(colors.background) - .child(view.upgrade()?) + .child(zoomed_view) .inset_0() - .shadow_lg()) + .shadow_lg(); + + Some(match self.zoomed_position { + Some(DockPosition::Left) => div.right_2().border_r_1(), + Some(DockPosition::Right) => div.left_2().border_l_1(), + Some(DockPosition::Bottom) => div.top_2().border_t_1(), + None => { + div.top_2().bottom_2().left_2().right_2().border_1() + } + }) })) .children(self.render_notifications(window, cx)), ) From 5fd29d37a63539c991ebae477bd4a78c849e0a78 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 25 Aug 2025 14:28:11 -0400 Subject: [PATCH 665/693] acp: Model-specific prompt capabilities for 1PA (#36879) Adds support for per-session prompt capabilities and capability changes on the Zed side (ACP itself still only has per-connection static capabilities for now), and uses it to reflect image support accurately in 1PA threads based on the currently-selected model. Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 38 +++++++++++++++++------ crates/acp_thread/src/connection.rs | 18 +++++------ crates/agent2/src/agent.rs | 13 +++----- crates/agent2/src/thread.rs | 21 +++++++++++++ crates/agent_servers/src/acp.rs | 9 +++--- crates/agent_servers/src/claude.rs | 16 +++++----- crates/agent_ui/src/acp/message_editor.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 20 ++++++------ crates/agent_ui/src/agent_diff.rs | 1 + crates/watch/src/watch.rs | 13 ++++++++ 10 files changed, 98 insertions(+), 53 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index cc33879586..779f9964da 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -756,6 +756,8 @@ pub struct AcpThread { connection: Rc, session_id: acp::SessionId, token_usage: Option, + prompt_capabilities: acp::PromptCapabilities, + _observe_prompt_capabilities: Task>, } #[derive(Debug)] @@ -770,6 +772,7 @@ pub enum AcpThreadEvent { Stopped, Error, LoadError(LoadError), + PromptCapabilitiesUpdated, } impl EventEmitter for AcpThread {} @@ -821,7 +824,20 @@ impl AcpThread { project: Entity, action_log: Entity, session_id: acp::SessionId, + mut prompt_capabilities_rx: watch::Receiver, + cx: &mut Context, ) -> Self { + let prompt_capabilities = *prompt_capabilities_rx.borrow(); + let task = cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| { + loop { + let caps = prompt_capabilities_rx.recv().await?; + this.update(cx, |this, cx| { + this.prompt_capabilities = caps; + cx.emit(AcpThreadEvent::PromptCapabilitiesUpdated); + })?; + } + }); + Self { action_log, shared_buffers: Default::default(), @@ -833,9 +849,15 @@ impl AcpThread { connection, session_id, token_usage: None, + prompt_capabilities, + _observe_prompt_capabilities: task, } } + pub fn prompt_capabilities(&self) -> acp::PromptCapabilities { + self.prompt_capabilities + } + pub fn connection(&self) -> &Rc { &self.connection } @@ -2599,13 +2621,19 @@ mod tests { .into(), ); let action_log = cx.new(|_| ActionLog::new(project.clone())); - let thread = cx.new(|_cx| { + let thread = cx.new(|cx| { AcpThread::new( "Test", self.clone(), project, action_log, session_id.clone(), + watch::Receiver::constant(acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + }), + cx, ) }); self.sessions.lock().insert(session_id, thread.downgrade()); @@ -2639,14 +2667,6 @@ mod tests { } } - fn prompt_capabilities(&self) -> acp::PromptCapabilities { - acp::PromptCapabilities { - image: true, - audio: true, - embedded_context: true, - } - } - fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { let sessions = self.sessions.lock(); let thread = sessions.get(session_id).unwrap().clone(); diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 5f5032e588..af229b7545 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -38,8 +38,6 @@ pub trait AgentConnection { cx: &mut App, ) -> Task>; - fn prompt_capabilities(&self) -> acp::PromptCapabilities; - fn resume( &self, _session_id: &acp::SessionId, @@ -329,13 +327,19 @@ mod test_support { ) -> Task>> { let session_id = acp::SessionId(self.sessions.lock().len().to_string().into()); let action_log = cx.new(|_| ActionLog::new(project.clone())); - let thread = cx.new(|_cx| { + let thread = cx.new(|cx| { AcpThread::new( "Test", self.clone(), project, action_log, session_id.clone(), + watch::Receiver::constant(acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + }), + cx, ) }); self.sessions.lock().insert( @@ -348,14 +352,6 @@ mod test_support { Task::ready(Ok(thread)) } - fn prompt_capabilities(&self) -> acp::PromptCapabilities { - acp::PromptCapabilities { - image: true, - audio: true, - embedded_context: true, - } - } - fn authenticate( &self, _method_id: acp::AuthMethodId, diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 1576c3cf96..ecfaea4b49 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -240,13 +240,16 @@ impl NativeAgent { let title = thread.title(); let project = thread.project.clone(); let action_log = thread.action_log.clone(); - let acp_thread = cx.new(|_cx| { + let prompt_capabilities_rx = thread.prompt_capabilities_rx.clone(); + let acp_thread = cx.new(|cx| { acp_thread::AcpThread::new( title, connection, project.clone(), action_log.clone(), session_id.clone(), + prompt_capabilities_rx, + cx, ) }); let subscriptions = vec![ @@ -925,14 +928,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection { }) } - fn prompt_capabilities(&self) -> acp::PromptCapabilities { - acp::PromptCapabilities { - image: true, - audio: false, - embedded_context: true, - } - } - fn resume( &self, session_id: &acp::SessionId, diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 2d1e608297..1b1c014b79 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -575,11 +575,22 @@ pub struct Thread { templates: Arc, model: Option>, summarization_model: Option>, + prompt_capabilities_tx: watch::Sender, + pub(crate) prompt_capabilities_rx: watch::Receiver, pub(crate) project: Entity, pub(crate) action_log: Entity, } impl Thread { + fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities { + let image = model.map_or(true, |model| model.supports_images()); + acp::PromptCapabilities { + image, + audio: false, + embedded_context: true, + } + } + pub fn new( project: Entity, project_context: Entity, @@ -590,6 +601,8 @@ impl Thread { ) -> Self { let profile_id = AgentSettings::get_global(cx).default_profile.clone(); let action_log = cx.new(|_cx| ActionLog::new(project.clone())); + let (prompt_capabilities_tx, prompt_capabilities_rx) = + watch::channel(Self::prompt_capabilities(model.as_deref())); Self { id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()), prompt_id: PromptId::new(), @@ -617,6 +630,8 @@ impl Thread { templates, model, summarization_model: None, + prompt_capabilities_tx, + prompt_capabilities_rx, project, action_log, } @@ -750,6 +765,8 @@ impl Thread { .or_else(|| registry.default_model()) .map(|model| model.model) }); + let (prompt_capabilities_tx, prompt_capabilities_rx) = + watch::channel(Self::prompt_capabilities(model.as_deref())); Self { id, @@ -779,6 +796,8 @@ impl Thread { project, action_log, updated_at: db_thread.updated_at, + prompt_capabilities_tx, + prompt_capabilities_rx, } } @@ -946,10 +965,12 @@ impl Thread { pub fn set_model(&mut self, model: Arc, cx: &mut Context) { let old_usage = self.latest_token_usage(); self.model = Some(model); + let new_caps = Self::prompt_capabilities(self.model.as_deref()); let new_usage = self.latest_token_usage(); if old_usage != new_usage { cx.emit(TokenUsageUpdated(new_usage)); } + self.prompt_capabilities_tx.send(new_caps).log_err(); cx.notify() } diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index c9c938c6c0..5a4efe12e5 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -185,13 +185,16 @@ impl AgentConnection for AcpConnection { let session_id = response.session_id; let action_log = cx.new(|_| ActionLog::new(project.clone()))?; - let thread = cx.new(|_cx| { + let thread = cx.new(|cx| { AcpThread::new( self.server_name.clone(), self.clone(), project, action_log, session_id.clone(), + // ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically. + watch::Receiver::constant(self.prompt_capabilities), + cx, ) })?; @@ -279,10 +282,6 @@ impl AgentConnection for AcpConnection { }) } - fn prompt_capabilities(&self) -> acp::PromptCapabilities { - self.prompt_capabilities - } - fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) { session.suppress_abort_err = true; diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 048563103f..6006bf3edb 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -249,13 +249,19 @@ impl AgentConnection for ClaudeAgentConnection { }); let action_log = cx.new(|_| ActionLog::new(project.clone()))?; - let thread = cx.new(|_cx| { + let thread = cx.new(|cx| { AcpThread::new( "Claude Code", self.clone(), project, action_log, session_id.clone(), + watch::Receiver::constant(acp::PromptCapabilities { + image: true, + audio: false, + embedded_context: true, + }), + cx, ) })?; @@ -319,14 +325,6 @@ impl AgentConnection for ClaudeAgentConnection { cx.foreground_executor().spawn(async move { end_rx.await? }) } - fn prompt_capabilities(&self) -> acp::PromptCapabilities { - acp::PromptCapabilities { - image: true, - audio: false, - embedded_context: true, - } - } - fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) { let sessions = self.sessions.borrow(); let Some(session) = sessions.get(session_id) else { diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 70faa0ed27..12ae893c31 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -373,7 +373,7 @@ impl MessageEditor { if Img::extensions().contains(&extension) && !extension.contains("svg") { if !self.prompt_capabilities.get().image { - return Task::ready(Err(anyhow!("This agent does not support images yet"))); + return Task::ready(Err(anyhow!("This model does not support images yet"))); } let task = self .project diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 18a65ec634..faba18acb1 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -474,7 +474,7 @@ impl AcpThreadView { let action_log = thread.read(cx).action_log().clone(); this.prompt_capabilities - .set(connection.prompt_capabilities()); + .set(thread.read(cx).prompt_capabilities()); let count = thread.read(cx).entries().len(); this.list_state.splice(0..0, count); @@ -1163,6 +1163,10 @@ impl AcpThreadView { }); } } + AcpThreadEvent::PromptCapabilitiesUpdated => { + self.prompt_capabilities + .set(thread.read(cx).prompt_capabilities()); + } AcpThreadEvent::TokenUsageUpdated => {} } cx.notify(); @@ -5367,6 +5371,12 @@ pub(crate) mod tests { project, action_log, SessionId("test".into()), + watch::Receiver::constant(acp::PromptCapabilities { + image: true, + audio: true, + embedded_context: true, + }), + cx, ) }))) } @@ -5375,14 +5385,6 @@ pub(crate) mod tests { &[] } - fn prompt_capabilities(&self) -> acp::PromptCapabilities { - acp::PromptCapabilities { - image: true, - audio: true, - embedded_context: true, - } - } - fn authenticate( &self, _method_id: acp::AuthMethodId, diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index e07424987c..1e1ff95178 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1529,6 +1529,7 @@ impl AgentDiff { | AcpThreadEvent::TokenUsageUpdated | AcpThreadEvent::EntriesRemoved(_) | AcpThreadEvent::ToolAuthorizationRequired + | AcpThreadEvent::PromptCapabilitiesUpdated | AcpThreadEvent::Retry(_) => {} } } diff --git a/crates/watch/src/watch.rs b/crates/watch/src/watch.rs index f0ed5b4a18..71dab74820 100644 --- a/crates/watch/src/watch.rs +++ b/crates/watch/src/watch.rs @@ -162,6 +162,19 @@ impl Receiver { pending_waker_id: None, } } + + /// Creates a new [`Receiver`] holding an initial value that will never change. + pub fn constant(value: T) -> Self { + let state = Arc::new(RwLock::new(State { + value, + wakers: BTreeMap::new(), + next_waker_id: WakerId::default(), + version: 0, + closed: false, + })); + + Self { state, version: 0 } + } } impl Receiver { From c786c0150f6b315ba4074117241b90bddd8f00fc Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:45:24 -0300 Subject: [PATCH 666/693] agent: Add section for agent servers in settings view (#35206) Release Notes: - N/A --------- Co-authored-by: Cole Miller --- crates/agent_servers/src/agent_servers.rs | 2 +- crates/agent_servers/src/gemini.rs | 18 +- crates/agent_ui/src/acp/thread_view.rs | 4 +- crates/agent_ui/src/agent_configuration.rs | 249 +++++++++++++++++++-- crates/agent_ui/src/agent_panel.rs | 2 + crates/agent_ui/src/agent_ui.rs | 1 + 6 files changed, 254 insertions(+), 22 deletions(-) diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index fa59201338..0439934094 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -97,7 +97,7 @@ pub struct AgentServerCommand { } impl AgentServerCommand { - pub(crate) async fn resolve( + pub async fn resolve( path_bin_name: &'static str, extra_args: &[&'static str], fallback_path: Option<&Path>, diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 9ebcee745c..d09829fe65 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -53,7 +53,7 @@ impl AgentServer for Gemini { return Err(LoadError::NotInstalled { error_message: "Failed to find Gemini CLI binary".into(), install_message: "Install Gemini CLI".into(), - install_command: "npm install -g @google/gemini-cli@preview".into() + install_command: Self::install_command().into(), }.into()); }; @@ -88,7 +88,7 @@ impl AgentServer for Gemini { current_version ).into(), upgrade_message: "Upgrade Gemini CLI to latest".into(), - upgrade_command: "npm install -g @google/gemini-cli@preview".into(), + upgrade_command: Self::upgrade_command().into(), }.into()) } } @@ -101,6 +101,20 @@ impl AgentServer for Gemini { } } +impl Gemini { + pub fn binary_name() -> &'static str { + "gemini" + } + + pub fn install_command() -> &'static str { + "npm install -g @google/gemini-cli@preview" + } + + pub fn upgrade_command() -> &'static str { + "npm install -g @google/gemini-cli@preview" + } +} + #[cfg(test)] pub(crate) mod tests { use super::*; diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index faba18acb1..97af249ae5 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2811,7 +2811,7 @@ impl AcpThreadView { let cwd = project.first_project_directory(cx); let shell = project.terminal_settings(&cwd, cx).shell.clone(); let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId("install".to_string()), + id: task::TaskId(install_command.clone()), full_label: install_command.clone(), label: install_command.clone(), command: Some(install_command.clone()), @@ -2868,7 +2868,7 @@ impl AcpThreadView { let cwd = project.first_project_directory(cx); let shell = project.terminal_settings(&cwd, cx).shell.clone(); let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId("upgrade".to_string()), + id: task::TaskId(upgrade_command.to_string()), full_label: upgrade_command.clone(), label: upgrade_command.clone(), command: Some(upgrade_command.clone()), diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index f33f0ba032..52fb7eed4b 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -5,6 +5,7 @@ mod tool_picker; use std::{sync::Arc, time::Duration}; +use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini}; use agent_settings::AgentSettings; use assistant_tool::{ToolSource, ToolWorkingSet}; use cloud_llm_client::Plan; @@ -15,7 +16,7 @@ use extension_host::ExtensionStore; use fs::Fs; use gpui::{ Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle, - Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage, + Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage, }; use language::LanguageRegistry; use language_model::{ @@ -23,10 +24,11 @@ use language_model::{ }; use notifications::status_toast::{StatusToast, ToastIcon}; use project::{ + Project, context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore}, project_settings::{ContextServerSettings, ProjectSettings}, }; -use settings::{Settings, update_settings_file}; +use settings::{Settings, SettingsStore, update_settings_file}; use ui::{ Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu, Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*, @@ -39,7 +41,7 @@ pub(crate) use configure_context_server_modal::ConfigureContextServerModal; pub(crate) use manage_profiles_modal::ManageProfilesModal; use crate::{ - AddContextServer, + AddContextServer, ExternalAgent, NewExternalAgentThread, agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider}, }; @@ -47,6 +49,7 @@ pub struct AgentConfiguration { fs: Arc, language_registry: Arc, workspace: WeakEntity, + project: WeakEntity, focus_handle: FocusHandle, configuration_views_by_provider: HashMap, context_server_store: Entity, @@ -56,6 +59,8 @@ pub struct AgentConfiguration { _registry_subscription: Subscription, scroll_handle: ScrollHandle, scrollbar_state: ScrollbarState, + gemini_is_installed: bool, + _check_for_gemini: Task<()>, } impl AgentConfiguration { @@ -65,6 +70,7 @@ impl AgentConfiguration { tools: Entity, language_registry: Arc, workspace: WeakEntity, + project: WeakEntity, window: &mut Window, cx: &mut Context, ) -> Self { @@ -89,6 +95,11 @@ impl AgentConfiguration { cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify()) .detach(); + cx.observe_global_in::(window, |this, _, cx| { + this.check_for_gemini(cx); + cx.notify(); + }) + .detach(); let scroll_handle = ScrollHandle::new(); let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); @@ -97,6 +108,7 @@ impl AgentConfiguration { fs, language_registry, workspace, + project, focus_handle, configuration_views_by_provider: HashMap::default(), context_server_store, @@ -106,8 +118,11 @@ impl AgentConfiguration { _registry_subscription: registry_subscription, scroll_handle, scrollbar_state, + gemini_is_installed: false, + _check_for_gemini: Task::ready(()), }; this.build_provider_configuration_views(window, cx); + this.check_for_gemini(cx); this } @@ -137,6 +152,34 @@ impl AgentConfiguration { self.configuration_views_by_provider .insert(provider.id(), configuration_view); } + + fn check_for_gemini(&mut self, cx: &mut Context) { + let project = self.project.clone(); + let settings = AllAgentServersSettings::get_global(cx).clone(); + self._check_for_gemini = cx.spawn({ + async move |this, cx| { + let Some(project) = project.upgrade() else { + return; + }; + let gemini_is_installed = AgentServerCommand::resolve( + Gemini::binary_name(), + &[], + // TODO expose fallback path from the Gemini/CC types so we don't have to hardcode it again here + None, + settings.gemini, + &project, + cx, + ) + .await + .is_some(); + this.update(cx, |this, cx| { + this.gemini_is_installed = gemini_is_installed; + cx.notify(); + }) + .ok(); + } + }); + } } impl Focusable for AgentConfiguration { @@ -211,7 +254,6 @@ impl AgentConfiguration { .child( h_flex() .id(provider_id_string.clone()) - .cursor_pointer() .px_2() .py_0p5() .w_full() @@ -231,10 +273,7 @@ impl AgentConfiguration { h_flex() .w_full() .gap_1() - .child( - Label::new(provider_name.clone()) - .size(LabelSize::Large), - ) + .child(Label::new(provider_name.clone())) .map(|this| { if is_zed_provider && is_signed_in { this.child( @@ -279,7 +318,7 @@ impl AgentConfiguration { "Start New Thread", ) .icon_position(IconPosition::Start) - .icon(IconName::Plus) + .icon(IconName::Thread) .icon_size(IconSize::Small) .icon_color(Color::Muted) .label_size(LabelSize::Small) @@ -378,7 +417,7 @@ impl AgentConfiguration { ), ) .child( - Label::new("Add at least one provider to use AI-powered features.") + Label::new("Add at least one provider to use AI-powered features with Zed's native agent.") .color(Color::Muted), ), ), @@ -519,6 +558,14 @@ impl AgentConfiguration { } } + fn card_item_bg_color(&self, cx: &mut Context) -> Hsla { + cx.theme().colors().background.opacity(0.25) + } + + fn card_item_border_color(&self, cx: &mut Context) -> Hsla { + cx.theme().colors().border.opacity(0.6) + } + fn render_context_servers_section( &mut self, window: &mut Window, @@ -536,7 +583,12 @@ impl AgentConfiguration { v_flex() .gap_0p5() .child(Headline::new("Model Context Protocol (MCP) Servers")) - .child(Label::new("Connect to context servers through the Model Context Protocol, either using Zed extensions or directly.").color(Color::Muted)), + .child( + Label::new( + "All context servers connected through the Model Context Protocol.", + ) + .color(Color::Muted), + ), ) .children( context_server_ids.into_iter().map(|context_server_id| { @@ -546,7 +598,7 @@ impl AgentConfiguration { .child( h_flex() .justify_between() - .gap_2() + .gap_1p5() .child( h_flex().w_full().child( Button::new("add-context-server", "Add Custom Server") @@ -637,8 +689,6 @@ impl AgentConfiguration { .map_or([].as_slice(), |tools| tools.as_slice()); let tool_count = tools.len(); - let border_color = cx.theme().colors().border.opacity(0.6); - let (source_icon, source_tooltip) = if is_from_extension { ( IconName::ZedMcpExtension, @@ -781,8 +831,8 @@ impl AgentConfiguration { .id(item_id.clone()) .border_1() .rounded_md() - .border_color(border_color) - .bg(cx.theme().colors().background.opacity(0.2)) + .border_color(self.card_item_border_color(cx)) + .bg(self.card_item_bg_color(cx)) .overflow_hidden() .child( h_flex() @@ -790,7 +840,11 @@ impl AgentConfiguration { .justify_between() .when( error.is_some() || are_tools_expanded && tool_count >= 1, - |element| element.border_b_1().border_color(border_color), + |element| { + element + .border_b_1() + .border_color(self.card_item_border_color(cx)) + }, ) .child( h_flex() @@ -972,6 +1026,166 @@ impl AgentConfiguration { )) }) } + + fn render_agent_servers_section(&mut self, cx: &mut Context) -> impl IntoElement { + let settings = AllAgentServersSettings::get_global(cx).clone(); + let user_defined_agents = settings + .custom + .iter() + .map(|(name, settings)| { + self.render_agent_server( + IconName::Ai, + name.clone(), + ExternalAgent::Custom { + name: name.clone(), + settings: settings.clone(), + }, + None, + cx, + ) + .into_any_element() + }) + .collect::>(); + + v_flex() + .border_b_1() + .border_color(cx.theme().colors().border) + .child( + v_flex() + .p(DynamicSpacing::Base16.rems(cx)) + .pr(DynamicSpacing::Base20.rems(cx)) + .gap_2() + .child( + v_flex() + .gap_0p5() + .child(Headline::new("External Agents")) + .child( + Label::new( + "Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.", + ) + .color(Color::Muted), + ), + ) + .child(self.render_agent_server( + IconName::AiGemini, + "Gemini CLI", + ExternalAgent::Gemini, + (!self.gemini_is_installed).then_some(Gemini::install_command().into()), + cx, + )) + // TODO add CC + .children(user_defined_agents), + ) + } + + fn render_agent_server( + &self, + icon: IconName, + name: impl Into, + agent: ExternalAgent, + install_command: Option, + cx: &mut Context, + ) -> impl IntoElement { + let name = name.into(); + h_flex() + .p_1() + .pl_2() + .gap_1p5() + .justify_between() + .border_1() + .rounded_md() + .border_color(self.card_item_border_color(cx)) + .bg(self.card_item_bg_color(cx)) + .overflow_hidden() + .child( + h_flex() + .gap_1p5() + .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)) + .child(Label::new(name.clone())), + ) + .map(|this| { + if let Some(install_command) = install_command { + this.child( + Button::new( + SharedString::from(format!("install_external_agent-{name}")), + "Install Agent", + ) + .label_size(LabelSize::Small) + .icon(IconName::Plus) + .icon_position(IconPosition::Start) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .tooltip(Tooltip::text(install_command.clone())) + .on_click(cx.listener( + move |this, _, window, cx| { + let Some(project) = this.project.upgrade() else { + return; + }; + let Some(workspace) = this.workspace.upgrade() else { + return; + }; + let cwd = project.read(cx).first_project_directory(cx); + let shell = + project.read(cx).terminal_settings(&cwd, cx).shell.clone(); + let spawn_in_terminal = task::SpawnInTerminal { + id: task::TaskId(install_command.to_string()), + full_label: install_command.to_string(), + label: install_command.to_string(), + command: Some(install_command.to_string()), + args: Vec::new(), + command_label: install_command.to_string(), + cwd, + env: Default::default(), + use_new_terminal: true, + allow_concurrent_runs: true, + reveal: Default::default(), + reveal_target: Default::default(), + hide: Default::default(), + shell, + show_summary: true, + show_command: true, + show_rerun: false, + }; + let task = workspace.update(cx, |workspace, cx| { + workspace.spawn_in_terminal(spawn_in_terminal, window, cx) + }); + cx.spawn(async move |this, cx| { + task.await; + this.update(cx, |this, cx| { + this.check_for_gemini(cx); + }) + .ok(); + }) + .detach(); + }, + )), + ) + } else { + this.child( + h_flex().gap_1().child( + Button::new( + SharedString::from(format!("start_acp_thread-{name}")), + "Start New Thread", + ) + .label_size(LabelSize::Small) + .icon(IconName::Thread) + .icon_position(IconPosition::Start) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .on_click(move |_, window, cx| { + window.dispatch_action( + NewExternalAgentThread { + agent: Some(agent.clone()), + } + .boxed_clone(), + cx, + ); + }), + ), + ) + } + }) + } } impl Render for AgentConfiguration { @@ -991,6 +1205,7 @@ impl Render for AgentConfiguration { .size_full() .overflow_y_scroll() .child(self.render_general_settings_section(cx)) + .child(self.render_agent_servers_section(cx)) .child(self.render_context_servers_section(window, cx)) .child(self.render_provider_configuration_section(cx)), ) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index f1a8a744ee..c825785755 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -241,6 +241,7 @@ enum WhichFontSize { None, } +// TODO unify this with ExternalAgent #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub enum AgentType { #[default] @@ -1474,6 +1475,7 @@ impl AgentPanel { tools, self.language_registry.clone(), self.workspace.clone(), + self.project.downgrade(), window, cx, ) diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 40f6c6a2bb..d159f375b5 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -160,6 +160,7 @@ pub struct NewNativeAgentThreadFromSummary { from_session_id: agent_client_protocol::SessionId, } +// TODO unify this with AgentType #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] enum ExternalAgent { From 59af2a7d1f513d9f58fc07d4429d118c6a944069 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 25 Aug 2025 20:51:23 +0200 Subject: [PATCH 667/693] acp: Add telemetry (#36894) Release Notes: - N/A --------- Co-authored-by: Conrad Irwin --- crates/agent2/src/native_agent_server.rs | 4 ++ crates/agent2/src/tests/mod.rs | 1 + crates/agent_servers/src/agent_servers.rs | 1 + crates/agent_servers/src/claude.rs | 4 ++ crates/agent_servers/src/custom.rs | 4 ++ crates/agent_servers/src/gemini.rs | 4 ++ crates/agent_ui/src/acp/thread_view.rs | 79 ++++++++++++++++------- crates/agent_ui/src/agent_panel.rs | 8 +++ crates/agent_ui/src/agent_ui.rs | 9 +++ crates/agent_ui/src/text_thread_editor.rs | 1 + 10 files changed, 93 insertions(+), 22 deletions(-) diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs index 33ee44c9a3..9ff98ccd18 100644 --- a/crates/agent2/src/native_agent_server.rs +++ b/crates/agent2/src/native_agent_server.rs @@ -22,6 +22,10 @@ impl NativeAgentServer { } impl AgentServer for NativeAgentServer { + fn telemetry_id(&self) -> &'static str { + "zed" + } + fn name(&self) -> SharedString { "Zed Agent".into() } diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 87ecc1037c..864fbf8b10 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1685,6 +1685,7 @@ async fn test_truncate_second_message(cx: &mut TestAppContext) { } #[gpui::test] +#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows async fn test_title_generation(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 0439934094..7c7e124ca7 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -36,6 +36,7 @@ pub trait AgentServer: Send { fn name(&self) -> SharedString; fn empty_state_headline(&self) -> SharedString; fn empty_state_message(&self) -> SharedString; + fn telemetry_id(&self) -> &'static str; fn connect( &self, diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 6006bf3edb..250e564526 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -43,6 +43,10 @@ use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri pub struct ClaudeCode; impl AgentServer for ClaudeCode { + fn telemetry_id(&self) -> &'static str { + "claude-code" + } + fn name(&self) -> SharedString { "Claude Code".into() } diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs index e544c4f21f..72823026d7 100644 --- a/crates/agent_servers/src/custom.rs +++ b/crates/agent_servers/src/custom.rs @@ -22,6 +22,10 @@ impl CustomAgentServer { } impl crate::AgentServer for CustomAgentServer { + fn telemetry_id(&self) -> &'static str { + "custom" + } + fn name(&self) -> SharedString { self.name.clone() } diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index d09829fe65..5d6a70fa64 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -17,6 +17,10 @@ pub struct Gemini; const ACP_ARG: &str = "--experimental-acp"; impl AgentServer for Gemini { + fn telemetry_id(&self) -> &'static str { + "gemini-cli" + } + fn name(&self) -> SharedString { "Gemini CLI".into() } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 97af249ae5..d80f4eabce 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -892,6 +892,8 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) { + let agent_telemetry_id = self.agent.telemetry_id(); + self.thread_error.take(); self.editing_message.take(); self.thread_feedback.clear(); @@ -936,6 +938,9 @@ impl AcpThreadView { } }); drop(guard); + + telemetry::event!("Agent Message Sent", agent = agent_telemetry_id); + thread.send(contents, cx) })?; send.await @@ -1246,30 +1251,44 @@ impl AcpThreadView { pending_auth_method.replace(method.clone()); let authenticate = connection.authenticate(method, cx); cx.notify(); - self.auth_task = Some(cx.spawn_in(window, { - let project = self.project.clone(); - let agent = self.agent.clone(); - async move |this, cx| { - let result = authenticate.await; + self.auth_task = + Some(cx.spawn_in(window, { + let project = self.project.clone(); + let agent = self.agent.clone(); + async move |this, cx| { + let result = authenticate.await; - this.update_in(cx, |this, window, cx| { - if let Err(err) = result { - this.handle_thread_error(err, cx); - } else { - this.thread_state = Self::initial_state( - agent, - None, - this.workspace.clone(), - project.clone(), - window, - cx, - ) + match &result { + Ok(_) => telemetry::event!( + "Authenticate Agent Succeeded", + agent = agent.telemetry_id() + ), + Err(_) => { + telemetry::event!( + "Authenticate Agent Failed", + agent = agent.telemetry_id(), + ) + } } - this.auth_task.take() - }) - .ok(); - } - })); + + this.update_in(cx, |this, window, cx| { + if let Err(err) = result { + this.handle_thread_error(err, cx); + } else { + this.thread_state = Self::initial_state( + agent, + None, + this.workspace.clone(), + project.clone(), + window, + cx, + ) + } + this.auth_task.take() + }) + .ok(); + } + })); } fn authorize_tool_call( @@ -2776,6 +2795,12 @@ impl AcpThreadView { .on_click({ let method_id = method.id.clone(); cx.listener(move |this, _, window, cx| { + telemetry::event!( + "Authenticate Agent Started", + agent = this.agent.telemetry_id(), + method = method_id + ); + this.authenticate(method_id.clone(), window, cx) }) }) @@ -2804,6 +2829,8 @@ impl AcpThreadView { .icon_color(Color::Muted) .icon_position(IconPosition::Start) .on_click(cx.listener(move |this, _, window, cx| { + telemetry::event!("Agent Install CLI", agent = this.agent.telemetry_id()); + let task = this .workspace .update(cx, |workspace, cx| { @@ -2861,6 +2888,8 @@ impl AcpThreadView { .icon_color(Color::Muted) .icon_position(IconPosition::Start) .on_click(cx.listener(move |this, _, window, cx| { + telemetry::event!("Agent Upgrade CLI", agent = this.agent.telemetry_id()); + let task = this .workspace .update(cx, |workspace, cx| { @@ -3708,6 +3737,8 @@ impl AcpThreadView { } }) .ok(); + + telemetry::event!("Follow Agent Selected", following = !following); } fn render_follow_toggle(&self, cx: &mut Context) -> impl IntoElement { @@ -5323,6 +5354,10 @@ pub(crate) mod tests { where C: 'static + AgentConnection + Send + Clone, { + fn telemetry_id(&self) -> &'static str { + "test" + } + fn logo(&self) -> ui::IconName { ui::IconName::Ai } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index c825785755..1eafb8dd4d 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1026,6 +1026,8 @@ impl AgentPanel { } fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context) { + telemetry::event!("Agent Thread Started", agent = "zed-text"); + let context = self .context_store .update(cx, |context_store, cx| context_store.create(cx)); @@ -1118,6 +1120,8 @@ impl AgentPanel { } }; + telemetry::event!("Agent Thread Started", agent = ext_agent.name()); + let server = ext_agent.server(fs, history); this.update_in(cx, |this, window, cx| { @@ -2327,6 +2331,8 @@ impl AgentPanel { .menu({ let menu = self.assistant_navigation_menu.clone(); move |window, cx| { + telemetry::event!("View Thread History Clicked"); + if let Some(menu) = menu.as_ref() { menu.update(cx, |_, cx| { cx.defer_in(window, |menu, window, cx| { @@ -2505,6 +2511,8 @@ impl AgentPanel { let workspace = self.workspace.clone(); move |window, cx| { + telemetry::event!("New Thread Clicked"); + let active_thread = active_thread.clone(); Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { menu = menu diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index d159f375b5..110c432df3 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -175,6 +175,15 @@ enum ExternalAgent { } impl ExternalAgent { + fn name(&self) -> &'static str { + match self { + Self::NativeAgent => "zed", + Self::Gemini => "gemini-cli", + Self::ClaudeCode => "claude-code", + Self::Custom { .. } => "custom", + } + } + pub fn server( &self, fs: Arc, diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index edb672a872..e9e7eba4b6 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -361,6 +361,7 @@ impl TextThreadEditor { if self.sending_disabled(cx) { return; } + telemetry::event!("Agent Message Sent", agent = "zed-text"); self.send_to_model(window, cx); } From 79e74b880bafaf32778eead2e336cb0f7d68cde7 Mon Sep 17 00:00:00 2001 From: Cretezy Date: Mon, 25 Aug 2025 15:02:19 -0400 Subject: [PATCH 668/693] workspace: Allow disabling of padding on zoomed panels (#31913) Screenshot: | Before | After | | -------|------| | ![image](https://github.com/user-attachments/assets/629e7da2-6070-4abb-b469-3b0824524ca4) | ![image](https://github.com/user-attachments/assets/99e54412-2e0b-4df9-9c40-a89b0411f6d8) | | ![image](https://github.com/user-attachments/assets/e99da846-f39b-47b5-808e-65c22a1af47b) | ![image](https://github.com/user-attachments/assets/ccd4408f-8cce-44ec-a69a-81794125ec99) | Release Notes: - Added `zoomed_padding` to allow disabling of padding around zoomed panels Co-authored-by: Mikayla Maki --- assets/settings/default.json | 6 ++++++ crates/workspace/src/workspace.rs | 4 ++++ crates/workspace/src/workspace_settings.rs | 7 +++++++ 3 files changed, 17 insertions(+) diff --git a/assets/settings/default.json b/assets/settings/default.json index 59450dcc15..f0b9e11e57 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -162,6 +162,12 @@ // 2. Always quit the application // "on_last_window_closed": "quit_app", "on_last_window_closed": "platform_default", + // Whether to show padding for zoomed panels. + // When enabled, zoomed center panels (e.g. code editor) will have padding all around, + // while zoomed bottom/left/right panels will have padding to the top/right/left (respectively). + // + // Default: true + "zoomed_padding": true, // Whether to use the system provided dialogs for Open and Save As. // When set to false, Zed will use the built-in keyboard-first pickers. "use_system_path_prompts": true, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3654df09be..0b4694601e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -6633,6 +6633,10 @@ impl Render for Workspace { .inset_0() .shadow_lg(); + if !WorkspaceSettings::get_global(cx).zoomed_padding { + return Some(div); + } + Some(match self.zoomed_position { Some(DockPosition::Left) => div.right_2().border_r_1(), Some(DockPosition::Right) => div.left_2().border_l_1(), diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 5635347514..3b6bc1ea97 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -29,6 +29,7 @@ pub struct WorkspaceSettings { pub on_last_window_closed: OnLastWindowClosed, pub resize_all_panels_in_dock: Vec, pub close_on_file_delete: bool, + pub zoomed_padding: bool, } #[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema)] @@ -202,6 +203,12 @@ pub struct WorkspaceSettingsContent { /// /// Default: false pub close_on_file_delete: Option, + /// Whether to show padding for zoomed panels. + /// When enabled, zoomed bottom panels will have some top padding, + /// while zoomed left/right panels will have padding to the right/left (respectively). + /// + /// Default: true + pub zoomed_padding: Option, } #[derive(Deserialize)] From 949398cb93b6f68986cab45dc4ef91e6b54fb2b6 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:07:30 -0300 Subject: [PATCH 669/693] thread view: Fix some design papercuts (#36893) Release Notes: - N/A --------- Co-authored-by: Conrad Irwin Co-authored-by: Ben Brandt Co-authored-by: Matt Miller --- crates/agent2/src/tools/find_path_tool.rs | 27 +- crates/agent_ui/src/acp/thread_history.rs | 2 +- crates/agent_ui/src/acp/thread_view.rs | 296 +++++++++---------- crates/assistant_tools/src/find_path_tool.rs | 8 +- crates/assistant_tools/src/read_file_tool.rs | 2 +- 5 files changed, 152 insertions(+), 183 deletions(-) diff --git a/crates/agent2/src/tools/find_path_tool.rs b/crates/agent2/src/tools/find_path_tool.rs index 5b35c40f85..384bd56e77 100644 --- a/crates/agent2/src/tools/find_path_tool.rs +++ b/crates/agent2/src/tools/find_path_tool.rs @@ -165,16 +165,17 @@ fn search_paths(glob: &str, project: Entity, cx: &mut App) -> Task(if hovered || selected { + .end_slot::(if hovered { Some( IconButton::new("delete", IconName::Trash) .shape(IconButtonShape::Square) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index d80f4eabce..837ce6f90a 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -35,6 +35,7 @@ use prompt_store::{PromptId, PromptStore}; use rope::Point; use settings::{Settings as _, SettingsStore}; use std::cell::Cell; +use std::path::Path; use std::sync::Arc; use std::time::Instant; use std::{collections::BTreeMap, rc::Rc, time::Duration}; @@ -1551,12 +1552,11 @@ impl AcpThreadView { return primary; }; - let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); - let primary = if entry_ix == total_entries - 1 && !is_generating { + let primary = if entry_ix == total_entries - 1 { v_flex() .w_full() .child(primary) - .child(self.render_thread_controls(cx)) + .child(self.render_thread_controls(&thread, cx)) .when_some( self.thread_feedback.comments_editor.clone(), |this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)), @@ -1698,15 +1698,16 @@ impl AcpThreadView { .into_any_element() } - fn render_tool_call_icon( + fn render_tool_call( &self, - group_name: SharedString, entry_ix: usize, - is_collapsible: bool, - is_open: bool, tool_call: &ToolCall, + window: &Window, cx: &Context, ) -> Div { + let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix)); + let card_header_id = SharedString::from("inner-tool-call-header"); + let tool_icon = if tool_call.kind == acp::ToolKind::Edit && tool_call.locations.len() == 1 { FileIcons::get_icon(&tool_call.locations[0].path, cx) @@ -1714,7 +1715,7 @@ impl AcpThreadView { .unwrap_or(Icon::new(IconName::ToolPencil)) } else { Icon::new(match tool_call.kind { - acp::ToolKind::Read => IconName::ToolRead, + acp::ToolKind::Read => IconName::ToolSearch, acp::ToolKind::Edit => IconName::ToolPencil, acp::ToolKind::Delete => IconName::ToolDeleteFile, acp::ToolKind::Move => IconName::ArrowRightLeft, @@ -1728,59 +1729,6 @@ impl AcpThreadView { .size(IconSize::Small) .color(Color::Muted); - let base_container = h_flex().flex_shrink_0().size_4().justify_center(); - - if is_collapsible { - base_container - .child( - div() - .group_hover(&group_name, |s| s.invisible().w_0()) - .child(tool_icon), - ) - .child( - h_flex() - .absolute() - .inset_0() - .invisible() - .justify_center() - .group_hover(&group_name, |s| s.visible()) - .child( - Disclosure::new(("expand", entry_ix), is_open) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronRight) - .on_click(cx.listener({ - let id = tool_call.id.clone(); - move |this: &mut Self, _, _, cx: &mut Context| { - if is_open { - this.expanded_tool_calls.remove(&id); - } else { - this.expanded_tool_calls.insert(id.clone()); - } - cx.notify(); - } - })), - ), - ) - } else { - base_container.child(tool_icon) - } - } - - fn render_tool_call( - &self, - entry_ix: usize, - tool_call: &ToolCall, - window: &Window, - cx: &Context, - ) -> Div { - let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix)); - let card_header_id = SharedString::from("inner-tool-call-header"); - - let in_progress = match &tool_call.status { - ToolCallStatus::InProgress => true, - _ => false, - }; - let failed_or_canceled = match &tool_call.status { ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true, _ => false, @@ -1880,6 +1828,7 @@ impl AcpThreadView { .child( h_flex() .id(header_id) + .group(&card_header_id) .relative() .w_full() .max_w_full() @@ -1897,19 +1846,11 @@ impl AcpThreadView { }) .child( h_flex() - .group(&card_header_id) .relative() .w_full() .h(window.line_height() - px(2.)) .text_size(self.tool_name_font_size()) - .child(self.render_tool_call_icon( - card_header_id, - entry_ix, - is_collapsible, - is_open, - tool_call, - cx, - )) + .child(tool_icon) .child(if tool_call.locations.len() == 1 { let name = tool_call.locations[0] .path @@ -1937,13 +1878,13 @@ impl AcpThreadView { }) .child(name) .tooltip(Tooltip::text("Jump to File")) + .cursor(gpui::CursorStyle::PointingHand) .on_click(cx.listener(move |this, _, window, cx| { this.open_tool_call_location(entry_ix, 0, window, cx); })) .into_any_element() } else { h_flex() - .id("non-card-label-container") .relative() .w_full() .max_w_full() @@ -1954,47 +1895,39 @@ impl AcpThreadView { default_markdown_style(false, true, window, cx), ))) .child(gradient_overlay(gradient_color)) - .on_click(cx.listener({ - let id = tool_call.id.clone(); - move |this: &mut Self, _, _, cx: &mut Context| { - if is_open { - this.expanded_tool_calls.remove(&id); - } else { - this.expanded_tool_calls.insert(id.clone()); - } - cx.notify(); - } - })) .into_any() }), ) - .when(in_progress && use_card_layout && !is_open, |this| { - this.child( - div().absolute().right_2().child( - Icon::new(IconName::ArrowCircle) - .color(Color::Muted) - .size(IconSize::Small) - .with_animation( - "running", - Animation::new(Duration::from_secs(3)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage( - delta, - ))) - }, - ), - ), - ) - }) - .when(failed_or_canceled, |this| { - this.child( - div().absolute().right_2().child( - Icon::new(IconName::Close) - .color(Color::Error) - .size(IconSize::Small), - ), - ) - }), + .child( + h_flex() + .gap_px() + .when(is_collapsible, |this| { + this.child( + Disclosure::new(("expand", entry_ix), is_open) + .opened_icon(IconName::ChevronUp) + .closed_icon(IconName::ChevronDown) + .visible_on_hover(&card_header_id) + .on_click(cx.listener({ + let id = tool_call.id.clone(); + move |this: &mut Self, _, _, cx: &mut Context| { + if is_open { + this.expanded_tool_calls.remove(&id); + } else { + this.expanded_tool_calls.insert(id.clone()); + } + cx.notify(); + } + })), + ) + }) + .when(failed_or_canceled, |this| { + this.child( + Icon::new(IconName::Close) + .color(Color::Error) + .size(IconSize::Small), + ) + }), + ), ) .children(tool_output_display) } @@ -2064,9 +1997,27 @@ impl AcpThreadView { cx: &Context, ) -> AnyElement { let uri: SharedString = resource_link.uri.clone().into(); + let is_file = resource_link.uri.strip_prefix("file://"); - let label: SharedString = if let Some(path) = resource_link.uri.strip_prefix("file://") { - path.to_string().into() + let label: SharedString = if let Some(abs_path) = is_file { + if let Some(project_path) = self + .project + .read(cx) + .project_path_for_absolute_path(&Path::new(abs_path), cx) + && let Some(worktree) = self + .project + .read(cx) + .worktree_for_id(project_path.worktree_id, cx) + { + worktree + .read(cx) + .full_path(&project_path.path) + .to_string_lossy() + .to_string() + .into() + } else { + abs_path.to_string().into() + } } else { uri.clone() }; @@ -2083,10 +2034,12 @@ impl AcpThreadView { Button::new(button_id, label) .label_size(LabelSize::Small) .color(Color::Muted) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) .truncate(true) + .when(is_file.is_none(), |this| { + this.icon(IconName::ArrowUpRight) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + }) .on_click(cx.listener({ let workspace = self.workspace.clone(); move |_, _, window, cx: &mut Context| { @@ -3727,16 +3680,19 @@ impl AcpThreadView { fn toggle_following(&mut self, window: &mut Window, cx: &mut Context) { let following = self.is_following(cx); + self.should_be_following = !following; - self.workspace - .update(cx, |workspace, cx| { - if following { - workspace.unfollow(CollaboratorId::Agent, window, cx); - } else { - workspace.follow(CollaboratorId::Agent, window, cx); - } - }) - .ok(); + if self.thread().map(|thread| thread.read(cx).status()) == Some(ThreadStatus::Generating) { + self.workspace + .update(cx, |workspace, cx| { + if following { + workspace.unfollow(CollaboratorId::Agent, window, cx); + } else { + workspace.follow(CollaboratorId::Agent, window, cx); + } + }) + .ok(); + } telemetry::event!("Follow Agent Selected", following = !following); } @@ -3744,6 +3700,20 @@ impl AcpThreadView { fn render_follow_toggle(&self, cx: &mut Context) -> impl IntoElement { let following = self.is_following(cx); + let tooltip_label = if following { + if self.agent.name() == "Zed Agent" { + format!("Stop Following the {}", self.agent.name()) + } else { + format!("Stop Following {}", self.agent.name()) + } + } else { + if self.agent.name() == "Zed Agent" { + format!("Follow the {}", self.agent.name()) + } else { + format!("Follow {}", self.agent.name()) + } + }; + IconButton::new("follow-agent", IconName::Crosshair) .icon_size(IconSize::Small) .icon_color(Color::Muted) @@ -3751,10 +3721,10 @@ impl AcpThreadView { .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor))) .tooltip(move |window, cx| { if following { - Tooltip::for_action("Stop Following Agent", &Follow, window, cx) + Tooltip::for_action(tooltip_label.clone(), &Follow, window, cx) } else { Tooltip::with_meta( - "Follow Agent", + tooltip_label.clone(), Some(&Follow), "Track the agent's location as it reads and edits files.", window, @@ -4175,7 +4145,20 @@ impl AcpThreadView { } } - fn render_thread_controls(&self, cx: &Context) -> impl IntoElement { + fn render_thread_controls( + &self, + thread: &Entity, + cx: &Context, + ) -> impl IntoElement { + let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); + if is_generating { + return h_flex().id("thread-controls-container").ml_1().child( + div() + .py_2() + .px(rems_from_px(22.)) + .child(SpinnerLabel::new().size(LabelSize::Small)), + ); + } let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::Small) @@ -4899,45 +4882,30 @@ impl Render for AcpThreadView { .items_center() .justify_end() .child(self.render_load_error(e, cx)), - ThreadState::Ready { thread, .. } => { - let thread_clone = thread.clone(); - - v_flex().flex_1().map(|this| { - if has_messages { - this.child( - list( - self.list_state.clone(), - cx.processor(|this, index: usize, window, cx| { - let Some((entry, len)) = this.thread().and_then(|thread| { - let entries = &thread.read(cx).entries(); - Some((entries.get(index)?, entries.len())) - }) else { - return Empty.into_any(); - }; - this.render_entry(index, len, entry, window, cx) - }), - ) - .with_sizing_behavior(gpui::ListSizingBehavior::Auto) - .flex_grow() - .into_any(), + ThreadState::Ready { .. } => v_flex().flex_1().map(|this| { + if has_messages { + this.child( + list( + self.list_state.clone(), + cx.processor(|this, index: usize, window, cx| { + let Some((entry, len)) = this.thread().and_then(|thread| { + let entries = &thread.read(cx).entries(); + Some((entries.get(index)?, entries.len())) + }) else { + return Empty.into_any(); + }; + this.render_entry(index, len, entry, window, cx) + }), ) - .child(self.render_vertical_scrollbar(cx)) - .children( - match thread_clone.read(cx).status() { - ThreadStatus::Idle - | ThreadStatus::WaitingForToolConfirmation => None, - ThreadStatus::Generating => div() - .py_2() - .px(rems_from_px(22.)) - .child(SpinnerLabel::new().size(LabelSize::Small)) - .into(), - }, - ) - } else { - this.child(self.render_recent_history(window, cx)) - } - }) - } + .with_sizing_behavior(gpui::ListSizingBehavior::Auto) + .flex_grow() + .into_any(), + ) + .child(self.render_vertical_scrollbar(cx)) + } else { + this.child(self.render_recent_history(window, cx)) + } + }), }) // The activity bar is intentionally rendered outside of the ThreadState::Ready match // above so that the scrollbar doesn't render behind it. The current setup allows diff --git a/crates/assistant_tools/src/find_path_tool.rs b/crates/assistant_tools/src/find_path_tool.rs index ac2c7a32ab..d1451132ae 100644 --- a/crates/assistant_tools/src/find_path_tool.rs +++ b/crates/assistant_tools/src/find_path_tool.rs @@ -435,8 +435,8 @@ mod test { assert_eq!( matches, &[ - PathBuf::from("root/apple/banana/carrot"), - PathBuf::from("root/apple/bandana/carbonara") + PathBuf::from(path!("root/apple/banana/carrot")), + PathBuf::from(path!("root/apple/bandana/carbonara")) ] ); @@ -447,8 +447,8 @@ mod test { assert_eq!( matches, &[ - PathBuf::from("root/apple/banana/carrot"), - PathBuf::from("root/apple/bandana/carbonara") + PathBuf::from(path!("root/apple/banana/carrot")), + PathBuf::from(path!("root/apple/bandana/carbonara")) ] ); } diff --git a/crates/assistant_tools/src/read_file_tool.rs b/crates/assistant_tools/src/read_file_tool.rs index 766ee3b161..a6e984fca6 100644 --- a/crates/assistant_tools/src/read_file_tool.rs +++ b/crates/assistant_tools/src/read_file_tool.rs @@ -68,7 +68,7 @@ impl Tool for ReadFileTool { } fn icon(&self) -> IconName { - IconName::ToolRead + IconName::ToolSearch } fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { From 4605b9663059df38176db1c19c9edb1d52ac0ece Mon Sep 17 00:00:00 2001 From: John Tur Date: Mon, 25 Aug 2025 15:45:28 -0400 Subject: [PATCH 670/693] Fix constant thread creation on Windows (#36779) See https://github.com/zed-industries/zed/issues/36057#issuecomment-3215808649 Fixes https://github.com/zed-industries/zed/issues/36057 Release Notes: - N/A --- crates/gpui/src/platform/windows/dispatcher.rs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/crates/gpui/src/platform/windows/dispatcher.rs b/crates/gpui/src/platform/windows/dispatcher.rs index e5b9c020d5..f554dea128 100644 --- a/crates/gpui/src/platform/windows/dispatcher.rs +++ b/crates/gpui/src/platform/windows/dispatcher.rs @@ -9,10 +9,8 @@ use parking::Parker; use parking_lot::Mutex; use util::ResultExt; use windows::{ - Foundation::TimeSpan, System::Threading::{ - ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemOptions, - WorkItemPriority, + ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemPriority, }, Win32::{ Foundation::{LPARAM, WPARAM}, @@ -56,12 +54,7 @@ impl WindowsDispatcher { Ok(()) }) }; - ThreadPool::RunWithPriorityAndOptionsAsync( - &handler, - WorkItemPriority::High, - WorkItemOptions::TimeSliced, - ) - .log_err(); + ThreadPool::RunWithPriorityAsync(&handler, WorkItemPriority::High).log_err(); } fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) { @@ -72,12 +65,7 @@ impl WindowsDispatcher { Ok(()) }) }; - let delay = TimeSpan { - // A time period expressed in 100-nanosecond units. - // 10,000,000 ticks per second - Duration: (duration.as_nanos() / 100) as i64, - }; - ThreadPoolTimer::CreateTimer(&handler, delay).log_err(); + ThreadPoolTimer::CreateTimer(&handler, duration.into()).log_err(); } } From 0470baca50a557491d0a193ec125500c5bf22770 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 25 Aug 2025 13:50:08 -0600 Subject: [PATCH 671/693] open_ai: Remove `model` field from ResponseStreamEvent (#36902) Closes #36901 Release Notes: - Fixed use of Open WebUI as an LLM provider. --- crates/open_ai/src/open_ai.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index acf6ec434a..08be82b830 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -446,7 +446,6 @@ pub enum ResponseStreamResult { #[derive(Serialize, Deserialize, Debug)] pub struct ResponseStreamEvent { - pub model: String, pub choices: Vec, pub usage: Option, } From 9cc006ff7473afbfa999c3424b221326ade4ccf1 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 25 Aug 2025 14:07:10 -0600 Subject: [PATCH 672/693] acp: Update error matching (#36898) Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 21 ++++++++++++--------- crates/agent_servers/src/acp.rs | 4 +++- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 779f9964da..4ded647a74 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -183,16 +183,15 @@ impl ToolCall { language_registry: Arc, cx: &mut App, ) -> Self { + let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") { + first_line.to_owned() + "…" + } else { + tool_call.title + }; Self { id: tool_call.id, - label: cx.new(|cx| { - Markdown::new( - tool_call.title.into(), - Some(language_registry.clone()), - None, - cx, - ) - }), + label: cx + .new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)), kind: tool_call.kind, content: tool_call .content @@ -233,7 +232,11 @@ impl ToolCall { if let Some(title) = title { self.label.update(cx, |label, cx| { - label.replace(title, cx); + if let Some((first_line, _)) = title.split_once("\n") { + label.replace(first_line.to_owned() + "…", cx) + } else { + label.replace(title, cx); + } }); } diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index 5a4efe12e5..9080fc1ab0 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -266,7 +266,9 @@ impl AgentConnection for AcpConnection { match serde_json::from_value(data.clone()) { Ok(ErrorDetails { details }) => { - if suppress_abort_err && details.contains("This operation was aborted") + if suppress_abort_err + && (details.contains("This operation was aborted") + || details.contains("The user aborted a request")) { Ok(acp::PromptResponse { stop_reason: acp::StopReason::Cancelled, From 823a0018e5a5f758c63ab68622db23e1dfa45fba Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 25 Aug 2025 16:10:17 -0400 Subject: [PATCH 673/693] acp: Show output for read_file tool in a code block (#36900) Release Notes: - N/A --- crates/agent2/src/tools/read_file_tool.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index fea9732093..e771c26eca 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -11,6 +11,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; use std::{path::Path, sync::Arc}; +use util::markdown::MarkdownCodeBlock; use crate::{AgentTool, ToolCallEventStream}; @@ -243,6 +244,19 @@ impl AgentTool for ReadFileTool { }]), ..Default::default() }); + if let Ok(LanguageModelToolResultContent::Text(text)) = &result { + let markdown = MarkdownCodeBlock { + tag: &input.path, + text, + } + .to_string(); + event_stream.update_fields(ToolCallUpdateFields { + content: Some(vec![acp::ToolCallContent::Content { + content: markdown.into(), + }]), + ..Default::default() + }) + } } })?; From 99cee8778cc7c6ee9ddd405f5f00caa713299d68 Mon Sep 17 00:00:00 2001 From: Alvaro Parker <64918109+AlvaroParker@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:18:03 -0400 Subject: [PATCH 674/693] tab_switcher: Add support for diagnostics (#34547) Support to show diagnostics on the tab switcher in the same way they are displayed on the tab bar. This follows the setting `tabs.show_diagnostics`. This will improve user experience when disabling the tab bar and still being able to see the diagnostics when switching tabs Preview: Screenshot From 2025-07-16 11-02-42 Release Notes: - Added diagnostics indicators to the tab switcher --------- Co-authored-by: Kirill Bulatov --- crates/language/src/buffer.rs | 20 +++-- crates/project/src/lsp_store.rs | 23 +++-- crates/tab_switcher/src/tab_switcher.rs | 114 +++++++++++++++++------- 3 files changed, 108 insertions(+), 49 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index b106110c33..4ddc2b3018 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1569,11 +1569,21 @@ impl Buffer { self.send_operation(op, true, cx); } - pub fn get_diagnostics(&self, server_id: LanguageServerId) -> Option<&DiagnosticSet> { - let Ok(idx) = self.diagnostics.binary_search_by_key(&server_id, |v| v.0) else { - return None; - }; - Some(&self.diagnostics[idx].1) + pub fn buffer_diagnostics( + &self, + for_server: Option, + ) -> Vec<&DiagnosticEntry> { + match for_server { + Some(server_id) => match self.diagnostics.binary_search_by_key(&server_id, |v| v.0) { + Ok(idx) => self.diagnostics[idx].1.iter().collect(), + Err(_) => Vec::new(), + }, + None => self + .diagnostics + .iter() + .flat_map(|(_, diagnostic_set)| diagnostic_set.iter()) + .collect(), + } } fn request_autoindent(&mut self, cx: &mut Context) { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 853490ddac..deebaedd74 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -7588,19 +7588,16 @@ impl LspStore { let snapshot = buffer_handle.read(cx).snapshot(); let buffer = buffer_handle.read(cx); let reused_diagnostics = buffer - .get_diagnostics(server_id) - .into_iter() - .flat_map(|diag| { - diag.iter() - .filter(|v| merge(buffer, &v.diagnostic, cx)) - .map(|v| { - let start = Unclipped(v.range.start.to_point_utf16(&snapshot)); - let end = Unclipped(v.range.end.to_point_utf16(&snapshot)); - DiagnosticEntry { - range: start..end, - diagnostic: v.diagnostic.clone(), - } - }) + .buffer_diagnostics(Some(server_id)) + .iter() + .filter(|v| merge(buffer, &v.diagnostic, cx)) + .map(|v| { + let start = Unclipped(v.range.start.to_point_utf16(&snapshot)); + let end = Unclipped(v.range.end.to_point_utf16(&snapshot)); + DiagnosticEntry { + range: start..end, + diagnostic: v.diagnostic.clone(), + } }) .collect::>(); diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index 7c70bcd5b5..bf3ce7b568 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -2,12 +2,14 @@ mod tab_switcher_tests; use collections::HashMap; -use editor::items::entry_git_aware_label_color; +use editor::items::{ + entry_diagnostic_aware_icon_decoration_and_color, entry_git_aware_label_color, +}; use fuzzy::StringMatchCandidate; use gpui::{ Action, AnyElement, App, Context, DismissEvent, Entity, EntityId, EventEmitter, FocusHandle, - Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Render, - Styled, Task, WeakEntity, Window, actions, rems, + Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Point, + Render, Styled, Task, WeakEntity, Window, actions, rems, }; use picker::{Picker, PickerDelegate}; use project::Project; @@ -15,11 +17,14 @@ use schemars::JsonSchema; use serde::Deserialize; use settings::Settings; use std::{cmp::Reverse, sync::Arc}; -use ui::{ListItem, ListItemSpacing, Tooltip, prelude::*}; +use ui::{ + DecoratedIcon, IconDecoration, IconDecorationKind, ListItem, ListItemSpacing, Tooltip, + prelude::*, +}; use util::ResultExt; use workspace::{ ModalView, Pane, SaveIntent, Workspace, - item::{ItemHandle, ItemSettings, TabContentParams}, + item::{ItemHandle, ItemSettings, ShowDiagnostics, TabContentParams}, pane::{Event as PaneEvent, render_item_indicator, tab_details}, }; @@ -233,6 +238,77 @@ pub struct TabSwitcherDelegate { restored_items: bool, } +impl TabMatch { + fn icon( + &self, + project: &Entity, + selected: bool, + window: &Window, + cx: &App, + ) -> Option { + let icon = self.item.tab_icon(window, cx)?; + let item_settings = ItemSettings::get_global(cx); + let show_diagnostics = item_settings.show_diagnostics; + let git_status_color = item_settings + .git_status + .then(|| { + let path = self.item.project_path(cx)?; + let project = project.read(cx); + let entry = project.entry_for_path(&path, cx)?; + let git_status = project + .project_path_git_status(&path, cx) + .map(|status| status.summary()) + .unwrap_or_default(); + Some(entry_git_aware_label_color( + git_status, + entry.is_ignored, + selected, + )) + }) + .flatten(); + let colored_icon = icon.color(git_status_color.unwrap_or_default()); + + let most_sever_diagostic_level = if show_diagnostics == ShowDiagnostics::Off { + None + } else { + let buffer_store = project.read(cx).buffer_store().read(cx); + let buffer = self + .item + .project_path(cx) + .and_then(|path| buffer_store.get_by_path(&path)) + .map(|buffer| buffer.read(cx)); + buffer.and_then(|buffer| { + buffer + .buffer_diagnostics(None) + .iter() + .map(|diagnostic_entry| diagnostic_entry.diagnostic.severity) + .min() + }) + }; + + let decorations = + entry_diagnostic_aware_icon_decoration_and_color(most_sever_diagostic_level) + .filter(|(d, _)| { + *d != IconDecorationKind::Triangle + || show_diagnostics != ShowDiagnostics::Errors + }) + .map(|(icon, color)| { + let knockout_item_color = if selected { + cx.theme().colors().element_selected + } else { + cx.theme().colors().element_background + }; + IconDecoration::new(icon, knockout_item_color, cx) + .color(color.color(cx)) + .position(Point { + x: px(-2.), + y: px(-2.), + }) + }); + Some(DecoratedIcon::new(colored_icon, decorations)) + } +} + impl TabSwitcherDelegate { #[allow(clippy::complexity)] fn new( @@ -574,31 +650,7 @@ impl PickerDelegate for TabSwitcherDelegate { }; let label = tab_match.item.tab_content(params, window, cx); - let icon = tab_match.item.tab_icon(window, cx).map(|icon| { - let git_status_color = ItemSettings::get_global(cx) - .git_status - .then(|| { - tab_match - .item - .project_path(cx) - .as_ref() - .and_then(|path| { - let project = self.project.read(cx); - let entry = project.entry_for_path(path, cx)?; - let git_status = project - .project_path_git_status(path, cx) - .map(|status| status.summary()) - .unwrap_or_default(); - Some((entry, git_status)) - }) - .map(|(entry, git_status)| { - entry_git_aware_label_color(git_status, entry.is_ignored, selected) - }) - }) - .flatten(); - - icon.color(git_status_color.unwrap_or_default()) - }); + let icon = tab_match.icon(&self.project, selected, window, cx); let indicator = render_item_indicator(tab_match.item.boxed_clone(), cx); let indicator_color = if let Some(ref indicator) = indicator { @@ -640,7 +692,7 @@ impl PickerDelegate for TabSwitcherDelegate { .inset(true) .toggle_state(selected) .child(h_flex().w_full().child(label)) - .start_slot::(icon) + .start_slot::(icon) .map(|el| { if self.selected_index == ix { el.end_slot::(close_button) From ad25aba990cc26b41903a91cdbff9bfec07ff95c Mon Sep 17 00:00:00 2001 From: Gwen Lg <105106246+gwen-lg@users.noreply.github.com> Date: Mon, 25 Aug 2025 22:23:29 +0200 Subject: [PATCH 675/693] remote_server: Improve error reporting (#33770) Closes #33736 Use `thiserror` to implement error stack and `anyhow` to report is to user. Also move some code from main to remote_server to have better crate isolation. Release Notes: - N/A --------- Co-authored-by: Kirill Bulatov --- Cargo.lock | 1 + crates/remote_server/Cargo.toml | 1 + crates/remote_server/src/main.rs | 90 ++----------- crates/remote_server/src/remote_server.rs | 74 +++++++++++ crates/remote_server/src/unix.rs | 155 ++++++++++++++++++---- 5 files changed, 216 insertions(+), 105 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c835b503ad..42649b137f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13521,6 +13521,7 @@ dependencies = [ "smol", "sysinfo", "telemetry_events", + "thiserror 2.0.12", "toml 0.8.20", "unindent", "util", diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index dcec9f6fe0..5dbb9a2771 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -65,6 +65,7 @@ telemetry_events.workspace = true util.workspace = true watch.workspace = true worktree.workspace = true +thiserror.workspace = true [target.'cfg(not(windows))'.dependencies] crashes.workspace = true diff --git a/crates/remote_server/src/main.rs b/crates/remote_server/src/main.rs index 03b0c3eda3..368c7cb639 100644 --- a/crates/remote_server/src/main.rs +++ b/crates/remote_server/src/main.rs @@ -1,6 +1,7 @@ #![cfg_attr(target_os = "windows", allow(unused, dead_code))] -use clap::{Parser, Subcommand}; +use clap::Parser; +use remote_server::Commands; use std::path::PathBuf; #[derive(Parser)] @@ -21,105 +22,34 @@ struct Cli { printenv: bool, } -#[derive(Subcommand)] -enum Commands { - Run { - #[arg(long)] - log_file: PathBuf, - #[arg(long)] - pid_file: PathBuf, - #[arg(long)] - stdin_socket: PathBuf, - #[arg(long)] - stdout_socket: PathBuf, - #[arg(long)] - stderr_socket: PathBuf, - }, - Proxy { - #[arg(long)] - reconnect: bool, - #[arg(long)] - identifier: String, - }, - Version, -} - #[cfg(windows)] fn main() { unimplemented!() } #[cfg(not(windows))] -fn main() { - use release_channel::{RELEASE_CHANNEL, ReleaseChannel}; - use remote::proxy::ProxyLaunchError; - use remote_server::unix::{execute_proxy, execute_run}; - +fn main() -> anyhow::Result<()> { let cli = Cli::parse(); if let Some(socket_path) = &cli.askpass { askpass::main(socket_path); - return; + return Ok(()); } if let Some(socket) = &cli.crash_handler { crashes::crash_server(socket.as_path()); - return; + return Ok(()); } if cli.printenv { util::shell_env::print_env(); - return; + return Ok(()); } - let result = match cli.command { - Some(Commands::Run { - log_file, - pid_file, - stdin_socket, - stdout_socket, - stderr_socket, - }) => execute_run( - log_file, - pid_file, - stdin_socket, - stdout_socket, - stderr_socket, - ), - Some(Commands::Proxy { - identifier, - reconnect, - }) => match execute_proxy(identifier, reconnect) { - Ok(_) => Ok(()), - Err(err) => { - if let Some(err) = err.downcast_ref::() { - std::process::exit(err.to_exit_code()); - } - Err(err) - } - }, - Some(Commands::Version) => { - let release_channel = *RELEASE_CHANNEL; - match release_channel { - ReleaseChannel::Stable | ReleaseChannel::Preview => { - println!("{}", env!("ZED_PKG_VERSION")) - } - ReleaseChannel::Nightly | ReleaseChannel::Dev => { - println!( - "{}", - option_env!("ZED_COMMIT_SHA").unwrap_or(release_channel.dev_name()) - ) - } - }; - std::process::exit(0); - } - None => { - eprintln!("usage: remote "); - std::process::exit(1); - } - }; - if let Err(error) = result { - log::error!("exiting due to error: {}", error); + if let Some(command) = cli.command { + remote_server::run(command) + } else { + eprintln!("usage: remote "); std::process::exit(1); } } diff --git a/crates/remote_server/src/remote_server.rs b/crates/remote_server/src/remote_server.rs index 52003969af..c14a4828ac 100644 --- a/crates/remote_server/src/remote_server.rs +++ b/crates/remote_server/src/remote_server.rs @@ -6,4 +6,78 @@ pub mod unix; #[cfg(test)] mod remote_editing_tests; +use clap::Subcommand; +use std::path::PathBuf; + pub use headless_project::{HeadlessAppState, HeadlessProject}; + +#[derive(Subcommand)] +pub enum Commands { + Run { + #[arg(long)] + log_file: PathBuf, + #[arg(long)] + pid_file: PathBuf, + #[arg(long)] + stdin_socket: PathBuf, + #[arg(long)] + stdout_socket: PathBuf, + #[arg(long)] + stderr_socket: PathBuf, + }, + Proxy { + #[arg(long)] + reconnect: bool, + #[arg(long)] + identifier: String, + }, + Version, +} + +#[cfg(not(windows))] +pub fn run(command: Commands) -> anyhow::Result<()> { + use anyhow::Context; + use release_channel::{RELEASE_CHANNEL, ReleaseChannel}; + use unix::{ExecuteProxyError, execute_proxy, execute_run}; + + match command { + Commands::Run { + log_file, + pid_file, + stdin_socket, + stdout_socket, + stderr_socket, + } => execute_run( + log_file, + pid_file, + stdin_socket, + stdout_socket, + stderr_socket, + ), + Commands::Proxy { + identifier, + reconnect, + } => execute_proxy(identifier, reconnect) + .inspect_err(|err| { + if let ExecuteProxyError::ServerNotRunning(err) = err { + std::process::exit(err.to_exit_code()); + } + }) + .context("running proxy on the remote server"), + Commands::Version => { + let release_channel = *RELEASE_CHANNEL; + match release_channel { + ReleaseChannel::Stable | ReleaseChannel::Preview => { + println!("{}", env!("ZED_PKG_VERSION")) + } + ReleaseChannel::Nightly | ReleaseChannel::Dev => { + println!( + "{}", + option_env!("ZED_COMMIT_SHA").unwrap_or(release_channel.dev_name()) + ) + } + }; + Ok(()) + } + } +} diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index b8a7351552..c6d1566d60 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -36,6 +36,7 @@ use smol::Async; use smol::{net::unix::UnixListener, stream::StreamExt as _}; use std::ffi::OsStr; use std::ops::ControlFlow; +use std::process::ExitStatus; use std::str::FromStr; use std::sync::LazyLock; use std::{env, thread}; @@ -46,6 +47,7 @@ use std::{ sync::Arc, }; use telemetry_events::LocationData; +use thiserror::Error; use util::ResultExt; pub static VERSION: LazyLock<&str> = LazyLock::new(|| match *RELEASE_CHANNEL { @@ -526,7 +528,23 @@ pub fn execute_run( Ok(()) } -#[derive(Clone)] +#[derive(Debug, Error)] +pub(crate) enum ServerPathError { + #[error("Failed to create server_dir `{path}`")] + CreateServerDir { + #[source] + source: std::io::Error, + path: PathBuf, + }, + #[error("Failed to create logs_dir `{path}`")] + CreateLogsDir { + #[source] + source: std::io::Error, + path: PathBuf, + }, +} + +#[derive(Clone, Debug)] struct ServerPaths { log_file: PathBuf, pid_file: PathBuf, @@ -536,10 +554,19 @@ struct ServerPaths { } impl ServerPaths { - fn new(identifier: &str) -> Result { + fn new(identifier: &str) -> Result { let server_dir = paths::remote_server_state_dir().join(identifier); - std::fs::create_dir_all(&server_dir)?; - std::fs::create_dir_all(&logs_dir())?; + std::fs::create_dir_all(&server_dir).map_err(|source| { + ServerPathError::CreateServerDir { + source, + path: server_dir.clone(), + } + })?; + let log_dir = logs_dir(); + std::fs::create_dir_all(log_dir).map_err(|source| ServerPathError::CreateLogsDir { + source: source, + path: log_dir.clone(), + })?; let pid_file = server_dir.join("server.pid"); let stdin_socket = server_dir.join("stdin.sock"); @@ -557,7 +584,43 @@ impl ServerPaths { } } -pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> { +#[derive(Debug, Error)] +pub(crate) enum ExecuteProxyError { + #[error("Failed to init server paths")] + ServerPath(#[from] ServerPathError), + + #[error(transparent)] + ServerNotRunning(#[from] ProxyLaunchError), + + #[error("Failed to check PidFile '{path}'")] + CheckPidFile { + #[source] + source: CheckPidError, + path: PathBuf, + }, + + #[error("Failed to kill existing server with pid '{pid}'")] + KillRunningServer { + #[source] + source: std::io::Error, + pid: u32, + }, + + #[error("failed to spawn server")] + SpawnServer(#[source] SpawnServerError), + + #[error("stdin_task failed")] + StdinTask(#[source] anyhow::Error), + #[error("stdout_task failed")] + StdoutTask(#[source] anyhow::Error), + #[error("stderr_task failed")] + StderrTask(#[source] anyhow::Error), +} + +pub(crate) fn execute_proxy( + identifier: String, + is_reconnecting: bool, +) -> Result<(), ExecuteProxyError> { init_logging_proxy(); let server_paths = ServerPaths::new(&identifier)?; @@ -574,12 +637,19 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> { log::info!("starting proxy process. PID: {}", std::process::id()); - let server_pid = check_pid_file(&server_paths.pid_file)?; + let server_pid = check_pid_file(&server_paths.pid_file).map_err(|source| { + ExecuteProxyError::CheckPidFile { + source, + path: server_paths.pid_file.clone(), + } + })?; let server_running = server_pid.is_some(); if is_reconnecting { if !server_running { log::error!("attempted to reconnect, but no server running"); - anyhow::bail!(ProxyLaunchError::ServerNotRunning); + return Err(ExecuteProxyError::ServerNotRunning( + ProxyLaunchError::ServerNotRunning, + )); } } else { if let Some(pid) = server_pid { @@ -590,7 +660,7 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> { kill_running_server(pid, &server_paths)?; } - spawn_server(&server_paths)?; + spawn_server(&server_paths).map_err(ExecuteProxyError::SpawnServer)?; }; let stdin_task = smol::spawn(async move { @@ -630,9 +700,9 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> { if let Err(forwarding_result) = smol::block_on(async move { futures::select! { - result = stdin_task.fuse() => result.context("stdin_task failed"), - result = stdout_task.fuse() => result.context("stdout_task failed"), - result = stderr_task.fuse() => result.context("stderr_task failed"), + result = stdin_task.fuse() => result.map_err(ExecuteProxyError::StdinTask), + result = stdout_task.fuse() => result.map_err(ExecuteProxyError::StdoutTask), + result = stderr_task.fuse() => result.map_err(ExecuteProxyError::StderrTask), } }) { log::error!( @@ -645,12 +715,12 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> { Ok(()) } -fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<()> { +fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<(), ExecuteProxyError> { log::info!("killing existing server with PID {}", pid); std::process::Command::new("kill") .arg(pid.to_string()) .output() - .context("failed to kill existing server")?; + .map_err(|source| ExecuteProxyError::KillRunningServer { source, pid })?; for file in [ &paths.pid_file, @@ -664,18 +734,39 @@ fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<()> { Ok(()) } -fn spawn_server(paths: &ServerPaths) -> Result<()> { +#[derive(Debug, Error)] +pub(crate) enum SpawnServerError { + #[error("failed to remove stdin socket")] + RemoveStdinSocket(#[source] std::io::Error), + + #[error("failed to remove stdout socket")] + RemoveStdoutSocket(#[source] std::io::Error), + + #[error("failed to remove stderr socket")] + RemoveStderrSocket(#[source] std::io::Error), + + #[error("failed to get current_exe")] + CurrentExe(#[source] std::io::Error), + + #[error("failed to launch server process")] + ProcessStatus(#[source] std::io::Error), + + #[error("failed to launch and detach server process: {status}\n{paths}")] + LaunchStatus { status: ExitStatus, paths: String }, +} + +fn spawn_server(paths: &ServerPaths) -> Result<(), SpawnServerError> { if paths.stdin_socket.exists() { - std::fs::remove_file(&paths.stdin_socket)?; + std::fs::remove_file(&paths.stdin_socket).map_err(SpawnServerError::RemoveStdinSocket)?; } if paths.stdout_socket.exists() { - std::fs::remove_file(&paths.stdout_socket)?; + std::fs::remove_file(&paths.stdout_socket).map_err(SpawnServerError::RemoveStdoutSocket)?; } if paths.stderr_socket.exists() { - std::fs::remove_file(&paths.stderr_socket)?; + std::fs::remove_file(&paths.stderr_socket).map_err(SpawnServerError::RemoveStderrSocket)?; } - let binary_name = std::env::current_exe()?; + let binary_name = std::env::current_exe().map_err(SpawnServerError::CurrentExe)?; let mut server_process = std::process::Command::new(binary_name); server_process .arg("run") @@ -692,11 +783,17 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> { let status = server_process .status() - .context("failed to launch server process")?; - anyhow::ensure!( - status.success(), - "failed to launch and detach server process" - ); + .map_err(SpawnServerError::ProcessStatus)?; + + if !status.success() { + return Err(SpawnServerError::LaunchStatus { + status, + paths: format!( + "log file: {:?}, pid file: {:?}", + paths.log_file, paths.pid_file, + ), + }); + } let mut total_time_waited = std::time::Duration::from_secs(0); let wait_duration = std::time::Duration::from_millis(20); @@ -717,7 +814,15 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> { Ok(()) } -fn check_pid_file(path: &Path) -> Result> { +#[derive(Debug, Error)] +#[error("Failed to remove PID file for missing process (pid `{pid}`")] +pub(crate) struct CheckPidError { + #[source] + source: std::io::Error, + pid: u32, +} + +fn check_pid_file(path: &Path) -> Result, CheckPidError> { let Some(pid) = std::fs::read_to_string(&path) .ok() .and_then(|contents| contents.parse::().ok()) @@ -742,7 +847,7 @@ fn check_pid_file(path: &Path) -> Result> { log::debug!( "Found PID file, but process with that PID does not exist. Removing PID file." ); - std::fs::remove_file(&path).context("Failed to remove PID file")?; + std::fs::remove_file(&path).map_err(|source| CheckPidError { source, pid })?; Ok(None) } } From 628a9cd8eab0c41aee0011bfab1462c7bc54adf5 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:34:55 -0300 Subject: [PATCH 676/693] thread view: Add link to docs in the toolbar plus menu (#36883) Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 10 ++++++++++ crates/ui/src/components/context_menu.rs | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 1eafb8dd4d..269aec3365 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -9,6 +9,7 @@ use agent_servers::AgentServerSettings; use agent2::{DbThreadMetadata, HistoryEntry}; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; +use zed_actions::OpenBrowser; use zed_actions::agent::ReauthenticateAgent; use crate::acp::{AcpThreadHistory, ThreadHistoryEvent}; @@ -2689,6 +2690,15 @@ impl AgentPanel { } menu + }) + .when(cx.has_flag::(), |menu| { + menu.separator().link( + "Add Your Own Agent", + OpenBrowser { + url: "https://agentclientprotocol.com/".into(), + } + .boxed_clone(), + ) }); menu })) diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 25575c4f1e..21ab283d88 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -561,7 +561,7 @@ impl ContextMenu { action: Some(action.boxed_clone()), handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)), icon: Some(IconName::ArrowUpRight), - icon_size: IconSize::Small, + icon_size: IconSize::XSmall, icon_position: IconPosition::End, icon_color: None, disabled: false, From 65de969cc858fe2d309895643754d5a0ad3d7880 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 26 Aug 2025 00:16:37 +0300 Subject: [PATCH 677/693] Do not show directories in the `InvalidBufferView` (#36906) Follow-up of https://github.com/zed-industries/zed/pull/36764 Release Notes: - N/A --- crates/editor/src/items.rs | 2 +- crates/language_tools/src/lsp_log.rs | 1 - crates/workspace/src/invalid_buffer_view.rs | 10 +- crates/workspace/src/item.rs | 4 +- crates/workspace/src/workspace.rs | 119 +++++++------------- 5 files changed, 50 insertions(+), 86 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 641e8a97ed..b7110190fd 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1404,7 +1404,7 @@ impl ProjectItem for Editor { } fn for_broken_project_item( - abs_path: PathBuf, + abs_path: &Path, is_local: bool, e: &anyhow::Error, window: &mut Window, diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 43c0365291..d5206c1f26 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -1743,6 +1743,5 @@ pub enum Event { } impl EventEmitter for LogStore {} -impl EventEmitter for LspLogView {} impl EventEmitter for LspLogView {} impl EventEmitter for LspLogView {} diff --git a/crates/workspace/src/invalid_buffer_view.rs b/crates/workspace/src/invalid_buffer_view.rs index b017373474..b8c0db29d3 100644 --- a/crates/workspace/src/invalid_buffer_view.rs +++ b/crates/workspace/src/invalid_buffer_view.rs @@ -1,4 +1,4 @@ -use std::{path::PathBuf, sync::Arc}; +use std::{path::Path, sync::Arc}; use gpui::{EventEmitter, FocusHandle, Focusable}; use ui::{ @@ -12,7 +12,7 @@ use crate::Item; /// A view to display when a certain buffer fails to open. pub struct InvalidBufferView { /// Which path was attempted to open. - pub abs_path: Arc, + pub abs_path: Arc, /// An error message, happened when opening the buffer. pub error: SharedString, is_local: bool, @@ -21,7 +21,7 @@ pub struct InvalidBufferView { impl InvalidBufferView { pub fn new( - abs_path: PathBuf, + abs_path: &Path, is_local: bool, e: &anyhow::Error, _: &mut Window, @@ -29,7 +29,7 @@ impl InvalidBufferView { ) -> Self { Self { is_local, - abs_path: Arc::new(abs_path), + abs_path: Arc::from(abs_path), error: format!("{e}").into(), focus_handle: cx.focus_handle(), } @@ -43,7 +43,7 @@ impl Item for InvalidBufferView { // Ensure we always render at least the filename. detail += 1; - let path = self.abs_path.as_path(); + let path = self.abs_path.as_ref(); let mut prefix = path; while detail > 0 { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 3485fcca43..db91bd82b9 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -23,7 +23,7 @@ use std::{ any::{Any, TypeId}, cell::RefCell, ops::Range, - path::PathBuf, + path::Path, rc::Rc, sync::Arc, time::Duration, @@ -1168,7 +1168,7 @@ pub trait ProjectItem: Item { /// with the error from that failure as an argument. /// Allows to open an item that can gracefully display and handle errors. fn for_broken_project_item( - _abs_path: PathBuf, + _abs_path: &Path, _is_local: bool, _e: &anyhow::Error, _window: &mut Window, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 0b4694601e..044601df97 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -613,48 +613,59 @@ impl ProjectItemRegistry { self.build_project_item_for_path_fns .push(|project, project_path, window, cx| { let project_path = project_path.clone(); - let abs_path = project.read(cx).absolute_path(&project_path, cx); + let is_file = project + .read(cx) + .entry_for_path(&project_path, cx) + .is_some_and(|entry| entry.is_file()); + let entry_abs_path = project.read(cx).absolute_path(&project_path, cx); let is_local = project.read(cx).is_local(); let project_item = ::try_open(project, &project_path, cx)?; let project = project.clone(); - Some(window.spawn(cx, async move |cx| match project_item.await { - Ok(project_item) => { - let project_item = project_item; - let project_entry_id: Option = - project_item.read_with(cx, project::ProjectItem::entry_id)?; - let build_workspace_item = Box::new( - |pane: &mut Pane, window: &mut Window, cx: &mut Context| { - Box::new(cx.new(|cx| { - T::for_project_item( - project, - Some(pane), - project_item, - window, - cx, - ) - })) as Box - }, - ) as Box<_>; - Ok((project_entry_id, build_workspace_item)) - } - Err(e) => match abs_path { - Some(abs_path) => match cx.update(|window, cx| { - T::for_broken_project_item(abs_path, is_local, &e, window, cx) - })? { - Some(broken_project_item_view) => { - let build_workspace_item = Box::new( + Some(window.spawn(cx, async move |cx| { + match project_item.await.with_context(|| { + format!( + "opening project path {:?}", + entry_abs_path.as_deref().unwrap_or(&project_path.path) + ) + }) { + Ok(project_item) => { + let project_item = project_item; + let project_entry_id: Option = + project_item.read_with(cx, project::ProjectItem::entry_id)?; + let build_workspace_item = Box::new( + |pane: &mut Pane, window: &mut Window, cx: &mut Context| { + Box::new(cx.new(|cx| { + T::for_project_item( + project, + Some(pane), + project_item, + window, + cx, + ) + })) as Box + }, + ) as Box<_>; + Ok((project_entry_id, build_workspace_item)) + } + Err(e) => match entry_abs_path.as_deref().filter(|_| is_file) { + Some(abs_path) => match cx.update(|window, cx| { + T::for_broken_project_item(abs_path, is_local, &e, window, cx) + })? { + Some(broken_project_item_view) => { + let build_workspace_item = Box::new( move |_: &mut Pane, _: &mut Window, cx: &mut Context| { cx.new(|_| broken_project_item_view).boxed_clone() }, ) as Box<_>; - Ok((None, build_workspace_item)) - } + Ok((None, build_workspace_item)) + } + None => Err(e)?, + }, None => Err(e)?, }, - None => Err(e)?, - }, + } })) }); } @@ -4011,52 +4022,6 @@ impl Workspace { maybe_pane_handle } - pub fn split_pane_with_item( - &mut self, - pane_to_split: WeakEntity, - split_direction: SplitDirection, - from: WeakEntity, - item_id_to_move: EntityId, - window: &mut Window, - cx: &mut Context, - ) { - let Some(pane_to_split) = pane_to_split.upgrade() else { - return; - }; - let Some(from) = from.upgrade() else { - return; - }; - - let new_pane = self.add_pane(window, cx); - move_item(&from, &new_pane, item_id_to_move, 0, true, window, cx); - self.center - .split(&pane_to_split, &new_pane, split_direction) - .unwrap(); - cx.notify(); - } - - pub fn split_pane_with_project_entry( - &mut self, - pane_to_split: WeakEntity, - split_direction: SplitDirection, - project_entry: ProjectEntryId, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - let pane_to_split = pane_to_split.upgrade()?; - let new_pane = self.add_pane(window, cx); - self.center - .split(&pane_to_split, &new_pane, split_direction) - .unwrap(); - - let path = self.project.read(cx).path_for_entry(project_entry, cx)?; - let task = self.open_path(path, Some(new_pane.downgrade()), true, window, cx); - Some(cx.foreground_executor().spawn(async move { - task.await?; - Ok(()) - })) - } - pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context) { let active_item = self.active_pane.read(cx).active_item(); for pane in &self.panes { From 1460573dd4397e193764e80f2854ba33d94495ce Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 25 Aug 2025 16:04:44 -0600 Subject: [PATCH 678/693] acp: Rename dev command (#36908) Release Notes: - N/A --- crates/acp_tools/src/acp_tools.rs | 4 ++-- crates/zed/src/zed.rs | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs index ee12b04cde..e20a040e9d 100644 --- a/crates/acp_tools/src/acp_tools.rs +++ b/crates/acp_tools/src/acp_tools.rs @@ -21,12 +21,12 @@ use ui::prelude::*; use util::ResultExt as _; use workspace::{Item, Workspace}; -actions!(acp, [OpenDebugTools]); +actions!(dev, [OpenAcpLogs]); pub fn init(cx: &mut App) { cx.observe_new( |workspace: &mut Workspace, _window, _cx: &mut Context| { - workspace.register_action(|workspace, _: &OpenDebugTools, window, cx| { + workspace.register_action(|workspace, _: &OpenAcpLogs, window, cx| { let acp_tools = Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx))); workspace.add_item_to_active_pane(acp_tools, None, true, window, cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 1b9657dcc6..638e1dca0e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4434,7 +4434,6 @@ mod tests { assert_eq!(actions_without_namespace, Vec::<&str>::new()); let expected_namespaces = vec![ - "acp", "activity_indicator", "agent", #[cfg(not(target_os = "macos"))] From f8667a837949597200e2ae8e490d947c6cda75aa Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 25 Aug 2025 16:23:58 -0600 Subject: [PATCH 679/693] Remove unused files (#36909) Closes #ISSUE Release Notes: - N/A --- crates/agent2/src/tests/mod.rs | 1 + crates/agent_servers/src/acp/v0.rs | 524 ----------------------------- crates/agent_servers/src/acp/v1.rs | 376 --------------------- 3 files changed, 1 insertion(+), 900 deletions(-) delete mode 100644 crates/agent_servers/src/acp/v0.rs delete mode 100644 crates/agent_servers/src/acp/v1.rs diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 864fbf8b10..093b8ba971 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1347,6 +1347,7 @@ async fn test_cancellation(cx: &mut TestAppContext) { } #[gpui::test] +#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs deleted file mode 100644 index be96048929..0000000000 --- a/crates/agent_servers/src/acp/v0.rs +++ /dev/null @@ -1,524 +0,0 @@ -// Translates old acp agents into the new schema -use action_log::ActionLog; -use agent_client_protocol as acp; -use agentic_coding_protocol::{self as acp_old, AgentRequest as _}; -use anyhow::{Context as _, Result, anyhow}; -use futures::channel::oneshot; -use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity}; -use project::Project; -use std::{any::Any, cell::RefCell, path::Path, rc::Rc}; -use ui::App; -use util::ResultExt as _; - -use crate::AgentServerCommand; -use acp_thread::{AcpThread, AgentConnection, AuthRequired}; - -#[derive(Clone)] -struct OldAcpClientDelegate { - thread: Rc>>, - cx: AsyncApp, - next_tool_call_id: Rc>, - // sent_buffer_versions: HashMap, HashMap>, -} - -impl OldAcpClientDelegate { - fn new(thread: Rc>>, cx: AsyncApp) -> Self { - Self { - thread, - cx, - next_tool_call_id: Rc::new(RefCell::new(0)), - } - } -} - -impl acp_old::Client for OldAcpClientDelegate { - async fn stream_assistant_message_chunk( - &self, - params: acp_old::StreamAssistantMessageChunkParams, - ) -> Result<(), acp_old::Error> { - let cx = &mut self.cx.clone(); - - cx.update(|cx| { - self.thread - .borrow() - .update(cx, |thread, cx| match params.chunk { - acp_old::AssistantMessageChunk::Text { text } => { - thread.push_assistant_content_block(text.into(), false, cx) - } - acp_old::AssistantMessageChunk::Thought { thought } => { - thread.push_assistant_content_block(thought.into(), true, cx) - } - }) - .log_err(); - })?; - - Ok(()) - } - - async fn request_tool_call_confirmation( - &self, - request: acp_old::RequestToolCallConfirmationParams, - ) -> Result { - let cx = &mut self.cx.clone(); - - let old_acp_id = *self.next_tool_call_id.borrow() + 1; - self.next_tool_call_id.replace(old_acp_id); - - let tool_call = into_new_tool_call( - acp::ToolCallId(old_acp_id.to_string().into()), - request.tool_call, - ); - - let mut options = match request.confirmation { - acp_old::ToolCallConfirmation::Edit { .. } => vec![( - acp_old::ToolCallConfirmationOutcome::AlwaysAllow, - acp::PermissionOptionKind::AllowAlways, - "Always Allow Edits".to_string(), - )], - acp_old::ToolCallConfirmation::Execute { root_command, .. } => vec![( - acp_old::ToolCallConfirmationOutcome::AlwaysAllow, - acp::PermissionOptionKind::AllowAlways, - format!("Always Allow {}", root_command), - )], - acp_old::ToolCallConfirmation::Mcp { - server_name, - tool_name, - .. - } => vec![ - ( - acp_old::ToolCallConfirmationOutcome::AlwaysAllowMcpServer, - acp::PermissionOptionKind::AllowAlways, - format!("Always Allow {}", server_name), - ), - ( - acp_old::ToolCallConfirmationOutcome::AlwaysAllowTool, - acp::PermissionOptionKind::AllowAlways, - format!("Always Allow {}", tool_name), - ), - ], - acp_old::ToolCallConfirmation::Fetch { .. } => vec![( - acp_old::ToolCallConfirmationOutcome::AlwaysAllow, - acp::PermissionOptionKind::AllowAlways, - "Always Allow".to_string(), - )], - acp_old::ToolCallConfirmation::Other { .. } => vec![( - acp_old::ToolCallConfirmationOutcome::AlwaysAllow, - acp::PermissionOptionKind::AllowAlways, - "Always Allow".to_string(), - )], - }; - - options.extend([ - ( - acp_old::ToolCallConfirmationOutcome::Allow, - acp::PermissionOptionKind::AllowOnce, - "Allow".to_string(), - ), - ( - acp_old::ToolCallConfirmationOutcome::Reject, - acp::PermissionOptionKind::RejectOnce, - "Reject".to_string(), - ), - ]); - - let mut outcomes = Vec::with_capacity(options.len()); - let mut acp_options = Vec::with_capacity(options.len()); - - for (index, (outcome, kind, label)) in options.into_iter().enumerate() { - outcomes.push(outcome); - acp_options.push(acp::PermissionOption { - id: acp::PermissionOptionId(index.to_string().into()), - name: label, - kind, - }) - } - - let response = cx - .update(|cx| { - self.thread.borrow().update(cx, |thread, cx| { - thread.request_tool_call_authorization(tool_call.into(), acp_options, cx) - }) - })?? - .context("Failed to update thread")? - .await; - - let outcome = match response { - Ok(option_id) => outcomes[option_id.0.parse::().unwrap_or(0)], - Err(oneshot::Canceled) => acp_old::ToolCallConfirmationOutcome::Cancel, - }; - - Ok(acp_old::RequestToolCallConfirmationResponse { - id: acp_old::ToolCallId(old_acp_id), - outcome, - }) - } - - async fn push_tool_call( - &self, - request: acp_old::PushToolCallParams, - ) -> Result { - let cx = &mut self.cx.clone(); - - let old_acp_id = *self.next_tool_call_id.borrow() + 1; - self.next_tool_call_id.replace(old_acp_id); - - cx.update(|cx| { - self.thread.borrow().update(cx, |thread, cx| { - thread.upsert_tool_call( - into_new_tool_call(acp::ToolCallId(old_acp_id.to_string().into()), request), - cx, - ) - }) - })?? - .context("Failed to update thread")?; - - Ok(acp_old::PushToolCallResponse { - id: acp_old::ToolCallId(old_acp_id), - }) - } - - async fn update_tool_call( - &self, - request: acp_old::UpdateToolCallParams, - ) -> Result<(), acp_old::Error> { - let cx = &mut self.cx.clone(); - - cx.update(|cx| { - self.thread.borrow().update(cx, |thread, cx| { - thread.update_tool_call( - acp::ToolCallUpdate { - id: acp::ToolCallId(request.tool_call_id.0.to_string().into()), - fields: acp::ToolCallUpdateFields { - status: Some(into_new_tool_call_status(request.status)), - content: Some( - request - .content - .into_iter() - .map(into_new_tool_call_content) - .collect::>(), - ), - ..Default::default() - }, - }, - cx, - ) - }) - })? - .context("Failed to update thread")??; - - Ok(()) - } - - async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> { - let cx = &mut self.cx.clone(); - - cx.update(|cx| { - self.thread.borrow().update(cx, |thread, cx| { - thread.update_plan( - acp::Plan { - entries: request - .entries - .into_iter() - .map(into_new_plan_entry) - .collect(), - }, - cx, - ) - }) - })? - .context("Failed to update thread")?; - - Ok(()) - } - - async fn read_text_file( - &self, - acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams, - ) -> Result { - let content = self - .cx - .update(|cx| { - self.thread.borrow().update(cx, |thread, cx| { - thread.read_text_file(path, line, limit, false, cx) - }) - })? - .context("Failed to update thread")? - .await?; - Ok(acp_old::ReadTextFileResponse { content }) - } - - async fn write_text_file( - &self, - acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams, - ) -> Result<(), acp_old::Error> { - self.cx - .update(|cx| { - self.thread - .borrow() - .update(cx, |thread, cx| thread.write_text_file(path, content, cx)) - })? - .context("Failed to update thread")? - .await?; - - Ok(()) - } -} - -fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall { - acp::ToolCall { - id, - title: request.label, - kind: acp_kind_from_old_icon(request.icon), - status: acp::ToolCallStatus::InProgress, - content: request - .content - .into_iter() - .map(into_new_tool_call_content) - .collect(), - locations: request - .locations - .into_iter() - .map(into_new_tool_call_location) - .collect(), - raw_input: None, - raw_output: None, - } -} - -fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind { - match icon { - acp_old::Icon::FileSearch => acp::ToolKind::Search, - acp_old::Icon::Folder => acp::ToolKind::Search, - acp_old::Icon::Globe => acp::ToolKind::Search, - acp_old::Icon::Hammer => acp::ToolKind::Other, - acp_old::Icon::LightBulb => acp::ToolKind::Think, - acp_old::Icon::Pencil => acp::ToolKind::Edit, - acp_old::Icon::Regex => acp::ToolKind::Search, - acp_old::Icon::Terminal => acp::ToolKind::Execute, - } -} - -fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus { - match status { - acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress, - acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed, - acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed, - } -} - -fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent { - match content { - acp_old::ToolCallContent::Markdown { markdown } => markdown.into(), - acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff { - diff: into_new_diff(diff), - }, - } -} - -fn into_new_diff(diff: acp_old::Diff) -> acp::Diff { - acp::Diff { - path: diff.path, - old_text: diff.old_text, - new_text: diff.new_text, - } -} - -fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation { - acp::ToolCallLocation { - path: location.path, - line: location.line, - } -} - -fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry { - acp::PlanEntry { - content: entry.content, - priority: into_new_plan_priority(entry.priority), - status: into_new_plan_status(entry.status), - } -} - -fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority { - match priority { - acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low, - acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium, - acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High, - } -} - -fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus { - match status { - acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending, - acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress, - acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed, - } -} - -pub struct AcpConnection { - pub name: &'static str, - pub connection: acp_old::AgentConnection, - pub _child_status: Task>, - pub current_thread: Rc>>, -} - -impl AcpConnection { - pub fn stdio( - name: &'static str, - command: AgentServerCommand, - root_dir: &Path, - cx: &mut AsyncApp, - ) -> Task> { - let root_dir = root_dir.to_path_buf(); - - cx.spawn(async move |cx| { - let mut child = util::command::new_smol_command(&command.path) - .args(command.args.iter()) - .current_dir(root_dir) - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::inherit()) - .kill_on_drop(true) - .spawn()?; - - let stdin = child.stdin.take().unwrap(); - let stdout = child.stdout.take().unwrap(); - log::trace!("Spawned (pid: {})", child.id()); - - let foreground_executor = cx.foreground_executor().clone(); - - let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid())); - - let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent( - OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()), - stdin, - stdout, - move |fut| foreground_executor.spawn(fut).detach(), - ); - - let io_task = cx.background_spawn(async move { - io_fut.await.log_err(); - }); - - let child_status = cx.background_spawn(async move { - let result = match child.status().await { - Err(e) => Err(anyhow!(e)), - Ok(result) if result.success() => Ok(()), - Ok(result) => Err(anyhow!(result)), - }; - drop(io_task); - result - }); - - Ok(Self { - name, - connection, - _child_status: child_status, - current_thread: thread_rc, - }) - }) - } -} - -impl AgentConnection for AcpConnection { - fn new_thread( - self: Rc, - project: Entity, - _cwd: &Path, - cx: &mut App, - ) -> Task>> { - let task = self.connection.request_any( - acp_old::InitializeParams { - protocol_version: acp_old::ProtocolVersion::latest(), - } - .into_any(), - ); - let current_thread = self.current_thread.clone(); - cx.spawn(async move |cx| { - let result = task.await?; - let result = acp_old::InitializeParams::response_from_any(result)?; - - if !result.is_authenticated { - anyhow::bail!(AuthRequired::new()) - } - - cx.update(|cx| { - let thread = cx.new(|cx| { - let session_id = acp::SessionId("acp-old-no-id".into()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); - AcpThread::new(self.name, self.clone(), project, action_log, session_id) - }); - current_thread.replace(thread.downgrade()); - thread - }) - }) - } - - fn auth_methods(&self) -> &[acp::AuthMethod] { - &[] - } - - fn authenticate(&self, _method_id: acp::AuthMethodId, cx: &mut App) -> Task> { - let task = self - .connection - .request_any(acp_old::AuthenticateParams.into_any()); - cx.foreground_executor().spawn(async move { - task.await?; - Ok(()) - }) - } - - fn prompt( - &self, - _id: Option, - params: acp::PromptRequest, - cx: &mut App, - ) -> Task> { - let chunks = params - .prompt - .into_iter() - .filter_map(|block| match block { - acp::ContentBlock::Text(text) => { - Some(acp_old::UserMessageChunk::Text { text: text.text }) - } - acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path { - path: link.uri.into(), - }), - _ => None, - }) - .collect(); - - let task = self - .connection - .request_any(acp_old::SendUserMessageParams { chunks }.into_any()); - cx.foreground_executor().spawn(async move { - task.await?; - anyhow::Ok(acp::PromptResponse { - stop_reason: acp::StopReason::EndTurn, - }) - }) - } - - fn prompt_capabilities(&self) -> acp::PromptCapabilities { - acp::PromptCapabilities { - image: false, - audio: false, - embedded_context: false, - } - } - - fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) { - let task = self - .connection - .request_any(acp_old::CancelSendMessageParams.into_any()); - cx.foreground_executor() - .spawn(async move { - task.await?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx) - } - - fn into_any(self: Rc) -> Rc { - self - } -} diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs deleted file mode 100644 index 1945ad2483..0000000000 --- a/crates/agent_servers/src/acp/v1.rs +++ /dev/null @@ -1,376 +0,0 @@ -use acp_tools::AcpConnectionRegistry; -use action_log::ActionLog; -use agent_client_protocol::{self as acp, Agent as _, ErrorCode}; -use anyhow::anyhow; -use collections::HashMap; -use futures::AsyncBufReadExt as _; -use futures::channel::oneshot; -use futures::io::BufReader; -use project::Project; -use serde::Deserialize; -use std::path::Path; -use std::rc::Rc; -use std::{any::Any, cell::RefCell}; - -use anyhow::{Context as _, Result}; -use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity}; - -use crate::{AgentServerCommand, acp::UnsupportedVersion}; -use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError}; - -pub struct AcpConnection { - server_name: &'static str, - connection: Rc, - sessions: Rc>>, - auth_methods: Vec, - prompt_capabilities: acp::PromptCapabilities, - _io_task: Task>, -} - -pub struct AcpSession { - thread: WeakEntity, - suppress_abort_err: bool, -} - -const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1; - -impl AcpConnection { - pub async fn stdio( - server_name: &'static str, - command: AgentServerCommand, - root_dir: &Path, - cx: &mut AsyncApp, - ) -> Result { - let mut child = util::command::new_smol_command(&command.path) - .args(command.args.iter().map(|arg| arg.as_str())) - .envs(command.env.iter().flatten()) - .current_dir(root_dir) - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .kill_on_drop(true) - .spawn()?; - - let stdout = child.stdout.take().context("Failed to take stdout")?; - let stdin = child.stdin.take().context("Failed to take stdin")?; - let stderr = child.stderr.take().context("Failed to take stderr")?; - log::trace!("Spawned (pid: {})", child.id()); - - let sessions = Rc::new(RefCell::new(HashMap::default())); - - let client = ClientDelegate { - sessions: sessions.clone(), - cx: cx.clone(), - }; - let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, { - let foreground_executor = cx.foreground_executor().clone(); - move |fut| { - foreground_executor.spawn(fut).detach(); - } - }); - - let io_task = cx.background_spawn(io_task); - - cx.background_spawn(async move { - let mut stderr = BufReader::new(stderr); - let mut line = String::new(); - while let Ok(n) = stderr.read_line(&mut line).await - && n > 0 - { - log::warn!("agent stderr: {}", &line); - line.clear(); - } - }) - .detach(); - - cx.spawn({ - let sessions = sessions.clone(); - async move |cx| { - let status = child.status().await?; - - for session in sessions.borrow().values() { - session - .thread - .update(cx, |thread, cx| { - thread.emit_load_error(LoadError::Exited { status }, cx) - }) - .ok(); - } - - anyhow::Ok(()) - } - }) - .detach(); - - let connection = Rc::new(connection); - - cx.update(|cx| { - AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| { - registry.set_active_connection(server_name, &connection, cx) - }); - })?; - - let response = connection - .initialize(acp::InitializeRequest { - protocol_version: acp::VERSION, - client_capabilities: acp::ClientCapabilities { - fs: acp::FileSystemCapability { - read_text_file: true, - write_text_file: true, - }, - }, - }) - .await?; - - if response.protocol_version < MINIMUM_SUPPORTED_VERSION { - return Err(UnsupportedVersion.into()); - } - - Ok(Self { - auth_methods: response.auth_methods, - connection, - server_name, - sessions, - prompt_capabilities: response.agent_capabilities.prompt_capabilities, - _io_task: io_task, - }) - } -} - -impl AgentConnection for AcpConnection { - fn new_thread( - self: Rc, - project: Entity, - cwd: &Path, - cx: &mut App, - ) -> Task>> { - let conn = self.connection.clone(); - let sessions = self.sessions.clone(); - let cwd = cwd.to_path_buf(); - cx.spawn(async move |cx| { - let response = conn - .new_session(acp::NewSessionRequest { - mcp_servers: vec![], - cwd, - }) - .await - .map_err(|err| { - if err.code == acp::ErrorCode::AUTH_REQUIRED.code { - let mut error = AuthRequired::new(); - - if err.message != acp::ErrorCode::AUTH_REQUIRED.message { - error = error.with_description(err.message); - } - - anyhow!(error) - } else { - anyhow!(err) - } - })?; - - let session_id = response.session_id; - let action_log = cx.new(|_| ActionLog::new(project.clone()))?; - let thread = cx.new(|_cx| { - AcpThread::new( - self.server_name, - self.clone(), - project, - action_log, - session_id.clone(), - ) - })?; - - let session = AcpSession { - thread: thread.downgrade(), - suppress_abort_err: false, - }; - sessions.borrow_mut().insert(session_id, session); - - Ok(thread) - }) - } - - fn auth_methods(&self) -> &[acp::AuthMethod] { - &self.auth_methods - } - - fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task> { - let conn = self.connection.clone(); - cx.foreground_executor().spawn(async move { - let result = conn - .authenticate(acp::AuthenticateRequest { - method_id: method_id.clone(), - }) - .await?; - - Ok(result) - }) - } - - fn prompt( - &self, - _id: Option, - params: acp::PromptRequest, - cx: &mut App, - ) -> Task> { - let conn = self.connection.clone(); - let sessions = self.sessions.clone(); - let session_id = params.session_id.clone(); - cx.foreground_executor().spawn(async move { - let result = conn.prompt(params).await; - - let mut suppress_abort_err = false; - - if let Some(session) = sessions.borrow_mut().get_mut(&session_id) { - suppress_abort_err = session.suppress_abort_err; - session.suppress_abort_err = false; - } - - match result { - Ok(response) => Ok(response), - Err(err) => { - if err.code != ErrorCode::INTERNAL_ERROR.code { - anyhow::bail!(err) - } - - let Some(data) = &err.data else { - anyhow::bail!(err) - }; - - // Temporary workaround until the following PR is generally available: - // https://github.com/google-gemini/gemini-cli/pull/6656 - - #[derive(Deserialize)] - #[serde(deny_unknown_fields)] - struct ErrorDetails { - details: Box, - } - - match serde_json::from_value(data.clone()) { - Ok(ErrorDetails { details }) => { - if suppress_abort_err && details.contains("This operation was aborted") - { - Ok(acp::PromptResponse { - stop_reason: acp::StopReason::Cancelled, - }) - } else { - Err(anyhow!(details)) - } - } - Err(_) => Err(anyhow!(err)), - } - } - } - }) - } - - fn prompt_capabilities(&self) -> acp::PromptCapabilities { - self.prompt_capabilities - } - - fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { - if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) { - session.suppress_abort_err = true; - } - let conn = self.connection.clone(); - let params = acp::CancelNotification { - session_id: session_id.clone(), - }; - cx.foreground_executor() - .spawn(async move { conn.cancel(params).await }) - .detach(); - } - - fn into_any(self: Rc) -> Rc { - self - } -} - -struct ClientDelegate { - sessions: Rc>>, - cx: AsyncApp, -} - -impl acp::Client for ClientDelegate { - async fn request_permission( - &self, - arguments: acp::RequestPermissionRequest, - ) -> Result { - let cx = &mut self.cx.clone(); - let rx = self - .sessions - .borrow() - .get(&arguments.session_id) - .context("Failed to get session")? - .thread - .update(cx, |thread, cx| { - thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx) - })?; - - let result = rx?.await; - - let outcome = match result { - Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option }, - Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled, - }; - - Ok(acp::RequestPermissionResponse { outcome }) - } - - async fn write_text_file( - &self, - arguments: acp::WriteTextFileRequest, - ) -> Result<(), acp::Error> { - let cx = &mut self.cx.clone(); - let task = self - .sessions - .borrow() - .get(&arguments.session_id) - .context("Failed to get session")? - .thread - .update(cx, |thread, cx| { - thread.write_text_file(arguments.path, arguments.content, cx) - })?; - - task.await?; - - Ok(()) - } - - async fn read_text_file( - &self, - arguments: acp::ReadTextFileRequest, - ) -> Result { - let cx = &mut self.cx.clone(); - let task = self - .sessions - .borrow() - .get(&arguments.session_id) - .context("Failed to get session")? - .thread - .update(cx, |thread, cx| { - thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx) - })?; - - let content = task.await?; - - Ok(acp::ReadTextFileResponse { content }) - } - - async fn session_notification( - &self, - notification: acp::SessionNotification, - ) -> Result<(), acp::Error> { - let cx = &mut self.cx.clone(); - let sessions = self.sessions.borrow(); - let session = sessions - .get(¬ification.session_id) - .context("Failed to get session")?; - - session.thread.update(cx, |thread, cx| { - thread.handle_session_update(notification.update, cx) - })??; - - Ok(()) - } -} From d43df9e841bce3af1df219690c5c796f8bbff99a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 25 Aug 2025 17:27:52 -0700 Subject: [PATCH 680/693] Fix workspace migration failure (#36911) This fixes a regression on nightly introduced in https://github.com/zed-industries/zed/pull/36714 Release Notes: - N/A --- crates/command_palette/src/persistence.rs | 18 +- crates/db/src/db.rs | 118 +--- crates/db/src/kvp.rs | 30 +- crates/editor/src/persistence.rs | 27 +- crates/image_viewer/src/image_viewer.rs | 19 +- crates/onboarding/src/onboarding.rs | 21 +- crates/onboarding/src/welcome.rs | 21 +- crates/settings_ui/src/keybindings.rs | 15 +- crates/sqlez/src/domain.rs | 14 +- crates/sqlez/src/migrations.rs | 64 +- crates/sqlez/src/thread_safe_connection.rs | 18 +- crates/terminal_view/src/persistence.rs | 18 +- crates/vim/src/state.rs | 18 +- crates/workspace/src/path_list.rs | 14 +- crates/workspace/src/persistence.rs | 643 +++++++++--------- .../src/zed/component_preview/persistence.rs | 19 +- 16 files changed, 582 insertions(+), 495 deletions(-) diff --git a/crates/command_palette/src/persistence.rs b/crates/command_palette/src/persistence.rs index 5be97c36bc..01cf403083 100644 --- a/crates/command_palette/src/persistence.rs +++ b/crates/command_palette/src/persistence.rs @@ -1,7 +1,10 @@ use anyhow::Result; use db::{ - define_connection, query, - sqlez::{bindable::Column, statement::Statement}, + query, + sqlez::{ + bindable::Column, domain::Domain, statement::Statement, + thread_safe_connection::ThreadSafeConnection, + }, sqlez_macros::sql, }; use serde::{Deserialize, Serialize}; @@ -50,8 +53,11 @@ impl Column for SerializedCommandInvocation { } } -define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()> = - &[sql!( +pub struct CommandPaletteDB(ThreadSafeConnection); + +impl Domain for CommandPaletteDB { + const NAME: &str = stringify!(CommandPaletteDB); + const MIGRATIONS: &[&str] = &[sql!( CREATE TABLE IF NOT EXISTS command_invocations( id INTEGER PRIMARY KEY AUTOINCREMENT, command_name TEXT NOT NULL, @@ -59,7 +65,9 @@ define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()> last_invoked INTEGER DEFAULT (unixepoch()) NOT NULL ) STRICT; )]; -); +} + +db::static_connection!(COMMAND_PALETTE_HISTORY, CommandPaletteDB, []); impl CommandPaletteDB { pub async fn write_command_invocation( diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 8b790cbec8..0802bd8bb7 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -110,11 +110,14 @@ pub async fn open_test_db(db_name: &str) -> ThreadSafeConnection { } /// Implements a basic DB wrapper for a given domain +/// +/// Arguments: +/// - static variable name for connection +/// - type of connection wrapper +/// - dependencies, whose migrations should be run prior to this domain's migrations #[macro_export] -macro_rules! define_connection { - (pub static ref $id:ident: $t:ident<()> = $migrations:expr; $($global:ident)?) => { - pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection); - +macro_rules! static_connection { + ($id:ident, $t:ident, [ $($d:ty),* ] $(, $global:ident)?) => { impl ::std::ops::Deref for $t { type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection; @@ -123,16 +126,6 @@ macro_rules! define_connection { } } - impl $crate::sqlez::domain::Domain for $t { - fn name() -> &'static str { - stringify!($t) - } - - fn migrations() -> &'static [&'static str] { - $migrations - } - } - impl $t { #[cfg(any(test, feature = "test-support"))] pub async fn open_test_db(name: &'static str) -> Self { @@ -142,7 +135,8 @@ macro_rules! define_connection { #[cfg(any(test, feature = "test-support"))] pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { - $t($crate::smol::block_on($crate::open_test_db::<$t>(stringify!($id)))) + #[allow(unused_parens)] + $t($crate::smol::block_on($crate::open_test_db::<($($d,)* $t)>(stringify!($id)))) }); #[cfg(not(any(test, feature = "test-support")))] @@ -153,46 +147,10 @@ macro_rules! define_connection { } else { $crate::RELEASE_CHANNEL.dev_name() }; - $t($crate::smol::block_on($crate::open_db::<$t>(db_dir, scope))) + #[allow(unused_parens)] + $t($crate::smol::block_on($crate::open_db::<($($d,)* $t)>(db_dir, scope))) }); - }; - (pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr; $($global:ident)?) => { - pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection); - - impl ::std::ops::Deref for $t { - type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - impl $crate::sqlez::domain::Domain for $t { - fn name() -> &'static str { - stringify!($t) - } - - fn migrations() -> &'static [&'static str] { - $migrations - } - } - - #[cfg(any(test, feature = "test-support"))] - pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { - $t($crate::smol::block_on($crate::open_test_db::<($($d),+, $t)>(stringify!($id)))) - }); - - #[cfg(not(any(test, feature = "test-support")))] - pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { - let db_dir = $crate::database_dir(); - let scope = if false $(|| stringify!($global) == "global")? { - "global" - } else { - $crate::RELEASE_CHANNEL.dev_name() - }; - $t($crate::smol::block_on($crate::open_db::<($($d),+, $t)>(db_dir, scope))) - }); - }; + } } pub fn write_and_log(cx: &App, db_write: impl FnOnce() -> F + Send + 'static) @@ -219,17 +177,12 @@ mod tests { enum BadDB {} impl Domain for BadDB { - fn name() -> &'static str { - "db_tests" - } - - fn migrations() -> &'static [&'static str] { - &[ - sql!(CREATE TABLE test(value);), - // failure because test already exists - sql!(CREATE TABLE test(value);), - ] - } + const NAME: &str = "db_tests"; + const MIGRATIONS: &[&str] = &[ + sql!(CREATE TABLE test(value);), + // failure because test already exists + sql!(CREATE TABLE test(value);), + ]; } let tempdir = tempfile::Builder::new() @@ -251,25 +204,15 @@ mod tests { enum CorruptedDB {} impl Domain for CorruptedDB { - fn name() -> &'static str { - "db_tests" - } - - fn migrations() -> &'static [&'static str] { - &[sql!(CREATE TABLE test(value);)] - } + const NAME: &str = "db_tests"; + const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)]; } enum GoodDB {} impl Domain for GoodDB { - fn name() -> &'static str { - "db_tests" //Notice same name - } - - fn migrations() -> &'static [&'static str] { - &[sql!(CREATE TABLE test2(value);)] //But different migration - } + const NAME: &str = "db_tests"; //Notice same name + const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; } let tempdir = tempfile::Builder::new() @@ -305,25 +248,16 @@ mod tests { enum CorruptedDB {} impl Domain for CorruptedDB { - fn name() -> &'static str { - "db_tests" - } + const NAME: &str = "db_tests"; - fn migrations() -> &'static [&'static str] { - &[sql!(CREATE TABLE test(value);)] - } + const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)]; } enum GoodDB {} impl Domain for GoodDB { - fn name() -> &'static str { - "db_tests" //Notice same name - } - - fn migrations() -> &'static [&'static str] { - &[sql!(CREATE TABLE test2(value);)] //But different migration - } + const NAME: &str = "db_tests"; //Notice same name + const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; // But different migration } let tempdir = tempfile::Builder::new() diff --git a/crates/db/src/kvp.rs b/crates/db/src/kvp.rs index 256b789c9b..8ea877b35b 100644 --- a/crates/db/src/kvp.rs +++ b/crates/db/src/kvp.rs @@ -2,16 +2,26 @@ use gpui::App; use sqlez_macros::sql; use util::ResultExt as _; -use crate::{define_connection, query, write_and_log}; +use crate::{ + query, + sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, + write_and_log, +}; -define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> = - &[sql!( +pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnection); + +impl Domain for KeyValueStore { + const NAME: &str = stringify!(KeyValueStore); + + const MIGRATIONS: &[&str] = &[sql!( CREATE TABLE IF NOT EXISTS kv_store( key TEXT PRIMARY KEY, value TEXT NOT NULL ) STRICT; )]; -); +} + +crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []); pub trait Dismissable { const KEY: &'static str; @@ -91,15 +101,19 @@ mod tests { } } -define_connection!(pub static ref GLOBAL_KEY_VALUE_STORE: GlobalKeyValueStore<()> = - &[sql!( +pub struct GlobalKeyValueStore(ThreadSafeConnection); + +impl Domain for GlobalKeyValueStore { + const NAME: &str = stringify!(GlobalKeyValueStore); + const MIGRATIONS: &[&str] = &[sql!( CREATE TABLE IF NOT EXISTS kv_store( key TEXT PRIMARY KEY, value TEXT NOT NULL ) STRICT; )]; - global -); +} + +crate::static_connection!(GLOBAL_KEY_VALUE_STORE, GlobalKeyValueStore, [], global); impl GlobalKeyValueStore { query! { diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index 88fde53947..ec7c149b4e 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -1,13 +1,17 @@ use anyhow::Result; -use db::sqlez::bindable::{Bind, Column, StaticColumnCount}; -use db::sqlez::statement::Statement; +use db::{ + query, + sqlez::{ + bindable::{Bind, Column, StaticColumnCount}, + domain::Domain, + statement::Statement, + }, + sqlez_macros::sql, +}; use fs::MTime; use itertools::Itertools as _; use std::path::PathBuf; -use db::sqlez_macros::sql; -use db::{define_connection, query}; - use workspace::{ItemId, WorkspaceDb, WorkspaceId}; #[derive(Clone, Debug, PartialEq, Default)] @@ -83,7 +87,11 @@ impl Column for SerializedEditor { } } -define_connection!( +pub struct EditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection); + +impl Domain for EditorDb { + const NAME: &str = stringify!(EditorDb); + // Current schema shape using pseudo-rust syntax: // editors( // item_id: usize, @@ -113,7 +121,8 @@ define_connection!( // start: usize, // end: usize, // ) - pub static ref DB: EditorDb = &[ + + const MIGRATIONS: &[&str] = &[ sql! ( CREATE TABLE editors( item_id INTEGER NOT NULL, @@ -189,7 +198,9 @@ define_connection!( ) STRICT; ), ]; -); +} + +db::static_connection!(DB, EditorDb, [WorkspaceDb]); // https://www.sqlite.org/limits.html // > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER, diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index b96557b391..2dca57424b 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -401,12 +401,19 @@ pub fn init(cx: &mut App) { mod persistence { use std::path::PathBuf; - use db::{define_connection, query, sqlez_macros::sql}; + use db::{ + query, + sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, + sqlez_macros::sql, + }; use workspace::{ItemId, WorkspaceDb, WorkspaceId}; - define_connection! { - pub static ref IMAGE_VIEWER: ImageViewerDb = - &[sql!( + pub struct ImageViewerDb(ThreadSafeConnection); + + impl Domain for ImageViewerDb { + const NAME: &str = stringify!(ImageViewerDb); + + const MIGRATIONS: &[&str] = &[sql!( CREATE TABLE image_viewers ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -417,9 +424,11 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + )]; } + db::static_connection!(IMAGE_VIEWER, ImageViewerDb, [WorkspaceDb]); + impl ImageViewerDb { query! { pub async fn save_image_path( diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 884374a72f..873dd63201 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -850,13 +850,19 @@ impl workspace::SerializableItem for Onboarding { } mod persistence { - use db::{define_connection, query, sqlez_macros::sql}; + use db::{ + query, + sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, + sqlez_macros::sql, + }; use workspace::WorkspaceDb; - define_connection! { - pub static ref ONBOARDING_PAGES: OnboardingPagesDb = - &[ - sql!( + pub struct OnboardingPagesDb(ThreadSafeConnection); + + impl Domain for OnboardingPagesDb { + const NAME: &str = stringify!(OnboardingPagesDb); + + const MIGRATIONS: &[&str] = &[sql!( CREATE TABLE onboarding_pages ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -866,10 +872,11 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - ), - ]; + )]; } + db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]); + impl OnboardingPagesDb { query! { pub async fn save_onboarding_page( diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index 3fe9c32a48..8ff55d812b 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -414,13 +414,19 @@ impl workspace::SerializableItem for WelcomePage { } mod persistence { - use db::{define_connection, query, sqlez_macros::sql}; + use db::{ + query, + sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, + sqlez_macros::sql, + }; use workspace::WorkspaceDb; - define_connection! { - pub static ref WELCOME_PAGES: WelcomePagesDb = - &[ - sql!( + pub struct WelcomePagesDb(ThreadSafeConnection); + + impl Domain for WelcomePagesDb { + const NAME: &str = stringify!(WelcomePagesDb); + + const MIGRATIONS: &[&str] = (&[sql!( CREATE TABLE welcome_pages ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -430,10 +436,11 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - ), - ]; + )]); } + db::static_connection!(WELCOME_PAGES, WelcomePagesDb, [WorkspaceDb]); + impl WelcomePagesDb { query! { pub async fn save_welcome_page( diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 9c76725972..288f59c8e0 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -3348,12 +3348,15 @@ impl SerializableItem for KeymapEditor { } mod persistence { - use db::{define_connection, query, sqlez_macros::sql}; + use db::{query, sqlez::domain::Domain, sqlez_macros::sql}; use workspace::WorkspaceDb; - define_connection! { - pub static ref KEYBINDING_EDITORS: KeybindingEditorDb = - &[sql!( + pub struct KeybindingEditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection); + + impl Domain for KeybindingEditorDb { + const NAME: &str = stringify!(KeybindingEditorDb); + + const MIGRATIONS: &[&str] = &[sql!( CREATE TABLE keybinding_editors ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -3362,9 +3365,11 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + )]; } + db::static_connection!(KEYBINDING_EDITORS, KeybindingEditorDb, [WorkspaceDb]); + impl KeybindingEditorDb { query! { pub async fn save_keybinding_editor( diff --git a/crates/sqlez/src/domain.rs b/crates/sqlez/src/domain.rs index a83f4e18d6..5744a67da2 100644 --- a/crates/sqlez/src/domain.rs +++ b/crates/sqlez/src/domain.rs @@ -1,8 +1,12 @@ use crate::connection::Connection; pub trait Domain: 'static { - fn name() -> &'static str; - fn migrations() -> &'static [&'static str]; + const NAME: &str; + const MIGRATIONS: &[&str]; + + fn should_allow_migration_change(_index: usize, _old: &str, _new: &str) -> bool { + false + } } pub trait Migrator: 'static { @@ -17,7 +21,11 @@ impl Migrator for () { impl Migrator for D { fn migrate(connection: &Connection) -> anyhow::Result<()> { - connection.migrate(Self::name(), Self::migrations()) + connection.migrate( + Self::NAME, + Self::MIGRATIONS, + Self::should_allow_migration_change, + ) } } diff --git a/crates/sqlez/src/migrations.rs b/crates/sqlez/src/migrations.rs index 7c59ffe658..2429ddeb41 100644 --- a/crates/sqlez/src/migrations.rs +++ b/crates/sqlez/src/migrations.rs @@ -34,7 +34,12 @@ impl Connection { /// Note: Unlike everything else in SQLez, migrations are run eagerly, without first /// preparing the SQL statements. This makes it possible to do multi-statement schema /// updates in a single string without running into prepare errors. - pub fn migrate(&self, domain: &'static str, migrations: &[&'static str]) -> Result<()> { + pub fn migrate( + &self, + domain: &'static str, + migrations: &[&'static str], + mut should_allow_migration_change: impl FnMut(usize, &str, &str) -> bool, + ) -> Result<()> { self.with_savepoint("migrating", || { // Setup the migrations table unconditionally self.exec(indoc! {" @@ -65,9 +70,14 @@ impl Connection { &sqlformat::QueryParams::None, Default::default(), ); - if completed_migration == migration { + if completed_migration == migration + || migration.trim().starts_with("-- ALLOW_MIGRATION_CHANGE") + { // Migration already run. Continue continue; + } else if should_allow_migration_change(index, &completed_migration, &migration) + { + continue; } else { anyhow::bail!(formatdoc! {" Migration changed for {domain} at step {index} @@ -108,6 +118,7 @@ mod test { a TEXT, b TEXT )"}], + disallow_migration_change, ) .unwrap(); @@ -136,6 +147,7 @@ mod test { d TEXT )"}, ], + disallow_migration_change, ) .unwrap(); @@ -214,7 +226,11 @@ mod test { // Run the migration verifying that the row got dropped connection - .migrate("test", &["DELETE FROM test_table"]) + .migrate( + "test", + &["DELETE FROM test_table"], + disallow_migration_change, + ) .unwrap(); assert_eq!( connection @@ -232,7 +248,11 @@ mod test { // Run the same migration again and verify that the table was left unchanged connection - .migrate("test", &["DELETE FROM test_table"]) + .migrate( + "test", + &["DELETE FROM test_table"], + disallow_migration_change, + ) .unwrap(); assert_eq!( connection @@ -252,27 +272,28 @@ mod test { .migrate( "test migration", &[ - indoc! {" - CREATE TABLE test ( - col INTEGER - )"}, - indoc! {" - INSERT INTO test (col) VALUES (1)"}, + "CREATE TABLE test (col INTEGER)", + "INSERT INTO test (col) VALUES (1)", ], + disallow_migration_change, ) .unwrap(); + let mut migration_changed = false; + // Create another migration with the same domain but different steps let second_migration_result = connection.migrate( "test migration", &[ - indoc! {" - CREATE TABLE test ( - color INTEGER - )"}, - indoc! {" - INSERT INTO test (color) VALUES (1)"}, + "CREATE TABLE test (color INTEGER )", + "INSERT INTO test (color) VALUES (1)", ], + |_, old, new| { + assert_eq!(old, "CREATE TABLE test (col INTEGER)"); + assert_eq!(new, "CREATE TABLE test (color INTEGER)"); + migration_changed = true; + false + }, ); // Verify new migration returns error when run @@ -284,7 +305,11 @@ mod test { let connection = Connection::open_memory(Some("test_create_alter_drop")); connection - .migrate("first_migration", &["CREATE TABLE table1(a TEXT) STRICT;"]) + .migrate( + "first_migration", + &["CREATE TABLE table1(a TEXT) STRICT;"], + disallow_migration_change, + ) .unwrap(); connection @@ -305,6 +330,7 @@ mod test { ALTER TABLE table2 RENAME TO table1; "}], + disallow_migration_change, ) .unwrap(); @@ -312,4 +338,8 @@ mod test { assert_eq!(res, "test text"); } + + fn disallow_migration_change(_: usize, _: &str, _: &str) -> bool { + false + } } diff --git a/crates/sqlez/src/thread_safe_connection.rs b/crates/sqlez/src/thread_safe_connection.rs index afdc96586e..58d3afe78f 100644 --- a/crates/sqlez/src/thread_safe_connection.rs +++ b/crates/sqlez/src/thread_safe_connection.rs @@ -278,12 +278,8 @@ mod test { enum TestDomain {} impl Domain for TestDomain { - fn name() -> &'static str { - "test" - } - fn migrations() -> &'static [&'static str] { - &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"] - } + const NAME: &str = "test"; + const MIGRATIONS: &[&str] = &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"]; } for _ in 0..100 { @@ -312,12 +308,9 @@ mod test { fn wild_zed_lost_failure() { enum TestWorkspace {} impl Domain for TestWorkspace { - fn name() -> &'static str { - "workspace" - } + const NAME: &str = "workspace"; - fn migrations() -> &'static [&'static str] { - &[" + const MIGRATIONS: &[&str] = &[" CREATE TABLE workspaces( workspace_id INTEGER PRIMARY KEY, dock_visible INTEGER, -- Boolean @@ -336,8 +329,7 @@ mod test { ON DELETE CASCADE ON UPDATE CASCADE ) STRICT; - "] - } + "]; } let builder = diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index b93b267f58..c7ebd314e4 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -9,7 +9,11 @@ use std::path::{Path, PathBuf}; use ui::{App, Context, Pixels, Window}; use util::ResultExt as _; -use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql}; +use db::{ + query, + sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection}, + sqlez_macros::sql, +}; use workspace::{ ItemHandle, ItemId, Member, Pane, PaneAxis, PaneGroup, SerializableItem as _, Workspace, WorkspaceDb, WorkspaceId, @@ -375,9 +379,13 @@ impl<'de> Deserialize<'de> for SerializedAxis { } } -define_connection! { - pub static ref TERMINAL_DB: TerminalDb = - &[sql!( +pub struct TerminalDb(ThreadSafeConnection); + +impl Domain for TerminalDb { + const NAME: &str = stringify!(TerminalDb); + + const MIGRATIONS: &[&str] = &[ + sql!( CREATE TABLE terminals ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -414,6 +422,8 @@ define_connection! { ]; } +db::static_connection!(TERMINAL_DB, TerminalDb, [WorkspaceDb]); + impl TerminalDb { query! { pub async fn update_workspace_id( diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index c0176cb12c..fe4bc7433d 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -7,8 +7,10 @@ use crate::{motion::Motion, object::Object}; use anyhow::Result; use collections::HashMap; use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor}; -use db::define_connection; -use db::sqlez_macros::sql; +use db::{ + sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, + sqlez_macros::sql, +}; use editor::display_map::{is_invisible, replacement}; use editor::{Anchor, ClipboardSelection, Editor, MultiBuffer, ToPoint as EditorToPoint}; use gpui::{ @@ -1668,8 +1670,12 @@ impl MarksView { } } -define_connection! ( - pub static ref DB: VimDb = &[ +pub struct VimDb(ThreadSafeConnection); + +impl Domain for VimDb { + const NAME: &str = stringify!(VimDb); + + const MIGRATIONS: &[&str] = &[ sql! ( CREATE TABLE vim_marks ( workspace_id INTEGER, @@ -1689,7 +1695,9 @@ define_connection! ( ON vim_global_marks_paths(workspace_id, mark_name); ), ]; -); +} + +db::static_connection!(DB, VimDb, [WorkspaceDb]); struct SerializedMark { path: Arc, diff --git a/crates/workspace/src/path_list.rs b/crates/workspace/src/path_list.rs index 4f9ed42312..cf463e6b22 100644 --- a/crates/workspace/src/path_list.rs +++ b/crates/workspace/src/path_list.rs @@ -58,11 +58,7 @@ impl PathList { let mut paths: Vec = if serialized.paths.is_empty() { Vec::new() } else { - serde_json::from_str::>(&serialized.paths) - .unwrap_or(Vec::new()) - .into_iter() - .map(|s| SanitizedPath::from(s).into()) - .collect() + serialized.paths.split('\n').map(PathBuf::from).collect() }; let mut order: Vec = serialized @@ -85,7 +81,13 @@ impl PathList { pub fn serialize(&self) -> SerializedPathList { use std::fmt::Write as _; - let paths = serde_json::to_string(&self.paths).unwrap_or_default(); + let mut paths = String::new(); + for path in self.paths.iter() { + if !paths.is_empty() { + paths.push('\n'); + } + paths.push_str(&path.to_string_lossy()); + } let mut order = String::new(); for ix in self.order.iter() { diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 39a1e08c93..89e1147d8a 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -10,7 +10,11 @@ use std::{ use anyhow::{Context as _, Result, bail}; use collections::HashMap; -use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; +use db::{ + query, + sqlez::{connection::Connection, domain::Domain}, + sqlez_macros::sql, +}; use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size}; use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}; @@ -275,186 +279,189 @@ impl sqlez::bindable::Bind for SerializedPixels { } } -define_connection! { - pub static ref DB: WorkspaceDb<()> = - &[ - sql!( - CREATE TABLE workspaces( - workspace_id INTEGER PRIMARY KEY, - workspace_location BLOB UNIQUE, - dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. - dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. - dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. - left_sidebar_open INTEGER, // Boolean - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) - ) STRICT; +pub struct WorkspaceDb(ThreadSafeConnection); - CREATE TABLE pane_groups( - group_id INTEGER PRIMARY KEY, - workspace_id INTEGER NOT NULL, - parent_group_id INTEGER, // NULL indicates that this is a root node - position INTEGER, // NULL indicates that this is a root node - axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - ) STRICT; +impl Domain for WorkspaceDb { + const NAME: &str = stringify!(WorkspaceDb); - CREATE TABLE panes( - pane_id INTEGER PRIMARY KEY, - workspace_id INTEGER NOT NULL, - active INTEGER NOT NULL, // Boolean - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE - ) STRICT; + const MIGRATIONS: &[&str] = &[ + sql!( + CREATE TABLE workspaces( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. + dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. + dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. + left_sidebar_open INTEGER, // Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) + ) STRICT; - CREATE TABLE center_panes( - pane_id INTEGER PRIMARY KEY, - parent_group_id INTEGER, // NULL means that this is a root pane - position INTEGER, // NULL means that this is a root pane - FOREIGN KEY(pane_id) REFERENCES panes(pane_id) - ON DELETE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - ) STRICT; + CREATE TABLE pane_groups( + group_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + parent_group_id INTEGER, // NULL indicates that this is a root node + position INTEGER, // NULL indicates that this is a root node + axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; - CREATE TABLE items( - item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique - workspace_id INTEGER NOT NULL, - pane_id INTEGER NOT NULL, - kind TEXT NOT NULL, - position INTEGER NOT NULL, - active INTEGER NOT NULL, - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE, - FOREIGN KEY(pane_id) REFERENCES panes(pane_id) - ON DELETE CASCADE, - PRIMARY KEY(item_id, workspace_id) - ) STRICT; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN window_state TEXT; - ALTER TABLE workspaces ADD COLUMN window_x REAL; - ALTER TABLE workspaces ADD COLUMN window_y REAL; - ALTER TABLE workspaces ADD COLUMN window_width REAL; - ALTER TABLE workspaces ADD COLUMN window_height REAL; - ALTER TABLE workspaces ADD COLUMN display BLOB; - ), - // Drop foreign key constraint from workspaces.dock_pane to panes table. - sql!( - CREATE TABLE workspaces_2( - workspace_id INTEGER PRIMARY KEY, - workspace_location BLOB UNIQUE, - dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. - dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. - dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. - left_sidebar_open INTEGER, // Boolean - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - window_state TEXT, - window_x REAL, - window_y REAL, - window_width REAL, - window_height REAL, - display BLOB - ) STRICT; - INSERT INTO workspaces_2 SELECT * FROM workspaces; - DROP TABLE workspaces; - ALTER TABLE workspaces_2 RENAME TO workspaces; - ), - // Add panels related information - sql!( - ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT; - ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT; - ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT; - ), - // Add panel zoom persistence - sql!( - ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool - ), - // Add pane group flex data - sql!( - ALTER TABLE pane_groups ADD COLUMN flexes TEXT; - ), - // Add fullscreen field to workspace - // Deprecated, `WindowBounds` holds the fullscreen state now. - // Preserving so users can downgrade Zed. - sql!( - ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool - ), - // Add preview field to items - sql!( - ALTER TABLE items ADD COLUMN preview INTEGER; //bool - ), - // Add centered_layout field to workspace - sql!( - ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool - ), - sql!( - CREATE TABLE remote_projects ( - remote_project_id INTEGER NOT NULL UNIQUE, - path TEXT, - dev_server_name TEXT - ); - ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER; - ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths; - ), - sql!( - DROP TABLE remote_projects; - CREATE TABLE dev_server_projects ( - id INTEGER NOT NULL UNIQUE, - path TEXT, - dev_server_name TEXT - ); - ALTER TABLE workspaces DROP COLUMN remote_project_id; - ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL; - ), - sql!( - ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0; - ), - sql!( - CREATE TABLE ssh_projects ( - id INTEGER PRIMARY KEY, - host TEXT NOT NULL, - port INTEGER, - path TEXT NOT NULL, - user TEXT - ); - ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE; - ), - sql!( - ALTER TABLE ssh_projects RENAME COLUMN path TO paths; - ), - sql!( - CREATE TABLE toolchains ( - workspace_id INTEGER, - worktree_id INTEGER, - language_name TEXT NOT NULL, - name TEXT NOT NULL, - path TEXT NOT NULL, - PRIMARY KEY (workspace_id, worktree_id, language_name) - ); - ), - sql!( - ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}"; - ), - sql!( + CREATE TABLE panes( + pane_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + active INTEGER NOT NULL, // Boolean + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE + ) STRICT; + + CREATE TABLE center_panes( + pane_id INTEGER PRIMARY KEY, + parent_group_id INTEGER, // NULL means that this is a root pane + position INTEGER, // NULL means that this is a root pane + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; + + CREATE TABLE items( + item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique + workspace_id INTEGER NOT NULL, + pane_id INTEGER NOT NULL, + kind TEXT NOT NULL, + position INTEGER NOT NULL, + active INTEGER NOT NULL, + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + PRIMARY KEY(item_id, workspace_id) + ) STRICT; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN window_state TEXT; + ALTER TABLE workspaces ADD COLUMN window_x REAL; + ALTER TABLE workspaces ADD COLUMN window_y REAL; + ALTER TABLE workspaces ADD COLUMN window_width REAL; + ALTER TABLE workspaces ADD COLUMN window_height REAL; + ALTER TABLE workspaces ADD COLUMN display BLOB; + ), + // Drop foreign key constraint from workspaces.dock_pane to panes table. + sql!( + CREATE TABLE workspaces_2( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. + dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. + dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. + left_sidebar_open INTEGER, // Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + window_state TEXT, + window_x REAL, + window_y REAL, + window_width REAL, + window_height REAL, + display BLOB + ) STRICT; + INSERT INTO workspaces_2 SELECT * FROM workspaces; + DROP TABLE workspaces; + ALTER TABLE workspaces_2 RENAME TO workspaces; + ), + // Add panels related information + sql!( + ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT; + ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT; + ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT; + ), + // Add panel zoom persistence + sql!( + ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool + ), + // Add pane group flex data + sql!( + ALTER TABLE pane_groups ADD COLUMN flexes TEXT; + ), + // Add fullscreen field to workspace + // Deprecated, `WindowBounds` holds the fullscreen state now. + // Preserving so users can downgrade Zed. + sql!( + ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool + ), + // Add preview field to items + sql!( + ALTER TABLE items ADD COLUMN preview INTEGER; //bool + ), + // Add centered_layout field to workspace + sql!( + ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool + ), + sql!( + CREATE TABLE remote_projects ( + remote_project_id INTEGER NOT NULL UNIQUE, + path TEXT, + dev_server_name TEXT + ); + ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER; + ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths; + ), + sql!( + DROP TABLE remote_projects; + CREATE TABLE dev_server_projects ( + id INTEGER NOT NULL UNIQUE, + path TEXT, + dev_server_name TEXT + ); + ALTER TABLE workspaces DROP COLUMN remote_project_id; + ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL; + ), + sql!( + ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0; + ), + sql!( + CREATE TABLE ssh_projects ( + id INTEGER PRIMARY KEY, + host TEXT NOT NULL, + port INTEGER, + path TEXT NOT NULL, + user TEXT + ); + ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE; + ), + sql!( + ALTER TABLE ssh_projects RENAME COLUMN path TO paths; + ), + sql!( + CREATE TABLE toolchains ( + workspace_id INTEGER, + worktree_id INTEGER, + language_name TEXT NOT NULL, + name TEXT NOT NULL, + path TEXT NOT NULL, + PRIMARY KEY (workspace_id, worktree_id, language_name) + ); + ), + sql!( + ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}"; + ), + sql!( CREATE TABLE breakpoints ( workspace_id INTEGER NOT NULL, path TEXT NOT NULL, @@ -466,141 +473,165 @@ define_connection! { ON UPDATE CASCADE ); ), - sql!( - ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT; - CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array); - ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT; - ), - sql!( - ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL - ), - sql!( - ALTER TABLE breakpoints DROP COLUMN kind - ), - sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL), - sql!( - ALTER TABLE breakpoints ADD COLUMN condition TEXT; - ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT; - ), - sql!(CREATE TABLE toolchains2 ( - workspace_id INTEGER, - worktree_id INTEGER, - language_name TEXT NOT NULL, - name TEXT NOT NULL, - path TEXT NOT NULL, - raw_json TEXT NOT NULL, - relative_worktree_path TEXT NOT NULL, - PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT; - INSERT INTO toolchains2 - SELECT * FROM toolchains; - DROP TABLE toolchains; - ALTER TABLE toolchains2 RENAME TO toolchains; - ), - sql!( - CREATE TABLE ssh_connections ( - id INTEGER PRIMARY KEY, - host TEXT NOT NULL, - port INTEGER, - user TEXT - ); + sql!( + ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT; + CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array); + ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT; + ), + sql!( + ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL + ), + sql!( + ALTER TABLE breakpoints DROP COLUMN kind + ), + sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL), + sql!( + ALTER TABLE breakpoints ADD COLUMN condition TEXT; + ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT; + ), + sql!(CREATE TABLE toolchains2 ( + workspace_id INTEGER, + worktree_id INTEGER, + language_name TEXT NOT NULL, + name TEXT NOT NULL, + path TEXT NOT NULL, + raw_json TEXT NOT NULL, + relative_worktree_path TEXT NOT NULL, + PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT; + INSERT INTO toolchains2 + SELECT * FROM toolchains; + DROP TABLE toolchains; + ALTER TABLE toolchains2 RENAME TO toolchains; + ), + sql!( + CREATE TABLE ssh_connections ( + id INTEGER PRIMARY KEY, + host TEXT NOT NULL, + port INTEGER, + user TEXT + ); - INSERT INTO ssh_connections (host, port, user) - SELECT DISTINCT host, port, user - FROM ssh_projects; + INSERT INTO ssh_connections (host, port, user) + SELECT DISTINCT host, port, user + FROM ssh_projects; - CREATE TABLE workspaces_2( - workspace_id INTEGER PRIMARY KEY, - paths TEXT, - paths_order TEXT, - ssh_connection_id INTEGER REFERENCES ssh_connections(id), - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - window_state TEXT, - window_x REAL, - window_y REAL, - window_width REAL, - window_height REAL, - display BLOB, - left_dock_visible INTEGER, - left_dock_active_panel TEXT, - right_dock_visible INTEGER, - right_dock_active_panel TEXT, - bottom_dock_visible INTEGER, - bottom_dock_active_panel TEXT, - left_dock_zoom INTEGER, - right_dock_zoom INTEGER, - bottom_dock_zoom INTEGER, - fullscreen INTEGER, - centered_layout INTEGER, - session_id TEXT, - window_id INTEGER - ) STRICT; + CREATE TABLE workspaces_2( + workspace_id INTEGER PRIMARY KEY, + paths TEXT, + paths_order TEXT, + ssh_connection_id INTEGER REFERENCES ssh_connections(id), + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + window_state TEXT, + window_x REAL, + window_y REAL, + window_width REAL, + window_height REAL, + display BLOB, + left_dock_visible INTEGER, + left_dock_active_panel TEXT, + right_dock_visible INTEGER, + right_dock_active_panel TEXT, + bottom_dock_visible INTEGER, + bottom_dock_active_panel TEXT, + left_dock_zoom INTEGER, + right_dock_zoom INTEGER, + bottom_dock_zoom INTEGER, + fullscreen INTEGER, + centered_layout INTEGER, + session_id TEXT, + window_id INTEGER + ) STRICT; - INSERT - INTO workspaces_2 - SELECT - workspaces.workspace_id, - CASE - WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths + INSERT + INTO workspaces_2 + SELECT + workspaces.workspace_id, + CASE + WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths + ELSE + CASE + WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN + NULL + ELSE + replace(workspaces.local_paths_array, ',', "\n") + END + END as paths, + + CASE + WHEN ssh_projects.id IS NOT NULL THEN "" + ELSE workspaces.local_paths_order_array + END as paths_order, + + CASE + WHEN ssh_projects.id IS NOT NULL THEN ( + SELECT ssh_connections.id + FROM ssh_connections + WHERE + ssh_connections.host IS ssh_projects.host AND + ssh_connections.port IS ssh_projects.port AND + ssh_connections.user IS ssh_projects.user + ) + ELSE NULL + END as ssh_connection_id, + + workspaces.timestamp, + workspaces.window_state, + workspaces.window_x, + workspaces.window_y, + workspaces.window_width, + workspaces.window_height, + workspaces.display, + workspaces.left_dock_visible, + workspaces.left_dock_active_panel, + workspaces.right_dock_visible, + workspaces.right_dock_active_panel, + workspaces.bottom_dock_visible, + workspaces.bottom_dock_active_panel, + workspaces.left_dock_zoom, + workspaces.right_dock_zoom, + workspaces.bottom_dock_zoom, + workspaces.fullscreen, + workspaces.centered_layout, + workspaces.session_id, + workspaces.window_id + FROM + workspaces LEFT JOIN + ssh_projects ON + workspaces.ssh_project_id = ssh_projects.id; + + DROP TABLE ssh_projects; + DROP TABLE workspaces; + ALTER TABLE workspaces_2 RENAME TO workspaces; + + CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths); + ), + // Fix any data from when workspaces.paths were briefly encoded as JSON arrays + sql!( + UPDATE workspaces + SET paths = CASE + WHEN substr(paths, 1, 2) = '[' || '"' AND substr(paths, -2, 2) = '"' || ']' THEN + replace( + substr(paths, 3, length(paths) - 4), + '"' || ',' || '"', + CHAR(10) + ) ELSE - CASE - WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN - NULL - ELSE - json('[' || '"' || replace(workspaces.local_paths_array, ',', '"' || "," || '"') || '"' || ']') - END - END as paths, - - CASE - WHEN ssh_projects.id IS NOT NULL THEN "" - ELSE workspaces.local_paths_order_array - END as paths_order, - - CASE - WHEN ssh_projects.id IS NOT NULL THEN ( - SELECT ssh_connections.id - FROM ssh_connections - WHERE - ssh_connections.host IS ssh_projects.host AND - ssh_connections.port IS ssh_projects.port AND - ssh_connections.user IS ssh_projects.user - ) - ELSE NULL - END as ssh_connection_id, - - workspaces.timestamp, - workspaces.window_state, - workspaces.window_x, - workspaces.window_y, - workspaces.window_width, - workspaces.window_height, - workspaces.display, - workspaces.left_dock_visible, - workspaces.left_dock_active_panel, - workspaces.right_dock_visible, - workspaces.right_dock_active_panel, - workspaces.bottom_dock_visible, - workspaces.bottom_dock_active_panel, - workspaces.left_dock_zoom, - workspaces.right_dock_zoom, - workspaces.bottom_dock_zoom, - workspaces.fullscreen, - workspaces.centered_layout, - workspaces.session_id, - workspaces.window_id - FROM - workspaces LEFT JOIN - ssh_projects ON - workspaces.ssh_project_id = ssh_projects.id; - - DROP TABLE ssh_projects; - DROP TABLE workspaces; - ALTER TABLE workspaces_2 RENAME TO workspaces; - - CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths); - ), + replace(paths, ',', CHAR(10)) + END + WHERE paths IS NOT NULL + ), ]; + + // Allow recovering from bad migration that was initially shipped to nightly + // when introducing the ssh_connections table. + fn should_allow_migration_change(_index: usize, old: &str, new: &str) -> bool { + old.starts_with("CREATE TABLE ssh_connections") + && new.starts_with("CREATE TABLE ssh_connections") + } } +db::static_connection!(DB, WorkspaceDb, []); + impl WorkspaceDb { /// Returns a serialized workspace for the given worktree_roots. If the passed array /// is empty, the most recent workspace is returned instead. If no workspace for the @@ -1803,6 +1834,7 @@ mod tests { ON DELETE CASCADE ) STRICT; )], + |_, _, _| false, ) .unwrap(); }) @@ -1851,6 +1883,7 @@ mod tests { REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT;)], + |_, _, _| false, ) }) .await diff --git a/crates/zed/src/zed/component_preview/persistence.rs b/crates/zed/src/zed/component_preview/persistence.rs index 780f7f7626..c37a4cc389 100644 --- a/crates/zed/src/zed/component_preview/persistence.rs +++ b/crates/zed/src/zed/component_preview/persistence.rs @@ -1,10 +1,17 @@ use anyhow::Result; -use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql}; +use db::{ + query, + sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection}, + sqlez_macros::sql, +}; use workspace::{ItemId, WorkspaceDb, WorkspaceId}; -define_connection! { - pub static ref COMPONENT_PREVIEW_DB: ComponentPreviewDb = - &[sql!( +pub struct ComponentPreviewDb(ThreadSafeConnection); + +impl Domain for ComponentPreviewDb { + const NAME: &str = stringify!(ComponentPreviewDb); + + const MIGRATIONS: &[&str] = &[sql!( CREATE TABLE component_previews ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -13,9 +20,11 @@ define_connection! { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + )]; } +db::static_connection!(COMPONENT_PREVIEW_DB, ComponentPreviewDb, [WorkspaceDb]); + impl ComponentPreviewDb { pub async fn save_active_page( &self, From 633ce23ae974211de452683e6d5b2e1a0bf21431 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 25 Aug 2025 18:55:24 -0600 Subject: [PATCH 681/693] acp: Send user-configured MCP tools (#36910) Release Notes: - N/A --- crates/agent2/src/tests/mod.rs | 1 + crates/agent_servers/src/acp.rs | 30 ++++++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 093b8ba971..78e5c88280 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -72,6 +72,7 @@ async fn test_echo(cx: &mut TestAppContext) { } #[gpui::test] +#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows async fn test_thinking(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index 9080fc1ab0..b4e897374a 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -162,12 +162,34 @@ impl AgentConnection for AcpConnection { let conn = self.connection.clone(); let sessions = self.sessions.clone(); let cwd = cwd.to_path_buf(); + let context_server_store = project.read(cx).context_server_store().read(cx); + let mcp_servers = context_server_store + .configured_server_ids() + .iter() + .filter_map(|id| { + let configuration = context_server_store.configuration_for_server(id)?; + let command = configuration.command(); + Some(acp::McpServer { + name: id.0.to_string(), + command: command.path.clone(), + args: command.args.clone(), + env: if let Some(env) = command.env.as_ref() { + env.iter() + .map(|(name, value)| acp::EnvVariable { + name: name.clone(), + value: value.clone(), + }) + .collect() + } else { + vec![] + }, + }) + }) + .collect(); + cx.spawn(async move |cx| { let response = conn - .new_session(acp::NewSessionRequest { - mcp_servers: vec![], - cwd, - }) + .new_session(acp::NewSessionRequest { mcp_servers, cwd }) .await .map_err(|err| { if err.code == acp::ErrorCode::AUTH_REQUIRED.code { From bb5cfe118f588336d54b0499be998cd7744fb8a2 Mon Sep 17 00:00:00 2001 From: Romans Malinovskis Date: Tue, 26 Aug 2025 04:37:29 +0100 Subject: [PATCH 682/693] Add "shift-r" and "g ." support for helix mode (#35468) Related #4642 Compatible with #34136 Release Notes: - Helix: `Shift+R` works as Paste instead of taking you to ReplaceMode - Helix: `g .` goes to last modification place (similar to `. in vim) --- assets/keymaps/vim.json | 2 + crates/vim/src/helix.rs | 87 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 62e50b3c8c..67add61bd3 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -428,11 +428,13 @@ "g h": "vim::StartOfLine", "g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s" "g e": "vim::EndOfDocument", + "g .": "vim::HelixGotoLastModification", // go to last modification "g r": "editor::FindAllReferences", // zed specific "g t": "vim::WindowTop", "g c": "vim::WindowMiddle", "g b": "vim::WindowBottom", + "shift-r": "editor::Paste", "x": "editor::SelectLine", "shift-x": "editor::SelectLine", "%": "editor::SelectAll", diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 2bc531268d..726022021d 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -23,6 +23,8 @@ actions!( HelixInsert, /// Appends at the end of the selection. HelixAppend, + /// Goes to the location of the last modification. + HelixGotoLastModification, ] ); @@ -31,6 +33,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, Vim::helix_insert); Vim::action(editor, cx, Vim::helix_append); Vim::action(editor, cx, Vim::helix_yank); + Vim::action(editor, cx, Vim::helix_goto_last_modification); } impl Vim { @@ -430,6 +433,15 @@ impl Vim { }); self.switch_mode(Mode::HelixNormal, true, window, cx); } + + pub fn helix_goto_last_modification( + &mut self, + _: &HelixGotoLastModification, + window: &mut Window, + cx: &mut Context, + ) { + self.jump(".".into(), false, false, window, cx); + } } #[cfg(test)] @@ -441,6 +453,7 @@ mod test { #[gpui::test] async fn test_word_motions(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); // « // ˇ // » @@ -502,6 +515,7 @@ mod test { #[gpui::test] async fn test_delete(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); // test delete a selection cx.set_state( @@ -582,6 +596,7 @@ mod test { #[gpui::test] async fn test_f_and_t(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); cx.set_state( indoc! {" @@ -635,6 +650,7 @@ mod test { #[gpui::test] async fn test_newline_char(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal); @@ -652,6 +668,7 @@ mod test { #[gpui::test] async fn test_insert_selected(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); cx.set_state( indoc! {" «The ˇ»quick brown @@ -674,6 +691,7 @@ mod test { #[gpui::test] async fn test_append(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); // test from the end of the selection cx.set_state( indoc! {" @@ -716,6 +734,7 @@ mod test { #[gpui::test] async fn test_replace(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); // No selection (single character) cx.set_state("ˇaa", Mode::HelixNormal); @@ -763,4 +782,72 @@ mod test { cx.shared_clipboard().assert_eq("worl"); cx.assert_state("hello «worlˇ»d", Mode::HelixNormal); } + #[gpui::test] + async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); + + // First copy some text to clipboard + cx.set_state("«hello worldˇ»", Mode::HelixNormal); + cx.simulate_keystrokes("y"); + + // Test paste with shift-r on single cursor + cx.set_state("foo ˇbar", Mode::HelixNormal); + cx.simulate_keystrokes("shift-r"); + + cx.assert_state("foo hello worldˇbar", Mode::HelixNormal); + + // Test paste with shift-r on selection + cx.set_state("foo «barˇ» baz", Mode::HelixNormal); + cx.simulate_keystrokes("shift-r"); + + cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal); + } + + #[gpui::test] + async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); + + // Make a modification at a specific location + cx.set_state("ˇhello", Mode::HelixNormal); + assert_eq!(cx.mode(), Mode::HelixNormal); + cx.simulate_keystrokes("i"); + assert_eq!(cx.mode(), Mode::Insert); + cx.simulate_keystrokes("escape"); + assert_eq!(cx.mode(), Mode::HelixNormal); + } + + #[gpui::test] + async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); + + // Make a modification at a specific location + cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal); + cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal); + cx.simulate_keystrokes("i"); + cx.simulate_keystrokes("escape"); + cx.simulate_keystrokes("i"); + cx.simulate_keystrokes("m o d i f i e d space"); + cx.simulate_keystrokes("escape"); + + // TODO: this fails, because state is no longer helix + cx.assert_state( + "line one\nline modified ˇtwo\nline three", + Mode::HelixNormal, + ); + + // Move cursor away from the modification + cx.simulate_keystrokes("up"); + + // Use "g ." to go back to last modification + cx.simulate_keystrokes("g ."); + + // Verify we're back at the modification location and still in HelixNormal mode + cx.assert_state( + "line one\nline modifiedˇ two\nline three", + Mode::HelixNormal, + ); + } } From bf5ed6d1c9795369310b5b9d6c752d9dc54991b5 Mon Sep 17 00:00:00 2001 From: Rui Ning <107875822+iryanin@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:40:53 +0800 Subject: [PATCH 683/693] Remote: Change "sh -c" to "sh -lc" to make config in $HOME/.profile effective (#36760) Closes #ISSUE Release Notes: - The environment of original remote dev cannot be changed without sudo because of the behavior of "sh -c". This PR changes "sh -c" to "sh -lc" to let the shell source $HOME/.profile and support customized environment like customized $PATH variable. --- crates/remote/src/ssh_session.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index b9af528643..6794018470 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -445,7 +445,7 @@ impl SshSocket { } async fn platform(&self) -> Result { - let uname = self.run_command("sh", &["-c", "uname -sm"]).await?; + let uname = self.run_command("sh", &["-lc", "uname -sm"]).await?; let Some((os, arch)) = uname.split_once(" ") else { anyhow::bail!("unknown uname: {uname:?}") }; @@ -476,7 +476,7 @@ impl SshSocket { } async fn shell(&self) -> String { - match self.run_command("sh", &["-c", "echo $SHELL"]).await { + match self.run_command("sh", &["-lc", "echo $SHELL"]).await { Ok(shell) => shell.trim().to_owned(), Err(e) => { log::error!("Failed to get shell: {e}"); @@ -1533,7 +1533,7 @@ impl RemoteConnection for SshRemoteConnection { let ssh_proxy_process = match self .socket - .ssh_command("sh", &["-c", &start_proxy_command]) + .ssh_command("sh", &["-lc", &start_proxy_command]) // IMPORTANT: we kill this process when we drop the task that uses it. .kill_on_drop(true) .spawn() @@ -1910,7 +1910,7 @@ impl SshRemoteConnection { .run_command( "sh", &[ - "-c", + "-lc", &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), ], ) @@ -1988,7 +1988,7 @@ impl SshRemoteConnection { .run_command( "sh", &[ - "-c", + "-lc", &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), ], ) @@ -2036,7 +2036,7 @@ impl SshRemoteConnection { dst_path = &dst_path.to_string() ) }; - self.socket.run_command("sh", &["-c", &script]).await?; + self.socket.run_command("sh", &["-lc", &script]).await?; Ok(()) } From 64b14ef84859de5d03c5959faedc1216415a2b52 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 25 Aug 2025 22:21:05 -0700 Subject: [PATCH 684/693] Fix Sqlite newline syntax in workspace migration (#36916) Fixes one more case where I incorrectly tried to use a `\n` escape sequence for a newline in sqlite. Release Notes: - N/A --- crates/workspace/src/persistence.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 89e1147d8a..12e719cfd9 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -553,7 +553,7 @@ impl Domain for WorkspaceDb { WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN NULL ELSE - replace(workspaces.local_paths_array, ',', "\n") + replace(workspaces.local_paths_array, ',', CHAR(10)) END END as paths, From 428fc6d483b785227dfd56e4e493ee7ccc3c384d Mon Sep 17 00:00:00 2001 From: Dan Dascalescu Date: Tue, 26 Aug 2025 12:05:40 +0300 Subject: [PATCH 685/693] chore: Fix typo in `10_bug_report.yml` (#36922) Release Notes: - N/A --- .github/ISSUE_TEMPLATE/10_bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/10_bug_report.yml b/.github/ISSUE_TEMPLATE/10_bug_report.yml index e132eca1e5..1bf6c80e40 100644 --- a/.github/ISSUE_TEMPLATE/10_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/10_bug_report.yml @@ -14,7 +14,7 @@ body: ### Description From c14d84cfdb61a4d6fbaeabe14c3b5ca0909163af Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 26 Aug 2025 11:20:33 +0200 Subject: [PATCH 686/693] acp: Add button to configure custom agent in the configuration view (#36923) Release Notes: - N/A --- crates/agent_ui/src/agent_configuration.rs | 107 ++++++++++++++++++++- 1 file changed, 102 insertions(+), 5 deletions(-) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 52fb7eed4b..aa9b2ca94f 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -5,18 +5,21 @@ mod tool_picker; use std::{sync::Arc, time::Duration}; -use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini}; +use agent_servers::{AgentServerCommand, AgentServerSettings, AllAgentServersSettings, Gemini}; use agent_settings::AgentSettings; +use anyhow::Result; use assistant_tool::{ToolSource, ToolWorkingSet}; use cloud_llm_client::Plan; use collections::HashMap; use context_server::ContextServerId; +use editor::{Editor, SelectionEffects, scroll::Autoscroll}; use extension::ExtensionManifest; use extension_host::ExtensionStore; use fs::Fs; use gpui::{ - Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle, - Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage, + Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity, + EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, + WeakEntity, percentage, }; use language::LanguageRegistry; use language_model::{ @@ -34,7 +37,7 @@ use ui::{ Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*, }; use util::ResultExt as _; -use workspace::Workspace; +use workspace::{Workspace, create_and_open_local_file}; use zed_actions::ExtensionCategoryFilter; pub(crate) use configure_context_server_modal::ConfigureContextServerModal; @@ -1058,7 +1061,36 @@ impl AgentConfiguration { .child( v_flex() .gap_0p5() - .child(Headline::new("External Agents")) + .child( + h_flex() + .w_full() + .gap_2() + .justify_between() + .child(Headline::new("External Agents")) + .child( + Button::new("add-agent", "Add Agent") + .icon_position(IconPosition::Start) + .icon(IconName::Plus) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .label_size(LabelSize::Small) + .on_click( + move |_, window, cx| { + if let Some(workspace) = window.root().flatten() { + let workspace = workspace.downgrade(); + window + .spawn(cx, async |cx| { + open_new_agent_servers_entry_in_settings_editor( + workspace, + cx, + ).await + }) + .detach_and_log_err(cx); + } + } + ), + ) + ) .child( Label::new( "Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.", @@ -1324,3 +1356,68 @@ fn show_unable_to_uninstall_extension_with_context_server( workspace.toggle_status_toast(status_toast, cx); } + +async fn open_new_agent_servers_entry_in_settings_editor( + workspace: WeakEntity, + cx: &mut AsyncWindowContext, +) -> Result<()> { + let settings_editor = workspace + .update_in(cx, |_, window, cx| { + create_and_open_local_file(paths::settings_file(), window, cx, || { + settings::initial_user_settings_content().as_ref().into() + }) + })? + .await? + .downcast::() + .unwrap(); + + settings_editor + .downgrade() + .update_in(cx, |item, window, cx| { + let text = item.buffer().read(cx).snapshot(cx).text(); + + let settings = cx.global::(); + + let edits = settings.edits_for_update::(&text, |file| { + let unique_server_name = (0..u8::MAX) + .map(|i| { + if i == 0 { + "your_agent".into() + } else { + format!("your_agent_{}", i).into() + } + }) + .find(|name| !file.custom.contains_key(name)); + if let Some(server_name) = unique_server_name { + file.custom.insert( + server_name, + AgentServerSettings { + command: AgentServerCommand { + path: "path_to_executable".into(), + args: vec![], + env: Some(HashMap::default()), + }, + }, + ); + } + }); + + if !edits.is_empty() { + let ranges = edits + .iter() + .map(|(range, _)| range.clone()) + .collect::>(); + + item.edit(edits, cx); + + item.change_selections( + SelectionEffects::scroll(Autoscroll::newest()), + window, + cx, + |selections| { + selections.select_ranges(ranges); + }, + ); + } + }) +} From b249593abee31e420fe447f6b551b5e2130b1bc8 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Tue, 26 Aug 2025 02:46:29 -0700 Subject: [PATCH 687/693] agent2: Always finalize diffs from the edit tool (#36918) Previously, we wouldn't finalize the diff if an error occurred during editing or the tool call was canceled. Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra --- crates/agent2/src/thread.rs | 24 +++++ crates/agent2/src/tools/edit_file_tool.rs | 103 ++++++++++++++++++++- crates/language_model/src/fake_provider.rs | 31 ++++++- 3 files changed, 152 insertions(+), 6 deletions(-) diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 1b1c014b79..4acd72f275 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -2459,6 +2459,30 @@ impl ToolCallEventStreamReceiver { } } + pub async fn expect_update_fields(&mut self) -> acp::ToolCallUpdateFields { + let event = self.0.next().await; + if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields( + update, + )))) = event + { + update.fields + } else { + panic!("Expected update fields but got: {:?}", event); + } + } + + pub async fn expect_diff(&mut self) -> Entity { + let event = self.0.next().await; + if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateDiff( + update, + )))) = event + { + update.diff + } else { + panic!("Expected diff but got: {:?}", event); + } + } + pub async fn expect_terminal(&mut self) -> Entity { let event = self.0.next().await; if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal( diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 5a68d0c70a..f86bfd25f7 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -273,6 +273,13 @@ impl AgentTool for EditFileTool { let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?; event_stream.update_diff(diff.clone()); + let _finalize_diff = util::defer({ + let diff = diff.downgrade(); + let mut cx = cx.clone(); + move || { + diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok(); + } + }); let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; let old_text = cx @@ -389,8 +396,6 @@ impl AgentTool for EditFileTool { }) .await; - diff.update(cx, |diff, cx| diff.finalize(cx)).ok(); - let input_path = input.path.display(); if unified_diff.is_empty() { anyhow::ensure!( @@ -1545,6 +1550,100 @@ mod tests { ); } + #[gpui::test] + async fn test_diff_finalization(cx: &mut TestAppContext) { + init_test(cx); + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree("/", json!({"main.rs": ""})).await; + + let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await; + let languages = project.read_with(cx, |project, _cx| project.languages().clone()); + let context_server_registry = + cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); + let model = Arc::new(FakeLanguageModel::default()); + let thread = cx.new(|cx| { + Thread::new( + project.clone(), + cx.new(|_cx| ProjectContext::default()), + context_server_registry.clone(), + Templates::new(), + Some(model.clone()), + cx, + ) + }); + + // Ensure the diff is finalized after the edit completes. + { + let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone())); + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let edit = cx.update(|cx| { + tool.run( + EditFileToolInput { + display_description: "Edit file".into(), + path: path!("/main.rs").into(), + mode: EditFileMode::Edit, + }, + stream_tx, + cx, + ) + }); + stream_rx.expect_update_fields().await; + let diff = stream_rx.expect_diff().await; + diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_)))); + cx.run_until_parked(); + model.end_last_completion_stream(); + edit.await.unwrap(); + diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_)))); + } + + // Ensure the diff is finalized if an error occurs while editing. + { + model.forbid_requests(); + let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone())); + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let edit = cx.update(|cx| { + tool.run( + EditFileToolInput { + display_description: "Edit file".into(), + path: path!("/main.rs").into(), + mode: EditFileMode::Edit, + }, + stream_tx, + cx, + ) + }); + stream_rx.expect_update_fields().await; + let diff = stream_rx.expect_diff().await; + diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_)))); + edit.await.unwrap_err(); + diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_)))); + model.allow_requests(); + } + + // Ensure the diff is finalized if the tool call gets dropped. + { + let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone())); + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let edit = cx.update(|cx| { + tool.run( + EditFileToolInput { + display_description: "Edit file".into(), + path: path!("/main.rs").into(), + mode: EditFileMode::Edit, + }, + stream_tx, + cx, + ) + }); + stream_rx.expect_update_fields().await; + let diff = stream_rx.expect_diff().await; + diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_)))); + drop(edit); + cx.run_until_parked(); + diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_)))); + } + } + fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); diff --git a/crates/language_model/src/fake_provider.rs b/crates/language_model/src/fake_provider.rs index ebfd37d16c..b06a475f93 100644 --- a/crates/language_model/src/fake_provider.rs +++ b/crates/language_model/src/fake_provider.rs @@ -4,12 +4,16 @@ use crate::{ LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, }; +use anyhow::anyhow; use futures::{FutureExt, channel::mpsc, future::BoxFuture, stream::BoxStream}; use gpui::{AnyView, App, AsyncApp, Entity, Task, Window}; use http_client::Result; use parking_lot::Mutex; use smol::stream::StreamExt; -use std::sync::Arc; +use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering::SeqCst}, +}; #[derive(Clone)] pub struct FakeLanguageModelProvider { @@ -106,6 +110,7 @@ pub struct FakeLanguageModel { >, )>, >, + forbid_requests: AtomicBool, } impl Default for FakeLanguageModel { @@ -114,11 +119,20 @@ impl Default for FakeLanguageModel { provider_id: LanguageModelProviderId::from("fake".to_string()), provider_name: LanguageModelProviderName::from("Fake".to_string()), current_completion_txs: Mutex::new(Vec::new()), + forbid_requests: AtomicBool::new(false), } } } impl FakeLanguageModel { + pub fn allow_requests(&self) { + self.forbid_requests.store(false, SeqCst); + } + + pub fn forbid_requests(&self) { + self.forbid_requests.store(true, SeqCst); + } + pub fn pending_completions(&self) -> Vec { self.current_completion_txs .lock() @@ -251,9 +265,18 @@ impl LanguageModel for FakeLanguageModel { LanguageModelCompletionError, >, > { - let (tx, rx) = mpsc::unbounded(); - self.current_completion_txs.lock().push((request, tx)); - async move { Ok(rx.boxed()) }.boxed() + if self.forbid_requests.load(SeqCst) { + async move { + Err(LanguageModelCompletionError::Other(anyhow!( + "requests are forbidden" + ))) + } + .boxed() + } else { + let (tx, rx) = mpsc::unbounded(); + self.current_completion_txs.lock().push((request, tx)); + async move { Ok(rx.boxed()) }.boxed() + } } fn as_fake(&self) -> &Self { From e96b68bc1599b92b6404f77326d79e198f4a8efb Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 26 Aug 2025 12:55:45 +0200 Subject: [PATCH 688/693] acp: Polish UI (#36927) Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra --- crates/agent2/src/tests/mod.rs | 2 +- crates/agent2/src/thread.rs | 14 ++++++++++++-- crates/agent_ui/src/acp/thread_view.rs | 7 ++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 78e5c88280..a55eaacee3 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -472,7 +472,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { tool_name: ToolRequiringPermission::name().into(), is_error: true, content: "Permission to run tool denied by user".into(), - output: None + output: Some("Permission to run tool denied by user".into()) }) ] ); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 4acd72f275..97ea1caf1d 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -732,7 +732,17 @@ impl Thread { stream.update_tool_call_fields( &tool_use.id, acp::ToolCallUpdateFields { - status: Some(acp::ToolCallStatus::Completed), + status: Some( + tool_result + .as_ref() + .map_or(acp::ToolCallStatus::Failed, |result| { + if result.is_error { + acp::ToolCallStatus::Failed + } else { + acp::ToolCallStatus::Completed + } + }), + ), raw_output: output, ..Default::default() }, @@ -1557,7 +1567,7 @@ impl Thread { tool_name: tool_use.name, is_error: true, content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())), - output: None, + output: Some(error.to_string().into()), }, } })) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 837ce6f90a..6d8f8fb82e 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1684,7 +1684,7 @@ impl AcpThreadView { div() .relative() .mt_1p5() - .ml(px(7.)) + .ml(rems(0.4)) .pl_4() .border_l_1() .border_color(self.tool_card_border_color(cx)) @@ -1850,6 +1850,7 @@ impl AcpThreadView { .w_full() .h(window.line_height() - px(2.)) .text_size(self.tool_name_font_size()) + .gap_0p5() .child(tool_icon) .child(if tool_call.locations.len() == 1 { let name = tool_call.locations[0] @@ -1968,7 +1969,7 @@ impl AcpThreadView { v_flex() .mt_1p5() - .ml(px(7.)) + .ml(rems(0.4)) .px_3p5() .gap_2() .border_l_1() @@ -2025,7 +2026,7 @@ impl AcpThreadView { let button_id = SharedString::from(format!("item-{}", uri)); div() - .ml(px(7.)) + .ml(rems(0.4)) .pl_2p5() .border_l_1() .border_color(self.tool_card_border_color(cx)) From 10a1140d49fc0af2c1adf301433a3c9a34374417 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 26 Aug 2025 13:18:50 +0200 Subject: [PATCH 689/693] acp: Improve matching logic when adding new entry to agent_servers (#36926) Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra --- crates/agent_ui/src/agent_configuration.rs | 75 +++++++++++++++++----- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index aa9b2ca94f..c279115880 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -3,7 +3,7 @@ mod configure_context_server_modal; mod manage_profiles_modal; mod tool_picker; -use std::{sync::Arc, time::Duration}; +use std::{ops::Range, sync::Arc, time::Duration}; use agent_servers::{AgentServerCommand, AgentServerSettings, AllAgentServersSettings, Gemini}; use agent_settings::AgentSettings; @@ -1378,8 +1378,9 @@ async fn open_new_agent_servers_entry_in_settings_editor( let settings = cx.global::(); + let mut unique_server_name = None; let edits = settings.edits_for_update::(&text, |file| { - let unique_server_name = (0..u8::MAX) + let server_name: Option = (0..u8::MAX) .map(|i| { if i == 0 { "your_agent".into() @@ -1388,7 +1389,8 @@ async fn open_new_agent_servers_entry_in_settings_editor( } }) .find(|name| !file.custom.contains_key(name)); - if let Some(server_name) = unique_server_name { + if let Some(server_name) = server_name { + unique_server_name = Some(server_name.clone()); file.custom.insert( server_name, AgentServerSettings { @@ -1402,22 +1404,61 @@ async fn open_new_agent_servers_entry_in_settings_editor( } }); - if !edits.is_empty() { - let ranges = edits - .iter() - .map(|(range, _)| range.clone()) - .collect::>(); + if edits.is_empty() { + return; + } - item.edit(edits, cx); + let ranges = edits + .iter() + .map(|(range, _)| range.clone()) + .collect::>(); - item.change_selections( - SelectionEffects::scroll(Autoscroll::newest()), - window, - cx, - |selections| { - selections.select_ranges(ranges); - }, - ); + item.edit(edits, cx); + if let Some((unique_server_name, buffer)) = + unique_server_name.zip(item.buffer().read(cx).as_singleton()) + { + let snapshot = buffer.read(cx).snapshot(); + if let Some(range) = + find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot) + { + item.change_selections( + SelectionEffects::scroll(Autoscroll::newest()), + window, + cx, + |selections| { + selections.select_ranges(vec![range]); + }, + ); + } } }) } + +fn find_text_in_buffer( + text: &str, + start: usize, + snapshot: &language::BufferSnapshot, +) -> Option> { + let chars = text.chars().collect::>(); + + let mut offset = start; + let mut char_offset = 0; + for c in snapshot.chars_at(start) { + if char_offset >= chars.len() { + break; + } + offset += 1; + + if c == chars[char_offset] { + char_offset += 1; + } else { + char_offset = 0; + } + } + + if char_offset == chars.len() { + Some(offset.saturating_sub(chars.len())..offset) + } else { + None + } +} From 372b3c7af632caffbc4e73d5b84bc804d375904a Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 26 Aug 2025 15:30:26 +0200 Subject: [PATCH 690/693] acp: Enable feature flag for everyone (#36928) Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 11 ----------- crates/feature_flags/src/feature_flags.rs | 6 +++++- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 269aec3365..267c76d73f 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -591,17 +591,6 @@ impl AgentPanel { None }; - // Wait for the Gemini/Native feature flag to be available. - let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?; - if !client.status().borrow().is_signed_out() { - cx.update(|_, cx| { - cx.wait_for_flag_or_timeout::( - Duration::from_secs(2), - ) - })? - .await; - } - let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = cx.new(|cx| { Self::new( diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index 422979c429..f5f7fc42b3 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -98,6 +98,10 @@ impl FeatureFlag for GeminiAndNativeFeatureFlag { // integration too, and we'd like to turn Gemini/Native on in new builds // without enabling Claude Code in old builds. const NAME: &'static str = "gemini-and-native"; + + fn enabled_for_all() -> bool { + true + } } pub struct ClaudeCodeFeatureFlag; @@ -201,7 +205,7 @@ impl FeatureFlagAppExt for App { fn has_flag(&self) -> bool { self.try_global::() .map(|flags| flags.has_flag::()) - .unwrap_or(false) + .unwrap_or(T::enabled_for_all()) } fn is_staff(&self) -> bool { From aa0f7a2d09c06331dbb176a8b0e45235f1ad8516 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 26 Aug 2025 09:33:42 -0400 Subject: [PATCH 691/693] Fix conflicts in Linux default keymap (#36519) Closes https://github.com/zed-industries/zed/issues/29746 | Action | New Key | Old Key | Former Conflict | | - | - | - | - | | `edit_prediction::ToggleMenu` | `ctrl-alt-shift-i` | `ctrl-shift-i` | `editor::Format` | | `editor::ToggleEditPrediction` | `ctrl-alt-shift-e` | `ctrl-shift-e` | `project_panel::ToggleFocus` | These aren't great keys and I'm open to alternate suggestions, but the will work out of the box without conflict. Release Notes: - N/A --- assets/keymaps/default-linux.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index e84f4834af..3cca560c00 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -40,7 +40,7 @@ "shift-f11": "debugger::StepOut", "f11": "zed::ToggleFullScreen", "ctrl-alt-z": "edit_prediction::RateCompletions", - "ctrl-shift-i": "edit_prediction::ToggleMenu", + "ctrl-alt-shift-i": "edit_prediction::ToggleMenu", "ctrl-alt-l": "lsp_tool::ToggleMenu" } }, @@ -120,7 +120,7 @@ "alt-g m": "git::OpenModifiedFiles", "menu": "editor::OpenContextMenu", "shift-f10": "editor::OpenContextMenu", - "ctrl-shift-e": "editor::ToggleEditPrediction", + "ctrl-alt-shift-e": "editor::ToggleEditPrediction", "f9": "editor::ToggleBreakpoint", "shift-f9": "editor::EditLogBreakpoint" } From 76dbcde62836445d146c3918edce26dbaec25314 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 26 Aug 2025 09:35:45 -0400 Subject: [PATCH 692/693] Support disabling drag-and-drop in Project Panel (#36719) Release Notes: - Added setting for disabling drag and drop in project panel. `{ "project_panel": {"drag_and_drop": false } }` --- assets/settings/default.json | 2 + crates/project_panel/src/project_panel.rs | 74 ++++++++++--------- .../src/project_panel_settings.rs | 5 ++ docs/src/configuring-zed.md | 1 + docs/src/visual-customization.md | 1 + 5 files changed, 50 insertions(+), 33 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index f0b9e11e57..804198090f 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -653,6 +653,8 @@ // "never" "show": "always" }, + // Whether to enable drag-and-drop operations in the project panel. + "drag_and_drop": true, // Whether to hide the root entry when only one folder is open in the window. "hide_root": false }, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index c99f5f8172..5a30a3e9bc 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -4089,6 +4089,7 @@ impl ProjectPanel { .when(!is_sticky, |this| { this .when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over)) + .when(settings.drag_and_drop, |this| this .on_drag_move::(cx.listener( move |this, event: &DragMoveEvent, _, cx| { let is_current_target = this.drag_target_entry.as_ref() @@ -4222,7 +4223,7 @@ impl ProjectPanel { } this.drag_onto(selections, entry_id, kind.is_file(), window, cx); }), - ) + )) }) .on_mouse_down( MouseButton::Left, @@ -4433,6 +4434,7 @@ impl ProjectPanel { div() .when(!is_sticky, |div| { div + .when(settings.drag_and_drop, |div| div .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| { this.hover_scroll_task.take(); this.drag_target_entry = None; @@ -4464,7 +4466,7 @@ impl ProjectPanel { } }, - )) + ))) }) .child( Label::new(DELIMITER.clone()) @@ -4484,6 +4486,7 @@ impl ProjectPanel { .when(index != components_len - 1, |div|{ let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned(); div + .when(settings.drag_and_drop, |div| div .on_drag_move(cx.listener( move |this, event: &DragMoveEvent, _, _| { if event.bounds.contains(&event.event.position) { @@ -4521,7 +4524,7 @@ impl ProjectPanel { target.index == index ), |this| { this.bg(item_colors.drag_over) - }) + })) }) }) .on_click(cx.listener(move |this, _, _, cx| { @@ -5029,7 +5032,8 @@ impl ProjectPanel { sticky_parents.reverse(); - let git_status_enabled = ProjectPanelSettings::get_global(cx).git_status; + let panel_settings = ProjectPanelSettings::get_global(cx); + let git_status_enabled = panel_settings.git_status; let root_name = OsStr::new(worktree.root_name()); let git_summaries_by_id = if git_status_enabled { @@ -5113,11 +5117,11 @@ impl Render for ProjectPanel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let has_worktree = !self.visible_entries.is_empty(); let project = self.project.read(cx); - let indent_size = ProjectPanelSettings::get_global(cx).indent_size; - let show_indent_guides = - ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always; + let panel_settings = ProjectPanelSettings::get_global(cx); + let indent_size = panel_settings.indent_size; + let show_indent_guides = panel_settings.indent_guides.show == ShowIndentGuides::Always; let show_sticky_entries = { - if ProjectPanelSettings::get_global(cx).sticky_scroll { + if panel_settings.sticky_scroll { let is_scrollable = self.scroll_handle.is_scrollable(); let is_scrolled = self.scroll_handle.offset().y < px(0.); is_scrollable && is_scrolled @@ -5205,8 +5209,10 @@ impl Render for ProjectPanel { h_flex() .id("project-panel") .group("project-panel") - .on_drag_move(cx.listener(handle_drag_move::)) - .on_drag_move(cx.listener(handle_drag_move::)) + .when(panel_settings.drag_and_drop, |this| { + this.on_drag_move(cx.listener(handle_drag_move::)) + .on_drag_move(cx.listener(handle_drag_move::)) + }) .size_full() .relative() .on_modifiers_changed(cx.listener( @@ -5544,30 +5550,32 @@ impl Render for ProjectPanel { })), ) .when(is_local, |div| { - div.drag_over::(|style, _, _, cx| { - style.bg(cx.theme().colors().drop_target_background) + div.when(panel_settings.drag_and_drop, |div| { + div.drag_over::(|style, _, _, cx| { + style.bg(cx.theme().colors().drop_target_background) + }) + .on_drop(cx.listener( + move |this, external_paths: &ExternalPaths, window, cx| { + this.drag_target_entry = None; + this.hover_scroll_task.take(); + if let Some(task) = this + .workspace + .update(cx, |workspace, cx| { + workspace.open_workspace_for_paths( + true, + external_paths.paths().to_owned(), + window, + cx, + ) + }) + .log_err() + { + task.detach_and_log_err(cx); + } + cx.stop_propagation(); + }, + )) }) - .on_drop(cx.listener( - move |this, external_paths: &ExternalPaths, window, cx| { - this.drag_target_entry = None; - this.hover_scroll_task.take(); - if let Some(task) = this - .workspace - .update(cx, |workspace, cx| { - workspace.open_workspace_for_paths( - true, - external_paths.paths().to_owned(), - window, - cx, - ) - }) - .log_err() - { - task.detach_and_log_err(cx); - } - cx.stop_propagation(); - }, - )) }) } } diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index 8a243589ed..fc399d66a7 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -47,6 +47,7 @@ pub struct ProjectPanelSettings { pub scrollbar: ScrollbarSettings, pub show_diagnostics: ShowDiagnostics, pub hide_root: bool, + pub drag_and_drop: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -160,6 +161,10 @@ pub struct ProjectPanelSettingsContent { /// /// Default: true pub sticky_scroll: Option, + /// Whether to enable drag-and-drop operations in the project panel. + /// + /// Default: true + pub drag_and_drop: Option, } impl Settings for ProjectPanelSettings { diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index fb139db6e4..a8a4689689 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -3243,6 +3243,7 @@ Run the `theme selector: toggle` action in the command palette to see a current "indent_size": 20, "auto_reveal_entries": true, "auto_fold_dirs": true, + "drag_and_drop": true, "scrollbar": { "show": null }, diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 24b2a9d769..4fc5a9ba88 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -431,6 +431,7 @@ Project panel can be shown/hidden with {#action project_panel::ToggleFocus} ({#k "auto_reveal_entries": true, // Show file in panel when activating its buffer "auto_fold_dirs": true, // Fold dirs with single subdir "sticky_scroll": true, // Stick parent directories at top of the project panel. + "drag_and_drop": true, // Whether drag and drop is enabled "scrollbar": { // Project panel scrollbar settings "show": null // Show/hide: (auto, system, always, never) }, From b7dad2cf7199e4e31ce149d707dc87683981bc5d Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 26 Aug 2025 09:41:57 -0400 Subject: [PATCH 693/693] Fix initial_tasks.json triggering diagnostic warning (#36523) `zed::OpenProjectTasks` without an existing tasks.json will recreate it from the template. This file will immediately show a warning. Screenshot 2025-08-19 at 17 16 07 Release Notes: - N/A --- assets/settings/initial_tasks.json | 4 ++-- docs/src/tasks.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/settings/initial_tasks.json b/assets/settings/initial_tasks.json index a79c550671..5cead67b6d 100644 --- a/assets/settings/initial_tasks.json +++ b/assets/settings/initial_tasks.json @@ -43,8 +43,8 @@ // "args": ["--login"] // } // } - "shell": "system", + "shell": "system" // Represents the tags for inline runnable indicators, or spawning multiple tasks at once. - "tags": [] + // "tags": [] } ] diff --git a/docs/src/tasks.md b/docs/src/tasks.md index 9550563432..bff3eac860 100644 --- a/docs/src/tasks.md +++ b/docs/src/tasks.md @@ -45,9 +45,9 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to // Whether to show the task line in the output of the spawned task, defaults to `true`. "show_summary": true, // Whether to show the command line in the output of the spawned task, defaults to `true`. - "show_output": true, + "show_output": true // Represents the tags for inline runnable indicators, or spawning multiple tasks at once. - "tags": [] + // "tags": [] } ] ```
-

Crate serde

- - source · - -

M#xG_^v&aGThhK+?;N~+sUR&&vd7mG9@ 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 bdSf5B^#~pZzklsiB#8IWVET3lT|EZ?Fp;;J diff --git a/assets/settings/default.json b/assets/settings/default.json index 0f1818ac7f..2c3bf6930d 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -28,7 +28,9 @@ "edit_prediction_provider": "zed" }, // The name of a font to use for rendering text in the editor - "buffer_font_family": "Zed Plex Mono", + // ".ZedMono" currently aliases to Lilex + // but this may change in the future. + "buffer_font_family": ".ZedMono", // Set the buffer text's font fallbacks, this will be merged with // the platform's default fallbacks. "buffer_font_fallbacks": null, @@ -54,7 +56,9 @@ "buffer_line_height": "comfortable", // The name of a font to use for rendering text in the UI // You can set this to ".SystemUIFont" to use the system font - "ui_font_family": "Zed Plex Sans", + // ".ZedSans" currently aliases to "IBM Plex Sans", but this may + // change in the future + "ui_font_family": ".ZedSans", // Set the UI's font fallbacks, this will be merged with the platform's // default font fallbacks. "ui_font_fallbacks": null, @@ -1402,7 +1406,7 @@ // "font_size": 15, // Set the terminal's font family. If this option is not included, // the terminal will default to matching the buffer's font family. - // "font_family": "Zed Plex Mono", + // "font_family": ".ZedMono", // Set the terminal's font fallbacks. If this option is not included, // the terminal will default to matching the buffer's font fallbacks. // This will be merged with the platform's default font fallbacks diff --git a/crates/assets/src/assets.rs b/crates/assets/src/assets.rs index fad0c58b73..5c7e671159 100644 --- a/crates/assets/src/assets.rs +++ b/crates/assets/src/assets.rs @@ -58,9 +58,7 @@ impl Assets { pub fn load_test_fonts(&self, cx: &App) { cx.text_system() .add_fonts(vec![ - self.load("fonts/plex-mono/ZedPlexMono-Regular.ttf") - .unwrap() - .unwrap(), + self.load("fonts/lilex/Lilex-Regular.ttf").unwrap().unwrap(), ]) .unwrap() } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index e25c02432d..c4c9f2004a 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -2290,8 +2290,6 @@ mod tests { fn test_blocks_on_wrapped_lines(cx: &mut gpui::TestAppContext) { cx.update(init_test); - let _font_id = cx.text_system().font_id(&font("Helvetica")).unwrap(); - let text = "one two three\nfour five six\nseven eight"; let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx)); diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 269f8f0c40..caa4882a6e 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1223,7 +1223,7 @@ mod tests { let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); let font = test_font(); - let _font_id = text_system.font_id(&font); + let _font_id = text_system.resolve_font(&font); let font_size = px(14.0); log::info!("Tab size: {}", tab_size); diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 0a9d5e9535..f328945dbe 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -53,7 +53,7 @@ pub fn marked_display_snapshot( let (unmarked_text, markers) = marked_text_offsets(text); let font = Font { - family: "Zed Plex Mono".into(), + family: ".ZedMono".into(), features: FontFeatures::default(), fallbacks: None, weight: FontWeight::default(), diff --git a/crates/gpui/src/platform/linux/text_system.rs b/crates/gpui/src/platform/linux/text_system.rs index e6f6e9a680..f66a2e71d4 100644 --- a/crates/gpui/src/platform/linux/text_system.rs +++ b/crates/gpui/src/platform/linux/text_system.rs @@ -213,11 +213,7 @@ impl CosmicTextSystemState { features: &FontFeatures, ) -> Result> { // TODO: Determine the proper system UI font. - let name = if name == ".SystemUIFont" { - "Zed Plex Sans" - } else { - name - }; + let name = crate::text_system::font_name_with_fallbacks(name, "IBM Plex Sans"); let families = self .font_system diff --git a/crates/gpui/src/platform/mac/text_system.rs b/crates/gpui/src/platform/mac/text_system.rs index c45888bce7..849925c727 100644 --- a/crates/gpui/src/platform/mac/text_system.rs +++ b/crates/gpui/src/platform/mac/text_system.rs @@ -211,11 +211,7 @@ impl MacTextSystemState { features: &FontFeatures, fallbacks: Option<&FontFallbacks>, ) -> Result> { - let name = if name == ".SystemUIFont" { - ".AppleSystemUIFont" - } else { - name - }; + let name = crate::text_system::font_name_with_fallbacks(name, ".AppleSystemUIFont"); let mut font_ids = SmallVec::new(); let family = self diff --git a/crates/gpui/src/platform/windows/direct_write.rs b/crates/gpui/src/platform/windows/direct_write.rs index 587cb7b4a6..75cb50243b 100644 --- a/crates/gpui/src/platform/windows/direct_write.rs +++ b/crates/gpui/src/platform/windows/direct_write.rs @@ -498,8 +498,9 @@ impl DirectWriteState { ) .unwrap() } else { + let family = self.system_ui_font_name.clone(); self.find_font_id( - target_font.family.as_ref(), + font_name_with_fallbacks(target_font.family.as_ref(), family.as_ref()), target_font.weight, target_font.style, &target_font.features, @@ -512,7 +513,6 @@ impl DirectWriteState { } #[cfg(not(any(test, feature = "test-support")))] { - let family = self.system_ui_font_name.clone(); log::error!("{} not found, use {} instead.", target_font.family, family); self.get_font_id_from_font_collection( family.as_ref(), diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index ed1307c6cd..b48c3a2935 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -65,7 +65,7 @@ impl TextSystem { font_runs_pool: Mutex::default(), fallback_font_stack: smallvec![ // TODO: Remove this when Linux have implemented setting fallbacks. - font("Zed Plex Mono"), + font(".ZedMono"), font("Helvetica"), font("Segoe UI"), // Windows font("Cantarell"), // Gnome @@ -96,7 +96,7 @@ impl TextSystem { } /// Get the FontId for the configure font family and style. - pub fn font_id(&self, font: &Font) -> Result { + fn font_id(&self, font: &Font) -> Result { fn clone_font_id_result(font_id: &Result) -> Result { match font_id { Ok(font_id) => Ok(*font_id), @@ -844,3 +844,16 @@ impl FontMetrics { (self.bounding_box / self.units_per_em as f32 * font_size.0).map(px) } } + +#[allow(unused)] +pub(crate) fn font_name_with_fallbacks<'a>(name: &'a str, system: &'a str) -> &'a str { + // Note: the "Zed Plex" fonts were deprecated as we are not allowed to use "Plex" + // in a derived font name. They are essentially indistinguishable from IBM Plex/Lilex, + // and so retained here for backward compatibility. + match name { + ".SystemUIFont" => system, + ".ZedSans" | "Zed Plex Sans" => "IBM Plex Sans", + ".ZedMono" | "Zed Plex Mono" => "Lilex", + _ => name, + } +} diff --git a/crates/gpui/src/text_system/line_wrapper.rs b/crates/gpui/src/text_system/line_wrapper.rs index 5de26511d3..648d714c89 100644 --- a/crates/gpui/src/text_system/line_wrapper.rs +++ b/crates/gpui/src/text_system/line_wrapper.rs @@ -327,7 +327,7 @@ mod tests { fn build_wrapper() -> LineWrapper { let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0)); let cx = TestAppContext::build(dispatcher, None); - let id = cx.text_system().font_id(&font("Zed Plex Mono")).unwrap(); + let id = cx.text_system().resolve_font(&font(".ZedMono")); LineWrapper::new(id, px(16.), cx.text_system().platform_text_system.clone()) } diff --git a/crates/markdown/examples/markdown.rs b/crates/markdown/examples/markdown.rs index bf685bd9ac..c651c7921d 100644 --- a/crates/markdown/examples/markdown.rs +++ b/crates/markdown/examples/markdown.rs @@ -77,16 +77,16 @@ impl Render for MarkdownExample { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let markdown_style = MarkdownStyle { base_text_style: gpui::TextStyle { - font_family: "Zed Plex Sans".into(), + font_family: ".ZedSans".into(), color: cx.theme().colors().terminal_ansi_black, ..Default::default() }, code_block: StyleRefinement::default() - .font_family("Zed Plex Mono") + .font_family(".ZedMono") .m(rems(1.)) .bg(rgb(0xAAAAAAA)), inline_code: gpui::TextStyleRefinement { - font_family: Some("Zed Mono".into()), + font_family: Some(".ZedMono".into()), color: Some(cx.theme().colors().editor_foreground), background_color: Some(cx.theme().colors().editor_background), ..Default::default() diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index 4c5b6272ef..ac01e6c5c8 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -128,7 +128,7 @@ impl Render for StoryWrapper { .flex() .flex_col() .size_full() - .font_family("Zed Plex Mono") + .font_family(".ZedMono") .child(self.story.clone()) } } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 661bb71c91..51bf2dd131 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -284,9 +284,7 @@ pub fn init(cx: &mut App) { let count = Vim::take_count(cx).unwrap_or(1) as f32; Vim::take_forced_motion(cx); let theme = ThemeSettings::get_global(cx); - let Ok(font_id) = window.text_system().font_id(&theme.buffer_font) else { - return; - }; + let font_id = window.text_system().resolve_font(&theme.buffer_font); let Ok(width) = window .text_system() .advance(font_id, theme.buffer_font_size(cx), 'm') @@ -300,9 +298,7 @@ pub fn init(cx: &mut App) { let count = Vim::take_count(cx).unwrap_or(1) as f32; Vim::take_forced_motion(cx); let theme = ThemeSettings::get_global(cx); - let Ok(font_id) = window.text_system().font_id(&theme.buffer_font) else { - return; - }; + let font_id = window.text_system().resolve_font(&theme.buffer_font); let Ok(width) = window .text_system() .advance(font_id, theme.buffer_font_size(cx), 'm') diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 23020d3a9b..ceda403fdd 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4401,11 +4401,11 @@ mod tests { cx.text_system() .add_fonts(vec![ Assets - .load("fonts/plex-mono/ZedPlexMono-Regular.ttf") + .load("fonts/lilex/Lilex-Regular.ttf") .unwrap() .unwrap(), Assets - .load("fonts/plex-sans/ZedPlexSans-Regular.ttf") + .load("fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf") .unwrap() .unwrap(), ]) diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 5d11dfe833..b4cb1fcb9b 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -294,11 +294,11 @@ Define extensions which should be installed (`true`) or never installed (`false` - Description: The name of a font to use for rendering text in the editor. - Setting: `buffer_font_family` -- Default: `Zed Plex Mono` +- Default: `.ZedMono`. This currently aliases to [Lilex](https://lilex.myrt.co). **Options** -The name of any font family installed on the user's system +The name of any font family installed on the user's system, or `".ZedMono"`. ## Buffer Font Features @@ -3511,11 +3511,11 @@ Float values between `0.0` and `0.9`, where: - Description: The name of the font to use for text in the UI. - Setting: `ui_font_family` -- Default: `Zed Plex Sans` +- Default: `.ZedSans`. This currently aliases to [IBM Plex](https://www.ibm.com/plex/). **Options** -The name of any font family installed on the system. +The name of any font family installed on the system, `".ZedSans"` to use the Zed-provided default, or `".SystemUIFont"` to use the system's default UI font (on macOS and Windows). ## UI Font Features @@ -3603,7 +3603,7 @@ For example, to use `Nerd Font` as a fallback, add the following to your setting "soft_wrap": "none", "buffer_font_size": 18, - "buffer_font_family": "Zed Plex Mono", + "buffer_font_family": ".ZedMono", "autosave": "on_focus_change", "format_on_save": "off", diff --git a/docs/src/fonts.md b/docs/src/fonts.md deleted file mode 100644 index 93c687b134..0000000000 --- a/docs/src/fonts.md +++ /dev/null @@ -1,56 +0,0 @@ -# Fonts - - - -Zed ships two fonts: Zed Plex Mono and Zed Plex Sans. These are based on IBM Plex Mono and IBM Plex Sans, respectively. - - - -## Settings - - - -- Buffer fonts - - `buffer-font-family` - - `buffer-font-features` - - `buffer-font-size` - - `buffer-line-height` -- UI fonts - - `ui_font_family` - - `ui_font_fallbacks` - - `ui_font_features` - - `ui_font_weight` - - `ui_font_size` -- Terminal fonts - - `terminal.font-size` - - `terminal.font-family` - - `terminal.font-features` - -## Old Zed Fonts - -Previously, Zed shipped with `Zed Mono` and `Zed Sans`, customized versions of the [Iosevka](https://typeof.net/Iosevka/) typeface. You can find more about them in the [zed-fonts](https://github.com/zed-industries/zed-fonts/) repository. - -Here's how you can use the old Zed fonts instead of `Zed Plex Mono` and `Zed Plex Sans`: - -1. Download [zed-app-fonts-1.2.0.zip](https://github.com/zed-industries/zed-fonts/releases/download/1.2.0/zed-app-fonts-1.2.0.zip) from the [zed-fonts releases](https://github.com/zed-industries/zed-fonts/releases) page. -2. Open macOS `Font Book.app` -3. Unzip the file and drag the `ttf` files into the Font Book app. -4. Update your settings `ui_font_family` and `buffer_font_family` to use `Zed Mono` or `Zed Sans` in your `settings.json` file. - -```json -{ - "ui_font_family": "Zed Sans Extended", - "buffer_font_family": "Zed Mono Extend", - "terminal": { - "font-family": "Zed Mono Extended" - } -} -``` - -5. Note there will be red squiggles under the font name. (this is a bug, but harmless.) diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 46de078d89..7e75f6287d 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -39,13 +39,15 @@ If you would like to use distinct themes for light mode/dark mode that can be se ## Fonts ```json - // UI Font. Use ".SystemUIFont" to use the default system font (SF Pro on macOS) - "ui_font_family": "Zed Plex Sans", + // UI Font. Use ".SystemUIFont" to use the default system font (SF Pro on macOS), + // or ".ZedSans" for the bundled default (currently IBM Plex) + "ui_font_family": ".SystemUIFont", "ui_font_weight": 400, // Font weight in standard CSS units from 100 to 900. "ui_font_size": 16, // Buffer Font - Used by editor buffers - "buffer_font_family": "Zed Plex Mono", // Font name for editor buffers + // use ".ZedMono" for the bundled default monospace (currently Lilex) + "buffer_font_family": "Berkeley Mono", // Font name for editor buffers "buffer_font_size": 15, // Font size for editor buffers "buffer_font_weight": 400, // Font weight in CSS units [100-900] // Line height "comfortable" (1.618), "standard" (1.3) or custom: `{ "custom": 2 }` @@ -53,7 +55,7 @@ If you would like to use distinct themes for light mode/dark mode that can be se // Terminal Font Settings "terminal": { - "font_family": "Zed Plex Mono", + "font_family": "", "font_size": 15, // Terminal line height: comfortable (1.618), standard(1.3) or `{ "custom": 2 }` "line_height": "comfortable", @@ -473,7 +475,7 @@ See [Zed AI Documentation](./ai/overview.md) for additional non-visual AI settin "show": null // Show/hide: (auto, system, always, never) }, // Terminal Font Settings - "font_family": "Zed Plex Mono", + "font_family": "Fira Code", "font_size": 15, "font_weight": 400, // Terminal line height: comfortable (1.618), standard(1.3) or `{ "custom": 2 }` diff --git a/nix/build.nix b/nix/build.nix index 70b4f76932..03403cc1c9 100644 --- a/nix/build.nix +++ b/nix/build.nix @@ -171,8 +171,8 @@ let ZSTD_SYS_USE_PKG_CONFIG = true; FONTCONFIG_FILE = makeFontsConf { fontDirectories = [ - ../assets/fonts/plex-mono - ../assets/fonts/plex-sans + ../assets/fonts/lilex + ../assets/fonts/ibm-plex-sans ]; }; ZED_UPDATE_EXPLANATION = "Zed has been installed using Nix. Auto-updates have thus been disabled."; diff --git a/nix/shell.nix b/nix/shell.nix index b78eb5c001..b6f1efd366 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -46,8 +46,8 @@ # outside the nix store instead of to `$src` FONTCONFIG_FILE = makeFontsConf { fontDirectories = [ - "./assets/fonts/plex-mono" - "./assets/fonts/plex-sans" + "./assets/fonts/lilex" + "./assets/fonts/ibm-plex-sans" ]; }; PROTOC = "${protobuf}/bin/protoc"; From 389d382f426f507d7567c7a8b89e951a3f6723d0 Mon Sep 17 00:00:00 2001 From: smit Date: Thu, 14 Aug 2025 00:59:12 +0530 Subject: [PATCH 324/693] ci: Disable FreeBSD builds (#36140) Revert accidental change introduced in [#35880](https://github.com/zed-industries/zed/pull/35880/files#diff-b803fcb7f17ed9235f1e5cb1fcd2f5d3b2838429d4368ae4c57ce4436577f03fL706) Release Notes: - N/A --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b70271e57..f4ba227168 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -718,7 +718,7 @@ jobs: timeout-minutes: 60 runs-on: github-8vcpu-ubuntu-2404 if: | - ( startsWith(github.ref, 'refs/tags/v') + false && ( startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') ) needs: [linux_tests] name: Build Zed on FreeBSD From 389d24d7e5b54d01bf4dede2e572c1a64c6c7e96 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 13 Aug 2025 17:11:32 -0300 Subject: [PATCH 325/693] Fully support all mention kinds (#36134) Feature parity with the agent1 @mention kinds: - File - Symbols - Selections - Threads - Rules - Fetch Release Notes: - N/A --------- Co-authored-by: Cole Miller --- Cargo.lock | 3 + crates/acp_thread/Cargo.toml | 2 + crates/acp_thread/src/acp_thread.rs | 26 +- crates/acp_thread/src/mention.rs | 340 ++++- crates/agent/src/thread_store.rs | 16 + crates/agent2/src/thread.rs | 79 +- crates/agent_ui/Cargo.toml | 3 + .../agent_ui/src/acp/completion_provider.rs | 1297 +++++++++++++++-- crates/agent_ui/src/acp/message_history.rs | 6 +- crates/agent_ui/src/acp/thread_view.rs | 222 ++- crates/agent_ui/src/agent_panel.rs | 5 + crates/agent_ui/src/context_picker.rs | 68 +- .../src/context_picker/completion_provider.rs | 4 +- crates/assistant_context/Cargo.toml | 3 + crates/assistant_context/src/context_store.rs | 21 + crates/editor/src/editor.rs | 7 + crates/prompt_store/src/prompt_store.rs | 9 + 17 files changed, 1787 insertions(+), 324 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b1337eece..f0fd3049c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,6 +7,7 @@ name = "acp_thread" version = "0.1.0" dependencies = [ "action_log", + "agent", "agent-client-protocol", "anyhow", "buffer_diff", @@ -21,6 +22,7 @@ dependencies = [ "markdown", "parking_lot", "project", + "prompt_store", "rand 0.8.5", "serde", "serde_json", @@ -392,6 +394,7 @@ dependencies = [ "ui", "ui_input", "unindent", + "url", "urlencoding", "util", "uuid", diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index b3ec217bad..2ac15de08f 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -18,6 +18,7 @@ test-support = ["gpui/test-support", "project/test-support"] [dependencies] action_log.workspace = true agent-client-protocol.workspace = true +agent.workspace = true anyhow.workspace = true buffer_diff.workspace = true collections.workspace = true @@ -28,6 +29,7 @@ itertools.workspace = true language.workspace = true markdown.workspace = true project.workspace = true +prompt_store.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index f8a5bf8032..a5b512f31a 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -399,7 +399,7 @@ impl ContentBlock { } } - let new_content = self.extract_content_from_block(block); + let new_content = self.block_string_contents(block); match self { ContentBlock::Empty => { @@ -409,7 +409,7 @@ impl ContentBlock { markdown.update(cx, |markdown, cx| markdown.append(&new_content, cx)); } ContentBlock::ResourceLink { resource_link } => { - let existing_content = Self::resource_link_to_content(&resource_link.uri); + let existing_content = Self::resource_link_md(&resource_link.uri); let combined = format!("{}\n{}", existing_content, new_content); *self = Self::create_markdown_block(combined, language_registry, cx); @@ -417,14 +417,6 @@ impl ContentBlock { } } - fn resource_link_to_content(uri: &str) -> String { - if let Some(uri) = MentionUri::parse(&uri).log_err() { - uri.to_link() - } else { - uri.to_string().clone() - } - } - fn create_markdown_block( content: String, language_registry: &Arc, @@ -436,11 +428,11 @@ impl ContentBlock { } } - fn extract_content_from_block(&self, block: acp::ContentBlock) -> String { + fn block_string_contents(&self, block: acp::ContentBlock) -> String { match block { acp::ContentBlock::Text(text_content) => text_content.text.clone(), acp::ContentBlock::ResourceLink(resource_link) => { - Self::resource_link_to_content(&resource_link.uri) + Self::resource_link_md(&resource_link.uri) } acp::ContentBlock::Resource(acp::EmbeddedResource { resource: @@ -449,13 +441,21 @@ impl ContentBlock { .. }), .. - }) => Self::resource_link_to_content(&uri), + }) => Self::resource_link_md(&uri), acp::ContentBlock::Image(_) | acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => String::new(), } } + fn resource_link_md(uri: &str) -> String { + if let Some(uri) = MentionUri::parse(&uri).log_err() { + uri.as_link().to_string() + } else { + uri.to_string() + } + } + fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str { match self { ContentBlock::Empty => "", diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 59c479d87b..03174608fb 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -1,13 +1,40 @@ -use agent_client_protocol as acp; -use anyhow::{Result, bail}; -use std::path::PathBuf; +use agent::ThreadId; +use anyhow::{Context as _, Result, bail}; +use prompt_store::{PromptId, UserPromptId}; +use std::{ + fmt, + ops::Range, + path::{Path, PathBuf}, +}; +use url::Url; #[derive(Clone, Debug, PartialEq, Eq)] pub enum MentionUri { File(PathBuf), - Symbol(PathBuf, String), - Thread(acp::SessionId), - Rule(String), + Symbol { + path: PathBuf, + name: String, + line_range: Range, + }, + Thread { + id: ThreadId, + name: String, + }, + TextThread { + path: PathBuf, + name: String, + }, + Rule { + id: PromptId, + name: String, + }, + Selection { + path: PathBuf, + line_range: Range, + }, + Fetch { + url: Url, + }, } impl MentionUri { @@ -17,7 +44,34 @@ impl MentionUri { match url.scheme() { "file" => { if let Some(fragment) = url.fragment() { - Ok(Self::Symbol(path.into(), fragment.into())) + let range = fragment + .strip_prefix("L") + .context("Line range must start with \"L\"")?; + let (start, end) = range + .split_once(":") + .context("Line range must use colon as separator")?; + let line_range = start + .parse::() + .context("Parsing line range start")? + .checked_sub(1) + .context("Line numbers should be 1-based")? + ..end + .parse::() + .context("Parsing line range end")? + .checked_sub(1) + .context("Line numbers should be 1-based")?; + if let Some(name) = single_query_param(&url, "symbol")? { + Ok(Self::Symbol { + name, + path: path.into(), + line_range, + }) + } else { + Ok(Self::Selection { + path: path.into(), + line_range, + }) + } } else { let file_path = PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path)); @@ -26,100 +80,292 @@ impl MentionUri { } } "zed" => { - if let Some(thread) = path.strip_prefix("/agent/thread/") { - Ok(Self::Thread(acp::SessionId(thread.into()))) - } else if let Some(rule) = path.strip_prefix("/agent/rule/") { - Ok(Self::Rule(rule.into())) + if let Some(thread_id) = path.strip_prefix("/agent/thread/") { + let name = single_query_param(&url, "name")?.context("Missing thread name")?; + Ok(Self::Thread { + id: thread_id.into(), + name, + }) + } else if let Some(path) = path.strip_prefix("/agent/text-thread/") { + let name = single_query_param(&url, "name")?.context("Missing thread name")?; + Ok(Self::TextThread { + path: path.into(), + name, + }) + } else if let Some(rule_id) = path.strip_prefix("/agent/rule/") { + let name = single_query_param(&url, "name")?.context("Missing rule name")?; + let rule_id = UserPromptId(rule_id.parse()?); + Ok(Self::Rule { + id: rule_id.into(), + name, + }) } else { bail!("invalid zed url: {:?}", input); } } + "http" | "https" => Ok(MentionUri::Fetch { url }), other => bail!("unrecognized scheme {:?}", other), } } - pub fn name(&self) -> String { + fn name(&self) -> String { match self { - MentionUri::File(path) => path.file_name().unwrap().to_string_lossy().into_owned(), - MentionUri::Symbol(_path, name) => name.clone(), - MentionUri::Thread(thread) => thread.to_string(), - MentionUri::Rule(rule) => rule.clone(), + MentionUri::File(path) => path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .into_owned(), + MentionUri::Symbol { name, .. } => name.clone(), + MentionUri::Thread { name, .. } => name.clone(), + MentionUri::TextThread { name, .. } => name.clone(), + MentionUri::Rule { name, .. } => name.clone(), + MentionUri::Selection { + path, line_range, .. + } => selection_name(path, line_range), + MentionUri::Fetch { url } => url.to_string(), } } - pub fn to_link(&self) -> String { - let name = self.name(); - let uri = self.to_uri(); - format!("[{name}]({uri})") + pub fn as_link<'a>(&'a self) -> MentionLink<'a> { + MentionLink(self) } - pub fn to_uri(&self) -> String { + pub fn to_uri(&self) -> Url { match self { MentionUri::File(path) => { - format!("file://{}", path.display()) + let mut url = Url::parse("file:///").unwrap(); + url.set_path(&path.to_string_lossy()); + url } - MentionUri::Symbol(path, name) => { - format!("file://{}#{}", path.display(), name) + MentionUri::Symbol { + path, + name, + line_range, + } => { + let mut url = Url::parse("file:///").unwrap(); + url.set_path(&path.to_string_lossy()); + url.query_pairs_mut().append_pair("symbol", name); + url.set_fragment(Some(&format!( + "L{}:{}", + line_range.start + 1, + line_range.end + 1 + ))); + url } - MentionUri::Thread(thread) => { - format!("zed:///agent/thread/{}", thread.0) + MentionUri::Selection { path, line_range } => { + let mut url = Url::parse("file:///").unwrap(); + url.set_path(&path.to_string_lossy()); + url.set_fragment(Some(&format!( + "L{}:{}", + line_range.start + 1, + line_range.end + 1 + ))); + url } - MentionUri::Rule(rule) => { - format!("zed:///agent/rule/{}", rule) + MentionUri::Thread { name, id } => { + let mut url = Url::parse("zed:///").unwrap(); + url.set_path(&format!("/agent/thread/{id}")); + url.query_pairs_mut().append_pair("name", name); + url } + MentionUri::TextThread { path, name } => { + let mut url = Url::parse("zed:///").unwrap(); + url.set_path(&format!("/agent/text-thread/{}", path.to_string_lossy())); + url.query_pairs_mut().append_pair("name", name); + url + } + MentionUri::Rule { name, id } => { + let mut url = Url::parse("zed:///").unwrap(); + url.set_path(&format!("/agent/rule/{id}")); + url.query_pairs_mut().append_pair("name", name); + url + } + MentionUri::Fetch { url } => url.clone(), } } } +pub struct MentionLink<'a>(&'a MentionUri); + +impl fmt::Display for MentionLink<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[@{}]({})", self.0.name(), self.0.to_uri()) + } +} + +fn single_query_param(url: &Url, name: &'static str) -> Result> { + let pairs = url.query_pairs().collect::>(); + match pairs.as_slice() { + [] => Ok(None), + [(k, v)] => { + if k != name { + bail!("invalid query parameter") + } + + Ok(Some(v.to_string())) + } + _ => bail!("too many query pairs"), + } +} + +pub fn selection_name(path: &Path, line_range: &Range) -> String { + format!( + "{} ({}:{})", + path.file_name().unwrap_or_default().display(), + line_range.start + 1, + line_range.end + 1 + ) +} + #[cfg(test)] mod tests { use super::*; #[test] - fn test_mention_uri_parse_and_display() { - // Test file URI + fn test_parse_file_uri() { let file_uri = "file:///path/to/file.rs"; let parsed = MentionUri::parse(file_uri).unwrap(); match &parsed { MentionUri::File(path) => assert_eq!(path.to_str().unwrap(), "/path/to/file.rs"), _ => panic!("Expected File variant"), } - assert_eq!(parsed.to_uri(), file_uri); + assert_eq!(parsed.to_uri().to_string(), file_uri); + } - // Test symbol URI - let symbol_uri = "file:///path/to/file.rs#MySymbol"; + #[test] + fn test_parse_symbol_uri() { + let symbol_uri = "file:///path/to/file.rs?symbol=MySymbol#L10:20"; let parsed = MentionUri::parse(symbol_uri).unwrap(); match &parsed { - MentionUri::Symbol(path, symbol) => { + MentionUri::Symbol { + path, + name, + line_range, + } => { assert_eq!(path.to_str().unwrap(), "/path/to/file.rs"); - assert_eq!(symbol, "MySymbol"); + assert_eq!(name, "MySymbol"); + assert_eq!(line_range.start, 9); + assert_eq!(line_range.end, 19); } _ => panic!("Expected Symbol variant"), } - assert_eq!(parsed.to_uri(), symbol_uri); + assert_eq!(parsed.to_uri().to_string(), symbol_uri); + } - // Test thread URI - let thread_uri = "zed:///agent/thread/session123"; + #[test] + fn test_parse_selection_uri() { + let selection_uri = "file:///path/to/file.rs#L5:15"; + let parsed = MentionUri::parse(selection_uri).unwrap(); + match &parsed { + MentionUri::Selection { path, line_range } => { + assert_eq!(path.to_str().unwrap(), "/path/to/file.rs"); + assert_eq!(line_range.start, 4); + assert_eq!(line_range.end, 14); + } + _ => panic!("Expected Selection variant"), + } + assert_eq!(parsed.to_uri().to_string(), selection_uri); + } + + #[test] + fn test_parse_thread_uri() { + let thread_uri = "zed:///agent/thread/session123?name=Thread+name"; let parsed = MentionUri::parse(thread_uri).unwrap(); match &parsed { - MentionUri::Thread(session_id) => assert_eq!(session_id.0.as_ref(), "session123"), + MentionUri::Thread { + id: thread_id, + name, + } => { + assert_eq!(thread_id.to_string(), "session123"); + assert_eq!(name, "Thread name"); + } _ => panic!("Expected Thread variant"), } - assert_eq!(parsed.to_uri(), thread_uri); + assert_eq!(parsed.to_uri().to_string(), thread_uri); + } - // Test rule URI - let rule_uri = "zed:///agent/rule/my_rule"; + #[test] + fn test_parse_rule_uri() { + let rule_uri = "zed:///agent/rule/d8694ff2-90d5-4b6f-be33-33c1763acd52?name=Some+rule"; let parsed = MentionUri::parse(rule_uri).unwrap(); match &parsed { - MentionUri::Rule(rule) => assert_eq!(rule, "my_rule"), + MentionUri::Rule { id, name } => { + assert_eq!(id.to_string(), "d8694ff2-90d5-4b6f-be33-33c1763acd52"); + assert_eq!(name, "Some rule"); + } _ => panic!("Expected Rule variant"), } - assert_eq!(parsed.to_uri(), rule_uri); + assert_eq!(parsed.to_uri().to_string(), rule_uri); + } - // Test invalid scheme - assert!(MentionUri::parse("http://example.com").is_err()); + #[test] + fn test_parse_fetch_http_uri() { + let http_uri = "http://example.com/path?query=value#fragment"; + let parsed = MentionUri::parse(http_uri).unwrap(); + match &parsed { + MentionUri::Fetch { url } => { + assert_eq!(url.to_string(), http_uri); + } + _ => panic!("Expected Fetch variant"), + } + assert_eq!(parsed.to_uri().to_string(), http_uri); + } - // Test invalid zed path + #[test] + fn test_parse_fetch_https_uri() { + let https_uri = "https://example.com/api/endpoint"; + let parsed = MentionUri::parse(https_uri).unwrap(); + match &parsed { + MentionUri::Fetch { url } => { + assert_eq!(url.to_string(), https_uri); + } + _ => panic!("Expected Fetch variant"), + } + assert_eq!(parsed.to_uri().to_string(), https_uri); + } + + #[test] + fn test_invalid_scheme() { + assert!(MentionUri::parse("ftp://example.com").is_err()); + assert!(MentionUri::parse("ssh://example.com").is_err()); + assert!(MentionUri::parse("unknown://example.com").is_err()); + } + + #[test] + fn test_invalid_zed_path() { assert!(MentionUri::parse("zed:///invalid/path").is_err()); + assert!(MentionUri::parse("zed:///agent/unknown/test").is_err()); + } + + #[test] + fn test_invalid_line_range_format() { + // Missing L prefix + assert!(MentionUri::parse("file:///path/to/file.rs#10:20").is_err()); + + // Missing colon separator + assert!(MentionUri::parse("file:///path/to/file.rs#L1020").is_err()); + + // Invalid numbers + assert!(MentionUri::parse("file:///path/to/file.rs#L10:abc").is_err()); + assert!(MentionUri::parse("file:///path/to/file.rs#Labc:20").is_err()); + } + + #[test] + fn test_invalid_query_parameters() { + // Invalid query parameter name + assert!(MentionUri::parse("file:///path/to/file.rs#L10:20?invalid=test").is_err()); + + // Too many query parameters + assert!( + MentionUri::parse("file:///path/to/file.rs#L10:20?symbol=test&another=param").is_err() + ); + } + + #[test] + fn test_zero_based_line_numbers() { + // Test that 0-based line numbers are rejected (should be 1-based) + assert!(MentionUri::parse("file:///path/to/file.rs#L0:10").is_err()); + assert!(MentionUri::parse("file:///path/to/file.rs#L1:0").is_err()); + assert!(MentionUri::parse("file:///path/to/file.rs#L0:0").is_err()); } } diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index cc7cb50c91..12c94a522d 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -205,6 +205,22 @@ impl ThreadStore { (this, ready_rx) } + #[cfg(any(test, feature = "test-support"))] + pub fn fake(project: Entity, cx: &mut App) -> Self { + Self { + project, + tools: cx.new(|_| ToolWorkingSet::default()), + prompt_builder: Arc::new(PromptBuilder::new(None).unwrap()), + prompt_store: None, + context_server_tool_ids: HashMap::default(), + threads: Vec::new(), + project_context: SharedProjectContext::default(), + reload_system_prompt_tx: mpsc::channel(0).0, + _reload_system_prompt_task: Task::ready(()), + _subscriptions: vec![], + } + } + fn handle_project_event( &mut self, _project: Entity, diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 204b489124..b48f9001ac 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -25,8 +25,8 @@ use schemars::{JsonSchema, Schema}; use serde::{Deserialize, Serialize}; use settings::{Settings, update_settings_file}; use smol::stream::StreamExt; -use std::fmt::Write; use std::{cell::RefCell, collections::BTreeMap, path::Path, rc::Rc, sync::Arc}; +use std::{fmt::Write, ops::Range}; use util::{ResultExt, markdown::MarkdownCodeBlock}; #[derive(Debug, Clone, PartialEq, Eq)] @@ -79,9 +79,9 @@ impl UserMessage { } UserMessageContent::Mention { uri, content } => { if !content.is_empty() { - markdown.push_str(&format!("{}\n\n{}\n", uri.to_link(), content)); + let _ = write!(&mut markdown, "{}\n\n{}\n", uri.as_link(), content); } else { - markdown.push_str(&format!("{}\n", uri.to_link())); + let _ = write!(&mut markdown, "{}\n", uri.as_link()); } } } @@ -104,12 +104,14 @@ impl UserMessage { const OPEN_FILES_TAG: &str = ""; const OPEN_SYMBOLS_TAG: &str = ""; const OPEN_THREADS_TAG: &str = ""; + const OPEN_FETCH_TAG: &str = ""; const OPEN_RULES_TAG: &str = "\nThe user has specified the following rules that should be applied:\n"; let mut file_context = OPEN_FILES_TAG.to_string(); let mut symbol_context = OPEN_SYMBOLS_TAG.to_string(); let mut thread_context = OPEN_THREADS_TAG.to_string(); + let mut fetch_context = OPEN_FETCH_TAG.to_string(); let mut rules_context = OPEN_RULES_TAG.to_string(); for chunk in &self.content { @@ -122,21 +124,40 @@ impl UserMessage { } UserMessageContent::Mention { uri, content } => { match uri { - MentionUri::File(path) | MentionUri::Symbol(path, _) => { + MentionUri::File(path) => { write!( &mut symbol_context, "\n{}", MarkdownCodeBlock { - tag: &codeblock_tag(&path), + tag: &codeblock_tag(&path, None), text: &content.to_string(), } ) .ok(); } - MentionUri::Thread(_session_id) => { + MentionUri::Symbol { + path, line_range, .. + } + | MentionUri::Selection { + path, line_range, .. + } => { + write!( + &mut rules_context, + "\n{}", + MarkdownCodeBlock { + tag: &codeblock_tag(&path, Some(line_range)), + text: &content + } + ) + .ok(); + } + MentionUri::Thread { .. } => { write!(&mut thread_context, "\n{}\n", content).ok(); } - MentionUri::Rule(_user_prompt_id) => { + MentionUri::TextThread { .. } => { + write!(&mut thread_context, "\n{}\n", content).ok(); + } + MentionUri::Rule { .. } => { write!( &mut rules_context, "\n{}", @@ -147,9 +168,12 @@ impl UserMessage { ) .ok(); } + MentionUri::Fetch { url } => { + write!(&mut fetch_context, "\nFetch: {}\n\n{}", url, content).ok(); + } } - language_model::MessageContent::Text(uri.to_link()) + language_model::MessageContent::Text(uri.as_link().to_string()) } }; @@ -179,6 +203,13 @@ impl UserMessage { .push(language_model::MessageContent::Text(thread_context)); } + if fetch_context.len() > OPEN_FETCH_TAG.len() { + fetch_context.push_str("\n"); + message + .content + .push(language_model::MessageContent::Text(fetch_context)); + } + if rules_context.len() > OPEN_RULES_TAG.len() { rules_context.push_str("\n"); message @@ -200,6 +231,26 @@ impl UserMessage { } } +fn codeblock_tag(full_path: &Path, line_range: Option<&Range>) -> String { + let mut result = String::new(); + + if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) { + let _ = write!(result, "{} ", extension); + } + + let _ = write!(result, "{}", full_path.display()); + + if let Some(range) = line_range { + if range.start == range.end { + let _ = write!(result, ":{}", range.start + 1); + } else { + let _ = write!(result, ":{}-{}", range.start + 1, range.end + 1); + } + } + + result +} + impl AgentMessage { pub fn to_markdown(&self) -> String { let mut markdown = String::from("## Assistant\n\n"); @@ -1367,18 +1418,6 @@ impl std::ops::DerefMut for ToolCallEventStreamReceiver { } } -fn codeblock_tag(full_path: &Path) -> String { - let mut result = String::new(); - - if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) { - let _ = write!(result, "{} ", extension); - } - - let _ = write!(result, "{}", full_path.display()); - - result -} - impl From<&str> for UserMessageContent { fn from(text: &str) -> Self { Self::Text(text.into()) diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index de0a27c2cb..b6a5710aa4 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -93,6 +93,7 @@ time.workspace = true time_format.workspace = true ui.workspace = true ui_input.workspace = true +url.workspace = true urlencoding.workspace = true util.workspace = true uuid.workspace = true @@ -102,6 +103,8 @@ workspace.workspace = true zed_actions.workspace = true [dev-dependencies] +agent = { workspace = true, features = ["test-support"] } +assistant_context = { workspace = true, features = ["test-support"] } assistant_tools.workspace = true buffer_diff = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] } diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 3c2bea53a7..46c8aa92f1 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -3,71 +3,184 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::AtomicBool; -use acp_thread::MentionUri; -use anyhow::{Context as _, Result}; -use collections::HashMap; +use acp_thread::{MentionUri, selection_name}; +use anyhow::{Context as _, Result, anyhow}; +use collections::{HashMap, HashSet}; use editor::display_map::CreaseId; -use editor::{CompletionProvider, Editor, ExcerptId}; +use editor::{CompletionProvider, Editor, ExcerptId, ToOffset as _}; use file_icons::FileIcons; use futures::future::try_join_all; +use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{App, Entity, Task, WeakEntity}; +use http_client::HttpClientWithUrl; +use itertools::Itertools as _; use language::{Buffer, CodeLabel, HighlightId}; use lsp::CompletionContext; use parking_lot::Mutex; -use project::{Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, WorktreeId}; +use project::{ + Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId, +}; +use prompt_store::PromptStore; use rope::Point; -use text::{Anchor, ToPoint}; +use text::{Anchor, OffsetRangeExt as _, ToPoint as _}; use ui::prelude::*; +use url::Url; use workspace::Workspace; +use workspace::notifications::NotifyResultExt; -use crate::context_picker::MentionLink; -use crate::context_picker::file_context_picker::{extract_file_name_and_directory, search_files}; +use agent::{ + context::RULES_ICON, + thread_store::{TextThreadStore, ThreadStore}, +}; + +use crate::context_picker::fetch_context_picker::fetch_url_content; +use crate::context_picker::file_context_picker::{FileMatch, search_files}; +use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules}; +use crate::context_picker::symbol_context_picker::SymbolMatch; +use crate::context_picker::symbol_context_picker::search_symbols; +use crate::context_picker::thread_context_picker::{ + ThreadContextEntry, ThreadMatch, search_threads, +}; +use crate::context_picker::{ + ContextPickerAction, ContextPickerEntry, ContextPickerMode, RecentEntry, + available_context_picker_entries, recent_context_picker_entries, selection_ranges, +}; #[derive(Default)] pub struct MentionSet { - paths_by_crease_id: HashMap, + uri_by_crease_id: HashMap, + fetch_results: HashMap, } impl MentionSet { - pub fn insert(&mut self, crease_id: CreaseId, path: PathBuf) { - self.paths_by_crease_id - .insert(crease_id, MentionUri::File(path)); + pub fn insert(&mut self, crease_id: CreaseId, uri: MentionUri) { + self.uri_by_crease_id.insert(crease_id, uri); + } + + pub fn add_fetch_result(&mut self, url: Url, content: String) { + self.fetch_results.insert(url, content); } pub fn drain(&mut self) -> impl Iterator { - self.paths_by_crease_id.drain().map(|(id, _)| id) + self.fetch_results.clear(); + self.uri_by_crease_id.drain().map(|(id, _)| id) } pub fn contents( &self, project: Entity, + thread_store: Entity, + text_thread_store: Entity, + window: &mut Window, cx: &mut App, ) -> Task>> { let contents = self - .paths_by_crease_id + .uri_by_crease_id .iter() - .map(|(crease_id, uri)| match uri { - MentionUri::File(path) => { - let crease_id = *crease_id; - let uri = uri.clone(); - let path = path.to_path_buf(); - let buffer_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(path, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_buffer(path, cx)) - }); + .map(|(&crease_id, uri)| { + match uri { + MentionUri::File(path) => { + let uri = uri.clone(); + let path = path.to_path_buf(); + let buffer_task = project.update(cx, |project, cx| { + let path = project + .find_project_path(path, cx) + .context("Failed to find project path")?; + anyhow::Ok(project.open_buffer(path, cx)) + }); - cx.spawn(async move |cx| { - let buffer = buffer_task?.await?; - let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; + cx.spawn(async move |cx| { + let buffer = buffer_task?.await?; + let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; - anyhow::Ok((crease_id, Mention { uri, content })) - }) - } - _ => { - // TODO - unimplemented!() + anyhow::Ok((crease_id, Mention { uri, content })) + }) + } + MentionUri::Symbol { + path, line_range, .. + } + | MentionUri::Selection { + path, line_range, .. + } => { + let uri = uri.clone(); + let path_buf = path.clone(); + let line_range = line_range.clone(); + + let buffer_task = project.update(cx, |project, cx| { + let path = project + .find_project_path(&path_buf, cx) + .context("Failed to find project path")?; + anyhow::Ok(project.open_buffer(path, cx)) + }); + + cx.spawn(async move |cx| { + let buffer = buffer_task?.await?; + let content = buffer.read_with(cx, |buffer, _cx| { + buffer + .text_for_range( + Point::new(line_range.start, 0) + ..Point::new( + line_range.end, + buffer.line_len(line_range.end), + ), + ) + .collect() + })?; + + anyhow::Ok((crease_id, Mention { uri, content })) + }) + } + MentionUri::Thread { id: thread_id, .. } => { + let open_task = thread_store.update(cx, |thread_store, cx| { + thread_store.open_thread(&thread_id, window, cx) + }); + + let uri = uri.clone(); + cx.spawn(async move |cx| { + let thread = open_task.await?; + let content = thread.read_with(cx, |thread, _cx| { + thread.latest_detailed_summary_or_text().to_string() + })?; + + anyhow::Ok((crease_id, Mention { uri, content })) + }) + } + MentionUri::TextThread { path, .. } => { + let context = text_thread_store.update(cx, |text_thread_store, cx| { + text_thread_store.open_local_context(path.as_path().into(), cx) + }); + let uri = uri.clone(); + cx.spawn(async move |cx| { + let context = context.await?; + let xml = context.update(cx, |context, cx| context.to_xml(cx))?; + anyhow::Ok((crease_id, Mention { uri, content: xml })) + }) + } + MentionUri::Rule { id: prompt_id, .. } => { + let Some(prompt_store) = thread_store.read(cx).prompt_store().clone() + else { + return Task::ready(Err(anyhow!("missing prompt store"))); + }; + let text_task = prompt_store.read(cx).load(*prompt_id, cx); + let uri = uri.clone(); + cx.spawn(async move |_| { + // TODO: report load errors instead of just logging + let text = text_task.await?; + anyhow::Ok((crease_id, Mention { uri, content: text })) + }) + } + MentionUri::Fetch { url } => { + let Some(content) = self.fetch_results.get(&url) else { + return Task::ready(Err(anyhow!("missing fetch result"))); + }; + Task::ready(Ok(( + crease_id, + Mention { + uri: uri.clone(), + content: content.clone(), + }, + ))) + } } }) .collect::>(); @@ -79,30 +192,458 @@ impl MentionSet { } } +#[derive(Debug)] pub struct Mention { pub uri: MentionUri, pub content: String, } +pub(crate) enum Match { + File(FileMatch), + Symbol(SymbolMatch), + Thread(ThreadMatch), + Fetch(SharedString), + Rules(RulesContextEntry), + Entry(EntryMatch), +} + +pub struct EntryMatch { + mat: Option, + entry: ContextPickerEntry, +} + +impl Match { + pub fn score(&self) -> f64 { + match self { + Match::File(file) => file.mat.score, + Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.), + Match::Thread(_) => 1., + Match::Symbol(_) => 1., + Match::Rules(_) => 1., + Match::Fetch(_) => 1., + } + } +} + +fn search( + mode: Option, + query: String, + cancellation_flag: Arc, + recent_entries: Vec, + prompt_store: Option>, + thread_store: WeakEntity, + text_thread_context_store: WeakEntity, + workspace: Entity, + cx: &mut App, +) -> Task> { + match mode { + Some(ContextPickerMode::File) => { + let search_files_task = + search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); + cx.background_spawn(async move { + search_files_task + .await + .into_iter() + .map(Match::File) + .collect() + }) + } + + Some(ContextPickerMode::Symbol) => { + let search_symbols_task = + search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx); + cx.background_spawn(async move { + search_symbols_task + .await + .into_iter() + .map(Match::Symbol) + .collect() + }) + } + + Some(ContextPickerMode::Thread) => { + if let Some((thread_store, context_store)) = thread_store + .upgrade() + .zip(text_thread_context_store.upgrade()) + { + let search_threads_task = search_threads( + query.clone(), + cancellation_flag.clone(), + thread_store, + context_store, + cx, + ); + cx.background_spawn(async move { + search_threads_task + .await + .into_iter() + .map(Match::Thread) + .collect() + }) + } else { + Task::ready(Vec::new()) + } + } + + Some(ContextPickerMode::Fetch) => { + if !query.is_empty() { + Task::ready(vec![Match::Fetch(query.into())]) + } else { + Task::ready(Vec::new()) + } + } + + Some(ContextPickerMode::Rules) => { + if let Some(prompt_store) = prompt_store.as_ref() { + let search_rules_task = + search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx); + cx.background_spawn(async move { + search_rules_task + .await + .into_iter() + .map(Match::Rules) + .collect::>() + }) + } else { + Task::ready(Vec::new()) + } + } + + None => { + if query.is_empty() { + let mut matches = recent_entries + .into_iter() + .map(|entry| match entry { + RecentEntry::File { + project_path, + path_prefix, + } => Match::File(FileMatch { + mat: fuzzy::PathMatch { + score: 1., + positions: Vec::new(), + worktree_id: project_path.worktree_id.to_usize(), + path: project_path.path, + path_prefix, + is_dir: false, + distance_to_relative_ancestor: 0, + }, + is_recent: true, + }), + RecentEntry::Thread(thread_context_entry) => Match::Thread(ThreadMatch { + thread: thread_context_entry, + is_recent: true, + }), + }) + .collect::>(); + + matches.extend( + available_context_picker_entries( + &prompt_store, + &Some(thread_store.clone()), + &workspace, + cx, + ) + .into_iter() + .map(|mode| { + Match::Entry(EntryMatch { + entry: mode, + mat: None, + }) + }), + ); + + Task::ready(matches) + } else { + let executor = cx.background_executor().clone(); + + let search_files_task = + search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); + + let entries = available_context_picker_entries( + &prompt_store, + &Some(thread_store.clone()), + &workspace, + cx, + ); + let entry_candidates = entries + .iter() + .enumerate() + .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword())) + .collect::>(); + + cx.background_spawn(async move { + let mut matches = search_files_task + .await + .into_iter() + .map(Match::File) + .collect::>(); + + let entry_matches = fuzzy::match_strings( + &entry_candidates, + &query, + false, + true, + 100, + &Arc::new(AtomicBool::default()), + executor, + ) + .await; + + matches.extend(entry_matches.into_iter().map(|mat| { + Match::Entry(EntryMatch { + entry: entries[mat.candidate_id], + mat: Some(mat), + }) + })); + + matches.sort_by(|a, b| { + b.score() + .partial_cmp(&a.score()) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + matches + }) + } + } + } +} + pub struct ContextPickerCompletionProvider { - workspace: WeakEntity, - editor: WeakEntity, mention_set: Arc>, + workspace: WeakEntity, + thread_store: WeakEntity, + text_thread_store: WeakEntity, + editor: WeakEntity, } impl ContextPickerCompletionProvider { pub fn new( mention_set: Arc>, workspace: WeakEntity, + thread_store: WeakEntity, + text_thread_store: WeakEntity, editor: WeakEntity, ) -> Self { Self { mention_set, workspace, + thread_store, + text_thread_store, editor, } } + fn completion_for_entry( + entry: ContextPickerEntry, + excerpt_id: ExcerptId, + source_range: Range, + editor: Entity, + mention_set: Arc>, + workspace: &Entity, + cx: &mut App, + ) -> Option { + match entry { + ContextPickerEntry::Mode(mode) => Some(Completion { + replace_range: source_range.clone(), + new_text: format!("@{} ", mode.keyword()), + label: CodeLabel::plain(mode.label().to_string(), None), + icon_path: Some(mode.icon().path().into()), + documentation: None, + source: project::CompletionSource::Custom, + insert_text_mode: None, + // This ensures that when a user accepts this completion, the + // completion menu will still be shown after "@category " is + // inserted + confirm: Some(Arc::new(|_, _, _| true)), + }), + ContextPickerEntry::Action(action) => { + let (new_text, on_action) = match action { + ContextPickerAction::AddSelections => { + let selections = selection_ranges(workspace, cx); + + const PLACEHOLDER: &str = "selection "; + + let new_text = std::iter::repeat(PLACEHOLDER) + .take(selections.len()) + .chain(std::iter::once("")) + .join(" "); + + let callback = Arc::new({ + let mention_set = mention_set.clone(); + let selections = selections.clone(); + move |_, window: &mut Window, cx: &mut App| { + let editor = editor.clone(); + let mention_set = mention_set.clone(); + let selections = selections.clone(); + window.defer(cx, move |window, cx| { + let mut current_offset = 0; + + for (buffer, selection_range) in selections { + let snapshot = + editor.read(cx).buffer().read(cx).snapshot(cx); + let Some(start) = snapshot + .anchor_in_excerpt(excerpt_id, source_range.start) + else { + return; + }; + + let offset = start.to_offset(&snapshot) + current_offset; + let text_len = PLACEHOLDER.len() - 1; + + let range = snapshot.anchor_after(offset) + ..snapshot.anchor_after(offset + text_len); + + let path = buffer + .read(cx) + .file() + .map_or(PathBuf::from("untitled"), |file| { + file.path().to_path_buf() + }); + + let point_range = snapshot + .as_singleton() + .map(|(_, _, snapshot)| { + selection_range.to_point(&snapshot) + }) + .unwrap_or_default(); + let line_range = point_range.start.row..point_range.end.row; + let crease = crate::context_picker::crease_for_mention( + selection_name(&path, &line_range).into(), + IconName::Reader.path().into(), + range, + editor.downgrade(), + ); + + let [crease_id]: [_; 1] = + editor.update(cx, |editor, cx| { + let crease_ids = + editor.insert_creases(vec![crease.clone()], cx); + editor.fold_creases( + vec![crease], + false, + window, + cx, + ); + crease_ids.try_into().unwrap() + }); + + mention_set.lock().insert( + crease_id, + MentionUri::Selection { path, line_range }, + ); + + current_offset += text_len + 1; + } + }); + + false + } + }); + + (new_text, callback) + } + }; + + Some(Completion { + replace_range: source_range.clone(), + new_text, + label: CodeLabel::plain(action.label().to_string(), None), + icon_path: Some(action.icon().path().into()), + documentation: None, + source: project::CompletionSource::Custom, + insert_text_mode: None, + // This ensures that when a user accepts this completion, the + // completion menu will still be shown after "@category " is + // inserted + confirm: Some(on_action), + }) + } + } + } + + fn completion_for_thread( + thread_entry: ThreadContextEntry, + excerpt_id: ExcerptId, + source_range: Range, + recent: bool, + editor: Entity, + mention_set: Arc>, + ) -> Completion { + let icon_for_completion = if recent { + IconName::HistoryRerun + } else { + IconName::Thread + }; + + let uri = match &thread_entry { + ThreadContextEntry::Thread { id, title } => MentionUri::Thread { + id: id.clone(), + name: title.to_string(), + }, + ThreadContextEntry::Context { path, title } => MentionUri::TextThread { + path: path.to_path_buf(), + name: title.to_string(), + }, + }; + let new_text = format!("{} ", uri.as_link()); + + let new_text_len = new_text.len(); + Completion { + replace_range: source_range.clone(), + new_text, + label: CodeLabel::plain(thread_entry.title().to_string(), None), + documentation: None, + insert_text_mode: None, + source: project::CompletionSource::Custom, + icon_path: Some(icon_for_completion.path().into()), + confirm: Some(confirm_completion_callback( + IconName::Thread.path().into(), + thread_entry.title().clone(), + excerpt_id, + source_range.start, + new_text_len - 1, + editor.clone(), + mention_set, + uri, + )), + } + } + + fn completion_for_rules( + rule: RulesContextEntry, + excerpt_id: ExcerptId, + source_range: Range, + editor: Entity, + mention_set: Arc>, + ) -> Completion { + let uri = MentionUri::Rule { + id: rule.prompt_id.into(), + name: rule.title.to_string(), + }; + let new_text = format!("{} ", uri.as_link()); + let new_text_len = new_text.len(); + Completion { + replace_range: source_range.clone(), + new_text, + label: CodeLabel::plain(rule.title.to_string(), None), + documentation: None, + insert_text_mode: None, + source: project::CompletionSource::Custom, + icon_path: Some(RULES_ICON.path().into()), + confirm: Some(confirm_completion_callback( + RULES_ICON.path().into(), + rule.title.clone(), + excerpt_id, + source_range.start, + new_text_len - 1, + editor.clone(), + mention_set, + uri, + )), + } + } + pub(crate) fn completion_for_path( project_path: ProjectPath, path_prefix: &str, @@ -114,9 +655,12 @@ impl ContextPickerCompletionProvider { mention_set: Arc>, project: Entity, cx: &App, - ) -> Completion { + ) -> Option { let (file_name, directory) = - extract_file_name_and_directory(&project_path.path, path_prefix); + crate::context_picker::file_context_picker::extract_file_name_and_directory( + &project_path.path, + path_prefix, + ); let label = build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx); @@ -138,9 +682,12 @@ impl ContextPickerCompletionProvider { crease_icon_path.clone() }; - let new_text = format!("{} ", MentionLink::for_file(&file_name, &full_path)); + let abs_path = project.read(cx).absolute_path(&project_path, cx)?; + + let file_uri = MentionUri::File(abs_path); + let new_text = format!("{} ", file_uri.as_link()); let new_text_len = new_text.len(); - Completion { + Some(Completion { replace_range: source_range.clone(), new_text, label, @@ -151,15 +698,153 @@ impl ContextPickerCompletionProvider { confirm: Some(confirm_completion_callback( crease_icon_path, file_name, - project_path, excerpt_id, source_range.start, new_text_len - 1, editor, - mention_set, - project, + mention_set.clone(), + file_uri, )), - } + }) + } + + fn completion_for_symbol( + symbol: Symbol, + excerpt_id: ExcerptId, + source_range: Range, + editor: Entity, + mention_set: Arc>, + workspace: Entity, + cx: &mut App, + ) -> Option { + let project = workspace.read(cx).project().clone(); + + let label = CodeLabel::plain(symbol.name.clone(), None); + + let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?; + let uri = MentionUri::Symbol { + path: abs_path, + name: symbol.name.clone(), + line_range: symbol.range.start.0.row..symbol.range.end.0.row, + }; + let new_text = format!("{} ", uri.as_link()); + let new_text_len = new_text.len(); + Some(Completion { + replace_range: source_range.clone(), + new_text, + label, + documentation: None, + source: project::CompletionSource::Custom, + icon_path: Some(IconName::Code.path().into()), + insert_text_mode: None, + confirm: Some(confirm_completion_callback( + IconName::Code.path().into(), + symbol.name.clone().into(), + excerpt_id, + source_range.start, + new_text_len - 1, + editor.clone(), + mention_set.clone(), + uri, + )), + }) + } + + fn completion_for_fetch( + source_range: Range, + url_to_fetch: SharedString, + excerpt_id: ExcerptId, + editor: Entity, + mention_set: Arc>, + http_client: Arc, + ) -> Option { + let new_text = format!("@fetch {} ", url_to_fetch.clone()); + let new_text_len = new_text.len(); + Some(Completion { + replace_range: source_range.clone(), + new_text, + label: CodeLabel::plain(url_to_fetch.to_string(), None), + documentation: None, + source: project::CompletionSource::Custom, + icon_path: Some(IconName::ToolWeb.path().into()), + insert_text_mode: None, + confirm: Some({ + let start = source_range.start; + let content_len = new_text_len - 1; + let editor = editor.clone(); + let url_to_fetch = url_to_fetch.clone(); + let source_range = source_range.clone(); + Arc::new(move |_, window, cx| { + let Some(url) = url::Url::parse(url_to_fetch.as_ref()) + .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}"))) + .notify_app_err(cx) + else { + return false; + }; + let mention_uri = MentionUri::Fetch { url: url.clone() }; + + let editor = editor.clone(); + let mention_set = mention_set.clone(); + let http_client = http_client.clone(); + let source_range = source_range.clone(); + window.defer(cx, move |window, cx| { + let url = url.clone(); + + let Some(crease_id) = crate::context_picker::insert_crease_for_mention( + excerpt_id, + start, + content_len, + url.to_string().into(), + IconName::ToolWeb.path().into(), + editor.clone(), + window, + cx, + ) else { + return; + }; + + let editor = editor.clone(); + let mention_set = mention_set.clone(); + let http_client = http_client.clone(); + let source_range = source_range.clone(); + window + .spawn(cx, async move |cx| { + if let Some(content) = + fetch_url_content(http_client, url.to_string()) + .await + .notify_async_err(cx) + { + mention_set.lock().add_fetch_result(url, content); + mention_set.lock().insert(crease_id, mention_uri.clone()); + } else { + // Remove crease if we failed to fetch + editor + .update(cx, |editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let Some(anchor) = snapshot + .anchor_in_excerpt(excerpt_id, source_range.start) + else { + return; + }; + editor.display_map.update(cx, |display_map, cx| { + display_map.unfold_intersecting( + vec![anchor..anchor], + true, + cx, + ); + }); + editor.remove_creases([crease_id], cx); + }) + .ok(); + } + Some(()) + }) + .detach(); + }); + false + }) + }), + }) } } @@ -206,16 +891,66 @@ impl CompletionProvider for ContextPickerCompletionProvider { }; let project = workspace.read(cx).project().clone(); + let http_client = workspace.read(cx).client().http_client(); let snapshot = buffer.read(cx).snapshot(); let source_range = snapshot.anchor_before(state.source_range.start) ..snapshot.anchor_after(state.source_range.end); + let thread_store = self.thread_store.clone(); + let text_thread_store = self.text_thread_store.clone(); let editor = self.editor.clone(); - let mention_set = self.mention_set.clone(); - let MentionCompletion { argument, .. } = state; + + let MentionCompletion { mode, argument, .. } = state; let query = argument.unwrap_or_else(|| "".to_string()); - let search_task = search_files(query.clone(), Arc::::default(), &workspace, cx); + let (exclude_paths, exclude_threads) = { + let mention_set = self.mention_set.lock(); + + let mut excluded_paths = HashSet::default(); + let mut excluded_threads = HashSet::default(); + + for uri in mention_set.uri_by_crease_id.values() { + match uri { + MentionUri::File(path) => { + excluded_paths.insert(path.clone()); + } + MentionUri::Thread { id, .. } => { + excluded_threads.insert(id.clone()); + } + _ => {} + } + } + + (excluded_paths, excluded_threads) + }; + + let recent_entries = recent_context_picker_entries( + Some(thread_store.clone()), + Some(text_thread_store.clone()), + workspace.clone(), + &exclude_paths, + &exclude_threads, + cx, + ); + + let prompt_store = thread_store + .read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone()) + .ok() + .flatten(); + + let search_task = search( + mode, + query, + Arc::::default(), + recent_entries, + prompt_store, + thread_store.clone(), + text_thread_store.clone(), + workspace.clone(), + cx, + ); + + let mention_set = self.mention_set.clone(); cx.spawn(async move |_, cx| { let matches = search_task.await; @@ -226,25 +961,74 @@ impl CompletionProvider for ContextPickerCompletionProvider { let completions = cx.update(|cx| { matches .into_iter() - .map(|mat| { - let path_match = &mat.mat; - let project_path = ProjectPath { - worktree_id: WorktreeId::from_usize(path_match.worktree_id), - path: path_match.path.clone(), - }; + .filter_map(|mat| match mat { + Match::File(FileMatch { mat, is_recent }) => { + let project_path = ProjectPath { + worktree_id: WorktreeId::from_usize(mat.worktree_id), + path: mat.path.clone(), + }; - Self::completion_for_path( - project_path, - &path_match.path_prefix, - mat.is_recent, - path_match.is_dir, + Self::completion_for_path( + project_path, + &mat.path_prefix, + is_recent, + mat.is_dir, + excerpt_id, + source_range.clone(), + editor.clone(), + mention_set.clone(), + project.clone(), + cx, + ) + } + + Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol( + symbol, excerpt_id, source_range.clone(), editor.clone(), mention_set.clone(), - project.clone(), + workspace.clone(), cx, - ) + ), + + Match::Thread(ThreadMatch { + thread, is_recent, .. + }) => Some(Self::completion_for_thread( + thread, + excerpt_id, + source_range.clone(), + is_recent, + editor.clone(), + mention_set.clone(), + )), + + Match::Rules(user_rules) => Some(Self::completion_for_rules( + user_rules, + excerpt_id, + source_range.clone(), + editor.clone(), + mention_set.clone(), + )), + + Match::Fetch(url) => Self::completion_for_fetch( + source_range.clone(), + url, + excerpt_id, + editor.clone(), + mention_set.clone(), + http_client.clone(), + ), + + Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry( + entry, + excerpt_id, + source_range.clone(), + editor.clone(), + mention_set.clone(), + &workspace, + cx, + ), }) .collect() })?; @@ -296,23 +1080,21 @@ impl CompletionProvider for ContextPickerCompletionProvider { fn confirm_completion_callback( crease_icon_path: SharedString, crease_text: SharedString, - project_path: ProjectPath, excerpt_id: ExcerptId, start: Anchor, content_len: usize, editor: Entity, mention_set: Arc>, - project: Entity, + mention_uri: MentionUri, ) -> Arc bool + Send + Sync> { Arc::new(move |_, window, cx| { let crease_text = crease_text.clone(); let crease_icon_path = crease_icon_path.clone(); let editor = editor.clone(); - let project_path = project_path.clone(); let mention_set = mention_set.clone(); - let project = project.clone(); + let mention_uri = mention_uri.clone(); window.defer(cx, move |window, cx| { - let crease_id = crate::context_picker::insert_crease_for_mention( + if let Some(crease_id) = crate::context_picker::insert_crease_for_mention( excerpt_id, start, content_len, @@ -321,14 +1103,8 @@ fn confirm_completion_callback( editor.clone(), window, cx, - ); - - let Some(path) = project.read(cx).absolute_path(&project_path, cx) else { - return; - }; - - if let Some(crease_id) = crease_id { - mention_set.lock().insert(crease_id, path); + ) { + mention_set.lock().insert(crease_id, mention_uri.clone()); } }); false @@ -338,6 +1114,7 @@ fn confirm_completion_callback( #[derive(Debug, Default, PartialEq)] struct MentionCompletion { source_range: Range, + mode: Option, argument: Option, } @@ -357,17 +1134,37 @@ impl MentionCompletion { } let rest_of_line = &line[last_mention_start + 1..]; + + let mut mode = None; let mut argument = None; let mut parts = rest_of_line.split_whitespace(); let mut end = last_mention_start + 1; - if let Some(argument_text) = parts.next() { - end += argument_text.len(); - argument = Some(argument_text.to_string()); + if let Some(mode_text) = parts.next() { + end += mode_text.len(); + + if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok() { + mode = Some(parsed_mode); + } else { + argument = Some(mode_text.to_string()); + } + match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) { + Some(whitespace_count) => { + if let Some(argument_text) = parts.next() { + argument = Some(argument_text.to_string()); + end += whitespace_count + argument_text.len(); + } + } + None => { + // Rest of line is entirely whitespace + end += rest_of_line.len() - mode_text.len(); + } + } } Some(Self { source_range: last_mention_start + offset_to_line..end + offset_to_line, + mode, argument, }) } @@ -376,10 +1173,12 @@ impl MentionCompletion { #[cfg(test)] mod tests { use super::*; + use editor::AnchorRangeExt; use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext}; use project::{Project, ProjectPath}; use serde_json::json; use settings::SettingsStore; + use smol::stream::StreamExt as _; use std::{ops::Deref, rc::Rc}; use util::path; use workspace::{AppState, Item}; @@ -392,14 +1191,61 @@ mod tests { MentionCompletion::try_parse("Lorem @", 0), Some(MentionCompletion { source_range: 6..7, + mode: None, argument: None, }) ); + assert_eq!( + MentionCompletion::try_parse("Lorem @file", 0), + Some(MentionCompletion { + source_range: 6..11, + mode: Some(ContextPickerMode::File), + argument: None, + }) + ); + + assert_eq!( + MentionCompletion::try_parse("Lorem @file ", 0), + Some(MentionCompletion { + source_range: 6..12, + mode: Some(ContextPickerMode::File), + argument: None, + }) + ); + + assert_eq!( + MentionCompletion::try_parse("Lorem @file main.rs", 0), + Some(MentionCompletion { + source_range: 6..19, + mode: Some(ContextPickerMode::File), + argument: Some("main.rs".to_string()), + }) + ); + + assert_eq!( + MentionCompletion::try_parse("Lorem @file main.rs ", 0), + Some(MentionCompletion { + source_range: 6..19, + mode: Some(ContextPickerMode::File), + argument: Some("main.rs".to_string()), + }) + ); + + assert_eq!( + MentionCompletion::try_parse("Lorem @file main.rs Ipsum", 0), + Some(MentionCompletion { + source_range: 6..19, + mode: Some(ContextPickerMode::File), + argument: Some("main.rs".to_string()), + }) + ); + assert_eq!( MentionCompletion::try_parse("Lorem @main", 0), Some(MentionCompletion { source_range: 6..11, + mode: None, argument: Some("main".to_string()), }) ); @@ -456,16 +1302,16 @@ mod tests { json!({ "editor": "", "a": { - "one.txt": "", - "two.txt": "", - "three.txt": "", - "four.txt": "" + "one.txt": "1", + "two.txt": "2", + "three.txt": "3", + "four.txt": "4" }, "b": { - "five.txt": "", - "six.txt": "", - "seven.txt": "", - "eight.txt": "", + "five.txt": "5", + "six.txt": "6", + "seven.txt": "7", + "eight.txt": "8", } }), ) @@ -540,12 +1386,17 @@ mod tests { let mention_set = Arc::new(Mutex::new(MentionSet::default())); + let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx)); + let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + let editor_entity = editor.downgrade(); editor.update_in(&mut cx, |editor, window, cx| { window.focus(&editor.focus_handle(cx)); editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new( mention_set.clone(), workspace.downgrade(), + thread_store.downgrade(), + text_thread_store.downgrade(), editor_entity, )))); }); @@ -569,22 +1420,10 @@ mod tests { "seven.txt dir/b/", "six.txt dir/b/", "five.txt dir/b/", - "four.txt dir/a/", - "three.txt dir/a/", - "two.txt dir/a/", - "one.txt dir/a/", - "dir ", - "a dir/", - "four.txt dir/a/", - "one.txt dir/a/", - "three.txt dir/a/", - "two.txt dir/a/", - "b dir/", - "eight.txt dir/b/", - "five.txt dir/b/", - "seven.txt dir/b/", - "six.txt dir/b/", - "editor dir/" + "Files & Directories", + "Symbols", + "Threads", + "Fetch" ] ); }); @@ -602,8 +1441,264 @@ mod tests { cx.run_until_parked(); editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem [@four.txt](@file:dir/a/four.txt) "); + assert_eq!(editor.text(cx), "Lorem @file "); + assert!(editor.has_visible_completions_menu()); }); + + cx.simulate_input("one"); + + editor.update(&mut cx, |editor, cx| { + assert_eq!(editor.text(cx), "Lorem @file one"); + assert!(editor.has_visible_completions_menu()); + assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]); + }); + + editor.update_in(&mut cx, |editor, window, cx| { + assert!(editor.has_visible_completions_menu()); + editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); + }); + + editor.update(&mut cx, |editor, cx| { + assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) "); + assert!(!editor.has_visible_completions_menu()); + assert_eq!( + fold_ranges(editor, cx), + vec![Point::new(0, 6)..Point::new(0, 39)] + ); + }); + + let contents = cx + .update(|window, cx| { + mention_set.lock().contents( + project.clone(), + thread_store.clone(), + text_thread_store.clone(), + window, + cx, + ) + }) + .await + .unwrap() + .into_values() + .collect::>(); + + assert_eq!(contents.len(), 1); + assert_eq!(contents[0].content, "1"); + assert_eq!( + contents[0].uri.to_uri().to_string(), + "file:///dir/a/one.txt" + ); + + cx.simulate_input(" "); + + editor.update(&mut cx, |editor, cx| { + assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) "); + assert!(!editor.has_visible_completions_menu()); + assert_eq!( + fold_ranges(editor, cx), + vec![Point::new(0, 6)..Point::new(0, 39)] + ); + }); + + cx.simulate_input("Ipsum "); + + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum ", + ); + assert!(!editor.has_visible_completions_menu()); + assert_eq!( + fold_ranges(editor, cx), + vec![Point::new(0, 6)..Point::new(0, 39)] + ); + }); + + cx.simulate_input("@file "); + + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum @file ", + ); + assert!(editor.has_visible_completions_menu()); + assert_eq!( + fold_ranges(editor, cx), + vec![Point::new(0, 6)..Point::new(0, 39)] + ); + }); + + editor.update_in(&mut cx, |editor, window, cx| { + editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); + }); + + cx.run_until_parked(); + + let contents = cx + .update(|window, cx| { + mention_set.lock().contents( + project.clone(), + thread_store.clone(), + text_thread_store.clone(), + window, + cx, + ) + }) + .await + .unwrap() + .into_values() + .collect::>(); + + assert_eq!(contents.len(), 2); + let new_mention = contents + .iter() + .find(|mention| mention.uri.to_uri().to_string() == "file:///dir/b/eight.txt") + .unwrap(); + assert_eq!(new_mention.content, "8"); + + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) " + ); + assert!(!editor.has_visible_completions_menu()); + assert_eq!( + fold_ranges(editor, cx), + vec![ + Point::new(0, 6)..Point::new(0, 39), + Point::new(0, 47)..Point::new(0, 84) + ] + ); + }); + + let plain_text_language = Arc::new(language::Language::new( + language::LanguageConfig { + name: "Plain Text".into(), + matcher: language::LanguageMatcher { + path_suffixes: vec!["txt".to_string()], + ..Default::default() + }, + ..Default::default() + }, + None, + )); + + // Register the language and fake LSP + let language_registry = project.read_with(&cx, |project, _| project.languages().clone()); + language_registry.add(plain_text_language); + + let mut fake_language_servers = language_registry.register_fake_lsp( + "Plain Text", + language::FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + workspace_symbol_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + }, + ); + + // Open the buffer to trigger LSP initialization + let buffer = project + .update(&mut cx, |project, cx| { + project.open_local_buffer(path!("/dir/a/one.txt"), cx) + }) + .await + .unwrap(); + + // Register the buffer with language servers + let _handle = project.update(&mut cx, |project, cx| { + project.register_buffer_with_language_servers(&buffer, cx) + }); + + cx.run_until_parked(); + + let fake_language_server = fake_language_servers.next().await.unwrap(); + fake_language_server.set_request_handler::( + |_, _| async move { + Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![ + #[allow(deprecated)] + lsp::SymbolInformation { + name: "MySymbol".into(), + location: lsp::Location { + uri: lsp::Url::from_file_path(path!("/dir/a/one.txt")).unwrap(), + range: lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 1), + ), + }, + kind: lsp::SymbolKind::CONSTANT, + tags: None, + container_name: None, + deprecated: None, + }, + ]))) + }, + ); + + cx.simulate_input("@symbol "); + + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) @symbol " + ); + assert!(editor.has_visible_completions_menu()); + assert_eq!( + current_completion_labels(editor), + &[ + "MySymbol", + ] + ); + }); + + editor.update_in(&mut cx, |editor, window, cx| { + editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); + }); + + let contents = cx + .update(|window, cx| { + mention_set.lock().contents( + project.clone(), + thread_store, + text_thread_store, + window, + cx, + ) + }) + .await + .unwrap() + .into_values() + .collect::>(); + + assert_eq!(contents.len(), 3); + let new_mention = contents + .iter() + .find(|mention| { + mention.uri.to_uri().to_string() == "file:///dir/a/one.txt?symbol=MySymbol#L1:1" + }) + .unwrap(); + assert_eq!(new_mention.content, "1"); + + cx.run_until_parked(); + + editor.read_with(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) [@MySymbol](file:///dir/a/one.txt?symbol=MySymbol#L1:1) " + ); + }); + } + + fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec> { + let snapshot = editor.buffer().read(cx).snapshot(cx); + editor.display_map.update(cx, |display_map, cx| { + display_map + .snapshot(cx) + .folds_in_range(0..snapshot.len()) + .map(|fold| fold.range.to_point(&snapshot)) + .collect() + }) } fn current_completion_labels(editor: &Editor) -> Vec { diff --git a/crates/agent_ui/src/acp/message_history.rs b/crates/agent_ui/src/acp/message_history.rs index c6106c7578..c8280573a0 100644 --- a/crates/agent_ui/src/acp/message_history.rs +++ b/crates/agent_ui/src/acp/message_history.rs @@ -45,12 +45,8 @@ impl MessageHistory { None }) } - - #[cfg(test)] - pub fn items(&self) -> &[T] { - &self.items - } } + #[cfg(test)] mod tests { use super::*; diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 0b3ace1baf..3aefae7265 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -4,15 +4,17 @@ use acp_thread::{ }; use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; +use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol as acp; use agent_servers::AgentServer; use agent_settings::{AgentSettings, NotifyWhenAgentWaiting}; use audio::{Audio, Sound}; use buffer_diff::BufferDiff; use collections::{HashMap, HashSet}; +use editor::scroll::Autoscroll; use editor::{ AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode, - EditorStyle, MinimapVisibility, MultiBuffer, PathKey, + EditorStyle, MinimapVisibility, MultiBuffer, PathKey, SelectionEffects, }; use file_icons::FileIcons; use gpui::{ @@ -27,8 +29,10 @@ use language::{Buffer, Language}; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; use parking_lot::Mutex; use project::{CompletionIntent, Project}; +use prompt_store::PromptId; use rope::Point; use settings::{Settings as _, SettingsStore}; +use std::fmt::Write as _; use std::path::PathBuf; use std::{ cell::RefCell, collections::BTreeMap, path::Path, process::ExitStatus, rc::Rc, sync::Arc, @@ -44,6 +48,7 @@ use ui::{ use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, Workspace}; use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage, ToggleModelSelector}; +use zed_actions::assistant::OpenRulesLibrary; use crate::acp::AcpModelSelectorPopover; use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet}; @@ -61,6 +66,8 @@ pub struct AcpThreadView { agent: Rc, workspace: WeakEntity, project: Entity, + thread_store: Entity, + text_thread_store: Entity, thread_state: ThreadState, diff_editors: HashMap>, terminal_views: HashMap>, @@ -108,6 +115,8 @@ impl AcpThreadView { agent: Rc, workspace: WeakEntity, project: Entity, + thread_store: Entity, + text_thread_store: Entity, message_history: Rc>>>, min_lines: usize, max_lines: Option, @@ -145,6 +154,8 @@ impl AcpThreadView { editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new( mention_set.clone(), workspace.clone(), + thread_store.downgrade(), + text_thread_store.downgrade(), cx.weak_entity(), )))); editor.set_context_menu_options(ContextMenuOptions { @@ -188,6 +199,8 @@ impl AcpThreadView { agent: agent.clone(), workspace: workspace.clone(), project: project.clone(), + thread_store, + text_thread_store, thread_state: Self::initial_state(agent, workspace, project, window, cx), message_editor, model_selector: None, @@ -401,7 +414,13 @@ impl AcpThreadView { let mut chunks: Vec = Vec::new(); let project = self.project.clone(); - let contents = self.mention_set.lock().contents(project, cx); + let thread_store = self.thread_store.clone(); + let text_thread_store = self.text_thread_store.clone(); + + let contents = + self.mention_set + .lock() + .contents(project, thread_store, text_thread_store, window, cx); cx.spawn_in(window, async move |this, cx| { let contents = match contents.await { @@ -439,7 +458,7 @@ impl AcpThreadView { acp::TextResourceContents { mime_type: None, text: mention.content.clone(), - uri: mention.uri.to_uri(), + uri: mention.uri.to_uri().to_string(), }, ), })); @@ -614,8 +633,7 @@ impl AcpThreadView { let path = PathBuf::from(&resource.uri); let project_path = project.read(cx).project_path_for_absolute_path(&path, cx); let start = text.len(); - let content = MentionUri::File(path).to_uri(); - text.push_str(&content); + let _ = write!(&mut text, "{}", MentionUri::File(path).to_uri()); let end = text.len(); if let Some(project_path) = project_path { let filename: SharedString = project_path @@ -663,7 +681,9 @@ impl AcpThreadView { ); if let Some(crease_id) = crease_id { - mention_set.lock().insert(crease_id, project_path); + mention_set + .lock() + .insert(crease_id, MentionUri::File(project_path)); } } } @@ -2698,9 +2718,72 @@ impl AcpThreadView { .detach_and_log_err(cx); } } - _ => { - // TODO - unimplemented!() + MentionUri::Symbol { + path, line_range, .. + } + | MentionUri::Selection { path, line_range } => { + let project = workspace.project(); + let Some((path, _)) = project.update(cx, |project, cx| { + let path = project.find_project_path(path, cx)?; + let entry = project.entry_for_path(&path, cx)?; + Some((path, entry)) + }) else { + return; + }; + + let item = workspace.open_path(path, None, true, window, cx); + window + .spawn(cx, async move |cx| { + let Some(editor) = item.await?.downcast::() else { + return Ok(()); + }; + let range = + Point::new(line_range.start, 0)..Point::new(line_range.start, 0); + editor + .update_in(cx, |editor, window, cx| { + editor.change_selections( + SelectionEffects::scroll(Autoscroll::center()), + window, + cx, + |s| s.select_ranges(vec![range]), + ); + }) + .ok(); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + MentionUri::Thread { id, .. } => { + if let Some(panel) = workspace.panel::(cx) { + panel.update(cx, |panel, cx| { + panel + .open_thread_by_id(&id, window, cx) + .detach_and_log_err(cx) + }); + } + } + MentionUri::TextThread { path, .. } => { + if let Some(panel) = workspace.panel::(cx) { + panel.update(cx, |panel, cx| { + panel + .open_saved_prompt_editor(path.as_path().into(), window, cx) + .detach_and_log_err(cx); + }); + } + } + MentionUri::Rule { id, .. } => { + let PromptId::User { uuid } = id else { + return; + }; + window.dispatch_action( + Box::new(OpenRulesLibrary { + prompt_to_select: Some(uuid.0), + }), + cx, + ) + } + MentionUri::Fetch { url } => { + cx.open_url(url.as_str()); } }) } else { @@ -3090,7 +3173,7 @@ impl AcpThreadView { .unwrap_or(path.path.as_os_str()) .display() .to_string(); - let completion = ContextPickerCompletionProvider::completion_for_path( + let Some(completion) = ContextPickerCompletionProvider::completion_for_path( path, &path_prefix, false, @@ -3101,7 +3184,9 @@ impl AcpThreadView { self.mention_set.clone(), self.project.clone(), cx, - ); + ) else { + continue; + }; self.message_editor.update(cx, |message_editor, cx| { message_editor.edit( @@ -3431,17 +3516,14 @@ fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { #[cfg(test)] mod tests { + use agent::{TextThreadStore, ThreadStore}; use agent_client_protocol::SessionId; use editor::EditorSettings; use fs::FakeFs; use futures::future::try_join_all; use gpui::{SemanticVersion, TestAppContext, VisualTestContext}; - use lsp::{CompletionContext, CompletionTriggerKind}; - use project::CompletionIntent; use rand::Rng; - use serde_json::json; use settings::SettingsStore; - use util::path; use super::*; @@ -3554,109 +3636,6 @@ mod tests { ); } - #[gpui::test] - async fn test_crease_removal(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/project", json!({"file": ""})).await; - let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; - let agent = StubAgentServer::default(); - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let thread_view = cx.update(|window, cx| { - cx.new(|cx| { - AcpThreadView::new( - Rc::new(agent), - workspace.downgrade(), - project, - Rc::new(RefCell::new(MessageHistory::default())), - 1, - None, - window, - cx, - ) - }) - }); - - cx.run_until_parked(); - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - let excerpt_id = message_editor.update(cx, |editor, cx| { - editor - .buffer() - .read(cx) - .excerpt_ids() - .into_iter() - .next() - .unwrap() - }); - let completions = message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Hello @", window, cx); - let buffer = editor.buffer().read(cx).as_singleton().unwrap(); - let completion_provider = editor.completion_provider().unwrap(); - completion_provider.completions( - excerpt_id, - &buffer, - Anchor::MAX, - CompletionContext { - trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER, - trigger_character: Some("@".into()), - }, - window, - cx, - ) - }); - let [_, completion]: [_; 2] = completions - .await - .unwrap() - .into_iter() - .flat_map(|response| response.completions) - .collect::>() - .try_into() - .unwrap(); - - message_editor.update_in(cx, |editor, window, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let start = snapshot - .anchor_in_excerpt(excerpt_id, completion.replace_range.start) - .unwrap(); - let end = snapshot - .anchor_in_excerpt(excerpt_id, completion.replace_range.end) - .unwrap(); - editor.edit([(start..end, completion.new_text)], cx); - (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx); - }); - - cx.run_until_parked(); - - // Backspace over the inserted crease (and the following space). - message_editor.update_in(cx, |editor, window, cx| { - editor.backspace(&Default::default(), window, cx); - editor.backspace(&Default::default(), window, cx); - }); - - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.chat(&Chat, window, cx); - }); - - cx.run_until_parked(); - - let content = thread_view.update_in(cx, |thread_view, _window, _cx| { - thread_view - .message_history - .borrow() - .items() - .iter() - .flatten() - .cloned() - .collect::>() - }); - - // We don't send a resource link for the deleted crease. - pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]); - } - async fn setup_thread_view( agent: impl AgentServer + 'static, cx: &mut TestAppContext, @@ -3666,12 +3645,19 @@ mod tests { let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let thread_store = + cx.update(|_window, cx| cx.new(|cx| ThreadStore::fake(project.clone(), cx))); + let text_thread_store = + cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx))); + let thread_view = cx.update(|window, cx| { cx.new(|cx| { AcpThreadView::new( Rc::new(agent), workspace.downgrade(), project, + thread_store.clone(), + text_thread_store.clone(), Rc::new(RefCell::new(MessageHistory::default())), 1, None, diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index a641d62296..9aeb7867ac 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -973,6 +973,9 @@ impl AgentPanel { agent: crate::ExternalAgent, } + let thread_store = self.thread_store.clone(); + let text_thread_store = self.context_store.clone(); + cx.spawn_in(window, async move |this, cx| { let server: Rc = match agent_choice { Some(agent) => { @@ -1011,6 +1014,8 @@ impl AgentPanel { server, workspace.clone(), project, + thread_store.clone(), + text_thread_store.clone(), message_history, MIN_EDITOR_LINES, Some(MAX_EDITOR_LINES), diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index 58f11313e6..7dc00bfae2 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -1,15 +1,16 @@ mod completion_provider; -mod fetch_context_picker; +pub(crate) mod fetch_context_picker; pub(crate) mod file_context_picker; -mod rules_context_picker; -mod symbol_context_picker; -mod thread_context_picker; +pub(crate) mod rules_context_picker; +pub(crate) mod symbol_context_picker; +pub(crate) mod thread_context_picker; use std::ops::Range; use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::{Result, anyhow}; +use collections::HashSet; pub use completion_provider::ContextPickerCompletionProvider; use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId}; use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset}; @@ -45,7 +46,7 @@ use agent::{ }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ContextPickerEntry { +pub(crate) enum ContextPickerEntry { Mode(ContextPickerMode), Action(ContextPickerAction), } @@ -74,7 +75,7 @@ impl ContextPickerEntry { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ContextPickerMode { +pub(crate) enum ContextPickerMode { File, Symbol, Fetch, @@ -83,7 +84,7 @@ enum ContextPickerMode { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ContextPickerAction { +pub(crate) enum ContextPickerAction { AddSelections, } @@ -531,7 +532,7 @@ impl ContextPicker { return vec![]; }; - recent_context_picker_entries( + recent_context_picker_entries_with_store( context_store, self.thread_store.clone(), self.text_thread_store.clone(), @@ -585,7 +586,8 @@ impl Render for ContextPicker { }) } } -enum RecentEntry { + +pub(crate) enum RecentEntry { File { project_path: ProjectPath, path_prefix: Arc, @@ -593,7 +595,7 @@ enum RecentEntry { Thread(ThreadContextEntry), } -fn available_context_picker_entries( +pub(crate) fn available_context_picker_entries( prompt_store: &Option>, thread_store: &Option>, workspace: &Entity, @@ -630,24 +632,56 @@ fn available_context_picker_entries( entries } -fn recent_context_picker_entries( +fn recent_context_picker_entries_with_store( context_store: Entity, thread_store: Option>, text_thread_store: Option>, workspace: Entity, exclude_path: Option, cx: &App, +) -> Vec { + let project = workspace.read(cx).project(); + + let mut exclude_paths = context_store.read(cx).file_paths(cx); + exclude_paths.extend(exclude_path); + + let exclude_paths = exclude_paths + .into_iter() + .filter_map(|project_path| project.read(cx).absolute_path(&project_path, cx)) + .collect(); + + let exclude_threads = context_store.read(cx).thread_ids(); + + recent_context_picker_entries( + thread_store, + text_thread_store, + workspace, + &exclude_paths, + exclude_threads, + cx, + ) +} + +pub(crate) fn recent_context_picker_entries( + thread_store: Option>, + text_thread_store: Option>, + workspace: Entity, + exclude_paths: &HashSet, + exclude_threads: &HashSet, + cx: &App, ) -> Vec { let mut recent = Vec::with_capacity(6); - let mut current_files = context_store.read(cx).file_paths(cx); - current_files.extend(exclude_path); let workspace = workspace.read(cx); let project = workspace.project().read(cx); recent.extend( workspace .recent_navigation_history_iter(cx) - .filter(|(path, _)| !current_files.contains(path)) + .filter(|(_, abs_path)| { + abs_path + .as_ref() + .map_or(true, |path| !exclude_paths.contains(path.as_path())) + }) .take(4) .filter_map(|(project_path, _)| { project @@ -659,8 +693,6 @@ fn recent_context_picker_entries( }), ); - let current_threads = context_store.read(cx).thread_ids(); - let active_thread_id = workspace .panel::(cx) .and_then(|panel| Some(panel.read(cx).active_thread(cx)?.read(cx).id())); @@ -672,7 +704,7 @@ fn recent_context_picker_entries( let mut threads = unordered_thread_entries(thread_store, text_thread_store, cx) .filter(|(_, thread)| match thread { ThreadContextEntry::Thread { id, .. } => { - Some(id) != active_thread_id && !current_threads.contains(id) + Some(id) != active_thread_id && !exclude_threads.contains(id) } ThreadContextEntry::Context { .. } => true, }) @@ -710,7 +742,7 @@ fn add_selections_as_context( }) } -fn selection_ranges( +pub(crate) fn selection_ranges( workspace: &Entity, cx: &mut App, ) -> Vec<(Entity, Range)> { diff --git a/crates/agent_ui/src/context_picker/completion_provider.rs b/crates/agent_ui/src/context_picker/completion_provider.rs index 8123b3437d..962c0df03d 100644 --- a/crates/agent_ui/src/context_picker/completion_provider.rs +++ b/crates/agent_ui/src/context_picker/completion_provider.rs @@ -35,7 +35,7 @@ use super::symbol_context_picker::search_symbols; use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads}; use super::{ ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry, - available_context_picker_entries, recent_context_picker_entries, selection_ranges, + available_context_picker_entries, recent_context_picker_entries_with_store, selection_ranges, }; use crate::message_editor::ContextCreasesAddon; @@ -787,7 +787,7 @@ impl CompletionProvider for ContextPickerCompletionProvider { .and_then(|b| b.read(cx).file()) .map(|file| ProjectPath::from_file(file.as_ref(), cx)); - let recent_entries = recent_context_picker_entries( + let recent_entries = recent_context_picker_entries_with_store( context_store.clone(), thread_store.clone(), text_thread_store.clone(), diff --git a/crates/assistant_context/Cargo.toml b/crates/assistant_context/Cargo.toml index 8f5ff98790..45c0072418 100644 --- a/crates/assistant_context/Cargo.toml +++ b/crates/assistant_context/Cargo.toml @@ -11,6 +11,9 @@ workspace = true [lib] path = "src/assistant_context.rs" +[features] +test-support = [] + [dependencies] agent_settings.workspace = true anyhow.workspace = true diff --git a/crates/assistant_context/src/context_store.rs b/crates/assistant_context/src/context_store.rs index 3090a7b234..622d8867a7 100644 --- a/crates/assistant_context/src/context_store.rs +++ b/crates/assistant_context/src/context_store.rs @@ -138,6 +138,27 @@ impl ContextStore { }) } + #[cfg(any(test, feature = "test-support"))] + pub fn fake(project: Entity, cx: &mut Context) -> Self { + Self { + contexts: Default::default(), + contexts_metadata: Default::default(), + context_server_slash_command_ids: Default::default(), + host_contexts: Default::default(), + fs: project.read(cx).fs().clone(), + languages: project.read(cx).languages().clone(), + slash_commands: Arc::default(), + telemetry: project.read(cx).client().telemetry().clone(), + _watch_updates: Task::ready(None), + client: project.read(cx).client(), + project, + project_is_shared: false, + client_subscription: None, + _project_subscriptions: Default::default(), + prompt_builder: Arc::new(PromptBuilder::new(None).unwrap()), + } + } + async fn handle_advertise_contexts( this: Entity, envelope: TypedEnvelope, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8a9398e71f..c77262143d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -12176,6 +12176,8 @@ impl Editor { let clipboard_text = Cow::Borrowed(text); self.transact(window, cx, |this, window, cx| { + let had_active_edit_prediction = this.has_active_edit_prediction(); + if let Some(mut clipboard_selections) = clipboard_selections { let old_selections = this.selections.all::(cx); let all_selections_were_entire_line = @@ -12248,6 +12250,11 @@ impl Editor { } else { this.insert(&clipboard_text, window, cx); } + + let trigger_in_words = + this.show_edit_predictions_in_menu() || !had_active_edit_prediction; + + this.trigger_completion_on_input(&text, trigger_in_words, window, cx); }); } diff --git a/crates/prompt_store/src/prompt_store.rs b/crates/prompt_store/src/prompt_store.rs index f9cb26ed9a..06a65b97cd 100644 --- a/crates/prompt_store/src/prompt_store.rs +++ b/crates/prompt_store/src/prompt_store.rs @@ -90,6 +90,15 @@ impl From for UserPromptId { } } +impl std::fmt::Display for PromptId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PromptId::User { uuid } => write!(f, "{}", uuid.0), + PromptId::EditWorkflow => write!(f, "Edit workflow"), + } + } +} + pub struct PromptStore { env: heed::Env, metadata_cache: RwLock, From 9be44517cb5db0a63f7d9727d93c034fb8fdcdc6 Mon Sep 17 00:00:00 2001 From: Alvaro Parker <64918109+AlvaroParker@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:24:13 -0400 Subject: [PATCH 326/693] Remove Services menu on non-macOS systems (#36142) Closes #ISSUE image Release Notes: - Remove Services menu on non-macOS systems which was causing an empty menu item being rendered --- crates/zed/src/zed/app_menus.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index 9df55a2fb1..6c7ab0b374 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -35,6 +35,7 @@ pub fn app_menus() -> Vec

#lX4AYWo$+6JtC*M z6Je3yF$?YgUzl{61Q>Wx_w~8?f<{;1vvd4>j0}?Ee2V;v9Bd4{jJ#0yLI=h~MWDk# zaF;^{#uzy>OTC)IL4=QiEhjG%v%V&XW@ItosYpxi^Q}lr>i5mHGV|jU=hV}M(cm}( zy9=BLK=Yk!tj?fut^fZSK;qyrRYr(7Y)lm-4jxm5io?cKLE^fgF;%EIXp9t8PlCk3 zW2#_r#F(n~e={b8dc>G2=>B?`K3(QLOlnYlurXDTdEhZss5op)6(kNGQ-zAd##BM# z;4xLOIATl{Y(F>+gZu{?J7vZi9I!a5~fnk1rw8p&3&HQ#q3w zBS;)Io{-fe$5R71o*?RBW89$lYXG-LK;mqyen{y&f>{Zg&e&L8ki>OCeOz#SL&wCy z?$iP0W4L->b5NbZ{QuqmV@z30b_}8nN(}l8&JGT|JY39740?(R%*;%pplN7kW*=@w z1~1SIBY3t5l=Yb*yL-JD{Zy32#gz3`^u?6Kl{D4axFod2)y<(tQi+3h4T3VivXZ(m zXj+9$1eEd3)j%VG<|d$pbf)ZLMlRt}Ud`bFW`QuqL2x0!$jEp=EZES7la~?9w6cvf za!Ky-m*kj~^&A95pl!3aqunL!flM}{7(-7&PY;LBj!^mK7YvyY1 zYN)Scs%xsI0y?EnfRB@%L7P#V7czFE4m;5WJUk!{X;Wb?ivdqJfD2sEyew#{4%Ey7 z&D1gW>)P?LvdI}KJNO&MIcr*Rv52|p*!zf>rGOU1$jL})@I*_7@yh6MGkVy|{5vh9 z!X?gZ0490VH277eB9pV`MW`w%s(G8qSQ{&XR>5$q>T1iY+qyCywsm1=Hc%In_&3MU zs!LSJ$-ufxNYsgef$9I}{}xO}OakDwF4G*iAS+uy%UmD@i!AtvLeMUM(5OAMz2)T# znzHv`hP6>Zr5<>-1hg~(%};ZwGW)=o-F*GwpUIN2g0dBYK_BO_?l26P?+6xFc00~CYcf&~$SkOmZJ?FM>OqbWp>YN#}5`U-bc zLl=0k3M+#<+R#P{sNP@(6-=m86k=kGR;DNmJOn0{pTk_>ae~nqzQiNpuL+|r#u5+k z0bWd@Oe_q-48B|0pnVTeO(_HF2ZHy~L;D$^ZS`)@ow?Euq6~~IOpM{oj7*@DFj&$d zJ9ZFNZqaG71Vj%0gL`B5J z#ligrMke1PsUR_5JzY~daW8dyRog6Q*F32JK^Gl$eQ60_N3TvE9gRSFd081zVO9Z7 zc?(@De@%6zATc=^8Bsx2K|VYC(8T}$!Dp38gZ&O#jL*jEtPB~?2KmJd+<$o9@*=)}8ZeU<#WMnM>O-_RL zlS0BE3A9xN)Kzd$MN-DX%2?b`=BKA)DkJG->xT$e4LvDIUwfajWJc&(Fxd7w@ClKuS}K9^ z@^Ui5BCG;@Hcr;Cz?KyjVin}GwF``ctqDVfuM)V;4GCXYQ22u4V2^u^HSCh)h<_NYD8~co&bg2mmv5A3>jAl0S zRzb#!#zI1ZvIZ8MtjwyKy5j1x`kbsRin6M9{5(u-4P|j5KJg-~JRbJiVeZ;O{Ne06 z1`^U-iYob98WPf6vWjNj3=D|*Yf*+6wE1gkP}D=_uR-Mj=aj>&7NHI!*QZKm44w}gT1v_M#5j=0k$HXY+WXrDFA{?xs$STqycS`t_fyf*W z8!aar9ycoqtvf|hN>$s6{yku?4l+?OkN~YEg}GOeq0E63G!o7N8eoICSc!v?i4!{W z586G;&dAKn5)bOYw5AHYPJj>G|6m92 zSOu+$L-@uVl8_)i0#BZT8p3RBjFQgw>|8N|F^a*MYl!!( zQ<^!WLbX*Pb6p_+l```%sWF0*5olb}lR*PA!mR-wW(3EX4mi$?7}h%otEwpRv#>L1 zfeI$@7GBUi7UcX|$QEAk+0dX_PRJ@bXu$+p1_&ycAR(s;+U^V5$BSRFgECAVD+z#x>>12eKi1bX@^`N_h|Nm#O|MwXbj*zehkM)AiX=Y?F z`2U5;j){dqjv;3&=&*O#@To9pUJE?H!;r)Z8axGcY#{+C4v_*K_W|CV&X5FINhs|A zmSKs7{G02IjiK~E?3cyyPf_hb;&Bow$?Vvnk2Htzj7$(9h>uYCZk60-n zr3PLpFewZ^hR7)V?+&lLu!yFl7{8f3Bk$io|Nle$1Fru9ApIlI!Z5fvACnp*MBE)k z9MmR&h`XVP3&PdAB8!8^)gbB}kj0tzLe;ag=7X-Wf!gcCydN%JfFy1Q9#a9C!_HcW zB3=MjUj!F71H}_u9Jc2e9DhjubwY8E1YErt$Q%YpeD^VxGpI5&ZdGCgoplCj+)6O8 zFtW5lVivTaG>MfB+UtWvx(u>BE96uH25^-RN*E3ZY498}G+n^TN^n)Lr~oQx1o=5R z7*rWmL4_q~QVg`i7HLZ^Xdjv|Xo)F!cQbey?p`dZAQ-g4Sy6@21Jq;j<$)|5K~E3G z=^BRY{{pJ*J)rT>2M(uLv~*#{)CV5Z0EvUfG$4DMLE(z59ywh5z+)N^_26)YnGYV* z0Ex4)`XR+bFnCM@6whp|E=b~b;4uw|xHm`~96vgscxEtWSOeMD#3#YX%EHaaz{JDI z$N)ZH0xG}^5r~8+lVt~uW`nyHOi7@&whO4O4en6-F~U2VFh!t|T?STG$PoZ23LO+d zYFJrX!K1%e)kH=*@G~$l7&92FiL0ops)Np`V`N6&Z3J0jZmtfFKj@+$?e)LzVzKtdc`5NCpr9g?iKCuHd`@+P?zyvzU7<4$t zc4;XQApvI4gn%)#xE&Md1Qc^n^B&wT1Eq9xW%K8bJPzJULIO=vl9q|pjt=(B%*v5W zEdOFS#Dmh!l$>P^3+SYl&FV`yFkIS?f)B1ai$;}WPk z15I&(yO`$6Y&nK{tSn|_wv5dD0{oKP{Hz9rW{fO+0+KqioSbR=!upyDftqI8OzbK= zHimqb5{xeYwsXjE3J8k|NvJTI{oBML!_Ft>Rq3MaA}J>mU>rHqSVPB2Cl55{4E8fP zzh*-6E9iLe|Nj{v;-GRFBJPST4vtrdxC62{k~vNwaR$c!fBt`Aie(aDa0RWrcCxn8 zQIwQm=U`-E^s!`QWb_74gMqRUIJOxQVWlA>gM*!+zM6`h3=bEBE2Aq?(3ykRTZ2|v ztEofQm4gSfL`7kvSt9K0;FYA{oB}@T2^^57%4}>|MmCI$CH$fya-uwJjEu}|%+0p$ zYA&i~ChTHPhKAORC4wT7N+KMLj67|&#(Dv&23G8%t~?&DLV}!p+}i3I4&I*9JYt~j zO3{I)vPvFK{K0NMcKi|?d;)6f7Gbe!ypr5(vA((*sviD9t_+L}Z2uwUj5b5210N3~ z3o~r7Ca6gT?!beV(nI?0pdlLQf=p0}4r)p{$gnapvoN=U!UVI=%W;V8)@I{m=Tzv4hfc7Lk&;$^qEuaNjObJ~&>1-DU zn_6UK`u7&mEco+Jp|<+}e~5p<=^!7Ho=dju>Yxk@LIx%j zwV(zj>{gRV2Wf3BMh0U;ElX`nHB|*U3CIFfO-4=7ifwWDX-n`0s-Rp9J{<&{96|Ld zI6T!s3x+_wCeX@0c1ZY!NH{8~S*UY~vdIM6`8u<)s87;jW|B}eQQ?tdQwD`@nVbv@ zi>#u&Gz*LD6*;|7SqVuO`(_76Z&PU_$rrx_B-zDOwD?R_FM&e2NkW)STvSqA1Vn=J zFgT4Ng}DCY6hDVUbim*5%6ALFJ zGZVPo&B6xiHA_2yL|KtULB|6|LZ*SFK*zE%F&BZRbQ3|%Q#W5mMEeFb@hJ^a!O(zR z2_#mTnLvwS8yHww!7XFhBq-=A8c@anw}jL{7n`uKGBq&3R5LMQQ3#s)5oTnNmJ$^f z;OFIL2OmEM$|S5xYU-fXCg!S;0S;wT6Eo0whqyUtJCeA$x;bRTh>dZcimzrW3s0>G zql1jGf~0y*q?dlO2@@Mnt;oMUN4Vue`2~69t$6rFO_)6^{)r1I=x~O`shBXadRF}V zr^*|lDlMxgu3)7C4tvl(Emv?{l|%A}2xMFXA`WUFL&V*X#X)O_86o11AaT&yfd9WR zNrCr2>M>M1@Ch<9F$*vtCTRRf1-v?&ArX9}f->l^ z5fw}&3@j{iP<8B#pfj-iA{``^6d4)RRTcG=^kk%=J8l&i6*$>(?zjb2U(DcU33wAI zX!-!$^*HVw-dq#U!lY}YXD{s~Ul5R9!@_LP!_O+_'Z#uch-%EHK~%Hv`i6C=jM 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